Skip to content

微信支付 WeChatPay OpenAPI v2&v3' SDK,以命令行方式与接口交互,play the openapi requests over command line

License

Notifications You must be signed in to change notification settings

TheNorthMemory/wechatpay-axios-plugin

Repository files navigation

微信支付 OpenAPI SDK

Promise based and chained WeChatPay OpenAPI client SDK for NodeJS

GitHub actions GitHub release Vulnerabilities types Node NPM downloads per month NPM license

系统要求

NodeJs >= 12

安装

$ npm install wechatpay-axios-plugin

const { Wechatpay } = require('wechatpay-axios-plugin');
const { readFileSync } = require('fs');

// 商户号,支持「普通商户/特约商户」或「服务商商户」
const merchantId = '190000****';

// 「商户API证书」的「证书序列号」
const merchantCertificateSerial = '3775B6A45ACD588826D15E583A95F5DD********';

// 「商户API私钥」`file://`协议的本地文件绝对路径
const merchantPrivateKeyFilePath = 'file:///path/to/merchant/apiclient_key.pem';

// 「平台证书」的「证书序列号」
// 可以从「平台证书」文件解析,也可以在 商户平台 -> 账户中心 -> API安全 查询到
const platformCertificateSerial = '7132d72a03e93cddf8c03bbd1f37eedf********';

// 「平台证书」`file://`协议的本地文件绝对路径
// 「平台证书」文件可由内置的CLI工具下载到
const platformCertificateFilePath  = 'file:///path/to/wechatpay/certificate.pem';

// 「微信支付公钥」`file://`协议的本地文件绝对路径
// 需要在 商户平台 -> 账户中心 -> API安全 下载
const platformPublicKeyFilePath    = 'file:///path/to/wechatpay/publickey.pem';

// 「微信支付公钥」的「微信支付公钥ID」
// 需要在 商户平台 -> 账户中心 -> API安全 查询
const platformPublicKeyId = 'PUB_KEY_ID_01142321349124100000000000********';

// 构造一个 APIv2 & APIv3 客户端实例
const wxpay = new Wechatpay({
  mchid: merchantId,
  serial: merchantCertificateSerial,
  privateKey: merchantPrivateKeyFilePath,
  certs: {
    // 「平台证书」 模式,则 platformCertificate* 行必填,多证书时配多行
    [platformCertificateSerial]: platformCertificateFilePath,
    // 「微信支付公钥」 模式,则 platformPublicKey* 必填
    [platformPublicKeyId]: platformPublicKeyFilePath,
  },
  // 使用APIv2(密钥32字节)时,需要至少设置 `secret`字段
  secret: 'your_merchant_secret_key_string',
  // 接口不要求证书情形,例如仅收款merchant对象参数可选
  merchant: {
    cert: readFileSync('/path/to/merchant/apiclient_cert.pem'),
    key: readFileSync(merchantPrivateKeyFilePath.slice(7)),
    // 或者配置如下`passphrase`及`pfx`配置项
    // passphrase: 'your_merchant_id',
    // **注**: Node17.1开始使用OpenSSL3,老的p12文件需要额外格式转换
    // pfx: readFileSync('/your/merchant/cert/apiclient_cert.p12'),
  },
});

初始化字典说明如下:

  • mchid 为你的商户号,一般是10字节纯数字
  • serial 为你的商户证书序列号,一般是40字节字符串
  • privateKey 为你的商户API私钥,一般是通过官方证书生成工具生成的文件名是apiclient_key.pem文件,支持纯字符串或者文件流buffer格式
  • certs{[serial_number]:string}key/value键值对,键为平台证书序列号/微信支付公钥ID,值为平台证书/微信支付公钥pem格式的纯字符串或者文件流buffer格式
  • secret 为APIv2版的密钥,商户平台上设置的32字节字符串
  • merchant.cert 为你的商户证书,一般是文件名为apiclient_cert.pem文件,支持纯字符串或者文件流buffer格式
  • merchant.key 为你的商户API私钥,一般是通过官方证书生成工具生成的文件名是apiclient_key.pem文件,支持纯字符串或者文件流buffer格式
  • merchant.passphrase 一般为你的商户号
  • merchant.pfx 为你的商户PKCS12格式的证书,文件名一般为apiclient_cert.p12,支持二进制文件流buffer格式(: Node17.1开始使用OpenSSL3,老的p12文件需要额外格式转换)

注: APIv2&APIv3以及Axios初始参数,均融合在一个型参上。

APIv3

示例代码
wxpay.v3.pay.transactions.native
  .post({
    mchid: '1900006XXX',
    out_trade_no: 'native12177525012014070332333',
    appid: 'wxdace645e0bc2cXXX',
    description: 'Image形象店-深圳腾大-QQ公仔',
    notify_url: 'https://weixin.qq.com/',
    amount: {
      total: 1,
      currency: 'CNY'
    },
  })
  .then(({data: {code_url}}) => console.info(code_url))
  .catch(({response: {
    status,
    statusText,
    data
  } }) => console.error(status, statusText, data))
