微信小程序支付及退款整体流程 您所在的位置:网站首页 微信付款了怎么退款 微信小程序支付及退款整体流程

微信小程序支付及退款整体流程

2024-05-09 09:20| 来源: 网络整理| 查看: 265

最近做了微信支付及退款一系列操作,微信文档写的也比较简略,网上博客也并不详细,也踩了一些坑,在这里记录下。当然主要还是得根据微信小程序文档一步一步来。

一、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 = '' bodyData += `${wxConfig.AppID}` // 小程序ID bodyData += `${wxConfig.Mch_id}` // 商户号 bodyData += `${body}` // 商品描述 bodyData += `${nonce_str}` // 随机字符串 bodyData += `${notify_url}` // 支付成功的回调地址 bodyData += `${params.openid}` // 用户标识(openid,JSAPI方式支付时必需传该参数) bodyData += `${out_trade_no}` // 商户订单号 bodyData += `${params.ip}` // 终端IP bodyData += `${total_fee}` // 总金额 单位为分 bodyData += 'JSAPI' // 交易类型 小程序取值: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 + '' bodyData += '' // 微信小程序统一下单接口 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 = ` ` } } } return ctx.response.body = ` ` }

  

wxConfig.validPayBacksign(xml),这里一定要校验下支付成功的回调签名。校验规则就是微信返回的xml里除了 sign 不放入参数校验外,其他的均要拿出 key - value 值进行生产 md5 加密,然后与微信返回的 sign 值比对即可。

  校验成功之后,修改订单表对应数据的状态即可。

三、申请退款和确认退款

  申请退款其实没什么说的,就是用户侧申请退款,然后更改用户侧订单的状态,主要说一下商家确认退款给买家的流程。

   申请退款的微信文档

   特别需要注意的是:请求需要双向证书。 详见证书使用

   进入证书使用链接,去查看关于“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 = ''; bodyData += '' + wxConfig.AppID + ''; bodyData += '' + wxConfig.Mch_id + ''; bodyData += '' + nonce_str + ''; bodyData += '' + wxConfig.Mch_id + ''; bodyData += '' + nonce_str + ''; bodyData += '' + out_trade_no + ''; bodyData += '' + total_fee + ''; bodyData += '' + 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 + '' bodyData += '' 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,我们用的是同一个随机串。

  然后经过校验之后,获取证书内容 及 商户号,作为参数传给微信提供的申请退款接口接口。返回退款成功之后,做自己用户侧的相关业务处理即可。



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有