thread-master/apps/backend/internal/module/radar/usecase/sweep_schedule.go

103 lines
3.2 KiB
Go
Raw Normal View History

2026-08-03 05:52:02 +00:00
package usecase
import (
"context"
"fmt"
"time"
"apps/backend/internal/module/radar/domain"
)
// DailySweepHourUTC is the fixed daily schedule (spec §4.2UTC 22:00 =台北 06:00)。
const DailySweepHourUTC = 22
// SweepJobScheduler schedules one radar_sweep job per watch per UTC day.
// Implemented by a thin adapter over job usecase.Service (see worker / service_context).
type SweepJobScheduler interface {
ScheduleRadarSweep(ctx context.Context, ownerUID int64, watchID string, runAt int64) (jobID string, err error)
}
// SweepJobSchedulerFunc adapts a function to SweepJobScheduler.
type SweepJobSchedulerFunc func(ctx context.Context, ownerUID int64, watchID string, runAt int64) (jobID string, err error)
func (f SweepJobSchedulerFunc) ScheduleRadarSweep(ctx context.Context, ownerUID int64, watchID string, runAt int64) (string, error) {
return f(ctx, ownerUID, watchID, runAt)
}
// ScheduleDailySweeps 在已過當日 UTC 22:00 時,為每個 active watch 建一筆 radar_sweep Job。
//
// 呼叫端必須已持有 worker maintenance Redis lockvalue = workerID
// 本函式本身不做分散式鎖;未過 22:00 時回 0 且不建 Job。
//
// 回傳建立(或已存在而回傳)的 job 數。
func (s *Service) ScheduleDailySweeps(ctx context.Context, now time.Time) (int, error) {
if s.SweepJobs == nil {
return 0, fmt.Errorf("%w: sweep job scheduler not configured", domain.ErrNotReady)
}
if now.IsZero() {
now = time.Now().UTC()
} else {
now = now.UTC()
}
if !PastDailySweepSlot(now) {
return 0, nil
}
runAt := DailySweepRunAt(now)
watches, err := s.Repo.ListAllActiveWatches(ctx)
if err != nil {
return 0, err
}
n := 0
for _, w := range watches {
if w == nil || w.Status != domain.WatchActive {
continue
}
if _, err := s.SweepJobs.ScheduleRadarSweep(ctx, w.OwnerUID, w.ID, runAt); err != nil {
return n, fmt.Errorf("schedule watch %s owner %d: %w", w.ID, w.OwnerUID, err)
}
n++
}
return n, nil
}
// PastDailySweepSlot reports whether now is at or after today's UTC 22:00.
func PastDailySweepSlot(now time.Time) bool {
now = now.UTC()
slot := time.Date(now.Year(), now.Month(), now.Day(), DailySweepHourUTC, 0, 0, 0, time.UTC)
return !now.Before(slot)
}
// DailySweepRunAt returns unix-ns for today's UTC 22:00 (the slot being scheduled).
func DailySweepRunAt(now time.Time) int64 {
now = now.UTC()
slot := time.Date(now.Year(), now.Month(), now.Day(), DailySweepHourUTC, 0, 0, 0, time.UTC)
return slot.UnixNano()
}
// BeginSweepRecord creates the RadarSweep shell for a claimed job (T527).
// Fetch / judge (T528T530) attach progress onto the same record via UpdateSweep.
func (s *Service) BeginSweepRecord(ctx context.Context, ownerUID int64, watchID, jobID, path string) (*domain.RadarSweep, error) {
if ownerUID <= 0 || watchID == "" {
return nil, fmt.Errorf("%w: owner_uid and watch_id required", domain.ErrValidation)
}
if path == "" {
path = domain.SweepPathAPI
}
sw := &domain.RadarSweep{
ID: domain.NewID(),
OwnerUID: ownerUID,
WatchID: watchID,
JobID: jobID,
Path: path,
StartedAt: domain.NowNano(),
}
if err := sw.Normalize(); err != nil {
return nil, err
}
if err := s.Repo.CreateSweep(ctx, sw); err != nil {
return nil, err
}
return sw, nil
}