whats-alipay

Yet Another Alipay OpenAPI SDK

Usage no npm install needed!

<script type="module">
  import whatsAlipay from 'https://cdn.skypack.dev/whats-alipay';
</script>

README

支付宝 OpenAPI SDK

Yet Another Alipay OpenAPI Smart Development Kit

GitHub actions GitHub issues GitHub release Vulnerabilities NPM module version NPM module downloads per month NPM module license

主要功能

  • OOP风格化的,可弹性扩容的,支付宝OpenAPI SDK
  • 低依赖,目前仅依赖 Axios
  • 使用Node原生代码实现支付宝OpenAPI的AES(aes-128-cbc)加/解密功能
  • 使用Node原生代码实现支付宝OpenAPI的RSA(sha1WithRSAEncryption)及RSA2(sha256WithRSAEncryption)签名、验签功能
  • 异步通知消息验签
  • 命令行网关交互工具

SDK约定

  • 使用 method({请求参数}[, {公共请求参数}[, {特殊头参数}]]) 作为HTTP接口驱动,释义如下:
    • 接口定义中公共请求参数method,即作为本SDK标准方法链,弹性扩容,示例使用方法如下,详细审查见文末
    • 接口定义中的请求参数,以 Object 对象作为第一个入参
    • 接口定义中的公共请求参数,以 Object 对象作为第二个入参
    • 特别的,对于图片/视频上传,需要定义 multipart/form-data 头信息,以 Object 对象作为第三个入参,见如下示例
    • 特别的,对于页面表单提交型,即所谓的page类接口,第三参数须定义为 Formatter.page,返回结果可直接作为页面/接口输出,说明用法见如下示例
  • 请求数据签名以及返回数据验签均自动完成,开发者仅需关注业务代码即可;特别地,对于验签结果有依赖的情况,可以从返回值的头部获取:
    • headers[x-alipay-verified] 为验签结果,值可能为 ok, undefined
    • headers[x-alipay-signature] 为源返回数据签名值,值可能为 undefined
    • headers[x-alipay-responder] 为源返回值字段名转译,值可能为 undefined, error 或者实际请求的 method

使用手册

whatsCli 命令行工具

以命令行的形式,与OpenAPI网关交互,play the OpenAPI requests over command line.

$ ./node_modules/.bin/whatsCli -h (click to toggle display)
Usage: cli.js [options]

Options:
  -k, --privateKey  The privateKey pem file path                                                              [required]
  -p, --publicCert  The publicCert pem file path                                                              [required]
  -m, --method      The method, eg: alipay.trade.query                                                        [required]
  -s, --search      The search parameters, eg: search.app_id=2088                                             [required]
  -b, --biz         The biz_content, eg: biz.out_trade_no=abcd1234                                            [required]
  -d, --log         Turn on the request trace log                                                              [boolean]
  -u, --baseURL     The OpenAPI gateway, eg: https://openapi.alipaydev.com/gateway.do
  -h, --help        Show help                                                                                  [boolean]
  -V, --version     Show version number                                                                        [boolean]

Examples:
  cli.js -k merchant.key -p alipay.pub -m alipay.trade.pay      The Face2Face barCode scenario
  -s.app_id=2088 -b.subject=HelloKitty
  -b.out_trade_no=Kitty0001 -b.scene=bar_code
  -b.total_amount=0.01 -b.auth_code=
  cli.js -k merchant.key -p alipay.pub -m alipay.trade.refund   The trade refund scenario
  -s.app_id=2088 -b.refund_amount=0.01 -b.refund_currency=CNY
  -b.out_trade_no=Kitty0001
  cli.js -d -u https://openapi.alipaydev.com/gateway.do -k      The trade query scenario over the sandbox environment
  merchant.key -p alipay.pub -m alipay.trade.query              with trace logging
  -s.app_id=2088 -b.out_trade_no=Kitty0001

