主要改动:
自动排课接口立即返回 202 + jobId 后台服务独立执行,不受浏览器关闭或前端超时影响 进度、成功、失败状态持久化到数据库 服务重启后自动恢复排队中或中断的任务 同一草稿禁止重复提交后台任务 运行期间禁止编辑、发布或删除该课表 排课结果和任务成功状态在同一事务中提交 前端每秒轮询进度,显示已处理教学班和已规划安排数
This commit is contained in:
@@ -26,6 +26,7 @@ public sealed class AutomaticScheduleGeneratorTests
|
||||
await migrator.MigrateAsync();
|
||||
|
||||
Assert.True(await db.ScheduleTimeSlots.CountAsync() == 0);
|
||||
Assert.True(await db.AutomaticScheduleJobs.CountAsync() == 0);
|
||||
Assert.True(await db.Database
|
||||
.SqlQueryRaw<int>(
|
||||
"""
|
||||
@@ -112,15 +113,167 @@ public sealed class AutomaticScheduleGeneratorTests
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var progressUpdates = new List<AutomaticScheduleProgress>();
|
||||
var result = await new AutomaticScheduleGenerator(db)
|
||||
.GenerateAsync(plan, CancellationToken.None);
|
||||
.GenerateAsync(
|
||||
plan,
|
||||
(progress, _) =>
|
||||
{
|
||||
progressUpdates.Add(progress);
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
true,
|
||||
CancellationToken.None);
|
||||
var entry = await db.ScheduleEntries.SingleAsync();
|
||||
|
||||
Assert.Equal(1, result.CreatedEntries);
|
||||
Assert.Empty(result.Messages);
|
||||
Assert.Collection(
|
||||
progressUpdates,
|
||||
progress =>
|
||||
{
|
||||
Assert.Equal(1, progress.TotalTasks);
|
||||
Assert.Equal(0, progress.ProcessedTasks);
|
||||
},
|
||||
progress =>
|
||||
{
|
||||
Assert.Equal(1, progress.TotalTasks);
|
||||
Assert.Equal(1, progress.ProcessedTasks);
|
||||
Assert.Equal(1, progress.CreatedEntries);
|
||||
});
|
||||
Assert.Equal(3, entry.DayOfWeek);
|
||||
Assert.Equal(1, entry.StartPeriod);
|
||||
Assert.Equal(2, entry.PeriodCount);
|
||||
Assert.Null(entry.ClassroomId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Generator_does_not_attach_duplicate_buildings_from_untracked_classrooms()
|
||||
{
|
||||
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-B",
|
||||
Name = "2026 秋季",
|
||||
AcademicYear = "2026-2027",
|
||||
Season = TermSeason.Autumn,
|
||||
StartDate = new DateOnly(2026, 9, 1),
|
||||
EndDate = new DateOnly(2027, 1, 15)
|
||||
};
|
||||
var campus = new Campus { Code = "MAIN", Name = "主校区" };
|
||||
var building = new Building
|
||||
{
|
||||
Code = "A",
|
||||
Name = "教学楼 A",
|
||||
Campus = campus
|
||||
};
|
||||
var smallRoom = new Classroom
|
||||
{
|
||||
Code = "A101",
|
||||
Name = "A101",
|
||||
Building = building,
|
||||
Capacity = 20
|
||||
};
|
||||
var largeRoom = new Classroom
|
||||
{
|
||||
Code = "A102",
|
||||
Name = "A102",
|
||||
Building = building,
|
||||
Capacity = 30
|
||||
};
|
||||
var college = new College { Code = "CS", Name = "计算机学院" };
|
||||
var course = new Course
|
||||
{
|
||||
Code = "CS-01",
|
||||
Name = "程序设计",
|
||||
College = college,
|
||||
Credits = 2,
|
||||
TotalHours = 32,
|
||||
LectureHours = 32
|
||||
};
|
||||
var smallTask = new TeachingTask
|
||||
{
|
||||
TaskNumber = "TASK-SMALL",
|
||||
Name = "小班教学任务",
|
||||
AcademicTerm = term,
|
||||
Course = course,
|
||||
Capacity = 20,
|
||||
StartWeek = 1,
|
||||
EndWeek = 16,
|
||||
WeeklyHours = 2,
|
||||
Status = TeachingTaskStatus.Published
|
||||
};
|
||||
var largeTask = new TeachingTask
|
||||
{
|
||||
TaskNumber = "TASK-LARGE",
|
||||
Name = "大班教学任务",
|
||||
AcademicTerm = term,
|
||||
Course = course,
|
||||
Capacity = 30,
|
||||
StartWeek = 1,
|
||||
EndWeek = 16,
|
||||
WeeklyHours = 2,
|
||||
Status = TeachingTaskStatus.Published
|
||||
};
|
||||
var plan = new SchedulePlan
|
||||
{
|
||||
AcademicTerm = term,
|
||||
Name = "同楼多教室排课",
|
||||
Version = "V1"
|
||||
};
|
||||
db.AddRange(
|
||||
term,
|
||||
campus,
|
||||
building,
|
||||
smallRoom,
|
||||
largeRoom,
|
||||
college,
|
||||
course,
|
||||
smallTask,
|
||||
largeTask,
|
||||
plan);
|
||||
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)
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
db.ChangeTracker.Clear();
|
||||
plan = await db.SchedulePlans.SingleAsync();
|
||||
|
||||
var result = await new AutomaticScheduleGenerator(db)
|
||||
.GenerateAsync(plan, CancellationToken.None);
|
||||
var entries = await db.ScheduleEntries
|
||||
.OrderBy(x => x.TeachingTaskId)
|
||||
.ToListAsync();
|
||||
|
||||
Assert.Equal(2, result.CreatedEntries);
|
||||
Assert.Empty(result.Messages);
|
||||
Assert.Equal(2, entries.Count);
|
||||
Assert.True(
|
||||
new HashSet<Guid> { smallRoom.Id, largeRoom.Id }
|
||||
.SetEquals(entries.Select(x => x.ClassroomId!.Value)));
|
||||
Assert.Equal(1, await db.Buildings.CountAsync());
|
||||
Assert.Equal(2, await db.Classrooms.CountAsync());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user