Ver Fonte

feat: 出金回调

max há 7 meses atrás
pai
commit
75078639f7

+ 0 - 1
src/modules/api/controller/beneficiaryAddress.ts

@@ -89,7 +89,6 @@ export class BeneficiaryAddressController extends BaseController {
     summary: '获取受益人地址',
   })
   async getBeneficiaryAddressForId(@Param('id') id: string) {
-    console.log(98989, id);
     return this.ok(await this.beneficiaryAddressService.info(id));
   }
   /**

+ 2 - 17
src/modules/api/controller/customer.ts

@@ -34,7 +34,7 @@ export class CustomerController extends BaseController {
   //  * 获取创建客户必填字段
   //  */
   // @Get('/CustomerRequiredFields', { summary: '获取创建客户必填字段' })
-  // async getCustomerRequiredFields(@Body(ALL) params: CreateUserParams) {
+  // async getCustomerRequiredFields(@Body(ALL) params: any) {
   //   return this.ok(await this.sunPayAdapter.validateCustomerInfo(params));
   // }
   // /**
@@ -42,7 +42,7 @@ export class CustomerController extends BaseController {
   //  * /api/v3/Fiat/Customer/Validate
   //  */
   // @Post('/Customer/Validate', { summary: '验证客户必填字段' })
-  // async param(@Body(ALL) business: CreateUserParams) {
+  // async param(@Body(ALL) params: any) {
   //   return this.ok(await this.sunPayAdapter.validateCustomerInfo(params));
   // }
   /**
@@ -53,19 +53,4 @@ export class CustomerController extends BaseController {
   async updateCustomer(@Body() params: IndividualEntity) {
     return await this.customerService.updateCustomer(params);
   }
-  // /**
-  //  * TODO 创建客户回调通知
-  //  *
-  //  */
-  // @Post('/callback', { summary: '创建客户回调通知' })
-  // async createCustomerCallback(@Body(ALL) business: BusinessEntity) {
-  //   // if (!this.allowKeys.includes(key)) {
-  //   //   return this.fail('非法操作');
-  //   // }
-  //   // 关键参数校验
-  //   // 数据落库
-  //   // 回调
-  //   console.log(business);
-  //   return this.ok('hello, cool-admin!!!');
-  // }
 }

+ 0 - 15
src/modules/api/controller/payIn.ts

@@ -56,19 +56,4 @@ export class PayInController extends BaseController {
     console.log('cancelPayInOrderForOrderNo-=-=-=111',orderNo);
     return this.ok(await this.orderService.rechargeOrderCancel({orderNo:orderNo}));
   }
-  // /**
-  //  * TODO 创建订单回调通知
-  //  * /api/v3/Fiat/PayIn/{orderNo}/Cancel
-  //  */
-  // @Post('Fiat/PayIn/orderNo/Cancel', { summary: '创建订单回调通知' })
-  // async createPayInOrderForCallback(@Body(ALL) business: BusinessEntity) {
-  //   // if (!this.allowKeys.includes(key)) {
-  //   //   return this.fail('非法操作');
-  //   // }
-  //   // 关键参数校验
-  //   // 数据落库
-  //   // 回调
-  //   console.log(business);
-  //   return this.ok('hello, cool-admin!!!');
-  // }
 }

+ 16 - 55
src/modules/api/controller/payOut.ts

@@ -1,6 +1,8 @@
 import { CoolController, BaseController } from '@cool-midway/core';
-import { ALL, Body, Get, Inject, Post, Provide } from '@midwayjs/decorator';
+import { ALL, Body, Get, Inject, Param, Post, Provide } from '@midwayjs/decorator';
 import { BusinessEntity } from '../../payment/entity/business';
+import { OrderService } from '../../payment/service/order';
+import { OrderEntity } from '../../payment/entity/order';
 
 /**
  * 入金
@@ -8,79 +10,38 @@ import { BusinessEntity } from '../../payment/entity/business';
 @Provide()
 @CoolController('/api/v1/Fiat')
 export class CustomerController extends BaseController {
+  @Inject()
+    orderService: OrderService;
   /**
    * 创建订单
    * /api/v3/Fiat/Payout
    */
   @Post('/PayOut', { summary: '创建订单' })
