ToutiaoPayService.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  1. <?php
  2. namespace app\common\service\pay;
  3. use app\common\enum\PayEnum;
  4. use app\common\enum\YesNoEnum;
  5. use app\common\logic\PayNotifyLogic;
  6. use app\common\model\Order;
  7. use app\common\model\RechargeOrder;
  8. use app\common\model\Refund;
  9. use app\common\service\ConfigService;
  10. use think\Exception;
  11. class ToutiaoPayService
  12. {
  13. private $config;
  14. public function __construct()
  15. {
  16. $this->config = $this->getConfig();
  17. if (empty($this->config['appid']) || empty($this->config['secret'])) {
  18. throw new \Exception("请先在后台配置appid和secret");
  19. }
  20. }
  21. /**
  22. * @notes 获取配置
  23. * @return array
  24. * @author Tab
  25. * @date 2021/11/12 9:21
  26. */
  27. private function getConfig()
  28. {
  29. return [
  30. 'appid' => ConfigService::get("toutiao", "appid", ''),
  31. 'secret' => ConfigService::get("toutiao", "secret", ''),
  32. 'access_token' => ConfigService::get("toutiao", "access_token", ''),
  33. 'expires_in' => ConfigService::get("toutiao", "expires_in", ''),
  34. 'expires_in_time' => ConfigService::get("toutiao", "expires_in_time", ''),
  35. 'pay_salt' => ConfigService::get("toutiao", "pay_salt", ''),
  36. ];
  37. }
  38. /**
  39. * @notes 获取access_token
  40. * 官方:access_token 最多2小时即过期,重复获取 access_token 会导致上次 access_token失效
  41. * 官方:为了平滑过渡,新老 access_token 在 5 分钟内都可使用
  42. * @return mixed
  43. * @throws \Exception
  44. * @author Tab
  45. * @date 2021/11/12 9:25
  46. */
  47. public function getAccessToken()
  48. {
  49. // 已获取过access_token并且仍在有效期
  50. if (!empty($this->config['access_token']) && $this->config['expires_in_time'] > time()) {
  51. return $this->config['access_token'];
  52. }
  53. // 重新获取access_token
  54. $url = 'https://developer.toutiao.com/api/apps/v2/token';
  55. $data = [
  56. "appid" => $this->config['appid'],
  57. "secret" => $this->config['secret'],
  58. "grant_type" => 'client_credential' // 固定值
  59. ];
  60. $data = json_encode($data, JSON_UNESCAPED_UNICODE);
  61. $result = $this->http_post($url, $data);
  62. $result = json_decode($result, true);
  63. // 获取成功
  64. if ($result['err_no'] == 0) {
  65. // 获取成功存入数据库
  66. ConfigService::set('toutiao', 'access_token', $result['data']['access_token']);
  67. // expires_in : access_token 有效时间,单位:秒
  68. ConfigService::set('toutiao', 'expires_in', $result['data']['expires_in']);
  69. // 有效期比官方缩短10分钟,提前去重新获取access_token
  70. $expires_in_time = time() + $result['data']['expires_in'] - 600;
  71. ConfigService::set('toutiao', 'expires_in_time', $expires_in_time);
  72. return $result['data']['access_token'];
  73. }
  74. // 获取失败
  75. throw new \Exception("access_token获取失败:" . $result['err_tips']);
  76. }
  77. /**
  78. * @notes post请求
  79. * 官方:参数应以 JSON 字符串形式写入, 需要设置请求头 "content-type": "application/json"
  80. * @param $url
  81. * @param $data
  82. * @return bool|string
  83. * @throws \Exception
  84. * @author Tab
  85. * @date 2021/11/12 9:25
  86. */
  87. public function http_post($url,$data){
  88. $curl = curl_init();
  89. curl_setopt($curl, CURLOPT_URL, $url);
  90. curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
  91. curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
  92. curl_setopt($curl, CURLOPT_POST, true);
  93. curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
  94. curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
  95. curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-Type:application/json', 'Content-Length:' . strlen($data)));
  96. $result = curl_exec($curl);
  97. if(curl_errno($curl)) {
  98. throw new \Exception('Errno'.curl_errno($curl));
  99. }
  100. curl_close($curl);
  101. return $result;
  102. }
  103. /**
  104. * @notes 预下单
  105. * $params 需包含3个元素: order_id 订单id from 来源类型(order/recharge) pay_way 支付方式
  106. * @author Tab
  107. * @date 2021/11/12 17:19
  108. */
  109. public function createOrder($params)
  110. {
  111. $order = $this->getOrderInfo($params);
  112. // 核心字段
  113. $map = [
  114. // 添加from前缀,避免订单表与充值表单号冲突
  115. "out_order_no" => strtolower($params['from']).$order['sn'],
  116. "total_amount" => $order['order_amount'] * 100,
  117. "subject" => $order['sn']."预下单",
  118. "body" => $order['sn']."预下单",
  119. "valid_time" => 24 * 3600,
  120. "cp_extra" => strtolower($params['from']),
  121. ];
  122. $url = 'https://developer.toutiao.com/api/apps/ecpay/v1/create_order';
  123. $data = [
  124. "app_id" => $this->config['appid'],
  125. // 订单号
  126. "out_order_no" => $map['out_order_no'],
  127. // 支付金额,单位为:分
  128. "total_amount" => $map['total_amount'],
  129. // 商品描述; 长度限制 128 字节,不超过 42 个汉字
  130. "subject" => $map['subject'],
  131. // 商品详情
  132. "body" => $map['body'],
  133. // 订单过期时间(秒); 最小 15 分钟,最大两天
  134. "valid_time" => $map['valid_time'],
  135. // 开发者自定义字段,回调原样回传
  136. "cp_extra" => $map['cp_extra'],
  137. // 核心字段签名
  138. "sign" => $this->sign($map)
  139. ];
  140. $data = json_encode($data, JSON_UNESCAPED_UNICODE);
  141. $result = $this->http_post($url, $data);
  142. $result = json_decode($result, true);
  143. if ($result['err_no'] == 0) {
  144. // 返回给前端的orderInfo
  145. return [
  146. "config" => $result["data"],
  147. "pay_way" => $params['pay_way']
  148. ];
  149. }
  150. throw new \Exception("错误码:".$result['err_no'].";出错原因:".$result['err_tips']);
  151. }
  152. /**
  153. * @notes 统一回调接口(支付、退款、分账)
  154. * @author Tab
  155. * @date 2021/11/17 11:41
  156. */
  157. public function notify($params)
  158. {
  159. switch ($params['type']) {
  160. // 支付回调
  161. case "payment":
  162. return $this->paymentNotify($params);
  163. // 退款回调
  164. case "refund":
  165. return $this->refundNotify($params);
  166. // 分账回调
  167. case "settle":
  168. return $this->settleNotify($params);
  169. }
  170. }
  171. /**
  172. * @notes 支付回调
  173. * @author Tab
  174. * @date 2021/11/18 9:40
  175. */
  176. public function paymentNotify($params)
  177. {
  178. // 验证请求是否来自字节小程序平台服务端
  179. $msg = json_decode($params['msg'], true);
  180. if ($msg['cp_extra'] != 'order' && $msg['cp_extra'] != 'recharge') {
  181. // 与预下单时自定义的字段不同,不是预想的回调请求
  182. return false;
  183. }
  184. // 支付成功
  185. if ($msg['status'] == 'SUCCESS') {
  186. // 提取订单号
  187. $orderSn = $this->getOrderSn($msg);
  188. $result = PayNotifyLogic::handle($msg['cp_extra'], $orderSn, ['transaction_id' => $msg['payment_order_no']]);
  189. return $result === true ? $this->processSuccess() : false;
  190. }
  191. return false;
  192. }
  193. /**
  194. * @notes 退款回调
  195. * @author Tab
  196. * @date 2021/11/18 9:41
  197. */
  198. public function refundNotify($params)
  199. {
  200. // 验证请求是否来自字节小程序平台服务端
  201. $msg = json_decode($params['msg'], true);
  202. if ($msg['cp_extra'] != 'tk') {
  203. // 与预下单时自定义的字段不同,不是预想的回调请求
  204. return false;
  205. }
  206. unset($msg['appid']);
  207. $offset = strlen($msg['cp_extra']);
  208. $sn = substr($msg['cp_refundno'], $offset);
  209. Refund::update(['refund_msg' => json_encode($msg)], ['sn' => $sn]);
  210. return $this->processSuccess();
  211. }
  212. /**
  213. * @notes 分账回调
  214. * @author Tab
  215. * @date 2021/11/18 9:43
  216. */
  217. public function settleNotify()
  218. {
  219. }
  220. /**
  221. * @notes 提取订单号
  222. * @param $msg
  223. * @author Tab
  224. * @date 2021/11/18 11:06
  225. */
  226. public function getOrderSn($msg)
  227. {
  228. $offset = strlen($msg['cp_extra']);
  229. return substr($msg['cp_orderno'], $offset);
  230. }
  231. /**
  232. * @notes 获取订单信息
  233. * @param $params
  234. * @author Tab
  235. * @date 2021/11/18 9:58
  236. */
  237. public function getOrderInfo($params)
  238. {
  239. switch ($params['from']) {
  240. case "order":
  241. $order = Order::findOrEmpty($params['order_id']);
  242. break;
  243. case "recharge":
  244. $order = RechargeOrder::findOrEmpty($params['order_id']);
  245. break;
  246. }
  247. if ($order->isEmpty()) {
  248. throw new \Exception("订单不存在");
  249. }
  250. if ($order->pay_status == PayEnum::ISPAID) {
  251. throw new \Exception("订单已支付,请勿重复预下单");
  252. }
  253. return $order->toArray();
  254. }
  255. /**
  256. * @notes 退款
  257. * @author Tab
  258. * @date 2021/11/18 14:10
  259. */
  260. public function refund($order, $refundAmount, $refundOrder)
  261. {
  262. // 核心字段
  263. $map = [
  264. // 商户分配订单号,标识进行退款的订单
  265. 'out_order_no' => 'order' . $order['sn'],
  266. // 商户分配退款号
  267. 'out_refund_no' => 'tk' . $refundOrder['sn'],
  268. // 退款原因
  269. 'reason' => '订单退款',
  270. // 退款金额,单位[分]
  271. 'refund_amount' => $refundAmount * 100,
  272. // 开发者自定义字段,回调原样回传
  273. 'cp_extra' => 'tk',
  274. // 是否为分账后退款,1-分账后退款;0-分账前退款。分账后退款会扣减可提现金额,请保证余额充足
  275. 'all_settle' => 0,
  276. ];
  277. $url = 'https://developer.toutiao.com/api/apps/ecpay/v1/create_refund';
  278. $data = [
  279. "app_id" => $this->config['appid'],
  280. 'out_order_no' => $map['out_order_no'],
  281. 'out_refund_no' => $map['out_refund_no'],
  282. 'reason' => $map['reason'],
  283. 'refund_amount' => $map['refund_amount'],
  284. 'cp_extra' => $map['cp_extra'],
  285. 'all_settle' => $map['all_settle'],
  286. "sign" => $this->sign($map)
  287. ];
  288. $data = json_encode($data, JSON_UNESCAPED_UNICODE);
  289. $result = $this->http_post($url, $data);
  290. $result = json_decode($result, true);
  291. if ($result['err_no'] == 0) {
  292. Refund::update([
  293. 'id' => $refundOrder['id'],
  294. 'transaction_id' => $refundOrder['refund_no'],
  295. 'refund_msg' => json_encode($result),
  296. ]);
  297. return true;
  298. }
  299. throw new \Exception("错误码:".$result['err_no'].";出错原因:".$result['err_tips']);
  300. }
  301. /**
  302. * @notes 请求加签
  303. * @author Tab
  304. * @date 2021/11/17 11:45
  305. */
  306. public function sign($map)
  307. {
  308. if (empty($this->config['pay_salt'])) {
  309. throw new \Exception("后台请配置支付SALT");
  310. }
  311. $rList = array();
  312. foreach($map as $k =>$v) {
  313. if ($k == "other_settle_params" || $k == "app_id" || $k == "sign" || $k == "thirdparty_id")
  314. continue;
  315. $value = trim(strval($v));
  316. $len = strlen($value);
  317. if ($len > 1 && substr($value, 0,1)=="\"" && substr($value,$len, $len-1)=="\"")
  318. $value = substr($value,1, $len-1);
  319. $value = trim($value);
  320. if ($value == "" || $value == "null")
  321. continue;
  322. array_push($rList, $value);
  323. }
  324. array_push($rList, $this->config['pay_salt']);
  325. sort($rList, 2);
  326. return md5(implode('&', $rList));
  327. }
  328. /**
  329. * @notes 查询订单
  330. * @return mixed
  331. * @throws \Exception
  332. * @author Tab
  333. * @date 2021/11/17 19:08
  334. */
  335. public function queryOrder($orderId, $from)
  336. {
  337. if ($from == 'order') {
  338. $order = Order::findOrEmpty($orderId);
  339. }
  340. if ($order->isEmpty()) {
  341. throw new \Exception("订单不存在");
  342. }
  343. if ($order->pay_status == PayEnum::ISPAID) {
  344. throw new \Exception("订单已支付,无需查询");
  345. }
  346. $order = $order->toArray();
  347. $url = 'https://developer.toutiao.com/api/apps/ecpay/v1/query_order';
  348. $map = [
  349. 'out_order_no' => $order['sn']
  350. ];
  351. $data = [
  352. "app_id" => $this->config['appid'],
  353. "out_order_no" => $map['out_order_no'],
  354. "sign" => $this->sign($map),
  355. ];
  356. $data = json_encode($data, JSON_UNESCAPED_UNICODE);
  357. $result = $this->http_post($url, $data);
  358. $result = json_decode($result, true);
  359. // 查询成功
  360. if ($result['err_no'] == 0) {
  361. // 返回整个查询结果
  362. return $result;
  363. }
  364. // 查询失败
  365. throw new \Exception("查询失败:" . $result['err_tips']);
  366. }
  367. /**
  368. * @notes 查询退款
  369. * @author Tab
  370. * @date 2021/11/18 14:43
  371. */
  372. public function queryRefund($refundSn)
  373. {
  374. // 核心字段
  375. $map = [
  376. 'out_refund_no' => 'tk'.$refundSn
  377. ];
  378. $url = 'https://developer.toutiao.com/api/apps/ecpay/v1/query_refund';
  379. $data = [
  380. "app_id" => $this->config['appid'],
  381. 'out_refund_no' => $map['out_refund_no'],
  382. "sign" => $this->sign($map)
  383. ];
  384. $data = json_encode($data, JSON_UNESCAPED_UNICODE);
  385. $result = $this->http_post($url, $data);
  386. $result = json_decode($result, true);
  387. if ($result['err_no'] == 0) {
  388. if ($result['refundInfo']['refund_status'] == 'SUCCESS') {
  389. return true;
  390. }
  391. if ($result['refundInfo']['refund_status'] == 'FAIL') {
  392. return false;
  393. }
  394. }
  395. return null;
  396. }
  397. /**
  398. * @notes 回调处理成功信息
  399. * @return array
  400. * @author Tab
  401. * @date 2021/11/18 9:36
  402. */
  403. public function processSuccess()
  404. {
  405. $data = [
  406. "err_no" => 0,
  407. "err_tips" => "success",
  408. ];
  409. return json_encode($data);
  410. }
  411. public function getQrcode(array $param){
  412. //获取access_token
  413. $accessToken = $this->getAccessToken();
  414. $url = 'https://developer.toutiao.com/api/apps/qrcode';
  415. $data = [
  416. "access_token" => $accessToken,
  417. "path" => $param['page'],
  418. "appname" => $param['appname']
  419. ];
  420. $data = json_encode($data, JSON_UNESCAPED_UNICODE);
  421. $result = $this->http_post($url,$data);
  422. $error = json_decode($result,true);
  423. if($error){
  424. throw new Exception($error['errmsg']);
  425. };
  426. $mpBase64 = chunk_split(base64_encode($result));
  427. $contents = 'data:image/png;base64,' . $mpBase64;
  428. return $contents;
  429. }
  430. }