- 微信接口文档及调试工具
收集归纳了400+开放接口文档,力求简洁明了的书写接口IO文档,开发者之友。 可直接访问阅读微信API文档
2019-06-21 - 免充值产品测试验收用例的wechatpay-axios-plugin教学帖
社区内大佬们都有贡献 「免充值代金券」 的测试用例实现,这篇文章也来凑一下热闹,顺带帮你把一下为啥要做这个 「测试验收」 以及 「验收注意」 细节。 引言 官方用例文档链接 一共25页,非常详细,照着描一步步做,很快就能验收完。本篇作为「教学帖」,试着让友商们理解,这个验收的重要性,如果希望能够获取帮助直接验收,请阅读代金券接口升级验收脚本 用例组合1001+1002+1003+1004+1005。 安装nodejs sdk 本篇以 [代码]wechatpay-axios-plugin[代码] 这款npm开发包展开,详细介绍可阅读 从APIv3到APIv2再到企业微信,这款微信支付开发包的README你应该来读一读。 [代码]npm install wechatpay-axios-plugin@"v0.5.5" [代码] v0.6系列做了[代码]返回数据签名强校验[代码],以下示例代码需要做特殊处理,本篇以v0.5.5展开。 获取沙箱密钥 先要理解,沙箱环境是个仿真环境,不是生产环境,友商朋友们应该做环境隔离。起步需要商户使用生产环境的 [代码]API密钥[代码],去获取[代码]沙箱密钥[代码],后续所有[代码]沙箱环境[代码]操作都要使用由[代码]沙箱密钥[代码]生成的[代码]数据签名sign[代码]。 [代码]const { Wechatpay, Formatter } = require('wechatpay-axios-plugin'); const mchid = '你的商户号'; const secret = '你的32字节的`API密钥`字符串'; const appid = '你的APPID字符串'; const mch_id = mchid; const noop = {serial: 'any', privateKey: 'any', certs: {any: undefined}}; // 实例化一个对象 let wxpay = new Wechatpay({ secret, mchid, ...noop }); const { data: { sandbox_signkey } } = await wxpay.v2.sandboxnew.pay.getsignkey({ mch_id, nonce_str: Formatter.nonce() }); [代码] 1001 付款码(刷卡)支付 订单金额 [代码]5.01[代码] 元,其中 [代码]0.01[代码] 元使用免充值券,用户实际支付 [代码]5.00[代码] 元。验证商户具备正确解析及识别免充值代金券字段的能力。 1001.1 请求支付 [代码]// 重新实例化一个沙箱环境的对象 wxpay = new Wechatpay({ secret: sandbox_signkey, mchid, ...noop }); // 模拟一个商户订单号 let out_trade_no = `SD${+new Date()}_501`; const { data: { coupon_fee, settlement_total_fee, total_fee } } = await wxpay .v2.sandboxnew.pay.micropay({ appid, mch_id, nonce_str: Formatter.nonce(), out_trade_no, body: 'dummybot', total_fee: 501, spbill_create_ip: '127.0.0.1', auth_code: '120061098828009406' }); console.table({ 代金券金额: coupon_fee, 应结订单金额: settlement_total_fee, 订单金额: total_fee }); [代码] 打印日志应如下: [代码]┌──────┬─────┐ │ (index) │ Values │ ├──────┼─────┤ │ 代金券金额 │ '1' │ │应结订单金额 │ '500' │ │ 订单金额 │ '501' │ └──────┴─────┘ [代码] 1001.2 获取支付结果 [代码]const { data: { settlement_total_fee, total_fee, coupon_fee, coupon_fee_0, coupon_type_0, coupon_count, } } = await wxpay .v2.sandboxnew.pay.orderquery({ appid, mch_id, nonce_str: Formatter.nonce(), out_trade_no, }); console.table({ 代金券金额: coupon_fee, 应结订单金额: settlement_total_fee, 订单金额: total_fee, 单个代金券支付金额: coupon_fee_0, 代金券类型: coupon_type_0, 代金券使用数量: coupon_count }); [代码] 打印日志应如下: [代码]┌──────────┬──────┐ │ (index) │ Values │ ├──────────┼──────┤ │ 代金券金额 │ '1' │ │ 应结订单金额 │ '500' │ │ 订单金额 │ '501' │ │单个代金券支付金额 │ '1' │ │ 代金券类型 │ 'NO_CASH'│ │ 代金券使用数量 │ '1' │ └──────────┴──────┘ [代码] 1002 付款码(刷卡)支付退款 订单金额 [代码]5.02[代码] 元,其中 [代码]0.01[代码] 元使用免充值代金劵,实际支付 [代码]5.01[代码] 元,退款查询升级。 1002.1 请求支付 [代码]//模拟重置一个商户订单号 out_trade_no = `SD${+new Date()}_502`; const { data: { coupon_fee, settlement_total_fee, total_fee, } } = await wxpay .v2.sandboxnew.pay.micropay({ appid, mch_id, nonce_str: Formatter.nonce(), out_trade_no, body: 'dummybot', total_fee: 502, spbill_create_ip: '127.0.0.1', auth_code: '120061098828009406' }); console.table({ 代金券金额: coupon_fee, 应结订单金额: settlement_total_fee, 订单金额: total_fee }); [代码] 打印日志应如下: [代码]┌────────┬─────┐ │ (index) │ Values │ ├────────┼─────┤ │ 代金券金额 │ '1' │ │ 应结订单金额 │ '501' │ │ 订单金额 │ '502' │ └────────┴─────┘ [代码] 1002.2 获取支付结果 [代码]const { data: { settlement_total_fee, total_fee, coupon_fee, coupon_fee_0, coupon_type_0, coupon_count } } = await wxpay .v2.sandboxnew.pay.orderquery({ appid, mch_id, nonce_str: Formatter.nonce(), out_trade_no, }); console.table({ 商户订单号: out_trade_no, 代金券金额: coupon_fee, 应结订单金额: settlement_total_fee, 订单金额: total_fee, 单个代金券支付金额: coupon_fee_0, 代金券类型: coupon_type_0, 代金券使用数量: coupon_count }); [代码] 打印日志应如下: [代码]┌──────────┬────────────┐ │ (index) │ Values │ ├──────────┼────────────┤ │ 商户订单号 │'SD1618966329677_502'│ │ 代金券金额 │ '1' │ │ 应结订单金额 │ '501' │ │ 订单金额 │ '502' │ │单个代金券支付金额│ '1' │ │ 代金券类型 │ 'NO_CASH' │ │ 代金券使用数量 │ '1' │ └──────────┴─────────────┘ [代码] 1002.3 请求退款 [代码]const { data: { cash_refund_fee, cash_fee, refund_fee, total_fee } } = await wxpay .v2.sandboxnew.pay.refund({ appid, mch_id, nonce_str: Formatter.nonce(), out_trade_no, out_refund_no: `RD${out_trade_no}`, total_fee: 502, refund_fee: 501, }); console.table({ 退款金额: refund_fee, 标价金额: total_fee, 现金支付金额: cash_fee, 现金退款金额: cash_refund_fee, }); [代码] 打印日志应如下: [代码]┌────────┬─────┐ │ (index) │ Values │ ├────────┼────┤ │ 退款金额 │ '502' │ │ 标价金额 │ '502' │ │ 现金支付金额 │ '501' │ │ 现金退款金额 │ '501' │ └───────┴─────┘ [代码] 1002.4 获取退款结果 [代码]const { data: { settlement_total_fee, total_fee, cash_fee, settlement_refund_fee, coupon_refund_fee_0, coupon_type_0_0, coupon_refund_fee_0_0, refund_fee_0, coupon_refund_count_0, } } = await wxpay .v2.sandboxnew.pay.refundquery({ appid, mch_id, out_trade_no, nonce_str: Formatter.nonce(), }); console.table({ 应结订单金额: settlement_total_fee, 订单金额: total_fee, 现金支付金额: cash_fee, 退款金额: settlement_refund_fee, 总代金券退款金额: coupon_refund_fee_0, 代金券类型: coupon_type_0_0, 总代金券退款金额: coupon_refund_fee_0_0, 申请退款金额: refund_fee_0, 退款代金券使用数量: coupon_refund_count_0, }); [代码] 1003 JSAPI/APP/Native支付 订单金额 [代码]5.51[代码] 元,其中 [代码]0.01[代码] 元使用免充值券,实际支付 [代码]5.50[代码] 元。 验证正常支付流程,商户使用免充值代金券支付。 1003.1 统一下单 [代码]//模拟重置一个商户订单号 out_trade_no = `SD${+new Date()}_551`; const {data: { prepay_id } } = await wxpay .v2.sandboxnew.pay.unifiedorder({ appid, mch_id, out_trade_no, nonce_str: Formatter.nonce(), body: 'dummybot', total_fee: 551, notify_url: 'https://www.weixin.qq.com/wxpay/pay.php', spbill_create_ip: '127.0.0.1', trade_type: 'JSAPI' }); console.table({ 预支付交易会话标识: prepay_id }); [代码] 1003.2 获取支付结果 [代码]const { data: { out_trade_no, total_fee, cash_fee, coupon_fee, coupon_count, coupon_fee_0, coupon_type_0, coupon_fee_0 } } = wxpay .v2.sandboxnew.pay.orderquery({ appid, mch_id, out_trade_no, nonce_str: Formatter.nonce(), }); console.table({ out_trade_no, total_fee, cash_fee, coupon_fee, coupon_count, coupon_fee_0, coupon_type_0, coupon_fee_0 }); [代码] 1004 JSAPI/APP/Native支付退款 订单金额 [代码]5.52[代码] 元,其中 [代码]0.01[代码] 元使用免充值券,实际支付 [代码]5.51[代码] 元。 1004.1 统一下单 [代码]//模拟重置一个商户订单号 out_trade_no = `SD${+new Date()}_551`; const {data: { prepay_id } } = await wxpay .v2.sandboxnew.pay.unifiedorder({ appid, mch_id, out_trade_no, nonce_str: Formatter.nonce(), body: 'dummybot', total_fee: 552, notify_url: 'https://www.weixin.qq.com/wxpay/pay.php', spbill_create_ip: '127.0.0.1', trade_type: 'JSAPI' }); console.table({ 预支付交易会话标识: prepay_id }); [代码] 1004.2 获取支付结果 [代码]const { data: { out_trade_no, total_fee, cash_fee, coupon_fee, coupon_count, coupon_fee_0, coupon_type_0, coupon_fee_0 } } = wxpay .v2.sandboxnew.pay.orderquery({ appid, mch_id, out_trade_no, nonce_str: Formatter.nonce(), }); console.table({ out_trade_no, total_fee, cash_fee, coupon_fee, coupon_count, coupon_fee_0, coupon_type_0, coupon_fee_0 }); [代码] 1004.3 请求退款 [代码]const { data: { cash_refund_fee, cash_fee, refund_fee, total_fee } } = await wxpay .v2.sandboxnew.pay.refund({ appid, mch_id, nonce_str: Formatter.nonce(), out_trade_no, out_refund_no: `RD${out_trade_no}`, total_fee: 552, refund_fee: 551, }); console.table({ 退款金额: refund_fee, 标价金额: total_fee, 现金支付金额: cash_fee, 现金退款金额: cash_refund_fee, }); [代码] 打印日志应如下: [代码]┌───────┬────┐ │ (index) │ Values│ ├───────┼────┤ │ 退款金额 │ '552' │ │ 标价金额 │ '552'│ │现金支付金额│ '551' │ │现金退款金额│ '551' │ └──────┴────┘ [代码] 1004.4 获取退款结果 [代码]const { data: { settlement_total_fee, total_fee, cash_fee, settlement_refund_fee, coupon_refund_fee_0, coupon_type_0_0, coupon_refund_fee_0_0, refund_fee_0, coupon_refund_count_0, } } = await wxpay .v2.sandboxnew.pay.refundquery({ appid, mch_id, out_trade_no, nonce_str: Formatter.nonce(), }); console.table({ 应结订单金额: settlement_total_fee 订单金额: total_fee 现金支付金额: cash_fee, 退款金额: settlement_refund_fee 总代金券退款金额: coupon_refund_fee_0 代金券类型: coupon_type_0_0, 总代金券退款金额: coupon_refund_fee_0_0 申请退款金额: refund_fee_0 退款代金券使用数量: coupon_refund_count_0, }); [代码] 1005 交易对账单下载 使用了免充值券的订单,免充值券部分的金额不计入结算金额。验证商户对账能正确理解到这一点,对账无误。这里预期会返回 [代码]1269[代码] 条明细数据。 汇总结果:总交易单数,应结订单总金额,退款总金额,充值券退款总金额,手续费总金额,订单总金额,申请退款金额。 这里数据应为: [代码]1269, `10.79, `5.93, `0.24, `0.0,`11.27, `6.37 [代码] 以生产标准为例,查询当前时间偏移两天以前的账单: [代码]const { data } = await wxpay .v2.sandboxnew.pay.downloadbill({ appid, mch_id, bill_type: 'ALL', bill_date: ( new Date(+new Date() + (8 - 48)*3600*1000) ).toISOString().slice(0, 10), nonce_str: Formatter.nonce(), }, { transformResponse: [] }); console.table(data); [代码] 打印日志形如: [代码]交易时间,公众账号ID,商户号,子商户号,设备号,微信订单号,商户订单号,用户标识,交易类型,交易状态,付款银行,货币种类,应结订单金额,代金券金额,微信退款单号,商户退款单号,退款金额,充值券退款金额,退款类型,退款状态,商品名称,商户数据包,手续费,费率,订单金额,申请退款金额,费率备注 `2016-05-04 02:18:18,`wxf7c30a8258df4208,`10014843,`0,`harryma007,`4.00123E+27,`autotest_20160501030456_45023,`oT2kauIMXH398DZBeJ4m22CuSDQ0,`NATIVE,`REFUND,`PAB_DEBIT,`CNY,`0,`0,`2.00123E+27,`REF4001232001201605015390231647,`0.01,`0,`ORIGINAL,`PROCESSING,`body中文测试,`attach中文测试,`0,`0.60%,`0,`0.01,` `2016-05-04 02:18:18,`wxf7c30a8258df4208,`10014843,`0,`harryma007,`4.00123E+27,`autotest_20160501060418_79156,`oT2kauIMXH398DZBeJ4m22CuSDQ0,`NATIVE,`REFUND,`PAB_DEBIT,`CNY,`0,`0,`2.00123E+27,`REF4001232001201605015391766944,`0.01,`0,`ORIGINAL,`PROCESSING,`body中文测试,`attach中文测试,`0,`0.60%,`0,`0.01,` ... 14:51:51,`wxf7c30a8258df4208,`10014843,`0,`harryma8888,`4.00968E+27,`wxautotest1462344441,`oT2kauGtJag902bjdvevrJbpGuxo,`NATIVE,`SUCCESS,`CMBC_CREDIT,`CNY,`0.05,`0.01,`0,`0,`0,`0,`中文[body],`测试中文[attach],`0,`0.60%,`0.05,`0,` 总交易单数,应结订单总金额,退款总金额,充值券退款总金额,手续费总金额,订单总金额,申请退款总金额 1269,`10.79,`5.93,`0.24,`0,`11.27,`6.37 [代码] 一铲到底 以下是一键验收全流程的一个[代码]魔性 chain[代码]到底的实现,感谢您阅读至此。 [图片] 本文如果对你开通 「免充代金券」 功能有帮助,那就来个赞呗。
2021-05-10 - 从APIv3到APIv2再到企业微信,这款微信支付开发包的README你应该来读一读
The WeChatPay OpenAPI v2&v3’ Smart Development Kit [图片][图片][图片][图片][图片][图片] 主要功能 使用Node原生[代码]crypto[代码]实现微信支付APIv3的AES加/解密功能([代码]aes-256-gcm[代码] with [代码]aad[代码]) 使用Node原生[代码]crypto[代码]实现微信支付APIv3的RSA加/解密、签名、验签功能([代码]sha256WithRSAEncryption[代码] with [代码]RSA_PKCS1_OAEP_PADDING[代码]) 大部分微信支付APIv3的HTTP GET/POST/PUT/PATCH/DELETE应该能够正常工作,依赖 Axios, 示例代码如下 支持微信支付APIv3的媒体文件上传(图片/视频)功能,需手动安装 form-data, 示例代码如下 支持微信支付APIv3的应答证书下载功能,需手动安装 yargs, 使用手册如下 支持微信支付APIv3的帐单下载及解析功能,示例代码如下 支持微信支付APIv2 & APIv3面向对象编程模式,示例代码如下 支持 [代码]Typescript[代码] 支持微信支付XML风格的接口(通常所说v2)调用,依赖 node-xml2js, 示例代码如下 支持微信支付APIv2版的 [代码]AES-256-ECB/PKCS7PADDING[代码] 通知消息加/解密 APIv2 & APIv3 与微信交互的各种数据签名用法示例 支持 企业微信-企业支付-企业红包/向员工付款 功能,示例用法及代码如下 系统要求 NodeJS原生[代码]crypto[代码]模块,自v12.9.0在 [代码]publicEncrypt[代码] 及 [代码]privateDecrypt[代码] 增加了 [代码]oaepHash[代码] 入参选项,本类库封装的 [代码]Rsa.encrypt[代码] 及 [代码]Rsa.decrypt[代码] 显式声明了此入参,测试下来在NodeJS10.15.0上可正常工作;虽然在v10.15上可用,不过仍旧推荐使用 NodeJS >= v12.9.0。 安装 [代码]$ npm install wechatpay-axios-plugin[代码] 起步 v3平台证书 微信支付APIv3使用 (RESTful API with JSON over HTTP)接口设计,数据交换采用非对称([代码]RSA-OAEP[代码])加/解密方案。 API上行所需的[代码]商户API私钥[代码],可以由商户官方专用证书生成工具生成, API下行所需的[代码]平台证书[代码]须从[代码]v3/certificates[代码]接口获取(应答证书还经过了对称[代码]AES-GCM[代码]加密,须采用[代码]APIv3密钥[代码]才能解密)。 本项目也提供了命令行下载工具,使用手册如下: [代码]$ ./node_modules/.bin/wxpay crt --help[代码] [代码]wxpay crt The WeChatPay APIv3's Certificate Downloader cert -m, --mchid The merchant's ID, aka mchid. [string] [required] -s, --serialno The serial number of the merchant's certificate aka serialno. [string] [required] -f, --privatekey The path of the merchant's private key certificate aka privatekey. [string] [required] -k, --key The secret key string of the merchant's APIv3 aka key. [string] [required] -o, --output Path to output the downloaded WeChatPay's platform certificate(s) [string] [default: "/tmp"] Options: --version Show version number [boolean] --help Show help [boolean] -u, --baseURL The baseURL [string] [default: "https://api.mch.weixin.qq.com/"] [代码] 注: 像其他通用命令行工具一样,[代码]--help[代码] 均会打印出帮助手册,说明档里的[代码][required][代码]指 必选参数; [代码][string][代码]指 字符串类型,[代码][default][代码]指默认值 [代码]$ ./node_modules/.bin/wxpay crt</b> -m N -s S -f F.pem -k K -o .[代码] [代码]The WeChatPay Platform Certificate#0 serial=HEXADECIAL notBefore=Wed, 22 Apr 2020 01:43:19 GMT notAfter=Mon, 21 Apr 2025 01:43:19 GMT Saved to: wechatpay_HEXADECIAL.pem You may confirm the above infos again even if this library already did(by Rsa.verify): openssl x509 -in wechatpay_HEXADECIAL.pem -noout -serial -dates [代码] 注: 提供必选参数且运行后,屏幕即打印出如上信息,提示[代码]证书序列号[代码]及[代码]起、止格林威治(GMT)时间[代码]及证书下载保存位置。 命令行请求 v0.5版,命令行工具做了加强,增加了基础请求方法,可以用来做快速接入体验,用法如下: 帮助信息 [代码]$ ./node_modules/.bin/wxpay req --help[代码] [代码]wxpay req <uri> Play the WeChatPay OpenAPI requests over command line <uri> -c, --config The configuration [required] -b, --binary Point out the response as `arraybuffer` [boolean] -m, --method The request HTTP verb [choices: "DELETE", "GET", "POST", "PUT", "PATCH", "delete", "get", "post", "put", "patch"] [default: "POST"] -h, --headers Special request HTTP header(s) -d, --data The request HTTP body -p, --params The request HTTP query parameter(s) Options: --version Show version number [boolean] --help Show help [boolean] -u, --baseURL The baseURL [string] [default: "https://api.mch.weixin.qq.com/"] [代码] v3版Native付 [代码]./node_modules/.bin/wxpay v3.pay.transactions.native \ -c.mchid 1230000109 \ -c.serial HEXADECIAL \ -c.privateKey /path/your/merchant/mchid.key \ -c.certs.HEXADECIAL /path/the/platform/certificates/HEXADECIAL.pem \ -d.appid wxd678efh567hg6787 \ -d.mchid 1230000109 \ -d.description 'Image形象店-深圳腾大-QQ公仔' \ -d.out_trade_no '1217752501201407033233368018' \ -d.notify_url 'https://www.weixin.qq.com/wxpay/pay.php' \ -d.amount.total 100 \ -d.amount.currency CNY [代码] v2版付款码付 [代码]./node_modules/.bin/wxpay v2.pay.micropay \ -c.mchid 1230000109 \ -c.serial any \ -c.privateKey any \ -c.certs.any \ -c.secret your_merchant_secret_key_string \ -d.appid wxd678efh567hg6787 \ -d.mch_id 1230000109 \ -d.device_info 013467007045764 \ -d.nonce_str 5K8264ILTKCH16CQ2502SI8ZNMTM67VS \ -d.detail 'Image形象店-深圳腾大-QQ公仔' \ -d.spbill_create_ip 8.8.8.8 \ -d.out_trade_no '1217752501201407033233368018' \ -d.total_fee 100 \ -d.fee_type CNY \ -d.auth_code 120061098828009406 [代码] v2版付款码查询openid [代码]./node_modules/.bin/wxpay v2/tools/authcodetoopenid \ -c.mchid 1230000109 \ -c.serial any \ -c.privateKey any \ -c.certs.any \ -c.secret your_merchant_secret_key_string \ -d.appid wxd678efh567hg6787 \ -d.mch_id 1230000109 \ -d.nonce_str 5K8264ILTKCH16CQ2502SI8ZNMTM67VS \ -d.auth_code 120061098828009406 [代码] 面向对象模式 本类库自[代码]0.2[代码]开始,按照 [代码]URL.pathname[代码] 以[代码]/[代码]做切分,映射成对象属性,[代码]0.4[代码]版开始,支持APIv2的[代码]pathname[代码]映射,编码书写方式有如下约定: 请求 [代码]pathname[代码] 作为级联对象,可以轻松构建请求对象,例如 [代码]v3/pay/transactions/native[代码] 即自然翻译成 [代码]v3.pay.transactions.native[代码]; 每个 [代码]pathname[代码] 所支持的 [代码]HTTP METHOD[代码],即作为 请求对象的末尾执行方法,例如: [代码]v3.pay.transactions.native.post({})[代码]; 每个 [代码]pathname[代码] 级联对象默认为HTTP[代码]POST[代码]函数,其同时隐式内置[代码]GET/POST/PUT/PATCH/DELETE[代码] 操作方法链,支持全大写及全小写两种编码方式,说明见[代码]变更历史[代码]; 每个 [代码]pathname[代码] 有中线(dash)分隔符的,可以使用驼峰[代码]camelCase[代码]风格书写,例如: [代码]merchant-service[代码]可写成 [代码]merchantService[代码],或者属性风格,例如 [代码]v3['merchant-service'][代码]; 每个 [代码]pathname[代码] 中,若有动态参数,例如 [代码]business_code/{business_code}[代码] 可写成 [代码]business_code.$business_code$[代码] 或者属性风格书写,例如 [代码]business_code['{business_code}'][代码],抑或按属性风格,直接写值也可以,例如 [代码]business_code['2000001234567890'][代码]; SDK内置的 [代码]v2/[代码] 对象,其特殊标识为APIv2级联对象,之后串接切分后的[代码]pathname[代码],如源 [代码]pay/micropay[代码] 翻译成 [代码]v2.pay.micropay[代码] 即以XML形式请求远端接口; 建议 [代码]pathname[代码] 按照 [代码]PascalCase[代码] 风格书写, [代码]TS Definition[代码] 已在路上(还有若干问题没解决),将是这种风格,代码提示将会很自然; 以下示例用法,均以[代码]Promise[代码]或[代码]Async/Await[代码]结合此种编码模式展开,级联对象操作符的调试信息见文档末。 初始化 [代码]const {Wechatpay, Formatter} = require('wechatpay-axios-plugin') const wxpay = new Wechatpay({ // 商户号 mchid: 'your_merchant_id', // 商户证书序列号 serial: 'serial_number_of_your_merchant_public_cert', // 商户API私钥 PEM格式的文本字符串或者文件buffer privateKey: '-----BEGIN PRIVATE KEY-----\n-FULL-OF-THE-FILE-CONTENT-\n-----END PRIVATE KEY-----', certs: { // CLI `wxpay crt -m {商户号} -s {商户证书序列号} -f {商户API私钥文件路径} -k {APIv3密钥(32字节)} -o {保存地址}` 生成 'serial_number': '-----BEGIN CERTIFICATE-----\n-FULL-OF-THE-FILE-CONTENT-\n-----END CERTIFICATE-----', }, // APIv2密钥(32字节) v0.4 开始支持 secret: 'your_merchant_secret_key_string', // 接口不要求证书情形,例如仅收款merchant对象参数可选 merchant: { // 商户证书 PEM格式的文本字符串或者文件buffer cert: '-----BEGIN CERTIFICATE-----\n-FULL-OF-THE-FILE-CONTENT-\n-----END CERTIFICATE-----', // 商户API私钥 PEM格式的文本字符串或者文件buffer key: '-----BEGIN PRIVATE KEY-----\n-FULL-OF-THE-FILE-CONTENT-\n-----END PRIVATE KEY-----', // or // passphrase: 'your_merchant_id', // pfx: fs.readFileSync('/your/merchant/cert/apiclient_cert.p12'), }, // APIv2沙箱环境地址 // baseURL: 'https://api.mch.weixin.qq.com/sandboxnew/', // 建议初始化设置此参数,详细说明见Axios官方README // maxRedirects: 0, }) [代码] 初始化字典说明如下: [代码]mchid[代码] 为你的商户号,一般是10字节纯数字 [代码]serial[代码] 为你的商户证书序列号,一般是40字节字符串 [代码]privateKey[代码] 为你的商户API私钥,一般是通过官方证书生成工具生成的文件名是[代码]apiclient_key.pem[代码]文件,支持纯字符串或者文件流[代码]buffer[代码]格式 [代码]certs{[serial_number]:string}[代码] 为通过下载工具下载的平台证书[代码]key/value[代码]键值对,键为平台证书序列号,值为平台证书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[代码]格式 注: 0.4版本做了重构及优化,APIv2&v3以及Axios初始参数,均融合在一个型参上。 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)) [代码] 查询订单 [代码]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)) [代码] 关闭订单 [代码]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)) [代码] 合单支付下单 [代码]wxpay.v3.combineTransactions.jsapi .post({/*文档参数放这里就好*/}) .then(res => console.info(res.data)) .catch(({response: {status, statusText, data}}) => console.error(status, statusText, data)) [代码] H5下单 [代码]wxpay.v3.pay.transactions.h5 .post({/*文档参数放这里就好*/}) .then(({data: {h5_url}}) => console.info(h5_url)) .catch(console.error) [代码] 对账单下载及解析 [代码]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) }) [代码] 创建商家券 [代码]wxpay.v3.marketing.busifavor.stocks .post({/*商家券创建条件*/}) .then(({data}) => console.info(data)) .catch(({response: {status, statusText, data}}) => console.error(status, statusText, data)) [代码] 查询用户单张券详情 [代码];(async () => { try { const {data: detail} = await wxpay.v3.marketing.busifavor .users.$openid$.coupons['{coupon_code}'].appids['wx233544546545989'] .get({openid: '2323dfsdf342342', coupon_code: '123446565767'}) console.info(detail) } catch({response: {status, statusText, data}}) { console.error(status, statusText, data) } } [代码] 服务商模式Native下单 [代码];(async () => { try { const res = await wxpay.v3.pay.partner.transactions.native({ sp_appid, sp_mchid, sub_mchid, description, out_trade_no, time_expire: new Date( (+new Date) + 33*60*1000 ), //after 33 minutes attach, notify_url, amount: { total: 1, } }) console.info(res.data.code_url) } catch (error) { console.error(error) } })() [代码] 支付即服务 [代码];(async () => { try { const {status, statusText} = await wxpay.v3.smartguide.guides.$guide_id$.assign .post({sub_mchid, out_trade_no}, {guide_id}) console.info(status, statusText) } catch({response: {status, statusText, data}}) { console.error(status, statusText, data) } } [代码] 商业投诉查询 [代码];(async () => { try { const res = await wxpay.v3.merchantService.complaints.get({ params: { limit : 5, offset : 0, begin_date : '2020-03-07', end_date : '2020-03-14', } }) console.info(res.data) } catch (error) { console.error(error) } })() [代码] 图片上传 [代码]const FormData = require('form-data') const {createReadStream} = require('fs') const imageMeta = { filename: 'hellowechatpay.png', // easy calculated by the command `sha256sum hellowechatpay.png` on OSX // or by require('wechatpay-axios-plugin').Hash.sha256(filebuffer) sha256: '1a47b1eb40f501457eaeafb1b1417edaddfbe7a4a8f9decec2d330d1b4477fbe', } const imageData = new FormData() imageData.append('meta', JSON.stringify(imageMeta), {contentType: 'application/json'}) imageData.append('file', createReadStream('./hellowechatpay.png')) ;(async () => { try { const res = await wxpay.v3.marketing.favor.media.imageUpload.post(imageData, { meta: imageMeta, headers: imageData.getHeaders() }) console.info(res.data.media_url) } catch (error) { console.error(error) } })() [代码] 查询优惠券详情 [代码];(async () => { try { const res = await wxpay.v3.marketing.favor.stocks.$stock_id$.post({ params: { stock_creator_mchid, }, stock_id, }) console.info(res.data) } catch(error) { console.error(error) } })() [代码] 优惠券委托营销 [代码](async () => { try { const res = await wxpay.v3.marketing.partnerships.build.post({ partner: { type, appid }, authorized_data: { business_type, stock_id } }, { headers: { [`Idempotency-Key`]: 12345 } }) console.info(res.data) } catch (error) { console.error(error) } })() [代码] 优惠券核销记录下载 [代码](async () => { try { let res = await wxpay.v3.marketing.favor.stocks.$stock_id$.useFlow.get({stock_id}) res = await wxpay.v3.billdownload.file.get({ params: (new URL(res.data.url)).searchParams, responseType: 'arraybuffer' }) // 备注:此接口下载的文件格式与商户平台下载的不完全一致,Formatter.castCsvBill解析有差异 console.info(res.data.toString()) } catch (error) { console.error(error) } })() [代码] 视频文件上传 [代码]const FormData = require('form-data') const {createReadStream} = require('fs') const videoMeta = { filename: 'hellowechatpay.mp4', // easy calculated by the command `sha256sum hellowechatpay.mp4` on OSX // or by require('wechatpay-axios-plugin').Hash.sha256(filebuffer) sha256: '1a47b1eb40f501457eaeafb1b1417edaddfbe7a4a8f9decec2d330d1b4477fbe', } const videoData = new FormData() videoData.append('meta', JSON.stringify(videoMeta), {contentType: 'application/json'}) videoData.append('file', createReadStream('./hellowechatpay.mp4')) ;(async () => { try { const res = await wxpay.v3.merchant.media.video_upload.post(videoData, { meta: videoMeta, headers: videoData.getHeaders() }) console.info(res.data.media_id) } catch (error) { console.error(error) } })() [代码] GZIP下载资金账单 [代码]const {unzipSync} = require('zlib') const assert = require('assert') const {Hash: {sha1}} = require('wechatpay-axios-plugin') ;(async () => { try { const {data: {download_url, hash_value}} = await wxpay.v3.bill.fundflowbill.GET({ params: { bill_date: '2020-02-12', bill_type: 'BASIC', tar_type: 'GZIP', } }) const {data} = await wxpay.v3.billdownload.file.GET({ params: (new URL(download_url)).searchParams, responseType: 'arraybuffer' }) // note here: previous `hash_value` was about the source `csv`, not the `gzip` data // so it needs unziped first, then to compare the `SHA1` degest const bill = unzipSync(data) assert.ok(hash_value === sha1(bill.toString()), 'SHA1 verification failed') console.info(Formatter.castCsvBill(bill)) } catch (error) { console.error(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(({response: {status, statusText, data}}) => console.error(status, statusText, data)) [代码] H5支付 [代码]wxpay.v2.pay.unifiedorder({ appid: 'wx2421b1c4370ec43b', attach: '支付测试', body: 'H5支付测试', mch_id: '10000100', nonce_str: Formatter.nonce(), notify_url: 'http://wxpay.wxutil.com/pub_v2/pay/notify.v2.php', openid: 'oUpF8uMuAJO_M2pxb1Q9zNjWeS6o', out_trade_no: '1415659990', spbill_create_ip: '14.23.150.211', total_fee: 1, trade_type: 'MWEB', scene_info: JSON.stringify({ h5_info: { type:"IOS", app_name: "王者荣耀", package_name: "com.tencent.tmgp.sgame" } }), }).then(({data: {mweb_url}}) => console.info(mweb_url)).catch(console.error); [代码] 申请退款 [代码]wxpay.v2.secapi.pay.refund.post({ appid: 'wx8888888888888888', mch_id: '1900000109', out_trade_no: '1217752501201407033233368018', out_refund_no: '1217752501201407033233368018', total_fee: 100, refund_fee: 100, refund_fee_type: 'CNY', nonce_str: Formatter.nonce(), }) .then(res => console.info(res.data)) .catch(({response: {status, statusText, data}}) => console.error(status, statusText, data)) [代码] 现金红包 [代码]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.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(({response: {status, statusText, data}}) => console.error(status, statusText, data)) [代码] 企业付款到银行卡-获取RSA公钥 [代码]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(({response: {status, statusText, data}}) => console.error(status, statusText, data)) [代码] 企业微信 企业微信的企业支付,数据请求包需要额外的签名,仅需做如下简单扩展适配,即可支持;以下签名注入函数所需的两个参数[代码]agentId[代码] [代码]agentSecret[代码]来自企业微信工作台,以下为示例值。 [代码]const agentId = 1001001 const agentSecret = 'from_wework_agent_special_string' const {Hash} = require('wechatpay-axios-plugin') [代码] 企业红包-注入签名规则 [代码]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 }) [代码] 发放企业红包 [代码]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) [代码] 向员工付款-注入签名规则 [代码]Wechatpay.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({ 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 日志 Wechatpay.client.v2.defaults.transformRequest.push(data => (console.log(data), data)) Wechatpay.client.v2.defaults.transformResponse.unshift(data => (console.log(data), data)) // APIv3 日志 Wechatpay.client.v3.defaults.transformRequest.push((data, headers) => (console.log(data, headers), data)) Wechatpay.client.v3.defaults.transformResponse.unshift((data, headers) => (console.log(data, headers), data)) [代码] 获取RSA公钥 非标准接口地址,也可以这样调用 [代码]Wechatpay.client.v2.post('https://fraud.mch.weixin.qq.com/risk/getpublickey', { mch_id: '1900000109', nonce_str: Formatter.nonce(), sign_type: 'HMAC-SHA256', }) .then(({data}) => console.info(data)) .catch(({response}) => console.error(response)) [代码] 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: APIv3消息通知,[代码]AES-256-GCM[代码]加密字段,应该如何解密? 官方文档有介绍,APIv3平台证书及消息通知关键信息均使用[代码]AesGcm[代码]加解密,依赖[代码]APIv3密钥[代码],商户侧解密可参考[代码]bin/cli/cert.js[代码]证书下载工具,例如: [代码]AesGcm.decrypt(nonce, secret, ciphertext, 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 文末打印一波示例方法链 [代码][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] { '1217752501201407033233368018': [Function: v3/pay/transactions/out-trade-no/1217752501201407033233368018] } }, partner: [Function: v3/pay/partner] { transactions: [Function: v3/pay/partner/transactions] { native: [Function: v3/pay/partner/transactions/native] } } }, marketing: [Function: v3/marketing] { busifavor: [Function: v3/marketing/busifavor] { stocks: [Function: v3/marketing/busifavor/stocks], users: [Function: v3/marketing/busifavor/users] { '$openid$': [Function: v3/marketing/busifavor/users/{openid}] { coupons: [Function: v3/marketing/busifavor/users/{openid}/coupons] { '{coupon_code}': [Function: v3/marketing/busifavor/users/{openid}/coupons/{coupon_code}] { appids: [Function: v3/marketing/busifavor/users/{openid}/coupons/{coupon_code}/appids] { wx233544546545989: [Function: v3/marketing/busifavor/users/{openid}/coupons/{coupon_code}/appids/wx233544546545989] } } } } } }, 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] } }, combineTransactions: [Function: v3/combine-transactions] { jsapi: [Function: v3/combine-transactions/jsapi] }, bill: [Function: v3/bill] { tradebill: [Function: v3/bill/tradebill], fundflowbill: [Function: v3/bill/fundflowbill] }, billdownload: [Function: v3/billdownload] { file: [Function: v3/billdownload/file] }, 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] } } }, merchantService: [Function: v3/merchant-service] { complaints: [Function: v3/merchant-service/complaints] }, merchant: [Function: v3/merchant] { media: [Function: v3/merchant/media] { video_upload: [Function: v3/merchant/media/video_upload] } } } } [代码] 变更历史 v0.5.5 (2021-04-13) 优化文档,[代码]证书[代码]相关名词与官方文档保持一致 优化代码,使用ES6 [代码]Reflect.set[代码]代替[代码]param-reassign[代码],性能更高 新增函数[代码]Hash.hmac[代码]方法,广度支持[代码]Hash-based Message Authentication Code[代码] 调整函数[代码]Hash.hmacSha256[代码]为不推荐方法,内部改写为固定[代码]Hash.hmac[代码]调用 调整CLI [代码]req <uri>[代码]成功调用仅返回[代码]{config, headers, data}[代码]数据结构 v0.5.4 (2021-04-08) 优化CLI,[代码]wxpay crt[代码] 下载平台证书仅在成功验签完成后写入文件 优化文档,[代码]AesGcm[代码] 解密示例 优化内部[代码]chain[代码]逻辑,遵循 [代码]RFC3986[代码] 规范,[代码]baseURL[代码]支持带部分路径的海外接入点 优化代码[代码]SonarQube[代码]检测结果[代码]3A+0.5%[代码] v0.5.3 优化CLI,[代码]wxpay <uri>[代码] 向前兼容以支持slash(/)结尾的请求,形如 [代码]v3/applyment4sub/applyment/[代码] v0.5.2 优化CLI,[代码]wxpay <uri>[代码] 现在支持型如 [代码]v2.pay.micropay[代码], [代码]v3.pay.transactions.native[代码] 调用 优化[代码]README[代码]文档,适配最新CLI用法;增加APIv3消息通知QA章节;增加技术交流QQ群说明 v0.5.1 优化CLI,可以直接 [代码]wxpay <uri>[代码] 发起请求 优化[代码]README[代码]文档,适配最新CLI用法 v0.5.0 新增命令行方式与微信支付接口交互工具 调整可选依赖包为[代码]peerDependencies[代码],使用完整功能需手动安装 [代码]form-data[代码] 或/及 [代码]yargs[代码] v0.4.6 使用最新版[代码]eslint[代码]及[代码]eslint-config-airbnb-base[代码] 增加[代码]utils.merge[代码]依赖函数测试校验 v0.4.5 支持APIv2版的俩账单下载,调用方法与APIv3类同; 增加测试用例覆盖,初始化参数[代码]secret[代码](for APIv2)如未设置,[代码]HMAC-SHA256[代码]数据签名时,可能引发 #14 v0.4.4 优化[代码]Wechatpay[代码]在多次实例化时赋值[代码]Symbol(CLIENT)[代码]异常问题,增加[代码]wechatpay.test.js[代码]测试用例覆盖 v0.4.3 支持 企业微信-企业支付 链式调用,需要额外注入签名规则,见上述文档用法示例 v0.4.2 文件名大小写问题 #11 感谢 @LiuXiaoZhuang 报告此问题 v0.4.1 解决了一个[代码]AES-GCM[代码]在[代码]Node10[代码]上的解密兼容性问题,程序在[代码]Node10[代码]上有可能崩溃,建议[代码]Node10[代码]用户升级至此版本 v0.4.0 重构 [代码]Wechatpay[代码] 类,同时支持 APIv2&v3’s 链式调用 改变 [代码]Wechatpay.client[代码] 返回值为[代码]Wechatpay.client.v3[代码],[代码]Wechatpay.client.v2[代码] 为 [代码]xmlBased[代码] 接口客户端 废弃 [代码]withEntities[代码] 方法,其在链式多次调用时,有可能达不到预期,详情见 #10,感谢 @ali-pay 报告此问题 README 文档中文化 完善补缺 [代码]tsd[代码] 声明 v0.3.4 Typed and tips on [代码]Wechatpay[代码] class(#9), thanks @ipoa v0.3.3 Upgrade Axios for the CVE-2020-28168 v0.3.2 Optim: Let [代码]Aes.pkcs7.padding[代码] strictly following the [代码]rfc2315[代码] spec Optim: Better of the [代码]Hash.md5[代码] and [代码]Hash.hmacSha256[代码] Coding comments and README v0.3.1 Optim: new param on [代码]xmlBased({mchid})[代码], while passed in, then [代码]Transformer.signer[代码] doing the [代码]assert[代码] with the post data. Feature: Customize the HTTP [代码]User-Agent[代码]. Refactor: Split [代码]aes.js[代码] as of [代码]Aes[代码], [代码]AesGcm[代码] and [代码]AesEcb[代码] classes for [代码]aes-256-ecb/pkcs7padding[代码] algo. v0.3.0 Feature: The XML based API requests. v0.2.3 Optim: Coding quality. v0.2.2 Fix: #8 [代码]verfier[代码] on the [代码]204[代码] status case. v0.2.1 Optim: Back compatible for [代码]12.4.0[代码] < [代码]Node[代码] ≧ [代码]10.15.0[代码]. v0.2.0 Feature: [代码]OOP[代码] developing style of the wechatpay APIv3. v0.1.0 Optim: Toggle the [代码]Nodejs[代码] version ≧ [代码]10.15.0[代码]. Optim: Documentation and coding comments. v0.0.9 Feature: definition of the [代码]Typescript[代码] v0.0.8 Optim: on [代码]castCsvBill[代码], drop the [代码]trim[代码] on each rows Optim: on [代码]response[代码] validation, checking ± 5 mins first then to [代码]Rsa.verify[代码] Optim: moved the [代码]commander[代码] optional dependency, because it’s only for the [代码]CLI[代码] tool Feature: shipped 99 tests([代码]npm test[代码]) v0.0.7 Feature: billdownload and castCsvBill eslint enabled ([代码]npm run lint[代码]) v0.0.6 Chinese document v0.0.5 Renew document and codes comments v0.0.4 Feature: certificate downloader, deps on [代码]commander[代码] v0.0.3 Feature: media file upload, optional deps on [代码]form-data[代码] v0.0.2 Feature: Assert the response’s timestamp ± 5 mins Refactor as CommonJS style(#6) Limits the communicating parameters(#7) Coding styles(#5) Coding comments and Document(#4, #3, #2, #1) v0.0.1 Init ES2015+ style License The MIT License (MIT) Copyright © 2020-2021 James ZHANG(TheNorthMemory) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
2021-05-10 - 真香:一行命令即可体验「微信支付」全系接口能力
你没看错,这款唯二的「Play the OpenAPI requests over command line」——「以命令行方式与微信支付接口交互」姗姗来了,旨在提供一种简洁高效的接入方式,希望能对开发者有所帮助。 nodejs版的 [代码]wechatpay-axios-plugin[代码] 已经进入[代码]v0.5[代码]版,v0.5做了这些改变: 全部代码(包括265条测试用例)使用 [代码]eslint-config-airbnb-base[代码] 代码风格校验; 使用 [代码]yargs[代码] 包替换了 [代码]commander[代码],重构了「平台证书下载器」工具,使其降级为此命令行一个特殊方法; 降级 [代码]form-data[代码] 及 [代码]yargs[代码] 包为 [代码]peerDependencies[代码],没有这俩包,90%+ 的接口是可以正常工作的,缩减依赖; 新增 [代码]bin/cli.js[代码],开发可以仅用一条命令就能跑得欢实不要不要的了; 使用 开始使用前,需要开发者自行安装[代码]cli[代码]模式依赖的npm包,即 [代码]npm i yargs[代码] 帮助手册 [代码]./node_modules/.bin/wxpay --help[代码] [代码]wxpay <command> Commands: wxpay crt The WeChatPay APIv3s Certificate Downloader wxpay req <uri> Play the OpenAPI requests over command line Options: --version Show version number [boolean] --help Show help [boolean] -u, --baseURL The baseURL [string] [default: "https://api.mch.weixin.qq.com/"] for more information visit "https://github.com/TheNorthMemory/wechatpay-axios-plugin" [代码] [代码]./node_modules/.bin/wxpay crt --help[代码] [代码]wxpay crt The WeChatPay APIv3s Certificate Downloader cert -m, --mchid The merchants ID. [string] [required] -s, --serialno The serial number. [string] [required] -f, --privatekey Path of the merchants private key certificate. [string] [required] -k, --key The secret key string of the merchants APIv3. [string] [required] -o, --output Path to output the platform certificate(s) [string] [default: "/tmp"] Options: --version Show version number [boolean] --help Show help [boolean] -u, --baseURL The baseURL [string] [default: "https://api.mch.weixin.qq.com/"] [代码] [代码]./node_modules/.bin/wxpay req --help[代码] [代码]wxpay req <uri> Play the WeChatPay OpenAPI requests over command line request <uri> -c, --config The configuration [required] -b, --binary Point out the response as `arraybuffer` [boolean] -m, --method The request HTTP verb [default: "POST"] -h, --headers Special request HTTP header(s) -d, --data The request HTTP body -p, --params The request HTTP query parameter(s) Options: --version Show version number [boolean] --help Show help [boolean] -u, --baseURL The baseURL [string] [default: "https://api.mch.weixin.qq.com/"] [代码] 证书下载 [代码]wxpay crt -m N -s S -f F.pem -k K -o .[代码] [代码]The WeChatPay Platform Certificate#0 serial=HEXADECIAL notBefore=Wed, 22 Apr 2020 01:43:19 GMT notAfter=Mon, 21 Apr 2025 01:43:19 GMT Saved to: wechatpay_HEXADECIAL.pem You may confirm the above infos again even if this library already did(by Rsa.verify): openssl x509 -in wechatpay_HEXADECIAL.pem -noout -serial -dates [代码] v3版Native下单 [代码]wxpay req v3/pay/transactions/native \ -c.mchid 1230000109 \ -c.serial HEXADECIAL \ -c.privateKey /path/your/merchant/mchid.key \ -c.certs.HEXADECIAL /path/the/platform/certificates/HEXADECIAL.pem \ -d.appid wxd678efh567hg6787 \ -d.mchid 1230000109 \ -d.description 'Image形象店-深圳腾大-QQ公仔' \ -d.out_trade_no '1217752501201407033233368018' \ -d.notify_url 'https://www.weixin.qq.com/wxpay/pay.php' \ -d.amount.total 100 \ -d.amount.currency CNY [代码] v3版查询订单 [代码]wxpay req v3/pay/transactions/id/1217752501201407033233368018 \ -c.mchid 1230000109 \ -c.serial HEXADECIAL \ -c.privateKey /path/your/merchant/mchid.key \ -c.certs.HEXADECIAL /path/the/platform/certificates/HEXADECIAL.pem \ -m get \ -p.mchid 1230000109 [代码] v3版关闭订单 [代码]wxpay req v3/pay/transactions/out-trade-no/1217752501201407033233368018 \ -c.mchid 1230000109 \ -c.serial HEXADECIAL \ -c.privateKey /path/your/merchant/mchid.key \ -c.certs.HEXADECIAL /path/the/platform/certificates/HEXADECIAL.pem \ -d.mchid 1230000109 [代码] v3版申请对账单 [代码]wxpay req v3/bill/tradebill \ -c.mchid 1230000109 \ -c.serial HEXADECIAL \ -c.privateKey /path/your/merchant/mchid.key \ -c.certs.HEXADECIAL /path/the/platform/certificates/HEXADECIAL.pem \ -m get \ -p.bill_date '2021-02-12' \ -p.bill_type 'ALL' [代码] v2版付款码付 [代码]wxpay req v2/pay/micropay \ -c.mchid 1230000109 \ -c.serial any \ -c.privateKey any \ -c.certs.any \ -c.secret your_merchant_secret_key_string \ -d.appid wxd678efh567hg6787 \ -d.mch_id 1230000109 \ -d.device_info 013467007045764 \ -d.nonce_str 5K8264ILTKCH16CQ2502SI8ZNMTM67VS \ -d.detail 'Image形象店-深圳腾大-QQ公仔' \ -d.spbill_create_ip 8.8.8.8 \ -d.out_trade_no '1217752501201407033233368018' \ -d.total_fee 100 \ -d.fee_type CNY \ -d.auth_code 120061098828009406 [代码] [代码]auth_code[代码] 输入配合扫码枪,体验就飞起来了~ v2版付款码查询openid [代码]wxpay req v2/tools/authcodetoopenid \ -c.mchid 1230000109 \ -c.serial any \ -c.privateKey any \ -c.certs.any \ -c.secret your_merchant_secret_key_string \ -d.appid wxd678efh567hg6787 \ -d.mch_id 1230000109 \ -d.nonce_str 5K8264ILTKCH16CQ2502SI8ZNMTM67VS \ -d.auth_code 120061098828009406 [代码] 设计 主程序 [代码]bin/cli.js[代码] 上,加入了一个中间件,代码如下: [代码].middleware((argv) => { if (argv.c && argv.c.privateKey && argv.c.privateKey !== 'any') { /* eslint-disable-next-line no-param-reassign */ argv.config.privateKey = readFileSync(argv.c.privateKey); } if (argv.c && argv.c.certs && Object.keys(argv.c.certs)[0] !== 'any') { /* eslint-disable-next-line no-return-assign, no-param-reassign, no-sequences */ argv.config.certs = Object.entries(argv.config.certs).reduce((o, [k, v]) => (o[k] = readFileSync(v), o), {}); } if (argv.c && argv.c.merchant) { if (argv.c.merchant.cert && argv.c.merchant.cert !== 'any') { /* eslint-disable-next-line no-param-reassign */ argv.config.merchant.cert = readFileSync(argv.c.merchant.cert); } if (argv.c.merchant.key && argv.c.merchant.key !== 'any') { /* eslint-disable-next-line no-param-reassign */ argv.config.merchant.key = readFileSync(argv.c.merchant.key); } if (argv.c.merchant.pfx && argv.c.merchant.pfx !== 'any') { /* eslint-disable-next-line no-param-reassign */ argv.config.merchant.pfx = readFileSync(argv.c.merchant.pfx); } } }, true) [代码] 其作用就是,把v3所需的证书以文件形式自动加载,供后续 [代码]cli/request.js[代码]方法直接使用,[代码]req[代码]方法核心代码也就10行! [代码]handler(argv) { const { baseURL, uri, config, method, data, params, headers, } = argv; const responseType = argv.binary ? 'arraybuffer' : undefined; const structure = [{ params, headers, responseType }]; if (data) { structure.unshift(data); } (new Wechatpay({ baseURL, ...config }))[uri][method](...structure) /* eslint-disable-next-line no-console */ .then(console.info).catch(console.error); }, [代码] 代码以 MIT 开放源码公开在 https://github.com/TheNorthMemory/wechatpay-axios-plugin ,如果喜欢,就来个 Star 。 后记 Q: 哇!这么简单? A: 不就是为了简单么。。。
2021-06-10 - wechatpay-axios-plugin 开发系列之「起步」
从github issues可以感知到,本SDK已有案例被应用到云开发及自主开发环境,从开发者角度来看,此类库还有些难以架弩,本系列将作为开发对接指导,希望能对开发有所帮助。 [图片] 起步 说起开发,最忌讳的是急急火火的先堆代码,作为一款有性格的SDK,源码全程是英文注释,README我可以用母语来书写,有些同学可能对此不适应,编码讲究的是效率,这里就不展开了。 这里还有一些思考,是关于「起步」,即:如何快速的对接「微信支付」接口,下面我们来展开一下 前置条件 注册微信支付商户号 登录商户平台,设置[代码]API密钥[代码] 使用官方专用证书生成工具,生成商户证书 设置API密钥及[代码]APIv3密钥[代码] 有一个nodejs开发环境,版本最低要求是10.15.0,windows、macos、linux操作系统均支持 懂得基本的npm包管理 开始 基于一些考虑,如云开发生产环境,依赖项决定了启动速度,这里需要区分一下生产环境及开发环境。面向生产环境,此款类库仅严格依赖两个包:[代码]axios[代码]及[代码]node-xml2js[代码],衍生总计仅需6个包;面向开发环境需要额外增加[代码]yargs[代码]包。此文以开发环境展开。 准备环境 准备一个工作目录,使用 [代码]npm init[代码] 先构建一个基础环境,如已有环境,则略过此步。 安装软件 基础包 [代码]npm install wechatpay-axios-plugin --save[代码] CLI依赖包 [代码]npm install yargs --save-dev[代码] 如需媒体上传,如「制券」及「拓展商户」,则需再安装 [代码]form-data[代码] 依赖 [代码]npm install form-data --save[代码] 平台证书 APIv3开发多了一个步骤,就是需要自行下载「平台证书」,本类库提供有下载器,执行命令如下: [代码]./node_modules/.bin/wxpay crt -m {商户号} -s {商户证书序列号} -f {商户私钥证书路径} -k {APIv3密钥(32字节)} -o {保存地址}[代码] 花括弧内为变量,按需自行替换,执行结果类似如下: [代码]The WeChatPay Platform Certificate#0 serial=HEXADECIAL notBefore=Wed, 22 Apr 2020 01:43:19 GMT notAfter=Mon, 21 Apr 2025 01:43:19 GMT Saved to: wechatpay_HEXADECIAL.pem You may confirm the above infos again even if this library already did(by Rsa.verify): openssl x509 -in wechatpay_HEXADECIAL.pem -noout -serial -dates [代码] 快速体验 接续上一篇: 一行命令即可体验「微信支付」全系接口能力,现在就可以在开发环境上,验证及调试官方接口了,额外地, v0.5.1做了优化,命令行现在可以这么执行了: [代码]./node_modules/.bin/wxpay v2/pay/micropay \ -c.mchid 1230000109 \ -c.serial any \ -c.privateKey any \ -c.certs.any \ -c.secret your_merchant_secret_key_string \ -d.appid wxd678efh567hg6787 \ -d.mch_id 1230000109 \ -d.device_info 013467007045764 \ -d.nonce_str 5K8264ILTKCH16CQ2502SI8ZNMTM67VS \ -d.detail 'Image形象店-深圳腾大-QQ公仔' \ -d.spbill_create_ip 8.8.8.8 \ -d.out_trade_no '1217752501201407033233368018' \ -d.total_fee 100 \ -d.fee_type CNY \ -d.auth_code 120061098828009406 [代码] 以上述命令行为例,此命令请求的是v2版的「付款码支付」接口,[代码]any[代码] 为CLI模式保留字,意思即无需此参数(不能不给),[代码]1230000109[代码] 替换为你的商户号,[代码]your_merchant_secret_key_string[代码] 替换为你的[代码]API密钥[代码],这条命令就可以验证支付能力了。屏幕打印的日志类似如下: [代码]{ status: 200, statusText: 'OK', headers: { server: 'nginx', date: 'Tue, 30 Mar 2021 00:49:30 GMT', 'content-type': 'text/plain', 'content-length': '105', connection: 'keep-alive', 'keep-alive': 'timeout=8', 'request-id': '089AEB89830610A10518B9BF8C5820EA442882F601-47002, 089AEB89830610F6041896C2EEA30620F98004289CF805-0', 'mmlas-verifyresult': 'CAA=' }, config: { url: '/pay/micropay', method: 'post', data: '<xml><appid>wxd678efh567hg6787</appid><mch_id>1230000109</mch_id><device_info>013467007045764</device_info><nonce_str>5K8264ILTKCH16CQ2502SI8ZNMTM67VS</nonce_str><detail>Image形象店-深圳腾大-QQ公仔</detail><spbill_create_ip>8.8.8.8</spbill_create_ip><out_trade_no>1217752501201407033233368018</out_trade_no><total_fee>100</total_fee><fee_type>CNY</fee_type><auth_code>120061098828009406</auth_code><sign>C5524E8CEB11D1060A9FF0E696F52F51</sign></xml>', headers: { Accept: 'application/json, text/plain, */*', 'Content-Type': 'text/xml; charset=utf-8', 'User-Agent': 'wechatpay-axios-plugin/0.5.1 axios/0.21.1 node/14.14.0 darwin/x64', 'Content-Length': 458 }, baseURL: 'https://api.mch.weixin.qq.com/', transformRequest: [ [Function: signer], [Function: toXml] ], transformResponse: [ [Function: toObject], [Function: verifier] ], timeout: 0, adapter: [Function: httpAdapter], responseType: 'text', xsrfCookieName: 'XSRF-TOKEN', xsrfHeaderName: 'X-XSRF-TOKEN', maxContentLength: -1, maxBodyLength: -1, httpsAgent: Agent { _events: [Object: null prototype], _eventsCount: 2, _maxListeners: undefined, defaultPort: 443, protocol: 'https:', options: [Object], requests: {}, sockets: {}, freeSockets: [Object], keepAliveMsecs: 1000, keepAlive: true, maxSockets: Infinity, maxFreeSockets: 256, scheduling: 'fifo', maxTotalSockets: Infinity, totalSocketCount: 0, maxCachedSessions: 100, _sessionCache: [Object], [Symbol(kCapture)]: false }, validateStatus: [Function: validateStatus], mchid: 1230000109, serial: 'any', privateKey: 'any', certs: { any: true }, secret: 'your_*********************tring' }, request: <ref *1> ClientRequest { _events: [Object: null prototype] { socket: [Function (anonymous)], abort: [Function (anonymous)], aborted: [Function (anonymous)], connect: [Function (anonymous)], error: [Function (anonymous)], timeout: [Function (anonymous)], prefinish: [Function: requestOnPrefinish] }, _eventsCount: 7, _maxListeners: undefined, outputData: [], outputSize: 0, writable: true, destroyed: true, _last: false, chunkedEncoding: false, shouldKeepAlive: true, _defaultKeepAlive: true, useChunkedEncodingByDefault: true, sendDate: false, _removedConnection: false, _removedContLen: false, _removedTE: false, _contentLength: null, _hasBody: true, _trailer: '', finished: true, _headerSent: true, socket: TLSSocket { _tlsOptions: [Object], _secureEstablished: true, _securePending: false, _newSessionPending: false, _controlReleased: true, secureConnecting: false, _SNICallback: null, servername: 'api.mch.weixin.qq.com', alpnProtocol: false, authorized: true, authorizationError: null, encrypted: true, _events: [Object: null prototype], _eventsCount: 9, connecting: false, _hadError: false, _parent: null, _host: 'api.mch.weixin.qq.com', _readableState: [ReadableState], _maxListeners: undefined, _writableState: [WritableState], allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: undefined, _server: null, ssl: [TLSWrap], _requestCert: true, _rejectUnauthorized: true, parser: null, _httpMessage: null, timeout: 0, [Symbol(res)]: [TLSWrap], [Symbol(verified)]: true, [Symbol(pendingSession)]: null, [Symbol(async_id_symbol)]: -1, [Symbol(kHandle)]: [TLSWrap], [Symbol(kSetNoDelay)]: false, [Symbol(lastWriteQueueSize)]: 0, [Symbol(timeout)]: null, [Symbol(kBuffer)]: null, [Symbol(kBufferCb)]: null, [Symbol(kBufferGen)]: null, [Symbol(kCapture)]: false, [Symbol(kBytesRead)]: 0, [Symbol(kBytesWritten)]: 0, [Symbol(connect-options)]: [Object], [Symbol(RequestTimeout)]: undefined }, _header: 'POST /pay/micropay HTTP/1.1\r\n' + 'Accept: application/json, text/plain, */*\r\n' + 'Content-Type: text/xml; charset=utf-8\r\n' + 'User-Agent: wechatpay-axios-plugin/0.5.1 axios/0.21.1 node/14.14.0 darwin/x64\r\n' + 'Content-Length: 458\r\n' + 'Host: api.mch.weixin.qq.com\r\n' + 'Connection: keep-alive\r\n' + '\r\n', _keepAliveTimeout: 0, _onPendingData: [Function: noopPendingOutput], agent: Agent { _events: [Object: null prototype], _eventsCount: 2, _maxListeners: undefined, defaultPort: 443, protocol: 'https:', options: [Object], requests: {}, sockets: {}, freeSockets: [Object], keepAliveMsecs: 1000, keepAlive: true, maxSockets: Infinity, maxFreeSockets: 256, scheduling: 'fifo', maxTotalSockets: Infinity, totalSocketCount: 0, maxCachedSessions: 100, _sessionCache: [Object], [Symbol(kCapture)]: false }, socketPath: undefined, method: 'POST', maxHeaderSize: undefined, insecureHTTPParser: undefined, path: '/pay/micropay', _ended: true, res: IncomingMessage { _readableState: [ReadableState], _events: [Object: null prototype], _eventsCount: 3, _maxListeners: undefined, socket: null, httpVersionMajor: 1, httpVersionMinor: 1, httpVersion: '1.1', complete: true, headers: [Object], rawHeaders: [Array], trailers: {}, rawTrailers: [], aborted: false, upgrade: false, url: '', method: null, statusCode: 200, statusMessage: 'OK', client: [TLSSocket], _consuming: false, _dumped: false, req: [Circular *1], responseUrl: 'https://api.mch.weixin.qq.com/pay/micropay', redirects: [], [Symbol(kCapture)]: false, [Symbol(RequestTimeout)]: undefined }, aborted: false, timeoutCb: null, upgradeOrConnect: false, parser: null, maxHeadersCount: null, reusedSocket: false, host: 'api.mch.weixin.qq.com', protocol: 'https:', _redirectable: Writable { _writableState: [WritableState], _events: [Object: null prototype], _eventsCount: 2, _maxListeners: undefined, _options: [Object], _ended: true, _ending: true, _redirectCount: 0, _redirects: [], _requestBodyLength: 458, _requestBodyBuffers: [], _onNativeResponse: [Function (anonymous)], _currentRequest: [Circular *1], _currentUrl: 'https://api.mch.weixin.qq.com/pay/micropay', [Symbol(kCapture)]: false }, [Symbol(kCapture)]: false, [Symbol(kNeedDrain)]: false, [Symbol(corked)]: 0, [Symbol(kOutHeaders)]: [Object: null prototype] { accept: [Array], 'content-type': [Array], 'user-agent': [Array], 'content-length': [Array], host: [Array] } }, data: { return_code: 'FAIL', return_msg: '缺少参数' } } [代码] 当你遇到困难,需要社区帮助的时候,类库也做了安全规范,把[代码]secret[代码]做了脱敏处理,上述日志你可以无脑地直接拷贝粘贴过来(再无脑也需要注意格式,以[代码]javascript[代码]代码形式粘贴,会方便阅读)。 海外主体 同样适用,在CLI驱动时,仅需在给一个 [代码]--baseURL[代码] 参数即可,如香港: 下载平台证书 [代码]./node_modules/.bin/wxpay crt \ --baseURL 'https://apihk.mch.weixin.qq.com' \ -m {商户号} \ -s {商户证书序列号} \ -f {商户私钥证书路径} \ -k {APIv3密钥(32字节)} \ -o {保存地址} [代码] 付款码支付 [代码]./node_modules/.bin/wxpay v2/pay/micropay \ --baseURL 'https://apihk.mch.weixin.qq.com' \ -c.mchid 1230000109 \ -c.serial any \ -c.privateKey any \ -c.certs.any \ -c.secret your_merchant_secret_key_string \ -d.appid wxd678efh567hg6787 \ -d.mch_id 1230000109 \ -d.device_info 013467007045764 \ -d.nonce_str 5K8264ILTKCH16CQ2502SI8ZNMTM67VS \ -d.detail 'Image形象店-深圳腾大-QQ公仔' \ -d.spbill_create_ip 8.8.8.8 \ -d.out_trade_no '1217752501201407033233368018' \ -d.total_fee 100 \ -d.fee_type CNY \ -d.auth_code 120061098828009406 [代码] 官方接口至少70%以上的接口,均可以在CLI模式上做快速体验,验证入参出参、调试,甚至可以作为CI工具集,希望能为你的开发带来帮助。 下一回 正统WEB编程。
2021-03-30 - 如何用十个月时间,做出一款迄今为止无法超越的微信支付SDK基础开发包
20年中在对接商家券的时候,翻了官方PHP SDK的牌子,0.1系缺个媒体文件上传比较优雅的解决方案,于是顺手就为其提了PR。之后,我就在思考,动态编程语言讲究的快,是不是应该可以在做点儿好玩的,有意义的,可以加速开发对接的,不被人抱怨的开发包呢。索性,仗着多年的从业经验,尝试从底层开始写,不断迭代,于是就有了这么一款小巧,低依赖,符合规范,面向对象模式,命令行模式,适用涵盖Windows、macOS、Linux的微信支付nodejs版基础开发包。 下面我以版本迭代时间线为过程,来讲讲这款SDK开发包的开发故事。 v0.0.1 初始版本 这个版本,发布于2020年6月26号,核心是3个类,4个文件共计11个方法,已经是完整实现了APIv3的技术规范:[代码]AES-GCM[代码]对称加解密,[代码]RSA-OAEP[代码]非对称加解密及验签规范。[代码]RSA-OAEP[代码]的原生公钥加密、私钥解密方法封装,是翻过无数遍nodejs的文档,最后精炼到各5行代码共计10行,这应该是nodejs中,[代码]RSA-OAEP[代码]加解密原生实现的最早封装。 v0.1.0 正式版本 这个系列,发布于2020年7月2号。在申城最热的一段时间,晚上睡不着就捋代码呗,哔哩啪啦猛敲代码(其实也没多少行代码,全被精简了),迭代了共计十个版本,于是就有了这么一个正式版。 v0.2 面向对象模式开发 这个系列,发布于2020年7月7号,止于2020年9月9号,四个版本。重要功能就是可以顺溜地使用动态对象来对接APIv3接口了,同时降低NodeJS版本兼容至10.15(这个是云开发的环境版本)。详细可阅读微信支付APIv3的Nodejs版SDK,让开发变得简单不再繁琐。 v0.3 支持APIv2调用 这个系列,发布于2020年9月11号,止于2021年1月22号。是个被“逼”出来的系列。这个系列加入了APIv2版的接口驱动,重要功能就是对「付款码支付」及「退款」能力的支持。详细可阅读适合云开发的微信支付v2及v3版Nodejs SDK,这就不展开了。在这一段时间里,有太多事情要跟进,所以一直没有太多时间写代码。展期了好久好久才进入了重要的下一重构时刻。 v0.4 同质化v2&v3 OOP开发 这个系列,发布于2021年2月28号,止于2021年3月25日,五个版本。这个系列的由来是因一位同学提的issue展开的,于是做了超量重构,不仅改写了入口程序,还加强了APIv2版的同质化调用,详细可阅读微信支付开发,可以简化到复制+粘贴这种地步 及纯属意外,还可以这么起底微信支付接口对接。 [图片] 有那么一瞬间,我以为这个版本就会是终点了,可以进入LTS版仅需长期维护了。殊不知,CLI工具集还有一漏勺没有完善好,那好吧,那就再来一波加强重组呗。 v0.5 命令行工具集 这个系列,发布于2021年3月27号,加强了命令行工具集,请求微信支付官方接口,现在可以在命令行上跑起来了。英文slogan就是: Play the OpenAPI requests over command line。 这得解释解释,为啥用“Play”呢,而不是其他词儿呢?其实这个功能早在20年10月,chain支付宝OpenAPI接口时,脑阔开了个洞,想出来的。微信支付的CLI工具已经迟到好久了,现在好了,两款驰名的支付平台,都可以如丝质般顺溜,搞起CI了。这就是play的由来——为开发而生: commit to play for running。 说说功能点,这个系列就加强了命令行方式与接口交互能力,同时降级了自0.0.4版就一直存在的 「平台证书」下载器 为工具集内一条特殊命令。详细玩法可阅读 开发系列之「起步」 及 真香:一行命令即可体验「微信支付」全系接口能力。 另外,这个加强重构版的CLI交互工具,从设计一开始就支持扩展能力,有兴趣的同学完全可以基于[代码]bin/cli/request.js[代码]构建出更多子命令,不干扰纯应用,跑着欢实就好。 NEXT 如果还有NEXT,那可能就是得把tsd文件翻新一下,[代码]ProxyConstructor[代码]那块是[代码]TypeScript[代码] 自4.0引入,动态属性类型签名的高级甚高级用法,还不太会,学习中,如果有娴熟的同学,可以提PR贡献一下。 哦,对了,文章如果看着还不错,SDK用着还顺手,那就star一下repo吧 https://github.com/TheNorthMemory/wechatpay-axios-plugin MIT开源,纯免费,随便用。做开源不易,况且还是为国民应用做底层基础开发包就更是难啊,平时又太忙,只能是按需来做,有空再续。
2021-05-10 - AES-GCM在Node10解密消息时有可能崩溃,使用云开发环境的同学需要进来看一下
背景知识 [代码]AES-GCM[代码]是微信支付APIv3的加解密方案之一,定义可见rfc5116,v3使用的是[代码]aead_aes_256_gcm[代码]。稍微补充一个[代码]aead[代码]的的描述,[代码]aead[代码]加密方式与其他对称加密方式主要不同的地方就是: 它每一段密文必定有对应的校验码,通过核对校验码来判断密文是否完整。 APIv3回调通知和平台证书下载文档上有介绍[代码]AES-GCM[代码]的使用场景。nodejs原生[代码]crypto[代码]模块,在处理[代码]GCM[代码]模式解密时,从变更历史上看,[代码]Node11[代码]加入了强制校验[代码]auth_tag[代码](authentication tag)长度规则,[代码]Node10[代码]目前全系列还没有合并这个向前兼容规则,详情可见 https://github.com/nodejs/node/pull/20039 。 测试代码 先上一段测试用js代码,来复现 nodejs#20039 上连带反馈的问题: [代码]const crypto = require('crypto') const decrypt = (ciphertext, key, iv, aad = '') => { const buf = Buffer.from(ciphertext, 'base64') const tag = buf.slice(-16) const payload = buf.slice(0, -16) const decipher = crypto.createDecipheriv( 'aes-256-gcm', key, iv ).setAuthTag(tag).setAAD(Buffer.from(aad)) return Buffer.concat([ decipher.update(payload, 'hex'), decipher.final() ]).toString('utf8') } const mockupIv = 'abcdef0123456789' const mockupKey = 'abcdef0123456789abcdef0123456789' try { decrypt('', mockupKey, mockupIv) } catch {} [代码] 上述代码,在node10.15-10.24,均抛出如下不可捕获的错误(fatal error),程序会直接挂掉,在12-15之间,可以正常运行。 错误日志 类似如下: [代码]node[97219]: ../src/node_crypto.cc:3047:CipherBase::UpdateResult node::crypto::CipherBase::Update(const char *, int, unsigned char **, int *): Assertion `MaybePassAuthTagToOpenSSL()' failed. 1: 0x100d69661 node::Abort() (.cold.1) [/Users/james/.nvm/versions/node/v10.24.0/bin/node] 2: 0x10003aeb4 node_module_register [/Users/james/.nvm/versions/node/v10.24.0/bin/node] 3: 0x100039fb9 node::AddEnvironmentCleanupHook(v8::Isolate*, void (*)(void*), void*) [/Users/james/.nvm/versions/node/v10.24.0/bin/node] 4: 0x100112fae node::StringBytes::InlineDecoder::Decode(node::Environment*, v8::Local<v8::String>, v8::Local<v8::Value>, node::encoding) [/Users/james/.nvm/versions/node/v10.24.0/bin/node] 5: 0x1001119dc node::crypto::CipherBase::Update(v8::FunctionCallbackInfo<v8::Value> const&) [/Users/james/.nvm/versions/node/v10.24.0/bin/node] 6: 0x1002386c3 v8::internal::FunctionCallbackArguments::Call(v8::internal::CallHandlerInfo*) [/Users/james/.nvm/versions/node/v10.24.0/bin/node] 7: 0x100237bae v8::internal::MaybeHandle<v8::internal::Object> v8::internal::(anonymous namespace)::HandleApiCallHelper<false>(v8::internal::Isolate*, v8::internal::Handle<v8::internal::HeapObject>, v8::internal::Handle<v8::internal::HeapObject>, v8::internal::Handle<v8::internal::FunctionTemplateInfo>, v8::internal::Handle<v8::internal::Object>, v8::internal::BuiltinArguments) [/Users/james/.nvm/versions/node/v10.24.0/bin/node] 8: 0x10023728a v8::internal::Builtin_Impl_HandleApiCall(v8::internal::BuiltinArguments, v8::internal::Isolate*) [/Users/james/.nvm/versions/node/v10.24.0/bin/node] 9: 0x37d3d8d5bf3d 10: 0x37d3d8d118d5 11: 0x37d3d8d0a5c3 12: 0x37d3d8d118d5 13: 0x37d3d8d0a5c3 [1] 97218 abort npm test [代码] 上述错误日志,发生在我本地的[代码]Node10[代码]环境中。我花了几个小时,翻了好几遍github issues,最后找到了 nodejs#20039 pull requests,通读下来并反复测试了10.19-10.24版本,均无法正常捕获,这应该是上述pr没合并至[代码]Node10[代码]系列所致。 产生条件 稍微分析一下,可能产生致命错误的条件: 密文为空字符串时,程序会崩 密文为 [代码]Cg==[代码](base64空字符串) CLI会有 Warning DEP0090 弹出 (node:987) [DEP0090] DeprecationWarning: Permitting authentication tag lengths of 1 bytes is deprecated. Valid GCM tag lengths are 4, 8, 12, 13, 14, 15, 16. 微信支付官方文档在解密示例代码 常量定义了这个[代码]auth_tag[代码]长度为128位16字节,匹配rfc5116规范并且取的是最大值。 这下问题来了,万一无法正常获取到待解密字符串或者获取到的是空字符串,[代码]GCM[代码]模式校验码位又必须是16字节,业务逻辑又强依赖解密后字符串(验签证书是v3通讯强依赖)这崩掉了,着急上火的可真就是摊上事儿了! 向前兼容方案 找到问题关键点,那就打个业务逻辑补丁:应用端,对输入待解密字符串,做长度校验,长度为0的,不进入解密函数;或者可以采用如下向前兼容js patch补丁: [代码]- ).setAuthTag(tag).setAAD(Buffer.from(aad)) + ) + + // Restrict valid GCM tag length, patches for Node < 11.0.0 + // more @see https://github.com/nodejs/node/pull/20039 + const tagLen = tag.length + if (tagLen > 16 || (tagLen < 12 && tagLen != 8 && tagLen != 4)) { + let backport = new TypeError(`Invalid authentication tag length: ${tagLen}`) + backport.code = 'ERR_CRYPTO_INVALID_AUTH_TAG' + throw backport + } + decipher.setAuthTag(tag).setAAD(Buffer.from(aad)) [代码] 上述代码取自 wechatpay-axios-plugin@aa36a56,也已随源码用例覆盖[代码]Node[代码]10-15版本,均达预期,可安全使用。 可能的影响面 小程序云开发标配目前是[代码]Node10[代码],不清楚云开发团队在处理[代码]消息通知及关键信息解密[代码]时,是否采用的是轻量化如nodejs原生[代码]crypto[代码]这样的解决方案,这个就需要云产品团队相关的同学进来看看,评估一下有无风险点了。 对自主对接云开发的开发者来说,建议尽快给打下业务逻辑补丁或者程序解密补丁,避免不可预期的错误发生(虽然极小概率,但支付的事,可真不是小事儿)。 题外话 建议云开发平台,能够升级一下[代码]Node10[代码]至最新[代码]lts[代码]运行时,一并建议能同时支持[代码]Node12[代码]、[代码]Node14[代码]运行时。
2021-03-10 - 微信支付开发,可以简化到复制+粘贴这种地步
距离上一版(0.3)过去了有几个月了,这几个月没少折腾微信团队,春节前后“突然”利好不断,许多几乎不可能的事项都有显著改善,这也许就是认(一)真(本)做(正)事(经)的回报吧。2月下旬的时候,有一小伙伴,开了个issue#10 反馈,lib在变量重入的时候,存在bug,无法正确获取订单信息。仔细推演之后,决定重构一下,把树形结构[代码]Wechatpay SDK[代码]起个底儿,许多(BT)用法,真的是事后测试用例覆盖时才发现,啪一下吧。 0.4版做了这些改进 [代码]README[代码]文档中文化 重构 [代码]Wechatpay[代码] 类,同时支持 [代码]APIv2&v3[代码] 链式调用 改变 [代码]Wechatpay.client[代码] 返回值为[代码]Wechatpay.client.v3[代码],[代码]Wechatpay.client.v2[代码] 为 [代码]xmlBased[代码] 接口客户端 废弃 [代码]withEntities[代码] 方法,其在链式多次调用时,有可能达不到预期 解决 [代码]AES-GCM[代码]在[代码]Node10[代码]上的解密兼容性问题,程序在[代码]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 - 纯属意外,还可以这么起底微信支付接口对接
上一扒讲到的,从设计初是按照[代码]URL.pathname[代码]切分出实体entity做属性树扩展,map服务端服务接口;重构完后,再写测试用例的时候,竟然发现了有如下“变态”用法,这不得不也得分享出来,以供絮叨。 再提一次树 以目标函数 [代码][Function: /v2/pay/micropay][代码] 为例,这里有一种更简洁的构建方式,即[代码]Object.property[代码]方式构建,可以把层叠树平展展成类似一层[代码]key/value[代码]结构,树构建代码如下: [代码]// 实例化不表了,这里直接以wxpay代指实例化handler wxpay['v3/pay/transactions/native'] wxpay['v3/pay/transactions/id/{transaction_id}'] wxpay['v3/pay/transactions/out-trade-no/{out_trade_no}'] wxpay['v3/pay/partner/transactions/native'] wxpay['v3/marketing/busifavor/users/{openid}/coupons/{coupon_code}/appids/{appid}'] wxpay['v3/marketing/favor/stocks/{stock_id}/use-flow'] wxpay['v3/marketing/partnerships/build'] wxpay['v3/marketing/favor/media/image-upload'] wxpay['v2/pay/micropay'] wxpay['v2/risk/getpublickey'] wxpay['v2/secapi/pay/refund'] wxpay['v2/mmpaymkttransfers/promotion/transfers'] wxpay['v2/mmpaymkttransfers/promotion/paywwsptrans2pocket'] wxpay['v2/mmpaymkttransfers/sendworkwxredpack'] // ... ;console.info(wxpay) [代码] 打印出的结构如下: [代码][Function (anonymous)] { 'v3/pay/transactions/native': [Function: /v3/pay/transactions/native], 'v3/pay/transactions/id/{transaction_id}': [Function: /v3/pay/transactions/id/{transaction_id}], 'v3/pay/transactions/out-trade-no/{out_trade_no}': [Function: /v3/pay/transactions/out-trade-no/{out_trade_no}], 'v3/pay/partner/transactions/native': [Function: /v3/pay/partner/transactions/native], 'v3/marketing/busifavor/users/{openid}/coupons/{coupon_code}/appids/{appid}': [Function: /v3/marketing/busifavor/users/{openid}/coupons/{coupon_code}/appids/{appid}], 'v3/marketing/favor/stocks/{stock_id}/use-flow': [Function: /v3/marketing/favor/stocks/{stock_id}/use-flow], 'v3/marketing/partnerships/build': [Function: /v3/marketing/partnerships/build], 'v3/marketing/favor/media/image-upload': [Function: /v3/marketing/favor/media/image-upload], 'v3/bill/tradebill': [Function: /v3/bill/tradebill], 'v3/billdownload/file': [Function: /v3/billdownload/file], 'v2/pay/micropay': [Function: /v2/pay/micropay], 'v2/risk/getpublickey': [Function: /v2/risk/getpublickey], 'v2/secapi/pay/refund': [Function: /v2/secapi/pay/refund], 'v2/mmpaymkttransfers/promotion/transfers': [Function: /v2/mmpaymkttransfers/promotion/transfers], 'v2/mmpaymkttransfers/promotion/paywwsptrans2pocket': [Function: /v2/mmpaymkttransfers/promotion/paywwsptrans2pocket], 'v2/mmpaymkttransfers/sendworkwxredpack': [Function: /v2/mmpaymkttransfers/sendworkwxredpack] } [代码] 毫无疑问,这种方式结构看起来相对更简洁一些,相应地,编程语法糖,可以这么写了~ APIv3 Native下单 [代码]wxpay['v3/pay/transactions/native']({ /*文档参数放这里就好*/ }) .then(({data: {code_url}}) => console.info(code_url)) .catch(console.error) [代码] APIv3 查询订单 [代码]wxpay['v3/pay/transactions/id/{transaction_id}'].get({ params: {mchid: '1230000109'}, transaction_id: '1217752501201407033233368018' }) .then(({data}) => console.info(data)) .catch(console.error) [代码] APIv3 关单 [代码]wxpay['v3/pay/transactions/out-trade-no/{out_trade_no}']({ mchid: '1230000109' }, { out_trade_no: '1217752501201407033233368018' } ) .then(({status, statusText}) => console.info(status, statusText)) .catch(console.error) [代码] 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(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) [代码] 现金红包 [代码]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) [代码] 企业付款到零钱 [代码]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) [代码] 企业付款到银行卡-获取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) [代码] 发放企业红包 [代码]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) [代码] 这么搞下去。。。还愁啥对接啊!一致开发体验,尽在: https://github.com/TheNorthMemory/wechatpay-axios-plugin
2021-03-09 - 微信支付APIv3图片、视频上传类接口nodejs纯语言无依赖版参考实现
受官方技术助手伙伴在某次问答亲测有效代码启发,以下ES2015代码为v3上传文件文档补充实现而做,供其他开发语言参考实现。 上传媒体文件API的是基于 RFC2388 协议,圈定的范围是,有大小限制的图片(2M)或者视频(5M)类型单文件,按API接口标准,需要对文件的meta{filename,sha256}信息做数据签名,并且这个meta还须随请求载核一同发送给服务端,难就难在这里了,唉嘘~ 官方文档小分队犯了一个错误,就是试图用文本语言来表达非字符内容,整得一众开发者迷途了,社区反馈波澜滔滔。这支小分队应该每人扣一个长鹅抱宠,捐给像俺这样努力帮扶开发者的贡献者(😄)。其实rfc2388标准上有写,这个协议就是扩展HTTP文本协议,用来传输非字符内容的,引述如下: multipart/form-data can be used for forms that are presented using representations other than HTML (spreadsheets, Portable Document Format, etc), and for transport using other means than electronic mail or HTTP. This document defines the representation of form values independently of the application for which it is used. 上述引述内容,提到3种文件,HTML文件还算是字符型文件,表格及PDF文件就已经算是二进制文件了,文件内容人类得借助专用软件翻译,才能转成可被识别内容(肉眼能直接识别的是大牛,不再此列)。受官方技术助手伙伴在某次问答亲测有效代码截图启示,特意又研读了几遍RFC协议,国庆档给抽出成无依赖ES2015版本,已内置于另一款著名支付产品SDK包中,亲测可用,以下版本是微信支付社区特供,单文件、无依赖,适合云开发集成使用。 废话说了一箩筐,还是上代码吧: [代码]const {extname} = require('path') /** * Simple and lite of `multipart/form-data` implementation, most similar to `form-data` * * ```js * (new Form) * .append('a', 1) * .append('b', '2') * .append('c', Buffer.from('31')) * .append('d', JSON.stringify({}), 'any.json') * .append('e', require('fs').readFileSync('/path/your/file.jpg'), 'file.jpg') * .getBuffer() * ``` */ class Form { /** * Create a `multipart/form-data` buffer container for the file uploading. * * @constructor */ constructor() { Object.defineProperties(this, { /** * built-in mime-type mapping * @type {Object<string,string>} */ mimeTypes: { value: { bmp: `image/bmp`, gif: `image/gif`, png: `image/png`, jpg: `image/jpeg`, jpe: `image/jpeg`, jpeg: `image/jpeg`, mp4: `video/mp4`, mpeg: `video/mpeg`, json: `application/json`, }, configurable: false, enumerable: false, writable: true, }, /** * @type {Buffer} */ dashDash: { value: Buffer.from(`--`), configurable: false, enumerable: false, writable: false, }, /** * @type {Buffer} */ boundary: { value: Buffer.from(`${`-`.repeat(26)}${`0`.repeat(24).replace(/0/g, () => Math.random()*10|0)}`), configurable: false, enumerable: false, writable: false, }, /** * @type {Buffer} */ CRLF: { value: Buffer.from(`\r\n`), configurable: false, enumerable: false, writable: false, }, /** * The Form's data storage * @type {array<Buffer>} */ data: { value: [], configurable: false, enumerable: true, writable: true, }, /** * The entities' value indices whose were in `this.data` * @type {Object<string, number>} */ indices: { value: {}, configurable: false, enumerable: true, writable: true, }, }) } /** * To retrieve the `data` buffer * * @return {Buffer} - The payload buffer */ getBuffer() { return Buffer.concat([ this.dashDash, this.boundary, this.CRLF, ...this.data.slice(0, -2), this.boundary, this.dashDash, this.CRLF, ]) } /** * To retrieve the `Content-Type` multipart/form-data header * * @return {Object<string, string>} - The `Content-Type` header With `this.boundary` */ getHeaders() { return { 'Content-Type': `multipart/form-data; boundary=${this.boundary}` } } /** * Append a customized mime-type(s) * * @param {Object<string,string>} things - The mime-type * * @return {Form} - The `Form` class instance self */ appendMimeTypes(things) { Object.assign(this.mimeTypes, things) return this } /** * Append data wrapped by `boundary` * * @param {string} field - The field * @param {string|Buffer} value - The value * @param {String} [filename] - Optional filename, when provided, then append the `Content-Type` after of the `Content-Disposition` * * @return {Form} - The `Form` class instance self */ append(field, value, filename = '') { const {data, dashDash, boundary, CRLF, mimeTypes, indices} = this data.push(Buffer.from(`Content-Disposition: form-data; name="${field}"${filename && Buffer.isBuffer(value) ? `; filename="${filename}"` : ``}`)) data.push(CRLF) if (filename || Buffer.isBuffer(value)) { data.push(Buffer.from(`Content-Type: ${mimeTypes[extname(filename).substring(1).toLowerCase()] || `application/octet-stream`}`)) data.push(CRLF) } data.push(CRLF) indices[field] = data.push(Buffer.isBuffer(value) ? value : Buffer.from(String(value))) data.push(CRLF) data.push(dashDash) data.push(boundary) data.push(CRLF) return this } } module.exports = Form module.exports.default = Form [代码] 测试用例如下: [代码]lib/form ✓ should be class `Form` new Form ✓ should instanceOf Form and have properties `data` and `indices` ✓ The `mimeTypes` property should be there and only allowed append(cannot deleted) ✓ The `dashDash` Buffer property should be there and cannot be deleted/modified ✓ The `boundary` Buffer property should be there and cannot be deleted/modified ✓ The `CRLF` Buffer property should be there and cannot be deleted/modified ✓ The `data` property should be instanceOf Array and cannot deleted ✓ The `indices` property should be instanceOf Object and cannot deleted ✓ Method `getBuffer()` should returns a Buffer instance and had fixed length(108) default ✓ Method `getHeaders()` should returns a Object[`Content-type`] with `multipart/form-data; boundary=` ✓ Method `appendMimeTypes()` should returns the Form instance ✓ Method `appendMimeTypes({any: 'mock'})` should returns the Form instance, and affected `form.data` property ✓ Method `append()` should returns the Form instance, and affected `form.data` property ✓ Method `append()` should append name="undefined" disposition onto the `form.data` property ✓ Method `append({}, 1)` should append name="[object Object]" disposition onto the `form.data` property ✓ Method `append('meta', JSON.stringify({}), 'meta.json')` should append a `Content-Type: application/json` onto the `form.data` property ✓ Method `append('image_content', Buffer.from('R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', 'base64'), 'demo.gif')` should append a `Content-Type: image/gif` onto the `form.data` property [代码] API文档如下 Simple and lite of [代码]multipart/form-data[代码] implementation, most similar to [代码]form-data[代码] [代码](new Form) .append('a', 1) .append('b', '2') .append('c', Buffer.from('31')) .append('d', JSON.stringify({}), 'any.json') .append('e', require('fs').readFileSync('/path/your/file.jpg'), 'file.jpg') .getBuffer() [代码] Form new Form() .getBuffer() ⇒ [代码]Buffer[代码] .getHeaders() ⇒ [代码]Object.<string, string>[代码] .appendMimeTypes(things) ⇒ [代码]Form[代码] .append(field, value, [filename]) ⇒ [代码]Form[代码] new Form() Create a [代码]multipart/form-data[代码] buffer container for the file uploading. form.getBuffer() ⇒ [代码]Buffer[代码] To retrieve the [代码]data[代码] buffer Kind: instance method of [代码]Form[代码] Returns: [代码]Buffer[代码] - - The payload buffer form.getHeaders() ⇒ [代码]Object.<string, string>[代码] To retrieve the [代码]Content-Type[代码] multipart/form-data header Kind: instance method of [代码]Form[代码] Returns: [代码]Object.<string, string>[代码] - - The [代码]Content-Type[代码] header With [代码]this.boundary[代码] form.appendMimeTypes(things) ⇒ [代码]Form[代码] Append a customized mime-type(s) Kind: instance method of [代码]Form[代码] Returns: [代码]Form[代码] - - The [代码]Form[代码] class instance self Param Type Description things [代码]Object.<string, string>[代码] The mime-type form.append(field, value, [filename]) ⇒ [代码]Form[代码] Append data wrapped by [代码]boundary[代码] Kind: instance method of [代码]Form[代码] Returns: [代码]Form[代码] - - The [代码]Form[代码] class instance self Param Type Description field [代码]string[代码] The field value [代码]string[代码] | [代码]Buffer[代码] The value [filename] [代码]String[代码] Optional filename, when provided, then append the [代码]Content-Type[代码] after of the [代码]Content-Disposition[代码] mimeTypes : [代码]Object.<string, string>[代码] built-in mime-type mapping Kind: global variable dashDash : [代码]Buffer[代码] Kind: global variable boundary : [代码]Buffer[代码] Kind: global variable CRLF : [代码]Buffer[代码] Kind: global variable data : [代码]array.<Buffer>[代码] The Form’s data storage Kind: global variable indices : [代码]Object.<string, number>[代码] The entities’ value indices whose were in [代码]this.data[代码] 此类内置常用的几种文件类型([代码]append[代码]第三入参以文件名后缀比对),已经够用了,视频仅内置了两种,对于 官方接口支持的[代码]avi[代码], [代码]wmv[代码], [代码]mov[代码], [代码]mkv[代码], [代码]flv[代码], [代码]f4v[代码], [代码]m4v[代码], [代码]rmvb[代码],开发者可用透过 [代码]appendMimeTypes[代码] 方法,自行扩展以符合 RFC2388 规范。 图片上传接口,用法 [代码]const form = new Form form.append( 'file', require('fs').readFileSync('/path/your/file.jpg'), 'file.jpg' ) .append( 'meta', JSON.stringify({ filename:'file.jpg', sha256:'779a563f99f824975b3651bfd8597555e69fb135925e460dae3996d47c415fb0' }), 'meta.json' ) [代码] 整个需要发送的表单体就准备妥当了,然后按照v3开发规范,该数据签名的签名,想用什么[代码]client[代码]提交就用什么[代码]client[代码],然后就没然后了。。。 以我习惯用的 [代码]axios[代码] 为例,数据提交类似如下: [代码]const axios = require('axios') //伪代码, baseURL 要按实际接口地址赋值, Authorization 要按v3规范赋值 axios.create({baseURL}).post( form.getBuffer(), {headers: { Authorization: 'WECHATPAY2-SHA256-RSA2048 ', Accept: 'application/json', ...form.getHeaders(), }} ) .catch(({response: {headers, data}}) => ({headers, data})) .then(({headers, data}) => ({headers, data})) .then(console.log) [代码] 写到最后 Form类文件以MIT开源,文章转载请注明出处「来自微信开发者社区」。
2020-10-24 - 适合云开发的微信支付v2及v3版Nodejs SDK
接续微信支付APIv3的Nodejs版SDK,在2020这个时间节点,之所以再造一遍微信支付v2(相对于APIv3来说)的轮子,实属是无奈之举,线下交易场景常见的付款码支付及退款功能,官方当下还没有开放出来v3版的,只能借助v2接口来处理;[代码]wechatpay-axios-plugin[代码] 从一开始目标就是为云原生而设计,遂再造一遍轮子,也抽出一些共生方法,为v3而用。 设计思路 此类库核心部件是利用了Axios的transform功能,数据在内部流转过程中,会经过 [代码]transformRequest[代码] 及 [代码]transformResponse[代码] 处理,通过构造两个自定义transformer,完整实现v2版的技术规格要求,从而完成 HTTP 请求/响应 处理。 Transformer.request 方法返回值是个数组,含两个方法 [代码][signer, toXml][代码],字面意思即,对输入数据签名,然后转换成xml; Transformer.response 方法返回值是个数组,含两个方法 [代码][toObject, verifier][代码],字面意思即,返回值做数据转换为对象,然后校验签名; 证书设置 凡是涉及资金变动的接口,均需要商户证书,此实现同时支持 [代码]pem[代码] 及 [代码]p12[代码] 格式的证书,使用方法见随包README: [代码]const {Wechatpay, Formatter: fmt} = require('wechatpay-axios-plugin') const client = Wechatpay.xmlBased({ secret: 'your_merchant_secret_key_string', 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'), }, }) [代码] 实力化一个 [代码]client[代码] 的最小参数为 [代码]secret[代码],即所谓的 密钥,字符串形式,32字节长度。 自定义打印日志 按需,如果需要检测类库的数据情况,在实例化完成后,可以加入如下类似两段代码,即可以打印出日志;当然也可以按需把日志输出至文件等,抛砖引玉而已。 [代码]//在格式转换完后,打印日志 client.defaults.transformRequest.push(data => (console.log(data), data)) //在请求返回,先行打印日志 client.defaults.transformResponse.unshift(data => (console.log(data), data)) [代码] 使用示例 实例化对象 [代码]secret[代码] 所对应的商户类型,可以是服务商、普通商户、特约商户,入参按照官方文档,手捋填入即可,以下几个方法,均测试过,正常运转。 申请退款 [代码]client.post('/secapi/pay/refund', { appid: 'wx8888888888888888', mch_id: '1900000109', out_trade_no: '1217752501201407033233368018', out_refund_no: '1217752501201407033233368018', total_fee: 100, refund_fee: 100, refund_fee_type: 'CNY', nonce_str: fmt.nonce(), }).then(res => console.info(res.data)).catch(({response}) => console.error(response)) [代码] [代码]//log输入 { return_code: 'SUCCESS', return_msg: 'OK', appid: 'wx8888888888888888', mch_id: '1365319302', nonce_str: 'X8bpYtUJbPHK0Fyd', sign: '12BDC0390958455875108947AD51D897', result_code: 'SUCCESS', transaction_id: '4200000684202009114087736848', out_trade_no: '1217752501201407033233368018', out_refund_no: '1217752501201407033233368018', refund_id: '50300005642020091102621479983', refund_channel: '', refund_fee: '100', coupon_refund_fee: '0', total_fee: '100', cash_fee: '100', coupon_refund_count: '0', cash_refund_fee: '100' } [代码] 付款码支付 [代码]client.post('/pay/micropay', { appid: 'wx8888888888888888', mch_id: '1900000109', nonce_str: fmt.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(({response}) => console.error(response)) [代码] 现金红包 [代码]client.post('/mmpaymkttransfers/sendredpack', { nonce_str: fmt.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}) => console.error(response)) [代码] 企业付款 [代码]client.post('/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: fmt.nonce(), }).then(res => console.info(res.data)).catch(({response}) => console.error(response)) [代码] 和v3一起用 [代码]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-----', } }) [代码] 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)) [代码] 查询订单 [代码]wxpay.v3.pay.transactions.id['{transaction_id}'] .withEntities({transaction_id: '1217752501201407033233368018'}) .get({params: {mchid: '1230000109'}}) .then(({data}) => console.info(data)) .catch(({response: {status, statusText, data}}) => console.error(status, statusText, data)) [代码] 关单 [代码]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)) [代码] 创建商家券 [代码]wxpay.v3.marketing.busifavor.stocks .post({/*商家券创建条件*/}) .then(({data}) => console.info(data)) .catch(({response: {status, statusText, data}}) => console.error(status, statusText, data)) [代码] 查询用户单张券详情 [代码];(async () => { try { const {data: detail} = await wxpay.v3.marketing.busifavor.users.$openid$.coupons['{coupon_code}'].appids['wx233544546545989'] .withEntities({openid: '2323dfsdf342342', coupon_code: '123446565767'}) .get() console.info(detail) } catch({response: {status, statusText, data}}) { console.error(status, statusText, data) } } [代码] 上传图片 [代码]const FormData = require('form-data') const {createReadStream} = require('fs') const imageMeta = { filename: 'hellowechatpay.png', // easy calculated by the command `sha256sum hellowechatpay.png` on OSX // or by require('wechatpay-axios-plugin').Hash.sha256(filebuffer) sha256: '1a47b1eb40f501457eaeafb1b1417edaddfbe7a4a8f9decec2d330d1b4477fbe', } const imageData = new FormData() imageData.append('meta', JSON.stringify(imageMeta), {contentType: 'application/json'}) imageData.append('file', createReadStream('./hellowechatpay.png')) Wechatpay.client.post('/v3/marketing/favor/media/image-upload', imageData, { meta: imageMeta, headers: imageData.getHeaders() }).then(res => { console.info(res.data.media_url) }).catch(error => { console.error(error) }) [代码] 下载账单并格式化 [代码]const assert = require('assert') const {Hash: {sha1}} = require('wechatpay-axios-plugin') Wechatpay.client.get('/v3/bill/tradebill', { params: { bill_date: '2020-06-01', bill_type: 'ALL', } }).then(({data: {download_url, hash_value}}) => Wechatpay.client.get(download_url, { signed: hash_value, responseType: 'arraybuffer', })).then(res => { assert(sha1(res.data) === res.config.signed, 'verify the SHA1 digest failed.') console.info(fmt.castCsvBill(res.data)) }).catch(error => { console.error(error) }) [代码] 委托营销 [代码](async () => { try { const res = await Wechatpay.client.post(`/v3/marketing/partnerships/build`, { partner: { type, appid }, authorized_data: { business_type, stock_id } }, { headers: { [`Idempotency-Key`]: 12345 } }) console.info(res.data) } catch (error) { console.error(error) } })() [代码] 查询投诉信息并解密 [代码];(async () => { try { const res = await Wechatpay.client.get('/v3/merchant-service/complaints', {params: { limit: 50, offset: 0, begin_date: (new Date(+new Date - 29*86400*1000)).toJSON().slice(0, 10), end_date: (new Date).toJSON().slice(0, 10), }}) // decrypt the `Sensitive Information` res.data.data.map(row => (row.payer_phone = rsa.decrypt(row.payer_phone, merchantPrivateKey), row)) console.info(res.data) } catch({response: {status, statusText, data, headers}, request, config}) { console.error(status, statusText, data) } })() [代码] TODO v2版的[代码]AES-256-ECB/PKCS7Padding[代码]未做封装,这个不难,npm上也有许多优秀的类库可用,暂且先这样。 写到最后 MIT开放源码@npm, github ,可用于企业商业用途。
2020-10-16 - 微信支付APIv3的Nodejs版SDK,让开发变得简单不再繁琐
在向云端推送这个 [代码]wechatpay-axios-plugin[代码] 业务实现时,发现0.1系列还不够好用,还需要进行更多层级的包裹包装,遂再次做了重大更新,让SDK使用起来更简单、飘逸。 先看官方文档,每一个接口,文档都至少标示了[代码]请求URL[代码] [代码]请求方式[代码] [代码]请求参数[代码] [代码]返回参数[代码] 这几个要素,[代码]URL[代码] 可以拆分成 [代码]Base[代码] 及 [代码]URI[代码],按照这种思路,封装SDK其实完全就可以不用动脑,即,对[代码]URI[代码]资源的 [代码]POST[代码] 或 [代码]GET[代码] 请求(条件带上[代码]参数[代码]),取得[代码]返回参数[代码]。 更近一步,我们设想一下,如果把众多接口的[代码]URI[代码]按照斜线([代码]/[代码] [代码]slash[代码])分割,然后组织在一起,是不是就可以构建出一颗树,这颗树的每个节点(实体[代码]Entity[代码])都存在有若干个方法([代码]HTTP METHODs[代码]),这是不是就能把接口[代码]SDK实现[代码]更简单化了?! 例如: /v3/certificates /v3/bill/tradebill /v3/ecommerce/fund/withdraw /v3/ecommerce/profitsharing/orders /v3/marketing/busifavor/users/{openid}/coupons/{coupon_code}/appids/{appid} 树形化即: [代码]v3 ├── certificates ├── bill │ └── tradebill ├── ecommerce │ ├── fund │ │ └── withdraw │ └── profitsharing │ └── orders └── marketing └── busifavor └── users └── {openid} └── coupons └── {coupon_code} └── appids └── {appid} [代码] 按照这种树形构想,我们来看下需要做的[代码]封装实现[代码]工作: 把实体对象,按照实体的排列顺序,映射出请求的URI; 每个对象实体,包含有若干操作方法,其中可选带参数发起RPC请求; 随官方放出更多的接口,SDK需要能够弹性扩容; wechatpay-axios-plugin~0.2.0 版本实现了上述这3个目标,代码包如下截屏: [图片] 我们用伪代码来校验看一下这个[代码]封装实现[代码]: [代码]require('util').inspect.defaultOptions.depth = 10; const { Wechatpay } = require('wechatpay-axios-plugin'); const wxpay = new Wechatpay({mchid: '1', serial: '2', privateKey: '3', certs: {'4': '5'}}); wxpay.v3.certificates; wxpay.v3.bill.tradebill; wxpay.v3.ecommerce.fund.withdraw; wxpay.v3.marketing.busifavor.users['{openid}'].coupons.$coupon_code$.appids['wx233544546545989']; console.info(wxpay); //以下是输出内容 { entities: [], withEntities: [Function: withEntities], get: [AsyncFunction: get], post: [AsyncFunction: post], upload: [AsyncFunction: upload], v3: { entities: [ 'v3' ], withEntities: [Function: withEntities], get: [AsyncFunction: get], post: [AsyncFunction: post], upload: [AsyncFunction: upload], certificates: { entities: [ 'v3', 'certificates' ], withEntities: [Function: withEntities], get: [AsyncFunction: get], post: [AsyncFunction: post], upload: [AsyncFunction: upload] }, bill: { entities: [ 'v3', 'bill' ], withEntities: [Function: withEntities], get: [AsyncFunction: get], post: [AsyncFunction: post], upload: [AsyncFunction: upload], tradebill: { entities: [ 'v3', 'bill', 'tradebill' ], withEntities: [Function: withEntities], get: [AsyncFunction: get], post: [AsyncFunction: post], upload: [AsyncFunction: upload] } }, ecommerce: { entities: [ 'v3', 'ecommerce' ], withEntities: [Function: withEntities], get: [AsyncFunction: get], post: [AsyncFunction: post], upload: [AsyncFunction: upload], fund: { entities: [ 'v3', 'ecommerce', 'fund' ], withEntities: [Function: withEntities], get: [AsyncFunction: get], post: [AsyncFunction: post], upload: [AsyncFunction: upload], withdraw: { entities: [ 'v3', 'ecommerce', 'fund', 'withdraw' ], withEntities: [Function: withEntities], get: [AsyncFunction: get], post: [AsyncFunction: post], upload: [AsyncFunction: upload] } } }, marketing: { entities: [ 'v3', 'marketing' ], withEntities: [Function: withEntities], get: [AsyncFunction: get], post: [AsyncFunction: post], upload: [AsyncFunction: upload], busifavor: { entities: [ 'v3', 'marketing', 'busifavor' ], withEntities: [Function: withEntities], get: [AsyncFunction: get], post: [AsyncFunction: post], upload: [AsyncFunction: upload], users: { entities: [ 'v3', 'marketing', 'busifavor', 'users' ], withEntities: [Function: withEntities], get: [AsyncFunction: get], post: [AsyncFunction: post], upload: [AsyncFunction: upload], '{openid}': { entities: [ 'v3', 'marketing', 'busifavor', 'users', '{openid}' ], withEntities: [Function: withEntities], get: [AsyncFunction: get], post: [AsyncFunction: post], upload: [AsyncFunction: upload], coupons: { entities: [ 'v3', 'marketing', 'busifavor', 'users', '{openid}', 'coupons' ], withEntities: [Function: withEntities], get: [AsyncFunction: get], post: [AsyncFunction: post], upload: [AsyncFunction: upload], '$coupon_code$': { entities: [ 'v3', 'marketing', 'busifavor', 'users', '{openid}', 'coupons', '{coupon_code}' ], withEntities: [Function: withEntities], get: [AsyncFunction: get], post: [AsyncFunction: post], upload: [AsyncFunction: upload], appids: { entities: [ 'v3', 'marketing', 'busifavor', 'users', '{openid}', 'coupons', '{coupon_code}', 'appids' ], withEntities: [Function: withEntities], get: [AsyncFunction: get], post: [AsyncFunction: post], upload: [AsyncFunction: upload], wx233544546545989: { entities: [ 'v3', 'marketing', 'busifavor', 'users', '{openid}', 'coupons', '{coupon_code}', 'appids', 'wx233544546545989' ], withEntities: [Function: withEntities], get: [AsyncFunction: get], post: [AsyncFunction: post], upload: [AsyncFunction: upload] } } } } } } } } } } [代码] 注: API树实体节点,存储在每个 [代码]entities[代码] 属性上,方便后续的[代码]get[代码], [代码]post[代码] 抑或 [代码]upload[代码] 方法调用调用前,反构成最终请求的[代码]URI[代码];特别地,对于动态树实体节点来说,每个实体节点均提供了 [代码]withEntities[代码] 方法,用来在最终请求前,把动态实体节点替换成实际的值。 正常用法示例如下: [代码]const {Wechatpay} = require('wechatpay-axios-plugin'); const wxpay = new Wechatpay({/*初始化参数,README有*/}, {/*可选调整axios的参数*/}); //拿证书 wxpay.v3.certificates.get(); //带参申请交易账单 wxpay.v3.bill.tradebill.get({params: {bill_date}}); //带参发起账户余额提现 wxpay.v3.ecommerce.fund.withdraw.post({sub_mchid, out_request_no, amount, remark, bank_memo}); //查询用户单张券详情 wxpay.v3.marketing.busifavor.users['{openid}'].coupons.$coupon_code$.appids['wx233544546545989'].withEntities({openid, coupon_code}).get(); [代码] 请求APIv3是不是就“丧心病狂”般的简单了?! 详细功能说明及用法示例,npmjs及github的README均有。 如果喜欢,就给来个 赞 及 Star 吧。
2020-07-17