119 lines
4.0 KiB
Go
119 lines
4.0 KiB
Go
package usecase
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
"apps/backend/internal/module/radar/domain"
|
||
)
|
||
|
||
// DailySweepHourUTC is the fixed daily schedule (spec §4.2:UTC 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)
|
||
}
|
||
|
||
// 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)
|
||
}
|
||
|
||
// 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 為每個 active watch 補齊「今天(台北)已到期」的時段 Job。
|
||
//
|
||
// 預設時段是台北 06:00(等同舊的 UTC 22:00)。會員可在 /radar/schedule 多選時段。
|
||
// 呼叫端必須已持有 worker maintenance Redis lock。回傳建立(或已存在)的 slot 數。
|
||
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()
|
||
}
|
||
|
||
watches, err := s.Repo.ListAllActiveWatches(ctx)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
hoursByOwner := map[int64][]int{}
|
||
n := 0
|
||
for _, w := range watches {
|
||
if w == nil || w.Status != domain.WatchActive {
|
||
continue
|
||
}
|
||
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++
|
||
}
|
||
}
|
||
return n, nil
|
||
}
|
||
|
||
// PastDailySweepSlot reports whether the default Taipei 06:00 slot has started.
|
||
func PastDailySweepSlot(now time.Time) bool {
|
||
return len(domain.DueSweepSlots(now, domain.DefaultSweepHours())) > 0
|
||
}
|
||
|
||
// DailySweepRunAt returns unix-ns for today's default Taipei 06:00 slot (or next if not yet).
|
||
func DailySweepRunAt(now time.Time) int64 {
|
||
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)
|
||
return slot.UnixNano()
|
||
}
|
||
|
||
// BeginSweepRecord creates the RadarSweep shell for a claimed job (T527).
|
||
// Fetch / judge (T528–T530) 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 {
|
||
return nil, fmt.Errorf("%w: owner_uid required", domain.ErrValidation)
|
||
}
|
||
// watchID may be empty for on-demand explore (no subscription).
|
||
if path == "" {
|
||
path = domain.SweepPathAPI
|
||
}
|
||
sw := &domain.RadarSweep{
|
||
ID: domain.NewID(),
|
||
OwnerUID: ownerUID,
|
||
WatchID: strings.TrimSpace(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
|
||
}
|