自动排课接口立即返回 202 + jobId 后台服务独立执行,不受浏览器关闭或前端超时影响 进度、成功、失败状态持久化到数据库 服务重启后自动恢复排队中或中断的任务 同一草稿禁止重复提交后台任务 运行期间禁止编辑、发布或删除该课表 排课结果和任务成功状态在同一事务中提交 前端每秒轮询进度,显示已处理教学班和已规划安排数
140 lines
5.4 KiB
C#
140 lines
5.4 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|