certHelper 命令行工具

$ ./node_modules/.bin/certHelper -h (click to toggle display)
Usage: cert.js [command] [options]

Commands:
  cert.js SN       Get the certificatie(s) `SN`                 [default]
  cert.js extract  Extract the chained certificate(s)

Options:
  -f, --file     The certificate(s) file path                   [required]
  -p, --pattern  The algo prefix or suffix, dot(.) for all
  -h, --help     Show help                                      [boolean]
  -V, --version  Show version number                            [boolean]

Examples:
  cert.js SN -f alipayRootCert.crt                              get the `sha256`(default) certificate `SN`
  cert.js SN -f alipayRootCert.crt -p ec                        get the signatureAlgorithm whose contains `ec` words
                                                                certificate `SN`
  cert.js SN -f alipayRootCert.crt -p .                         get all chained certificate(s) `SN`
  cert.js extract -f alipayRootCert.crt                         extract the `sha256`(default) certificate
  cert.js extract -f alipayRootCert.crt -p sha1                 extract the `sha1` certificate
  cert.js extract -f alipayRootCert.crt -p .                    extract all chained certificate(s)
  cert.js extract -f alipayRootCert.crt -p sha1 | openssl x509  piped openssl x509 command
  -noout -text
  cert.js extract -f alipayRootCert.crt -p sha1 > tmp.pem       save to a file

此命令行工具,主要是用来计算并获取 公钥证书模式 所需的 应用公钥证书SN(app_cert_sn)及 支付宝公钥证书SN(alipay_root_cert_sn)。

./bin/cert.js SN -f /path/your/app_cert.crt

./bin/cert.js SN -f /path/your/alipay_root_cert.crt -p RSAEncryption

SN 命令是 Helpers.SN 的语法糖,可从如下API文档查看更详细用法

初始化

const { Alipay, Rsa } = require('whats-alipay');

//应用app_id
const app_id = '2014072300007148';

//商户RSA私钥,入参是'从官方工具获取到的BASE64字符串'
const privateKey = Rsa.fromPkcs1('MIIEpAIBAAKCAQEApdXuft3as2x...');
// 以上是下列代码的语法糖,格式为 'private.pkcs1://' + '从官方工具获取到的字符串'
// const privateKey = Rsa.from('private.pkcs1://MIIEpAIBAAKCAQEApdXuft3as2x...');
// Node10仅支持以下方式,须保证`private_key.pem`为完整X509格式
// const privateKey = require('fs').readFileSync('/your/openapi/private_key.pem');

//支付宝RSA公钥,入参是'从官方工具获取到的BASE64字符串'
const publicKey = Rsa.fromSpki('MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCg...');
// 以上是下列代码的语法糖,格式为 'public.spki://' + '从官方工具获取到的字符串'
// const publicKey = Rsa.from('public.spki://MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCg...');
// Node10仅支持以下方式,须保证`public_key.pem`为完整X509格式
// const publicKey = require('fs').readFileSync('/the/alipay/public_key.pem');

const whats = new Alipay({ privateKey, publicKey, params: { app_id, } });

统一收单线下交易查询

whats
  .alipay.trade.query({out_trade_no})
  .then(({headers,data}) => ({headers,data}))
  .catch(({response: {data}}) => data)
  .then(console.log)