示例代码
// _placeholder_ 语法糖会转换成 '{placeholder}' 格式
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))
示例代码
// $placeholder$ 语法糖会转换成 '{placeholder}' 格式
wxpay.v3.pay.transactions.outTradeNo.$out_trade_no$.close
  .post({
    mchid: '1230000109'
  }, {
    //当商户订单号有大写字符时,只能这样参数化传递
    out_trade_no: 'P1217752501201407033233368018'
  })
  .then(({status, statusText}) => console.info(status, statusText))
  .catch(({response: {
    status,
    statusText,
    data
  } }) => console.error(status, statusText, data))
示例代码
;(async () => {
  try {
    const res = await wxpay.v3.refund.domestic.refunds.post({
      transaction_id: '1217752501201407033233368018',
      out_refund_no: '1217752501201407033233368018',
      reason: '商品已售完',
      notify_url: 'https://weixin.qq.com',
      funds_account: 'AVAILABLE',
      amount: {
        refund: 888,
        from: [{
          account: 'AVAILABLE',
          amount: 444,
        }],
        total: 888,
        currency: 'CNY'
      },
    });
  } catch({response: {status, statusText, data}}) {
    console.error(status, statusText, data)
  }
})()

更多示例代码访问这里

APIv2

示例代码
wxpay.v2.mmpaymkttransfers.sendredpack.post({
  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(({response: {status, statusText, data}}) => console.error(status, statusText, data))
示例代码
wxpay.v2.risk.getpublickey.post({
  mch_id: '1900000109',
  sign_type: 'MD5',
  nonce_str: Formatter.nonce(),
}, {
  baseURL: 'https://fraud.mch.weixin.qq.com/',
  // 返回值无`sign`字段,无需数据校验
  transformResponse: [Transformer.toObject],
})
.then(res => console.info(res.data))
.catch(({response: {status, statusText, data}}) => console.error(status, statusText, data))

更多示例代码访问这里

企业微信

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

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

企业红包-注入签名规则

示例代码
wxpay.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
})

发放企业红包

