修复一些问题,主要是生效套餐
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m16s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m16s
This commit is contained in:
@@ -290,38 +290,34 @@ func (s *ActivationService) ActivateSpecificPackage(ctx context.Context, package
|
||||
return nil
|
||||
}
|
||||
|
||||
// 按 ExpiryBase 选择激活时间基准
|
||||
now := time.Now()
|
||||
var activatedAt time.Time
|
||||
if pkg.ExpiryBase == "from_purchase" {
|
||||
activatedAt = usage.CreatedAt
|
||||
} else {
|
||||
activatedAt = now
|
||||
}
|
||||
expiresAt := CalculateExpiryTime(pkg.CalendarType, activatedAt, pkg.DurationMonths, pkg.DurationDays)
|
||||
nextResetAt := CalculateNextResetTime(pkg.DataResetCycle, pkg.CalendarType, now, activatedAt)
|
||||
if currentUsage.MasterUsageID == nil {
|
||||
hasActive, err := s.hasActiveMainPackage(ctx, tx, carrierType, carrierID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if hasActive {
|
||||
s.logger.Info("已有生效主套餐,跳过指定套餐激活",
|
||||
zap.Uint("usage_id", currentUsage.ID),
|
||||
zap.String("carrier_type", carrierType),
|
||||
zap.Uint("carrier_id", carrierID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 更新套餐状态
|
||||
updates := map[string]interface{}{
|
||||
"status": constants.PackageUsageStatusActive,
|
||||
"activated_at": activatedAt,
|
||||
"expires_at": expiresAt,
|
||||
}
|
||||
if nextResetAt != nil {
|
||||
updates["next_reset_at"] = *nextResetAt
|
||||
}
|
||||
if err := tx.Model(&usage).Updates(updates).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "激活套餐失败")
|
||||
canActivate, err := s.canActivatePendingUsage(ctx, tx, ¤tUsage, carrierType, carrierID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !canActivate {
|
||||
s.logger.Info("指定套餐暂不满足激活条件",
|
||||
zap.Uint("usage_id", currentUsage.ID),
|
||||
zap.Bool("pending_realname_activation", currentUsage.PendingRealnameActivation))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 同步载体状态
|
||||
s.syncCarrierStatusActivated(ctx, tx, &usage, carrierType, carrierID)
|
||||
|
||||
s.logger.Info("指定套餐已激活",
|
||||
zap.Uint("usage_id", usage.ID),
|
||||
zap.Uint("package_id", usage.PackageID),
|
||||
zap.String("carrier_type", carrierType),
|
||||
zap.Uint("carrier_id", carrierID))
|
||||
if err := s.activatePendingUsage(ctx, tx, ¤tUsage, &pkg, carrierType, carrierID, time.Now(), "指定套餐已激活"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 异步复机
|
||||
if s.resumeCallback != nil {
|
||||
@@ -339,6 +335,84 @@ func (s *ActivationService) ActivateSpecificPackage(ctx context.Context, package
|
||||
})
|
||||
}
|
||||
|
||||
// ActivateNextPendingMainPackage 按购买顺序激活载体的下一个待生效主套餐。
|
||||
// 返回 true 表示本次成功激活了一个主套餐;队首套餐未满足实名条件时返回 false。
|
||||
func (s *ActivationService) ActivateNextPendingMainPackage(ctx context.Context, carrierType string, carrierID uint) (bool, error) {
|
||||
if carrierType != constants.AssetTypeIotCard && carrierType != constants.AssetTypeDevice {
|
||||
return false, errors.New(errors.CodeInvalidParam, "无效的载体类型")
|
||||
}
|
||||
if carrierID == 0 {
|
||||
return false, errors.New(errors.CodeInvalidParam, "无效的载体ID")
|
||||
}
|
||||
|
||||
lockKey := constants.RedisPackageActivationLockKey(carrierType, carrierID)
|
||||
lockValue := time.Now().String()
|
||||
locked, err := s.redis.SetNX(ctx, lockKey, lockValue, 30*time.Second).Result()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(errors.CodeRedisError, err, "获取分布式锁失败")
|
||||
}
|
||||
if !locked {
|
||||
s.logger.Warn("套餐激活正在进行中,跳过",
|
||||
zap.String("carrier_type", carrierType),
|
||||
zap.Uint("carrier_id", carrierID))
|
||||
return false, errors.New(errors.CodePackageActivationConflict)
|
||||
}
|
||||
defer s.redis.Del(ctx, lockKey)
|
||||
|
||||
activated := false
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
hasActive, err := s.hasActiveMainPackage(ctx, tx, carrierType, carrierID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if hasActive {
|
||||
return nil
|
||||
}
|
||||
|
||||
nextMain, err := s.getNextPendingMainPackage(ctx, tx, carrierType, carrierID)
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
canActivate, err := s.canActivatePendingUsage(ctx, tx, nextMain, carrierType, carrierID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !canActivate {
|
||||
s.logger.Info("队首待生效套餐暂不满足激活条件",
|
||||
zap.Uint("usage_id", nextMain.ID),
|
||||
zap.String("carrier_type", carrierType),
|
||||
zap.Uint("carrier_id", carrierID),
|
||||
zap.Bool("pending_realname_activation", nextMain.PendingRealnameActivation))
|
||||
return nil
|
||||
}
|
||||
|
||||
var pkg model.Package
|
||||
if err := tx.First(&pkg, nextMain.PackageID).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询套餐信息失败")
|
||||
}
|
||||
|
||||
if err := s.activatePendingUsage(ctx, tx, nextMain, &pkg, carrierType, carrierID, time.Now(), "队首待生效套餐已激活"); err != nil {
|
||||
return err
|
||||
}
|
||||
activated = true
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return activated, nil
|
||||
}
|
||||
|
||||
// HasActiveMainPackage 查询载体当前是否存在生效中的主套餐。
|
||||
func (s *ActivationService) HasActiveMainPackage(ctx context.Context, carrierType string, carrierID uint) (bool, error) {
|
||||
return s.hasActiveMainPackage(ctx, s.db.WithContext(ctx), carrierType, carrierID)
|
||||
}
|
||||
|
||||
// invalidateAddons 任务 9.7: 加油包级联失效
|
||||
func (s *ActivationService) invalidateAddons(ctx context.Context, tx *gorm.DB, masterUsageID uint) error {
|
||||
var addons []*model.PackageUsage
|
||||
@@ -374,13 +448,7 @@ func (s *ActivationService) invalidateAddons(ctx context.Context, tx *gorm.DB, m
|
||||
// activateNextMainPackage 任务 9.6: 激活下一个待生效主套餐
|
||||
func (s *ActivationService) activateNextMainPackage(ctx context.Context, tx *gorm.DB, carrierType string, carrierID uint, now time.Time) error {
|
||||
// 查询下一个待生效主套餐
|
||||
var nextMain model.PackageUsage
|
||||
err := tx.Where("status = ?", constants.PackageUsageStatusPending).
|
||||
Where("master_usage_id IS NULL").
|
||||
Where(carrierType+"_id = ?", carrierID).
|
||||
Order("priority ASC").
|
||||
First(&nextMain).Error
|
||||
|
||||
nextMain, err := s.getNextPendingMainPackage(ctx, tx, carrierType, carrierID)
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
s.logger.Info("没有待生效的主套餐",
|
||||
zap.String("carrier_type", carrierType),
|
||||
@@ -388,7 +456,20 @@ func (s *ActivationService) activateNextMainPackage(ctx context.Context, tx *gor
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询下一个待生效主套餐失败")
|
||||
return err
|
||||
}
|
||||
|
||||
canActivate, err := s.canActivatePendingUsage(ctx, tx, nextMain, carrierType, carrierID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !canActivate {
|
||||
s.logger.Info("下一个待生效主套餐暂不满足激活条件",
|
||||
zap.Uint("usage_id", nextMain.ID),
|
||||
zap.String("carrier_type", carrierType),
|
||||
zap.Uint("carrier_id", carrierID),
|
||||
zap.Bool("pending_realname_activation", nextMain.PendingRealnameActivation))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 查询套餐信息
|
||||
@@ -397,11 +478,81 @@ func (s *ActivationService) activateNextMainPackage(ctx context.Context, tx *gor
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询套餐信息失败")
|
||||
}
|
||||
|
||||
// 激活套餐
|
||||
// 按 ExpiryBase 选择激活计时基准:from_purchase-购买时起算用 CreatedAt,否则用当前时刻
|
||||
return s.activatePendingUsage(ctx, tx, nextMain, &pkg, carrierType, carrierID, now, "排队主套餐已激活")
|
||||
}
|
||||
|
||||
func (s *ActivationService) getNextPendingMainPackage(ctx context.Context, tx *gorm.DB, carrierType string, carrierID uint) (*model.PackageUsage, error) {
|
||||
var nextMain model.PackageUsage
|
||||
err := tx.WithContext(ctx).
|
||||
Where("status = ?", constants.PackageUsageStatusPending).
|
||||
Where("master_usage_id IS NULL").
|
||||
Where(carrierType+"_id = ?", carrierID).
|
||||
Order("priority ASC, created_at ASC").
|
||||
First(&nextMain).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, err
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询下一个待生效主套餐失败")
|
||||
}
|
||||
return &nextMain, nil
|
||||
}
|
||||
|
||||
func (s *ActivationService) hasActiveMainPackage(ctx context.Context, tx *gorm.DB, carrierType string, carrierID uint) (bool, error) {
|
||||
var count int64
|
||||
if err := tx.WithContext(ctx).
|
||||
Model(&model.PackageUsage{}).
|
||||
Where("status = ?", constants.PackageUsageStatusActive).
|
||||
Where("master_usage_id IS NULL").
|
||||
Where(carrierType+"_id = ?", carrierID).
|
||||
Count(&count).Error; err != nil {
|
||||
return false, errors.Wrap(errors.CodeDatabaseError, err, "检查生效中主套餐失败")
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (s *ActivationService) canActivatePendingUsage(ctx context.Context, tx *gorm.DB, usage *model.PackageUsage, carrierType string, carrierID uint) (bool, error) {
|
||||
if usage == nil {
|
||||
return false, nil
|
||||
}
|
||||
if !usage.PendingRealnameActivation {
|
||||
return true, nil
|
||||
}
|
||||
return s.isCarrierRealnamed(ctx, tx, carrierType, carrierID)
|
||||
}
|
||||
|
||||
func (s *ActivationService) isCarrierRealnamed(ctx context.Context, tx *gorm.DB, carrierType string, carrierID uint) (bool, error) {
|
||||
switch carrierType {
|
||||
case constants.AssetTypeIotCard:
|
||||
var card model.IotCard
|
||||
if err := tx.WithContext(ctx).
|
||||
Select("real_name_status").
|
||||
First(&card, carrierID).Error; err != nil {
|
||||
return false, errors.Wrap(errors.CodeDatabaseError, err, "查询卡实名状态失败")
|
||||
}
|
||||
return card.RealNameStatus == constants.RealNameStatusVerified, nil
|
||||
case constants.AssetTypeDevice:
|
||||
var count int64
|
||||
subQuery := tx.WithContext(ctx).
|
||||
Model(&model.DeviceSimBinding{}).
|
||||
Select("iot_card_id").
|
||||
Where("device_id = ? AND bind_status = ?", carrierID, constants.BindStatusBound)
|
||||
if err := tx.WithContext(ctx).
|
||||
Model(&model.IotCard{}).
|
||||
Where("id IN (?) AND real_name_status = ?", subQuery, constants.RealNameStatusVerified).
|
||||
Count(&count).Error; err != nil {
|
||||
return false, errors.Wrap(errors.CodeDatabaseError, err, "查询设备实名状态失败")
|
||||
}
|
||||
return count > 0, nil
|
||||
default:
|
||||
return false, errors.New(errors.CodeInvalidParam, "无效的载体类型")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ActivationService) activatePendingUsage(ctx context.Context, tx *gorm.DB, usage *model.PackageUsage, pkg *model.Package, carrierType string, carrierID uint, now time.Time, logMessage string) error {
|
||||
var activatedAt time.Time
|
||||
if pkg.ExpiryBase == "from_purchase" {
|
||||
activatedAt = nextMain.CreatedAt
|
||||
activatedAt = usage.CreatedAt
|
||||
} else {
|
||||
activatedAt = now
|
||||
}
|
||||
@@ -412,23 +563,24 @@ func (s *ActivationService) activateNextMainPackage(ctx context.Context, tx *gor
|
||||
|
||||
// 更新套餐使用记录
|
||||
updates := map[string]interface{}{
|
||||
"status": constants.PackageUsageStatusActive,
|
||||
"activated_at": activatedAt,
|
||||
"expires_at": expiresAt,
|
||||
"status": constants.PackageUsageStatusActive,
|
||||
"pending_realname_activation": false,
|
||||
"activated_at": activatedAt,
|
||||
"expires_at": expiresAt,
|
||||
}
|
||||
if nextResetAt != nil {
|
||||
updates["next_reset_at"] = *nextResetAt
|
||||
}
|
||||
|
||||
if err := tx.Model(&nextMain).Updates(updates).Error; err != nil {
|
||||
if err := tx.Model(usage).Updates(updates).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "激活排队主套餐失败")
|
||||
}
|
||||
|
||||
s.syncCarrierStatusActivated(ctx, tx, &nextMain, carrierType, carrierID)
|
||||
s.syncCarrierStatusActivated(ctx, tx, usage, carrierType, carrierID)
|
||||
|
||||
s.logger.Info("排队主套餐已激活",
|
||||
zap.Uint("usage_id", nextMain.ID),
|
||||
zap.Uint("package_id", nextMain.PackageID),
|
||||
s.logger.Info(logMessage,
|
||||
zap.Uint("usage_id", usage.ID),
|
||||
zap.Uint("package_id", usage.PackageID),
|
||||
zap.Time("activated_at", activatedAt),
|
||||
zap.Time("expires_at", expiresAt))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user