修复实验排课
This commit is contained in:
@@ -105,6 +105,7 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
|
|||||||
var constraints = await db.TeachingTaskScheduleConstraints.AsNoTracking()
|
var constraints = await db.TeachingTaskScheduleConstraints.AsNoTracking()
|
||||||
.WhereIn(taskIds, x => x.TeachingTaskId)
|
.WhereIn(taskIds, x => x.TeachingTaskId)
|
||||||
.Include(x => x.AllowedClassrooms)
|
.Include(x => x.AllowedClassrooms)
|
||||||
|
.Include(x => x.AllowedExperimentClassrooms)
|
||||||
.ToDictionaryAsync(x => x.TeachingTaskId, cancellationToken);
|
.ToDictionaryAsync(x => x.TeachingTaskId, cancellationToken);
|
||||||
return Ok(tasks.Select(task =>
|
return Ok(tasks.Select(task =>
|
||||||
{
|
{
|
||||||
@@ -137,6 +138,8 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
|
|||||||
constraint?.LatestPeriod,
|
constraint?.LatestPeriod,
|
||||||
AllowedClassroomIds = constraint?.AllowedClassrooms
|
AllowedClassroomIds = constraint?.AllowedClassrooms
|
||||||
.Select(x => x.ClassroomId) ?? [],
|
.Select(x => x.ClassroomId) ?? [],
|
||||||
|
AllowedExperimentClassroomIds = constraint?.AllowedExperimentClassrooms
|
||||||
|
.Select(x => x.ClassroomId) ?? [],
|
||||||
AllowedExperimentVenueNatures = constraint?.AllowedExperimentVenueNatures ?? 0
|
AllowedExperimentVenueNatures = constraint?.AllowedExperimentVenueNatures ?? 0
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
@@ -168,6 +171,7 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
|
|||||||
{
|
{
|
||||||
var flexibleConstraint = await db.TeachingTaskScheduleConstraints
|
var flexibleConstraint = await db.TeachingTaskScheduleConstraints
|
||||||
.Include(x => x.AllowedClassrooms)
|
.Include(x => x.AllowedClassrooms)
|
||||||
|
.Include(x => x.AllowedExperimentClassrooms)
|
||||||
.FirstOrDefaultAsync(x => x.TeachingTaskId == teachingTaskId, cancellationToken);
|
.FirstOrDefaultAsync(x => x.TeachingTaskId == teachingTaskId, cancellationToken);
|
||||||
if (flexibleConstraint is not null)
|
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))
|
allowedRooms.Any(x => x.Building!.CampusId != request.RequiredCampusId))
|
||||||
return ValidationProblem("指定教室必须位于所选校区。");
|
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
|
var constraint = await db.TeachingTaskScheduleConstraints
|
||||||
.Include(x => x.AllowedClassrooms)
|
.Include(x => x.AllowedClassrooms)
|
||||||
|
.Include(x => x.AllowedExperimentClassrooms)
|
||||||
.FirstOrDefaultAsync(x => x.TeachingTaskId == teachingTaskId, cancellationToken);
|
.FirstOrDefaultAsync(x => x.TeachingTaskId == teachingTaskId, cancellationToken);
|
||||||
if (constraint is null)
|
if (constraint is null)
|
||||||
{
|
{
|
||||||
@@ -230,10 +249,15 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
|
|||||||
constraint.LatestPeriod = request.LatestPeriod;
|
constraint.LatestPeriod = request.LatestPeriod;
|
||||||
constraint.AllowedExperimentVenueNatures = request.AllowedExperimentVenueNatures;
|
constraint.AllowedExperimentVenueNatures = request.AllowedExperimentVenueNatures;
|
||||||
db.TeachingTaskAllowedClassrooms.RemoveRange(constraint.AllowedClassrooms);
|
db.TeachingTaskAllowedClassrooms.RemoveRange(constraint.AllowedClassrooms);
|
||||||
|
db.TeachingTaskAllowedExperimentClassrooms.RemoveRange(constraint.AllowedExperimentClassrooms);
|
||||||
constraint.AllowedClassrooms = request.RequiresClassroom
|
constraint.AllowedClassrooms = request.RequiresClassroom
|
||||||
? request.AllowedClassroomIds.Distinct().Select(classroomId =>
|
? request.AllowedClassroomIds.Distinct().Select(classroomId =>
|
||||||
new TeachingTaskAllowedClassroom { ClassroomId = classroomId }).ToList()
|
new TeachingTaskAllowedClassroom { ClassroomId = classroomId }).ToList()
|
||||||
: [];
|
: [];
|
||||||
|
constraint.AllowedExperimentClassrooms = request.RequiresClassroom
|
||||||
|
? allowedExperimentRoomIds.Select(classroomId =>
|
||||||
|
new TeachingTaskAllowedExperimentClassroom { ClassroomId = classroomId }).ToList()
|
||||||
|
: [];
|
||||||
await db.SaveChangesAsync(cancellationToken);
|
await db.SaveChangesAsync(cancellationToken);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
@@ -259,18 +283,23 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
|
|||||||
!request.RequiresClassroom.HasValue &&
|
!request.RequiresClassroom.HasValue &&
|
||||||
request.AllowedDayOfWeeks is null &&
|
request.AllowedDayOfWeeks is null &&
|
||||||
!request.UpdateClassroomScope &&
|
!request.UpdateClassroomScope &&
|
||||||
|
!request.UpdateExperimentClassroomScope &&
|
||||||
|
!request.AllowedExperimentVenueNatures.HasValue &&
|
||||||
!request.UpdatePeriodRange &&
|
!request.UpdatePeriodRange &&
|
||||||
!request.EarliestPeriod.HasValue &&
|
!request.EarliestPeriod.HasValue &&
|
||||||
!request.LatestPeriod.HasValue)
|
!request.LatestPeriod.HasValue)
|
||||||
return ValidationProblem("请至少选择一项需要批量修改的设置。");
|
return ValidationProblem("请至少选择一项需要批量修改的设置。");
|
||||||
if (request.UpdateClassroomScope && request.RequiresClassroom == false)
|
if (request.UpdateClassroomScope && request.RequiresClassroom == false)
|
||||||
return ValidationProblem("批量指定教室范围时,场地要求不能设置为不占用教室。");
|
return ValidationProblem("批量指定教室范围时,场地要求不能设置为不占用教室。");
|
||||||
|
if (request.UpdateExperimentClassroomScope && request.RequiresClassroom == false)
|
||||||
|
return ValidationProblem("批量指定实验场地时,场地要求不能设置为不占用教室。");
|
||||||
|
|
||||||
var tasks = await db.TeachingTasks
|
var tasks = await db.TeachingTasks
|
||||||
.Where(x =>
|
.Where(x =>
|
||||||
x.AcademicTermId == request.AcademicTermId &&
|
x.AcademicTermId == request.AcademicTermId &&
|
||||||
x.Status == TeachingTaskStatus.Published)
|
x.Status == TeachingTaskStatus.Published)
|
||||||
.WhereIn(taskIds, x => x.Id)
|
.WhereIn(taskIds, x => x.Id)
|
||||||
|
.Include(x => x.Course)
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
if (tasks.Count != taskIds.Length)
|
if (tasks.Count != taskIds.Length)
|
||||||
return ValidationProblem("部分教学任务不存在、未发布或不属于当前学期。");
|
return ValidationProblem("部分教学任务不存在、未发布或不属于当前学期。");
|
||||||
@@ -281,9 +310,15 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
|
|||||||
(request.SchedulingMode ?? task.SchedulingMode) ==
|
(request.SchedulingMode ?? task.SchedulingMode) ==
|
||||||
TeachingTaskSchedulingMode.Flexible))
|
TeachingTaskSchedulingMode.Flexible))
|
||||||
return ConflictProblem("非排时课程不能指定教室,请先将当前筛选结果限定为正常排课课程。");
|
return ConflictProblem("非排时课程不能指定教室,请先将当前筛选结果限定为正常排课课程。");
|
||||||
|
if (request.UpdateExperimentClassroomScope && tasks.Any(task =>
|
||||||
|
(request.SchedulingMode ?? task.SchedulingMode) ==
|
||||||
|
TeachingTaskSchedulingMode.Flexible))
|
||||||
|
return ConflictProblem("非排时课程不能指定实验场地,请先将当前筛选结果限定为正常排课课程。");
|
||||||
|
|
||||||
Building? building = null;
|
Building? building = null;
|
||||||
List<Classroom> allowedRooms = [];
|
List<Classroom> allowedRooms = [];
|
||||||
|
var experimentRoomIds = request.AllowedExperimentClassroomIds?.Distinct().ToArray() ?? [];
|
||||||
|
List<Classroom> allowedExperimentRooms = [];
|
||||||
if (request.UpdateClassroomScope)
|
if (request.UpdateClassroomScope)
|
||||||
{
|
{
|
||||||
if (request.RequiredBuildingId.HasValue)
|
if (request.RequiredBuildingId.HasValue)
|
||||||
@@ -320,10 +355,20 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
|
|||||||
x.Building!.CampusId != request.RequiredCampusId))
|
x.Building!.CampusId != request.RequiredCampusId))
|
||||||
return ValidationProblem("指定教室必须位于所选校区。");
|
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
|
var constraints = await db.TeachingTaskScheduleConstraints
|
||||||
.WhereIn(taskIds, x => x.TeachingTaskId)
|
.WhereIn(taskIds, x => x.TeachingTaskId)
|
||||||
.Include(x => x.AllowedClassrooms)
|
.Include(x => x.AllowedClassrooms)
|
||||||
|
.Include(x => x.AllowedExperimentClassrooms)
|
||||||
.ToDictionaryAsync(x => x.TeachingTaskId, cancellationToken);
|
.ToDictionaryAsync(x => x.TeachingTaskId, cancellationToken);
|
||||||
foreach (var task in tasks)
|
foreach (var task in tasks)
|
||||||
{
|
{
|
||||||
@@ -342,6 +387,8 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
|
|||||||
request.RequiresClassroom.HasValue ||
|
request.RequiresClassroom.HasValue ||
|
||||||
request.AllowedDayOfWeeks is not null ||
|
request.AllowedDayOfWeeks is not null ||
|
||||||
request.UpdateClassroomScope ||
|
request.UpdateClassroomScope ||
|
||||||
|
request.UpdateExperimentClassroomScope ||
|
||||||
|
request.AllowedExperimentVenueNatures.HasValue ||
|
||||||
request.UpdatePeriodRange;
|
request.UpdatePeriodRange;
|
||||||
if (!changesConstraint) continue;
|
if (!changesConstraint) continue;
|
||||||
constraint = new TeachingTaskScheduleConstraint { TeachingTaskId = task.Id };
|
constraint = new TeachingTaskScheduleConstraint { TeachingTaskId = task.Id };
|
||||||
@@ -357,7 +404,10 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
|
|||||||
constraint.RequiredCampusId = null;
|
constraint.RequiredCampusId = null;
|
||||||
constraint.RequiredBuildingId = null;
|
constraint.RequiredBuildingId = null;
|
||||||
db.TeachingTaskAllowedClassrooms.RemoveRange(constraint.AllowedClassrooms);
|
db.TeachingTaskAllowedClassrooms.RemoveRange(constraint.AllowedClassrooms);
|
||||||
|
db.TeachingTaskAllowedExperimentClassrooms.RemoveRange(
|
||||||
|
constraint.AllowedExperimentClassrooms);
|
||||||
constraint.AllowedClassrooms = [];
|
constraint.AllowedClassrooms = [];
|
||||||
|
constraint.AllowedExperimentClassrooms = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (request.AllowedDayOfWeeks is not null)
|
if (request.AllowedDayOfWeeks is not null)
|
||||||
@@ -379,6 +429,15 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
|
|||||||
ClassroomId = room.Id
|
ClassroomId = room.Id
|
||||||
}).ToList();
|
}).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)
|
if (request.UpdatePeriodRange)
|
||||||
{
|
{
|
||||||
constraint.EarliestPeriod = request.EarliestPeriod;
|
constraint.EarliestPeriod = request.EarliestPeriod;
|
||||||
@@ -406,7 +465,9 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
|
|||||||
constraint.EarliestPeriod = null;
|
constraint.EarliestPeriod = null;
|
||||||
constraint.LatestPeriod = null;
|
constraint.LatestPeriod = null;
|
||||||
db.TeachingTaskAllowedClassrooms.RemoveRange(constraint.AllowedClassrooms);
|
db.TeachingTaskAllowedClassrooms.RemoveRange(constraint.AllowedClassrooms);
|
||||||
|
db.TeachingTaskAllowedExperimentClassrooms.RemoveRange(constraint.AllowedExperimentClassrooms);
|
||||||
constraint.AllowedClassrooms = [];
|
constraint.AllowedClassrooms = [];
|
||||||
|
constraint.AllowedExperimentClassrooms = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
private ActionResult ConflictProblem(string detail) =>
|
private ActionResult ConflictProblem(string detail) =>
|
||||||
@@ -441,7 +502,8 @@ public sealed record TeachingTaskScheduleConstraintRequest(
|
|||||||
IReadOnlyList<int> AllowedDayOfWeeks,
|
IReadOnlyList<int> AllowedDayOfWeeks,
|
||||||
[Range(1, 30)] int? EarliestPeriod,
|
[Range(1, 30)] int? EarliestPeriod,
|
||||||
[Range(1, 30)] int? LatestPeriod,
|
[Range(1, 30)] int? LatestPeriod,
|
||||||
TeachingVenueNature AllowedExperimentVenueNatures = 0);
|
TeachingVenueNature AllowedExperimentVenueNatures = 0,
|
||||||
|
IReadOnlyList<Guid>? AllowedExperimentClassroomIds = null);
|
||||||
|
|
||||||
public sealed record TeachingTaskScheduleConstraintBatchRequest(
|
public sealed record TeachingTaskScheduleConstraintBatchRequest(
|
||||||
Guid AcademicTermId,
|
Guid AcademicTermId,
|
||||||
@@ -455,4 +517,7 @@ public sealed record TeachingTaskScheduleConstraintBatchRequest(
|
|||||||
IReadOnlyList<Guid>? AllowedClassroomIds,
|
IReadOnlyList<Guid>? AllowedClassroomIds,
|
||||||
bool UpdatePeriodRange,
|
bool UpdatePeriodRange,
|
||||||
[Range(1, 30)] int? EarliestPeriod,
|
[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()
|
var constraint = await db.TeachingTaskScheduleConstraints.AsNoTracking()
|
||||||
.Include(x => x.AllowedClassrooms)
|
.Include(x => x.AllowedClassrooms)
|
||||||
|
.Include(x => x.AllowedExperimentClassrooms)
|
||||||
.FirstOrDefaultAsync(
|
.FirstOrDefaultAsync(
|
||||||
x => x.TeachingTaskId == request.TeachingTaskId,
|
x => x.TeachingTaskId == request.TeachingTaskId,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
@@ -580,10 +581,6 @@ public sealed class SchedulesController(
|
|||||||
x => x.Id == request.ClassroomId && x.IsEnabled,
|
x => x.Id == request.ClassroomId && x.IsEnabled,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
if (classroom is null) return ValidationProblem("所选教室不存在或已停用。");
|
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 &&
|
if (constraint?.RequiredCampusId is Guid campusId &&
|
||||||
classroom.Building!.CampusId != campusId)
|
classroom.Building!.CampusId != campusId)
|
||||||
return ValidationProblem("所选教室不在该课程指定的校区。");
|
return ValidationProblem("所选教室不在该课程指定的校区。");
|
||||||
@@ -601,6 +598,13 @@ public sealed class SchedulesController(
|
|||||||
allowedNatures != 0 &&
|
allowedNatures != 0 &&
|
||||||
(classroom.TeachingVenueNature & allowedNatures) == 0)
|
(classroom.TeachingVenueNature & allowedNatures) == 0)
|
||||||
return ValidationProblem("所选场地不在该实验课允许的教学场地性质范围内。");
|
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 =>
|
var studentCount = task.Classes.Sum(x =>
|
||||||
x.AdministrativeClass!.Students.Count(student =>
|
x.AdministrativeClass!.Students.Count(student =>
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ public sealed class TeachingTaskScheduleConstraint : EntityBase
|
|||||||
public int? LatestPeriod { get; set; }
|
public int? LatestPeriod { get; set; }
|
||||||
public TeachingVenueNature AllowedExperimentVenueNatures { get; set; }
|
public TeachingVenueNature AllowedExperimentVenueNatures { get; set; }
|
||||||
public ICollection<TeachingTaskAllowedClassroom> AllowedClassrooms { get; set; } = [];
|
public ICollection<TeachingTaskAllowedClassroom> AllowedClassrooms { get; set; } = [];
|
||||||
|
public ICollection<TeachingTaskAllowedExperimentClassroom> AllowedExperimentClassrooms { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class TeachingTaskAllowedClassroom
|
public sealed class TeachingTaskAllowedClassroom
|
||||||
@@ -67,6 +68,14 @@ public sealed class TeachingTaskAllowedClassroom
|
|||||||
public Classroom? Classroom { get; set; }
|
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 sealed class AutomaticScheduleJob : EntityBase
|
||||||
{
|
{
|
||||||
public Guid SchedulePlanId { get; set; }
|
public Guid SchedulePlanId { get; set; }
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
|||||||
Set<TeachingTaskScheduleConstraint>();
|
Set<TeachingTaskScheduleConstraint>();
|
||||||
public DbSet<TeachingTaskAllowedClassroom> TeachingTaskAllowedClassrooms =>
|
public DbSet<TeachingTaskAllowedClassroom> TeachingTaskAllowedClassrooms =>
|
||||||
Set<TeachingTaskAllowedClassroom>();
|
Set<TeachingTaskAllowedClassroom>();
|
||||||
|
public DbSet<TeachingTaskAllowedExperimentClassroom> TeachingTaskAllowedExperimentClassrooms =>
|
||||||
|
Set<TeachingTaskAllowedExperimentClassroom>();
|
||||||
public DbSet<AutomaticScheduleJob> AutomaticScheduleJobs =>
|
public DbSet<AutomaticScheduleJob> AutomaticScheduleJobs =>
|
||||||
Set<AutomaticScheduleJob>();
|
Set<AutomaticScheduleJob>();
|
||||||
public DbSet<SchedulePublishJob> SchedulePublishJobs =>
|
public DbSet<SchedulePublishJob> SchedulePublishJobs =>
|
||||||
@@ -530,6 +532,19 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
|||||||
.OnDelete(DeleteBehavior.Restrict);
|
.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 =>
|
builder.Entity<AutomaticScheduleJob>(entity =>
|
||||||
{
|
{
|
||||||
entity.Property(x => x.ErrorMessage).HasMaxLength(2000);
|
entity.Property(x => x.ErrorMessage).HasMaxLength(2000);
|
||||||
|
|||||||
@@ -92,6 +92,8 @@ public sealed class DevelopmentSqliteMigrator(
|
|||||||
"20260809_48_teaching_task_grade_analytics";
|
"20260809_48_teaching_task_grade_analytics";
|
||||||
private const string SwaggerDocumentationSettingMigration =
|
private const string SwaggerDocumentationSettingMigration =
|
||||||
"20260809_49_swagger_documentation_setting";
|
"20260809_49_swagger_documentation_setting";
|
||||||
|
private const string ExperimentClassroomConstraintsMigration =
|
||||||
|
"20260809_50_experiment_classroom_constraints";
|
||||||
|
|
||||||
public async Task MigrateAsync(CancellationToken cancellationToken = default)
|
public async Task MigrateAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -679,6 +681,14 @@ public sealed class DevelopmentSqliteMigrator(
|
|||||||
SwaggerDocumentationSettingMigration,
|
SwaggerDocumentationSettingMigration,
|
||||||
swaggerSettingsExist ? [] : SwaggerDocumentationSettingStatements,
|
swaggerSettingsExist ? [] : SwaggerDocumentationSettingStatements,
|
||||||
cancellationToken);
|
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(
|
private async Task ApplyMigrationAsync(
|
||||||
@@ -2940,4 +2950,25 @@ public sealed class DevelopmentSqliteMigrator(
|
|||||||
ON "SystemFeatureSettings" ("Key");
|
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");
|
||||||
|
"""
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
+6631
File diff suppressed because it is too large
Load Diff
+52
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
@@ -4102,6 +4102,21 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
|||||||
b.ToTable("TeachingTaskAllowedClassrooms");
|
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 =>
|
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskClass", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("TeachingTaskId")
|
b.Property<Guid>("TeachingTaskId")
|
||||||
@@ -6198,6 +6213,25 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
|||||||
b.Navigation("TeachingTaskScheduleConstraint");
|
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 =>
|
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskClass", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Jiaowu.Api.Domain.Academic.AdministrativeClass", "AdministrativeClass")
|
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 =>
|
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskScheduleConstraint", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("AllowedClassrooms");
|
b.Navigation("AllowedClassrooms");
|
||||||
|
|
||||||
|
b.Navigation("AllowedExperimentClassrooms");
|
||||||
});
|
});
|
||||||
#pragma warning restore 612, 618
|
#pragma warning restore 612, 618
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ public sealed class AutomaticScheduleGenerator(AppDbContext db)
|
|||||||
var constraints = await db.TeachingTaskScheduleConstraints.AsNoTracking()
|
var constraints = await db.TeachingTaskScheduleConstraints.AsNoTracking()
|
||||||
.WhereIn(taskIds, x => x.TeachingTaskId)
|
.WhereIn(taskIds, x => x.TeachingTaskId)
|
||||||
.Include(x => x.AllowedClassrooms)
|
.Include(x => x.AllowedClassrooms)
|
||||||
|
.Include(x => x.AllowedExperimentClassrooms)
|
||||||
.ToDictionaryAsync(x => x.TeachingTaskId, cancellationToken);
|
.ToDictionaryAsync(x => x.TeachingTaskId, cancellationToken);
|
||||||
var classrooms = await db.Classrooms.AsNoTracking()
|
var classrooms = await db.Classrooms.AsNoTracking()
|
||||||
.Where(x => x.IsEnabled)
|
.Where(x => x.IsEnabled)
|
||||||
@@ -279,6 +280,9 @@ public sealed class AutomaticScheduleGenerator(AppDbContext db)
|
|||||||
var allowedRoomIds = constraint?.AllowedClassrooms
|
var allowedRoomIds = constraint?.AllowedClassrooms
|
||||||
.Select(x => x.ClassroomId)
|
.Select(x => x.ClassroomId)
|
||||||
.ToHashSet() ?? [];
|
.ToHashSet() ?? [];
|
||||||
|
var allowedExperimentRoomIds = constraint?.AllowedExperimentClassrooms
|
||||||
|
.Select(x => x.ClassroomId)
|
||||||
|
.ToHashSet() ?? [];
|
||||||
var minimumCapacity = Math.Max(
|
var minimumCapacity = Math.Max(
|
||||||
task.Capacity,
|
task.Capacity,
|
||||||
task.Classes.Sum(x =>
|
task.Classes.Sum(x =>
|
||||||
@@ -291,11 +295,11 @@ public sealed class AutomaticScheduleGenerator(AppDbContext db)
|
|||||||
(constraint?.RequiredBuildingId is not Guid requiredBuildingId ||
|
(constraint?.RequiredBuildingId is not Guid requiredBuildingId ||
|
||||||
room.BuildingId == requiredBuildingId) &&
|
room.BuildingId == requiredBuildingId) &&
|
||||||
(allowedRoomIds.Count == 0 || allowedRoomIds.Contains(room.Id)) &&
|
(allowedRoomIds.Count == 0 || allowedRoomIds.Contains(room.Id)) &&
|
||||||
(kind != ScheduleEntryKind.Experiment ||
|
|
||||||
TeachingVenueNatureRules.SupportsExperiment(room.TeachingVenueNature)) &&
|
|
||||||
(kind != ScheduleEntryKind.Experiment || constraint is null ||
|
(kind != ScheduleEntryKind.Experiment || constraint is null ||
|
||||||
constraint.AllowedExperimentVenueNatures == 0 ||
|
constraint.AllowedExperimentVenueNatures == 0 ||
|
||||||
(room.TeachingVenueNature & constraint.AllowedExperimentVenueNatures) != 0))
|
(room.TeachingVenueNature & constraint.AllowedExperimentVenueNatures) != 0) &&
|
||||||
|
(kind != ScheduleEntryKind.Experiment || allowedExperimentRoomIds.Count == 0 ||
|
||||||
|
allowedExperimentRoomIds.Contains(room.Id)))
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ public sealed class SchedulePlanPublisher(AppDbContext db)
|
|||||||
var constraints = await db.TeachingTaskScheduleConstraints.AsNoTracking()
|
var constraints = await db.TeachingTaskScheduleConstraints.AsNoTracking()
|
||||||
.WhereIn(taskIds, x => x.TeachingTaskId)
|
.WhereIn(taskIds, x => x.TeachingTaskId)
|
||||||
.Include(x => x.AllowedClassrooms)
|
.Include(x => x.AllowedClassrooms)
|
||||||
|
.Include(x => x.AllowedExperimentClassrooms)
|
||||||
.ToDictionaryAsync(x => x.TeachingTaskId, cancellationToken);
|
.ToDictionaryAsync(x => x.TeachingTaskId, cancellationToken);
|
||||||
|
|
||||||
foreach (var entry in plan.Entries)
|
foreach (var entry in plan.Entries)
|
||||||
@@ -270,9 +271,6 @@ public sealed class SchedulePlanPublisher(AppDbContext db)
|
|||||||
{
|
{
|
||||||
if (classroom is null || !classroom.IsEnabled)
|
if (classroom is null || !classroom.IsEnabled)
|
||||||
Fail(entry, "所选教室不存在或已停用");
|
Fail(entry, "所选教室不存在或已停用");
|
||||||
if (entry.Kind == ScheduleEntryKind.Experiment &&
|
|
||||||
!IsExperimentRoom(classroom.RoomType))
|
|
||||||
Fail(entry, $"实验课不能安排在“{classroom.RoomType}”类型的场地");
|
|
||||||
if (constraint?.RequiredCampusId is Guid campusId &&
|
if (constraint?.RequiredCampusId is Guid campusId &&
|
||||||
classroom.Building!.CampusId != campusId)
|
classroom.Building!.CampusId != campusId)
|
||||||
Fail(entry, "所选教室不在指定校区");
|
Fail(entry, "所选教室不在指定校区");
|
||||||
@@ -285,6 +283,18 @@ public sealed class SchedulePlanPublisher(AppDbContext db)
|
|||||||
if (allowedClassroomIds.Count > 0 &&
|
if (allowedClassroomIds.Count > 0 &&
|
||||||
!allowedClassroomIds.Contains(classroom.Id))
|
!allowedClassroomIds.Contains(classroom.Id))
|
||||||
Fail(entry, "所选教室不在指定教室范围内");
|
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 =>
|
var studentCount = task.Classes.Sum(x =>
|
||||||
@@ -305,12 +315,6 @@ public sealed class SchedulePlanPublisher(AppDbContext db)
|
|||||||
.Select(int.Parse)
|
.Select(int.Parse)
|
||||||
.ToHashSet();
|
.ToHashSet();
|
||||||
|
|
||||||
private static bool IsExperimentRoom(string roomType) =>
|
|
||||||
roomType.Contains("实验", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
roomType.Contains("实训", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
roomType.Contains("机房", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
roomType.Contains("语音", StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
[DoesNotReturn]
|
[DoesNotReturn]
|
||||||
private static void Fail(ScheduleEntry entry, string message)
|
private static void Fail(ScheduleEntry entry, string message)
|
||||||
{
|
{
|
||||||
|
|||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<JiaowuBackendVersion>2.3.2-beta.2</JiaowuBackendVersion>
|
<JiaowuBackendVersion>2.3.2-beta.3</JiaowuBackendVersion>
|
||||||
<JiaowuFrontendVersion>2.3.2-beta.2</JiaowuFrontendVersion>
|
<JiaowuFrontendVersion>2.3.2-beta.3</JiaowuFrontendVersion>
|
||||||
<JiaowuSwaggerVersion>2.3.2-beta.2</JiaowuSwaggerVersion>
|
<JiaowuSwaggerVersion>2.3.2-beta.3</JiaowuSwaggerVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -55,8 +55,10 @@ const weekdays = [
|
|||||||
{ value: 7, label: '星期日' },
|
{ value: 7, label: '星期日' },
|
||||||
]
|
]
|
||||||
const experimentVenueNatures = [
|
const experimentVenueNatures = [
|
||||||
{ value: 2, label: '实验室' }, { value: 4, label: '实训室' },
|
{ value: 1, label: '普通教室' }, { value: 2, label: '实验室' },
|
||||||
{ value: 8, label: '计算机机房' }, { value: 16, label: '语音室' },
|
{ value: 4, label: '实训室' }, { value: 8, label: '计算机机房' },
|
||||||
|
{ value: 16, label: '语音室' }, { value: 32, label: '体育场地' },
|
||||||
|
{ value: 64, label: '艺术场地' },
|
||||||
]
|
]
|
||||||
const periods = computed(() => {
|
const periods = computed(() => {
|
||||||
const configured = timeSlots.value
|
const configured = timeSlots.value
|
||||||
@@ -129,8 +131,6 @@ const publishStatusText = computed(() => {
|
|||||||
const selectedTaskConstraint = computed(() =>
|
const selectedTaskConstraint = computed(() =>
|
||||||
constraints.value.find((item) => item.id === entryForm.teachingTaskId),
|
constraints.value.find((item) => item.id === entryForm.teachingTaskId),
|
||||||
)
|
)
|
||||||
const isExperimentRoom = (room: any) =>
|
|
||||||
(Number(room.teachingVenueNature) & (2 | 4 | 8 | 16)) !== 0
|
|
||||||
const entryClassrooms = computed(() =>
|
const entryClassrooms = computed(() =>
|
||||||
classrooms.value.filter((room) =>
|
classrooms.value.filter((room) =>
|
||||||
(!selectedTaskConstraint.value?.requiredCampusId
|
(!selectedTaskConstraint.value?.requiredCampusId
|
||||||
@@ -139,7 +139,9 @@ const entryClassrooms = computed(() =>
|
|||||||
|| room.buildingId === selectedTaskConstraint.value.requiredBuildingId) &&
|
|| room.buildingId === selectedTaskConstraint.value.requiredBuildingId) &&
|
||||||
(!selectedTaskConstraint.value?.allowedClassroomIds?.length
|
(!selectedTaskConstraint.value?.allowedClassroomIds?.length
|
||||||
|| selectedTaskConstraint.value.allowedClassroomIds.includes(room.id)) &&
|
|| selectedTaskConstraint.value.allowedClassroomIds.includes(room.id)) &&
|
||||||
(entryForm.kind !== 'Experiment' || isExperimentRoom(room)),
|
(entryForm.kind !== 'Experiment' ||
|
||||||
|
!selectedTaskConstraint.value?.allowedExperimentClassroomIds?.length ||
|
||||||
|
selectedTaskConstraint.value.allowedExperimentClassroomIds.includes(room.id)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
const entryWeekdays = computed(() => {
|
const entryWeekdays = computed(() => {
|
||||||
@@ -192,6 +194,13 @@ const filteredClassrooms = computed(() =>
|
|||||||
(!constraintForm.requiredBuildingId || item.buildingId === constraintForm.requiredBuildingId),
|
(!constraintForm.requiredBuildingId || item.buildingId === constraintForm.requiredBuildingId),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
const filteredExperimentClassrooms = computed(() =>
|
||||||
|
filteredClassrooms.value.filter((item) =>
|
||||||
|
(!(constraintForm.allowedExperimentVenueNatures ?? []).length ||
|
||||||
|
(Number(item.teachingVenueNature) & (constraintForm.allowedExperimentVenueNatures ?? [])
|
||||||
|
.reduce((value: number, nature: number) => value | nature, 0)) !== 0),
|
||||||
|
),
|
||||||
|
)
|
||||||
const batchFilteredBuildings = computed(() =>
|
const batchFilteredBuildings = computed(() =>
|
||||||
constraintBatchForm.requiredCampusId
|
constraintBatchForm.requiredCampusId
|
||||||
? buildings.value.filter((item) => item.campusId === constraintBatchForm.requiredCampusId)
|
? buildings.value.filter((item) => item.campusId === constraintBatchForm.requiredCampusId)
|
||||||
@@ -205,6 +214,14 @@ const batchFilteredClassrooms = computed(() =>
|
|||||||
item.buildingId === constraintBatchForm.requiredBuildingId),
|
item.buildingId === constraintBatchForm.requiredBuildingId),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
const batchFilteredExperimentClassrooms = computed(() =>
|
||||||
|
batchFilteredClassrooms.value.filter((item) =>
|
||||||
|
(!(constraintBatchForm.allowedExperimentVenueNatures ?? []).length ||
|
||||||
|
(Number(item.teachingVenueNature) &
|
||||||
|
(constraintBatchForm.allowedExperimentVenueNatures ?? [])
|
||||||
|
.reduce((value: number, nature: number) => value | nature, 0)) !== 0),
|
||||||
|
),
|
||||||
|
)
|
||||||
const filteredEntries = computed(() => {
|
const filteredEntries = computed(() => {
|
||||||
const text = keyword.value.trim().toLowerCase()
|
const text = keyword.value.trim().toLowerCase()
|
||||||
if (!text) return selected.value?.entries ?? []
|
if (!text) return selected.value?.entries ?? []
|
||||||
@@ -293,11 +310,13 @@ function openConstraint(item: any) {
|
|||||||
Object.assign(constraintForm, {
|
Object.assign(constraintForm, {
|
||||||
teachingTaskId: item.id,
|
teachingTaskId: item.id,
|
||||||
title: `${item.taskNumber} · ${item.name}`,
|
title: `${item.taskNumber} · ${item.name}`,
|
||||||
|
coursePracticeHours: item.coursePracticeHours,
|
||||||
schedulingMode: item.schedulingMode,
|
schedulingMode: item.schedulingMode,
|
||||||
requiresClassroom: item.requiresClassroom,
|
requiresClassroom: item.requiresClassroom,
|
||||||
requiredCampusId: item.requiredCampusId,
|
requiredCampusId: item.requiredCampusId,
|
||||||
requiredBuildingId: item.requiredBuildingId,
|
requiredBuildingId: item.requiredBuildingId,
|
||||||
allowedClassroomIds: [...item.allowedClassroomIds],
|
allowedClassroomIds: [...item.allowedClassroomIds],
|
||||||
|
allowedExperimentClassroomIds: [...item.allowedExperimentClassroomIds],
|
||||||
allowedExperimentVenueNatures: experimentVenueNatures
|
allowedExperimentVenueNatures: experimentVenueNatures
|
||||||
.filter((nature) => (Number(item.allowedExperimentVenueNatures) & nature.value) !== 0)
|
.filter((nature) => (Number(item.allowedExperimentVenueNatures) & nature.value) !== 0)
|
||||||
.map((nature) => nature.value),
|
.map((nature) => nature.value),
|
||||||
@@ -318,6 +337,7 @@ async function saveConstraint() {
|
|||||||
requiredCampusId: constraintForm.requiredCampusId || null,
|
requiredCampusId: constraintForm.requiredCampusId || null,
|
||||||
requiredBuildingId: constraintForm.requiredBuildingId || null,
|
requiredBuildingId: constraintForm.requiredBuildingId || null,
|
||||||
allowedClassroomIds: constraintForm.allowedClassroomIds ?? [],
|
allowedClassroomIds: constraintForm.allowedClassroomIds ?? [],
|
||||||
|
allowedExperimentClassroomIds: constraintForm.allowedExperimentClassroomIds ?? [],
|
||||||
allowedExperimentVenueNatures: (constraintForm.allowedExperimentVenueNatures ?? [])
|
allowedExperimentVenueNatures: (constraintForm.allowedExperimentVenueNatures ?? [])
|
||||||
.reduce((value: number, nature: number) => value | nature, 0),
|
.reduce((value: number, nature: number) => value | nature, 0),
|
||||||
allowedDayOfWeeks: constraintForm.allowedDayOfWeeks ?? [],
|
allowedDayOfWeeks: constraintForm.allowedDayOfWeeks ?? [],
|
||||||
@@ -358,6 +378,9 @@ function openConstraintBatch() {
|
|||||||
requiredCampusId: undefined,
|
requiredCampusId: undefined,
|
||||||
requiredBuildingId: undefined,
|
requiredBuildingId: undefined,
|
||||||
allowedClassroomIds: [],
|
allowedClassroomIds: [],
|
||||||
|
updateExperimentClassroomScope: false,
|
||||||
|
allowedExperimentVenueNatures: [],
|
||||||
|
allowedExperimentClassroomIds: [],
|
||||||
updateDays: false,
|
updateDays: false,
|
||||||
allowedDayOfWeeks: [1, 2, 3, 4, 5],
|
allowedDayOfWeeks: [1, 2, 3, 4, 5],
|
||||||
updatePeriodRange: false,
|
updatePeriodRange: false,
|
||||||
@@ -371,6 +394,7 @@ async function saveConstraintBatch() {
|
|||||||
if (!constraintBatchForm.updateSchedulingMode &&
|
if (!constraintBatchForm.updateSchedulingMode &&
|
||||||
!constraintBatchForm.updateRequiresClassroom &&
|
!constraintBatchForm.updateRequiresClassroom &&
|
||||||
!constraintBatchForm.updateClassroomScope &&
|
!constraintBatchForm.updateClassroomScope &&
|
||||||
|
!constraintBatchForm.updateExperimentClassroomScope &&
|
||||||
!constraintBatchForm.updateDays &&
|
!constraintBatchForm.updateDays &&
|
||||||
!constraintBatchForm.updatePeriodRange) {
|
!constraintBatchForm.updatePeriodRange) {
|
||||||
ElMessage.warning('请至少勾选一项需要批量修改的设置。')
|
ElMessage.warning('请至少勾选一项需要批量修改的设置。')
|
||||||
@@ -390,6 +414,9 @@ async function saveConstraintBatch() {
|
|||||||
!(constraintBatchForm.updateRequiresClassroom &&
|
!(constraintBatchForm.updateRequiresClassroom &&
|
||||||
!constraintBatchForm.requiresClassroom) &&
|
!constraintBatchForm.requiresClassroom) &&
|
||||||
constraintBatchForm.updateClassroomScope
|
constraintBatchForm.updateClassroomScope
|
||||||
|
const updateExperimentClassroomScope = !flexible &&
|
||||||
|
!(constraintBatchForm.updateRequiresClassroom && !constraintBatchForm.requiresClassroom) &&
|
||||||
|
constraintBatchForm.updateExperimentClassroomScope
|
||||||
const { data } = await http.put('/schedules/constraints/batch', {
|
const { data } = await http.put('/schedules/constraints/batch', {
|
||||||
academicTermId: termId.value,
|
academicTermId: termId.value,
|
||||||
teachingTaskIds: targets.map((item) => item.id),
|
teachingTaskIds: targets.map((item) => item.id),
|
||||||
@@ -409,6 +436,14 @@ async function saveConstraintBatch() {
|
|||||||
allowedClassroomIds: updateClassroomScope
|
allowedClassroomIds: updateClassroomScope
|
||||||
? constraintBatchForm.allowedClassroomIds
|
? constraintBatchForm.allowedClassroomIds
|
||||||
: null,
|
: null,
|
||||||
|
updateExperimentClassroomScope,
|
||||||
|
allowedExperimentVenueNatures: updateExperimentClassroomScope
|
||||||
|
? (constraintBatchForm.allowedExperimentVenueNatures ?? [])
|
||||||
|
.reduce((value: number, nature: number) => value | nature, 0)
|
||||||
|
: null,
|
||||||
|
allowedExperimentClassroomIds: updateExperimentClassroomScope
|
||||||
|
? constraintBatchForm.allowedExperimentClassroomIds
|
||||||
|
: null,
|
||||||
allowedDayOfWeeks: !flexible && constraintBatchForm.updateDays
|
allowedDayOfWeeks: !flexible && constraintBatchForm.updateDays
|
||||||
? constraintBatchForm.allowedDayOfWeeks
|
? constraintBatchForm.allowedDayOfWeeks
|
||||||
: null,
|
: null,
|
||||||
@@ -711,7 +746,8 @@ function changeEntryTask() {
|
|||||||
(task.requiredCampusId && room.campusId !== task.requiredCampusId) ||
|
(task.requiredCampusId && room.campusId !== task.requiredCampusId) ||
|
||||||
(task.requiredBuildingId && room.buildingId !== task.requiredBuildingId) ||
|
(task.requiredBuildingId && room.buildingId !== task.requiredBuildingId) ||
|
||||||
(task.allowedClassroomIds?.length && !task.allowedClassroomIds.includes(room.id)) ||
|
(task.allowedClassroomIds?.length && !task.allowedClassroomIds.includes(room.id)) ||
|
||||||
(entryForm.kind === 'Experiment' && !isExperimentRoom(room))
|
(entryForm.kind === 'Experiment' && task.allowedExperimentClassroomIds?.length &&
|
||||||
|
!task.allowedExperimentClassroomIds.includes(room.id))
|
||||||
)) {
|
)) {
|
||||||
entryForm.classroomId = null
|
entryForm.classroomId = null
|
||||||
}
|
}
|
||||||
@@ -719,7 +755,9 @@ function changeEntryTask() {
|
|||||||
|
|
||||||
function changeEntryKind() {
|
function changeEntryKind() {
|
||||||
const room = classrooms.value.find((item) => item.id === entryForm.classroomId)
|
const room = classrooms.value.find((item) => item.id === entryForm.classroomId)
|
||||||
if (entryForm.kind === 'Experiment' && room && !isExperimentRoom(room)) {
|
const task = selectedTaskConstraint.value
|
||||||
|
if (entryForm.kind === 'Experiment' && room && task?.allowedExperimentClassroomIds?.length &&
|
||||||
|
!task.allowedExperimentClassroomIds.includes(room.id)) {
|
||||||
entryForm.classroomId = null
|
entryForm.classroomId = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1047,7 +1085,7 @@ onBeforeUnmount(() => {
|
|||||||
/>
|
/>
|
||||||
<el-form-item
|
<el-form-item
|
||||||
v-else
|
v-else
|
||||||
:label="entryForm.kind === 'Experiment' ? '实验室 / 实训室 / 机房' : '教室'"
|
:label="entryForm.kind === 'Experiment' ? '教学场地' : '教室'"
|
||||||
required
|
required
|
||||||
:hint="selectedTaskConstraint?.requiredBuildingId ? '仅显示约束范围内教室' : ''"
|
:hint="selectedTaskConstraint?.requiredBuildingId ? '仅显示约束范围内教室' : ''"
|
||||||
>
|
>
|
||||||
@@ -1233,6 +1271,17 @@ onBeforeUnmount(() => {
|
|||||||
</el-checkbox-group>
|
</el-checkbox-group>
|
||||||
<small class="field-hint">仅约束实验课;不勾选时可使用全部实验教学场地。</small>
|
<small class="field-hint">仅约束实验课;不勾选时可使用全部实验教学场地。</small>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item v-if="constraintForm.coursePracticeHours" label="实验课指定可用场地">
|
||||||
|
<el-select v-model="constraintForm.allowedExperimentClassroomIds" multiple filterable collapse-tags>
|
||||||
|
<el-option
|
||||||
|
v-for="item in filteredExperimentClassrooms"
|
||||||
|
:key="item.id"
|
||||||
|
:label="`${item.buildingName} / ${item.name}(${item.roomType},${item.capacity}人)`"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<small class="field-hint">可在上方场地性质范围内指定实验室;不选择时按场地性质自动筛选。</small>
|
||||||
|
</el-form-item>
|
||||||
</template>
|
</template>
|
||||||
<el-form-item label="允许上课日">
|
<el-form-item label="允许上课日">
|
||||||
<el-checkbox-group v-model="constraintForm.allowedDayOfWeeks">
|
<el-checkbox-group v-model="constraintForm.allowedDayOfWeeks">
|
||||||
@@ -1317,6 +1366,32 @@ onBeforeUnmount(() => {
|
|||||||
<el-option v-for="item in batchFilteredBuildings" :key="item.id" :label="item.name" :value="item.id" />
|
<el-option v-for="item in batchFilteredBuildings" :key="item.id" :label="item.name" :value="item.id" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
<el-checkbox v-model="constraintBatchForm.updateExperimentClassroomScope">
|
||||||
|
批量指定实验课场地
|
||||||
|
</el-checkbox>
|
||||||
|
<div v-if="constraintBatchForm.updateExperimentClassroomScope" class="batch-classroom-scope">
|
||||||
|
<el-alert
|
||||||
|
title="仅作用于含实验学时的教学任务;不选择具体场地时,按场地性质自动分配。"
|
||||||
|
type="warning"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
/>
|
||||||
|
<el-form-item label="统一实验课允许的场地性质">
|
||||||
|
<el-checkbox-group v-model="constraintBatchForm.allowedExperimentVenueNatures">
|
||||||
|
<el-checkbox v-for="nature in experimentVenueNatures" :key="nature.value" :value="nature.value">{{ nature.label }}</el-checkbox>
|
||||||
|
</el-checkbox-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="统一实验课指定可用场地">
|
||||||
|
<el-select v-model="constraintBatchForm.allowedExperimentClassroomIds" multiple filterable collapse-tags placeholder="不选择则允许符合性质的任意实验场地">
|
||||||
|
<el-option
|
||||||
|
v-for="item in batchFilteredExperimentClassrooms"
|
||||||
|
:key="item.id"
|
||||||
|
:label="`${item.buildingName} / ${item.name}(${item.roomType},${item.capacity}人)`"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
<el-form-item label="统一指定可用教室">
|
<el-form-item label="统一指定可用教室">
|
||||||
<el-select
|
<el-select
|
||||||
|
|||||||
Reference in New Issue
Block a user