实验课现在直接进入普通排课流程,不再要求先到实验排课模块逐个安排。
普通“添加排课”新增“理论课 / 实验课”类型。 自动排课会分别补足理论学时和实验学时。 实验课只能安排到实验室、实训室、机房、语音室等场地。 发布课表时分别校验理论、实验学时;任一未排足都不能发布。 普通课表及 Excel 导出会标注“实验课”。 历史排课保持不变,迁移后默认识别为理论课;后续新建或修订版本时再补充实验课。
This commit is contained in:
1 parent
9d525826ce
commit
f0f59419d5
22 files changed
+6446
-224
No files matched your search
@@ -3,7 +3,6 @@ using Jiaowu.Api.Domain.Academic;
|
|||||||
using Jiaowu.Api.Domain.Identity;
|
using Jiaowu.Api.Domain.Identity;
|
||||||
using Jiaowu.Api.Infrastructure.Caching;
|
using Jiaowu.Api.Infrastructure.Caching;
|
||||||
using Jiaowu.Api.Infrastructure.Persistence;
|
using Jiaowu.Api.Infrastructure.Persistence;
|
||||||
using Jiaowu.Api.Infrastructure.Teaching;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -110,15 +109,6 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
|
|||||||
return Ok(tasks.Select(task =>
|
return Ok(tasks.Select(task =>
|
||||||
{
|
{
|
||||||
constraints.TryGetValue(task.Id, out var constraint);
|
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
|
return new
|
||||||
{
|
{
|
||||||
task.Id,
|
task.Id,
|
||||||
@@ -132,7 +122,7 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
|
|||||||
task.Capacity,
|
task.Capacity,
|
||||||
task.StartWeek,
|
task.StartWeek,
|
||||||
task.EndWeek,
|
task.EndWeek,
|
||||||
WeeklyHours = weeklyHours,
|
task.WeeklyHours,
|
||||||
task.CourseTotalHours,
|
task.CourseTotalHours,
|
||||||
task.CoursePracticeHours,
|
task.CoursePracticeHours,
|
||||||
task.SchedulingMode,
|
task.SchedulingMode,
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ public sealed class SchedulesController(
|
|||||||
{
|
{
|
||||||
entry.Id,
|
entry.Id,
|
||||||
entry.TeachingTaskId,
|
entry.TeachingTaskId,
|
||||||
|
entry.Kind,
|
||||||
TaskNumber = entry.TeachingTask!.TaskNumber,
|
TaskNumber = entry.TeachingTask!.TaskNumber,
|
||||||
TaskName = entry.TeachingTask.Name,
|
TaskName = entry.TeachingTask.Name,
|
||||||
CourseCode = entry.TeachingTask.Course!.Code,
|
CourseCode = entry.TeachingTask.Course!.Code,
|
||||||
@@ -167,6 +168,7 @@ public sealed class SchedulesController(
|
|||||||
Entries = source.Entries.Select(entry => new ScheduleEntry
|
Entries = source.Entries.Select(entry => new ScheduleEntry
|
||||||
{
|
{
|
||||||
TeachingTaskId = entry.TeachingTaskId,
|
TeachingTaskId = entry.TeachingTaskId,
|
||||||
|
Kind = entry.Kind,
|
||||||
ClassroomId = entry.ClassroomId,
|
ClassroomId = entry.ClassroomId,
|
||||||
DayOfWeek = entry.DayOfWeek,
|
DayOfWeek = entry.DayOfWeek,
|
||||||
StartPeriod = entry.StartPeriod,
|
StartPeriod = entry.StartPeriod,
|
||||||
@@ -419,6 +421,7 @@ public sealed class SchedulesController(
|
|||||||
var validation = await ValidateEntryAsync(plan, entryId, request, cancellationToken);
|
var validation = await ValidateEntryAsync(plan, entryId, request, cancellationToken);
|
||||||
if (validation is not null) return validation;
|
if (validation is not null) return validation;
|
||||||
entry.TeachingTaskId = request.TeachingTaskId;
|
entry.TeachingTaskId = request.TeachingTaskId;
|
||||||
|
entry.Kind = request.Kind;
|
||||||
entry.ClassroomId = request.ClassroomId;
|
entry.ClassroomId = request.ClassroomId;
|
||||||
entry.DayOfWeek = request.DayOfWeek;
|
entry.DayOfWeek = request.DayOfWeek;
|
||||||
entry.StartPeriod = request.StartPeriod;
|
entry.StartPeriod = request.StartPeriod;
|
||||||
@@ -508,34 +511,52 @@ public sealed class SchedulesController(
|
|||||||
return ValidationProblem("非排时课程不进入正常课表,无需设置星期、节次或教室。");
|
return ValidationProblem("非排时课程不进入正常课表,无需设置星期、节次或教室。");
|
||||||
if (request.StartWeek < task.StartWeek || request.EndWeek > task.EndWeek)
|
if (request.StartWeek < task.StartWeek || request.EndWeek > task.EndWeek)
|
||||||
return ValidationProblem("排课周次必须位于教学任务的授课周次内。");
|
return ValidationProblem("排课周次必须位于教学任务的授课周次内。");
|
||||||
if (!TeachingTaskHours.TryResolveRegularWeeklyHours(
|
var targetHours = TeachingTaskHours.TargetHours(
|
||||||
task.Course!,
|
task.Course!,
|
||||||
task.StartWeek,
|
request.Kind);
|
||||||
task.EndWeek,
|
if (targetHours == 0)
|
||||||
out var requiredWeeklyHours))
|
|
||||||
return ValidationProblem(
|
return ValidationProblem(
|
||||||
"该课程的普通排课学时不能按授课周次整除,请先调整教学任务周次。");
|
request.Kind == ScheduleEntryKind.Experiment
|
||||||
if (requiredWeeklyHours == 0)
|
? "该课程没有实践学时,不能安排实验课。"
|
||||||
return ValidationProblem(
|
: "该课程没有理论学时,不能安排理论课。");
|
||||||
"该课程全部为实践学时,无需进入普通课表,请在实验管理中安排。");
|
var existingEntries = await db.ScheduleEntries.AsNoTracking()
|
||||||
var existingHours = await db.ScheduleEntries.AsNoTracking()
|
|
||||||
.Where(x =>
|
.Where(x =>
|
||||||
x.SchedulePlanId == plan.Id &&
|
x.SchedulePlanId == plan.Id &&
|
||||||
x.TeachingTaskId == request.TeachingTaskId &&
|
x.TeachingTaskId == request.TeachingTaskId &&
|
||||||
|
x.Kind == request.Kind &&
|
||||||
x.Id != entryId)
|
x.Id != entryId)
|
||||||
.SumAsync(x => x.PeriodCount, cancellationToken);
|
.Select(x => new
|
||||||
if (existingHours + request.PeriodCount > requiredWeeklyHours)
|
{
|
||||||
|
x.StartWeek,
|
||||||
|
x.EndWeek,
|
||||||
|
x.WeekPattern,
|
||||||
|
x.PeriodCount
|
||||||
|
})
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
var existingHours = existingEntries.Sum(x =>
|
||||||
|
TeachingTaskHours.ScheduledHours(
|
||||||
|
x.StartWeek,
|
||||||
|
x.EndWeek,
|
||||||
|
x.WeekPattern,
|
||||||
|
x.PeriodCount));
|
||||||
|
var proposedHours = TeachingTaskHours.ScheduledHours(
|
||||||
|
request.StartWeek,
|
||||||
|
request.EndWeek,
|
||||||
|
request.WeekPattern,
|
||||||
|
request.PeriodCount);
|
||||||
|
if (existingHours + proposedHours > targetHours)
|
||||||
return ValidationProblem(
|
return ValidationProblem(
|
||||||
$"该教学任务普通课表每周只需 {requiredWeeklyHours} 学时;" +
|
$"该教学任务{(request.Kind == ScheduleEntryKind.Experiment ? "实验" : "理论")}课" +
|
||||||
$"当前操作后将达到 {existingHours + request.PeriodCount} 学时," +
|
$"共需 {targetHours} 学时;当前操作后将达到 " +
|
||||||
"实践学时请在实验管理中安排。");
|
$"{existingHours + proposedHours} 学时。");
|
||||||
|
|
||||||
var constraint = await db.TeachingTaskScheduleConstraints.AsNoTracking()
|
var constraint = await db.TeachingTaskScheduleConstraints.AsNoTracking()
|
||||||
.Include(x => x.AllowedClassrooms)
|
.Include(x => x.AllowedClassrooms)
|
||||||
.FirstOrDefaultAsync(
|
.FirstOrDefaultAsync(
|
||||||
x => x.TeachingTaskId == request.TeachingTaskId,
|
x => x.TeachingTaskId == request.TeachingTaskId,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
var requiresClassroom = constraint?.RequiresClassroom ?? true;
|
var requiresClassroom = request.Kind == ScheduleEntryKind.Experiment ||
|
||||||
|
constraint?.RequiresClassroom != false;
|
||||||
if (requiresClassroom && !request.ClassroomId.HasValue)
|
if (requiresClassroom && !request.ClassroomId.HasValue)
|
||||||
return ValidationProblem("该课程需要占用教室,请选择教室。");
|
return ValidationProblem("该课程需要占用教室,请选择教室。");
|
||||||
if (!requiresClassroom && request.ClassroomId.HasValue)
|
if (!requiresClassroom && request.ClassroomId.HasValue)
|
||||||
@@ -559,6 +580,10 @@ public sealed class SchedulesController(
|
|||||||
x => x.Id == request.ClassroomId && x.IsEnabled,
|
x => x.Id == request.ClassroomId && x.IsEnabled,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
if (classroom is null) return ValidationProblem("所选教室不存在或已停用。");
|
if (classroom is null) return ValidationProblem("所选教室不存在或已停用。");
|
||||||
|
if (request.Kind == ScheduleEntryKind.Experiment &&
|
||||||
|
!IsExperimentRoom(classroom.RoomType))
|
||||||
|
return ValidationProblem(
|
||||||
|
$"实验课必须安排在实验室、实训室或机房;“{classroom.Name}”的场地类型为“{classroom.RoomType}”。");
|
||||||
if (constraint?.RequiredCampusId is Guid campusId &&
|
if (constraint?.RequiredCampusId is Guid campusId &&
|
||||||
classroom.Building!.CampusId != campusId)
|
classroom.Building!.CampusId != campusId)
|
||||||
return ValidationProblem("所选教室不在该课程指定的校区。");
|
return ValidationProblem("所选教室不在该课程指定的校区。");
|
||||||
@@ -612,6 +637,7 @@ public sealed class SchedulesController(
|
|||||||
{
|
{
|
||||||
SchedulePlanId = planId,
|
SchedulePlanId = planId,
|
||||||
TeachingTaskId = request.TeachingTaskId,
|
TeachingTaskId = request.TeachingTaskId,
|
||||||
|
Kind = request.Kind,
|
||||||
ClassroomId = request.ClassroomId,
|
ClassroomId = request.ClassroomId,
|
||||||
DayOfWeek = request.DayOfWeek,
|
DayOfWeek = request.DayOfWeek,
|
||||||
StartPeriod = request.StartPeriod,
|
StartPeriod = request.StartPeriod,
|
||||||
@@ -629,6 +655,12 @@ public sealed class SchedulesController(
|
|||||||
.Select(int.Parse)
|
.Select(int.Parse)
|
||||||
.ToHashSet();
|
.ToHashSet();
|
||||||
|
|
||||||
|
private static bool IsExperimentRoom(string roomType) =>
|
||||||
|
roomType.Contains("实验", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
roomType.Contains("实训", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
roomType.Contains("机房", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
roomType.Contains("语音", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
private async Task<ActionResult> SaveAsync(
|
private async Task<ActionResult> SaveAsync(
|
||||||
Guid id,
|
Guid id,
|
||||||
bool created,
|
bool created,
|
||||||
@@ -726,7 +758,8 @@ public sealed record ScheduleEntryRequest(
|
|||||||
[Range(1, 30)] int StartWeek,
|
[Range(1, 30)] int StartWeek,
|
||||||
[Range(1, 30)] int EndWeek,
|
[Range(1, 30)] int EndWeek,
|
||||||
WeekPattern WeekPattern,
|
WeekPattern WeekPattern,
|
||||||
[MaxLength(500)] string? Notes);
|
[MaxLength(500)] string? Notes,
|
||||||
|
ScheduleEntryKind Kind = ScheduleEntryKind.Lecture);
|
||||||
|
|
||||||
public sealed record AutomaticScheduleJobResponse(
|
public sealed record AutomaticScheduleJobResponse(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ public sealed class ScheduleEntry : EntityBase
|
|||||||
public SchedulePlan? SchedulePlan { get; set; }
|
public SchedulePlan? SchedulePlan { get; set; }
|
||||||
public Guid TeachingTaskId { get; set; }
|
public Guid TeachingTaskId { get; set; }
|
||||||
public TeachingTask? TeachingTask { get; set; }
|
public TeachingTask? TeachingTask { get; set; }
|
||||||
|
public ScheduleEntryKind Kind { get; set; } = ScheduleEntryKind.Lecture;
|
||||||
public Guid? ClassroomId { get; set; }
|
public Guid? ClassroomId { get; set; }
|
||||||
public Classroom? Classroom { get; set; }
|
public Classroom? Classroom { get; set; }
|
||||||
public int DayOfWeek { get; set; }
|
public int DayOfWeek { get; set; }
|
||||||
@@ -129,3 +130,9 @@ public enum WeekPattern
|
|||||||
Odd = 2,
|
Odd = 2,
|
||||||
Even = 3
|
Even = 3
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum ScheduleEntryKind
|
||||||
|
{
|
||||||
|
Lecture = 1,
|
||||||
|
Experiment = 2
|
||||||
|
}
|
||||||
Loaded 3 of 22 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user