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

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

一、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_ + 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,我们用的是同一个随机串。

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