修复实验排课

This commit is contained in:
2026-08-09 15:21:58 +08:00 Unverified
parent bb2b43b0d9
commit 1e60e46c7b
12 changed files with 6968 additions and 42 deletions
@@ -38,6 +38,8 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
Set<TeachingTaskScheduleConstraint>();
public DbSet<TeachingTaskAllowedClassroom> TeachingTaskAllowedClassrooms =>
Set<TeachingTaskAllowedClassroom>();
public DbSet<TeachingTaskAllowedExperimentClassroom> TeachingTaskAllowedExperimentClassrooms =>
Set<TeachingTaskAllowedExperimentClassroom>();
public DbSet<AutomaticScheduleJob> AutomaticScheduleJobs =>
Set<AutomaticScheduleJob>();
public DbSet<SchedulePublishJob> SchedulePublishJobs =>
@@ -530,6 +532,19 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
.OnDelete(DeleteBehavior.Restrict);
});
builder.Entity<TeachingTaskAllowedExperimentClassroom>(entity =>
{
entity.HasKey(x => new { x.TeachingTaskScheduleConstraintId, x.ClassroomId });
entity.HasOne(x => x.TeachingTaskScheduleConstraint)
.WithMany(x => x.AllowedExperimentClassrooms)
.HasForeignKey(x => x.TeachingTaskScheduleConstraintId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(x => x.Classroom)
.WithMany()
.HasForeignKey(x => x.ClassroomId)
.OnDelete(DeleteBehavior.Restrict);
});
builder.Entity<AutomaticScheduleJob>(entity =>
{
entity.Property(x => x.ErrorMessage).HasMaxLength(2000);
@@ -92,6 +92,8 @@ public sealed class DevelopmentSqliteMigrator(
"20260809_48_teaching_task_grade_analytics";
private const string SwaggerDocumentationSettingMigration =
"20260809_49_swagger_documentation_setting";
private const string ExperimentClassroomConstraintsMigration =
"20260809_50_experiment_classroom_constraints";
public async Task MigrateAsync(CancellationToken cancellationToken = default)
{
@@ -679,6 +681,14 @@ public sealed class DevelopmentSqliteMigrator(
SwaggerDocumentationSettingMigration,
swaggerSettingsExist ? [] : SwaggerDocumentationSettingStatements,
cancellationToken);
var experimentClassroomConstraintsExist = await db.Database
.SqlQueryRaw<int>(
"SELECT COUNT(*) AS \"Value\" FROM sqlite_master WHERE type = 'table' AND name = 'TeachingTaskAllowedExperimentClassrooms'")
.AnyAsync(value => value > 0, cancellationToken);
await ApplyMigrationAsync(
ExperimentClassroomConstraintsMigration,
experimentClassroomConstraintsExist ? [] : ExperimentClassroomConstraintStatements,
cancellationToken);
}
private async Task ApplyMigrationAsync(
@@ -2940,4 +2950,25 @@ public sealed class DevelopmentSqliteMigrator(
ON "SystemFeatureSettings" ("Key");
"""
];
private static readonly string[] ExperimentClassroomConstraintStatements =
[
"""
CREATE TABLE "TeachingTaskAllowedExperimentClassrooms" (
"TeachingTaskScheduleConstraintId" TEXT NOT NULL,
"ClassroomId" TEXT NOT NULL,
CONSTRAINT "PK_TeachingTaskAllowedExperimentClassrooms"
PRIMARY KEY ("TeachingTaskScheduleConstraintId", "ClassroomId"),
CONSTRAINT "FK_TeachingTaskAllowedExperimentClassrooms_Constraints"
FOREIGN KEY ("TeachingTaskScheduleConstraintId")
REFERENCES "TeachingTaskScheduleConstraints" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_TeachingTaskAllowedExperimentClassrooms_Classrooms"
FOREIGN KEY ("ClassroomId") REFERENCES "Classrooms" ("Id") ON DELETE RESTRICT
);
""",
"""
CREATE INDEX "IX_TeachingTaskAllowedExperimentClassrooms_ClassroomId"
ON "TeachingTaskAllowedExperimentClassrooms" ("ClassroomId");
"""
];
}
@@ -0,0 +1,52 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
{
/// <inheritdoc />
public partial class AddExperimentClassroomConstraints : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "TeachingTaskAllowedExperimentClassrooms",
columns: table => new
{
TeachingTaskScheduleConstraintId = table.Column<Guid>(type: "char(36)", nullable: false),
ClassroomId = table.Column<Guid>(type: "char(36)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_TeachingTaskAllowedExperimentClassrooms", x => new { x.TeachingTaskScheduleConstraintId, x.ClassroomId });
table.ForeignKey(
name: "FK_TeachingTaskAllowedExperimentClassrooms_Classrooms_Classroom~",
column: x => x.ClassroomId,
principalTable: "Classrooms",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_TeachingTaskAllowedExperimentClassrooms_TeachingTaskSchedule~",
column: x => x.TeachingTaskScheduleConstraintId,
principalTable: "TeachingTaskScheduleConstraints",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_TeachingTaskAllowedExperimentClassrooms_ClassroomId",
table: "TeachingTaskAllowedExperimentClassrooms",
column: "ClassroomId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "TeachingTaskAllowedExperimentClassrooms");
}
}
}
@@ -4102,6 +4102,21 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
b.ToTable("TeachingTaskAllowedClassrooms");
});
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskAllowedExperimentClassroom", b =>
{
b.Property<Guid>("TeachingTaskScheduleConstraintId")
.HasColumnType("char(36)");
b.Property<Guid>("ClassroomId")
.HasColumnType("char(36)");
b.HasKey("TeachingTaskScheduleConstraintId", "ClassroomId");
b.HasIndex("ClassroomId");
b.ToTable("TeachingTaskAllowedExperimentClassrooms");
});
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskClass", b =>
{
b.Property<Guid>("TeachingTaskId")
@@ -6198,6 +6213,25 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
b.Navigation("TeachingTaskScheduleConstraint");
});
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskAllowedExperimentClassroom", b =>
{
b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom")
.WithMany()
.HasForeignKey("ClassroomId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTaskScheduleConstraint", "TeachingTaskScheduleConstraint")
.WithMany("AllowedExperimentClassrooms")
.HasForeignKey("TeachingTaskScheduleConstraintId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Classroom");
b.Navigation("TeachingTaskScheduleConstraint");
});
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskClass", b =>
{
b.HasOne("Jiaowu.Api.Domain.Academic.AdministrativeClass", "AdministrativeClass")
@@ -6585,6 +6619,8 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskScheduleConstraint", b =>
{
b.Navigation("AllowedClassrooms");
b.Navigation("AllowedExperimentClassrooms");
});
#pragma warning restore 612, 618
}
@@ -44,6 +44,7 @@ public sealed class AutomaticScheduleGenerator(AppDbContext db)
var constraints = await db.TeachingTaskScheduleConstraints.AsNoTracking()
.WhereIn(taskIds, x => x.TeachingTaskId)
.Include(x => x.AllowedClassrooms)
.Include(x => x.AllowedExperimentClassrooms)
.ToDictionaryAsync(x => x.TeachingTaskId, cancellationToken);
var classrooms = await db.Classrooms.AsNoTracking()
.Where(x => x.IsEnabled)
@@ -279,6 +280,9 @@ public sealed class AutomaticScheduleGenerator(AppDbContext db)
var allowedRoomIds = constraint?.AllowedClassrooms
.Select(x => x.ClassroomId)
.ToHashSet() ?? [];
var allowedExperimentRoomIds = constraint?.AllowedExperimentClassrooms
.Select(x => x.ClassroomId)
.ToHashSet() ?? [];
var minimumCapacity = Math.Max(
task.Capacity,
task.Classes.Sum(x =>
@@ -291,11 +295,11 @@ public sealed class AutomaticScheduleGenerator(AppDbContext db)
(constraint?.RequiredBuildingId is not Guid requiredBuildingId ||
room.BuildingId == requiredBuildingId) &&
(allowedRoomIds.Count == 0 || allowedRoomIds.Contains(room.Id)) &&
(kind != ScheduleEntryKind.Experiment ||
TeachingVenueNatureRules.SupportsExperiment(room.TeachingVenueNature)) &&
(kind != ScheduleEntryKind.Experiment || constraint is null ||
constraint.AllowedExperimentVenueNatures == 0 ||
(room.TeachingVenueNature & constraint.AllowedExperimentVenueNatures) != 0))
constraint.AllowedExperimentVenueNatures == 0 ||
(room.TeachingVenueNature & constraint.AllowedExperimentVenueNatures) != 0) &&
(kind != ScheduleEntryKind.Experiment || allowedExperimentRoomIds.Count == 0 ||
allowedExperimentRoomIds.Contains(room.Id)))
.ToList();
}
@@ -167,6 +167,7 @@ public sealed class SchedulePlanPublisher(AppDbContext db)
var constraints = await db.TeachingTaskScheduleConstraints.AsNoTracking()
.WhereIn(taskIds, x => x.TeachingTaskId)
.Include(x => x.AllowedClassrooms)
.Include(x => x.AllowedExperimentClassrooms)
.ToDictionaryAsync(x => x.TeachingTaskId, cancellationToken);
foreach (var entry in plan.Entries)
@@ -270,9 +271,6 @@ public sealed class SchedulePlanPublisher(AppDbContext db)
{
if (classroom is null || !classroom.IsEnabled)
Fail(entry, "所选教室不存在或已停用");
if (entry.Kind == ScheduleEntryKind.Experiment &&
!IsExperimentRoom(classroom.RoomType))
Fail(entry, $"实验课不能安排在“{classroom.RoomType}”类型的场地");
if (constraint?.RequiredCampusId is Guid campusId &&
classroom.Building!.CampusId != campusId)
Fail(entry, "所选教室不在指定校区");
@@ -285,6 +283,18 @@ public sealed class SchedulePlanPublisher(AppDbContext db)
if (allowedClassroomIds.Count > 0 &&
!allowedClassroomIds.Contains(classroom.Id))
Fail(entry, "所选教室不在指定教室范围内");
var allowedExperimentClassroomIds = constraint?.AllowedExperimentClassrooms
.Select(x => x.ClassroomId)
.ToHashSet() ?? [];
if (entry.Kind == ScheduleEntryKind.Experiment &&
allowedExperimentClassroomIds.Count > 0 &&
!allowedExperimentClassroomIds.Contains(classroom.Id))
Fail(entry, "所选场地不在实验课指定场地范围内");
if (entry.Kind == ScheduleEntryKind.Experiment &&
constraint?.AllowedExperimentVenueNatures is { } allowedNatures &&
allowedNatures != 0 &&
(classroom.TeachingVenueNature & allowedNatures) == 0)
Fail(entry, "所选场地不在实验课允许的场地性质范围内");
}
var studentCount = task.Classes.Sum(x =>
@@ -305,12 +315,6 @@ public sealed class SchedulePlanPublisher(AppDbContext db)
.Select(int.Parse)
.ToHashSet();
private static bool IsExperimentRoom(string roomType) =>
roomType.Contains("实验", StringComparison.OrdinalIgnoreCase) ||
roomType.Contains("实训", StringComparison.OrdinalIgnoreCase) ||
roomType.Contains("机房", StringComparison.OrdinalIgnoreCase) ||
roomType.Contains("语音", StringComparison.OrdinalIgnoreCase);
[DoesNotReturn]
private static void Fail(ScheduleEntry entry, string message)
{