让迁移套餐恢复月流量重置调度
缺少 next_reset_at 时,轮询根据已有激活时间或到期时间与套餐天数推算下一重置点;已有值通过查询条件和条件更新双重保护,不会被覆盖。 Constraint: 兼容迁移套餐缺少 activated_at 与 next_reset_at 的历史数据 Rejected: 单次 SQL 人工回填 | 后续迁移数据仍可能再次遗漏 Confidence: high Scope-risk: narrow Directive: 保持 next_reset_at 非空记录不可覆盖 Not-tested: 按用户要求未运行测试
This commit is contained in:
@@ -113,6 +113,10 @@ func (s *ResetService) resetMonthlyUsageWithDB(ctx context.Context, db *gorm.DB)
|
||||
|
||||
var resetPackages []*model.PackageUsage
|
||||
err := db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := s.backfillMissingMonthlyResetAt(tx, now); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var packages []*model.PackageUsage
|
||||
err := tx.Where("data_reset_cycle = ?", constants.PackageDataResetMonthly).
|
||||
Where("next_reset_at <= ?", now).
|
||||
@@ -178,6 +182,66 @@ func (s *ResetService) resetMonthlyUsageWithDB(ctx context.Context, db *gorm.DB)
|
||||
return nil
|
||||
}
|
||||
|
||||
type monthlyResetBackfillCandidate struct {
|
||||
ID uint
|
||||
ActivatedAt *time.Time
|
||||
ExpiresAt *time.Time
|
||||
CalendarType string
|
||||
DurationMonths int
|
||||
DurationDays int
|
||||
}
|
||||
|
||||
// backfillMissingMonthlyResetAt 为迁移套餐补齐缺失的下次流量重置时间。
|
||||
func (s *ResetService) backfillMissingMonthlyResetAt(tx *gorm.DB, now time.Time) error {
|
||||
var candidates []monthlyResetBackfillCandidate
|
||||
if err := tx.Table("tb_package_usage AS usage").
|
||||
Select("usage.id, usage.activated_at, usage.expires_at, pkg.calendar_type, pkg.duration_months, pkg.duration_days").
|
||||
Joins("JOIN tb_package AS pkg ON pkg.id = usage.package_id AND pkg.deleted_at IS NULL").
|
||||
Where("usage.data_reset_cycle = ?", constants.PackageDataResetMonthly).
|
||||
Where("usage.next_reset_at IS NULL").
|
||||
Where("usage.status IN ?", []int{constants.PackageUsageStatusActive, constants.PackageUsageStatusDepleted}).
|
||||
Find(&candidates).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询待补齐重置时间的套餐失败")
|
||||
}
|
||||
|
||||
for _, candidate := range candidates {
|
||||
nextResetAt := calculateMissingMonthlyResetAt(candidate, now)
|
||||
if nextResetAt == nil {
|
||||
s.logger.Warn("缺少套餐计时信息,无法补齐月流量重置时间", zap.Uint("usage_id", candidate.ID))
|
||||
continue
|
||||
}
|
||||
|
||||
result := tx.Model(&model.PackageUsage{}).
|
||||
Where("id = ? AND next_reset_at IS NULL", candidate.ID).
|
||||
Update("next_reset_at", *nextResetAt)
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "补齐套餐月流量重置时间失败")
|
||||
}
|
||||
if result.RowsAffected > 0 {
|
||||
s.logger.Info("已补齐套餐月流量重置时间",
|
||||
zap.Uint("usage_id", candidate.ID),
|
||||
zap.Time("next_reset_at", *nextResetAt))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func calculateMissingMonthlyResetAt(candidate monthlyResetBackfillCandidate, now time.Time) *time.Time {
|
||||
activatedAt := now
|
||||
if candidate.ActivatedAt != nil && !candidate.ActivatedAt.IsZero() {
|
||||
activatedAt = *candidate.ActivatedAt
|
||||
} else if candidate.CalendarType == constants.PackageCalendarTypeByDay {
|
||||
validityDays := calculateByDayValidityDays(candidate.DurationMonths, candidate.DurationDays)
|
||||
if candidate.ExpiresAt == nil || candidate.ExpiresAt.IsZero() || validityDays <= 0 {
|
||||
return nil
|
||||
}
|
||||
activatedAt = candidate.ExpiresAt.AddDate(0, 0, -validityDays)
|
||||
}
|
||||
|
||||
return CalculateNextResetTime(constants.PackageDataResetMonthly, candidate.CalendarType, now, activatedAt)
|
||||
}
|
||||
|
||||
// ResetYearlyUsage 任务 11.6-11.7: 重置年流量
|
||||
func (s *ResetService) ResetYearlyUsage(ctx context.Context) error {
|
||||
return s.resetYearlyUsageWithDB(ctx, s.db)
|
||||
|
||||
Reference in New Issue
Block a user