排课
Build and publish Jiaowu packages and container image / Test, package and publish (push) Successful in 32m28s
Build and publish Jiaowu packages and container image / Test, package and publish (push) Successful in 32m28s
This commit is contained in:
@@ -224,6 +224,78 @@ public sealed class MakeupExamsController(
|
||||
return await SaveAsync(session.Id, true, cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("plans/{planId:guid}/sessions/batch")]
|
||||
[Authorize(Roles = Managers)]
|
||||
public async Task<ActionResult> CreateSessionsBatch(
|
||||
Guid planId,
|
||||
CreateMakeupExamSessionsBatchRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var plan = await db.MakeupExamPlans.FindAsync([planId], cancellationToken);
|
||||
if (plan is null) return NotFound();
|
||||
if (plan.Status != MakeupExamPlanStatus.Draft)
|
||||
return ConflictProblem("已发布的补考计划不能调整场次。");
|
||||
|
||||
var taskIds = request.TeachingTaskIds.Distinct().ToArray();
|
||||
if (taskIds.Length == 0)
|
||||
return ValidationProblem("请至少选择一个教学班。");
|
||||
if (taskIds.Length > 100)
|
||||
return ValidationProblem("一次最多选择100个教学班。");
|
||||
|
||||
var timeResult = ResolveExamTime(plan.AcademicTermId,
|
||||
request.ExamDate, request.StartPeriod, request.PeriodCount);
|
||||
if (timeResult.Error is not null) return timeResult.Error;
|
||||
var (startsAt, endsAt) = (timeResult.StartsAt, timeResult.EndsAt);
|
||||
|
||||
var validTaskIds = await db.TeachingTasks.AsNoTracking()
|
||||
.Where(x => x.AcademicTermId == plan.AcademicTermId &&
|
||||
x.Status == TeachingTaskStatus.Published)
|
||||
.WhereIn(taskIds, x => x.Id)
|
||||
.Select(x => x.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
if (validTaskIds.Count != taskIds.Length)
|
||||
return ValidationProblem("所选教学班包含不存在、跨学期或未发布的教学班。");
|
||||
|
||||
var duplicateTaskIds = await db.MakeupExamSessions.AsNoTracking()
|
||||
.Where(x => x.MakeupExamPlanId == planId)
|
||||
.WhereIn(taskIds, x => x.TeachingTaskId)
|
||||
.Select(x => x.TeachingTaskId)
|
||||
.ToListAsync(cancellationToken);
|
||||
if (duplicateTaskIds.Count > 0)
|
||||
return ConflictProblem($"所选教学班中有 {duplicateTaskIds.Count} 个已在当前补考计划中安排。");
|
||||
|
||||
foreach (var taskId in taskIds)
|
||||
{
|
||||
var validation = await ValidateSessionAsync(plan, null,
|
||||
taskId, null, null, startsAt, endsAt, cancellationToken);
|
||||
if (validation is not null) return validation;
|
||||
}
|
||||
|
||||
db.MakeupExamSessions.AddRange(taskIds.Select(taskId => new MakeupExamSession
|
||||
{
|
||||
MakeupExamPlanId = planId,
|
||||
TeachingTaskId = taskId,
|
||||
ExamDate = request.ExamDate,
|
||||
StartPeriod = request.StartPeriod,
|
||||
PeriodCount = request.PeriodCount,
|
||||
StartsAt = startsAt,
|
||||
EndsAt = endsAt,
|
||||
RequiredBuildingId = request.RequiredBuildingId,
|
||||
RequiredInvigilatorCount = request.RequiredInvigilatorCount,
|
||||
Notes = Normalize(request.Notes)
|
||||
}));
|
||||
|
||||
try
|
||||
{
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
return Ok(new { createdCount = taskIds.Length });
|
||||
}
|
||||
catch (DbUpdateException)
|
||||
{
|
||||
return ConflictProblem("批量创建补考场次失败,关联数据可能已发生变化。");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut("plans/{planId:guid}/sessions/{id:guid}")]
|
||||
[Authorize(Roles = Managers)]
|
||||
public async Task<ActionResult> UpdateSession(
|
||||
@@ -292,9 +364,16 @@ public sealed class MakeupExamsController(
|
||||
[Authorize(Roles = Managers)]
|
||||
public async Task<ActionResult> AutoArrange(
|
||||
Guid planId,
|
||||
ExamAutoArrangeRequest? request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await arrangementService.ArrangeAsync(planId, cancellationToken);
|
||||
request ??= new ExamAutoArrangeRequest();
|
||||
var result = await arrangementService.ArrangeAsync(
|
||||
planId,
|
||||
request.SessionIds,
|
||||
request.AssignClassrooms,
|
||||
request.AssignInvigilators,
|
||||
cancellationToken);
|
||||
if (!result.Success)
|
||||
return ConflictProblem(result.Message);
|
||||
return Ok(new { message = result.Message });
|
||||
@@ -555,19 +634,43 @@ public sealed class MakeupExamsController(
|
||||
x.TeachingTask!.TaskNumber,
|
||||
CourseName = x.TeachingTask.Course!.Name,
|
||||
ClassroomName = x.Classroom != null ? x.Classroom.Name : "待分配",
|
||||
x.StartsAt,
|
||||
Students = x.Enrollments.OrderBy(e => e.Student!.StudentNumber)
|
||||
.Select(e => new
|
||||
{
|
||||
e.StudentId,
|
||||
e.Student!.StudentNumber,
|
||||
e.Student.Name,
|
||||
ClassName = e.Student.AdministrativeClass!.Name,
|
||||
e.Reason,
|
||||
e.MakeupScore
|
||||
}).ToList()
|
||||
x.StartsAt
|
||||
}).FirstOrDefaultAsync(cancellationToken);
|
||||
return session is null ? NotFound() : Ok(session);
|
||||
if (session is null) return NotFound();
|
||||
|
||||
var students = await db.MakeupExamEnrollments.AsNoTracking()
|
||||
.Where(e => e.MakeupExamSessionId == id)
|
||||
.OrderBy(e => e.Student!.StudentNumber)
|
||||
.Select(e => new
|
||||
{
|
||||
e.StudentId,
|
||||
e.Student!.StudentNumber,
|
||||
e.Student.Name,
|
||||
ClassName = e.Student.AdministrativeClass!.Name,
|
||||
e.Reason,
|
||||
e.MakeupScore
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
session.Id,
|
||||
session.TeachingTaskId,
|
||||
session.TaskNumber,
|
||||
session.CourseName,
|
||||
session.ClassroomName,
|
||||
session.StartsAt,
|
||||
Students = students.Select((student, index) => new
|
||||
{
|
||||
student.StudentId,
|
||||
student.StudentNumber,
|
||||
student.Name,
|
||||
student.ClassName,
|
||||
student.Reason,
|
||||
student.MakeupScore,
|
||||
SeatNumber = (index + 1).ToString("D3")
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
@@ -699,7 +802,7 @@ public sealed class MakeupExamsController(
|
||||
.Select(x => (Guid?)x.Id).FirstOrDefaultAsync(cancellationToken);
|
||||
if (!studentId.HasValue)
|
||||
return ConflictProblem("当前账号未关联学生档案。");
|
||||
return Ok(await db.MakeupExamEnrollments.AsNoTracking()
|
||||
var schedule = await db.MakeupExamEnrollments.AsNoTracking()
|
||||
.Where(x => x.StudentId == studentId &&
|
||||
x.MakeupExamSession!.MakeupExamPlan!.Status == MakeupExamPlanStatus.Published)
|
||||
.OrderBy(x => x.MakeupExamSession!.ExamDate)
|
||||
@@ -724,7 +827,49 @@ public sealed class MakeupExamsController(
|
||||
x.MakeupScore,
|
||||
IsMakeup = true
|
||||
})
|
||||
.ToListAsync(cancellationToken));
|
||||
.ToListAsync(cancellationToken);
|
||||
if (schedule.Count == 0) return Ok(schedule);
|
||||
|
||||
var sessionIds = schedule.Select(x => x.Id).Distinct().ToArray();
|
||||
var seatRows = await db.MakeupExamEnrollments.AsNoTracking()
|
||||
.WhereIn(sessionIds, x => x.MakeupExamSessionId)
|
||||
.OrderBy(x => x.Student!.StudentNumber)
|
||||
.Select(x => new
|
||||
{
|
||||
x.MakeupExamSessionId,
|
||||
x.StudentId,
|
||||
x.Student!.StudentNumber
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
var seatNumbers = seatRows
|
||||
.GroupBy(x => x.MakeupExamSessionId)
|
||||
.SelectMany(group => group.Select((row, index) => new
|
||||
{
|
||||
row.MakeupExamSessionId,
|
||||
row.StudentId,
|
||||
SeatNumber = (index + 1).ToString("D3")
|
||||
}))
|
||||
.ToDictionary(x => (x.MakeupExamSessionId, x.StudentId), x => x.SeatNumber);
|
||||
|
||||
return Ok(schedule.Select(x => new
|
||||
{
|
||||
x.Id,
|
||||
x.PlanName,
|
||||
x.ExamDate,
|
||||
x.StartPeriod,
|
||||
x.PeriodCount,
|
||||
x.StartsAt,
|
||||
x.EndsAt,
|
||||
x.TaskNumber,
|
||||
x.CourseCode,
|
||||
x.CourseName,
|
||||
x.ClassroomName,
|
||||
x.BuildingName,
|
||||
x.Reason,
|
||||
x.MakeupScore,
|
||||
SeatNumber = seatNumbers.GetValueOrDefault((x.Id, studentId.Value)),
|
||||
x.IsMakeup
|
||||
}));
|
||||
}
|
||||
if (scope.IsInRole(SystemRoles.Teacher))
|
||||
{
|
||||
@@ -941,6 +1086,15 @@ public sealed record CreateMakeupExamSessionRequest(
|
||||
IReadOnlyCollection<Guid>? InvigilatorIds,
|
||||
[MaxLength(500)] string? Notes);
|
||||
|
||||
public sealed record CreateMakeupExamSessionsBatchRequest(
|
||||
[Required] IReadOnlyCollection<Guid> TeachingTaskIds,
|
||||
DateOnly ExamDate,
|
||||
[Range(1, 30)] int StartPeriod,
|
||||
[Range(1, 6)] int PeriodCount,
|
||||
Guid? RequiredBuildingId,
|
||||
[Range(1, 10)] int RequiredInvigilatorCount,
|
||||
[MaxLength(500)] string? Notes);
|
||||
|
||||
public sealed record EnrollStudentsRequest(
|
||||
[Required] IReadOnlyCollection<Guid> StudentIds);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user