From 533ff6b9f611d3922c036382a0d2d36a2e805bde Mon Sep 17 00:00:00 2001 From: biss Date: Sat, 25 Jul 2026 11:32:13 +0800 Subject: [PATCH] =?UTF-8?q?=E8=87=AA=E5=8A=A8=E6=8E=92=E8=AF=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/ScheduleSettingsController.cs | 4 ++ .../Scheduling/AutomaticScheduleGenerator.cs | 8 +-- .../AutomaticScheduleGeneratorTests.cs | 30 +++++++++- web/src/api/http.ts | 14 ++++- web/src/views/SchedulesView.vue | 56 ++++++++++++++++++- 5 files changed, 102 insertions(+), 10 deletions(-) diff --git a/src/Jiaowu.Api/Controllers/ScheduleSettingsController.cs b/src/Jiaowu.Api/Controllers/ScheduleSettingsController.cs index ccd13a6..2846050 100644 --- a/src/Jiaowu.Api/Controllers/ScheduleSettingsController.cs +++ b/src/Jiaowu.Api/Controllers/ScheduleSettingsController.cs @@ -91,6 +91,8 @@ public sealed class ScheduleSettingsController(AppDbContext db) : ControllerBase .OrderByDescending(item => item.IsPrimary) .Select(item => item.Teacher!.Name), x.Capacity, + x.StartWeek, + x.EndWeek, x.WeeklyHours, x.SchedulingMode }) @@ -114,6 +116,8 @@ public sealed class ScheduleSettingsController(AppDbContext db) : ControllerBase task.CollegeName, task.TeacherNames, task.Capacity, + task.StartWeek, + task.EndWeek, task.WeeklyHours, task.SchedulingMode, HasCustomConstraint = constraint is not null, diff --git a/src/Jiaowu.Api/Infrastructure/Scheduling/AutomaticScheduleGenerator.cs b/src/Jiaowu.Api/Infrastructure/Scheduling/AutomaticScheduleGenerator.cs index d064412..fc15dbc 100644 --- a/src/Jiaowu.Api/Infrastructure/Scheduling/AutomaticScheduleGenerator.cs +++ b/src/Jiaowu.Api/Infrastructure/Scheduling/AutomaticScheduleGenerator.cs @@ -233,10 +233,10 @@ public sealed class AutomaticScheduleGenerator(AppDbContext db) student.Status == StudentStatus.Active) ?? 0)); return classrooms.Where(room => room.Capacity >= minimumCapacity && - (!constraint?.RequiredCampusId.HasValue ?? true || - room.Building!.CampusId == constraint.RequiredCampusId) && - (!constraint?.RequiredBuildingId.HasValue ?? true || - room.BuildingId == constraint.RequiredBuildingId) && + (constraint?.RequiredCampusId is not Guid requiredCampusId || + room.Building!.CampusId == requiredCampusId) && + (constraint?.RequiredBuildingId is not Guid requiredBuildingId || + room.BuildingId == requiredBuildingId) && (allowedRoomIds.Count == 0 || allowedRoomIds.Contains(room.Id))) .ToList(); } diff --git a/tests/Jiaowu.Api.Tests/AutomaticScheduleGeneratorTests.cs b/tests/Jiaowu.Api.Tests/AutomaticScheduleGeneratorTests.cs index c634370..a0b93ee 100644 --- a/tests/Jiaowu.Api.Tests/AutomaticScheduleGeneratorTests.cs +++ b/tests/Jiaowu.Api.Tests/AutomaticScheduleGeneratorTests.cs @@ -224,7 +224,7 @@ public sealed class AutomaticScheduleGeneratorTests } [Fact] - public async Task Generator_does_not_attach_duplicate_buildings_from_untracked_classrooms() + public async Task Generator_uses_scoped_rooms_without_attaching_duplicate_buildings() { await using var connection = new SqliteConnection("Data Source=:memory:"); await connection.OpenAsync(); @@ -333,6 +333,34 @@ public sealed class AutomaticScheduleGeneratorTests EndsAt = new TimeOnly(9, 40) }); await db.SaveChangesAsync(); + db.TeachingTaskScheduleConstraints.AddRange( + new TeachingTaskScheduleConstraint + { + TeachingTaskId = smallTask.Id, + RequiredCampusId = campus.Id, + RequiredBuildingId = building.Id, + AllowedClassrooms = + [ + new TeachingTaskAllowedClassroom + { + ClassroomId = smallRoom.Id + } + ] + }, + new TeachingTaskScheduleConstraint + { + TeachingTaskId = largeTask.Id, + RequiredCampusId = campus.Id, + RequiredBuildingId = building.Id, + AllowedClassrooms = + [ + new TeachingTaskAllowedClassroom + { + ClassroomId = largeRoom.Id + } + ] + }); + await db.SaveChangesAsync(); db.ChangeTracker.Clear(); plan = await db.SchedulePlans.SingleAsync(); diff --git a/web/src/api/http.ts b/web/src/api/http.ts index de601ed..5399ef9 100644 --- a/web/src/api/http.ts +++ b/web/src/api/http.ts @@ -26,10 +26,18 @@ http.interceptors.response.use( export function apiErrorMessage(error: unknown) { if (!axios.isAxiosError(error)) return '操作失败,请稍后重试。' const data = error.response?.data - if (data?.errors) { - return Object.values(data.errors).flat().join(';') + if (data?.errors && typeof data.errors === 'object') { + const validationMessage = Object.values(data.errors) + .flat() + .map((item) => String(item).trim()) + .filter(Boolean) + .join(';') + if (validationMessage) return validationMessage } - return data?.detail ?? data?.title ?? '操作失败,请检查网络连接。' + const detail = typeof data?.detail === 'string' ? data.detail.trim() : '' + if (detail) return detail + const title = typeof data?.title === 'string' ? data.title.trim() : '' + return title || '操作失败,请检查网络连接。' } export default http diff --git a/web/src/views/SchedulesView.vue b/web/src/views/SchedulesView.vue index f7e2995..727bb19 100644 --- a/web/src/views/SchedulesView.vue +++ b/web/src/views/SchedulesView.vue @@ -97,6 +97,12 @@ const autoStatusText = computed(() => { const selectedTaskConstraint = computed(() => constraints.value.find((item) => item.id === entryForm.teachingTaskId), ) +const entryWeekdays = computed(() => { + const allowedDays = selectedTaskConstraint.value?.allowedDayOfWeeks ?? [] + return allowedDays.length + ? weekdays.filter((day) => allowedDays.includes(day.value)) + : weekdays +}) const constraintColleges = computed(() => { const result = new Map() constraints.value.forEach((item) => result.set(item.collegeId, item.collegeName)) @@ -586,12 +592,51 @@ function openEntry(entry?: any, day?: number, period?: number) { entryDialog.value = true } +function changeEntryTask() { + if (editingEntryId.value) return + const task = selectedTaskConstraint.value + if (!task) return + entryForm.startWeek = task.startWeek + entryForm.endWeek = task.endWeek + if (task.allowedDayOfWeeks?.length && + !task.allowedDayOfWeeks.includes(entryForm.dayOfWeek)) { + entryForm.dayOfWeek = task.allowedDayOfWeeks[0] + } + if (task.requiresClassroom === false) { + entryForm.classroomId = null + return + } + const room = classrooms.value.find((item) => item.id === entryForm.classroomId) + if (room && ( + (task.requiredCampusId && room.campusId !== task.requiredCampusId) || + (task.requiredBuildingId && room.buildingId !== task.requiredBuildingId) || + (task.allowedClassroomIds?.length && !task.allowedClassroomIds.includes(room.id)) + )) { + entryForm.classroomId = null + } +} + async function saveEntry() { if (!entryForm.teachingTaskId || (selectedTaskConstraint.value?.requiresClassroom !== false && !entryForm.classroomId)) { ElMessage.warning('请选择教学任务,并按课程要求选择教室。') return } + const task = selectedTaskConstraint.value + if (task && ( + entryForm.startWeek < task.startWeek || + entryForm.endWeek > task.endWeek + )) { + ElMessage.warning( + `排课周次必须位于该教学任务的第 ${task.startWeek}—${task.endWeek} 周内。`, + ) + return + } + if (task?.allowedDayOfWeeks?.length && + !task.allowedDayOfWeeks.includes(entryForm.dayOfWeek)) { + ElMessage.warning('所选星期不在该教学任务允许的上课日内。') + return + } if (selectedTaskConstraint.value?.requiresClassroom === false) entryForm.classroomId = null try { const base = `/schedules/plans/${selected.value.id}/entries` @@ -827,10 +872,17 @@ onBeforeUnmount(clearAutoSchedulePoll) - + +
- +