fix(trading-service): 使用事务确保成交时账户余额更新的原子性
## 问题描述 用户 D25122700015 的卖单成交后,订单状态变为 FILLED,成交记录存在, 但 frozenShares 未释放,cashBalance 未增加,交易流水缺少 SELL 记录。 ## 根本原因 `tryMatch` 方法中的数据库操作没有使用事务: 1. trade.create - 单独提交 2. orderRepository.save - 单独提交 3. accountRepository.save(buyerAccount) - 单独提交 4. accountRepository.save(sellerAccount) - 可能因前面异常而跳过 如果步骤 1-3 成功但步骤 4 失败,会导致: - 成交记录存在 ✓ - 订单状态 FILLED ✓ - 买方账户正常 ✓ - 卖方账户未更新 ✗ ## 修复方案 使用 Prisma 事务包装所有关键操作,确保原子性: 1. 创建成交记录 2. 更新买单状态 3. 更新卖单状态(含销毁信息) 4. 更新买方账户(扣除冻结现金,增加积分股) 5. 更新卖方账户(扣除冻结积分股,增加现金) 6. 记录交易流水 任何一步失败,整个事务回滚。 ## 受影响用户 - D25122700015: 订单 OMKXYTXS6KKC3A6 - 成交记录: TMKXYTXXH8CYQZ7 - 需要手动修复现有数据 ## 回滚方法 git revert <此commit> Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
1974c43eba
commit
94f9e7d5b5
|
|
@ -196,28 +196,137 @@ export class OrderService {
|
|||
// 卖方实际收到:总收益 - 手续费
|
||||
const sellerReceiveAmount = new Money(sellerGrossAmount.value.minus(tradeFee.value));
|
||||
|
||||
// 保存成交记录(包含销毁信息、手续费和来源标识)
|
||||
// quantity 存储有效积分股(含销毁倍数),originalQuantity 存储原始卖出数量
|
||||
await this.prisma.trade.create({
|
||||
data: {
|
||||
tradeNo: match.trade.tradeNo,
|
||||
buyOrderId: match.buyOrder.id!,
|
||||
sellOrderId: match.sellOrder.id!,
|
||||
buyerSequence: match.buyOrder.accountSequence,
|
||||
sellerSequence: match.sellOrder.accountSequence,
|
||||
price: match.trade.price.value,
|
||||
quantity: effectiveQuantity.value, // 有效积分股(倍数后)
|
||||
originalQuantity: tradeQuantity.value, // 原始卖出数量
|
||||
burnQuantity: burnQuantity.value,
|
||||
effectiveQty: effectiveQuantity.value,
|
||||
amount: sellerReceiveAmount.value,
|
||||
fee: tradeFee.value,
|
||||
buyerSource: match.buyOrder.source,
|
||||
sellerSource: match.sellOrder.source,
|
||||
},
|
||||
});
|
||||
// ========== 使用事务确保成交记录、订单状态、账户余额的原子性 ==========
|
||||
// 修复 Bug:之前没有使用事务,导致成交记录创建成功但账户余额更新可能失败
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
// 1. 保存成交记录
|
||||
await tx.trade.create({
|
||||
data: {
|
||||
tradeNo: match.trade.tradeNo,
|
||||
buyOrderId: match.buyOrder.id!,
|
||||
sellOrderId: match.sellOrder.id!,
|
||||
buyerSequence: match.buyOrder.accountSequence,
|
||||
sellerSequence: match.sellOrder.accountSequence,
|
||||
price: match.trade.price.value,
|
||||
quantity: effectiveQuantity.value,
|
||||
originalQuantity: tradeQuantity.value,
|
||||
burnQuantity: burnQuantity.value,
|
||||
effectiveQty: effectiveQuantity.value,
|
||||
amount: sellerReceiveAmount.value,
|
||||
fee: tradeFee.value,
|
||||
buyerSource: match.buyOrder.source,
|
||||
sellerSource: match.sellOrder.source,
|
||||
},
|
||||
});
|
||||
|
||||
// 卖出的积分股进入流通池
|
||||
// 2. 更新买单状态
|
||||
await tx.order.update({
|
||||
where: { id: match.buyOrder.id! },
|
||||
data: {
|
||||
status: match.buyOrder.status,
|
||||
filledQuantity: match.buyOrder.filledQuantity.value,
|
||||
remainingQuantity: match.buyOrder.remainingQuantity.value,
|
||||
averagePrice: match.buyOrder.averagePrice.value,
|
||||
totalAmount: match.buyOrder.totalAmount.value,
|
||||
completedAt: match.buyOrder.completedAt,
|
||||
},
|
||||
});
|
||||
|
||||
// 3. 更新卖单状态(含销毁信息)
|
||||
await tx.order.update({
|
||||
where: { id: match.sellOrder.id! },
|
||||
data: {
|
||||
status: match.sellOrder.status,
|
||||
filledQuantity: match.sellOrder.filledQuantity.value,
|
||||
remainingQuantity: match.sellOrder.remainingQuantity.value,
|
||||
averagePrice: match.sellOrder.averagePrice.value,
|
||||
totalAmount: match.sellOrder.totalAmount.value,
|
||||
burnQuantity: burnQuantity.value,
|
||||
burnMultiplier: burnQuantity.isZero()
|
||||
? 0
|
||||
: burnQuantity.value.dividedBy(match.sellOrder.filledQuantity.value),
|
||||
effectiveQuantity: effectiveQuantity.value,
|
||||
completedAt: match.sellOrder.completedAt,
|
||||
},
|
||||
});
|
||||
|
||||
// 4. 更新买方账户(扣除冻结现金,增加积分股)
|
||||
const buyerAccountRecord = await tx.tradingAccount.findUnique({
|
||||
where: { accountSequence: match.buyOrder.accountSequence },
|
||||
});
|
||||
if (buyerAccountRecord) {
|
||||
const newFrozenCash = Number(buyerAccountRecord.frozenCash) - buyerPayAmount.value.toNumber();
|
||||
const newCashBalance = Number(buyerAccountRecord.cashBalance) - buyerPayAmount.value.toNumber();
|
||||
const newShareBalance = Number(buyerAccountRecord.shareBalance) + tradeQuantity.value.toNumber();
|
||||
const newTotalBought = Number(buyerAccountRecord.totalBought) + tradeQuantity.value.toNumber();
|
||||
|
||||
await tx.tradingAccount.update({
|
||||
where: { accountSequence: match.buyOrder.accountSequence },
|
||||
data: {
|
||||
frozenCash: newFrozenCash,
|
||||
cashBalance: newCashBalance,
|
||||
shareBalance: newShareBalance,
|
||||
totalBought: newTotalBought,
|
||||
},
|
||||
});
|
||||
|
||||
// 记录买方交易流水
|
||||
await tx.tradingTransaction.create({
|
||||
data: {
|
||||
accountSequence: match.buyOrder.accountSequence,
|
||||
type: 'BUY',
|
||||
assetType: 'SHARE',
|
||||
amount: tradeQuantity.value,
|
||||
balanceBefore: buyerAccountRecord.shareBalance,
|
||||
balanceAfter: newShareBalance,
|
||||
referenceId: match.trade.tradeNo,
|
||||
referenceType: 'TRADE',
|
||||
description: '买入成交',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 5. 更新卖方账户(扣除冻结积分股,增加现金)
|
||||
const sellerAccountRecord = await tx.tradingAccount.findUnique({
|
||||
where: { accountSequence: match.sellOrder.accountSequence },
|
||||
});
|
||||
if (sellerAccountRecord) {
|
||||
const newFrozenShares = Number(sellerAccountRecord.frozenShares) - tradeQuantity.value.toNumber();
|
||||
const newShareBalance = Number(sellerAccountRecord.shareBalance) - tradeQuantity.value.toNumber();
|
||||
const newCashBalance = Number(sellerAccountRecord.cashBalance) + sellerReceiveAmount.value.toNumber();
|
||||
const newTotalSold = Number(sellerAccountRecord.totalSold) + tradeQuantity.value.toNumber();
|
||||
|
||||
await tx.tradingAccount.update({
|
||||
where: { accountSequence: match.sellOrder.accountSequence },
|
||||
data: {
|
||||
frozenShares: newFrozenShares,
|
||||
shareBalance: newShareBalance,
|
||||
cashBalance: newCashBalance,
|
||||
totalSold: newTotalSold,
|
||||
},
|
||||
});
|
||||
|
||||
// 记录卖方交易流水
|
||||
await tx.tradingTransaction.create({
|
||||
data: {
|
||||
accountSequence: match.sellOrder.accountSequence,
|
||||
type: 'SELL',
|
||||
assetType: 'SHARE',
|
||||
amount: tradeQuantity.value,
|
||||
balanceBefore: sellerAccountRecord.shareBalance,
|
||||
balanceAfter: newShareBalance,
|
||||
referenceId: match.trade.tradeNo,
|
||||
referenceType: 'TRADE',
|
||||
description: '卖出成交',
|
||||
},
|
||||
});
|
||||
}
|
||||
}, {
|
||||
timeout: 30000, // 30秒超时
|
||||
});
|
||||
// ========== 事务结束 ==========
|
||||
|
||||
// 卖出的积分股进入流通池(非关键操作,可以在事务外执行)
|
||||
try {
|
||||
await this.circulationPoolRepository.addSharesFromSell(
|
||||
tradeQuantity,
|
||||
|
|
@ -229,7 +338,7 @@ export class OrderService {
|
|||
this.logger.warn(`Failed to add shares to circulation pool: ${error}`);
|
||||
}
|
||||
|
||||
// 10%手续费进入积分股池(greenPoints)
|
||||
// 10%手续费进入积分股池(非关键操作,可以在事务外执行)
|
||||
try {
|
||||
await this.sharePoolRepository.feeIn(
|
||||
tradeFee,
|
||||
|
|
@ -246,24 +355,6 @@ export class OrderService {
|
|||
this.logger.error(`Failed to add trade fee to share pool: ${error}`);
|
||||
}
|
||||
|
||||
// 更新订单(包含销毁信息)
|
||||
await this.orderRepository.save(match.buyOrder);
|
||||
await this.orderRepository.saveWithBurnInfo(match.sellOrder, burnQuantity, effectiveQuantity);
|
||||
|
||||
// 更新买方账户(支付原始交易额)
|
||||
const buyerAccount = await this.accountRepository.findByAccountSequence(match.buyOrder.accountSequence);
|
||||
if (buyerAccount) {
|
||||
buyerAccount.executeBuy(tradeQuantity, buyerPayAmount, match.trade.tradeNo);
|
||||
await this.accountRepository.save(buyerAccount);
|
||||
}
|
||||
|
||||
// 更新卖方账户(收到扣除手续费后的金额)
|
||||
const sellerAccount = await this.accountRepository.findByAccountSequence(match.sellOrder.accountSequence);
|
||||
if (sellerAccount) {
|
||||
sellerAccount.executeSell(tradeQuantity, sellerReceiveAmount, match.trade.tradeNo);
|
||||
await this.accountRepository.save(sellerAccount);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Trade executed: ${match.trade.tradeNo}, price=${match.trade.price.toFixed(8)}, ` +
|
||||
`qty=${tradeQuantity.toFixed(8)}, burn=${burnQuantity.toFixed(8)}, ` +
|
||||
|
|
|
|||
Loading…
Reference in New Issue