package duty_schedule import ( "errors" "sort" "strings" "time" ) var ( ErrInvalid = errors.New("值班排班信息不符合要求") ErrNotFound = errors.New("值班组或排班不存在") ErrConflict = errors.New("数据已被其他人员更新,请刷新后重试") ErrCoverage = errors.New("周排班存在空档或重叠") ) type GroupInput struct { Name string `json:"name"` Timezone string `json:"timezone"` ExpectedVersion int `json:"expectedVersion"` } type MemberInput struct { ContactID string `json:"contactId"` Role string `json:"role"` } type SlotInput struct { Weekday int `json:"weekday"` StartMinute int `json:"startMinute"` EndMinute int `json:"endMinute"` PrimaryContactID string `json:"primaryContactId"` BackupContactID string `json:"backupContactId"` } type ScheduleInput struct { EffectiveFrom time.Time `json:"effectiveFrom"` EffectiveTo *time.Time `json:"effectiveTo"` Slots []SlotInput `json:"slots"` } type OverrideInput struct { OriginalContactID string `json:"originalContactId"` ReplacementContactID string `json:"replacementContactId"` StartsAt time.Time `json:"startsAt"` EndsAt time.Time `json:"endsAt"` Reason string `json:"reason"` } func normalizeGroup(input GroupInput) (GroupInput, error) { input.Name = strings.TrimSpace(input.Name) input.Timezone = strings.TrimSpace(input.Timezone) if input.Name == "" || len([]rune(input.Name)) > 128 { return GroupInput{}, ErrInvalid } if _, err := time.LoadLocation(input.Timezone); err != nil { return GroupInput{}, ErrInvalid } return input, nil } func validateSlots(slots []SlotInput) error { if len(slots) == 0 { return ErrCoverage } byDay := map[int][]SlotInput{} for _, slot := range slots { if slot.Weekday < 0 || slot.Weekday > 6 || slot.StartMinute < 0 || slot.EndMinute > 1440 || slot.StartMinute >= slot.EndMinute || slot.PrimaryContactID == "" || slot.BackupContactID == "" || slot.PrimaryContactID == slot.BackupContactID { return ErrInvalid } byDay[slot.Weekday] = append(byDay[slot.Weekday], slot) } for day := 0; day < 7; day++ { daySlots := byDay[day] sort.Slice(daySlots, func(i, j int) bool { return daySlots[i].StartMinute < daySlots[j].StartMinute }) cursor := 0 for _, slot := range daySlots { if slot.StartMinute != cursor { return ErrCoverage } cursor = slot.EndMinute } if cursor != 1440 { return ErrCoverage } } return nil } func normalizeOverride(input OverrideInput) (OverrideInput, error) { input.Reason = strings.TrimSpace(input.Reason) if input.OriginalContactID == "" || input.ReplacementContactID == "" || input.OriginalContactID == input.ReplacementContactID || input.StartsAt.IsZero() || !input.EndsAt.After(input.StartsAt) || input.Reason == "" || len([]rune(input.Reason)) > 256 { return OverrideInput{}, ErrInvalid } return input, nil }