<?php
namespace app\waimai\service\api;
use Exception;
// 抖音开放平台接口类
class DouyinOpenApi
{
const LOG_DIR = 'service/douyin';
private $apiUrl;
private $accountId;
private $appId;
private $appSecret;
protected $clientToken;
protected $isSandbox = true;
protected $sandboxToken = '';
public $redis;
private static $instance = null;
public function __construct()
{
$config = config('douyin');
$this->apiUrl = $config['open_api_url'];
$this->accountId = $config['account_id'];
$this->appId = $config['app_id'];
$this->appSecret = $config['app_secret'];
$this->isSandbox = (bool)$config['is_sandbox'];
if ($this->isSandbox) {
$this->sandboxToken = $config['sandbox_token'];
}
$this->redis = get_redis(18);
$this->getClientToken();
}
public static function getInstance()
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* 获取门店信息
*
* @param int $storeId 抖音门店ID
*
* @return array
*/
public function getStoreInfo($storeId)
{
$path = '/goodlife/v1/hermes/delivery/delivery_info/get/';
return $this->request('GET', $path, ['poi_id' => $storeId]);
}
/**
* 更新门店信息
*
* @param int $storeId 抖音门店ID
* @param array $deliveryInfo 门店信息
*
* @return array
*/
public function updateStoreInfo($storeId, $deliveryInfo)
{
$path = '/goodlife/v1/hermes/delivery/delivery_info/save/';
return $this->request('POST', $path, ['poi_id' => $storeId, 'delivery_info' => $deliveryInfo]);
}
/**
* 获取门店接单状态
*
* @param int $storeId 抖音门店ID
* @param int $status 0:关闭 1:开启
*
* @return array
*/
public function updateStoreStatus($storeId, $status)
{
$path = '/goodlife/v1/hermes/delivery/accept_status/save/';
return $this->request('POST', $path, ['poi_id' => $storeId, 'status' => $status]);
}
/**
* 确认接单
*
* @param string $orderId 订单ID
*
* @return array
*/
public function confirmOrder($orderId)
{
$path = '/goodlife/v1/trade/buy/merchant_confirm_order/';
return $this->request('POST', $path, ['order_id' => (string)$orderId]);
}
/**
* 备餐完成
*
* @param string $orderId 订单ID
*
* @return array
*/
public function setOrderPrepared($orderId)
{
$path = '/goodlife/v1/hermes/preparation/complete/confirm/';
return $this->request('POST', $path, ['order_id' => (string)$orderId]);
}
/**
* 获取订单详情
*
* @param string $orderId 订单ID
*
* @return array
* @throws Exception
*/
public function getOrderDetail($orderId)
{
$path = '/goodlife/v1/hermes/trade/order/query/';
return $this->request('GET', $path, ['account_id' => $this->accountId, 'order_id' => (string)$orderId]);
}
/**
* 同意/拒绝退款
*
* @param string $afterSaleId 售后单ID
* @param bool $isApproved 是否同意
*
* @return array
*/
public function dealRefund($afterSaleId, $isApproved)
{
$path = '/goodlife/v1/after_sale/audit/notify/';
return $this->request('POST', $path, ['after_sale_id' => (string)$afterSaleId, 'is_approved' => (bool)$isApproved]);
}
/**
* 配送状态同步
* https://developer.open-douyin.com/docs/resource/zh-CN/local-life/develop/OpenAPI/takeout/takeout_order/takeout_order_distribution_sync
*
* @param array $params 配送信息
*
* @return array
* @throws Exception
*/
public function riderPosition($params)
{
$path = '/goodlife/v1/fulfilment/distribution/order/sync_status/';
return $this->request('POST', $path, $params);
}
/**
* 签名OpenApi
*
* @param string $httpBody 请求体
*
* @return string
*/
public function signOpenApi($httpBody)
{
return sha1($this->appSecret . $httpBody);
}
/**
* 验证Spi签名
*
* @param string $clientKey clientKey
* @param string $timestamp 时间戳
* @param string $httpBody 请求体
*
* @return string
*/
public function signSpi($clientKey, $timestamp, $httpBody)
{
$params = [
'client_key' => $clientKey,
'timestamp' => $timestamp,
'http_body' => $httpBody
];
// 构建待签名字符串
$str = $this->appSecret;
foreach ($params as $key => $value) {
if ($key != 'sign') {
$str .= '&' . $key . '=' . $value;
}
}
return hash('sha256', $str);
}
/**
* 获取抖音开放平台的client_token
*
* @return string
* @throws Exception
*/
public function getClientToken()
{
// 沙箱环境是抖音写死的token
if ($this->isSandbox) {
$this->clientToken = $this->sandboxToken;
return $this->clientToken;
}
$authName = 'douyin_client_token:' . $this->appId;
$cache = $this->redis->get($authName);
if ($cache) {
$this->clientToken = $cache['access_token'];
return $this->clientToken;
}
$params = [
'client_key' => $this->appId,
'client_secret' => $this->appSecret,
'grant_type' => 'client_credential',
];
$path = '/oauth/client_token/';
$result = $this->request('POST', $path, $params);
if ($result['code'] === 0) {
return false;
}
$this->clientToken = $result['data']['access_token'];
$expire = $result['data']['expires_in'] ? intval($result['data']['expires_in']) - 600 : 3600;
$cache = [
'access_token' => $this->clientToken,
'expire' => time() + $expire,
];
$this->redis->set($authName, $cache, $expire);
return $this->clientToken;
}
/**
* 格式化结果输出
*
* @param array|false $output 输出结果
*
* @return array
*/
private function parseOutput($output)
{
if ($output === false) {
$result['code'] = 0;
$result['msg'] = '请求服务器失败';
$result['data'] = [];
return $result;
}
// 抖音文档垃圾,有的key是大写,有的是小写,统一转成小写
$output = array_change_key_case($output);
if (!isset($output['extra'])) {
if (isset($output['data'])) {
if (isset($output['data']['error_code']) && $output['data']['error_code'] == 0) {
$result['code'] = 1;
$result['msg'] = 'success';
} else {
$result['code'] = 0;
$result['msg'] = $output['data']['description'];
}
$result['data'] = $output['data'];
} else {
$result['code'] = 0;
$result['msg'] = '请求结果格式错误';
$result['data'] = [];
}
} elseif ($output['extra']['error_code'] != 0) {
$result['code'] = 0;
$result['msg'] = $output['extra']['description'];
$result['data'] = [];
} else {
$result['code'] = 1;
$result['msg'] = 'success';
$result['data'] = isset($output['data']) ? $output['data'] : [];
}
if ($result['code'] == 0) {
saveLog($result, 3, '抖音返回异常', self::LOG_DIR);
}
return $result;
}
/**
* 抖音开放平台 - 新
*
* @param string $method 请求方法
* @param string $path 请求地址
* @param array $params 请求参数
*
* @return array
* @throws Exception
*/
public function request($method, $path, $params = [])
{
$headers = ['access-token: ' . $this->clientToken];
if ($this->isSandbox) {
$headers[] = 'x-sandbox-token: 1';
}
$retry = 0;
do {
$respObject = $this->curl($method, $path, $params, $headers);
if ($respObject === false) {
$retry++;
usleep(200000);
} else {
break;
}
} while ($retry < 3);
return $this->parseOutput($respObject);
}
/**
* 向抖音发起POST请求
*
* @param string $method 请求方法
* @param string $path 请求地址
* @param array $params 请求参数
* @param array $headers 请求header
*
* @return false|string
* @throws Exception
*/
public function curl($method, $path, $params = [], $headers = [])
{
try {
if ($method === 'GET' && !empty($params)) {
$path .= '?' . http_build_query($params);
}
if ($method === 'POST') {
$headers[] = 'content-type: application/json';
}
$oCurl = curl_init();
curl_setopt($oCurl, CURLOPT_URL, $this->apiUrl . $path);
curl_setopt($oCurl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($oCurl, CURLOPT_HTTPHEADER, $headers);
curl_setopt($oCurl, CURLOPT_CONNECTTIMEOUT, 5);
curl_setopt($oCurl, CURLOPT_TIMEOUT, 30);
curl_setopt($oCurl, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($oCurl, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($oCurl, CURLOPT_SSLVERSION, 1);
if ($method === 'POST') {
curl_setopt($oCurl, CURLOPT_POST, true);
curl_setopt($oCurl, CURLOPT_POSTFIELDS, json_encode($params));
}
$response = curl_exec($oCurl);
saveLog($response, 1, '抖音接口请求结果-' . $method, self::LOG_DIR, ['params' => $params, 'headers' => $headers, 'url' => $path]);
if (curl_errno($oCurl)) {
throw new Exception(curl_error($oCurl), curl_errno($oCurl));
} else {
$httpStatusCode = curl_getinfo($oCurl, CURLINFO_HTTP_CODE);
if (200 !== $httpStatusCode) {
throw new Exception($response, $httpStatusCode);
}
}
curl_close($oCurl);
return json_decode($response, true);
} catch (Exception $e) {
saveLog(exceptionInfo($e), 2, '抖音请求失败-' . $method, self::LOG_DIR, ['params' => $params, 'headers' => $headers, 'url' => $path]);
return false;
}
}
}
抖音开放平台api
于 2024-03-30 11:12:15 首次发布