评论

适合云开发的微信支付v2及v3版Nodejs SDK

之所以再造一遍v2(相对于APIv3来说)的轮子,实属是无奈之举,线下交易场景常见的付款码支付及退款功能,目前只能借助v2接口来处理;同时为支持云原生开发,也抽出一些共生方法,为v3而用。

接续微信支付APIv3的Nodejs版SDK,在2020这个时间节点,之所以再造一遍微信支付v2(相对于APIv3来说)的轮子,实属是无奈之举,线下交易场景常见的付款码支付退款功能,官方当下还没有开放出来v3版的,只能借助v2接口来处理;wechatpay-axios-plugin 从一开始目标就是为云原生而设计,遂再造一遍轮子,也抽出一些共生方法,为v3而用。

设计思路

此类库核心部件是利用了Axios的transform功能,数据在内部流转过程中,会经过 transformRequesttransformResponse 处理,通过构造两个自定义transformer,完整实现v2版的技术规格要求,从而完成 HTTP 请求/响应 处理。

Transformer.request

方法返回值是个数组,含两个方法 [signer, toXml],字面意思即,对输入数据签名,然后转换成xml;

Transformer.response

方法返回值是个数组,含两个方法 [toObject, verifier],字面意思即,返回值做数据转换为对象,然后校验签名;

证书设置

凡是涉及资金变动的接口,均需要商户证书,此实现同时支持 pemp12 格式的证书,使用方法见随包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  
点赞 4
收藏
评论

3 个评论

  • Titan(潘翼)
    Titan(潘翼)
    2021-12-17

    您好,上传图片的URL是"/v3/merchant/media/upload" 这个,您的那个是营销专用的。。 我在您的源码里改成这个/v3/merchant/media/upload 后,就用不了了

    2021-12-17
    赞同
    回复
  • 韦不吕
    韦不吕
    2020-12-05

    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,

    })

    2020-12-05
    赞同
    回复
  • 北望沣渭
    北望沣渭
    2020-10-16

    备注一下文章里的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`)
    



    2020-10-16
    赞同
    回复
登录 后发表内容