最近做小程序时用到了微信支付很是开心,因为之前支付一直都没有做过,终于又可以学点东西了。于是很开心的去看了下微信小程序的支付接口,没想到,事情基本都是后端做的,前端只要调用wx.requestPayment接口就可以了,于是决定自己用node来做接口测试一遍。废话不多说,先整理下支付的流程。
一、微信小程序支付(支付API传送门)
首先借用下官方的图片:
大概流程就是:前端请求后端支付接口,并把登录的code(因为是自己测试所以要把code传给后端,用来获取用户的openID)和支付的money传给后端;后端调用小程序的登录API获取用户的openID,然后调用统一下单API,把必要的参数传过去,就可以获取预付单信息(prepay_id),然后获取当前的时间搓(timeStamp),整合预付单信息(prepay_id,参数是packpage)、随机字符串(nonceStr)、支付签名(paySign),签名算法(signType,这个我没有返回,前端直接写死MD5了)一起返回给前端就可以。最后前端对后端返回的数据进行二次加密(传送门),然后调用wx.requestPayment接口,并把对应参数传给微信就可以发起支付了。
二、统一下单接口调用分解
调用预下单时,自己只传了一些必要的参数过去,其他非必填的都没有传(非必填这边有一个参数需要注意下:attach,附加参数,在查询API和支付通知中原样返回,可作为自定义参数使用。意思就是我可以在这边传一些自定义的参数,比如商品id,用户id,订单id等,这样在通知接口那边就可以知道是那个用户支付了哪个商品了,从而改变订单的状态)。参数的话,部分写死,其他的就需要自己获取。
(1)获取用户openID
function getOpenid(code,appid){ //发起请求获取用户的openID
return new Promise(function(resolve,reject){
request('https://api.weixin.qq.com/sns/jscode2session?appid=wx33ff58965464254d&secret=39afb1e055fadb6e3906c2c26ed2e9a8&js_code='+code+'&grant_type=authorization_code',function(error,response,body){
if(!error && response.statusCode == 200){
var bodyJson = JSON.parse(body)
//console.log(bodyJson,'获取openID返回信息')
resolve(bodyJson.openid);
}
})
})
}
直接调用微信的jscode2session接口就可以获取了,这里用到了promise(这个在处理异步问题时是真的好用),因为需要先获取到openID后,才能调用统一下单接口。这边遇到了个问题就是:一直报错40013,提示code无效,仔细排查后,原来是小程序的appid弄错了,写成了另外一个小程序的了。
(2)获取随机字符串(这个很简单,没什么好说的)
function randomStr(){ //产生一个32位随机字符串
var str = "";
var arr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
for(var i=1;i<=32;i++){
var random = Math.floor(Math.random()*arr.length);
str += arr[random];
}
return str;
}
(3)签名算法(传送门)
function createSign(obj){ //签名算法(把所有的非空的参数,按字典顺序组合起来+key,然后md5加密,再把加密结果都转成大写的即可)
var stringA = 'appid='+obj.appid+'&body='+obj.body+'&mch_id='+obj.mch_id+'&nonce_str='+obj.nonce_str+'¬ify_url='+obj.notify_url+'&openid='+obj.openid+'&out_trade_no='+obj.out_trade_no+'&spbill_create_ip='+obj.spbill_create_ip+'&total_fee='+obj.total_fee+'&trade_type='+obj.trade_type;
let stringSignTemp = stringA+'&key=自己的商户号秘钥';
stringSignTemp = md5(stringSignTemp);
let signValue = stringSignTemp.toUpperCase();
return signValue
}
签名算法是比较麻烦的一步,整个步骤就是:把你所有要传的非空参数,按字典顺序拼接起来得到stringA,然后加上key, (key设置路径:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置)得到stringSignTemp,然后对stringSignTemp进行md5加密,再把加密后的字符串都转换为大写即可。md5加密这边用到了md5-node模块,安装命令:cnpm install md5-node --save,然后引入模块即可使用。
(4)xml数据解析
请求统一下单接口时,所传的参数需拼接成xml数据,然后调用成功后,微信会返回一串xml数据给你,然后再把这串数据解析成json数据就可以了。xml数据解析这边用到了xml2js模块,安装与使用和上面的md5-node一样。解析代码如下:
xml2js.parseString(body,function(error,result){
console.log(JSON.stringify(result),'xml解析成惊悚字符串')
let reData = result.xml;
let responseData = {
timeStamp: new Date().getTime(),
nonceStr: reData.nonce_str[0],
package: reData.prepay_id[0],
paySign: reData.sign[0]
}
res.end(JSON.stringify(responseData))
})
完整的js代码如下:
var express = require('express');
var app = express();
var request = require('request') //引入request请求模块
var md5 = require('md5-node') //引入md5加密模块
var xml2js = require('xml2js'); //引入xml解析模块
app.get('/',function(req,res){
res.send('hello nodejs')
})
app.get('/pay',function(req,res){ //小程序微信支付
let query = req.query;
console.log(query,'获取请求参数')
let time = new Date().getTime(); //商户订单号
let nonce_str = randomStr();
let openid = null;
let total_fee = Number(query.money)*100;
let appid = ''; //自己的小程序appid
let mch_id =''; //自己的商户号id
getOpenid(query.code,appid).then(function(res1){
openid = res1;
let sign = createSign({ //签名
appid: appid,
body: '微信支付,商品详细描述',
mch_id: mch_id,
nonce_str: nonce_str,
notify_url: 'https://www.kdsou.com/kdchange/service_bak/notify.php',
openid: openid,
out_trade_no: time,
spbill_create_ip: '127.0.0.1',
total_fee: total_fee,
trade_type: 'JSAPI'
});
let reqUrl = 'https://api.mch.weixin.qq.com/pay/unifiedorder';
/*var reqData = {
appid: '', //小程序appid
mch_id: '', //商户号
nonce_str: nonce_str, //随机字符串
sign: sign, //签名
body: '微信支付,商品详细描述', //商品描述
out_trade_no: time, //商品订单号
total_fee: total_fee, //商品价格
spbill_create_ip: '127.0.0.1', //本地服务器地址
notify_url: 'https://www.kdsou.com/kdchange/service_bak/notify.php', //通知地址
trade_type: 'JSAPI', //交易类型,JSAPI为小程序交易类型
openid: openid, //交易类型是JSAPI的话,此参数必传
}*/
let formData = `<xml>
<appid>${appid}</appid>
<mch_id>${mch_id}</mch_id>
<nonce_str>${nonce_str}</nonce_str>
<sign>${sign}</sign>
<body>微信支付,商品详细描述</body>
<out_trade_no>${time}</out_trade_no>
<total_fee>${total_fee}</total_fee>
<spbill_create_ip>127.0.0.1</spbill_create_ip>
<notify_url>https://www.kdsou.com/kdchange/service_bak/notify.php</notify_url>
<trade_type>JSAPI</trade_type>
<openid>${openid}</openid>
</xml>`;
//console.log(formData,'xml格式')
//发起请求,获取微信支付的一些必要信息
request({
url: reqUrl,
method: "POST",
json: true,
headers: {
"content-type": "application/json",
},
body: formData
}, function(error, response, body) {
if (!error && response.statusCode == 200) {
console.log(body,'统一下单接口返回的数据') // 请求成功的处理逻辑
xml2js.parseString(body,function(error,result){
console.log(JSON.stringify(result),'xml解析成惊悚字符串')
let reData = result.xml;
let responseData = {
timeStamp: new Date().getTime(),
nonceStr: reData.nonce_str[0],
package: reData.prepay_id[0],
paySign: reData.sign[0]
}
res.end(JSON.stringify(responseData))
})
}
});
});
})
function getOpenid(code,appid){ //发起请求获取用户的openID
return new Promise(function(resolve,reject){
request('https://api.weixin.qq.com/sns/jscode2session?appid='+appid+'&secret=39afb1e055fadb6e3906c2c26ed2e9a8&js_code='+code+'&grant_type=authorization_code',function(error,response,body){
if(!error && response.statusCode == 200){
var bodyJson = JSON.parse(body)
//console.log(bodyJson,'获取openID返回信息')
resolve(bodyJson.openid);
}
})
})
}
function createSign(obj){ //签名算法(把所有的非空的参数,按字典顺序组合起来+key,然后md5加密,再把加密结果都转成大写的即可)
var stringA = 'appid='+obj.appid+'&body='+obj.body+'&mch_id='+obj.mch_id+'&nonce_str='+obj.nonce_str+'¬ify_url='+obj.notify_url+'&openid='+obj.openid+'&out_trade_no='+obj.out_trade_no+'&spbill_create_ip='+obj.spbill_create_ip+'&total_fee='+obj.total_fee+'&trade_type='+obj.trade_type;
var stringSignTemp = stringA+'&key=自己的商户号key';
stringSignTemp = md5(stringSignTemp);
var signValue = stringSignTemp.toUpperCase();
return signValue
}
function randomStr(){ //产生一个随机字符串
var str = "";
var arr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
for(var i=1;i<=32;i++){
var random = Math.floor(Math.random()*arr.length);
str += arr[random];
}
return str;
}
var server = app.listen(8099,function(){
var host = server.address().address;
var port = server.address().port;
console.log('应用实例,访问地址为:tttp://%s:%s',host,port)
})
前端发起的请求的代码如下:
pay(){
console.log('发起微信支付')
let money = 0.01;
wx.login({
success(res){
if(res.code){
wx.request({
url: 'http://localhost:8099/pay?code='+res.code+'&money='+money,
success(res){
console.log(res,'统一下单接口返回信息')
let appid = 'wx33ff58965464254d';
let timeStamp = res.data.timeStamp;
let nonceStr = res.data.nonceStr;
let packages = 'prepay_id='+res.data.package;
let signType = 'MD5';
let key = '商户后台设置的key';
let str = 'appId=' + appid + '&nonceStr=' + nonceStr + '&package=' + packages + '&signType=' + signType +'&timeStamp='+timeStamp+'&key='+key;
let paySign = md5.md5(str)
wx.requestPayment({
timeStamp: timeStamp+'', //时间搓
nonceStr: nonceStr, //随机字符串
package: packages, //repay_id
signType: 'MD5', //签名算法
paySign: paySign, //签名
success(res){
console.log(res,'微信支付成功!!!')
}
})
}
})
}
}
})
}
本地编辑器调试效果如下: