From be842cc1d5652c0cecb0f123f0ee172a77dc694d Mon Sep 17 00:00:00 2001 From: biss Date: Tue, 28 Jul 2026 11:13:23 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=BB=9F=E8=AE=A1=E6=8A=A5?= =?UTF-8?q?=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/StatisticsController.cs | 9 +- .../Timetables/TimetableDataService.cs | 162 ++++++++++-------- 2 files changed, 96 insertions(+), 75 deletions(-) diff --git a/src/Jiaowu.Api/Controllers/StatisticsController.cs b/src/Jiaowu.Api/Controllers/StatisticsController.cs index b712558..06f1d43 100644 --- a/src/Jiaowu.Api/Controllers/StatisticsController.cs +++ b/src/Jiaowu.Api/Controllers/StatisticsController.cs @@ -957,13 +957,14 @@ public sealed class StatisticsController( async token => { var source = await factory(token); - if (source.Result is not null || source.Value is null) + var data = source.Result is ObjectResult { Value: not null } objectResult + ? objectResult.Value + : source.Value; + if (data is null) throw new InvalidOperationException( "Statistics cache source did not return a successful value."); - return JsonSerializer.SerializeToElement( - source.Value, - source.Value.GetType()); + return JsonSerializer.SerializeToElement(data, data.GetType()); }, AppCacheProfile.Analytics, [ diff --git a/src/Jiaowu.Api/Infrastructure/Timetables/TimetableDataService.cs b/src/Jiaowu.Api/Infrastructure/Timetables/TimetableDataService.cs index 9d96012..d8f4e72 100644 --- a/src/Jiaowu.Api/Infrastructure/Timetables/TimetableDataService.cs +++ b/src/Jiaowu.Api/Infrastructure/Timetables/TimetableDataService.cs @@ -350,49 +350,103 @@ public sealed class TimetableDataService(AppDbContext db) MapToEntryDto(x, slotLookup))); // ── Mixed-room sessions (ExamRoomAssignment) ── - var mixedQuery = db.ExamRoomSessions.AsNoTracking() - .Where(link => - link.ExamRoom!.ExamPlan!.AcademicTermId == academicTermId && - link.ExamRoom.ExamPlan.Status == ExamPlanStatus.Published); - - mixedQuery = ApplyMixedResourceFilter( - mixedQuery, resourceType, resourceId, studentId); - - var mixedEntries = await mixedQuery - .OrderBy(link => link.ExamRoom!.ExamDate) - .ThenBy(link => link.ExamRoom!.StartPeriod) - .ThenBy(link => link.ExamSession!.TeachingTask!.Course!.Code) - .Select(link => new ExamSessionProjection( - link.ExamRoomId, - link.ExamSession!.TeachingTaskId, - link.ExamSession.TeachingTask!.TaskNumber, - link.ExamSession.TeachingTask.Name, - link.ExamSession.TeachingTask.Course!.Code, - link.ExamSession.TeachingTask.Course.Name, - link.ExamSession.TeachingTask.Teachers - .OrderByDescending(t => t.IsPrimary) - .Select(t => t.Teacher!.Name).ToList(), - link.ExamSession.TeachingTask.Classes - .Select(c => c.AdministrativeClass!.Name).ToList(), - link.ExamRoom!.Classroom!.Name, - link.ExamRoom.Classroom.Building!.Name, - link.ExamRoom.Classroom.Building.Campus!.Name, - link.ExamRoom.ExamDate, - link.ExamRoom.StartPeriod, - link.ExamRoom.PeriodCount, - link.ExamRoom.ExamPlan!.Name, - link.ExamRoom.Invigilators - .Select(i => i.Teacher!.Name).ToList(), - link.ExamRoom.ExamPlan.Notes, - link.ExamRoom.UpdatedAt)) + // Load all mixed rooms for this term into memory, then filter + var allMixedRooms = await db.ExamRooms.AsNoTracking() + .Where(room => + room.ExamPlan!.AcademicTermId == academicTermId && + room.ExamPlan.Status == ExamPlanStatus.Published) + .Include(room => room.ExamPlan) + .Include(room => room.Classroom) + .ThenInclude(c => c!.Building) + .ThenInclude(b => b!.Campus) + .Include(room => room.Invigilators) + .ThenInclude(i => i.Teacher) + .Include(room => room.SessionLinks) + .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() .ToListAsync(cancellationToken); - result.AddRange(mixedEntries.Select(x => - MapToEntryDto(x, slotLookup))); + foreach (var room in allMixedRooms) + { + foreach (var link in room.SessionLinks) + { + var session = link.ExamSession; + if (session == null || session.TeachingTask == null) continue; + + // Apply resource filter in memory + if (!MatchesMixedResource( + 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) + .Select(t => t.Teacher!.Name).ToList(), + session.TeachingTask.Classes + .Select(c => c.AdministrativeClass!.Name).ToList(), + room.Classroom!.Name, + room.Classroom.Building!.Name, + room.Classroom.Building.Campus!.Name, + room.ExamDate, + room.StartPeriod, + room.PeriodCount, + room.ExamPlan!.Name, + room.Invigilators + .Select(i => i.Teacher!.Name).ToList(), + null, + room.UpdatedAt), slotLookup)); + } + } return result; } + private static bool MatchesMixedResource( + ExamRoomAssignment room, + ExamSession session, + TimetableResourceType resourceType, + Guid resourceId, + Guid? studentId) + { + switch (resourceType) + { + case TimetableResourceType.Classroom: + return room.ClassroomId == resourceId; + case TimetableResourceType.Teacher: + return room.Invigilators.Any(i => i.TeacherId == resourceId) || + session.TeachingTask!.Teachers.Any( + t => t.TeacherId == resourceId); + case TimetableResourceType.Class: + if (studentId.HasValue) + return session.TeachingTask!.Classes.Any(c => + c.AdministrativeClassId == resourceId) || + room.Seats.Any(s => s.StudentId == studentId.Value); + return session.TeachingTask!.Classes.Any(c => + c.AdministrativeClassId == resourceId); + default: + return true; + } + } + private IQueryable ApplyLegacyResourceFilter( IQueryable source, TimetableResourceType resourceType, @@ -427,40 +481,6 @@ public sealed class TimetableDataService(AppDbContext db) } } - private static IQueryable ApplyMixedResourceFilter( - IQueryable source, - TimetableResourceType resourceType, - Guid resourceId, - Guid? studentId) - { - switch (resourceType) - { - case TimetableResourceType.Classroom: - return source.Where(link => - link.ExamRoom!.ClassroomId == resourceId); - case TimetableResourceType.Teacher: - return source.Where(link => - link.ExamRoom!.Invigilators.Any(i => - i.TeacherId == resourceId) || - link.ExamSession!.TeachingTask!.Teachers.Any(t => - t.TeacherId == resourceId)); - case TimetableResourceType.Class: - 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)); - } - return source.Where(link => - link.ExamSession!.TeachingTask!.Classes.Any(c => - c.AdministrativeClassId == resourceId)); - default: - return source; - } - } - private static TimetableEntryDto MapToEntryDto( ExamSessionProjection x, IReadOnlyDictionary slotLookup)