修复实验排课

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
@@ -105,6 +105,7 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
var constraints = await db.TeachingTaskScheduleConstraints.AsNoTracking()
.WhereIn(taskIds, x => x.TeachingTaskId)
.Include(x => x.AllowedClassrooms)
.Include(x => x.AllowedExperimentClassrooms)
.ToDictionaryAsync(x => x.TeachingTaskId, cancellationToken);
return Ok(tasks.Select(task =>
{
@@ -137,6 +138,8 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
constraint?.LatestPeriod,
AllowedClassroomIds = constraint?.AllowedClassrooms
.Select(x => x.ClassroomId) ?? [],
AllowedExperimentClassroomIds = constraint?.AllowedExperimentClassrooms
.Select(x => x.ClassroomId) ?? [],
AllowedExperimentVenueNatures = constraint?.AllowedExperimentVenueNatures ?? 0
};
}));
@@ -168,6 +171,7 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
{
var flexibleConstraint = await db.TeachingTaskScheduleConstraints
.Include(x => x.AllowedClassrooms)
.Include(x => x.AllowedExperimentClassrooms)
.FirstOrDefaultAsync(x => x.TeachingTaskId == teachingTaskId, cancellationToken);
if (flexibleConstraint is not null)
{
@@ -208,8 +212,23 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
allowedRooms.Any(x => x.Building!.CampusId != request.RequiredCampusId))
return ValidationProblem("指定教室必须位于所选校区。");
var allowedExperimentRoomIds = request.AllowedExperimentClassroomIds?.Distinct().ToArray() ?? [];
var allowedExperimentRooms = await db.Classrooms.AsNoTracking()
.Where(x => x.IsEnabled)
.WhereIn(allowedExperimentRoomIds, x => x.Id)
.Include(x => x.Building)
.ToListAsync(cancellationToken);
if (allowedExperimentRooms.Count != allowedExperimentRoomIds.Length)
return ValidationProblem("部分指定实验场地不存在或已停用。");
if (building is not null && allowedExperimentRooms.Any(x => x.BuildingId != building.Id))
return ValidationProblem("指定实验场地必须位于所选教学楼。");
if (request.RequiredCampusId.HasValue &&
allowedExperimentRooms.Any(x => x.Building!.CampusId != request.RequiredCampusId))
return ValidationProblem("指定实验场地必须位于所选校区。");
var constraint = await db.TeachingTaskScheduleConstraints
.Include(x => x.AllowedClassrooms)
.Include(x => x.AllowedExperimentClassrooms)
.FirstOrDefaultAsync(x => x.TeachingTaskId == teachingTaskId, cancellationToken);
if (constraint is null)
{
@@ -230,10 +249,15 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
constraint.LatestPeriod = request.LatestPeriod;
constraint.AllowedExperimentVenueNatures = request.AllowedExperimentVenueNatures;
db.TeachingTaskAllowedClassrooms.RemoveRange(constraint.AllowedClassrooms);
db.TeachingTaskAllowedExperimentClassrooms.RemoveRange(constraint.AllowedExperimentClassrooms);
constraint.AllowedClassrooms = request.RequiresClassroom
? request.AllowedClassroomIds.Distinct().Select(classroomId =>
new TeachingTaskAllowedClassroom { ClassroomId = classroomId }).ToList()
: [];
constraint.AllowedExperimentClassrooms = request.RequiresClassroom
? allowedExperimentRoomIds.Select(classroomId =>
new TeachingTaskAllowedExperimentClassroom { ClassroomId = classroomId }).ToList()
: [];
await db.SaveChangesAsync(cancellationToken);
return NoContent();
}
@@ -256,21 +280,26 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
!Enum.IsDefined(request.SchedulingMode.Value))
return ValidationProblem("授课方式无效。");
if (!request.SchedulingMode.HasValue &&
!request.RequiresClassroom.HasValue &&
request.AllowedDayOfWeeks is null &&
!request.UpdateClassroomScope &&
!request.UpdatePeriodRange &&
!request.RequiresClassroom.HasValue &&
request.AllowedDayOfWeeks is null &&
!request.UpdateClassroomScope &&
!request.UpdateExperimentClassroomScope &&
!request.AllowedExperimentVenueNatures.HasValue &&
!request.UpdatePeriodRange &&
!request.EarliestPeriod.HasValue &&
!request.LatestPeriod.HasValue)
return ValidationProblem("请至少选择一项需要批量修改的设置。");
if (request.UpdateClassroomScope && request.RequiresClassroom == false)
return ValidationProblem("批量指定教室范围时,场地要求不能设置为不占用教室。");
if (request.UpdateExperimentClassroomScope && request.RequiresClassroom == false)
return ValidationProblem("批量指定实验场地时,场地要求不能设置为不占用教室。");
var tasks = await db.TeachingTasks
.Where(x =>
x.AcademicTermId == request.AcademicTermId &&
x.Status == TeachingTaskStatus.Published)
.WhereIn(taskIds, x => x.Id)
.Include(x => x.Course)
.ToListAsync(cancellationToken);
if (tasks.Count != taskIds.Length)
return ValidationProblem("部分教学任务不存在、未发布或不属于当前学期。");
@@ -281,9 +310,15 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
(request.SchedulingMode ?? task.SchedulingMode) ==
TeachingTaskSchedulingMode.Flexible))
return ConflictProblem("非排时课程不能指定教室,请先将当前筛选结果限定为正常排课课程。");
if (request.UpdateExperimentClassroomScope && tasks.Any(task =>
(request.SchedulingMode ?? task.SchedulingMode) ==
TeachingTaskSchedulingMode.Flexible))
return ConflictProblem("非排时课程不能指定实验场地,请先将当前筛选结果限定为正常排课课程。");
Building? building = null;
List<Classroom> allowedRooms = [];
var experimentRoomIds = request.AllowedExperimentClassroomIds?.Distinct().ToArray() ?? [];
List<Classroom> allowedExperimentRooms = [];
if (request.UpdateClassroomScope)
{
if (request.RequiredBuildingId.HasValue)
@@ -320,10 +355,20 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
x.Building!.CampusId != request.RequiredCampusId))
return ValidationProblem("指定教室必须位于所选校区。");
}
if (request.UpdateExperimentClassroomScope)
{
allowedExperimentRooms = await db.Classrooms.AsNoTracking()
.Where(x => x.IsEnabled)
.WhereIn(experimentRoomIds, x => x.Id)
.ToListAsync(cancellationToken);
if (allowedExperimentRooms.Count != experimentRoomIds.Length)
return ValidationProblem("部分指定实验场地不存在或已停用。");
}
var constraints = await db.TeachingTaskScheduleConstraints
.WhereIn(taskIds, x => x.TeachingTaskId)
.Include(x => x.AllowedClassrooms)
.Include(x => x.AllowedClassrooms)
.Include(x => x.AllowedExperimentClassrooms)
.ToDictionaryAsync(x => x.TeachingTaskId, cancellationToken);
foreach (var task in tasks)
{
@@ -342,6 +387,8 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
request.RequiresClassroom.HasValue ||
request.AllowedDayOfWeeks is not null ||
request.UpdateClassroomScope ||
request.UpdateExperimentClassroomScope ||
request.AllowedExperimentVenueNatures.HasValue ||
request.UpdatePeriodRange;
if (!changesConstraint) continue;
constraint = new TeachingTaskScheduleConstraint { TeachingTaskId = task.Id };
@@ -357,7 +404,10 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
constraint.RequiredCampusId = null;
constraint.RequiredBuildingId = null;
db.TeachingTaskAllowedClassrooms.RemoveRange(constraint.AllowedClassrooms);
db.TeachingTaskAllowedExperimentClassrooms.RemoveRange(
constraint.AllowedExperimentClassrooms);
constraint.AllowedClassrooms = [];
constraint.AllowedExperimentClassrooms = [];
}
}
if (request.AllowedDayOfWeeks is not null)
@@ -379,6 +429,15 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
ClassroomId = room.Id
}).ToList();
}
if (task.Course?.PracticeHours > 0 && request.AllowedExperimentVenueNatures.HasValue)
constraint.AllowedExperimentVenueNatures = request.AllowedExperimentVenueNatures.Value;
if (task.Course?.PracticeHours > 0 && request.UpdateExperimentClassroomScope)
{
db.TeachingTaskAllowedExperimentClassrooms.RemoveRange(
constraint.AllowedExperimentClassrooms);
constraint.AllowedExperimentClassrooms = allowedExperimentRooms.Select(room =>
new TeachingTaskAllowedExperimentClassroom { ClassroomId = room.Id }).ToList();
}
if (request.UpdatePeriodRange)
{
constraint.EarliestPeriod = request.EarliestPeriod;
@@ -406,7 +465,9 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
constraint.EarliestPeriod = null;
constraint.LatestPeriod = null;
db.TeachingTaskAllowedClassrooms.RemoveRange(constraint.AllowedClassrooms);
db.TeachingTaskAllowedExperimentClassrooms.RemoveRange(constraint.AllowedExperimentClassrooms);
constraint.AllowedClassrooms = [];
constraint.AllowedExperimentClassrooms = [];
}
private ActionResult ConflictProblem(string detail) =>
@@ -441,7 +502,8 @@ public sealed record TeachingTaskScheduleConstraintRequest(
IReadOnlyList<int> AllowedDayOfWeeks,
[Range(1, 30)] int? EarliestPeriod,
[Range(1, 30)] int? LatestPeriod,
TeachingVenueNature AllowedExperimentVenueNatures = 0);
TeachingVenueNature AllowedExperimentVenueNatures = 0,
IReadOnlyList<Guid>? AllowedExperimentClassroomIds = null);
public sealed record TeachingTaskScheduleConstraintBatchRequest(
Guid AcademicTermId,
@@ -455,4 +517,7 @@ public sealed record TeachingTaskScheduleConstraintBatchRequest(
IReadOnlyList<Guid>? AllowedClassroomIds,
bool UpdatePeriodRange,
[Range(1, 30)] int? EarliestPeriod,
[Range(1, 30)] int? LatestPeriod);
[Range(1, 30)] int? LatestPeriod,
bool UpdateExperimentClassroomScope = false,
TeachingVenueNature? AllowedExperimentVenueNatures = null,
IReadOnlyList<Guid>? AllowedExperimentClassroomIds = null);
@@ -552,6 +552,7 @@ public sealed class SchedulesController(
var constraint = await db.TeachingTaskScheduleConstraints.AsNoTracking()
.Include(x => x.AllowedClassrooms)
.Include(x => x.AllowedExperimentClassrooms)
.FirstOrDefaultAsync(
x => x.TeachingTaskId == request.TeachingTaskId,
cancellationToken);
@@ -580,10 +581,6 @@ public sealed class SchedulesController(
x => x.Id == request.ClassroomId && x.IsEnabled,
cancellationToken);
if (classroom is null) return ValidationProblem("所选教室不存在或已停用。");
if (request.Kind == ScheduleEntryKind.Experiment &&
!TeachingVenueNatureRules.SupportsExperiment(classroom.TeachingVenueNature))
return ValidationProblem(
$"实验课必须安排在具有实验教学性质的场地;“{classroom.Name}”未标注实验室、实训室、机房或语音室性质。 ");
if (constraint?.RequiredCampusId is Guid campusId &&
classroom.Building!.CampusId != campusId)
return ValidationProblem("所选教室不在该课程指定的校区。");
@@ -601,6 +598,13 @@ public sealed class SchedulesController(
allowedNatures != 0 &&
(classroom.TeachingVenueNature & allowedNatures) == 0)
return ValidationProblem("所选场地不在该实验课允许的教学场地性质范围内。");
var allowedExperimentClassroomIds = constraint?.AllowedExperimentClassrooms
.Select(x => x.ClassroomId)
.ToHashSet() ?? [];
if (request.Kind == ScheduleEntryKind.Experiment &&
allowedExperimentClassroomIds.Count > 0 &&
!allowedExperimentClassroomIds.Contains(classroom.Id))
return ValidationProblem("所选场地不在该实验课指定的场地范围内。");
}
var studentCount = task.Classes.Sum(x =>
x.AdministrativeClass!.Students.Count(student =>
@@ -57,6 +57,7 @@ public sealed class TeachingTaskScheduleConstraint : EntityBase
public int? LatestPeriod { get; set; }
public TeachingVenueNature AllowedExperimentVenueNatures { get; set; }
public ICollection<TeachingTaskAllowedClassroom> AllowedClassrooms { get; set; } = [];
public ICollection<TeachingTaskAllowedExperimentClassroom> AllowedExperimentClassrooms { get; set; } = [];
}
public sealed class TeachingTaskAllowedClassroom
@@ -67,6 +68,14 @@ public sealed class TeachingTaskAllowedClassroom
public Classroom? Classroom { get; set; }
}
public sealed class TeachingTaskAllowedExperimentClassroom
{
public Guid TeachingTaskScheduleConstraintId { get; set; }
public TeachingTaskScheduleConstraint? TeachingTaskScheduleConstraint { get; set; }
public Guid ClassroomId { get; set; }
public Classroom? Classroom { get; set; }
}
public sealed class AutomaticScheduleJob : EntityBase
{
public Guid SchedulePlanId { get; set; }
@@ -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)
{