From 94f9e7d5b5d2e5921ba451aaa517eb3aa794f4f4 Mon Sep 17 00:00:00 2001 From: hailin Date: Wed, 28 Jan 2026 17:20:31 -0800 Subject: [PATCH] =?UTF-8?q?fix(trading-service):=20=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E4=BA=8B=E5=8A=A1=E7=A1=AE=E4=BF=9D=E6=88=90=E4=BA=A4=E6=97=B6?= =?UTF-8?q?=E8=B4=A6=E6=88=B7=E4=BD=99=E9=A2=9D=E6=9B=B4=E6=96=B0=E7=9A=84?= =?UTF-8?q?=E5=8E=9F=E5=AD=90=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 问题描述 用户 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 --- .../src/application/services/order.service.ts | 171 ++++++++++++++---- 1 file changed, 131 insertions(+), 40 deletions(-) diff --git a/backend/services/trading-service/src/application/services/order.service.ts b/backend/services/trading-service/src/application/services/order.service.ts index 95ccb5f9..2967f7d9 100644 --- a/backend/services/trading-service/src/application/services/order.service.ts +++ b/backend/services/trading-service/src/application/services/order.service.ts @@ -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)}, ` +