-  async createPayOutOrder(@Body(ALL) business: BusinessEntity) {
-    // if (!this.allowKeys.includes(key)) {
-    //   return this.fail('非法操作');
-    // }
-    // 关键参数校验
-    // 数据落库
-    // 回调
-    console.log(business);
-    return this.ok('hello, cool-admin!!!');
+  async createPayOutOrder(@Body(ALL) paramas: OrderEntity) {
+    return this.ok(await this.orderService.openApiCreatePayOut(paramas));
   }
   /**
    * 查询订单
    * /api/v3/Fiat/PayOut/{orderNo}
    */
   @Get('/PayOut/:orderNo', { summary: '查询订单' })
-  async PayOutOutForOrderNo(@Body(ALL) business: BusinessEntity) {
-    // if (!this.allowKeys.includes(key)) {
-    //   return this.fail('非法操作');
-    // }
-    // 关键参数校验
-    // 数据落库
-    // 回调
-    console.log(business);
-    return this.ok('hello, cool-admin!!!');
+  async PayOutOutForOrderNo(@Param('orderNo') orderNo: string) {
+    return this.ok(await this.orderService.openApiPatOutInfo(orderNo));
   }
   /**
    * 取消订单
    * /api/v3/Fiat/PayOut/{orderNo}/Cancel
    */
   @Post('/PayOut/:orderNo/Cancel', { summary: '取消订单' })
-  async cancelPayOutOrderForOrderNo(@Body(ALL) business: BusinessEntity) {
-    // if (!this.allowKeys.includes(key)) {
-    //   return this.fail('非法操作');
-    // }
-    // 关键参数校验
-    // 数据落库
-    // 回调
-    console.log(business);
-    return this.ok('hello, cool-admin!!!');
+  async cancelPayOutOrderForOrderNo(@Param('orderNo') orderNo: string) {
+    return this.ok(await this.orderService.openApiCancel(orderNo));
   }
-    /**
-   * 确认订单
-   * /api/v3/Fiat/PayOut/{orderNo}/Cancel
-   */
-    @Post('/PayOut/:orderNo/Confirm', { summary: '确认订单' })
-    async confirmPayOutOrderForOrderNo(@Body(ALL) business: BusinessEntity) {
-      // if (!this.allowKeys.includes(key)) {
-      //   return this.fail('非法操作');
-      // }
-      // 关键参数校验
-      // 数据落库
-      // 回调
-      console.log(business);
-      return this.ok('hello, cool-admin!!!');
-    }
   /**
-   * TODO 创建订单回调通知
+   * 确认订单
    * /api/v3/Fiat/PayOut/{orderNo}/Cancel
    */
-  // @Post('Fiat/PayOut/orderNo/Cancel', { summary: '创建订单回调通知' })
-  // async createPayOutOrderForCallback(@Body(ALL) business: BusinessEntity) {
-  //   // if (!this.allowKeys.includes(key)) {
-  //   //   return this.fail('非法操作');
-  //   // }
-  //   // 关键参数校验
-  //   // 数据落库
-  //   // 回调
-  //   console.log(business);
-  //   return this.ok('hello, cool-admin!!!');
-  // }
+  @Post('/PayOut/:orderNo/Confirm', { summary: '确认订单' })
+  async confirmPayOutOrderForOrderNo(@Param('orderNo') orderNo: string) {
+    return this.ok(await this.orderService.openApiConfirm(orderNo));
+  }
 }

+ 5 - 2
src/modules/payment/adapter/sunpay.adapter.ts

@@ -137,7 +137,7 @@ export class SunPayAdapter implements ChannelAdapter {
   /**
    * 验证用户信息
    */
-  private async validateCustomerInfo(params: CreateUserParams): Promise<void> {
+  async validateCustomerInfo(params: CreateUserParams): Promise<void> {
     const { data } = await this.request('POST', '/Customer/Validate', {
       type: params.userType,
       out_user_id: params.userId, // 外部用户ID
@@ -404,7 +404,10 @@ export class SunPayAdapter implements ChannelAdapter {
   async payOutOrder(orderNo: string) {
     return this.request('GET', `/Fiat/PayOut/${orderNo}`);
   }
-
+  
+  async cancelPayOut(orderNo: string) {
+    return this.request('POST', `/Fiat/PayOut/${orderNo}/Cancel`);
+  }
   async confirmPayOut(orderNo: string) {
     return this.request('POST', `/Fiat/PayOut/${orderNo}/Confirm`);
   }

+ 2 - 0
src/modules/payment/entity/order.ts

@@ -12,9 +12,11 @@ export class OrderEntity extends BaseEntity {
   @Column({ comment: '渠道', length: 64 })
   channel: string;
 
+  // 下游在 fusionPay 创建的订单编号
   @Column({ comment: '订单号', length: 64, unique: true })
   order_no: string;
 
+  // fusionPay 在 sunpay 创建的订单编号
   @Column({ comment: '商户订单号', length: 64, nullable: true })
   out_order_no?: string;
 

+ 2 - 0
src/modules/payment/interface/channel.adapter.ts

@@ -132,6 +132,8 @@ export interface ChannelAdapter {
   payOutOrder(params: any): Promise<any>;
   // 确认出金
   confirmPayOut(orderNo: string): Promise<any>;
+  // 取消出金
+  cancelPayOut(orderNo: string): Promise<any>;
   // 数字货币入金
   payInByChain(params: any): Promise<any>;
   // 数字货币出金

+ 278 - 0
src/modules/payment/service/order.ts

@@ -16,6 +16,7 @@ import { OrderEntity } from '../entity/order';
 import { PayeeAddressEntity } from '../entity/payee_address';
 import { Utils } from '../../../comm/utils';
 import { AmountCalculator } from '../../../comm/amount-calculator';
+import axios from 'axios';
 
 /**
  * 描述
@@ -62,6 +63,15 @@ export class OrderService extends BaseService {
     return merchant
   }
 
+
+  async getCustomer(customer_id): Promise<CustomerEntity> {
+    return await this.customerEntity.findOneBy({
+      customer_id: customer_id,
+    });
+  }
+
+  
+
   getMerchantId(params) {
     let merchantId;
     if (this._isOpenApi) {
@@ -247,6 +257,196 @@ export class OrderService extends BaseService {
       throw new CoolCommException('出金处理失败:' + err.message);
     }
   }
+
+  /* openapi */
+  async openApiCreatePayOut(params) {
+    let {
+      channel = 'SUNPAY', // 渠道
+      currency, // 币种
+      amount, //  金额
+      beneficiary_address_id, // 账户地址id
+      customer_id, // 客户ID
+    } = params;
+    if (amount < 100) {
+      throw new CoolCommException('金额不能小于100');
+    }
+    
+
+    // 检查对应钱包余额
+    const wallet = await this.walletEntity.findOne({
+      where: {
+        customerId: customer_id,
+        channel: channel,
+        currency: currency,
+      },
+    });
+
+    // 检查可用余额是否足够(余额减去已冻结金额)
+    const availableBalance = this.amountCalculator.subtract(
+      wallet.balance,
+      wallet.frozenAmount
+    );
+    if (!wallet || availableBalance < amount) {
+      throw new CoolCommException('可用余额不足');
+    }
+
+    const customer = await this.getCustomer(customer_id)
+
+    if (!customer) {
+      throw new CoolCommException('客户不存在');
+    }
+
+    const payeeAddress = await this.payeeAddressEntity.findOne({
+      where: {
+        id: beneficiary_address_id,
+        customerId: customer_id,
+      },
+    });
+    if (!payeeAddress) {
+      throw new CoolCommException('收款账户不存在');
+    }
+    console.log(currency);
+    const type = ['ERC20', 'TRC20'].includes(currency) ? 'CRYPTO' : 'BANK';
+    const rate = await this.rateEntity.findOne({
+      where: {
+        currency: params.currency,
+        channelId: channel,
+        merchantId: customer.merchantId,
+        type: 'WITHDRAWAL',
+      },
+    });
+    if (!rate) {
+      throw new CoolCommException('支付方式不存在,请联系客服');
+    }
+    let orderData;
+    // 计算手续费率 (rate.rate 为百分比)
+    const feeRate = rate.rate / 100;
+    // 计算手续费金额
+    const feeAmount = this.amountCalculator.multiply(amount, feeRate);
+    // 扣除手续费后的最终金额
+    amount = this.amountCalculator.subtract(amount, feeAmount);
+
+    if (amount < 100) {
+      throw new CoolCommException('扣除手续费后金额小于100,无法提交');
+    }
+    switch (channel) {
+      case 'SUNPAY':
+        orderData = await this.generateOrderDataBySunpay(type, {
+          ...params,
+          amount: amount,
+          merchantId: customer.merchantId,
+          customerId: customer.customer_id,
+          beneficiary_address_id: payeeAddress.beneficiary_address_id,
+          cryptoAddress: payeeAddress.address,
+          paymentType: rate.method,
+        });
+        //  orderData = {
+        //   ...params
+        //  }
+      break;
+    }
+    // 先冻结金额
+    wallet.frozenAmount = this.amountCalculator.add(
+      wallet.frozenAmount,
+      params.amount
+    );
+    await this.walletEntity.save(wallet);
+    // 1. 创建订单
+    const order = await this.orderEntity.save({
+      merchantId: customer.merchantId,
+      channel: channel,
+      customer_id: customer_id,
+      order_type: 'WITHDRAW',
+      amount: orderData.amount,
+      currency: params.currency,
+      payment_type: rate.method,
+      status: 'PENDING',
+      out_order_no: orderData.out_order_no, // fusionPay 在 sunpay 创建的订单编号
+      order_no: params.out_order_no, // 下游在 fusionPay 创建的订单编号
+      webhook_url: params.webhook_url,
+      crypto_address: orderData.address ?? '',
+      beneficiary_address_id: beneficiary_address_id ?? '',
+    });
+    try {
+      // 2. 调用出金接口
+      let res;
+      if (type == 'CRYPTO') {
+        res = await this.paymentService .setChannel(channel).payOutByChain(orderData);
+      } else {
+        res = await this.paymentService.setChannel(channel).payOut({
+          ...orderData,
+          webhook_url: this.config.callback.sunpay,
+        });
+      }
+      // 3. 更新订单状态
+      order.status = 'PROCESSING';
+      order.out_order_no = res.data.order_no;
+      await this.orderEntity.update(order.id, order);
+    } catch (error) {
+      // 如果失败,解冻金额
+      wallet.frozenAmount = this.amountCalculator.subtract(
+        wallet.frozenAmount,
+        amount
+      );
+      await this.walletEntity.save(wallet);
+      // 记录错误日志
+      order.status = 'FAILED';
+      await this.orderEntity.update(order.id, order);
+      throw new CoolCommException('出金处理失败:' + error.message);
+    }
+  }
+
+  async openApiConfirm(orderNo) {
+    const channel = 'SUBPAY'
+    // 查询订单是否存在
+    const orderInfo = await this.orderEntity.findOneBy({
+      order_no: orderNo,
+      order_type: 'WITHDRAW' // 出金
+    })
+
+    if(!orderInfo) {
+      throw new CoolCommException('订单不存在');
+    }
+    await this.paymentService
+      .setChannel(channel)
+      .confirmPayOut(orderInfo.out_order_no);
+    return ''
+  }  
+  async openApiCancel(orderNo) {
+    const channel = 'SUBPAY'
+    // 查询订单是否存在
+    const orderInfo = await this.orderEntity.findOneBy({
+      order_no: orderNo,
+      order_type: 'WITHDRAW' // 出金
+    })
+
+    if(!orderInfo) {
+      throw new CoolCommException('订单不存在');
+    }
+
+    if(orderInfo.status === 'PENDING') {
+      await this.paymentService
+        .setChannel(channel)
+        .cancelPayOut(orderInfo.out_order_no);
+      orderInfo.status = 'REJECTED'
+      await this.orderEntity.save(orderInfo)
+    }
+    return ''
+  }
+  async openApiPatOutInfo(orderNo) {
+     // 查询订单是否存在
+     const orderInfo = await this.orderEntity.findOneBy({
+      order_no: orderNo,
+      order_type: 'WITHDRAW' // 出金
+    })
+    if(!orderInfo) {
+      throw new CoolCommException('订单不存在');
+    }
+
+    return orderInfo
+    
+  }
+
   private generateOrderNo() {
     return 'O' + Date.now() + Math.random().toString(36).substring(2, 15);
   }
@@ -400,13 +600,30 @@ export class OrderService extends BaseService {
         }
       }
     }
+    const amount = this.getOrderFee(order)
     if (order.status == 'SUCCESS') {
+      await this.customeCallback('SUCCESS', biz_type, {
+        order_no: order.out_order_no, 
+        out_order_no: order.order_no, 
+        out_user_id: order.merchantId, 
+        amount: order.amount, 
+        fee: Number(amount) - order.amount,
+        currency: order.currency
+      })
       return {
         is_success: true,
         message: '成功',
       };
     }
     if (order.status == 'FAILED') {
+      await this.customeCallback('FAIL',biz_type, {
+        order_no: order.out_order_no, 
+        out_order_no: order.order_no, 
+        out_user_id: order.merchantId, 
+        amount: order.amount, 
+        fee: Number(amount) - order.amount,
+        currency: order.currency
+      })
       return {
         is_success: false,
         message: '订单已失效',
@@ -500,6 +717,7 @@ export class OrderService extends BaseService {
     });
 
     if (!wallet) {
+      this.customeCallback('payOut', order)
       throw new CoolCommException('钱包不存在');
     }
     const merchant = await this.merchantEntity.findOne({
@@ -611,5 +829,65 @@ export class OrderService extends BaseService {
     // 更新钱包余额和冻结金额
     wallet.frozenAmount = newFrozenAmount;
     await manager.save(wallet);
+    this.customeCallback('SUCCESS','PAYOUT', {
+      order_no: order.out_order_no, 
+      out_order_no: order.order_no, 
+      out_user_id: order.merchantId, 
+      amount: order.amount, 
+      fee: amount - order.amount, 
+      currency: order.currency
+    })
+  }
+
+  /**
+   * 发送请求到 商户 API
+   */
+  private async customeCallback(  biz_status:'SUCCESS'|'FAIL' = 'SUCCESS', biz_type:'PAYOUT'|"PAYIN", data?: any, ) {
+    const {order_no, out_order_no, out_user_id, amount, fee, currency} = data
+    // 查询订单是否存在
+    const orderInfo = await this.orderEntity.findOneBy({
+      out_order_no: out_order_no,
+      order_type:  biz_type === 'PAYOUT' ? 'WITHDRAW' : 'DEPOSIT'//  // 出金
+    })
+    try {
+      if (orderInfo.webhook_url && orderInfo.webhook_url !== this.config.callback.sunpay) {
+        await axios({
+          method: 'post',
+          url: orderInfo.webhook_url,
+          data: {
+            biz_status, 
+            biz_type,
+            data: {
+              order_no, 
+              out_order_no, 
+              out_user_id, 
+              amount, 
+              fee,
+              currency
+            }
+          }
+        });
+      }
+    } catch (error) {
+    }
+  }
+  async getOrderFee(order) {
+    const rate = await this.rateEntity.findOne({
+      where: {
+        currency: order.currency,
+        channelId: 'SUNPAY',
+        merchantId: order.merchantId,
+        type: 'WITHDRAWAL',
+      },
+    });
+    let feeRate;
+    if (!rate) {
+      feeRate = 0.1;
+    } else {
+      feeRate = rate.rate / 100;
+    }
+    // 订单的金额是扣除了手续费的 所以要根据手续费返推之前的金额
+    const amount = this.amountCalculator.divide(order.amount, 1 - feeRate);
+    return amount
   }
 }

+ 4 - 0
src/modules/payment/service/payment.ts

@@ -148,6 +148,10 @@ export class PaymentService extends BaseService {
     const adapter = this.getChannelAdapter(this.channel);
     return adapter.confirmPayOut(orderNo);
   }
+  async cancelPayOut(orderNo: string) {
+    const adapter = this.getChannelAdapter(this.channel);
+    return adapter.cancelPayOut(orderNo);
+  }
   async payInByChain(params: any) {
     const adapter = this.getChannelAdapter(this.channel);
     return adapter.payInByChain(params);