fix(c2c): 修复过期恢复的竞态条件,SELL订单保持冻结避免资金窗口

- SELL MATCHED/PAID 订单过期恢复时,不再先解冻再重新冻结
- 保持 maker 的冻结不变,直接转给新 PENDING 订单
- 消除了解冻→重新冻结之间的竞态窗口(余额可能被其他操作消耗)
- 恢复失败时补充解冻,防止 maker 资金被永久冻结
- BUY 订单恢复时不复制上一个 taker 的 sellerKavaAddress

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-02-01 02:11:47 -08:00
parent 7f2479d995
commit 33dda98e81
1 changed files with 22 additions and 18 deletions

View File

@ -723,6 +723,7 @@ export class C2cService {
}
const quantityDecimal = new Decimal(freshOrder.quantity);
const shouldRestore = freshOrder.status === C2C_ORDER_STATUS.MATCHED || freshOrder.status === C2C_ORDER_STATUS.PAID;
// 解冻卖方的积分值C2C交易的是积分值买方不冻结
if (freshOrder.status === C2C_ORDER_STATUS.PENDING) {
@ -736,8 +737,12 @@ export class C2cService {
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卖方的积分值
// MATCHED/PAID SELL订单不恢复时解冻maker卖方的积分值
await this.tradingAccountRepository.unfreezeCash(freshOrder.makerAccountSequence, quantityDecimal);
}
@ -749,7 +754,7 @@ export class C2cService {
this.logger.log(`C2C订单已过期: ${freshOrder.orderNo}, 原状态: ${freshOrder.status}`);
// MATCHED/PAID 订单过期后,将数量退还为新的 PENDING 订单(恢复到市场)
if (freshOrder.status === C2C_ORDER_STATUS.MATCHED || freshOrder.status === C2C_ORDER_STATUS.PAID) {
if (shouldRestore) {
await this.restoreExpiredOrder(freshOrder, quantityDecimal);
}
} finally {
@ -760,26 +765,17 @@ export class C2cService {
/**
* 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 订单需要重新冻结 maker 的积分值
if (expiredOrder.type === C2C_ORDER_TYPE.SELL) {
const makerAccount = await this.tradingAccountRepository.findByAccountSequence(expiredOrder.makerAccountSequence);
if (!makerAccount) {
this.logger.warn(`过期恢复: maker ${expiredOrder.makerAccountSequence} 账户不存在,跳过恢复`);
return;
}
const quantityMoney = new Money(quantityDecimal);
if (makerAccount.availableCash.isLessThan(quantityMoney)) {
this.logger.warn(`过期恢复: maker ${expiredOrder.makerAccountSequence} 可用余额不足 (需要 ${quantityDecimal}, 可用 ${makerAccount.availableCash}),跳过恢复`);
return;
}
await this.tradingAccountRepository.freezeCash(expiredOrder.makerAccountSequence, quantityDecimal);
}
// SELL 订单:冻结已在 expireOrder 中保留,无需重新冻结
// BUY 订单:无冻结,直接创建新 PENDING 订单
// 创建新的 PENDING 订单
const restoreOrderNo = this.generateOrderNo();
@ -797,14 +793,22 @@ export class C2cService {
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.sellerKavaAddress,
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);
}
}
}
}
}