修正代理主钱包余额快照口径
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m0s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m0s
代理主钱包扣款流水必须记录扣款前后的真实余额,避免资金概况与流水展示出现双重扣减错觉。 Constraint: 项目禁止自动化测试,使用构建和数据库对账验证。 Rejected: 在资金汇总接口按历史充值总量或流水末条余额展示 | 资金概况口径应来自主钱包剩余余额。 Confidence: high Scope-risk: narrow Directive: 修改代理钱包扣款链路时必须同时维护余额字段与流水快照一致性。 Tested: go build ./...;migrate up 已执行到 141;只读 SQL 对账确认主钱包余额、成功流水累计、最新流水余额一致且 mismatch_tx_count=0。 Not-tested: 未新增自动化测试(项目规范禁止)。
This commit is contained in:
@@ -987,14 +987,17 @@ func (s *Service) getCostPrice(ctx context.Context, shopID uint, packageID uint)
|
|||||||
// createWalletTransaction 创建钱包流水记录
|
// createWalletTransaction 创建钱包流水记录
|
||||||
// ctx: 上下文
|
// ctx: 上下文
|
||||||
// tx: 事务对象
|
// tx: 事务对象
|
||||||
// walletID: 钱包ID
|
// wallet: 扣款前的钱包快照
|
||||||
// orderID: 订单ID
|
// orderID: 订单ID
|
||||||
// amount: 扣款金额(正数)
|
// amount: 扣款金额(正数)
|
||||||
// purchaseRole: 订单角色
|
// purchaseRole: 订单角色
|
||||||
// relatedShopID: 关联店铺ID(代购场景填充下级店铺ID)
|
// 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
|
var subtype *string
|
||||||
remark := "购买套餐"
|
remark := "购买套餐"
|
||||||
|
if purchaseRole == "" {
|
||||||
|
purchaseRole = model.PurchaseRoleSelfPurchase
|
||||||
|
}
|
||||||
|
|
||||||
// 根据订单角色确定交易子类型和备注
|
// 根据订单角色确定交易子类型和备注
|
||||||
switch purchaseRole {
|
switch purchaseRole {
|
||||||
@@ -1021,36 +1024,24 @@ func (s *Service) createWalletTransaction(ctx context.Context, tx *gorm.DB, wall
|
|||||||
|
|
||||||
// 创建钱包流水记录
|
// 创建钱包流水记录
|
||||||
transaction := &model.AgentWalletTransaction{
|
transaction := &model.AgentWalletTransaction{
|
||||||
AgentWalletID: walletID,
|
AgentWalletID: wallet.ID,
|
||||||
ShopID: 0, // 将在下面从钱包记录获取
|
ShopID: wallet.ShopID,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
TransactionType: constants.AgentTransactionTypeDeduct,
|
TransactionType: constants.AgentTransactionTypeDeduct,
|
||||||
TransactionSubtype: subtype,
|
TransactionSubtype: subtype,
|
||||||
Amount: -amount, // 扣款为负数
|
Amount: -amount, // 扣款为负数
|
||||||
BalanceBefore: 0, // 将在下面填充
|
BalanceBefore: wallet.Balance,
|
||||||
BalanceAfter: 0, // 将在下面填充
|
BalanceAfter: wallet.Balance - amount,
|
||||||
Status: constants.TransactionStatusSuccess,
|
Status: constants.TransactionStatusSuccess,
|
||||||
ReferenceType: strPtr(constants.ReferenceTypeOrder),
|
ReferenceType: strPtr(constants.ReferenceTypeOrder),
|
||||||
ReferenceID: &orderID,
|
ReferenceID: &orderID,
|
||||||
RelatedShopID: relatedShopID,
|
RelatedShopID: relatedShopID,
|
||||||
Remark: &remark,
|
Remark: &remark,
|
||||||
Creator: userID,
|
Creator: userID,
|
||||||
ShopIDTag: 0, // 将在下面填充
|
ShopIDTag: wallet.ShopIDTag,
|
||||||
EnterpriseIDTag: nil,
|
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 {
|
if err := tx.Create(transaction).Error; err != nil {
|
||||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建钱包流水失败")
|
return errors.Wrap(errors.CodeDatabaseError, err, "创建钱包流水失败")
|
||||||
}
|
}
|
||||||
@@ -1122,7 +1113,7 @@ func (s *Service) createOrderWithWalletPayment(ctx context.Context, order *model
|
|||||||
if order.PurchaseRole == model.PurchaseRolePurchaseForSubordinate {
|
if order.PurchaseRole == model.PurchaseRolePurchaseForSubordinate {
|
||||||
relatedShopID = &buyerShopID
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1602,6 +1593,10 @@ func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string,
|
|||||||
return errors.New(errors.CodeInsufficientBalance, "余额不足或并发冲突")
|
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 {
|
if err := s.createWalletPaymentRecord(tx, order, model.PaymentByWallet); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- 回滚说明:本迁移是历史资金流水快照修复,不执行反向恢复。
|
||||||
|
-- 反向恢复会重新制造余额不一致,破坏主钱包剩余余额口径。
|
||||||
|
|
||||||
|
SELECT 1;
|
||||||
@@ -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;
|
||||||
Reference in New Issue
Block a user