package packagepkg import ( "context" "time" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/internal/store/postgres" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/redis/go-redis/v9" "go.uber.org/zap" "gorm.io/gorm" ) type ResetService struct { db *gorm.DB redis *redis.Client packageUsageStore *postgres.PackageUsageStore logger *zap.Logger resumeCallback ResumeCallback } func NewResetService( db *gorm.DB, redis *redis.Client, packageUsageStore *postgres.PackageUsageStore, logger *zap.Logger, ) *ResetService { return &ResetService{ db: db, redis: redis, packageUsageStore: packageUsageStore, logger: logger, } } // SetResumeCallback 设置复机回调,流量重置后对满足条件的卡触发自动复机 func (s *ResetService) SetResumeCallback(callback ResumeCallback) { s.resumeCallback = callback } // ResetDailyUsage 任务 11.2-11.3: 重置日流量 func (s *ResetService) ResetDailyUsage(ctx context.Context) error { return s.resetDailyUsageWithDB(ctx, s.db) } // resetDailyUsageWithDB 内部方法,支持传入 DB/TX func (s *ResetService) resetDailyUsageWithDB(ctx context.Context, db *gorm.DB) error { now := time.Now() var resetPackages []*model.PackageUsage err := db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { var packages []*model.PackageUsage err := tx.Where("data_reset_cycle = ?", constants.PackageDataResetDaily). Where("next_reset_at <= ?", now). Where("status IN ?", []int{constants.PackageUsageStatusActive, constants.PackageUsageStatusDepleted}). Find(&packages).Error if err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询待重置套餐失败") } if len(packages) == 0 { s.logger.Info("没有需要重置的日流量套餐") return nil } packageIDs := make([]uint, len(packages)) for i, pkg := range packages { packageIDs[i] = pkg.ID } nextReset := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, now.Location()) updates := map[string]interface{}{ "data_usage_mb": 0, "last_reset_at": now, "next_reset_at": nextReset, "status": constants.PackageUsageStatusActive, } if err := tx.Model(&model.PackageUsage{}). Where("id IN ?", packageIDs). Updates(updates).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "批量重置日流量失败") } s.logger.Info("日流量重置完成", zap.Int("count", len(packages)), zap.Time("next_reset_at", nextReset)) resetPackages = packages return nil }) if err != nil { return err } s.triggerResumeForPackages(ctx, resetPackages) return nil } // ResetMonthlyUsage 任务 11.4-11.5: 重置月流量 func (s *ResetService) ResetMonthlyUsage(ctx context.Context) error { return s.resetMonthlyUsageWithDB(ctx, s.db) } // resetMonthlyUsageWithDB 内部方法,支持传入 DB/TX func (s *ResetService) resetMonthlyUsageWithDB(ctx context.Context, db *gorm.DB) error { now := time.Now() var resetPackages []*model.PackageUsage err := db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { var packages []*model.PackageUsage err := tx.Where("data_reset_cycle = ?", constants.PackageDataResetMonthly). Where("next_reset_at <= ?", now). Where("status IN ?", []int{constants.PackageUsageStatusActive, constants.PackageUsageStatusDepleted}). Find(&packages).Error if err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询待重置套餐失败") } if len(packages) == 0 { s.logger.Info("没有需要重置的月流量套餐") return nil } for _, usage := range packages { var pkg model.Package if err := tx.First(&pkg, usage.PackageID).Error; err != nil { s.logger.Error("查询套餐信息失败", zap.Uint("usage_id", usage.ID), zap.Uint("package_id", usage.PackageID), zap.Error(err)) continue } activatedAt := now if usage.ActivatedAt != nil && !usage.ActivatedAt.IsZero() { activatedAt = *usage.ActivatedAt } nextResetAt := CalculateNextResetTime(constants.PackageDataResetMonthly, pkg.CalendarType, now, activatedAt) if nextResetAt == nil { s.logger.Warn("计算下次重置时间失败", zap.Uint("usage_id", usage.ID)) continue } updates := map[string]interface{}{ "data_usage_mb": 0, "last_reset_at": now, "next_reset_at": *nextResetAt, "status": constants.PackageUsageStatusActive, } if err := tx.Model(usage).Updates(updates).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "重置月流量失败") } s.logger.Info("月流量已重置", zap.Uint("usage_id", usage.ID), zap.String("calendar_type", pkg.CalendarType), zap.Time("next_reset_at", *nextResetAt)) } resetPackages = packages return nil }) if err != nil { return err } s.triggerResumeForPackages(ctx, resetPackages) return nil } // ResetYearlyUsage 任务 11.6-11.7: 重置年流量 func (s *ResetService) ResetYearlyUsage(ctx context.Context) error { return s.resetYearlyUsageWithDB(ctx, s.db) } // resetYearlyUsageWithDB 内部方法,支持传入 DB/TX func (s *ResetService) resetYearlyUsageWithDB(ctx context.Context, db *gorm.DB) error { now := time.Now() var resetPackages []*model.PackageUsage err := db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { var packages []*model.PackageUsage err := tx.Where("data_reset_cycle = ?", constants.PackageDataResetYearly). Where("next_reset_at <= ?", now). Where("status IN ?", []int{constants.PackageUsageStatusActive, constants.PackageUsageStatusDepleted}). Find(&packages).Error if err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询待重置套餐失败") } if len(packages) == 0 { s.logger.Info("没有需要重置的年流量套餐") return nil } packageIDs := make([]uint, len(packages)) for i, pkg := range packages { packageIDs[i] = pkg.ID } nextReset := time.Date(now.Year()+1, 1, 1, 0, 0, 0, 0, now.Location()) updates := map[string]interface{}{ "data_usage_mb": 0, "last_reset_at": now, "next_reset_at": nextReset, "status": constants.PackageUsageStatusActive, } if err := tx.Model(&model.PackageUsage{}). Where("id IN ?", packageIDs). Updates(updates).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "批量重置年流量失败") } s.logger.Info("年流量重置完成", zap.Int("count", len(packages)), zap.Time("next_reset_at", nextReset)) resetPackages = packages return nil }) if err != nil { return err } s.triggerResumeForPackages(ctx, resetPackages) return nil } // triggerResumeForPackages 对重置后的套餐,遍历其载体并触发自动复机 // 流量重置后,因流量耗尽而停机的卡满足复机条件,此处异步触发避免阻塞重置流程 func (s *ResetService) triggerResumeForPackages(ctx context.Context, packages []*model.PackageUsage) { if s.resumeCallback == nil || len(packages) == 0 { return } for _, pkg := range packages { var carrierType string var carrierID uint if pkg.DeviceID > 0 { carrierType = "device" carrierID = pkg.DeviceID } else if pkg.IotCardID > 0 { carrierType = "iot_card" carrierID = pkg.IotCardID } else { continue } go func(ct string, cid uint) { if err := s.resumeCallback.ResumeCardIfStopped(context.Background(), ct, cid); err != nil { s.logger.Warn("流量重置后自动复机失败", zap.String("carrier_type", ct), zap.Uint("carrier_id", cid), zap.Error(err)) } }(carrierType, carrierID) } }