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

274 lines
9.1 KiB
C#

using Jiaowu.Api.Controllers;
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.System;
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;
namespace Jiaowu.Api.Tests;
public sealed class SchedulesControllerTests
{
[Fact]
public async Task Manual_entry_adds_experiment_hours_to_regular_schedule()
{
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-LAB",
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 = "LAB", Name = "实验学院" };
var campus = new Campus { Code = "LAB-CAMPUS", Name = "实验校区" };
var building = new Building
{
Code = "LAB-BUILDING",
Name = "实验楼",
Campus = campus
};
var laboratory = new Classroom
{
Code = "LAB-201",
Name = "实验室 201",
Building = building,
Capacity = 40,
RoomType = "实验室"
};
var course = new Course
{
Code = "LAB-01",
Name = "实验混合课程",
College = college,
Credits = 2,
TotalHours = 32,
LectureHours = 16,
PracticeHours = 16
};
var task = new TeachingTask
{
TaskNumber = "TASK-LAB",
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"
};
plan.Entries.Add(new ScheduleEntry
{
TeachingTask = task,
DayOfWeek = 1,
StartPeriod = 1,
PeriodCount = 1,
StartWeek = 1,
EndWeek = 16,
WeekPattern = WeekPattern.All
});
db.AddRange(term, college, campus, building, laboratory, course, task, 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)
});
db.TeachingTaskScheduleConstraints.Add(
new TeachingTaskScheduleConstraint
{
TeachingTask = task,
RequiresClassroom = false
});
await db.SaveChangesAsync();
var controller = new SchedulesController(db);
var result = await controller.CreateEntry(
plan.Id,
new ScheduleEntryRequest(
task.Id,
laboratory.Id,
2,
1,
1,
1,
16,
WeekPattern.All,
null,
ScheduleEntryKind.Experiment),
CancellationToken.None);
Assert.IsType<CreatedResult>(result);
var entries = await db.ScheduleEntries.OrderBy(x => x.Kind).ToListAsync();
Assert.Equal(2, entries.Count);
Assert.Equal(ScheduleEntryKind.Experiment, entries[1].Kind);
Assert.Equal(laboratory.Id, entries[1].ClassroomId);
}
[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)
{
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());
var outbox = await db.BackgroundJobOutboxMessages.SingleAsync();
Assert.Equal(BackgroundJobKind.SchedulePublish, outbox.JobKind);
Assert.Equal(response.Id, outbox.JobId);
Assert.Equal(BackgroundJobOutboxState.Pending, outbox.State);
}
[Fact]
public async Task Latest_auto_schedule_job_remains_available_after_completion()
{
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-F",
Name = "2026 秋季",
AcademicYear = "2026-2027",
Season = TermSeason.Autumn,
StartDate = new DateOnly(2026, 9, 7),
EndDate = new DateOnly(2027, 1, 17)
};
var plan = new SchedulePlan
{
AcademicTerm = term,
Name = "排课草稿",
Version = "V1"
};
var completed = new AutomaticScheduleJob
{
SchedulePlan = plan,
Status = AutomaticScheduleJobStatus.Succeeded,
CreatedEntries = 12,
CompletedTasks = 5,
TotalTasks = 6,
ProcessedTasks = 6,
MessagesJson = "[\"TASK-06 仍有 2 学时无法安排\"]",
CompletedAt = DateTime.UtcNow
};
db.AddRange(term, plan, completed);
await db.SaveChangesAsync();
var controller = new SchedulesController(db);
var result = await controller.GetLatestAutomaticScheduleJob(
plan.Id,
CancellationToken.None);
var ok = Assert.IsType<OkObjectResult>(result.Result);
var response = Assert.IsType<AutomaticScheduleJobResponse>(ok.Value);
Assert.Equal(completed.Id, response.Id);
Assert.Equal(AutomaticScheduleJobStatus.Succeeded, response.Status);
Assert.Equal(
"TASK-06 仍有 2 学时无法安排",
Assert.Single(response.Messages));
}
}