Procházet zdrojové kódy

基础的提交代付模块

test před 10 měsíci
rodič
revize
879b1906b4

+ 3 - 3
src/modules/dj/controller/admin/open.ts

@@ -71,13 +71,13 @@ export class PayOpenController extends BaseController {
   }
 
   @CoolTag(TagTypes.IGNORE_TOKEN)
-  @Post('/withdraw/create', { summary: '发起代付' })
+  @Post('/withdraw', { summary: '发起代付' })
   public async withdraw(@Body(ALL) payload: any) {
-    return this.ok(await this.repayService.query(payload));
+    return this.ok(await this.repayService.order(payload));
   }
 
   @CoolTag(TagTypes.IGNORE_TOKEN)
-  @Post('/withdraw/query', { summary: '代收查询' })
+  @Post('/withdrawQuery', { summary: '代收查询' })
   public async withdrawQuery(@Body(ALL) payload: any) {
     return this.ok(await this.repayService.query(payload));
   }

+ 27 - 0
src/modules/dj/controller/admin/withdrawChannel.ts

@@ -0,0 +1,27 @@
+import { Provide, Post, Inject, Body, ALL } from '@midwayjs/decorator';
+import { CoolController, BaseController } from '@cool-midway/core';
+import { DispatchService } from '../../service/channels/dispatch';
+import { WithdrawChannelEntity } from '../../entity/withdrawChannel';
+import { WithdrawChannelService } from '../../service/withdrawChannel';
+
+@Provide()
+@CoolController({
+  api: ['add', 'delete', 'update', 'list', 'info', 'page'],
+  entity: WithdrawChannelEntity,
+  service: WithdrawChannelService,
+  pageQueryOp: {
+    keyWordLikeFields: ['name', 'code', '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));
+  }
+}

+ 42 - 0
src/modules/dj/entity/withdrawChannel.ts

@@ -0,0 +1,42 @@
+import { BaseEntity } from '@cool-midway/core';
+import { Column, Entity, Index } from 'typeorm';
+
+/**
+ * 字典信息
+ */
+@Entity('dj_withdraw_channel')
+export class WithdrawChannelEntity extends BaseEntity {
+  @Column({ comment: '通道名' })
+  name: string;
+
+  @Index({ unique: true })
+  @Column({ comment: '通道编码' })
+  code: string;
+
+  @Column({ comment: '代码服务' })
+  service: string;
+
+  @Column({ comment: '货币' })
+  currency: string;
+
+  @Column({ comment: '费率' })
+  rate: string;
+
+  @Column({ comment: '单笔固定费用', type: 'decimal', precision: 10, scale: 2 })
+  basicFee: string;
+
+  @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: '状态 0-未启用 1 启用', type: 'tinyint', default: 1 })
+  status: number;
+
+  @Column({ comment: '备注', nullable: true })
+  remark: string;
+}

+ 240 - 0
src/modules/dj/service/repay.ts

