package domain import ( "fmt" "sort" "time" ) const ( ScheduleTimezone = "Asia/Taipei" DefaultSweepHour = 6 MaxSweepHours = 6 minSweepHour = 0 maxSweepHour = 23 ) // RadarSchedule is the owner's automatic patrol timetable (Taipei local hours). type RadarSchedule struct { OwnerUID int64 `bson:"_id" json:"owner_uid"` Hours []int `bson:"hours" json:"hours"` UpdatedAt int64 `bson:"updated_at" json:"updated_at"` } func TaipeiLocation() *time.Location { loc, err := time.LoadLocation(ScheduleTimezone) if err != nil { return time.FixedZone(ScheduleTimezone, 8*3600) } return loc } func DefaultSweepHours() []int { return []int{DefaultSweepHour} } func NormalizeSweepHours(hours []int) ([]int, error) { if len(hours) == 0 { return DefaultSweepHours(), nil } seen := map[int]bool{} out := make([]int, 0, len(hours)) for _, h := range hours { if h < minSweepHour || h > maxSweepHour { return nil, fmt.Errorf("%w: hours must be 0–23 (got %d)", ErrValidation, h) } if seen[h] { continue } seen[h] = true out = append(out, h) } if len(out) == 0 { return DefaultSweepHours(), nil } if len(out) > MaxSweepHours { return nil, fmt.Errorf("%w: at most %d patrol hours", ErrValidation, MaxSweepHours) } sort.Ints(out) return out, nil } // SweepSlot is one due automatic patrol (Taipei calendar day + hour). type SweepSlot struct { Date string Hour int RunAt int64 } // DueSweepSlots returns selected hours that have already started today (Taipei) // plus any earlier selected hours the same day, so a late worker still catches up. func DueSweepSlots(now time.Time, hours []int) []SweepSlot { hours, err := NormalizeSweepHours(hours) if err != nil { hours = DefaultSweepHours() } loc := TaipeiLocation() local := now.In(loc) day := time.Date(local.Year(), local.Month(), local.Day(), 0, 0, 0, 0, loc) out := make([]SweepSlot, 0, len(hours)) for _, h := range hours { slot := day.Add(time.Duration(h) * time.Hour) if local.Before(slot) { continue } out = append(out, SweepSlot{ Date: day.Format("2006-01-02"), Hour: h, RunAt: slot.UnixNano(), }) } return out }