自动排课完成后逐条显示“需人工处理”的任务和原因,并提供“检查排课约束”入口;刷新页面后仍可读取最近一次结果。

批量排课约束新增校区、教学楼、多教室统一指定,也支持清除原有限定。
课表新增“总视图”,同一时段、不同周次课程会并列显示。
周视图按教学周过滤,支持上一周、下一周、周次下拉和回到本周。
日视图按周次过滤;连续两节课程会跨两行完整占格。
This commit is contained in:
2026-07-25 10:55:09 +08:00 Unverified
parent db91988264
commit e4a12931ea
8 changed files with 652 additions and 51 deletions
@@ -245,10 +245,13 @@ public sealed class ScheduleSettingsController(AppDbContext db) : ControllerBase
if (!request.SchedulingMode.HasValue &&
!request.RequiresClassroom.HasValue &&
request.AllowedDayOfWeeks is null &&
!request.UpdateClassroomScope &&
!request.UpdatePeriodRange &&
!request.EarliestPeriod.HasValue &&
!request.LatestPeriod.HasValue)
return ValidationProblem("请至少选择一项需要批量修改的设置。");
if (request.UpdateClassroomScope && request.RequiresClassroom == false)
return ValidationProblem("批量指定教室范围时,场地要求不能设置为不占用教室。");
var tasks = await db.TeachingTasks
.Where(x =>
@@ -261,6 +264,48 @@ public sealed class ScheduleSettingsController(AppDbContext db) : ControllerBase
if (request.SchedulingMode == TeachingTaskSchedulingMode.Flexible &&
await HasScheduleEntriesAsync(taskIds, cancellationToken))
return ConflictProblem("所选教学任务中存在已有正常排课记录的课程,请先删除这些排课记录。");
if (request.UpdateClassroomScope && tasks.Any(task =>
(request.SchedulingMode ?? task.SchedulingMode) ==
TeachingTaskSchedulingMode.Flexible))
return ConflictProblem("非排时课程不能指定教室,请先将当前筛选结果限定为正常排课课程。");
Building? building = null;
List<Classroom> allowedRooms = [];
if (request.UpdateClassroomScope)
{
if (request.RequiredBuildingId.HasValue)
{
building = await db.Buildings.AsNoTracking()
.FirstOrDefaultAsync(
x => x.Id == request.RequiredBuildingId && x.IsEnabled,
cancellationToken);
if (building is null)
return ValidationProblem("指定教学楼不存在或已停用。");
if (request.RequiredCampusId.HasValue &&
building.CampusId != request.RequiredCampusId)
return ValidationProblem("指定教学楼不属于所选校区。");
}
if (request.RequiredCampusId.HasValue &&
!await db.Campuses.AnyAsync(
x => x.Id == request.RequiredCampusId && x.IsEnabled,
cancellationToken))
return ValidationProblem("指定校区不存在或已停用。");
var roomIds = request.AllowedClassroomIds?.Distinct().ToArray() ?? [];
allowedRooms = await db.Classrooms.AsNoTracking()
.Where(x => roomIds.Contains(x.Id) && x.IsEnabled)
.Include(x => x.Building)
.ToListAsync(cancellationToken);
if (allowedRooms.Count != roomIds.Length)
return ValidationProblem("部分指定教室不存在或已停用。");
if (building is not null &&
allowedRooms.Any(x => x.BuildingId != building.Id))
return ValidationProblem("指定教室必须位于所选教学楼。");
if (request.RequiredCampusId.HasValue &&
allowedRooms.Any(x =>
x.Building!.CampusId != request.RequiredCampusId))
return ValidationProblem("指定教室必须位于所选校区。");
}
var constraints = await db.TeachingTaskScheduleConstraints
.Where(x => taskIds.Contains(x.TeachingTaskId))
@@ -282,6 +327,7 @@ public sealed class ScheduleSettingsController(AppDbContext db) : ControllerBase
var changesConstraint =
request.RequiresClassroom.HasValue ||
request.AllowedDayOfWeeks is not null ||
request.UpdateClassroomScope ||
request.UpdatePeriodRange;
if (!changesConstraint) continue;
constraint = new TeachingTaskScheduleConstraint { TeachingTaskId = task.Id };
@@ -306,6 +352,19 @@ public sealed class ScheduleSettingsController(AppDbContext db) : ControllerBase
? null
: string.Join(',', request.AllowedDayOfWeeks.Distinct().Order());
}
if (request.UpdateClassroomScope)
{
constraint.RequiresClassroom = true;
constraint.RequiredCampusId = request.RequiredCampusId;
constraint.RequiredBuildingId = request.RequiredBuildingId;
db.TeachingTaskAllowedClassrooms.RemoveRange(
constraint.AllowedClassrooms);
constraint.AllowedClassrooms = allowedRooms.Select(room =>
new TeachingTaskAllowedClassroom
{
ClassroomId = room.Id
}).ToList();
}
if (request.UpdatePeriodRange)
{
constraint.EarliestPeriod = request.EarliestPeriod;
@@ -374,6 +433,10 @@ public sealed record TeachingTaskScheduleConstraintBatchRequest(
TeachingTaskSchedulingMode? SchedulingMode,
bool? RequiresClassroom,
IReadOnlyList<int>? AllowedDayOfWeeks,
bool UpdateClassroomScope,
Guid? RequiredCampusId,
Guid? RequiredBuildingId,
IReadOnlyList<Guid>? AllowedClassroomIds,
bool UpdatePeriodRange,
[Range(1, 30)] int? EarliestPeriod,
[Range(1, 30)] int? LatestPeriod);
@@ -353,12 +353,12 @@ public sealed class SchedulesController(
[HttpGet("plans/{planId:guid}/auto-schedule-job")]
public async Task<ActionResult<AutomaticScheduleJobResponse?>>
GetActiveAutomaticScheduleJob(
GetLatestAutomaticScheduleJob(
Guid planId,
CancellationToken cancellationToken)
CancellationToken cancellationToken)
{
var job = await db.AutomaticScheduleJobs.AsNoTracking()
.Where(x => x.ActiveSchedulePlanId == planId)
.Where(x => x.SchedulePlanId == planId)
.OrderByDescending(x => x.CreatedAt)
.FirstOrDefaultAsync(cancellationToken);
return Ok(job is null ? null : ToResponse(job));