教学任务按 授课周数 × 周学时 = 课程总学时 双端校验,不匹配会显示具体差额并阻止保存;公共课批量生成同样校验。
排课约束增加课程、教师、学院、授课方式、场地要求、约束状态筛选,并支持批量修改当前筛选结果。 新增“非排时课程”授课方式:不进入自动排课 不占星期、节次和教室 不阻塞课表发布 允许正常选课 在班级课表和学生个人课表中单独展示
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user