WxService.php 17 KB

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