已改成后台“检查并发布”任务,原来的超时主要就是同步逐条校验导致的。
发布接口立即返回 202 Accepted,页面轮询显示检查进度。 校验改为批量读取,减少重复数据库查询。 检查失败保留草稿并显示具体原因;成功后原子完成旧课表归档和新课表发布。 同一学期禁止两个发布任务并发,服务重启后未完成任务会自动恢复。 发布期间锁定编辑、自动排课等冲突操作。 已补齐 SQLite 开发迁移及 MySQL 生产迁移。
This commit is contained in:
@@ -38,6 +38,8 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
Set<TeachingTaskAllowedClassroom>();
|
||||
public DbSet<AutomaticScheduleJob> AutomaticScheduleJobs =>
|
||||
Set<AutomaticScheduleJob>();
|
||||
public DbSet<SchedulePublishJob> SchedulePublishJobs =>
|
||||
Set<SchedulePublishJob>();
|
||||
public DbSet<CourseSelectionRound> CourseSelectionRounds =>
|
||||
Set<CourseSelectionRound>();
|
||||
public DbSet<CourseSelectionOffering> CourseSelectionOfferings =>
|
||||
@@ -400,6 +402,24 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
});
|
||||
|
||||
builder.Entity<SchedulePublishJob>(entity =>
|
||||
{
|
||||
entity.Property(x => x.CurrentStep).HasMaxLength(200);
|
||||
entity.Property(x => x.ErrorMessage).HasMaxLength(2000);
|
||||
entity.HasIndex(x => x.ActiveAcademicTermId).IsUnique();
|
||||
entity.HasIndex(x => new { x.SchedulePlanId, x.CreatedAt });
|
||||
entity.HasIndex(x => new { x.Status, x.CreatedAt });
|
||||
entity.HasIndex(x => x.RequestedByUserId);
|
||||
entity.HasOne(x => x.SchedulePlan)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.SchedulePlanId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
entity.HasOne<ApplicationUser>()
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.RequestedByUserId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
});
|
||||
|
||||
builder.Entity<CourseSelectionRound>(entity =>
|
||||
{
|
||||
entity.Property(x => x.Name).HasMaxLength(120);
|
||||
|
||||
@@ -27,6 +27,8 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
"20260724_16_automatic_schedule_jobs";
|
||||
private const string TeachingTaskSchedulingModesMigration =
|
||||
"20260725_17_teaching_task_scheduling_modes";
|
||||
private const string SchedulePublishJobsMigration =
|
||||
"20260725_18_schedule_publish_jobs";
|
||||
|
||||
public async Task MigrateAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -163,6 +165,18 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
TeachingTaskSchedulingModesMigration,
|
||||
teachingTaskSchedulingModeExists ? [] : TeachingTaskSchedulingModeStatements,
|
||||
cancellationToken);
|
||||
var schedulePublishJobsExist = await db.Database
|
||||
.SqlQueryRaw<int>(
|
||||
"""
|
||||
SELECT COUNT(*) AS "Value"
|
||||
FROM sqlite_master
|
||||
WHERE type = 'table' AND name = 'SchedulePublishJobs'
|
||||
""")
|
||||
.AnyAsync(value => value > 0, cancellationToken);
|
||||
await ApplyMigrationAsync(
|
||||
SchedulePublishJobsMigration,
|
||||
schedulePublishJobsExist ? [] : SchedulePublishJobStatements,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private async Task ApplyMigrationAsync(
|
||||
@@ -1104,4 +1118,48 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
ADD COLUMN "SchedulingMode" INTEGER NOT NULL DEFAULT 1;
|
||||
"""
|
||||
];
|
||||
|
||||
private static readonly string[] SchedulePublishJobStatements =
|
||||
[
|
||||
"""
|
||||
CREATE TABLE "SchedulePublishJobs" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_SchedulePublishJobs" PRIMARY KEY,
|
||||
"SchedulePlanId" TEXT NOT NULL,
|
||||
"AcademicTermId" TEXT NOT NULL,
|
||||
"ActiveAcademicTermId" TEXT NULL,
|
||||
"RequestedByUserId" TEXT NULL,
|
||||
"Status" INTEGER NOT NULL,
|
||||
"TotalSteps" INTEGER NOT NULL,
|
||||
"CompletedSteps" INTEGER NOT NULL,
|
||||
"CurrentStep" TEXT NULL,
|
||||
"ErrorMessage" TEXT NULL,
|
||||
"StartedAt" TEXT NULL,
|
||||
"CompletedAt" TEXT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"UpdatedAt" TEXT NOT NULL,
|
||||
CONSTRAINT "FK_SchedulePublishJobs_SchedulePlans"
|
||||
FOREIGN KEY ("SchedulePlanId") REFERENCES "SchedulePlans" ("Id")
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT "FK_SchedulePublishJobs_RequestedBy"
|
||||
FOREIGN KEY ("RequestedByUserId") REFERENCES "AspNetUsers" ("Id")
|
||||
ON DELETE SET NULL
|
||||
);
|
||||
""",
|
||||
"""
|
||||
CREATE UNIQUE INDEX "IX_SchedulePublishJobs_ActiveAcademicTermId"
|
||||
ON "SchedulePublishJobs" ("ActiveAcademicTermId");
|
||||
""",
|
||||
"""
|
||||
CREATE INDEX "IX_SchedulePublishJobs_SchedulePlanId_CreatedAt"
|
||||
ON "SchedulePublishJobs" ("SchedulePlanId", "CreatedAt");
|
||||
""",
|
||||
"""
|
||||
CREATE INDEX "IX_SchedulePublishJobs_Status_CreatedAt"
|
||||
ON "SchedulePublishJobs" ("Status", "CreatedAt");
|
||||
""",
|
||||
"""
|
||||
CREATE INDEX "IX_SchedulePublishJobs_RequestedByUserId"
|
||||
ON "SchedulePublishJobs" ("RequestedByUserId");
|
||||
"""
|
||||
];
|
||||
}
|
||||
|
||||
+2953
File diff suppressed because it is too large
Load Diff
+80
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class SchedulePublishJobs : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SchedulePublishJobs",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
SchedulePlanId = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
AcademicTermId = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
ActiveAcademicTermId = table.Column<Guid>(type: "char(36)", nullable: true),
|
||||
RequestedByUserId = table.Column<Guid>(type: "char(36)", nullable: true),
|
||||
Status = table.Column<int>(type: "int", nullable: false),
|
||||
TotalSteps = table.Column<int>(type: "int", nullable: false),
|
||||
CompletedSteps = 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_SchedulePublishJobs", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_SchedulePublishJobs_AspNetUsers_RequestedByUserId",
|
||||
column: x => x.RequestedByUserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
table.ForeignKey(
|
||||
name: "FK_SchedulePublishJobs_SchedulePlans_SchedulePlanId",
|
||||
column: x => x.SchedulePlanId,
|
||||
principalTable: "SchedulePlans",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySQL:Charset", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SchedulePublishJobs_ActiveAcademicTermId",
|
||||
table: "SchedulePublishJobs",
|
||||
column: "ActiveAcademicTermId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SchedulePublishJobs_RequestedByUserId",
|
||||
table: "SchedulePublishJobs",
|
||||
column: "RequestedByUserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SchedulePublishJobs_SchedulePlanId_CreatedAt",
|
||||
table: "SchedulePublishJobs",
|
||||
columns: new[] { "SchedulePlanId", "CreatedAt" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SchedulePublishJobs_Status_CreatedAt",
|
||||
table: "SchedulePublishJobs",
|
||||
columns: new[] { "Status", "CreatedAt" });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "SchedulePublishJobs");
|
||||
}
|
||||
}
|
||||
}
|
||||
+77
@@ -1448,6 +1448,67 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
b.ToTable("SchedulePlans");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.SchedulePublishJob", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("AcademicTermId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid?>("ActiveAcademicTermId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<DateTime?>("CompletedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<int>("CompletedSteps")
|
||||
.HasColumnType("int");
|
||||
|
||||
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<Guid?>("RequestedByUserId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("SchedulePlanId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("TotalSteps")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ActiveAcademicTermId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("RequestedByUserId");
|
||||
|
||||
b.HasIndex("SchedulePlanId", "CreatedAt");
|
||||
|
||||
b.HasIndex("Status", "CreatedAt");
|
||||
|
||||
b.ToTable("SchedulePublishJobs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ScheduleTimeSlot", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -2553,6 +2614,22 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
b.Navigation("AcademicTerm");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.SchedulePublishJob", b =>
|
||||
{
|
||||
b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RequestedByUserId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("Jiaowu.Api.Domain.Academic.SchedulePlan", "SchedulePlan")
|
||||
.WithMany()
|
||||
.HasForeignKey("SchedulePlanId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("SchedulePlan");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ScheduleTimeSlot", b =>
|
||||
{
|
||||
b.HasOne("Jiaowu.Api.Domain.Academic.AcademicTerm", "AcademicTerm")
|
||||
|
||||
Reference in New Issue
Block a user