From 8659dfc658429057beb8f773191cd9493badc1b5 Mon Sep 17 00:00:00 2001 From: break Date: Wed, 5 Aug 2026 14:33:16 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=A9=E8=BF=81=E7=A7=BB=E5=A5=97=E9=A4=90?= =?UTF-8?q?=E6=81=A2=E5=A4=8D=E6=9C=88=E6=B5=81=E9=87=8F=E9=87=8D=E7=BD=AE?= =?UTF-8?q?=E8=B0=83=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 缺少 next_reset_at 时,轮询根据已有激活时间或到期时间与套餐天数推算下一重置点;已有值通过查询条件和条件更新双重保护,不会被覆盖。 Constraint: 兼容迁移套餐缺少 activated_at 与 next_reset_at 的历史数据 Rejected: 单次 SQL 人工回填 | 后续迁移数据仍可能再次遗漏 Confidence: high Scope-risk: narrow Directive: 保持 next_reset_at 非空记录不可覆盖 Not-tested: 按用户要求未运行测试 --- internal/service/package/reset_service.go | 64 +++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/internal/service/package/reset_service.go b/internal/service/package/reset_service.go index f1eadeb..ee87a8d 100644 --- a/internal/service/package/reset_service.go +++ b/internal/service/package/reset_service.go @@ -141,6 +141,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). @@ -228,6 +232,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)