主要改动:
自动排课接口立即返回 202 + jobId 后台服务独立执行,不受浏览器关闭或前端超时影响 进度、成功、失败状态持久化到数据库 服务重启后自动恢复排队中或中断的任务 同一草稿禁止重复提交后台任务 运行期间禁止编辑、发布或删除该课表 排课结果和任务成功状态在同一事务中提交 前端每秒轮询进度,显示已处理教学班和已规划安排数
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
using Jiaowu.Api.Domain.Academic;
|
||||
using Jiaowu.Api.Infrastructure.Persistence;
|
||||
using Jiaowu.Api.Infrastructure.Scheduling;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Jiaowu.Api.Tests;
|
||||
|
||||
public sealed class AutomaticScheduleJobProcessorTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Processor_persists_progress_result_and_schedule_entries()
|
||||
{
|
||||
var databasePath = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
$"jiaowu-auto-schedule-{Guid.NewGuid():N}.sqlite");
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
services.AddDbContext<AppDbContext>(
|
||||
options => options.UseSqlite(
|
||||
$"Data Source={databasePath};Pooling=False"));
|
||||
services.AddScoped<AutomaticScheduleGenerator>();
|
||||
services.AddScoped<AutomaticScheduleJobProcessor>();
|
||||
var provider = services.BuildServiceProvider();
|
||||
|
||||
try
|
||||
{
|
||||
Guid jobId;
|
||||
await using (var seedScope = provider.CreateAsyncScope())
|
||||
{
|
||||
var db = seedScope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
await db.Database.EnsureCreatedAsync();
|
||||
var term = new AcademicTerm
|
||||
{
|
||||
Code = "2026-JOB",
|
||||
Name = "后台排课学期",
|
||||
AcademicYear = "2026-2027",
|
||||
Season = TermSeason.Autumn,
|
||||
StartDate = new DateOnly(2026, 9, 1),
|
||||
EndDate = new DateOnly(2027, 1, 15)
|
||||
};
|
||||
var college = new College { Code = "JOB", Name = "后台任务学院" };
|
||||
var course = new Course
|
||||
{
|
||||
Code = "JOB-01",
|
||||
Name = "后台任务课程",
|
||||
College = college,
|
||||
Credits = 1,
|
||||
TotalHours = 32,
|
||||
LectureHours = 32
|
||||
};
|
||||
var task = new TeachingTask
|
||||
{
|
||||
TaskNumber = "TASK-JOB",
|
||||
Name = "后台任务教学班",
|
||||
AcademicTerm = term,
|
||||
Course = course,
|
||||
Capacity = 100,
|
||||
StartWeek = 1,
|
||||
EndWeek = 16,
|
||||
WeeklyHours = 2,
|
||||
Status = TeachingTaskStatus.Published
|
||||
};
|
||||
var plan = new SchedulePlan
|
||||
{
|
||||
AcademicTerm = term,
|
||||
Name = "后台排课草稿",
|
||||
Version = "V1"
|
||||
};
|
||||
var job = new AutomaticScheduleJob
|
||||
{
|
||||
SchedulePlan = plan,
|
||||
ActiveSchedulePlanId = plan.Id
|
||||
};
|
||||
jobId = job.Id;
|
||||
db.AddRange(term, college, course, task, plan, job);
|
||||
db.ScheduleTimeSlots.AddRange(
|
||||
new ScheduleTimeSlot
|
||||
{
|
||||
AcademicTerm = term,
|
||||
PeriodNumber = 1,
|
||||
Name = "第 1 节",
|
||||
StartsAt = new TimeOnly(8, 0),
|
||||
EndsAt = new TimeOnly(8, 45)
|
||||
},
|
||||
new ScheduleTimeSlot
|
||||
{
|
||||
AcademicTerm = term,
|
||||
PeriodNumber = 2,
|
||||
Name = "第 2 节",
|
||||
StartsAt = new TimeOnly(8, 55),
|
||||
EndsAt = new TimeOnly(9, 40)
|
||||
});
|
||||
db.TeachingTaskScheduleConstraints.Add(
|
||||
new TeachingTaskScheduleConstraint
|
||||
{
|
||||
TeachingTask = task,
|
||||
RequiresClassroom = false,
|
||||
AllowedDayOfWeeks = "2",
|
||||
EarliestPeriod = 1,
|
||||
LatestPeriod = 2
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
await using (var workerScope = provider.CreateAsyncScope())
|
||||
{
|
||||
var processor = workerScope.ServiceProvider
|
||||
.GetRequiredService<AutomaticScheduleJobProcessor>();
|
||||
await processor.ProcessAsync(jobId, CancellationToken.None);
|
||||
}
|
||||
|
||||
await using (var assertScope = provider.CreateAsyncScope())
|
||||
{
|
||||
var db = assertScope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
var job = await db.AutomaticScheduleJobs.SingleAsync();
|
||||
var entry = await db.ScheduleEntries.SingleAsync();
|
||||
|
||||
Assert.Equal(AutomaticScheduleJobStatus.Succeeded, job.Status);
|
||||
Assert.Null(job.ActiveSchedulePlanId);
|
||||
Assert.Equal(1, job.TotalTasks);
|
||||
Assert.Equal(1, job.ProcessedTasks);
|
||||
Assert.Equal(1, job.CreatedEntries);
|
||||
Assert.Equal(1, job.CompletedTasks);
|
||||
Assert.Equal("[]", job.MessagesJson);
|
||||
Assert.NotNull(job.StartedAt);
|
||||
Assert.NotNull(job.CompletedAt);
|
||||
Assert.Null(job.ErrorMessage);
|
||||
Assert.Equal(2, entry.DayOfWeek);
|
||||
Assert.Equal(2, entry.PeriodCount);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
await provider.DisposeAsync();
|
||||
File.Delete(databasePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user