WxPay.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  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. /**
  18. * 微信支付
  19. * Class WxPay
  20. * @package app\common\library\wechat
  21. */
  22. class WxPay extends WxBase
  23. {
  24. // 微信支付配置
  25. private $config;
  26. // 订单模型
  27. private $modelClass = [
  28. OrderTypeEnum::ORDER => 'app\api\service\order\PaySuccess',
  29. OrderTypeEnum::RECHARGE => 'app\api\service\recharge\PaySuccess',
  30. ];
  31. /**
  32. * 构造函数
  33. * WxPay constructor.
  34. * @param $config
  35. */
  36. public function __construct($config = false)
  37. {
  38. parent::__construct();
  39. $this->config = $config;
  40. $this->config !== false && $this->setConfig($this->config['app_id'], $this->config['app_secret']);
  41. }
  42. /**
  43. * 统一下单API
  44. * @param $orderNo
  45. * @param $openid
  46. * @param $totalFee
  47. * @param int $orderType 订单类型
  48. * @return array
  49. * @throws BaseException
  50. */
  51. public function unifiedorder($orderNo, $openid, $totalFee, $orderType = OrderTypeEnum::ORDER)
  52. {
  53. // 当前时间
  54. $time = time();
  55. // 生成随机字符串
  56. $nonceStr = md5($time . $openid);
  57. // API参数
  58. $params = [
  59. 'appid' => $this->appId,
  60. 'attach' => helper::jsonEncode(['order_type' => $orderType]),
  61. 'body' => $orderNo,
  62. 'mch_id' => $this->config['mchid'],
  63. 'nonce_str' => $nonceStr,
  64. 'notify_url' => base_url() . 'notice.php', // 异步通知地址
  65. 'openid' => $openid,
  66. 'out_trade_no' => $orderNo,
  67. 'spbill_create_ip' => \request()->ip(),
  68. 'total_fee' => $totalFee * 100, // 价格:单位分
  69. 'trade_type' => 'JSAPI',
  70. ];
  71. // 生成签名
  72. $params['sign'] = $this->makeSign($params);
  73. // 请求API
  74. $url = 'https://api.mch.weixin.qq.com/pay/unifiedorder';
  75. $result = $this->post($url, $this->toXml($params));
  76. $prepay = $this->fromXml($result);
  77. // 请求失败
  78. if ($prepay['return_code'] === 'FAIL') {
  79. $errMsg = "微信支付api:{$prepay['return_msg']}";
  80. throwError($errMsg, null, ['error_code' => 'WECHAT_PAY', 'is_created' => true]);
  81. }
  82. if ($prepay['result_code'] === 'FAIL') {
  83. $errMsg = "微信支付api:{$prepay['err_code_des']}";
  84. throwError($errMsg, null, ['error_code' => 'WECHAT_PAY', 'is_created' => true]);
  85. }
  86. // 生成 nonce_str 供前端使用
  87. $paySign = $this->makePaySign($params['nonce_str'], $prepay['prepay_id'], $time);
  88. return [
  89. 'prepay_id' => $prepay['prepay_id'],
  90. 'nonceStr' => $nonceStr,
  91. 'timeStamp' => (string)$time,
  92. 'paySign' => $paySign
  93. ];
  94. }
  95. /**
  96. * 支付成功异步通知
  97. * @throws BaseException
  98. * @throws \Exception
  99. */
  100. public function notify()
  101. {
  102. // $xml = <<<EOF
  103. //<xml><appid><![CDATA[wx8908532a27c5dd4f]]></appid>
  104. //<attach><![CDATA[{"order_type":10}]]></attach>
  105. //<bank_type><![CDATA[CFT]]></bank_type>
  106. //<cash_fee><![CDATA[1]]></cash_fee>
  107. //<fee_type><![CDATA[CNY]]></fee_type>
  108. //<is_subscribe><![CDATA[N]]></is_subscribe>
  109. //<mch_id><![CDATA[1509822581]]></mch_id>
  110. //<nonce_str><![CDATA[ca1fe6d2b4f667cf249bd1d7176c6178]]></nonce_str>
  111. //<openid><![CDATA[oZDDE5JLnVyc6qe6nbNWdbFHtY5I]]></openid>
  112. //<out_trade_no><![CDATA[2019040155491005]]></out_trade_no>
  113. //<result_code><![CDATA[SUCCESS]]></result_code>
  114. //<return_code><![CDATA[SUCCESS]]></return_code>
  115. //<sign><![CDATA[3880232710B7328822D079DC405FB09D]]></sign>
  116. //<time_end><![CDATA[20190401104804]]></time_end>
  117. //<total_fee>1</total_fee>
  118. //<trade_type><![CDATA[JSAPI]]></trade_type>
  119. //<transaction_id><![CDATA[4200000265201904014227830207]]></transaction_id>
  120. //</xml>
  121. //EOF;
  122. if (!$xml = file_get_contents('php://input')) {
  123. $this->returnCode(false, 'Not found DATA');
  124. }
  125. // 将服务器返回的XML数据转化为数组
  126. $data = $this->fromXml($xml);
  127. // 记录日志
  128. log_record($xml);
  129. log_record($data);
  130. // 实例化订单模型
  131. $model = $this->getOrderModel($data['out_trade_no'], $data['attach']);
  132. // 订单信息
  133. $order = $model->getOrderInfo();
  134. empty($order) && $this->returnCode(false, '订单不存在');
  135. // 小程序配置信息
  136. $wxConfig = WxappModel::getWxappCache($order['store_id']);
  137. // 设置支付秘钥
  138. $this->config['apikey'] = $wxConfig['apikey'];
  139. // 保存微信服务器返回的签名sign
  140. $dataSign = $data['sign'];
  141. // sign不参与签名算法
  142. unset($data['sign']);
  143. // 生成签名
  144. $sign = $this->makeSign($data);
  145. // 判断签名是否正确 判断支付状态
  146. if (
  147. ($sign !== $dataSign)
  148. || ($data['return_code'] !== 'SUCCESS')
  149. || ($data['result_code'] !== 'SUCCESS')
  150. ) {
  151. $this->returnCode(false, '签名失败');
  152. }
  153. // 订单支付成功业务处理
  154. $status = $model->onPaySuccess(OrderPayTypeEnum::WECHAT, $data);
  155. if ($status == false) {
  156. $this->returnCode(false, $model->getError());
  157. }
  158. // 返回状态
  159. $this->returnCode(true, 'OK');
  160. }
  161. /**
  162. * 申请退款API
  163. * @param $transaction_id
  164. * @param double $total_fee 订单总金额
  165. * @param double $refund_fee 退款金额
  166. * @return bool
  167. * @throws BaseException
  168. */
  169. public function refund($transaction_id, $total_fee, $refund_fee)
  170. {
  171. // 当前时间
  172. $time = time();
  173. // 生成随机字符串
  174. $nonceStr = md5($time . $transaction_id . $total_fee . $refund_fee);
  175. // API参数
  176. $params = [
  177. 'appid' => $this->appId,
  178. 'mch_id' => $this->config['mchid'],
  179. 'nonce_str' => $nonceStr,
  180. 'transaction_id' => $transaction_id,
  181. 'out_refund_no' => $time,
  182. 'total_fee' => $total_fee * 100,
  183. 'refund_fee' => $refund_fee * 100,
  184. ];
  185. // 生成签名
  186. $params['sign'] = $this->makeSign($params);
  187. // 请求API
  188. $url = 'https://api.mch.weixin.qq.com/secapi/pay/refund';
  189. $result = $this->post($url, $this->toXml($params), true, $this->getCertPem());
  190. // 请求失败
  191. if (empty($result)) {
  192. throwError('微信退款api请求失败');
  193. }
  194. // 格式化返回结果
  195. $prepay = $this->fromXml($result);
  196. // 记录日志
  197. log_record(['name' => '微信退款API', [
  198. 'params' => $params,
  199. 'result' => $result,
  200. 'prepay' => $prepay
  201. ]]);
  202. // 请求失败
  203. if ($prepay['return_code'] === 'FAIL') {
  204. throwError("return_msg: {$prepay['return_msg']}");
  205. }
  206. if ($prepay['result_code'] === 'FAIL') {
  207. throwError("err_code_des: {$prepay['err_code_des']}");
  208. }
  209. return true;
  210. }
  211. /**
  212. * 企业付款到零钱API
  213. * @param $orderNo
  214. * @param $openid
  215. * @param $amount
  216. * @param $desc
  217. * @return bool
  218. * @throws BaseException
  219. */
  220. public function transfers($orderNo, $openid, $amount, $desc)
  221. {
  222. // API参数
  223. $params = [
  224. 'mch_appid' => $this->appId,
  225. 'mchid' => $this->config['mchid'],
  226. 'nonce_str' => md5(uniqid()),
  227. 'partner_trade_no' => $orderNo,
  228. 'openid' => $openid,
  229. 'check_name' => 'NO_CHECK',
  230. 'amount' => $amount * 100,
  231. 'desc' => $desc,
  232. 'spbill_create_ip' => \request()->ip(),
  233. ];
  234. // 生成签名
  235. $params['sign'] = $this->makeSign($params);
  236. // 请求API
  237. $url = 'https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers';
  238. $result = $this->post($url, $this->toXml($params), true, $this->getCertPem());
  239. // 请求失败
  240. if (empty($result)) {
  241. throwError('企业付款到零钱API请求失败');
  242. }
  243. // 格式化返回结果
  244. $prepay = $this->fromXml($result);
  245. // 请求失败
  246. if ($prepay['return_code'] === 'FAIL') {
  247. throwError("return_msg: {$prepay['return_msg']}");
  248. }
  249. if ($prepay['result_code'] === 'FAIL') {
  250. throwError("err_code_des: {$prepay['err_code_des']}");
  251. }
  252. return true;
  253. }
  254. /**
  255. * 获取cert证书文件
  256. * @return array
  257. * @throws BaseException
  258. */
  259. private function getCertPem()
  260. {
  261. if (empty($this->config['cert_pem']) || empty($this->config['key_pem'])) {
  262. throwError('请先到后台小程序设置填写微信支付证书文件');
  263. }
  264. // cert目录
  265. $filePath = __DIR__ . '/cert/' . $this->config['store_id'] . '/';
  266. return [
  267. 'certPem' => $filePath . 'cert.pem',
  268. 'keyPem' => $filePath . 'key.pem'
  269. ];
  270. }
  271. /**
  272. * 实例化订单模型 (根据attach判断)
  273. * @param $orderNo
  274. * @param null $attach
  275. * @return mixed
  276. */
  277. private function getOrderModel($orderNo, $attach = null)
  278. {
  279. $attach = helper::jsonDecode($attach);
  280. // 判断订单类型返回对应的订单模型
  281. $model = $this->modelClass[$attach['order_type']];
  282. return new $model($orderNo);
  283. }
  284. /**
  285. * 返回状态给微信服务器
  286. * @param boolean $returnCode
  287. * @param string $msg
  288. */
  289. private function returnCode($returnCode = true, $msg = null)
  290. {
  291. // 返回状态
  292. $return = [
  293. 'return_code' => $returnCode ? 'SUCCESS' : 'FAIL',
  294. 'return_msg' => $msg ?: 'OK',
  295. ];
  296. // 记录日志
  297. log_record([
  298. 'name' => '返回微信支付状态',
  299. 'data' => $return
  300. ]);
  301. die($this->toXml($return));
  302. }
  303. /**
  304. * 生成paySign
  305. * @param $nonceStr
  306. * @param $prepay_id
  307. * @param $timeStamp
  308. * @return string
  309. */
  310. private function makePaySign($nonceStr, $prepay_id, $timeStamp)
  311. {
  312. $data = [
  313. 'appId' => $this->appId,
  314. 'nonceStr' => $nonceStr,
  315. 'package' => 'prepay_id=' . $prepay_id,
  316. 'signType' => 'MD5',
  317. 'timeStamp' => $timeStamp,
  318. ];
  319. // 签名步骤一:按字典序排序参数
  320. ksort($data);
  321. $string = $this->toUrlParams($data);
  322. // 签名步骤二:在string后加入KEY
  323. $string = $string . '&key=' . $this->config['apikey'];
  324. // 签名步骤三:MD5加密
  325. $string = md5($string);
  326. // 签名步骤四:所有字符转为大写
  327. $result = strtoupper($string);
  328. return $result;
  329. }
  330. /**
  331. * 将xml转为array
  332. * @param $xml
  333. * @return mixed
  334. */
  335. private function fromXml($xml)
  336. {
  337. // 禁止引用外部xml实体
  338. libxml_disable_entity_loader(true);
  339. return helper::jsonDecode(helper::jsonEncode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)));
  340. }
  341. /**
  342. * 生成签名
  343. * @param $values
  344. * @return string 本函数不覆盖sign成员变量,如要设置签名需要调用SetSign方法赋值
  345. */
  346. private function makeSign($values)
  347. {
  348. //签名步骤一:按字典序排序参数
  349. ksort($values);
  350. $string = $this->toUrlParams($values);
  351. //签名步骤二:在string后加入KEY
  352. $string = $string . '&key=' . $this->config['apikey'];
  353. //签名步骤三:MD5加密
  354. $string = md5($string);
  355. //签名步骤四:所有字符转为大写
  356. $result = strtoupper($string);
  357. return $result;
  358. }
  359. /**
  360. * 格式化参数格式化成url参数
  361. * @param $values
  362. * @return string
  363. */
  364. private function toUrlParams($values)
  365. {
  366. $buff = '';
  367. foreach ($values as $k => $v) {
  368. if ($k != 'sign' && $v != '' && !is_array($v)) {
  369. $buff .= $k . '=' . $v . '&';
  370. }
  371. }
  372. return trim($buff, '&');
  373. }
  374. /**
  375. * 输出xml字符
  376. * @param $values
  377. * @return bool|string
  378. */
  379. private function toXml($values)
  380. {
  381. if (!is_array($values)
  382. || count($values) <= 0
  383. ) {
  384. return false;
  385. }
  386. $xml = "<xml>";
  387. foreach ($values as $key => $val) {
  388. if (is_numeric($val)) {
  389. $xml .= "<" . $key . ">" . $val . "</" . $key . ">";
  390. } else {
  391. $xml .= "<" . $key . "><![CDATA[" . $val . "]]></" . $key . ">";
  392. }
  393. }
  394. $xml .= "</xml>";
  395. return $xml;
  396. }
  397. }