123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359 |
- /* eslint-disable strict */
- const urllib = require('urllib');
- const { KJUR, hextob64 } = require('jsrsasign');
- const assert = require('assert');
- // const nodeAesGcm = require('node-aes-gcm');
- const crypto = require('crypto');
- const x509 = require('@peculiar/x509');
- class Payment {
- constructor({ appid, mchid, private_key, serial_no, apiv3_private_key, notify_url } = {}) {
- assert(appid, 'appid is required');
- assert(mchid, 'mchid is required');
- assert(private_key, 'private_key is required');
- assert(serial_no, 'serial_no is required');
- assert(apiv3_private_key, 'apiv3_private_key is required');
- this.appid = appid;
- this.mchid = mchid;
- this.private_key = private_key;
- this.serial_no = serial_no;
- this.apiv3_private_key = apiv3_private_key;
- this.notify_url = notify_url;
- this.urls = {
- jsapi: () => {
- return {
- url: 'https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi',
- method: 'POST',
- pathname: '/v3/pay/transactions/jsapi',
- };
- },
- app: () => {
- return {
- url: 'https://api.mch.weixin.qq.com/v3/pay/transactions/app',
- method: 'POST',
- pathname: '/v3/pay/transactions/app',
- };
- },
- h5: () => {
- return {
- url: 'https://api.mch.weixin.qq.com/v3/pay/transactions/h5',
- method: 'POST',
- pathname: '/v3/pay/transactions/h5',
- };
- },
- native: () => {
- return {
- url: 'https://api.mch.weixin.qq.com/v3/pay/transactions/native',
- method: 'POST',
- pathname: '/v3/pay/transactions/native',
- };
- },
- getTransactionsById: ({ pathParams }) => {
- return {
- url: `https://api.mch.weixin.qq.com/v3/pay/transactions/id/${pathParams.transaction_id}?mchid=${this.mchid}`,
- method: 'GET',
- pathname: `/v3/pay/transactions/id/${pathParams.transaction_id}?mchid=${this.mchid}`,
- };
- },
- getTransactionsByOutTradeNo: ({ pathParams }) => {
- return {
- url: `https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/${pathParams.out_trade_no}?mchid=${this.mchid}`,
- method: 'GET',
- pathname: `/v3/pay/transactions/out-trade-no/${pathParams.out_trade_no}?mchid=${this.mchid}`,
- };
- },
- close: ({ pathParams }) => {
- return {
- url: `https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/${pathParams.out_trade_no}/close`,
- method: 'POST',
- pathname: `/v3/pay/transactions/out-trade-no/${pathParams.out_trade_no}/close`,
- };
- },
- refund: () => {
- return {
- url: 'https://api.mch.weixin.qq.com/v3/refund/domestic/refunds',
- method: 'POST',
- pathname: '/v3/refund/domestic/refunds',
- };
- },
- getRefund: ({ pathParams }) => {
- return {
- url: `https://api.mch.weixin.qq.com/v3/refund/domestic/refunds/${pathParams.out_refund_no}`,
- method: 'GET',
- pathname: `/v3/refund/domestic/refunds/${pathParams.out_refund_no}`,
- };
- },
- getCertificates: () => {
- return {
- url: 'https://api.mch.weixin.qq.com/v3/certificates',
- method: 'GET',
- pathname: '/v3/certificates',
- };
- },
- tradebill: ({ queryParams }) => {
- const { bill_date, bill_type, tar_type } = queryParams;
- return {
- url: `https://api.mch.weixin.qq.com/v3/bill/tradebill?bill_date=${bill_date}${bill_type ? '&bill_type=' + bill_type : ''}${tar_type ? '&tar_type=' + tar_type : ''}`,
- method: 'GET',
- pathname: `/v3/bill/tradebill?bill_date=${bill_date}${bill_type ? '&bill_type=' + bill_type : ''}${tar_type ? '&tar_type=' + tar_type : ''}`,
- };
- },
- fundflowbill: ({ queryParams }) => {
- const { bill_date, account_type, tar_type } = queryParams;
- return {
- url: `https://api.mch.weixin.qq.com/v3/bill/fundflowbill?bill_date=${bill_date}${account_type ? '&account_type=' + account_type : ''}${tar_type ? '&tar_type=' + tar_type : ''}`,
- method: 'GET',
- pathname: `/v3/bill/fundflowbill?bill_date=${bill_date}${account_type ? '&account_type=' + account_type : ''}${tar_type ? '&tar_type=' + tar_type : ''}`,
- };
- },
- downloadbill: ({ pathParams }) => {
- const url = pathParams;
- const index = url.indexOf('/v3');
- const pathname = url.substr(index);
- return {
- url,
- method: 'GET',
- pathname,
- };
- },
- };
- this.decodeCertificates();
- }
- // 调用封装 制作签名
- async run({ pathParams, queryParams, bodyParams, type }) {
- assert(type, 'type is required');
- const { url, method, pathname } = this.urls[type]({ pathParams, queryParams });
- const timestamp = Math.floor(Date.now() / 1000);
- const onece_str = this.generate();
- const bodyParamsStr = bodyParams && Object.keys(bodyParams).length ? JSON.stringify(bodyParams) : '';
- const signature = this.rsaSign(`${method}\n${pathname}\n${timestamp}\n${onece_str}\n${bodyParamsStr}\n`, this.private_key, 'SHA256withRSA');
- const Authorization = `WECHATPAY2-SHA256-RSA2048 mchid="${this.mchid}",nonce_str="${onece_str}",timestamp="${timestamp}",signature="${signature}",serial_no="${this.serial_no}"`;
- const { status, data } = await urllib.request(url, {
- method,
- dataType: 'text',
- data: method === 'GET' ? '' : bodyParams,
- timeout: [ 10000, 15000 ],
- headers: {
- 'Content-Type': 'application/json',
- Accept: 'application/json',
- Authorization,
- },
- });
- return { status, data };
- }
- // jsapi统一下单
- async jsapi(params) {
- const bodyParams = {
- ...params,
- appid: this.appid,
- mchid: this.mchid,
- notify_url: params.notify_url || this.notify_url,
- };
- return await this.run({ bodyParams, type: 'jsapi' });
- }
- // app统一下单
- async app(params) {
- const bodyParams = {
- ...params,
- appid: this.appid,
- mchid: this.mchid,
- notify_url: params.notify_url || this.notify_url,
- };
- return await this.run({ bodyParams, type: 'app' });
- }
- // h5统一下单
- async h5(params) {
- const bodyParams = {
- ...params,
- appid: this.appid,
- mchid: this.mchid,
- notify_url: params.notify_url || this.notify_url,
- };
- return await this.run({ bodyParams, type: 'h5' });
- }
- // native统一下单
- async native(params) {
- const bodyParams = {
- ...params,
- appid: this.appid,
- mchid: this.mchid,
- notify_url: params.notify_url || this.notify_url,
- };
- return await this.run({ bodyParams, type: 'native' });
- }
- // 通过transaction_id查询订单
- async getTransactionsById(params) {
- return await this.run({ pathParams: params, type: 'getTransactionsById' });
- }
- // 通过out_trade_no查询订单
- async getTransactionsByOutTradeNo(params) {
- return await this.run({ pathParams: params, type: 'getTransactionsByOutTradeNo' });
- }
- // 关闭订单
- async close(params) {
- return await this.run({ pathParams: {
- out_trade_no: params.out_trade_no,
- }, bodyParams: {
- mchid: this.mchid,
- }, type: 'close' });
- }
- // 退款
- async refund(params) {
- const bodyParams = {
- ...params,
- notify_url: params.notify_url || this.notify_url,
- };
- return await this.run({ bodyParams, type: 'refund' });
- }
- // 查询单笔退款订单
- async getRefund(params) {
- return await this.run({ pathParams: params, type: 'getRefund' });
- }
- // 获取平台证书列表
- async getCertificates() {
- return await this.run({ type: 'getCertificates' });
- }
- // 解密证书列表 解出CERTIFICATE以及public key
- async decodeCertificates() {
- const result = await this.getCertificates();
- if (result.status !== 200) {
- throw new Error('获取证书列表失败');
- }
- const certificates = typeof result.data === 'string' ? JSON.parse(result.data).data : result.data.data;
- for (const cert of certificates) {
- const plaintext = this.decode(cert.encrypt_certificate);
- cert.decrypt_certificate = plaintext.toString();
- const beginIndex = cert.decrypt_certificate.indexOf('-\n');
- const endIndex = cert.decrypt_certificate.indexOf('\n-');
- const str = cert.decrypt_certificate.substring(beginIndex + 2, endIndex);
- const x509Certificate = new x509.X509Certificate(Buffer.from(str, 'base64'));
- const public_key = Buffer.from(x509Certificate.publicKey.rawData).toString('base64');
- cert.public_key = '-----BEGIN PUBLIC KEY-----\n' + public_key + '\n-----END PUBLIC KEY-----';
- }
- // eslint-disable-next-line no-return-assign
- return this.certificates = certificates;
- }
- // 验证签名 timestamp,nonce,serial,signature均在HTTP头中获取,body为请求参数
- async verifySign({ timestamp, nonce, serial, body, signature }, repeatVerify = true) {
- const data = `${timestamp}\n${nonce}\n${typeof body === 'string' ? body : JSON.stringify(body)}\n`;
- const verify = crypto.createVerify('RSA-SHA256');
- verify.update(Buffer.from(data));
- let verifySerialNoPass = false;
- for (const cert of this.certificates) {
- if (cert.serial_no === serial) {
- verifySerialNoPass = true;
- return verify.verify(cert.public_key, signature, 'base64');
- }
- }
- if (!verifySerialNoPass && repeatVerify) {
- await this.decodeCertificates();
- return await this.verifySign({ timestamp, nonce, serial, body, signature }, false);
- }
- throw new Error('平台证书序列号不相符');
- }
- // 申请交易账单
- async tradebill(params) {
- return await this.run({ queryParams: params, type: 'tradebill' });
- }
- // 申请资金账单
- async fundflowbill(params) {
- return await this.run({ queryParams: params, type: 'fundflowbill' });
- }
- // 下载账单
- async downloadbill(download_url) {
- return await this.run({ pathParams: download_url, type: 'downloadbill' });
- }
- // 解密支付退款通知资源数据
- decodeResource(resource) {
- const { plaintext } = this.decode(resource);
- return JSON.parse(plaintext.toString());
- }
- // 解密
- decode(params) {
- const AUTH_KEY_LENGTH = 16;
- // ciphertext = 密文,associated_data = 填充内容, nonce = 位移
- const { ciphertext, associated_data, nonce } = params;
- // 密钥
- const key_bytes = Buffer.from(this.apiv3_private_key, 'utf8');
- // 位移
- const nonce_bytes = Buffer.from(nonce, 'utf8');
- // 填充内容
- const associated_data_bytes = Buffer.from(associated_data, 'utf8');
- // 密文Buffer
- const ciphertext_bytes = Buffer.from(ciphertext, 'base64');
- // 计算减去16位长度
- const cipherdata_length = ciphertext_bytes.length - AUTH_KEY_LENGTH;
- // upodata
- const cipherdata_bytes = ciphertext_bytes.slice(0, cipherdata_length);
- // tag
- const auth_tag_bytes = ciphertext_bytes.slice(cipherdata_length, ciphertext_bytes.length);
- const decipher = crypto.createDecipheriv(
- 'aes-256-gcm', key_bytes, nonce_bytes
- );
- decipher.setAuthTag(auth_tag_bytes);
- decipher.setAAD(Buffer.from(associated_data_bytes));
- const output = Buffer.concat([
- decipher.update(cipherdata_bytes),
- decipher.final(),
- ]);
- // output = Buffer对象
- return output;
- // return nodeAesGcm.decrypt(key_bytes, nonce_bytes, cipherdata_bytes, associated_data_bytes, auth_tag_bytes);
- }
- // 生成随机字符串
- generate(length = 32) {
- const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
- let noceStr = '',
- // eslint-disable-next-line prefer-const
- maxPos = chars.length;
- // eslint-disable-next-line no-bitwise
- while (length--) noceStr += chars[Math.random() * maxPos | 0];
- return noceStr;
- }
- /*
- * rsa签名
- * 签名内容
- * 私钥,PKCS#1
- * hash算法,SHA256withRSA,SHA1withRSA
- * 返回签名字符串,base64
- */
- rsaSign(content, privateKey, hash = 'SHA256withRSA') {
- // 创建 Signature 对象
- const signature = new KJUR.crypto.Signature({
- alg: hash,
- // !这里指定 私钥 pem!
- prvkeypem: privateKey,
- });
- signature.updateString(content);
- const signData = signature.sign();
- // 将内容转成base64
- return hextob64(signData);
- }
- }
- module.exports = Payment;
|