2020年7月

在thinkjs框架下使用tenpay开发native微信扫码付款应用.

因为需要做一个付款在winform应用程序上扫码付款,所以采用native扫码的模式一来实现.

微信支付扫码的签名验证借用了github上的开源框架 node-tenpay

node后台笔者偏爱于插件式的开源轻量级框架thinkjs

再补充一个微信扫码技术文档 微信开发文档

第一步: 商户支付回调URL设置:

进入商户平台-->产品中心-->开发配置 扫码支付回调链接
我设置的是 https://域名/pay/notify

第二步: 生成二维码:

首先 npm 引入tenpay 框架

npm i tenpay --save

全局创建一个tenpay api对象

// wxpay.js
const Tenpay = require('tenpay')
const config = require('../config/config.js')
/**
 * tenpayConfig: {
    appid: '公众号ID',
    mchid: '微信商户号',
    partnerKey: '微信支付安全密钥',
    notify_url: 'https://域名/pay/complete', // 支付后回调的接口
    spbill_create_ip: '服务器ip地址'
  }
 */
let api = null
module.exports = {
  getApi () {
    if(!api) api = new Tenpay(config.tenpayConfig)
    return api
  }
}

根据商品ID生成url链接

    api.getNativeUrl({
       product_id: product.id
    })

将商品链接转成二维码
node二维码生成开源框架:node-qrcode

npm install --save qrcode // npm 安装.
const QRCode = require('qrcode')

...
const qr = await QRCode.toDataURL(order.url)    // 转成base64
this.header('Content-Type', 'image/png; charset=UTF-8')    // 设置头部
this.body = base64ToSteam(qr)    // 输出图片流
// 将base64图片转流方法.
function base64ToSteam (data) {
    const base64 = data.replace(/^data:image\/\w+;base64,/, '')
    const dataBuffer = Buffer.from(base64, 'base64')

    const bufferStream = new stream.PassThrough()
    bufferStream.end(dataBuffer)

    return bufferStream
}

生成商品二维码后,只要有用户用微信扫码,微信会自动调你之前在商户后台设置的回调链接

第三步 扫码回调链接开发

tenpay框架已经将几个回调的参数校验解析已中间件的形式开放出来,这里我们只需要将他集成到thinkjs的中间件管理里,因为有多个地方用到了中间件,集中封装一下:

const wxpay = require('./wxpay') // 代码参考最上面,就是拿到tenpay的实例对象.
// 默认options
const defaultOptions = {
  type: 'nativePay' // 扫码的回调中间件传入nativePay
}
module.exports = (options = {}) => {
  options = Object.assign({}, defaultOptions, options);

  let api = wxpay.getApi()    // 拿到实例对象

  let pay = api.middleware(options.type)  // 创建回调函数

  return pay
}

然后在thinkjs的middleware.js 中间件管理中的后面插入:

// middleware.js
   const pay = require('../utils/paycallback')

   ....
   {
    handle: 'payload', // thinkjs 自己的参数转换中间件,类似于bodyParser的功能.
    options: {
      ...
    }
  },
   // 扫码回调
  {
    handle: pay,
    options: {
      type: 'nativePay'
    },
    match: '/pay/notify' // 这里根据自己的接口匹配设置,代表中间件只在这个接口起作用.
  }

然后开发 /pay/notify 接口:
中间件会帮你在ctx对象下绑定两个方法:
replyNative : 扫描成功,传入微信的预订单编号
reply : 失败,传入失败原因.

    try {
      let res = this.ctx.request
      let info = res.weixin // 中间件会将微信的参数解析到request下的weixin对象中.

      let product_id = info.product_id // 商品id,就是你生成url链接时候,传入的商品id
      let openid = info.openid        // 用户的openid  在你商户下对应微信唯一ID

      // 下面是业务代码
      // 获取商品信息.
      let product = await this.model('product_list').where({ id: product_id }).find()

      if (think.isEmpty(product)) {
        return this.ctx.reply('未找到商品')
      }
      
      // 自己数据库生成订单号.
      ....

      // 调用tenpay的unifiedOrder方法统一下单,生成预订单编号.
      let result = await api.unifiedOrder({
        out_trade_no: orderId, // 订单编号
        body: product.name, // 商品名称
        total_fee: product.amount * 100, // 我们数据库是元为单位,微信要求分为单位提交
        openid: openid,
        trade_type: 'NATIVE'
      })

      ....

      return this.ctx.replyNative(result.prepay_id) // 成功后将统一下单生成的prepay_id 通过replyNative方法返回给微信服务器
    } catch (error) {
      this.ctx.reply(error.message || '服务器错误')
    }

