实验排课

This commit is contained in:
2026-08-09 12:30:33 +08:00 Unverified
parent 1229f3163d
commit c692002676
25 changed files with 21294 additions and 1189 deletions
@@ -491,6 +491,7 @@ public sealed class BaseDataController(AppDbContext db, IAppCache cache) : Contr
x.Building.Campus!.Name,
x.Capacity,
x.RoomType,
x.TeachingVenueNature,
x.Equipment,
x.IsEnabled,
x.SortOrder))
@@ -557,6 +558,9 @@ public sealed class BaseDataController(AppDbContext db, IAppCache cache) : Contr
BuildingId = request.BuildingId,
Capacity = request.Capacity,
RoomType = request.RoomType.Trim(),
TeachingVenueNature = request.TeachingVenueNature == 0
? TeachingVenueNature.GeneralClassroom
: request.TeachingVenueNature,
Equipment = request.Equipment?.Trim(),
SortOrder = request.SortOrder,
IsEnabled = request.IsEnabled
@@ -577,6 +581,9 @@ public sealed class BaseDataController(AppDbContext db, IAppCache cache) : Contr
entity.BuildingId = request.BuildingId;
entity.Capacity = request.Capacity;
entity.RoomType = request.RoomType.Trim();
entity.TeachingVenueNature = request.TeachingVenueNature == 0
? TeachingVenueNature.GeneralClassroom
: request.TeachingVenueNature;
entity.Equipment = request.Equipment?.Trim();
await SaveAndInvalidateAsync(cancellationToken);
return entity;
@@ -728,6 +735,7 @@ public sealed record ClassroomRequest(
Guid BuildingId,
[Range(1, 1000)] int Capacity,
[Required, MaxLength(40)] string RoomType,
TeachingVenueNature TeachingVenueNature,
[MaxLength(300)] string? Equipment)
: CatalogRequest(Code, Name, SortOrder, IsEnabled);
@@ -784,6 +792,7 @@ public sealed record ClassroomListItem(
string CampusName,
int Capacity,
string RoomType,
TeachingVenueNature TeachingVenueNature,
string? Equipment,
bool IsEnabled,
int SortOrder);
@@ -27,7 +27,7 @@ public sealed class BaseDataExcelController(AppDbContext db, IAppCache cache) :
["classes"] = ["编码", "名称", "所属专业编码", "年级", "辅导员工号", "排序", "状态"],
["terms"] = ["编码", "名称", "学年", "学期季", "开始日期", "结束日期", "当前学期", "状态"],
["buildings"] = ["编码", "名称", "所属校区编码", "排序", "状态"],
["classrooms"] = ["编码", "名称", "所属教学楼编码", "容量", "教室类型", "设备", "排序", "状态"],
["classrooms"] = ["编码", "名称", "所属教学楼编码", "容量", "教室类型", "教学场地性质", "设备", "排序", "状态"],
["course-categories"] = ["编码", "名称", "排序", "状态"]
};
@@ -70,7 +70,10 @@ public sealed class BaseDataExcelController(AppDbContext db, IAppCache cache) :
IReadOnlyList<ExcelRow> rows;
try
{
rows = await ExcelWorkbookHelper.ReadAsync(file, headers, cancellationToken);
var requiredHeaders = kind.Equals("classrooms", StringComparison.OrdinalIgnoreCase)
? headers.Where(x => x != "教学场地性质").ToArray()
: headers;
rows = await ExcelWorkbookHelper.ReadAsync(file, requiredHeaders, cancellationToken);
}
catch (InvalidDataException exception)
{
@@ -165,6 +168,7 @@ public sealed class BaseDataExcelController(AppDbContext db, IAppCache cache) :
"classrooms" => (await db.Classrooms.AsNoTracking().Include(x => x.Building)
.OrderBy(x => x.Code).ToListAsync(cancellationToken))
.Select(x => Row(x.Code, x.Name, x.Building!.Code, x.Capacity, x.RoomType,
VenueNatureName(x.TeachingVenueNature),
x.Equipment, x.SortOrder, Status(x.IsEnabled))).ToList(),
"course-categories" => (await db.CourseCategories.AsNoTracking()
.OrderBy(x => x.SortOrder).ThenBy(x => x.Code)
@@ -473,8 +477,9 @@ public sealed class BaseDataExcelController(AppDbContext db, IAppCache cache) :
var buildingCode = Required(row, "所属教学楼编码", errors);
var capacity = ParseInt(row, "容量", 1, 1000, errors);
var roomType = Required(row, "教室类型", errors);
var venueNature = ParseVenueNature(row, roomType, errors);
if (code is null || name is null || buildingCode is null ||
capacity is null || roomType is null) continue;
capacity is null || roomType is null || venueNature is null) continue;
if (!buildings.TryGetValue(buildingCode, out var building))
{
errors.Add($"第 {row.RowNumber} 行:所属教学楼编码“{buildingCode}”不存在。");
@@ -488,7 +493,8 @@ public sealed class BaseDataExcelController(AppDbContext db, IAppCache cache) :
Code = code,
Name = name,
BuildingId = building.Id,
RoomType = roomType
RoomType = roomType,
TeachingVenueNature = venueNature.Value
};
db.Classrooms.Add(entity);
existing[code] = entity;
@@ -499,6 +505,7 @@ public sealed class BaseDataExcelController(AppDbContext db, IAppCache cache) :
entity.BuildingId = building.Id;
entity.Capacity = capacity.Value;
entity.RoomType = roomType;
entity.TeachingVenueNature = venueNature.Value;
entity.Equipment = Optional(row, "设备");
}
return new(created, updated, rows.Count);
@@ -597,6 +604,57 @@ public sealed class BaseDataExcelController(AppDbContext db, IAppCache cache) :
return true;
}
private static TeachingVenueNature? ParseVenueNature(
ExcelRow row,
string? roomType,
List<string> errors)
{
var value = Optional(row, "教学场地性质");
if (value is null) return InferVenueNature(roomType ?? string.Empty);
var result = (TeachingVenueNature)0;
foreach (var part in value.Split(['、', '', ',', ';', ''],
StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))
{
result |= part switch
{
"普通教室" => TeachingVenueNature.GeneralClassroom,
"实验室" => TeachingVenueNature.Laboratory,
"实训室" => TeachingVenueNature.TrainingRoom,
"计算机机房" or "机房" => TeachingVenueNature.ComputerLab,
"语音室" => TeachingVenueNature.LanguageLab,
"体育场地" => TeachingVenueNature.SportsVenue,
"艺术场地" => TeachingVenueNature.ArtsVenue,
_ => (TeachingVenueNature)0
};
if (part is not ("普通教室" or "实验室" or "实训室" or "计算机机房" or "机房" or "语音室" or "体育场地" or "艺术场地"))
errors.Add($"第 {row.RowNumber} 行:“教学场地性质”包含不支持的值“{part}”。");
}
return result == 0 ? null : result;
}
private static TeachingVenueNature InferVenueNature(string roomType) =>
roomType.Contains("机房", StringComparison.OrdinalIgnoreCase)
? TeachingVenueNature.Laboratory | TeachingVenueNature.ComputerLab
: roomType.Contains("语音", StringComparison.OrdinalIgnoreCase)
? TeachingVenueNature.Laboratory | TeachingVenueNature.LanguageLab
: roomType.Contains("实训", StringComparison.OrdinalIgnoreCase)
? TeachingVenueNature.TrainingRoom
: roomType.Contains("实验", StringComparison.OrdinalIgnoreCase)
? TeachingVenueNature.Laboratory
: TeachingVenueNature.GeneralClassroom;
private static string VenueNatureName(TeachingVenueNature value) => string.Join("、",
new[]
{
(TeachingVenueNature.GeneralClassroom, "普通教室"),
(TeachingVenueNature.Laboratory, "实验室"),
(TeachingVenueNature.TrainingRoom, "实训室"),
(TeachingVenueNature.ComputerLab, "计算机机房"),
(TeachingVenueNature.LanguageLab, "语音室"),
(TeachingVenueNature.SportsVenue, "体育场地"),
(TeachingVenueNature.ArtsVenue, "艺术场地")
}.Where(x => (value & x.Item1) != 0).Select(x => x.Item2));
private static bool ParseBoolean(
ExcelRow row, string header, bool defaultValue, List<string> errors)
{
@@ -91,6 +91,36 @@ public sealed class ExperimentsController(
.Select(item => item.AdministrativeClass!.Name)
})
.ToListAsync(cancellationToken),
ScheduleEntries = await db.ScheduleEntries.AsNoTracking()
.Where(x => x.SchedulePlan!.Status == SchedulePlanStatus.Published &&
x.Kind == ScheduleEntryKind.Experiment &&
x.ClassroomId.HasValue &&
AccessibleTeachingTasks().Select(task => task.Id)
.Contains(x.TeachingTaskId))
.Where(x => !academicTermId.HasValue ||
x.SchedulePlan!.AcademicTermId == academicTermId.Value)
.OrderBy(x => x.TeachingTask!.Course!.Code)
.ThenBy(x => x.TeachingTask!.TaskNumber)
.ThenBy(x => x.DayOfWeek)
.ThenBy(x => x.StartPeriod)
.Select(x => new
{
x.Id,
x.TeachingTaskId,
TaskNumber = x.TeachingTask!.TaskNumber,
CourseCode = x.TeachingTask.Course!.Code,
CourseName = x.TeachingTask.Course.Name,
x.DayOfWeek,
x.StartPeriod,
x.PeriodCount,
x.StartWeek,
x.EndWeek,
x.WeekPattern,
ClassroomName = x.Classroom!.Name,
BuildingName = x.Classroom.Building!.Name,
CampusName = x.Classroom.Building.Campus!.Name
})
.ToListAsync(cancellationToken),
Classrooms = await db.Classrooms.AsNoTracking()
.Where(x => x.IsEnabled)
.OrderBy(x => x.Building!.Campus!.SortOrder)
@@ -103,6 +133,7 @@ public sealed class ExperimentsController(
BuildingName = x.Building!.Name,
CampusName = x.Building.Campus!.Name,
x.Capacity
,x.TeachingVenueNature
})
.ToListAsync(cancellationToken),
Periods = periodItems
@@ -135,6 +166,7 @@ public sealed class ExperimentsController(
{
x.Id,
x.TeachingTaskId,
x.ScheduleEntryId,
x.Code,
x.Name,
x.ArrangementMode,
@@ -157,6 +189,18 @@ public sealed class ExperimentsController(
ClassNames = x.TeachingTask.Classes
.OrderBy(item => item.AdministrativeClass!.Code)
.Select(item => item.AdministrativeClass!.Name),
ScheduleEntry = x.ScheduleEntryId == null ? null : new
{
x.ScheduleEntry!.DayOfWeek,
x.ScheduleEntry.StartPeriod,
x.ScheduleEntry.PeriodCount,
x.ScheduleEntry.StartWeek,
x.ScheduleEntry.EndWeek,
x.ScheduleEntry.WeekPattern,
ClassroomName = x.ScheduleEntry.Classroom!.Name,
BuildingName = x.ScheduleEntry.Classroom.Building!.Name,
CampusName = x.ScheduleEntry.Classroom.Building.Campus!.Name
},
Sessions = x.Sessions
.OrderBy(item => item.SessionDate)
.ThenBy(item => item.StartPeriod)
@@ -209,6 +253,7 @@ public sealed class ExperimentsController(
x.Code,
x.Name,
x.ArrangementMode,
x.ScheduleEntryId,
x.Description,
x.Requirements,
x.StartDate,
@@ -222,6 +267,18 @@ public sealed class ExperimentsController(
TeacherNames = x.TeachingTask.Teachers
.OrderByDescending(item => item.IsPrimary)
.Select(item => item.Teacher!.Name),
ScheduleEntry = x.ScheduleEntryId == null ? null : new
{
x.ScheduleEntry!.DayOfWeek,
x.ScheduleEntry.StartPeriod,
x.ScheduleEntry.PeriodCount,
x.ScheduleEntry.StartWeek,
x.ScheduleEntry.EndWeek,
x.ScheduleEntry.WeekPattern,
ClassroomName = x.ScheduleEntry.Classroom!.Name,
BuildingName = x.ScheduleEntry.Classroom.Building!.Name,
CampusName = x.ScheduleEntry.Classroom.Building.Campus!.Name
},
Sessions = x.Sessions
.Where(item => item.Status == ExperimentSessionStatus.Scheduled)
.OrderBy(item => item.SessionDate)
@@ -272,6 +329,9 @@ public sealed class ExperimentsController(
var problem = ValidateProjectRequest(request, task.AcademicTerm!);
if (problem is not null) return ValidationProblem(problem);
var scheduleEntry = await ValidateScheduleEntryAsync(
request, task.Id, cancellationToken);
if (scheduleEntry.Problem is not null) return ValidationProblem(scheduleEntry.Problem);
var code = request.Code.Trim();
if (await db.ExperimentProjects.AnyAsync(x =>
@@ -283,6 +343,7 @@ public sealed class ExperimentsController(
var project = new ExperimentProject
{
TeachingTaskId = request.TeachingTaskId,
ScheduleEntryId = scheduleEntry.Entry?.Id,
Code = code,
Name = request.Name.Trim(),
ArrangementMode = request.ArrangementMode,
@@ -302,6 +363,8 @@ public sealed class ExperimentsController(
ExperimentProjectBatchRequest request,
CancellationToken cancellationToken)
{
if (request.ArrangementMode == ExperimentArrangementMode.Centralized)
return ValidationProblem("集中安排的实验项目请逐项绑定已发布课表中的实验课。");
var taskIds = request.TeachingTaskIds
.Where(x => x != Guid.Empty)
.Distinct()
@@ -387,6 +450,9 @@ public sealed class ExperimentsController(
request,
project.TeachingTask!.AcademicTerm!);
if (problem is not null) return ValidationProblem(problem);
var scheduleEntry = await ValidateScheduleEntryAsync(
request, project.TeachingTaskId, cancellationToken);
if (scheduleEntry.Problem is not null) return ValidationProblem(scheduleEntry.Problem);
var code = request.Code.Trim();
if (await db.ExperimentProjects.AnyAsync(x =>
@@ -399,6 +465,7 @@ public sealed class ExperimentsController(
project.Code = code;
project.Name = request.Name.Trim();
project.ArrangementMode = request.ArrangementMode;
project.ScheduleEntryId = scheduleEntry.Entry?.Id;
project.Description = Normalize(request.Description);
project.Requirements = Normalize(request.Requirements);
project.StartDate = request.StartDate;
@@ -438,9 +505,14 @@ public sealed class ExperimentsController(
if (project is null) return NotFound();
if (project.Status != ExperimentProjectStatus.Draft)
return ConflictProblem("只有草稿实验项目可以发布。");
if (!project.Sessions.Any(x =>
x.Status == ExperimentSessionStatus.Scheduled))
return ConflictProblem("请至少安排一个有效实验场次后再发布。");
var hasSchedule = project.ArrangementMode == ExperimentArrangementMode.Centralized
? project.ScheduleEntryId.HasValue || project.Sessions.Any(x =>
x.Status == ExperimentSessionStatus.Scheduled)
: project.Sessions.Any(x => x.Status == ExperimentSessionStatus.Scheduled);
if (!hasSchedule)
return ConflictProblem(project.ArrangementMode == ExperimentArrangementMode.Centralized
? "请先绑定已发布课表中的实验课后再发布。"
: "请至少安排一个有效实验场次后再发布。");
if (project.Sessions.Any(x =>
x.Status == ExperimentSessionStatus.Scheduled &&
(x.SessionDate < project.StartDate ||
@@ -506,6 +578,8 @@ public sealed class ExperimentsController(
if (project is null) return NotFound();
if (project.Status == ExperimentProjectStatus.Closed)
return ConflictProblem("已关闭实验项目不能再增加场次。");
if (project.ArrangementMode == ExperimentArrangementMode.Centralized)
return ConflictProblem("集中安排的实验项目直接使用已发布课表中的实验课,不能在此重复排时派地点。");
var problem = await ValidateSessionAsync(
project,
@@ -588,6 +662,9 @@ public sealed class ExperimentsController(
if (project.Status == ExperimentProjectStatus.Closed)
return ConflictProblem(
$"实验项目“{project.Name}”已关闭,不能再增加场次。");
if (project.ArrangementMode == ExperimentArrangementMode.Centralized)
return ConflictProblem(
$"实验项目“{project.Name}”为集中安排,请直接使用已发布课表中的实验课。");
var sessionRequest = item.ToSessionRequest();
var problem = await ValidateSessionAsync(
@@ -930,6 +1007,8 @@ public sealed class ExperimentsController(
x.Id == request.ClassroomId && x.IsEnabled,
cancellationToken);
if (classroom is null) return "实验教室不存在或已停用。";
if (!TeachingVenueNatureRules.SupportsExperiment(classroom.TeachingVenueNature))
return "所选场地未标注实验教学性质。";
if (request.Capacity > classroom.Capacity)
return $"场次容量不能超过教室容量 {classroom.Capacity} 人。";
if (project.ArrangementMode ==
@@ -1173,6 +1252,30 @@ public sealed class ExperimentsController(
return null;
}
private async Task<(ScheduleEntry? Entry, string? Problem)> ValidateScheduleEntryAsync(
ExperimentProjectRequest request,
Guid teachingTaskId,
CancellationToken cancellationToken)
{
if (request.ArrangementMode == ExperimentArrangementMode.SelfScheduled)
return request.ScheduleEntryId.HasValue
? (null, "自行安排的实验项目不能绑定课表实验课。")
: (null, null);
if (!request.ScheduleEntryId.HasValue)
return (null, "集中安排的实验项目必须绑定已发布课表中的实验课。");
var entry = await db.ScheduleEntries
.Include(x => x.SchedulePlan)
.FirstOrDefaultAsync(x => x.Id == request.ScheduleEntryId, cancellationToken);
if (entry is null || entry.SchedulePlan!.Status != SchedulePlanStatus.Published ||
entry.Kind != ScheduleEntryKind.Experiment || !entry.ClassroomId.HasValue ||
entry.TeachingTaskId != teachingTaskId)
return (null, "只能绑定本教学任务已发布、已安排实验室的实验课。");
if (!await AccessibleTeachingTasks().AnyAsync(x => x.Id == teachingTaskId, cancellationToken))
return (null, "该教学任务不在当前管理范围内。");
return (entry, null);
}
private static string? Normalize(string? value) =>
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
@@ -1193,7 +1296,8 @@ public sealed record ExperimentProjectRequest(
[MaxLength(1000)] string? Description,
[MaxLength(1000)] string? Requirements,
DateOnly StartDate,
DateOnly EndDate);
DateOnly EndDate,
Guid? ScheduleEntryId = null);
public sealed record ExperimentProjectBatchRequest(
[Required] IReadOnlyList<Guid> TeachingTaskIds,
@@ -136,7 +136,8 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
constraint?.EarliestPeriod,
constraint?.LatestPeriod,
AllowedClassroomIds = constraint?.AllowedClassrooms
.Select(x => x.ClassroomId) ?? []
.Select(x => x.ClassroomId) ?? [],
AllowedExperimentVenueNatures = constraint?.AllowedExperimentVenueNatures ?? 0
};
}));
}
@@ -227,6 +228,7 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
: string.Join(',', request.AllowedDayOfWeeks.Distinct().Order());
constraint.EarliestPeriod = request.EarliestPeriod;
constraint.LatestPeriod = request.LatestPeriod;
constraint.AllowedExperimentVenueNatures = request.AllowedExperimentVenueNatures;
db.TeachingTaskAllowedClassrooms.RemoveRange(constraint.AllowedClassrooms);
constraint.AllowedClassrooms = request.RequiresClassroom
? request.AllowedClassroomIds.Distinct().Select(classroomId =>
@@ -438,7 +440,8 @@ public sealed record TeachingTaskScheduleConstraintRequest(
IReadOnlyList<Guid> AllowedClassroomIds,
IReadOnlyList<int> AllowedDayOfWeeks,
[Range(1, 30)] int? EarliestPeriod,
[Range(1, 30)] int? LatestPeriod);
[Range(1, 30)] int? LatestPeriod,
TeachingVenueNature AllowedExperimentVenueNatures = 0);
public sealed record TeachingTaskScheduleConstraintBatchRequest(
Guid AcademicTermId,
@@ -581,9 +581,9 @@ public sealed class SchedulesController(
cancellationToken);
if (classroom is null) return ValidationProblem("所选教室不存在或已停用。");
if (request.Kind == ScheduleEntryKind.Experiment &&
!IsExperimentRoom(classroom.RoomType))
!TeachingVenueNatureRules.SupportsExperiment(classroom.TeachingVenueNature))
return ValidationProblem(
$"实验课必须安排在实验室、实训室或机房;“{classroom.Name}”的场地类型为“{classroom.RoomType}”。");
$"实验课必须安排在具有实验教学性质的场地;“{classroom.Name}”未标注实验室、实训室、机房或语音室性质。 ");
if (constraint?.RequiredCampusId is Guid campusId &&
classroom.Building!.CampusId != campusId)
return ValidationProblem("所选教室不在该课程指定的校区。");
@@ -596,6 +596,11 @@ public sealed class SchedulesController(
if (allowedClassroomIds.Count > 0 &&
!allowedClassroomIds.Contains(classroom.Id))
return ValidationProblem("所选教室不在该课程指定的教室范围内。");
if (request.Kind == ScheduleEntryKind.Experiment &&
constraint?.AllowedExperimentVenueNatures is { } allowedNatures &&
allowedNatures != 0 &&
(classroom.TeachingVenueNature & allowedNatures) == 0)
return ValidationProblem("所选场地不在该实验课允许的教学场地性质范围内。");
}
var studentCount = task.Classes.Sum(x =>
x.AdministrativeClass!.Students.Count(student =>
@@ -655,11 +660,6 @@ public sealed class SchedulesController(
.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);
private async Task<ActionResult> SaveAsync(
Guid id,
@@ -6,6 +6,9 @@ public sealed class ExperimentProject : EntityBase
{
public Guid TeachingTaskId { get; set; }
public TeachingTask? TeachingTask { get; set; }
// 集中安排的项目复用已发布课表中的实验课,不再维护一份重复的场次。
public Guid? ScheduleEntryId { get; set; }
public ScheduleEntry? ScheduleEntry { get; set; }
public required string Code { get; set; }
public required string Name { get; set; }
public ExperimentArrangementMode ArrangementMode { get; set; }
@@ -46,9 +46,35 @@ public sealed class Classroom : CatalogEntity
public Building? Building { get; set; }
public int Capacity { get; set; }
public string RoomType { get; set; } = "普通教室";
public TeachingVenueNature TeachingVenueNature { get; set; } =
TeachingVenueNature.GeneralClassroom;
public string? Equipment { get; set; }
}
[Flags]
public enum TeachingVenueNature
{
GeneralClassroom = 1,
Laboratory = 2,
TrainingRoom = 4,
ComputerLab = 8,
LanguageLab = 16,
SportsVenue = 32,
ArtsVenue = 64
}
public static class TeachingVenueNatureRules
{
public const TeachingVenueNature ExperimentTeaching =
TeachingVenueNature.Laboratory |
TeachingVenueNature.TrainingRoom |
TeachingVenueNature.ComputerLab |
TeachingVenueNature.LanguageLab;
public static bool SupportsExperiment(TeachingVenueNature value) =>
(value & ExperimentTeaching) != 0;
}
public sealed class AcademicTerm : CatalogEntity
{
public required string AcademicYear { get; set; }
@@ -55,6 +55,7 @@ public sealed class TeachingTaskScheduleConstraint : EntityBase
public string? AllowedDayOfWeeks { get; set; }
public int? EarliestPeriod { get; set; }
public int? LatestPeriod { get; set; }
public TeachingVenueNature AllowedExperimentVenueNatures { get; set; }
public ICollection<TeachingTaskAllowedClassroom> AllowedClassrooms { get; set; } = [];
}
@@ -617,6 +617,10 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
entity.HasOne(x => x.TeachingTask).WithMany()
.HasForeignKey(x => x.TeachingTaskId)
.OnDelete(DeleteBehavior.Restrict);
entity.HasIndex(x => x.ScheduleEntryId);
entity.HasOne(x => x.ScheduleEntry).WithMany()
.HasForeignKey(x => x.ScheduleEntryId)
.OnDelete(DeleteBehavior.Restrict);
});
builder.Entity<ExperimentSession>(entity =>
@@ -0,0 +1,50 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
{
/// <inheritdoc />
public partial class BindCentralizedExperimentProjectsToSchedules : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Guid>(
name: "ScheduleEntryId",
table: "ExperimentProjects",
type: "char(36)",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_ExperimentProjects_ScheduleEntryId",
table: "ExperimentProjects",
column: "ScheduleEntryId");
migrationBuilder.AddForeignKey(
name: "FK_ExperimentProjects_ScheduleEntries_ScheduleEntryId",
table: "ExperimentProjects",
column: "ScheduleEntryId",
principalTable: "ScheduleEntries",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_ExperimentProjects_ScheduleEntries_ScheduleEntryId",
table: "ExperimentProjects");
migrationBuilder.DropIndex(
name: "IX_ExperimentProjects_ScheduleEntryId",
table: "ExperimentProjects");
migrationBuilder.DropColumn(
name: "ScheduleEntryId",
table: "ExperimentProjects");
}
}
}
@@ -0,0 +1,42 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
{
/// <inheritdoc />
public partial class AddTeachingVenueNatures : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "TeachingVenueNature",
table: "Classrooms",
type: "int",
nullable: false,
defaultValue: 1);
migrationBuilder.Sql("""
UPDATE `Classrooms`
SET `TeachingVenueNature` = CASE
WHEN `RoomType` LIKE '%%' THEN 10
WHEN `RoomType` LIKE '%%' THEN 18
WHEN `RoomType` LIKE '%%' THEN 4
WHEN `RoomType` LIKE '%%' THEN 2
WHEN `RoomType` LIKE '%%' THEN 32
WHEN `RoomType` LIKE '%%' THEN 64
ELSE 1
END;
""");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "TeachingVenueNature",
table: "Classrooms");
}
}
}
@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
{
/// <inheritdoc />
public partial class AddExperimentVenueNatureConstraints : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "AllowedExperimentVenueNatures",
table: "TeachingTaskScheduleConstraints",
type: "int",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "AllowedExperimentVenueNatures",
table: "TeachingTaskScheduleConstraints");
}
}
}
@@ -525,6 +525,9 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
b.Property<int>("SortOrder")
.HasColumnType("int");
b.Property<int>("TeachingVenueNature")
.HasColumnType("int");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime(6)");
@@ -2364,6 +2367,9 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
.HasMaxLength(1000)
.HasColumnType("varchar(1000)");
b.Property<Guid?>("ScheduleEntryId")
.HasColumnType("char(36)");
b.Property<DateTime>("StartDate")
.HasColumnType("date");
@@ -2378,6 +2384,8 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
b.HasKey("Id");
b.HasIndex("ScheduleEntryId");
b.HasIndex("TeachingTaskId", "Code")
.IsUnique();
@@ -4240,6 +4248,9 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
.HasMaxLength(20)
.HasColumnType("varchar(20)");
b.Property<int>("AllowedExperimentVenueNatures")
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
@@ -5621,12 +5632,19 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentProject", b =>
{
b.HasOne("Jiaowu.Api.Domain.Academic.ScheduleEntry", "ScheduleEntry")
.WithMany()
.HasForeignKey("ScheduleEntryId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask")
.WithMany()
.HasForeignKey("TeachingTaskId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("ScheduleEntry");
b.Navigation("TeachingTask");
});
@@ -291,15 +291,14 @@ 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 || IsExperimentRoom(room.RoomType)))
(kind != ScheduleEntryKind.Experiment ||
TeachingVenueNatureRules.SupportsExperiment(room.TeachingVenueNature)) &&
(kind != ScheduleEntryKind.Experiment || constraint is null ||
constraint.AllowedExperimentVenueNatures == 0 ||
(room.TeachingVenueNature & constraint.AllowedExperimentVenueNatures) != 0))
.ToList();
}
private static bool IsExperimentRoom(string roomType) =>
roomType.Contains("实验", StringComparison.OrdinalIgnoreCase) ||
roomType.Contains("实训", StringComparison.OrdinalIgnoreCase) ||
roomType.Contains("机房", StringComparison.OrdinalIgnoreCase) ||
roomType.Contains("语音", StringComparison.OrdinalIgnoreCase);
private static int[] ParseAllowedDays(string? value)
{