队列异步执行,拆分查询消除笛卡尔积。
修改的文件(共 13 个)
┌───────────────────────────────────────────────────────────────┬──────────────────────────────────────────────────┐
│ 文件 │ 变更 │
├───────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤
│ Domain/Academic/ExamEntities.cs │ 新增 ExamPublishJob 实体 + ExamPublishJobStatus │
│ │ / ExamPublishJobKind 枚举 │
├───────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤
│ Domain/System/BackgroundJobOutboxMessage.cs │ BackgroundJobKind 新增 ExamPublish = 6 │
├───────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤
│ Infrastructure/BackgroundJobs/BackgroundJobOptions.cs │ 新增 ExamPublishConcurrency 配置项 │
├───────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤
│ Infrastructure/BackgroundJobs/BackgroundJobRunner.cs │ RunAsync 和 MarkJobRetryLimitExceeded 添加 │
│ │ ExamPublish 分支 │
├───────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤
│ Infrastructure/BackgroundJobs/RabbitMqBackgroundJobs.cs │ JobKinds 数组和 RoutingKey 添加 ExamPublish → │
│ │ "exam.publish" │
├───────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤
│ Infrastructure/BackgroundJobs/BackgroundJobOutboxPublisher.cs │ 启动恢复逻辑添加 ExamPublishJobs │
├───────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤
│ Infrastructure/Persistence/AppDbContext.cs │ 新增 ExamPublishJobs DbSet │
├───────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤
│ Controllers/OperationsController.cs │ CountFailedJobsAsync / GetFailedJobs │
│ │ 添加考试发布失败统计和筛选 │
├───────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤
│ Program.cs │ 校验 ExamPublishConcurrency + 注册 │
│ │ ExamPublishJobProcessor │
├───────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤
│ Infrastructure/Exams/ExamPublishJobs.cs │ 新文件 — │
│ │ ExamPublishJobProcessor,拆分查询校验后发布 │
├───────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤
│ Controllers/ExamsController.cs │ Publish 改为创建后台任务 + 202 返回;新增 GET │
│ │ publish-jobs/{id} / GET plans/{id}/publish-job │
├───────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤
│ Controllers/MakeupExamsController.cs │ 同上改造 │
├───────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤
│ tests/.../TeachingWorkflowRosterTests.cs │ 更新测试适配新的异步发布模式 │
└───────────────────────────────────────────────────────────────┴──────────────────────────────────────────────────┘
笛卡尔积消除
之前:一个 Include 链拉全部 → EF Core 生成 Sessions × Invigilators × RoomLinks × Seats 笛卡尔积
之后:
- 场次计数:db.ExamSessions.CountAsync(无 JOIN)
- 场次摘要:Select new { Id, ClassroomId, InvigilatorCount, RoomLinkCount }(只查所需列)
- 容量超限:db.ExamRooms.Select(r => new { SeatCount = r.Seats.Count, Capacity })(单表 JOIN)
- 课程冲突:db.ExamRoomSessions.Where(link => ...CourseId != link.ExamRoom!.CourseId)(独立查询)
- 每个查询只做自己需要的 JOIN,互不干扰
测试结果
213 通过,0 失败,0 跳过
配置方式
- BackgroundJobs__Transport=RabbitMq → 走 RabbitMQ 队列 jiaowu.background-jobs.exam.publish
- BackgroundJobs__Transport=InMemory(默认) → 走内存 Channel
- BackgroundJobs__ExamPublishConcurrency=1(默认,可调 1-16)
161 lines
5.0 KiB
C#
161 lines
5.0 KiB
C#
using Jiaowu.Api.Domain.Common;
|
|
|
|
namespace Jiaowu.Api.Domain.Academic;
|
|
|
|
public sealed class ExamPlan : EntityBase
|
|
{
|
|
public Guid AcademicTermId { get; set; }
|
|
public AcademicTerm? AcademicTerm { get; set; }
|
|
public required string Name { get; set; }
|
|
public ExamPlanStatus Status { get; set; } = ExamPlanStatus.Draft;
|
|
public string? Notes { get; set; }
|
|
public DateTime? PublishedAt { get; set; }
|
|
public ICollection<ExamSession> Sessions { get; set; } = [];
|
|
public ICollection<ExamRoomAssignment> Rooms { get; set; } = [];
|
|
}
|
|
|
|
public sealed class ExamArrangementJob : EntityBase
|
|
{
|
|
public ExamArrangementKind Kind { get; set; }
|
|
public Guid PlanId { get; set; }
|
|
public Guid? ActivePlanId { get; set; }
|
|
public Guid? RequestedByUserId { get; set; }
|
|
public string? SessionIdsJson { get; set; }
|
|
public bool AssignClassrooms { get; set; }
|
|
public bool AssignInvigilators { get; set; }
|
|
public ExamArrangementJobStatus Status { get; set; } =
|
|
ExamArrangementJobStatus.Queued;
|
|
public int TotalSessions { get; set; }
|
|
public int ProcessedSessions { get; set; }
|
|
public string? CurrentStep { get; set; }
|
|
public string? ResultMessage { get; set; }
|
|
public string? ErrorMessage { get; set; }
|
|
public DateTime? StartedAt { get; set; }
|
|
public DateTime? CompletedAt { get; set; }
|
|
}
|
|
|
|
public enum ExamArrangementKind
|
|
{
|
|
FormalExam = 1,
|
|
MakeupExam = 2
|
|
}
|
|
|
|
public enum ExamArrangementJobStatus
|
|
{
|
|
Queued = 1,
|
|
Running = 2,
|
|
Succeeded = 3,
|
|
Failed = 4
|
|
}
|
|
|
|
public sealed class ExamSession : EntityBase
|
|
{
|
|
public Guid ExamPlanId { get; set; }
|
|
public ExamPlan? ExamPlan { get; set; }
|
|
public Guid TeachingTaskId { get; set; }
|
|
public TeachingTask? TeachingTask { get; set; }
|
|
public Guid? ClassroomId { get; set; }
|
|
public Classroom? Classroom { get; set; }
|
|
public DateOnly ExamDate { get; set; }
|
|
public int StartPeriod { get; set; }
|
|
public int PeriodCount { get; set; } = 2;
|
|
public DateTime StartsAt { get; set; }
|
|
public DateTime EndsAt { get; set; }
|
|
public Guid? RequiredBuildingId { get; set; }
|
|
public Building? RequiredBuilding { get; set; }
|
|
public string? RequiredBuildingIds { get; set; }
|
|
public int RequiredInvigilatorCount { get; set; } = 2;
|
|
public string? Notes { get; set; }
|
|
public ICollection<ExamSessionInvigilator> Invigilators { get; set; } = [];
|
|
public ICollection<ExamRoomSession> RoomLinks { get; set; } = [];
|
|
public ICollection<ExamSeatAssignment> SeatAssignments { get; set; } = [];
|
|
}
|
|
|
|
public sealed class ExamSessionInvigilator
|
|
{
|
|
public Guid ExamSessionId { get; set; }
|
|
public ExamSession? ExamSession { get; set; }
|
|
public Guid TeacherId { get; set; }
|
|
public Teacher? Teacher { get; set; }
|
|
}
|
|
|
|
public sealed class ExamRoomAssignment : EntityBase
|
|
{
|
|
public Guid ExamPlanId { get; set; }
|
|
public ExamPlan? ExamPlan { get; set; }
|
|
public Guid CourseId { get; set; }
|
|
public Course? Course { get; set; }
|
|
public Guid ClassroomId { get; set; }
|
|
public Classroom? Classroom { get; set; }
|
|
public DateOnly ExamDate { get; set; }
|
|
public int StartPeriod { get; set; }
|
|
public int PeriodCount { get; set; }
|
|
public DateTime StartsAt { get; set; }
|
|
public DateTime EndsAt { get; set; }
|
|
public int RequiredInvigilatorCount { get; set; } = 2;
|
|
public ICollection<ExamRoomSession> SessionLinks { get; set; } = [];
|
|
public ICollection<ExamSeatAssignment> Seats { get; set; } = [];
|
|
public ICollection<ExamRoomInvigilator> Invigilators { get; set; } = [];
|
|
}
|
|
|
|
public sealed class ExamRoomSession
|
|
{
|
|
public Guid ExamRoomId { get; set; }
|
|
public ExamRoomAssignment? ExamRoom { get; set; }
|
|
public Guid ExamSessionId { get; set; }
|
|
public ExamSession? ExamSession { get; set; }
|
|
}
|
|
|
|
public sealed class ExamSeatAssignment
|
|
{
|
|
public Guid ExamRoomId { get; set; }
|
|
public ExamRoomAssignment? ExamRoom { get; set; }
|
|
public Guid ExamSessionId { get; set; }
|
|
public ExamSession? ExamSession { get; set; }
|
|
public Guid StudentId { get; set; }
|
|
public Student? Student { get; set; }
|
|
public int SeatNumber { get; set; }
|
|
}
|
|
|
|
public sealed class ExamRoomInvigilator
|
|
{
|
|
public Guid ExamRoomId { get; set; }
|
|
public ExamRoomAssignment? ExamRoom { get; set; }
|
|
public Guid TeacherId { get; set; }
|
|
public Teacher? Teacher { get; set; }
|
|
}
|
|
|
|
public enum ExamPlanStatus
|
|
{
|
|
Draft = 1,
|
|
Published = 2,
|
|
Archived = 3
|
|
}
|
|
|
|
public sealed class ExamPublishJob : EntityBase
|
|
{
|
|
public ExamPublishJobKind Kind { get; set; }
|
|
public Guid PlanId { get; set; }
|
|
public Guid? ActivePlanId { get; set; }
|
|
public Guid? RequestedByUserId { get; set; }
|
|
public ExamPublishJobStatus Status { get; set; } = ExamPublishJobStatus.Queued;
|
|
public string? CurrentStep { get; set; }
|
|
public string? ErrorMessage { get; set; }
|
|
public DateTime? StartedAt { get; set; }
|
|
public DateTime? CompletedAt { get; set; }
|
|
}
|
|
|
|
public enum ExamPublishJobKind
|
|
{
|
|
FormalExam = 1,
|
|
MakeupExam = 2
|
|
}
|
|
|
|
public enum ExamPublishJobStatus
|
|
{
|
|
Queued = 1,
|
|
Running = 2,
|
|
Succeeded = 3,
|
|
Failed = 4
|
|
}
|