From 627c3c943c16eb4fc36b234a815fb554e834deb6 Mon Sep 17 00:00:00 2001 From: hailin Date: Sun, 1 Feb 2026 02:43:48 -0800 Subject: [PATCH] =?UTF-8?q?refactor(c2c):=20expireOrder=E4=BD=BF=E7=94=A8P?= =?UTF-8?q?risma=E4=BA=8B=E5=8A=A1=E7=A1=AE=E4=BF=9D=E5=8E=9F=E5=AD=90?= =?UTF-8?q?=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 expireOrder 中的 解冻/状态更新/恢复创建 包在 prisma.$transaction 中: - 事务失败全部回滚,不会出现半完成的不一致状态 - SELL MATCHED/PAID 过期恢复:保持冻结 + 标记EXPIRED + 创建PENDING 原子执行 - BUY MATCHED/PAID 过期恢复:解冻taker + 标记EXPIRED + 创建PENDING 原子执行 - PENDING 过期:解冻 + 标记EXPIRED 原子执行 - 删除 restoreExpiredOrder 方法,逻辑合并到事务内 - 遵循现有模式(executeTransfer 等),在事务回调中直接使用 tx 操作 Co-Authored-By: Claude Opus 4.5 --- .../src/application/services/c2c.service.ts | 139 ++++++++---------- 1 file changed, 63 insertions(+), 76 deletions(-) diff --git a/backend/services/trading-service/src/application/services/c2c.service.ts b/backend/services/trading-service/src/application/services/c2c.service.ts index 99b5f3f0..70c770fb 100644 --- a/backend/services/trading-service/src/application/services/c2c.service.ts +++ b/backend/services/trading-service/src/application/services/c2c.service.ts @@ -703,6 +703,7 @@ export class C2cService { /** * 使订单过期(解冻资产并标记为过期) + * 使用 Prisma 事务确保 解冻/状态更新/恢复创建 的原子性 */ private async expireOrder(order: C2cOrderEntity): Promise { const lockKey = `c2c:expire:${order.orderNo}`; @@ -724,91 +725,77 @@ export class C2cService { const quantityDecimal = new Decimal(freshOrder.quantity); const shouldRestore = freshOrder.status === C2C_ORDER_STATUS.MATCHED || freshOrder.status === C2C_ORDER_STATUS.PAID; + const isSell = freshOrder.type === C2C_ORDER_TYPE.SELL; + let restoreOrderNo: string | null = null; - // 解冻卖方的积分值(C2C交易的是积分值,买方不冻结) - if (freshOrder.status === C2C_ORDER_STATUS.PENDING) { - // PENDING 状态:只有 SELL 订单冻结了 maker 的积分值 - if (freshOrder.type === C2C_ORDER_TYPE.SELL) { - await this.tradingAccountRepository.unfreezeCash(freshOrder.makerAccountSequence, quantityDecimal); + await this.prisma.$transaction(async (tx) => { + // 1. 解冻卖方的积分值(如果需要) + if (freshOrder.status === C2C_ORDER_STATUS.PENDING) { + // PENDING SELL:解冻 maker + if (isSell) { + await tx.tradingAccount.update({ + where: { accountSequence: freshOrder.makerAccountSequence }, + data: { frozenCash: { decrement: quantityDecimal.toNumber() } }, + }); + } + // PENDING BUY:无冻结 + } else if (!isSell && freshOrder.takerAccountSequence) { + // MATCHED/PAID BUY:解冻 taker(卖方) + await tx.tradingAccount.update({ + where: { accountSequence: freshOrder.takerAccountSequence }, + data: { frozenCash: { decrement: quantityDecimal.toNumber() } }, + }); } - // BUY 订单 PENDING 状态没有冻结资产,直接过期即可 - } else if (freshOrder.type === C2C_ORDER_TYPE.BUY) { - // MATCHED/PAID: BUY订单解冻taker(卖方)的积分值 - if (freshOrder.takerAccountSequence) { - await this.tradingAccountRepository.unfreezeCash(freshOrder.takerAccountSequence, quantityDecimal); - } - } else if (shouldRestore) { - // MATCHED/PAID SELL订单将恢复为新PENDING:保持maker的冻结不变 - // 冻结资金直接转给新PENDING订单,避免 解冻→重新冻结 的竞态窗口 - this.logger.log(`SELL订单 ${freshOrder.orderNo} 将恢复,保持冻结 ${quantityDecimal}`); - } else { - // MATCHED/PAID SELL订单不恢复时解冻maker(卖方)的积分值 - await this.tradingAccountRepository.unfreezeCash(freshOrder.makerAccountSequence, quantityDecimal); - } + // MATCHED/PAID SELL + shouldRestore:不解冻,冻结直接转给新 PENDING 订单 - // 更新订单状态为过期 - await this.c2cOrderRepository.updateStatus(freshOrder.orderNo, C2C_ORDER_STATUS.EXPIRED as any, { - expiredAt: new Date(), + // 2. 标记订单为过期 + await tx.c2cOrder.update({ + where: { orderNo: freshOrder.orderNo }, + data: { + status: C2C_ORDER_STATUS.EXPIRED as any, + expiredAt: new Date(), + }, + }); + + // 3. MATCHED/PAID 订单恢复为新的 PENDING 订单 + if (shouldRestore) { + const priceDecimal = new Decimal(freshOrder.price); + restoreOrderNo = this.generateOrderNo(); + await tx.c2cOrder.create({ + data: { + orderNo: restoreOrderNo, + type: freshOrder.type as any, + status: C2C_ORDER_STATUS.PENDING as any, + makerAccountSequence: freshOrder.makerAccountSequence, + makerUserId: freshOrder.makerUserId, + makerPhone: freshOrder.makerPhone, + makerNickname: freshOrder.makerNickname, + price: freshOrder.price, + quantity: quantityDecimal.toString(), + totalAmount: priceDecimal.mul(quantityDecimal).toString(), + minAmount: '0', + maxAmount: '0', + paymentMethod: isSell ? freshOrder.paymentMethod : null, + paymentAccount: isSell ? freshOrder.paymentAccount : null, + paymentQrCode: isSell ? freshOrder.paymentQrCode : null, + paymentRealName: isSell ? freshOrder.paymentRealName : null, + sellerKavaAddress: isSell ? freshOrder.sellerKavaAddress : null, + remark: freshOrder.remark, + }, + }); + } }); + // 事务成功后记日志 this.logger.log(`C2C订单已过期: ${freshOrder.orderNo}, 原状态: ${freshOrder.status}`); - - // MATCHED/PAID 订单过期后,将数量退还为新的 PENDING 订单(恢复到市场) - if (shouldRestore) { - await this.restoreExpiredOrder(freshOrder, quantityDecimal); + if (restoreOrderNo) { + this.logger.log(`过期恢复: 订单 ${freshOrder.orderNo} 已恢复为新 PENDING 订单 ${restoreOrderNo}, 数量: ${quantityDecimal}`); } + } catch (error: any) { + // 事务失败 = 全部回滚,不会出现不一致状态 + this.logger.error(`expireOrder 事务失败: ${order.orderNo}: ${error.message}`); } finally { await this.redis.releaseLock(lockKey, lockValue); } } - - /** - * 过期订单恢复:为 maker 重新创建一个 PENDING 订单 - * 将过期的成交数量退还到市场,让其他用户可以继续接单 - * - * 注意:SELL 订单的冻结在 expireOrder 中已保持不变(未解冻), - * 直接转给新的 PENDING 订单,避免 解冻→重新冻结 的竞态条件。 - */ - private async restoreExpiredOrder(expiredOrder: C2cOrderEntity, quantityDecimal: Decimal): Promise { - try { - const priceDecimal = new Decimal(expiredOrder.price); - const totalAmountDecimal = priceDecimal.mul(quantityDecimal); - - // SELL 订单:冻结已在 expireOrder 中保留,无需重新冻结 - // BUY 订单:无冻结,直接创建新 PENDING 订单 - - // 创建新的 PENDING 订单 - const restoreOrderNo = this.generateOrderNo(); - await this.c2cOrderRepository.create({ - orderNo: restoreOrderNo, - type: expiredOrder.type as any, - makerAccountSequence: expiredOrder.makerAccountSequence, - makerUserId: expiredOrder.makerUserId ?? undefined, - makerPhone: expiredOrder.makerPhone ?? undefined, - makerNickname: expiredOrder.makerNickname ?? undefined, - price: expiredOrder.price, - quantity: quantityDecimal.toString(), - totalAmount: totalAmountDecimal.toString(), - paymentMethod: expiredOrder.type === C2C_ORDER_TYPE.SELL ? (expiredOrder.paymentMethod ?? undefined) : undefined, - paymentAccount: expiredOrder.type === C2C_ORDER_TYPE.SELL ? (expiredOrder.paymentAccount ?? undefined) : undefined, - paymentQrCode: expiredOrder.type === C2C_ORDER_TYPE.SELL ? (expiredOrder.paymentQrCode ?? undefined) : undefined, - paymentRealName: expiredOrder.type === C2C_ORDER_TYPE.SELL ? (expiredOrder.paymentRealName ?? undefined) : undefined, - sellerKavaAddress: expiredOrder.type === C2C_ORDER_TYPE.SELL ? (expiredOrder.sellerKavaAddress ?? undefined) : undefined, - remark: expiredOrder.remark ?? undefined, - }); - - this.logger.log(`过期恢复: 订单 ${expiredOrder.orderNo} 已恢复为新 PENDING 订单 ${restoreOrderNo}, 数量: ${quantityDecimal}`); - } catch (error) { - this.logger.error(`过期恢复失败: ${expiredOrder.orderNo}`, error); - // SELL 订单恢复失败时,需要解冻之前保留的冻结(否则 maker 资金被永久冻结) - if (expiredOrder.type === C2C_ORDER_TYPE.SELL) { - try { - await this.tradingAccountRepository.unfreezeCash(expiredOrder.makerAccountSequence, quantityDecimal); - this.logger.warn(`过期恢复失败,已解冻 ${quantityDecimal} for ${expiredOrder.makerAccountSequence}`); - } catch (unfreezeError) { - this.logger.error(`过期恢复失败后解冻也失败: ${expiredOrder.orderNo}`, unfreezeError); - } - } - } - } }