Files
Academic-Affairs-System/tests/Jiaowu.Api.Tests/SchedulePublishJobProcessorTests.cs
biss f0f59419d5 实验课现在直接进入普通排课流程,不再要求先到实验排课模块逐个安排。
普通“添加排课”新增“理论课 / 实验课”类型。
自动排课会分别补足理论学时和实验学时。
实验课只能安排到实验室、实训室、机房、语音室等场地。
发布课表时分别校验理论、实验学时;任一未排足都不能发布。
普通课表及 Excel 导出会标注“实验课”。
历史排课保持不变,迁移后默认识别为理论课;后续新建或修订版本时再补充实验课。
2026-08-02 16:54:41 +08:00

249 lines
9.3 KiB
C#

using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Infrastructure.Caching;
using Jiaowu.Api.Infrastructure.Persistence;
using Jiaowu.Api.Infrastructure.Scheduling;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace Jiaowu.Api.Tests;
public sealed class SchedulePublishJobProcessorTests
{
[Fact]
public async Task Processor_validates_and_publishes_plan_atomically()
{
var result = await RunPublishAsync(weeklyHours: 2, scheduledHours: 2);
Assert.Equal(SchedulePublishJobStatus.Succeeded, result.JobStatus);
Assert.Equal(SchedulePlanStatus.Published, result.PlanStatus);
Assert.Null(result.ActiveAcademicTermId);
Assert.Equal(5, result.CompletedSteps);
Assert.Equal("课表已发布", result.CurrentStep);
Assert.Null(result.ErrorMessage);
Assert.NotNull(result.PublishedAt);
}
[Fact]
public async Task Processor_keeps_draft_when_validation_fails()
{
var result = await RunPublishAsync(weeklyHours: 2, scheduledHours: 1);
Assert.Equal(SchedulePublishJobStatus.Failed, result.JobStatus);
Assert.Equal(SchedulePlanStatus.Draft, result.PlanStatus);
Assert.Null(result.ActiveAcademicTermId);
Assert.Equal("检查未通过", result.CurrentStep);
Assert.Contains("理论课应安排 32 学时,当前已安排 16 学时", result.ErrorMessage);
Assert.Null(result.PublishedAt);
}
[Fact]
public async Task Processor_requires_experiment_hours_in_regular_schedule()
{
var result = await RunPublishAsync(
weeklyHours: 2,
scheduledHours: 1,
practiceHours: 16,
experimentScheduledHours: 1);
Assert.Equal(SchedulePublishJobStatus.Succeeded, result.JobStatus);
Assert.Equal(SchedulePlanStatus.Published, result.PlanStatus);
}
[Fact]
public async Task Processor_rejects_missing_experiment_hours()
{
var result = await RunPublishAsync(
weeklyHours: 2,
scheduledHours: 1,
practiceHours: 16);
Assert.Equal(SchedulePublishJobStatus.Failed, result.JobStatus);
Assert.Equal(SchedulePlanStatus.Draft, result.PlanStatus);
Assert.Contains("实验课应安排 16 学时,当前已安排 0 学时", result.ErrorMessage);
}
private static async Task<PublishResult> RunPublishAsync(
int weeklyHours,
int scheduledHours,
int practiceHours = 0,
int experimentScheduledHours = 0)
{
var databasePath = Path.Combine(
Path.GetTempPath(),
$"jiaowu-publish-{Guid.NewGuid():N}.sqlite");
var services = new ServiceCollection();
services.AddLogging();
services.AddDbContext<AppDbContext>(
options => options.UseSqlite(
$"Data Source={databasePath};Pooling=False"));
services.AddScoped<SchedulePlanPublisher>();
services.AddScoped<SchedulePublishJobProcessor>();
services.AddSingleton<IAppCache>(NoOpAppCache.Instance);
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-PUBLISH-{weeklyHours}",
Name = "后台发布学期",
AcademicYear = "2026-2027",
Season = TermSeason.Autumn,
StartDate = new DateOnly(2026, 9, 1),
EndDate = new DateOnly(2027, 1, 15)
};
var college = new College { Code = "PUB", Name = "发布测试学院" };
var campus = new Campus { Code = "PUB-CAMPUS", Name = "发布测试校区" };
var building = new Building
{
Code = "PUB-BUILDING",
Name = "实验楼",
Campus = campus
};
var laboratory = new Classroom
{
Code = "PUB-LAB",
Name = "发布测试实验室",
Building = building,
Capacity = 80,
RoomType = "实验室"
};
var course = new Course
{
Code = "PUB-01",
Name = "发布测试课程",
College = college,
Credits = 1,
TotalHours = 32,
LectureHours = 32 - practiceHours,
PracticeHours = practiceHours
};
var task = new TeachingTask
{
TaskNumber = $"TASK-PUB-{weeklyHours}",
Name = "发布测试教学班",
AcademicTerm = term,
Course = course,
Capacity = 60,
StartWeek = 1,
EndWeek = 16,
WeeklyHours = weeklyHours,
Status = TeachingTaskStatus.Published
};
var plan = new SchedulePlan
{
AcademicTerm = term,
Name = "待发布课表",
Version = "V1"
};
plan.Entries.Add(new ScheduleEntry
{
TeachingTask = task,
DayOfWeek = 1,
StartPeriod = 1,
PeriodCount = scheduledHours,
StartWeek = 1,
EndWeek = 16,
WeekPattern = WeekPattern.All
});
if (experimentScheduledHours > 0)
{
plan.Entries.Add(new ScheduleEntry
{
TeachingTask = task,
Kind = ScheduleEntryKind.Experiment,
Classroom = laboratory,
DayOfWeek = 2,
StartPeriod = 1,
PeriodCount = experimentScheduledHours,
StartWeek = 1,
EndWeek = 16,
WeekPattern = WeekPattern.All
});
}
var job = new SchedulePublishJob
{
SchedulePlan = plan,
AcademicTermId = term.Id,
ActiveAcademicTermId = term.Id,
CurrentStep = "等待后台检查"
};
jobId = job.Id;
db.AddRange(
term,
college,
campus,
building,
laboratory,
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
});
await db.SaveChangesAsync();
}
await using (var workerScope = provider.CreateAsyncScope())
{
var processor = workerScope.ServiceProvider
.GetRequiredService<SchedulePublishJobProcessor>();
await processor.ProcessAsync(jobId, CancellationToken.None);
}
await using var assertScope = provider.CreateAsyncScope();
var assertDb = assertScope.ServiceProvider.GetRequiredService<AppDbContext>();
var jobResult = await assertDb.SchedulePublishJobs.SingleAsync();
var planResult = await assertDb.SchedulePlans.SingleAsync();
return new(
jobResult.Status,
planResult.Status,
jobResult.ActiveAcademicTermId,
jobResult.CompletedSteps,
jobResult.CurrentStep,
jobResult.ErrorMessage,
planResult.PublishedAt);
}
finally
{
await provider.DisposeAsync();
File.Delete(databasePath);
}
}
private sealed record PublishResult(
SchedulePublishJobStatus JobStatus,
SchedulePlanStatus PlanStatus,
Guid? ActiveAcademicTermId,
int CompletedSteps,
string? CurrentStep,
string? ErrorMessage,
DateTime? PublishedAt);
}