1. 教室容量按 1/2 计算
- 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 启动时恢复未完成的任务,重试超限自动标记失败
This commit is contained in:
@@ -60,6 +60,8 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
public DbSet<ExamPlan> ExamPlans => Set<ExamPlan>();
|
||||
public DbSet<ExamArrangementJob> ExamArrangementJobs =>
|
||||
Set<ExamArrangementJob>();
|
||||
public DbSet<ExamSignInExportJob> ExamSignInExportJobs =>
|
||||
Set<ExamSignInExportJob>();
|
||||
public DbSet<ExamSession> ExamSessions => Set<ExamSession>();
|
||||
public DbSet<ExamSessionInvigilator> ExamSessionInvigilators =>
|
||||
Set<ExamSessionInvigilator>();
|
||||
@@ -709,6 +711,7 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
entity.HasIndex(x => x.TeachingTaskId);
|
||||
entity.HasIndex(x => new { x.ExamPlanId, x.ExamDate });
|
||||
entity.HasIndex(x => x.RequiredBuildingId);
|
||||
entity.Property(x => x.RequiredBuildingIds).HasColumnType("longtext");
|
||||
entity.HasOne(x => x.ExamPlan).WithMany(x => x.Sessions)
|
||||
.HasForeignKey(x => x.ExamPlanId).OnDelete(DeleteBehavior.Cascade);
|
||||
entity.HasOne(x => x.TeachingTask).WithMany()
|
||||
@@ -798,6 +801,15 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
entity.HasIndex(x => new { x.Status, x.CreatedAt });
|
||||
entity.HasIndex(x => x.RequestedByUserId);
|
||||
});
|
||||
builder.Entity<ExamSignInExportJob>(entity =>
|
||||
{
|
||||
entity.Property(x => x.CurrentStep).HasMaxLength(200);
|
||||
entity.Property(x => x.FileName).HasMaxLength(200);
|
||||
entity.Property(x => x.ErrorMessage).HasMaxLength(2000);
|
||||
entity.HasIndex(x => new { x.PlanId, x.CreatedAt });
|
||||
entity.HasIndex(x => new { x.Status, x.CreatedAt });
|
||||
entity.HasIndex(x => x.RequestedByUserId);
|
||||
});
|
||||
builder.Entity<MakeupExamPlan>(entity =>
|
||||
{
|
||||
entity.Property(x => x.Name).HasMaxLength(120);
|
||||
@@ -813,6 +825,7 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
entity.HasIndex(x => x.TeachingTaskId);
|
||||
entity.HasIndex(x => new { x.MakeupExamPlanId, x.ExamDate });
|
||||
entity.HasIndex(x => x.RequiredBuildingId);
|
||||
entity.Property(x => x.RequiredBuildingIds).HasColumnType("longtext");
|
||||
entity.HasOne(x => x.MakeupExamPlan).WithMany(x => x.Sessions)
|
||||
.HasForeignKey(x => x.MakeupExamPlanId).OnDelete(DeleteBehavior.Cascade);
|
||||
entity.HasOne(x => x.TeachingTask).WithMany()
|
||||
|
||||
@@ -373,6 +373,14 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
examArrangementJobsExist ? [] : ExamArrangementJobStatements,
|
||||
cancellationToken);
|
||||
|
||||
var exportJobsExist = await db.Database
|
||||
.SqlQueryRaw<int>("SELECT COUNT(*) AS \"Value\" FROM sqlite_master WHERE type = 'table' AND name = 'ExamSignInExportJobs'")
|
||||
.AnyAsync(value => value > 0, cancellationToken);
|
||||
await ApplyMigrationAsync(
|
||||
ExamSignInExportJobsMigration,
|
||||
exportJobsExist ? [] : ExamSignInExportJobStatements,
|
||||
cancellationToken);
|
||||
|
||||
var academicTermArchivingExists = await db.Database
|
||||
.SqlQueryRaw<int>(
|
||||
"""
|
||||
@@ -1666,6 +1674,9 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
ALTER TABLE "ExamSessions" ADD COLUMN "RequiredBuildingId" TEXT NULL;
|
||||
""",
|
||||
"""
|
||||
ALTER TABLE "ExamSessions" ADD COLUMN "RequiredBuildingIds" TEXT NULL;
|
||||
""",
|
||||
"""
|
||||
ALTER TABLE "ExamSessions" ADD COLUMN "RequiredInvigilatorCount" INTEGER NOT NULL DEFAULT 2;
|
||||
""",
|
||||
"""
|
||||
@@ -1680,6 +1691,7 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
"StartsAt" TEXT NOT NULL,
|
||||
"EndsAt" TEXT NOT NULL,
|
||||
"RequiredBuildingId" TEXT NULL,
|
||||
"RequiredBuildingIds" TEXT NULL,
|
||||
"RequiredInvigilatorCount" INTEGER NOT NULL,
|
||||
"Notes" TEXT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
@@ -1975,7 +1987,7 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
[
|
||||
"""CREATE TABLE "MakeupExamPlans" ("Id" TEXT NOT NULL, "CreatedAt" TEXT NOT NULL, "UpdatedAt" TEXT NOT NULL, "AcademicTermId" TEXT NOT NULL, "Name" TEXT NOT NULL, "Status" INTEGER NOT NULL, "Notes" TEXT NULL, "PublishedAt" TEXT NULL, CONSTRAINT "PK_MakeupExamPlans" PRIMARY KEY ("Id"), CONSTRAINT "FK_MakeupExamPlans_AcademicTerms_AcademicTermId" FOREIGN KEY ("AcademicTermId") REFERENCES "AcademicTerms" ("Id") ON DELETE RESTRICT);""",
|
||||
"""CREATE INDEX "IX_MakeupExamPlans_AcademicTermId_Status" ON "MakeupExamPlans" ("AcademicTermId", "Status");""",
|
||||
"""CREATE TABLE "MakeupExamSessions" ("Id" TEXT NOT NULL, "CreatedAt" TEXT NOT NULL, "UpdatedAt" TEXT NOT NULL, "MakeupExamPlanId" TEXT NOT NULL, "TeachingTaskId" TEXT NOT NULL, "ClassroomId" TEXT NULL, "ExamDate" TEXT NOT NULL, "StartPeriod" INTEGER NOT NULL, "PeriodCount" INTEGER NOT NULL, "StartsAt" TEXT NOT NULL, "EndsAt" TEXT NOT NULL, "RequiredBuildingId" TEXT NULL, "RequiredInvigilatorCount" INTEGER NOT NULL, "Notes" TEXT NULL, CONSTRAINT "PK_MakeupExamSessions" PRIMARY KEY ("Id"), CONSTRAINT "FK_MakeupExamSessions_MakeupExamPlans_MakeupExamPlanId" FOREIGN KEY ("MakeupExamPlanId") REFERENCES "MakeupExamPlans" ("Id") ON DELETE CASCADE, CONSTRAINT "FK_MakeupExamSessions_TeachingTasks_TeachingTaskId" FOREIGN KEY ("TeachingTaskId") REFERENCES "TeachingTasks" ("Id") ON DELETE RESTRICT, CONSTRAINT "FK_MakeupExamSessions_Classrooms_ClassroomId" FOREIGN KEY ("ClassroomId") REFERENCES "Classrooms" ("Id") ON DELETE SET NULL, CONSTRAINT "FK_MakeupExamSessions_Buildings_RequiredBuildingId" FOREIGN KEY ("RequiredBuildingId") REFERENCES "Buildings" ("Id") ON DELETE SET NULL);""",
|
||||
"""CREATE TABLE "MakeupExamSessions" ("Id" TEXT NOT NULL, "CreatedAt" TEXT NOT NULL, "UpdatedAt" TEXT NOT NULL, "MakeupExamPlanId" TEXT NOT NULL, "TeachingTaskId" TEXT NOT NULL, "ClassroomId" TEXT NULL, "ExamDate" TEXT NOT NULL, "StartPeriod" INTEGER NOT NULL, "PeriodCount" INTEGER NOT NULL, "StartsAt" TEXT NOT NULL, "EndsAt" TEXT NOT NULL, "RequiredBuildingId" TEXT NULL, "RequiredBuildingIds" TEXT NULL, "RequiredInvigilatorCount" INTEGER NOT NULL, "Notes" TEXT NULL, CONSTRAINT "PK_MakeupExamSessions" PRIMARY KEY ("Id"), CONSTRAINT "FK_MakeupExamSessions_MakeupExamPlans_MakeupExamPlanId" FOREIGN KEY ("MakeupExamPlanId") REFERENCES "MakeupExamPlans" ("Id") ON DELETE CASCADE, CONSTRAINT "FK_MakeupExamSessions_TeachingTasks_TeachingTaskId" FOREIGN KEY ("TeachingTaskId") REFERENCES "TeachingTasks" ("Id") ON DELETE RESTRICT, CONSTRAINT "FK_MakeupExamSessions_Classrooms_ClassroomId" FOREIGN KEY ("ClassroomId") REFERENCES "Classrooms" ("Id") ON DELETE SET NULL, CONSTRAINT "FK_MakeupExamSessions_Buildings_RequiredBuildingId" FOREIGN KEY ("RequiredBuildingId") REFERENCES "Buildings" ("Id") ON DELETE SET NULL);""",
|
||||
"""CREATE INDEX "IX_MakeupExamSessions_MakeupExamPlanId_StartsAt" ON "MakeupExamSessions" ("MakeupExamPlanId", "StartsAt");""",
|
||||
"""CREATE INDEX "IX_MakeupExamSessions_TeachingTaskId" ON "MakeupExamSessions" ("TeachingTaskId");""",
|
||||
"""CREATE INDEX "IX_MakeupExamSessions_MakeupExamPlanId_ExamDate" ON "MakeupExamSessions" ("MakeupExamPlanId", "ExamDate");""",
|
||||
@@ -2005,6 +2017,16 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
"""CREATE UNIQUE INDEX "UX_ExamArrangementJobs_Kind_ActivePlan" ON "ExamArrangementJobs" ("Kind", "ActivePlanId");""",
|
||||
];
|
||||
|
||||
private const string ExamSignInExportJobsMigration = "ExamSignInExportJobs";
|
||||
|
||||
private static readonly string[] ExamSignInExportJobStatements =
|
||||
[
|
||||
"""CREATE TABLE "ExamSignInExportJobs" ("Id" TEXT NOT NULL, "PlanId" TEXT NOT NULL, "Status" INTEGER NOT NULL, "RequestedByUserId" TEXT NULL, "FileName" TEXT NULL, "FileBytes" BLOB NULL, "FileSize" INTEGER NOT NULL, "CurrentStep" TEXT NULL, "ErrorMessage" TEXT NULL, "StartedAt" TEXT NULL, "CompletedAt" TEXT NULL, "CreatedAt" TEXT NOT NULL, "UpdatedAt" TEXT NOT NULL, CONSTRAINT "PK_ExamSignInExportJobs" PRIMARY KEY ("Id"));""",
|
||||
"""CREATE INDEX "IX_ExamSignInExportJobs_PlanId_CreatedAt" ON "ExamSignInExportJobs" ("PlanId", "CreatedAt");""",
|
||||
"""CREATE INDEX "IX_ExamSignInExportJobs_Status_CreatedAt" ON "ExamSignInExportJobs" ("Status", "CreatedAt");""",
|
||||
"""CREATE INDEX "IX_ExamSignInExportJobs_RequestedByUserId" ON "ExamSignInExportJobs" ("RequestedByUserId");""",
|
||||
];
|
||||
|
||||
private static readonly string[] UnifiedMessageCenterStatements =
|
||||
[
|
||||
"""
|
||||
|
||||
+5221
File diff suppressed because it is too large
Load Diff
+38
@@ -0,0 +1,38 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddRequiredBuildingIds : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "RequiredBuildingIds",
|
||||
table: "MakeupExamSessions",
|
||||
type: "longtext",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "RequiredBuildingIds",
|
||||
table: "ExamSessions",
|
||||
type: "longtext",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RequiredBuildingIds",
|
||||
table: "MakeupExamSessions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RequiredBuildingIds",
|
||||
table: "ExamSessions");
|
||||
}
|
||||
}
|
||||
}
|
||||
+5277
File diff suppressed because it is too large
Load Diff
+61
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ExamSignInExportJobs : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ExamSignInExportJobs",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
PlanId = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
Status = table.Column<int>(type: "int", nullable: false),
|
||||
RequestedByUserId = table.Column<Guid>(type: "char(36)", nullable: true),
|
||||
FileName = table.Column<string>(type: "varchar(200)", maxLength: 200, nullable: true),
|
||||
FileBytes = table.Column<byte[]>(type: "longblob", nullable: true),
|
||||
FileSize = table.Column<int>(type: "int", nullable: false),
|
||||
CurrentStep = table.Column<string>(type: "varchar(200)", maxLength: 200, nullable: true),
|
||||
ErrorMessage = table.Column<string>(type: "varchar(2000)", maxLength: 2000, nullable: true),
|
||||
StartedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
|
||||
CompletedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ExamSignInExportJobs", x => x.Id);
|
||||
})
|
||||
.Annotation("MySQL:Charset", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ExamSignInExportJobs_PlanId_CreatedAt",
|
||||
table: "ExamSignInExportJobs",
|
||||
columns: new[] { "PlanId", "CreatedAt" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ExamSignInExportJobs_RequestedByUserId",
|
||||
table: "ExamSignInExportJobs",
|
||||
column: "RequestedByUserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ExamSignInExportJobs_Status_CreatedAt",
|
||||
table: "ExamSignInExportJobs",
|
||||
columns: new[] { "Status", "CreatedAt" });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ExamSignInExportJobs");
|
||||
}
|
||||
}
|
||||
}
|
||||
+62
@@ -1778,6 +1778,9 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
b.Property<Guid?>("RequiredBuildingId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<string>("RequiredBuildingIds")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<int>("RequiredInvigilatorCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
@@ -1823,6 +1826,62 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
b.ToTable("ExamSessionInvigilators");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamSignInExportJob", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<DateTime?>("CompletedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("CurrentStep")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.Property<string>("ErrorMessage")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("varchar(2000)");
|
||||
|
||||
b.Property<byte[]>("FileBytes")
|
||||
.HasColumnType("longblob");
|
||||
|
||||
b.Property<string>("FileName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.Property<int>("FileSize")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid>("PlanId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid?>("RequestedByUserId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RequestedByUserId");
|
||||
|
||||
b.HasIndex("PlanId", "CreatedAt");
|
||||
|
||||
b.HasIndex("Status", "CreatedAt");
|
||||
|
||||
b.ToTable("ExamSignInExportJobs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.GradeItem", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -2488,6 +2547,9 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
b.Property<Guid?>("RequiredBuildingId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<string>("RequiredBuildingIds")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<int>("RequiredInvigilatorCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user