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


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

    一、wx.requestPayment

      发起微信支付。了解更多信息,请查看微信支付接口文档

      所谓的发起微信支付,指的是用户侧这边唤起微信支付窗口的api,这个api需要按规范传参数

    1. wx.requestPayment({
    2. timeStamp: '',
    3. nonceStr: '',
    4. package: '',
    5. signType: 'MD5',
    6. paySign: '',
    7. success (res) { },
    8. fail (res) { }
    9. })

      这些参数均需要从后台获取。那么我们进入“微信支付接口文档”查看是怎么个流程

    二、微信支付具体流程

      文档也写的很清楚,不细说,主要看下面这个流程

    商户系统和微信支付系统主要交互:

    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次签名。

      实例代码如下

    1. // 统一下单
    2. let unifiedorder = async (params = {}, ctx) => {
    3. let body = '......' // 商品描述
    4. let notify_url = 'https://....../wxPayBack' // 支付成功的回调地址 可访问 不带参数
    5. let nonce_str = wxConfig.getNonceStr() // 随机数
    6. let out_trade_no = params.orderCode // 商户订单号(用户系统自定义的商户订单号)
    7. let total_fee = ctx.request.body.orderPay * 100 // 订单价格 单位是 分
    8. let bodyData = ''
    9. bodyData += `${wxConfig.AppID}` // 小程序ID
    10. bodyData += `${wxConfig.Mch_id}` // 商户号
    11. bodyData += `${body}` // 商品描述
    12. bodyData += `${nonce_str}` // 随机字符串
    13. bodyData += `${notify_url}` // 支付成功的回调地址
    14. bodyData += `${params.openid}` // 用户标识(openid,JSAPI方式支付时必需传该参数)
    15. bodyData += `${out_trade_no}` // 商户订单号
    16. bodyData += `${params.ip}` // 终端IP
    17. bodyData += `${total_fee}` // 总金额 单位为分
    18. bodyData += 'JSAPI' // 交易类型 小程序取值:JSAPI
    19. // 签名(根据上面这些参数,有个签名算法,文档里也有描述)
    20. var sign = wxConfig.paysignjsapi(
    21. wxConfig.AppID,
    22. body,
    23. wxConfig.Mch_id,
    24. nonce_str,
    25. notify_url,
    26. params.openid,
    27. out_trade_no,
    28. params.ip,
    29. total_fee
    30. );
    31. bodyData += '' + sign + ''
    32. bodyData += ''
    33. // 微信小程序统一下单接口
    34. var urlStr = 'https://api.mch.weixin.qq.com/pay/unifiedorder'
    35. let option={
    36. method:'POST',
    37. uri: urlStr,
    38. body:bodyData
    39. }
    40. let result = await rp(option)
    41. let returnValue = {}
    42. parseString(result, function(err,result){
    43. if (result.xml.return_code[0] == 'SUCCESS') {
    44. returnValue.out_trade_no = out_trade_no; // 商户订单号
    45. // 小程序 客户端支付需要 nonceStr,timestamp,package,paySign 这四个参数
    46. returnValue.nonceStr = result.xml.nonce_str[0]; // 随机字符串
    47. returnValue.timeStamp = Math.round(new Date().getTime() / 1000) + '';
    48. returnValue.package = 'prepay_id=' + result.xml.prepay_id[0]; // 统一下单接口返回的 prepay_id 参数值
    49. returnValue.paySign = wxConfig.paysignjs(
    50. wxConfig.AppID,
    51. returnValue.nonceStr,
    52. returnValue.package,
    53. 'MD5',
    54. returnValue.timeStamp
    55. ) // 签名
    56. // emitToSocket(total_fee)
    57. return ctx.response.body={
    58. success: true,
    59. msg: '操作成功',
    60. data: returnValue
    61. }
    62. } else{
    63. returnValue.msg = result.xml.return_msg[0]
    64. return ctx.response.body={
    65. success: false,
    66. msg: '操作失败',
    67. data: returnValue
    68. }
    69. }
    70. })
    71. }

      写的一个微信支付的配置项

    1. const cryptoMO = require('crypto') // MD5算法
    2. /* 微信参数AppID 和 Secret */
    3. const wxConfig = {
    4. AppID: "......", // 小程序ID
    5. Secret: "......", // 小程序Secret
    6. Mch_id: "......", // 商户号
    7. Mch_key: "......", // 商户key
    8. // 生成商户订单号
    9. getWxPayOrdrID: function(){
    10. let myDate = new Date();
    11. let year = myDate.getFullYear();
    12. let mouth = myDate.getMonth() + 1;
    13. let day = myDate.getDate();
    14. let hour = myDate.getHours();
    15. let minute = myDate.getMinutes();
    16. let second = myDate.getSeconds();
    17. let msecond = myDate.getMilliseconds(); //获取当前毫秒数(0-999)
    18. if(mouth < 10){ /*月份小于10 就在前面加个0*/
    19. mouth = String(String(0) + String(mouth));
    20. }
    21. if(day < 10){ /*日期小于10 就在前面加个0*/
    22. day = String(String(0) + String(day));
    23. }
    24. if(hour < 10){ /*时小于10 就在前面加个0*/
    25. hour = String(String(0) + String(hour));
    26. }
    27. if(minute < 10){ /*分小于10 就在前面加个0*/
    28. minute = String(String(0) + String(minute));
    29. }
    30. if(second < 10){ /*秒小于10 就在前面加个0*/
    31. second = String(String(0) + String(second));
    32. }
    33. if (msecond < 10) {
    34. msecond = String(String('00') + String(second));
    35. } else if(msecond >= 10 && msecond < 100){
    36. msecond = String(String(0) + String(second));
    37. }
    38. let currentDate = String(year) + String(mouth) + String(day) + String(hour) + String(minute) + String(second) + String(msecond);
    39. return currentDate
    40. },
    41. //获取随机字符串
    42. getNonceStr(){
    43. return Math.random().toString(36).substr(2, 15)
    44. },
    45. // 统一下单签名
    46. paysignjsapi (appid,body,mch_id,nonce_str,notify_url,openid,out_trade_no,spbill_create_ip,total_fee) {
    47. let ret = {
    48. appid: appid,
    49. body: body,
    50. mch_id: mch_id,
    51. nonce_str: nonce_str,
    52. notify_url:notify_url,
    53. openid:openid,
    54. out_trade_no:out_trade_no,
    55. spbill_create_ip:spbill_create_ip,
    56. total_fee:total_fee,
    57. trade_type: 'JSAPI'
    58. }
    59. let str = this.raw(ret, true)
    60. str = str + '&key=' + wxConfig.Mch_key
    61. let md5Str = cryptoMO.createHash('md5').update(str, 'utf-8').digest('hex')
    62. md5Str = md5Str.toUpperCase()
    63. return md5Str
    64. },
    65. raw (args, lower) {
    66. let keys = Object.keys(args)
    67. keys = keys.sort()
    68. let newArgs = {}
    69. keys.forEach(key => {
    70. lower ? newArgs[key.toLowerCase()] = args[key] : newArgs[key] = args[key]
    71. })
    72. let str = ''
    73. for(let k in newArgs) {
    74. str += '&' + k + '=' + newArgs[k]
    75. }
    76. str = str.substr(1)
    77. return str
    78. },
    79. //小程序支付签名
    80. paysignjs (appid, nonceStr, packages, signType, timeStamp) {
    81. let ret = {
    82. appId: appid,
    83. nonceStr: nonceStr,
    84. package: packages,
    85. signType: signType,
    86. timeStamp: timeStamp
    87. }
    88. let str = this.raw(ret)
    89. str = str + '&key=' + this.Mch_key
    90. let md5Str = cryptoMO.createHash('md5').update(str, 'utf-8').digest('hex')
    91. md5Str = md5Str.toUpperCase()
    92. return md5Str
    93. },
    94. // 校验支付成功回调签名
    95. validPayBacksign (xml) {
    96. let ret = {}
    97. let _paysign = xml.sign[0]
    98. for (let key in xml) {
    99. if (key !== 'sign' && xml[key][0]) ret[key] = xml[key][0]
    100. }
    101. let str = this.raw(ret, true)
    102. str = str + '&key=' + wxConfig.Mch_key
    103. let md5Str = cryptoMO.createHash('md5').update(str, 'utf-8').digest('hex')
    104. md5Str = md5Str.toUpperCase()
    105. return _paysign === md5Str
    106. },
    107. // 确认退款签名
    108. refundOrderSign(appid,mch_id,nonce_str,op_user_id,out_refund_no,out_trade_no,refund_fee,total_fee) {
    109. let ret = {
    110. appid: appid,
    111. mch_id: mch_id,
    112. nonce_str: nonce_str,
    113. op_user_id: op_user_id,
    114. out_refund_no: out_refund_no,
    115. out_trade_no: out_trade_no,
    116. refund_fee: refund_fee,
    117. total_fee: total_fee
    118. }
    119. let str = this.raw(ret, true)
    120. str = str + '&key='+wxConfig.Mch_key
    121. let md5Str = cryptoMO.createHash('md5').update(str, 'utf-8').digest('hex')
    122. md5Str = md5Str.toUpperCase()
    123. return md5Str
    124. }
    125. }

      这个配置项里的就是raw方法得注意下,有个区分,有的签名是key值全小写,有的签名就是支付二次签名校验的时候,key值是要保持驼峰,所以加了点区分。

      当时在此处确实遇到了问题,查了很多博客,解决办法都模棱两可并没有效。其实,微信提供了签名校验工具,可以将自己的参数传入看和生成的是否一致,然后就可以单步调试看是哪里出了问题,比较方便快捷。(签名校验工具)

      从上面代码也可以看出流程:

      根据文档需要传的参数 —— 生成下单签名 —— 签名与参数一起传入 —— 调用微信统一下单api —— 返回下单接口的XML —— 解析XML返回数据参数,再次生成签名 —— 数据返回前台供 wx.requestPayment() 调用

      至此微信支付就可以正常唤起窗口付款了。但是还有个重要的问题,就是下单成功通知。也就是下统一下单里传入的 notify_url:支付成功回答地址

    3、支付成功结果通知

      我们需要提供一个接口供微信支付成功回调:'POST /order/wxPayBack': wxPayBack, // 微信支付成功回调

    1. const parseString = require('xml2js').parseString // xml转js对象
    2. let wxPayBack = async (ctx, next) => {
    3. console.log('wxPayBack', ctx.request.body) // 我们可以打印看下微信返回的xml长啥样
    4. parseString(ctx.request.body, function (err, result) {
    5. payBack(result.xml, ctx)
    6. })
    7. }
    8. let payBack = async (xml, ctx) => {
    9. if (xml.return_code[0] == 'SUCCESS') {
    10. let out_trade_no = xml.out_trade_no[0] // 商户订单号
    11. let total_free = xml.total_fee[0] // 付款总价
    12. console.log('订单:', out_trade_no, '价格:', total_free)
    13. if (wxConfig.validPayBacksign(xml)) {
    14. let out_order = await model.orderInfo.find({
    15. where: {
    16. orderCode: out_trade_no
    17. }
    18. })
    19. if (out_order && (out_order.orderPay * 100) - total_free === 0 && out_order.orderState === 1) {
    20. await model.orderInfo.update({ orderState: 2 }, {
    21. where: {
    22. orderCode: out_trade_no
    23. }
    24. })
    25. // emitToSocket(total_fee)
    26. return ctx.response.body = `<return_code>return_code><return_msg>return_msg> `
    27. }
    28. }
    29. }
    30. return ctx.response.body = `<return_code>return_code><return_msg>return_msg> `

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

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

    4、主动查询订单状态

      有时候微信回调通知异常有误,文档也有说明,所以最好需要主动查询一下订单支付状态,也比较简单。代码如下:

    1. // 查询微信支付交易订单状态
    2. let orderquery = async (ctx, next) => {
    3. let { orderCode } = ctx.request.query
    4. let nonce_str = wxConfig.getNonceStr()
    5. let bodyData = '';
    6. bodyData += '' + wxConfig.AppID + '';
    7. bodyData += '' + wxConfig.Mch_id + '';
    8. bodyData += '' + orderCode + '';
    9. bodyData += '' + nonce_str + '';
    10. // 签名
    11. let sign = wxConfig.orderquerySign(
    12. wxConfig.AppID,
    13. wxConfig.Mch_id,
    14. orderCode,
    15. nonce_str
    16. )
    17. bodyData += '' + sign + ''
    18. bodyData += ''
    19. // 微信小程序支付查询接口
    20. var urlStr = 'https://api.mch.weixin.qq.com/pay/orderquery'
    21. let option={
    22. method:'POST',
    23. uri: urlStr,
    24. body:bodyData
    25. }
    26. let result = await rp(option)
    27. parseString(result, function(err,result){
    28. if (result.xml.trade_state[0] == 'SUCCESS') {
    29. model.orderInfo.update({ orderState: 2 }, {
    30. where: {
    31. orderCode: orderCode
    32. }
    33. })
    34. return ctx.response.body={
    35. success: true,
    36. msg: '交易成功'
    37. }
    38. } else{
    39. return ctx.response.body={
    40. success: false,
    41. msg: '交易失败',
    42. data: result.xml.trade_state[0]
    43. }
    44. }
    45. })
    46. }
    1. // 查询支付结果签名
    2. orderquerySign(appid, mch_id, orderCode, nonce_str) {
    3. let ret = {
    4. appid: appid,
    5. mch_id: mch_id,
    6. out_trade_no: orderCode,
    7. nonce_str: nonce_str
    8. }
    9. let str = this.raw(ret, true)
    10. str = str + '&key='+wxConfig.Mch_key
    11. let md5Str = cryptoMO.createHash('md5').update(str, 'utf-8').digest('hex')
    12. md5Str = md5Str.toUpperCase()
    13. return md5Str
    14. }

    三、申请退款和确认退款

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

       申请退款的微信文档

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

       进入证书使用链接,去查看关于“3、API证书”相关的使用东西。也就是说需要从商户号那边下载一些证书,放在工程里,再调用微信三方提供的退款接口:https://api.mch.weixin.qq.com/secapi/pay/refund 时,需要校该证书,以确保安全。

      实例代码:

    1. // 确认退款
    2. let confirmRefund = async (ctx, next) => {
    3. let _body = ctx.request.body
    4. let out_trade_no = _body.orderCode // 商户订单号
    5. let nonce_str = wxConfig.getNonceStr()
    6. let total_fee = _body.orderPay * 100 // 订单价格 单位是 分
    7. let refund_fee = _body.orderPay * 100
    8. let bodyData = '';
    9. bodyData += '' + wxConfig.AppID + '';
    10. bodyData += '' + wxConfig.Mch_id + '';
    11. bodyData += '' + nonce_str + '';
    12. bodyData += '' + wxConfig.Mch_id + '';
    13. bodyData += '' + nonce_str + '';
    14. bodyData += '' + out_trade_no + '';
    15. bodyData += '' + total_fee + '';
    16. bodyData += '' + refund_fee + '';
    17. // 签名
    18. let sign = wxConfig.refundOrderSign(
    19. wxConfig.AppID,
    20. wxConfig.Mch_id,
    21. nonce_str,
    22. wxConfig.Mch_id,
    23. nonce_str, // 商户退款单号 给一个随机字符串即可out_refund_no
    24. out_trade_no,
    25. refund_fee,
    26. total_fee
    27. )
    28. bodyData += '' + sign + ''
    29. bodyData += ''
    30. let agentOptions = {
    31. pfx: fs.readFileSync(path.join(__dirname,'/wx_pay/apiclient_cert.p12')),
    32. passphrase: wxConfig.Mch_id,
    33. }
    34. // 微信小程序退款接口
    35. let urlStr = 'https://api.mch.weixin.qq.com/secapi/pay/refund'
    36. let option={
    37. method:'POST',
    38. uri: urlStr,
    39. body: bodyData,
    40. agentOptions: agentOptions
    41. }
    42. let result = await rp(option)
    43. parseString(result, function(err, result){
    44. if (result.xml.result_code[0] == 'SUCCESS') {
    45. refundBack(_body.id)
    46. return ctx.response.body={
    47. success: true,
    48. msg: '操作成功'
    49. }
    50. } else{
    51. return ctx.response.body={
    52. success: false,
    53. msg: result.xml.err_code_des[0]
    54. }
    55. }
    56. })
    57. }
    58. let refundBack = async (orderId) => {
    59. model.orderInfo.update({ orderState: 8 }, {
    60. where: { id: orderId }
    61. })
    62. let orderfoods = await model.foodsOrder.findAll({
    63. where: { orderId: orderId }
    64. })
    65. orderfoods.forEach(food => {
    66. dealFood(food, 'plus')
    67. })
    68. }

      可以看到:随机字符串 nonce_str,商户退款单号 out_refund_no,我们用的是同一个随机串。

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

  • 相关阅读:
    【面试】Redis的热key问题如何发现和解决?
    小物体的目标检测的研究综述
    JavaFx 实现按钮防抖和软件重启(Kotlin)
    go的gin框架实现接受多个图片和单个视频并保存到本地服务器的接口
    环保企业网站前后台管理系统(Java+SSM+MySQL)
    MySQL 8.0 OCP (1Z0-908) 考点精析-架构考点6:InnoDB Tablespaces之系统表空间(System Tablespace)
    企业专线成本高?贝锐蒲公英轻松实现财务系统远程访问
    lenovo联想笔记本小新 潮7000-14IKBR 2018款(81GA)原装出厂Windows10系统镜像
    DNS域名解析过程
    探索C++在软件开发中的应用
  • 原文地址:https://blog.csdn.net/qq_47443027/article/details/126238313