补考增强
This commit is contained in:
@@ -18,10 +18,13 @@ public sealed class MakeupExamsController(
|
||||
AppDbContext db,
|
||||
ICurrentUserDataScope currentUserDataScope,
|
||||
MakeupExamEligibilityService eligibilityService,
|
||||
MakeupExamArrangementService arrangementService) : ControllerBase
|
||||
MakeupExamArrangementService arrangementService,
|
||||
MakeupExamAutoJobQueue autoJobQueue) : ControllerBase
|
||||
{
|
||||
private const string Managers =
|
||||
SystemRoles.SuperAdmin + "," + SystemRoles.AcademicAdmin;
|
||||
private const string ScoreEnterers =
|
||||
SystemRoles.SuperAdmin + "," + SystemRoles.AcademicAdmin + "," + SystemRoles.Teacher;
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// Plans
|
||||
@@ -284,6 +287,74 @@ public sealed class MakeupExamsController(
|
||||
return Ok(new { message = result.Message });
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// Auto-create (background job)
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
[HttpPost("plans/{planId:guid}/auto-create")]
|
||||
[Authorize(Roles = Managers)]
|
||||
public async Task<ActionResult> AutoCreate(
|
||||
Guid planId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var plan = await db.MakeupExamPlans
|
||||
.FirstOrDefaultAsync(x => x.Id == planId, cancellationToken);
|
||||
if (plan is null) return NotFound();
|
||||
if (plan.Status != MakeupExamPlanStatus.Draft)
|
||||
return ConflictProblem("只有草稿状态的补考计划可以自动生成。");
|
||||
|
||||
// Check for existing active job
|
||||
var existing = await db.MakeupExamAutoJobs.AsNoTracking()
|
||||
.Where(x => x.MakeupExamPlanId == planId &&
|
||||
(x.Status == MakeupExamAutoJobStatus.Queued ||
|
||||
x.Status == MakeupExamAutoJobStatus.Running))
|
||||
.OrderByDescending(x => x.CreatedAt)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
if (existing is not null)
|
||||
return Ok(new { jobId = existing.Id, status = existing.Status.ToString(),
|
||||
message = "该计划已有正在执行的任务。" });
|
||||
|
||||
var job = new MakeupExamAutoJob
|
||||
{
|
||||
MakeupExamPlanId = planId
|
||||
};
|
||||
db.MakeupExamAutoJobs.Add(job);
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
|
||||
autoJobQueue.Enqueue(job.Id);
|
||||
|
||||
return AcceptedAtAction(nameof(GetAutoJob), new { jobId = job.Id },
|
||||
new { jobId = job.Id, status = job.Status.ToString() });
|
||||
}
|
||||
|
||||
[HttpGet("auto-jobs/{jobId:guid}")]
|
||||
[Authorize(Roles = Managers)]
|
||||
public async Task<ActionResult> GetAutoJob(
|
||||
Guid jobId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var job = await db.MakeupExamAutoJobs.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.Id == jobId, cancellationToken);
|
||||
if (job is null) return NotFound();
|
||||
return Ok(new
|
||||
{
|
||||
job.Id,
|
||||
job.MakeupExamPlanId,
|
||||
Status = job.Status.ToString(),
|
||||
job.TotalCourses,
|
||||
job.ProcessedCourses,
|
||||
job.CreatedSessions,
|
||||
job.EnrolledStudents,
|
||||
Messages = !string.IsNullOrEmpty(job.MessagesJson)
|
||||
? System.Text.Json.JsonSerializer.Deserialize<List<string>>(job.MessagesJson)
|
||||
: new List<string>(),
|
||||
job.ErrorMessage,
|
||||
job.CreatedAt,
|
||||
job.StartedAt,
|
||||
job.CompletedAt
|
||||
});
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// Enrollments
|
||||
// ═══════════════════════════════════════════
|
||||
@@ -390,7 +461,7 @@ public sealed class MakeupExamsController(
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
[HttpPut("sessions/{id:guid}/scores")]
|
||||
[Authorize(Roles = Managers)]
|
||||
[Authorize(Roles = ScoreEnterers)]
|
||||
public async Task<ActionResult> RecordScores(
|
||||
Guid id,
|
||||
List<RecordMakeupScoreRequest> scores,
|
||||
@@ -399,11 +470,23 @@ public sealed class MakeupExamsController(
|
||||
var session = await db.MakeupExamSessions
|
||||
.Include(x => x.MakeupExamPlan)
|
||||
.Include(x => x.Enrollments)
|
||||
.Include(x => x.TeachingTask!)
|
||||
.ThenInclude(x => x.Teachers)
|
||||
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
if (session is null) return NotFound();
|
||||
if (session.MakeupExamPlan!.Status != MakeupExamPlanStatus.Published)
|
||||
return ConflictProblem("只有已发布的补考计划可以录入成绩。");
|
||||
|
||||
// Teachers can only record scores for courses they teach
|
||||
if (!IsManager())
|
||||
{
|
||||
var userId = currentUserDataScope.Current.UserId;
|
||||
var teaches = session.TeachingTask!.Teachers
|
||||
.Any(t => t.Teacher!.UserId == userId);
|
||||
if (!teaches)
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
var enrollmentByStudent = session.Enrollments.ToDictionary(e => e.StudentId);
|
||||
|
||||
foreach (var entry in scores)
|
||||
@@ -413,19 +496,22 @@ public sealed class MakeupExamsController(
|
||||
|
||||
enrollment.MakeupScore = entry.Score;
|
||||
|
||||
// Update the grade record
|
||||
// Update the grade record — 补考合格按60分记
|
||||
if (enrollment.SourceGradeRecordId.HasValue)
|
||||
{
|
||||
var record = await db.GradeRecords
|
||||
.Include(x => x.GradeSheet)
|
||||
.FirstOrDefaultAsync(x => x.Id == enrollment.SourceGradeRecordId.Value,
|
||||
cancellationToken);
|
||||
if (record is not null)
|
||||
{
|
||||
record.ExamStatus = GradeExamStatus.Makeup;
|
||||
record.TotalScore = entry.Score;
|
||||
record.GradePoint = GradeCalculator.CalculateGradePoint(entry.Score);
|
||||
record.Notes = "补考成绩";
|
||||
// Cap passing score at 60
|
||||
var cappedScore = entry.Score >= 60 ? 60m : entry.Score;
|
||||
record.TotalScore = cappedScore;
|
||||
record.GradePoint = GradeCalculator.CalculateGradePoint(cappedScore);
|
||||
record.Notes = entry.Score >= 60
|
||||
? $"补考合格(原始{entry.Score}分,按60分记)"
|
||||
: $"补考不合格({entry.Score}分)";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -650,6 +736,50 @@ public sealed class MakeupExamsController(
|
||||
return Ok(Array.Empty<object>());
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// My teaching sessions (for score entry)
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
[HttpGet("my-teaching-sessions")]
|
||||
[Authorize(Roles = SystemRoles.Teacher)]
|
||||
public async Task<ActionResult> GetMyTeachingSessions(CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = currentUserDataScope.Current.UserId;
|
||||
return Ok(await db.MakeupExamSessions.AsNoTracking()
|
||||
.Where(x => x.MakeupExamPlan!.Status == MakeupExamPlanStatus.Published &&
|
||||
x.TeachingTask!.Teachers.Any(t => t.Teacher!.UserId == userId))
|
||||
.OrderBy(x => x.ExamDate).ThenBy(x => x.StartPeriod)
|
||||
.Select(x => new
|
||||
{
|
||||
x.Id,
|
||||
PlanName = x.MakeupExamPlan!.Name,
|
||||
x.ExamDate,
|
||||
x.StartPeriod,
|
||||
x.PeriodCount,
|
||||
x.StartsAt,
|
||||
x.EndsAt,
|
||||
x.TeachingTask!.TaskNumber,
|
||||
CourseCode = x.TeachingTask.Course!.Code,
|
||||
CourseName = x.TeachingTask.Course.Name,
|
||||
ClassroomName = x.Classroom != null ? x.Classroom.Name : null,
|
||||
BuildingName = x.Classroom != null ? x.Classroom.Building!.Name : null,
|
||||
EnrolledCount = x.Enrollments.Count,
|
||||
GradedCount = x.Enrollments.Count(e => e.MakeupScore != null),
|
||||
Enrollments = 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(),
|
||||
IsMakeup = true
|
||||
})
|
||||
.ToListAsync(cancellationToken));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// Private helpers
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user