From 49ba2fcb19505ff750a133ad12f1be0d3432b775 Mon Sep 17 00:00:00 2001 From: hailin Date: Sun, 1 Feb 2026 03:09:50 -0800 Subject: [PATCH] =?UTF-8?q?feat(c2c):=20=E6=B7=BB=E5=8A=A0expireOrder?= =?UTF-8?q?=E4=BA=8B=E5=8A=A1=E6=B5=81=E7=A8=8B=E7=9A=84=E8=AF=A6=E7=BB=86?= =?UTF-8?q?=E8=B0=83=E8=AF=95=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.5 --- .../src/application/services/c2c.service.ts | 70 ++++++++++++++----- 1 file changed, 53 insertions(+), 17 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 70c770fb..5840e160 100644 --- a/backend/services/trading-service/src/application/services/c2c.service.ts +++ b/backend/services/trading-service/src/application/services/c2c.service.ts @@ -682,7 +682,15 @@ export class C2cService { async processExpiredOrders(): Promise { // 计算 PENDING 订单的过期截止时间 const pendingCutoff = new Date(Date.now() - DEFAULT_PENDING_TIMEOUT_HOURS * 60 * 60 * 1000); + this.logger.debug(`[EXPIRY] 开始扫描超时订单, PENDING截止时间: ${pendingCutoff.toISOString()}`); + const expiredOrders = await this.c2cOrderRepository.findExpiredOrders(pendingCutoff); + + if (expiredOrders.length === 0) { + return 0; + } + + this.logger.log(`[EXPIRY] 发现 ${expiredOrders.length} 个超时订单: ${expiredOrders.map(o => `${o.orderNo}(${o.type}/${o.status})`).join(', ')}`); let processedCount = 0; for (const order of expiredOrders) { @@ -690,14 +698,11 @@ export class C2cService { await this.expireOrder(order); processedCount++; } catch (error) { - this.logger.error(`处理超时订单失败: ${order.orderNo}`, error); + this.logger.error(`[EXPIRY] 处理超时订单失败: ${order.orderNo}`, error); } } - if (processedCount > 0) { - this.logger.log(`处理了 ${processedCount} 个超时订单`); - } - + this.logger.log(`[EXPIRY] 本轮处理完成: 成功 ${processedCount}/${expiredOrders.length}`); return processedCount; } @@ -709,17 +714,23 @@ export class C2cService { const lockKey = `c2c:expire:${order.orderNo}`; const lockValue = await this.redis.acquireLock(lockKey, 30); if (!lockValue) { - return; // 其他进程正在处理 + this.logger.debug(`[EXPIRY] 订单 ${order.orderNo} 正被其他进程处理,跳过`); + return; } try { // 重新获取订单,确保状态一致 const freshOrder = await this.c2cOrderRepository.findByOrderNo(order.orderNo); - if (!freshOrder || ( + if (!freshOrder) { + this.logger.warn(`[EXPIRY] 订单 ${order.orderNo} 不存在,跳过`); + return; + } + if ( freshOrder.status !== C2C_ORDER_STATUS.PENDING && freshOrder.status !== C2C_ORDER_STATUS.MATCHED && freshOrder.status !== C2C_ORDER_STATUS.PAID - )) { + ) { + this.logger.debug(`[EXPIRY] 订单 ${order.orderNo} 状态已变为 ${freshOrder.status},无需处理`); return; } @@ -728,27 +739,39 @@ export class C2cService { const isSell = freshOrder.type === C2C_ORDER_TYPE.SELL; let restoreOrderNo: string | null = null; + this.logger.log( + `[EXPIRY] 开始处理订单 ${freshOrder.orderNo}: ` + + `类型=${freshOrder.type}, 状态=${freshOrder.status}, ` + + `数量=${freshOrder.quantity}, 价格=${freshOrder.price}, ` + + `maker=${freshOrder.makerAccountSequence}, taker=${freshOrder.takerAccountSequence || '无'}, ` + + `需恢复=${shouldRestore}`, + ); + await this.prisma.$transaction(async (tx) => { // 1. 解冻卖方的积分值(如果需要) if (freshOrder.status === C2C_ORDER_STATUS.PENDING) { - // PENDING SELL:解冻 maker if (isSell) { + this.logger.log(`[EXPIRY][TX] 步骤1: 解冻 PENDING SELL maker ${freshOrder.makerAccountSequence} 的 frozenCash -= ${quantityDecimal}`); await tx.tradingAccount.update({ where: { accountSequence: freshOrder.makerAccountSequence }, data: { frozenCash: { decrement: quantityDecimal.toNumber() } }, }); + } else { + this.logger.debug(`[EXPIRY][TX] 步骤1: PENDING BUY 无冻结资产,跳过解冻`); } - // PENDING BUY:无冻结 } else if (!isSell && freshOrder.takerAccountSequence) { // MATCHED/PAID BUY:解冻 taker(卖方) + this.logger.log(`[EXPIRY][TX] 步骤1: 解冻 ${freshOrder.status} BUY taker ${freshOrder.takerAccountSequence} 的 frozenCash -= ${quantityDecimal}`); await tx.tradingAccount.update({ where: { accountSequence: freshOrder.takerAccountSequence }, data: { frozenCash: { decrement: quantityDecimal.toNumber() } }, }); + } else if (isSell && shouldRestore) { + this.logger.log(`[EXPIRY][TX] 步骤1: ${freshOrder.status} SELL 保持冻结不变,冻结量将转给恢复的 PENDING 订单`); } - // MATCHED/PAID SELL + shouldRestore:不解冻,冻结直接转给新 PENDING 订单 // 2. 标记订单为过期 + this.logger.log(`[EXPIRY][TX] 步骤2: 标记订单 ${freshOrder.orderNo} 状态 ${freshOrder.status} -> EXPIRED`); await tx.c2cOrder.update({ where: { orderNo: freshOrder.orderNo }, data: { @@ -761,6 +784,12 @@ export class C2cService { if (shouldRestore) { const priceDecimal = new Decimal(freshOrder.price); restoreOrderNo = this.generateOrderNo(); + const totalAmount = priceDecimal.mul(quantityDecimal).toString(); + this.logger.log( + `[EXPIRY][TX] 步骤3: 恢复为新 PENDING 订单 ${restoreOrderNo}, ` + + `类型=${freshOrder.type}, maker=${freshOrder.makerAccountSequence}, ` + + `数量=${quantityDecimal}, 总金额=${totalAmount}`, + ); await tx.c2cOrder.create({ data: { orderNo: restoreOrderNo, @@ -772,7 +801,7 @@ export class C2cService { makerNickname: freshOrder.makerNickname, price: freshOrder.price, quantity: quantityDecimal.toString(), - totalAmount: priceDecimal.mul(quantityDecimal).toString(), + totalAmount, minAmount: '0', maxAmount: '0', paymentMethod: isSell ? freshOrder.paymentMethod : null, @@ -783,17 +812,24 @@ export class C2cService { remark: freshOrder.remark, }, }); + } else { + this.logger.debug(`[EXPIRY][TX] 步骤3: PENDING 订单无需恢复`); } }); // 事务成功后记日志 - this.logger.log(`C2C订单已过期: ${freshOrder.orderNo}, 原状态: ${freshOrder.status}`); - if (restoreOrderNo) { - this.logger.log(`过期恢复: 订单 ${freshOrder.orderNo} 已恢复为新 PENDING 订单 ${restoreOrderNo}, 数量: ${quantityDecimal}`); - } + this.logger.log( + `[EXPIRY] 事务提交成功: 订单 ${freshOrder.orderNo} (${freshOrder.type}/${freshOrder.status}) -> EXPIRED` + + (restoreOrderNo ? `, 恢复为 ${restoreOrderNo} (PENDING, 数量=${quantityDecimal})` : ''), + ); } catch (error: any) { // 事务失败 = 全部回滚,不会出现不一致状态 - this.logger.error(`expireOrder 事务失败: ${order.orderNo}: ${error.message}`); + this.logger.error( + `[EXPIRY] 事务失败,已回滚: 订单=${order.orderNo}, ` + + `错误=${error.message}`, + error.stack, + ); + throw error; } finally { await this.redis.releaseLock(lockKey, lockValue); }