已改成后台“检查并发布”任务,原来的超时主要就是同步逐条校验导致的。

发布接口立即返回 202 Accepted,页面轮询显示检查进度。
校验改为批量读取,减少重复数据库查询。
检查失败保留草稿并显示具体原因;成功后原子完成旧课表归档和新课表发布。
同一学期禁止两个发布任务并发,服务重启后未完成任务会自动恢复。
发布期间锁定编辑、自动排课等冲突操作。
已补齐 SQLite 开发迁移及 MySQL 生产迁移。
This commit is contained in:
2026-07-25 12:07:03 +08:00 Unverified
parent fdca6a3edf
commit b015514115
12 changed files with 4116 additions and 81 deletions
@@ -2,6 +2,7 @@ using Jiaowu.Api.Controllers;
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Infrastructure.Persistence;
using Jiaowu.Api.Infrastructure.Scheduling;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
@@ -10,6 +11,87 @@ namespace Jiaowu.Api.Tests;
public sealed class SchedulesControllerTests
{
[Fact]
public async Task Publish_returns_accepted_before_background_validation_runs()
{
await using var connection = new SqliteConnection("Data Source=:memory:");
await connection.OpenAsync();
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseSqlite(connection)
.Options;
await using var db = new AppDbContext(options);
await db.Database.EnsureCreatedAsync();
var term = new AcademicTerm
{
Code = "2026-P",
Name = "2026 发布测试",
AcademicYear = "2026-2027",
Season = TermSeason.Autumn,
StartDate = new DateOnly(2026, 9, 7),
EndDate = new DateOnly(2027, 1, 17)
};
var college = new College { Code = "P", Name = "发布学院" };
var course = new Course
{
Code = "P101",
Name = "发布课程",
College = college,
Credits = 1,
TotalHours = 16,
LectureHours = 16
};
var task = new TeachingTask
{
TaskNumber = "TASK-P",
Name = "发布教学班",
AcademicTerm = term,
Course = course,
Capacity = 30,
WeeklyHours = 1,
Status = TeachingTaskStatus.Published
};
var plan = new SchedulePlan
{
AcademicTerm = term,
Name = "发布草稿",
Version = "V1"
};
plan.Entries.Add(new ScheduleEntry
{
TeachingTask = task,
DayOfWeek = 1,
StartPeriod = 1,
PeriodCount = 1,
StartWeek = 1,
EndWeek = 16,
WeekPattern = WeekPattern.All
});
db.AddRange(term, college, course, task, plan);
await db.SaveChangesAsync();
var controller = new SchedulesController(
db,
new AutomaticScheduleJobQueue(),
new SchedulePublishJobQueue())
{
ControllerContext = new ControllerContext
{
HttpContext = new DefaultHttpContext()
}
};
var result = await controller.PublishPlan(plan.Id, CancellationToken.None);
var accepted = Assert.IsType<AcceptedAtActionResult>(result.Result);
var response = Assert.IsType<SchedulePublishJobResponse>(accepted.Value);
Assert.Equal(SchedulePublishJobStatus.Queued, response.Status);
Assert.Equal(plan.Id, response.SchedulePlanId);
Assert.Equal(
SchedulePlanStatus.Draft,
(await db.SchedulePlans.SingleAsync()).Status);
Assert.Equal(1, await db.SchedulePublishJobs.CountAsync());
}
[Fact]
public async Task Latest_auto_schedule_job_remains_available_after_completion()
{
@@ -52,7 +134,8 @@ public sealed class SchedulesControllerTests
var controller = new SchedulesController(
db,
new AutomaticScheduleJobQueue());
new AutomaticScheduleJobQueue(),
new SchedulePublishJobQueue());
var result = await controller.GetLatestAutomaticScheduleJob(
plan.Id,
CancellationToken.None);