已改成“普通排课与实验学时分离”:

普通课表学时 = 总学时 − 实践学时。
自动排课、手工排课、课表发布都会阻止把实践学时重复排入。
全部为实践学时的课程应设为“非排时课程”,再由实验管理安排。
教学任务和排课页面会显示“常规 / 实践学时”。
已有教学任务即使仍保存旧周学时,排课时也会按拆分后的课程学时计算。
无需数据库迁移。
This commit is contained in:
2026-07-29 09:57:49 +08:00 Unverified
parent 356c410788
commit 692b521453
14 changed files with 525 additions and 30 deletions
@@ -1,5 +1,6 @@
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Infrastructure.Persistence;
using Jiaowu.Api.Infrastructure.Teaching;
using Microsoft.EntityFrameworkCore;
namespace Jiaowu.Api.Infrastructure.Scheduling;
@@ -34,6 +35,7 @@ public sealed class AutomaticScheduleGenerator(AppDbContext db)
.Include(x => x.Classes)
.ThenInclude(x => x.AdministrativeClass)
.ThenInclude(x => x!.Students)
.Include(x => x.Course)
.OrderByDescending(x => x.Classes.Count + x.Teachers.Count)
.ThenByDescending(x => x.Capacity)
.ThenBy(x => x.TaskNumber)
@@ -72,10 +74,43 @@ public sealed class AutomaticScheduleGenerator(AppDbContext db)
{
cancellationToken.ThrowIfCancellationRequested();
constraints.TryGetValue(task.Id, out var constraint);
if (!TeachingTaskHours.TryResolveRegularWeeklyHours(
task.Course!,
task.StartWeek,
task.EndWeek,
out var requiredWeeklyHours))
{
messages.Add(
$"{task.TaskNumber} · {task.Name} 的普通排课学时不能按授课周次整除,请调整教学任务周次。");
processedTasks++;
if (reportProgress is not null)
{
await reportProgress(
new(tasks.Count, processedTasks, created, completedTasks),
cancellationToken);
}
continue;
}
var scheduledHours = entries
.Where(x => x.TeachingTaskId == task.Id)
.Sum(x => x.PeriodCount);
var remainingHours = Math.Max(0, task.WeeklyHours - scheduledHours);
if (scheduledHours > requiredWeeklyHours)
{
messages.Add(
$"{task.TaskNumber} · {task.Name} 已安排每周 {scheduledHours} 学时," +
$"普通课表只需 {requiredWeeklyHours} 学时;请删除已包含的实践学时。");
processedTasks++;
if (reportProgress is not null)
{
await reportProgress(
new(tasks.Count, processedTasks, created, completedTasks),
cancellationToken);
}
continue;
}
var remainingHours = requiredWeeklyHours - scheduledHours;
if (remainingHours == 0)
{
completedTasks++;
@@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis;
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Infrastructure.Caching;
using Jiaowu.Api.Infrastructure.Persistence;
using Jiaowu.Api.Infrastructure.Teaching;
using Microsoft.EntityFrameworkCore;
namespace Jiaowu.Api.Infrastructure.Scheduling;
@@ -179,19 +180,68 @@ public sealed class SchedulePlanPublisher(AppDbContext db)
x.AcademicTermId == plan.AcademicTermId &&
x.Status == TeachingTaskStatus.Published &&
x.SchedulingMode == TeachingTaskSchedulingMode.Standard)
.Select(x => new { x.Id, x.TaskNumber, x.Name, x.WeeklyHours })
.Select(x => new
{
x.Id,
x.TaskNumber,
x.Name,
x.StartWeek,
x.EndWeek,
CourseTotalHours = x.Course!.TotalHours,
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 = requiredTasks.FirstOrDefault(task =>
!scheduledHours.TryGetValue(task.Id, out var hours) ||
hours < task.WeeklyHours);
var incomplete = requiredWeeklyHours.FirstOrDefault(item =>
scheduledHours.GetValueOrDefault(item.Task.Id) < item.Hours);
if (incomplete is not null)
{
throw new SchedulePublishValidationException(
$"{incomplete.TaskNumber} · {incomplete.Name} 尚未达到每周 " +
$"{incomplete.WeeklyHours} 学时,不能发布。");
$"{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} 学时;" +
"请删除已包含的实践学时后再发布。");
}
await reportProgress(3, "检查教师、行政班和教室冲突", cancellationToken);
@@ -7,15 +7,75 @@ public static class TeachingTaskHours
public static int Calculate(int startWeek, int endWeek, int weeklyHours) =>
endWeek < startWeek ? 0 : (endWeek - startWeek + 1) * weeklyHours;
public static int RegularScheduleHours(Course course) =>
RegularScheduleHours(course.TotalHours, course.PracticeHours);
public static int RegularScheduleHours(int totalHours, int practiceHours) =>
Math.Max(0, totalHours - practiceHours);
public static int TargetHours(
Course course,
TeachingTaskSchedulingMode schedulingMode) =>
schedulingMode == TeachingTaskSchedulingMode.Flexible
? course.TotalHours
: RegularScheduleHours(course);
public static bool TryResolveRegularWeeklyHours(
Course course,
int startWeek,
int endWeek,
out int weeklyHours) =>
TryResolveRegularWeeklyHours(
course.TotalHours,
course.PracticeHours,
startWeek,
endWeek,
out weeklyHours);
public static bool TryResolveRegularWeeklyHours(
int totalHours,
int practiceHours,
int startWeek,
int endWeek,
out int weeklyHours)
{
weeklyHours = 0;
var weekCount = endWeek - startWeek + 1;
var regularHours = RegularScheduleHours(totalHours, practiceHours);
if (weekCount <= 0 || regularHours % weekCount != 0) return false;
weeklyHours = regularHours / weekCount;
return true;
}
public static string? Validate(
Course course,
int startWeek,
int endWeek,
int weeklyHours)
int weeklyHours,
TeachingTaskSchedulingMode schedulingMode =
TeachingTaskSchedulingMode.Standard)
{
var plannedHours = Calculate(startWeek, endWeek, weeklyHours);
return plannedHours == course.TotalHours
? null
: $"课程“{course.Name}”总学时为 {course.TotalHours};当前第 {startWeek}—{endWeek} 周、每周 {weeklyHours} 学时,共 {plannedHours} 学时。请调整授课周次或周学时。";
var targetHours = TargetHours(course, schedulingMode);
if (plannedHours == targetHours) return null;
if (schedulingMode == TeachingTaskSchedulingMode.Standard)
{
if (targetHours == 0)
{
return $"课程“{course.Name}”的 {course.TotalHours} 学时均为实践学时," +
"无需进入普通课表;请将授课方式设为“非排时课程”,并在实验管理中安排。";
}
return $"课程“{course.Name}”总学时为 {course.TotalHours},其中实践学时 " +
$"{course.PracticeHours},普通课表应安排 {targetHours} 学时;当前第 " +
$"{startWeek}—{endWeek} 周、每周 {weeklyHours} 学时,共 " +
$"{plannedHours} 学时。请调整授课周次或周学时。";
}
return $"课程“{course.Name}”总学时为 {course.TotalHours};当前第 " +
$"{startWeek}—{endWeek} 周、每周 {weeklyHours} 学时,共 " +
$"{plannedHours} 学时。请调整授课周次或周学时。";
}
}