@@ -0,0 +1,240 @@
+import { Inject, Logger, Provide } from '@midwayjs/decorator';
+import { BaseService } from '@cool-midway/core';
+import { HttpService } from '@midwayjs/axios';
+import { ILogger } from '@midwayjs/logger';
+import { CoolCommException } from '@cool-midway/core';
+import * as _ from 'lodash';
+import { WithdrawChannelService } from './withdrawChannel';
+import { MerchantService } from './merchant';
+import { DispatchService } from './channels/dispatch';
+import { Utils } from '../../../comm/utils';
+import * as md5 from 'md5';
+import { WithdrawService } from './withdraw';
+import { MchBalanceService } from './mchBalance';
+
+@Provide()
+export class RepayService extends BaseService {
+  @Inject()
+  httpService: HttpService;
+
+  @Inject()
+  withdrawChannelService: WithdrawChannelService;
+
+  @Inject()
+  merchantService: MerchantService;
+
+  @Inject()
+  withdrawService: WithdrawService;
+
+  @Inject()
+  dispatchService: DispatchService;
+
+  @Inject()
+  mchBalanceService: MchBalanceService;
+
+  @Logger()
+  logger: ILogger;
+
+  @Inject()
+  utils: Utils;
+
+  async query(payload) {
+    try {
+      this.logger.info('代付查询', JSON.stringify(payload));
+      await this.validQueryParam(payload);
+      const merchant = await this.validMerchant(payload);
+      await this.validSign(payload, merchant);
+      const order = await this.withdrawService.findByOutOrderNo(
+        payload.outOrderNo
+      );
+      if (!order || order.mchId !== payload.mchId) {
+        throw new Error('订单不存在');
+      }
+      return {
+        mchId: payload.mchId,
+        orderNo: order.orderNo,
+        outOrderNo: payload.outOrderNo,
+        amount: order.amount,
+        status: order.status,
+      };
+    } catch (err) {
+      this.logger.error('查询失败', JSON.stringify(payload), err.message);
+      throw new CoolCommException(err.message);
+    }
+  }
+
+  async validQueryParam(payload) {
+    if (_.isEmpty(payload.mchId)) {
+      throw new Error('商户号【mchId】不能为空');
+    }
+    if (_.isEmpty(payload.outOrderNo)) {
+      throw new Error('商户订单号【outOrderNo】不能为空');
+    }
+    if (_.isEmpty(payload.sign)) {
+      throw new Error('签名【sign】不能为空');
+    }
+  }
+
+  async order(payload) {
+    let withdraw: any = null;
+    try {
+      this.logger.info('代付下单', JSON.stringify(payload));
+      await this.validParam(payload);
+      const merchant = await this.validMerchant(payload);
+      const channel = await this.validPayWay(payload);
+      await this.validSign(payload, merchant);
+      await this.validOutOrderNo(payload);
+      await this.validBalance(merchant, payload);
+      let channelCharge = (+payload.amount * +channel.rate) / 100 + +channel.basicFee;
+      if(channelCharge < payload.feeMin) {
+        channelCharge = payload.feeMin;
+      }
+      let charge = (+payload.amount * +merchant.withdrawRate) / 100 + +merchant.withdrawBasicFee;
+      if(charge < merchant.withdrawFeeMin) {
+        charge = merchant.withdrawFeeMin;
+      }
+      withdraw = {
+        orderNo: this.utils.createOrderNo('DS'),
+        outOrderNo: payload.outOrderNo,
+        traceNo: '',
+        code: channel.code,
+        mchId: merchant.mchId,
+        amount: payload.amount,
+        currency: payload.currency,
+        accountNo: payload.accountNo,
+        accountName: payload.accountName,
+        bankCode: payload.bankCode,
+        bankName: payload.bankName,
+        channelCharge,
+        charge,
+        notifyUrl: payload.notifyUrl,
+        status: 1,
+        remark: ''
+      };
+      await this.withdrawService.create(withdraw);
+      return {
+        orderNo: withdraw.orderNo,
+        outOrderNo: withdraw.outOrderNo,
+        currency: withdraw.currency,
+        status: withdraw.status
+      };
+    } catch (err) {
+      this.logger.error('代付失败', JSON.stringify(payload), err.message);
+      if (withdraw) {
+        withdraw.remark = err.message;
+        withdraw.status = 3;
+        await this.withdrawService.create(withdraw);
+      }
+      throw new CoolCommException(err.message);
+    }
+  }
+
+  async validParam(payload) {
+    if (_.isEmpty(payload.mchId)) {
+      throw new Error('商户号【mchId】不能为空');
+    }
+    if (_.isEmpty(payload.outOrderNo)) {
+      throw new Error('商户订单号【outOrderNo】不能为空');
+    }
+    if (payload.outOrderNo.length < 8 || payload.outOrderNo.length > 32) {
+      throw new Error('商户订单号的长度范围为:【8,32】');
+    }
+    if (_.isEmpty(payload.notifyUrl)) {
+      throw new Error('代付成功回调地址【notifyUrl】不能为空');
+    }
+    if (_.isEmpty(payload.amount + '')) {
+      throw new Error('支付金额【amount】不能为空');
+    }
+    if (+payload.amount <= 0) {
+      throw new Error('支付金额【amount】必须大于0');
+    }
+    if (_.isEmpty(payload.currency)) {
+      throw new Error('货币单位【currency】不能为空');
+    }
+    if (_.isEmpty(payload.accountNo)) {
+      throw new Error('银行账号【accountNo】不能为空');
+    }
+    if (_.isEmpty(payload.accountName)) {
+      throw new Error('收款人姓名【accountName】不能为空');
+    }
+    if (_.isEmpty(payload.bankName)) {
+      throw new Error('银行名称【bankName】不能为空');
+    }
+    if (_.isEmpty(payload.bankCode)) {
+      throw new Error('银行编码【bankCode】不能为空');
+    }
+    if (_.isEmpty(payload.sign)) {
+      throw new Error('签名【sign】不能为空');
+    }
+  }
+
+  async validPayWay(payload) {
+    if(!payload.payType) {
+      return {
+        code: '',
+        rate: 0,
+        basicFee: 0,
+        feeMin: 0
+      };
+    }
+    const channel = await this.withdrawChannelService.queryByCode(payload.payType);
+    if (!channel) {
+      throw new Error('当前通道码不可用');
+    }
+    let max = channel.max || 0;
+    let min = channel.min || 0;
+    if (+payload.amount > +max || +payload.amount < +min) {
+      throw new Error(`单笔最大限额【${+min}, ${+max}】`);
+    }
+    return channel;
+  }
+
+  async validMerchant(payload) {
+    const merchant = await this.merchantService.getByMchId(payload.mchId);
+    if (!merchant) {
+      throw new Error('商户不存在');
+    }
+    if (+merchant.status !== 1) {
+      throw new Error('商户状态异常');
+    }
+    if (+merchant.withdrawStatus !== 1) {
+      throw new Error('商户代付状态异常');
+    }
+    return merchant;
+  }
+
+  async validSign(payload, merchant) {
+    const data = { ...payload };
+    const sign = data.sign;
+    delete data.sign;
+    const signStr = this.utils.signSort(data);
+    const verifySign = md5(signStr + `&key=${merchant.key}`);
+    this.logger.info(
+      '代付下单加密结果',
+      sign,
+      signStr,
+      verifySign,
+      sign === verifySign
+    );
+    if (sign !== verifySign) {
+      throw new Error(
+        '验签失败,请确认签名是否正确,正确待签名字符串为:' + signStr
+      );
+    }
+  }
+
+  async validOutOrderNo(payload: any) {
+    const order = await this.withdrawService.findByOutOrderNo(payload.outOrderNo);
+    if (order) {
+      throw new Error('商户订单号已存在,请勿重复下单');
+    }
+  }
+
+  async validBalance(merchant, data) {
+    const mchBalance = await this.mchBalanceService.queryByMerchant(merchant.mchId);
+    const { balance = 0 } = mchBalance;
+    if(+data.amount > +balance) {
+      throw new Error('当前商户余额不足,请确认!当前商户余额:' + balance);
+    }
+  }
+}

+ 19 - 0
src/modules/dj/service/withdrawChannel.ts

@@ -0,0 +1,19 @@
+import { 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';
+
+@Provide()
+export class WithdrawChannelService extends BaseService {
+  @InjectEntityModel(WithdrawChannelEntity)
+  withdrawChannelEntity: Repository<WithdrawChannelEntity>;
+
+  async queryByCode(code) {
+    return await this.withdrawChannelEntity.findOneBy({
+      status: 1,
+      code,
+    });
+  }
+}