接续微信支付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上也有许多优秀的类库可用,暂且先这样。
您好,上传图片的URL是"/v3/merchant/media/upload" 这个,您的那个是营销专用的。。 我在您的源码里改成这个/v3/merchant/media/upload 后,就用不了了
scf按照底下这个发现金红包,什么反应都没有。楼主看看是什么问题。谢谢。
// 云函数入口文件
const crypto = require('crypto')
const {Wechatpay, Formatter: fmt} = require('wechatpay-axios-plugin')
const cloud = require('wx-server-sdk')
cloud.init({
env: cloud.DYNAMIC_CURRENT_ENV,
traceUser: true,
})
const client = Wechatpay.xmlBased({
secret: process.env.api_secret,
})
备注一下文章里的TODO,也已经支持了,示例用法如下。。。
aes-256-ecb/pcks7padding
v0.3.1开始支持解密
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)
v0.3.2开始支持加密
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`)