From 97851c259552a29b8a38100d2e89c0251d202971 Mon Sep 17 00:00:00 2001 From: huang Date: Mon, 11 May 2026 10:22:48 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E4=BB=A3=E7=90=86=E4=B8=BB?= =?UTF-8?q?=E9=92=B1=E5=8C=85=E4=BD=99=E9=A2=9D=E5=BF=AB=E7=85=A7=E5=8F=A3?= =?UTF-8?q?=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 代理主钱包扣款流水必须记录扣款前后的真实余额,避免资金概况与流水展示出现双重扣减错觉。 Constraint: 项目禁止自动化测试,使用构建和数据库对账验证。 Rejected: 在资金汇总接口按历史充值总量或流水末条余额展示 | 资金概况口径应来自主钱包剩余余额。 Confidence: high Scope-risk: narrow Directive: 修改代理钱包扣款链路时必须同时维护余额字段与流水快照一致性。 Tested: go build ./...;migrate up 已执行到 141;只读 SQL 对账确认主钱包余额、成功流水累计、最新流水余额一致且 mismatch_tx_count=0。 Not-tested: 未新增自动化测试(项目规范禁止)。 --- internal/service/order/service.go | 37 +++++------- ...n_wallet_deduct_balance_snapshots.down.sql | 4 ++ ...ain_wallet_deduct_balance_snapshots.up.sql | 59 +++++++++++++++++++ 3 files changed, 79 insertions(+), 21 deletions(-) create mode 100644 migrations/000141_repair_agent_main_wallet_deduct_balance_snapshots.down.sql create mode 100644 migrations/000141_repair_agent_main_wallet_deduct_balance_snapshots.up.sql diff --git a/internal/service/order/service.go b/internal/service/order/service.go index 26f5c96..87e77b6 100644 --- a/internal/service/order/service.go +++ b/internal/service/order/service.go @@ -987,14 +987,17 @@ func (s *Service) getCostPrice(ctx context.Context, shopID uint, packageID uint) // createWalletTransaction 创建钱包流水记录 // ctx: 上下文 // tx: 事务对象 -// walletID: 钱包ID +// wallet: 扣款前的钱包快照 // orderID: 订单ID // amount: 扣款金额(正数) // purchaseRole: 订单角色 // relatedShopID: 关联店铺ID(代购场景填充下级店铺ID) -func (s *Service) createWalletTransaction(ctx context.Context, tx *gorm.DB, walletID uint, orderID uint, amount int64, purchaseRole string, relatedShopID *uint) error { +func (s *Service) createWalletTransaction(ctx context.Context, tx *gorm.DB, wallet *model.AgentWallet, orderID uint, amount int64, purchaseRole string, relatedShopID *uint) error { var subtype *string remark := "购买套餐" + if purchaseRole == "" { + purchaseRole = model.PurchaseRoleSelfPurchase + } // 根据订单角色确定交易子类型和备注 switch purchaseRole { @@ -1021,36 +1024,24 @@ func (s *Service) createWalletTransaction(ctx context.Context, tx *gorm.DB, wall // 创建钱包流水记录 transaction := &model.AgentWalletTransaction{ - AgentWalletID: walletID, - ShopID: 0, // 将在下面从钱包记录获取 + AgentWalletID: wallet.ID, + ShopID: wallet.ShopID, UserID: userID, TransactionType: constants.AgentTransactionTypeDeduct, TransactionSubtype: subtype, Amount: -amount, // 扣款为负数 - BalanceBefore: 0, // 将在下面填充 - BalanceAfter: 0, // 将在下面填充 + BalanceBefore: wallet.Balance, + BalanceAfter: wallet.Balance - amount, Status: constants.TransactionStatusSuccess, ReferenceType: strPtr(constants.ReferenceTypeOrder), ReferenceID: &orderID, RelatedShopID: relatedShopID, Remark: &remark, Creator: userID, - ShopIDTag: 0, // 将在下面填充 - EnterpriseIDTag: nil, + ShopIDTag: wallet.ShopIDTag, + EnterpriseIDTag: wallet.EnterpriseIDTag, } - // 查询钱包记录,获取 shop_id 和余额信息 - var wallet model.AgentWallet - if err := tx.Where("id = ?", walletID).First(&wallet).Error; err != nil { - return errors.Wrap(errors.CodeDatabaseError, err, "查询钱包信息失败") - } - - transaction.ShopID = wallet.ShopID - transaction.ShopIDTag = wallet.ShopIDTag - transaction.EnterpriseIDTag = wallet.EnterpriseIDTag - transaction.BalanceBefore = wallet.Balance - transaction.BalanceAfter = wallet.Balance - amount - if err := tx.Create(transaction).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "创建钱包流水失败") } @@ -1122,7 +1113,7 @@ func (s *Service) createOrderWithWalletPayment(ctx context.Context, order *model if order.PurchaseRole == model.PurchaseRolePurchaseForSubordinate { relatedShopID = &buyerShopID } - if err := s.createWalletTransaction(ctx, tx, wallet.ID, order.ID, actualAmount, order.PurchaseRole, relatedShopID); err != nil { + if err := s.createWalletTransaction(ctx, tx, wallet, order.ID, actualAmount, order.PurchaseRole, relatedShopID); err != nil { return err } @@ -1602,6 +1593,10 @@ func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string, return errors.New(errors.CodeInsufficientBalance, "余额不足或并发冲突") } + if err := s.createWalletTransaction(ctx, tx, wallet, order.ID, order.TotalAmount, order.PurchaseRole, nil); err != nil { + return err + } + if err := s.createWalletPaymentRecord(tx, order, model.PaymentByWallet); err != nil { return err } diff --git a/migrations/000141_repair_agent_main_wallet_deduct_balance_snapshots.down.sql b/migrations/000141_repair_agent_main_wallet_deduct_balance_snapshots.down.sql new file mode 100644 index 0000000..1a09150 --- /dev/null +++ b/migrations/000141_repair_agent_main_wallet_deduct_balance_snapshots.down.sql @@ -0,0 +1,4 @@ +-- 回滚说明:本迁移是历史资金流水快照修复,不执行反向恢复。 +-- 反向恢复会重新制造余额不一致,破坏主钱包剩余余额口径。 + +SELECT 1; diff --git a/migrations/000141_repair_agent_main_wallet_deduct_balance_snapshots.up.sql b/migrations/000141_repair_agent_main_wallet_deduct_balance_snapshots.up.sql new file mode 100644 index 0000000..fb4cefb --- /dev/null +++ b/migrations/000141_repair_agent_main_wallet_deduct_balance_snapshots.up.sql @@ -0,0 +1,59 @@ +-- 修复代理主钱包扣款流水余额快照 +-- 历史扣款流水曾在扣款后重新读取钱包,导致 balance_before/balance_after 多扣一次。 +-- 以成功流水的 amount 顺序累计值为准,重算主钱包流水快照,并同步主钱包剩余余额。 + +WITH ordered_transactions AS ( + SELECT + t.id, + SUM(t.amount) OVER ( + PARTITION BY t.agent_wallet_id + ORDER BY t.created_at, t.id + ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW + ) AS repaired_balance_after, + SUM(t.amount) OVER ( + PARTITION BY t.agent_wallet_id + ORDER BY t.created_at, t.id + ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW + ) - t.amount AS repaired_balance_before + FROM tb_agent_wallet_transaction t + INNER JOIN tb_agent_wallet w ON w.id = t.agent_wallet_id + WHERE w.wallet_type = 'main' + AND w.deleted_at IS NULL + AND t.deleted_at IS NULL + AND t.status = 1 +), +updated_transactions AS ( + UPDATE tb_agent_wallet_transaction t + SET + balance_before = ordered_transactions.repaired_balance_before, + balance_after = ordered_transactions.repaired_balance_after, + updated_at = NOW() + FROM ordered_transactions + WHERE t.id = ordered_transactions.id + AND ( + t.balance_before <> ordered_transactions.repaired_balance_before + OR t.balance_after <> ordered_transactions.repaired_balance_after + ) + RETURNING t.agent_wallet_id +), +wallet_balances AS ( + SELECT + t.agent_wallet_id, + SUM(t.amount) AS repaired_balance + FROM tb_agent_wallet_transaction t + INNER JOIN tb_agent_wallet w ON w.id = t.agent_wallet_id + WHERE w.wallet_type = 'main' + AND w.deleted_at IS NULL + AND t.deleted_at IS NULL + AND t.status = 1 + GROUP BY t.agent_wallet_id +) +UPDATE tb_agent_wallet w +SET + balance = wallet_balances.repaired_balance, + updated_at = NOW() +FROM wallet_balances +WHERE w.id = wallet_balances.agent_wallet_id + AND w.wallet_type = 'main' + AND w.deleted_at IS NULL + AND w.balance <> wallet_balances.repaired_balance;