班级查询按“考试场次”合并,显示为“分散至 N 个考场”,不再生成大量重复卡片。[后端实现 (line 416)](E:/jiaowu/src/Jiaowu.Api/Infrastructure/Timetables/TimetableDataService.cs:416)

学生登录后只显示本人座位对应的考场,同时修复混排考场可能带出其他课程的问题。[筛选逻辑 (line 561)](E:/jiaowu/src/Jiaowu.Api/Infrastructure/Timetables/TimetableDataService.cs:561)
多考场卡片提示学生登录查看本人考场及监考教师。[前端展示 (line 1122)](E:/jiaowu/web/src/views/TimetableView.vue:1122)
教室课表、监考教师课表仍按具体考场显示,不受合并影响。
已增加班级合并和学生精准考场回归测试。[测试 (line 183)](E:/jiaowu/tests/Jiaowu.Api.Tests/ExamArrangementServiceTests.cs:183)
This commit is contained in:
2026-08-02 17:09:26 +08:00 Unverified
parent f0f59419d5
commit 8cb9b4f57b
3 changed files with 98 additions and 9 deletions
@@ -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);
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<ExamSessionProjection> sessions,
IReadOnlyDictionary<int, TimetableSlotDto> 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,
@@ -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]
+5 -1
View File
@@ -1117,7 +1117,11 @@ onMounted(async () => {
第 {{ entry.startWeek }} 周 ·
第 {{ entry.startPeriod }}—{{ entry.startPeriod + entry.periodCount - 1 }} 节
</small>
<small>{{ entry.teacherNames.join('、') || '监考教师待定' }}</small>
<small v-if="entry.examRoomCount > 1">
任课教师:{{ entry.teacherNames.join('、') || '待定' }} ·
学生登录后可查看本人考场及监考教师
</small>
<small v-else>{{ entry.teacherNames.join('、') || '监考教师待定' }}</small>
</div>
</article>
</div>