V3.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | 萤火商城系统 [ 致力于通过产品和服务,帮助商家高效化开拓市场 ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2017~2024 https://www.yiovo.com All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // | Licensed 这不是一个自由软件,不允许对程序代码以任何形式任何目的的再发行
  8. // +----------------------------------------------------------------------
  9. // | Author: 萤火科技 <admin@yiovo.com>
  10. // +----------------------------------------------------------------------
  11. declare (strict_types=1);
  12. namespace app\common\library\payment\gateway\driver\wechat;
  13. use WeChatPay\Builder;
  14. use WeChatPay\Formatter;
  15. use WeChatPay\Crypto\Rsa;
  16. use WeChatPay\Crypto\AesGcm;
  17. use WeChatPay\Util\PemUtil;
  18. use app\common\library\Log;
  19. use app\common\library\helper;
  20. use app\common\enum\Client as ClientEnum;
  21. use cores\traits\ErrorTrait;
  22. use cores\exception\BaseException;
  23. use Psr\Http\Message\ResponseInterface;
  24. /**
  25. * 微信支付驱动 [V3]
  26. * Class Wechat
  27. * @package app\common\library\payment\gateway\driver
  28. */
  29. class V3
  30. {
  31. use ErrorTrait;
  32. /**
  33. * 支付的客户端
  34. * @var string|null
  35. */
  36. protected ?string $client = null;
  37. /**
  38. * 支付配置参数
  39. * @var array
  40. */
  41. protected array $config = [];
  42. // 统一下单API的返回结果
  43. private array $result;
  44. // 异步通知的请求参数 (由第三方支付发送)
  45. private array $notifyParams;
  46. /**
  47. * 设置支付配置参数
  48. * @param array $options 配置信息
  49. * @param string $client 下单客户端
  50. * @return static|null
  51. */
  52. public function setOptions(array $options, string $client): ?V3
  53. {
  54. $this->client = $client ?: null;
  55. $this->config = $this->getConfig($options);
  56. return $this;
  57. }
  58. /**
  59. * 统一下单API
  60. * @param string $outTradeNo 交易订单号
  61. * @param string $totalFee 实际付款金额
  62. * @param array $extra 附加的数据 (需要携带openid)
  63. * @return bool
  64. * @throws BaseException
  65. */
  66. public function unify(string $outTradeNo, string $totalFee, array $extra = []): bool
  67. {
  68. // 下单的参数
  69. $params = [
  70. 'out_trade_no' => $outTradeNo,
  71. 'description' => '线上商城商品',
  72. 'notify_url' => $this->notifyUrl(), // 支付结果异步通知地址
  73. 'amount' => ['total' => (int)helper::bcmul($totalFee, 100), 'currency' => 'CNY'],
  74. 'scene_info' => ['payer_client_ip' => \request()->ip()]
  75. ];
  76. // 普通商户参数和服务商支付参数
  77. if ($this->isProvider()) {
  78. $params['sp_appid'] = $this->config['app_id'];
  79. $params['sp_mchid'] = $this->config['mch_id'];
  80. $params['sub_appid'] = $this->config['sub_appid'];
  81. $params['sub_mchid'] = $this->config['sub_mchid'];
  82. } else {
  83. $params['appid'] = $this->config['app_id'];
  84. $params['mchid'] = $this->config['mch_id'];
  85. }
  86. // 用户的openid (只有JSAPI支付时需要)
  87. if ($this->tradeType() === 'jsapi') {
  88. $params['payer'][$this->isProvider() ? 'sub_openid' : 'openid'] = $extra['openid'];
  89. }
  90. // H5info
  91. if ($this->tradeType() === 'h5') {
  92. $params['scene_info']['h5_info'] = ['type' => 'Wap'];
  93. }
  94. try {
  95. // 统一下单API
  96. // Doc: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml
  97. $resp = $this->getApp()
  98. ->chain($this->getUnifyApiUrl())
  99. ->post(['json' => $params]);
  100. // 记录api返回的数据
  101. $unifyResult = helper::jsonDecode((string)$resp->getBody());
  102. $this->result = $unifyResult;
  103. // 生成app支付的配置
  104. if ($this->client === ClientEnum::APP) {
  105. $this->result = $this->appConfig($unifyResult['prepay_id']);
  106. }
  107. // 生成jssdk支付的配置
  108. if (in_array($this->client, [ClientEnum::MP_WEIXIN])) {
  109. $this->result = $this->bridgeConfig($unifyResult['prepay_id']);
  110. }
  111. // 记录商户订单号
  112. $this->result['out_trade_no'] = $outTradeNo;
  113. // 记录日志
  114. Log::append('Wechat-unify', [
  115. 'client' => $this->client,
  116. 'params' => $params,
  117. 'extra' => $extra,
  118. 'result' => $this->result
  119. ]);
  120. return true;
  121. } catch (\Throwable $e) {
  122. // 异常处理
  123. $message = $this->getThrowMessage($e);
  124. $this->throwError('unify', "微信支付API下单失败:{$message}");
  125. }
  126. return false;
  127. }
  128. /**
  129. * 交易查询 (主动查询订单支付状态)
  130. * @param string $outTradeNo 交易订单号
  131. * @return array|null
  132. * @throws BaseException
  133. */
  134. public function tradeQuery(string $outTradeNo): ?array
  135. {
  136. // 下单的参数
  137. $params = [];
  138. // 普通商户参数和服务商支付参数
  139. if ($this->isProvider()) {
  140. $params['sp_mchid'] = $this->config['mch_id'];
  141. $params['sub_mchid'] = $this->config['sub_mchid'];
  142. } else {
  143. $params['mchid'] = $this->config['mch_id'];
  144. }
  145. try {
  146. // 订单查询API
  147. // Doc: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_2.shtml
  148. $resp = $this->getApp()
  149. ->chain($this->getTradeApiUrl($outTradeNo))
  150. ->get(['query' => $params]);
  151. // 记录api返回的数据
  152. $result = helper::jsonDecode((string)$resp->getBody());
  153. // 记录日志
  154. Log::append('Wechat-tradeQuery', ['outTradeNo' => $outTradeNo, 'result' => $result]);
  155. // 判断订单支付成功
  156. return [
  157. // 支付状态: true成功 false失败
  158. 'paySuccess' => $result['trade_state'] === 'SUCCESS',
  159. // 第三方交易流水号
  160. 'tradeNo' => $result['transaction_id'] ?? ''
  161. ];
  162. } catch (\Throwable $e) {
  163. // 异常处理
  164. $message = $this->getThrowMessage($e);
  165. $this->throwError('tradeQuery', "微信支付交易查询失败:{$message}");
  166. }
  167. return null;
  168. }
  169. /**
  170. * 支付成功后的异步通知
  171. * @param string $apiv3Key 微信支付v3秘钥
  172. * @param string $platformCertificateFilePath 平台证书路径
  173. * @return bool|string
  174. */
  175. public function notify(string $apiv3Key, string $platformCertificateFilePath)
  176. {
  177. // 微信异步通知参数
  178. $header = \request()->header();
  179. $inBody = file_get_contents('php://input');
  180. // 微信支付平台证书
  181. $platformPublicKeyInstance = Rsa::from("file://{$platformCertificateFilePath}", Rsa::KEY_TYPE_PUBLIC);
  182. // 检查通知时间偏移量,允许5分钟之内的偏移
  183. // $timeOffsetStatus = 300 >= abs(Formatter::timestamp() - (int)$inWechatpayTimestamp);
  184. $timeOffsetStatus = true;
  185. $verifiedStatus = Rsa::verify(
  186. // 构造验签名串
  187. Formatter::joinedByLineFeed($header['wechatpay-timestamp'], $header['wechatpay-nonce'], $inBody),
  188. $header['wechatpay-signature'],
  189. $platformPublicKeyInstance
  190. );
  191. if ($timeOffsetStatus && $verifiedStatus) {
  192. // 转换通知的JSON文本消息为PHP Array数组
  193. $inBodyArray = (array)json_decode($inBody, true);
  194. // 使用PHP7的数据解构语法,从Array中解构并赋值变量
  195. ['resource' => [
  196. 'ciphertext' => $ciphertext,
  197. 'nonce' => $nonce,
  198. 'associated_data' => $aad
  199. ]] = $inBodyArray;
  200. // 加密文本消息解密
  201. $inBodyResource = AesGcm::decrypt($ciphertext, $apiv3Key, $nonce, $aad);
  202. // 把解密后的文本转换为PHP Array数组
  203. $this->notifyParams = helper::jsonDecode($inBodyResource);
  204. // 记录日志
  205. Log::append('Wechat-notify', ['message' => '微信异步回调验证成功']);
  206. return $this->notifyParams['out_trade_no'];
  207. }
  208. return false;
  209. }
  210. /**
  211. * 微信支付退款API
  212. * @param string $outTradeNo 第三方交易单号
  213. * @param string $refundAmount 退款金额
  214. * @param array $extra 附加数据 (需要携带订单付款总金额)
  215. * @return bool
  216. * @throws BaseException
  217. */
  218. public function refund(string $outTradeNo, string $refundAmount, array $extra = []): bool
  219. {
  220. // 下单的参数
  221. $params = [
  222. 'out_trade_no' => $outTradeNo,
  223. 'out_refund_no' => time() . '-' . uniqid(),
  224. 'amount' => [
  225. 'refund' => (int)helper::bcmul($refundAmount, 100),
  226. 'total' => (int)helper::bcmul($extra['totalFee'], 100),
  227. 'currency' => 'CNY',
  228. ],
  229. ];
  230. // 普通商户参数和服务商支付参数
  231. if ($this->isProvider()) {
  232. $params['sub_mchid'] = $this->config['sub_mchid'];
  233. }
  234. try {
  235. // 申请退款API
  236. // Doc: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_3_9.shtml
  237. $resp = $this->getApp()
  238. ->chain($this->getRefundApiUrl())
  239. ->post(['json' => $params]);
  240. // 记录api返回的数据
  241. $result = helper::jsonDecode((string)$resp->getBody());
  242. // 记录日志
  243. Log::append('Wechat-refund', [
  244. 'outTradeNo' => $outTradeNo,
  245. 'refundAmount' => $refundAmount,
  246. 'result' => $result
  247. ]);
  248. // 请求成功
  249. return true;
  250. } catch (\Throwable $e) {
  251. // 异常处理
  252. $message = $this->getThrowMessage($e);
  253. $this->throwError('tradeQuery', "微信退款api请求失败:{$message}");
  254. }
  255. return false;
  256. }
  257. /**
  258. * 商家转账到零钱API
  259. * @param string $outTradeNo 交易订单号
  260. * @param string $totalFee 实际付款金额
  261. * @param array $extra 附加的数据 (需要携带openid、desc)
  262. * @return bool
  263. * @throws BaseException
  264. */
  265. public function transfers(string $outTradeNo, string $totalFee, array $extra = []): bool
  266. {
  267. // 下单的参数
  268. $params = [
  269. 'appid' => $this->config['app_id'],
  270. 'out_batch_no' => $outTradeNo,
  271. 'batch_name' => $extra['desc'],
  272. 'batch_remark' => $extra['desc'],
  273. 'total_amount' => (int)helper::bcmul($totalFee, 100), // 转账金额,单位:分
  274. 'total_num' => 1, // 转账总笔数
  275. 'transfer_detail_list' => [
  276. [
  277. 'out_detail_no' => time() . uniqid(),
  278. 'transfer_amount' => (int)helper::bcmul($totalFee, 100),
  279. 'transfer_remark' => $extra['desc'],
  280. 'openid' => $extra['openid'],
  281. ]
  282. ]
  283. ];
  284. try {
  285. // 商家转账到零钱API
  286. // Doc: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_3_1.shtml
  287. $resp = $this->getApp()
  288. ->chain($this->getTransfersUrl())
  289. ->post(['json' => $params]);
  290. // 记录api返回的数据
  291. $result = helper::jsonDecode((string)$resp->getBody());
  292. // 记录日志
  293. Log::append('Wechat-transfers', ['outTradeNo' => $outTradeNo, 'result' => $result]);
  294. // 请求成功
  295. return true;
  296. } catch (\Throwable $e) {
  297. // 异常处理
  298. $message = $this->getThrowMessage($e);
  299. $this->throwError('transfers', "商家转账到零钱api请求失败:{$message}");
  300. }
  301. return false;
  302. }
  303. /**
  304. * 获取异步回调的请求参数
  305. * @return array
  306. */
  307. public function getNotifyParams(): array
  308. {
  309. return [
  310. // 第三方交易流水号
  311. 'tradeNo' => $this->notifyParams['transaction_id']
  312. ];
  313. }
  314. /**
  315. * 返回异步通知结果的输出内容
  316. * @return string
  317. * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
  318. */
  319. public function getNotifyResponse(): string
  320. {
  321. return 'SUCCESS';
  322. }
  323. /**
  324. * 返回统一下单API的结果 (用于前端)
  325. * @return array
  326. * @throws BaseException
  327. */
  328. public function getUnifyResult(): array
  329. {
  330. if (empty($this->result)) {
  331. $this->throwError('getUnifyResult', '当前没有unify结果');
  332. }
  333. // 允许输出的字段 (防止泄露敏感信息)
  334. $result = helper::pick($this->result, [
  335. 'out_trade_no',
  336. 'nonce_str', 'prepay_id', 'sign', 'trade_type', 'mweb_url', 'h5_url',
  337. 'appid', 'partnerid', 'noncestr', 'prepayid', 'timestamp', 'package', 'sign',
  338. 'appId', 'timeStamp', 'nonceStr', 'package', 'signType', 'paySign',
  339. ]);
  340. // 当前的时间戳
  341. $result['time_stamp'] = (string)time();
  342. return $result;
  343. }
  344. /**
  345. * 设置异步通知的错误信息
  346. * @param string $error 错误信息
  347. * @param bool $outputFail 是否输出fail信息 (会使微信服务器重复发起通知)
  348. */
  349. private function notifyPaidError(string $error, bool $outputFail = true)
  350. {
  351. }
  352. /**
  353. * 输出错误信息
  354. * @param string $action 当前的操作
  355. * @param string $errMessage 错误信息
  356. * @throws BaseException
  357. */
  358. private function throwError(string $action, string $errMessage)
  359. {
  360. $this->error = $errMessage;
  361. Log::append("Wechat-{$action}", ['errMessage' => $errMessage]);
  362. throwError($errMessage);
  363. }
  364. /**
  365. * 根据客户端选择对应的微信支付方式
  366. * @return string
  367. * @throws BaseException
  368. */
  369. private function tradeType(): string
  370. {
  371. $tradeTypes = [
  372. ClientEnum::H5 => 'h5',
  373. ClientEnum::MP_WEIXIN => 'jsapi',
  374. ClientEnum::APP => 'app'
  375. ];
  376. if (!isset($tradeTypes[$this->client])) {
  377. $this->throwError('tradeType', '未找到当前客户端适配的微信支付方式');
  378. }
  379. return $tradeTypes[$this->client];
  380. }
  381. /**
  382. * 请求错误时错误信息
  383. */
  384. private function resultError(ResponseInterface $resp)
  385. {
  386. }
  387. /**
  388. * 获取微信支付应用类
  389. * @return \WeChatPay\BuilderChainable
  390. * @throws BaseException
  391. */
  392. private function getApp(): \WeChatPay\BuilderChainable
  393. {
  394. // 从本地文件中加载「商户API私钥」,「商户API私钥」会用来生成请求的签名
  395. $merchantPrivateKeyInstance = $this->getMerchantPrivateKeyInstance();
  396. // 从本地文件中加载「微信支付平台证书」,用来验证微信支付应答的签名
  397. $platformCertificateFilePath = "file://{$this->config['platform_cert_path']}";
  398. try {
  399. $platformPublicKeyInstance = Rsa::from($platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);
  400. } catch (\UnexpectedValueException $e) {
  401. $platformPublicKeyInstance = null;
  402. throwError('证书文件(PLATFORM)不正确');
  403. }
  404. // 从「微信支付平台证书」中获取「证书序列号」
  405. $platformCertificateSerial = PemUtil::parseCertificateSerialNo($platformCertificateFilePath);
  406. // 构造一个 APIv3 客户端实例
  407. return Builder::factory([
  408. // 微信支付商户号
  409. 'mchid' => $this->config['mch_id'],
  410. // 「商户API证书」的「证书序列号」
  411. 'serial' => $this->serialno($this->config['cert_path']),
  412. 'privateKey' => $merchantPrivateKeyInstance,
  413. 'certs' => [
  414. // 从「微信支付平台证书」中获取「证书序列号」
  415. $platformCertificateSerial => $platformPublicKeyInstance,
  416. ],
  417. ]);
  418. }
  419. /**
  420. * 从本地文件中加载「商户API私钥」,「商户API私钥」会用来生成请求的签名
  421. * @return mixed|\OpenSSLAsymmetricKey|resource
  422. * @throws BaseException
  423. */
  424. private function getMerchantPrivateKeyInstance()
  425. {
  426. try {
  427. return Rsa::from("file://{$this->config['key_path']}", Rsa::KEY_TYPE_PRIVATE);
  428. } catch (\UnexpectedValueException $e) {
  429. throwError('证书文件(KEY)不正确');
  430. }
  431. return null;
  432. }
  433. /**
  434. * 读取公钥中的序列号
  435. * @param string $publicKey
  436. * @return mixed
  437. * @throws BaseException
  438. */
  439. private function serialno(string $publicKey)
  440. {
  441. $content = file_get_contents($publicKey);
  442. $plaintext = !empty($content) ? openssl_x509_parse($content) : false;
  443. empty($plaintext) && throwError('证书文件(CERT)不正确');
  444. return $plaintext['serialNumberHex'];
  445. }
  446. /**
  447. * 构建微信支付配置
  448. * @return string[]
  449. */
  450. private function getConfig($options): array
  451. {
  452. if ($options['mchType'] === 'provider') {
  453. return [
  454. 'mch_type' => 'provider',
  455. 'app_id' => $options['provider']['spAppId'],
  456. 'mch_id' => $options['provider']['spMchId'],
  457. 'key' => $options['provider']['spApiKey'],
  458. 'cert_path' => $options['provider']['spApiclientCertPath'],
  459. 'key_path' => $options['provider']['spApiclientKeyPath'],
  460. 'platform_cert_path' => $options['provider']['platformCertPath'],
  461. 'sub_mchid' => $options['provider']['subMchId'],
  462. 'sub_appid' => $options['provider']['subAppId'],
  463. ];
  464. } else {
  465. return [
  466. 'mch_type' => 'normal',
  467. 'app_id' => $options['normal']['appId'],
  468. 'mch_id' => $options['normal']['mchId'],
  469. 'key' => $options['normal']['apiKey'],
  470. 'cert_path' => $options['normal']['apiclientCertPath'],
  471. 'key_path' => $options['normal']['apiclientKeyPath'],
  472. 'platform_cert_path' => $options['normal']['platformCertPath'],
  473. ];
  474. }
  475. }
  476. /**
  477. * 异步回调地址
  478. * @return string
  479. */
  480. private function notifyUrl(): string
  481. {
  482. // 例如:https://www.xxxx.com/wxpayNoticeV3.php
  483. return base_url() . 'wxpayNoticeV3.php';
  484. }
  485. /**
  486. * 当前是否为服务商模式
  487. * @return bool
  488. */
  489. private function isProvider(): bool
  490. {
  491. return $this->config['mch_type'] === 'provider';
  492. }
  493. /**
  494. * Generate app payment parameters.
  495. * @param string $prepayId
  496. * @return array
  497. * @throws BaseException
  498. */
  499. private function appConfig(string $prepayId): array
  500. {
  501. $params = [
  502. 'appid' => $this->config['app_id'],
  503. 'partnerid' => $this->config['mch_id'],
  504. 'prepayid' => $prepayId,
  505. 'noncestr' => Formatter::nonce(),
  506. 'timestamp' => (string)Formatter::timestamp(),
  507. 'package' => 'Sign=WXPay',
  508. ];
  509. $params += ['sign' => Rsa::sign(
  510. Formatter::joinedByLineFeed(...array_values($params)),
  511. $this->getMerchantPrivateKeyInstance()
  512. )];
  513. return $params;
  514. }
  515. /**
  516. * [WeixinJSBridge] Generate js config for payment.
  517. *
  518. * <pre>
  519. * WeixinJSBridge.invoke(
  520. * 'getBrandWCPayRequest',
  521. * ...
  522. * );
  523. * </pre>
  524. *
  525. * @param string $prepayId
  526. * @return string|array
  527. * @throws BaseException
  528. */
  529. private function bridgeConfig(string $prepayId)
  530. {
  531. $params = [
  532. 'appId' => $this->isProvider() ? $this->config['sub_appid'] : $this->config['app_id'],
  533. 'timeStamp' => (string)Formatter::timestamp(),
  534. 'nonceStr' => Formatter::nonce(),
  535. 'package' => "prepay_id=$prepayId",
  536. ];
  537. $params += ['paySign' => Rsa::sign(
  538. Formatter::joinedByLineFeed(...array_values($params)),
  539. $this->getMerchantPrivateKeyInstance()
  540. ), 'signType' => 'RSA'];
  541. return $params;
  542. }
  543. /**
  544. * 处理API的异常
  545. * @param \Throwable $e
  546. * @return mixed|string
  547. */
  548. private function getThrowMessage(\Throwable $e)
  549. {
  550. $message = $e->getMessage();
  551. if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
  552. $body = (string)$e->getResponse()->getBody();
  553. if (!empty($body)) {
  554. $result = helper::jsonDecode($body);
  555. isset($result['message']) && $message = $result['message'];
  556. }
  557. }
  558. return $message;
  559. }
  560. /**
  561. * 统一下单API的Url [需判断是否为服务商支付以及客户端]
  562. * @return string
  563. * @throws BaseException
  564. */
  565. private function getUnifyApiUrl(): string
  566. {
  567. $partnerNodo = $this->isProvider() ? 'partner/' : '';
  568. return "v3/pay/{$partnerNodo}transactions/" . $this->tradeType();
  569. }
  570. /**
  571. * 订单查询API的Url [需判断是否为服务商支付以及客户端]
  572. * @param string $outTradeNo
  573. * @return string
  574. */
  575. private function getTradeApiUrl(string $outTradeNo): string
  576. {
  577. $partnerNodo = $this->isProvider() ? 'partner/' : '';
  578. return "v3/pay/{$partnerNodo}transactions/out-trade-no/{$outTradeNo}";
  579. }
  580. /**
  581. * 申请退款API的Url
  582. * @return string
  583. */
  584. private function getRefundApiUrl(): string
  585. {
  586. return 'v3/refund/domestic/refunds';
  587. }
  588. /**
  589. * 商家转账到零钱API的Url
  590. * @return string
  591. */
  592. private function getTransfersUrl(): string
  593. {
  594. return 'v3/transfer/batches';
  595. }
  596. }