班级查询按“考试场次”合并,显示为“分散至 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:
@@ -340,6 +340,7 @@ public sealed class TimetableDataService(AppDbContext db)
|
|||||||
.OrderBy(x => x.ExamDate)
|
.OrderBy(x => x.ExamDate)
|
||||||
.ThenBy(x => x.StartPeriod)
|
.ThenBy(x => x.StartPeriod)
|
||||||
.Select(x => new ExamSessionProjection(
|
.Select(x => new ExamSessionProjection(
|
||||||
|
x.Id,
|
||||||
x.Id,
|
x.Id,
|
||||||
x.TeachingTaskId,
|
x.TeachingTaskId,
|
||||||
x.TeachingTask!.TaskNumber,
|
x.TeachingTask!.TaskNumber,
|
||||||
@@ -384,6 +385,7 @@ public sealed class TimetableDataService(AppDbContext db)
|
|||||||
.ThenBy(link => link.ExamRoom!.StartPeriod)
|
.ThenBy(link => link.ExamRoom!.StartPeriod)
|
||||||
.ThenBy(link => link.ExamSession!.TeachingTask!.Course!.Code)
|
.ThenBy(link => link.ExamSession!.TeachingTask!.Course!.Code)
|
||||||
.Select(link => new ExamSessionProjection(
|
.Select(link => new ExamSessionProjection(
|
||||||
|
link.ExamSessionId,
|
||||||
link.ExamRoomId,
|
link.ExamRoomId,
|
||||||
link.ExamSession!.TeachingTaskId,
|
link.ExamSession!.TeachingTaskId,
|
||||||
link.ExamSession.TeachingTask!.TaskNumber,
|
link.ExamSession.TeachingTask!.TaskNumber,
|
||||||
@@ -408,8 +410,17 @@ public sealed class TimetableDataService(AppDbContext db)
|
|||||||
link.ExamRoom.UpdatedAt))
|
link.ExamRoom.UpdatedAt))
|
||||||
.ToListAsync(cancellationToken);
|
.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 =>
|
result.AddRange(mixedSessions.Select(x =>
|
||||||
MapToEntryDto(x, slotLookup)));
|
MapToEntryDto(x, slotLookup)));
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -545,10 +556,9 @@ public sealed class TimetableDataService(AppDbContext db)
|
|||||||
if (studentId.HasValue)
|
if (studentId.HasValue)
|
||||||
{
|
{
|
||||||
return source.Where(link =>
|
return source.Where(link =>
|
||||||
link.ExamSession!.TeachingTask!.Classes.Any(c =>
|
|
||||||
c.AdministrativeClassId == resourceId) ||
|
|
||||||
link.ExamRoom!.Seats.Any(s =>
|
link.ExamRoom!.Seats.Any(s =>
|
||||||
s.StudentId == studentId.Value));
|
s.StudentId == studentId.Value &&
|
||||||
|
s.ExamSessionId == link.ExamSessionId));
|
||||||
}
|
}
|
||||||
return source.Where(link =>
|
return source.Where(link =>
|
||||||
link.ExamSession!.TeachingTask!.Classes.Any(c =>
|
link.ExamSession!.TeachingTask!.Classes.Any(c =>
|
||||||
@@ -604,7 +614,7 @@ public sealed class TimetableDataService(AppDbContext db)
|
|||||||
new[] { "监考:" + string.Join("、", x.InvigilatorNames) })
|
new[] { "监考:" + string.Join("、", x.InvigilatorNames) })
|
||||||
: x.TeacherNames;
|
: x.TeacherNames;
|
||||||
return new TimetableEntryDto(
|
return new TimetableEntryDto(
|
||||||
x.Id,
|
x.ExamRoomId,
|
||||||
x.TeachingTaskId,
|
x.TeachingTaskId,
|
||||||
x.TaskNumber,
|
x.TaskNumber,
|
||||||
x.TaskName,
|
x.TaskName,
|
||||||
@@ -627,8 +637,42 @@ public sealed class TimetableDataService(AppDbContext db)
|
|||||||
x.UpdatedAt);
|
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(
|
private sealed record ExamSessionProjection(
|
||||||
Guid Id,
|
Guid ExamSessionId,
|
||||||
|
Guid ExamRoomId,
|
||||||
Guid TeachingTaskId,
|
Guid TeachingTaskId,
|
||||||
string TaskNumber,
|
string TaskNumber,
|
||||||
string TaskName,
|
string TaskName,
|
||||||
@@ -759,7 +803,8 @@ public sealed record TimetableEntryDto(
|
|||||||
string? ExperimentProjectName = null,
|
string? ExperimentProjectName = null,
|
||||||
DateOnly? ExperimentDate = null,
|
DateOnly? ExperimentDate = null,
|
||||||
ExperimentArrangementMode? ExperimentArrangementMode = null,
|
ExperimentArrangementMode? ExperimentArrangementMode = null,
|
||||||
ScheduleEntryKind Kind = ScheduleEntryKind.Lecture);
|
ScheduleEntryKind Kind = ScheduleEntryKind.Lecture,
|
||||||
|
int ExamRoomCount = 1);
|
||||||
|
|
||||||
public sealed record FlexibleCourseDto(
|
public sealed record FlexibleCourseDto(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
|
|||||||
@@ -151,6 +151,46 @@ public sealed class ExamArrangementServiceTests
|
|||||||
Assert.Equal(6, await db.ExamSeats.CountAsync());
|
Assert.Equal(6, await db.ExamSeats.CountAsync());
|
||||||
Assert.Contains("2个教学班混排至2个考场", result.Message);
|
Assert.Contains("2个教学班混排至2个考场", result.Message);
|
||||||
Assert.Contains("自动包含同组1个场次", 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]
|
[Fact]
|
||||||
|
|||||||
@@ -1117,7 +1117,11 @@ onMounted(async () => {
|
|||||||
第 {{ entry.startWeek }} 周 ·
|
第 {{ entry.startWeek }} 周 ·
|
||||||
第 {{ entry.startPeriod }}—{{ entry.startPeriod + entry.periodCount - 1 }} 节
|
第 {{ entry.startPeriod }}—{{ entry.startPeriod + entry.periodCount - 1 }} 节
|
||||||
</small>
|
</small>
|
||||||
<small>{{ entry.teacherNames.join('、') || '监考教师待定' }}</small>
|
<small v-if="entry.examRoomCount > 1">
|
||||||
|
任课教师:{{ entry.teacherNames.join('、') || '待定' }} ·
|
||||||
|
学生登录后可查看本人考场及监考教师
|
||||||
|
</small>
|
||||||
|
<small v-else>{{ entry.teacherNames.join('、') || '监考教师待定' }}</small>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user