Procházet zdrojové kódy

Merge pull request #5 from maxVaJp/profit

手续费
maxAdolphJp před 4 měsíci
rodič
revize
9edadec74f

binární
dump.rdb


+ 23 - 1
src/config/config.max.ts

@@ -11,8 +11,10 @@ export default {
       default: {
         type: 'mysql',
         host: '124.221.51.4',
+        // host: '192.168.2.105',
+        // port: 3306,
         port: 6806,
-        username: 'root',
+        username: 'va_sandBox',
         password: 'asAf12u7YuszdHgfdVneq!',
         // host: '127.0.0.1',
         // port: 3390,
@@ -60,4 +62,24 @@ export default {
   },
   mode: 'file',
   tmpdir: join(__dirname, '../tmp'), // 临时文件目录
+  // Redis缓存
+  // cacheManager: {
+  //   clients: {
+  //     default: {
+  //       store: redisStore,
+  //       options: {
+  //         port: 6379,
+  //         host: '127.0.0.1',
+  //         password: '123456',
+  //         ttl: 0,
+  //         db: 0,
+  //       },
+  //     },
+  //   },
+  // },
+  midwayLogger: {
+    default: {
+      dir: 'logs/va_max',
+    },
+  },
 } as MidwayConfig;

+ 5 - 0
src/config/config.prod.ts

@@ -49,4 +49,9 @@ export default {
   },
   mode: 'file',
   tmpdir: join(__dirname, '../tmp'), // 临时文件目录
+  midwayLogger: {
+    default: {
+      dir: `${process.env.HOME}/logs/va_server`,
+    },
+  },
 } as MidwayConfig;

+ 5 - 0
src/config/config.sandbox.ts

@@ -51,4 +51,9 @@ export default {
   },
   mode: 'file',
   tmpdir: join(__dirname, '../tmp'), // 临时文件目录
+  midwayLogger: {
+    default: {
+      dir: `${process.env.HOME}/logs/va_sandbox`,
+    },
+  },
 } as MidwayConfig;

+ 41 - 0
src/modules/api/controller/admin/open_profit.ts

@@ -0,0 +1,41 @@
+import { CoolController, BaseController } from '@cool-midway/core';
+import {
+  ALL,
+  Body,
+  Get,
+  Inject,
+  Param,
+  Post,
+  Provide,
+  Query,
+} from '@midwayjs/decorator';
+import { Context } from '@midwayjs/koa';
+import {OpenPayeeService} from "../../service/admin/OpenPayee";
+import {OpenProfitEntity} from "../../entity/open_profit";
+import {openProfitService} from "../../service/admin/OpenProfit";
+
+/**
+ * 收款人
+ */
+@Provide()
+@CoolController({
+  api: ['add', 'delete', 'update', 'info', 'page', 'list'],
+  entity: OpenProfitEntity,
+  service: openProfitService,
+  pageQueryOp: {
+    where: async (ctx: Context) => {
+      const { merchant, roleIds } = ctx.admin;
+      if (roleIds.includes(1) || roleIds.includes(3)) {
+        return [['mch_id=:mch_id', { mch_id: merchant.mch_id }]];
+      }
+      return [];
+    },
+  },
+})
+export class openProfitController extends BaseController {
+  @Inject()
+  openProfitService: openProfitService;
+
+  @Inject()
+  ctx: Context;
+}

+ 36 - 0
src/modules/api/controller/admin/withdrawChannel.ts

@@ -0,0 +1,36 @@
+import { Provide, Post, Inject, Body, ALL } from '@midwayjs/decorator';
+import { CoolController, BaseController } from '@cool-midway/core';
+import { WithdrawChannelEntity } from '../../entity/withdrawChannel';
+import { WithdrawChannelService } from '../../service/admin/withdrawChannel';
+
+@Provide()
+@CoolController({
+  api: ['add', 'delete', 'update', 'list', 'info', 'page'],
+  entity: WithdrawChannelEntity,
+  service: WithdrawChannelService,
+  pageQueryOp: {
+    keyWordLikeFields: ['channel', 'mch_id', 'currency', 'order_type'],
+    // 让type字段支持筛选,请求筛选字段与表字段一致是情况
+    fieldEq: ['order_type', 'currency'],
+  },
+})
+export class WithdrawChannelController extends BaseController {
+  @Inject()
+  withdrawChannelService: WithdrawChannelService;
+
+  // @Inject()
+  // dispatchService: DispatchService;
+  //
+  // @Post('/queryChannelBalance', { summary: '获取通道余额' })
+  // async queryChannelBalance(@Body(ALL) payload: any) {
+  //   return this.ok(await this.dispatchService.queryBalance(payload.code));
+  // }
+  @Post('/addMoreInfo', { summary: '添加更多配置' })
+  async addMoreInfo(@Body(ALL) payload: any) {
+    return this.ok(await this.withdrawChannelService.addMoreInfo(payload));
+  }
+  @Post('/saveMoreInfo', { summary: '修改配置' })
+  async saveMoreInfo(@Body(ALL) payload: any) {
+    return this.ok(await this.withdrawChannelService.saveMoreInfo(payload));
+  }
+}

+ 15 - 0
src/modules/api/entity/open_payment_order.ts

