qnWxPay.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. <?php
  2. /**
  3. * 微信支付
  4. * Created by PhpStorm.
  5. * User: phperstar
  6. * Date: 2019/11/28
  7. * Time: 2:12 PM
  8. */
  9. namespace Util\WeiXin;
  10. class QnWxPay
  11. {
  12. /**
  13. * 小程序 appId
  14. * @var string $appid
  15. */
  16. private $appid;
  17. /**
  18. * 商户号
  19. * @var string $mch_id
  20. */
  21. private $mch_id;
  22. /**
  23. * 商户支付密钥
  24. */
  25. private $partnerKey;
  26. /**
  27. * 微信支付异步通知地址
  28. */
  29. private $notifyUrl = PAY_NOTIFY_URL.'/common/WeiXinPayNotify/notify';
  30. /**
  31. * 公共的接口请求地址
  32. * @var string
  33. */
  34. private $apiUrl = 'https://api.mch.weixin.qq.com/pay/';
  35. /**
  36. * 基础得请求地址
  37. */
  38. private $baseUrl = 'https://api.mch.weixin.qq.com/';
  39. private $config;
  40. /**
  41. * Pay constructor.
  42. * @param string $appid
  43. * @param $mch_id
  44. * @param $partnerKey
  45. */
  46. public function __construct($appid='', $mch_id, $partnerKey)
  47. {
  48. $this->appid = $appid;
  49. $this->mch_id = $mch_id;
  50. $this->partnerKey = $partnerKey;
  51. if(empty($config)) $config = config('wxpay');
  52. $this->config = $config;
  53. }
  54. public function wxmpPay($post=[]){
  55. $url = $this->apiUrl.'unifiedorder';
  56. $params = [
  57. 'appid' => $this->config["APPID"], // 小程序ID/微信开放平台审核通过的应用APPID
  58. 'mch_id' => $this->config["MCHID"], // 商户号
  59. 'nonce_str' => md5(md5(time().randString(10))), // 32位随机字符串
  60. 'out_trade_no' => $post["out_trade_no"], // 商户订单号
  61. 'total_fee' => (int)(floatval($post["total"])*100), // 订单总金额,单位分
  62. 'spbill_create_ip' => empty($post["payer_client_ip"])?"127.0.0.1":$post["payer_client_ip"], // 终端ip
  63. 'trade_type' => "JSAPI", // 交易类型
  64. 'notify_url' => $this->config["NOTIFY_URL"], // 通知地址
  65. 'body' => empty($post["body"]) ?"微信小程序支付":$post["body"],
  66. 'attach' => empty($post["attach"])?"微信小程序支付":$post["attach"],
  67. 'openid' => $post["openid"],
  68. ];
  69. $params["sign"] = self::getSign($params);
  70. // $post_xml = $this->arrayToWeiXinXml($params);
  71. $curl_content = request($url, $post_xml);
  72. }
  73. /**
  74. * 统一下单接口
  75. */
  76. public function unifiedorder($orderNo, $total_fee, $ip, $source, $shopName, $openid = '', $attach = '')
  77. {
  78. $url = $this->apiUrl.'unifiedorder';
  79. switch ($source){
  80. case 'H5':
  81. $trade_type = 'MWEB';
  82. break;
  83. case 'APP':
  84. $trade_type = 'APP';
  85. break;
  86. default:
  87. $trade_type = 'JSAPI';
  88. break;
  89. }
  90. $params = [
  91. 'appid' => $this->appid, // 小程序ID/微信开放平台审核通过的应用APPID
  92. 'mch_id' => $this->mch_id, // 商户号
  93. 'nonce_str' => md5(md5(time().'qianniao.vip')), // 32位随机字符串
  94. 'out_trade_no' => $orderNo, // 商户订单号
  95. 'total_fee' => yuanToFen($total_fee), // 订单总金额,单位分
  96. 'spbill_create_ip' => $ip, // 终端ip
  97. 'trade_type' => $trade_type, // 交易类型
  98. 'notify_url' => $this->notifyUrl, // 通知地址
  99. 'body' => $shopName.'-'.'线上商城',
  100. 'attach' => $attach, // 附加数据
  101. ];
  102. if($trade_type == 'JSAPI'){
  103. $params['openid'] = $openid;
  104. }
  105. $params['sign'] = self::getSign($params);
  106. $post_xml = $this->arrayToWeiXinXml($params);
  107. $curl_content = request($url, $post_xml);
  108. $result = $this->commonResult($curl_content);
  109. if(!$result->isSuccess()){
  110. return ResultWrapper::fail($result->getData(), $result->getErrorCode());
  111. }
  112. $response_content = $result->getData();
  113. //获取统一下单返回的id
  114. $prepay_id = $response_content['prepay_id'];
  115. // H5支付返回的支付跳转链接
  116. $mweb_url = isset($response_content['mweb_url']) ? $response_content['mweb_url'] : '';
  117. switch ($source){
  118. case 'byteDanceH5':
  119. $payParams = $mweb_url; // 字节跳动小程序调用微信H5支付获取支付链接
  120. break;
  121. case 'H5':
  122. $payParams = self::payment($prepay_id);
  123. break;
  124. case 'APP':
  125. $payParams = self::payment($prepay_id);
  126. break;
  127. default:
  128. $payParams = self::paymentToMinprogram($prepay_id);
  129. break;
  130. }
  131. return ResultWrapper::success($payParams);
  132. }
  133. /**
  134. * 小程序调起支付用到的参数
  135. * 官方接口地址: https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=7_7&index=3
  136. */
  137. public function paymentToMinprogram($prepay_id)
  138. {
  139. unset($params);
  140. $params = [
  141. 'appId' => $this->appid, // 小程序ID
  142. 'timeStamp' => (string)time(), //时间戳
  143. 'nonceStr' => md5(md5(time().'qianniao.vip')), //32位随机字符串
  144. 'package' => 'prepay_id='.$prepay_id, // 数据包
  145. 'signType' => 'MD5', // 签名方式
  146. ];
  147. $params['paySign'] = self::getSign($params);
  148. return $params;
  149. }
  150. /**
  151. * 调起支付用到的参数
  152. * 官方接口地址: https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=9_12
  153. * @param $prepay_id
  154. */
  155. public function payment($prepay_id)
  156. {
  157. unset($parm);
  158. $parm = array(
  159. 'appid' => $this->appid,
  160. 'partnerid' => $this->mch_id,
  161. 'timestamp' => (string)time(), //时间戳
  162. 'noncestr' => md5(md5(time().'qianniao.vip')), //32位随机字符串
  163. 'prepayid' => $prepay_id, //统一下单接口返回的prepay_id 参数值
  164. 'package' => 'Sign=WXPay',
  165. );
  166. $parm['sign'] = self::getSign($parm);
  167. return $parm;
  168. }
  169. /**
  170. * 付款码支付
  171. * 官方文档地址: https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=9_10&index=1
  172. */
  173. public function micropay($orderNo, $total_fee, $ip, $shopName, $auth_code)
  174. {
  175. $url = $this->apiUrl.'micropay';
  176. $params = [
  177. 'appid' => $this->appid, // 小程序ID/微信开放平台审核通过的应用APPID
  178. 'mch_id' => $this->mch_id, // 商户号
  179. 'nonce_str' => md5(md5(time().'qianniao.vip')), // 32位随机字符串
  180. 'body' => $shopName.'-'.'线上商城',
  181. 'out_trade_no' => $orderNo, // 商户订单号
  182. 'total_fee' => yuanToFen($total_fee), // 订单总金额,单位分
  183. 'spbill_create_ip' => $ip, // 终端ip
  184. 'auth_code' => $auth_code,
  185. ];
  186. $params['sign'] = self::getSign($params);
  187. $post_xml = $this->arrayToWeiXinXml($params);
  188. $curl_content = request($url, $post_xml);
  189. if($curl_content['httpcode'] != '200'){
  190. return ResultWrapper::fail($curl_content['errorMsg'], ErrorCode::$apiNotResult);
  191. }
  192. $response_content = (array)simplexml_load_string($curl_content['content'], 'SimpleXMLElement', LIBXML_NOCDATA);
  193. if($response_content['return_code'] != 'SUCCESS'){
  194. return ResultWrapper::fail($response_content['return_msg'], ErrorCode::$weixinPayError);
  195. }
  196. if($response_content['result_code'] != 'SUCCESS' && $response_content['err_code'] != 'USERPAYING' ){
  197. return ResultWrapper::fail($response_content['err_code_des'], ErrorCode::$weixinPayError);
  198. }
  199. // 20s内查询支付状态,如果有明确错误直接返回错误,其他情况继续查询。如果成功直接返回成功
  200. for($i=1; $i<=10; $i++)
  201. {
  202. $url = $this->apiUrl.'orderquery';
  203. $params = [
  204. 'appid' => $this->appid, // 小程序ID/微信开放平台审核通过的应用APPID
  205. 'mch_id' => $this->mch_id, // 商户号
  206. 'nonce_str' => md5(md5(time().'qianniao.vip')), // 32位随机字符串
  207. 'out_trade_no' => $orderNo, // 商户订单号
  208. ];
  209. $params['sign'] = self::getSign($params);
  210. $post_xml = $this->arrayToWeiXinXml($params);
  211. $curl_content = request($url, $post_xml);
  212. if($curl_content['httpcode'] != '200'){
  213. return ResultWrapper::fail($curl_content['errorMsg'], ErrorCode::$apiNotResult);
  214. }
  215. $response_content = (array)simplexml_load_string($curl_content['content'], 'SimpleXMLElement', LIBXML_NOCDATA);
  216. if($response_content['return_code'] != 'SUCCESS'){
  217. return ResultWrapper::fail($response_content['return_msg'], ErrorCode::$weixinPayError);
  218. }
  219. if($response_content['result_code'] != 'SUCCESS' && $response_content['err_code'] == 'ORDERNOTEXIST'){
  220. return ResultWrapper::fail($response_content['err_code_des'], ErrorCode::$weixinPayError);
  221. }
  222. if($response_content['return_code'] == 'SUCCESS' && $response_content['result_code'] == 'SUCCESS' && $response_content['trade_state'] == 'SUCCESS'){
  223. return ResultWrapper::success($response_content['trade_state']);
  224. }
  225. sleep(2);
  226. }
  227. // 20s内没有支付则撤销订单
  228. for($i=1; $i<=10; $i++){
  229. $url = $this->baseUrl.'secapi/pay/reverse';
  230. $params = [
  231. 'appid' => $this->appid, // 小程序ID/微信开放平台审核通过的应用APPID
  232. 'mch_id' => $this->mch_id, // 商户号
  233. 'nonce_str' => md5(md5(time().'qianniao.vip')), // 32位随机字符串
  234. 'out_trade_no' => $orderNo, // 商户订单号
  235. ];
  236. $params['sign'] = self::getSign($params);
  237. $post_xml = $this->arrayToWeiXinXml($params);
  238. $curl_content = request($url, $post_xml, 10, false, [], true);
  239. if($curl_content['httpcode'] != '200'){
  240. return ResultWrapper::fail($curl_content['errorMsg'], ErrorCode::$apiNotResult);
  241. }
  242. $response_content = (array)simplexml_load_string($curl_content['content'], 'SimpleXMLElement', LIBXML_NOCDATA);
  243. if($response_content['return_code'] != 'SUCCESS'){
  244. return ResultWrapper::fail($response_content['return_msg'], ErrorCode::$weixinPayError);
  245. }
  246. if($response_content['result_code'] != 'SUCCESS' && $response_content['recall'] == 'N'){
  247. return ResultWrapper::fail($response_content['err_code_des'], ErrorCode::$weixinPayError);
  248. }
  249. if($response_content['return_code'] == 'SUCCESS' && $response_content['result_code'] == 'SUCCESS' && $response_content['recall'] == 'N'){
  250. return ResultWrapper::success($response_content['result_code']);
  251. }
  252. }
  253. }
  254. /**
  255. * H5查询支付状态接口
  256. */
  257. public function orderquery($orderNo)
  258. {
  259. $url = $this->apiUrl.'orderquery';
  260. $params = [
  261. 'appid' => $this->appid, // 小程序ID/微信开放平台审核通过的应用APPID
  262. 'mch_id' => $this->mch_id, // 商户号
  263. 'nonce_str' => md5(md5(time().'qianniao.vip')), // 32位随机字符串
  264. 'out_trade_no' => $orderNo, // 商户订单号
  265. ];
  266. $params['sign'] = self::getSign($params);
  267. $post_xml = $this->arrayToWeiXinXml($params);
  268. $curl_content = request($url, $post_xml);
  269. $result = $this->commonResult($curl_content);
  270. if(!$result->isSuccess()){
  271. return ResultWrapper::fail($result->getData(), $result->getErrorCode());
  272. }
  273. $response_content = $result->getData();
  274. $trade_state = $response_content['trade_state'];
  275. return ResultWrapper::success($trade_state);
  276. }
  277. /**
  278. * 申请退款API
  279. * 官方文档地址:https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=9_4
  280. */
  281. public function transfers($outerTradeNo, $out_refund_no, $refundMoney, $orderMoney, $sslData)
  282. {
  283. $url = $this->baseUrl.'secapi/pay/refund';
  284. $params = [
  285. 'appid' => $this->appid,
  286. 'mch_id' => $this->mch_id,
  287. 'nonce_str' => md5(md5(time().'qianniao.vip')), // 32位随机字符串
  288. 'transaction_id' => $outerTradeNo, // 微信交易号
  289. 'out_refund_no' => $out_refund_no, // 商户退款单号
  290. 'refund_fee' => yuanToFen($refundMoney), // 退款金额
  291. 'total_fee' => yuanToFen($orderMoney), // 订单总金额
  292. ];
  293. $params['sign'] = self::getSign($params);
  294. $post_xml = $this->arrayToWeiXinXml($params);
  295. $curl_content = request($url, $post_xml, 10, false, [], true, $sslData);
  296. $result = $this->commonResult($curl_content);
  297. if(!$result->isSuccess()){
  298. return ResultWrapper::fail($result->getData(), $result->getErrorCode());
  299. }
  300. $response_content = $result->getData();
  301. return ResultWrapper::success($response_content['refund_id']);
  302. }
  303. /**
  304. * 公共处理返回结果方法
  305. */
  306. public function commonResult($curl_content)
  307. {
  308. if($curl_content['httpcode'] == '200'){
  309. $response_content = (array)simplexml_load_string($curl_content['content'], 'SimpleXMLElement', LIBXML_NOCDATA);
  310. if($response_content['return_code'] == 'SUCCESS'){
  311. if($response_content['result_code'] == 'SUCCESS'){
  312. return ResultWrapper::success($response_content);
  313. }else{
  314. return ResultWrapper::fail($response_content['err_code_des'], ErrorCode::$weixinPayError);
  315. }
  316. }else{
  317. return ResultWrapper::fail($response_content['return_msg'], ErrorCode::$weixinPayError);
  318. }
  319. }else{
  320. return ResultWrapper::fail($curl_content['errorMsg'], ErrorCode::$apiNotResult);
  321. }
  322. }
  323. /**
  324. * 生成签名方法
  325. * 官方接口地址: https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=4_3
  326. * 官方签名测试地址: https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=20_1
  327. * @param $parm
  328. * @return string
  329. */
  330. public function getSign($parm)
  331. {
  332. //非空参数值的参数按照参数名ASCII码从小到大排序(字典序)
  333. if(ksort($parm)){
  334. $stringA = '';
  335. //使用URL键值对的格式拼接成字符串stringA
  336. foreach($parm as $key => $value){
  337. if($value == 0 || !empty($value)){
  338. $stringA .=$key.'='.$value.'&';
  339. }
  340. }
  341. }else{
  342. echo "对参数排序出错";
  343. exit();
  344. }
  345. //在stringA最后拼接上key=商户支付密钥
  346. $stringSignTemp = $stringA.'key='.$this->config["ApiV2Key"];
  347. //对stringSignTemp进行MD5运算,再将得到的字符串所有字符转换为大写
  348. $signValue = strtoupper(md5($stringSignTemp));
  349. return $signValue;
  350. }
  351. //数组转xml
  352. public function arrayToWeiXinXml($arr)
  353. {
  354. $xml = '<xml>';
  355. foreach($arr as $key => $value){
  356. if (is_string($value)) {
  357. $xml .= '<' . $key . '><![CDATA[' . $value . ']]></' . $key . '>';
  358. } else {
  359. $xml .= '<' . $key . '>' . $value . '</' . $key . '>';
  360. }
  361. }
  362. $xml .='</xml>';
  363. return $xml;
  364. }
  365. }