当前位置:网站首页>微信小程序支付及退款整体流程
微信小程序支付及退款整体流程
2022-08-09 11:44:00 【火兰】
最近做了微信支付及退款一系列操作,微信文档写的也比较简略,网上博客也并不详细,也踩了一些坑,在这里记录下。当然主要还是得根据微信小程序文档一步一步来。
一、wx.requestPayment
发起微信支付。了解更多信息,请查看微信支付接口文档
所谓的发起微信支付,指的是用户侧这边唤起微信支付窗口的api,这个api需要按规范传参数
wx.requestPayment({
timeStamp: '',
nonceStr: '',
package: '',
signType: 'MD5',
paySign: '',
success (res) { },
fail (res) { }
})
这些参数均需要从后台获取。那么我们进入“微信支付接口文档”查看是怎么个流程
二、微信支付具体流程
文档也写的很清楚,不细说,主要看下面这个流程
商户系统和微信支付系统主要交互:
1、小程序内调用登录接口,获取到用户的openid,api参见公共api【小程序登录API】
2、商户server调用支付统一下单,api参见公共api【统一下单API】
3、商户server调用再次签名,api参见公共api【再次签名】
4、商户server接收支付通知,api参见公共api【支付结果通知API】
5、商户server查询支付结果,api参见公共api【查询订单API】
1、调用wx.login获取code,然后通过code,调取微信三方接口,获取openid。如果用户系统有openid记录,可以省略这步操作。
主要是因为下面的统一下单api里的参数配置:
openid参数:trade_type=JSAPI,此参数必传,用户在商户appid下的唯一标识。openid如何获取,可参考【获取openid】。
2、统一下单api、二次签名api返回参数
看文档里的参数,传那些参数,调用微信三方接口即可。一般不会有啥问题,主要问题也会在于2次签名。
实例代码如下
// 统一下单
let unifiedorder = async (params = {}, ctx) => {
let body = '......' // 商品描述
let notify_url = 'https://....../wxPayBack' // 支付成功的回调地址 可访问 不带参数
let nonce_str = wxConfig.getNonceStr() // 随机数
let out_trade_no = params.orderCode // 商户订单号(用户系统自定义的商户订单号)
let total_fee = ctx.request.body.orderPay * 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 = 'https://api.mch.weixin.qq.com/pay/unifiedorder'
let option={
method:'POST',
uri: urlStr,
body:bodyData
}
let result = await rp(option)
let returnValue = {}
parseString(result, function(err,result){
if (result.xml.return_code[0] == 'SUCCESS') {
returnValue.out_trade_no = out_trade_no; // 商户订单号
// 小程序 客户端支付需要 nonceStr,timestamp,package,paySign 这四个参数
returnValue.nonceStr = result.xml.nonce_str[0]; // 随机字符串
returnValue.timeStamp = Math.round(new Date().getTime() / 1000) + '';
returnValue.package = 'prepay_id=' + result.xml.prepay_id[0]; // 统一下单接口返回的 prepay_id 参数值
returnValue.paySign = wxConfig.paysignjs(
wxConfig.AppID,
returnValue.nonceStr,
returnValue.package,
'MD5',
returnValue.timeStamp
) // 签名
// emitToSocket(total_fee)
return ctx.response.body={
success: true,
msg: '操作成功',
data: returnValue
}
} else{
returnValue.msg = result.xml.return_msg[0]
return ctx.response.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 = myDate.getFullYear();
let mouth = myDate.getMonth() + 1;
let day = myDate.getDate();
let hour = myDate.getHours();
let minute = myDate.getMinutes();
let second = myDate.getSeconds();
let msecond = myDate.getMilliseconds(); //获取当前毫秒数(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 = cryptoMO.createHash('md5').update(str, 'utf-8').digest('hex')
md5Str = md5Str.toUpperCase()
return md5Str
},
raw (args, lower) {
let keys = Object.keys(args)
keys = keys.sort()
let newArgs = {}
keys.forEach(key => {
lower ? newArgs[key.toLowerCase()] = 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 = cryptoMO.createHash('md5').update(str, 'utf-8').digest('hex')
md5Str = md5Str.toUpperCase()
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 = cryptoMO.createHash('md5').update(str, 'utf-8').digest('hex')
md5Str = md5Str.toUpperCase()
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 = cryptoMO.createHash('md5').update(str, 'utf-8').digest('hex')
md5Str = md5Str.toUpperCase()
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', ctx.request.body) // 我们可以打印看下微信返回的xml长啥样
parseString(ctx.request.body, function (err, result) {
payBack(result.xml, ctx)
})
}
let payBack = async (xml, ctx) => {
if (xml.return_code[0] == 'SUCCESS') {
let out_trade_no = xml.out_trade_no[0] // 商户订单号
let total_free = xml.total_fee[0] // 付款总价
console.log('订单:', out_trade_no, '价格:', total_free)
if (wxConfig.validPayBacksign(xml)) {
let out_order = await model.orderInfo.find({
where: {
orderCode: out_trade_no
}
})
if (out_order && (out_order.orderPay * 100) - total_free === 0 && out_order.orderState === 1) {
await model.orderInfo.update({ orderState: 2 }, {
where: {
orderCode: out_trade_no
}
})
// emitToSocket(total_fee)
return ctx.response.body = `<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml> `
}
}
}
return ctx.response.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 } = ctx.request.query
let nonce_str = wxConfig.getNonceStr()
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 = wxConfig.orderquerySign(
wxConfig.AppID,
wxConfig.Mch_id,
orderCode,
nonce_str
)
bodyData += '<sign>' + sign + '</sign>'
bodyData += '</xml>'
// 微信小程序支付查询接口
var urlStr = 'https://api.mch.weixin.qq.com/pay/orderquery'
let option={
method:'POST',
uri: urlStr,
body:bodyData
}
let result = await rp(option)
parseString(result, function(err,result){
if (result.xml.trade_state[0] == 'SUCCESS') {
model.orderInfo.update({ orderState: 2 }, {
where: {
orderCode: orderCode
}
})
return ctx.response.body={
success: true,
msg: '交易成功'
}
} else{
return ctx.response.body={
success: false,
msg: '交易失败',
data: result.xml.trade_state[0]
}
}
})
}
// 查询支付结果签名
orderquerySign(appid, mch_id, orderCode, nonce_str) {
let ret = {
appid: appid,
mch_id: mch_id,
out_trade_no: orderCode,
nonce_str: nonce_str
}
let str = this.raw(ret, true)
str = str + '&key='+wxConfig.Mch_key
let md5Str = cryptoMO.createHash('md5').update(str, 'utf-8').digest('hex')
md5Str = md5Str.toUpperCase()
return md5Str
}
三、申请退款和确认退款
申请退款其实没什么说的,就是用户侧申请退款,然后更改用户侧订单的状态,主要说一下商家确认退款给买家的流程。
特别需要注意的是:请求需要双向证书。 详见证书使用
进入证书使用链接,去查看关于“3、API证书”相关的使用东西。也就是说需要从商户号那边下载一些证书,放在工程里,再调用微信三方提供的退款接口:https://api.mch.weixin.qq.com/secapi/pay/refund 时,需要校该证书,以确保安全。
实例代码:
// 确认退款
let confirmRefund = async (ctx, next) => {
let _body = ctx.request.body
let out_trade_no = _body.orderCode // 商户订单号
let nonce_str = wxConfig.getNonceStr()
let total_fee = _body.orderPay * 100 // 订单价格 单位是 分
let refund_fee = _body.orderPay * 100
let bodyData = '<xml>';
bodyData += '<appid>' + wxConfig.AppID + '</appid>';
bodyData += '<mch_id>' + wxConfig.Mch_id + '</mch_id>';
bodyData += '<nonce_str>' + nonce_str + '</nonce_str>';
bodyData += '<op_user_id>' + wxConfig.Mch_id + '</op_user_id>';
bodyData += '<out_refund_no>' + nonce_str + '</out_refund_no>';
bodyData += '<out_trade_no>' + out_trade_no + '</out_trade_no>';
bodyData += '<total_fee>' + total_fee + '</total_fee>';
bodyData += '<refund_fee>' + refund_fee + '</refund_fee>';
// 签名
let sign = wxConfig.refundOrderSign(
wxConfig.AppID,
wxConfig.Mch_id,
nonce_str,
wxConfig.Mch_id,
nonce_str, // 商户退款单号 给一个随机字符串即可out_refund_no
out_trade_no,
refund_fee,
total_fee
)
bodyData += '<sign>' + sign + '</sign>'
bodyData += '</xml>'
let agentOptions = {
pfx: fs.readFileSync(path.join(__dirname,'/wx_pay/apiclient_cert.p12')),
passphrase: wxConfig.Mch_id,
}
// 微信小程序退款接口
let urlStr = 'https://api.mch.weixin.qq.com/secapi/pay/refund'
let option={
method:'POST',
uri: urlStr,
body: bodyData,
agentOptions: agentOptions
}
let result = await rp(option)
parseString(result, function(err, result){
if (result.xml.result_code[0] == 'SUCCESS') {
refundBack(_body.id)
return ctx.response.body={
success: true,
msg: '操作成功'
}
} else{
return ctx.response.body={
success: false,
msg: result.xml.err_code_des[0]
}
}
})
}
let refundBack = async (orderId) => {
model.orderInfo.update({ orderState: 8 }, {
where: { id: orderId }
})
let orderfoods = await model.foodsOrder.findAll({
where: { orderId: orderId }
})
orderfoods.forEach(food => {
dealFood(food, 'plus')
})
}
可以看到:随机字符串 nonce_str,商户退款单号 out_refund_no,我们用的是同一个随机串。
然后经过校验之后,获取证书内容 及 商户号,作为参数传给微信提供的申请退款接口接口。返回退款成功之后,做自己用户侧的相关业务处理即可。
边栏推荐
猜你喜欢
matlab simulink的scope 示波器光标如何移动记录
Fapi_StatusType Fapi_issueProgrammingCommand使用注意事项
Senior told me that the giant MySQL is through SSH connection
enum in c language
C# Get system installed .NET version
PTA 实验7-5 输出大写英文字母(10 分)
BISS绝对值编码器_TI方案_线路延迟补偿
x86 exception handling and interrupt mechanism (2) interrupt vector table
TIC2000系列处理器在线升级
Semaphore SIGCHLD use, how to make the parent that the child performs over, how to make the distinction between multiple child processes. The end
随机推荐
LeetCode_单调栈_中等_456.132 模式
F280049库函数API编程、直接寄存器控制编程和混合编程方法
【面试高频题】可逐步优化的链表高频题
es6递归函数
ThreadLocal类
BeanFacroty和FactoryBean到底是什么?AppliacationContext它又是什么?
goalng-sync/atomic原子操作
PAT1004
wpf path xaml写法和c#写法对比
redis内存的淘汰机制
ClickHouse物化视图(八)
【VQA survey】视觉问答中的语言学问题
mysql + redis + flask + flask-sqlalchemy + flask-session 配置及项目打包移植部署
The use of C language typedef 】 : structure, basic data types, and array
LeetCode 1413.逐步求和得到正数的最小值
富媒体在客服IM消息通信中的秒发实践
Django cannot link mysql database
二重指针-char **、int **的作用
OpenSSF的开源软件风险评估工具:Scorecards
软件测试——金融测试类面试题,看完直接去面试了