评论

微信支付开发,可以简化到复制+粘贴这种地步

由一个issue引发的重构,许多“变态”用法,都是在unit tests发掘出来的,耙一啪 wechatpay-axios-plugin 这次主要重构。

距离上一版(0.3)过去了有几个月了,这几个月没少折腾微信团队,春节前后“突然”利好不断,许多几乎不可能的事项都有显著改善,这也许就是认(一)真(本)做(正)事(经)的回报吧。2月下旬的时候,有一小伙伴,开了个issue#10 反馈,lib在变量重入的时候,存在bug,无法正确获取订单信息。仔细推演之后,决定重构一下,把树形结构Wechatpay SDK起个底儿,许多(BT)用法,真的是事后测试用例覆盖时才发现,啪一下吧。

0.4版做了这些改进

  • README文档中文化
  • 重构 Wechatpay 类,同时支持 APIv2&v3 链式调用
  • 改变 Wechatpay.client 返回值为Wechatpay.client.v3Wechatpay.client.v2xmlBased 接口客户端
  • 废弃 withEntities 方法,其在链式多次调用时,有可能达不到预期
  • 解决 AES-GCMNode10上的解密兼容性问题,程序在Node10上有可能崩溃,建议Node10用户升级至此版本
  • 支持 企业微信-企业支付 链式调用,需要额外注入签名规则,见用法示例
  • 优化Wechatpay在多次实例化时赋值Symbol(CLIENT)异常问题,增加wechatpay.test.js测试用例覆盖

链式chain调用

这个想法是自0.2版开始,是把URL.pathname/做切分,映射成对象属性,0.4版做了极致优化,支持APIv2的pathname映射,效果较上一贴 微信支付APIv3的Nodejs版SDK,打印的树形更优美了,效果如下:

[Function (anonymous)] {
  v2: [Function: /v2] {
    risk: [Function: /v2/risk] {
      getpublickey: [Function: /v2/risk/getpublickey]
    },
    pay: [Function: /v2/pay] { micropay: [Function: /v2/pay/micropay] },
    secapi: [Function: /v2/secapi] {
      pay: [Function: /v2/secapi/pay] {
        refund: [Function: /v2/secapi/pay/refund]
      }
    },
    mmpaymkttransfers: [Function: /v2/mmpaymkttransfers] {
      sendredpack: [Function: /v2/mmpaymkttransfers/sendredpack],
      promotion: [Function: /v2/mmpaymkttransfers/promotion] {
        transfers: [Function: /v2/mmpaymkttransfers/promotion/transfers],
        paywwsptrans2pocket: [Function: /v2/mmpaymkttransfers/promotion/paywwsptrans2pocket]
      },
      sendworkwxredpack: [Function: /v2/mmpaymkttransfers/sendworkwxredpack]
    }
  },
  v3: [Function: /v3] {
    pay: [Function: /v3/pay] {
      transactions: [Function: /v3/pay/transactions] {
        native: [Function: /v3/pay/transactions/native],
        id: [Function: /v3/pay/transactions/id] {
          '{transaction_id}': [Function: /v3/pay/transactions/id/{transaction_id}]
        },
        outTradeNo: [Function: /v3/pay/transactions/out-trade-no] {
          '{out_trade_no}': [Function: /v3/pay/transactions/out-trade-no/{out_trade_no}]
        }
      }
      favor: [Function: /v3/marketing/favor] {
        media: [Function: /v3/marketing/favor/media] {
          imageUpload: [Function: /v3/marketing/favor/media/image-upload]
        },
        stocks: [Function: /v3/marketing/favor/stocks] {
          '$stock_id$': [Function: /v3/marketing/favor/stocks/{stock_id}] {
            useFlow: [Function: /v3/marketing/favor/stocks/{stock_id}/use-flow]
          }
        }
      },
      partnerships: [Function: /v3/marketing/partnerships] {
        build: [Function: /v3/marketing/partnerships/build]
      }
    },
    smartguide: [Function: /v3/smartguide] {
      guides: [Function: /v3/smartguide/guides] {
        '$guide_id$': [Function: /v3/smartguide/guides/{guide_id}] {
          assign: [Function: /v3/smartguide/guides/{guide_id}/assign]
        }
      }
    }
  }
}