$ console.log sample result (click to toggle display)
{
  headers: {
    server: 'Tengine/2.1.0',
    date: 'Fri, 25 Sep 2020 03:27:32 GMT',
    'content-type': 'text/html;charset=utf-8',
    'content-length': '829',
    connection: 'close',
    'set-cookie': [
      'JSESSIONID=6FF6F65BB1EC785992117C95EA7C27BB; Path=/; HttpOnly',
      'spanner=ubOmkyHGnYLtouOsffShT4n70pJgSji6Xt2T4qEYgj0=;path=/;secure;'
    ],
    via: 'spanner-internet-5396.sa127[200]',
    'x-alipay-responder': 'alipay.trade.query',
    'x-alipay-signature': 'gALtsKSWEWRG4wSx8==',
    'x-alipay-verified': 'ok'
  },
  data: {
    code: '10000',
    msg: 'Success',
    buyer_logon_id: '134******38',
    buyer_pay_amount: '183.00',
    buyer_user_id: '2088101117955611',
    fund_bill_list: [ { amount: '183.00', fund_channel: 'ALIPAYACCOUNT' } ],
    invoice_amount: '183.00',
    out_trade_no: '6823789339978248',
    point_amount: '0.00',
    receipt_amount: '183.00',
    send_pay_date: '2020-09-19 17:00:28',
    total_amount: '183.00',
    trade_no: '2013112011001004330000121536',
    trade_status: 'TRADE_SUCCESS'
  }
}

统一收单交易支付接口

whats
  .alipay.trade.pay({
    out_trade_no,
    scene,
    auth_code,
    product_code,
    subject,
    total_amount,
  })
  .then(({data}) => data)
  .catch(({response: {data}}) => data)
  .then(console.log)

统一收单线下交易预创建

whats
  .alipay.trade.precreate({
    out_trade_no,
    subject,
    total_amount,
  })
  .then(({data}) => data)
  .catch(({response: {data}}) => data)
  .then(console.log)

手机网站支付接口2.0

whats
  .alipay.trade.wap.pay({
    out_trade_no,
    subject,
    total_amount,
    product_code,
    quit_url,
  }, {}, Formatter.page)
  .then(res => res)
  .then(console.log)
  • 注: 特别地 res 结构做了优化,直接支持 literal(独立服务模式: ${res}) 或者 JSON.stringify (二次接口模式: JSON.stringify(res))

统一收单下单并支付页面接口

whats
  .alipay.trade.page.pay({
    out_trade_no,
    subject,
    total_amount,
    product_code,
  }, {return_url}, Formatter.page)
  .then(res => res)
  .then(console.log)
  • 注: 特别地 res 结构做了优化,直接支持 literal(独立服务模式: ${res}) 或者 JSON.stringify (二次接口模式: JSON.stringify(res))

订单咨询服务

whats
  .alipay.trade.advance.consult({/*文档上的参数就好*/})
  .then(({data}) => data)
  .catch(({response: {data}}) => data)
  .then(console.log)

口碑营销活动列表查询

whats
  .koubei.marketing.campaign.activity.batchquery(/*文档上的参数就好*/})
  .then(({data}) => data)
  .catch(({response: {data}}) => data)
  .then(console.log)

支付API > 图片上传

const { Multipart } = require('whats-alipay');
const form = new Multipart();
form.append(
  'image_content',
  require('fs').readFileSync('/path/for/uploading.jpg'),
  'uploading.jpg'
);

whats
  .ant.merchant.expand.indirect.image.upload(
    form.getBuffer(),
    {image_type: 'jpg'},
    {...form.getHeaders()}
  )
  .then(({data}) => data)
  .catch(({response: {data}}) => data)
  .then(console.log)

店铺API > 上传门店照片和视频接口

const { Multipart } = require('whats-alipay');
const form = new Multipart();
form.append(
  'image_content',
  require('fs').readFileSync('/path/for/uploading.jpg'),
  'uploading.jpg'
);

whats
  .alipay.offline.material.image.upload(
    form.getBuffer(),
    {image_type: 'jpg', image_name: 'uploading.jpg'},
    {...form.getHeaders()}
  )
  .then(({data}) => data)
  .catch(({response: {data}}) => data)
  .then(console.log)

芝麻信用API > 授权查询

whats
  .zhima.auth.info.authquery(/*文档上的参数就好*/})
  .then(({data}) => data)
  .catch(({response: {data}}) => data)
  .then(console.log)

车主服务API > 车辆驶入接口

