appId = $config['app_id']; $this->secret = $config['app_secret']; $this->slat = $config['slat']; $this->token = $config['token']; $this->accessTokenFile = storage_path('app/bytedance/bytedance_access_token.json'); $this->accessToken = $this->checkAccessToken(); $this->noticeUrl = env('APP_URL').'/api/pay/bytedance/notify'; return $this; } /** * @param string $code * @return array|mixed * @throws \Exception */ public function login($code = '') { return $this->post(ByteDanceAPI::LOGIN, [ 'appid' => $this->appId, 'secret' => $this->secret, 'code' => $code, ]); } /** * @param string $sessionKey * @param string $iv * @param string $encrypted * @return array * @throws \Exception */ public function decryptData(string $sessionKey, string $iv, string $encrypted): array { $decrypted = openssl_decrypt( base64_decode($encrypted,true), 'AES-128-CBC', base64_decode($sessionKey), OPENSSL_RAW_DATA, base64_decode($iv) ); $decrypted = json_decode($decrypted,true); if(empty($decrypted)){ throw new \Exception('解密数据错误'); } return $decrypted; } /** * 校验access token 是否过期 * @return mixed */ private function checkAccessToken() : string { try { $dir = storage_path('app/bytedance'); if (!is_dir($dir)) mkdir($dir, 0755); if (!is_file($this->accessTokenFile)) touch($this->accessTokenFile); $accessToken = file_get_contents($this->accessTokenFile); $accessToken = $accessToken ? json_decode($accessToken,true) : null; if (empty($accessToken) || $accessToken['expires_at'] < time()) { $accessToken = $this->getAccessToken(); }else{ $accessToken = $accessToken['access_token']; } return $accessToken; } catch (\Exception $e) { } } /** * 获取 access token * @throws \Exception */ private function getAccessToken() : string { $res = $this->post(ByteDanceAPI::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']; } /** * 接口请求 * * @param string $uri * @param array $data * @return array|mixed * @throws \Exception */ private 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); if(!empty($res['err_no'])){ throw new \Exception("请求字节跳动API接口错误,错误码:{$res['err_no']},错误信息:{$res['err_tips']}"); } return $res['data']; } catch (GuzzleException $e) { \Log::error($e->getMessage()); throw new \Exception($e->getMessage()); } } /** * @param $outOrderNo * @param $totalAmount * @return array|mixed * @throws \Exception */ public function createOrder($outOrderNo, $totalAmount) { $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( ByteDanceAPI::CREATE_ORDER, $data ); } /** * @param array $data * @return string */ public function getSign(array $data) { $rList = array(); foreach ($data as $k => $v) { if ($k == "other_settle_params" || $k == "app_id" || $k == "sign" || $k == "thirdparty_id") 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 == "" || $value == "null") continue; array_push($rList, $value); } array_push($rList, $this->slat); sort($rList, SORT_STRING); return md5(implode('&', $rList)); } /** * @param array $data * @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))); } /** * @param \Closure $closure * @return \Illuminate\Http\JsonResponse * @throws \Exception */ public function payNotify(\Closure $closure) { call_user_func($closure, $this->getNoticeData()); return response()->json($data); if ($notify['msg_signature'] !== $this->getNotifySign($notify)) { \Log::error('回调验证错误'); $data = [ 'err_no' => 1, 'err_tips' => 'fail' ]; } else { $order = json_decode($notify['msg'], true); //处理订单 $payId = $order['cp_orderno']; $pay = Pay::find($payId); $pay->pay_type = $order['way']; $pay->serial_number = $order['channel_no']; $pay->status = $order['status'] == 'SUCCESS'; $pay->pay_dt = Carbon::now()->toDateString(); $pay->save(); $data = [ 'err_no' => 0, 'err_tips' => 'success' ]; } return response()->json($data); } private function getNoticeData() { $notify = \request()->all(); $notify = '{"msg":"{\"appid\":\"tt5b312d8cc40f46b701\",\"cp_orderno\":\"10022082800824490007\",\"cp_extra\":\"\",\"way\":\"2\",\"channel_no\":\"2022082822001477591433078541\",\"channel_gateway_no\":\"\",\"payment_order_no\":\"PCP2022082822540531221069106962\",\"out_channel_order_no\":\"2022082822001477591433078541\",\"total_amount\":1,\"status\":\"SUCCESS\",\"seller_uid\":\"71227862181355706950\",\"extra\":\"\",\"item_id\":\"\",\"paid_at\":1661698458,\"message\":\"\",\"order_id\":\"7136939355201063205\",\"trade_item_list\":null,\"ec_pay_trade_no\":\"NEP2022082822540409483061846962\"}","msg_signature":"804a60e48936b14a739230cef21fe6204427732e","nonce":"78","timestamp":"1661698459","type":"payment"}'; //获取订单信息 $notify = json_decode($notify,true); \Log::info('抖音支付回调==>'.json_encode($notify,JSON_UNESCAPED_SLASHES)); if($notify['msg_signature'] !== $this->getNotifySign($notify)){ throw new \Exception('签名验证错误'); } return json_decode($notify['msg'], true); } }