ByteDance.php 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. <?php
  2. namespace App\Helper;
  3. use App\Helper\Bytedance\ByteDanceAPI;
  4. use App\Models\Pay;
  5. use Carbon\Carbon;
  6. use GuzzleHttp\Client;
  7. use GuzzleHttp\Exception\GuzzleException;
  8. /**
  9. * Class ByteDance
  10. *
  11. * @package App\Helper
  12. * @property-read string $appId
  13. * @property-read string $slat
  14. * @property-read string $secret
  15. * @property-read string $token
  16. * @property-read string $accessTokenFile
  17. * @property-read string $accessToken
  18. * @property-read string $noticeUrl
  19. * @property-read string $validTimestamp
  20. */
  21. class ByteDance
  22. {
  23. private $appId = null;
  24. private $secret = null;
  25. private $slat = null;
  26. private $token = null;
  27. private $accessTokenFile = null;
  28. private $accessToken = null;
  29. private $noticeUrl = null;
  30. //订单过期时间(秒);
  31. private $validTimestamp = 24 * 60 * 60;
  32. /**
  33. * @param array $config
  34. * @return $this
  35. */
  36. public function factory($config = [])
  37. {
  38. $this->appId = $config['app_id'];
  39. $this->secret = $config['app_secret'];
  40. $this->slat = $config['slat'];
  41. $this->token = $config['token'];
  42. $this->accessTokenFile = storage_path('app/bytedance/bytedance_access_token.json');
  43. $this->accessToken = $this->checkAccessToken();
  44. $this->noticeUrl = env('APP_URL').'/api/pay/bytedance/notify';
  45. return $this;
  46. }
  47. /**
  48. * @param string $code
  49. * @return array|mixed
  50. * @throws \Exception
  51. */
  52. public function login($code = '')
  53. {
  54. return $this->post(ByteDanceAPI::LOGIN, [
  55. 'appid' => $this->appId,
  56. 'secret' => $this->secret,
  57. 'code' => $code,
  58. ]);
  59. }
  60. /**
  61. * @param string $sessionKey
  62. * @param string $iv
  63. * @param string $encrypted
  64. * @return array
  65. * @throws \Exception
  66. */
  67. public function decryptData(string $sessionKey, string $iv, string $encrypted): array
  68. {
  69. $decrypted = openssl_decrypt(
  70. base64_decode($encrypted,true),
  71. 'AES-128-CBC',
  72. base64_decode($sessionKey),
  73. OPENSSL_RAW_DATA,
  74. base64_decode($iv)
  75. );
  76. $decrypted = json_decode($decrypted,true);
  77. if(empty($decrypted)){
  78. throw new \Exception('解密数据错误');
  79. }
  80. return $decrypted;
  81. }
  82. /**
  83. * 校验access token 是否过期
  84. * @return mixed
  85. */
  86. private function checkAccessToken() : string
  87. {
  88. try {
  89. $dir = storage_path('app/bytedance');
  90. if (!is_dir($dir)) mkdir($dir, 0755);
  91. if (!is_file($this->accessTokenFile)) touch($this->accessTokenFile);
  92. $accessToken = file_get_contents($this->accessTokenFile);
  93. $accessToken = $accessToken ? json_decode($accessToken,true) : null;
  94. if (empty($accessToken) || $accessToken['expires_at'] < time()) {
  95. $accessToken = $this->getAccessToken();
  96. }else{
  97. $accessToken = $accessToken['access_token'];
  98. }
  99. return $accessToken;
  100. } catch (\Exception $e) {
  101. }
  102. }
  103. /**
  104. * 获取 access token
  105. * @throws \Exception
  106. */
  107. private function getAccessToken() : string
  108. {
  109. $res = $this->post(ByteDanceAPI::ACCESS_TOKEN, [
  110. 'grant_type' => 'client_credential',
  111. 'appid' => $this->appId,
  112. 'secret' => $this->secret,
  113. ]);
  114. if (!empty($res['err_no'])) {
  115. throw new \Exception('获取access token 错误');
  116. }
  117. file_put_contents($this->accessTokenFile, json_encode([
  118. 'access_token' => $res['access_token'],
  119. 'expires_at' => $res['expiresAt']
  120. ]));
  121. return $res['access_token'];
  122. }
  123. /**
  124. * 接口请求
  125. *
  126. * @param string $uri
  127. * @param array $data
  128. * @return array|mixed
  129. * @throws \Exception
  130. */
  131. private function post($uri = '', $data = []) : array
  132. {
  133. try {
  134. $client = new Client();
  135. $res = $client->post($uri, [
  136. 'verify' => false,
  137. 'headers' => ['Content-Type' => 'application/json'],
  138. 'body' => json_encode($data)
  139. ]);
  140. $stringBody = (string)$res->getBody();
  141. $res = json_decode($stringBody, true);
  142. if(!empty($res['err_no'])){
  143. throw new \Exception("请求字节跳动API接口错误,错误码:{$res['err_no']},错误信息:{$res['err_tips']}");
  144. }
  145. return $res['data'];
  146. } catch (GuzzleException $e) {
  147. \Log::error($e->getMessage());
  148. throw new \Exception($e->getMessage());
  149. }
  150. }
  151. /**
  152. * @param $outOrderNo
  153. * @param $totalAmount
  154. * @return array|mixed
  155. * @throws \Exception
  156. */
  157. public function createOrder($outOrderNo, $totalAmount)
  158. {
  159. $data = [
  160. 'app_id' => $this->appId,
  161. 'out_order_no' => $outOrderNo,
  162. 'total_amount' => intval($totalAmount * 100),
  163. 'subject' => "订单号:".$outOrderNo,
  164. 'body' => '抖音担保支付',
  165. 'valid_time' => $this->validTimestamp,
  166. 'sign' => $this->secret
  167. //'notify_url' => $notifyUrl, // 可以不设置 使用小程序后台设置的回调
  168. ];
  169. $data = array_filter($data);
  170. $data['sign'] = $this->getSign($data);
  171. return $this->post(
  172. ByteDanceAPI::CREATE_ORDER,
  173. $data
  174. );
  175. }
  176. /**
  177. * @param array $data
  178. * @return string
  179. */
  180. public function getSign(array $data)
  181. {
  182. $rList = array();
  183. foreach ($data as $k => $v) {
  184. if ($k == "other_settle_params" || $k == "app_id" || $k == "sign" || $k == "thirdparty_id")
  185. continue;
  186. $value = trim(strval($v));
  187. $len = strlen($value);
  188. if ($len > 1 && substr($value, 0, 1) == "\"" && substr($value, $len, $len - 1) == "\"")
  189. $value = substr($value, 1, $len - 1);
  190. $value = trim($value);
  191. if ($value == "" || $value == "null")
  192. continue;
  193. array_push($rList, $value);
  194. }
  195. array_push($rList, $this->slat);
  196. sort($rList, SORT_STRING);
  197. return md5(implode('&', $rList));
  198. }
  199. /**
  200. * @param array $data
  201. * @return string
  202. */
  203. public function getNotifySign(array $data)
  204. {
  205. $filtered = [];
  206. foreach ($data as $key => $value) {
  207. if (in_array($key, ['msg_signature', 'type'])) {
  208. continue;
  209. }
  210. $value = trim(strval($value));
  211. $len = strlen($value);
  212. if ($len > 1 && substr($value, 0, 1) == "\"" && substr($value, $len, $len - 1) == "\"")
  213. $value = substr($value, 1, $len - 1);
  214. $filtered[] =
  215. is_string($value)
  216. ? trim($value)
  217. : $value;
  218. }
  219. $filtered[] = trim($this->token);
  220. sort($filtered, SORT_STRING);
  221. return sha1(trim(implode('', $filtered)));
  222. }
  223. /**
  224. * @param \Closure $closure
  225. * @return \Illuminate\Http\JsonResponse
  226. * @throws \Exception
  227. */
  228. public function payNotify(\Closure $closure)
  229. {
  230. call_user_func($closure, $this->getNoticeData());
  231. return response()->json($data);
  232. if ($notify['msg_signature'] !== $this->getNotifySign($notify)) {
  233. \Log::error('回调验证错误');
  234. $data = [
  235. 'err_no' => 1,
  236. 'err_tips' => 'fail'
  237. ];
  238. } else {
  239. $order = json_decode($notify['msg'], true);
  240. //处理订单
  241. $payId = $order['cp_orderno'];
  242. $pay = Pay::find($payId);
  243. $pay->pay_type = $order['way'];
  244. $pay->serial_number = $order['channel_no'];
  245. $pay->status = $order['status'] == 'SUCCESS';
  246. $pay->pay_dt = Carbon::now()->toDateString();
  247. $pay->save();
  248. $data = [
  249. 'err_no' => 0,
  250. 'err_tips' => 'success'
  251. ];
  252. }
  253. return response()->json($data);
  254. }
  255. private function getNoticeData()
  256. {
  257. $notify = \request()->all();
  258. $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"}';
  259. //获取订单信息
  260. $notify = json_decode($notify,true);
  261. \Log::info('抖音支付回调==>'.json_encode($notify,JSON_UNESCAPED_SLASHES));
  262. if($notify['msg_signature'] !== $this->getNotifySign($notify)){
  263. throw new \Exception('签名验证错误');
  264. }
  265. return json_decode($notify['msg'], true);
  266. }
  267. }