WechatPay.php 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817
  1. <?php
  2. namespace common\components\payment;
  3. use common\helpers\Url;
  4. use common\logic\wechat\WechatMessageLogic;
  5. use common\models\user\User;
  6. use EasyWeChat\Factory;
  7. use GuzzleHttp\Exception\GuzzleException;
  8. use http\Client;
  9. use Yii;
  10. use yii\helpers\ArrayHelper;
  11. use Omnipay\Omnipay;
  12. use common\enums\WechatPayTypeEnum;
  13. use WeChatPay\Builder;
  14. use WeChatPay\Crypto\Rsa;
  15. use WeChatPay\Util\PemUtil;
  16. use common\models\wechat\WechatTransferLog;
  17. /**
  18. * 微信支付类
  19. *
  20. * Class WechatPay
  21. * @package common\components\payment
  22. */
  23. class WechatPay
  24. {
  25. const DEFAULT = 'WechatPay';
  26. const APP = 'WechatPay_App';
  27. const NATIVE = 'WechatPay_Native';
  28. const JS = 'WechatPay_Js';
  29. const POS = 'WechatPay_Pos';
  30. const MWEB = 'WechatPay_Mweb';
  31. /**
  32. * 订单
  33. *
  34. * @var array
  35. */
  36. protected $order;
  37. /**
  38. * 配置
  39. *
  40. * @var
  41. */
  42. protected $config;
  43. /**
  44. * WechatPay constructor.
  45. */
  46. public function __construct($config)
  47. {
  48. $this->order = [
  49. 'spbill_create_ip' => Yii::$app->request->userIP??'',
  50. 'fee_type' => 'CNY',
  51. 'notify_url' => '',
  52. ];
  53. $this->config = $config;
  54. }
  55. /**
  56. * 实例化类
  57. *
  58. * @param $type
  59. * @return \Omnipay\WechatPay\AppGateway
  60. */
  61. private function create($type)
  62. {
  63. /* @var $gateway \Omnipay\WechatPay\AppGateway */
  64. $gateway = Omnipay::create($type);
  65. $gateway->setMchId($this->config['mch_id']);
  66. $gateway->setAppId($this->config['app_id']);
  67. $gateway->setApiKey($this->config['api_key']);
  68. $gateway->setCertPath($this->config['cert_client']);
  69. $gateway->setKeyPath($this->config['cert_key']);
  70. return $gateway;
  71. }
  72. /**
  73. * 回调
  74. *
  75. * @return \Omnipay\WechatPay\Message\CompletePurchaseResponse
  76. */
  77. public function notify()
  78. {
  79. $gateway = $this->create(self::DEFAULT);
  80. return $gateway->completePurchase([
  81. 'request_params' => file_get_contents('php://input')
  82. ])->send();
  83. }
  84. /**
  85. * 微信APP支付网关
  86. * @param array $order
  87. * [
  88. * 'body' => 'The test order',
  89. * 'out_trade_no' => date('YmdHis') . mt_rand(1000, 9999),
  90. * 'total_fee' => 1, //=0.01
  91. * ]
  92. * @param bool $debug
  93. * @return mixed
  94. */
  95. public function app($order, $debug = false)
  96. {
  97. $gateway = $this->create(self::APP);
  98. $gateway->setAppId($this->config['open_app_id']);
  99. $request = $gateway->purchase(ArrayHelper::merge($this->order, $order));
  100. $response = $request->send();
  101. return $debug ? $response->getData() : $response->getAppOrderData();
  102. }
  103. /**
  104. * 微信原生扫码支付支付网关
  105. *
  106. * @param array $order
  107. * [
  108. * 'body' => 'The test order',
  109. * 'out_trade_no' => date('YmdHis') . mt_rand(1000, 9999),
  110. * 'total_fee' => 1, //=0.01
  111. * ]
  112. * @param bool $debug
  113. * @return mixed
  114. */
  115. public function native($order, $debug = false)
  116. {
  117. $gateway = $this->create(self::NATIVE);
  118. $request = $gateway->purchase(ArrayHelper::merge($this->order, $order));
  119. $response = $request->send();
  120. return $debug ? $response->getData() : $response->getCodeUrl();
  121. }
  122. /**
  123. * 微信js支付支付网关
  124. *
  125. * @param array $order
  126. * [
  127. * 'body' => 'The test order',
  128. * 'out_trade_no' => date('YmdHis') . mt_rand(1000, 9999),
  129. * 'total_fee' => 1, //=0.01
  130. * 'openid' => 'ojPztwJ5bRWRt_Ipg', //=0.01
  131. * ]
  132. * @param bool $debug
  133. * @return mixed
  134. */
  135. public function js($order, $debug = false)
  136. {
  137. $gateway = $this->create(self::JS);
  138. $request = $gateway->purchase(ArrayHelper::merge($this->order, $order));
  139. $response = $request->send();
  140. // 兼容EasyWechat
  141. Yii::$app->params['wechatPaymentConfig'] = [
  142. 'app_id' => $this->config['app_id'],
  143. 'mch_id' => $this->config['mch_id'],
  144. 'key' => $this->config['api_key'],
  145. 'cert_path' => Yii::getAlias($this->config['cert_client']),
  146. 'key_path' => Yii::getAlias($this->config['cert_key']),
  147. ];
  148. $data = $response->getJsOrderData();
  149. if (isset($data['timeStamp'])) {
  150. $data['timestamp'] = $data['timeStamp'];
  151. unset($data['timeStamp']);
  152. }
  153. return $debug ? $response->getData() : $data;
  154. }
  155. /**
  156. * @param $order
  157. * @param bool $debug
  158. * @return array|mixed|null
  159. */
  160. public function miniProgram($order, $debug = false)
  161. {
  162. $gateway = $this->create(self::JS);
  163. $gateway->setAppId($this->config['mini_program_app_id']);
  164. $request = $gateway->purchase(ArrayHelper::merge($this->order, $order));
  165. $response = $request->send();
  166. $data = $response->getJsOrderData();
  167. if (isset($data['timeStamp'])) {
  168. $data['timestamp'] = $data['timeStamp'];
  169. unset($data['timeStamp']);
  170. }
  171. return $debug ? $response->getData() : $data;
  172. }
  173. /**
  174. * 微信刷卡支付网关
  175. *
  176. * @param array $order
  177. * [
  178. * 'body' => 'The test order',
  179. * 'out_trade_no' => date('YmdHis') . mt_rand(1000, 9999),
  180. * 'total_fee' => 1, //=0.01,
  181. * 'auth_code' => '',
  182. * ]
  183. * @param bool $debug
  184. * @return mixed
  185. */
  186. public function pos($order, $debug = false)
  187. {
  188. $gateway = $this->create(self::POS);
  189. $request = $gateway->purchase(ArrayHelper::merge($this->order, $order));
  190. $response = $request->send();
  191. return $debug ? $response->getData() : $response->getData();
  192. }
  193. /**
  194. * 微信H5支付网关
  195. * @param array $order
  196. * [
  197. * 'body' => 'The test order',
  198. * 'out_trade_no' => date('YmdHis') . mt_rand(1000, 9999),
  199. * 'total_fee' => 1, //=0.01
  200. * ]
  201. * @param bool $debug
  202. * @return mixed
  203. */
  204. public function mweb($order, $debug = false)
  205. {
  206. $gateway = $this->create(self::MWEB);
  207. $request = $gateway->purchase(ArrayHelper::merge($this->order, $order));
  208. $response = $request->send();
  209. return $debug ? $response->getData() : $response->getData();
  210. }
  211. /**
  212. * 关闭订单
  213. *
  214. * @param $out_trade_no
  215. * @return
  216. */
  217. public function close($out_trade_no)
  218. {
  219. $gateway = $this->create(self::DEFAULT);
  220. $response = $gateway->close([
  221. 'out_trade_no' => $out_trade_no, //The merchant trade no
  222. ])->send();
  223. return $response->getData();
  224. }
  225. /**
  226. * 查询订单
  227. *
  228. * @param $transaction_id
  229. * @return
  230. */
  231. public function query($transaction_id)
  232. {
  233. $gateway = $this->create(self::DEFAULT);
  234. $response = $gateway->query([
  235. 'transaction_id' => $transaction_id, //The wechat trade no
  236. ])->send();
  237. return $response->getData();
  238. }
  239. public function queryByOutTradeNo($out_trade_no)
  240. {
  241. $gateway = $this->create(self::DEFAULT);
  242. $response = $gateway->query([
  243. 'out_trade_no' => $out_trade_no, //The wechat trade no
  244. ])->send();
  245. return $response->getData();
  246. }
  247. public function queryRefund($out_trade_no){
  248. $gateway = $this->create(self::DEFAULT);
  249. $response = $gateway->queryRefund([
  250. 'out_trade_no' => $out_trade_no, //The wechat trade no
  251. ])->send();
  252. return $response->getData();
  253. }
  254. /**
  255. * 退款
  256. *
  257. * 订单类型
  258. *
  259. * @param $info
  260. * [
  261. * 'transaction_id' => $transaction_id, //The wechat trade no
  262. * 'out_refund_no' => $outRefundNo,
  263. * 'total_fee' => 1, //=0.01
  264. * 'refund_fee' => 1, //=0.01
  265. * ]
  266. * @param $type
  267. * @return
  268. */
  269. public function refund($info, $type = WechatPayTypeEnum::JS)
  270. {
  271. $gateway = $this->create(self::DEFAULT);
  272. switch ($type) {
  273. case WechatPayTypeEnum::JS :
  274. //小程序支付回调 trade_type = JSAPI,这里得判断相关app_id 是否设置
  275. if(empty($this->config['app_id'])){
  276. $app_id = $this->config['mini_program_app_id'] ?: $this->config['open_app_id'];
  277. $gateway->setAppId($app_id);
  278. }
  279. break;
  280. case WechatPayTypeEnum::MINI_PROGRAM :
  281. //小程序
  282. $gateway->setAppId($this->config['mini_program_app_id']);
  283. break;
  284. case WechatPayTypeEnum::APP :
  285. //app
  286. $gateway->setAppId($this->config['open_app_id']);
  287. break;
  288. }
  289. $response = $gateway->refund($info)->send();
  290. return $response->getData();
  291. }
  292. /**
  293. * User: wniu
  294. * Date: 2021/11/1
  295. * Time: 3:19 下午
  296. * *
  297. * @param $paymentTransfer
  298. * @param $userInfo
  299. * @return bool
  300. * @throws \Exception
  301. */
  302. public function transfer($paymentTransfer, $userInfo)
  303. {
  304. $t = \Yii::$app->db->beginTransaction();
  305. try {
  306. $config = Yii::$app->services->pay->getWechatConfig($paymentTransfer->mall_id);
  307. $wechatConfig = [
  308. 'app_id' => $config['app_id'],
  309. 'mch_id' => $config['wechat_mch_id'],
  310. 'key' => $config['wechat_pay_secret'], // API 密钥
  311. 'cert_path' => dirname(Yii::$app->basePath).'/'.$config['wechat_cert_pem_file'],
  312. 'key_path' => dirname(Yii::$app->basePath).'/'.$config['wechat_key_pem_file'],
  313. 'notify_url' => ''//回调地址
  314. ];
  315. if($userInfo->platform == User::PLATFORM_MP_WX) {
  316. $mini_program = \Yii::$app->services->setting->mall->get($paymentTransfer->mall_id, 'mini_program');
  317. $wechatConfig['app_id'] =$mini_program['app_id'];
  318. }
  319. $payment = Factory::payment($wechatConfig);
  320. //打款操作调整到保存成功后再调用打款,避免保存出错,款项依然打出去了,造成重复打款的问题
  321. $paymentTransfer->is_pay = 1;
  322. if (!$paymentTransfer->save()) {
  323. throw new \Exception(json_encode($paymentTransfer->errors));
  324. }else{
  325. //todo 需要在微信开通该产品权限
  326. $result = $payment->transfer->toBalance([
  327. 'partner_trade_no' => $paymentTransfer->order_no, // 商户订单号,需保持唯一性(只能是字母或者数字,不能包含有符号)
  328. 'openid' => $userInfo->openid,
  329. 'check_name' => 'NO_CHECK', // NO_CHECK:不校验真实姓名, FORCE_CHECK:强校验真实姓名
  330. 're_user_name' => '', // 如果 check_name 设置为FORCE_CHECK,则必填用户真实姓名
  331. 'amount' => $paymentTransfer->amount * 100, // 企业付款金额,单位为分
  332. 'desc' => '收益提现', // 企业付款操作说明信息。必填
  333. ]);
  334. if(isset($result["return_code"]) && $result["return_code"] == "SUCCESS" && isset($result["result_code"]) && $result["result_code"] == "SUCCESS"){
  335. }else{
  336. $msg = isset($result["return_msg"]) ? $result["return_msg"] : "提现失败";
  337. $msg = isset($result["err_code_des"]) ? $result["err_code_des"] : $msg;
  338. throw new \Exception($msg);
  339. }
  340. }
  341. $t->commit();
  342. return true;
  343. }
  344. catch (\Exception $e) {
  345. $t->rollBack();
  346. throw new \Exception($e->getMessage());
  347. }
  348. }
  349. /**
  350. * @desc:退款
  351. * @Author: hua
  352. * @Date: 2021/12/20
  353. * @Time: 15:34
  354. * @Copyright: copyright (c) 2021 广东七件事集团
  355. * @param $payment_refund
  356. * @param $pay_total_fee
  357. * @return bool
  358. * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
  359. */
  360. public function refunds($payment_refund,$pay_total_fee){
  361. $config = Yii::$app->services->pay->getWechatConfig($payment_refund->mall_id);
  362. $wechatConfig = [
  363. 'app_id' => $config['app_id'],
  364. 'mch_id' => $config['wechat_mch_id'],
  365. 'key' => $config['wechat_pay_secret'], // API 密钥
  366. 'cert_path' => dirname(Yii::$app->basePath).'/'.$config['wechat_cert_pem_file'],
  367. 'key_path' => dirname(Yii::$app->basePath).'/'.$config['wechat_key_pem_file'],
  368. 'notify_url' => ''//回调地址
  369. ];
  370. $payment = Factory::payment($wechatConfig);
  371. $res = $payment->refund->byOutTradeNumber($payment_refund->out_trade_no, $payment_refund->order_no, $pay_total_fee*100, $payment_refund->amount*100);
  372. if(isset($res["return_code"]) && $res["return_code"] == "SUCCESS" && isset($res["result_code"]) && $res["result_code"] == "SUCCESS"){
  373. $payment_refund->is_pay = 1;
  374. $payment_refund->save();
  375. return true;
  376. }else{
  377. throw new \Exception($res['err_code_des']);
  378. }
  379. }
  380. /**
  381. * 商家转账到零钱
  382. * @param \common\models\common\PaymentTransfer $paymentTransfer
  383. * @param $userInfo
  384. * @throws \Exception
  385. * @return bool
  386. */
  387. public function v3_transfer_batches($paymentTransfer, $userInfo)
  388. {
  389. $cash_info = $paymentTransfer->getCashInfo();
  390. $mall_id = $paymentTransfer->mall_id;
  391. $config = Yii::$app->services->pay->getWechatConfig($paymentTransfer->mall_id);
  392. if($userInfo->platform == User::PLATFORM_MP_WX) {
  393. $mini_program = \Yii::$app->services->setting->mall->get($paymentTransfer->mall_id, 'mini_program');
  394. $config['app_id'] =$mini_program['app_id'];
  395. }
  396. $instance = $this->getV3Instance($mall_id,$config);
  397. $paymentTransfer->is_pay = 1;
  398. if (!$paymentTransfer->save()) {
  399. throw new \Exception($paymentTransfer->getErrorMessage());
  400. }
  401. $total_amount = bcmul($paymentTransfer->amount,100);
  402. $total_amount = intval($total_amount);
  403. $batch_name = date('Y').'年'.date('m').'月收益提现';
  404. $data = [
  405. 'out_batch_no' => $paymentTransfer->order_no, //商家批次单号
  406. 'appid' => $config['app_id'], //申请商户号的appid或商户号绑定的appid
  407. 'batch_name' => $batch_name, //批次名称
  408. 'batch_remark' => $batch_name, //批次备注
  409. 'total_amount' => $total_amount , //转账总金额 ,int 类型,转账金额单位为“分”
  410. 'total_num' => 1 , //转账总笔数
  411. 'transfer_detail_list' => [
  412. [
  413. 'out_detail_no' => $paymentTransfer->order_no.'1', //商家明细单号
  414. 'transfer_amount' => intval(bcmul($paymentTransfer->amount,100)), //转账金额
  415. 'transfer_remark' => '收益提现', //转账备注
  416. 'openid' => $userInfo->openid,
  417. //'user_name' => '' //非必要
  418. ]
  419. ],
  420. ];
  421. //以下是打款的零钱的操作
  422. try {
  423. $resp = $instance
  424. ->chain('v3/transfer/batches')
  425. ->post(['json' => $data]);
  426. $result = $resp->getBody();
  427. $transfer_result = '';
  428. if($resp->getStatusCode() == 200 && $result){
  429. $result = json_decode($result,true);
  430. $transfer_result = json_encode($result);
  431. }
  432. \Yii::error('wechat transfer getStatusCode='.$resp->getStatusCode().' getBody'.$resp->getBody());
  433. \Yii::error('insert WechatTransfer data:'.$paymentTransfer->transfer_order_no.','.json_encode($data).','.$resp->getBody().','.$mall_id.','.$cash_info->id);
  434. /** @var WechatTransferLog $transfer_model */
  435. $transfer_model = new WechatTransferLog();
  436. $transfer_model->out_trade_no = $paymentTransfer->order_no;
  437. $transfer_model->transfer_data = json_encode($data);
  438. $transfer_model->transfer_result = $transfer_result;
  439. $transfer_model->mall_id = $mall_id;
  440. $transfer_model->cash_id = $cash_info->id;
  441. $res = $transfer_model->save();
  442. if(!$res){
  443. Yii::error('wechat transfer log result save error:'.$transfer_model->getErrorMessage());
  444. }
  445. return true;
  446. } catch (\Exception $e) {
  447. \Yii::error('wechat transfer exception='.$e->getMessage());
  448. // 进行错误处理
  449. if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
  450. $r = $e->getResponse();
  451. \Yii::error('wechat transfer exception getStatusCode='.$r->getStatusCode() . ' ' . $r->getReasonPhrase());
  452. \Yii::error('wechat transfer exception getBody='.$r->getBody());
  453. $body = json_decode($r->getBody(),true);
  454. throw new \Exception($body['message']??'');
  455. }
  456. return false;
  457. }
  458. }
  459. public function v3_transfer_bills($paymentTransfer, $userInfo)
  460. {
  461. $cash_info = $paymentTransfer->getCashInfo();
  462. $mall_id = $paymentTransfer->mall_id;
  463. $config = Yii::$app->services->pay->getWechatConfig($paymentTransfer->mall_id);
  464. if ($userInfo->platform == User::PLATFORM_MP_WX) {
  465. $mini_program = \Yii::$app->services->setting->mall->get($paymentTransfer->mall_id, 'mini_program');
  466. $config['app_id'] = $mini_program['app_id'];
  467. }
  468. $paymentTransfer->is_pay = 1;
  469. if (!$paymentTransfer->save()) {
  470. throw new \Exception($paymentTransfer->getErrorMessage());
  471. }
  472. $total_amount = bcmul($paymentTransfer->amount, 100);
  473. $total_amount = intval($total_amount);
  474. $batch_name = date('Y') . '年' . date('m') . '月收益提现';
  475. // 获取当前微信支付 apiv3 秘钥
  476. $wechatSetting = Yii::$app->services->setting->mall->get($paymentTransfer->mall_id, 'pay.wechat');
  477. if ($wechatSetting) $wechatSetting = json_decode($wechatSetting, true);
  478. $wechat_pay_secret_v3 = $wechatSetting['wechat_pay_secret_v3'] ?? '';
  479. $data = [
  480. 'appid' => $config['app_id'], //申请商户号的appid或商户号绑定的appid
  481. 'out_bill_no' => $paymentTransfer->order_no, //商家批次单号
  482. "transfer_scene_id" => "1000",
  483. 'openid' => $userInfo->openid,
  484. 'transfer_amount' => $total_amount, //转账总金额 ,int 类型,转账金额单位为“分”
  485. 'transfer_remark' => $batch_name, //批次备注
  486. 'notify_url' => Url::toApi(["notify/wechat-transfer/{$wechat_pay_secret_v3}"], TRUE), // 回调地址
  487. "user_recv_perception" => "现金奖励",
  488. 'transfer_scene_report_infos' => [
  489. [
  490. "info_type" => "活动名称",
  491. "info_content" => "提现"
  492. ],
  493. [
  494. 'info_type' => '奖励说明', //转账备注
  495. 'info_content' => '收益提现', //转账备注
  496. ]
  497. ],
  498. ];
  499. $body = json_encode($data, JSON_UNESCAPED_UNICODE);
  500. $host = 'https://api.mch.weixin.qq.com';
  501. $uri = '/v3/fund-app/mch-transfer/transfer-bills';
  502. // 请求头
  503. $headers = $this->getHeaders($mall_id, $config, $data, $uri);
  504. //以下是打款的零钱的操作
  505. try {
  506. $client = new \GuzzleHttp\Client();
  507. $resp = $client->request('POST', $host . $uri, [
  508. 'headers' => $headers,
  509. 'body' => $body
  510. ]);
  511. $result = $resp->getBody();
  512. $result = json_decode($result, true);
  513. if ($resp->getStatusCode() == 200 && $result) {
  514. $transfer_result = json_encode($result);
  515. } else {
  516. throw new \Exception($body['message'] ?? '');
  517. }
  518. \Yii::error('wechat transfer getStatusCode=' . $resp->getStatusCode() . ' getBody' . $resp->getBody());
  519. /** @var WechatTransferLog $transfer_model */
  520. $transfer_model = new WechatTransferLog();
  521. $transfer_model->out_trade_no = $paymentTransfer->order_no;
  522. $transfer_model->transfer_data = json_encode($data);
  523. $transfer_model->transfer_result = $transfer_result;
  524. $transfer_model->mall_id = $mall_id;
  525. $transfer_model->cash_id = $cash_info->id;
  526. $res = $transfer_model->save();
  527. if (!$res) {
  528. Yii::error('wechat transfer log result save error:' . $transfer_model->getErrorMessage());
  529. }
  530. return $result;
  531. } catch (\Exception $e) {
  532. // 进行错误处理
  533. if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
  534. $r = $e->getResponse();
  535. $body = json_decode($r->getBody(), true);
  536. throw new \Exception($body['message'] ?? '');
  537. }
  538. return false;
  539. }
  540. }
  541. protected function getHeaders($mall_id, $config, $data, $uri)
  542. {
  543. $timestamp = time();
  544. $nonceStr = uniqid();
  545. $body = json_encode($data, JSON_UNESCAPED_UNICODE);
  546. $message = "POST\n{$uri}\n" . $timestamp . "\n" . $nonceStr . "\n" . $body . "\n";
  547. if(empty($data)){
  548. $message = "GET\n{$uri}\n" . $timestamp . "\n" . $nonceStr . "\n". "\n";
  549. }
  550. $privateKey = $config['wechat_key_pem'];
  551. openssl_sign($message, $rawSignature, $privateKey, 'SHA256');
  552. $signature = base64_encode($rawSignature);
  553. $merchantId = $config['wechat_mch_id'];
  554. $wechat_serial = $config['wechat_serial'];
  555. //微信支付平台证书
  556. $platformCertificateFilePath = dirname(Yii::$app->basePath) . '/backend/runtime/pem/' . $mall_id . '/wechatpay.pem';
  557. if (!file_exists($platformCertificateFilePath)) {
  558. //证书生成方式: 更新vendor包后,在项目更目录执行 composer exec CertificateDownloader.php -- -m 你的商户号 -s 40字节你的商户证书序列号 -f 你的私钥文件路径 -k 你的APIv3密钥 -o /www/wwwroot/qimall/backend/runtime/pem/1/
  559. //生成后名称如下:/www/wwwroot/qimall/backend/runtime/pem/1/wechatpay_703A2B6B830559BB4D59BE21EC7694F4909E57D2.pem
  560. //生成后需要 修改文件名称 : wechatpay.pem,如:wechatpay.pem
  561. $platformCertificateSerial = $config['wechat_pay_public_key_id'];
  562. } else {
  563. $platformCertificateFilePath = 'file://' . $platformCertificateFilePath;
  564. // 从「微信支付平台证书」中获取「证书序列号」
  565. $platformCertificateSerial = PemUtil::parseCertificateSerialNo($platformCertificateFilePath);
  566. }
  567. return [
  568. 'Accept' => 'application/json',
  569. 'Content-Type' => 'application/json',
  570. 'Authorization' => 'WECHATPAY2-SHA256-RSA2048 mchid="' . $merchantId . '",nonce_str="' . $nonceStr . '",timestamp="' . $timestamp . '",serial_no="' . $wechat_serial . '",signature="' . $signature . '"',
  571. 'Wechatpay-Serial' => $platformCertificateSerial,
  572. ];
  573. }
  574. /**
  575. * 获取微信V3支付初始化类
  576. * @param int $mall_id
  577. * @param array $config 商家微信配置
  578. * @return mixed
  579. * @throws \Exception
  580. */
  581. private function getV3Instance($mall_id,$config)
  582. {
  583. //商户号
  584. $merchantId = $config['wechat_mch_id'];
  585. // 从本地文件中加载「商户API私钥」,「商户API私钥」会用来生成请求的签名
  586. $merchantPrivateKeyFilePath = 'file://'.dirname(Yii::$app->basePath).'/'.$config['wechat_key_pem_file'];
  587. $merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath, Rsa::KEY_TYPE_PRIVATE);
  588. // 「商户API证书」的「证书序列号」
  589. $merchantCertificateSerial = $config['wechat_serial'];
  590. //微信支付平台证书
  591. $platformCertificateFilePath = dirname(Yii::$app->basePath).'/backend/runtime/pem/'.$mall_id.'/wechatpay.pem';
  592. if(!file_exists($platformCertificateFilePath)){
  593. //证书生成方式: 更新vendor包后,在项目更目录执行 composer exec CertificateDownloader.php -- -m 你的商户号 -s 40字节你的商户证书序列号 -f 你的私钥文件路径 -k 你的APIv3密钥 -o /www/wwwroot/qimall/backend/runtime/pem/1/
  594. //生成后名称如下:/www/wwwroot/qimall/backend/runtime/pem/1/wechatpay_703A2B6B830559BB4D59BE21EC7694F4909E57D2.pem
  595. //生成后需要 修改文件名称 : wechatpay.pem,如:wechatpay.pem
  596. throw new \Exception('微信支付平台证书不存在');
  597. }
  598. $platformCertificateFilePath = 'file://'.$platformCertificateFilePath;
  599. $platformPublicKeyInstance = Rsa::from($platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);
  600. // 从「微信支付平台证书」中获取「证书序列号」
  601. $platformCertificateSerial = PemUtil::parseCertificateSerialNo($platformCertificateFilePath);
  602. // 构造一个 APIv3 客户端实例
  603. $instance = Builder::factory([
  604. 'mchid' => $merchantId,
  605. 'serial' => $merchantCertificateSerial,
  606. 'privateKey' => $merchantPrivateKeyInstance,
  607. 'certs' => [
  608. $platformCertificateSerial => $platformPublicKeyInstance,
  609. ],
  610. ]);
  611. return $instance;
  612. }
  613. /**
  614. * 查询转账批次单以及指定状态的转账明细单
  615. * @param WechatTransferLog $wechat_transfer_log
  616. * @return bool
  617. * @throws \Exception
  618. */
  619. public function queryBatches(&$wechat_transfer_log)
  620. {
  621. $mall_id = $wechat_transfer_log->mall_id;
  622. $config = Yii::$app->services->pay->getWechatConfig($mall_id);
  623. $instance = $this->getV3Instance($mall_id,$config);
  624. $wechat_transfer = json_decode($wechat_transfer_log->transfer_result,true);
  625. $batch_id = $wechat_transfer['batch_id'];
  626. //以下是打款的零钱的操作
  627. try {
  628. //微信批次单号查询批次单API
  629. $resp = $instance
  630. ->v3->transfer->batches->batchId->_batch_id_->getAsync([
  631. // Query 参数
  632. 'query' => ['need_query_detail' => true,'offset' => 0,'limit'=>100,'detail_status'=>'ALL'],
  633. // 变量名 => 变量值
  634. 'batch_id' => $batch_id,
  635. ])->then(static function($response) {
  636. // 正常逻辑回调处理
  637. return $response;
  638. })->wait();
  639. $batches = json_decode($resp->getBody()->getContents(), true);
  640. if(!$batches){
  641. return [];
  642. }
  643. //微信批次状态
  644. $batch_status = $batches['transfer_batch']['batch_status'] ?? '';
  645. $wechat_transfer_log->batch_status = $batch_status;
  646. //明细状态
  647. $detail_status = '';
  648. $transfer_detail_list = $batches['transfer_detail_list'] ?? [];
  649. $detail_info = [];
  650. if($batches && $transfer_detail_list){
  651. foreach($batches['transfer_detail_list'] as $detail){
  652. $detail_status = $detail['detail_status'] ?? '';
  653. //微信明细单号查询明细单API
  654. if($detail_status == 'FAIL'){
  655. //如果明细单号失败,查询 微信明细单号查询明细单API
  656. $resp = $instance
  657. ->v3->transfer->batches->batchId->_batch_id_->details->detailId->_detail_id_->getAsync([
  658. // Query 参数
  659. // 变量名 => 变量值
  660. 'batch_id' => $batch_id,
  661. 'detail_id' => $detail['detail_id']
  662. ])->then(static function($response) {
  663. // 正常逻辑回调处理
  664. return $response;
  665. })->wait();
  666. $detail_info = json_decode($resp->getBody()->getContents(), true);
  667. $wechat_transfer_log->transfer_detail = json_encode($detail_info);
  668. }
  669. }
  670. }
  671. if($detail_status){
  672. $wechat_transfer_log->detail_status = $detail_status;
  673. }
  674. if($wechat_transfer_log->save() === false){
  675. \Yii::error('wechat transfer save error='.$wechat_transfer_log->getErrorMessage());
  676. }
  677. return $batches;
  678. } catch (\Exception $e) {
  679. \Yii::error('wechat getBatchesInfo exception='.$e->getMessage());
  680. // 进行错误处理
  681. if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
  682. $r = $e->getResponse();
  683. \Yii::error('wechat getBatchesInfo exception getStatusCode='.$r->getStatusCode() . ' ' . $r->getReasonPhrase());
  684. \Yii::error('wechat getBatchesInfo exception getBody='.$r->getBody());
  685. }
  686. return false;
  687. }
  688. }
  689. /**
  690. * out_trade_no/out_bill_no
  691. * @param $out_bill_no
  692. * @return bool|string
  693. * @throws GuzzleException
  694. */
  695. public function mchTransfer($out_bill_no){
  696. try {
  697. $wechatTransferLog = \common\models\wechat\WechatTransferLog::find()->where(['out_trade_no' => $out_bill_no])->one();
  698. if (empty($wechatTransferLog)) throw new \Exception($out_bill_no.'记录不存在');
  699. $transferResult = json_decode($wechatTransferLog->transfer_result,true);
  700. $config = Yii::$app->services->pay->getWechatConfig($wechatTransferLog->mall_id);
  701. $mini_program = \Yii::$app->services->setting->mall->get($wechatTransferLog->mall_id, 'mini_program');
  702. $config['app_id'] = $mini_program['app_id'];
  703. $host = 'https://api.mch.weixin.qq.com';
  704. $uri = '/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/'.$out_bill_no;
  705. // 请求头
  706. $headers = $this->getHeaders($wechatTransferLog->mall_id, $config, [], $uri);
  707. //以下是打款的零钱的操作
  708. $client = new \GuzzleHttp\Client();
  709. $resp = $client->request('get', $host . $uri, [
  710. 'headers' => $headers,
  711. ]);
  712. $result = $resp->getBody();
  713. $result = json_decode($result, true);
  714. if(isset($result['code']) && !isset($result['state'])){
  715. throw new \Exception($result['code']);
  716. }
  717. $transferResult['state'] = $result['state'];
  718. $wechatTransferLog->transfer_result = json_encode($transferResult);
  719. $res = $wechatTransferLog->save();
  720. if(!$res){
  721. throw new \Exception($out_bill_no.'数据更新失败');
  722. }
  723. return true;
  724. }catch (\Exception $e){
  725. return $e->getMessage();
  726. }
  727. }
  728. }