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:
hailin 2026-01-28 17:20:31 -08:00
parent 1974c43eba
commit 94f9e7d5b5
1 changed files with 131 additions and 40 deletions

View File

@ -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)}, ` +