WxPay.php 13 KB

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