xiongzhu há 2 anos atrás
pai
commit
b1421fc144

+ 3 - 0
package.json

@@ -24,6 +24,7 @@
     "@alicloud/dysmsapi20170525": "2.0.23",
     "@dqbd/tiktoken": "^1.0.6",
     "@esm2cjs/p-timeout": "^6.0.0",
+    "@fidm/x509": "^1.2.1",
     "@nestjs/common": "^9.3.3",
     "@nestjs/config": "^2.3.1",
     "@nestjs/core": "^9.3.3",
@@ -42,6 +43,7 @@
     "bcrypt": "^5.1.0",
     "class-transformer": "^0.5.1",
     "class-validator": "^0.13.0",
+    "crypto": "^1.0.1",
     "date-fns": "^2.29.3",
     "eventsource-parser": "^1.0.0",
     "express-basic-auth": "^1.2.1",
@@ -64,6 +66,7 @@
     "rxjs": "^7.8.0",
     "tnwx": "^2.5.6",
     "typeorm": "^0.3.12",
+    "wechatpay-node-v3": "^2.1.1",
     "yup": "^1.0.0"
   },
   "devDependencies": {

+ 199 - 0
src/weixin/lib/base.ts

@@ -0,0 +1,199 @@
+import axios from 'axios'
+import { Output } from './interface-v2'
+
+export class Base {
+    protected userAgent = '127.0.0.1' // User-Agent
+    constructor() {}
+
+    /**
+     * get 请求参数处理
+     * @param object query 请求参数
+     * @param exclude 需要排除的字段
+     * @returns
+     */
+    protected objectToQueryString(object: Record<string, any>, exclude: string[] = []): string {
+        let str = Object.keys(object)
+            .filter((key) => !exclude.includes(key))
+            .map((key) => {
+                return encodeURIComponent(key) + '=' + encodeURIComponent(object[key])
+            })
+            .join('&')
+        if (str) str = '?' + str
+        return str || ''
+    }
+    /**
+     * post 请求
+     * @param url  请求接口
+     * @param params 请求参数
+     */
+    protected async postRequest(
+        url: string,
+        params: Record<string, any>,
+        authorization: string
+    ): Promise<Record<string, any>> {
+        try {
+            const result = await axios.post(url, params, {
+                headers: {
+                    Accept: 'application/json',
+                    'Content-Type': 'application/json',
+                    'User-Agent': this.userAgent,
+                    Authorization: authorization,
+                    'Accept-Encoding': 'gzip'
+                }
+            })
+            return {
+                status: result.status,
+                ...result.data
+            }
+        } catch (error) {
+            const err = JSON.parse(JSON.stringify(error))
+            return {
+                status: err.status,
+                ...(err.response.text && JSON.parse(err.response.text))
+            }
+        }
+    }
+    /**
+     * post 请求 V2
+     * @param url  请求接口
+     * @param params 请求参数
+     */
+    protected async postRequestV2(
+        url: string,
+        params: Record<string, any>,
+        authorization: string,
+        headers = {}
+    ): Promise<Output> {
+        try {
+            const result = await axios.post(url, params, {
+                headers: {
+                    ...headers,
+                    Accept: 'application/json',
+                    'Content-Type': 'application/json',
+                    'User-Agent': this.userAgent,
+                    Authorization: authorization,
+                    'Accept-Encoding': 'gzip'
+                }
+            })
+
+            return {
+                status: result.status,
+                data: result.data
+            }
+        } catch (error) {
+            const err = JSON.parse(JSON.stringify(error))
+            return {
+                status: err.status as number,
+                error: err.response.text && JSON.parse(err.response.text)
+            }
+        }
+    }
+    /**
+     * get 请求
+     * @param url  请求接口
+     * @param query 请求参数
+     */
+    protected async getRequest(
+        url: string,
+        authorization: string,
+        query: Record<string, any> = {}
+    ): Promise<Record<string, any>> {
+        try {
+            const result = await axios.get(url, {
+                params: query,
+                headers: {
+                    Accept: 'application/json',
+                    'User-Agent': this.userAgent,
+                    Authorization: authorization,
+                    'Accept-Encoding': 'gzip'
+                }
+            })
+
+            let data = {}
+            switch (result.headers['Content-Type']) {
+                case 'application/json':
+                    data = {
+                        status: result.status,
+                        ...result.data
+                    }
+                    break
+                case 'text/plain':
+                    data = {
+                        status: result.status,
+                        data: result.data
+                    }
+                    break
+                case 'application/x-gzip':
+                    data = {
+                        status: result.status,
+                        data: result.data
+                    }
+                    break
+                default:
+                    data = {
+                        status: result.status,
+                        ...result.data
+                    }
+            }
+            return data
+        } catch (error) {
+            const err = JSON.parse(JSON.stringify(error))
+            return {
+                status: err.status,
+                ...(err.response.text && JSON.parse(err.response.text))
+            }
+        }
+    }
+    /**
+     * get 请求 v2
+     * @param url  请求接口
+     * @param query 请求参数
+     */
+    protected async getRequestV2(url: string, authorization: string, query: Record<string, any> = {}): Promise<Output> {
+        try {
+            const result = await axios.get(url, {
+                params: query,
+                headers: {
+                    Accept: 'application/json',
+                    'User-Agent': this.userAgent,
+                    Authorization: authorization,
+                    'Accept-Encoding': 'gzip'
+                }
+            })
+
+            let data: any = {}
+            switch (result.headers['Content-Type']) {
+                case 'application/json':
+                    data = {
+                        status: result.status,
+                        data: result.data
+                    }
+                    break
+                case 'text/plain':
+                    data = {
+                        status: result.status,
+                        data: result.data
+                    }
+                    break
+                case 'application/x-gzip':
+                    data = {
+                        status: result.status,
+                        data: result.data
+                    }
+                    break
+                default:
+                    data = {
+                        status: result.status,
+                        data: result.data
+                    }
+            }
+            return data
+        } catch (error) {
+            const err = JSON.parse(JSON.stringify(error))
+            return {
+                status: err.status,
+                error: err.response.text && JSON.parse(err.response.text)
+            }
+        }
+    }
+}

+ 78 - 0
src/weixin/lib/combine_interface.ts

@@ -0,0 +1,78 @@
+// H5场景信息
+interface Ih5Info {
+    type: string
+    app_name: string
+    app_url?: string
+    bundle_id?: string
+    package_name?: string
+}
+interface IsceneInfoNative {
+    device_id?: string
+    payer_client_ip: string
+}
+interface IsceneInfoH5 {
+    payer_client_ip: string
+    device_id: string
+    h5_info: Ih5Info
+}
+interface Iamount {
+    total_amount: number
+    currency: string
+}
+interface IsettleInfo {
+    profit_sharing?: boolean
+    subsidy_amount?: number
+}
+interface IsubOrders {
+    mchid: string
+    attach: string
+    amount: Iamount
+    out_trade_no: string
+    sub_mchid?: string // 直连商户不用传二级商户号。
+    description: string
+    settle_info?: IsettleInfo
+}
+interface IcombinePayerInfo {
+    openid: string
+}
+
+// 抛出
+export interface IcombineH5 {
+    combine_out_trade_no: string
+    scene_info: IsceneInfoH5
+    time_start?: string
+    time_expire?: string
+    notify_url: string
+    sub_orders: IsubOrders[]
+}
+export interface IcombineNative {
+    combine_out_trade_no: string
+    scene_info: IsceneInfoNative
+    time_start?: string
+    time_expire?: string
+    notify_url: string
+    sub_orders: IsubOrders[]
+}
+export interface IcombineApp {
+    combine_out_trade_no: string
+    scene_info: IsceneInfoNative
+    time_start?: string
+    time_expire?: string
+    notify_url: string
+    sub_orders: IsubOrders[]
+    combine_payer_info?: IcombinePayerInfo
+}
+export interface IcombineJsapi {
+    combine_out_trade_no: string
+    scene_info: IsceneInfoNative
+    time_start?: string
+    time_expire?: string
+    notify_url: string
+    sub_orders: IsubOrders[]
+    combine_payer_info: IcombinePayerInfo
+}
+export interface IcloseSubOrders {
+    mchid: string
+    out_trade_no: string
+    sub_mchid?: string
+}

+ 266 - 0
src/weixin/lib/interface-v2.ts

@@ -0,0 +1,266 @@
+/**
+ * 统一返回格式
+ */
+export interface Output {
+    status: number
+    error?: any
+    data?: any
+}
+
+/**
+ * 发起商家转账零钱
+ */
+export declare namespace BatchesTransfer {
+    export interface TransferDetailList {
+        /** 商家明细单号 */
+        out_detail_no: string
+        /** 转账金额 */
+        transfer_amount: number
+        /** 转账备注 */
+        transfer_remark: string
+        /** 用户在直连商户应用下的用户标示 */
+        openid: string
+        /** 收款用户姓名 */
+        user_name?: string
+    }
+
+    /**
+     * 发起商家转账API 请求参数
+     */
+    export interface Input {
+        /** 直连商户的appid -不传 默认使用初始化数据 */
+        appid?: string
+        /** 商家批次单号 */
+        out_batch_no: string
+        /** 批次名称 */
+        batch_name: string
+        /** 批次备注 */
+        batch_remark: string
+        /** 转账总金额 */
+        total_amount: number
+        /** 转账总笔数 */
+        total_num: number
+        /** 转账明细列表 */
+        transfer_detail_list: TransferDetailList[]
+        /** 转账场景ID */
+        transfer_scene_id?: string
+        /** 微信平台证书序列号-Wechatpay-Serial(当有敏感信息加密时,需要当前参数) */
+        wx_serial_no?: string
+    }
+
+    export interface DataOutput {
+        out_batch_no: string
+        batch_id: string
+        create_time: Date
+    }
+
+    /**
+     * 发起商家转账API 返回参数
+     */
+    export interface IOutput extends Output {
+        data?: DataOutput
+    }
+
+    /**
+     * 转账批次单
+     */
+    export interface QueryTransferBatch {
+        /** 微信支付分配的商户号 */
+        mchid: string
+        /** 商户系统内部的商家批次单号,在商户系统内部唯一 */
+        out_batch_no: string
+        /** 微信批次单号,微信商家转账系统返回的唯一标识 */
+        batch_id: string
+        /** 申请商户号的appid或商户号绑定的appid(企业号corpid即为此appid) */
+        appid: string
+        /** 批次状态 */
+        batch_status: string
+        /** 批次类型 */
+        batch_type: string
+        /** 该笔批量转账的名称 */
+        batch_name: string
+        /** 转账说明,UTF8编码,最多允许32个字符 */
+        batch_remark: string
+        /** 批次关闭原因 */
+        close_reason?: string
+        /** 转账总金额 */
+        total_amount: number
+        /** 转账总笔数 */
+        total_num: number
+        /** 批次创建时间	 */
+        create_time?: Date
+        /** 批次更新时间 */
+        update_time?: Date
+        /** 转账成功金额 */
+        success_amount?: number
+        /** 转账成功笔数 */
+        success_num?: number
+        /** 转账失败金额 */
+        fail_amount?: number
+        /** 转账失败笔数 */
+        fail_num?: number
+    }
+
+    /**
+     * 转账明细单列表
+     */
+    export interface QueryTransferDetailList {
+        /** 微信明细单号 */
+        detail_id: string
+        /** 商家明细单号 */
+        out_detail_no: string
+        /** 明细状态 */
+        detail_status: string
+    }
+
+    /**
+     * 商家批次单号查询批次单API
+     */
+    export namespace QueryBatchesTransferList {
+        /**
+         * 商家批次单号查询参数
+         */
+        export interface Input {
+            /**商户系统内部的商家批次单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一 */
+            out_batch_no: string
+            /**商户可选择是否查询指定状态的转账明细单,当转账批次单状态为“FINISHED”(已完成)时,才会返回满足条件的转账明细单 */
+            need_query_detail: boolean
+            /**该次请求资源(转账明细单)的起始位置,从0开始,默认值为0 */
+            offset?: number
+            /**该次请求可返回的最大资源(转账明细单)条数,最小20条,最大100条,不传则默认20条。不足20条按实际条数返回 */
+            limit?: number
+            /**查询指定状态的转账明细单,当need_query_detail为true时,该字段必填 */
+            detail_status?: 'ALL' | 'SUCCESS' | 'FAIL'
+        }
+
+        export interface IOutput extends Output {
+            data?: {
+                limit: number
+                offset: number
+                transfer_batch: QueryTransferBatch
+                transfer_detail_list: QueryTransferDetailList[]
+            }
+        }
+    }
+
+    /**
+     * 微信批次单号查询批次单API
+     */
+    export namespace QueryBatchesTransferByWx {
+        export interface Input {
+            /** 微信批次单号,微信商家转账系统返回的唯一标识 */
+            batch_id: string
+            /**商户可选择是否查询指定状态的转账明细单,当转账批次单状态为“FINISHED”(已完成)时,才会返回满足条件的转账明细单 */
+            need_query_detail: boolean
+            /**该次请求资源(转账明细单)的起始位置,从0开始,默认值为0 */
+            offset?: number
+            /**该次请求可返回的最大资源(转账明细单)条数,最小20条,最大100条,不传则默认20条。不足20条按实际条数返回 */
+            limit?: number
+            /**查询指定状态的转账明细单,当need_query_detail为true时,该字段必填 */
+            detail_status?: 'ALL' | 'SUCCESS' | 'FAIL'
+        }
+
+        export interface IOutput extends Output {
+            data?: {
+                limit: number
+                offset: number
+                transfer_batch: QueryTransferBatch
+                transfer_detail_list: QueryTransferDetailList[]
+            }
+        }
+    }
+
+    /**
+     * 微信明细单号查询明细单API
+     */
+    export namespace QueryBatchesTransferDetailByWx {
+        export interface Input {
+            /** 微信批次单号 */
+            batch_id: string
+            /** 微信明细单号 */
+            detail_id: string
+        }
+
+        export interface DetailOutput {
+            /** 商户号 */
+            mchid: string
+            /** 商家批次单号 */
+            out_batch_no: string
+            /** 微信批次单号 */
+            batch_id: string
+            /** 直连商户的appid */
+            appid: string
+            /** 商家明细单号 */
+            out_detail_no: string
+            /** 微信明细单号 */
+            detail_id: string
+            /** 明细状态 */
+            detail_status: string
+            /** 转账金额 */
+            transfer_amount: number
+            /** 转账备注 */
+            transfer_remark: string
+            /** 明细失败原因 */
+            fail_reason?: string
+            /** 用户在直连商户应用下的用户标示 */
+            openid: string
+            /** 收款用户姓名 */
+            user_name?: string
+            /** 转账发起时间 */
+            initiate_time: Date
+            /** 明细更新时间 */
+            update_time: Date
+        }
+
+        export interface IOutput extends Output {
+            data?: DetailOutput
+        }
+    }
+
+    /**
+     * 商家明细单号查询明细单API
+     */
+    export namespace QueryBatchesTransferDetail {
+        export interface Input {
+            /** 商家明细单号 */
+            out_detail_no: string
+            /** 商家批次单号 */
+            out_batch_no: string
+        }
+
+        export interface DetailOutput {
+            /** 商户号 */
+            mchid: string
+            /** 商家批次单号 */
+            out_batch_no: string
+            /** 微信批次单号 */
+            batch_id: string
+            /** 直连商户的appid */
+            appid: string
+            /** 商家明细单号 */
+            out_detail_no: string
+            /** 微信明细单号 */
+            detail_id: string
+            /** 明细状态 */
+            detail_status: string
+            /** 转账金额 */
+            transfer_amount: number
+            /** 转账备注 */
+            transfer_remark: string
+            /** 明细失败原因 */
+            fail_reason?: string
+            /** 用户在直连商户应用下的用户标示 */
+            openid: string
+            /** 收款用户姓名 */
+            user_name?: string
+            /** 转账发起时间 */
+            initiate_time: Date
+            /** 明细更新时间 */
+            update_time: Date
+        }
+
+        export interface IOutput extends Output {
+            data?: DetailOutput
+        }
+    }
+}

+ 177 - 0
src/weixin/lib/interface.ts

@@ -0,0 +1,177 @@
+// 订单金额信息
+interface Iamount {
+    total: number
+    currency?: string
+}
+// 优惠功能
+interface Idetail {
+    cost_price?: number
+    invoice_id?: string
+    goods_detail?: IgoodsDetail[]
+}
+// 单品列表信息
+interface IgoodsDetail {
+    merchant_goods_id: string
+    wechatpay_goods_id?: string
+    goods_name?: string
+    quantity: number
+    unit_price: number
+}
+// 支付者
+interface Ipayer {
+    openid: string
+}
+// 支付场景描述
+interface IsceneInfoH5 {
+    payer_client_ip: string
+    device_id?: string
+    store_info?: IstoreInfo
+    h5_info: Ih5Info
+}
+interface IsceneInfoNative {
+    payer_client_ip: string
+    device_id?: string
+    store_info?: IstoreInfo
+}
+// 商户门店信息
+interface IstoreInfo {
+    id: string
+    name?: string
+    area_code?: string
+    address?: string
+}
+// H5场景信息
+interface Ih5Info {
+    type: string
+    app_name: string
+    app_url?: string
+    bundle_id?: string
+    package_name?: string
+}
+
+// 抛出
+export interface Ioptions {
+    userAgent?: string
+    authType?: string
+    key?: string
+    serial_no?: string
+}
+export interface Ipay {
+    appid: string //  直连商户申请的公众号或移动应用appid。
+    mchid: string // 商户号
+    serial_no?: string // 证书序列号
+    publicKey: Buffer // 公钥
+    privateKey: Buffer // 密钥
+    authType?: string // 认证类型,目前为WECHATPAY2-SHA256-RSA2048
+    userAgent?: string
+    key?: string
+}
+export interface Ih5 {
+    description: string
+    out_trade_no: string
+    time_expire?: string
+    attach?: string
+    notify_url: string
+    goods_tag?: string
+    amount: Iamount
+    detail?: Idetail
+    scene_info: IsceneInfoH5
+}
+export interface Inative {
+    description: string
+    out_trade_no: string
+    time_expire?: string
+    attach?: string
+    notify_url: string
+    goods_tag?: string
+    amount: Iamount
+    detail?: Idetail
+    scene_info?: IsceneInfoNative
+}
+export interface Ijsapi {
+    description: string
+    out_trade_no: string
+    time_expire?: string
+    attach?: string
+    notify_url: string
+    goods_tag?: string
+    amount: Iamount
+    payer: Ipayer
+    detail?: Idetail
+    scene_info?: IsceneInfoNative
+}
+export interface Iapp {
+    description: string
+    out_trade_no: string
+    time_expire?: string
+    attach?: string
+    notify_url: string
+    goods_tag?: string
+    amount: Iamount
+    detail?: Idetail
+    scene_info?: IsceneInfoNative
+}
+export interface Iquery1 {
+    transaction_id: string
+    out_trade_no?: string
+}
+export interface Iquery2 {
+    transaction_id?: string
+    out_trade_no: string
+}
+export interface Itradebill {
+    bill_date: string
+    sub_mchid?: string
+    bill_type: string
+    tar_type?: string
+}
+export interface Ifundflowbill {
+    bill_date: string
+    account_type: string
+    tar_type?: string
+}
+export interface Irefunds {
+    out_refund_no: string
+    reason?: string
+    notify_url?: string
+    funds_account?: string
+    amount: IRamount
+    goods_detail?: IRgoodsDetail[]
+}
+export interface Irefunds1 extends Irefunds {
+    transaction_id: string
+    out_trade_no?: string
+}
+export interface Irefunds2 extends Irefunds {
+    transaction_id?: string
+    out_trade_no: string
+}
+interface IRamount {
+    total: number
+    currency: string
+    refund: number
+}
+interface IRgoodsDetail {
+    merchant_goods_id: string
+    wechatpay_goods_id?: string
+    goods_name?: string
+    refund_quantity: number
+    unit_price: number
+    refund_amount: number
+}
+
+/**
+ * 证书信息
+ */
+export interface ICertificates {
+    effective_time: string
+    expire_time: string
+    serial_no: string
+    publicKey?: string
+    encrypt_certificate: {
+        algorithm: string
+        associated_data: string
+        ciphertext: string
+        nonce: string
+    }
+}

+ 773 - 0
src/weixin/pay.ts

@@ -0,0 +1,773 @@
+import * as crypto from 'crypto'
+const x509_1 = require('@fidm/x509')
+
+import {
+    Ipay,
+    Ih5,
+    Inative,
+    Ijsapi,
+    Iquery1,
+    Iquery2,
+    Itradebill,
+    Ifundflowbill,
+    Iapp,
+    Ioptions,
+    Irefunds1,
+    Irefunds2,
+    ICertificates
+} from './lib/interface'
+import { IcombineH5, IcombineNative, IcombineApp, IcombineJsapi, IcloseSubOrders } from './lib/combine_interface'
+import { BatchesTransfer } from './lib/interface-v2'
+import { Base } from './lib/base'
+
+export class Pay extends Base {
+    private appid: string //  直连商户申请的公众号或移动应用appid。
+    private mchid: string // 商户号
+    private serial_no = '' // 证书序列号
+    private publicKey?: Buffer // 公钥
+    private privateKey?: Buffer // 密钥
+    private authType = 'WECHATPAY2-SHA256-RSA2048' // 认证类型,目前为WECHATPAY2-SHA256-RSA2048
+
+    private key?: string // APIv3密钥
+    private static certificates: { [key in string]: string } = {} // 微信平台证书 key 是 serialNo, value 是 publicKey
+    /**
+     * 构造器
+     * @param appid 直连商户申请的公众号或移动应用appid。
+     * @param mchid 商户号
+     * @param publicKey 公钥
+     * @param privateKey 密钥
+     * @param optipns 可选参数 object 包括下面参数
+     *
+     * @param serial_no  证书序列号
+     * @param authType 可选参数 认证类型,目前为WECHATPAY2-SHA256-RSA2048
+     * @param userAgent 可选参数 User-Agent
+     * @param key 可选参数 APIv3密钥
+     */
+    public constructor(appid: string, mchid: string, publicKey: Buffer, privateKey: Buffer, optipns?: Ioptions)
+    /**
+     * 构造器
+     * @param obj object类型 包括下面参数
+     *
+     * @param appid 直连商户申请的公众号或移动应用appid。
+     * @param mchid 商户号
+     * @param serial_no  可选参数 证书序列号
+     * @param publicKey 公钥
+     * @param privateKey 密钥
+     * @param authType 可选参数 认证类型,目前为WECHATPAY2-SHA256-RSA2048
+     * @param userAgent 可选参数 User-Agent
+     * @param key 可选参数 APIv3密钥
+     */
+    public constructor(obj: Ipay)
+    public constructor(
+        arg1: Ipay | string,
+        mchid?: string,
+        publicKey?: Buffer,
+        privateKey?: Buffer,
+        optipns?: Ioptions
+    ) {
+        super()
+
+        if (arg1 instanceof Object) {
+            this.appid = arg1.appid
+            this.mchid = arg1.mchid
+            if (arg1.serial_no) this.serial_no = arg1.serial_no
+            this.publicKey = arg1.publicKey
+            if (!this.publicKey) throw new Error('缺少公钥')
+            this.privateKey = arg1.privateKey
+            if (!arg1.serial_no) this.serial_no = this.getSN(this.publicKey)
+
+            this.authType = arg1.authType || 'WECHATPAY2-SHA256-RSA2048'
+            this.userAgent = arg1.userAgent || '127.0.0.1'
+            this.key = arg1.key
+        } else {
+            const _optipns = optipns || {}
+            this.appid = arg1
+            this.mchid = mchid || ''
+            this.publicKey = publicKey
+            this.privateKey = privateKey
+
+            this.authType = _optipns.authType || 'WECHATPAY2-SHA256-RSA2048'
+            this.userAgent = _optipns.userAgent || '127.0.0.1'
+            this.key = _optipns.key
+            this.serial_no = _optipns.serial_no || ''
+            if (!this.publicKey) throw new Error('缺少公钥')
+            if (!this.serial_no) this.serial_no = this.getSN(this.publicKey)
+        }
+    }
+    /**
+     * 获取微信平台key
+     * @param apiSecret APIv3密钥
+     * @returns
+     */
+    public async get_certificates(apiSecret: string): Promise<ICertificates[]> {
+        const url = 'https://api.mch.weixin.qq.com/v3/certificates'
+        const authorization = this.init('GET', url)
+        const result = await this.getRequest(url, authorization)
+
+        if (result.status === 200) {
+            const data = result.data as ICertificates[]
+
+            for (const item of data) {
+                const decryptCertificate = this.decipher_gcm<string>(
+                    item.encrypt_certificate.ciphertext,
+                    item.encrypt_certificate.associated_data,
+                    item.encrypt_certificate.nonce,
+                    apiSecret
+                )
+                item.publicKey = x509_1.Certificate.fromPEM(Buffer.from(decryptCertificate)).publicKey.toPEM()
+            }
+
+            return data
+        } else {
+            throw new Error('拉取平台证书失败')
+        }
+    }
+    /**
+     * 拉取平台证书到 Pay.certificates 中
+     * @param apiSecret APIv3密钥
+     * https://pay.weixin.qq.com/wiki/doc/apiv3/apis/wechatpay5_1.shtml
+     */
+    private async fetchCertificates(apiSecret?: string) {
+        const url = 'https://api.mch.weixin.qq.com/v3/certificates'
+        const authorization = this.init('GET', url)
+        const result = await this.getRequest(url, authorization)
+
+        if (result.status === 200) {
+            const data = result.data as {
+                effective_time: string
+                expire_time: string
+                serial_no: string
+                encrypt_certificate: {
+                    algorithm: string
+                    associated_data: string
+                    ciphertext: string
+                    nonce: string
+                }
+            }[]
+
+            const newCertificates = {} as { [key in string]: string }
+
+            data.forEach((item) => {
+                const decryptCertificate = this.decipher_gcm<string>(
+                    item.encrypt_certificate.ciphertext,
+                    item.encrypt_certificate.associated_data,
+                    item.encrypt_certificate.nonce,
+                    apiSecret
+                )
+
+                newCertificates[item.serial_no] = x509_1.Certificate.fromPEM(
+                    Buffer.from(decryptCertificate)
+                ).publicKey.toPEM()
+            })
+
+            Pay.certificates = {
+                ...Pay.certificates,
+                ...newCertificates
+            }
+        } else {
+            throw new Error('拉取平台证书失败')
+        }
+    }
+    /**
+     * 验证签名,提醒:node 取头部信息时需要用小写,例如:req.headers['wechatpay-timestamp']
+     * @param params.timestamp HTTP头Wechatpay-Timestamp 中的应答时间戳
+     * @param params.nonce HTTP头Wechatpay-Nonce 中的应答随机串
+     * @param params.body 应答主体(response Body),需要按照接口返回的顺序进行验签,错误的顺序将导致验签失败。
+     * @param params.serial HTTP头Wechatpay-Serial 证书序列号
+     * @param params.signature HTTP头Wechatpay-Signature 签名
+     * @param params.apiSecret APIv3密钥,如果在 构造器 中有初始化该值(this.key),则可以不传入。当然传入也可以
+     */
+    public async verifySign(params: {
+        timestamp: string | number
+        nonce: string
+        body: Record<string, any> | string
+        serial: string
+        signature: string
+        apiSecret?: string
+    }) {
+        const { timestamp, nonce, body, serial, signature, apiSecret } = params
+
+        let publicKey = Pay.certificates[serial]
+
+        if (!publicKey) {
+            await this.fetchCertificates(apiSecret)
+        }
+
+        publicKey = Pay.certificates[serial]
+
+        if (!publicKey) {
+            throw new Error('平台证书序列号不相符,未找到平台序列号')
+        }
+
+        const bodyStr = typeof body === 'string' ? body : JSON.stringify(body)
+        const data = `${timestamp}\n${nonce}\n${bodyStr}\n`
+        const verify = crypto.createVerify('RSA-SHA256')
+        verify.update(data)
+
+        return verify.verify(publicKey, signature, 'base64')
+    }
+    /**
+     * 敏感信息加密
+     * @param str 敏感信息字段(如用户的住址、银行卡号、手机号码等)
+     * @returns
+     */
+    public publicEncrypt(str: string, wxPublicKey: Buffer, padding = crypto.constants.RSA_PKCS1_OAEP_PADDING) {
+        if (![crypto.constants.RSA_PKCS1_PADDING, crypto.constants.RSA_PKCS1_OAEP_PADDING].includes(padding)) {
+            throw new Error(
+                `Doesn't supported the padding mode(${padding}), here's only support RSA_PKCS1_OAEP_PADDING or RSA_PKCS1_PADDING.`
+            )
+        }
+        const encrypted = crypto
+            .publicEncrypt({ key: wxPublicKey, padding, oaepHash: 'sha1' }, Buffer.from(str, 'utf8'))
+            .toString('base64')
+        return encrypted
+    }
+    /**
+     * 敏感信息解密
+     * @param str 敏感信息字段(如用户的住址、银行卡号、手机号码等)
+     * @returns
+     */
+    public privateDecrypt(str: string, padding = crypto.constants.RSA_PKCS1_OAEP_PADDING) {
+        if (![crypto.constants.RSA_PKCS1_PADDING, crypto.constants.RSA_PKCS1_OAEP_PADDING].includes(padding)) {
+            throw new Error(
+                `Doesn't supported the padding mode(${padding}), here's only support RSA_PKCS1_OAEP_PADDING or RSA_PKCS1_PADDING.`
+            )
+        }
+        const decrypted = crypto.privateDecrypt(
+            { key: this.privateKey as Buffer, padding, oaepHash: 'sha1' },
+            Buffer.from(str, 'base64')
+        )
+        return decrypted.toString('utf8')
+    }
+    /**
+     * 构建请求签名参数
+     * @param method Http 请求方式
+     * @param url 请求接口 例如/v3/certificates
+     * @param timestamp 获取发起请求时的系统当前时间戳
+     * @param nonceStr 随机字符串
+     * @param body 请求报文主体
+     */
+    public getSignature(
+        method: string,
+        nonce_str: string,
+        timestamp: string,
+        url: string,
+        body?: string | Record<string, any>
+    ): string {
+        let str = method + '\n' + url + '\n' + timestamp + '\n' + nonce_str + '\n'
+        if (body && body instanceof Object) body = JSON.stringify(body)
+        if (body) str = str + body + '\n'
+        if (method === 'GET') str = str + '\n'
+        return this.sha256WithRsa(str)
+    }
+    // jsapi 和 app 支付参数签名 加密自动顺序如下 不能错乱
+    // 应用id
+    // 时间戳
+    // 随机字符串
+    // 预支付交易会话ID
+    private sign(str: string) {
+        return this.sha256WithRsa(str)
+    }
+    // 获取序列号
+    public getSN(fileData?: string | Buffer): string {
+        if (!fileData && !this.publicKey) throw new Error('缺少公钥')
+        if (!fileData) fileData = this.publicKey
+        if (typeof fileData == 'string') {
+            fileData = Buffer.from(fileData)
+        }
+
+        const certificate = x509_1.Certificate.fromPEM(fileData)
+        return certificate.serialNumber
+    }
+    /**
+     * SHA256withRSA
+     * @param data 待加密字符
+     * @param privatekey 私钥key  key.pem   fs.readFileSync(keyPath)
+     */
+    public sha256WithRsa(data: string): string {
+        if (!this.privateKey) throw new Error('缺少秘钥文件')
+        return crypto.createSign('RSA-SHA256').update(data).sign(this.privateKey, 'base64')
+    }
+    /**
+     * 获取授权认证信息
+     * @param nonceStr  请求随机串
+     * @param timestamp 时间戳
+     * @param signature 签名值
+     */
+    public getAuthorization(nonce_str: string, timestamp: string, signature: string): string {
+        const _authorization =
+            'mchid="' +
+            this.mchid +
+            '",' +
+            'nonce_str="' +
+            nonce_str +
+            '",' +
+            'timestamp="' +
+            timestamp +
+            '",' +
+            'serial_no="' +
+            this.serial_no +
+            '",' +
+            'signature="' +
+            signature +
+            '"'
+        return this.authType.concat(' ').concat(_authorization)
+    }
+    /**
+     * 回调解密
+     * @param ciphertext  Base64编码后的开启/停用结果数据密文
+     * @param associated_data 附加数据
+     * @param nonce 加密使用的随机串
+     * @param key  APIv3密钥
+     */
+    public decipher_gcm<T extends any>(ciphertext: string, associated_data: string, nonce: string, key?: string): T {
+        if (key) this.key = key
+        if (!this.key) throw new Error('缺少key')
+
+        const _ciphertext = Buffer.from(ciphertext, 'base64')
+
+        // 解密 ciphertext字符  AEAD_AES_256_GCM算法
+        const authTag: any = _ciphertext.slice(_ciphertext.length - 16)
+        const data = _ciphertext.slice(0, _ciphertext.length - 16)
+        const decipher = crypto.createDecipheriv('aes-256-gcm', this.key, nonce)
+        decipher.setAuthTag(authTag)
+        decipher.setAAD(Buffer.from(associated_data))
+        const decoded = decipher.update(data, undefined, 'utf8')
+        decipher.final()
+
+        try {
+            return JSON.parse(decoded)
+        } catch (e) {
+            return decoded as T
+        }
+    }
+    /**
+     * 参数初始化
+     */
+    private init(method: string, url: string, params?: Record<string, any>) {
+        const nonce_str = Math.random().toString(36).substr(2, 15),
+            timestamp = parseInt(+new Date() / 1000 + '').toString()
+
+        const signature = this.getSignature(
+            method,
+            nonce_str,
+            timestamp,
+            url.replace('https://api.mch.weixin.qq.com', ''),
+            params
+        )
+        const authorization = this.getAuthorization(nonce_str, timestamp, signature)
+        return authorization
+    }
+    // ---------------支付相关接口--------------//
+    /**
+     * h5支付
+     * @param params 请求参数 object 参数介绍 请看文档https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_3_1.shtml
+     */
+    public async transactions_h5(params: Ih5): Promise<Record<string, any>> {
+        // 请求参数
+        const _params = {
+            appid: this.appid,
+            mchid: this.mchid,
+            ...params
+        }
+        const url = 'https://api.mch.weixin.qq.com/v3/pay/transactions/h5'
+
+        const authorization = this.init('POST', url, _params)
+
+        return await this.postRequest(url, _params, authorization)
+    }
+    /**
+     * 合单h5支付
+     * @param params 请求参数 object 参数介绍 请看文档https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter5_1_2.shtml
+     */
+    public async combine_transactions_h5(params: IcombineH5): Promise<Record<string, any>> {
+        // 请求参数
+        const _params = {
+            combine_appid: this.appid,
+            combine_mchid: this.mchid,
+            ...params
+        }
+        const url = 'https://api.mch.weixin.qq.com/v3/combine-transactions/h5'
+
+        const authorization = this.init('POST', url, _params)
+
+        return await this.postRequest(url, _params, authorization)
+    }
+    /**
+     * native支付
+     * @param params 请求参数 object 参数介绍 请看文档https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml
+     */
+    public async transactions_native(params: Inative): Promise<Record<string, any>> {
+        // 请求参数
+        const _params = {
+            appid: this.appid,
+            mchid: this.mchid,
+            ...params
+        }
+        const url = 'https://api.mch.weixin.qq.com/v3/pay/transactions/native'
+
+        const authorization = this.init('POST', url, _params)
+
+        return await this.postRequest(url, _params, authorization)
+    }
+    /**
+     * 合单native支付
+     * @param params 请求参数 object 参数介绍 请看文档https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter5_1_5.shtml
+     */
+    public async combine_transactions_native(params: IcombineNative): Promise<Record<string, any>> {
+        // 请求参数
+        const _params = {
+            combine_appid: this.appid,
+            combine_mchid: this.mchid,
+            ...params
+        }
+        const url = 'https://api.mch.weixin.qq.com/v3/combine-transactions/native'
+
+        const authorization = this.init('POST', url, _params)
+
+        return await this.postRequest(url, _params, authorization)
+    }
+    /**
+     * app支付
+     * @param params 请求参数 object 参数介绍 请看文档https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_2_1.shtml
+     */
+    public async transactions_app(params: Iapp): Promise<Record<string, any>> {
+        // 请求参数
+        const _params = {
+            appid: this.appid,
+            mchid: this.mchid,
+            ...params
+        }
+        const url = 'https://api.mch.weixin.qq.com/v3/pay/transactions/app'
+
+        const authorization = this.init('POST', url, _params)
+
+        const result: any = await this.postRequest(url, _params, authorization)
+        if (result.status === 200 && result.prepay_id) {
+            const data = {
+                status: result.status,
+                appid: this.appid,
+                partnerid: this.mchid,
+                package: 'Sign=WXPay',
+                timestamp: parseInt(+new Date() / 1000 + '').toString(),
+                noncestr: Math.random().toString(36).substr(2, 15),
+                prepayid: result.prepay_id,
+                sign: ''
+            }
+            const str = [data.appid, data.timestamp, data.noncestr, data.prepayid, ''].join('\n')
+            data.sign = this.sign(str)
+            return data
+        }
+        return result
+    }
+    /**
+     * 合单app支付
+     * @param params 请求参数 object 参数介绍 请看文档https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter5_1_1.shtml
+     */
+    public async combine_transactions_app(params: IcombineApp): Promise<Record<string, any>> {
+        // 请求参数
+        const _params = {
+            combine_appid: this.appid,
+            combine_mchid: this.mchid,
+            ...params
+        }
+        const url = 'https://api.mch.weixin.qq.com/v3/combine-transactions/app'
+
+        const authorization = this.init('POST', url, _params)
+
+        const result: any = await this.postRequest(url, _params, authorization)
+        if (result.status === 200 && result.prepay_id) {
+            const data = {
+                status: result.status,
+                appid: this.appid,
+                partnerid: this.mchid,
+                package: 'Sign=WXPay',
+                timestamp: parseInt(+new Date() / 1000 + '').toString(),
+                noncestr: Math.random().toString(36).substr(2, 15),
+                prepayid: result.prepay_id,
+                sign: ''
+            }
+            const str = [data.appid, data.timestamp, data.noncestr, data.prepayid, ''].join('\n')
+            data.sign = this.sign(str)
+            return data
+        }
+        return result
+    }
+    /**
+     * JSAPI支付 或者 小程序支付
+     * @param params 请求参数 object 参数介绍 请看文档https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml
+     */
+    public async transactions_jsapi(params: Ijsapi): Promise<Record<string, any>> {
+        // 请求参数
+        const _params = {
+            appid: this.appid,
+            mchid: this.mchid,
+            ...params
+        }
+        const url = 'https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi'
+
+        const authorization = this.init('POST', url, _params)
+
+        const result: any = await this.postRequest(url, _params, authorization)
+        if (result.status === 200 && result.prepay_id) {
+            const data = {
+                status: result.status,
+                appId: this.appid,
+                timeStamp: parseInt(+new Date() / 1000 + '').toString(),
+                nonceStr: Math.random().toString(36).substr(2, 15),
+                package: `prepay_id=${result.prepay_id}`,
+                signType: 'RSA',
+                paySign: ''
+            }
+            const str = [data.appId, data.timeStamp, data.nonceStr, data.package, ''].join('\n')
+            data.paySign = this.sign(str)
+            return data
+        }
+        return result
+    }
+    /**
+     * 合单JSAPI支付 或者 小程序支付
+     * @param params 请求参数 object 参数介绍 请看文档https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter5_1_3.shtml
+     */
+    public async combine_transactions_jsapi(params: IcombineJsapi): Promise<Record<string, any>> {
+        // 请求参数
+        const _params = {
+            combine_appid: this.appid,
+            combine_mchid: this.mchid,
+            ...params
+        }
+        const url = 'https://api.mch.weixin.qq.com/v3/combine-transactions/jsapi'
+
+        const authorization = this.init('POST', url, _params)
+
+        const result: any = await this.postRequest(url, _params, authorization)
+        if (result.status === 200 && result.prepay_id) {
+            const data = {
+                status: result.status,
+                appId: this.appid,
+                timeStamp: parseInt(+new Date() / 1000 + '').toString(),
+                nonceStr: Math.random().toString(36).substr(2, 15),
+                package: `prepay_id=${result.prepay_id}`,
+                signType: 'RSA',
+                paySign: ''
+            }
+            const str = [data.appId, data.timeStamp, data.nonceStr, data.package, ''].join('\n')
+            data.paySign = this.sign(str)
+            return data
+        }
+        return result
+    }
+    /**
+     * 查询订单
+     * @param params 请求参数 object 参数介绍 请看文档https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_3_2.shtml
+     */
+    public async query(params: Iquery1 | Iquery2): Promise<Record<string, any>> {
+        let url = ''
+        if (params.transaction_id) {
+            url = `https://api.mch.weixin.qq.com/v3/pay/transactions/id/${params.transaction_id}?mchid=${this.mchid}`
+        } else if (params.out_trade_no) {
+            url = `https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/${params.out_trade_no}?mchid=${this.mchid}`
+        } else {
+            throw new Error('缺少transaction_id或者out_trade_no')
+        }
+
+        const authorization = this.init('GET', url)
+        return await this.getRequest(url, authorization)
+    }
+    /**
+     * 合单查询订单
+     * @param params 请求参数 object 参数介绍 请看文档https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter5_1_11.shtml
+     */
+    public async combine_query(combine_out_trade_no: string): Promise<Record<string, any>> {
+        if (!combine_out_trade_no) throw new Error('缺少combine_out_trade_no')
+        const url = `https://api.mch.weixin.qq.com/v3/combine-transactions/out-trade-no/${combine_out_trade_no}`
+
+        const authorization = this.init('GET', url)
+        return await this.getRequest(url, authorization)
+    }
+    /**
+     * 关闭订单
+     * @param out_trade_no 请求参数 商户订单号 参数介绍 请看文档https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_3_3.shtml
+     */
+    public async close(out_trade_no: string): Promise<Record<string, any>> {
+        if (!out_trade_no) throw new Error('缺少out_trade_no')
+
+        // 请求参数
+        const _params = {
+            mchid: this.mchid
+        }
+        const url = `https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/${out_trade_no}/close`
+        const authorization = this.init('POST', url, _params)
+
+        return await this.postRequest(url, _params, authorization)
+    }
+    /**
+     * 合单关闭订单
+     * @param combine_out_trade_no 请求参数 总订单号 参数介绍 请看文档https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter5_1_12.shtml
+     * @param sub_orders array 子单信息
+     */
+    public async combine_close(
+        combine_out_trade_no: string,
+        sub_orders: IcloseSubOrders[]
+    ): Promise<Record<string, any>> {
+        if (!combine_out_trade_no) throw new Error('缺少out_trade_no')
+
+        // 请求参数
+        const _params = {
+            combine_appid: this.appid,
+            sub_orders
+        }
+        const url = `https://api.mch.weixin.qq.com/v3/combine-transactions/out-trade-no/${combine_out_trade_no}/close`
+        const authorization = this.init('POST', url, _params)
+
+        return await this.postRequest(url, _params, authorization)
+    }
+    /**
+     * 申请交易账单
+     * @param params 请求参数 object 参数介绍 请看文档https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_6.shtml
+     */
+    public async tradebill(params: Itradebill): Promise<Record<string, any>> {
+        let url = 'https://api.mch.weixin.qq.com/v3/bill/tradebill'
+        const _params: any = {
+            ...params
+        }
+        const querystring = Object.keys(_params)
+            .filter((key) => {
+                return !!_params[key]
+            })
+            .sort()
+            .map((key) => {
+                return key + '=' + _params[key]
+            })
+            .join('&')
+        url = url + `?${querystring}`
+        const authorization = this.init('GET', url)
+        return await this.getRequest(url, authorization)
+    }
+    /**
+     * 申请资金账单
+     * @param params 请求参数 object 参数介绍 请看文档https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_7.shtml
+     */
+    public async fundflowbill(params: Ifundflowbill): Promise<Record<string, any>> {
+        let url = 'https://api.mch.weixin.qq.com/v3/bill/fundflowbill'
+        const _params: any = {
+            ...params
+        }
+        const querystring = Object.keys(_params)
+            .filter((key) => {
+                return !!_params[key]
+            })
+            .sort()
+            .map((key) => {
+                return key + '=' + _params[key]
+            })
+            .join('&')
+        url = url + `?${querystring}`
+        const authorization = this.init('GET', url)
+        return await this.getRequest(url, authorization)
+    }
+    /**
+     * 下载账单
+     * @param download_url 请求参数 路径 参数介绍 请看文档https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_8.shtml
+     */
+    public async downloadbill(download_url: string) {
+        const authorization = this.init('GET', download_url)
+        return await this.getRequest(download_url, authorization)
+    }
+    /**
+     * 申请退款
+     * @param params 请求参数 路径 参数介绍 请看文档https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_2_9.shtml
+     */
+    public async refunds(params: Irefunds1 | Irefunds2): Promise<Record<string, any>> {
+        const url = 'https://api.mch.weixin.qq.com/v3/refund/domestic/refunds'
+        // 请求参数
+        const _params = {
+            ...params
+        }
+
+        const authorization = this.init('POST', url, _params)
+
+        return await this.postRequest(url, _params, authorization)
+    }
+    /**
+     * 查询单笔退款
+     * @documentation 请求参数 路径 参数介绍 请看文档https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_2_10.shtml
+     */
+    public async find_refunds(out_refund_no: string): Promise<Record<string, any>> {
+        if (!out_refund_no) throw new Error('缺少out_refund_no')
+        const url = `https://api.mch.weixin.qq.com/v3/refund/domestic/refunds/${out_refund_no}`
+
+        const authorization = this.init('GET', url)
+        return await this.getRequest(url, authorization)
+    }
+    /**
+     * 发起商家转账零钱
+     * @documentation 请看文档https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_3_1.shtml
+     */
+    public async batches_transfer(params: BatchesTransfer.Input): Promise<BatchesTransfer.IOutput> {
+        const url = 'https://api.mch.weixin.qq.com/v3/transfer/batches'
+        // 请求参数
+        const _params = {
+            appid: this.appid,
+            ...params
+        }
+
+        const serial_no = _params?.wx_serial_no
+        delete _params.wx_serial_no
+        const authorization = this.init('POST', url, _params)
+
+        return await this.postRequestV2(url, _params, authorization, {
+            'Wechatpay-Serial': serial_no || this.serial_no
+        })
+    }
+    /**
+     * 微信批次单号查询批次单API
+     * @documentation 请看文档https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_3_2.shtml
+     */
+    public async query_batches_transfer_list_wx(
+        params: BatchesTransfer.QueryBatchesTransferByWx.Input
+    ): Promise<BatchesTransfer.QueryBatchesTransferByWx.IOutput> {
+        const baseUrl = `https://api.mch.weixin.qq.com/v3/transfer/batches/batch-id/${params.batch_id}`
+        const url = baseUrl + this.objectToQueryString(params, ['batch_id'])
+        const authorization = this.init('GET', url)
+        return await this.getRequestV2(url, authorization)
+    }
+    /**
+     * 微信明细单号查询明细单API
+     * @documentation 请看文档https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_3_3.shtml
+     */
+    public async query_batches_transfer_detail_wx(
+        params: BatchesTransfer.QueryBatchesTransferDetailByWx.Input
+    ): Promise<BatchesTransfer.QueryBatchesTransferDetailByWx.IOutput> {
+        const baseUrl = `https://api.mch.weixin.qq.com/v3/transfer/batches/batch-id/${params.batch_id}/details/detail-id/${params.detail_id}`
+        const url = baseUrl + this.objectToQueryString(params, ['batch_id', 'detail_id'])
+        const authorization = this.init('GET', url)
+        return await this.getRequestV2(url, authorization)
+    }
+    /**
+     * 商家批次单号查询批次单API
+     * @documentation 请看文档https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_3_5.shtml
+     */
+    public async query_batches_transfer_list(
+        params: BatchesTransfer.QueryBatchesTransferList.Input
+    ): Promise<BatchesTransfer.QueryBatchesTransferList.IOutput> {
+        const baseUrl = `https://api.mch.weixin.qq.com/v3/transfer/batches/out-batch-no/${params.out_batch_no}`
+        const url = baseUrl + this.objectToQueryString(params, ['out_batch_no'])
+        const authorization = this.init('GET', url)
+        return await this.getRequestV2(url, authorization)
+    }
+    /**
+     * 商家明细单号查询明细单API
+     * @documentation 请看文档https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_3_6.shtml
+     */
+    public async query_batches_transfer_detail(
+        params: BatchesTransfer.QueryBatchesTransferDetail.Input
+    ): Promise<BatchesTransfer.QueryBatchesTransferDetail.IOutput> {
+        const baseUrl = `https://api.mch.weixin.qq.com/v3/transfer/batches/out-batch-no/${params.out_batch_no}/details/out-detail-no/${params.out_detail_no}`
+        const url = baseUrl + this.objectToQueryString(params, ['out_batch_no', 'out_detail_no'])
+        const authorization = this.init('GET', url)
+        return await this.getRequestV2(url, authorization)
+    }
+}
+
+exports = Pay

+ 6 - 0
src/weixin/weixin.controller.ts

@@ -34,6 +34,12 @@ export class WeixinController {
         return await this.weixinService.pay(openid)
     }
 
+    @Public()
+    @Get('/pay1')
+    public async pay1(@Query() { openid }) {
+        return await this.weixinService.pay1(openid)
+    }
+
     @Public()
     @Get('/jsapiSign')
     public async jsapiSign(@Query() { url }) {

+ 64 - 2
src/weixin/weixin.service.ts

@@ -27,6 +27,8 @@ import { addSeconds } from 'date-fns'
 import * as fs from 'node:fs'
 import * as path from 'path'
 import { JsapiTicket } from './entities/jsapiTicket.entity'
+import { Pay as WxPay } from './pay'
+import axios from 'axios'
 
 @Injectable()
 export class WeixinService {
@@ -125,8 +127,7 @@ export class WeixinService {
                 openid
             }
         }
-        let result = await PayKit.v3(
-            RequestMethod.POST,
+        let result = await PayKit.exePost(
             WX_DOMAIN.CHINA, //
             WX_API_TYPE.JS_API_PAY,
             this.weixinConfiguration.mchId,
@@ -161,6 +162,67 @@ export class WeixinService {
         }
     }
 
+    async pay1(openid: string) {
+        const pay = new WxPay({
+            appid: this.weixinConfiguration.appId,
+            mchid: this.weixinConfiguration.mchId,
+            publicKey: this.publicKey, // 公钥
+            privateKey: this.privateKey // 秘钥
+        })
+        // 这里以h5支付为例
+        try {
+            // // 参数介绍请看h5支付文档 https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_3_1.shtml
+            // const params = {
+            //     appid: this.weixinConfiguration.appId,
+            //     mchid: this.weixinConfiguration.mchId,
+            //     description: '测试',
+            //     out_trade_no: new Date().getTime().toString(),
+            //     notify_url: 'https://chillgpt.raexmeta.com/weixin/notify',
+            //     amount: {
+            //         total: 1,
+            //         currency: 'CNY'
+            //     },
+            //     payer: {
+            //         openid
+            //     }
+            // }
+            // const nonce_str = Math.random().toString(36).substr(2, 15), // 随机字符串
+            //     timestamp = parseInt(+new Date() / 1000 + '').toString(), // 时间戳 秒
+            //     url = '/v3/pay/transactions/jsapi'
+
+            // // 获取签名
+            // const signature = pay.getSignature('POST', nonce_str, timestamp, url, params) // 如果是get 请求 则不需要params 参数拼接在url上 例如 /v3/pay/transactions/id/12177525012014?mchid=1230000109
+            // // 获取头部authorization 参数
+            // const authorization = pay.getAuthorization(nonce_str, timestamp, signature)
+
+            // const result = await axios.post('https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi', params, {
+            //     headers: {
+            //         Accept: 'application/json',
+            //         'Content-Type': 'application/json',
+            //         'User-Agent':
+            //             'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36',
+            //         Authorization: authorization
+            //     }
+            // })
+
+            const result = await pay.transactions_jsapi({
+                description: 'test111',
+                out_trade_no: new Date().getTime().toString(),
+                notify_url: 'https://chillgpt.raexmeta.com/weixin/notify',
+                amount: {
+                    total: 1,
+                    currency: 'CNY'
+                },
+                payer: {
+                    openid
+                }
+            })
+            console.log('result==========>', result)
+        } catch (error) {
+            console.log(error)
+        }
+    }
+
     async getCert() {
         try {
             let result = await PayKit.exeGet(

+ 32 - 1
yarn.lock

@@ -471,6 +471,19 @@
   resolved "https://registry.npmmirror.com/@esm2cjs/p-timeout/-/p-timeout-6.0.0.tgz#6d5c9a28f3479bb5d45cfa571d51113c9dc0e18e"
   integrity sha512-pGJ/8I7UfSfLZhw0JbwKGta8inZf1FYStEHyKIV9lK3c5rTIP5QZNZkPDB2O5u4LuxNd2bNB3iTOJCY35LQeog==
 
+"@fidm/asn1@^1.0.4":
+  version "1.0.4"
+  resolved "https://registry.npmmirror.com/@fidm/asn1/-/asn1-1.0.4.tgz#afbf9f10a0cb83aca2114c6f59a97dd48eb7dd84"
+  integrity sha512-esd1jyNvRb2HVaQGq2Gg8Z0kbQPXzV9Tq5Z14KNIov6KfFD6PTaRIO8UpcsYiTNzOqJpmyzWgVTrUwFV3UF4TQ==
+
+"@fidm/x509@^1.2.1":
+  version "1.2.1"
+  resolved "https://registry.npmmirror.com/@fidm/x509/-/x509-1.2.1.tgz#ae546a661005d776cc4dca674b399c1132c1e0e4"
+  integrity sha512-nwc2iesjyc9hkuzcrMCBXQRn653XuAUKorfWM8PZyJawiy1QzLj4vahwzaI25+pfpwOLvMzbJ0uKpWLDNmo16w==
+  dependencies:
+    "@fidm/asn1" "^1.0.4"
+    tweetnacl "^1.0.1"
+
 "@humanwhocodes/config-array@^0.11.8":
   version "0.11.8"
   resolved "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9"
@@ -2440,6 +2453,11 @@ crypt@0.0.2:
   resolved "https://registry.npmmirror.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b"
   integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==
 
+crypto@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.npmmirror.com/crypto/-/crypto-1.0.1.tgz#2af1b7cad8175d24c8a1b0778255794a21803037"
+  integrity sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==
+
 dashdash@^1.12.0:
   version "1.14.1"
   resolved "https://registry.npmmirror.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
@@ -5945,7 +5963,7 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
   resolved "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
   integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
 
-superagent@^8.0.5:
+superagent@^8.0.5, superagent@^8.0.6:
   version "8.0.9"
   resolved "https://registry.npmmirror.com/superagent/-/superagent-8.0.9.tgz#2c6fda6fadb40516515f93e9098c0eb1602e0535"
   integrity sha512-4C7Bh5pyHTvU33KpZgwrNKh/VQnvgtCSqPRfJAUdmrtSYePVzVg4E4OzsrbkhJj9O7SO6Bnv75K/F8XVZT8YHA==
@@ -6260,6 +6278,11 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0:
   resolved "https://registry.npmmirror.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
   integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==
 
+tweetnacl@^1.0.1:
+  version "1.0.3"
+  resolved "https://registry.npmmirror.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596"
+  integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==
+
 type-check@^0.4.0, type-check@~0.4.0:
   version "0.4.0"
   resolved "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
@@ -6558,6 +6581,14 @@ webpack@5.76.2:
     watchpack "^2.4.0"
     webpack-sources "^3.2.3"
 
+wechatpay-node-v3@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.npmmirror.com/wechatpay-node-v3/-/wechatpay-node-v3-2.1.1.tgz#d84f8463cb44b811b5990740991c8c5d1ae472b4"
+  integrity sha512-pAWxzXd7xz4YonFDXvJTG4hc5o+3NPWDwKrC8wykQ0yCTltHFfrPwrEqvMFq28aqz69jp223gY6At3taDkpdCg==
+  dependencies:
+    "@fidm/x509" "^1.2.1"
+    superagent "^8.0.6"
+
 whatwg-fetch@^3.4.1:
   version "3.6.2"
   resolved "https://registry.npmmirror.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c"