微信小程序支付(PHP后台)“支付统一下单“接口的坑

按照微信的文档来看确实流程是什么样的,但某些数据却神一般的缺少说明,硬生生调了一天才知道完整的使用数据。

官方的流程图是这样子的

微信小程序支付(PHP后台)“支付统一下单“接口的坑

小程序提交订单后就需要后台请求两次API,一次为获取openid(某文档说是在小程序内获取不安全,所以丢给后台来获取),后面一次为获取prepay_id。最后那次“推送支付结果”是用到回调地址"<notify_url>https://xxx.com</notify_url>",好像强制地址要求https,但微信小程序付款成功也有对应调用方法,所以放弃回调服务端的方法改订单状态,主要原因是没钱买证书。。。。

获取openId

官方文档里有说明,凭js_code获取openid,appid和secret(小程序**)这个在微信公众平台的小程序的管理中设置。

微信小程序支付(PHP后台)“支付统一下单“接口的坑

$data=file_get_contents('https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code');
$openId=json_decode($data)->openid;

设置xml的提交数据

$appAttr = array();
$appAttr['appid'] = '小程序ID';
$appAttr['attach'] = 'msg';
$appAttr['body'] = 'order';
$appAttr['detail'] = '{"goods_detail":[{"goods_id":"iphone6s_32G","wxpay_goods_id":"1002","goods_name":"iPhone6s 32G","quantity":1,"price":608800,"goods_category":"123789","body":"苹果手机"}]}';
$appAttr['mch_id'] = '商户ID';
$appAttr['nonce_str'] = md5(time());
$appAttr['notify_url'] = 'http://xxxx.com';
$appAttr['openid'] = $openId;
$appAttr['out_trade_no'] = '商户订单号';
$appAttr['spbill_create_ip'] = '127.0.0.1';
$appAttr['total_fee'] = 1;//单位分
$appAttr['trade_type'] = 'JSAPI';

签名(sign),官方的文档要求“ASCII字典“排序,所以要提交的数据都要加进去。

//推荐上面有数据按官方的“ASCII字典”排序的设键放置数据
$signStr = '';
$appAttrKeys = array_keys($appAttr);
foreach ($appAttrKeys as $appAt) {
    $signStr .= $appAt . '=' . $appAttr[$appAt].'&';
}
//key设置路径:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->**设置
$signStr=$signStr.'key=xxxxxxx';//key不参与ASCII字典排序,放置在最后
$appAttr['sign'] = strtoupper(md5($signStr));

生成xml数据:

$xmlDom = new DOMDocument();
$xmlDomRoot = $xmlDom->createElement('xml');
$xmlDom->appendChild($xmlDomRoot);
$childDomAttr = ['appid', 'attach','body', 'mch_id', 'detail', 'notify_url','nonce_str', 'openid', 'out_trade_no', 'spbill_create_ip', 'total_fee', 'trade_type', 'sign'];
foreach ($childDomAttr as $child) {
    $nowChild = $xmlDom->createElement($child);
    /*[!CDATA[]]特殊处理*/
    if($child=='detail'){
        $childText = $xmlDom->createCDATASection('<![CDATA[' .$appAttr[$child]. ']]>');
    }else{
        $childText = $xmlDom->createTextNode($appAttr[$child]);
    }
    $nowChild->appendChild($childText);
    $xmlDomRoot->appendChild($nowChild);
}
$xmlStr=$xmlDom->saveHTML();

 如果用saveXML(),生成xml的就会包含开头的<?xml version="1.0"?>的单标签。我看到的微信的文档的示例数据xml标签是包含提交的数据标签的,所以简单点用了saveHTML(),导出来的数据就是理想的结构。(没提交使用过saveXML导出的数据)

导出的中文会进行html编码,所以存在中文的需要做处理,这可能就是没用字符串拼接大法的不好之处吧,所以我直接sign之前的数据都不用中文,(主要是懒)。

调用“支付统一接口”

$wechatPayCurl = curl_init();
curl_setopt($wechatPayCurl, CURLOPT_URL, 'https://api.mch.weixin.qq.com/pay/unifiedorder');
curl_setopt($wechatPayCurl, CURLOPT_POST, 1);
curl_setopt($wechatPayCurl, CURLOPT_HTTPHEADER, ["Content-type: text/xml"]);
curl_setopt($wechatPayCurl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($wechatPayCurl, CURLOPT_POSTFIELDS, $xmlStr);
curl_setopt($wechatPayCurl, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($wechatPayCurl, CURLOPT_SSL_VERIFYHOST, false);
$payData=curl_exec($wechatPayCurl);
curl_close($wechatPayCurl);

使用curl进行提交,模拟POST,把xml数据按文件二进制形式提交

保证了前面sign值正确,微信的后台服务就会返回类似这样的数据。如果说签名错误,去官方的签名校验工具,用XML的数据校验出md5之前的sign文本数据,文本对比一下就知道那里有错了。

<xml>
   <return_code><![CDATA[SUCCESS]]></return_code>
   <return_msg><![CDATA[OK]]></return_msg>
   <appid><![CDATA[wx2421b1c4370ec43b]]></appid>
   <mch_id><![CDATA[10000100]]></mch_id>
   <nonce_str><![CDATA[IITRi8Iabbblz1Jc]]></nonce_str>
   <openid><![CDATA[oUpF8uMuAJO_M2pxb1Q9zNjWeS6o]]></openid>
   <sign><![CDATA[7921E432F65EB8ED0CE9755F0E86D72F]]></sign>
   <result_code><![CDATA[SUCCESS]]></result_code>
   <prepay_id><![CDATA[wx201411101639507cbf6ffd8b0779950874]]></prepay_id>
   <trade_type><![CDATA[JSAPI]]></trade_type>
</xml>

再用DOMdocument的loadXML($payData)的方法解析得到结果,实际这一步的接口就是为拿到"prepay_id"这个值。

$resultData=array();
$resultData['appId']=appId;
$resultData['nonceStr']=$loadXml->getElementsByTagName('nonce_str')->item(0)->nodeValue;
$resultData['package']='prepay_id='.$loadXml->getElementsByTagName('prepay_id')->item(0)->nodeValue;
$resultData['signType']='MD5';
$resultData['timeStamp']=(string)time();
$resultKeys=array_keys($resultData);
$signStr = '';
foreach ($resultKeys as $rk) {
    $signStr .= $rk . '=' . $resultData[$rk].'&';
}
$signStr.='key='.key;
$resultData['paySign']=strtoupper(md5($signStr));

这是给小程序那边的"package"用的,注意"package"的值为package="prepay_id=wx201411101639507cbf6ffd8b0779950874",不然小程序那边调起支付出现“JSAPI缺少total_fee参数”。所以直接在这边拼接好。

流程图里返回“五个参数和sign”给小程序的五个参数是指"appId","nonceStr","package","sigType","timeStamp"和使用ASII字典排序这五个参数再md5得到的"paySign"。而小程序对应的除appId不填,requestPayment()里其他参数都填进去。

wx.requestPayment(
{
'timeStamp': res.timeStamp,
'nonceStr': res.nonceStr,
'package': res.package,
'signType': res.signType,
'paySign': res.paySign,
'success':function(res){
    //支付成功
},
'fail':function(res){},
'complete':function(res){}
})