1. 程式人生 > 實用技巧 >在RAC上部署OGG並配置OGG高可用

在RAC上部署OGG並配置OGG高可用

最近做了微信支付及退款一系列操作,微信文件寫的也比較簡略,網上部落格也並不詳細,也踩了一些坑,在這裡記錄下。當然主要還是得根據微信小程式文件一步一步來。

一、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 值比對即可。

  校驗成功之後,修改訂單表對應資料的狀態即可。

三、申請退款和確認退款

  申請退款其實沒什麼說的,就是使用者側申請退款,然後更改使用者側訂單的狀態,主要說一下商家確認退款給買家的流程。

  申請退款的微信文件

  特別需要注意的是:請求需要雙向證書。 詳見證書使用

  進入證書使用連結,去檢視關於“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,我們用的是同一個隨機串。

  然後經過校驗之後,獲取證書內容 及 商戶號,作為引數傳給微信提供的申請退款介面介面。返回退款成功之後,做自己使用者側的相關業務處理即可。