修复统计报表
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user