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