Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/internal/domain/availability_submission.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type AvailabilitySubmission struct {
ID int64 `json:"id"`
SchedulePlanID int64 `json:"schedulePlanID"`
UserID int64 `json:"userID"`
Username string `json:"username"`
Items []AvailabilitySubmissionItem `json:"items"`
CreatedAt time.Time `json:"createdAt"`
Version int32 `json:"-"`
Expand Down
21 changes: 17 additions & 4 deletions backend/internal/handler/schedule_plans.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,13 +305,13 @@ func (h *Handler) SubmitSchedulingResult(w http.ResponseWriter, r *http.Request)
return
}

if err := utils.ValidateSchedulingResultWithSubmissions(schedulingResult, submissions); err != nil {
if err := utils.ValidateSchedulingResultWithSubmissions(schedulingResult, submissions, template, h.repository); err != nil {
h.badRequest(w, r, err)
return
}

// 最后要检查是否存在重复的助理
if err := utils.ValidIfExistsDuplicateAssistant(schedulingResult); err != nil {
if err := utils.ValidIfExistsDuplicateAssistant(schedulingResult, template, h.repository); err != nil {
h.badRequest(w, r, err)
return
}
Expand Down Expand Up @@ -401,8 +401,21 @@ func (h *Handler) GenerateSchedulingResult(w http.ResponseWriter, r *http.Reques
return
}

res, err := scheduler.Schedule()
if err != nil {
res := scheduler.Schedule()

// 还需要检查一下结果是否满足约束条件(调用 validate 包中的方法就可以了)
schedulingResult := &domain.SchedulingResult{
Shifts: make([]domain.SchedulingResultShift, len(res)),
}
for i, shift := range res {
schedulingResult.Shifts[i] = *shift
}

if err := utils.ValidateSchedulingResultWithSubmissions(schedulingResult, submissions, template, h.repository); err != nil {
h.internalServerError(w, r, err)
return
}
if err := utils.ValidIfExistsDuplicateAssistant(schedulingResult, template, h.repository); err != nil {
h.internalServerError(w, r, err)
return
}
Expand Down
20 changes: 2 additions & 18 deletions backend/internal/scheduler/scheduler.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"sort"

"github.com/sysu-ecnc-dev/shift-manager/backend/internal/domain"
"github.com/sysu-ecnc-dev/shift-manager/backend/internal/utils"
)

type Scheduler struct {
Expand Down Expand Up @@ -68,7 +67,7 @@ func New(parameters *Parameters, users []*domain.User, template *domain.Schedule
return s, nil
}

func (s *Scheduler) Schedule() ([]*domain.SchedulingResultShift, error) {
func (s *Scheduler) Schedule() []*domain.SchedulingResultShift {
// 生成初始种群
pop := make([]*Chromosome, s.parameters.PopulationSize)
for i := 0; i < int(s.parameters.PopulationSize); i++ {
Expand Down Expand Up @@ -167,20 +166,5 @@ func (s *Scheduler) Schedule() ([]*domain.SchedulingResultShift, error) {
})
}

// 还需要检查一下结果是否满足约束条件(调用 validate 包中的方法就可以了)
schedulingResult := &domain.SchedulingResult{
Shifts: make([]domain.SchedulingResultShift, len(result)),
}
for i, shift := range result {
schedulingResult.Shifts[i] = *shift
}

if err := utils.ValidateSchedulingResultWithSubmissions(schedulingResult, s.submissions); err != nil {
return nil, err
}
if err := utils.ValidIfExistsDuplicateAssistant(schedulingResult); err != nil {
return nil, err
}

return result, nil
return result
}
203 changes: 154 additions & 49 deletions backend/internal/utils/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,22 @@ import (
"time"

"github.com/sysu-ecnc-dev/shift-manager/backend/internal/domain"
"github.com/sysu-ecnc-dev/shift-manager/backend/internal/repository"
)

func ValidateScheduleTemplateShiftTime(st *domain.ScheduleTemplate) error {
// 检查每一个班次的结束时间是不是都大于开始时间
for id, shift := range st.Shifts {
startTime, err := time.Parse("15:04:05", shift.StartTime)
if err != nil {
return fmt.Errorf("班次 %d 的开始时间格式错误", id)
return fmt.Errorf("班次 %d 的开始时间格式错误: %s", id, shift.StartTime)
}
endTime, err := time.Parse("15:04:05", shift.EndTime)
if err != nil {
return fmt.Errorf("班次 %d 的结束时间格式错误", id)
return fmt.Errorf("班次 %d 的结束时间格式错误: %s", id, shift.EndTime)
}
if endTime.Before(startTime) {
return fmt.Errorf("班次 %d 的结束时间不能小于开始时间", id)
return fmt.Errorf("班次 %d 的结束时间 %s 不能小于开始时间 %s", id, shift.EndTime, shift.StartTime)
}
}

Expand All @@ -35,7 +36,7 @@ func ValidateScheduleTemplateShiftTime(st *domain.ScheduleTemplate) error {
jEndTime, _ := time.Parse("15:04:05", st.Shifts[j].EndTime)

if !(jStartTime.After(iEndTime) || jStartTime.Equal(iEndTime) || iStartTime.After(jEndTime) || iStartTime.Equal(jEndTime)) {
return fmt.Errorf("班次 %d 和班次 %d 之间的时间冲突", i, j)
return fmt.Errorf("班次 %d (%s~%s) 和班次 %d (%s~%s) 之间的时间冲突", i, iStartTime, iEndTime, j, jStartTime, jEndTime)
}
}
}
Expand All @@ -58,34 +59,62 @@ func ValidateSchedulePlanTime(plan *domain.SchedulePlan) error {
return nil
}

// 根据班次的 ID 获取模板中对应的班次
func GetShiftByID(template *domain.ScheduleTemplate, shiftID int64) (*domain.ScheduleTemplateShift, error) {
var templateShift *domain.ScheduleTemplateShift = nil

for _, shift := range template.Shifts {
if shift.ID == shiftID {
templateShift = &shift
break
}
}

if templateShift == nil {
return nil, fmt.Errorf("班次 %d 不存在于该排班模版中", shiftID)
}

return templateShift, nil
}

func WeekDay(i int32) string {
switch i {
case 1:
return "周一"
case 2:
return "周二"
case 3:
return "周三"
case 4:
return "周四"
case 5:
return "周五"
case 6:
return "周六"
case 7:
return "周日"
default:
return "[未知]"
}
}

func ValidateSubmissionWithTemplate(submission *domain.AvailabilitySubmission, template *domain.ScheduleTemplate) error {
if len(template.Shifts) != len(submission.Items) {
return errors.New("提交的空闲时间中的班次数量和模板中的班次数量不匹配")
}

for i, item := range submission.Items {
isValid := false

for _, shift := range template.Shifts {
if shift.ID == item.ShiftID {
containAllDays := true

for _, day := range item.Days {
if !slices.Contains(shift.ApplicableDays, day) {
containAllDays = false
break
}
}

if containAllDays {
isValid = true
break
}
}
shift, err := GetShiftByID(template, item.ShiftID)
if err != nil {
return fmt.Errorf("第 %d 项提交的班次 %d 不存在于该排班模版中", i, item.ShiftID)
}

if !isValid {
return fmt.Errorf("第 %d 项不符合模板中的班次", i+1)
for _, day := range item.Days {
if !slices.Contains(shift.ApplicableDays, day) {
return fmt.Errorf(
"第 %d 项提交的班次时间不存在, 对应模板中%s班次 %d (%s~%s) 无值班安排",
i+1, WeekDay(day), shift.ID, shift.StartTime, shift.EndTime,
)
}
}
}

Expand All @@ -98,17 +127,9 @@ func ValidateSchedulingResultWithTemplate(result *domain.SchedulingResult, templ
}

for _, resultShift := range result.Shifts {
// 找到模板中对应的班次
var templateShift *domain.ScheduleTemplateShift = nil

for _, shift := range template.Shifts {
if shift.ID == resultShift.ShiftID {
templateShift = &shift
}
}

if templateShift == nil {
return fmt.Errorf("排班结果中的第 %d 项不存在于排班模板中", resultShift.ShiftID)
templateShift, err := GetShiftByID(template, resultShift.ShiftID)
if err != nil {
return err
}

for _, day := range templateShift.ApplicableDays {
Expand All @@ -122,17 +143,26 @@ func ValidateSchedulingResultWithTemplate(result *domain.SchedulingResult, templ
}

if !containDay {
return fmt.Errorf("排班结果中的第 %d 项的班次存在没有提交结果的天数 %d", resultShift.ShiftID, day)
return fmt.Errorf(
"排班结果中%s班次 %d (%s~%s) 无人值班",
WeekDay(day), templateShift.ID, templateShift.StartTime, templateShift.EndTime,
)
}
}

for _, item := range resultShift.Items {
if !slices.Contains(templateShift.ApplicableDays, item.Day) {
return fmt.Errorf("排班结果中的第 %d 项的第 %d 天不符合模板中的班次", resultShift.ShiftID, item.Day)
return fmt.Errorf(
"排班结果中%s班次 %d (%s~%s) 在排班模版中不需要安排值班",
WeekDay(item.Day), resultShift.ShiftID, templateShift.StartTime, templateShift.EndTime,
)
}
// +1 是因为负责人也算一个助理
if len(item.AssistantIDs)+1 > int(templateShift.RequiredAssistantNumber) {
return fmt.Errorf("排班结果中的第 %d 项的第 %d 天的助理人数超过了模板中的要求", resultShift.ShiftID, item.Day)
return fmt.Errorf(
"排班结果中的%s班次 %d (%s~%s) 的的助理人数超过了模板中的要求",
WeekDay(item.Day), resultShift.ShiftID, templateShift.StartTime, templateShift.EndTime,
)
}
}
}
Expand All @@ -149,14 +179,30 @@ func getSubmissionByAssistantID(submissions []*domain.AvailabilitySubmission, as
return nil
}

func ValidateSchedulingResultWithSubmissions(result *domain.SchedulingResult, submissions []*domain.AvailabilitySubmission) error {
for i, shift := range result.Shifts {
func ValidateSchedulingResultWithSubmissions(
result *domain.SchedulingResult,
submissions []*domain.AvailabilitySubmission,
template *domain.ScheduleTemplate,
repo *repository.Repository,
) error {
for _, shift := range result.Shifts {
for _, item := range shift.Items {
if item.PrincipalID != nil {
// 找到这个负责人对应的提交
submission := getSubmissionByAssistantID(submissions, *item.PrincipalID)
if submission == nil {
return fmt.Errorf("班次 %d 的第 %d 天的 id 为 %d 的负责人没有提交空闲时间", i+1, item.Day, item.PrincipalID)
templateShift, err := GetShiftByID(template, shift.ShiftID)
if err != nil {
return err
}
user, err := repo.GetUserByID(*item.PrincipalID)
if err != nil {
return err
}
return fmt.Errorf(
"负责人%s没有提交%s班次 %d (%s~%s) 的空闲时间",
user.FullName, WeekDay(item.Day), templateShift.ID, templateShift.StartTime, templateShift.EndTime,
)
}

// 检查这个负责人是否在第 item.Day 天有空闲时间
Expand All @@ -168,14 +214,36 @@ func ValidateSchedulingResultWithSubmissions(result *domain.SchedulingResult, su
}
}
if !ok {
return fmt.Errorf("id 为 %d 的负责人在班次 %d 的第 %d 天没有空闲时间", item.PrincipalID, shift.ShiftID, item.Day)
templateShift, err := GetShiftByID(template, shift.ShiftID)
if err != nil {
return err
}
user, err := repo.GetUserByID(*item.PrincipalID)
if err != nil {
return err
}
return fmt.Errorf(
"负责人%s在%s班次 %d (%s~%s) 没有空闲时间",
user.FullName, WeekDay(item.Day), templateShift.ID, templateShift.StartTime, templateShift.EndTime,
)
}
}
for _, assistantID := range item.AssistantIDs {
// 找到这个助理对应的提交
submission := getSubmissionByAssistantID(submissions, assistantID)
if submission == nil {
return fmt.Errorf("班次 %d 的第 %d 天的 id 为 %d 的助理没有提交空闲时间", i+1, item.Day, assistantID)
templateShift, err := GetShiftByID(template, shift.ShiftID)
if err != nil {
return err
}
user, err := repo.GetUserByID(assistantID)
if err != nil {
return err
}
return fmt.Errorf(
"助理%s没有提交%s班次 %d (%s~%s) 的空闲时间",
user.FullName, WeekDay(item.Day), templateShift.ID, templateShift.StartTime, templateShift.EndTime,
)
}

// 检查这个助理是否在第 item.Day 天有空闲时间
Expand All @@ -187,7 +255,18 @@ func ValidateSchedulingResultWithSubmissions(result *domain.SchedulingResult, su
}
}
if !ok {
return fmt.Errorf("id 为 %d 的助理在班次 %d 的第 %d 天没有空闲时间", assistantID, shift.ShiftID, item.Day)
templateShift, err := GetShiftByID(template, shift.ShiftID)
if err != nil {
return err
}
user, err := repo.GetUserByID(assistantID)
if err != nil {
return err
}
return fmt.Errorf(
"助理%s在%s班次 %d (%s~%s) 没有空闲时间",
user.FullName, WeekDay(item.Day), templateShift.ID, templateShift.StartTime, templateShift.EndTime,
)
}
}
}
Expand All @@ -196,19 +275,45 @@ func ValidateSchedulingResultWithSubmissions(result *domain.SchedulingResult, su
return nil
}

func ValidIfExistsDuplicateAssistant(result *domain.SchedulingResult) error {
func ValidIfExistsDuplicateAssistant(
result *domain.SchedulingResult,
template *domain.ScheduleTemplate,
repo *repository.Repository,
) error {
// 检查是否存在某个班次中的某一天有重复的助理
for i, resultShift := range result.Shifts {
for _, resultShift := range result.Shifts {
for _, resultShiftItem := range resultShift.Items {
// 先检查负责人是不是存在于助理数组中
if resultShiftItem.PrincipalID != nil && slices.Contains(resultShiftItem.AssistantIDs, *resultShiftItem.PrincipalID) {
return fmt.Errorf("班次 %d 的第 %d 天中负责人和助理重复", i, resultShiftItem.Day)
templateShift, err := GetShiftByID(template, resultShift.ShiftID)
if err != nil {
return err
}
user, err := repo.GetUserByID(*resultShiftItem.PrincipalID)
if err != nil {
return err
}
return fmt.Errorf(
"%s班次 %d (%s~%s) 的负责人和助理重复(%s)",
WeekDay(resultShiftItem.Day), resultShift.ShiftID, templateShift.StartTime, templateShift.EndTime, user.FullName,
)
}
// 检查助理之间是否有重复
seen := make(map[int64]bool)
for _, assistantID := range resultShiftItem.AssistantIDs {
if seen[assistantID] {
return fmt.Errorf("班次 %d 的第 %d 天中存在重复助理", i, resultShiftItem.Day)
templateShift, err := GetShiftByID(template, resultShift.ShiftID)
if err != nil {
return err
}
user, err := repo.GetUserByID(assistantID)
if err != nil {
return err
}
return fmt.Errorf(
"%s班次 %d (%s~%s) 的助理重复(%s)",
WeekDay(resultShiftItem.Day), resultShift.ShiftID, templateShift.StartTime, templateShift.EndTime, user.FullName,
)
}
seen[assistantID] = true
}
Expand Down
Loading