上述 /v2 树节点,是本SDK“创造”出来的,用以区分APIv3调用,默认不是 /v2开头的节点,均以APIv3方式调用;

这么做是因为,微信支付APIv3版,URL.pathname 目前已知至少有一个不是以 /v3 开头的;

/v2 官方又没用到,咱就先占着用吧,所以如是APIv2版的调用,可以这么 chain, v2.pay.micropay = /v2/pay/micropay 即以XML负载形式请求远端接口;

较0.2版,每个级联对象为APIv2做了改变,每个节点本身是个Function,链接的是HTTP POST方法,入参接受两个参数 (data, config),返回值是Promise对象;

另外每个节点同时隐式内置了GET/POST/PUT/PATCH/DELETE 操作方法链,支持全大写及全小写(未来有可能会删除)两种编码方式,完美支持APIv3的多HTTP verbs协议(目前统计出来,有至少5处蓝瘦的地方,有空了再说)。

怎么用

一句话可能就说完了,实例化后,按pathname拆分,然后再执行文档上说的HTTP verbs方法,附带所需参数,然后就没有然后了~

实例化

const {Wechatpay, Formatter} = require('wechatpay-axios-plugin')
const wxpay = new Wechatpay({
  mchid: 'your_merchant_id',
  serial: 'serial_number_of_your_merchant_public_cert',
  privateKey: '-----BEGIN PRIVATE KEY-----' + '...' + '-----END PRIVATE KEY-----',
  certs: {
    'serial_number': '-----BEGIN CERTIFICATE-----' + '...' + '-----END CERTIFICATE-----',
  },
  // APIv2参数 >= 0.4.0 开始支持
  secret: 'your_merchant_secret_key_string',
  // 注: 如果不涉及资金变动,如仅收款,merchant参数可选,仅需 `secret` 一个参数,注意其为v2版的。
  merchant: {
    cert: '-----BEGIN CERTIFICATE-----' + '...' + '-----END CERTIFICATE-----',
    key: '-----BEGIN PRIVATE KEY-----' + '...' + '-----END PRIVATE KEY-----',
    // or
    // passphrase: 'your_merchant_id',
    // pfx: fs.readFileSync('/your/merchant/cert/apiclient_cert.p12'),
  },
})

APIv3 Native下单

wxpay.v3.pay.transactions.native
  .post({/*文档参数放这里就好*/})
  .then(({data: {code_url}}) => console.info(code_url))
  .catch(({response: {status, statusText, data}}) => console.error(status, statusText, data))

APIv3 查询订单

wxpay.v3.pay.transactions.id['{transaction_id}']
  .get({params: {mchid: '1230000109'}, transaction_id: '1217752501201407033233368018'})
  .then(({data}) => console.info(data))
  .catch(({response: {status, statusText, data}}) => console.error(status, statusText, data))

APIv3 关单

wxpay.v3.pay.transactions.outTradeNo['1217752501201407033233368018']
  .post({mchid: '1230000109'})
  .then(({status, statusText}) => console.info(status, statusText))
  .catch(({response: {status, statusText, data}}) => console.error(status, statusText, data))

APIv3 对账单下载及解析

const assert = require('assert')
const {Hash: {sha1}} = require('wechatpay-axios-plugin')

wxpay.v3.bill.tradebill.get({
  params: {
    bill_date: '2021-02-12',
    bill_type: 'ALL',
  }
}).then(({data: {download_url, hash_value}}) => wxpay.v3.billdownload.file.get({
  params: (new URL(download_url)).searchParams,
  signed: hash_value,
  responseType: 'arraybuffer',
})).then(res => {
  assert(sha1(res.data.toString()) === res.config.signed, 'verify the SHA1 digest failed.')
  console.info(Formatter.castCsvBill(res.data))
}).catch(error => {
  console.error(error)
})

APIv2 Native下单

wxpay.v2.pay.unifiedorder
  .post({/*文档参数放这里就好*/})
  .then(({data: {code_url}}) => console.info(code_url))
  .catch(console.error)

APIv2 付款码(刷卡)支付

wxpay.v2.pay.micropay({
  appid: 'wx8888888888888888',
  mch_id: '1900000109',
  nonce_str: Formatter.nonce(),
  sign_type: 'HMAC-SHA256',
  body: 'image形象店-深圳腾大-QQ公仔',
  out_trade_no: '1217752501201407033233368018',
  total_fee: 888,
  fee_type: 'CNY',
  spbill_create_ip: '8.8.8.8',
  auth_code: '120061098828009406',
})
.then(res => console.info(res.data))
.catch(console.error)

