⼩程序⽀付及退款整体流程
最近做了⽀付及退款⼀系列操作,⽂档写的也⽐较简略,⽹上博客也并不详细,也踩了⼀些坑,在这⾥记录下。当然主要还是得根据⼩程序⽂档⼀步⼀步来。
⼀、wx.requestPayment
发起⽀付。了解更多信息,请查看
所谓的发起⽀付,指的是⽤户侧这边唤起⽀付窗⼝的api,这个api需要按规范传参数
timeStamp: '',
nonceStr: '',
package: '',
signType: 'MD5',
paySign: '',
success (res) { },
fail (res) { }
})
这些参数均需要从后台获取。那么我们进⼊“⽀付接⼝⽂档”查看是怎么个流程
⼆、⽀付具体流程
⽂档也写的很清楚,不细说,主要看下⾯这个流程
商户系统和⽀付系统主要交互:
1、⼩程序内调⽤登录接⼝,获取到⽤户的openid,api参见公共api【】
2、商户server调⽤⽀付统⼀下单,api参见公共api【】
3、商户server调⽤再次签名,api参见公共api【】
4、商户server接收⽀付通知,api参见公共api【】
5、商户server查询⽀付结果,api参见公共api【】
1、调⽤wx.login获取code,然后通过code,调取三⽅接⼝,获取openid。如果⽤户系统有openid记录,可以省略这步操作。
主要是因为下⾯的统⼀下单api⾥的参数配置:
openid参数:trade_type=JSAPI,此参数必传,⽤户在商户appid下的唯⼀标识。openid如何获取,可参考【】。
2、统⼀下单api、⼆次签名api返回参数
看⽂档⾥的参数,传那些参数,调⽤三⽅接⼝即可。⼀般不会有啥问题,主要问题也会在于2次签名。
实例代码如下
// 统⼀下单
let unifiedorder = async (params = {}, ctx) => {
let body = '......'// 商品描述
let notify_url = '....../wxPayBack'// ⽀付成功的回调地址可访问不带参数
let nonce_str = NonceStr() // 随机数
let out_trade_no = derCode // 商户订单号(⽤户系统⾃定义的商户订单号)
let total_fee = derPay * 100// 订单价格单位是分
let bodyData = '<xml>'
bodyData += `<appid>${wxConfig.AppID}</appid>` // ⼩程序ID
bodyData += `<mch_id>${wxConfig.Mch_id}</mch_id>` // 商户号
bodyData += `<body>${body}</body>` // 商品描述
bodyData += `<nonce_str>${nonce_str}</nonce_str>` // 随机字符串
bodyData += `<notify_url>${notify_url}</notify_url>` // ⽀付成功的回调地址
bodyData += `<openid>${params.openid}</openid>` // ⽤户标识(openid,JSAPI⽅式⽀付时必需传该参数)
bodyData += `<out_trade_no>${out_trade_no}</out_trade_no>` // 商户订单号
bodyData += `<spbill_create_ip>${params.ip}</spbill_create_ip>` // 终端IP
bodyData += `<total_fee>${total_fee}</total_fee>` // 总⾦额单位为分
bodyData += '<trade_type>JSAPI</trade_type>'// 交易类型⼩程序取值:JSAPI
// 签名(根据上⾯这些参数,有个签名算法,⽂档⾥也有描述)
var sign = wxConfig.paysignjsapi(
wxConfig.AppID,
body,
wxConfig.Mch_id,
nonce_str,
notify_url,
params.openid,
out_trade_no,
params.ip,
total_fee
);
bodyData += '<sign>' + sign + '</sign>'
祝福短信息bodyData += '</xml>'
// ⼩程序统⼀下单接⼝
var urlStr = 'h.weixin.qq/pay/unifiedorder'
let option={
method:'POST',
uri: urlStr,
body:bodyData
}
let result = await rp(option)
let returnValue = {}
parseString(result, function(err,result){
if (urn_code[0] == 'SUCCESS') {
returnValue.out_trade_no = out_trade_no; // 商户订单号
// ⼩程序客户端⽀付需要 nonceStr,timestamp,package,paySign 这四个参数
returnValue.timeStamp = und(new Date().getTime() / 1000) + '';
returnValue.package = 'prepay_id=' + l.prepay_id[0]; // 统⼀下单接⼝返回的 prepay_id 参数值 returnValue.paySign = wxConfig.paysignjs(
wxConfig.AppID,
returnValue.package,
'MD5',
returnValue.timeStamp
) // 签名
// emitToSocket(total_fee)
sponse.body={
success: true,
msg: '操作成功',
data: returnValue
}
} else{
returnValue.msg = urn_msg[0]
sponse.body={
success: false,
msg: '操作失败',
data: returnValue
}
}
})
}
写的⼀个⽀付的配置项
const cryptoMO = require('crypto') // MD5算法
/* 参数AppID 和 Secret */
const wxConfig = {
AppID: "......", // ⼩程序ID
Secret: "......", // ⼩程序Secret
Mch_id: "......", // 商户号
Mch_key: "......", // 商户key
// ⽣成商户订单号
getWxPayOrdrID: function(){
let myDate = new Date();
let year = FullYear();
let mouth = Month() + 1;
打印机拒绝访问let day = Date();
let hour = Hours();
let minute = Minutes();
let second = Seconds();
let msecond = Milliseconds(); //获取当前毫秒数(0-999)
if(mouth < 10){ /*⽉份⼩于10 就在前⾯加个0*/
mouth = String(String(0) + String(mouth));
}
if(day < 10){ /*⽇期⼩于10 就在前⾯加个0*/
day = String(String(0) + String(day));
}
if(hour < 10){ /*时⼩于10 就在前⾯加个0*/
hour = String(String(0) + String(hour));
}
if(minute < 10){ /*分⼩于10 就在前⾯加个0*/
minute = String(String(0) + String(minute));
}
if(second < 10){ /*秒⼩于10 就在前⾯加个0*/
second = String(String(0) + String(second));
关于描写英雄人物的成语}
if (msecond < 10) {
msecond = String(String('00') + String(second));
} else if(msecond >= 10 && msecond < 100){
msecond = String(String(0) + String(second));
}
let currentDate = String(year) + String(mouth) + String(day) + String(hour) + String(minute) + String(second) + String(msecond);
return currentDate
},
//获取随机字符串
getNonceStr(){
return Math.random().toString(36).substr(2, 15)
},
// 统⼀下单签名
paysignjsapi (appid,body,mch_id,nonce_str,notify_url,openid,out_trade_no,spbill_create_ip,total_fee) {
let ret = {
appid: appid,
body: body,
mch_id: mch_id,
nonce_str: nonce_str,
notify_url:notify_url,
openid:openid,
out_trade_no:out_trade_no,
spbill_create_ip:spbill_create_ip,
total_fee:total_fee,
trade_type: 'JSAPI'
}
let str = this.raw(ret, true)
str = str + '&key=' + wxConfig.Mch_key
let md5Str = ateHash('md5').update(str, 'utf-8').digest('hex')
md5Str = UpperCase()
return md5Str
},
raw (args, lower) {
let keys = Object.keys(args)
keys = keys.sort()
戏曲作文let newArgs = {}
keys.forEach(key => {
lower ? LowerCase()] = args[key] : newArgs[key] = args[key]
})
let str = ''
for(let k in newArgs) {
str += '&' + k + '=' + newArgs[k]
}
str = str.substr(1)
return str
},
//⼩程序⽀付签名
paysignjs (appid, nonceStr, packages, signType, timeStamp) {
let ret = {
appId: appid,
nonceStr: nonceStr,
package: packages,
signType: signType,
timeStamp: timeStamp
}
let str = this.raw(ret)
str = str + '&key=' + this.Mch_key
let md5Str = ateHash('md5').update(str, 'utf-8').digest('hex')
md5Str = UpperCase()
return md5Str
},
// 校验⽀付成功回调签名
validPayBacksign (xml) {
let ret = {}
let _paysign = xml.sign[0]
for (let key in xml) {
if (key !== 'sign' && xml[key][0]) ret[key] = xml[key][0]
}
let str = this.raw(ret, true)
str = str + '&key=' + wxConfig.Mch_key
let md5Str = ateHash('md5').update(str, 'utf-8').digest('hex')
md5Str = UpperCase()
return _paysign === md5Str
},
// 确认退款签名
refundOrderSign(appid,mch_id,nonce_str,op_user_id,out_refund_no,out_trade_no,refund_fee,total_fee) {
let ret = {
appid: appid,
mch_id: mch_id,
nonce_str: nonce_str,
op_user_id: op_user_id,
out_refund_no: out_refund_no,
out_trade_no: out_trade_no,
refund_fee: refund_fee,
total_fee: total_fee
}
let str = this.raw(ret, true)
str = str + '&key='+wxConfig.Mch_key
let md5Str = ateHash('md5').update(str, 'utf-8').digest('hex')
md5Str = UpperCase()
return md5Str
}
}
这个配置项⾥的就是raw⽅法得注意下,有个区分,有的签名是key值全⼩写,有的签名就是⽀付⼆次签名校验的时候,key值是要保持驼峰,所以加了点区分。
当时在此处确实遇到了问题,查了很多博客,解决办法都模棱两可并没有效。其实,提供了签名校验⼯具,可以将⾃⼰的参数传⼊看和⽣成的是否⼀致,然后就可以单步调试看是哪⾥出了问题,⽐较⽅便快捷。
从上⾯代码也可以看出流程:
根据⽂档需要传的参数 —— ⽣成下单签名 —— 签名与参数⼀起传⼊ —— 调⽤统⼀下单api —— 返回下单接⼝的XML —— 解析XML返回数据参数,再次⽣成签名 —— 数据返回前台供 wx.requestPayment() 调⽤
⾄此⽀付就可以正常唤起窗⼝付款了。但是还有个重要的问题,就是下单成功通知。也就是下统⼀下单⾥传⼊的 notify_url:⽀付成功回答地址
3、⽀付成功结果通知
我们需要提供⼀个接⼝供⽀付成功回调:'POST /order/wxPayBack':wxPayBack, // ⽀付成功回调
const parseString = require('xml2js').parseString // xml转js对象
let wxPayBack = async (ctx, next) => {
console.log('wxPayBack', quest.body) // 我们可以打印看下返回的xml长啥样
quest.body, function (err, result) {
l, ctx)
})
}
let payBack = async (xml, ctx) => {
if (urn_code[0] == 'SUCCESS') {
let out_trade_no = xml.out_trade_no[0] // 商户订单号
let total_free = al_fee[0] // 付款总价
console.log('订单:', out_trade_no, '价格:', total_free)
if (wxConfig.validPayBacksign(xml)) {
let out_order = derInfo.find({
where: {
orderCode: out_trade_no
}
})
if (out_order && (derPay * 100) - total_free === 0 && derState === 1) {
derInfo.update({ orderState: 2 }, {
where: {
orderCode: out_trade_no
}
})
// emitToSocket(total_fee)
sponse.body = `<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml> ` }
}
}
御小说sponse.body = `<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[参数错误]]></return_msg></xml> ` }
wxConfig.validPayBacksign(xml),这⾥⼀定要校验下⽀付成功的回调签名。校验规则就是返回的xml⾥除了 sign 不放⼊参数校验外,其他的均要拿出 key - value 值进⾏⽣产 md5 加密,然后与返回的 sign 值⽐对即可。
校验成功之后,修改订单表对应数据的状态即可。
4、主动查询订单状态
如何做红烧肉 有时候回调通知异常有误,⽂档也有说明,所以最好需要主动查询⼀下订单⽀付状态,也⽐较简单。代码如下:
// 查询⽀付交易订单状态
let orderquery = async (ctx, next) => {
let { orderCode } = quest.query
let nonce_str = NonceStr()
let bodyData = '<xml>';
bodyData += '<appid>' + wxConfig.AppID + '</appid>';
bodyData += '<mch_id>' + wxConfig.Mch_id + '</mch_id>';
bodyData += '<out_trade_no>' + orderCode + '</out_trade_no>';
bodyData += '<nonce_str>' + nonce_str + '</nonce_str>';
// 签名
let sign = derquerySign(
wxConfig.AppID,
wxConfig.Mch_id,
orderCode,
nonce_str
)
bodyData += '<sign>' + sign + '</sign>'
bodyData += '</xml>'
// ⼩程序⽀付查询接⼝
var urlStr = 'h.weixin.qq/pay/orderquery'
let option={
method:'POST',
uri: urlStr,
body:bodyData
}
let result = await rp(option)
parseString(result, function(err,result){
if (ade_state[0] == 'SUCCESS') {
where: {
orderCode: orderCode
}
})
sponse.body={
success: true,
msg: '交易成功'
}
} else{
sponse.body={
success: false,
msg: '交易失败',
data: ade_state[0]
}
}
})
}
// 查询⽀付结果签名
orderquerySign(appid, mch_id, orderCode, nonce_str) {
let ret = {
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论