diff --git a/src/Jiaowu.Api/Infrastructure/Timetables/TimetableDataService.cs b/src/Jiaowu.Api/Infrastructure/Timetables/TimetableDataService.cs index 089a772..24b8fc7 100644 --- a/src/Jiaowu.Api/Infrastructure/Timetables/TimetableDataService.cs +++ b/src/Jiaowu.Api/Infrastructure/Timetables/TimetableDataService.cs @@ -340,6 +340,7 @@ public sealed class TimetableDataService(AppDbContext db) .OrderBy(x => x.ExamDate) .ThenBy(x => x.StartPeriod) .Select(x => new ExamSessionProjection( + x.Id, x.Id, x.TeachingTaskId, x.TeachingTask!.TaskNumber, @@ -384,6 +385,7 @@ public sealed class TimetableDataService(AppDbContext db) .ThenBy(link => link.ExamRoom!.StartPeriod) .ThenBy(link => link.ExamSession!.TeachingTask!.Course!.Code) .Select(link => new ExamSessionProjection( + link.ExamSessionId, link.ExamRoomId, link.ExamSession!.TeachingTaskId, link.ExamSession.TeachingTask!.TaskNumber, @@ -408,8 +410,17 @@ public sealed class TimetableDataService(AppDbContext db) link.ExamRoom.UpdatedAt)) .ToListAsync(cancellationToken); - result.AddRange(mixedSessions.Select(x => - MapToEntryDto(x, slotLookup))); + if (resourceType == TimetableResourceType.Class && !studentId.HasValue) + { + result.AddRange(mixedSessions + .GroupBy(x => x.ExamSessionId) + .Select(group => MapClassExamEntryDto(group, slotLookup))); + } + else + { + result.AddRange(mixedSessions.Select(x => + MapToEntryDto(x, slotLookup))); + } return result; } @@ -545,10 +556,9 @@ public sealed class TimetableDataService(AppDbContext db) if (studentId.HasValue) { return source.Where(link => - link.ExamSession!.TeachingTask!.Classes.Any(c => - c.AdministrativeClassId == resourceId) || link.ExamRoom!.Seats.Any(s => - s.StudentId == studentId.Value)); + s.StudentId == studentId.Value && + s.ExamSessionId == link.ExamSessionId)); } return source.Where(link => link.ExamSession!.TeachingTask!.Classes.Any(c => @@ -604,7 +614,7 @@ public sealed class TimetableDataService(AppDbContext db) new[] { "监考:" + string.Join("、", x.InvigilatorNames) }) : x.TeacherNames; return new TimetableEntryDto( - x.Id, + x.ExamRoomId, x.TeachingTaskId, x.TaskNumber, x.TaskName, @@ -627,8 +637,42 @@ public sealed class TimetableDataService(AppDbContext db) x.UpdatedAt); } + private static TimetableEntryDto MapClassExamEntryDto( + IEnumerable sessions, + IReadOnlyDictionary slotLookup) + { + var rooms = sessions.ToList(); + var first = rooms[0]; + var roomCount = rooms + .Select(x => x.ExamRoomId) + .Distinct() + .Count(); + var buildingNames = rooms + .Select(x => x.BuildingName) + .Distinct() + .ToList(); + var campusNames = rooms + .Select(x => x.CampusName) + .Distinct() + .ToList(); + var entry = MapToEntryDto(first, slotLookup); + return entry with + { + Id = first.ExamSessionId, + TeacherNames = first.TeacherNames, + ClassroomName = roomCount == 1 + ? first.ClassroomName + : $"分散至 {roomCount} 个考场", + BuildingName = buildingNames.Count == 1 ? buildingNames[0] : null, + CampusName = campusNames.Count == 1 ? campusNames[0] : null, + UpdatedAt = rooms.Max(x => x.UpdatedAt), + ExamRoomCount = roomCount + }; + } + private sealed record ExamSessionProjection( - Guid Id, + Guid ExamSessionId, + Guid ExamRoomId, Guid TeachingTaskId, string TaskNumber, string TaskName, @@ -759,7 +803,8 @@ public sealed record TimetableEntryDto( string? ExperimentProjectName = null, DateOnly? ExperimentDate = null, ExperimentArrangementMode? ExperimentArrangementMode = null, - ScheduleEntryKind Kind = ScheduleEntryKind.Lecture); + ScheduleEntryKind Kind = ScheduleEntryKind.Lecture, + int ExamRoomCount = 1); public sealed record FlexibleCourseDto( Guid Id, diff --git a/tests/Jiaowu.Api.Tests/ExamArrangementServiceTests.cs b/tests/Jiaowu.Api.Tests/ExamArrangementServiceTests.cs index 27f4544..32370ef 100644 --- a/tests/Jiaowu.Api.Tests/ExamArrangementServiceTests.cs +++ b/tests/Jiaowu.Api.Tests/ExamArrangementServiceTests.cs @@ -151,6 +151,46 @@ public sealed class ExamArrangementServiceTests Assert.Equal(6, await db.ExamSeats.CountAsync()); Assert.Contains("2个教学班混排至2个考场", result.Message); Assert.Contains("自动包含同组1个场次", result.Message); + + plan = await db.ExamPlans.SingleAsync(x => x.Id == plan.Id); + plan.Status = ExamPlanStatus.Published; + await db.SaveChangesAsync(); + db.ChangeTracker.Clear(); + + var student = await db.Students.AsNoTracking() + .OrderBy(x => x.StudentNumber) + .FirstAsync(); + var timetableService = new TimetableDataService(db); + var classTimetable = await timetableService.BuildAsync( + TimetableResourceType.Class, + student.AdministrativeClassId, + seed.Term.Id, + null, + false, + null, + null, + CancellationToken.None); + var studentTimetable = await timetableService.BuildAsync( + TimetableResourceType.Class, + student.AdministrativeClassId, + seed.Term.Id, + null, + false, + student.Id, + new TimetableStudentDto(student.StudentNumber, student.Name), + CancellationToken.None); + + var classExam = Assert.Single(classTimetable!.ExamEntries); + Assert.Equal(first.Id, classExam.Id); + Assert.Equal(2, classExam.ExamRoomCount); + Assert.Equal("分散至 2 个考场", classExam.ClassroomName); + Assert.DoesNotContain("监考:", classExam.TeacherNames); + + var studentExam = Assert.Single(studentTimetable!.ExamEntries); + Assert.Equal(1, studentExam.ExamRoomCount); + Assert.Contains( + studentExam.ClassroomName, + new[] { seed.LargeClassroom.Name, seed.SmallClassroom.Name }); } [Fact] diff --git a/web/src/views/TimetableView.vue b/web/src/views/TimetableView.vue index d446b26..91159c0 100644 --- a/web/src/views/TimetableView.vue +++ b/web/src/views/TimetableView.vue @@ -1117,7 +1117,11 @@ onMounted(async () => { 第 {{ entry.startWeek }} 周 · 第 {{ entry.startPeriod }}—{{ entry.startPeriod + entry.periodCount - 1 }} 节 - {{ entry.teacherNames.join('、') || '监考教师待定' }} + + 任课教师:{{ entry.teacherNames.join('、') || '待定' }} · + 学生登录后可查看本人考场及监考教师 + + {{ entry.teacherNames.join('、') || '监考教师待定' }}