支持为同一学期、同一课程的多个教学任务批量创建相同实验项目。

新增批量排课表,可为多个实验项目分别设置日期、节次、实验室和容量。
批量排课采用原子事务:任一项目发生教室、教师、班级或课表冲突,整批不写入。
已发布项目批量新增场次时,继续按原逻辑通知学生。
后端继续执行管理范围、教学任务状态、开放日期和容量校验。
页面在 390px 手机宽度下无横向溢出。
This commit is contained in:
2026-08-01 16:08:03 +08:00 Unverified
parent 551f1143b7
commit 9d525826ce
3 changed files with 560 additions and 15 deletions
@@ -12,6 +12,112 @@ namespace Jiaowu.Api.Tests;
public sealed class ExperimentsControllerTests
{
[Fact]
public async Task BatchProjects_CreateSameDefinitionForSameCourseTasks()
{
await using var fixture = await ExperimentFixture.CreateAsync();
var controller = fixture.Controller(fixture.ManagerScope);
var result = await controller.CreateProjects(
fixture.BatchProjectRequest(
ExperimentArrangementMode.Centralized),
CancellationToken.None);
Assert.IsType<CreatedResult>(result);
var projects = await fixture.Db.ExperimentProjects
.OrderBy(x => x.TeachingTaskId)
.ToListAsync();
Assert.Equal(2, projects.Count);
Assert.Equal(2, projects.Select(x => x.TeachingTaskId).Distinct().Count());
Assert.All(projects, project =>
{
Assert.Equal("LAB-BATCH", project.Code);
Assert.Equal("公共实验任务", project.Name);
Assert.Equal(ExperimentProjectStatus.Draft, project.Status);
});
}
[Fact]
public async Task BatchSessions_RollsBackAllRowsWhenOneConflicts()
{
await using var fixture = await ExperimentFixture.CreateAsync();
var controller = fixture.Controller(fixture.ManagerScope);
await controller.CreateProjects(
fixture.BatchProjectRequest(
ExperimentArrangementMode.SelfScheduled),
CancellationToken.None);
var projectIds = await fixture.Db.ExperimentProjects
.OrderBy(x => x.TeachingTaskId)
.Select(x => x.Id)
.ToListAsync();
var result = await controller.CreateSessions(
new ExperimentSessionBatchRequest(
[
new ExperimentSessionBatchItem(
projectIds[0],
fixture.Classroom.Id,
fixture.Term.StartDate,
1,
2,
20,
null),
new ExperimentSessionBatchItem(
projectIds[1],
fixture.Classroom.Id,
fixture.Term.StartDate,
1,
2,
20,
null)
]),
CancellationToken.None);
Assert.IsType<ConflictObjectResult>(result);
fixture.Db.ChangeTracker.Clear();
Assert.Empty(await fixture.Db.ExperimentSessions.ToListAsync());
}
[Fact]
public async Task BatchSessions_CreatesRowsAtomicallyWhenAllAreValid()
{
await using var fixture = await ExperimentFixture.CreateAsync();
var controller = fixture.Controller(fixture.ManagerScope);
await controller.CreateProjects(
fixture.BatchProjectRequest(
ExperimentArrangementMode.SelfScheduled),
CancellationToken.None);
var projectIds = await fixture.Db.ExperimentProjects
.OrderBy(x => x.TeachingTaskId)
.Select(x => x.Id)
.ToListAsync();
var result = await controller.CreateSessions(
new ExperimentSessionBatchRequest(
[
new ExperimentSessionBatchItem(
projectIds[0],
fixture.Classroom.Id,
fixture.Term.StartDate,
1,
2,
20,
null),
new ExperimentSessionBatchItem(
projectIds[1],
fixture.SecondClassroom.Id,
fixture.Term.StartDate,
1,
2,
20,
null)
]),
CancellationToken.None);
Assert.IsType<CreatedResult>(result);
Assert.Equal(2, await fixture.Db.ExperimentSessions.CountAsync());
}
[Fact]
public async Task CentralizedProject_PublishesAndUsesTeachingTaskRoster()
{
@@ -421,6 +527,15 @@ public sealed class ExperimentsControllerTests
}
]
};
var secondTask = new TeachingTask
{
TaskNumber = "2099-1-CSLAB-02",
Name = "系统实验教学班 02",
AcademicTermId = term.Id,
CourseId = course.Id,
Capacity = 40,
Status = TeachingTaskStatus.Published
};
db.AddRange(
manager,
studentUser,
@@ -435,7 +550,8 @@ public sealed class ExperimentsControllerTests
teacher,
term,
course,
task);
task,
secondTask);
for (var period = 1; period <= 12; period++)
{
db.ScheduleTimeSlots.Add(new ScheduleTimeSlot
@@ -486,6 +602,21 @@ public sealed class ExperimentsControllerTests
Term.StartDate,
Term.StartDate.AddDays(14));
public ExperimentProjectBatchRequest BatchProjectRequest(
ExperimentArrangementMode mode) =>
new(
Db.TeachingTasks
.OrderBy(x => x.TaskNumber)
.Select(x => x.Id)
.ToList(),
"LAB-BATCH",
"公共实验任务",
mode,
"多个教学任务共用的实验内容。",
"携带校园卡。",
Term.StartDate,
Term.StartDate.AddDays(14));
public ExperimentSessionRequest SessionRequest(
int startPeriod,
int periodCount,