优化考试排考
This commit is contained in:
@@ -7,8 +7,27 @@ namespace Jiaowu.Api.Infrastructure.Exams;
|
||||
|
||||
public sealed class ExamArrangementService(AppDbContext db)
|
||||
{
|
||||
private sealed record RoomOccupancy(Guid ClassroomId, DateTime StartsAt, DateTime EndsAt);
|
||||
private sealed record InvigilatorOccupancy(Guid TeacherId, DateTime StartsAt, DateTime EndsAt);
|
||||
private sealed record RoomGroupKey(
|
||||
Guid CourseId,
|
||||
DateOnly ExamDate,
|
||||
int StartPeriod,
|
||||
int PeriodCount,
|
||||
Guid? RequiredBuildingId);
|
||||
|
||||
private sealed record RoomOccupancy(
|
||||
Guid ClassroomId,
|
||||
DateTime StartsAt,
|
||||
DateTime EndsAt);
|
||||
|
||||
private sealed record InvigilatorOccupancy(
|
||||
Guid TeacherId,
|
||||
DateTime StartsAt,
|
||||
DateTime EndsAt);
|
||||
|
||||
private sealed record CandidateSeat(
|
||||
Guid ExamSessionId,
|
||||
Guid StudentId,
|
||||
string StudentNumber);
|
||||
|
||||
public async Task<ExamArrangementResult> ArrangeAsync(
|
||||
Guid planId,
|
||||
@@ -30,6 +49,12 @@ public sealed class ExamArrangementService(AppDbContext db)
|
||||
.Include(x => x.Sessions)
|
||||
.ThenInclude(x => x.TeachingTask)
|
||||
.ThenInclude(x => x!.Course)
|
||||
.Include(x => x.Rooms)
|
||||
.ThenInclude(x => x.SessionLinks)
|
||||
.Include(x => x.Rooms)
|
||||
.ThenInclude(x => x.Seats)
|
||||
.Include(x => x.Rooms)
|
||||
.ThenInclude(x => x.Invigilators)
|
||||
.FirstOrDefaultAsync(x => x.Id == planId, cancellationToken);
|
||||
|
||||
if (plan is null)
|
||||
@@ -46,206 +71,524 @@ public sealed class ExamArrangementService(AppDbContext db)
|
||||
plan.Sessions.Count(x => requestedIds.Contains(x.Id)) != requestedIds.Count)
|
||||
return ExamArrangementResult.Fail("所选场次不存在或不属于当前考试计划。");
|
||||
|
||||
var sessions = plan.Sessions
|
||||
.Where(x => requestedIds.Count == 0 || requestedIds.Contains(x.Id))
|
||||
.OrderBy(x => x.ExamDate)
|
||||
.ThenBy(x => x.StartPeriod)
|
||||
.ThenByDescending(x => x.RequiredInvigilatorCount)
|
||||
.ToList();
|
||||
if (sessions.Count == 0)
|
||||
return ExamArrangementResult.Fail("没有可处理的考试场次。");
|
||||
|
||||
var termId = plan.AcademicTermId;
|
||||
var timeSlots = await db.ScheduleTimeSlots.AsNoTracking()
|
||||
.Where(x => x.AcademicTermId == termId && x.IsEnabled)
|
||||
.Where(x => x.AcademicTermId == plan.AcademicTermId && x.IsEnabled)
|
||||
.OrderBy(x => x.PeriodNumber)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (timeSlots.Count == 0)
|
||||
return ExamArrangementResult.Fail("当前学期未配置上课时间表,无法计算考试时间段。");
|
||||
|
||||
var timeSlotLookup = timeSlots.ToDictionary(x => x.PeriodNumber);
|
||||
var rosterCounts = (await TeachingTaskRosterQuery.LoadForTasksAsync(
|
||||
db,
|
||||
sessions.Select(x => x.TeachingTaskId),
|
||||
cancellationToken))
|
||||
.GroupBy(x => x.TeachingTaskId)
|
||||
.ToDictionary(x => x.Key, x => x.Count());
|
||||
|
||||
int assignedRooms = 0;
|
||||
int assignedInvigilators = 0;
|
||||
int unavailableRooms = 0;
|
||||
int unavailableInvigilators = 0;
|
||||
var messages = new List<string>();
|
||||
|
||||
// Track occupied time slots to avoid conflicts
|
||||
var occupiedRooms = sessions
|
||||
.Where(x => x.ClassroomId.HasValue)
|
||||
.Select(x => new RoomOccupancy(x.ClassroomId!.Value, x.StartsAt, x.EndsAt))
|
||||
.ToList();
|
||||
|
||||
var occupiedInvigilators = sessions
|
||||
.SelectMany(x => x.Invigilators.Select(i =>
|
||||
new InvigilatorOccupancy(i.TeacherId, x.StartsAt, x.EndsAt)))
|
||||
.ToList();
|
||||
|
||||
foreach (var session in sessions)
|
||||
{
|
||||
// Compute StartsAt/EndsAt from time slots
|
||||
foreach (var session in plan.Sessions)
|
||||
ComputeTimesFromSlots(session, timeSlotLookup);
|
||||
var studentCount = rosterCounts.GetValueOrDefault(session.TeachingTaskId);
|
||||
|
||||
// ── Auto-assign classroom ──
|
||||
if (assignClassrooms && !session.ClassroomId.HasValue)
|
||||
var explicitlySelected = plan.Sessions
|
||||
.Where(x => requestedIds.Count == 0 || requestedIds.Contains(x.Id))
|
||||
.ToList();
|
||||
if (explicitlySelected.Count == 0)
|
||||
return ExamArrangementResult.Fail("没有可处理的考试场次。");
|
||||
|
||||
var selectedKeys = explicitlySelected
|
||||
.Select(GroupKey)
|
||||
.ToHashSet();
|
||||
var sessions = plan.Sessions
|
||||
.Where(x => selectedKeys.Contains(GroupKey(x)))
|
||||
.OrderBy(x => x.ExamDate)
|
||||
.ThenBy(x => x.StartPeriod)
|
||||
.ThenBy(x => x.TeachingTask!.Course!.Code)
|
||||
.ThenBy(x => x.TeachingTask!.TaskNumber)
|
||||
.ToList();
|
||||
var sessionIds = sessions.Select(x => x.Id).ToHashSet();
|
||||
|
||||
var roster = await TeachingTaskRosterQuery.LoadForTasksAsync(
|
||||
db,
|
||||
sessions.Select(x => x.TeachingTaskId),
|
||||
cancellationToken);
|
||||
var sessionByTaskId = sessions
|
||||
.GroupBy(x => x.TeachingTaskId)
|
||||
.ToDictionary(x => x.Key, x => x.First());
|
||||
|
||||
var replacedRooms = assignClassrooms
|
||||
? plan.Rooms
|
||||
.Where(room => room.SessionLinks.Any(link =>
|
||||
sessionIds.Contains(link.ExamSessionId)))
|
||||
.ToList()
|
||||
: [];
|
||||
var replacedRoomIds = replacedRooms.Select(x => x.Id).ToHashSet();
|
||||
if (replacedRooms.Count > 0)
|
||||
db.ExamRooms.RemoveRange(replacedRooms);
|
||||
|
||||
var occupiedRooms = await LoadOccupiedRoomsAsync(
|
||||
plan,
|
||||
sessionIds,
|
||||
replacedRoomIds,
|
||||
cancellationToken);
|
||||
var rooms = await db.Classrooms.AsNoTracking()
|
||||
.Include(x => x.Building)
|
||||
.Where(x => x.IsEnabled)
|
||||
.OrderBy(x => x.Building!.Name)
|
||||
.ThenBy(x => x.Name)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var targetRooms = new List<ExamRoomAssignment>();
|
||||
var messages = new List<string>();
|
||||
var seatedStudents = 0;
|
||||
var unavailableStudents = 0;
|
||||
|
||||
if (assignClassrooms)
|
||||
{
|
||||
foreach (var session in sessions)
|
||||
{
|
||||
var room = await FindBestClassroomAsync(
|
||||
session, studentCount, occupiedRooms, cancellationToken);
|
||||
if (room is not null)
|
||||
session.ClassroomId = null;
|
||||
if (session.Invigilators.Count > 0)
|
||||
{
|
||||
session.ClassroomId = room.Id;
|
||||
occupiedRooms.Add(new RoomOccupancy(room.Id, session.StartsAt, session.EndsAt));
|
||||
assignedRooms++;
|
||||
messages.Add(
|
||||
$"“{session.TeachingTask!.Course!.Name}”→{room.Name}({room.Capacity}座)");
|
||||
}
|
||||
else
|
||||
{
|
||||
unavailableRooms++;
|
||||
messages.Add(
|
||||
$"“{session.TeachingTask!.Course!.Name}”:无可用考场(需≥{studentCount}座)");
|
||||
db.ExamSessionInvigilators.RemoveRange(session.Invigilators);
|
||||
session.Invigilators.Clear();
|
||||
}
|
||||
}
|
||||
// ── Auto-assign invigilators ──
|
||||
var currentInvigilatorCount = session.Invigilators.Count;
|
||||
var needed = session.RequiredInvigilatorCount - currentInvigilatorCount;
|
||||
if (assignInvigilators && needed > 0)
|
||||
|
||||
foreach (var group in sessions
|
||||
.GroupBy(GroupKey)
|
||||
.OrderBy(x => x.Key.ExamDate)
|
||||
.ThenBy(x => x.Key.StartPeriod)
|
||||
.ThenBy(x => x.First().TeachingTask!.Course!.Code))
|
||||
{
|
||||
var courseTeacherIds = session.TeachingTask!.Teachers
|
||||
.Select(x => x.TeacherId).ToHashSet();
|
||||
var newlyAssigned = await FindInvigilatorsAsync(
|
||||
session, needed, courseTeacherIds,
|
||||
occupiedInvigilators, cancellationToken);
|
||||
foreach (var teacher in newlyAssigned)
|
||||
var groupSessions = group.ToList();
|
||||
var candidates = InterleaveCandidates(
|
||||
groupSessions,
|
||||
roster,
|
||||
sessionByTaskId);
|
||||
if (candidates.Count == 0)
|
||||
{
|
||||
session.Invigilators.Add(new ExamSessionInvigilator
|
||||
messages.Add(
|
||||
$"“{groupSessions[0].TeachingTask!.Course!.Name}”没有有效考生。");
|
||||
continue;
|
||||
}
|
||||
|
||||
var availableRooms = rooms
|
||||
.Where(room =>
|
||||
(!group.Key.RequiredBuildingId.HasValue ||
|
||||
room.BuildingId == group.Key.RequiredBuildingId.Value) &&
|
||||
occupiedRooms.All(occupied =>
|
||||
occupied.ClassroomId != room.Id ||
|
||||
!ExamConflictRules.TimeOverlaps(
|
||||
occupied.StartsAt,
|
||||
occupied.EndsAt,
|
||||
groupSessions[0].StartsAt,
|
||||
groupSessions[0].EndsAt)))
|
||||
.ToList();
|
||||
var selectedRooms = SelectRooms(
|
||||
availableRooms,
|
||||
candidates.Count);
|
||||
if (selectedRooms.Sum(x => x.Capacity) < candidates.Count)
|
||||
{
|
||||
unavailableStudents += candidates.Count;
|
||||
messages.Add(
|
||||
$"“{groupSessions[0].TeachingTask!.Course!.Name}”缺少足够考场容量," +
|
||||
$"需 {candidates.Count} 座、可用 {selectedRooms.Sum(x => x.Capacity)} 座。");
|
||||
continue;
|
||||
}
|
||||
|
||||
var offset = 0;
|
||||
foreach (var classroom in selectedRooms)
|
||||
{
|
||||
var roomCandidates = candidates
|
||||
.Skip(offset)
|
||||
.Take(classroom.Capacity)
|
||||
.ToList();
|
||||
if (roomCandidates.Count == 0) break;
|
||||
offset += roomCandidates.Count;
|
||||
|
||||
var room = new ExamRoomAssignment
|
||||
{
|
||||
ExamPlanId = plan.Id,
|
||||
CourseId = group.Key.CourseId,
|
||||
ClassroomId = classroom.Id,
|
||||
ExamDate = group.Key.ExamDate,
|
||||
StartPeriod = group.Key.StartPeriod,
|
||||
PeriodCount = group.Key.PeriodCount,
|
||||
StartsAt = groupSessions[0].StartsAt,
|
||||
EndsAt = groupSessions[0].EndsAt,
|
||||
RequiredInvigilatorCount =
|
||||
groupSessions.Max(x => x.RequiredInvigilatorCount),
|
||||
SessionLinks = roomCandidates
|
||||
.Select(x => x.ExamSessionId)
|
||||
.Distinct()
|
||||
.Select(sessionId => new ExamRoomSession
|
||||
{
|
||||
ExamSessionId = sessionId
|
||||
})
|
||||
.ToList(),
|
||||
Seats = roomCandidates
|
||||
.Select((candidate, index) =>
|
||||
new ExamSeatAssignment
|
||||
{
|
||||
ExamSessionId = candidate.ExamSessionId,
|
||||
StudentId = candidate.StudentId,
|
||||
SeatNumber = index + 1
|
||||
})
|
||||
.ToList()
|
||||
};
|
||||
db.ExamRooms.Add(room);
|
||||
targetRooms.Add(room);
|
||||
occupiedRooms.Add(new RoomOccupancy(
|
||||
classroom.Id,
|
||||
room.StartsAt,
|
||||
room.EndsAt));
|
||||
seatedStudents += roomCandidates.Count;
|
||||
}
|
||||
|
||||
messages.Add(
|
||||
$"“{groupSessions[0].TeachingTask!.Course!.Name}”" +
|
||||
$"{groupSessions.Count}个教学班混排至{selectedRooms.Count}个考场。");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
targetRooms.AddRange(plan.Rooms.Where(room =>
|
||||
room.SessionLinks.Any(link =>
|
||||
sessionIds.Contains(link.ExamSessionId))));
|
||||
var linkedSessionIds = targetRooms
|
||||
.SelectMany(x => x.SessionLinks)
|
||||
.Select(x => x.ExamSessionId)
|
||||
.ToHashSet();
|
||||
foreach (var session in sessions.Where(x =>
|
||||
!linkedSessionIds.Contains(x.Id) &&
|
||||
x.ClassroomId.HasValue))
|
||||
{
|
||||
var legacyRoom = CreateLegacyRoom(
|
||||
plan,
|
||||
session,
|
||||
roster.Where(x =>
|
||||
x.TeachingTaskId == session.TeachingTaskId));
|
||||
db.ExamRooms.Add(legacyRoom);
|
||||
targetRooms.Add(legacyRoom);
|
||||
session.ClassroomId = null;
|
||||
if (session.Invigilators.Count > 0)
|
||||
{
|
||||
db.ExamSessionInvigilators.RemoveRange(session.Invigilators);
|
||||
session.Invigilators.Clear();
|
||||
}
|
||||
}
|
||||
seatedStudents = targetRooms.Sum(x => x.Seats.Count);
|
||||
}
|
||||
|
||||
var assignedInvigilators = 0;
|
||||
var unavailableInvigilators = 0;
|
||||
if (assignInvigilators)
|
||||
{
|
||||
if (targetRooms.Count == 0)
|
||||
return ExamArrangementResult.Fail("尚未生成实际考场,请先分配考场。");
|
||||
|
||||
var occupiedInvigilators = plan.Rooms
|
||||
.Where(x => !replacedRoomIds.Contains(x.Id) &&
|
||||
!targetRooms.Any(target => target.Id == x.Id))
|
||||
.SelectMany(room => room.Invigilators.Select(item =>
|
||||
new InvigilatorOccupancy(
|
||||
item.TeacherId,
|
||||
room.StartsAt,
|
||||
room.EndsAt)))
|
||||
.Concat(plan.Sessions
|
||||
.Where(x => !sessionIds.Contains(x.Id))
|
||||
.SelectMany(session => session.Invigilators.Select(item =>
|
||||
new InvigilatorOccupancy(
|
||||
item.TeacherId,
|
||||
session.StartsAt,
|
||||
session.EndsAt))))
|
||||
.ToList();
|
||||
|
||||
foreach (var room in targetRooms
|
||||
.OrderBy(x => x.ExamDate)
|
||||
.ThenBy(x => x.StartPeriod)
|
||||
.ThenBy(x => x.ClassroomId))
|
||||
{
|
||||
var needed = room.RequiredInvigilatorCount -
|
||||
room.Invigilators.Count;
|
||||
if (needed <= 0) continue;
|
||||
|
||||
var linkedSessionIds = room.SessionLinks
|
||||
.Select(x => x.ExamSessionId)
|
||||
.ToHashSet();
|
||||
var excludedTeacherIds = sessions
|
||||
.Where(x => linkedSessionIds.Contains(x.Id))
|
||||
.SelectMany(x => x.TeachingTask!.Teachers)
|
||||
.Select(x => x.TeacherId)
|
||||
.ToHashSet();
|
||||
var teachers = await FindInvigilatorsAsync(
|
||||
room,
|
||||
needed,
|
||||
excludedTeacherIds,
|
||||
occupiedInvigilators,
|
||||
cancellationToken);
|
||||
foreach (var teacher in teachers)
|
||||
{
|
||||
room.Invigilators.Add(new ExamRoomInvigilator
|
||||
{
|
||||
ExamSessionId = session.Id,
|
||||
TeacherId = teacher.Id
|
||||
});
|
||||
occupiedInvigilators.Add(new InvigilatorOccupancy(
|
||||
teacher.Id, session.StartsAt, session.EndsAt));
|
||||
teacher.Id,
|
||||
room.StartsAt,
|
||||
room.EndsAt));
|
||||
assignedInvigilators++;
|
||||
}
|
||||
|
||||
if (newlyAssigned.Count < needed)
|
||||
{
|
||||
unavailableInvigilators += needed - newlyAssigned.Count;
|
||||
messages.Add(
|
||||
$"“{session.TeachingTask!.Course!.Name}”:仅找到{newlyAssigned.Count}/{needed}名监考教师");
|
||||
}
|
||||
if (teachers.Count < needed)
|
||||
unavailableInvigilators += needed - teachers.Count;
|
||||
}
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
|
||||
var expandedCount = sessions.Count - explicitlySelected.Count;
|
||||
var detail = messages.Count > 0
|
||||
? $" 详情:{string.Join(";", messages.Take(10))}"
|
||||
: "";
|
||||
return new ExamArrangementResult(
|
||||
true,
|
||||
$"{sessions.Count}个场次处理完成,分配{assignedRooms}个考场、{assignedInvigilators}名监考教师。" +
|
||||
(unavailableRooms > 0 ? $" {unavailableRooms}个场次暂无可用考场。" : "") +
|
||||
(unavailableInvigilators > 0 ? $" 仍缺{unavailableInvigilators}名监考教师。" : "") +
|
||||
(messages.Count > 0 ? $" 详情:{string.Join(";", messages.Take(10))}" : ""));
|
||||
$"{sessions.Count}个教学班场次处理完成,生成{targetRooms.Count}个实际考场," +
|
||||
$"安排{seatedStudents}名考生、{assignedInvigilators}名监考教师。" +
|
||||
(expandedCount > 0
|
||||
? $" 为保持混排完整性,自动包含同组{expandedCount}个场次。"
|
||||
: "") +
|
||||
(unavailableStudents > 0
|
||||
? $" {unavailableStudents}名考生尚未安排考场。"
|
||||
: "") +
|
||||
(unavailableInvigilators > 0
|
||||
? $" 仍缺{unavailableInvigilators}名监考教师。"
|
||||
: "") +
|
||||
detail);
|
||||
}
|
||||
|
||||
private async Task<List<RoomOccupancy>> LoadOccupiedRoomsAsync(
|
||||
ExamPlan plan,
|
||||
IReadOnlySet<Guid> targetSessionIds,
|
||||
IReadOnlySet<Guid> replacedRoomIds,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var occupied = plan.Rooms
|
||||
.Where(x => !replacedRoomIds.Contains(x.Id))
|
||||
.Select(x => new RoomOccupancy(
|
||||
x.ClassroomId,
|
||||
x.StartsAt,
|
||||
x.EndsAt))
|
||||
.ToList();
|
||||
occupied.AddRange(plan.Sessions
|
||||
.Where(x =>
|
||||
!targetSessionIds.Contains(x.Id) &&
|
||||
x.ClassroomId.HasValue)
|
||||
.Select(x => new RoomOccupancy(
|
||||
x.ClassroomId!.Value,
|
||||
x.StartsAt,
|
||||
x.EndsAt)));
|
||||
|
||||
var externalRooms = await db.ExamRooms.AsNoTracking()
|
||||
.Where(x =>
|
||||
x.ExamPlanId != plan.Id &&
|
||||
x.ExamPlan!.Status != ExamPlanStatus.Archived)
|
||||
.Select(x => new RoomOccupancy(
|
||||
x.ClassroomId,
|
||||
x.StartsAt,
|
||||
x.EndsAt))
|
||||
.ToListAsync(cancellationToken);
|
||||
occupied.AddRange(externalRooms);
|
||||
|
||||
var externalLegacyRooms = await db.ExamSessions.AsNoTracking()
|
||||
.Where(x =>
|
||||
x.ExamPlanId != plan.Id &&
|
||||
x.ExamPlan!.Status != ExamPlanStatus.Archived &&
|
||||
x.ClassroomId != null)
|
||||
.Select(x => new RoomOccupancy(
|
||||
x.ClassroomId!.Value,
|
||||
x.StartsAt,
|
||||
x.EndsAt))
|
||||
.ToListAsync(cancellationToken);
|
||||
occupied.AddRange(externalLegacyRooms);
|
||||
return occupied;
|
||||
}
|
||||
|
||||
private async Task<List<Teacher>> FindInvigilatorsAsync(
|
||||
ExamRoomAssignment room,
|
||||
int needed,
|
||||
HashSet<Guid> excludedTeacherIds,
|
||||
List<InvigilatorOccupancy> occupied,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var busyTeacherIds = occupied
|
||||
.Where(x => ExamConflictRules.TimeOverlaps(
|
||||
x.StartsAt,
|
||||
x.EndsAt,
|
||||
room.StartsAt,
|
||||
room.EndsAt))
|
||||
.Select(x => x.TeacherId)
|
||||
.ToHashSet();
|
||||
|
||||
var databaseBusyIds = await db.ExamRoomInvigilators.AsNoTracking()
|
||||
.Where(x =>
|
||||
x.ExamRoomId != room.Id &&
|
||||
x.ExamRoom!.ExamPlan!.Status != ExamPlanStatus.Archived &&
|
||||
x.ExamRoom.StartsAt < room.EndsAt &&
|
||||
room.StartsAt < x.ExamRoom.EndsAt)
|
||||
.Select(x => x.TeacherId)
|
||||
.ToListAsync(cancellationToken);
|
||||
foreach (var teacherId in databaseBusyIds)
|
||||
busyTeacherIds.Add(teacherId);
|
||||
|
||||
var legacyBusyIds = await db.ExamSessionInvigilators.AsNoTracking()
|
||||
.Where(x =>
|
||||
x.ExamSession!.ExamPlan!.Status != ExamPlanStatus.Archived &&
|
||||
x.ExamSession.StartsAt < room.EndsAt &&
|
||||
room.StartsAt < x.ExamSession.EndsAt)
|
||||
.Select(x => x.TeacherId)
|
||||
.ToListAsync(cancellationToken);
|
||||
foreach (var teacherId in legacyBusyIds)
|
||||
busyTeacherIds.Add(teacherId);
|
||||
foreach (var teacherId in excludedTeacherIds)
|
||||
busyTeacherIds.Add(teacherId);
|
||||
|
||||
var candidates = await InvigilatorCandidateQuery
|
||||
.Create(db, busyTeacherIds)
|
||||
.ToListAsync(cancellationToken);
|
||||
return candidates
|
||||
.OrderBy(_ => Random.Shared.Next())
|
||||
.Take(needed)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static List<Classroom> SelectRooms(
|
||||
IReadOnlyCollection<Classroom> availableRooms,
|
||||
int candidateCount)
|
||||
{
|
||||
var remainingRooms = availableRooms.ToList();
|
||||
var selected = new List<Classroom>();
|
||||
var remainingSeats = candidateCount;
|
||||
while (remainingSeats > 0 && remainingRooms.Count > 0)
|
||||
{
|
||||
var room = remainingRooms
|
||||
.Where(x => x.Capacity >= remainingSeats)
|
||||
.OrderBy(x => x.Capacity)
|
||||
.ThenBy(x => x.Name)
|
||||
.FirstOrDefault()
|
||||
?? remainingRooms
|
||||
.OrderByDescending(x => x.Capacity)
|
||||
.ThenBy(x => x.Name)
|
||||
.First();
|
||||
selected.Add(room);
|
||||
remainingRooms.Remove(room);
|
||||
remainingSeats -= room.Capacity;
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
private static List<CandidateSeat> InterleaveCandidates(
|
||||
IReadOnlyCollection<ExamSession> sessions,
|
||||
IReadOnlyCollection<TeachingTaskRosterEntry> roster,
|
||||
IReadOnlyDictionary<Guid, ExamSession> sessionByTaskId)
|
||||
{
|
||||
var sessionIds = sessions.Select(x => x.Id).ToHashSet();
|
||||
var queues = roster
|
||||
.Where(x =>
|
||||
sessionByTaskId.TryGetValue(
|
||||
x.TeachingTaskId,
|
||||
out var session) &&
|
||||
sessionIds.Contains(session.Id))
|
||||
.GroupBy(x => x.TeachingTaskId)
|
||||
.OrderBy(x => sessionByTaskId[x.Key].TeachingTask!.TaskNumber)
|
||||
.Select(group => new Queue<CandidateSeat>(
|
||||
group.OrderBy(x => x.StudentNumber)
|
||||
.Select(x => new CandidateSeat(
|
||||
sessionByTaskId[x.TeachingTaskId].Id,
|
||||
x.StudentId,
|
||||
x.StudentNumber))))
|
||||
.ToList();
|
||||
|
||||
var result = new List<CandidateSeat>();
|
||||
var assignedStudentIds = new HashSet<Guid>();
|
||||
while (queues.Any(x => x.Count > 0))
|
||||
{
|
||||
foreach (var queue in queues)
|
||||
{
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var candidate = queue.Dequeue();
|
||||
if (!assignedStudentIds.Add(candidate.StudentId))
|
||||
continue;
|
||||
result.Add(candidate);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ExamRoomAssignment CreateLegacyRoom(
|
||||
ExamPlan plan,
|
||||
ExamSession session,
|
||||
IEnumerable<TeachingTaskRosterEntry> roster)
|
||||
{
|
||||
var students = roster
|
||||
.OrderBy(x => x.StudentNumber)
|
||||
.ToList();
|
||||
return new ExamRoomAssignment
|
||||
{
|
||||
ExamPlanId = plan.Id,
|
||||
CourseId = session.TeachingTask!.CourseId,
|
||||
ClassroomId = session.ClassroomId!.Value,
|
||||
ExamDate = session.ExamDate,
|
||||
StartPeriod = session.StartPeriod,
|
||||
PeriodCount = session.PeriodCount,
|
||||
StartsAt = session.StartsAt,
|
||||
EndsAt = session.EndsAt,
|
||||
RequiredInvigilatorCount = session.RequiredInvigilatorCount,
|
||||
SessionLinks =
|
||||
[
|
||||
new ExamRoomSession
|
||||
{
|
||||
ExamSessionId = session.Id
|
||||
}
|
||||
],
|
||||
Seats = students.Select((student, index) =>
|
||||
new ExamSeatAssignment
|
||||
{
|
||||
ExamSessionId = session.Id,
|
||||
StudentId = student.StudentId,
|
||||
SeatNumber = index + 1
|
||||
}).ToList(),
|
||||
Invigilators = session.Invigilators.Select(x =>
|
||||
new ExamRoomInvigilator
|
||||
{
|
||||
TeacherId = x.TeacherId
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
private static RoomGroupKey GroupKey(ExamSession session) => new(
|
||||
session.TeachingTask!.CourseId,
|
||||
session.ExamDate,
|
||||
session.StartPeriod,
|
||||
session.PeriodCount,
|
||||
session.RequiredBuildingId);
|
||||
|
||||
private static void ComputeTimesFromSlots(
|
||||
ExamSession session,
|
||||
Dictionary<int, ScheduleTimeSlot> timeSlotLookup)
|
||||
IReadOnlyDictionary<int, ScheduleTimeSlot> timeSlotLookup)
|
||||
{
|
||||
var startSlot = timeSlotLookup.GetValueOrDefault(session.StartPeriod);
|
||||
var endSlot = timeSlotLookup.GetValueOrDefault(
|
||||
session.StartPeriod + session.PeriodCount - 1);
|
||||
if (startSlot is null || endSlot is null) return;
|
||||
|
||||
var examDate = session.ExamDate;
|
||||
session.StartsAt = examDate.ToDateTime(startSlot.StartsAt, DateTimeKind.Utc);
|
||||
session.EndsAt = examDate.ToDateTime(endSlot.EndsAt, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
private async Task<Classroom?> FindBestClassroomAsync(
|
||||
ExamSession session,
|
||||
int studentCount,
|
||||
List<RoomOccupancy> occupied,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var query = db.Classrooms.AsNoTracking()
|
||||
.Where(x => x.IsEnabled && x.Capacity >= studentCount);
|
||||
|
||||
if (session.RequiredBuildingId.HasValue)
|
||||
query = query.Where(x => x.BuildingId == session.RequiredBuildingId.Value);
|
||||
|
||||
// Exclude classrooms already occupied in-memory
|
||||
var occupiedRoomIds = occupied
|
||||
.Where(x => ExamConflictRules.TimeOverlaps(
|
||||
x.StartsAt, x.EndsAt, session.StartsAt, session.EndsAt))
|
||||
.Select(x => x.ClassroomId)
|
||||
.ToHashSet();
|
||||
|
||||
if (occupiedRoomIds.Count > 0)
|
||||
query = query.WhereNotIn(occupiedRoomIds, x => x.Id);
|
||||
|
||||
// Exclude classrooms occupied by DB sessions not yet tracked in memory
|
||||
var dbOccupiedRooms = await db.ExamSessions.AsNoTracking()
|
||||
.Where(x => x.ExamPlanId == session.ExamPlanId &&
|
||||
x.Id != session.Id &&
|
||||
x.ClassroomId != null &&
|
||||
x.StartsAt < session.EndsAt &&
|
||||
session.StartsAt < x.EndsAt)
|
||||
.Select(x => x.ClassroomId!.Value)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (dbOccupiedRooms.Count > 0)
|
||||
query = query.WhereNotIn(dbOccupiedRooms, x => x.Id);
|
||||
|
||||
return await query
|
||||
.OrderBy(x => x.Capacity)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<List<Teacher>> FindInvigilatorsAsync(
|
||||
ExamSession session,
|
||||
int needed,
|
||||
HashSet<Guid> excludeTeacherIds,
|
||||
List<InvigilatorOccupancy> occupied,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var busyTeacherIds = occupied
|
||||
.Where(x => ExamConflictRules.TimeOverlaps(
|
||||
x.StartsAt, x.EndsAt, session.StartsAt, session.EndsAt))
|
||||
.Select(x => x.TeacherId)
|
||||
.ToHashSet();
|
||||
|
||||
var dbBusyIds = await db.ExamSessionInvigilators.AsNoTracking()
|
||||
.Where(x => x.ExamSession!.ExamPlanId == session.ExamPlanId &&
|
||||
x.ExamSessionId != session.Id &&
|
||||
x.ExamSession!.StartsAt < session.EndsAt &&
|
||||
session.StartsAt < x.ExamSession.EndsAt)
|
||||
.Select(x => x.TeacherId)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
foreach (var id in dbBusyIds) busyTeacherIds.Add(id);
|
||||
foreach (var id in excludeTeacherIds) busyTeacherIds.Add(id);
|
||||
|
||||
var candidates = await InvigilatorCandidateQuery
|
||||
.Create(db, busyTeacherIds)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return candidates
|
||||
.OrderBy(_ => Random.Shared.Next())
|
||||
.Take(needed)
|
||||
.ToList();
|
||||
session.StartsAt = session.ExamDate.ToDateTime(
|
||||
startSlot.StartsAt,
|
||||
DateTimeKind.Utc);
|
||||
session.EndsAt = session.ExamDate.ToDateTime(
|
||||
endSlot.EndsAt,
|
||||
DateTimeKind.Utc);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record ExamArrangementResult(bool Success, string Message)
|
||||
{
|
||||
public static ExamArrangementResult Fail(string message) => new(false, message);
|
||||
public static ExamArrangementResult Fail(string message) =>
|
||||
new(false, message);
|
||||
}
|
||||
|
||||
@@ -163,11 +163,15 @@ public static class ExamSignInWorkbookExporter
|
||||
sheet.Row(7).Height = 27;
|
||||
|
||||
var row = 8;
|
||||
foreach (var student in session.Students.OrderBy(x => x.StudentNumber))
|
||||
foreach (var student in session.Students
|
||||
.OrderBy(x => x.SeatNumber)
|
||||
.ThenBy(x => x.StudentNumber))
|
||||
{
|
||||
var index = row - 7;
|
||||
sheet.Cell(row, 1).Value = index;
|
||||
sheet.Cell(row, 2).Value = index.ToString("D3");
|
||||
sheet.Cell(row, 2).Value =
|
||||
(student.SeatNumber > 0 ? student.SeatNumber : index)
|
||||
.ToString("D3");
|
||||
sheet.Cell(row, 3).Value = student.StudentNumber;
|
||||
sheet.Cell(row, 4).Value = student.Name;
|
||||
sheet.Cell(row, 5).Value = student.ClassName;
|
||||
@@ -297,4 +301,5 @@ public sealed record ExamSignInStudentData(
|
||||
Guid StudentId,
|
||||
string StudentNumber,
|
||||
string Name,
|
||||
string ClassName);
|
||||
string ClassName,
|
||||
int SeatNumber = 0);
|
||||
|
||||
@@ -61,6 +61,11 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
public DbSet<ExamSession> ExamSessions => Set<ExamSession>();
|
||||
public DbSet<ExamSessionInvigilator> ExamSessionInvigilators =>
|
||||
Set<ExamSessionInvigilator>();
|
||||
public DbSet<ExamRoomAssignment> ExamRooms => Set<ExamRoomAssignment>();
|
||||
public DbSet<ExamRoomSession> ExamRoomSessions => Set<ExamRoomSession>();
|
||||
public DbSet<ExamSeatAssignment> ExamSeats => Set<ExamSeatAssignment>();
|
||||
public DbSet<ExamRoomInvigilator> ExamRoomInvigilators =>
|
||||
Set<ExamRoomInvigilator>();
|
||||
public DbSet<MakeupExamPlan> MakeupExamPlans => Set<MakeupExamPlan>();
|
||||
public DbSet<MakeupExamSession> MakeupExamSessions => Set<MakeupExamSession>();
|
||||
public DbSet<MakeupExamSessionInvigilator> MakeupExamSessionInvigilators =>
|
||||
@@ -719,6 +724,65 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
entity.HasOne(x => x.Teacher).WithMany()
|
||||
.HasForeignKey(x => x.TeacherId).OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
builder.Entity<ExamRoomAssignment>(entity =>
|
||||
{
|
||||
entity.ToTable("ExamRooms");
|
||||
entity.HasIndex(x => new { x.ExamPlanId, x.StartsAt })
|
||||
.HasDatabaseName("IX_ExamRooms_Plan_Time");
|
||||
entity.HasIndex(x => new
|
||||
{
|
||||
x.ExamPlanId,
|
||||
x.ClassroomId,
|
||||
x.StartsAt
|
||||
})
|
||||
.HasDatabaseName("IX_ExamRooms_Plan_Room_Time");
|
||||
entity.HasIndex(x => x.CourseId)
|
||||
.HasDatabaseName("IX_ExamRooms_CourseId");
|
||||
entity.HasOne(x => x.ExamPlan).WithMany(x => x.Rooms)
|
||||
.HasForeignKey(x => x.ExamPlanId).OnDelete(DeleteBehavior.Cascade);
|
||||
entity.HasOne(x => x.Course).WithMany()
|
||||
.HasForeignKey(x => x.CourseId).OnDelete(DeleteBehavior.Restrict);
|
||||
entity.HasOne(x => x.Classroom).WithMany()
|
||||
.HasForeignKey(x => x.ClassroomId).OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
builder.Entity<ExamRoomSession>(entity =>
|
||||
{
|
||||
entity.ToTable("ExamRoomSessions");
|
||||
entity.HasKey(x => new { x.ExamRoomId, x.ExamSessionId });
|
||||
entity.HasIndex(x => x.ExamSessionId)
|
||||
.HasDatabaseName("IX_ExamRoomSessions_SessionId");
|
||||
entity.HasOne(x => x.ExamRoom).WithMany(x => x.SessionLinks)
|
||||
.HasForeignKey(x => x.ExamRoomId).OnDelete(DeleteBehavior.Cascade);
|
||||
entity.HasOne(x => x.ExamSession).WithMany(x => x.RoomLinks)
|
||||
.HasForeignKey(x => x.ExamSessionId).OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
builder.Entity<ExamSeatAssignment>(entity =>
|
||||
{
|
||||
entity.ToTable("ExamSeats");
|
||||
entity.HasKey(x => new { x.ExamRoomId, x.StudentId });
|
||||
entity.HasIndex(x => new { x.ExamSessionId, x.StudentId })
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_ExamSeats_Session_Student");
|
||||
entity.HasIndex(x => x.StudentId)
|
||||
.HasDatabaseName("IX_ExamSeats_StudentId");
|
||||
entity.HasOne(x => x.ExamRoom).WithMany(x => x.Seats)
|
||||
.HasForeignKey(x => x.ExamRoomId).OnDelete(DeleteBehavior.Cascade);
|
||||
entity.HasOne(x => x.ExamSession).WithMany(x => x.SeatAssignments)
|
||||
.HasForeignKey(x => x.ExamSessionId).OnDelete(DeleteBehavior.Restrict);
|
||||
entity.HasOne(x => x.Student).WithMany()
|
||||
.HasForeignKey(x => x.StudentId).OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
builder.Entity<ExamRoomInvigilator>(entity =>
|
||||
{
|
||||
entity.ToTable("ExamRoomInvigilators");
|
||||
entity.HasKey(x => new { x.ExamRoomId, x.TeacherId });
|
||||
entity.HasIndex(x => x.TeacherId)
|
||||
.HasDatabaseName("IX_ExamRoomInvigilators_TeacherId");
|
||||
entity.HasOne(x => x.ExamRoom).WithMany(x => x.Invigilators)
|
||||
.HasForeignKey(x => x.ExamRoomId).OnDelete(DeleteBehavior.Cascade);
|
||||
entity.HasOne(x => x.Teacher).WithMany()
|
||||
.HasForeignKey(x => x.TeacherId).OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
builder.Entity<MakeupExamPlan>(entity =>
|
||||
{
|
||||
entity.Property(x => x.Name).HasMaxLength(120);
|
||||
|
||||
@@ -64,6 +64,8 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
"20260727_34_course_adjustment_occurrences";
|
||||
private const string AcademicPlanningPrerequisitesMigration =
|
||||
"20260727_35_academic_planning_prerequisites";
|
||||
private const string ExamRoomMixingMigration =
|
||||
"20260727_36_exam_room_mixing";
|
||||
|
||||
public async Task MigrateAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -468,6 +470,19 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
AcademicPlanningPrerequisitesMigration,
|
||||
coursePrerequisitesExist ? [] : AcademicPlanningPrerequisiteStatements,
|
||||
cancellationToken);
|
||||
|
||||
var examRoomsExist = await db.Database
|
||||
.SqlQueryRaw<int>(
|
||||
"""
|
||||
SELECT COUNT(*) AS "Value"
|
||||
FROM sqlite_master
|
||||
WHERE type = 'table' AND name = 'ExamRooms'
|
||||
""")
|
||||
.AnyAsync(value => value > 0, cancellationToken);
|
||||
await ApplyMigrationAsync(
|
||||
ExamRoomMixingMigration,
|
||||
examRoomsExist ? [] : ExamRoomMixingStatements,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private async Task ApplyMigrationAsync(
|
||||
@@ -2132,4 +2147,112 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
ON "CoursePrerequisites" ("PrerequisiteCourseId");
|
||||
"""
|
||||
];
|
||||
|
||||
private static readonly string[] ExamRoomMixingStatements =
|
||||
[
|
||||
"""
|
||||
CREATE TABLE "ExamRooms" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_ExamRooms" PRIMARY KEY,
|
||||
"ExamPlanId" TEXT NOT NULL,
|
||||
"CourseId" TEXT NOT NULL,
|
||||
"ClassroomId" TEXT NOT NULL,
|
||||
"ExamDate" TEXT NOT NULL,
|
||||
"StartPeriod" INTEGER NOT NULL,
|
||||
"PeriodCount" INTEGER NOT NULL,
|
||||
"StartsAt" TEXT NOT NULL,
|
||||
"EndsAt" TEXT NOT NULL,
|
||||
"RequiredInvigilatorCount" INTEGER NOT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"UpdatedAt" TEXT NOT NULL,
|
||||
CONSTRAINT "FK_ExamRooms_ExamPlans_ExamPlanId"
|
||||
FOREIGN KEY ("ExamPlanId") REFERENCES "ExamPlans" ("Id")
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT "FK_ExamRooms_Courses_CourseId"
|
||||
FOREIGN KEY ("CourseId") REFERENCES "Courses" ("Id")
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT "FK_ExamRooms_Classrooms_ClassroomId"
|
||||
FOREIGN KEY ("ClassroomId") REFERENCES "Classrooms" ("Id")
|
||||
ON DELETE RESTRICT
|
||||
);
|
||||
""",
|
||||
"""
|
||||
CREATE INDEX "IX_ExamRooms_Plan_Time"
|
||||
ON "ExamRooms" ("ExamPlanId", "StartsAt");
|
||||
""",
|
||||
"""
|
||||
CREATE INDEX "IX_ExamRooms_Plan_Room_Time"
|
||||
ON "ExamRooms" ("ExamPlanId", "ClassroomId", "StartsAt");
|
||||
""",
|
||||
"""
|
||||
CREATE INDEX "IX_ExamRooms_CourseId"
|
||||
ON "ExamRooms" ("CourseId");
|
||||
""",
|
||||
"""
|
||||
CREATE INDEX "IX_ExamRooms_ClassroomId"
|
||||
ON "ExamRooms" ("ClassroomId");
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE "ExamRoomSessions" (
|
||||
"ExamRoomId" TEXT NOT NULL,
|
||||
"ExamSessionId" TEXT NOT NULL,
|
||||
CONSTRAINT "PK_ExamRoomSessions"
|
||||
PRIMARY KEY ("ExamRoomId", "ExamSessionId"),
|
||||
CONSTRAINT "FK_ExamRoomSessions_ExamRooms_ExamRoomId"
|
||||
FOREIGN KEY ("ExamRoomId") REFERENCES "ExamRooms" ("Id")
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT "FK_ExamRoomSessions_ExamSessions_ExamSessionId"
|
||||
FOREIGN KEY ("ExamSessionId") REFERENCES "ExamSessions" ("Id")
|
||||
ON DELETE RESTRICT
|
||||
);
|
||||
""",
|
||||
"""
|
||||
CREATE INDEX "IX_ExamRoomSessions_SessionId"
|
||||
ON "ExamRoomSessions" ("ExamSessionId");
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE "ExamSeats" (
|
||||
"ExamRoomId" TEXT NOT NULL,
|
||||
"ExamSessionId" TEXT NOT NULL,
|
||||
"StudentId" TEXT NOT NULL,
|
||||
"SeatNumber" INTEGER NOT NULL,
|
||||
CONSTRAINT "PK_ExamSeats"
|
||||
PRIMARY KEY ("ExamRoomId", "StudentId"),
|
||||
CONSTRAINT "FK_ExamSeats_ExamRooms_ExamRoomId"
|
||||
FOREIGN KEY ("ExamRoomId") REFERENCES "ExamRooms" ("Id")
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT "FK_ExamSeats_ExamSessions_ExamSessionId"
|
||||
FOREIGN KEY ("ExamSessionId") REFERENCES "ExamSessions" ("Id")
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT "FK_ExamSeats_Students_StudentId"
|
||||
FOREIGN KEY ("StudentId") REFERENCES "Students" ("Id")
|
||||
ON DELETE RESTRICT
|
||||
);
|
||||
""",
|
||||
"""
|
||||
CREATE UNIQUE INDEX "UX_ExamSeats_Session_Student"
|
||||
ON "ExamSeats" ("ExamSessionId", "StudentId");
|
||||
""",
|
||||
"""
|
||||
CREATE INDEX "IX_ExamSeats_StudentId"
|
||||
ON "ExamSeats" ("StudentId");
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE "ExamRoomInvigilators" (
|
||||
"ExamRoomId" TEXT NOT NULL,
|
||||
"TeacherId" TEXT NOT NULL,
|
||||
CONSTRAINT "PK_ExamRoomInvigilators"
|
||||
PRIMARY KEY ("ExamRoomId", "TeacherId"),
|
||||
CONSTRAINT "FK_ExamRoomInvigilators_ExamRooms_ExamRoomId"
|
||||
FOREIGN KEY ("ExamRoomId") REFERENCES "ExamRooms" ("Id")
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT "FK_ExamRoomInvigilators_Teachers_TeacherId"
|
||||
FOREIGN KEY ("TeacherId") REFERENCES "Teachers" ("Id")
|
||||
ON DELETE RESTRICT
|
||||
);
|
||||
""",
|
||||
"""
|
||||
CREATE INDEX "IX_ExamRoomInvigilators_TeacherId"
|
||||
ON "ExamRoomInvigilators" ("TeacherId");
|
||||
"""
|
||||
];
|
||||
}
|
||||
|
||||
src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260727105716_ExamRoomMixing.Designer.cs
Generated
+5139
File diff suppressed because it is too large
Load Diff
+196
@@ -0,0 +1,196 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ExamRoomMixing : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ExamRooms",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
ExamPlanId = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
CourseId = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
ClassroomId = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
ExamDate = table.Column<DateTime>(type: "date", nullable: false),
|
||||
StartPeriod = table.Column<int>(type: "int", nullable: false),
|
||||
PeriodCount = table.Column<int>(type: "int", nullable: false),
|
||||
StartsAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
EndsAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
RequiredInvigilatorCount = table.Column<int>(type: "int", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ExamRooms", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ExamRooms_Classrooms_ClassroomId",
|
||||
column: x => x.ClassroomId,
|
||||
principalTable: "Classrooms",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_ExamRooms_Courses_CourseId",
|
||||
column: x => x.CourseId,
|
||||
principalTable: "Courses",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_ExamRooms_ExamPlans_ExamPlanId",
|
||||
column: x => x.ExamPlanId,
|
||||
principalTable: "ExamPlans",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySQL:Charset", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ExamRoomInvigilators",
|
||||
columns: table => new
|
||||
{
|
||||
ExamRoomId = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
TeacherId = table.Column<Guid>(type: "char(36)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ExamRoomInvigilators", x => new { x.ExamRoomId, x.TeacherId });
|
||||
table.ForeignKey(
|
||||
name: "FK_ExamRoomInvigilators_ExamRooms_ExamRoomId",
|
||||
column: x => x.ExamRoomId,
|
||||
principalTable: "ExamRooms",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_ExamRoomInvigilators_Teachers_TeacherId",
|
||||
column: x => x.TeacherId,
|
||||
principalTable: "Teachers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
})
|
||||
.Annotation("MySQL:Charset", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ExamRoomSessions",
|
||||
columns: table => new
|
||||
{
|
||||
ExamRoomId = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
ExamSessionId = table.Column<Guid>(type: "char(36)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ExamRoomSessions", x => new { x.ExamRoomId, x.ExamSessionId });
|
||||
table.ForeignKey(
|
||||
name: "FK_ExamRoomSessions_ExamRooms_ExamRoomId",
|
||||
column: x => x.ExamRoomId,
|
||||
principalTable: "ExamRooms",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_ExamRoomSessions_ExamSessions_ExamSessionId",
|
||||
column: x => x.ExamSessionId,
|
||||
principalTable: "ExamSessions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
})
|
||||
.Annotation("MySQL:Charset", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ExamSeats",
|
||||
columns: table => new
|
||||
{
|
||||
ExamRoomId = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
StudentId = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
ExamSessionId = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
SeatNumber = table.Column<int>(type: "int", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ExamSeats", x => new { x.ExamRoomId, x.StudentId });
|
||||
table.ForeignKey(
|
||||
name: "FK_ExamSeats_ExamRooms_ExamRoomId",
|
||||
column: x => x.ExamRoomId,
|
||||
principalTable: "ExamRooms",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_ExamSeats_ExamSessions_ExamSessionId",
|
||||
column: x => x.ExamSessionId,
|
||||
principalTable: "ExamSessions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_ExamSeats_Students_StudentId",
|
||||
column: x => x.StudentId,
|
||||
principalTable: "Students",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
})
|
||||
.Annotation("MySQL:Charset", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ExamRoomInvigilators_TeacherId",
|
||||
table: "ExamRoomInvigilators",
|
||||
column: "TeacherId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ExamRooms_ClassroomId",
|
||||
table: "ExamRooms",
|
||||
column: "ClassroomId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ExamRooms_CourseId",
|
||||
table: "ExamRooms",
|
||||
column: "CourseId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ExamRooms_Plan_Room_Time",
|
||||
table: "ExamRooms",
|
||||
columns: new[] { "ExamPlanId", "ClassroomId", "StartsAt" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ExamRooms_Plan_Time",
|
||||
table: "ExamRooms",
|
||||
columns: new[] { "ExamPlanId", "StartsAt" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ExamRoomSessions_SessionId",
|
||||
table: "ExamRoomSessions",
|
||||
column: "ExamSessionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ExamSeats_StudentId",
|
||||
table: "ExamSeats",
|
||||
column: "StudentId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_ExamSeats_Session_Student",
|
||||
table: "ExamSeats",
|
||||
columns: new[] { "ExamSessionId", "StudentId" },
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ExamRoomInvigilators");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ExamRoomSessions");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ExamSeats");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ExamRooms");
|
||||
}
|
||||
}
|
||||
}
|
||||
+220
@@ -1558,6 +1558,119 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
b.ToTable("ExamPlans");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamRoomAssignment", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("ClassroomId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("CourseId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<DateTime>("EndsAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<DateTime>("ExamDate")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<Guid>("ExamPlanId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<int>("PeriodCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("RequiredInvigilatorCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("StartPeriod")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("StartsAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ClassroomId");
|
||||
|
||||
b.HasIndex("CourseId")
|
||||
.HasDatabaseName("IX_ExamRooms_CourseId");
|
||||
|
||||
b.HasIndex("ExamPlanId", "StartsAt")
|
||||
.HasDatabaseName("IX_ExamRooms_Plan_Time");
|
||||
|
||||
b.HasIndex("ExamPlanId", "ClassroomId", "StartsAt")
|
||||
.HasDatabaseName("IX_ExamRooms_Plan_Room_Time");
|
||||
|
||||
b.ToTable("ExamRooms", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamRoomInvigilator", b =>
|
||||
{
|
||||
b.Property<Guid>("ExamRoomId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("TeacherId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.HasKey("ExamRoomId", "TeacherId");
|
||||
|
||||
b.HasIndex("TeacherId")
|
||||
.HasDatabaseName("IX_ExamRoomInvigilators_TeacherId");
|
||||
|
||||
b.ToTable("ExamRoomInvigilators", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamRoomSession", b =>
|
||||
{
|
||||
b.Property<Guid>("ExamRoomId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("ExamSessionId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.HasKey("ExamRoomId", "ExamSessionId");
|
||||
|
||||
b.HasIndex("ExamSessionId")
|
||||
.HasDatabaseName("IX_ExamRoomSessions_SessionId");
|
||||
|
||||
b.ToTable("ExamRoomSessions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamSeatAssignment", b =>
|
||||
{
|
||||
b.Property<Guid>("ExamRoomId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("StudentId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("ExamSessionId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<int>("SeatNumber")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("ExamRoomId", "StudentId");
|
||||
|
||||
b.HasIndex("StudentId")
|
||||
.HasDatabaseName("IX_ExamSeats_StudentId");
|
||||
|
||||
b.HasIndex("ExamSessionId", "StudentId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_ExamSeats_Session_Student");
|
||||
|
||||
b.ToTable("ExamSeats", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamSession", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -4097,6 +4210,98 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
b.Navigation("AcademicTerm");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamRoomAssignment", b =>
|
||||
{
|
||||
b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom")
|
||||
.WithMany()
|
||||
.HasForeignKey("ClassroomId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Jiaowu.Api.Domain.Academic.Course", "Course")
|
||||
.WithMany()
|
||||
.HasForeignKey("CourseId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Jiaowu.Api.Domain.Academic.ExamPlan", "ExamPlan")
|
||||
.WithMany("Rooms")
|
||||
.HasForeignKey("ExamPlanId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Classroom");
|
||||
|
||||
b.Navigation("Course");
|
||||
|
||||
b.Navigation("ExamPlan");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamRoomInvigilator", b =>
|
||||
{
|
||||
b.HasOne("Jiaowu.Api.Domain.Academic.ExamRoomAssignment", "ExamRoom")
|
||||
.WithMany("Invigilators")
|
||||
.HasForeignKey("ExamRoomId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Jiaowu.Api.Domain.Academic.Teacher", "Teacher")
|
||||
.WithMany()
|
||||
.HasForeignKey("TeacherId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ExamRoom");
|
||||
|
||||
b.Navigation("Teacher");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamRoomSession", b =>
|
||||
{
|
||||
b.HasOne("Jiaowu.Api.Domain.Academic.ExamRoomAssignment", "ExamRoom")
|
||||
.WithMany("SessionLinks")
|
||||
.HasForeignKey("ExamRoomId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Jiaowu.Api.Domain.Academic.ExamSession", "ExamSession")
|
||||
.WithMany("RoomLinks")
|
||||
.HasForeignKey("ExamSessionId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ExamRoom");
|
||||
|
||||
b.Navigation("ExamSession");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamSeatAssignment", b =>
|
||||
{
|
||||
b.HasOne("Jiaowu.Api.Domain.Academic.ExamRoomAssignment", "ExamRoom")
|
||||
.WithMany("Seats")
|
||||
.HasForeignKey("ExamRoomId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Jiaowu.Api.Domain.Academic.ExamSession", "ExamSession")
|
||||
.WithMany("SeatAssignments")
|
||||
.HasForeignKey("ExamSessionId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student")
|
||||
.WithMany()
|
||||
.HasForeignKey("StudentId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ExamRoom");
|
||||
|
||||
b.Navigation("ExamSession");
|
||||
|
||||
b.Navigation("Student");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamSession", b =>
|
||||
{
|
||||
b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom")
|
||||
@@ -4832,12 +5037,27 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamPlan", b =>
|
||||
{
|
||||
b.Navigation("Rooms");
|
||||
|
||||
b.Navigation("Sessions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamRoomAssignment", b =>
|
||||
{
|
||||
b.Navigation("Invigilators");
|
||||
|
||||
b.Navigation("Seats");
|
||||
|
||||
b.Navigation("SessionLinks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamSession", b =>
|
||||
{
|
||||
b.Navigation("Invigilators");
|
||||
|
||||
b.Navigation("RoomLinks");
|
||||
|
||||
b.Navigation("SeatAssignments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeItem", b =>
|
||||
|
||||
Reference in New Issue
Block a user