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

排课约束增加课程、教师、学院、授课方式、场地要求、约束状态筛选,并支持批量修改当前筛选结果。
新增“非排时课程”授课方式:不进入自动排课
不占星期、节次和教室
不阻塞课表发布
允许正常选课
在班级课表和学生个人课表中单独展示
This commit is contained in:
biss committed 2026-07-25 08:18:25 +08:00
1 parent 30c15f89e5
commit fe508054d0
20 files changed
+3674 -25

No files matched your search

@@ -635,6 +635,7 @@ public sealed class CourseSelectionsController(
.Where(item => item.StudentId == student.Id) .Where(item => item.StudentId == student.Id)
.Select(item => (CourseEnrollmentStatus?)item.Status) .Select(item => (CourseEnrollmentStatus?)item.Status)
.FirstOrDefault(), .FirstOrDefault(),
x.TeachingTask.SchedulingMode == TeachingTaskSchedulingMode.Flexible,
db.ScheduleEntries db.ScheduleEntries
.Where(entry => .Where(entry =>
entry.TeachingTaskId == x.TeachingTaskId && entry.TeachingTaskId == x.TeachingTaskId &&
@@ -803,7 +804,8 @@ public sealed class CourseSelectionsController(
round.AcademicTermId, round.AcademicTermId,
[task.Id], [task.Id],
cancellationToken); cancellationToken);
if (candidateEntries.Count == 0) if (task.SchedulingMode == TeachingTaskSchedulingMode.Standard &&
candidateEntries.Count == 0)
return ConflictProblem("该教学班尚未发布课表,暂时不能选课。"); return ConflictProblem("该教学班尚未发布课表,暂时不能选课。");
var selectedTaskIds = await db.CourseEnrollments var selectedTaskIds = await db.CourseEnrollments
.Where(x => .Where(x =>
@@ -1010,6 +1012,7 @@ public sealed record StudentOfferingDto(
int EnrolledCount, int EnrolledCount,
bool IsOpenToAll, bool IsOpenToAll,
CourseEnrollmentStatus? EnrollmentStatus, CourseEnrollmentStatus? EnrollmentStatus,
bool IsFlexible,
IEnumerable<StudentScheduleDto> Schedules); IEnumerable<StudentScheduleDto> Schedules);
public sealed record StudentScheduleDto( public sealed record StudentScheduleDto(
@@ -131,6 +131,7 @@ public sealed class CoursesController(
x.CourseCategoryId, x.CourseCategoryId,
CategoryName = x.CourseCategory != null ? x.CourseCategory.Name : null, CategoryName = x.CourseCategory != null ? x.CourseCategory.Name : null,
x.Credits, x.Credits,
x.TotalHours,
x.Nature, x.Nature,
x.AssessmentMethod x.AssessmentMethod
}) })
@@ -83,12 +83,16 @@ public sealed class ScheduleSettingsController(AppDbContext db) : ControllerBase
x.Id, x.Id,
x.TaskNumber, x.TaskNumber,
x.Name, x.Name,
CourseCode = x.Course!.Code,
CourseName = x.Course!.Name, CourseName = x.Course!.Name,
CollegeId = x.Course.CollegeId,
CollegeName = x.Course.College!.Name,
TeacherNames = x.Teachers TeacherNames = x.Teachers
.OrderByDescending(item => item.IsPrimary) .OrderByDescending(item => item.IsPrimary)
.Select(item => item.Teacher!.Name), .Select(item => item.Teacher!.Name),
x.Capacity, x.Capacity,
x.WeeklyHours x.WeeklyHours,
x.SchedulingMode
}) })
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
var taskIds = tasks.Select(x => x.Id).ToList(); var taskIds = tasks.Select(x => x.Id).ToList();
@@ -104,11 +108,18 @@ public sealed class ScheduleSettingsController(AppDbContext db) : ControllerBase
task.Id, task.Id,
task.TaskNumber, task.TaskNumber,
task.Name, task.Name,
task.CourseCode,
task.CourseName, task.CourseName,
task.CollegeId,
task.CollegeName,
task.TeacherNames, task.TeacherNames,
task.Capacity, task.Capacity,
task.WeeklyHours, task.WeeklyHours,
RequiresClassroom = constraint?.RequiresClassroom ?? true, task.SchedulingMode,
HasCustomConstraint = constraint is not null,
RequiresClassroom = task.SchedulingMode == TeachingTaskSchedulingMode.Flexible
? false
: constraint?.RequiresClassroom ?? true,
constraint?.RequiredCampusId, constraint?.RequiredCampusId,
constraint?.RequiredBuildingId, constraint?.RequiredBuildingId,
AllowedDayOfWeeks = ParseDays(constraint?.AllowedDayOfWeeks), AllowedDayOfWeeks = ParseDays(constraint?.AllowedDayOfWeeks),
@@ -132,9 +143,28 @@ public sealed class ScheduleSettingsController(AppDbContext db) : ControllerBase
request.LatestPeriod.HasValue && request.LatestPeriod.HasValue &&
request.EarliestPeriod > request.LatestPeriod) request.EarliestPeriod > request.LatestPeriod)
return ValidationProblem("最早节次不能晚于最晚节次。"); return ValidationProblem("最早节次不能晚于最晚节次。");
if (!Enum.IsDefined(request.SchedulingMode))
return ValidationProblem("授课方式无效。");
var task = await db.TeachingTasks var task = await db.TeachingTasks
.FirstOrDefaultAsync(x => x.Id == teachingTaskId, cancellationToken); .FirstOrDefaultAsync(x => x.Id == teachingTaskId, cancellationToken);
if (task is null) return NotFound(); if (task is null) return NotFound();
if (request.SchedulingMode == TeachingTaskSchedulingMode.Flexible &&
await HasScheduleEntriesAsync([teachingTaskId], cancellationToken))
return ConflictProblem("该教学任务已有正常排课记录,请先删除排课记录后再改为非排时课程。");
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; Building? building = null;
if (request.RequiredBuildingId.HasValue) if (request.RequiredBuildingId.HasValue)
@@ -195,6 +225,124 @@ public sealed class ScheduleSettingsController(AppDbContext db) : ControllerBase
return NoContent(); 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 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) => private static int[] ParseDays(string? value) =>
string.IsNullOrWhiteSpace(value) string.IsNullOrWhiteSpace(value)
? [] ? []
@@ -211,6 +359,7 @@ public sealed record ScheduleTimeSlotRequest(
bool IsEnabled); bool IsEnabled);
public sealed record TeachingTaskScheduleConstraintRequest( public sealed record TeachingTaskScheduleConstraintRequest(
TeachingTaskSchedulingMode SchedulingMode,
bool RequiresClassroom, bool RequiresClassroom,
Guid? RequiredCampusId, Guid? RequiredCampusId,
Guid? RequiredBuildingId, Guid? RequiredBuildingId,
@@ -218,3 +367,13 @@ public sealed record TeachingTaskScheduleConstraintRequest(
IReadOnlyList<int> AllowedDayOfWeeks, IReadOnlyList<int> AllowedDayOfWeeks,
[Range(1, 30)] int? EarliestPeriod, [Range(1, 30)] int? EarliestPeriod,
[Range(1, 30)] int? LatestPeriod); [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);
Loaded 3 of 20 files, more files were not shown because too many files have changed in this diff. Show more