// Package cron 提供定时任务:邮件重试、过期扫描、回收、审计归档。 // M0 注册任务框架;M3 接入邮件失败重试;M4 接入生命周期(到期锁定/回收) // 与审计归档;单实例部署说明见 PLAN §11。 package cron import ( "context" "log/slog" "time" "github.com/robfig/cron/v3" "ws_usernode/internal/config" "ws_usernode/internal/mail" "ws_usernode/internal/service" ) // Jobs 聚合定时任务依赖与注册。 type Jobs struct { cfg *config.Config mailer mail.Retryable lifecycle *service.LifecycleService audit *service.AuditService settings *service.SettingService log *slog.Logger } // New 创建定时任务集合。 // mailer 为邮件队列(失败重试,M3);lifecycle 为账号生命周期(M4); // audit/settings 用于每日审计归档(M4)。 func New(cfg *config.Config, mailer mail.Retryable, lifecycle *service.LifecycleService, audit *service.AuditService, settings *service.SettingService, log *slog.Logger) *Jobs { return &Jobs{cfg: cfg, mailer: mailer, lifecycle: lifecycle, audit: audit, settings: settings, log: log} } // Register 将任务注册到 cron。 func (j *Jobs) Register(c *cron.Cron) { // 邮件失败重试(M3):每 5 分钟重试失败邮件(超出 mail.MaxRetries 停止) c.AddFunc("@every 5m", func() { if err := j.mailer.Retry(context.Background(), mail.MaxRetries); err != nil { j.log.Error("cron: mail retry failed", "err", err) } }) // 每日维护:过期扫描 → 回收 → 审计归档(M4) c.AddFunc("@daily", j.dailyMaintenance) // 到期扫描额外每 6 小时跑一次:避免错过当日到期账号 c.AddFunc("@every 6h", j.scanExpired) } // dailyMaintenance 每日一次:到期扫描、超期回收、审计归档。 func (j *Jobs) dailyMaintenance() { ctx := context.Background() j.scanExpired() if n, err := j.lifecycle.Recycle(ctx); err != nil { j.log.Error("cron: recycle failed", "err", err) } else if n > 0 { j.log.Info("cron: recycled accounts", "count", n) } j.archiveAudit() } // scanExpired 到期扫描:锁定系统账号 + 邮件通知。 func (j *Jobs) scanExpired() { ctx := context.Background() if n, err := j.lifecycle.ScanExpired(ctx); err != nil { j.log.Error("cron: expire scan failed", "err", err) } else if n > 0 { j.log.Info("cron: expired accounts locked", "count", n) } } // archiveAudit 审计归档:将超过保留期的记录归档后清理(archive_dir 未配置时跳过)。 func (j *Jobs) archiveAudit() { ctx := context.Background() retention := j.settings.AuditRetention(ctx) before := time.Now().Add(-retention) if n, err := j.audit.Archive(ctx, j.cfg.Audit.ArchiveDir, before); err != nil { j.log.Error("cron: audit archive failed", "err", err) } else if n > 0 { j.log.Info("cron: audit archived", "count", n, "before", before.Format(time.RFC3339)) } }