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

微信支付 OpenAPI SDK

Promise based and chained WeChatPay OpenAPI 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';

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

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

// APIv3 的「微信支付公钥」接入模式 {{{
// 「微信支付公钥」`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,
  // 根据商户号所能接入的APIv3模式(微信支付公钥/平台证书)按需配置certs对象内容
  certs: {
    // 「平台证书」 接入模式时,则填 platformCertificate* 配置项及配置行,多平台证书时配多行
    // [platformCertificateSerial]: platformCertificateFilePath,
    // 「微信支付公钥」 接入模式时,则填 platformPublicKey* 配置项及配置行,当前新商户只此模式
    [platformPublicKeyId]: platformPublicKeyFilePath,
  },
  // APIv2(密钥32字节)
  secret: 'your_merchant_secret_key_string',
  // 部分接口要求使用「商户API证书」的场景,需要额外配置如下{cert,key}或{pfx,passphrase}参数
  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(), // 自v0.9.0起可以无需声明
  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))
示例代码
const {Rsa} = require('wechatpay-axios-plugin')

wxpay.v2.risk.getpublickey.post({
  mch_id: '1900000109',
  sign_type: Hash.ALGO_MD5,
  nonce_str: Formatter.nonce(), // 自v0.9.0起可以无需声明
}, {
  baseURL: 'https://fraud.mch.weixin.qq.com/',
  // 声明请求是私有ssl协议,对应加载初始化的 merchant{key,cert} 参数
  security: true,
})
.then(res => {
  const b64 = res.data.pub_key.trim().split(/\r?\n/).slice(1, -1).join('')
  console.info(Rsa.fromPkcs1(b64, Rsa.KEY_TYPE_PUBLIC))
})
.catch(({response: {status, statusText, data}}) => console.error(status, statusText, data))

更多示例代码访问这里

企业微信

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

const {Hash} = require('wechatpay-axios-plugin')
const agentId = '0' // 企业微信应用ID,0是企微内置的特殊应用
const agentSecret = Hash.keyObjectFrom('from_wework_agent_special_string') // 自v0.9.0可用

企业红包-注入签名规则

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

wxpay.client.v2.defaults.transformRequest.unshift(function workwxredpack(data) {
  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(), // 需要显式声明
}, {
  // 声明请求是私有ssl协议,对应加载初始化的 merchant{key,cert} 参数
  security: true,
})
.then(res => console.info(res.data))
.catch(console.error)

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

示例代码
const {Hash,Formatter} = require('wechatpay-axios-plugin')
wxpay.client.v2.defaults.transformRequest.unshift(function wwsptrans2pocket(data) {
  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(), // 需要显式声明
}, {
  // 声明请求是私有ssl协议,对应加载初始化的 merchant{key,cert} 参数
  security: true,
})
.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 v2Secret = Hash.keyObjectFrom('exposed_your_key_here_have_risks') // 自v0.9.0可用
const xml = '<xml>' + ... '</xml>'
const obj = Transformer.toObject(xml)
const res = AesEcb.decrypt(obj.req_info, Hash.md5(v2Secret/*自v0.9.2开始支持*/))
obj.req_info = Transformer.toObject(res)
console.info(obj)

加密

示例代码
const obj = Transformer.toObject(xml)
const ciphertext = AesEcb.encrypt(obj.req_info, Hash.md5(v2Secret/*自v0.9.2开始支持*/))
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 = Hash.keyObjectFrom('exposed_your_key_here_have_risks') // 自v0.9.0可用
const params = {
  appId: 'wx8888888888888888',
  timeStamp: `${Formatter.timestamp()}`,
  nonceStr: Formatter.nonce(),
  package: 'prepay_id=wx201410272009395522657a690389285100',
  signType: Hash.ALGO_HMAC_SHA256,
}
params.paySign = Hash.sign(params.signType, params, v2Secret)

console.info(params)

APP

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

console.info(params)

APIv3数据签名

JSAPI

示例代码
const {Rsa, Formatter} = require('wechatpay-axios-plugin')
const merchantPrivateKeyInstance = Rsa.from('file:///your/merchant/priviate_key.pem') // 自v0.9.0可用

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
), merchantPrivateKeyInstance)

console.info(params)

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

示例代码
const {Hash, Formatter} = require('wechatpay-axios-plugin')
const v2Secret = Hash.keyObjectFrom('exposed_your_key_here_have_risks') // 自v0.9.0可用

// 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(Hash.ALGO_HMAC_SHA256, busiFavorFlat(busiFavor), v2Secret)

console.info(busiFavor)

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

示例代码
const {Hash, Formatter} = require('wechatpay-axios-plugin')
const v2Secret = Hash.keyObjectFrom('exposed_your_key_here_have_risks') // 自v0.9.0可用
const params = {
  stock_id: '12111100000001',
  out_request_no: '20191204550002',
  send_coupon_merchant: '10016226',
  open_id: 'oVvBvwEurkeUJpBzX90-6MfCHbec',
  coupon_code: '75345199',
}
params.sign = Hash.sign(Hash.ALGO_HMAC_SHA256, params, v2Secret)

console.info(params)

常见问题

Q: 在Next.js V14 集成解决方案中,如何解决TypeError: Cannot set property of class Wechatpay {异常提示?

参考这里,在next.config.js中加入声明serverComponentsExternalPackages: ['wechatpay-axios-plugin']

Q: APIv3上请求参数敏感信息如何加密?返回参数敏感信息如何解密?

接口区分国内版还是国际版,国内版的RSA填充方案是RSA_PKCS1_OAEP_PADDING,方法如下:

加密: Rsa.encrypt('原始信息', Rsa.from(platformPublicKeyFilePath, Rsa.KEY_TYPE_PUBLIC))

解密: Rsa.decrypt('密文base64', Rsa.from(merchantPrivateKeyFilePath, Rsa.KEY_TYPE_PRIVATE))

国际版RSA填充方案是RSA_PKCS1_PADDING,本类库放弃支持此种加/解密填充方案,同时node18.19.0也是最后一版默认支持加密的版本(延展阅读见CVE-2023-46809这里),如需使用,请自行寻替代方案。

Q: 如何安全地在应用内使用APIv2APIv3对称密钥?

v0.9.0提供了统一的对称密钥加载函数 Hash.keyObjectFrom(thing: BinaryLike): KeyObject,建议应用升级至v0.9.2使用此函数进行统一对称密钥管理;

Q: 如何加载RSA公/私钥和X509证书公钥?

v0.9.0提供了统一的加载函数 Rsa.from(thing: KeyLike, type: 'public'|'private'): KeyObject

  • Rsa.from(thing, type) 支持从文件/字符串/字节流加载公/私钥和证书,特别地,支持file://, private.pkcs8://, private.pkcs1://, public.pkcs1://, public.spki:// 协议的公/私钥字符串;
  • Rsa.fromPkcs1是个语法糖,支持加载 PKCS#1 格式的公/私钥,入参是 base64 字符串;
  • Rsa.fromPkcs8是个语法糖,支持加载 PKCS#8 格式的私钥,入参是 base64 字符串;
  • Rsa.fromSpki是个语法糖,支持加载 SPKI 格式的公钥,入参是 base64 字符串;

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 对象,可以按需上送额外头参数,例如:

可参考 这里这里 的实现

单元测试

npm install && npm test

技术交流

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

QQ群: 684379275

链接

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

许可证

MIT

About

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

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Sponsor this project

Contributors 5