APIv2 现金红包

wxpay.v2.mmpaymkttransfers.sendredpack({
  nonce_str: Formatter.nonce(),
  mch_billno: '10000098201411111234567890',
  mch_id: '10000098',
  wxappid: 'wx8888888888888888',
  send_name: '鹅企支付',
  re_openid: 'oxTWIuGaIt6gTKsQRLau2M0yL16E',
  total_amount: 1000,
  total_num: 1,
  wishing: 'HAPPY BIRTHDAY',
  client_ip: '192.168.0.1',
  act_name: '回馈活动',
  remark: '会员回馈活动',
  scene_id: 'PRODUCT_4',
})
.then(res => console.info(res.data))
.catch(console.error)

APIv2 企业付款到零钱

wxpay.v2.mmpaymkttransfers.promotion.transfers({
  appid: 'wx8888888888888888',
  mch_id: '1900000109',
  partner_trade_no: '10000098201411111234567890',
  openid: 'oxTWIuGaIt6gTKsQRLau2M0yL16E',
  check_name: 'FORCE_CHECK',
  re_user_name: '王小王',
  amount: 10099,
  desc: '理赔',
  spbill_create_ip: '192.168.0.1',
  nonce_str: Formatter.nonce(),
})
.then(res => console.info(res.data))
.catch(console.error)

APIv2 企业付款到银行卡-获取RSA公钥

这个用法,是测试用例发掘出来的,注意请求带了第二个参数 {baseURL:'https://fraud.mch.weixin.qq.com'}

wxpay.v2.risk.getpublickey({
  mch_id: '1900000109',
  sign_type: 'MD5',
  nonce_str: Formatter.nonce(),
}, {
  baseURL: 'https://fraud.mch.weixin.qq.com'
})
.then(res => console.info(res.data))
.catch(console.error)

企业微信

企业微信的企业支付,数据请求包需要额外的签名,仅需做如下简单扩展适配,即可支持;以下签名注入函数所需的两个参数agentId agentSecret来自企业微信工作台,以下为示例值。

const agentId = 1001001
const agentSecret = 'from_wework_agent_special_string'
const {Hash} = require('wechatpay-axios-plugin')

APIv2 企业红包-注入签名规则

Wechatpay.client.v2.defaults.transformRequest.unshift(function workwxredpack(data, headers) {
  const {act_name, mch_billno, mch_id, nonce_str, re_openid, total_amount, wxappid} = data

  if (!(act_name && mch_billno && mch_id && nonce_str && re_openid && total_amount && wxappid)) {
    return data
  }

  data.workwx_sign = Hash.md5(
    Formatter.queryStringLike(Formatter.ksort({
      act_name, mch_billno, mch_id, nonce_str, re_openid, total_amount, wxappid
    })), agentSecret, agentId
  ).toUpperCase()

  return data
})

APIv2 发放企业红包

wxpay.v2.mmpaymkttransfers.sendworkwxredpack({
  mch_billno: '123456',
  wxappid: 'wx8888888888888888',
  sender_name: 'XX活动',
  sender_header_media_id: '1G6nrLmr5EC3MMb_-zK1dDdzmd0p7cNliYu9V5w7o8K0',
  re_openid: 'oxTWIuGaIt6gTKsQRLau2M0yL16E',
  total_amount: 1000,
  wishing: '感谢您参加猜灯谜活动,祝您元宵节快乐!',
  act_name: '猜灯谜抢红包活动',
  remark: '猜越多得越多,快来抢!',
  mch_id: '1900000109',
  nonce_str: Formatter.nonce(),
})
.then(res => console.info(res.data))
.catch(console.error)

APIv2及APIv3之间的编程体验,几乎是完全一致的了~

更多用法示例,已经完全中文化发在github上,地址是:https://github.com/TheNorthMemory/wechatpay-axios-plugin 花式开发玩法,下一耙再叙~

最后一次编辑于  2021-03-08  
点赞 6
收藏
评论

1 个评论

  • 老张
    老张
    2021-03-08

    感谢分享!

    2021-03-08
    赞同 2
    回复
登录 后发表内容