实验排课

This commit is contained in:
2026-08-09 12:30:33 +08:00 Unverified
parent 1229f3163d
commit c692002676
25 changed files with 21294 additions and 1189 deletions
@@ -91,6 +91,36 @@ public sealed class ExperimentsController(
.Select(item => item.AdministrativeClass!.Name)
})
.ToListAsync(cancellationToken),
ScheduleEntries = await db.ScheduleEntries.AsNoTracking()
.Where(x => x.SchedulePlan!.Status == SchedulePlanStatus.Published &&
x.Kind == ScheduleEntryKind.Experiment &&
x.ClassroomId.HasValue &&
AccessibleTeachingTasks().Select(task => task.Id)
.Contains(x.TeachingTaskId))
.Where(x => !academicTermId.HasValue ||
x.SchedulePlan!.AcademicTermId == academicTermId.Value)
.OrderBy(x => x.TeachingTask!.Course!.Code)
.ThenBy(x => x.TeachingTask!.TaskNumber)
.ThenBy(x => x.DayOfWeek)
.ThenBy(x => x.StartPeriod)
.Select(x => new
{
x.Id,
x.TeachingTaskId,
TaskNumber = x.TeachingTask!.TaskNumber,
CourseCode = x.TeachingTask.Course!.Code,
CourseName = x.TeachingTask.Course.Name,
x.DayOfWeek,
x.StartPeriod,
x.PeriodCount,
x.StartWeek,
x.EndWeek,
x.WeekPattern,
ClassroomName = x.Classroom!.Name,
BuildingName = x.Classroom.Building!.Name,
CampusName = x.Classroom.Building.Campus!.Name
})
.ToListAsync(cancellationToken),
Classrooms = await db.Classrooms.AsNoTracking()
.Where(x => x.IsEnabled)
.OrderBy(x => x.Building!.Campus!.SortOrder)
@@ -103,6 +133,7 @@ public sealed class ExperimentsController(
BuildingName = x.Building!.Name,
CampusName = x.Building.Campus!.Name,
x.Capacity
,x.TeachingVenueNature
})
.ToListAsync(cancellationToken),
Periods = periodItems
@@ -135,6 +166,7 @@ public sealed class ExperimentsController(
{
x.Id,
x.TeachingTaskId,
x.ScheduleEntryId,
x.Code,
x.Name,
x.ArrangementMode,
@@ -157,6 +189,18 @@ public sealed class ExperimentsController(
ClassNames = x.TeachingTask.Classes
.OrderBy(item => item.AdministrativeClass!.Code)
.Select(item => item.AdministrativeClass!.Name),
ScheduleEntry = x.ScheduleEntryId == null ? null : new
{
x.ScheduleEntry!.DayOfWeek,
x.ScheduleEntry.StartPeriod,
x.ScheduleEntry.PeriodCount,
x.ScheduleEntry.StartWeek,
x.ScheduleEntry.EndWeek,
x.ScheduleEntry.WeekPattern,
ClassroomName = x.ScheduleEntry.Classroom!.Name,
BuildingName = x.ScheduleEntry.Classroom.Building!.Name,
CampusName = x.ScheduleEntry.Classroom.Building.Campus!.Name
},
Sessions = x.Sessions
.OrderBy(item => item.SessionDate)
.ThenBy(item => item.StartPeriod)
@@ -209,6 +253,7 @@ public sealed class ExperimentsController(
x.Code,
x.Name,
x.ArrangementMode,
x.ScheduleEntryId,
x.Description,
x.Requirements,
x.StartDate,
@@ -222,6 +267,18 @@ public sealed class ExperimentsController(
TeacherNames = x.TeachingTask.Teachers
.OrderByDescending(item => item.IsPrimary)
.Select(item => item.Teacher!.Name),
ScheduleEntry = x.ScheduleEntryId == null ? null : new
{
x.ScheduleEntry!.DayOfWeek,
x.ScheduleEntry.StartPeriod,
x.ScheduleEntry.PeriodCount,
x.ScheduleEntry.StartWeek,
x.ScheduleEntry.EndWeek,
x.ScheduleEntry.WeekPattern,
ClassroomName = x.ScheduleEntry.Classroom!.Name,
BuildingName = x.ScheduleEntry.Classroom.Building!.Name,
CampusName = x.ScheduleEntry.Classroom.Building.Campus!.Name
},
Sessions = x.Sessions
.Where(item => item.Status == ExperimentSessionStatus.Scheduled)
.OrderBy(item => item.SessionDate)
@@ -272,6 +329,9 @@ public sealed class ExperimentsController(
var problem = ValidateProjectRequest(request, task.AcademicTerm!);
if (problem is not null) return ValidationProblem(problem);
var scheduleEntry = await ValidateScheduleEntryAsync(
request, task.Id, cancellationToken);
if (scheduleEntry.Problem is not null) return ValidationProblem(scheduleEntry.Problem);
var code = request.Code.Trim();
if (await db.ExperimentProjects.AnyAsync(x =>
@@ -283,6 +343,7 @@ public sealed class ExperimentsController(
var project = new ExperimentProject
{
TeachingTaskId = request.TeachingTaskId,
ScheduleEntryId = scheduleEntry.Entry?.Id,
Code = code,
Name = request.Name.Trim(),
ArrangementMode = request.ArrangementMode,
@@ -302,6 +363,8 @@ public sealed class ExperimentsController(
ExperimentProjectBatchRequest request,
CancellationToken cancellationToken)
{
if (request.ArrangementMode == ExperimentArrangementMode.Centralized)
return ValidationProblem("集中安排的实验项目请逐项绑定已发布课表中的实验课。");
var taskIds = request.TeachingTaskIds
.Where(x => x != Guid.Empty)
.Distinct()
@@ -387,6 +450,9 @@ public sealed class ExperimentsController(
request,
project.TeachingTask!.AcademicTerm!);
if (problem is not null) return ValidationProblem(problem);
var scheduleEntry = await ValidateScheduleEntryAsync(
request, project.TeachingTaskId, cancellationToken);
if (scheduleEntry.Problem is not null) return ValidationProblem(scheduleEntry.Problem);
var code = request.Code.Trim();
if (await db.ExperimentProjects.AnyAsync(x =>
@@ -399,6 +465,7 @@ public sealed class ExperimentsController(
project.Code = code;
project.Name = request.Name.Trim();
project.ArrangementMode = request.ArrangementMode;
project.ScheduleEntryId = scheduleEntry.Entry?.Id;
project.Description = Normalize(request.Description);
project.Requirements = Normalize(request.Requirements);
project.StartDate = request.StartDate;
@@ -438,9 +505,14 @@ public sealed class ExperimentsController(
if (project is null) return NotFound();
if (project.Status != ExperimentProjectStatus.Draft)
return ConflictProblem("只有草稿实验项目可以发布。");
if (!project.Sessions.Any(x =>
x.Status == ExperimentSessionStatus.Scheduled))
return ConflictProblem("请至少安排一个有效实验场次后再发布。");
var hasSchedule = project.ArrangementMode == ExperimentArrangementMode.Centralized
? project.ScheduleEntryId.HasValue || project.Sessions.Any(x =>
x.Status == ExperimentSessionStatus.Scheduled)
: project.Sessions.Any(x => x.Status == ExperimentSessionStatus.Scheduled);
if (!hasSchedule)
return ConflictProblem(project.ArrangementMode == ExperimentArrangementMode.Centralized
? "请先绑定已发布课表中的实验课后再发布。"
: "请至少安排一个有效实验场次后再发布。");
if (project.Sessions.Any(x =>
x.Status == ExperimentSessionStatus.Scheduled &&
(x.SessionDate < project.StartDate ||
@@ -506,6 +578,8 @@ public sealed class ExperimentsController(
if (project is null) return NotFound();
if (project.Status == ExperimentProjectStatus.Closed)
return ConflictProblem("已关闭实验项目不能再增加场次。");
if (project.ArrangementMode == ExperimentArrangementMode.Centralized)
return ConflictProblem("集中安排的实验项目直接使用已发布课表中的实验课,不能在此重复排时派地点。");
var problem = await ValidateSessionAsync(
project,
@@ -588,6 +662,9 @@ public sealed class ExperimentsController(
if (project.Status == ExperimentProjectStatus.Closed)
return ConflictProblem(
$"实验项目“{project.Name}”已关闭,不能再增加场次。");
if (project.ArrangementMode == ExperimentArrangementMode.Centralized)
return ConflictProblem(
$"实验项目“{project.Name}”为集中安排,请直接使用已发布课表中的实验课。");
var sessionRequest = item.ToSessionRequest();
var problem = await ValidateSessionAsync(
@@ -930,6 +1007,8 @@ public sealed class ExperimentsController(
x.Id == request.ClassroomId && x.IsEnabled,
cancellationToken);
if (classroom is null) return "实验教室不存在或已停用。";
if (!TeachingVenueNatureRules.SupportsExperiment(classroom.TeachingVenueNature))
return "所选场地未标注实验教学性质。";
if (request.Capacity > classroom.Capacity)
return $"场次容量不能超过教室容量 {classroom.Capacity} 人。";
if (project.ArrangementMode ==
@@ -1173,6 +1252,30 @@ public sealed class ExperimentsController(
return null;
}
private async Task<(ScheduleEntry? Entry, string? Problem)> ValidateScheduleEntryAsync(
ExperimentProjectRequest request,
Guid teachingTaskId,
CancellationToken cancellationToken)
{
if (request.ArrangementMode == ExperimentArrangementMode.SelfScheduled)
return request.ScheduleEntryId.HasValue
? (null, "自行安排的实验项目不能绑定课表实验课。")
: (null, null);
if (!request.ScheduleEntryId.HasValue)
return (null, "集中安排的实验项目必须绑定已发布课表中的实验课。");
var entry = await db.ScheduleEntries
.Include(x => x.SchedulePlan)
.FirstOrDefaultAsync(x => x.Id == request.ScheduleEntryId, cancellationToken);
if (entry is null || entry.SchedulePlan!.Status != SchedulePlanStatus.Published ||
entry.Kind != ScheduleEntryKind.Experiment || !entry.ClassroomId.HasValue ||
entry.TeachingTaskId != teachingTaskId)
return (null, "只能绑定本教学任务已发布、已安排实验室的实验课。");
if (!await AccessibleTeachingTasks().AnyAsync(x => x.Id == teachingTaskId, cancellationToken))
return (null, "该教学任务不在当前管理范围内。");
return (entry, null);
}
private static string? Normalize(string? value) =>
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
@@ -1193,7 +1296,8 @@ public sealed record ExperimentProjectRequest(
[MaxLength(1000)] string? Description,
[MaxLength(1000)] string? Requirements,
DateOnly StartDate,
DateOnly EndDate);
DateOnly EndDate,
Guid? ScheduleEntryId = null);
public sealed record ExperimentProjectBatchRequest(
[Required] IReadOnlyList<Guid> TeachingTaskIds,