实验课现在直接进入普通排课流程,不再要求先到实验排课模块逐个安排。
普通“添加排课”新增“理论课 / 实验课”类型。 自动排课会分别补足理论学时和实验学时。 实验课只能安排到实验室、实训室、机房、语音室等场地。 发布课表时分别校验理论、实验学时;任一未排足都不能发布。 普通课表及 Excel 导出会标注“实验课”。 历史排课保持不变,迁移后默认识别为理论课;后续新建或修订版本时再补充实验课。
This commit is contained in:
@@ -191,57 +191,30 @@ public sealed class SchedulePlanPublisher(AppDbContext db)
|
||||
CoursePracticeHours = x.Course.PracticeHours
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
var invalidHours = requiredTasks.FirstOrDefault(task =>
|
||||
!TeachingTaskHours.TryResolveRegularWeeklyHours(
|
||||
task.CourseTotalHours,
|
||||
task.CoursePracticeHours,
|
||||
task.StartWeek,
|
||||
task.EndWeek,
|
||||
out _));
|
||||
if (invalidHours is not null)
|
||||
{
|
||||
throw new SchedulePublishValidationException(
|
||||
$"{invalidHours.TaskNumber} · {invalidHours.Name} 的普通排课学时" +
|
||||
"不能按授课周次整除,请先调整教学任务周次。");
|
||||
}
|
||||
|
||||
var requiredWeeklyHours = requiredTasks
|
||||
.Select(task =>
|
||||
{
|
||||
TeachingTaskHours.TryResolveRegularWeeklyHours(
|
||||
task.CourseTotalHours,
|
||||
task.CoursePracticeHours,
|
||||
task.StartWeek,
|
||||
task.EndWeek,
|
||||
out var hours);
|
||||
return new
|
||||
{
|
||||
Task = task,
|
||||
Hours = hours
|
||||
};
|
||||
})
|
||||
.ToList();
|
||||
var scheduledHours = plan.Entries
|
||||
.GroupBy(x => x.TeachingTaskId)
|
||||
.ToDictionary(group => group.Key, group => group.Sum(x => x.PeriodCount));
|
||||
var incomplete = requiredWeeklyHours.FirstOrDefault(item =>
|
||||
scheduledHours.GetValueOrDefault(item.Task.Id) < item.Hours);
|
||||
if (incomplete is not null)
|
||||
.GroupBy(x => new { x.TeachingTaskId, x.Kind })
|
||||
.ToDictionary(
|
||||
group => (group.Key.TeachingTaskId, group.Key.Kind),
|
||||
group => group.Sum(TeachingTaskHours.ScheduledHours));
|
||||
foreach (var task in requiredTasks)
|
||||
{
|
||||
throw new SchedulePublishValidationException(
|
||||
$"{incomplete.Task.TaskNumber} · {incomplete.Task.Name} 尚未达到每周 " +
|
||||
$"{incomplete.Hours} 个普通排课学时,不能发布。");
|
||||
}
|
||||
|
||||
var excessive = requiredWeeklyHours.FirstOrDefault(item =>
|
||||
scheduledHours.GetValueOrDefault(item.Task.Id) > item.Hours);
|
||||
if (excessive is not null)
|
||||
{
|
||||
var actualHours = scheduledHours.GetValueOrDefault(excessive.Task.Id);
|
||||
throw new SchedulePublishValidationException(
|
||||
$"{excessive.Task.TaskNumber} · {excessive.Task.Name} 已安排每周 " +
|
||||
$"{actualHours} 学时,普通课表应为 {excessive.Hours} 学时;" +
|
||||
"请删除已包含的实践学时后再发布。");
|
||||
var targets = new[]
|
||||
{
|
||||
(Kind: ScheduleEntryKind.Lecture,
|
||||
Hours: Math.Max(0, task.CourseTotalHours - task.CoursePracticeHours),
|
||||
Label: "理论课"),
|
||||
(Kind: ScheduleEntryKind.Experiment,
|
||||
Hours: Math.Max(0, task.CoursePracticeHours),
|
||||
Label: "实验课")
|
||||
};
|
||||
foreach (var target in targets)
|
||||
{
|
||||
var actual = scheduledHours.GetValueOrDefault((task.Id, target.Kind));
|
||||
if (actual == target.Hours) continue;
|
||||
throw new SchedulePublishValidationException(
|
||||
$"{task.TaskNumber} · {task.Name} 的{target.Label}应安排 " +
|
||||
$"{target.Hours} 学时,当前已安排 {actual} 学时,不能发布。");
|
||||
}
|
||||
}
|
||||
|
||||
await reportProgress(3, "检查教师、行政班和教室冲突", cancellationToken);
|
||||
@@ -275,7 +248,8 @@ public sealed class SchedulePlanPublisher(AppDbContext db)
|
||||
Fail(entry, "排课周次不在教学任务的授课周次内");
|
||||
|
||||
constraints.TryGetValue(entry.TeachingTaskId, out var constraint);
|
||||
var requiresClassroom = constraint?.RequiresClassroom ?? true;
|
||||
var requiresClassroom = entry.Kind == ScheduleEntryKind.Experiment ||
|
||||
constraint?.RequiresClassroom != false;
|
||||
if (requiresClassroom && entry.ClassroomId is null)
|
||||
Fail(entry, "该课程需要占用教室");
|
||||
if (!requiresClassroom && entry.ClassroomId is not null)
|
||||
@@ -296,6 +270,9 @@ public sealed class SchedulePlanPublisher(AppDbContext db)
|
||||
{
|
||||
if (classroom is null || !classroom.IsEnabled)
|
||||
Fail(entry, "所选教室不存在或已停用");
|
||||
if (entry.Kind == ScheduleEntryKind.Experiment &&
|
||||
!IsExperimentRoom(classroom.RoomType))
|
||||
Fail(entry, $"实验课不能安排在“{classroom.RoomType}”类型的场地");
|
||||
if (constraint?.RequiredCampusId is Guid campusId &&
|
||||
classroom.Building!.CampusId != campusId)
|
||||
Fail(entry, "所选教室不在指定校区");
|
||||
@@ -328,6 +305,12 @@ public sealed class SchedulePlanPublisher(AppDbContext db)
|
||||
.Select(int.Parse)
|
||||
.ToHashSet();
|
||||
|
||||
private static bool IsExperimentRoom(string roomType) =>
|
||||
roomType.Contains("实验", StringComparison.OrdinalIgnoreCase) ||
|
||||
roomType.Contains("实训", StringComparison.OrdinalIgnoreCase) ||
|
||||
roomType.Contains("机房", StringComparison.OrdinalIgnoreCase) ||
|
||||
roomType.Contains("语音", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
[DoesNotReturn]
|
||||
private static void Fail(ScheduleEntry entry, string message)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user