API = $api; } /** * 获取 access token. * * @throws \Exception */ protected function getAccessToken(): string { $res = $this->post($this->API::ACCESS_TOKEN, [ 'grant_type' => 'client_credential', 'appid' => $this->appId, 'secret' => $this->secret, ]); if (!empty($res['err_no'])) { throw new \Exception('获取access token 错误'); } file_put_contents($this->accessTokenFile, json_encode([ 'access_token' => $res['access_token'], 'expires_at' => $res['expiresAt'], ])); return $res['access_token']; } /** * @return array|mixed * * @throws \Exception */ public function createOrder($outOrderNo, $totalAmount, $openId): array { $data = [ 'app_id' => $this->appId, 'out_order_no' => $outOrderNo, 'total_amount' => intval($totalAmount * 100), 'subject' => '订单号:' . $outOrderNo, 'body' => '抖音担保支付', 'valid_time' => $this->validTimestamp, 'sign' => $this->secret, // 'notify_url' => $notifyUrl, // 可以不设置 使用小程序后台设置的回调 ]; $data = array_filter($data); $data['sign'] = $this->getSign($data); return $this->post( $this->API::CREATE_ORDER, $data ); } /** * 结算. * * @return array|mixed|string[] * * @throws \Exception */ public function settle($outOrderNo) { $data = [ 'app_id' => $this->appId, 'out_settle_no' => $outOrderNo, 'out_order_no' => $outOrderNo, 'settle_desc' => '主动结算', ]; $data = array_filter($data); $data['sign'] = $this->getSign($data); return $this->post( $this->API::SETTLE, $data ); } /** * @return string */ public function getSign(array $data) { $rList = []; foreach ($data as $k => $v) { if ('other_settle_params' == $k || 'app_id' == $k || 'sign' == $k || 'thirdparty_id' == $k) { continue; } $value = trim(strval($v)); $len = strlen($value); if ($len > 1 && '"' == substr($value, 0, 1) && '"' == substr($value, $len, $len - 1)) { $value = substr($value, 1, $len - 1); } $value = trim($value); if ('' == $value || 'null' == $value) { continue; } array_push($rList, $value); } array_push($rList, $this->slat); sort($rList, SORT_STRING); return md5(implode('&', $rList)); } /** * @return string */ public function getNotifySign(array $data) { $filtered = []; foreach ($data as $key => $value) { if (in_array($key, ['msg_signature', 'type'])) { continue; } $value = trim(strval($value)); $len = strlen($value); if ($len > 1 && '"' == substr($value, 0, 1) && '"' == substr($value, $len, $len - 1)) { $value = substr($value, 1, $len - 1); } $filtered[] = is_string($value) ? trim($value) : $value; } $filtered[] = trim($this->token); sort($filtered, SORT_STRING); return sha1(trim(implode('', $filtered))); } public function pushOrder($openid, $orderId, $goods, $status) { $data = [ 'access_token' => $this->accessToken, 'open_id' => $openid, 'order_type' => 0, // 0:普通小程序订单(非 POI 订单), 'order_status' => 1, 'app_name' => 'douyin', 'update_time' => (int) Carbon::now()->getPreciseTimestamp(3), 'order_detail' => json_encode([ 'order_id' => $orderId, 'create_time' => (int) Carbon::now()->getPreciseTimestamp(3), 'status' => $status, // 已支付 待支付 'amount' => 1, 'total_price' => $goods['price'] * 100, 'detail_url' => 'pages/my/consume', 'item_list' => [ [ 'item_code' => (string) $goods['id'], // 商品ID, 'img' => 'https://zhengda.oss-cn-chengdu.aliyuncs.com/zhangsiye/images/26474488be1b83e2fcb0b9475508a9bb.png', 'title' => $goods['title'], 'price' => $goods['price'] * 100, ], ], ], JSON_UNESCAPED_UNICODE), ]; return $this->post($this->API::ORDER_PUSH, $data); } /** * @param string $code * * @return array|mixed * * @throws \Exception */ public function login($code = ''): array { return $this->post($this->API::LOGIN, [ 'appid' => $this->appId, 'secret' => $this->secret, 'code' => $code, ]); } /** * @return array|mixed * * @throws \Exception */ public function generateQrcode() { $userId = \user()->id; return $this->post($this->API::CREATE_QRCODE, [ 'appname' => 'douyin', 'access_token' => $this->accessToken, 'path' => urlencode('pages/index/index?' . "user_id=$userId"), 'width' => 600, ]); } /** * 接口请求 * * @param string $uri * @param array $data * * @return array|mixed * * @throws \Exception */ protected function post($uri = '', $data = []): array { try { $client = new Client(); $res = $client->post($uri, [ 'verify' => false, 'headers' => ['Content-Type' => 'application/json'], 'body' => json_encode($data), ]); $stringBody = (string) $res->getBody(); $res = json_decode($stringBody, true); // 生成二维码接口是直接放回 buffer的 if (empty($res) && $uri == $this->API::CREATE_QRCODE) { return [$stringBody]; } if (isset($res['err_no']) && !empty($res['err_no'])) { throw new \Exception("请求字节跳动API接口错误,错误码:{$res['err_no']},错误信息:{$res['err_tips']}"); } if (isset($res['err_code']) && !empty($res['err_code'])) { throw new \Exception("请求字节跳动API接口错误,错误码:{$res['err_msg']},错误信息:{$res['err_msg']}"); } return $res['data'] ?? $res; } catch (GuzzleException $e) { \Log::error($e->getMessage()); throw new \Exception($e->getMessage()); } } protected function setAccessFileDir(): void { $this->accessTokenDir = storage_path('app/bytedance'); } protected function setAccessFilePath(): void { $this->accessTokenFile = storage_path('app/bytedance/bytedance_access_token.json'); } protected function setNoticeUrl(): void { $this->noticeUrl = env('APP_URL') . '/api/pay/bytedance/notify'; } }