课表查询中最明显的性能优化。
将混合考场查询从“加载整个学期的考场、座位、教师、班级后在内存筛选”,改为在数据库中按班级、教师或教室直接筛选。 改为窄字段投影,避免创建大量 EF 实体和关系对象。 添加查询标签,便于后续在 MySQL 慢查询和链路追踪中定位。 保留现有 HybridCache/可选 Redis 缓存体系,无需新增依赖或数据库迁移。 增加教室、无关教室、监考教师三种回归场景。
This commit is contained in:
@@ -356,72 +356,49 @@ public sealed class TimetableDataService(AppDbContext db)
|
|||||||
MapToEntryDto(x, slotLookup)));
|
MapToEntryDto(x, slotLookup)));
|
||||||
|
|
||||||
// ── Mixed-room sessions (ExamRoomAssignment) ──
|
// ── Mixed-room sessions (ExamRoomAssignment) ──
|
||||||
// Load all mixed rooms for this term into memory, then filter
|
// Start from room/session links so the resource predicate stays in SQL.
|
||||||
var allMixedRooms = await db.ExamRooms.AsNoTracking()
|
// Loading every room, seat and roster for the term made a single
|
||||||
.Where(room =>
|
// timetable request scale with the entire exam plan.
|
||||||
room.ExamPlan!.AcademicTermId == academicTermId &&
|
var mixedQuery = db.ExamRoomSessions.AsNoTracking()
|
||||||
room.ExamPlan.Status == ExamPlanStatus.Published)
|
.Where(link =>
|
||||||
.Include(room => room.ExamPlan)
|
link.ExamRoom!.ExamPlan!.AcademicTermId == academicTermId &&
|
||||||
.Include(room => room.Classroom)
|
link.ExamRoom.ExamPlan.Status == ExamPlanStatus.Published);
|
||||||
.ThenInclude(c => c!.Building)
|
mixedQuery = ApplyMixedResourceFilter(
|
||||||
.ThenInclude(b => b!.Campus)
|
mixedQuery, resourceType, resourceId, studentId);
|
||||||
.Include(room => room.Invigilators)
|
|
||||||
.ThenInclude(i => i.Teacher)
|
var mixedSessions = await mixedQuery
|
||||||
.Include(room => room.SessionLinks)
|
.TagWith("Timetable.LoadMixedExamEntries")
|
||||||
.ThenInclude(link => link.ExamSession)
|
|
||||||
.ThenInclude(s => s!.TeachingTask)
|
|
||||||
.ThenInclude(t => t!.Course)
|
|
||||||
.Include(room => room.SessionLinks)
|
|
||||||
.ThenInclude(link => link.ExamSession)
|
|
||||||
.ThenInclude(s => s!.TeachingTask)
|
|
||||||
.ThenInclude(t => t!.Teachers)
|
|
||||||
.ThenInclude(tt => tt.Teacher)
|
|
||||||
.Include(room => room.SessionLinks)
|
|
||||||
.ThenInclude(link => link.ExamSession)
|
|
||||||
.ThenInclude(s => s!.TeachingTask)
|
|
||||||
.ThenInclude(t => t!.Classes)
|
|
||||||
.ThenInclude(tc => tc.AdministrativeClass)
|
|
||||||
.Include(room => room.Seats)
|
|
||||||
.AsSplitQuery()
|
.AsSplitQuery()
|
||||||
.ToListAsync(cancellationToken);
|
.OrderBy(link => link.ExamRoom!.ExamDate)
|
||||||
|
.ThenBy(link => link.ExamRoom!.StartPeriod)
|
||||||
foreach (var room in allMixedRooms)
|
.ThenBy(link => link.ExamSession!.TeachingTask!.Course!.Code)
|
||||||
{
|
.Select(link => new ExamSessionProjection(
|
||||||
foreach (var link in room.SessionLinks)
|
link.ExamRoomId,
|
||||||
{
|
link.ExamSession!.TeachingTaskId,
|
||||||
var session = link.ExamSession;
|
link.ExamSession.TeachingTask!.TaskNumber,
|
||||||
if (session == null || session.TeachingTask == null) continue;
|
link.ExamSession.TeachingTask.Name,
|
||||||
|
link.ExamSession.TeachingTask.Course!.Code,
|
||||||
// Apply resource filter in memory
|
link.ExamSession.TeachingTask.Course.Name,
|
||||||
if (!MatchesMixedResource(
|
link.ExamSession.TeachingTask.Teachers
|
||||||
room, session, resourceType, resourceId, studentId))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
result.Add(MapToEntryDto(new ExamSessionProjection(
|
|
||||||
room.Id,
|
|
||||||
session.TeachingTaskId,
|
|
||||||
session.TeachingTask.TaskNumber,
|
|
||||||
session.TeachingTask.Name,
|
|
||||||
session.TeachingTask.Course!.Code,
|
|
||||||
session.TeachingTask.Course.Name,
|
|
||||||
session.TeachingTask.Teachers
|
|
||||||
.OrderByDescending(t => t.IsPrimary)
|
.OrderByDescending(t => t.IsPrimary)
|
||||||
.Select(t => t.Teacher!.Name).ToList(),
|
.Select(t => t.Teacher!.Name).ToList(),
|
||||||
session.TeachingTask.Classes
|
link.ExamSession.TeachingTask.Classes
|
||||||
.Select(c => c.AdministrativeClass!.Name).ToList(),
|
.Select(c => c.AdministrativeClass!.Name).ToList(),
|
||||||
room.Classroom!.Name,
|
link.ExamRoom!.Classroom!.Name,
|
||||||
room.Classroom.Building!.Name,
|
link.ExamRoom.Classroom.Building!.Name,
|
||||||
room.Classroom.Building.Campus!.Name,
|
link.ExamRoom.Classroom.Building.Campus!.Name,
|
||||||
room.ExamDate,
|
link.ExamRoom.ExamDate,
|
||||||
room.StartPeriod,
|
link.ExamRoom.StartPeriod,
|
||||||
room.PeriodCount,
|
link.ExamRoom.PeriodCount,
|
||||||
room.ExamPlan!.Name,
|
link.ExamRoom.ExamPlan!.Name,
|
||||||
room.Invigilators
|
link.ExamRoom.Invigilators
|
||||||
.Select(i => i.Teacher!.Name).ToList(),
|
.Select(i => i.Teacher!.Name).ToList(),
|
||||||
null,
|
null,
|
||||||
room.UpdatedAt), slotLookup));
|
link.ExamRoom.UpdatedAt))
|
||||||
}
|
.ToListAsync(cancellationToken);
|
||||||
}
|
|
||||||
|
result.AddRange(mixedSessions.Select(x =>
|
||||||
|
MapToEntryDto(x, slotLookup)));
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -535,9 +512,8 @@ public sealed class TimetableDataService(AppDbContext db)
|
|||||||
return date.AddDays(1 - dayOfWeek);
|
return date.AddDays(1 - dayOfWeek);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool MatchesMixedResource(
|
private static IQueryable<ExamRoomSession> ApplyMixedResourceFilter(
|
||||||
ExamRoomAssignment room,
|
IQueryable<ExamRoomSession> source,
|
||||||
ExamSession session,
|
|
||||||
TimetableResourceType resourceType,
|
TimetableResourceType resourceType,
|
||||||
Guid resourceId,
|
Guid resourceId,
|
||||||
Guid? studentId)
|
Guid? studentId)
|
||||||
@@ -545,20 +521,28 @@ public sealed class TimetableDataService(AppDbContext db)
|
|||||||
switch (resourceType)
|
switch (resourceType)
|
||||||
{
|
{
|
||||||
case TimetableResourceType.Classroom:
|
case TimetableResourceType.Classroom:
|
||||||
return room.ClassroomId == resourceId;
|
return source.Where(link =>
|
||||||
|
link.ExamRoom!.ClassroomId == resourceId);
|
||||||
case TimetableResourceType.Teacher:
|
case TimetableResourceType.Teacher:
|
||||||
return room.Invigilators.Any(i => i.TeacherId == resourceId) ||
|
return source.Where(link =>
|
||||||
session.TeachingTask!.Teachers.Any(
|
link.ExamRoom!.Invigilators.Any(i =>
|
||||||
t => t.TeacherId == resourceId);
|
i.TeacherId == resourceId) ||
|
||||||
|
link.ExamSession!.TeachingTask!.Teachers.Any(t =>
|
||||||
|
t.TeacherId == resourceId));
|
||||||
case TimetableResourceType.Class:
|
case TimetableResourceType.Class:
|
||||||
if (studentId.HasValue)
|
if (studentId.HasValue)
|
||||||
return session.TeachingTask!.Classes.Any(c =>
|
{
|
||||||
|
return source.Where(link =>
|
||||||
|
link.ExamSession!.TeachingTask!.Classes.Any(c =>
|
||||||
c.AdministrativeClassId == resourceId) ||
|
c.AdministrativeClassId == resourceId) ||
|
||||||
room.Seats.Any(s => s.StudentId == studentId.Value);
|
link.ExamRoom!.Seats.Any(s =>
|
||||||
return session.TeachingTask!.Classes.Any(c =>
|
s.StudentId == studentId.Value));
|
||||||
c.AdministrativeClassId == resourceId);
|
}
|
||||||
|
return source.Where(link =>
|
||||||
|
link.ExamSession!.TeachingTask!.Classes.Any(c =>
|
||||||
|
c.AdministrativeClassId == resourceId));
|
||||||
default:
|
default:
|
||||||
return true;
|
return source;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using Jiaowu.Api.Domain.Academic;
|
using Jiaowu.Api.Domain.Academic;
|
||||||
using Jiaowu.Api.Infrastructure.Exams;
|
using Jiaowu.Api.Infrastructure.Exams;
|
||||||
using Jiaowu.Api.Infrastructure.Persistence;
|
using Jiaowu.Api.Infrastructure.Persistence;
|
||||||
|
using Jiaowu.Api.Infrastructure.Timetables;
|
||||||
using Microsoft.Data.Sqlite;
|
using Microsoft.Data.Sqlite;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
@@ -55,6 +56,48 @@ public sealed class ExamArrangementServiceTests
|
|||||||
Assert.Contains("1个教学班场次处理完成", result.Message);
|
Assert.Contains("1个教学班场次处理完成", result.Message);
|
||||||
Assert.Contains("1名监考教师", result.Message);
|
Assert.Contains("1名监考教师", result.Message);
|
||||||
Assert.Contains("程序设计", result.Message);
|
Assert.Contains("程序设计", result.Message);
|
||||||
|
|
||||||
|
plan = await db.ExamPlans.SingleAsync(x => x.Id == plan.Id);
|
||||||
|
plan.Status = ExamPlanStatus.Published;
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
db.ChangeTracker.Clear();
|
||||||
|
|
||||||
|
var timetableService = new TimetableDataService(db);
|
||||||
|
var classroomTimetable = await timetableService.BuildAsync(
|
||||||
|
TimetableResourceType.Classroom,
|
||||||
|
seed.LargeClassroom.Id,
|
||||||
|
seed.Term.Id,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
CancellationToken.None);
|
||||||
|
var unrelatedClassroomTimetable = await timetableService.BuildAsync(
|
||||||
|
TimetableResourceType.Classroom,
|
||||||
|
seed.SmallClassroom.Id,
|
||||||
|
seed.Term.Id,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
CancellationToken.None);
|
||||||
|
var invigilatorTimetable = await timetableService.BuildAsync(
|
||||||
|
TimetableResourceType.Teacher,
|
||||||
|
room.Invigilators.Single().TeacherId,
|
||||||
|
seed.Term.Id,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
"程序设计",
|
||||||
|
Assert.Single(classroomTimetable!.ExamEntries).CourseName);
|
||||||
|
Assert.Empty(unrelatedClassroomTimetable!.ExamEntries);
|
||||||
|
Assert.Contains(
|
||||||
|
"监考:监考教师",
|
||||||
|
Assert.Single(invigilatorTimetable!.ExamEntries).TeacherNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
Reference in New Issue
Block a user