refactor(c2c): expireOrder使用Prisma事务确保原子性
将 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 <noreply@anthropic.com>
This commit is contained in:
parent
bcfa5143e3
commit
627c3c943c
|
|
@ -703,6 +703,7 @@ export class C2cService {
|
|||
|
||||
/**
|
||||
* 使订单过期(解冻资产并标记为过期)
|
||||
* 使用 Prisma 事务确保 解冻/状态更新/恢复创建 的原子性
|
||||
*/
|
||||
private async expireOrder(order: C2cOrderEntity): Promise<void> {
|
||||
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<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue