WxPay.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | 萤火商城系统 [ 致力于通过产品和服务,帮助商家高效化开拓市场 ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2017~2021 https://www.yiovo.com All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // | Licensed 这不是一个自由软件,不允许对程序代码以任何形式任何目的的再发行
  8. // +----------------------------------------------------------------------
  9. // | Author: 萤火科技 <admin@yiovo.com>
  10. // +----------------------------------------------------------------------
  11. namespace app\common\library\wechat;
  12. use app\common\model\Wxapp as WxappModel;
  13. use app\common\enum\OrderType as OrderTypeEnum;
  14. use app\common\enum\order\PayType as OrderPayTypeEnum;
  15. use app\common\library\helper;
  16. use app\common\exception\BaseException;
  17. use think\Log;
  18. /**
  19. * 微信支付
  20. * Class WxPay
  21. * @package app\common\library\wechat
  22. */
  23. class WxPay extends WxBase
  24. {
  25. // 微信支付配置
  26. private $config;
  27. // 订单模型
  28. private $modelClass = [
  29. OrderTypeEnum::ORDER => 'app\api\service\order\PaySuccess',
  30. OrderTypeEnum::RECHARGE => 'app\api\service\recharge\PaySuccess',
  31. OrderTypeEnum::RCORDER =>'app\api\service\ricecard\PaySuccess',
  32. OrderTypeEnum::GROUPBUY =>'app\api\service\groupbuy\PaySuccess',
  33. OrderTypeEnum::GROUPBUYLB =>'app\api\service\groupbuylb\PaySuccess',
  34. OrderTypeEnum::MEMBERCARDBUY =>'app\api\service\member\PaySuccess',
  35. OrderTypeEnum::ZA =>'app\api\service\za\PaySuccess'
  36. ];
  37. /**
  38. * 构造函数
  39. * WxPay constructor.
  40. * @param $config
  41. */
  42. public function __construct($config = false)
  43. {
  44. parent::__construct();
  45. $this->config = $config;
  46. $this->config !== false && $this->setConfig($this->config['app_id'], $this->config['app_secret']);
  47. }
  48. /**
  49. * 统一下单API
  50. * @param $orderNo
  51. * @param $openid
  52. * @param $totalFee
  53. * @param int $orderType 订单类型
  54. * @return array
  55. * @throws BaseException
  56. */
  57. public function unifiedorder($orderNo, $openid, $totalFee, $orderType = OrderTypeEnum::ORDER)
  58. {
  59. // 当前时间
  60. $time = time();
  61. // 生成随机字符串
  62. $nonceStr = md5($time . $openid);
  63. // API参数
  64. $params = [
  65. 'appid' => $this->appId,
  66. 'attach' => helper::jsonEncode(['order_type' => $orderType]),
  67. 'body' => $orderNo,
  68. 'mch_id' => $this->config['mchid'],
  69. 'nonce_str' => $nonceStr,
  70. 'notify_url' => base_url() . '/api/notify/wxpay', // 异步通知地址
  71. 'openid' => $openid,
  72. 'out_trade_no' => $orderNo,
  73. 'spbill_create_ip' => \request()->ip(),
  74. 'total_fee' => $totalFee * 100, // 价格:单位分
  75. 'trade_type' => 'JSAPI',
  76. ];
  77. // 生成签名
  78. $params['sign'] = $this->makeSign($params);
  79. // 请求API
  80. $url = 'https://api.mch.weixin.qq.com/pay/unifiedorder';
  81. $result = $this->post($url, $this->toXml($params));
  82. $prepay = $this->fromXml($result);
  83. // 请求失败
  84. if ($prepay['return_code'] === 'FAIL') {
  85. $errMsg = "微信支付api:{$prepay['return_msg']}";
  86. throwError($errMsg, null, ['error_code' => 'WECHAT_PAY', 'is_created' => true]);
  87. }
  88. if ($prepay['result_code'] === 'FAIL') {
  89. $errMsg = "微信支付api:{$prepay['err_code_des']}";
  90. throwError($errMsg, null, ['error_code' => 'WECHAT_PAY', 'is_created' => true]);
  91. }
  92. // 生成 nonce_str 供前端使用
  93. $paySign = $this->makePaySign($params['nonce_str'], $prepay['prepay_id'], $time);
  94. return [
  95. 'prepay_id' => $prepay['prepay_id'],
  96. 'nonceStr' => $nonceStr,
  97. 'timeStamp' => (string)$time,
  98. 'paySign' => $paySign
  99. ];
  100. }
  101. /**
  102. * 支付成功异步通知
  103. * @throws BaseException
  104. * @throws \Exception
  105. */
  106. public function notify()
  107. {
  108. // $xml = <<<EOF
  109. //<xml><appid><![CDATA[wx8908532a27c5dd4f]]></appid>
  110. //<attach><![CDATA[{"order_type":10}]]></attach>
  111. //<bank_type><![CDATA[CFT]]></bank_type>
  112. //<cash_fee><![CDATA[1]]></cash_fee>
  113. //<fee_type><![CDATA[CNY]]></fee_type>
  114. //<is_subscribe><![CDATA[N]]></is_subscribe>
  115. //<mch_id><![CDATA[1509822581]]></mch_id>
  116. //<nonce_str><![CDATA[ca1fe6d2b4f667cf249bd1d7176c6178]]></nonce_str>
  117. //<openid><![CDATA[oZDDE5JLnVyc6qe6nbNWdbFHtY5I]]></openid>
  118. //<out_trade_no><![CDATA[2019040155491005]]></out_trade_no>
  119. //<result_code><![CDATA[SUCCESS]]></result_code>
  120. //<return_code><![CDATA[SUCCESS]]></return_code>
  121. //<sign><![CDATA[3880232710B7328822D079DC405FB09D]]></sign>
  122. //<time_end><![CDATA[20190401104804]]></time_end>
  123. //<total_fee>1</total_fee>
  124. //<trade_type><![CDATA[JSAPI]]></trade_type>
  125. //<transaction_id><![CDATA[4200000265201904014227830207]]></transaction_id>
  126. //</xml>
  127. //EOF;
  128. if (!$xml = file_get_contents('php://input')) {
  129. $this->returnCode(false, 'Not found DATA');
  130. }
  131. // 将服务器返回的XML数据转化为数组
  132. $data = $this->fromXml($xml);
  133. // 记录日志
  134. log_record($xml);
  135. log_record($data);
  136. // 实例化订单模型
  137. $model = $this->getOrderModel($data['out_trade_no'], $data['attach']);
  138. // 订单信息
  139. $order = $model->getOrderInfo();
  140. empty($order) && $this->returnCode(false, '订单不存在');
  141. // 小程序配置信息
  142. $wxConfig = WxappModel::getWxappCache($order['store_id']);
  143. // 设置支付秘钥
  144. $this->config['apikey'] = $wxConfig['apikey'];
  145. // 保存微信服务器返回的签名sign
  146. $dataSign = $data['sign'];
  147. // sign不参与签名算法
  148. unset($data['sign']);
  149. // 生成签名
  150. $sign = $this->makeSign($data);
  151. // 判断签名是否正确 判断支付状态
  152. if (
  153. ($sign !== $dataSign)
  154. || ($data['return_code'] !== 'SUCCESS')
  155. || ($data['result_code'] !== 'SUCCESS')
  156. ) {
  157. $this->returnCode(false, '签名失败');
  158. }
  159. // 订单支付成功业务处理
  160. $status = $model->onPaySuccess(OrderPayTypeEnum::WECHAT, $data);
  161. if ($status == false) {
  162. $this->returnCode(false, $model->getError());
  163. }
  164. // 返回状态
  165. $this->returnCode(true, 'OK');
  166. }
  167. /**
  168. * 申请退款API
  169. * @param $transaction_id
  170. * @param double $total_fee 订单总金额
  171. * @param double $refund_fee 退款金额
  172. * @return bool
  173. * @throws BaseException
  174. */
  175. public function refund($transaction_id, $total_fee, $refund_fee)
  176. {
  177. // 当前时间
  178. $time = time();
  179. // 生成随机字符串
  180. $nonceStr = md5($time . $transaction_id . $total_fee . $refund_fee);
  181. // API参数
  182. $params = [
  183. 'appid' => $this->appId,
  184. 'mch_id' => $this->config['mchid'],
  185. 'nonce_str' => $nonceStr,
  186. 'transaction_id' => $transaction_id,
  187. 'out_refund_no' => $time,
  188. 'total_fee' => $total_fee * 100,
  189. 'refund_fee' => $refund_fee * 100,
  190. ];
  191. // 生成签名
  192. $params['sign'] = $this->makeSign($params);
  193. // 请求API
  194. $url = 'https://api.mch.weixin.qq.com/secapi/pay/refund';
  195. $result = $this->post($url, $this->toXml($params), true, $this->getCertPem());
  196. // 请求失败
  197. if (empty($result)) {
  198. throwError('微信退款api请求失败');
  199. }
  200. // 格式化返回结果
  201. $prepay = $this->fromXml($result);
  202. // 记录日志
  203. log_record(['name' => '微信退款API', [
  204. 'params' => $params,
  205. 'result' => $result,
  206. 'prepay' => $prepay
  207. ]]);
  208. // 请求失败
  209. if ($prepay['return_code'] === 'FAIL') {
  210. throwError("return_msg: {$prepay['return_msg']}");
  211. }
  212. if ($prepay['result_code'] === 'FAIL') {
  213. throwError("err_code_des: {$prepay['err_code_des']}");
  214. }
  215. return true;
  216. }
  217. /**
  218. * 申请退款API
  219. * @param $transaction_id
  220. * @param double $total_fee 订单总金额
  221. * @param double $refund_fee 退款金额
  222. * @return array
  223. * @throws BaseException
  224. */
  225. public function refundNew($out_trade_no, $out_refund_no, $total_fee, $refund_fee)
  226. {
  227. // 当前时间
  228. $time = time();
  229. // 生成随机字符串
  230. $nonceStr = md5($time . $out_trade_no . $total_fee . $refund_fee);
  231. // API参数
  232. $params = [
  233. 'appid' => $this->appId,
  234. 'mch_id' => $this->config['mchid'],
  235. 'nonce_str' => $nonceStr,
  236. 'out_trade_no' => $out_trade_no,
  237. 'out_refund_no' => $out_refund_no,
  238. 'total_fee' => $total_fee * 100,
  239. 'refund_fee' => $refund_fee * 100,
  240. ];
  241. // 生成签名
  242. $params['sign'] = $this->makeSign($params);
  243. // 请求API
  244. $url = 'https://api.mch.weixin.qq.com/secapi/pay/refund';
  245. //log_record(__METHOD__.json_encode($params),'error');
  246. $result = $this->post($url, $this->toXml($params), true, $this->getCertPem());
  247. // 请求失败
  248. if (empty($result)) {
  249. // Log::error($result);
  250. throw new BaseException(['msg' => '微信退款api请求失败']);
  251. }
  252. // 格式化返回结果
  253. $prepay = $this->fromXml($result);
  254. // 请求失败
  255. if ($prepay['return_code'] === 'FAIL') {
  256. // Log::error($prepay);
  257. log_record(__METHOD__.'-'.$out_trade_no.json_encode($prepay),'error');
  258. throw new BaseException(['msg' => 'return_msg: ' . $prepay['return_msg']]);
  259. }
  260. if ($prepay['result_code'] === 'FAIL') {
  261. // Log::error($prepay);
  262. log_record(__METHOD__.'--'.$out_trade_no.json_encode($prepay),'error');
  263. throw new BaseException(['msg' => 'err_code_des: ' . $prepay['err_code_des']]);
  264. }
  265. // 微信交易流水号 transaction_id
  266. // 微信退款单号 refund_id
  267. return ['status' => 200, 'data' => $prepay, 'message' => '成功'];
  268. }
  269. /**
  270. * 企业付款到零钱API
  271. * @param $orderNo
  272. * @param $openid
  273. * @param $amount
  274. * @param $desc
  275. * @return array
  276. * @throws BaseException
  277. */
  278. public function transfers($orderNo, $openid, $amount, $desc)
  279. {
  280. // API参数
  281. $params = [
  282. 'mch_appid' => $this->appId,
  283. 'mchid' => $this->config['mchid'],
  284. 'nonce_str' => md5(uniqid()),
  285. 'partner_trade_no' => $orderNo,
  286. 'openid' => $openid,
  287. 'check_name' => 'NO_CHECK',
  288. 'amount' => $amount * 100,
  289. 'desc' => $desc,
  290. 'spbill_create_ip' => \request()->ip(),
  291. ];
  292. // 生成签名
  293. $params['sign'] = $this->makeSign($params);
  294. // 请求API
  295. $url = 'https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers';
  296. $result = $this->post($url, $this->toXml($params), true, $this->getCertPem());
  297. // 请求失败
  298. if (empty($result)) {
  299. throwError('企业付款到零钱API请求失败');
  300. }
  301. // 格式化返回结果
  302. $prepay = $this->fromXml($result);
  303. log_record('prepay::'.json_encode($prepay),'error');
  304. // 请求失败
  305. if ($prepay['return_code'] === 'FAIL') {
  306. throwError("return_msg: {$prepay['return_msg']}", null, $prepay);
  307. }
  308. if ($prepay['result_code'] === 'FAIL') {
  309. throwError("err_code_des: {$prepay['err_code_des']}", null, $prepay);
  310. }
  311. $payment_no = $prepay['payment_no']??'';
  312. return ['status'=>true,'payment_no'=>$payment_no];
  313. return true;
  314. }
  315. /**
  316. * 企业付款到零钱-查询付款
  317. * 接口调用频率限制:30/s
  318. * @param string $orderNo 商户订单号
  319. *
  320. */
  321. public function getTransferInfo($orderNo)
  322. {
  323. // API参数
  324. $params = [
  325. 'mch_appid' => $this->appId,
  326. 'mchid' => $this->config['mchid'],
  327. 'nonce_str' => md5(uniqid()),
  328. 'partner_trade_no' => $orderNo,
  329. ];
  330. // 生成签名
  331. $params['sign'] = $this->makeSign($params);
  332. // 请求API
  333. $url = 'https://api.mch.weixin.qq.com/mmpaymkttransfers/gettransferinfo';
  334. $result = $this->post($url, $this->toXml($params), true, $this->getCertPem());
  335. // 请求失败
  336. if (empty($result)) {
  337. throwError('查询付款API请求失败');
  338. }
  339. // 格式化返回结果
  340. $prepay = $this->fromXml($result);
  341. // 请求失败
  342. if ($prepay['return_code'] === 'FAIL') {
  343. throwError("return_msg: {$prepay['return_msg']}", null, $prepay);
  344. }
  345. if ($prepay['result_code'] === 'FAIL') {
  346. throwError("err_code_des: {$prepay['err_code_des']}", null, $prepay);
  347. }
  348. return true;
  349. }
  350. /**
  351. * 获取cert证书文件
  352. * @return array
  353. * @throws BaseException
  354. */
  355. private function getCertPem()
  356. {
  357. if (empty($this->config['cert_pem']) || empty($this->config['key_pem'])) {
  358. throwError('请先到后台小程序设置填写微信支付证书文件');
  359. }
  360. // cert目录
  361. $filePath = __DIR__ . '/cert/' . $this->config['store_id'] . '/';
  362. return [
  363. 'certPem' => $filePath . 'cert.pem',
  364. 'keyPem' => $filePath . 'key.pem'
  365. ];
  366. }
  367. /**
  368. * 实例化订单模型 (根据attach判断)
  369. * @param $orderNo
  370. * @param null $attach
  371. * @return mixed
  372. */
  373. private function getOrderModel($orderNo, $attach = null)
  374. {
  375. $attach = helper::jsonDecode($attach);
  376. // 判断订单类型返回对应的订单模型
  377. $model = $this->modelClass[$attach['order_type']];
  378. return new $model($orderNo);
  379. }
  380. /**
  381. * 返回状态给微信服务器
  382. * @param boolean $returnCode
  383. * @param string $msg
  384. */
  385. private function returnCode($returnCode = true, $msg = null)
  386. {
  387. // 返回状态
  388. $return = [
  389. 'return_code' => $returnCode ? 'SUCCESS' : 'FAIL',
  390. 'return_msg' => $msg ?: 'OK',
  391. ];
  392. // 记录日志
  393. log_record([
  394. 'name' => '返回微信支付状态',
  395. 'data' => $return
  396. ]);
  397. die($this->toXml($return));
  398. }
  399. /**
  400. * 生成paySign
  401. * @param $nonceStr
  402. * @param $prepay_id
  403. * @param $timeStamp
  404. * @return string
  405. */
  406. private function makePaySign($nonceStr, $prepay_id, $timeStamp)
  407. {
  408. $data = [
  409. 'appId' => $this->appId,
  410. 'nonceStr' => $nonceStr,
  411. 'package' => 'prepay_id=' . $prepay_id,
  412. 'signType' => 'MD5',
  413. 'timeStamp' => $timeStamp,
  414. ];
  415. // 签名步骤一:按字典序排序参数
  416. ksort($data);
  417. $string = $this->toUrlParams($data);
  418. // 签名步骤二:在string后加入KEY
  419. $string = $string . '&key=' . $this->config['apikey'];
  420. // 签名步骤三:MD5加密
  421. $string = md5($string);
  422. // 签名步骤四:所有字符转为大写
  423. $result = strtoupper($string);
  424. return $result;
  425. }
  426. /**
  427. * 将xml转为array
  428. * @param $xml
  429. * @return mixed
  430. */
  431. private function fromXml($xml)
  432. {
  433. // 禁止引用外部xml实体
  434. libxml_disable_entity_loader(true);
  435. return helper::jsonDecode(helper::jsonEncode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)));
  436. }
  437. /**
  438. * 生成签名
  439. * @param $values
  440. * @return string 本函数不覆盖sign成员变量,如要设置签名需要调用SetSign方法赋值
  441. */
  442. private function makeSign($values)
  443. {
  444. //签名步骤一:按字典序排序参数
  445. ksort($values);
  446. $string = $this->toUrlParams($values);
  447. //签名步骤二:在string后加入KEY
  448. $string = $string . '&key=' . $this->config['apikey'];
  449. //签名步骤三:MD5加密
  450. $string = md5($string);
  451. //签名步骤四:所有字符转为大写
  452. $result = strtoupper($string);
  453. return $result;
  454. }
  455. /**
  456. * 格式化参数格式化成url参数
  457. * @param $values
  458. * @return string
  459. */
  460. private function toUrlParams($values)
  461. {
  462. $buff = '';
  463. foreach ($values as $k => $v) {
  464. if ($k != 'sign' && $v != '' && !is_array($v)) {
  465. $buff .= $k . '=' . $v . '&';
  466. }
  467. }
  468. return trim($buff, '&');
  469. }
  470. /**
  471. * 输出xml字符
  472. * @param $values
  473. * @return bool|string
  474. */
  475. private function toXml($values)
  476. {
  477. if (!is_array($values)
  478. || count($values) <= 0
  479. ) {
  480. return false;
  481. }
  482. $xml = "<xml>";
  483. foreach ($values as $key => $val) {
  484. if (is_numeric($val)) {
  485. $xml .= "<" . $key . ">" . $val . "</" . $key . ">";
  486. } else {
  487. $xml .= "<" . $key . "><![CDATA[" . $val . "]]></" . $key . ">";
  488. }
  489. }
  490. $xml .= "</xml>";
  491. return $xml;
  492. }
  493. }