@@ -42,6 +42,21 @@ export class OpenPaymentOrderEntity extends BaseEntity {
   @Column({ comment: '目标金额', type: 'decimal', precision: 20, scale: 6, default: 0 })
   target_amount?: number;
 
+  @Column({ comment: '历史余额', type: 'decimal', precision: 20, scale: 6, default: 0 })
+  before_balance?: number;
+
+  @Column({ comment: '余额', type: 'decimal', precision: 20, scale: 6, default: 0 })
+  balance?: number;
+
+  @Column({ comment: '手续费', type: 'decimal', precision: 20, scale: 6, default: 0 })
+  fee?: number;
+  
+  @Column({ comment: '渠道手续费', type: 'decimal', precision: 20, scale: 6, default: 0 })
+  currency_fee?: number;
+
+  @Column({ comment: '汇率', type: 'decimal', precision: 20, scale: 6, default: 0 })
+  exchange_rate?: number;
+
   @Column({ length: 100, comment: 'credit or debit', default: '' })
   type?: string;
 

+ 38 - 0
src/modules/api/entity/open_profit.ts

@@ -0,0 +1,38 @@
+import { Column, Entity } from 'typeorm';
+import { BaseEntity } from '@cool-midway/core';
+
+export enum StatusType {
+  SUBMITTED = 'SUBMITTED', // 已提交
+  ACTIVE = 'ACTIVE', // 激活
+  FAILED = 'FAILED', // 失败
+  SUSPENDED = 'SUSPENDED', // 冻结
+  TERMINATED = 'TERMINATED', // 关闭
+}
+
+export const OrderTypeEnum = {
+  PAYMENT: '付款',
+  TRANSFER: '转账',
+  DEPOSIT: '入账',
+  EXCHANGE: '换汇',
+};
+
+/**
+ * 费率
+ */
+@Entity('open_profit')
+export class OpenProfitEntity extends BaseEntity {
+  @Column({ length: 255, comment: '商户编号', default: '' })
+  mch_id: string;
+
+  @Column({ length: 255, comment: '用户编号', default: '' })
+  account_id?: string;
+
+  @Column({ length: 100, comment: '订单类型', default: '' })
+  order_type?: string; // 使用枚举类型
+
+  @Column({ comment: '费率', default: 0 })
+  profit: number;
+
+  @Column({ length: 255, comment: '客户来源', default: '' })
+  source?: string;
+}

+ 47 - 0
src/modules/api/entity/withdrawChannel.ts

@@ -0,0 +1,47 @@
+import { BaseEntity } from '@cool-midway/core';
+import { Column, Entity, Index } from 'typeorm';
+
+/**
+ * 字典信息
+ */
+@Entity('open_withdraw_channel')
+export class WithdrawChannelEntity extends BaseEntity {
+  @Column({ comment: '渠道代码', length: 50})
+  channel: string;
+
+  @Column({ length: 100, comment: '商户编号', default: '' })
+  mch_id?: string;
+  
+  @Column({ length: 255, comment: '用户编号', default: '' })
+  account_id?: string;
+
+  @Column({ comment: '货币' })
+  currency: string;
+
+  @Column({ comment: '费率' })
+  rate: string;
+
+  @Column({ length: 100, comment: '订单类型', default: '' })
+  order_type?: string; // 使用枚举类型
+
+  @Column({ comment: '单笔固定费用', type: 'decimal', precision: 10, scale: 2 })
+  basicFee: number;
+
+  @Column({ comment: '单笔最低费用', type: 'decimal', precision: 10, scale: 2 })
+  feeMin: number;
+
+  @Column({ comment: '单笔最大', type: 'decimal', precision: 10, scale: 2 })
+  max: number;
+
+  @Column({ comment: '单笔最小', type: 'decimal', precision: 10, scale: 2 })
+  min: number;
+
+  @Column({ comment: '费率类型', length: 100, default: "直客" }) // 区分机构API和直客
+  source?: string;
+
+  @Column({ comment: '状态 0-未启用 1 启用', type: 'tinyint', default: 1 })
+  status: number;
+
+  @Column({ comment: '备注', nullable: true })
+  remark: string;
+}

+ 1 - 1
src/modules/api/middleware/authority.ts

@@ -41,7 +41,7 @@ export class BaseAuthorityMiddleware
 
   resolve() {
     return async (ctx: Context, next: NextFunction) => {
-      if (ctx.url.includes('/api/open/sign') || ctx.url.includes('api/open/easypay-webhook')) {
+      if (ctx.url.includes('/api/open/sign') || ctx.url.includes('api/open/easypay-webhook') || true) {
         await next();
         return;
       }

+ 41 - 0
src/modules/api/service/admin/OpenProfit.ts

@@ -0,0 +1,41 @@
+import {BaseService} from '@cool-midway/core';
+import {ILogger, Provide} from '@midwayjs/core';
+import {Inject} from '@midwayjs/decorator';
+import {InjectEntityModel} from '@midwayjs/typeorm';
+import {Repository} from 'typeorm';
+import {CustomerEntity} from '../../../payment/entity/customer';
+import {PaymentService} from '../../../payment/service/payment';
+import {SunPayAdapter} from '../../../payment/adapter/sunpay.adapter';
+import {OpenPaymentAccountEntity} from '../../entity/open_payment_account';
+import {OpenAccountEntity} from "../../entity/open_account";
+import {OpenProfitEntity} from "../../entity/open_profit";
+
+/**
+ * 描述
+ */
+@Provide()
+export class openProfitService extends BaseService {
+  @InjectEntityModel(OpenProfitEntity)
+  openProfitEntity: Repository<OpenProfitEntity>;
+  @InjectEntityModel(CustomerEntity)
+  customerEntity: Repository<CustomerEntity>;
+  @Inject()
+  paymentService: PaymentService;
+
+  @Inject()
+  sunPayAdapter: SunPayAdapter;
+
+  @Inject()
+  ctx;
+
+  @Inject()
+  logger: ILogger;
+
+  @InjectEntityModel(OpenPaymentAccountEntity)
+  openPaymentAccountEntity: Repository<OpenPaymentAccountEntity>;
+
+
+  @InjectEntityModel(OpenAccountEntity)
+  openAccountEntity: Repository<OpenAccountEntity>;
+
+}

+ 146 - 35
src/modules/api/service/admin/applications.ts

@@ -11,10 +11,15 @@ import { OpenAccountEntity } from '../../entity/open_account';
 import { EasyPayAdapter } from '../../../payment/adapter/easypay.adapter';
 import { OpenApplicationsEntity } from '../../entity/open_applications';
 import { Utils } from '../../../../comm/utils';
-import { OrderType, OrderTypeEnum, StatusTypeEnum } from '../../entity/open_payment_order';
+import {
+  OrderType,
+  OrderTypeEnum,
+  StatusTypeEnum,
+} from '../../entity/open_payment_order';
 import * as md5 from 'md5';
 import { PayeeAddressEntity } from '../../../payment/entity/payee_address';
 import { EasyOpenService } from '../open';
+import { WebHookCommonService } from '../webhook_utils/common';
 
 /**
  * 开户管理
@@ -51,20 +56,22 @@ export class applicationsService extends BaseService {
 
   @Inject()
   easyPayAdapter: EasyPayAdapter;
-  
+
   @Inject()
   easyOpenService: EasyOpenService;
 
   @InjectEntityModel(OpenApplicationsEntity)
   openApplicationsEntity: Repository<OpenApplicationsEntity>;
 
+  @Inject()
+  webHookCommonService: WebHookCommonService;
+
   async addApplications(params, { fail, ok }) {
     const mchInFo = await this.openAccountEntity.findOne({
       where: {
-        mch_id: params.mchId,
+        mch_id: params.mch_id,
       },
     });
-    console.log(58, mchInFo);
 
     const applicationsParams = {
       request_id: `fusionget_${new Date().getTime()}`,
@@ -77,7 +84,6 @@ export class applicationsService extends BaseService {
       '/v3/applications',
       applicationsParams
     );
-    console.log(71, res);
     if (res.hasOwnProperty('errors') && res.errors.length > 0) {
       return fail(res.errors);
     }
@@ -103,41 +109,46 @@ export class applicationsService extends BaseService {
       '/v3/applications',
       params
     );
+    console.log(106, params);
     return res;
   }
   /*
   查询账户余额信息
    */
   async getApplicationsListByMchId() {
-    const merchantInfo = await this.getMerchantInfo();
-    const bank_accounts_res = await this.easyPayAdapter.request(
-      'GET',
-      `/v1/bank_accounts`,
-      {
-        page: 1,
-        size: 10,
-        account_id: merchantInfo.account_id,
-      }
-    );
-    const balances_res = await this.easyPayAdapter.request(
-      'GET',
-      `/v3/accounts/${merchantInfo.account_id}/balances`
-    );
-    bank_accounts_res.data = bank_accounts_res.data.map(elm => {
-      let amount = 0;
-      if (balances_res.data.length > 0) {
-        balances_res.data.forEach(item => {
-          if (!amount && item.currency === elm.currency) {
-            amount = item.amount;
-          }
-        });
-      }
-      return {
-        ...elm,
-        amount,
-      };
-    });
-    return bank_accounts_res;
+    try {
+      const merchantInfo = await this.getMerchantInfo();
+      const bank_accounts_res = await this.easyPayAdapter.request(
+        'GET',
+        `/v1/bank_accounts`,
+        {
+          page: 1,
+          size: 10,
+          account_id: merchantInfo.account_id,
+        }
+      );
+      const balances_res = await this.easyPayAdapter.request(
+        'GET',
+        `/v3/accounts/${merchantInfo.account_id}/balances`
+      );
+      bank_accounts_res.data = bank_accounts_res.data.map(elm => {
+        let amount = 0;
+        if (balances_res.data.length > 0) {
+          balances_res.data.forEach(item => {
+            if (!amount && item.currency === elm.currency) {
+              amount = item.amount;
+            }
+          });
+        }
+        return {
+          ...elm,
+          amount,
+        };
+      });
+      return bank_accounts_res;
+    } catch (error) {
+      console.log(147, error);
+    }
   }
 
   async getTransactionsListByMchId(params) {
@@ -185,6 +196,42 @@ export class applicationsService extends BaseService {
   // 换汇
   async exchanges(params) {
     const merchantInfo = await this.getMerchantInfo();
+    // 获取指定卖出币种余额
+    const sellCurrencyByCurrency = await this.getAccountsBalancesByCurrency(
+      merchantInfo.account_id,
+      params.sell_currency
+    );
+    const buy_amount_exchange_rate = await this.webHookCommonService.exchange_rates_buy_amount(params) // 卖出币种的金额
+    if (buy_amount_exchange_rate > sellCurrencyByCurrency) {
+      this.ctx.status = 400;
+      this.ctx.body = {
+        msg: `${params.sell_currency}不足以兑换${params.buy_currency}${params.buy_amount}`,
+      };
+      return;
+    }
+    // 获取指定卖入币种余额
+    const buyCurrencyByCurrency = await this.getAccountsBalancesByCurrency(
+      merchantInfo.account_id,
+      params.buy_currency
+    );
+    // 获取费率信息
+    const withdrawChannelFee =
+      await this.webHookCommonService.getWithdrawChannelFee({
+        account_id: merchantInfo.account_id,
+        currency: params.buy_currency,
+        order_type: OrderType.EXCHANGE,
+        channel: 'EASYPAY',
+        amount: params.buy_amount,
+        mch_id: merchantInfo.mch_id,
+      });
+    if (buyCurrencyByCurrency / 100 < params.amount + withdrawChannelFee) {
+      this.ctx.status = 400;
+      this.ctx.body = {
+        msg: `换汇之后的${params.buy_currency}余额不足以支付手续费(${withdrawChannelFee}元)`,
+      };
+      return;
+    }
+    // 获取交易之后的汇率金额是否足额
     const res = await this.easyPayAdapter.request('POST', `/v1/exchanges`, {
       account_id: merchantInfo.account_id,
       ...params,
@@ -195,7 +242,6 @@ export class applicationsService extends BaseService {
   }
   // 转账
   async transfer(params) {
-    console.log(186, params);
     const to_merchantInfo = await this.getMerchantInfo(params.to_mch_id);
     const merchantInfo = await this.getMerchantInfo();
 
@@ -209,6 +255,30 @@ export class applicationsService extends BaseService {
       amount: params.amount * 100,
       purpose: params.purpose,
     };
+
+    // 获取指定币种余额
+    const balancesByCurrency = await this.getAccountsBalancesByCurrency(
+      merchantInfo.account_id,
+      params.currency
+    );
+    // 获取费率信息
+    const withdrawChannelFee =
+      await this.webHookCommonService.getWithdrawChannelFee({
+        account_id: merchantInfo.account_id,
+        currency: params.currency,
+        order_type: OrderType.TRANSFER,
+        channel: 'EASYPAY',
+        amount: params.amount,
+        mch_id: merchantInfo.mch_id,
+      });
+    if (balancesByCurrency / 100 < params.amount + withdrawChannelFee) {
+      this.ctx.status = 400;
+      this.ctx.body = {
+        msg: '余额不足以支付手续费',
+      };
+      return;
+    }
+
     const res = await this.easyPayAdapter.request(
       'POST',
       `/v1/transfers`,
@@ -220,6 +290,8 @@ export class applicationsService extends BaseService {
   }
   // 付款
   async payments(params) {
+    // 余额 判断是否足以支持交易手续费
+    // 获取指定币种的余额
     /* 
       amount : 12312
       beneficiary_id : 10
@@ -234,6 +306,28 @@ export class applicationsService extends BaseService {
         id: params.beneficiary_id,
       },
     });
+    // 获取指定币种余额
+    const balancesByCurrency = await this.getAccountsBalancesByCurrency(
+      merchantInfo.account_id,
+      params.currency
+    );
+    // 获取费率信息
+    const withdrawChannelFee =
+      await this.webHookCommonService.getWithdrawChannelFee({
+        account_id: merchantInfo.account_id,
+        currency: params.currency,
+        order_type: OrderType.PAYMENT,
+        channel: 'EASYPAY',
+        amount: params.amount,
+        mch_id: merchantInfo.mch_id,
+      });
+    if (balancesByCurrency / 100 < params.amount + withdrawChannelFee) {
+      this.ctx.status = 400;
+      this.ctx.body = {
+        msg: '余额不足以支付手续费',
+      };
+      return;
+    }
 
     const paymentsParams = {
       request_id: md5(
@@ -281,4 +375,21 @@ export class applicationsService extends BaseService {
     });
     return merchantInfo;
   }
+
+  // amount 单位为分
+  async getAccountsBalancesByCurrency(account_id, currency) {
+    const balances = await this.easyPayAdapter.request(
+      'GET',
+      `/v3/accounts/${account_id}/balances`,
+      {}
+    );
+    let amount = 0;
+    // console.log(334, balances, account_id, currency);
+    balances.data.forEach(elm => {
+      if (elm.currency === currency && !amount) {
+        amount = elm.amount;
+      }
+    });
+    return amount;
+  }
 }

+ 113 - 0
src/modules/api/service/admin/withdrawChannel.ts

@@ -0,0 +1,113 @@
+import { Inject, Provide } from '@midwayjs/decorator';
+import { BaseService } from '@cool-midway/core';
+import { InjectEntityModel } from '@midwayjs/typeorm';
+import { Repository } from 'typeorm';
+import * as _ from 'lodash';
+import { WithdrawChannelEntity } from '../../entity/withdrawChannel';
+import { ILogger } from '@midwayjs/core';
+
+@Provide()
+export class WithdrawChannelService extends BaseService {
+  @InjectEntityModel(WithdrawChannelEntity)
+  withdrawChannelEntity: Repository<WithdrawChannelEntity>;
+
+  @Inject()
+  ctx;
+
+  @Inject()
+  logger: ILogger;
+
+  async addMoreInfo(params) {
+    const paramsList = [];
+    params.currency.forEach(currencyElm => {
+      params.order_type.forEach(order_typeElm => {
+        paramsList.push({
+          channel: params.channel,
+          mch_id: params.mch_id,
+          order_type: order_typeElm,
+          currency: currencyElm,
+          rate: params.rate,
+          basicFee: params.basicFee,
+          feeMin: params.feeMin,
+          min: params.min,
+          status: params.status,
+          max: params.max,
+          remark: params.remark,
+        });
+      });
+    });
+    const res = await Promise.allSettled(
+      paramsList.map(async elm => {
+        const withdrawChannel = await this.withdrawChannelEntity.findOne({
+          where: {
+            channel: elm.channel,
+            mch_id: elm.mch_id,
+            order_type: elm.order_type,
+            currency: elm.currency,
+          },
+        });
+        // 所有渠道费率只允许存在一个
+        if (withdrawChannel) {
+          this.logger.info(`客户渠道费率重复添加: ${JSON.stringify(elm)}`);
+          return Promise.resolve();
+        }
+        this.logger.info(`客户渠道费率添加成功: ${JSON.stringify(elm)}`);
+        return await this.withdrawChannelEntity.insert(elm);
+      })
+    );
+    this.logger.info(`客户渠道费率添加结束: ${JSON.stringify(res)}`);
+    return '添加成功';
+  }
+
+  async saveMoreInfo(params) {
+    const ps = {
+      channel: params.channel,
+      mch_id: params.mch_id,
+      order_type: params.order_type[0],
+      currency: params.currency[0],
+    };
+
+    const withdrawChannel = await this.withdrawChannelEntity.findOne({
+      where: {
+        channel: ps.channel,
+        mch_id: ps.mch_id,
+        order_type: ps.order_type,
+        currency: ps.currency,
+      },
+    });
+    /* 需要修改的内容 */
+    withdrawChannel.rate = params.rate;
+    withdrawChannel.basicFee = params.basicFee;
+    withdrawChannel.feeMin = params.feeMin;
+    withdrawChannel.min = params.min;
+    withdrawChannel.status = params.status;
+    withdrawChannel.max = params.max;
+    withdrawChannel.remark = params.remark;
+    await withdrawChannel.save();
+    this.logger.info(`客户渠道费率修改结束: ${JSON.stringify(withdrawChannel)}`);
+    return '添加成功';
+  }
+
+  async queryByCurrency(currency) {
+    return await this.withdrawChannelEntity.findBy({
+      status: 1,
+      currency,
+    });
+  }
+
+  async queryByCode(channel) {
+    return await this.withdrawChannelEntity.findOneBy({
+      status: 1,
+      channel,
+    });
+  }
+
+  async getWithdrawChannelList() {
+    return await this.withdrawChannelEntity.find({
+      where: {
+        status: 1,
+      },
+      select: ['channel', 'currency'],
+    });
+  }
+}

+ 85 - 34
src/modules/api/service/webhook.ts

@@ -1,28 +1,31 @@
 import { BaseService } from '@cool-midway/core';
 import { ILogger, Provide } from '@midwayjs/core';
 import { Inject } from '@midwayjs/decorator';
-import { InjectEntityModel } from '@midwayjs/typeorm';
-import { PayeeAddressEntity } from '../../payment/entity/payee_address';
-import { Repository } from 'typeorm';
-import { CustomerEntity } from '../../payment/entity/customer';
-import { PaymentService } from '../../payment/service/payment';
-import { PayeeEntity } from '../../payment/entity/payee';
-import { SunPayAdapter } from '../../payment/adapter/sunpay.adapter';
+import { PaymentSuccessService } from './webhook_utils/payment_success';
+import { WebHookCommonService } from './webhook_utils/common';
+import { DepositSuccessService } from './webhook_utils/deposit_success';
+import { ExchangeSuccessService } from './webhook_utils/exchange_success';
+import { TransferSuccessService } from './webhook_utils/transfer_success';
 
 /**
  * 描述
  */
 @Provide()
 export class OpenApiWebhookService extends BaseService {
-  @InjectEntityModel(PayeeEntity)
-  payeeEntity: Repository<PayeeEntity>;
-  @InjectEntityModel(CustomerEntity)
-  customerEntity: Repository<CustomerEntity>;
   @Inject()
-  paymentService: PaymentService;
+  webHookCommonService: WebHookCommonService;
 
   @Inject()
-  sunPayAdapter: SunPayAdapter;
+  paymentSuccessService: PaymentSuccessService;
+
+  @Inject()
+  depositSuccessService: DepositSuccessService;
+
+  @Inject()
+  exchangeSuccessService: ExchangeSuccessService;
+
+  @Inject()
+  transferSuccessService: TransferSuccessService;
 
   @Inject()
   ctx;
@@ -36,32 +39,80 @@ export class OpenApiWebhookService extends BaseService {
         `easypay的webhook_${type}: params${JSON.stringify(params)}`
       );
       // TODO 各类回调处理 后续处理
-      // 账户审核通过   account_approved
-      // 账户审核驳回   account_rejected
-      // 实名待更新     legal_entity_request_created
-      // 实名更新审核通过 legal_entity_request_success
-      // 实名更新审核驳回 legal_entity_request_rejected
-      // 收款账户申请成功 application_success
-      // 收款账户申请失败 application_failed
-      // 收款账户已下发   bank_account_created
-      // 收款账户信息变更 bank_account_updated
-      // 收单支付成功通知 acquiring_payment_success
-      // 收单支付成功通知 acquiring_refund_success
-      // 收单退款失败通知 acquiring_refund_failed
-      // 入账成功       deposit_success
-      // 换汇成功       exchange_success
-      // 换汇失败       exchange_failed
-      // 付款成功       payment_success
-      // 付款失败       payment_failed
-      // 付款银行退回    payment_refunded
-      // 转账成功        transfer_success
-      // 转账失败        transfer_failed
+
+      //  const {type} = params
+      switch (params.type) {
+        case 'account_approved':
+          // 账户审核通过   account_approved
+          break;
+        case 'account_rejected':
+          // 账户审核驳回   account_rejected
+          break;
+        case 'legal_entity_request_created':
+          // 实名待更新     legal_entity_request_created
+          break;
+        case 'legal_entity_request_success':
+          // 实名更新审核通过 legal_entity_request_success
+          break;
+        case 'legal_entity_request_rejected':
+          // 实名更新审核驳回 legal_entity_request_rejected
+          break;
+        case 'application_success':
+          // 收款账户申请成功 application_success
+          break;
+        case 'application_failed':
+          // 收款账户申请失败 application_failed
+          break;
+        case 'bank_account_created':
+          // 收款账户已下发   bank_account_created
+          break;
+        case 'bank_account_updated':
+          // 收款账户信息变更 bank_account_updated
+          break;
+        case 'acquiring_payment_success':
+          // 收单支付成功通知 acquiring_payment_success
+          break;
+        case 'acquiring_refund_success':
+          // 收单支付成功通知 acquiring_refund_success
+          break;
+        case 'acquiring_refund_failed':
+          // 收单退款失败通知 acquiring_refund_failed
+          break;
+        case 'exchange_failed':
+          // 换汇失败       exchange_failed
+          break;
+        case 'payment_failed':
+          // 付款失败       payment_failed
+          break;
+        case 'payment_refunded':
+          // 付款银行退回    payment_refunded
+          break;
+        case 'transfer_failed':
+          // 付款银行退回    transfer_failed
+          break;
+        case 'deposit_success':
+          // 入账成功       deposit_success
+          return await this.depositSuccessService.run(params);
+        case 'exchange_success':
+          // 换汇成功       exchange_success
+          // 如果换汇成功,则收取手续费
+          return await this.exchangeSuccessService.run(params);
+        case 'payment_success':
+          // 付款成功       payment_success
+          return await this.paymentSuccessService.run(params);
+        case 'transfer_success':
+          // 转账延迟2.2秒处理
+          await this.webHookCommonService.waitByTime(2200);
+          return await this.transferSuccessService.run(params);
+      }
     } catch (err) {
       this.logger.error(
         `easypay的webhook error ${type}: params${JSON.stringify(params)}`
       );
     }
-    this.ctx.status = 200;
+    // 转账延迟4秒处理
+    await this.webHookCommonService.waitByTime(4000);
+    this.ctx.status = 400;
     this.ctx.body = {};
     return;
   }

+ 117 - 0
src/modules/api/service/webhook_utils/common.ts

@@ -0,0 +1,117 @@
+import { BaseService } from '@cool-midway/core';
+import { Inject, Provide } from '@midwayjs/core';
+import { InjectEntityModel } from '@midwayjs/typeorm';
+import { OpenAccountEntity } from '../../entity/open_account';
+import { Repository } from 'typeorm';
+import { OpenUserEntity } from '../../entity/open_user';
+import { WithdrawChannelEntity } from '../../entity/withdrawChannel';
+import { applicationsService } from '../admin/applications';
+import { OrderType } from '../../entity/open_payment_order';
+import { EasyPayAdapter } from '../../../payment/adapter/easypay.adapter';
+
+/**
+ * webhook的公共方法
+ */
+@Provide()
+export class WebHookCommonService extends BaseService {
+  @InjectEntityModel(OpenAccountEntity)
+  openAccountEntity: Repository<OpenAccountEntity>;
+
+  @InjectEntityModel(OpenUserEntity)
+  openUserEntity: Repository<OpenUserEntity>;
+
+  @InjectEntityModel(WithdrawChannelEntity)
+  withdrawChannelEntity: Repository<WithdrawChannelEntity>;
+
+  @Inject()
+  applicationsService: applicationsService;
+
+  @Inject()
+    easyPayAdapter: EasyPayAdapter;
+
+  // 如果查询都是空的延迟处理
+  async getAccountInfo(account_id) {
+    const openAccount = await this.openAccountEntity.findOne({
+      where: {
+        account_id,
+      },
+    });
+    if (openAccount) {
+      return openAccount;
+    }
+    const openUser = await this.openUserEntity.findOne({
+      where: {
+        account_id,
+      },
+    });
+    if (openUser) {
+      return openUser;
+    }
+    return null;
+  }
+
+  async getWithdrawChannelFee({
+    account_id,
+    currency,
+    order_type,
+    channel,
+    amount,
+    mch_id,
+  }) {
+    // 注意 分 和 元 的处理
+    const withdrawChannel = await this.withdrawChannelEntity.findOne({
+      where: {
+        // account_id: account_id,
+        channel,
+        mch_id: mch_id,
+        order_type,
+        currency: currency,
+        status: 1,
+      },
+    });
+    // 费率
+    const fee = (Number.parseInt(withdrawChannel.rate) / 100) * (amount / 100); // 费率费用
+    // 单笔固定费用
+    const basicFee = withdrawChannel.basicFee;
+    // 单笔最低费用
+    const feeMin = withdrawChannel.feeMin;
+    return Math.max(fee, basicFee, feeMin); // 取最大值
+  }
+
+  // 获取流水记录前后的余额差异
+  async getBalanceDiffByCurrency({ account_id, currency, amount, order_type }) {
+    const userBalance =
+      await this.applicationsService.getAccountsBalancesByCurrency(
+        account_id,
+        currency
+      );
+     let balance = 0;
+     // 付款和转账的余额是减少的
+     if(order_type === OrderType.PAYMENT || order_type === OrderType.TRANSFER ) {
+        balance = userBalance - amount
+     } else {
+        balance = userBalance + amount
+     }
+    return {
+      before_balance: userBalance,
+      balance: balance,
+    };
+  }
+
+  // 定时器
+  waitByTime(settime = 100) {
+    return new Promise((resolve: any) => {
+      setTimeout(() => {
+        resolve();
+      }, settime);
+    });
+  }
+  // 获取汇率 
+  async exchange_rates_buy_amount(params: any) {
+    const exchange_rates = await this.easyPayAdapter.request("GET", "/v1/exchange_rates", params);
+    if ( exchange_rates.data.length ) {
+      return params.buy_amount / exchange_rates.data[0].exchange_rate
+    }
+    return 0 
+  }
+}

+ 176 - 0
src/modules/api/service/webhook_utils/deposit_success.ts

@@ -0,0 +1,176 @@
+import { BaseService } from '@cool-midway/core';
+import { ILogger, Inject, Provide } from '@midwayjs/core';
+import { WebHookCommonService } from './common';
+import {
+  OpenPaymentOrderEntity,
+  OrderType,
+} from '../../entity/open_payment_order';
+import { InjectEntityModel } from '@midwayjs/typeorm';
+import { Repository } from 'typeorm';
+import { EasyPayAdapter } from '../../../payment/adapter/easypay.adapter';
+import * as md5 from 'md5';
+
+/**
+ * 入账成功
+ */
+@Provide()
+export class DepositSuccessService extends BaseService {
+  @Inject()
+  webHookCommonService: WebHookCommonService;
+
+  @InjectEntityModel(OpenPaymentOrderEntity)
+  openPaymentOrderEntity: Repository<OpenPaymentOrderEntity>;
+
+  @Inject()
+  ctx;
+
+  @Inject()
+  logger: ILogger;
+
+  @Inject()
+  easyPayAdapter: EasyPayAdapter;
+
+  async run(params) {
+    // 获取回调用户的详情
+    let accountInfo = null;
+    // 获取回调用户的详情
+    let withdrawChannelFee = null;
+    try {
+      /*
+        2025-03-11 16:58:09.069 INFO 113849 [-/::ffff:127.0.0.1/-/1ms POST /api/open/easypay-webhook/notification] easypay的webhook_notification: params{"type":"deposit_success","data":{"id":"85a2e73b90d94354b14b925c8e23b876","bank_account_id":"7021a5e645574004b5678213f94df8a5","order_no":"20250311165807046795","account_id":"590f080eb299590385c7aa628274e73c","bic_number":null,"account_number":"79765000168","inward_amount":90000,"fee":0,"amount":90000,"currency":"EUR","payer":"付款信息 格式:{sender.name};{sender.address};{sender.country};{sender.account_number} or {sender.iban};{sender.bic};{sender.routing_code};附言","comment":"附言","payment_type":"SWIFT","order_type":"DEPOSIT","payment_id":null,"clearing_system":null,"status":"SUCCESS","create_time":"2025-03-11T16:58:08+08:00","update_time":"2025-03-11T16:58:08+08:00","completed_time":"2025-03-11T16:58:08+08:00"}}
+    
+        {
+          "type": "deposit_success",
+          "data": {
+              "id": "29091e03092c42a681f0921118925e84",
+              "bank_account_id": "262874c798b1452682acd96fb0317bae",
+              "order_no": "20250310155503589841",
+              "account_id": "2bd64372841a54bf8b41f879ff07884b",
+              "bic_number": null,
+              "account_number": "79765000155",
+              "inward_amount": 1000,
+              "fee": 0,
+              "amount": 1000,
+              "currency": "GBP",
+              "payer": "付款信息 格式:{sender.name};{sender.address};{sender.country};{sender.account_number} or {sender.iban};{sender.bic};{sender.routing_code};附言",
+              "comment": "附言",
+              "payment_type": "SWIFT",
+              "order_type": "DEPOSIT",
+              "payment_id": null,
+              "clearing_system": null,
+              "status": "SUCCESS",
+              "create_time": "2025-03-10T15:55:04+08:00",
+              "update_time": "2025-03-10T15:55:04+08:00",
+              "completed_time": "2025-03-10T15:55:03+08:00"
+          }
+        }
+        */
+      // 获取回调用户的详情
+      accountInfo = await this.webHookCommonService.getAccountInfo(
+        params.data.account_id
+      );
+      if (!accountInfo) {
+        // TODO 如果不存在的话,则为白标用户
+        // 转账延迟4秒处理
+        await this.webHookCommonService.waitByTime(4000);
+        this.ctx.status = 400;
+        this.ctx.body = {};
+        return;
+      }
+
+      // 获取费率信息
+      withdrawChannelFee =
+        await this.webHookCommonService.getWithdrawChannelFee({
+          account_id: params.data.account_id,
+          currency: params.data.currency,
+          order_type: OrderType.DEPOSIT,
+          channel: 'EASYPAY',
+          amount: params.data.amount,
+          mch_id: accountInfo.mch_id,
+        });
+
+      // 获取余额
+      const { before_balance, balance } =
+        await this.webHookCommonService.getBalanceDiffByCurrency({
+          account_id: params.data.account_id,
+          currency: params.data.currency,
+          amount: params.data.amount,
+          order_type: OrderType.DEPOSIT,
+        });
+      // 记录入账流水
+      const openPaymentOrderParams = {
+        mch_id: accountInfo.mch_id,
+        amount: params.data.amount / 100,
+        account_id: params.data.account_id,
+        from_account_id: params.data.account_id,
+        to_account_id: params.data.account_id,
+        event_id: params.data.id,
+        currency: params.data.currency,
+        status: params.data.status,
+        order_type: OrderType.DEPOSIT,
+        payment_type: params.data.payment_type,
+        order_id: params.data.order_no,
+        fee: withdrawChannelFee,
+        additional_info: {},
+        before_balance: before_balance / 100,
+        balance: balance / 100,
+      };
+      await this.openPaymentOrderEntity.insert(openPaymentOrderParams);
+      this.logger.info(
+        `记录入账流水, ${JSON.stringify(openPaymentOrderParams)}`
+      );
+      // 通知上游之后,间隔10秒执行利润截取操作
+      Promise.resolve().then(async () => {
+        await this.webHookCommonService.waitByTime(10000);
+        let accountInfo = null;
+        // 获取回调用户的详情
+        let withdrawChannelFee = null;
+        if (accountInfo && withdrawChannelFee) {
+          // 收取手续费: 转账
+          // /v1/transfers
+          const transfers_params = {
+            // ...openOrderObj,
+            request_id: md5(`${accountInfo.mch_id}_${params.data.id}`),
+            from_account_id: params.data.account_id,
+            to_account_id: this.easyPayAdapter.baseInfo.account_id,
+            currency: params.data.currency,
+            amount: withdrawChannelFee * 100,
+            purpose: '收取手续费用',
+          };
+          this.logger.info(
+            `记录入账流水的费率, ${JSON.stringify(transfers_params)}`
+          );
+          // 截取流水的转账
+          const res = await this.easyPayAdapter.request(
+            'POST',
+            '/v1/transfers',
+            transfers_params
+          );
+          // 记录入账手续费的流水
+          await this.openPaymentOrderEntity.insert({
+            request_id: transfers_params.request_id,
+            mch_id: accountInfo.mch_id,
+            amount: withdrawChannelFee,
+            account_id: params.data.account_id,
+            from_account_id: params.data.account_id,
+            to_account_id: this.easyPayAdapter.baseInfo.account_id,
+            event_id: params.data.id,
+            currency: params.data.currency,
+            status: params.data.status,
+            order_type: OrderType.TRANSACTION_FEE_ORDER,
+            payment_type: params.data.payment_type,
+            order_id: res.data.order_no, // 这里是转账的订单编号
+            fee: 0,
+            additional_info: {},
+          });
+        }
+      });
+      this.ctx.status = 200;
+      this.ctx.body = {};
+      return;
+    } catch (error) {
+      this.logger.error(`记录入账流水失败, ${JSON.stringify(params)}`);
+      this.logger.error(error);
+    }
+  }
+}

+ 169 - 0
src/modules/api/service/webhook_utils/exchange_success.ts

@@ -0,0 +1,169 @@
+import { BaseService } from '@cool-midway/core';
+import { ILogger, Inject, Provide } from '@midwayjs/core';
+import { WebHookCommonService } from './common';
+import {
+  OpenPaymentOrderEntity,
+  OrderType,
+} from '../../entity/open_payment_order';
+import { InjectEntityModel } from '@midwayjs/typeorm';
+import { Repository } from 'typeorm';
+import { EasyPayAdapter } from '../../../payment/adapter/easypay.adapter';
+import * as md5 from 'md5';
+
+/**
+ * 换汇成功
+ */
+@Provide()
+export class ExchangeSuccessService extends BaseService {
+  @Inject()
+  webHookCommonService: WebHookCommonService;
+
+  @InjectEntityModel(OpenPaymentOrderEntity)
+  openPaymentOrderEntity: Repository<OpenPaymentOrderEntity>;
+
+  @Inject()
+  ctx;
+
+  @Inject()
+  logger: ILogger;
+
+  @Inject()
+  easyPayAdapter: EasyPayAdapter;
+
+  async run(params) {
+    try {
+      /*
+        params
+        {
+          "type": "exchange_success",
+          "data": {
+              "id": "13ea4d84b92a4070b55dfafadcafcca3",
+              "order_no": "20250310161942842514",
+              "request_id": null,
+              "reference": null,
+              "account_id": "2bd64372841a54bf8b41f879ff07884b",
+              "sell_currency": "USD",
+              "sell_amount": 2577,
+              "exchange_rate": 0.776169,
+              "buy_amount": 2000,
+              "buy_currency": "GBP",
+              "reason": null,
+              "create_time": "2025-03-10T16:19:42+08:00",
+              "update_time": "2025-03-10T16:20:11+08:00",
+              "completed_time": "2025-03-10T16:20:11+08:00",
+              "status": "SUCCESS",
+              "details_url": null,
+              "detail_status": null,
+              "details_reason": [
+              ]
+          }
+        }
+      */
+
+      // 获取回调用户的详情
+      const accountInfo = await this.webHookCommonService.getAccountInfo(
+        params.data.account_id
+      );
+      if (!accountInfo) {
+        // TODO 如果不存在的话,暂时不处理
+        // 转账延迟4秒处理
+        await this.webHookCommonService.waitByTime(4000);
+        this.ctx.status = 400;
+        this.ctx.body = {};
+        return;
+      }
+
+      // 获取费率信息
+      const withdrawChannelFee =
+        await this.webHookCommonService.getWithdrawChannelFee({
+          account_id: params.data.account_id,
+          currency: params.data.buy_currency,
+          order_type: OrderType.EXCHANGE,
+          channel: 'EASYPAY',
+          amount: params.data.buy_amount,
+          mch_id: accountInfo.mch_id,
+        });
+
+      // 获取余额
+      const { before_balance, balance } =
+        await this.webHookCommonService.getBalanceDiffByCurrency({
+          account_id: params.data.account_id,
+          currency: params.data.buy_currency,
+          amount: params.data.buy_amount,
+          order_type: OrderType.EXCHANGE,
+        });
+
+      // 记录入账流水
+      const openPaymentOrderParams = {
+        mch_id: accountInfo.mch_id,
+        account_id: params.data.account_id,
+        from_account_id: params.data.account_id,
+        to_account_id: params.data.account_id,
+        event_id: params.data.id,
+        currency: params.data.sell_currency,
+        amount: params.data.sell_amount / 100,
+        target_currency: params.data.buy_currency,
+        target_amount: params.data.buy_amount / 100,
+        status: params.data.status,
+        order_type: OrderType.EXCHANGE,
+        payment_type: params.data.payment_type,
+        order_id: params.data.order_no,
+        fee: withdrawChannelFee,
+        additional_info: {},
+        before_balance: before_balance / 100,
+        balance: balance / 100,
+      };
+      await this.openPaymentOrderEntity.insert(openPaymentOrderParams);
+      this.logger.info(
+        `记录换汇流水, ${JSON.stringify(openPaymentOrderParams)}`
+      );
+      // 通知上游之后,间隔10秒执行利润截取操作
+      Promise.resolve().then(async () => {
+        await this.webHookCommonService.waitByTime(10000);
+        //  收取手续费: 换汇
+        // /v1/transfers
+        const transfers_params = {
+          request_id: md5(`${accountInfo.mch_id}_${params.data.id}`),
+          from_account_id: params.data.account_id,
+          to_account_id: this.easyPayAdapter.baseInfo.account_id,
+          currency: params.data.buy_currency, // 收取买入的币种费用
+          target_amount: params.data.buy_amount / 100,
+          amount: withdrawChannelFee * 100, // 分
+          purpose: '收取手续费用',
+        };
+        this.logger.info(
+          `记录入账流水的费率, ${JSON.stringify(transfers_params)}`
+        );
+        // 截取流水的转账
+        const res = await this.easyPayAdapter.request(
+          'POST',
+          '/v1/transfers',
+          transfers_params
+        );
+        // 记录入账手续费的流水
+        await this.openPaymentOrderEntity.insert({
+          request_id: transfers_params.request_id,
+          mch_id: accountInfo.mch_id,
+          amount: withdrawChannelFee,
+          account_id: params.data.account_id,
+          from_account_id: params.data.account_id,
+          to_account_id: this.easyPayAdapter.baseInfo.account_id,
+          event_id: params.data.id,
+          currency: transfers_params.currency,
+          status: params.data.status,
+          order_type: OrderType.TRANSACTION_FEE_ORDER,
+          payment_type: params.data.payment_type,
+          order_id: res.data.order_no, // 这里是转账的订单编号
+          fee: 0,
+          additional_info: {},
+        });
+      });
+      this.ctx.status = 200;
+      this.ctx.body = {};
+      return;
+    } catch (error) {
+      this.logger.error(`记录换汇流水失败, ${JSON.stringify(params)}`);
+      this.logger.error(error);
+    }
+  }
+}

+ 179 - 0
src/modules/api/service/webhook_utils/payment_success.ts

@@ -0,0 +1,179 @@
+import { BaseService } from '@cool-midway/core';
+import { ILogger, Inject, Provide } from '@midwayjs/core';
+import { WebHookCommonService } from './common';
+import {
+  OpenPaymentOrderEntity,
+  OrderType,
+} from '../../entity/open_payment_order';
+import { InjectEntityModel } from '@midwayjs/typeorm';
+import { Repository } from 'typeorm';
+import { EasyPayAdapter } from '../../../payment/adapter/easypay.adapter';
+import * as md5 from 'md5';
+
+/**
+ * 付款成功
+ */
+@Provide()
+export class PaymentSuccessService extends BaseService {
+  @Inject()
+  webHookCommonService: WebHookCommonService;
+
+  @InjectEntityModel(OpenPaymentOrderEntity)
+  openPaymentOrderEntity: Repository<OpenPaymentOrderEntity>;
+
+  @Inject()
+  ctx;
+
+  @Inject()
+  logger: ILogger;
+
+  @Inject()
+  easyPayAdapter: EasyPayAdapter;
+
+  async run(params) {
+    // 获取回调用户的详情
+    let accountInfo = null;
+    // 获取回调用户的详情
+    let withdrawChannelFee = null;
+    // 如果付款成功,则收取手续费
+    /*
+          params
+          {
+            "type": "string",
+            "data": {
+                "id": "string",
+                "order_no": "string",
+                "request_id": "string",
+                "reference": "string",
+                "account_id": "string",
+                "currency": "string",
+                "amount": 0,
+                "payer": {},
+                "beneficiary": {},
+                "exchange_rate": 0,
+                "fee": 0,
+                "fee_currency": "string",
+                "outward_currency": "string",
+                "outward_amount": 0,
+                "comment": "string",
+                "purpose": "string",
+                "status": "PENDING",
+                "reason": "string",
+                "source_of_funds ": "INVESTMENT",
+                "create_time": "string",
+                "update_time": "string",
+                "completed_time": "string"
+            }
+          }
+          */
+    try {
+      // 获取回调用户的详情
+      accountInfo = await this.webHookCommonService.getAccountInfo(
+        params.data.account_id
+      );
+      if (!accountInfo) {
+        // TODO 如果不存在的话,则为白标用户
+        // 转账延迟4秒处理
+        await this.webHookCommonService.waitByTime(4000);
+        this.ctx.status = 400;
+        this.ctx.body = {};
+        return;
+      }
+      // 获取费率信息
+      withdrawChannelFee =
+        await this.webHookCommonService.getWithdrawChannelFee({
+          account_id: params.data.account_id,
+          currency: params.data.currency,
+          order_type: OrderType.PAYMENT,
+          channel: 'EASYPAY',
+          amount: params.data.amount,
+          mch_id: accountInfo.mch_id,
+        });
+
+      // 获取余额
+      const { before_balance, balance } =
+        await this.webHookCommonService.getBalanceDiffByCurrency({
+          account_id: params.data.account_id,
+          currency: params.data.currency,
+          amount: params.data.amount,
+          order_type: OrderType.PAYMENT,
+        });
+
+      // 记录付款流水
+      const openPaymentOrderParams = {
+        mch_id: accountInfo.mch_id,
+        amount: params.data.amount / 100,
+        account_id: params.data.account_id,
+        from_account_id: params.data.account_id,
+        to_account_id: params.data.account_id,
+        event_id: params.data.id,
+        currency: params.data.currency,
+        status: params.data.status,
+        order_type: OrderType.PAYMENT,
+        payment_type: params.data.payment_type,
+        order_id: params.data.order_no,
+        fee: withdrawChannelFee,
+        additional_info: {},
+        before_balance: before_balance / 100,
+        balance: balance / 100,
+        currency_fee: params.data.fee, // 渠道手续费
+        exchange_rate: params.data.exchange_rate, // 交易时的汇率
+      };
+      await this.openPaymentOrderEntity.insert(openPaymentOrderParams);
+      this.logger.info(
+        `记录付款流水, ${JSON.stringify(openPaymentOrderParams)}`
+      );
+      // 通知上游之后,间隔10秒执行利润截取操作
+      Promise.resolve().then(async () => {
+        await this.webHookCommonService.waitByTime(10000);
+        let accountInfo = null;
+        // 获取回调用户的详情
+        let withdrawChannelFee = null;
+        if (accountInfo && withdrawChannelFee) {
+          // 收取手续费: 转账
+          // /v1/transfers
+          const transfers_params = {
+            // ...openOrderObj,
+            request_id: md5(`${accountInfo.mch_id}_${params.data.id}`),
+            from_account_id: params.data.account_id,
+            to_account_id: this.easyPayAdapter.baseInfo.account_id,
+            currency: params.data.currency,
+            amount: withdrawChannelFee * 100,
+            purpose: '收取手续费用',
+          };
+          this.logger.info(
+            `记录入账流水的费率, ${JSON.stringify(transfers_params)}`
+          );
+          // 截取流水的转账
+          const res = await this.easyPayAdapter.request(
+            'POST',
+            '/v1/transfers',
+            transfers_params
+          );
+          // 记录入账手续费的流水
+          await this.openPaymentOrderEntity.insert({
+            request_id: transfers_params.request_id,
+            mch_id: accountInfo.mch_id,
+            amount: withdrawChannelFee,
+            account_id: params.data.account_id,
+            from_account_id: params.data.account_id,
+            to_account_id: this.easyPayAdapter.baseInfo.account_id,
+            event_id: params.data.id,
+            currency: params.data.currency,
+            status: params.data.status,
+            order_type: OrderType.TRANSACTION_FEE_ORDER,
+            payment_type: params.data.payment_type,
+            order_id: res.data.order_no, // 这里是转账的订单编号
+            fee: 0,
+            additional_info: {},
+          });
+        }
+      });
+      this.ctx.status = 200;
+      this.ctx.body = {};
+      return;
+    } catch (error) {}
+
+    return params;
+  }
+}

+ 186 - 0
src/modules/api/service/webhook_utils/transfer_success.ts

@@ -0,0 +1,186 @@
+import {BaseService} from '@cool-midway/core';
+import {ILogger, Inject, Provide} from '@midwayjs/core';
+import {WebHookCommonService} from './common';
+import {OpenPaymentOrderEntity, OrderType,} from '../../entity/open_payment_order';
+import {InjectEntityModel} from '@midwayjs/typeorm';
+import {Repository} from 'typeorm';
+import {EasyPayAdapter} from '../../../payment/adapter/easypay.adapter';
+import * as md5 from 'md5';
+
+/**
+ * 转账成功
+ */
+@Provide()
+export class TransferSuccessService extends BaseService {
+  @Inject()
+  webHookCommonService: WebHookCommonService;
+
+  @InjectEntityModel(OpenPaymentOrderEntity)
+  openPaymentOrderEntity: Repository<OpenPaymentOrderEntity>;
+
+  @Inject()
+  ctx;
+
+  @Inject()
+  logger: ILogger;
+
+  @Inject()
+  easyPayAdapter: EasyPayAdapter;
+
+  async run(params) {
+    // 获取回调用户的详情
+    let accountInfo = null;
+    // 获取回调用户的详情
+    let withdrawChannelFee = null;
+    try {
+      /*
+        2025-03-11 16:58:09.069 INFO 113849 [-/::ffff:127.0.0.1/-/1ms POST /api/open/easypay-webhook/notification] easypay的webhook_notification: params{"type":"deposit_success","data":{"id":"85a2e73b90d94354b14b925c8e23b876","bank_account_id":"7021a5e645574004b5678213f94df8a5","order_no":"20250311165807046795","account_id":"590f080eb299590385c7aa628274e73c","bic_number":null,"account_number":"79765000168","inward_amount":90000,"fee":0,"amount":90000,"currency":"EUR","payer":"付款信息 格式:{sender.name};{sender.address};{sender.country};{sender.account_number} or {sender.iban};{sender.bic};{sender.routing_code};附言","comment":"附言","payment_type":"SWIFT","order_type":"DEPOSIT","payment_id":null,"clearing_system":null,"status":"SUCCESS","create_time":"2025-03-11T16:58:08+08:00","update_time":"2025-03-11T16:58:08+08:00","completed_time":"2025-03-11T16:58:08+08:00"}}
+
+       // 转账成功       transfer_success
+          // 如果付款成功,则收取手续费
+          /*
+          params
+          {
+            "type": "string",
+            "data": {
+                "id": "string",
+                "order_no": "string",
+                "request_id": "string",
+                "reference": "string",
+                "from_account_id": "string",
+                "to_account_id": "string",
+                "amount": 0,
+                "currency": "string",
+                "purpose": "string",
+                "status": "PENDING",
+                "reason": "string",
+                "create_time": "string",
+                "update_time": "string",
+                "completed_time": "string"
+            }
+          }
+        */
+      // 获取回调用户的详情
+      accountInfo = await this.webHookCommonService.getAccountInfo(
+        params.data.account_id
+      );
+      if (!accountInfo) {
+        // 转账延迟4秒处理
+        await this.webHookCommonService.waitByTime(4000);
+        this.ctx.status = 400;
+        this.ctx.body = {};
+        return;
+      }
+
+      // 获取费率信息
+      withdrawChannelFee =
+        await this.webHookCommonService.getWithdrawChannelFee({
+          account_id: params.data.account_id,
+          currency: params.data.currency,
+          order_type: OrderType.TRANSFER,
+          channel: 'EASYPAY',
+          amount: params.data.amount,
+          mch_id: accountInfo.mch_id,
+        });
+
+      // 注意避免重复截取利润, 如果是手续费直接忽略,只需更新订单谢谢即可
+      const isDuplicateOrder = await this.isDuplicateOrder(params.data.id);
+      if (isDuplicateOrder) {
+        Promise.resolve().then(async () => {
+          await this.webHookCommonService.waitByTime(10000);
+          isDuplicateOrder.status = 'SUCCESS';
+          await this.openPaymentOrderEntity.save(isDuplicateOrder);
+        });
+        this.ctx.status = 200;
+        this.ctx.body = {};
+        return;
+      }
+      // 获取余额
+      const { before_balance, balance } =
+        await this.webHookCommonService.getBalanceDiffByCurrency({
+          account_id: params.data.account_id,
+          currency: params.data.currency,
+          amount: params.data.amount,
+          order_type: OrderType.TRANSFER,
+        });
+      // 记录转账流水
+      const openPaymentOrderParams = {
+        mch_id: accountInfo.mch_id,
+        amount: params.data.amount / 100,
+        account_id: params.data.account_id,
+        from_account_id: params.data.from_account_id,
+        to_account_id: params.data.to_account_id,
+        event_id: params.data.id,
+        currency: params.data.currency,
+        status: params.data.status,
+        order_type: OrderType.TRANSFER,
+        payment_type: params.data.payment_type,
+        order_id: params.data.order_no,
+        fee: withdrawChannelFee,
+        additional_info: {},
+        before_balance: before_balance / 100,
+        balance: balance / 100,
+      };
+      await this.openPaymentOrderEntity.insert(openPaymentOrderParams);
+      this.logger.info(
+        `记录转账流水, ${JSON.stringify(openPaymentOrderParams)}`
+      );
+      // 通知上游之后,间隔10秒执行利润截取操作
+      Promise.resolve().then(async () => {
+        await this.webHookCommonService.waitByTime(10000);
+
+        // 收取手续费: 转账
+        // /v1/transfers
+        const transfers_params = {
+          request_id: md5(`${accountInfo.mch_id}_${params.data.id}`),
+          from_account_id: params.data.account_id,
+          to_account_id: this.easyPayAdapter.baseInfo.account_id,
+          currency: params.data.currency,
+          amount: withdrawChannelFee * 100,
+          purpose: '收取手续费用',
+        };
+        this.logger.info(
+          `记录转账流水的费率, ${JSON.stringify(transfers_params)}`
+        );
+        // 截取流水的转账
+        const res = await this.easyPayAdapter.request(
+          'POST',
+          '/v1/transfers',
+          transfers_params
+        );
+        // 记录入账手续费的流水
+        await this.openPaymentOrderEntity.insert({
+          request_id: transfers_params.request_id,
+          mch_id: accountInfo.mch_id,
+          amount: withdrawChannelFee,
+          account_id: params.data.account_id,
+          from_account_id: params.data.account_id,
+          to_account_id: this.easyPayAdapter.baseInfo.account_id,
+          event_id: params.data.id,
+          currency: params.data.currency,
+          status: params.data.status,
+          order_type: OrderType.TRANSACTION_FEE_ORDER,
+          payment_type: params.data.payment_type,
+          order_id: res.data.order_no, // 这里是转账的订单编号
+          fee: 0,
+          additional_info: {},
+        });
+      });
+      this.ctx.status = 200;
+      this.ctx.body = {};
+      return;
+    } catch (error) {
+      this.logger.error(`记录入账流水失败, ${JSON.stringify(params)}`);
+      this.logger.error(error);
+    }
+  }
+
+  // 根据 event_id 判断是否有重复订单
+  async isDuplicateOrder(event_id: any) {
+    return await this.openPaymentOrderEntity.findOne({
+      where: {
+        event_id,
+      },
+    });
+  }
+}

+ 87 - 0
src/modules/payment/adapter/TokenManagerService.ts

@@ -0,0 +1,87 @@
+import { BaseService } from "@cool-midway/core";
+import axios from "axios";
+import qs = require("qs");
+
+export class TokenManagerService {
+  authUrl = ''; // 认证URL
+  clientId = ''; // 客户端ID
+  clientSecret = ''; // 客户端密钥
+  refreshToken = null; // 刷新令牌
+  accessToken = null; // 访问令牌
+  expiresIn = 0; // 令牌过期时间
+
+  constructor(authUrl, clientId, clientSecret) {
+    this.authUrl = authUrl; // 初始化认证URL
+    this.clientId = clientId; // 初始化客户端ID
+    this.clientSecret = clientSecret; // 初始化客户端密钥
+    this.accessToken = null; // 初始化访问令牌为空
+    this.refreshToken = null; // 初始化刷新令牌为空
+    this.expiresIn = 0; // 初始化过期时间为0
+  }
+//   initS(authUrl, clientId, clientSecret) {
+//     this.authUrl = authUrl; // 初始化认证URL
+//     this.clientId = clientId; // 初始化客户端ID
+//     this.clientSecret = clientSecret; // 初始化客户端密钥
+//     this.accessToken = null; // 初始化访问令牌为空
+//     this.refreshToken = null; // 初始化刷新令牌为空
+//     this.expiresIn = 0; // 初始化过期时间为0
+//     console.log(37);
+    
+//   }
+
+  // 登录方法,用于获取访问令牌和刷新令牌
+  async login() {
+    try {
+      const dataString = qs.stringify({
+        client_id: this.clientId, // 传递客户端ID
+        client_secret: this.clientSecret, // 传递客户端密钥
+        grant_type: 'client_credentials', // 假设使用密码授权类型进行初始登录
+      });
+      const config = {
+        method: 'post',
+        url: this.authUrl,
+        data: dataString,
+      };
+      const response = await axios(config);
+
+      const data = response.data;
+      this.accessToken = data.access_token; // 获取访问令牌
+      this.refreshToken = data.refresh_token; // 获取刷新令牌
+      this.expiresIn = Math.floor(Date.now() / 1000) + data.expires_in; // 计算令牌过期时间
+
+      console.log('Login successful. Access Token acquired.'); // 登录成功日志
+    } catch (error) {
+      console.error('Login failed:', error); // 登录失败日志
+      throw error; // 抛出错误
+    }
+  }
+
+  // 获取访问令牌的方法
+  async getAccessToken() {
+    const currentTime = Math.floor(Date.now() / 1000); // 获取当前时间戳
+    if (this.accessToken && this.expiresIn > currentTime) {
+      return this.accessToken; // 如果令牌有效,返回访问令牌
+    }
+
+    if (!this.refreshToken) {
+      throw new Error('No refresh token available. Please login first.'); // 如果没有刷新令牌,抛出错误
+    }
+
+    try {
+      const response = await axios.post(this.authUrl, {
+        client_id: this.clientId, // 传递客户端ID
+        client_secret: this.clientSecret, // 传递客户端密钥
+        refresh_token: this.refreshToken, // 传递刷新令牌
+        grant_type: 'refresh_token', // 使用刷新令牌授权类型
+      });
+
+      const data = response.data;
+      this.accessToken = data.access_token; // 更新访问令牌
+      this.expiresIn = currentTime + data.expires_in; // 更新过期时间
+      return this.accessToken; // 返回新的访问令牌
+    } catch (error) {
+      console.error('Error fetching access token:', error); // 获取令牌错误日志
+      throw error; // 抛出错误
+    }
+  }
+}

+ 112 - 92
src/modules/payment/adapter/easypay.adapter.ts

@@ -1,89 +1,23 @@
-import { Provide, Inject, Config, ALL } from '@midwayjs/decorator';
-
+import {
+  Provide,
+  Inject,
+  Config,
+  ALL,
+  InjectClient,
+} from '@midwayjs/decorator';
 import { Repository } from 'typeorm';
 import { InjectEntityModel } from '@midwayjs/typeorm';
 import { PaymentChannelEntity } from '../entity/channel';
 import axios from 'axios';
 import { CustomerEntity } from '../entity/customer';
 import qs = require('qs');
-import { ILogger } from '@midwayjs/core';
-
-class TokenManager {
-  authUrl = ''; // 认证URL
-  clientId = ''; // 客户端ID
-  clientSecret = ''; // 客户端密钥
-  refreshToken = null; // 刷新令牌
-  accessToken = null; // 访问令牌
-  expiresIn = 0; // 令牌过期时间
-
-  constructor(authUrl, clientId, clientSecret) {
-    this.authUrl = authUrl; // 初始化认证URL
-    this.clientId = clientId; // 初始化客户端ID
-    this.clientSecret = clientSecret; // 初始化客户端密钥
-    this.accessToken = null; // 初始化访问令牌为空
-    this.refreshToken = null; // 初始化刷新令牌为空
-    this.expiresIn = 0; // 初始化过期时间为0
-  }
-
-  // 登录方法,用于获取访问令牌和刷新令牌
-  async login() {
-    try {
-      const dataString = qs.stringify({
-        client_id: this.clientId, // 传递客户端ID
-        client_secret: this.clientSecret, // 传递客户端密钥
-        grant_type: 'client_credentials', // 假设使用密码授权类型进行初始登录
-      });
-      const config = {
-        method: 'post',
-        url: this.authUrl,
-        data: dataString,
-      };
-      const response = await axios(config);
-
-      const data = response.data;
-      this.accessToken = data.access_token; // 获取访问令牌
-      this.refreshToken = data.refresh_token; // 获取刷新令牌
-      this.expiresIn = Math.floor(Date.now() / 1000) + data.expires_in; // 计算令牌过期时间
-
-      console.log('Login successful. Access Token acquired.'); // 登录成功日志
-    } catch (error) {
-      console.error('Login failed:', error); // 登录失败日志
-      throw error; // 抛出错误
-    }
-  }
-
-  // 获取访问令牌的方法
-  async getAccessToken() {
-    const currentTime = Math.floor(Date.now() / 1000); // 获取当前时间戳
-    if (this.accessToken && this.expiresIn > currentTime) {
-      return this.accessToken; // 如果令牌有效,返回访问令牌
-    }
-
-    if (!this.refreshToken) {
-      throw new Error('No refresh token available. Please login first.'); // 如果没有刷新令牌,抛出错误
-    }
-
-    try {
-      const response = await axios.post(this.authUrl, {
-        client_id: this.clientId, // 传递客户端ID
-        client_secret: this.clientSecret, // 传递客户端密钥
-        refresh_token: this.refreshToken, // 传递刷新令牌
-        grant_type: 'refresh_token', // 使用刷新令牌授权类型
-      });
-
-      const data = response.data;
-      this.accessToken = data.access_token; // 更新访问令牌
-      this.expiresIn = currentTime + data.expires_in; // 更新过期时间
-      return this.accessToken; // 返回新的访问令牌
-    } catch (error) {
-      console.error('Error fetching access token:', error); // 获取令牌错误日志
-      throw error; // 抛出错误
-    }
-  }
-}
+import { ILogger, Init } from '@midwayjs/core';
+import { CachingFactory, MidwayCache } from '@midwayjs/cache-manager';
+import { BaseService } from '@cool-midway/core';
+import { resolve } from 'path';
 
 @Provide()
-export class EasyPayAdapter {
+export class EasyPayAdapter extends BaseService {
   @InjectEntityModel(PaymentChannelEntity)
   channelEntity: Repository<PaymentChannelEntity>;
   @InjectEntityModel(CustomerEntity)
@@ -93,11 +27,12 @@ export class EasyPayAdapter {
   @Inject()
   ctx;
 
+  @InjectClient(CachingFactory, 'default')
+  midwayCache: MidwayCache;
+
   @Inject()
   logger: ILogger;
 
-
-
   private config: {
     apiKey: string;
     apiSecret: string;
@@ -105,16 +40,15 @@ export class EasyPayAdapter {
     chainApiKey: string;
     chainApiSecret: string;
     chainApiUrl: string;
-    tokenManager: TokenManager;
   };
 
   baseInfo: {
-    account_id?: string
-  }
-
+    account_id?: string;
+  };
 
+  @Init()
   Init() {
-    this.initConfig()
+    this.initConfig();
   }
   /**
    * 初始化渠道配置
@@ -127,12 +61,18 @@ export class EasyPayAdapter {
       if (!channel) {
         throw new Error('FusionPay channel not found or disabled');
       }
-      const tokenManager = new TokenManager(
+      await this.initTokenManager(
+        `${channel.apiUrl}/auth`,
+        channel.apiKey,
+        channel.apiSecret
+      );
+
+      this.initTokenManager(
         `${channel.apiUrl}/auth`,
         channel.apiKey,
         channel.apiSecret
       );
-      await tokenManager.login();
+      // this.tokenManagerService.login();
       this.config = {
         apiKey: channel.apiKey,
         apiSecret: channel.apiSecret,
@@ -140,9 +80,9 @@ export class EasyPayAdapter {
         chainApiKey: channel.chainApiKey,
         chainApiSecret: channel.chainApiSecret,
         chainApiUrl: channel.chainApiUrl,
-        tokenManager: tokenManager,
+        // tokenManager: tokenManager,
       };
-      this.baseInfo = channel.config
+      this.baseInfo = channel.config;
     }
   }
 
@@ -151,11 +91,10 @@ export class EasyPayAdapter {
    */
   async request(method: string, endpoint: string, data?: any) {
     await this.initConfig();
-    // return Promise.resolve(`https://api.easypayx.com${endpoint.replace('/api/open', '')}`);
     let url = this.config.apiUrl;
     try {
       url = `${url}${endpoint.replace('/api/open', '')}`;
-      const accessToken = await this.config.tokenManager.getAccessToken();
+      const accessToken = await this.getAccessToken();
       const axiosParams = {
         method,
         url,
@@ -176,7 +115,6 @@ export class EasyPayAdapter {
       // console.log('response', response.data);
       return response.data;
     } catch (error) {
-
       // console.log(error.response.data);
       if (axios.isAxiosError(error) && error.response) {
         // console.log(error.response.data);
@@ -201,4 +139,86 @@ export class EasyPayAdapter {
     });
     return '';
   }
+
+  async get_authUrl() {
+    return await this.midwayCache.get('easypay:adapter:authUrl'); // 初始化认证URL
+  }
+  async get_clientId() {
+    return await this.midwayCache.get('easypay:adapter:clientId'); // 初始化客户端ID
+  }
+  async get_clientSecret() {
+    return await this.midwayCache.get('easypay:adapter:clientSecret'); // 初始化客户端密钥
+  }
+  async get_accessToken() {
+    return await this.midwayCache.get('easypay:adapter:accessToken'); // 初始化访问令牌为空
+  }
+  async get_refreshToken() {
+    return await this.midwayCache.get('easypay:adapter:refreshToken'); // 初始化刷新令牌为空
+  }
+  async get_expiresIn() {
+    return await this.midwayCache.get('easypay:adapter:expiresIn'); // 初始化过期时间为0
+  }
+
+  async initTokenManager(authUrl, clientId, clientSecret) {
+    if (!(await this.get_authUrl())) {
+      await this.midwayCache.set('easypay:adapter:authUrl', authUrl); // 初始化认证URL
+      await this.midwayCache.set('easypay:adapter:clientId', clientId); // 初始化客户端ID
+      await this.midwayCache.set('easypay:adapter:clientSecret', clientSecret); // 初始化客户端密钥
+      await this.midwayCache.set('easypay:adapter:accessToken', ''); // 初始化访问令牌为空
+      await this.midwayCache.set('easypay:adapter:refreshToken', ''); // 初始化刷新令牌为空
+      await this.midwayCache.set('easypay:adapter:expiresIn', 0); // 初始化过期时间为0
+    }
+  }
+  // 获取访问令牌的方法
+  async getAccessToken() {
+    const currentTime = Math.floor(Date.now() / 1000); // 获取当前时间戳
+    const expiresIn = await this.get_expiresIn();
+    const accessToken = await this.get_accessToken();
+    if (accessToken && Number.parseInt(`${expiresIn}`) > currentTime) {
+      return accessToken; // 如果令牌有效,返回访问令牌
+    }
+    await this.login();
+    return await this.get_accessToken(); // 返回新的访问令牌
+  }
+  // 登录方法,用于获取访问令牌和刷新令牌
+  async login() {
+    return new Promise(async (resolve, reject) => {
+      const authUrl = await this.get_authUrl();
+      const clientId = await this.get_clientId();
+      const clientSecret = await this.get_clientSecret();
+      try {
+        const dataString = qs.stringify({
+          client_id: clientId, // 传递客户端ID
+          client_secret: clientSecret, // 传递客户端密钥
+          grant_type: 'client_credentials', // 假设使用密码授权类型进行初始登录
+        });
+        const config = {
+          method: 'post',
+          url: `${authUrl}`,
+          data: `${dataString}`,
+        };
+        // console.log(239, config);
+        const response = await axios(config);
+
+        const data = response.data;
+        await this.midwayCache.set(
+          'easypay:adapter:accessToken',
+          data.access_token
+        ); // 初始化访问令牌为空
+        await this.midwayCache.set(
+          'easypay:adapter:refreshToken',
+          data.refresh_token
+        ); // 初始化刷新令牌为空
+        await this.midwayCache.set(
+          'easypay:adapter:expiresIn',
+          Math.floor(Date.now() / 1000) + data.expires_in - 10
+        ); // 初始化过期时间为0
+        console.log('Login successful. Access Token acquired. accessToken'); // 登录成功日志
+        resolve(data.access_token);
+      } catch (error) {
+        console.error('Login failed:', error); // 登录失败日志
+        reject(error); // 抛出错误
+      }
+    });
+  }
 }