xiongzhu 2 лет назад
Родитель
Сommit
b7b768f3e2

+ 1 - 1
.env

@@ -1,5 +1,5 @@
 NODE_ENV=dev
-NODE_API_PORT=
+NODE_API_PORT=3001
 
 ENDPOINT_CORS=
 

+ 1 - 1
src/app.module.ts

@@ -17,7 +17,7 @@ import { AllExceptionsFilter } from './filters/all-exceptions-filter.filter'
     imports: [
         DevtoolsModule.register({
             http: process.env.NODE_ENV !== 'production',
-            port: 8000
+            port: 8001
         }),
         ConfigModule.forRoot({
             isGlobal: true,

+ 15 - 0
src/weixin/dto/attach.dto.ts

@@ -0,0 +1,15 @@
+export enum AttachType {
+    MEMBER_ORDER = 'MEMBER_ORDER'
+}
+
+export class Attach {
+    type: AttachType
+    id?: number
+    extra?: any
+
+    constructor(type: AttachType, id?: number, extra?: any) {
+        this.type = type
+        this.id = id
+        this.extra = extra
+    }
+}

+ 19 - 0
src/weixin/entities/access-token-cache.entity.ts

@@ -0,0 +1,19 @@
+import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'
+
+@Entity()
+export class AccessTokenCache {
+
+    @PrimaryGeneratedColumn()
+    id: number
+
+    @Column()
+    json: string
+
+    @Column()
+    expireAt: Date
+
+    constructor(json: string, expireAt: Date) {
+        this.json = json
+        this.expireAt = expireAt
+    }
+}

+ 18 - 0
src/weixin/entities/jsapi-ticket-cache.entity.ts

@@ -0,0 +1,18 @@
+import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'
+
+@Entity()
+export class JsapiTicketCache {
+    @PrimaryGeneratedColumn()
+    id: number
+
+    @Column()
+    json: string
+
+    @Column()
+    expireAt: Date
+
+    constructor(json: string, expireAt: Date) {
+        this.json = json
+        this.expireAt = expireAt
+    }
+}

+ 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

+ 91 - 0
src/weixin/types.ts

@@ -0,0 +1,91 @@
+declare namespace WechatPay {
+    type TradeState = 'SUCCESS' | 'REFUND' | 'NOTPAY' | 'CLOSED' | 'REVOKED' | 'USERPAYING' | 'PAYERROR'
+    type TradeType = 'JSAPI' | 'NATIVE' | 'APP' | 'MICROPAY' | 'MWEB' | 'FACEPAY'
+
+    interface Amount {
+        total: number
+        payer_total?: number
+        currency: string
+        payer_currency?: string
+    }
+
+    interface GoodsDetail {
+        goods_remark: string
+        quantity: number
+        discount_amount: number
+        goods_id: string
+        unit_price: number
+    }
+
+    interface PromotionDetail {
+        amount: number
+        wechatpay_contribute: number
+        coupon_id: string
+        scope: string
+        merchant_contribute: number
+        name: string
+        other_contribute: number
+        currency: string
+        stock_id: string
+        goods_detail: GoodsDetail[]
+    }
+
+    interface Payer {
+        openid: string
+    }
+
+    interface SceneInfo {
+        device_id: string
+    }
+
+    interface NotifyData {
+        transaction_id: string
+        amount: Amount
+        mchid: string
+        trade_state: TradeState
+        bank_type: string
+        success_time: Date
+        payer: Payer
+        out_trade_no: string
+        appid: string
+        trade_state_desc: string
+        trade_type: TradeType
+        attach?: string
+        scene_info?: any
+        promotion_detail?: any
+    }
+
+    interface Resource {
+        original_type?: string
+        algorithm: string
+        ciphertext: string
+        associated_data: string
+        nonce: string
+    }
+    interface NotifyEvent {
+        id: string
+        create_time: Date
+        resource_type: string
+        event_type: string
+        summary: string
+        resource: Resource
+    }
+
+    interface EncryptCertificate {
+        algorithm: string
+        nonce: string
+        associated_data: string
+        ciphertext: string
+    }
+
+    interface CertData {
+        serial_no: string
+        effective_time: Date
+        expire_time: Date
+        encrypt_certificate: EncryptCertificate
+    }
+
+    interface GetCertResponse {
+        data: CertData[]
+    }
+}

+ 18 - 0
src/weixin/weixin.config.ts

@@ -0,0 +1,18 @@
+import { ConfigService, registerAs } from '@nestjs/config'
+import { config } from 'dotenv'
+
+config()
+
+const configService = new ConfigService()
+
+export default registerAs('weixin', () => {
+    return {
+        appId: configService.get<string>('WX_APP_ID'),
+        appSecret: configService.get<string>('WX_APP_SECRET'),
+        mchId: configService.get<string>('WX_MCH_ID'),
+        mchKey: configService.get<string>('WX_MCH_KEY'),
+        certSerial: configService.get<string>('WX_MCH_CERT_SERIAL'),
+        certPath: configService.get<string>('WX_MCH_CERT_PATH'),
+        notifyUrl: configService.get<string>('WX_NOTIFY_URL'),
+    }
+})

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

@@ -0,0 +1,34 @@
+import { Body, Controller, Get, Post, Query } from '@nestjs/common'
+import { WeixinService } from './weixin.service'
+import { Public } from '../auth/public.decorator'
+import { ApiQuery, ApiTags } from '@nestjs/swagger'
+
+@ApiTags('weixin')
+@Controller('/weixin')
+export class WeixinController {
+    constructor(private readonly weixinService: WeixinService) {}
+
+    @Get('/accessToken')
+    public async getAccessToken() {
+        return await this.weixinService.getAccessToken()
+    }
+
+    @Public()
+    @Post('/redirectUrl')
+    @ApiQuery({ name: 'url' })
+    public getRedirectUrl(@Body() { url, state }) {
+        return { url: this.weixinService.getRedirectUrl(url, state) }
+    }
+
+    @Public()
+    @Get('/code2openid')
+    public async code2openid(@Query() { code }) {
+        return await this.weixinService.code2oenId(code)
+    }
+
+    @Public()
+    @Post('/jsapiSign')
+    public async jsapiSign(@Body() { url }) {
+        return await this.weixinService.jsapiSign(url)
+    }
+}

+ 16 - 0
src/weixin/weixin.module.ts

@@ -0,0 +1,16 @@
+import { Module } from '@nestjs/common'
+import { WeixinController } from './weixin.controller'
+import { WeixinService } from './weixin.service'
+import weixinConfig from './weixin.config'
+import { ConfigModule } from '@nestjs/config'
+import { TypeOrmModule } from '@nestjs/typeorm'
+import { AccessTokenCache } from './entities/access-token-cache.entity'
+import { JsapiTicketCache } from './entities/jsapi-ticket-cache.entity'
+
+@Module({
+    imports: [ConfigModule.forFeature(weixinConfig), TypeOrmModule.forFeature([AccessTokenCache, JsapiTicketCache])],
+    controllers: [WeixinController],
+    providers: [WeixinService],
+    exports: [WeixinService]
+})
+export class WeixinModule {}

+ 229 - 0
src/weixin/weixin.service.ts

@@ -0,0 +1,229 @@
+import { Inject, Injectable, InternalServerErrorException, Logger } from '@nestjs/common'
+import weixinConfig from './weixin.config'
+import { ConfigType } from '@nestjs/config'
+import {
+    ApiConfig,
+    ApiConfigKit,
+    AccessTokenApi,
+    SnsAccessTokenApi,
+    ScopeEnum,
+    PayKit,
+    WX_DOMAIN,
+    WX_API_TYPE,
+    Kits,
+    JsTicketApi,
+    JsApiType,
+    AccessToken,
+    JsTicket
+} from 'tnwx'
+import { InjectRepository } from '@nestjs/typeorm'
+import { AccessTokenCache } from './entities/access-token-cache.entity'
+import { LessThan, MoreThan, Not, Repository } from 'typeorm'
+import { addSeconds, differenceInMinutes } from 'date-fns'
+import { readFileSync, writeFileSync, statSync } from 'fs'
+import { JsapiTicketCache } from './entities/jsapi-ticket-cache.entity'
+import { Attach } from './dto/attach.dto'
+import { join } from 'path'
+import BigNumber from 'bignumber.js'
+
+@Injectable()
+export class WeixinService {
+    private privateKey: Buffer
+    private publicKey: Buffer
+    private platformPlublicKey: Buffer
+    constructor(
+        @Inject(weixinConfig.KEY)
+        private readonly weixinConfiguration: ConfigType<typeof weixinConfig>,
+        @InjectRepository(AccessTokenCache)
+        private readonly accessTokenRepository: Repository<AccessTokenCache>,
+        @InjectRepository(JsapiTicketCache)
+        private readonly jsapiTicketRepository: Repository<JsapiTicketCache>
+    ) {
+        let apiConfig = new ApiConfig(weixinConfiguration.appId, weixinConfiguration.appSecret, '')
+        ApiConfigKit.putApiConfig(apiConfig)
+        ApiConfigKit.devMode = true
+        ApiConfigKit.setCurrentAppId(apiConfig.getAppId)
+        this.privateKey = readFileSync(join(__dirname, '..', '..', 'certs', 'apiclient_key.pem'))
+        this.publicKey = readFileSync(join(__dirname, '..', '..', 'certs', 'apiclient_cert.pem'))
+        this.platformPlublicKey = readFileSync(join(__dirname, '..', '..', 'certs', 'platform_cert.pem'))
+        this.getCert()
+    }
+
+    async getAccessToken(): Promise<AccessToken> {
+        return await AccessTokenApi.getAccessToken()
+    }
+
+    async getJsapiTicket() {
+        return await JsTicketApi.getTicket(JsApiType.JSAPI, await this.getAccessToken())
+    }
+
+    async refreshAccessToken(): Promise<AccessToken> {
+        const res: AccessToken = await AccessTokenApi.getAccessToken()
+        Logger.log(JSON.stringify(res))
+        Logger.log(res.getJson)
+        if (res.getErrCode != 0) {
+            throw new InternalServerErrorException(res.getErrMsg)
+        }
+        const newToken = await this.accessTokenRepository.save(
+            new AccessTokenCache(res.getJson, addSeconds(new Date(), res.getExpiresIn - 300))
+        )
+        await this.accessTokenRepository.delete({ id: Not(newToken.id) })
+        return res
+    }
+
+    async refreshTicket() {
+        const res: JsTicket = await JsTicketApi.getTicket(JsApiType.JSAPI, await this.getAccessToken())
+        Logger.log(JSON.stringify(res))
+        Logger.log(res.getJson)
+        if (res.getErrCode != 0) {
+            throw new InternalServerErrorException(res.getErrMsg)
+        }
+        const newTicket = await this.jsapiTicketRepository.save(
+            new JsapiTicketCache(res.getJson, addSeconds(new Date(), res.getExpiresIn - 300))
+        )
+        await this.jsapiTicketRepository.delete({ id: Not(newTicket.id) })
+        return res
+    }
+
+    getRedirectUrl(url: string, state?: string) {
+        return SnsAccessTokenApi.getAuthorizeUrl(url, ScopeEnum.SNSAPI_BASE, state)
+    }
+
+    async code2oenId(code: string) {
+        const res = await SnsAccessTokenApi.getSnsAccessToken(code)
+        if (res.errcode) {
+            throw new InternalServerErrorException(res.errmsg)
+        }
+        return res.openid
+    }
+
+    async jsapiPay(description: string, amount: number, openid: string, attach: Attach) {
+        let realAmount = new BigNumber(amount)
+        if (realAmount.comparedTo(new BigNumber(100)) >= 0) {
+            realAmount = realAmount.minus(new BigNumber((Math.random() * 2).toFixed(2)))
+        }
+        let data = {
+            appid: this.weixinConfiguration.appId,
+            mchid: this.weixinConfiguration.mchId,
+            description: description,
+            out_trade_no: Kits.generateStr(),
+            notify_url: this.weixinConfiguration.notifyUrl,
+            amount: {
+                total: parseInt(realAmount.times(new BigNumber(100)).toFixed(0)),
+                currency: 'CNY'
+            },
+            payer: {
+                openid
+            },
+            attach: JSON.stringify(attach)
+        }
+        let result = await PayKit.exePost(
+            WX_DOMAIN.CHINA, //
+            WX_API_TYPE.JS_API_PAY,
+            this.weixinConfiguration.mchId,
+            this.weixinConfiguration.certSerial,
+            this.privateKey,
+            JSON.stringify(data)
+        )
+        Logger.log(JSON.stringify(result.data, null, 2), 'jsapi下单')
+        if (result.status === 200) {
+            const timeStamp = parseInt((new Date().getTime() / 1000).toString()).toString()
+            const nonceStr = Kits.generateStr()
+            const packageStr = `prepay_id=${result.data.prepay_id}`
+            const signType = 'RSA'
+            const signStr =
+                [this.weixinConfiguration.appId, timeStamp, nonceStr, packageStr].join(String.fromCharCode(10)) +
+                String.fromCharCode(10)
+            const paySign = Kits.sha256WithRsa(signStr, this.privateKey)
+            return {
+                appId: this.weixinConfiguration.appId,
+                timestamp: timeStamp,
+                nonceStr,
+                package: packageStr,
+                signType,
+                paySign
+            }
+        } else {
+            throw new InternalServerErrorException(result.data.message)
+        }
+    }
+
+    async getCert() {
+        try {
+            const certPath = join(__dirname, '..', '..', 'certs', 'platform_cert.pem')
+
+            let result = await PayKit.exeGet(
+                WX_DOMAIN.CHINA, //
+                WX_API_TYPE.GET_CERTIFICATES,
+                this.weixinConfiguration.mchId,
+                this.weixinConfiguration.certSerial,
+                this.privateKey
+            )
+            Logger.log('OK', '获取平台证书')
+            const data = result.data as WechatPay.GetCertResponse
+            let headers = result.headers
+            let serial = headers['wechatpay-serial']
+            let timestamp = headers['wechatpay-timestamp']
+            let nonce = headers['wechatpay-nonce']
+            let signature = headers['wechatpay-signature']
+
+            // 根据序列号查证书  验证签名
+            // let verifySignature: boolean = PayKit.verifySignature(signature, data, nonce, timestamp, wxPublicKey)
+            let verifySignature: boolean = PayKit.verifySign(headers, JSON.stringify(data), this.platformPlublicKey)
+            Logger.log(verifySignature, '验证签名')
+
+            result.data.data.sort((a, b) => {
+                return a.effective_time > b.effective_time ? -1 : 1
+            })
+            let decrypt = PayKit.aes256gcmDecrypt(
+                this.weixinConfiguration.mchKey,
+                data.data[0].encrypt_certificate.nonce,
+                data.data[0].encrypt_certificate.associated_data,
+                data.data[0].encrypt_certificate.ciphertext
+            )
+            // 保存证书
+            writeFileSync(certPath, decrypt)
+            this.platformPlublicKey = readFileSync(certPath)
+            Logger.log(certPath, '保存平台证书')
+            return data
+        } catch (error) {
+            Logger.error(error)
+        }
+    }
+
+    async jsapiSign(url: string) {
+        const noncestr = Kits.generateStr()
+        const timestamp = parseInt((new Date().getTime() / 1000).toString())
+        const jsapiTicket = (await this.getJsapiTicket()).getTicket
+        const str = `jsapi_ticket=${jsapiTicket}&noncestr=${noncestr}&timestamp=${timestamp}&url=${url}`
+        const signature = Kits.sha1(str)
+        Logger.log(`\n${url}\n${jsapiTicket}\n${noncestr}\n${timestamp}\n${url}`, 'jsapiSign body')
+        Logger.log(signature, 'jsapiSign signature')
+        return {
+            appId: this.weixinConfiguration.appId,
+            timestamp,
+            nonceStr: noncestr,
+            signature
+        }
+    }
+
+    async getNotifyData(headers, data: WechatPay.NotifyEvent) {
+        Logger.log(JSON.stringify(headers, null, 2), '微信回调Header')
+        Logger.log(JSON.stringify(data, null, 2), '微信回调Body')
+        let verifySignature: boolean = PayKit.verifySign(headers, JSON.stringify(data), this.platformPlublicKey)
+        Logger.log(verifySignature, '验证签名')
+        if (!verifySignature) {
+            throw new InternalServerErrorException('签名验证失败')
+        }
+        let notifyData: WechatPay.NotifyData = JSON.parse(
+            PayKit.aes256gcmDecrypt(
+                this.weixinConfiguration.mchKey,
+                data.resource.nonce,
+                data.resource.associated_data,
+                data.resource.ciphertext
+            )
+        )
+        Logger.log(JSON.stringify(notifyData, null, 2), '解密数据')
+        return notifyData
+    }
+}