示例代码
wxpay.v2.mmpaymkttransfers.sendworkwxredpack.post({
  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)

向员工付款-注入签名规则

示例代码
wxpay.client.v2.defaults.transformRequest.unshift(function wwsptrans2pocket(data, headers) {
  const {amount, appid, desc, mch_id, nonce_str, openid, partner_trade_no, ww_msg_type} = data

  if (!(amount && appid && desc && mch_id && nonce_str && openid && partner_trade_no && ww_msg_type)) {
    return data
  }

  data.workwx_sign = Hash.md5(
    Formatter.queryStringLike(Formatter.ksort({
      amount, appid, desc, mch_id, nonce_str, openid, partner_trade_no, ww_msg_type
    })), agentSecret, agentId
  ).toUpperCase()

  return data
})

向员工付款

示例代码
wxpay.v2.mmpaymkttransfers.promotion.paywwsptrans2pocket.post({
  appid: 'wxe062425f740c8888',
  device_info: '013467007045764',
  partner_trade_no: '100000982017072019616',
  openid: 'ohO4Gt7wVPxIT1A9GjFaMYMiZY1s',
  check_name: 'NO_CHECK',
  re_user_name: '张三',
  amount: '100',
  desc: '六月出差报销费用',
  spbill_create_ip: '10.2.3.10',
  ww_msg_type: 'NORMAL_MSG',
  act_name: '示例项目',
  mch_id: '1900000109',
  nonce_str: Formatter.nonce(),
})
.then(res => console.info(res.data))
.catch(console.error)

自定义打印日志

示例代码
// APIv2 日志
wxpay.client.v2.defaults.transformRequest.push(data => (console.log(data), data))
wxpay.client.v2.defaults.transformResponse.unshift(data => (console.log(data), data))
// APIv3 日志
wxpay.client.v3.defaults.transformRequest.push((data, headers) => (console.log(data, headers), data))
wxpay.client.v3.defaults.transformResponse.unshift((data, headers) => (console.log(data, headers), data))

XML形式通知应答

示例代码
const {Transformer} = require('wechatpay-axios-plugin')
const xml = Transformer.toXml({
  return_code: 'SUCCESS',
  return_msg: 'OK',
})

console.info(xml)

aes-256-ecb/pcks7padding

解密

示例代码
const {Aes: {AesEcb}, Transformer, Hash} = require('wechatpay-axios-plugin')
const secret = 'exposed_your_key_here_have_risks'
const xml = '<xml>' + ... '</xml>'
const obj = Transformer.toObject(xml)
const res = AesEcb.decrypt(obj.req_info, Hash.md5(secret))
obj.req_info = Transformer.toObject(res)
console.info(obj)

加密

示例代码
const obj = Transformer.toObject(xml)
const ciphertext = AesEcb.encrypt(obj.req_info, Hash.md5(secret))
console.assert(
  obj.req_info === ciphertext,
  `The notify hash digest should be matched the local one`
)

APIv2数据签名

JSAPI

示例代码
const {Hash, Formatter} = require('wechatpay-axios-plugin')
const v2Secret = 'exposed_your_key_here_have_risks'
const params = {
  appId: 'wx8888888888888888',
  timeStamp: `${Formatter.timestamp()}`,
  nonceStr: Formatter.nonce(),
  package: 'prepay_id=wx201410272009395522657a690389285100',
  signType: 'HMAC-SHA256',
}
params.paySign = Hash.sign(params.signType, params, v2Secret)

console.info(params)

APP

示例代码
const {Hash, Formatter} = require('wechatpay-axios-plugin')
const v2Secret = 'exposed_your_key_here_have_risks'
const params = {
  appid: 'wx8888888888888888',
  partnerid: '1900000109',
  prepayid: 'WX1217752501201407033233368018',
  package: 'Sign=WXPay',
  timestamp: `${Formatter.timestamp()}`,
  noncestr: Formatter.nonce(),
}
params.sign = Hash.sign('MD5', params, v2Secret)

console.info(params)

APIv3数据签名

JSAPI

示例代码
const {Rsa, Formatter} = require('wechatpay-axios-plugin')
const privateKey = require('fs').readFileSync('/your/merchant/priviate_key.pem')

const params = {
  appId: 'wx8888888888888888',
  timeStamp: `${Formatter.timestamp()}`,
  nonceStr: Formatter.nonce(),
  package: 'prepay_id=wx201410272009395522657a690389285100',
  signType: 'RSA',
}
params.paySign = Rsa.sign(Formatter.joinedByLineFeed(
  params.appId, params.timeStamp, params.nonceStr, params.package
), privateKey)

console.info(params)

商家券-小程序发券v2版签名规则

示例代码
const {Hash, Formatter} = require('wechatpay-axios-plugin')
const v2Secret = 'exposed_your_key_here_have_risks'

// flat the miniprogram data transferring structure for sign
const busiFavorFlat = ({send_coupon_merchant, send_coupon_params = []} = {}) => {
  return {
    send_coupon_merchant,
    ...send_coupon_params.reduce((des, row, idx) => (
      Object.keys(row).map(one => des[`${one}${idx}`] = row[one]), des
    ), {}),
  }
}

// the miniprogram data transferring structure
const busiFavor = {
  send_coupon_params: [
    {out_request_no:'1234567',stock_id:'abc123'},
    {out_request_no:'7654321',stock_id:'321cba'},
  ],
  send_coupon_merchant: '10016226'
}

busiFavor.sign = Hash.sign('HMAC-SHA256', busiFavorFlat(busiFavor), v2Secret)

console.info(busiFavor)

商家券-H5发券v2版签名规则

示例代码
const {Hash, Formatter} = require('wechatpay-axios-plugin')
const v2Secret = 'exposed_your_key_here_have_risks'
const params = {
  stock_id: '12111100000001',
  out_request_no: '20191204550002',
  send_coupon_merchant: '10016226',
  open_id: 'oVvBvwEurkeUJpBzX90-6MfCHbec',
  coupon_code: '75345199',
}
params.sign = Hash.sign('HMAC-SHA256', params, v2Secret)

console.info(params)

常见问题

Q: 平台证书下载工具,一直抛异常AssertionError [ERR_ASSERTION]: The response's Headers incomplete是为何?

命令行下的 -s参数,即:商户证书序列号参数给错了,就会抛上述异常,这个时候服务端其实返回的是401状态码,下一个版本会优化一下下载工具,对异常进行捕获,当前请校对你的商户证书序列号并且确保正确;

Q: APIv3消息通知,AES-256-GCM加密字段,应该如何解密?

官方文档有介绍,APIv3平台证书及消息通知关键信息均使用AesGcm加解密,依赖APIv3密钥,商户侧解密可参考bin/cli/cert.js证书下载工具,例如:

AesGcm.decrypt(ciphertext, secret, nonce, aad);

Q: 敏感信息或者幂等操作要求额外头信息上送时,应该如何构建请求参数?

DELETE/GET请求的第一个参数,POST/PUT/PATCH请求的第二个参数,是 AxiosRequestConfig 对象,可以按需上送额外头参数,例如:

wxpay.v3.applyment4sub.applyment.$noop$(
  {},
  { noop: '', headers: { 'Wechatpay-Serial': '123456' } },
).then(console.info).catch(console.error);

可参考 #17

Q: 接口地址为slash(/)结尾的,应该如何构建请求参数?

动态参数uri_template或者属性property方式构建,可参考 #16

单元测试

npm install && npm test

技术交流

如果遇到困难或建议可以 提ISSUE 或 加群,交流技术,分享经验。

QQ群: 684379275

链接

如果你觉得这个library不错,你可以扫如下赞赏码以资鼓励作者,博客更有部分"实战"内容,也可能对你的开发对接有所帮助。

许可证

MIT