Wechat.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. <?php
  2. namespace app\common\Service\Pay\Provider;
  3. use think\Log;
  4. use think\Exception;
  5. use think\exception\HttpResponseException;
  6. use Yansongda\Pay\Pay;
  7. use app\common\Enum\ChannelEnum;
  8. /**
  9. * 微信支付提供器
  10. */
  11. class Wechat extends Base
  12. {
  13. /**
  14. * 构造函数
  15. * @param mixed $payService
  16. * @param string $platform
  17. * @param string $channel
  18. */
  19. public function __construct($payService, $platform = null, $channel = null)
  20. {
  21. parent::__construct($payService, $platform, $channel);
  22. }
  23. public function pay($order, $config = [])
  24. {
  25. $this->init('wechat', $config);
  26. if (isset($this->config['wechat']['default']['mode']) && $this->config['wechat']['default']['mode'] === 2) {
  27. if (in_array($this->platform, ['WechatOfficialAccount', 'WechatMiniProgram'])) {
  28. $order['payer']['sub_openid'] = $order['payer']['openid'] ?? '';
  29. unset($order['payer']['openid']);
  30. }
  31. }
  32. //$order['amount']['total'] = intval(bcmul((string)$order['total_amount'], '100'));
  33. // 按分 为单位
  34. $order['amount']['total'] = 1;
  35. // 测试
  36. if ($this->platform == 'H5') {
  37. $order['_type'] = 'app'; // 使用 配置中的 app_id 字段
  38. $order['scene_info'] = [
  39. 'payer_client_ip' => request()->ip(),
  40. 'h5_info' => [
  41. 'type' => 'Wap',
  42. ]
  43. ];
  44. }
  45. unset($order['order_id'], $order['total_amount']);
  46. $method = $this->getMethod('wechat');
  47. $result = Pay::wechat()->$method($order);
  48. return $result;
  49. }
  50. public function transfer($payload, $config = [])
  51. {
  52. $this->init('wechat', $config, 'sub_mch');
  53. $code = 0;
  54. $payload['total_amount'] = intval(bcmul((string)$payload['total_amount'], '100'));
  55. foreach ($payload['transfer_detail_list'] as $key => &$detail) {
  56. $detail['transfer_amount'] = intval(bcmul((string)$detail['transfer_amount'], '100'));
  57. }
  58. if (isset($this->config['wechat']['default']['_type'])) {
  59. // 为了能正常获取 appid
  60. $payload['_type'] = $this->config['wechat']['default']['_type'];
  61. }
  62. // $payload['authorization_type'] = 'INFORMATION_AUTHORIZATION_TYPE';
  63. $payload['authorization_type'] = 'FUND_AUTHORIZATION_TYPE';
  64. // $payload['authorization_type'] = 'INFORMATION_AND_FUND_AUTHORIZATION_TYPE';
  65. $response = Pay::wechat()->transfer($payload);
  66. if (isset($response['batch_id']) && $response['batch_id']) {
  67. $code = 1;
  68. }
  69. return [$code, $response];
  70. }
  71. public function notify($callback, $config = [])
  72. {
  73. $this->init('wechat', $config);
  74. try {
  75. $originData = Pay::wechat()->callback(); // 是的,验签就这么简单!
  76. // {
  77. // "id": "a5c68a7c-5474-5151-825d-88b4143f8642",
  78. // "create_time": "2022-06-20T16:16:12+08:00",
  79. // "resource_type": "encrypt-resource",
  80. // "event_type": "TRANSACTION.SUCCESS",
  81. // "summary": "支付成功",
  82. // "resource": {
  83. // "original_type": "transaction",
  84. // "algorithm": "AEAD_AES_256_GCM",
  85. // "ciphertext": {
  86. // "mchid": "1623831039",
  87. // "appid": "wx43********d3d0",
  88. // "out_trade_no": "P202204155176122100021000",
  89. // "transaction_id": "4200001433202206201698588194",
  90. // "trade_type": "JSAPI",
  91. // "trade_state": "SUCCESS",
  92. // "trade_state_desc": "支付成功",
  93. // "bank_type": "OTHERS",
  94. // "attach": "",
  95. // "success_time": "2022-06-20T16:16:12+08:00",
  96. // "payer": {
  97. // "openid": "oRj5A44G6lgCVENzVMxZtoMfNeww"
  98. // },
  99. // "amount": {
  100. // "total": 1,
  101. // "payer_total": 1,
  102. // "currency": "CNY",
  103. // "payer_currency": "CNY"
  104. // }
  105. // },
  106. // "associated_data": "transaction",
  107. // "nonce": "qoJzoS9MCNgu"
  108. // }
  109. // }
  110. Log::write('pay-notify-origin-data:' . json_encode($originData));
  111. if ($originData['event_type'] == 'TRANSACTION.SUCCESS') {
  112. // 支付成功回调
  113. $data = $originData['resource']['ciphertext'] ?? [];
  114. if (isset($data['trade_state']) && $data['trade_state'] == 'SUCCESS') {
  115. // 交易成功
  116. $data['pay_fee'] = ($data['amount']['total'] / 100);
  117. $data['notify_time'] = date('Y-m-d H:i:s', strtotime((string)$data['success_time']));
  118. $data['buyer_info'] = $data['payer']['openid'] ?? ($data['payer']['sub_openid'] ?? '');
  119. $result = $callback($data, $originData);
  120. return $result;
  121. }
  122. return 'fail';
  123. } else {
  124. // 微信交易未成功,返回 false,让微信再次通知
  125. Log::error('notify-error:交易未成功:' . $originData['event_type']);
  126. return 'fail';
  127. }
  128. } catch (HttpResponseException $e) {
  129. $data = $e->getResponse()->getData();
  130. $message = $data ? ($data['msg'] ?? '') : $e->getMessage();
  131. $this->logPayment('微信支付回调HttpResponseException', ['error' => $message], 'error');
  132. return 'fail';
  133. } catch (\Exception $e) {
  134. $this->logPayment('微信支付回调异常', ['error' => $e->getMessage()], 'error');
  135. return 'fail';
  136. }
  137. }
  138. /**
  139. * 退款
  140. *
  141. * @param array $order_data
  142. * @param array $config
  143. * @return array
  144. */
  145. public function refund($order_data, $config = [])
  146. {
  147. $config['notify_url'] = $config['notify_url'] ?? request()->domain() . '/addons/shopro/pay/notifyRefund/payment/wechat/platform/' . $this->platform;
  148. $order_data['notify_url'] = $config['notify_url'];
  149. $this->init('wechat', $config);
  150. $order_data['amount']['total'] = intval(bcmul((string)$order_data['amount']['total'], '100'));
  151. $order_data['amount']['refund'] = intval(bcmul((string)$order_data['amount']['refund'], '100'));
  152. $result = Pay::wechat()->refund($order_data);
  153. Log::write('pay-refund-origin-data:' . json_encode($result, JSON_UNESCAPED_UNICODE));
  154. // { 返回数据字段
  155. // "amount": {
  156. // "currency": "CNY",
  157. // "discount_refund": 0,
  158. // "from": [],
  159. // "payer_refund": 1,
  160. // "payer_total": 1,
  161. // "refund": 1,
  162. // "settlement_refund": 1,
  163. // "settlement_total": 1,
  164. // "total": 1
  165. // },
  166. // "channel": "ORIGINAL",
  167. // "create_time": "2022-06-20T19:06:36+08:00",
  168. // "funds_account": "AVAILABLE",
  169. // "out_refund_no": "R202207063668479227002100",
  170. // "out_trade_no": "P202205155977315528002100",
  171. // "promotion_detail": [],
  172. // "refund_id": "50301802252022062021833667769",
  173. // "status": "PROCESSING",
  174. // "transaction_id": "4200001521202206207964248014",
  175. // "user_received_account": "\u652f\u4ed8\u7528\u6237\u96f6\u94b1"
  176. // }
  177. return $result;
  178. }
  179. /**
  180. * 微信退款回调
  181. *
  182. * @param array $callback
  183. * @param array $config
  184. * @return array
  185. */
  186. public function notifyRefund($callback, $config = [])
  187. {
  188. $this->init('wechat', $config);
  189. try {
  190. $originData = Pay::wechat()->callback(); // 是的,验签就这么简单!
  191. Log::write('pay-notifyrefund-callback-data:' . json_encode($originData));
  192. // {
  193. // "id": "4a553265-1f28-53a3-9395-8d902b902462",
  194. // "create_time": "2022-06-21T11:25:33+08:00",
  195. // "resource_type": "encrypt-resource",
  196. // "event_type": "REFUND.SUCCESS",
  197. // "summary": "\u9000\u6b3e\u6210\u529f",
  198. // "resource": {
  199. // "original_type": "refund",
  200. // "algorithm": "AEAD_AES_256_GCM",
  201. // "ciphertext": {
  202. // "mchid": "1623831039",
  203. // "out_trade_no": "P202211233042122753002100",
  204. // "transaction_id": "4200001417202206214219765470",
  205. // "out_refund_no": "R202211252676008994002100",
  206. // "refund_id": "50300002272022062121864292533",
  207. // "refund_status": "SUCCESS",
  208. // "success_time": "2022-06-21T11:25:33+08:00",
  209. // "amount": {
  210. // "total": 1,
  211. // "refund": 1,
  212. // "payer_total": 1,
  213. // "payer_refund": 1
  214. // },
  215. // "user_received_account": "\u652f\u4ed8\u7528\u6237\u96f6\u94b1"
  216. // },
  217. // "associated_data": "refund",
  218. // "nonce": "8xfQknYyLVop"
  219. // }
  220. // }
  221. if ($originData['event_type'] == 'REFUND.SUCCESS') {
  222. // 支付成功回调
  223. $data = $originData['resource']['ciphertext'] ?? [];
  224. if (isset($data['refund_status']) && $data['refund_status'] == 'SUCCESS') {
  225. // 退款成功
  226. $result = $callback($data, $originData);
  227. return $result;
  228. }
  229. return 'fail';
  230. } else {
  231. // 微信交易未成功,返回 false,让微信再次通知
  232. Log::error('notify-error:退款未成功:' . $originData['event_type']);
  233. return 'fail';
  234. }
  235. } catch (HttpResponseException $e) {
  236. $data = $e->getResponse()->getData();
  237. $message = $data ? ($data['msg'] ?? '') : $e->getMessage();
  238. $this->logPayment('微信退款回调HttpResponseException', ['error' => $message], 'error');
  239. return 'fail';
  240. } catch (\Exception $e) {
  241. $this->logPayment('微信退款回调异常', ['error' => $e->getMessage()], 'error');
  242. return 'fail';
  243. }
  244. }
  245. /**
  246. * 格式化支付参数
  247. *
  248. * @param [type] $params
  249. * @return void
  250. */
  251. protected function formatConfig($config, $data = [], $type = 'normal')
  252. {
  253. if ($config['mode'] == 2 && $type == 'sub_mch') {
  254. // 服务商模式,但需要子商户直连 ,重新定义 config(商家转账到零钱)
  255. $config = [
  256. 'mch_id' => $config['sub_mch_id'],
  257. 'mch_secret_key' => $config['sub_mch_secret_key'],
  258. 'mch_secret_cert' => $config['sub_mch_secret_cert'],
  259. 'mch_public_cert_path' => $config['sub_mch_public_cert_path'],
  260. ];
  261. $config['mode'] = 0; // 临时改为普通商户
  262. }
  263. if ($config['mode'] === 2) {
  264. // 首先将平台配置的 app_id 初始化到配置中
  265. $config['mp_app_id'] = $config['app_id']; // 服务商关联的公众号的 appid
  266. $config['sub_app_id'] = $data['app_id']; // 服务商特约子商户
  267. } else {
  268. $config['app_id'] = $data['app_id'];
  269. }
  270. switch ($this->platform) {
  271. case 'WechatMiniProgram':
  272. $config['_type'] = 'mini'; // 小程序提现,需要传 _type = mini 才能正确获取到 appid
  273. if ($config['mode'] === 2) {
  274. $config['sub_mini_app_id'] = $config['sub_app_id'];
  275. unset($config['sub_app_id']);
  276. } else {
  277. $config['mini_app_id'] = $config['app_id'];
  278. unset($config['app_id']);
  279. }
  280. break;
  281. case 'WechatOfficialAccount':
  282. $config['_type'] = 'mp'; // 小程序提现,需要传 _type = mp 才能正确获取到 appid
  283. if ($config['mode'] === 2) {
  284. $config['sub_mp_app_id'] = $config['sub_app_id'];
  285. unset($config['sub_app_id']);
  286. } else {
  287. $config['mp_app_id'] = $config['app_id'];
  288. unset($config['app_id']);
  289. }
  290. break;
  291. case 'App':
  292. case 'H5':
  293. default:
  294. break;
  295. }
  296. $config['notify_url'] = request()->domain() . '/api/pay/notify/payment/wechat/platform/' . $this->platform;
  297. // $config['mch_secret_cert'] = cdnurl($config['mch_secret_cert'], true);
  298. // $config['mch_public_cert_path'] = cdnurl($config['mch_public_cert_path'], true);
  299. // // $config['mch_secret_cert'] = ROOT_PATH . 'public/apiclient_cert.pem';
  300. // // $config['mch_public_cert_path'] = ROOT_PATH . 'public/apiclient_key.pem';
  301. $config['mch_secret_cert'] = ROOT_PATH . 'public' . $config['mch_secret_cert'];
  302. $config['mch_public_cert_path'] = ROOT_PATH . 'public' . $config['mch_public_cert_path'];
  303. // 可手动配置微信支付公钥证书
  304. $config['wechat_public_cert_id'] = $config['wechat_public_cert_id'] ?? '';
  305. $config['wechat_public_cert'] = $config['wechat_public_cert'] ?? '';
  306. if ($config['wechat_public_cert_id'] && $config['wechat_public_cert']) {
  307. $config['wechat_public_cert_path'] = [
  308. $config['wechat_public_cert_id'] => cdnurl($config['wechat_public_cert'], true)
  309. ];
  310. }
  311. unset($config['wechat_public_cert_id'], $config['wechat_public_cert']);
  312. return $config;
  313. }
  314. }