using System.Text.Json; using Jiaowu.Api.Domain.Academic; using Jiaowu.Api.Infrastructure.Caching; using Jiaowu.Api.Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; namespace Jiaowu.Api.Infrastructure.Exams; public sealed class ExamArrangementJobProcessor( AppDbContext db, ExamArrangementService examArrangementService, MakeupExamArrangementService makeupExamArrangementService, IAppCache cache, ILogger logger) { public async Task ProcessAsync(Guid jobId, CancellationToken stoppingToken) { try { var job = await db.ExamArrangementJobs .FirstOrDefaultAsync(x => x.Id == jobId, stoppingToken); if (job is null || job.Status is ExamArrangementJobStatus.Succeeded or ExamArrangementJobStatus.Failed) { return; } job.Status = ExamArrangementJobStatus.Running; job.StartedAt ??= DateTime.UtcNow; job.CompletedAt = null; job.ErrorMessage = null; job.ResultMessage = null; job.CurrentStep = job.Kind == ExamArrangementKind.FormalExam ? "正在编排正式考试" : "正在编排补考"; await db.SaveChangesAsync(stoppingToken); var sessionIds = DeserializeSessionIds(job.SessionIdsJson); var result = job.Kind switch { ExamArrangementKind.FormalExam => await examArrangementService.ArrangeAsync( job.PlanId, sessionIds, job.AssignClassrooms, job.AssignInvigilators, stoppingToken), ExamArrangementKind.MakeupExam => await makeupExamArrangementService.ArrangeAsync( job.PlanId, sessionIds, job.AssignClassrooms, job.AssignInvigilators, stoppingToken), _ => throw new InvalidOperationException( $"不支持的考试编排类型:{job.Kind}。") }; if (!result.Success) { await MarkFailedAsync(jobId, result.Message); return; } job.Status = ExamArrangementJobStatus.Succeeded; job.ProcessedSessions = job.TotalSessions; job.CurrentStep = "编排完成"; job.ResultMessage = result.Message; job.ActivePlanId = null; job.CompletedAt = DateTime.UtcNow; await db.SaveChangesAsync(stoppingToken); if (job.Kind == ExamArrangementKind.FormalExam) { await cache.RemoveByTagAsync( AppCacheTags.Timetables, stoppingToken); } logger.LogInformation( "Exam arrangement job {JobId} for {Kind}/{PlanId} completed.", job.Id, job.Kind, job.PlanId); } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { logger.LogInformation( "Exam arrangement job {JobId} was interrupted by application shutdown.", jobId); throw; } catch (Exception exception) { logger.LogError(exception, "Exam arrangement job {JobId} failed.", jobId); await MarkFailedAsync(jobId, exception.GetBaseException().Message); } } private static IReadOnlyCollection? DeserializeSessionIds(string? json) { if (string.IsNullOrWhiteSpace(json)) return null; var sessionIds = JsonSerializer.Deserialize(json); return sessionIds is { Length: > 0 } ? sessionIds : null; } private async Task MarkFailedAsync(Guid jobId, string message) { db.ChangeTracker.Clear(); var job = await db.ExamArrangementJobs.FirstOrDefaultAsync( x => x.Id == jobId, CancellationToken.None); if (job is null) return; job.Status = ExamArrangementJobStatus.Failed; job.ActivePlanId = null; job.CurrentStep = "编排失败"; job.ErrorMessage = message.Length <= 2000 ? message : message[..2000]; job.CompletedAt = DateTime.UtcNow; await db.SaveChangesAsync(CancellationToken.None); } }