API.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645
  1. <?php
  2. /*
  3. * This file is part of the overtrue/wechat.
  4. *
  5. * (c) overtrue <i@overtrue.me>
  6. *
  7. * This source file is subject to the MIT license that is bundled
  8. * with this source code in the file LICENSE.
  9. */
  10. /**
  11. * API.php.
  12. *
  13. * @author overtrue <i@overtrue.me>
  14. * @copyright 2015 overtrue <i@overtrue.me>
  15. *
  16. * @see https://github.com/overtrue
  17. * @see http://overtrue.me
  18. */
  19. namespace EasyWeChat\Payment;
  20. use Doctrine\Common\Cache\Cache;
  21. use Doctrine\Common\Cache\FilesystemCache;
  22. use EasyWeChat\Core\AbstractAPI;
  23. use EasyWeChat\Core\Exception;
  24. use EasyWeChat\Support\Collection;
  25. use EasyWeChat\Support\Log;
  26. use EasyWeChat\Support\XML;
  27. use Psr\Http\Message\ResponseInterface;
  28. /**
  29. * Class API.
  30. */
  31. class API extends AbstractAPI
  32. {
  33. /**
  34. * Merchant instance.
  35. *
  36. * @var Merchant
  37. */
  38. protected $merchant;
  39. /**
  40. * Sandbox box mode.
  41. *
  42. * @var bool
  43. */
  44. protected $sandboxEnabled = false;
  45. /**
  46. * Sandbox sign key.
  47. *
  48. * @var string
  49. */
  50. protected $sandboxSignKey;
  51. /**
  52. * Cache.
  53. *
  54. * @var \Doctrine\Common\Cache\Cache
  55. */
  56. protected $cache;
  57. const API_HOST = 'https://api.mch.weixin.qq.com';
  58. // api
  59. const API_PAY_ORDER = '/pay/micropay';
  60. const API_PREPARE_ORDER = '/pay/unifiedorder';
  61. const API_QUERY = '/pay/orderquery';
  62. const API_CLOSE = '/pay/closeorder';
  63. const API_REVERSE = '/secapi/pay/reverse';
  64. const API_REFUND = '/secapi/pay/refund';
  65. const API_QUERY_REFUND = '/pay/refundquery';
  66. const API_DOWNLOAD_BILL = '/pay/downloadbill';
  67. const API_REPORT = '/payitil/report';
  68. const API_PROFITSHARING = '/secapi/pay/profitsharing';
  69. const API_MULTI_PROFIT_SHARING = '/secapi/pay/multiprofitsharing';
  70. const API_PROFIT_SHARING_ADD_RECEIVER = '/secapi/pay/profitsharingaddreceiver';
  71. const API_PROFIT_SHARING_FINISH = '/secapi/pay/profitsharingfinish';
  72. const API_PROFIT_SHARING_QUERY = '/pay/profitsharingquery';
  73. const API_URL_SHORTEN = 'https://api.mch.weixin.qq.com/tools/shorturl';
  74. const API_AUTH_CODE_TO_OPENID = 'https://api.mch.weixin.qq.com/tools/authcodetoopenid';
  75. const API_SANDBOX_SIGN_KEY = 'https://api.mch.weixin.qq.com/sandboxnew/pay/getsignkey';
  76. // order id types.
  77. const TRANSACTION_ID = 'transaction_id';
  78. const OUT_TRADE_NO = 'out_trade_no';
  79. const OUT_REFUND_NO = 'out_refund_no';
  80. const REFUND_ID = 'refund_id';
  81. // bill types.
  82. const BILL_TYPE_ALL = 'ALL';
  83. const BILL_TYPE_SUCCESS = 'SUCCESS';
  84. const BILL_TYPE_REFUND = 'REFUND';
  85. const BILL_TYPE_REVOKED = 'REVOKED';
  86. /**
  87. * API constructor.
  88. *
  89. * @param \EasyWeChat\Payment\Merchant $merchant
  90. * @param \Doctrine\Common\Cache\Cache|null $cache
  91. */
  92. public function __construct(Merchant $merchant, Cache $cache = null)
  93. {
  94. $this->merchant = $merchant;
  95. $this->cache = $cache;
  96. }
  97. /**
  98. * Pay the order.
  99. *
  100. * @param Order $order
  101. *
  102. * @return \EasyWeChat\Support\Collection
  103. */
  104. public function pay(Order $order)
  105. {
  106. return $this->request($this->wrapApi(self::API_PAY_ORDER), $order->all());
  107. }
  108. /**
  109. * Prepare order to pay.
  110. *
  111. * @param Order $order
  112. *
  113. * @return \EasyWeChat\Support\Collection
  114. */
  115. public function prepare(Order $order)
  116. {
  117. $order->notify_url = $order->get('notify_url', $this->merchant->notify_url);
  118. $order->profit_sharing = $order->get('profit_sharing', $this->merchant->profit_sharing);
  119. if (is_null($order->spbill_create_ip)) {
  120. $order->spbill_create_ip = (Order::NATIVE === $order->trade_type) ? get_server_ip() : get_client_ip();
  121. }
  122. return $this->request($this->wrapApi(self::API_PREPARE_ORDER), $order->all());
  123. }
  124. /**
  125. * Query order.
  126. *
  127. * @param string $orderNo
  128. * @param string $type
  129. *
  130. * @return \EasyWeChat\Support\Collection
  131. */
  132. public function query($orderNo, $type = self::OUT_TRADE_NO)
  133. {
  134. $params = [
  135. $type => $orderNo,
  136. ];
  137. return $this->request($this->wrapApi(self::API_QUERY), $params);
  138. }
  139. /**
  140. * Query order by transaction_id.
  141. *
  142. * @param string $transactionId
  143. *
  144. * @return \EasyWeChat\Support\Collection
  145. */
  146. public function queryByTransactionId($transactionId)
  147. {
  148. return $this->query($transactionId, self::TRANSACTION_ID);
  149. }
  150. /**
  151. * Close order by out_trade_no.
  152. *
  153. * @param $tradeNo
  154. *
  155. * @return \EasyWeChat\Support\Collection
  156. */
  157. public function close($tradeNo)
  158. {
  159. $params = [
  160. 'out_trade_no' => $tradeNo,
  161. ];
  162. return $this->request($this->wrapApi(self::API_CLOSE), $params);
  163. }
  164. /**
  165. * Reverse order.
  166. *
  167. * @param string $orderNo
  168. * @param string $type
  169. *
  170. * @return \EasyWeChat\Support\Collection
  171. */
  172. public function reverse($orderNo, $type = self::OUT_TRADE_NO)
  173. {
  174. $params = [
  175. $type => $orderNo,
  176. ];
  177. return $this->safeRequest($this->wrapApi(self::API_REVERSE), $params);
  178. }
  179. /**
  180. * Reverse order by transaction_id.
  181. *
  182. * @param int $transactionId
  183. *
  184. * @return \EasyWeChat\Support\Collection
  185. */
  186. public function reverseByTransactionId($transactionId)
  187. {
  188. return $this->reverse($transactionId, self::TRANSACTION_ID);
  189. }
  190. /**
  191. * Make a refund request.
  192. *
  193. * @param string $orderNo
  194. * @param string $refundNo
  195. * @param float $totalFee
  196. * @param float $refundFee
  197. * @param string $opUserId
  198. * @param string $type
  199. * @param string $refundAccount
  200. * @param string $refundReason
  201. *
  202. * @return Collection
  203. */
  204. public function refund(
  205. $orderNo,
  206. $refundNo,
  207. $totalFee,
  208. $refundFee = null,
  209. $opUserId = null,
  210. $type = self::OUT_TRADE_NO,
  211. $refundAccount = 'REFUND_SOURCE_UNSETTLED_FUNDS',
  212. $refundReason = ''
  213. ) {
  214. $params = [
  215. $type => $orderNo,
  216. 'out_refund_no' => $refundNo,
  217. 'total_fee' => $totalFee,
  218. 'refund_fee' => $refundFee ?: $totalFee,
  219. 'refund_fee_type' => $this->merchant->fee_type,
  220. 'refund_account' => $refundAccount,
  221. 'refund_desc' => $refundReason,
  222. 'op_user_id' => $opUserId ?: $this->merchant->merchant_id,
  223. ];
  224. return $this->safeRequest($this->wrapApi(self::API_REFUND), $params);
  225. }
  226. /**
  227. * Refund by transaction id.
  228. *
  229. * @param string $orderNo
  230. * @param string $refundNo
  231. * @param float $totalFee
  232. * @param float $refundFee
  233. * @param string $opUserId
  234. * @param string $refundAccount
  235. * @param string $refundReason
  236. *
  237. * @return Collection
  238. */
  239. public function refundByTransactionId(
  240. $orderNo,
  241. $refundNo,
  242. $totalFee,
  243. $refundFee = null,
  244. $opUserId = null,
  245. $refundAccount = 'REFUND_SOURCE_UNSETTLED_FUNDS',
  246. $refundReason = ''
  247. ) {
  248. return $this->refund($orderNo, $refundNo, $totalFee, $refundFee, $opUserId, self::TRANSACTION_ID, $refundAccount, $refundReason);
  249. }
  250. /**
  251. * Query refund status.
  252. *
  253. * @param string $orderNo
  254. * @param string $type
  255. *
  256. * @return \EasyWeChat\Support\Collection
  257. */
  258. public function queryRefund($orderNo, $type = self::OUT_TRADE_NO)
  259. {
  260. $params = [
  261. $type => $orderNo,
  262. ];
  263. return $this->request($this->wrapApi(self::API_QUERY_REFUND), $params);
  264. }
  265. /**
  266. * Query refund status by out_refund_no.
  267. *
  268. * @param string $refundNo
  269. *
  270. * @return \EasyWeChat\Support\Collection
  271. */
  272. public function queryRefundByRefundNo($refundNo)
  273. {
  274. return $this->queryRefund($refundNo, self::OUT_REFUND_NO);
  275. }
  276. /**
  277. * Query refund status by transaction_id.
  278. *
  279. * @param string $transactionId
  280. *
  281. * @return \EasyWeChat\Support\Collection
  282. */
  283. public function queryRefundByTransactionId($transactionId)
  284. {
  285. return $this->queryRefund($transactionId, self::TRANSACTION_ID);
  286. }
  287. /**
  288. * Query refund status by refund_id.
  289. *
  290. * @param string $refundId
  291. *
  292. * @return \EasyWeChat\Support\Collection
  293. */
  294. public function queryRefundByRefundId($refundId)
  295. {
  296. return $this->queryRefund($refundId, self::REFUND_ID);
  297. }
  298. /**
  299. * Download bill history as a table file.
  300. *
  301. * @param string $date
  302. * @param string $type
  303. *
  304. * @return \Psr\Http\Message\ResponseInterface
  305. */
  306. public function downloadBill($date, $type = self::BILL_TYPE_ALL)
  307. {
  308. $params = [
  309. 'bill_date' => $date,
  310. 'bill_type' => $type,
  311. ];
  312. return $this->request($this->wrapApi(self::API_DOWNLOAD_BILL), $params, 'post', [\GuzzleHttp\RequestOptions::STREAM => true], true)->getBody();
  313. }
  314. /**
  315. * Convert long url to short url.
  316. *
  317. * @param string $url
  318. *
  319. * @return \EasyWeChat\Support\Collection
  320. */
  321. public function urlShorten($url)
  322. {
  323. return $this->request(self::API_URL_SHORTEN, ['long_url' => $url]);
  324. }
  325. /**
  326. * Report API status to WeChat.
  327. *
  328. * @param string $api
  329. * @param int $timeConsuming
  330. * @param string $resultCode
  331. * @param string $returnCode
  332. * @param array $other ex: err_code,err_code_des,out_trade_no,user_ip...
  333. *
  334. * @return \EasyWeChat\Support\Collection
  335. */
  336. public function report($api, $timeConsuming, $resultCode, $returnCode, array $other = [])
  337. {
  338. $params = array_merge([
  339. 'interface_url' => $api,
  340. 'execute_time_' => $timeConsuming,
  341. 'return_code' => $returnCode,
  342. 'return_msg' => null,
  343. 'result_code' => $resultCode,
  344. 'user_ip' => get_client_ip(),
  345. 'time' => time(),
  346. ], $other);
  347. return $this->request($this->wrapApi(self::API_REPORT), $params);
  348. }
  349. /**
  350. * Get openid by auth code.
  351. *
  352. * @param string $authCode
  353. *
  354. * @return \EasyWeChat\Support\Collection
  355. */
  356. public function authCodeToOpenId($authCode)
  357. {
  358. return $this->request(self::API_AUTH_CODE_TO_OPENID, ['auth_code' => $authCode]);
  359. }
  360. /**
  361. * Merchant setter.
  362. *
  363. * @param Merchant $merchant
  364. *
  365. * @return $this
  366. */
  367. public function setMerchant(Merchant $merchant)
  368. {
  369. $this->merchant = $merchant;
  370. }
  371. /**
  372. * Merchant getter.
  373. *
  374. * @return Merchant
  375. */
  376. public function getMerchant()
  377. {
  378. return $this->merchant;
  379. }
  380. /**
  381. * Set sandbox mode.
  382. *
  383. * @param bool $enabled
  384. *
  385. * @return $this
  386. */
  387. public function sandboxMode($enabled = false)
  388. {
  389. $this->sandboxEnabled = $enabled;
  390. return $this;
  391. }
  392. /**
  393. * Make a API request.
  394. *
  395. * @param string $api
  396. * @param array $params
  397. * @param string $method
  398. * @param array $options
  399. * @param bool $returnResponse
  400. *
  401. * @return \EasyWeChat\Support\Collection|\Psr\Http\Message\ResponseInterface
  402. */
  403. protected function request($api, array $params, $method = 'post', array $options = [], $returnResponse = false, $sign_type = 'md5')
  404. {
  405. $params = array_merge($params, $this->merchant->only(['sub_appid', 'sub_mch_id']));
  406. $params['appid'] = $this->merchant->app_id;
  407. $params['mch_id'] = $this->merchant->merchant_id;
  408. $params['device_info'] = $this->merchant->device_info;
  409. $params['nonce_str'] = uniqid();
  410. $params = array_filter($params);
  411. $params['sign'] = generate_sign($params, $this->getSignkey($api), $sign_type);
  412. $options = array_merge([
  413. 'body' => XML::build($params),
  414. ], $options);
  415. $response = $this->getHttp()->request($api, $method, $options);
  416. return $returnResponse ? $response : $this->parseResponse($response);
  417. }
  418. /**
  419. * Return key to sign.
  420. *
  421. * @param string $api
  422. *
  423. * @return string
  424. */
  425. public function getSignkey($api)
  426. {
  427. return $this->sandboxEnabled && self::API_SANDBOX_SIGN_KEY !== $api ? $this->getSandboxSignKey() : $this->merchant->key;
  428. }
  429. /**
  430. * Request with SSL.
  431. *
  432. * @param string $api
  433. * @param array $params
  434. * @param string $method
  435. *
  436. * @return \EasyWeChat\Support\Collection
  437. */
  438. protected function safeRequest($api, array $params, $method = 'post', $sign_type = 'md5')
  439. {
  440. $options = [
  441. 'cert' => $this->merchant->get('cert_path'),
  442. 'ssl_key' => $this->merchant->get('key_path'),
  443. ];
  444. return $this->request($api, $params, $method, $options,false, $sign_type);
  445. }
  446. /**
  447. * Parse Response XML to array.
  448. *
  449. * @param ResponseInterface $response
  450. *
  451. * @return \EasyWeChat\Support\Collection
  452. */
  453. protected function parseResponse($response)
  454. {
  455. if ($response instanceof ResponseInterface) {
  456. $response = $response->getBody();
  457. }
  458. return new Collection((array) XML::parse($response));
  459. }
  460. /**
  461. * Wrap API.
  462. *
  463. * @param string $resource
  464. *
  465. * @return string
  466. */
  467. protected function wrapApi($resource)
  468. {
  469. return self::API_HOST.($this->sandboxEnabled ? '/sandboxnew' : '').$resource;
  470. }
  471. /**
  472. * Get sandbox sign key.
  473. *
  474. * @return string
  475. */
  476. protected function getSandboxSignKey()
  477. {
  478. if ($this->sandboxSignKey) {
  479. return $this->sandboxSignKey;
  480. }
  481. // Try to get sandbox_signkey from cache
  482. $cacheKey = 'sandbox_signkey.'.$this->merchant->merchant_id.$this->merchant->sub_merchant_id;
  483. /** @var \Doctrine\Common\Cache\Cache $cache */
  484. $cache = $this->getCache();
  485. $this->sandboxSignKey = $cache->fetch($cacheKey);
  486. if (!$this->sandboxSignKey) {
  487. // Try to acquire a new sandbox_signkey from WeChat
  488. $result = $this->request(self::API_SANDBOX_SIGN_KEY, []);
  489. if ('SUCCESS' === $result->return_code) {
  490. $cache->save($cacheKey, $result->sandbox_signkey, 24 * 3600);
  491. return $this->sandboxSignKey = $result->sandbox_signkey;
  492. }
  493. throw new Exception($result->return_msg);
  494. }
  495. return $this->sandboxSignKey;
  496. }
  497. /**
  498. * Return the cache manager.
  499. *
  500. * @return \Doctrine\Common\Cache\Cache
  501. */
  502. public function getCache()
  503. {
  504. return $this->cache ?: $this->cache = new FilesystemCache(sys_get_temp_dir());
  505. }
  506. /**
  507. * @param $orderNo
  508. * @param $sharingNo
  509. * @param $receivers
  510. * @param string $type
  511. * @return Collection
  512. */
  513. public function profit_sharing(
  514. $orderNo,
  515. $sharingNo,
  516. $receivers,
  517. $type = self::TRANSACTION_ID
  518. )
  519. {
  520. $params = [
  521. $type => $orderNo,
  522. 'out_order_no' => $sharingNo,
  523. 'receivers' => json_encode($receivers)
  524. ];
  525. return $this->safeRequest($this->wrapApi(self::API_PROFITSHARING), $params, 'post','hash_hmac');
  526. }
  527. public function addReceiver(
  528. $receiver
  529. )
  530. {
  531. $params = [
  532. 'receiver' => json_encode($receiver)
  533. ];
  534. return $this->safeRequest($this->wrapApi(self::API_PROFIT_SHARING_ADD_RECEIVER), $params, 'post', 'hash_hmac');
  535. }
  536. public function multiProfitSharing(
  537. $orderNo,
  538. $sharingNo,
  539. $receivers,
  540. $type = self::TRANSACTION_ID
  541. )
  542. {
  543. $params = [
  544. $type => $orderNo,
  545. 'out_order_no' => $sharingNo,
  546. 'receivers' => json_encode($receivers)
  547. ];
  548. return $this->safeRequest($this->wrapApi(self::API_MULTI_PROFIT_SHARING), $params, 'post', 'hash_hmac');
  549. }
  550. public function profitSharingFinish(
  551. $orderNo,
  552. $sharingNo,
  553. $type = self::TRANSACTION_ID
  554. )
  555. {
  556. $params = [
  557. $type => $orderNo,
  558. 'out_order_no' => $sharingNo,
  559. 'description' => '分账已完成'
  560. ];
  561. return $this->safeRequest($this->wrapApi(self::API_PROFIT_SHARING_FINISH), $params, 'post', 'hash_hmac');
  562. }
  563. public function profitSharingQuery(
  564. $orderNo,
  565. $sharingNo,
  566. $type = self::TRANSACTION_ID
  567. )
  568. {
  569. $params = [
  570. $type => $orderNo,
  571. 'out_order_no' => $sharingNo,
  572. ];
  573. return $this->safeRequest($this->wrapApi(self::API_PROFIT_SHARING_QUERY), $params, 'post', 'hash_hmac');
  574. }
  575. }