导语
小程序支持微信支付,前提条件是小程序账号的主体不能为个人,并且已经开通了商户号,商户在完成签约后,需要确认当前商户号同appid的绑定关系,方可使用。
在前置准备做好了之后,我们先看下小程序是如何调起微信支付的,官方文档
wx.requestPayment(Object object)
发起微信支付。了解更多信息,请查看微信支付接口文档
参数
Object object
属性 | 类型 | 默认值 | 必填 | 说明 |
---|---|---|---|---|
timeStamp | string | 是 | 时间戳,从 1970 年 1 月 1 日 00:00:00 至今的秒数,即当前的时间 | |
nonceStr | string | 是 | 随机字符串,长度为32个字符以下 | |
package | string | 是 | 统一下单接口返回的 prepay_id 参数值,提交格式如:prepay_id=*** | |
signType | string | MD5 | 否 | 签名算法 |
paySign | string | 是 | 签名,具体签名方案参见 小程序支付接口文档 | |
success | function | 否 | 接口调用成功的回调函数 | |
fail | function | 否 | 接口调用失败的回调函数 | |
complete | function | 否 | 接口调用结束的回调函数(调用成功、失败都会执行) |
我们可以看到调起小程序支付只需要传入五个参数,便可调起支付,而这些参数需要从服务端请求获取,所以支付主要的工作都在服务端。
上图是官方给出的交互图,简单的可以概况为几个步骤
1.小程序触发生成订单事件,携带用户标识(在项目中我使用的是token)到服务端
2.服务端调用微信平台支付统一下单api,微信后台会返回预付单信息(prepay_id)
3.将返回的预付单信息进行组合以及再次签名,将5个参数和sign返回给微信小程序
4.小程序通过服务端返回的参数,即可直接调起微信支付,支付的结果会直接返回给小程序
5.支付成功后,微信后台会像服务器推送支付结果,服务器修改订单状态
以上五个步骤就是微信小程序的完整支付流程,下面开始通过node来实现小程序支付的服务端代码编写
一、调用微信支付统一下单API
我们直接上代码,具体可以去看微信的文档
//生成随机字符串的npm包
const stringRandom = require('string-random')
//node加密相关
const crypto = require('crypto');
//解析和生成xml文件的npm包
const Xml2js = require('xml2js');
/*方法接收2个参数,out_trade_no(服务器生成的订单id,需小于32位)
openid(当支付类型是JSAPI时需要传入,用户的openid)
total_fee(订单金额,单位为分,我们先设置默认值为1,记得改回来!)
*/
unifiedorder(out_trade_no,openid,total_fee=1) {
// 先将需要发送的参数创建好,然后根据参数名ASCII码从小到大排序(字典序)
let order = {
appid: config.getItem('wx.appId'),
body: "一乎小程序-发布任务支付赏金",
mch_id:config.getItem('wx.merchantId'),
// 生成随机字符串,长度32位以内,我们使用stringRandom库生成16位随机数
nonce_str: stringRandom(16),
notify_url: config.getItem('siteDomain')+'/v1/task/pay/result',
openid,
out_trade_no:out_trade_no,
spbill_create_ip: config.getItem('spbill_create_ip'),
total_fee,
trade_type: "JSAPI",
}
// 将参数对象转为key=value模式的字符串,用&分隔
let stringA = obj2String(order)
// 将生成的字符串末尾拼接上API密钥(key设置路径:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置)
let stringSignTemp = stringA + `&key=${config.getItem('wx.merchantKey')}`
// 通过HMAC-SHA256或者MD5生成sign签名,这里我们使用md5,然后将签名加入参数对象内
let md5 = crypto.createHash('md5')
md5.update(stringSignTemp);
order.sign = md5.digest('hex').toUpperCase()
// 将参数对象专为xml格式
const builder = new Xml2js .Builder();
const xml = builder.buildObject(order);
// 发送请求
/axios.post("https://api.mch.weixin.qq.com/pay/unifiedorder",xml)
// 由于微信服务器返回的data格式是xml,所以这里我们需要转成object
const parser = new Xml2js.Parser();
const xmlObj =await parser.parseStringPromise(result.data)
if(xmlObj.xml.return_code[0]==='FAIL'){
throw new Failed({
msg:`支付失败,${xmlObj.xml.return_msg[0]}`,
})
}
if(xmlObj.xml.result_code&&xmlObj.xml.result_code[0]==='SUCCESS'){
let payData= {
appId:xmlObj.xml.appid[0],
nonceStr:xmlObj.xml.nonce_str[0],
package:`prepay_id=${xmlObj.xml.prepay_id[0]}`,
signType:"MD5",
timeStamp:new Date().getTime().toString(),
key:config.getItem('wx.merchantKey')
}
const StringPay = obj2String(payData)
let payMd5 = crypto.createHash('md5')
payMd5.update(StringPay);
let paySign= payMd5.digest('hex').toUpperCase();
payData.paySign = paySign;
delete payData.key;
//前面已经将需要的字段拼接好,将对象从方法返回,服务端可以将对象直接传回给小程序客户端
return payData
}else{
throw new Failed({
msg:`支付失败,${xmlObj.xml.err_code[0]}:${xmlObj.xml.err_code_des[0]}`,
})
}
}
总结下遇到的几点问题
- 生成签名的时候一定要将object的属性按照ASCII码从小到大排序
- 返回给小程序的时间戳timeStamp,需为毫秒数,并且要是字符串格式
- 可能会遇到签名错误的报错,这时候要耐心的去看代码哪里出了问题,商户平台的key是手动填写的32位随机生成字符串
现在统一下单已经调用成功,我们也拿到了小程序调起支付的几个参数,我们先去小程序测试下能不能支付
二、小程序调起支付
//触发生成订单事件
function (){
//。。。省略部分代码
/*
res.data的格式如下
res.data={
appId: "wx14dcf5e8a4179d42"
nonceStr: "TuXBgZrQ1JNWuLiC"
package: "prepay_id=wx***********"
paySign: "***********"
signType: "MD5"
timeStamp: "1582709049872"
}
*/
orderModel.createOrder()
.then(res=>{
if(res.error_code==0){
wx.requestPayment({
...res.data,
success:re=>{
//成功支付后执行
console.log(re)
},
fail:(err)=>{
//取消支付或者支付失败后执行
console.log(err)
}
})
}
})
}
没意外的话我们已经成功调起微信支付了,如果有错误微信的报错还是挺清晰的,我们可以根据稳定一步一步去修改
接下来别忘了去接收支付成功后微信返回的支付结果
三、接收微信推送支付通知
这里接收微信推送的url是第一步调用微信支付统一下单API的notify_url字段,需要能访问
根据文档可知,微信服务器返回的数据是xml格式的,而koa-bodyparser无法解析xml,我们需要引入koa-xml-body中间件来解析xml
这里需要重点说明下,koa-xml-body官方文档上说明和koa-bodyparser是可以兼容使用的,但是引入中间件的时候,得先引用koa-xml-body再引入koa-bodyparser
const KoaBodyParser = require('koa-bodyparser');
const KoaXmlBody = require('koa-xml-body');
//注意先引入koa-xml-body
app.use(KoaXmlBody());
app.use(KoaBodyParser());
引入koa-xml-body就可以在ctx.request.body.xml里获得解析好的xml
router.post('/pay/result',async (ctx)=>{
const xml = ctx.request.body.xml;
const successXml= ‘ ’;
// 这里进行签名和校验返回xml数据的真实性,以防恶意调用接口
//校验过程省略...
if (returnCode === 'SUCCESS' && xml.result_code[0] === 'SUCCESS') {
// 根据自己的业务需求支付成功后的操作
//......
//返回xml告诉微信已经收到,并且不会再重新调用此接口
ctx.body = successXml
}
在收到微信推送支付的成功通知后,便可以根据业务需求去修改自己订单的状态,一个微信支付完整流程也完成了
有什么不对的地方或者疑问,欢迎在评论指出