Procházet zdrojové kódy

增加退款模块

test před 7 měsíci
rodič
revize
859c079dff

+ 5 - 0
src/modules/dj/controller/admin/order.ts

@@ -29,6 +29,11 @@ export class OrdertController extends BaseController {
     return this.ok(await this.orderService.notify(id));
   }
 
+  @Post('/toRefund', { summary: '退款' })
+  async toRefund(@Body() payload) {
+    return this.ok(await this.orderService.toRefund(payload));
+  }
+
   @Post('/code', { summary: '获取订单通道编码' })
   async code(@Body('id') id) {
     return this.ok(id);

+ 21 - 0
src/modules/dj/controller/admin/refund.ts

@@ -0,0 +1,21 @@
+import { RefundEntity } from '../../entity/refund';
+import { Inject, Post, Provide, Body } from '@midwayjs/decorator';
+import { CoolController, BaseController } from '@cool-midway/core';
+import { RefundService } from '../../service/refund';
+
+@Provide()
+@CoolController({
+  api: ['update', 'info', 'page'],
+  entity: RefundEntity,
+  service: RefundService,
+})
+export class RefundController extends BaseController {
+  @Inject()
+  refundService: RefundService;
+
+  @Post('/query', { summary: '查单' })
+  async query(@Body('id') id) {
+    return this.ok(await this.refundService.queryByApi(id));
+  }
+
+}

+ 1 - 1
src/modules/dj/entity/order.ts

@@ -54,7 +54,7 @@ export class OrderEntity extends BaseEntity {
   notifyUrl: string;
 
   @Column({
-    comment: '订单状态 0-待支付 1 已支付 2 支付失败',
+    comment: '订单状态 0-待支付 1 已支付 2 支付失败 3 退款成功',
     type: 'tinyint',
     default: 0,
   })

+ 55 - 0
src/modules/dj/entity/refund.ts

@@ -0,0 +1,55 @@
+import { BaseEntity } from '@cool-midway/core';
+import { Column, Entity, Index } from 'typeorm';
+
+/**
+ * 字典信息
+ */
+@Entity('dj_refund')
+export class RefundEntity extends BaseEntity {
+  @Column({ comment: '订单号' })
+  orderNo: string;
+
+  @Column({ comment: '商户订单号' })
+  outOrderNo: string;
+
+  @Column({ comment: '交易号', nullable: true })
+  traceNo: string;
+
+  @Index({ unique: true })
+  @Column({ comment: '退款订单号'})
+  refundNo: string;
+
+  @Column({ comment: '退款交易号'})
+  refundTraceNo: string;
+
+  @Column({ comment: '通道码' })
+  code: string;
+
+  @Column({ comment: '商户号' })
+  mchId: string
+
+  @Column({ comment: '退款金额', type: 'decimal', precision: 10, scale: 2 })
+  amount: number;
+
+  @Column({ comment: '手续费', type: 'decimal', precision: 10, scale: 2 })
+  charge: number;
+  
+  @Column({ comment: '货币单位' })
+  currency: string;
+
+  @Column({ comment: '退款原因', nullable: true })
+  reson: string;
+
+  @Column({ comment: '退款时间', nullable: true })
+  date: Date;
+
+  @Column({
+    comment: '订单状态 0-退款中 1 退款成功 2 退款失败',
+    type: 'tinyint',
+    default: 0,
+  })
+  status: number;
+
+  @Column({ comment: '备注', nullable: true })
+  remark: string;
+}

+ 23 - 0
src/modules/dj/service/channels/dispatch.ts

@@ -22,7 +22,30 @@ export class DispatchService extends BaseService {
 
   @Inject()
   hambitInrService: HambitInrService;
+  
+  async refund(refund) {
+    try {
+      const channel = await this.channelService.queryByCode(refund.code);
+      if (!this[channel.service] || !this[channel.service].refund) {
+        throw new Error('该渠道不支持退款');
+      }
+      return await this[channel.service].refund(refund);
+    } catch (e) {
+      throw new Error('请求通道接口失败,失败原因:' + e.message);
+    }
+  }
 
+  async queryRefund(refund) {
+    try {
+      const channel = await this.channelService.queryByCode(refund.code);
+      if (!this[channel.service] || !this[channel.service].refund) {
+        throw new Error('该渠道不支持退款');
+      }
+      return await this[channel.service].queryRefund(refund);
+    } catch (e) {
+      throw new Error('请求通道接口失败,失败原因:' + e.message);
+    }
+  }
 
   async order(order, channel) {
     try {

+ 82 - 1
src/modules/dj/service/channels/sunpay.ts

@@ -17,6 +17,8 @@ const BASIC_KEY_URL = HOST + '/api/v3-2/Fiat/Kyc/CreateBasicUser';
 const ADVANCED_KEY_URL = HOST + '/api/v3-2/Fiat/Kyc/CreateAdvancedUser';
 const PREMINUM_KEY_URL = HOST + '/api/v3-2/Fiat/Kyc/CreatePremiumUser';
 const COUNTRIES_URL = HOST + '/api/v3-2/Fiat/Countries';
+const REFUND_URL = HOST + '/api/v3-2/Fiat/Direct/Refund';
+const QUERY_REFUND_URL = HOST + '/api/v3-2/Fiat/x/Refund'
 
 // const API_KEY = '08dcd396-a91a-486a-8ca5-02150819d7ae';
 const API_KEY = '08dd0477-8676-4f68-83c1-215dceeffa2e';
@@ -201,7 +203,7 @@ export class SunPayService extends BaseService {
     if (!payload.traceNo) {
       return {
         status: 0,
-        msg: 'traceNo is'
+        msg: 'traceNo is no exits'
       }
     }
     const param = {
@@ -254,4 +256,83 @@ export class SunPayService extends BaseService {
       traceNo: data.order_no,
     };
   }
+
+  async refund(refund) {
+    const param = {
+      order_no: refund.traceNo,
+      amount: refund.amount,
+      out_refund_order_no: refund.refundNo,
+      webhook_url: "",
+      Reason: refund.reason,
+    };
+    const timestamp = +moment();
+    const nonce = md5(timestamp);
+    const data = timestamp + nonce + JSON.stringify(param);
+    const sign = this.utils.signBySha256(data, API_SERECT).toUpperCase();
+    const res = await this.httpService.post(REFUND_URL, param, {
+      headers: {
+        'SunPay-Key': API_KEY,
+        'SunPay-Timestamp': timestamp,
+        'SunPay-Nonce': nonce,
+        'SunPay-Sign': sign
+      }
+    });
+    this.logger.info('退款接口返回', param, JSON.stringify(res.data));
+    const { is_success, msg } = res.data;
+    if (is_success && res.data.data.refund_no) {
+      return {
+        refundTraceNo: res.data.data.refund_no,
+      };
+    } else {
+      throw new Error(msg);
+    }
+  }
+
+  async queryRefund(refund) {
+    const param = {
+      order_no: refund.traceNo,
+    };
+    const timestamp = +moment();
+    const nonce = md5(timestamp);
+    const signData = timestamp + nonce;
+    const sign = this.utils.signBySha256(signData, API_SERECT).toUpperCase();
+    const res = await this.httpService.get(QUERY_REFUND_URL.replace('{orderNo}', param.order_no), {
+      headers: {
+        'SunPay-Key': API_KEY,
+        'SunPay-Timestamp': timestamp,
+        'SunPay-Nonce': nonce,
+        'SunPay-Sign': sign
+      }
+    });
+    this.logger.info('退款接口查询返回', param, JSON.stringify(res.data));
+    const { is_success, msg, data = {} } = res.data;
+    if (is_success) {
+      const { refund_detail = [] } = data;
+      const bean = refund_detail.find(item => {
+        item.out_refund_no = refund.refundNo
+      });
+      if(bean && bean.status === 'SUCCESS') {
+        return {
+          status: 1,
+          refundTraceNo: bean.refund_no,
+          date: new Date(),
+          message: ''
+        }
+      } else if(bean && bean.status === 'FAIL') {
+        return {
+          status: 2,
+          refundTraceNo: bean.refund_no,
+          date: new Date(),
+          message: ''
+        }
+      } else {
+        return {
+          status: 0,
+          message: ''
+        }
+      }
+    } else {
+      throw new Error(msg);
+    }
+  }
 }

+ 31 - 20
src/modules/dj/service/order.ts

@@ -12,6 +12,7 @@ import { Utils } from '../../../comm/utils';
 import { DispatchService } from './channels/dispatch';
 import { ChannelService } from './channel';
 import { WalletService } from './wallet';
+import { RefundService } from './refund';
 
 @Provide()
 export class OrderService extends BaseService {
@@ -33,12 +34,26 @@ export class OrderService extends BaseService {
   @Inject()
   channelService: ChannelService;
 
+  @Inject()
+  refundService: RefundService;
+
   @Inject()
   ctx;
 
   @Inject()
   utils: Utils;
 
+  async toRefund(payload) {
+    const order = await this.orderEntity.findOneBy({ orderNo: payload.orderNo });
+    if (!order) {
+      throw new CoolCommException('订单不存在');
+    }
+    if (+order.status !== 1) {
+      throw new CoolCommException('订单未支付成功');
+    }
+    return await this.refundService.toRefund(order, payload.amount, payload.reson);
+  }
+
   async handleNotify(code, payload, headers) {
     const data = await this.dispatchService.handleOrderNotify(code, payload, headers);
     const order = await this.orderEntity.findOneBy({ orderNo: data.orderNo });
@@ -184,19 +199,17 @@ export class OrderService extends BaseService {
     SUM(IF(a.status = 1, a.charge, 0)) as num2,
     SUM(IF(a.status = 1, 1, 0)) as num3,
     count(*) as num4
-    ${
-      this.utils.isMerchant(roleIds)
+    ${this.utils.isMerchant(roleIds)
         ? ''
         : ',SUM(IF(a.status = 1, a.channelCharge, 0)) as num5'
-    }
+      }
     FROM dj_order a WHERE 1=1
       ${this.setSql(orderNo, 'and a.orderNo like ?', [`%${orderNo}%`])}
       ${this.setSql(outOrderNo, 'and a.outOrderNo like ?', [`%${outOrderNo}%`])}
       ${this.setSql(traceNo, 'and a.traceNo like ?', [`%${traceNo}%`])}
-      ${
-        this.utils.isMerchant(roleIds)
-          ? this.setSql(mchId, 'and a.mchId = ?', [`${mchId}`])
-          : this.setSql(mchId, 'and a.mchId like ?', [`%${mchId}%`])
+      ${this.utils.isMerchant(roleIds)
+        ? this.setSql(mchId, 'and a.mchId = ?', [`${mchId}`])
+        : this.setSql(mchId, 'and a.mchId like ?', [`%${mchId}%`])
       }
       ${this.setSql(payType, 'and a.payType = ?', [payType])}
       ${this.setSql(code, 'and a.code = ?', [code])}
@@ -238,16 +251,14 @@ export class OrderService extends BaseService {
         mchId = '-1';
       }
     }
-    const sql = `SELECT a.id, a.orderNo, a.outOrderNo, a.payUrl, a.traceNo, a.mchId,  a.code, a.payType, a.amount,${
-      this.utils.isMerchant(roleIds) ? '' : 'a.channelCharge,'
-    } a.charge, a.status, a.notifyStatus, a.userId, a.userIp, a.currency, a.date, a.notifyUrl, a.returnUrl, a.remark, a.createTime, a.updateTime FROM dj_order a WHERE 1=1
+    const sql = `SELECT a.id, a.orderNo, a.outOrderNo, a.payUrl, a.traceNo, a.mchId,  a.code, a.payType, a.amount,${this.utils.isMerchant(roleIds) ? '' : 'a.channelCharge,'
+      } a.charge, a.status, a.notifyStatus, a.userId, a.userIp, a.currency, a.date, a.notifyUrl, a.returnUrl, a.remark, a.createTime, a.updateTime FROM dj_order a WHERE 1=1
       ${this.setSql(orderNo, 'and a.orderNo like ?', [`%${orderNo}%`])}
       ${this.setSql(outOrderNo, 'and a.outOrderNo like ?', [`%${outOrderNo}%`])}
       ${this.setSql(traceNo, 'and a.traceNo like ?', [`%${traceNo}%`])}
-      ${
-        this.utils.isMerchant(roleIds)
-          ? this.setSql(mchId, 'and a.mchId = ?', [`${mchId}`])
-          : this.setSql(mchId, 'and a.mchId like ?', [`%${mchId}%`])
+      ${this.utils.isMerchant(roleIds)
+        ? this.setSql(mchId, 'and a.mchId = ?', [`${mchId}`])
+        : this.setSql(mchId, 'and a.mchId like ?', [`%${mchId}%`])
       }
       ${this.setSql(payType, 'and a.payType = ?', [payType])}
       ${this.setSql(code, 'and a.code = ?', [code])}
@@ -268,9 +279,9 @@ export class OrderService extends BaseService {
       moment().endOf('week').format('YYYY-MM-DD') + ' 23:59:59',
     ];
     const sql = `SELECT SUM(IF(a.status = 1, a.amount, 0)) as total FROM dj_order a WHERE 1=1 
-    ${ this.setSql(mchId, 'and a.mchId = ?', [`${mchId}`]) }
-    ${ this.setSql(userId, 'and a.userId = ?', [`${userId}`]) }
-    ${ this.setSql(code, 'and a.code = ?', [`${code}`]) }
+    ${this.setSql(mchId, 'and a.mchId = ?', [`${mchId}`])}
+    ${this.setSql(userId, 'and a.userId = ?', [`${userId}`])}
+    ${this.setSql(code, 'and a.code = ?', [`${code}`])}
     ${this.setSql(true, 'and (a.createTime between ? and ?)', time)}
     `
     return await this.nativeQuery(sql);
@@ -282,9 +293,9 @@ export class OrderService extends BaseService {
       moment().endOf('month').format('YYYY-MM-DD') + ' 23:59:59',
     ];
     const sql = `SELECT SUM(IF(a.status = 1, a.amount, 0)) as total FROM dj_order a WHERE 1=1 
-    ${ this.setSql(mchId, 'and a.mchId = ?', [`${mchId}`]) }
-    ${ this.setSql(userId, 'and a.userId = ?', [`${userId}`]) }
-    ${ this.setSql(code, 'and a.code = ?', [`${code}`]) }
+    ${this.setSql(mchId, 'and a.mchId = ?', [`${mchId}`])}
+    ${this.setSql(userId, 'and a.userId = ?', [`${userId}`])}
+    ${this.setSql(code, 'and a.code = ?', [`${code}`])}
     ${this.setSql(true, 'and (a.createTime between ? and ?)', time)}
     `
     return await this.nativeQuery(sql);

+ 218 - 0
src/modules/dj/service/refund.ts

@@ -0,0 +1,218 @@
+import { Inject, Logger, Provide } from '@midwayjs/decorator';
+import { BaseService } from '@cool-midway/core';
+import { InjectEntityModel } from '@midwayjs/typeorm';
+import { Repository } from 'typeorm';
+import * as _ from 'lodash';
+import { CoolCommException } from '@cool-midway/core';
+import { MerchantEntity } from '../entity/merchant';
+import { Utils } from '../../../comm/utils';
+import { DispatchService } from './channels/dispatch';
+import { WalletService } from './wallet';
+import { RefundEntity } from '../entity/refund';
+import { ILogger } from '@midwayjs/logger';
+
+@Provide()
+export class RefundService extends BaseService {
+  @InjectEntityModel(RefundEntity)
+  refundEntity: Repository<RefundEntity>;
+
+  @InjectEntityModel(MerchantEntity)
+  merchantEntity: Repository<MerchantEntity>;
+
+  @Inject()
+  walletService: WalletService;
+
+  @Inject()
+  dispatchService: DispatchService;
+
+  @Inject()
+  ctx;
+
+  @Inject()
+  utils: Utils;
+
+  @Logger()
+  logger: ILogger;
+
+  async toRefund(order, amount, reson) {
+    const refund: any = {
+      orderNo: order.orderNo,
+      outOrderNo: order.outOrderNo,
+      traceNo: order.traceNo,
+      refundNo: this.utils.createOrderNo('TK'),
+      refundTraceNo: '',
+      code: order.code,
+      mchId: order.mchId,
+      amount,
+      charge: order.charge,
+      currency: order.currency,
+      reson,
+      date: null,
+      status: 0,
+      remark: '',
+      createTime: new Date(),
+      updateTime: new Date()
+    }
+    const wallet = await this.walletService.queryByMchIdAndCurrency(refund.mchId, refund.currency);
+    const { balance = 0, freeze = 0 } = wallet;
+    const totalAmount = +refund.amount + +refund.charge
+    const withdrawAmount = +balance - +freeze
+    if (+totalAmount > +withdrawAmount) {
+      throw new Error('当前商户余额不足,无法发起退款!当前商户可提现余额:' + withdrawAmount);
+    }
+    await this.refundEntity.insert(refund);
+    await this.walletService.updateBalance({
+      orderNo: refund.orderNo,
+      mchId: refund.mchId,
+      type: 5,
+      amount: 0,
+      currency: refund.currency,
+      freeze: totalAmount
+    });
+    try {
+      this.logger.error('发起退款', refund.orderNo, refund.refundNo);
+      const { refundTraceNo } = await this.dispatchService.refund(refund);
+      refund.refundTraceNo = refundTraceNo;
+      await this.refundEntity.update(refund.id, refund);
+      return {
+        outOrderNo: refund.outOrderNo,
+        orderNo: refund.orderNo,
+        refundNo: refund.refundNo,
+        amount: refund.amount,
+        currency: refund.currency,
+        status: refund.status
+      }
+    } catch (err) {
+      this.logger.error('发起退款失败', refund.orderNo, err.message);
+      await this.updateRefundFail(refund, err.message);
+      throw new CoolCommException(err.message);
+    }
+  }
+
+  async updateRefundFail(refund, message) {
+    refund.remark = message;
+    refund.status = 2;
+    refund.updateTime = new Date();
+    const totalAmount = +refund.amount + +refund.charge
+    await this.refundEntity.update(refund.id, refund);
+    await this.walletService.updateBalance({
+      orderNo: refund.orderNo,
+      mchId: refund.mchId,
+      type: 5,
+      amount: 0,
+      currency: refund.currency,
+      freeze: -totalAmount
+    });
+    return true;
+  }
+
+  async updateRefundSuccess(data) {
+    const refund = await this.refundEntity.findOneBy({ id: data.id });
+    if (!refund || +refund.status === 1) {
+      // 订单早就已退款成功,无需做任何操作
+      return;
+    }
+    refund.status = 1;
+    refund.updateTime = new Date();
+    refund.date = data.date || new Date();
+    refund.refundTraceNo = data.refundTraceNo;
+    const totalAmount = +refund.amount + +refund.charge
+    await this.refundEntity.update(refund.id, refund);
+    await this.walletService.updateBalance({
+      orderNo: refund.orderNo,
+      mchId: refund.mchId,
+      type: 5,
+      amount: -totalAmount,
+      currency: refund.currency,
+      freeze: -totalAmount
+    });
+    return true;
+  }
+
+  async queryByApi(id) {
+    const refund = await this.refundEntity.findOneBy({ id });
+    if (!refund) {
+      throw new CoolCommException('订单不存在');
+    }
+    if (+refund.status === 0 || +refund.status === 2) {
+      const { status, message, refundTraceNo, date } =
+        await this.dispatchService.queryRefund(refund);
+      if (+status === 1) {
+        // 更新订单和更新余额
+        refund.refundTraceNo = refundTraceNo || refund.refundTraceNo;
+        refund.date = date;
+        await this.updateRefundSuccess(refund);
+      } else if (+status === 2) {
+        refund.refundTraceNo = refundTraceNo || refund.refundTraceNo;
+        refund.date = date;
+        await this.updateRefundFail(refund, message);
+      }
+      return { status, message };
+    } else {
+      return refund.status;
+    }
+  }
+
+  async update(param) {
+    const { roleIds } = this.ctx.admin;
+    if (this.utils.isMerchant(roleIds)) {
+      return;
+    }
+    const refund = await this.refundEntity.findOneBy({ id: param.id });
+    if (!refund) {
+      throw new Error('订单不存在');
+    }
+    await this.refundEntity.update(refund.id, {
+      refundTraceNo: param.refundTraceNo,
+      date: param.date,
+      remark: param.remark,
+      updateTime: new Date(),
+    });
+    if (+refund.status !== 1 && +param.status === 1) {
+      await this.updateRefundSuccess(refund);
+    }
+  }
+
+  async page(query) {
+    let {
+      orderNo = '',
+      outOrderNo = '',
+      traceNo = '',
+      refundNo = '',
+      refundTraceNo = '',
+      mchId = '',
+      status = '',
+      createTime = [],
+    } = query;
+    if (!createTime || createTime.length !== 2) {
+      throw new CoolCommException('请选择日期范围');
+    }
+    const { roleIds, userId } = this.ctx.admin;
+    if (this.utils.isMerchant(roleIds)) {
+      const merchant = await this.merchantEntity.findOneBy({ userId });
+      if (merchant) {
+        mchId = merchant.mchId;
+      } else {
+        mchId = '-1';
+      }
+    }
+    const sql = `SELECT * FROM dj_refund a WHERE 1=1
+      ${this.setSql(orderNo, 'and a.orderNo like ?', [`%${orderNo}%`])}
+      ${this.setSql(outOrderNo, 'and a.outOrderNo like ?', [`%${outOrderNo}%`])}
+      ${this.setSql(traceNo, 'and a.traceNo like ?', [`%${traceNo}%`])}
+      ${this.setSql(refundNo, 'and a.refundNo like ?', [`%${refundNo}%`])}
+      ${this.setSql(refundTraceNo, 'and a.refundTraceNo like ?', [`%${refundTraceNo}%`])}
+      ${this.utils.isMerchant(roleIds)
+        ? this.setSql(mchId, 'and a.mchId = ?', [`${mchId}`])
+        : this.setSql(mchId, 'and a.mchId like ?', [`%${mchId}%`])
+      }
+      ${this.setSql(status, 'and a.status = ?', [status])}
+      ${this.setSql(
+        createTime && createTime.length === 2,
+        'and (a.createTime between ? and ?)',
+        createTime
+      )}
+    `;
+    return this.sqlRenderPage(sql, query);
+  }
+}

+ 2 - 0
src/modules/dj/service/wallet.ts

@@ -88,6 +88,8 @@ export class WalletService extends BaseService {
       return 'CZ';
     } else if (+type === 4) {
       return 'NC';
+    } else if(+type === 5) {
+      return 'TK'
     } else {
       return 'DS';
     }