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

普通课表学时 = 总学时 − 实践学时。
自动排课、手工排课、课表发布都会阻止把实践学时重复排入。
全部为实践学时的课程应设为“非排时课程”,再由实验管理安排。
教学任务和排课页面会显示“常规 / 实践学时”。
已有教学任务即使仍保存旧周学时,排课时也会按拆分后的课程学时计算。
无需数据库迁移。
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
@@ -145,6 +145,8 @@ public sealed class CoursesController(
CategoryName = x.CourseCategory != null ? x.CourseCategory.Name : null,
x.Credits,
x.TotalHours,
x.LectureHours,
x.PracticeHours,
x.Nature,
x.AssessmentMethod
})
@@ -3,6 +3,7 @@ using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Infrastructure.Caching;
using Jiaowu.Api.Infrastructure.Persistence;
using Jiaowu.Api.Infrastructure.Teaching;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
@@ -96,6 +97,8 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
x.StartWeek,
x.EndWeek,
x.WeeklyHours,
CourseTotalHours = x.Course.TotalHours,
CoursePracticeHours = x.Course.PracticeHours,
x.SchedulingMode
})
.ToListAsync(cancellationToken);
@@ -107,6 +110,15 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
return Ok(tasks.Select(task =>
{
constraints.TryGetValue(task.Id, out var constraint);
var weeklyHours = task.WeeklyHours;
if (task.SchedulingMode == TeachingTaskSchedulingMode.Standard &&
TeachingTaskHours.TryResolveRegularWeeklyHours(
task.CourseTotalHours,
task.CoursePracticeHours,
task.StartWeek,
task.EndWeek,
out var regularWeeklyHours))
weeklyHours = regularWeeklyHours;
return new
{
task.Id,
@@ -120,7 +132,9 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
task.Capacity,
task.StartWeek,
task.EndWeek,
task.WeeklyHours,
WeeklyHours = weeklyHours,
task.CourseTotalHours,
task.CoursePracticeHours,
task.SchedulingMode,
HasCustomConstraint = constraint is not null,
RequiresClassroom = task.SchedulingMode == TeachingTaskSchedulingMode.Flexible
@@ -6,6 +6,7 @@ using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Domain.System;
using Jiaowu.Api.Infrastructure.Persistence;
using Jiaowu.Api.Infrastructure.Scheduling;
using Jiaowu.Api.Infrastructure.Teaching;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
@@ -497,6 +498,7 @@ public sealed class SchedulesController(
.Include(x => x.Classes)
.ThenInclude(x => x.AdministrativeClass)
.ThenInclude(x => x!.Students)
.Include(x => x.Course)
.FirstOrDefaultAsync(x => x.Id == request.TeachingTaskId, cancellationToken);
if (task is null ||
task.Status != TeachingTaskStatus.Published ||
@@ -506,6 +508,27 @@ public sealed class SchedulesController(
return ValidationProblem("非排时课程不进入正常课表,无需设置星期、节次或教室。");
if (request.StartWeek < task.StartWeek || request.EndWeek > task.EndWeek)
return ValidationProblem("排课周次必须位于教学任务的授课周次内。");
if (!TeachingTaskHours.TryResolveRegularWeeklyHours(
task.Course!,
task.StartWeek,
task.EndWeek,
out var requiredWeeklyHours))
return ValidationProblem(
"该课程的普通排课学时不能按授课周次整除,请先调整教学任务周次。");
if (requiredWeeklyHours == 0)
return ValidationProblem(
"该课程全部为实践学时,无需进入普通课表,请在实验管理中安排。");
var existingHours = await db.ScheduleEntries.AsNoTracking()
.Where(x =>
x.SchedulePlanId == plan.Id &&
x.TeachingTaskId == request.TeachingTaskId &&
x.Id != entryId)
.SumAsync(x => x.PeriodCount, cancellationToken);
if (existingHours + request.PeriodCount > requiredWeeklyHours)
return ValidationProblem(
$"该教学任务普通课表每周只需 {requiredWeeklyHours} 学时;" +
$"当前操作后将达到 {existingHours + request.PeriodCount} 学时," +
"实践学时请在实验管理中安排。");
var constraint = await db.TeachingTaskScheduleConstraints.AsNoTracking()
.Include(x => x.AllowedClassrooms)
@@ -82,6 +82,9 @@ public sealed class TeachingTasksController(
x.WeeklyHours,
x.SchedulingMode,
CourseTotalHours = x.Course.TotalHours,
CoursePracticeHours = x.Course.PracticeHours,
CourseRegularScheduleHours =
x.Course.TotalHours - x.Course.PracticeHours,
x.GenerationBatchCode,
x.Status,
TeacherNames = x.Teachers
@@ -127,6 +130,10 @@ public sealed class TeachingTasksController(
x.EndWeek,
x.WeeklyHours,
x.SchedulingMode,
CourseTotalHours = x.Course!.TotalHours,
CoursePracticeHours = x.Course.PracticeHours,
CourseRegularScheduleHours =
x.Course.TotalHours - x.Course.PracticeHours,
TeacherNames = x.Teachers
.OrderByDescending(item => item.IsPrimary)
.Select(item => item.Teacher!.Name),
@@ -163,6 +170,9 @@ public sealed class TeachingTasksController(
x.WeeklyHours,
x.SchedulingMode,
CourseTotalHours = x.Course.TotalHours,
CoursePracticeHours = x.Course.PracticeHours,
CourseRegularScheduleHours =
x.Course.TotalHours - x.Course.PracticeHours,
x.GenerationBatchCode,
x.Status,
x.Notes,
@@ -423,7 +433,8 @@ public sealed class TeachingTasksController(
course,
request.StartWeek,
request.EndWeek,
request.WeeklyHours);
request.WeeklyHours,
TeachingTaskSchedulingMode.Standard);
if (hoursProblem is not null) return ValidationProblem(hoursProblem);
if (course.Nature is not (CourseNature.GeneralRequired or CourseNature.GeneralElective))
return ValidationProblem("批量合班生成仅用于公共必修课或公共选修课。");
@@ -598,7 +609,8 @@ public sealed class TeachingTasksController(
course,
request.StartWeek,
request.EndWeek,
request.WeeklyHours);
request.WeeklyHours,
request.SchedulingMode);
if (hoursProblem is not null) return ValidationProblem(hoursProblem);
var collegeId = ScopedCollegeId();
if (!await db.AcademicTerms.AnyAsync(