避免套餐过期后排队权益永久失联
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m6s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m6s
线上保持现有纯 Asynq 架构,以公平孤儿扫描和提交后投递消除永久饥饿及事务可见性竞态。 Constraint: 线上保持现有纯 Asynq 架构,不引入 Outbox、迁移或新任务基础设施。 Rejected: 事务内投递或扩大扫描 LIMIT | 无法消除竞态和永久饥饿。 Confidence: high Scope-risk: narrow Directive: 后续分支整合时按目标分支的套餐接续架构独立处理,不混用本热修实现。 Tested: go build ./...(退出码 0);git diff --check;openspec validate fix-main-package-activation-starvation --strict。 Not-tested: 按用户要求未新增、修改或运行自动化测试;线上 SQL、查询计划和日志待部署后核验。
This commit is contained in:
@@ -228,14 +228,14 @@ func (s *ActivationService) ActivateQueuedPackage(ctx context.Context, carrierTy
|
||||
|
||||
// ActivateSpecificPackage 任务 4: 激活指定的套餐使用记录
|
||||
// 根据 PackageUsageID 精准激活目标套餐,而非重跑"查找过期包"流程
|
||||
func (s *ActivationService) ActivateSpecificPackage(ctx context.Context, packageUsageID uint) error {
|
||||
func (s *ActivationService) ActivateSpecificPackage(ctx context.Context, packageUsageID uint) (bool, error) {
|
||||
// 加载 PackageUsage
|
||||
var usage model.PackageUsage
|
||||
if err := s.db.WithContext(ctx).First(&usage, packageUsageID).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeNotFound, "套餐使用记录不存在")
|
||||
return false, errors.New(errors.CodeNotFound, "套餐使用记录不存在")
|
||||
}
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询套餐使用记录失败")
|
||||
return false, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐使用记录失败")
|
||||
}
|
||||
|
||||
// 幂等检查:非待生效状态直接返回
|
||||
@@ -243,7 +243,7 @@ func (s *ActivationService) ActivateSpecificPackage(ctx context.Context, package
|
||||
s.logger.Info("套餐无需激活(非待生效状态)",
|
||||
zap.Uint("usage_id", usage.ID),
|
||||
zap.Int("status", usage.Status))
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// 确定载体类型和 ID
|
||||
@@ -256,7 +256,7 @@ func (s *ActivationService) ActivateSpecificPackage(ctx context.Context, package
|
||||
carrierType = "device"
|
||||
carrierID = usage.DeviceID
|
||||
} else {
|
||||
return errors.New(errors.CodeInvalidParam, "套餐使用记录缺少载体信息")
|
||||
return false, errors.New(errors.CodeInvalidParam, "套餐使用记录缺少载体信息")
|
||||
}
|
||||
|
||||
// 获取分布式锁
|
||||
@@ -264,24 +264,25 @@ func (s *ActivationService) ActivateSpecificPackage(ctx context.Context, package
|
||||
lockValue := time.Now().String()
|
||||
locked, err := s.redis.SetNX(ctx, lockKey, lockValue, 30*time.Second).Result()
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.CodeRedisError, err, "获取分布式锁失败")
|
||||
return false, errors.Wrap(errors.CodeRedisError, err, "获取分布式锁失败")
|
||||
}
|
||||
if !locked {
|
||||
s.logger.Warn("套餐激活正在进行中,跳过",
|
||||
zap.String("carrier_type", carrierType),
|
||||
zap.Uint("carrier_id", carrierID))
|
||||
return nil
|
||||
return false, errors.New(errors.CodePackageActivationConflict)
|
||||
}
|
||||
defer s.redis.Del(ctx, lockKey)
|
||||
|
||||
// 加载关联 Package
|
||||
var pkg model.Package
|
||||
if err := s.db.WithContext(ctx).First(&pkg, usage.PackageID).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询套餐信息失败")
|
||||
return false, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐信息失败")
|
||||
}
|
||||
|
||||
// 事务内激活
|
||||
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
activated := false
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
// 再次幂等检查(防止锁释放期间被其他操作激活)
|
||||
var currentUsage model.PackageUsage
|
||||
if err := tx.First(¤tUsage, packageUsageID).Error; err != nil {
|
||||
@@ -319,6 +320,7 @@ func (s *ActivationService) ActivateSpecificPackage(ctx context.Context, package
|
||||
if err := s.activatePendingUsage(ctx, tx, ¤tUsage, &pkg, carrierType, carrierID, time.Now(), "指定套餐已激活"); err != nil {
|
||||
return err
|
||||
}
|
||||
activated = true
|
||||
|
||||
// 异步复机
|
||||
if s.resumeCallback != nil {
|
||||
@@ -334,6 +336,11 @@ func (s *ActivationService) ActivateSpecificPackage(ctx context.Context, package
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return activated, nil
|
||||
}
|
||||
|
||||
// ActivateNextPendingMainPackage 按购买顺序激活载体的下一个待生效主套餐。
|
||||
|
||||
Reference in New Issue
Block a user