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

119 lines
4.0 KiB
Go
Raw Normal View History

2026-08-03 05:52:02 +00:00
package usecase
import (
"context"
"fmt"
2026-08-09 07:57:35 +00:00
"strings"
2026-08-03 05:52:02 +00:00
"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)
}
2026-08-13 07:54:25 +00:00
// ManualSweepJobScheduler is optional. TriggerSweep uses it so 「立即巡邏」
// is not swallowed by a finished same-day daily job.
type ManualSweepJobScheduler interface {
ScheduleManualRadarSweep(ctx context.Context, ownerUID int64, watchID string, runAt int64) (jobID string, err error)
}
2026-08-03 05:52:02 +00:00
// 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)
}
2026-08-27 05:35:07 +00:00
// ScheduleDailySweeps 為每個 active watch 補齊「今天(台北)已到期」的時段 Job。
2026-08-03 05:52:02 +00:00
//
2026-08-27 05:35:07 +00:00
// 預設時段是台北 06:00等同舊的 UTC 22:00。會員可在 /radar/schedule 多選時段。
// 呼叫端必須已持有 worker maintenance Redis lock。回傳建立或已存在的 slot 數。
2026-08-03 05:52:02 +00:00
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() {
2026-08-27 05:35:07 +00:00
now = time.Now()
2026-08-03 05:52:02 +00:00
}
watches, err := s.Repo.ListAllActiveWatches(ctx)
if err != nil {
return 0, err
}
2026-08-27 05:35:07 +00:00
hoursByOwner := map[int64][]int{}
2026-08-03 05:52:02 +00:00
n := 0
for _, w := range watches {
if w == nil || w.Status != domain.WatchActive {
continue
}
2026-08-27 05:35:07 +00:00
hours, ok := hoursByOwner[w.OwnerUID]
if !ok {
sch, gerr := s.GetRadarSchedule(ctx, w.OwnerUID)
if gerr != nil {
return n, fmt.Errorf("schedule owner %d: %w", w.OwnerUID, gerr)
}
hours = sch.Hours
hoursByOwner[w.OwnerUID] = hours
}
slots := domain.DueSweepSlots(now, hours)
for _, slot := range slots {
if _, err := s.SweepJobs.ScheduleRadarSweep(ctx, w.OwnerUID, w.ID, slot.RunAt); err != nil {
return n, fmt.Errorf("schedule watch %s owner %d hour %d: %w", w.ID, w.OwnerUID, slot.Hour, err)
}
n++
2026-08-03 05:52:02 +00:00
}
}
return n, nil
}
2026-08-27 05:35:07 +00:00
// PastDailySweepSlot reports whether the default Taipei 06:00 slot has started.
2026-08-03 05:52:02 +00:00
func PastDailySweepSlot(now time.Time) bool {
2026-08-27 05:35:07 +00:00
return len(domain.DueSweepSlots(now, domain.DefaultSweepHours())) > 0
2026-08-03 05:52:02 +00:00
}
2026-08-27 05:35:07 +00:00
// DailySweepRunAt returns unix-ns for today's default Taipei 06:00 slot (or next if not yet).
2026-08-03 05:52:02 +00:00
func DailySweepRunAt(now time.Time) int64 {
2026-08-27 05:35:07 +00:00
slots := domain.DueSweepSlots(now, domain.DefaultSweepHours())
if len(slots) > 0 {
return slots[0].RunAt
}
loc := domain.TaipeiLocation()
local := now.In(loc)
slot := time.Date(local.Year(), local.Month(), local.Day(), domain.DefaultSweepHour, 0, 0, 0, loc)
2026-08-03 05:52:02 +00:00
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) {
2026-08-09 07:57:35 +00:00
if ownerUID <= 0 {
return nil, fmt.Errorf("%w: owner_uid required", domain.ErrValidation)
2026-08-03 05:52:02 +00:00
}
2026-08-09 07:57:35 +00:00
// watchID may be empty for on-demand explore (no subscription).
2026-08-03 05:52:02 +00:00
if path == "" {
path = domain.SweepPathAPI
}
sw := &domain.RadarSweep{
ID: domain.NewID(),
OwnerUID: ownerUID,
2026-08-09 07:57:35 +00:00
WatchID: strings.TrimSpace(watchID),
2026-08-03 05:52:02 +00:00
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
}