队列异步执行,拆分查询消除笛卡尔积。 修改的文件(共 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)
- ExamArrangementService.cs: SelectRooms() 使用 Capacity / 2 选择房间、计算剩余座位和总容量 - MakeupExamArrangementService.cs: 数据库查询用 x.Capacity >= enrolledCount * 2(等价于 Capacity/2 >= enrolledCount),消息显示有效座位数 2. 教学楼限制多选 - Domain: ExamSession 和 MakeupExamSession 新增 RequiredBuildingIds (JSON string),保留旧 RequiredBuildingId 向后兼容 - Service: RoomGroupKey 改为字符串键确保值相等;GroupKey() 合并新旧字段 - Controller: 请求 DTO 增加 RequiredBuildingIds (Guid 数组),响应包含该字段 - DB: MySQL 迁移 + SQLite migrator 添加新列 - Frontend: <el-select> 改为 multiple,新增 parseBuildingIds() 解析服务器返回的 JSON 3. 导出签名单后台任务 - 新增: ExamSignInExportJob 实体、ExamSignInExportJobProcessor、ExamSignInExportJobStatus 枚举 - BackgroundJobKind: 新增 ExamSignInExport = 5 - RabbitMQ: routing key exam.sign-in-export,队列 jiaowu.background-jobs.exam.sign-in-export - API: POST /sign-in-export 创建任务返回 202;GET /sign-in-exports/{jobId} 查询状态;GET /sign-in-exports/{jobId}/download 下载文件 - Frontend: 导出改为异步任务 + 轮询 + 自动下载,显示进度条 - 恢复: OutboxPublisher 启动时恢复未完成的任务,重试超限自动标记失败
任务参数、状态和结果持久化到 MySQL。 接入现有 outbox;配置 BackgroundJobs__Transport=RabbitMq 时使用 RabbitMQ 队列 exam.arrangement,否则使用 InMemory worker。 服务重启后可恢复未完成任务。 前端显示排队/执行/完成/失败状态,刷新页面可恢复正在执行的任务。 编排期间禁止修改、删除或发布对应计划。 补考原有“一键生成”保留,并与编排任务互斥。 运维后台增加“考试与补考编排”失败任务筛选。
业务任务与 Outbox 消息同一事务落库,避免“数据库成功但消息没发出去”。 RabbitMQ 使用持久化消息、发布确认、手动 ACK、Quorum Queue、死信队列。 增加处理租约、心跳、异常重试、最大重试次数和幂等状态控制。 保留 InMemory 模式,开发环境无需安装 RabbitMQ。 支持多实例竞争消费,后续可以横向扩容。 新增 /health/messaging 消息系统健康检查。
切换后,其他学期自动变为历史态;已归档学期进一步弱化。所有主要学期选择器统一默认当前,并显示“当前 / 历史 / 已归档”。[academicTerms.ts (line 1)](/E:/jiaowu/web/src/utils/academicTerms.ts:1) 桌面使用分层表格,390px 手机端改为独立学期卡片,操作不会再挤压表格。[BaseDataView.vue (line 211)](/E:/jiaowu/web/src/views/BaseDataView.vue:211) 归档定义为“历史显示状态”,不是成绩冻结:补考成绩仍可录入并回写,已发布成绩仍可走成绩更正审批。 已加入 SQLite 开发迁移、MySQL 正式迁移和切换/归档/撤销测试。[20260726143000_AcademicTermArchiving.cs (line 1)](/E:/jiaowu/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260726143000_AcademicTermArchiving.cs:1)