whats
  .alipay.eco.mycar.parking.enterinfo.sync(/*文档上的参数就好*/})
  .then(({data}) => data)
  .catch(({response: {data}}) => data)
  .then(console.log)

资金API > 单笔转账接口

whats
  .alipay.fund.trans.uni.transfer(/*文档上的参数就好*/})
  .then(({data}) => data)
  .catch(({response: {data}}) => data)
  .then(console.log)

何以弹性扩容

whats 上绑定多少 method,即扩容至多少,以上示例打印如下:

$ console.info sample result (click to toggle display)
console.info(whats)

[Function (anonymous)] {
  ant: [Function: ant] {
    merchant: [Function: ant.merchant] {
      expand: [Function: ant.merchant.expand] {
        indirect: [Function: ant.merchant.expand.indirect] {
          image: [Function: ant.merchant.expand.indirect.image] {
            upload: [Function: ant.merchant.expand.indirect.image.upload]
          }
        }
      }
    }
  },
  alipay: [Function: alipay] {
    trade: [Function: alipay.trade] {
      query: [Function: alipay.trade.query],
      precreate: [Function: alipay.trade.precreate],
      advance: [Function: alipay.trade.advance] {
        consult: [Function: alipay.trade.advance.consult]
      },
      page: [Function: alipay.trade.page] {
        pay: [Function: alipay.trade.page.pay]
      },
      wap: [Function: alipay.trade.wap] {
        pay: [Function: alipay.trade.wap.pay]
      }
    },
    fund: [Function: alipay.fund] {
      trans: [Function: alipay.fund.trans] {
        uni: [Function: alipay.fund.trans.uni] {
          transfer: [Function: alipay.fund.trans.uni.transfer]
        }
      }
    },
    eco: [Function: alipay.eco] {
      mycar: [Function: alipay.eco.mycar] {
        parking: [Function: alipay.eco.mycar.parking] {
          enterinfo: [Function: alipay.eco.mycar.parking.enterinfo] {
            sync: [Function: alipay.eco.mycar.parking.enterinfo.sync]
          }
        }
      }
    },
    offline: [Function: alipay.offline] {
      material: [Function: alipay.offline.material] {
        image: [Function: alipay.offline.material.image] {
          upload: [Function: alipay.offline.material.image.upload]
        }
      }
    }
  },
  zhima: [Function: zhima] {
    auth: [Function: zhima.auth] {
      info: [Function: zhima.auth.info] {
        authquery: [Function: zhima.auth.info.authquery]
      }
    }
  },
  koubei: [Function: koubei] {
    marketing: [Function: koubei.marketing] {
      campaign: [Function: koubei.marketing.campaign] {
        activity: [Function: koubei.marketing.campaign.activity] {
          batchquery: [Function: koubei.marketing.campaign.activity.batchquery]
        }
      }
    }
  }
}

异步通知消息验签

通知消息验签,依赖相关 webserver 提供的 POST 数据解析能力,以下函数在 http.createServer 上做过验证,仅供参考。

function notificationValidator(things, publicCert, withoutSignType = true) {
  const {groups: {sign}} = (typeof things === 'string' ? things : '').match(/\bsign=(?<sign>[^&]+)/) || {groups: {}}
  const source = [...new URLSearchParams(things)].reduce((des, [key, value]) => (des[key] = value, des), {})
  const signType = source['sign_type']
  const signature = (source.sign || '').replace(/ /g, '+').replace(/_/g, '/')
  // The signature with `sign_type` is only for the notifications whose come from `alipay.open.public.*` APIs.
  if (withoutSignType) {
      delete source['sign_type']
  }

  return Rsa.verify(Formatter.queryStringLike(Formatter.ksort(source)), sign || signature, publicCert, signType)
}

单元测试

npm install && npm test

To disable nock and request with the real gateway, just NOCK_OFF=true npm test

链接

许可证

MIT