然后将代码发布到服务器上后,你测试用微信扫码后,会发现服务器会返回报错,原因是tenpay要求开发人员将微信的xml内容解析后直接已字符串的形式放在body下,而thinkjs的payload会把xml内容解析后,将xml变成对象的形式,放在 body.xml 下.所以我们还要设置一步让payload不要解析xml,直接把流转成文字放在body下就可以了(这是我看了tenpay的源码后才发现的...)

  {
    handle: 'payload',
    options: {
      ....
      extendTypes: {
        text: ['text/xml', 'application/xml'] // xml以text形式解析.
      }
    }
  },

第四步 用户支付后微信回调

tenpay框架的config里如果设置了notify_url地址,微信在用户支付后会回调这个接口
首先一样,让tenpay框架的中间件去处理微信解析和校验的功能

// middleware.js
....
一样要放在payload后面
// 用户支付完成通知.
  {
    handle: pay,
    options: {
      type: 'pay'
    },
    match: '/pay/complete' // 你的回调链接 同上.
  },
....

然后开发支付回调/pay/complete接口:


    try {
      let info = this.ctx.request.weixin // 拿到中间件解析内容.
      const orderModel = this.model('order')
      if (info.return_code === 'SUCCESS') {
        let orderId = info.out_trade_no // 订单号

        let total_fee = parseInt(info.total_fee) / 100  // 金额
        if (info.result_code === 'SUCCESS') { // 支付成功.
          // 交易成功.
          let order = await orderModel.where({ id: orderId }).find()
          if (think.isEmpty(order)) throw new Error('未找到订单')
          
          // 自己数据库订单处理
          ....

        } else {  // 支付失败
          ....
        }
      }
      this.ctx.reply()  // 回复消息(参数为空回复成功, 传值则为错误消息)
    } catch (error) {
      this.ctx.reply(error.message || '服务器错误')
    }

第五步 主动向微信服务器查询订单信息

到第4步已经走完了,但是总有些订单有可能因为网络/数据库原因,complete接口没有接收到或者保存失败之类的.这时候要有一个主动向服务器请求订单状态的接口,这里简单写一下提供参考:

    let code = this.query('code')
    let result = await api.orderQuery({
      out_trade_no: code
    })
    // return this.success(result)
    if (result.return_code === 'SUCCESS') {
      if (result.result_code === 'SUCCESS') {
        if (result.trade_state === 'SUCCESS') {
          // 付款成功
          let amount = parseInt(result.total_fee) / 100
          let complete_time = result.time_end.replace(/(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/, '$1-$2-$3 $4:$5:$6')
          ....
          // 成功回调.
        } else {
          // 付款失败
          return this.success({
            status: 1,
            message: result.trade_state_desc,
            info: result
          })
        }
      } else {
        return this.fail(result.err_code_des)
      }
    } else {
      return this.fail(result.return_msg)
    }

postman接口测试

之前使用postman都只会设置静态的环境变量 如设置请求的域名. 这样切换就可以很方便的切换正式库和测试库.
最近由于需要做接口测试,必须先调用login接口后记录token值,然后再依次调用后面的所有的接口需要把token动态的写入.去查了postman的相关资料,终于发现在postman的测试(Tests)标签栏里是可以做接口返回数据校验已经设置环境变量当前值的.

以下是我写在login接口中的Tests脚本

pm.test("Status code is 200", function () {
    pm.response.to.have.status(200);
});

pm.test('success', function () {
    var jsonData = pm.response.json();
    pm.environment.set("token", jsonData.result.token);
    pm.expect(jsonData.code).to.eql(200);   // 返回的json code值为200
})

其他的接口里可以不设置token,单纯检测状态值返回以及json code返回

最后提一下: postman里面的runner是没法做并发测试的.

如果需要并发测试,可以参考 github开源框架
newman通过导出postman配置文件,用node进行并发测试.