diff --git a/backend/internal/domain/availability_submission.go b/backend/internal/domain/availability_submission.go index 6b37740..2aa86ec 100644 --- a/backend/internal/domain/availability_submission.go +++ b/backend/internal/domain/availability_submission.go @@ -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:"-"` diff --git a/backend/internal/handler/schedule_plans.go b/backend/internal/handler/schedule_plans.go index a46c409..71926c4 100644 --- a/backend/internal/handler/schedule_plans.go +++ b/backend/internal/handler/schedule_plans.go @@ -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 } @@ -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 } diff --git a/backend/internal/scheduler/scheduler.go b/backend/internal/scheduler/scheduler.go index a58caa2..6b794c0 100644 --- a/backend/internal/scheduler/scheduler.go +++ b/backend/internal/scheduler/scheduler.go @@ -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 { @@ -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++ { @@ -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 } diff --git a/backend/internal/utils/validation.go b/backend/internal/utils/validation.go index 97e4e2f..0633b82 100644 --- a/backend/internal/utils/validation.go +++ b/backend/internal/utils/validation.go @@ -7,6 +7,7 @@ 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 { @@ -14,14 +15,14 @@ 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) } } @@ -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) } } } @@ -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, + ) + } } } @@ -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 { @@ -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, + ) } } } @@ -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 天有空闲时间 @@ -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 天有空闲时间 @@ -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, + ) } } } @@ -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 } diff --git a/frontend/index.html b/frontend/index.html index 8ac53d2..3dcca7b 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,7 +2,7 @@
- +