Compare commits

2 Commits

Author SHA1 Message Date
b5b7f9a41e 迁移
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m53s
2026-06-29 12:29:31 +09:00
b930662817 从乐观锁变成悲观锁 2026-06-29 12:29:21 +09:00
5 changed files with 47 additions and 29 deletions

View File

@@ -1120,26 +1120,31 @@ func (s *Service) createOrderWithWalletPayment(ctx context.Context, order *model
}
actualAmount := *order.ActualPaidAmount
// 1. 事务外:检查钱包余额(快速失败)
wallet, err := s.agentWalletStore.GetMainWallet(ctx, operatorShopID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeWalletNotFound, "钱包不存在")
// 事务内:加锁读取钱包 + 余额检查 + 创建订单 + 扣款 + 创建流水 + 激活套餐
// 使用 FOR UPDATE 悲观锁替代乐观锁,避免并发请求因 version 过期导致误报余额不足
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
// 1. 加锁读取钱包,并发请求排队等待,不会冲突
var wallet model.AgentWallet
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("shop_id = ? AND wallet_type = ?", operatorShopID, constants.AgentWalletTypeMain).
First(&wallet).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeWalletNotFound, "钱包不存在")
}
return errors.Wrap(errors.CodeDatabaseError, err, "查询钱包失败")
}
return errors.Wrap(errors.CodeDatabaseError, err, "查询钱包失败")
}
if wallet.Balance < actualAmount {
return errors.New(errors.CodeInsufficientBalance, "余额不足")
}
// 2. 事务内:创建订单 + 扣款 + 创建流水 + 激活套餐
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
// 2.1 创建订单
// 2. 余额检查(加锁后读到的是最新余额,结果准确)
if wallet.Balance < actualAmount {
return errors.New(errors.CodeInsufficientBalance, "余额不足")
}
// 3. 创建订单
if err := tx.Create(order).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建订单失败")
}
// 2.2 创建订单明细
// 4. 创建订单明细
for _, item := range items {
item.OrderID = order.ID
}
@@ -1147,30 +1152,24 @@ func (s *Service) createOrderWithWalletPayment(ctx context.Context, order *model
return errors.Wrap(errors.CodeDatabaseError, err, "创建订单明细失败")
}
// 2.3 扣减钱包余额(乐观锁
result := tx.Model(&model.AgentWallet{}).
Where("id = ? AND balance >= ? AND version = ?", wallet.ID, actualAmount, wallet.Version).
Updates(map[string]any{
"balance": gorm.Expr("balance - ?", actualAmount),
"version": gorm.Expr("version + 1"),
})
if result.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, result.Error, "扣减钱包余额失败")
}
if result.RowsAffected == 0 {
return errors.New(errors.CodeInsufficientBalance, "余额不足或并发冲突")
// 5. 扣减钱包余额(FOR UPDATE 已保证串行,无需 version 条件
if err := tx.Model(&wallet).Updates(map[string]any{
"balance": gorm.Expr("balance - ?", actualAmount),
"version": gorm.Expr("version + 1"),
}).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "扣减钱包余额失败")
}
// 2.4 创建钱包流水
// 6. 创建钱包流水
var relatedShopID *uint
if order.PurchaseRole == model.PurchaseRolePurchaseForSubordinate {
relatedShopID = &buyerShopID
}
if err := s.createWalletTransaction(ctx, tx, wallet, order, actualAmount, order.PurchaseRole, relatedShopID); err != nil {
if err := s.createWalletTransaction(ctx, tx, &wallet, order, actualAmount, order.PurchaseRole, relatedShopID); err != nil {
return err
}
// 2.5 激活套餐
// 7. 激活套餐
if err := s.activatePackage(ctx, tx, order); err != nil {
return err
}

View File

@@ -0,0 +1,2 @@
ALTER TABLE tb_agent_recharge_record
DROP COLUMN IF EXISTS rejection_reason;

View File

@@ -0,0 +1,5 @@
-- 为代理预充值表新增驳回原因字段,支持管理员驳回待支付订单
ALTER TABLE tb_agent_recharge_record
ADD COLUMN IF NOT EXISTS rejection_reason VARCHAR(500) NULL;
COMMENT ON COLUMN tb_agent_recharge_record.rejection_reason IS '驳回原因,仅驳回时写入';

View File

@@ -0,0 +1,6 @@
-- 回滚将状态约束恢复为不包含驳回状态6
ALTER TABLE tb_agent_recharge_record
DROP CONSTRAINT IF EXISTS chk_agent_recharge_status;
ALTER TABLE tb_agent_recharge_record
ADD CONSTRAINT chk_agent_recharge_status CHECK (status IN (1, 2, 3, 4, 5));

View File

@@ -0,0 +1,6 @@
-- 更新代理预充值状态检查约束新增驳回状态6
ALTER TABLE tb_agent_recharge_record
DROP CONSTRAINT IF EXISTS chk_agent_recharge_status;
ALTER TABLE tb_agent_recharge_record
ADD CONSTRAINT chk_agent_recharge_status CHECK (status IN (1, 2, 3, 4, 5, 6));