API.php 15 KB

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