参照现有的 ExamArrangementJob / SchedulePublishJob 后台任务模式,将发布改为通过 RabbitMQ
队列异步执行,拆分查询消除笛卡尔积。
修改的文件(共 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)
This commit is contained in:
@@ -15,6 +15,7 @@ public sealed class BackgroundJobOptions
|
||||
public int MakeupExamAutoConcurrency { get; set; } = 1;
|
||||
public int ExamArrangementConcurrency { get; set; } = 1;
|
||||
public int ExamSignInExportConcurrency { get; set; } = 1;
|
||||
public int ExamPublishConcurrency { get; set; } = 1;
|
||||
public string Exchange { get; set; } = "jiaowu.background-jobs";
|
||||
public string QueuePrefix { get; set; } = "jiaowu.background-jobs";
|
||||
public bool UseQuorumQueues { get; set; } = true;
|
||||
@@ -33,6 +34,7 @@ public sealed class BackgroundJobOptions
|
||||
BackgroundJobKind.MakeupExamAuto => MakeupExamAutoConcurrency,
|
||||
BackgroundJobKind.ExamArrangement => ExamArrangementConcurrency,
|
||||
BackgroundJobKind.ExamSignInExport => ExamSignInExportConcurrency,
|
||||
BackgroundJobKind.ExamPublish => ExamPublishConcurrency,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -121,6 +121,15 @@ public sealed class BackgroundJobOutboxPublisher(
|
||||
message.JobId == x.Id))
|
||||
.Select(x => x.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
var publishJobs2 = await db.ExamPublishJobs.AsNoTracking()
|
||||
.Where(x =>
|
||||
(x.Status == ExamPublishJobStatus.Queued ||
|
||||
x.Status == ExamPublishJobStatus.Running) &&
|
||||
!db.BackgroundJobOutboxMessages.Any(message =>
|
||||
message.JobKind == BackgroundJobKind.ExamPublish &&
|
||||
message.JobId == x.Id))
|
||||
.Select(x => x.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var missingKeys = automaticJobs
|
||||
.Select(id => (BackgroundJobKind.AutomaticSchedule, id))
|
||||
@@ -132,6 +141,8 @@ public sealed class BackgroundJobOutboxPublisher(
|
||||
(BackgroundJobKind.ExamArrangement, id)))
|
||||
.Concat(exportJobs.Select(id =>
|
||||
(BackgroundJobKind.ExamSignInExport, id)))
|
||||
.Concat(publishJobs2.Select(id =>
|
||||
(BackgroundJobKind.ExamPublish, id)))
|
||||
.ToList();
|
||||
foreach (var (kind, jobId) in missingKeys)
|
||||
{
|
||||
|
||||
@@ -95,6 +95,11 @@ public sealed class BackgroundJobRunner(
|
||||
.GetRequiredService<ExamSignInExportJobProcessor>()
|
||||
.ProcessAsync(message.JobId, cancellationToken);
|
||||
break;
|
||||
case BackgroundJobKind.ExamPublish:
|
||||
await scope.ServiceProvider
|
||||
.GetRequiredService<ExamPublishJobProcessor>()
|
||||
.ProcessAsync(message.JobId, cancellationToken);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException(
|
||||
$"Unsupported background job kind '{message.JobKind}'.");
|
||||
@@ -286,6 +291,23 @@ public sealed class BackgroundJobRunner(
|
||||
.SetProperty(x => x.CompletedAt, completedAt),
|
||||
cancellationToken);
|
||||
break;
|
||||
case BackgroundJobKind.ExamPublish:
|
||||
await db.ExamPublishJobs
|
||||
.Where(x =>
|
||||
x.Id == message.JobId &&
|
||||
x.Status != ExamPublishJobStatus.Succeeded &&
|
||||
x.Status != ExamPublishJobStatus.Failed)
|
||||
.ExecuteUpdateAsync(
|
||||
setters => setters
|
||||
.SetProperty(
|
||||
x => x.Status,
|
||||
ExamPublishJobStatus.Failed)
|
||||
.SetProperty(x => x.ActivePlanId, (Guid?)null)
|
||||
.SetProperty(x => x.CurrentStep, "后台处理已停止")
|
||||
.SetProperty(x => x.ErrorMessage, error)
|
||||
.SetProperty(x => x.CompletedAt, completedAt),
|
||||
cancellationToken);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(message.JobKind),
|
||||
|
||||
@@ -318,7 +318,8 @@ internal static class RabbitMqBackgroundJobTopology
|
||||
BackgroundJobKind.SchedulePublish,
|
||||
BackgroundJobKind.MakeupExamAuto,
|
||||
BackgroundJobKind.ExamArrangement,
|
||||
BackgroundJobKind.ExamSignInExport
|
||||
BackgroundJobKind.ExamSignInExport,
|
||||
BackgroundJobKind.ExamPublish
|
||||
];
|
||||
|
||||
public static async Task<IConnection> CreateConnectionAsync(
|
||||
@@ -416,6 +417,7 @@ internal static class RabbitMqBackgroundJobTopology
|
||||
BackgroundJobKind.MakeupExamAuto => "makeup-exam.automatic",
|
||||
BackgroundJobKind.ExamArrangement => "exam.arrangement",
|
||||
BackgroundJobKind.ExamSignInExport => "exam.sign-in-export",
|
||||
BackgroundJobKind.ExamPublish => "exam.publish",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null)
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
using Jiaowu.Api.Domain.Academic;
|
||||
using Jiaowu.Api.Infrastructure.Caching;
|
||||
using Jiaowu.Api.Infrastructure.Persistence;
|
||||
using Jiaowu.Api.Infrastructure.Teaching;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Jiaowu.Api.Infrastructure.Exams;
|
||||
|
||||
public sealed class ExamPublishJobProcessor(
|
||||
AppDbContext db,
|
||||
IAppCache cache,
|
||||
ILogger<ExamPublishJobProcessor> logger)
|
||||
{
|
||||
public async Task ProcessAsync(Guid jobId, CancellationToken stoppingToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var job = await db.ExamPublishJobs
|
||||
.FirstOrDefaultAsync(x => x.Id == jobId, stoppingToken);
|
||||
if (job is null ||
|
||||
job.Status is ExamPublishJobStatus.Succeeded
|
||||
or ExamPublishJobStatus.Failed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
job.Status = ExamPublishJobStatus.Running;
|
||||
job.StartedAt ??= DateTime.UtcNow;
|
||||
job.CompletedAt = null;
|
||||
job.ErrorMessage = null;
|
||||
job.CurrentStep = "正在校验考试计划";
|
||||
await db.SaveChangesAsync(stoppingToken);
|
||||
|
||||
switch (job.Kind)
|
||||
{
|
||||
case ExamPublishJobKind.FormalExam:
|
||||
await PublishFormalExamAsync(job, stoppingToken);
|
||||
break;
|
||||
case ExamPublishJobKind.MakeupExam:
|
||||
await PublishMakeupExamAsync(job, stoppingToken);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException(
|
||||
$"不支持的考试发布类型:{job.Kind}。");
|
||||
}
|
||||
|
||||
job.Status = ExamPublishJobStatus.Succeeded;
|
||||
job.ActivePlanId = null;
|
||||
job.CurrentStep = "发布完成";
|
||||
job.CompletedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync(stoppingToken);
|
||||
|
||||
await cache.RemoveByTagAsync(
|
||||
AppCacheTags.Timetables,
|
||||
stoppingToken);
|
||||
|
||||
logger.LogInformation(
|
||||
"Exam publish job {JobId} for {Kind}/{PlanId} completed.",
|
||||
job.Id,
|
||||
job.Kind,
|
||||
job.PlanId);
|
||||
}
|
||||
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
logger.LogInformation(
|
||||
"Exam publish job {JobId} was interrupted by application shutdown.",
|
||||
jobId);
|
||||
throw;
|
||||
}
|
||||
catch (ExamPublishValidationException validationException)
|
||||
{
|
||||
logger.LogWarning(
|
||||
validationException,
|
||||
"Exam publish job {JobId} validation failed.",
|
||||
jobId);
|
||||
await MarkFailedAsync(jobId, validationException.Message);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(exception, "Exam publish job {JobId} failed.", jobId);
|
||||
await MarkFailedAsync(jobId, exception.GetBaseException().Message);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PublishFormalExamAsync(
|
||||
ExamPublishJob job,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var plan = await db.ExamPlans
|
||||
.FirstOrDefaultAsync(x => x.Id == job.PlanId, ct);
|
||||
if (plan is null)
|
||||
throw new ExamPublishValidationException("考试计划不存在。");
|
||||
if (plan.Status != ExamPlanStatus.Draft)
|
||||
throw new ExamPublishValidationException("只有草稿考试计划可以发布。");
|
||||
|
||||
// Step 1: check sessions exist — simple count, no JOIN
|
||||
var sessionCount = await db.ExamSessions
|
||||
.CountAsync(x => x.ExamPlanId == job.PlanId, ct);
|
||||
if (sessionCount == 0)
|
||||
throw new ExamPublishValidationException(
|
||||
"至少安排一个考试场次后才能发布。");
|
||||
|
||||
// Step 2: load session summaries — only necessary columns, no Include chains
|
||||
var sessions = await db.ExamSessions
|
||||
.AsNoTracking()
|
||||
.Where(x => x.ExamPlanId == job.PlanId)
|
||||
.Select(x => new
|
||||
{
|
||||
x.Id,
|
||||
x.TeachingTaskId,
|
||||
x.ClassroomId,
|
||||
InvigilatorCount = x.Invigilators.Count,
|
||||
RoomLinkCount = x.RoomLinks.Count
|
||||
})
|
||||
.ToListAsync(ct);
|
||||
|
||||
// Step 3: load roster counts — independent query
|
||||
var taskIds = sessions.Select(x => x.TeachingTaskId).ToArray();
|
||||
var rosterCounts = (await TeachingTaskRosterQuery
|
||||
.LoadForTasksAsync(db, taskIds, ct))
|
||||
.GroupBy(x => x.TeachingTaskId)
|
||||
.ToDictionary(x => x.Key, x => x.Count());
|
||||
|
||||
// Step 4: validate each session's assignment completeness
|
||||
var unassignedSessionIds = new List<Guid>();
|
||||
foreach (var session in sessions)
|
||||
{
|
||||
var rosterCount = rosterCounts.GetValueOrDefault(session.TeachingTaskId);
|
||||
if (session.RoomLinkCount > 0)
|
||||
{
|
||||
// Has room links — check seat assignment via separate query
|
||||
var assignedSeatCount = await db.ExamSeats
|
||||
.CountAsync(seat =>
|
||||
seat.ExamSessionId == session.Id,
|
||||
ct);
|
||||
if (assignedSeatCount != rosterCount)
|
||||
unassignedSessionIds.Add(session.Id);
|
||||
}
|
||||
else if (!session.ClassroomId.HasValue ||
|
||||
session.InvigilatorCount == 0)
|
||||
{
|
||||
unassignedSessionIds.Add(session.Id);
|
||||
}
|
||||
}
|
||||
|
||||
if (unassignedSessionIds.Count > 0)
|
||||
throw new ExamPublishValidationException(
|
||||
$"还有 {unassignedSessionIds.Count} 个教学班未完成考场座位或监考安排," +
|
||||
"请先完成自动编排。");
|
||||
|
||||
// Step 5: check room invigilator sufficiency — separate query
|
||||
var insufficientInvigilatorCount = await db.ExamRoomSessions
|
||||
.Where(link =>
|
||||
link.ExamRoom!.ExamPlanId == job.PlanId &&
|
||||
link.ExamRoom.Invigilators.Count <
|
||||
link.ExamRoom.RequiredInvigilatorCount)
|
||||
.Select(link => link.ExamSessionId)
|
||||
.Distinct()
|
||||
.CountAsync(ct);
|
||||
if (insufficientInvigilatorCount > 0)
|
||||
throw new ExamPublishValidationException(
|
||||
$"还有 {insufficientInvigilatorCount} 个场次的混排考场监考教师不足," +
|
||||
"请先完成自动编排。");
|
||||
|
||||
// Step 6: validate mixed rooms — split into two independent queries
|
||||
// to avoid Seats × SessionLinks Cartesian product
|
||||
|
||||
// 6a: capacity overflow
|
||||
var overCapacityCount = await db.ExamRooms
|
||||
.Where(r => r.ExamPlanId == job.PlanId)
|
||||
.Select(r => new
|
||||
{
|
||||
r.Id,
|
||||
SeatCount = r.Seats.Count,
|
||||
Capacity = r.Classroom!.Capacity
|
||||
})
|
||||
.CountAsync(x => x.SeatCount > x.Capacity, ct);
|
||||
if (overCapacityCount > 0)
|
||||
throw new ExamPublishValidationException(
|
||||
$"发现 {overCapacityCount} 个考场容量超限,请重新编排。");
|
||||
|
||||
// 6b: course mismatch
|
||||
var courseMismatchCount = await db.ExamRoomSessions
|
||||
.Where(link =>
|
||||
link.ExamRoom!.ExamPlanId == job.PlanId &&
|
||||
link.ExamSession!.TeachingTask!.CourseId !=
|
||||
link.ExamRoom.CourseId)
|
||||
.Select(link => link.ExamRoomId)
|
||||
.Distinct()
|
||||
.CountAsync(ct);
|
||||
if (courseMismatchCount > 0)
|
||||
throw new ExamPublishValidationException(
|
||||
$"发现 {courseMismatchCount} 个考场混入不同课程,请重新编排。");
|
||||
|
||||
// Step 7: publish
|
||||
plan.Status = ExamPlanStatus.Published;
|
||||
plan.PublishedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
private async Task PublishMakeupExamAsync(
|
||||
ExamPublishJob job,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var plan = await db.MakeupExamPlans
|
||||
.FirstOrDefaultAsync(x => x.Id == job.PlanId, ct);
|
||||
if (plan is null)
|
||||
throw new ExamPublishValidationException("补考计划不存在。");
|
||||
if (plan.Status != MakeupExamPlanStatus.Draft)
|
||||
throw new ExamPublishValidationException("只有草稿补考计划可以发布。");
|
||||
|
||||
// Step 1: check sessions exist — simple count
|
||||
var sessionCount = await db.MakeupExamSessions
|
||||
.CountAsync(x => x.MakeupExamPlanId == job.PlanId, ct);
|
||||
if (sessionCount == 0)
|
||||
throw new ExamPublishValidationException(
|
||||
"至少安排一个考试场次后才能发布。");
|
||||
|
||||
// Step 2: load session summaries — only necessary columns
|
||||
var sessions = await db.MakeupExamSessions
|
||||
.AsNoTracking()
|
||||
.Where(x => x.MakeupExamPlanId == job.PlanId)
|
||||
.Select(x => new
|
||||
{
|
||||
x.Id,
|
||||
x.ClassroomId,
|
||||
InvigilatorCount = x.Invigilators.Count,
|
||||
EnrollmentCount = x.Enrollments.Count
|
||||
})
|
||||
.ToListAsync(ct);
|
||||
|
||||
// Step 3: validate completeness — no Include chain needed
|
||||
var unassigned = sessions.Count(x =>
|
||||
!x.ClassroomId.HasValue || x.InvigilatorCount == 0);
|
||||
if (unassigned > 0)
|
||||
throw new ExamPublishValidationException(
|
||||
$"还有 {unassigned} 个场次未分配考场或监考教师,请先完成自动编排。");
|
||||
|
||||
// Step 4: validate enrollments
|
||||
var empty = sessions.Count(x => x.EnrollmentCount == 0);
|
||||
if (empty > 0)
|
||||
throw new ExamPublishValidationException(
|
||||
$"还有 {empty} 个场次没有登记补考学生。");
|
||||
|
||||
// Step 5: publish
|
||||
plan.Status = MakeupExamPlanStatus.Published;
|
||||
plan.PublishedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
private async Task MarkFailedAsync(Guid jobId, string message)
|
||||
{
|
||||
db.ChangeTracker.Clear();
|
||||
var job = await db.ExamPublishJobs.FirstOrDefaultAsync(
|
||||
x => x.Id == jobId,
|
||||
CancellationToken.None);
|
||||
if (job is null)
|
||||
return;
|
||||
|
||||
job.Status = ExamPublishJobStatus.Failed;
|
||||
job.ActivePlanId = null;
|
||||
job.CurrentStep = "发布失败";
|
||||
job.ErrorMessage = message.Length <= 2000 ? message : message[..2000];
|
||||
job.CompletedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ExamPublishValidationException(string message)
|
||||
: InvalidOperationException(message);
|
||||
@@ -62,6 +62,8 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
Set<ExamArrangementJob>();
|
||||
public DbSet<ExamSignInExportJob> ExamSignInExportJobs =>
|
||||
Set<ExamSignInExportJob>();
|
||||
public DbSet<ExamPublishJob> ExamPublishJobs =>
|
||||
Set<ExamPublishJob>();
|
||||
public DbSet<ExamSession> ExamSessions => Set<ExamSession>();
|
||||
public DbSet<ExamSessionInvigilator> ExamSessionInvigilators =>
|
||||
Set<ExamSessionInvigilator>();
|
||||
|
||||
+5321
File diff suppressed because it is too large
Load Diff
+45
@@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ExamPublishJobs : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ExamPublishJobs",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
Kind = table.Column<int>(type: "int", nullable: false),
|
||||
PlanId = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
ActivePlanId = 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),
|
||||
CurrentStep = table.Column<string>(type: "longtext", nullable: true),
|
||||
ErrorMessage = table.Column<string>(type: "longtext", 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_ExamPublishJobs", x => x.Id);
|
||||
})
|
||||
.Annotation("MySQL:Charset", "utf8mb4");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ExamPublishJobs");
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
@@ -1634,6 +1634,50 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
b.ToTable("ExamPlans");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamPublishJob", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid?>("ActivePlanId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<DateTime?>("CompletedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("CurrentStep")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("ErrorMessage")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<int>("Kind")
|
||||
.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.ToTable("ExamPublishJobs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExamRoomAssignment", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
|
||||
Reference in New Issue
Block a user