新增按学期配置的“自动排课优化”页签,可调教师/行政班同日集中、行政班空档与连堂、首末节、教室容量冗余等权重。

自动排课在所有硬约束满足后,对候选排法应用这些软约束评分;不会改动既有课程或已发布课表。
This commit is contained in:
biss committed 2026-08-17 10:51:10 +08:00
1 parent 8074d59e82
commit 0513e251df
10 files changed
+7588 -2

No files matched your search

@@ -14,6 +14,46 @@ namespace Jiaowu.Api.Controllers;
[Route("api/schedules")]
public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache) : ControllerBase
{
[HttpGet("optimization-settings")]
public async Task<ActionResult> GetOptimizationSettings(
Guid academicTermId,
CancellationToken cancellationToken)
{
var settings = await db.ScheduleOptimizationSettings.AsNoTracking()
.FirstOrDefaultAsync(x => x.AcademicTermId == academicTermId, cancellationToken)
?? ScheduleOptimizationSettings.CreateDefault(academicTermId);
return Ok(ToOptimizationSettingsResponse(settings));
}
[HttpPut("optimization-settings/{academicTermId:guid}")]
public async Task<ActionResult> SaveOptimizationSettings(
Guid academicTermId,
ScheduleOptimizationSettingsRequest request,
CancellationToken cancellationToken)
{
if (!await db.AcademicTerms.AnyAsync(
x => x.Id == academicTermId && x.IsEnabled,
cancellationToken))
return ValidationProblem("学期不存在或已停用。");
var settings = await db.ScheduleOptimizationSettings
.FirstOrDefaultAsync(x => x.AcademicTermId == academicTermId, cancellationToken);
if (settings is null)
{
settings = ScheduleOptimizationSettings.CreateDefault(academicTermId);
db.ScheduleOptimizationSettings.Add(settings);
}
settings.TeacherSameDayWeight = request.TeacherSameDayWeight;
settings.ClassSameDayWeight = request.ClassSameDayWeight;
settings.ClassGapWeight = request.ClassGapWeight;
settings.ConsecutiveClassWeight = request.ConsecutiveClassWeight;
settings.EarlyPeriodPenalty = request.EarlyPeriodPenalty;
settings.LatePeriodPenalty = request.LatePeriodPenalty;
settings.RoomCapacityWasteWeight = request.RoomCapacityWasteWeight;
await db.SaveChangesAsync(cancellationToken);
return Ok(ToOptimizationSettingsResponse(settings));
}
[HttpGet("time-slots")]
public async Task<ActionResult> GetTimeSlots(
Guid academicTermId,
@@ -32,6 +72,17 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
})
.ToListAsync(cancellationToken));
private static object ToOptimizationSettingsResponse(ScheduleOptimizationSettings settings) => new
{
settings.TeacherSameDayWeight,
settings.ClassSameDayWeight,
settings.ClassGapWeight,
settings.ConsecutiveClassWeight,
settings.EarlyPeriodPenalty,
settings.LatePeriodPenalty,
settings.RoomCapacityWasteWeight
};
[HttpPut("time-slots/{academicTermId:guid}")]
public async Task<ActionResult> ReplaceTimeSlots(
Guid academicTermId,
@@ -544,6 +595,15 @@ public sealed record ScheduleTimeSlotRequest(
TimeOnly EndsAt,
bool IsEnabled);
public sealed record ScheduleOptimizationSettingsRequest(
[Range(0, 10_000)] int TeacherSameDayWeight,
[Range(0, 10_000)] int ClassSameDayWeight,
[Range(0, 10_000)] int ClassGapWeight,
[Range(0, 10_000)] int ConsecutiveClassWeight,
[Range(0, 10_000)] int EarlyPeriodPenalty,
[Range(0, 10_000)] int LatePeriodPenalty,
[Range(0, 10_000)] int RoomCapacityWasteWeight);
public sealed record TeachingTaskScheduleConstraintRequest(
TeachingTaskSchedulingMode SchedulingMode,
bool RequiresClassroom,
@@ -43,6 +43,26 @@ public sealed class ScheduleTimeSlot : EntityBase
public bool IsEnabled { get; set; } = true;
}
/// <summary>
/// 学期级自动排课的软约束权重。所有值为 0 时表示关闭对应偏好;
/// 硬约束(冲突、容量和课程可用范围)始终优先于这些设置。
/// </summary>
public sealed class ScheduleOptimizationSettings : EntityBase
{
public Guid AcademicTermId { get; set; }
public AcademicTerm? AcademicTerm { get; set; }
public int TeacherSameDayWeight { get; set; } = 120;
public int ClassSameDayWeight { get; set; } = 100;
public int ClassGapWeight { get; set; } = 240;
public int ConsecutiveClassWeight { get; set; } = 60;
public int EarlyPeriodPenalty { get; set; } = 30;
public int LatePeriodPenalty { get; set; } = 30;
public int RoomCapacityWasteWeight { get; set; } = 1;
public static ScheduleOptimizationSettings CreateDefault(Guid academicTermId) =>
new() { AcademicTermId = academicTermId };
}
public sealed class TeachingTaskScheduleConstraint : EntityBase
{
public Guid TeachingTaskId { get; set; }
@@ -37,6 +37,8 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
public DbSet<ScheduleEntry> ScheduleEntries => Set<ScheduleEntry>();
public DbSet<PublishedScheduleOccurrence> PublishedScheduleOccurrences => Set<PublishedScheduleOccurrence>();
public DbSet<ScheduleTimeSlot> ScheduleTimeSlots => Set<ScheduleTimeSlot>();
public DbSet<ScheduleOptimizationSettings> ScheduleOptimizationSettings =>
Set<ScheduleOptimizationSettings>();
public DbSet<TeachingTaskScheduleConstraint> TeachingTaskScheduleConstraints =>
Set<TeachingTaskScheduleConstraint>();
public DbSet<TeachingTaskAllowedClassroom> TeachingTaskAllowedClassrooms =>
@@ -518,6 +520,15 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
.OnDelete(DeleteBehavior.Cascade);
});
builder.Entity<ScheduleOptimizationSettings>(entity =>
{
entity.HasIndex(x => x.AcademicTermId).IsUnique();
entity.HasOne(x => x.AcademicTerm)
.WithMany()
.HasForeignKey(x => x.AcademicTermId)
.OnDelete(DeleteBehavior.Cascade);
});
builder.Entity<TeachingTaskScheduleConstraint>(entity =>
{
entity.Property(x => x.AllowedDayOfWeeks).HasMaxLength(20);
Loaded 3 of 10 files, more files were not shown because too many files have changed in this diff. Show more