当前位置:网站首页>WeChat Mini Program Payment and Refund Overall Process

WeChat Mini Program Payment and Refund Overall Process

2022-08-09 12:33:00 Fire orchid

Recently, I have done a series of WeChat payment and refund operations,The WeChat document is also relatively simple to write,Online blogs are also not detailed,也踩了一些坑,在这里记录下.Of course, it is mainly necessary to follow the WeChat applet documentation step by step.

一、wx.requestPayment

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

  The so-called initiating WeChat payment,It means that the WeChat payment window is evoked on the user sideapi,这个apiParameters need to be passed according to the specification

wx.requestPayment({
  timeStamp: '',
  nonceStr: '',
  package: '',
  signType: 'MD5',
  paySign: '',
  success (res) { },
  fail (res) { }
})

  These parameters need to be obtained from the background.那么我们进入“微信支付接口文档”See what the process is

二、WeChat payment specific process

  The documentation is also very clear,不细说,Mainly look at the following process

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

1、小程序内调用登录接口,获取到用户的openid,api参见公共api【小程序登录API

2、商户server调用支付统一下单,api参见公共api【统一下单API

3、商户server调用再secondary signature,api参见公共api【再secondary signature

4、商户server接收支付通知,api参见公共api【支付结果通知API

5、商户server查询支付结果,api参见公共api【查询订单API

1、调用wx.login获取code,然后通过code,Call the WeChat third-party interface,获取openid.If the user system has itopenid记录,This step can be omitted.

  Mainly because of the unified order belowapiparameter configuration in:

  openid参数:trade_type=JSAPI,此参数必传,用户在商户appid下的唯一标识.openid如何获取,可参考【获取openid】.

2、统一下单api、二secondary signatureapi返回参数

  See the parameters in the documentation,Pass those parameters,You can call the WeChat third-party interface.一般不会有啥问题,The main problem will be that2secondary signature.

  实例代码如下

// 统一下单
let unifiedorder = async (params = {}, ctx) => {
  let body = '......' // 商品描述
  let notify_url = 'https://....../wxPayBack' // 支付成功的回调地址  可访问 不带参数
  let nonce_str = wxConfig.getNonceStr() // 随机数
  let out_trade_no = params.orderCode // 商户订单号(The order number of the merchant customized by the user system)
  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,JSAPIThis parameter must be passed when paying by method)
  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>' // 交易类型 Small program value:JSAPI
  // 签名(according to the above parameters,There is a signature algorithm,It is also described in the documentation)
  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>'

  // WeChat applet unified ordering interface
  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;  // 商户订单号
          // 小程序 Client payment needs 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
          }
      }
  })
}

  A configuration item of WeChat payment written

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
    },
    //Mini program payment signature
    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
    },
    // Verify the payment success callback signature
    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
    },
    // Confirm refund signature
    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
  }
}

  This configuration item israwThe method has to be noticed,There is a distinction,Some signatures arekeyValues ​​are all lowercase,Some signatures are paid for when the secondary signature verification is performed,keyThe value is to keep camel case,So add some distinction.

  I did have a problem here at the time,查了很多博客,The solutions are all ambiguous and ineffective.其实,WeChat provides a signature verification tool,You can pass in your own parameters to see if they are consistent with the generated ones,Then you can step through and debug to see what went wrong,比较方便快捷.(签名校验工具)

  The process can also be seen from the above code:

  Parameters that need to be passed according to the document —— Generate order signature —— The signature is passed in with the parameters —— 调用微信统一下单api —— Return to the order interfaceXML —— 解析XMLReturns the data parameter,再次生成签名 —— The data is returned to the foreground for supply wx.requestPayment() 调用

  At this point, WeChat payment can normally evoke window payment.但是还有个重要的问题,It is a notification that the order has been successfully placed.That is, it is passed in the unified order notify_url:The payment is successful to answer the address

3、Payment success result notification

  We need to provide an interface for the successful callback of WeChat payment:'POST /order/wxPayBack': wxPayBack, // 微信支付成功回调

const parseString = require('xml2js').parseString // xml转js对象

let wxPayBack = async (ctx, next) => {
  console.log('wxPayBack', ctx.request.body) // We can print and see what WeChat returnsxml长啥样
  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] // The total price of the payment
    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),Be sure to verify the callback signature of the successful payment here.The verification rules are returned by WeChatxml里除了 sign Do not put into the parameter verification,All others are to be taken out key - value value for production md5 加密,And then returned with WeChat sign Values ​​can be compared.

  校验成功之后,You can modify the status of the corresponding data in the order table.

4、主动查询订单状态

  Sometimes the WeChat callback notification is abnormal,文档也有说明,Therefore, it is best to actively check the payment status of the order,也比较简单.代码如下:

// Check WeChat Pay transaction order status
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>'

  // WeChat applet payment query interface
  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]
        }
    }
  })
}
// Query the signature of the payment result
    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
    }

三、Request a refund and confirm your refund

  Applying for a refund doesn't really mean anything,It is the user side to apply for a refund,Then change the status of the user-side order,Mainly talk about the process of the merchant confirming the refund to the buyer.

   WeChat document for applying for a refund

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

   Go to the certificate usage link,Go see about“3、API证书”related usage stuff.That is to say, you need to download some certificates from the merchant number,in the project,Then call the refund interface provided by the WeChat three parties:https://api.mch.weixin.qq.com/secapi/pay/refund 时,This certificate is required,以确保安全.

  实例代码:

// 确认退款
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, // 商户退款单号 Just give it a random stringout_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,
  }

  // WeChat applet refund interface
  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,We are using the same random string.

  Then after verification,获取证书内容 及 商户号,It is passed as a parameter to the refund application interface provided by WeChat.Return after a successful refund,You can do related business processing on your own user side.

原网站

版权声明
本文为[Fire orchid]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/221/202208091143391322.html