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));
|
const sellerReceiveAmount = new Money(sellerGrossAmount.value.minus(tradeFee.value));
|
||||||
|
|
||||||
// 保存成交记录(包含销毁信息、手续费和来源标识)
|
// ========== 使用事务确保成交记录、订单状态、账户余额的原子性 ==========
|
||||||
// quantity 存储有效积分股(含销毁倍数),originalQuantity 存储原始卖出数量
|
// 修复 Bug:之前没有使用事务,导致成交记录创建成功但账户余额更新可能失败
|
||||||
await this.prisma.trade.create({
|
await this.prisma.$transaction(async (tx) => {
|
||||||
data: {
|
// 1. 保存成交记录
|
||||||
tradeNo: match.trade.tradeNo,
|
await tx.trade.create({
|
||||||
buyOrderId: match.buyOrder.id!,
|
data: {
|
||||||
sellOrderId: match.sellOrder.id!,
|
tradeNo: match.trade.tradeNo,
|
||||||
buyerSequence: match.buyOrder.accountSequence,
|
buyOrderId: match.buyOrder.id!,
|
||||||
sellerSequence: match.sellOrder.accountSequence,
|
sellOrderId: match.sellOrder.id!,
|
||||||
price: match.trade.price.value,
|
buyerSequence: match.buyOrder.accountSequence,
|
||||||
quantity: effectiveQuantity.value, // 有效积分股(倍数后)
|
sellerSequence: match.sellOrder.accountSequence,
|
||||||
originalQuantity: tradeQuantity.value, // 原始卖出数量
|
price: match.trade.price.value,
|
||||||
burnQuantity: burnQuantity.value,
|
quantity: effectiveQuantity.value,
|
||||||
effectiveQty: effectiveQuantity.value,
|
originalQuantity: tradeQuantity.value,
|
||||||
amount: sellerReceiveAmount.value,
|
burnQuantity: burnQuantity.value,
|
||||||
fee: tradeFee.value,
|
effectiveQty: effectiveQuantity.value,
|
||||||
buyerSource: match.buyOrder.source,
|
amount: sellerReceiveAmount.value,
|
||||||
sellerSource: match.sellOrder.source,
|
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 {
|
try {
|
||||||
await this.circulationPoolRepository.addSharesFromSell(
|
await this.circulationPoolRepository.addSharesFromSell(
|
||||||
tradeQuantity,
|
tradeQuantity,
|
||||||
|
|
@ -229,7 +338,7 @@ export class OrderService {
|
||||||
this.logger.warn(`Failed to add shares to circulation pool: ${error}`);
|
this.logger.warn(`Failed to add shares to circulation pool: ${error}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 10%手续费进入积分股池(greenPoints)
|
// 10%手续费进入积分股池(非关键操作,可以在事务外执行)
|
||||||
try {
|
try {
|
||||||
await this.sharePoolRepository.feeIn(
|
await this.sharePoolRepository.feeIn(
|
||||||
tradeFee,
|
tradeFee,
|
||||||
|
|
@ -246,24 +355,6 @@ export class OrderService {
|
||||||
this.logger.error(`Failed to add trade fee to share pool: ${error}`);
|
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(
|
this.logger.log(
|
||||||
`Trade executed: ${match.trade.tradeNo}, price=${match.trade.price.toFixed(8)}, ` +
|
`Trade executed: ${match.trade.tradeNo}, price=${match.trade.price.toFixed(8)}, ` +
|
||||||
`qty=${tradeQuantity.toFixed(8)}, burn=${burnQuantity.toFixed(8)}, ` +
|
`qty=${tradeQuantity.toFixed(8)}, burn=${burnQuantity.toFixed(8)}, ` +
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue