教学任务按 授课周数 × 周学时 = 课程总学时 双端校验,不匹配会显示具体差额并阻止保存;公共课批量生成同样校验。

排课约束增加课程、教师、学院、授课方式、场地要求、约束状态筛选,并支持批量修改当前筛选结果。
新增“非排时课程”授课方式:不进入自动排课
不占星期、节次和教室
不阻塞课表发布
允许正常选课
在班级课表和学生个人课表中单独展示
This commit is contained in:
2026-07-25 08:18:25 +08:00 Unverified
parent 30c15f89e5
commit fe508054d0
20 changed files with 3785 additions and 136 deletions
@@ -82,13 +82,17 @@ public sealed class ScheduleSettingsController(AppDbContext db) : ControllerBase
{
x.Id,
x.TaskNumber,
x.Name,
CourseName = x.Course!.Name,
TeacherNames = x.Teachers
x.Name,
CourseCode = x.Course!.Code,
CourseName = x.Course!.Name,
CollegeId = x.Course.CollegeId,
CollegeName = x.Course.College!.Name,
TeacherNames = x.Teachers
.OrderByDescending(item => item.IsPrimary)
.Select(item => item.Teacher!.Name),
x.Capacity,
x.WeeklyHours
x.Capacity,
x.WeeklyHours,
x.SchedulingMode
})
.ToListAsync(cancellationToken);
var taskIds = tasks.Select(x => x.Id).ToList();
@@ -103,12 +107,19 @@ public sealed class ScheduleSettingsController(AppDbContext db) : ControllerBase
{
task.Id,
task.TaskNumber,
task.Name,
task.CourseName,
task.TeacherNames,
task.Capacity,
task.WeeklyHours,
RequiresClassroom = constraint?.RequiresClassroom ?? true,
task.Name,
task.CourseCode,
task.CourseName,
task.CollegeId,
task.CollegeName,
task.TeacherNames,
task.Capacity,
task.WeeklyHours,
task.SchedulingMode,
HasCustomConstraint = constraint is not null,
RequiresClassroom = task.SchedulingMode == TeachingTaskSchedulingMode.Flexible
? false
: constraint?.RequiresClassroom ?? true,
constraint?.RequiredCampusId,
constraint?.RequiredBuildingId,
AllowedDayOfWeeks = ParseDays(constraint?.AllowedDayOfWeeks),
@@ -128,15 +139,34 @@ public sealed class ScheduleSettingsController(AppDbContext db) : ControllerBase
{
if (request.AllowedDayOfWeeks.Any(day => day is < 1 or > 7))
return ValidationProblem("允许上课日必须位于星期一至星期日。");
if (request.EarliestPeriod.HasValue &&
request.LatestPeriod.HasValue &&
request.EarliestPeriod > request.LatestPeriod)
return ValidationProblem("最早节次不能晚于最晚节次。");
var task = await db.TeachingTasks
.FirstOrDefaultAsync(x => x.Id == teachingTaskId, cancellationToken);
if (task is null) return NotFound();
if (request.EarliestPeriod.HasValue &&
request.LatestPeriod.HasValue &&
request.EarliestPeriod > request.LatestPeriod)
return ValidationProblem("最早节次不能晚于最晚节次。");
if (!Enum.IsDefined(request.SchedulingMode))
return ValidationProblem("授课方式无效。");
var task = await db.TeachingTasks
.FirstOrDefaultAsync(x => x.Id == teachingTaskId, cancellationToken);
if (task is null) return NotFound();
if (request.SchedulingMode == TeachingTaskSchedulingMode.Flexible &&
await HasScheduleEntriesAsync([teachingTaskId], cancellationToken))
return ConflictProblem("该教学任务已有正常排课记录,请先删除排课记录后再改为非排时课程。");
Building? building = null;
task.SchedulingMode = request.SchedulingMode;
if (request.SchedulingMode == TeachingTaskSchedulingMode.Flexible)
{
var flexibleConstraint = await db.TeachingTaskScheduleConstraints
.Include(x => x.AllowedClassrooms)
.FirstOrDefaultAsync(x => x.TeachingTaskId == teachingTaskId, cancellationToken);
if (flexibleConstraint is not null)
{
ClearConstraint(flexibleConstraint);
}
await db.SaveChangesAsync(cancellationToken);
return NoContent();
}
Building? building = null;
if (request.RequiredBuildingId.HasValue)
{
building = await db.Buildings.AsNoTracking()
@@ -191,10 +221,128 @@ public sealed class ScheduleSettingsController(AppDbContext db) : ControllerBase
? request.AllowedClassroomIds.Distinct().Select(classroomId =>
new TeachingTaskAllowedClassroom { ClassroomId = classroomId }).ToList()
: [];
await db.SaveChangesAsync(cancellationToken);
return NoContent();
}
[HttpPut("constraints/batch")]
public async Task<ActionResult> SaveConstraintsBatch(
TeachingTaskScheduleConstraintBatchRequest request,
CancellationToken cancellationToken)
{
var taskIds = request.TeachingTaskIds.Distinct().ToArray();
if (taskIds.Length == 0)
return ValidationProblem("请至少选择一个教学任务。");
if (request.AllowedDayOfWeeks?.Any(day => day is < 1 or > 7) == true)
return ValidationProblem("允许上课日必须位于星期一至星期日。");
if (request.EarliestPeriod.HasValue &&
request.LatestPeriod.HasValue &&
request.EarliestPeriod > request.LatestPeriod)
return ValidationProblem("最早节次不能晚于最晚节次。");
if (request.SchedulingMode.HasValue &&
!Enum.IsDefined(request.SchedulingMode.Value))
return ValidationProblem("授课方式无效。");
if (!request.SchedulingMode.HasValue &&
!request.RequiresClassroom.HasValue &&
request.AllowedDayOfWeeks is null &&
!request.UpdatePeriodRange &&
!request.EarliestPeriod.HasValue &&
!request.LatestPeriod.HasValue)
return ValidationProblem("请至少选择一项需要批量修改的设置。");
var tasks = await db.TeachingTasks
.Where(x =>
taskIds.Contains(x.Id) &&
x.AcademicTermId == request.AcademicTermId &&
x.Status == TeachingTaskStatus.Published)
.ToListAsync(cancellationToken);
if (tasks.Count != taskIds.Length)
return ValidationProblem("部分教学任务不存在、未发布或不属于当前学期。");
if (request.SchedulingMode == TeachingTaskSchedulingMode.Flexible &&
await HasScheduleEntriesAsync(taskIds, cancellationToken))
return ConflictProblem("所选教学任务中存在已有正常排课记录的课程,请先删除这些排课记录。");
var constraints = await db.TeachingTaskScheduleConstraints
.Where(x => taskIds.Contains(x.TeachingTaskId))
.Include(x => x.AllowedClassrooms)
.ToDictionaryAsync(x => x.TeachingTaskId, cancellationToken);
foreach (var task in tasks)
{
if (request.SchedulingMode.HasValue)
task.SchedulingMode = request.SchedulingMode.Value;
if (task.SchedulingMode == TeachingTaskSchedulingMode.Flexible)
{
if (constraints.TryGetValue(task.Id, out var flexibleConstraint))
ClearConstraint(flexibleConstraint);
continue;
}
if (!constraints.TryGetValue(task.Id, out var constraint))
{
var changesConstraint =
request.RequiresClassroom.HasValue ||
request.AllowedDayOfWeeks is not null ||
request.UpdatePeriodRange;
if (!changesConstraint) continue;
constraint = new TeachingTaskScheduleConstraint { TeachingTaskId = task.Id };
constraints[task.Id] = constraint;
db.TeachingTaskScheduleConstraints.Add(constraint);
}
if (request.RequiresClassroom.HasValue)
{
constraint.RequiresClassroom = request.RequiresClassroom.Value;
if (!request.RequiresClassroom.Value)
{
constraint.RequiredCampusId = null;
constraint.RequiredBuildingId = null;
db.TeachingTaskAllowedClassrooms.RemoveRange(constraint.AllowedClassrooms);
constraint.AllowedClassrooms = [];
}
}
if (request.AllowedDayOfWeeks is not null)
{
constraint.AllowedDayOfWeeks = request.AllowedDayOfWeeks.Count == 0
? null
: string.Join(',', request.AllowedDayOfWeeks.Distinct().Order());
}
if (request.UpdatePeriodRange)
{
constraint.EarliestPeriod = request.EarliestPeriod;
constraint.LatestPeriod = request.LatestPeriod;
}
}
await db.SaveChangesAsync(cancellationToken);
return NoContent();
return Ok(new { AffectedCount = tasks.Count });
}
private Task<bool> HasScheduleEntriesAsync(
IReadOnlyCollection<Guid> taskIds,
CancellationToken cancellationToken) =>
db.ScheduleEntries.AsNoTracking()
.AnyAsync(x => taskIds.Contains(x.TeachingTaskId), cancellationToken);
private void ClearConstraint(TeachingTaskScheduleConstraint constraint)
{
constraint.RequiresClassroom = false;
constraint.RequiredCampusId = null;
constraint.RequiredBuildingId = null;
constraint.AllowedDayOfWeeks = null;
constraint.EarliestPeriod = null;
constraint.LatestPeriod = null;
db.TeachingTaskAllowedClassrooms.RemoveRange(constraint.AllowedClassrooms);
constraint.AllowedClassrooms = [];
}
private ActionResult ConflictProblem(string detail) =>
Conflict(new ProblemDetails
{
Title = "操作冲突",
Detail = detail,
Status = StatusCodes.Status409Conflict
});
private static int[] ParseDays(string? value) =>
string.IsNullOrWhiteSpace(value)
? []
@@ -211,6 +359,7 @@ public sealed record ScheduleTimeSlotRequest(
bool IsEnabled);
public sealed record TeachingTaskScheduleConstraintRequest(
TeachingTaskSchedulingMode SchedulingMode,
bool RequiresClassroom,
Guid? RequiredCampusId,
Guid? RequiredBuildingId,
@@ -218,3 +367,13 @@ public sealed record TeachingTaskScheduleConstraintRequest(
IReadOnlyList<int> AllowedDayOfWeeks,
[Range(1, 30)] int? EarliestPeriod,
[Range(1, 30)] int? LatestPeriod);
public sealed record TeachingTaskScheduleConstraintBatchRequest(
Guid AcademicTermId,
[MinLength(1), MaxLength(500)] IReadOnlyCollection<Guid> TeachingTaskIds,
TeachingTaskSchedulingMode? SchedulingMode,
bool? RequiresClassroom,
IReadOnlyList<int>? AllowedDayOfWeeks,
bool UpdatePeriodRange,
[Range(1, 30)] int? EarliestPeriod,
[Range(1, 30)] int? LatestPeriod);