修复统计报表
This commit is contained in:
@@ -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,
|
||||
[
|
||||
|
||||
@@ -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<ExamSession> ApplyLegacyResourceFilter(
|
||||
IQueryable<ExamSession> source,
|
||||
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(
|
||||
ExamSessionProjection x,
|
||||
IReadOnlyDictionary<int, TimetableSlotDto> slotLookup)
|
||||
|
||||
Reference in New Issue
Block a user