实验排课再次修正

This commit is contained in:
2026-08-09 15:57:05 +08:00 Unverified
parent 1e60e46c7b
commit fb4e5ac4d5
11 changed files with 6933 additions and 25 deletions
@@ -513,6 +513,14 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
.WithMany()
.HasForeignKey(x => x.RequiredBuildingId)
.OnDelete(DeleteBehavior.Restrict);
entity.HasOne(x => x.ExperimentRequiredCampus)
.WithMany()
.HasForeignKey(x => x.ExperimentRequiredCampusId)
.OnDelete(DeleteBehavior.Restrict);
entity.HasOne(x => x.ExperimentRequiredBuilding)
.WithMany()
.HasForeignKey(x => x.ExperimentRequiredBuildingId)
.OnDelete(DeleteBehavior.Restrict);
});
builder.Entity<TeachingTaskAllowedClassroom>(entity =>
@@ -94,6 +94,8 @@ public sealed class DevelopmentSqliteMigrator(
"20260809_49_swagger_documentation_setting";
private const string ExperimentClassroomConstraintsMigration =
"20260809_50_experiment_classroom_constraints";
private const string SeparateExperimentClassroomScopeMigration =
"20260809_51_separate_experiment_classroom_scope";
public async Task MigrateAsync(CancellationToken cancellationToken = default)
{
@@ -689,6 +691,16 @@ public sealed class DevelopmentSqliteMigrator(
ExperimentClassroomConstraintsMigration,
experimentClassroomConstraintsExist ? [] : ExperimentClassroomConstraintStatements,
cancellationToken);
var experimentScopeColumns = (await db.Database.SqlQueryRaw<string>(
"SELECT name AS \"Value\" FROM pragma_table_info('TeachingTaskScheduleConstraints')")
.ToListAsync(cancellationToken))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
await ApplyMigrationAsync(
SeparateExperimentClassroomScopeMigration,
experimentScopeColumns.Contains("ExperimentRequiredCampusId")
? []
: SeparateExperimentClassroomScopeStatements,
cancellationToken);
}
private async Task ApplyMigrationAsync(
@@ -2971,4 +2983,24 @@ public sealed class DevelopmentSqliteMigrator(
ON "TeachingTaskAllowedExperimentClassrooms" ("ClassroomId");
"""
];
private static readonly string[] SeparateExperimentClassroomScopeStatements =
[
"""
ALTER TABLE "TeachingTaskScheduleConstraints"
ADD COLUMN "ExperimentRequiredCampusId" TEXT NULL;
""",
"""
ALTER TABLE "TeachingTaskScheduleConstraints"
ADD COLUMN "ExperimentRequiredBuildingId" TEXT NULL;
""",
"""
CREATE INDEX "IX_TeachingTaskScheduleConstraints_ExperimentRequiredCampusId"
ON "TeachingTaskScheduleConstraints" ("ExperimentRequiredCampusId");
""",
"""
CREATE INDEX "IX_TeachingTaskScheduleConstraints_ExperimentRequiredBuildingId"
ON "TeachingTaskScheduleConstraints" ("ExperimentRequiredBuildingId");
"""
];
}
@@ -0,0 +1,81 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
{
/// <inheritdoc />
public partial class SeparateExperimentClassroomScope : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Guid>(
name: "ExperimentRequiredBuildingId",
table: "TeachingTaskScheduleConstraints",
type: "char(36)",
nullable: true);
migrationBuilder.AddColumn<Guid>(
name: "ExperimentRequiredCampusId",
table: "TeachingTaskScheduleConstraints",
type: "char(36)",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_TeachingTaskScheduleConstraints_ExperimentRequiredBuildingId",
table: "TeachingTaskScheduleConstraints",
column: "ExperimentRequiredBuildingId");
migrationBuilder.CreateIndex(
name: "IX_TeachingTaskScheduleConstraints_ExperimentRequiredCampusId",
table: "TeachingTaskScheduleConstraints",
column: "ExperimentRequiredCampusId");
migrationBuilder.AddForeignKey(
name: "FK_TeachingTaskScheduleConstraints_Buildings_ExperimentRequired~",
table: "TeachingTaskScheduleConstraints",
column: "ExperimentRequiredBuildingId",
principalTable: "Buildings",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
migrationBuilder.AddForeignKey(
name: "FK_TeachingTaskScheduleConstraints_Campuses_ExperimentRequiredC~",
table: "TeachingTaskScheduleConstraints",
column: "ExperimentRequiredCampusId",
principalTable: "Campuses",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_TeachingTaskScheduleConstraints_Buildings_ExperimentRequired~",
table: "TeachingTaskScheduleConstraints");
migrationBuilder.DropForeignKey(
name: "FK_TeachingTaskScheduleConstraints_Campuses_ExperimentRequiredC~",
table: "TeachingTaskScheduleConstraints");
migrationBuilder.DropIndex(
name: "IX_TeachingTaskScheduleConstraints_ExperimentRequiredBuildingId",
table: "TeachingTaskScheduleConstraints");
migrationBuilder.DropIndex(
name: "IX_TeachingTaskScheduleConstraints_ExperimentRequiredCampusId",
table: "TeachingTaskScheduleConstraints");
migrationBuilder.DropColumn(
name: "ExperimentRequiredBuildingId",
table: "TeachingTaskScheduleConstraints");
migrationBuilder.DropColumn(
name: "ExperimentRequiredCampusId",
table: "TeachingTaskScheduleConstraints");
}
}
}
@@ -4272,6 +4272,12 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
b.Property<int?>("EarliestPeriod")
.HasColumnType("int");
b.Property<Guid?>("ExperimentRequiredBuildingId")
.HasColumnType("char(36)");
b.Property<Guid?>("ExperimentRequiredCampusId")
.HasColumnType("char(36)");
b.Property<int?>("LatestPeriod")
.HasColumnType("int");
@@ -4292,6 +4298,10 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
b.HasKey("Id");
b.HasIndex("ExperimentRequiredBuildingId");
b.HasIndex("ExperimentRequiredCampusId");
b.HasIndex("RequiredBuildingId");
b.HasIndex("RequiredCampusId");
@@ -6295,6 +6305,16 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskScheduleConstraint", b =>
{
b.HasOne("Jiaowu.Api.Domain.Academic.Building", "ExperimentRequiredBuilding")
.WithMany()
.HasForeignKey("ExperimentRequiredBuildingId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("Jiaowu.Api.Domain.Academic.Campus", "ExperimentRequiredCampus")
.WithMany()
.HasForeignKey("ExperimentRequiredCampusId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("Jiaowu.Api.Domain.Academic.Building", "RequiredBuilding")
.WithMany()
.HasForeignKey("RequiredBuildingId")
@@ -6311,6 +6331,10 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ExperimentRequiredBuilding");
b.Navigation("ExperimentRequiredCampus");
b.Navigation("RequiredBuilding");
b.Navigation("RequiredCampus");
@@ -254,8 +254,15 @@ public sealed class AutomaticScheduleGenerator(AppDbContext db)
x.TeachingTaskId == task.Id && x.DayOfWeek == day);
var dayLoad = entries.Count(x => x.DayOfWeek == day);
var roomWaste = room is null ? 0 : Math.Max(0, room.Capacity - task.Capacity);
var experimentGeneralClassroomPenalty =
kind == ScheduleEntryKind.Experiment &&
constraint?.AllowedExperimentVenueNatures == 0 &&
room?.TeachingVenueNature == TeachingVenueNature.GeneralClassroom
? 100_000
: 0;
var score = sameTaskDay * 1000 + dayLoad * 10 + start +
roomWaste / 10 + startWeek;
roomWaste / 10 + startWeek +
experimentGeneralClassroomPenalty;
candidates.Add((proposed, score));
}
}
@@ -290,11 +297,20 @@ public sealed class AutomaticScheduleGenerator(AppDbContext db)
student.Status == StudentStatus.Active) ?? 0));
return classrooms.Where(room =>
room.Capacity >= minimumCapacity &&
(constraint?.RequiredCampusId is not Guid requiredCampusId ||
(kind == ScheduleEntryKind.Experiment ||
constraint?.RequiredCampusId is not Guid requiredCampusId ||
room.Building!.CampusId == requiredCampusId) &&
(constraint?.RequiredBuildingId is not Guid requiredBuildingId ||
(kind == ScheduleEntryKind.Experiment ||
constraint?.RequiredBuildingId is not Guid requiredBuildingId ||
room.BuildingId == requiredBuildingId) &&
(allowedRoomIds.Count == 0 || allowedRoomIds.Contains(room.Id)) &&
(kind != ScheduleEntryKind.Experiment ||
constraint?.ExperimentRequiredCampusId is not Guid experimentCampusId ||
room.Building!.CampusId == experimentCampusId) &&
(kind != ScheduleEntryKind.Experiment ||
constraint?.ExperimentRequiredBuildingId is not Guid experimentBuildingId ||
room.BuildingId == experimentBuildingId) &&
(kind == ScheduleEntryKind.Experiment ||
allowedRoomIds.Count == 0 || allowedRoomIds.Contains(room.Id)) &&
(kind != ScheduleEntryKind.Experiment || constraint is null ||
constraint.AllowedExperimentVenueNatures == 0 ||
(room.TeachingVenueNature & constraint.AllowedExperimentVenueNatures) != 0) &&
@@ -271,16 +271,26 @@ public sealed class SchedulePlanPublisher(AppDbContext db)
{
if (classroom is null || !classroom.IsEnabled)
Fail(entry, "所选教室不存在或已停用");
if (constraint?.RequiredCampusId is Guid campusId &&
if (entry.Kind != ScheduleEntryKind.Experiment &&
constraint?.RequiredCampusId is Guid campusId &&
classroom.Building!.CampusId != campusId)
Fail(entry, "所选教室不在指定校区");
if (constraint?.RequiredBuildingId is Guid buildingId &&
if (entry.Kind != ScheduleEntryKind.Experiment &&
constraint?.RequiredBuildingId is Guid buildingId &&
classroom.BuildingId != buildingId)
Fail(entry, "所选教室不在指定教学楼");
if (entry.Kind == ScheduleEntryKind.Experiment &&
constraint?.ExperimentRequiredCampusId is Guid experimentCampusId &&
classroom.Building!.CampusId != experimentCampusId)
Fail(entry, "所选场地不在实验课指定校区");
if (entry.Kind == ScheduleEntryKind.Experiment &&
constraint?.ExperimentRequiredBuildingId is Guid experimentBuildingId &&
classroom.BuildingId != experimentBuildingId)
Fail(entry, "所选场地不在实验课指定教学楼");
var allowedClassroomIds = constraint?.AllowedClassrooms
.Select(x => x.ClassroomId)
.ToHashSet() ?? [];
if (allowedClassroomIds.Count > 0 &&
if (entry.Kind != ScheduleEntryKind.Experiment && allowedClassroomIds.Count > 0 &&
!allowedClassroomIds.Contains(classroom.Id))
Fail(entry, "所选教室不在指定教室范围内");
var allowedExperimentClassroomIds = constraint?.AllowedExperimentClassrooms