93 lines
2.1 KiB
Go
93 lines
2.1 KiB
Go
|
|
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
|
|||
|
|
}
|