WxService.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. <?php
  2. namespace app\api\service;
  3. use WeChatPay\Builder;
  4. use WeChatPay\Crypto\Rsa;
  5. use WeChatPay\Formatter;
  6. use WeChatPay\Util\PemUtil;
  7. class WxService extends BaseService
  8. {
  9. private static function wxAppConfig()
  10. {
  11. return array_merge([
  12. "appid" => config("site.wx_app_appid") ?? "",
  13. "app_secret" => config("site.wx_app_secret") ?? "",
  14. ], WxService::wxMchConfig());
  15. }
  16. private static function wxWebConfig()
  17. {
  18. return array_merge([
  19. "appid" => config("site.wx_h5_appid") ?? "",
  20. "app_secret" => config("site.wx_h5_secret") ?? "",
  21. ], WxService::wxMchConfig());
  22. }
  23. private static function wxAppletConfig()
  24. {
  25. return array_merge([
  26. "appid" => config("site.wx_applet_appid") ?? "",
  27. "app_secret" => config("site.wx_applet_secret") ?? "",
  28. ], WxService::wxMchConfig());
  29. }
  30. private static function wxMchConfig()
  31. {
  32. return [
  33. "mch_id" => config("site.wx_mch_id") ?? "",
  34. "mch_v3_api" => config("site.wx_mch_v3_api") ?? "",
  35. "mch_certificate_serial" => config("site.wx_mch_certificate_serial") ?? "",
  36. "mch_private_key_file_path" => ROOT_PATH . "certificate" . DS . "wx" . DS . "apiclient_key.pem",
  37. "platform_certificate_file_path" => ROOT_PATH . "certificate" . DS . "wx" . DS . "apiclient_cert.pem"
  38. ];
  39. }
  40. public static function wxPayConfigByType($type = "app")
  41. {
  42. if ($type == "app")
  43. return self::wxAppConfig();
  44. if ($type == "web")
  45. return self::wxWebConfig();
  46. return self::wxAppletConfig();
  47. }
  48. /**
  49. * h5获取微信用户信息
  50. * @param $code
  51. * @return \SResult
  52. */
  53. public function wxLoginByWeb($code)
  54. {
  55. if (!$code)
  56. return $this->fail("code不存在!");
  57. $config = WxService::wxWebConfig();
  58. $s_result = $this->curl_get("https://api.weixin.qq.com/sns/oauth2/access_token?appid={$config["appid"]}&secret={$config["app_secret"]}&code={$code}&grant_type=authorization_code");
  59. if (0 === $s_result->code())
  60. return $s_result;
  61. $code_of_data = $s_result->data();
  62. // 请求用户信息
  63. $userinfo_result = $this->curl_get("https://api.weixin.qq.com/sns/userinfo?access_token={$code_of_data['access_token']}&openid={$code_of_data['openid']}");
  64. if (0 === $userinfo_result->code())
  65. return $userinfo_result;
  66. $userinfo_of_data = $userinfo_result->data();
  67. $merge_array = array_merge($code_of_data, $userinfo_of_data);
  68. return $this->ok($merge_array);
  69. }
  70. /**
  71. * Applet获取微信用户信息
  72. * @param $code
  73. * @return \SResult
  74. */
  75. public function wxLoginByApplet($code)
  76. {
  77. if (!$code)
  78. return $this->fail("code不存在!");
  79. $config = WxService::wxAppletConfig();
  80. $s_result = $this->curl_get("https://api.weixin.qq.com/sns/jscode2session?appid={$config["appid"]}&secret={$config["app_secret"]}&js_code={$code}&grant_type=authorization_code");
  81. if (0 === $s_result->code())
  82. return $s_result;
  83. return $this->ok($s_result->data());
  84. }
  85. // curl get请求
  86. private function curl_get($url)
  87. {
  88. $curl = curl_init();
  89. // 设置抓取的url
  90. curl_setopt($curl, CURLOPT_URL, $url);
  91. // 设置头文件的信息作为数据流输出
  92. curl_setopt($curl, CURLOPT_HEADER, false);
  93. // 超时设置,以秒为单位
  94. curl_setopt($curl, CURLOPT_TIMEOUT, 1);
  95. // 设置请求头
  96. curl_setopt($curl, CURLOPT_HTTPHEADER, [
  97. 'Accept: application/json',
  98. ]);
  99. // 设置获取的信息以文件流的形式返回,而不是直接输出。
  100. curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
  101. curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
  102. curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
  103. // 执行命令
  104. $data = curl_exec($curl);
  105. $error = curl_error($curl);
  106. curl_close($curl);
  107. // 显示错误信息
  108. if ($error)
  109. return $this->fail($error);
  110. $response = json_decode($data, true);
  111. if (isset($response["errcode"]) && $response["errcode"] !== 0)
  112. return $this->fail($response["errmsg"]);
  113. return $this->ok($response);
  114. }
  115. private function sign($message, $key_path)
  116. {
  117. $mch_private_key = file_get_contents($key_path);
  118. openssl_sign($message, $raw_sign, $mch_private_key, 'sha256WithRSAEncryption');
  119. return base64_encode($raw_sign);
  120. }
  121. public function instance($type = 'web')
  122. {
  123. $config = WxService::wxPayConfigByType($type);
  124. // 从本地文件中加载「商户API私钥」,「商户API私钥」会用来生成请求的签名
  125. $merchantPrivateKeyFilePath = "file://" . $config["mch_private_key_file_path"];
  126. $merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath, Rsa::KEY_TYPE_PRIVATE);
  127. // 从本地文件中加载「微信支付平台证书」,用来验证微信支付应答的签名
  128. $platformCertificateFilePath = "file://" . $config["platform_certificate_file_path"];
  129. $platformPublicKeyInstance = Rsa::from($platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);
  130. // 从「微信支付平台证书」中获取「证书序列号」
  131. $platformCertificateSerial = PemUtil::parseCertificateSerialNo($platformCertificateFilePath);
  132. // 构造一个 APIv3 客户端实例
  133. return Builder::factory([
  134. 'mchid' => $config["mch_id"],// 商户号
  135. 'serial' => $config["mch_certificate_serial"],// 「商户API证书」的「证书序列号」
  136. 'privateKey' => $merchantPrivateKeyInstance,
  137. 'certs' => [
  138. $platformCertificateSerial => $platformPublicKeyInstance,
  139. ],
  140. ]);
  141. }
  142. public function webPay($openid, $out_trade_no, $description, $amount)
  143. {
  144. try {
  145. $config = WxService::wxWebConfig();
  146. $body = [
  147. 'appid' => $config["appid"],
  148. 'mchid' => $config['mch_id'],
  149. 'description' => $description,
  150. 'out_trade_no' => $out_trade_no,
  151. 'notify_url' => "https://pbbaby.xunsoftware.com/api.php/api/callback/wxpay",
  152. 'amount' => [
  153. 'total' => fixed2Float($amount * 100),
  154. 'currency' => 'CNY'
  155. ],
  156. 'payer' => [
  157. "openid" => $openid
  158. ]
  159. ];
  160. $resp = $this->instance("web")
  161. ->chain('v3/pay/transactions/jsapi')
  162. ->post(['json' => $body]);
  163. if (200 === $resp->getStatusCode()) {
  164. $resp_data = json_decode($resp->getBody()->__toString(), true);
  165. $res = [
  166. 'appId' => $config['appid'],
  167. 'timeStamp' => time(),
  168. 'nonceStr' => Formatter::nonce(),
  169. 'package' => 'prepay_id=' . $resp_data['prepay_id'],
  170. 'signType' => 'RSA'
  171. ];
  172. $sign = $this->sign($res['appId'] . "\n" . $res["timeStamp"] . "\n" . $res["nonceStr"] . "\n" . $res["package"] . "\n", $config["mch_private_key_file_path"]);
  173. $res["paySign"] = $sign;
  174. return $this->ok($res);
  175. }
  176. return $this->fail("发起支付失败!");
  177. } catch (\Exception $e) {
  178. // 进行错误处理
  179. if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
  180. $r = $e->getResponse();
  181. return $this->fail($r->getBody()->__toString());
  182. }
  183. return $this->fail($e->getMessage());
  184. }
  185. }
  186. public function webRefund($out_trade_no, $description, $total_amount, $refund_amount)
  187. {
  188. try {
  189. $body = [
  190. 'out_trade_no' => $out_trade_no,
  191. "out_refund_no" => $out_trade_no,
  192. "reason" => $description,
  193. 'notify_url' => "https://pbbaby.xunsoftware.com/api.php/api/callback/wxrefund",
  194. 'amount' => [
  195. 'refund' => fixed2Float($refund_amount * 100),
  196. "total" => fixed2Float($total_amount * 100),
  197. 'currency' => 'CNY'
  198. ],
  199. ];
  200. $resp = $this->instance("web")
  201. ->chain('v3/refund/domestic/refunds')
  202. ->post(['json' => $body]);
  203. if (200 === $resp->getStatusCode()) {
  204. return $this->ok(json_decode($resp->getBody()->__toString(), true), "发起退款成功!");
  205. }
  206. return $this->fail("发起退款失败!");
  207. } catch (\Exception $e) {
  208. // 进行错误处理
  209. if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
  210. $response = json_decode($e->getResponse()->getBody()->__toString(), true);
  211. return $this->fail($response["message"]);
  212. }
  213. return $this->fail($e->getMessage());
  214. }
  215. }
  216. public function appletPay($openid, $out_trade_no, $description, $amount)
  217. {
  218. try {
  219. $config = WxService::wxAppletConfig();
  220. $resp = $this->instance("applet")
  221. ->chain('v3/pay/transactions/jsapi')
  222. ->post(['json' => [
  223. 'appid' => $config["appid"],
  224. 'mchid' => $config['mch_id'],
  225. 'description' => $description,
  226. 'out_trade_no' => $out_trade_no,
  227. 'notify_url' => "https://pbbaby.xunsoftware.com/api.php/api/callback/wxpay",
  228. 'amount' => [
  229. 'total' => fixed2Float($amount * 100),
  230. 'currency' => 'CNY'
  231. ],
  232. 'payer' => [
  233. "openid" => $openid
  234. ]
  235. ]]);
  236. if (200 === $resp->getStatusCode()) {
  237. $resp_data = json_decode($resp->getBody()->__toString(), true);
  238. $res = [
  239. 'appId' => $config['appid'],
  240. 'timeStamp' => time(),
  241. 'nonceStr' => Formatter::nonce(),
  242. 'package' => 'prepay_id=' . $resp_data['prepay_id'],
  243. 'signType' => 'RSA'
  244. ];
  245. $sign = $this->sign($res['appId'] . "\n" . $res["timeStamp"] . "\n" . $res["nonceStr"] . "\n" . $res["package"] . "\n", $config["mch_private_key_file_path"]);
  246. $res["paySign"] = $sign;
  247. return $this->ok($res);
  248. }
  249. return $this->fail("发起支付失败!");
  250. } catch (\Exception $e) {
  251. // 进行错误处理
  252. if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
  253. $r = $e->getResponse();
  254. return $this->fail($r->getBody()->__toString());
  255. }
  256. return $this->fail($e->getMessage());
  257. }
  258. }
  259. public function appletRefund($out_trade_no, $description, $total_amount, $refund_amount)
  260. {
  261. try {
  262. $body = [
  263. 'out_trade_no' => $out_trade_no,
  264. 'out_refund_no' => $out_trade_no,
  265. 'reason' => $description,
  266. 'notify_url' => 'https://pbbaby.xunsoftware.com/api.php/api/callback/wxrefund',
  267. 'amount' => [
  268. 'refund' => fixed2Float($refund_amount * 100),
  269. 'total' => fixed2Float($total_amount * 100),
  270. 'currency' => 'CNY'
  271. ],
  272. ];
  273. $resp = $this->instance("applet")
  274. ->chain('v3/refund/domestic/refunds')
  275. ->post(['json' => $body]);
  276. if (200 === $resp->getStatusCode()) {
  277. return $this->ok(json_decode($resp->getBody()->__toString(), true), "发起退款成功!");
  278. }
  279. return $this->fail("发起退款失败!");
  280. } catch (\Exception $e) {
  281. // 进行错误处理
  282. if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
  283. $response = json_decode($e->getResponse()->getBody()->__toString(), true);
  284. return $this->fail($response["message"]);
  285. }
  286. return $this->fail($e->getMessage());
  287. }
  288. }
  289. public function appPay($openid, $out_trade_no, $description, $amount)
  290. {
  291. $config = WxService::wxAppConfig();
  292. $body = [
  293. 'appid' => $config["appid"],
  294. 'mchid' => $config['mch_id'],
  295. 'description' => $description,
  296. 'out_trade_no' => $out_trade_no,
  297. 'notify_url' => "https://pbbaby.xunsoftware.com/api.php/api/callback/wxpay",
  298. 'amount' => [
  299. 'total' => fixed2Float($amount * 100),
  300. 'currency' => 'CNY'
  301. ],
  302. 'payer' => [
  303. "openid" => $openid
  304. ]
  305. ];
  306. return $this->ok([
  307. 'appId' => $config['appid'],
  308. 'timeStamp' => time(),
  309. 'nonceStr' => Formatter::nonce(),
  310. 'package' => 'prepay_id=21435325234534sdfdhasdg123',
  311. 'signType' => 'RSA'
  312. ]);
  313. }
  314. public function appRefund($out_trade_no, $description, $total_amount, $refund_amount)
  315. {
  316. try {
  317. $config = WxService::wxAppConfig();
  318. $body = [
  319. 'appid' => $config["appid"],
  320. 'mchid' => $config['mch_id'],
  321. 'description' => $description,
  322. 'out_trade_no' => $out_trade_no,
  323. "out_refund_no" => $out_trade_no,
  324. "reason" => $description,
  325. 'notify_url' => "https://pbbaby.xunsoftware.com/api.php/api/callback/wxrefund",
  326. 'amount' => [
  327. 'refund' => $refund_amount * 100,
  328. "total" => $total_amount * 100,
  329. 'currency' => 'CNY'
  330. ]
  331. ];
  332. $resp = $this->instance("web")
  333. ->chain('v3/refund/domestic/refunds')
  334. ->post(['json' => $body]);
  335. if (200 === $resp->getStatusCode()) {
  336. return $this->ok(true, "发起退款成功!");
  337. }
  338. return $this->fail("发起退款失败!");
  339. } catch (\Exception $e) {
  340. // 进行错误处理
  341. if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
  342. $r = $e->getResponse();
  343. return $this->fail($r->getBody()->__toString());
  344. }
  345. return $this->fail($e->getMessage());
  346. }
  347. }
  348. public function deposit($openid, $out_batch_no, $batch_name, $batch_remark, $total_amount)
  349. {
  350. $config = self::wxWebConfig();
  351. try {
  352. $resp = $this->instance("web")
  353. ->chain('/v3/transfer/batches')
  354. ->post(['json' => [
  355. 'appid' => $config["mch_id"],
  356. "out_batch_no" => $out_batch_no,
  357. "batch_name" => $batch_name,
  358. "batch_remark" => $batch_remark,
  359. "total_amount" => fixed2Float($total_amount * 100),
  360. 'total_num' => 1,
  361. 'transfer_detail_list' => [
  362. [
  363. "out_detail_no" => $out_batch_no,
  364. "transfer_amount" => fixed2Float($total_amount * 100),
  365. "transfer_remark" => $batch_remark,
  366. "openid" => $openid,
  367. ]
  368. ],
  369. ]]);
  370. if (200 === $resp->getStatusCode()) {
  371. return $this->ok(json_decode($resp->getBody()->__toString(), true), "提现成功!");
  372. }
  373. return $this->fail("提现失败!");
  374. } catch (\Exception $e) {
  375. // 进行错误处理
  376. if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
  377. $r = $e->getResponse();
  378. return $this->fail(json_decode($r->getBody()->__toString(), true));
  379. }
  380. return $this->fail($e->getMessage());
  381. }
  382. }
  383. }