修复统计报表

This commit is contained in:
2026-07-28 11:13:23 +08:00 Unverified
parent 7e14b22296
commit be842cc1d5
2 changed files with 96 additions and 75 deletions
@@ -957,13 +957,14 @@ public sealed class StatisticsController(
async token => async token =>
{ {
var source = await factory(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( throw new InvalidOperationException(
"Statistics cache source did not return a successful value."); "Statistics cache source did not return a successful value.");
return JsonSerializer.SerializeToElement( return JsonSerializer.SerializeToElement(data, data.GetType());
source.Value,
source.Value.GetType());
}, },
AppCacheProfile.Analytics, AppCacheProfile.Analytics,
[ [
@@ -350,49 +350,103 @@ public sealed class TimetableDataService(AppDbContext db)
MapToEntryDto(x, slotLookup))); MapToEntryDto(x, slotLookup)));
// ── Mixed-room sessions (ExamRoomAssignment) ── // ── Mixed-room sessions (ExamRoomAssignment) ──
var mixedQuery = db.ExamRoomSessions.AsNoTracking() // Load all mixed rooms for this term into memory, then filter
.Where(link => var allMixedRooms = await db.ExamRooms.AsNoTracking()
link.ExamRoom!.ExamPlan!.AcademicTermId == academicTermId && .Where(room =>
link.ExamRoom.ExamPlan.Status == ExamPlanStatus.Published); room.ExamPlan!.AcademicTermId == academicTermId &&
room.ExamPlan.Status == ExamPlanStatus.Published)
mixedQuery = ApplyMixedResourceFilter( .Include(room => room.ExamPlan)
mixedQuery, resourceType, resourceId, studentId); .Include(room => room.Classroom)
.ThenInclude(c => c!.Building)
var mixedEntries = await mixedQuery .ThenInclude(b => b!.Campus)
.OrderBy(link => link.ExamRoom!.ExamDate) .Include(room => room.Invigilators)
.ThenBy(link => link.ExamRoom!.StartPeriod) .ThenInclude(i => i.Teacher)
.ThenBy(link => link.ExamSession!.TeachingTask!.Course!.Code) .Include(room => room.SessionLinks)
.Select(link => new ExamSessionProjection( .ThenInclude(link => link.ExamSession)
link.ExamRoomId, .ThenInclude(s => s!.TeachingTask)
link.ExamSession!.TeachingTaskId, .ThenInclude(t => t!.Course)
link.ExamSession.TeachingTask!.TaskNumber, .Include(room => room.SessionLinks)
link.ExamSession.TeachingTask.Name, .ThenInclude(link => link.ExamSession)
link.ExamSession.TeachingTask.Course!.Code, .ThenInclude(s => s!.TeachingTask)
link.ExamSession.TeachingTask.Course.Name, .ThenInclude(t => t!.Teachers)
link.ExamSession.TeachingTask.Teachers .ThenInclude(tt => tt.Teacher)
.OrderByDescending(t => t.IsPrimary) .Include(room => room.SessionLinks)
.Select(t => t.Teacher!.Name).ToList(), .ThenInclude(link => link.ExamSession)
link.ExamSession.TeachingTask.Classes .ThenInclude(s => s!.TeachingTask)
.Select(c => c.AdministrativeClass!.Name).ToList(), .ThenInclude(t => t!.Classes)
link.ExamRoom!.Classroom!.Name, .ThenInclude(tc => tc.AdministrativeClass)
link.ExamRoom.Classroom.Building!.Name, .Include(room => room.Seats)
link.ExamRoom.Classroom.Building.Campus!.Name, .AsSplitQuery()
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))
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
result.AddRange(mixedEntries.Select(x => foreach (var room in allMixedRooms)
MapToEntryDto(x, slotLookup))); {
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; 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<ExamSession> ApplyLegacyResourceFilter( private IQueryable<ExamSession> ApplyLegacyResourceFilter(
IQueryable<ExamSession> source, IQueryable<ExamSession> source,
TimetableResourceType resourceType, TimetableResourceType resourceType,
@@ -427,40 +481,6 @@ public sealed class TimetableDataService(AppDbContext db)
} }
} }
private static IQueryable<ExamRoomSession> ApplyMixedResourceFilter(
IQueryable<ExamRoomSession> 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( private static TimetableEntryDto MapToEntryDto(
ExamSessionProjection x, ExamSessionProjection x,
IReadOnlyDictionary<int, TimetableSlotDto> slotLookup) IReadOnlyDictionary<int, TimetableSlotDto> slotLookup)