Compare commits
1
Commits
@@ -3,7 +3,6 @@ using Jiaowu.Api.Domain.Academic;
|
||||
using Jiaowu.Api.Domain.Identity;
|
||||
using Jiaowu.Api.Infrastructure.Auth;
|
||||
using Jiaowu.Api.Infrastructure.Persistence;
|
||||
using Jiaowu.Api.Infrastructure.Timetables;
|
||||
using Jiaowu.Api.Infrastructure.Scheduling;
|
||||
using Jiaowu.Api.Infrastructure.Teaching;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@@ -287,8 +286,6 @@ public sealed class CourseAdjustmentsController(
|
||||
|
||||
db.CourseAdjustments.Add(adj);
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
await new PublishedTimetableProjectionService(db)
|
||||
.RebuildPublishedPlansForTaskAsync(adj.TeachingTaskId, cancellationToken);
|
||||
|
||||
if (request.Submit)
|
||||
{
|
||||
|
||||
@@ -1011,11 +1011,7 @@ public sealed class CourseSelectionsController(
|
||||
x.TeachingTask!.Status == TeachingTaskStatus.Published &&
|
||||
(x.IsOpenToAll ||
|
||||
x.TeachingTask.Classes.Any(item =>
|
||||
item.AdministrativeClassId == student.AdministrativeClassId) ||
|
||||
x.Enrollments.Any(item =>
|
||||
item.StudentId == student.Id &&
|
||||
(item.Status == CourseEnrollmentStatus.Enrolled ||
|
||||
item.Status == CourseEnrollmentStatus.Waitlisted))))
|
||||
item.AdministrativeClassId == student.AdministrativeClassId)))
|
||||
.OrderBy(x => x.TeachingTask!.Course!.Code)
|
||||
.Select(x => new StudentOfferingDto(
|
||||
x.Id,
|
||||
|
||||
@@ -105,7 +105,6 @@ 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 =>
|
||||
{
|
||||
@@ -133,15 +132,11 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
|
||||
: constraint?.RequiresClassroom ?? true,
|
||||
constraint?.RequiredCampusId,
|
||||
constraint?.RequiredBuildingId,
|
||||
constraint?.ExperimentRequiredCampusId,
|
||||
constraint?.ExperimentRequiredBuildingId,
|
||||
AllowedDayOfWeeks = ParseDays(constraint?.AllowedDayOfWeeks),
|
||||
constraint?.EarliestPeriod,
|
||||
constraint?.LatestPeriod,
|
||||
AllowedClassroomIds = constraint?.AllowedClassrooms
|
||||
.Select(x => x.ClassroomId) ?? [],
|
||||
AllowedExperimentClassroomIds = constraint?.AllowedExperimentClassrooms
|
||||
.Select(x => x.ClassroomId) ?? [],
|
||||
AllowedExperimentVenueNatures = constraint?.AllowedExperimentVenueNatures ?? 0
|
||||
};
|
||||
}));
|
||||
@@ -173,7 +168,6 @@ 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)
|
||||
{
|
||||
@@ -201,22 +195,6 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
|
||||
cancellationToken))
|
||||
return ValidationProblem("指定校区不存在或已停用。");
|
||||
|
||||
Building? experimentBuilding = null;
|
||||
if (request.ExperimentRequiredBuildingId.HasValue)
|
||||
{
|
||||
experimentBuilding = await db.Buildings.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.Id == request.ExperimentRequiredBuildingId && x.IsEnabled,
|
||||
cancellationToken);
|
||||
if (experimentBuilding is null) return ValidationProblem("指定实验教学楼不存在或已停用。");
|
||||
if (request.ExperimentRequiredCampusId.HasValue &&
|
||||
experimentBuilding.CampusId != request.ExperimentRequiredCampusId)
|
||||
return ValidationProblem("指定实验教学楼不属于所选校区。");
|
||||
}
|
||||
if (request.ExperimentRequiredCampusId.HasValue &&
|
||||
!await db.Campuses.AnyAsync(x => x.Id == request.ExperimentRequiredCampusId && x.IsEnabled,
|
||||
cancellationToken))
|
||||
return ValidationProblem("指定实验校区不存在或已停用。");
|
||||
|
||||
var allowedRooms = await db.Classrooms.AsNoTracking()
|
||||
.Where(x => x.IsEnabled)
|
||||
.WhereIn(request.AllowedClassroomIds, x => x.Id)
|
||||
@@ -230,23 +208,8 @@ 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 (experimentBuilding is not null && allowedExperimentRooms.Any(x => x.BuildingId != experimentBuilding.Id))
|
||||
return ValidationProblem("指定实验场地必须位于所选实验教学楼。");
|
||||
if (request.ExperimentRequiredCampusId.HasValue &&
|
||||
allowedExperimentRooms.Any(x => x.Building!.CampusId != request.ExperimentRequiredCampusId))
|
||||
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)
|
||||
{
|
||||
@@ -260,12 +223,6 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
|
||||
constraint.RequiredBuildingId = request.RequiresClassroom
|
||||
? request.RequiredBuildingId
|
||||
: null;
|
||||
constraint.ExperimentRequiredCampusId = request.RequiresClassroom
|
||||
? request.ExperimentRequiredCampusId
|
||||
: null;
|
||||
constraint.ExperimentRequiredBuildingId = request.RequiresClassroom
|
||||
? request.ExperimentRequiredBuildingId
|
||||
: null;
|
||||
constraint.AllowedDayOfWeeks = request.AllowedDayOfWeeks.Count == 0
|
||||
? null
|
||||
: string.Join(',', request.AllowedDayOfWeeks.Distinct().Order());
|
||||
@@ -273,15 +230,10 @@ 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();
|
||||
}
|
||||
@@ -307,23 +259,18 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
|
||||
!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("部分教学任务不存在、未发布或不属于当前学期。");
|
||||
@@ -334,16 +281,9 @@ 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() ?? [];
|
||||
Building? experimentBuilding = null;
|
||||
List<Classroom> allowedExperimentRooms = [];
|
||||
if (request.UpdateClassroomScope)
|
||||
{
|
||||
if (request.RequiredBuildingId.HasValue)
|
||||
@@ -380,42 +320,10 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
|
||||
x.Building!.CampusId != request.RequiredCampusId))
|
||||
return ValidationProblem("指定教室必须位于所选校区。");
|
||||
}
|
||||
if (request.UpdateExperimentClassroomScope)
|
||||
{
|
||||
if (request.ExperimentRequiredBuildingId.HasValue)
|
||||
{
|
||||
experimentBuilding = await db.Buildings.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.Id == request.ExperimentRequiredBuildingId && x.IsEnabled,
|
||||
cancellationToken);
|
||||
if (experimentBuilding is null)
|
||||
return ValidationProblem("指定实验教学楼不存在或已停用。");
|
||||
if (request.ExperimentRequiredCampusId.HasValue &&
|
||||
experimentBuilding.CampusId != request.ExperimentRequiredCampusId)
|
||||
return ValidationProblem("指定实验教学楼不属于所选实验校区。");
|
||||
}
|
||||
if (request.ExperimentRequiredCampusId.HasValue &&
|
||||
!await db.Campuses.AnyAsync(x => x.Id == request.ExperimentRequiredCampusId && x.IsEnabled,
|
||||
cancellationToken))
|
||||
return ValidationProblem("指定实验校区不存在或已停用。");
|
||||
allowedExperimentRooms = await db.Classrooms.AsNoTracking()
|
||||
.Where(x => x.IsEnabled)
|
||||
.WhereIn(experimentRoomIds, x => x.Id)
|
||||
.Include(x => x.Building)
|
||||
.ToListAsync(cancellationToken);
|
||||
if (allowedExperimentRooms.Count != experimentRoomIds.Length)
|
||||
return ValidationProblem("部分指定实验场地不存在或已停用。");
|
||||
if (experimentBuilding is not null &&
|
||||
allowedExperimentRooms.Any(x => x.BuildingId != experimentBuilding.Id))
|
||||
return ValidationProblem("指定实验场地必须位于所选实验教学楼。");
|
||||
if (request.ExperimentRequiredCampusId.HasValue &&
|
||||
allowedExperimentRooms.Any(x => x.Building!.CampusId != request.ExperimentRequiredCampusId))
|
||||
return ValidationProblem("指定实验场地必须位于所选实验校区。");
|
||||
}
|
||||
|
||||
var constraints = await db.TeachingTaskScheduleConstraints
|
||||
.WhereIn(taskIds, x => x.TeachingTaskId)
|
||||
.Include(x => x.AllowedClassrooms)
|
||||
.Include(x => x.AllowedExperimentClassrooms)
|
||||
.ToDictionaryAsync(x => x.TeachingTaskId, cancellationToken);
|
||||
foreach (var task in tasks)
|
||||
{
|
||||
@@ -434,8 +342,6 @@ 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 };
|
||||
@@ -451,10 +357,7 @@ 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)
|
||||
@@ -476,17 +379,6 @@ 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)
|
||||
{
|
||||
constraint.ExperimentRequiredCampusId = request.ExperimentRequiredCampusId;
|
||||
constraint.ExperimentRequiredBuildingId = request.ExperimentRequiredBuildingId;
|
||||
db.TeachingTaskAllowedExperimentClassrooms.RemoveRange(
|
||||
constraint.AllowedExperimentClassrooms);
|
||||
constraint.AllowedExperimentClassrooms = allowedExperimentRooms.Select(room =>
|
||||
new TeachingTaskAllowedExperimentClassroom { ClassroomId = room.Id }).ToList();
|
||||
}
|
||||
if (request.UpdatePeriodRange)
|
||||
{
|
||||
constraint.EarliestPeriod = request.EarliestPeriod;
|
||||
@@ -510,15 +402,11 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
|
||||
constraint.RequiresClassroom = false;
|
||||
constraint.RequiredCampusId = null;
|
||||
constraint.RequiredBuildingId = null;
|
||||
constraint.ExperimentRequiredCampusId = null;
|
||||
constraint.ExperimentRequiredBuildingId = null;
|
||||
constraint.AllowedDayOfWeeks = null;
|
||||
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) =>
|
||||
@@ -553,10 +441,7 @@ public sealed record TeachingTaskScheduleConstraintRequest(
|
||||
IReadOnlyList<int> AllowedDayOfWeeks,
|
||||
[Range(1, 30)] int? EarliestPeriod,
|
||||
[Range(1, 30)] int? LatestPeriod,
|
||||
TeachingVenueNature AllowedExperimentVenueNatures = 0,
|
||||
IReadOnlyList<Guid>? AllowedExperimentClassroomIds = null,
|
||||
Guid? ExperimentRequiredCampusId = null,
|
||||
Guid? ExperimentRequiredBuildingId = null);
|
||||
TeachingVenueNature AllowedExperimentVenueNatures = 0);
|
||||
|
||||
public sealed record TeachingTaskScheduleConstraintBatchRequest(
|
||||
Guid AcademicTermId,
|
||||
@@ -570,9 +455,4 @@ public sealed record TeachingTaskScheduleConstraintBatchRequest(
|
||||
IReadOnlyList<Guid>? AllowedClassroomIds,
|
||||
bool UpdatePeriodRange,
|
||||
[Range(1, 30)] int? EarliestPeriod,
|
||||
[Range(1, 30)] int? LatestPeriod,
|
||||
bool UpdateExperimentClassroomScope = false,
|
||||
TeachingVenueNature? AllowedExperimentVenueNatures = null,
|
||||
IReadOnlyList<Guid>? AllowedExperimentClassroomIds = null,
|
||||
Guid? ExperimentRequiredCampusId = null,
|
||||
Guid? ExperimentRequiredBuildingId = null);
|
||||
[Range(1, 30)] int? LatestPeriod);
|
||||
|
||||
@@ -552,7 +552,6 @@ 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);
|
||||
@@ -581,26 +580,20 @@ public sealed class SchedulesController(
|
||||
x => x.Id == request.ClassroomId && x.IsEnabled,
|
||||
cancellationToken);
|
||||
if (classroom is null) return ValidationProblem("所选教室不存在或已停用。");
|
||||
if (request.Kind != ScheduleEntryKind.Experiment &&
|
||||
constraint?.RequiredCampusId is Guid campusId &&
|
||||
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("所选教室不在该课程指定的校区。");
|
||||
if (request.Kind != ScheduleEntryKind.Experiment &&
|
||||
constraint?.RequiredBuildingId is Guid buildingId &&
|
||||
if (constraint?.RequiredBuildingId is Guid buildingId &&
|
||||
classroom.BuildingId != buildingId)
|
||||
return ValidationProblem("所选教室不在该课程指定的教学楼。");
|
||||
if (request.Kind == ScheduleEntryKind.Experiment &&
|
||||
constraint?.ExperimentRequiredCampusId is Guid experimentCampusId &&
|
||||
classroom.Building!.CampusId != experimentCampusId)
|
||||
return ValidationProblem("所选场地不在该实验课指定的校区。");
|
||||
if (request.Kind == ScheduleEntryKind.Experiment &&
|
||||
constraint?.ExperimentRequiredBuildingId is Guid experimentBuildingId &&
|
||||
classroom.BuildingId != experimentBuildingId)
|
||||
return ValidationProblem("所选场地不在该实验课指定的教学楼。");
|
||||
var allowedClassroomIds = constraint?.AllowedClassrooms
|
||||
.Select(x => x.ClassroomId)
|
||||
.ToHashSet() ?? [];
|
||||
if (request.Kind != ScheduleEntryKind.Experiment && allowedClassroomIds.Count > 0 &&
|
||||
if (allowedClassroomIds.Count > 0 &&
|
||||
!allowedClassroomIds.Contains(classroom.Id))
|
||||
return ValidationProblem("所选教室不在该课程指定的教室范围内。");
|
||||
if (request.Kind == ScheduleEntryKind.Experiment &&
|
||||
@@ -608,13 +601,6 @@ 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 =>
|
||||
|
||||
@@ -52,16 +52,11 @@ public sealed class TeachingTaskScheduleConstraint : EntityBase
|
||||
public Campus? RequiredCampus { get; set; }
|
||||
public Guid? RequiredBuildingId { get; set; }
|
||||
public Building? RequiredBuilding { get; set; }
|
||||
public Guid? ExperimentRequiredCampusId { get; set; }
|
||||
public Campus? ExperimentRequiredCampus { get; set; }
|
||||
public Guid? ExperimentRequiredBuildingId { get; set; }
|
||||
public Building? ExperimentRequiredBuilding { get; set; }
|
||||
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; } = [];
|
||||
public ICollection<TeachingTaskAllowedExperimentClassroom> AllowedExperimentClassrooms { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class TeachingTaskAllowedClassroom
|
||||
@@ -72,31 +67,6 @@ public sealed class TeachingTaskAllowedClassroom
|
||||
public Classroom? Classroom { get; set; }
|
||||
}
|
||||
|
||||
public sealed class PublishedScheduleOccurrence : EntityBase
|
||||
{
|
||||
public Guid SchedulePlanId { get; set; }
|
||||
public Guid AcademicTermId { get; set; }
|
||||
public Guid ScheduleEntryId { get; set; }
|
||||
public Guid TeachingTaskId { get; set; }
|
||||
public Guid? ClassroomId { get; set; }
|
||||
public int Week { get; set; }
|
||||
public int DayOfWeek { get; set; }
|
||||
public int StartPeriod { get; set; }
|
||||
public int PeriodCount { get; set; }
|
||||
public ScheduleEntryKind Kind { get; set; }
|
||||
public ScheduleEntry? ScheduleEntry { get; set; }
|
||||
public TeachingTask? TeachingTask { 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 Guid SchedulePlanId { get; set; }
|
||||
|
||||
@@ -33,14 +33,11 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
Set<TeacherCourseApplication>();
|
||||
public DbSet<SchedulePlan> SchedulePlans => Set<SchedulePlan>();
|
||||
public DbSet<ScheduleEntry> ScheduleEntries => Set<ScheduleEntry>();
|
||||
public DbSet<PublishedScheduleOccurrence> PublishedScheduleOccurrences => Set<PublishedScheduleOccurrence>();
|
||||
public DbSet<ScheduleTimeSlot> ScheduleTimeSlots => Set<ScheduleTimeSlot>();
|
||||
public DbSet<TeachingTaskScheduleConstraint> TeachingTaskScheduleConstraints =>
|
||||
Set<TeachingTaskScheduleConstraint>();
|
||||
public DbSet<TeachingTaskAllowedClassroom> TeachingTaskAllowedClassrooms =>
|
||||
Set<TeachingTaskAllowedClassroom>();
|
||||
public DbSet<TeachingTaskAllowedExperimentClassroom> TeachingTaskAllowedExperimentClassrooms =>
|
||||
Set<TeachingTaskAllowedExperimentClassroom>();
|
||||
public DbSet<AutomaticScheduleJob> AutomaticScheduleJobs =>
|
||||
Set<AutomaticScheduleJob>();
|
||||
public DbSet<SchedulePublishJob> SchedulePublishJobs =>
|
||||
@@ -514,14 +511,6 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.RequiredBuildingId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
entity.HasOne(x => x.ExperimentRequiredCampus)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.ExperimentRequiredCampusId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
entity.HasOne(x => x.ExperimentRequiredBuilding)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.ExperimentRequiredBuildingId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
builder.Entity<TeachingTaskAllowedClassroom>(entity =>
|
||||
@@ -540,36 +529,6 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
.HasForeignKey(x => x.ClassroomId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
builder.Entity<PublishedScheduleOccurrence>(entity =>
|
||||
{
|
||||
entity.HasIndex(x => new { x.AcademicTermId, x.TeachingTaskId, x.Week });
|
||||
entity.HasIndex(x => new
|
||||
{
|
||||
x.AcademicTermId,
|
||||
x.Week,
|
||||
x.DayOfWeek,
|
||||
x.StartPeriod,
|
||||
x.ClassroomId
|
||||
});
|
||||
entity.HasIndex(x => new { x.SchedulePlanId, x.ClassroomId, x.Week, x.DayOfWeek, x.StartPeriod });
|
||||
entity.HasIndex(x => new { x.ScheduleEntryId, x.Week }).IsUnique();
|
||||
entity.HasOne(x => x.ScheduleEntry).WithMany().HasForeignKey(x => x.ScheduleEntryId).OnDelete(DeleteBehavior.Cascade);
|
||||
entity.HasOne(x => x.TeachingTask).WithMany().HasForeignKey(x => x.TeachingTaskId).OnDelete(DeleteBehavior.Restrict);
|
||||
entity.HasOne(x => x.Classroom).WithMany().HasForeignKey(x => x.ClassroomId).OnDelete(DeleteBehavior.SetNull);
|
||||
});
|
||||
|
||||
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 =>
|
||||
{
|
||||
|
||||
@@ -92,10 +92,6 @@ 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";
|
||||
private const string SeparateExperimentClassroomScopeMigration =
|
||||
"20260809_51_separate_experiment_classroom_scope";
|
||||
|
||||
public async Task MigrateAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -683,24 +679,6 @@ 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);
|
||||
var experimentScopeColumns = (await db.Database.SqlQueryRaw<string>(
|
||||
"SELECT name AS \"Value\" FROM pragma_table_info('TeachingTaskScheduleConstraints')")
|
||||
.ToListAsync(cancellationToken))
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
await ApplyMigrationAsync(
|
||||
SeparateExperimentClassroomScopeMigration,
|
||||
experimentScopeColumns.Contains("ExperimentRequiredCampusId")
|
||||
? []
|
||||
: SeparateExperimentClassroomScopeStatements,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private async Task ApplyMigrationAsync(
|
||||
@@ -2962,45 +2940,4 @@ 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");
|
||||
"""
|
||||
];
|
||||
|
||||
private static readonly string[] SeparateExperimentClassroomScopeStatements =
|
||||
[
|
||||
"""
|
||||
ALTER TABLE "TeachingTaskScheduleConstraints"
|
||||
ADD COLUMN "ExperimentRequiredCampusId" TEXT NULL;
|
||||
""",
|
||||
"""
|
||||
ALTER TABLE "TeachingTaskScheduleConstraints"
|
||||
ADD COLUMN "ExperimentRequiredBuildingId" TEXT NULL;
|
||||
""",
|
||||
"""
|
||||
CREATE INDEX "IX_TeachingTaskScheduleConstraints_ExperimentRequiredCampusId"
|
||||
ON "TeachingTaskScheduleConstraints" ("ExperimentRequiredCampusId");
|
||||
""",
|
||||
"""
|
||||
CREATE INDEX "IX_TeachingTaskScheduleConstraints_ExperimentRequiredBuildingId"
|
||||
ON "TeachingTaskScheduleConstraints" ("ExperimentRequiredBuildingId");
|
||||
"""
|
||||
];
|
||||
}
|
||||
|
||||
-6631
File diff suppressed because it is too large
Load Diff
-52
@@ -1,52 +0,0 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
-6655
File diff suppressed because it is too large
Load Diff
-81
@@ -1,81 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class SeparateExperimentClassroomScope : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "ExperimentRequiredBuildingId",
|
||||
table: "TeachingTaskScheduleConstraints",
|
||||
type: "char(36)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "ExperimentRequiredCampusId",
|
||||
table: "TeachingTaskScheduleConstraints",
|
||||
type: "char(36)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TeachingTaskScheduleConstraints_ExperimentRequiredBuildingId",
|
||||
table: "TeachingTaskScheduleConstraints",
|
||||
column: "ExperimentRequiredBuildingId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TeachingTaskScheduleConstraints_ExperimentRequiredCampusId",
|
||||
table: "TeachingTaskScheduleConstraints",
|
||||
column: "ExperimentRequiredCampusId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_TeachingTaskScheduleConstraints_Buildings_ExperimentRequired~",
|
||||
table: "TeachingTaskScheduleConstraints",
|
||||
column: "ExperimentRequiredBuildingId",
|
||||
principalTable: "Buildings",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_TeachingTaskScheduleConstraints_Campuses_ExperimentRequiredC~",
|
||||
table: "TeachingTaskScheduleConstraints",
|
||||
column: "ExperimentRequiredCampusId",
|
||||
principalTable: "Campuses",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_TeachingTaskScheduleConstraints_Buildings_ExperimentRequired~",
|
||||
table: "TeachingTaskScheduleConstraints");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_TeachingTaskScheduleConstraints_Campuses_ExperimentRequiredC~",
|
||||
table: "TeachingTaskScheduleConstraints");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_TeachingTaskScheduleConstraints_ExperimentRequiredBuildingId",
|
||||
table: "TeachingTaskScheduleConstraints");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_TeachingTaskScheduleConstraints_ExperimentRequiredCampusId",
|
||||
table: "TeachingTaskScheduleConstraints");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ExperimentRequiredBuildingId",
|
||||
table: "TeachingTaskScheduleConstraints");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ExperimentRequiredCampusId",
|
||||
table: "TeachingTaskScheduleConstraints");
|
||||
}
|
||||
}
|
||||
}
|
||||
-6739
File diff suppressed because it is too large
Load Diff
-90
@@ -1,90 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class PublishedTimetableOccurrences : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PublishedScheduleOccurrences",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
SchedulePlanId = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
AcademicTermId = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
ScheduleEntryId = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
TeachingTaskId = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
ClassroomId = table.Column<Guid>(type: "char(36)", nullable: true),
|
||||
Week = table.Column<int>(type: "int", nullable: false),
|
||||
DayOfWeek = table.Column<int>(type: "int", nullable: false),
|
||||
StartPeriod = table.Column<int>(type: "int", nullable: false),
|
||||
PeriodCount = table.Column<int>(type: "int", nullable: false),
|
||||
Kind = table.Column<int>(type: "int", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PublishedScheduleOccurrences", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_PublishedScheduleOccurrences_Classrooms_ClassroomId",
|
||||
column: x => x.ClassroomId,
|
||||
principalTable: "Classrooms",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
table.ForeignKey(
|
||||
name: "FK_PublishedScheduleOccurrences_ScheduleEntries_ScheduleEntryId",
|
||||
column: x => x.ScheduleEntryId,
|
||||
principalTable: "ScheduleEntries",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_PublishedScheduleOccurrences_TeachingTasks_TeachingTaskId",
|
||||
column: x => x.TeachingTaskId,
|
||||
principalTable: "TeachingTasks",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
})
|
||||
.Annotation("MySQL:Charset", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PublishedScheduleOccurrences_AcademicTermId_TeachingTaskId_W~",
|
||||
table: "PublishedScheduleOccurrences",
|
||||
columns: new[] { "AcademicTermId", "TeachingTaskId", "Week" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PublishedScheduleOccurrences_ClassroomId",
|
||||
table: "PublishedScheduleOccurrences",
|
||||
column: "ClassroomId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PublishedScheduleOccurrences_ScheduleEntryId_Week",
|
||||
table: "PublishedScheduleOccurrences",
|
||||
columns: new[] { "ScheduleEntryId", "Week" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PublishedScheduleOccurrences_SchedulePlanId_ClassroomId_Week~",
|
||||
table: "PublishedScheduleOccurrences",
|
||||
columns: new[] { "SchedulePlanId", "ClassroomId", "Week", "DayOfWeek", "StartPeriod" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PublishedScheduleOccurrences_TeachingTaskId",
|
||||
table: "PublishedScheduleOccurrences",
|
||||
column: "TeachingTaskId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "PublishedScheduleOccurrences");
|
||||
}
|
||||
}
|
||||
}
|
||||
-6741
File diff suppressed because it is too large
Load Diff
-27
@@ -1,27 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class OptimizePublishedTimetableOccurrenceLookup : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PublishedScheduleOccurrences_AcademicTermId_Week_DayOfWeek_S~",
|
||||
table: "PublishedScheduleOccurrences",
|
||||
columns: new[] { "AcademicTermId", "Week", "DayOfWeek", "StartPeriod", "ClassroomId" });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_PublishedScheduleOccurrences_AcademicTermId_Week_DayOfWeek_S~",
|
||||
table: "PublishedScheduleOccurrences");
|
||||
}
|
||||
}
|
||||
}
|
||||
-146
@@ -3499,66 +3499,6 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
b.ToTable("OtherExamResults");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.PublishedScheduleOccurrence", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("AcademicTermId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid?>("ClassroomId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<int>("DayOfWeek")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Kind")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("PeriodCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid>("ScheduleEntryId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("SchedulePlanId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<int>("StartPeriod")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid>("TeachingTaskId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<int>("Week")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ClassroomId");
|
||||
|
||||
b.HasIndex("TeachingTaskId");
|
||||
|
||||
b.HasIndex("ScheduleEntryId", "Week")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("AcademicTermId", "TeachingTaskId", "Week");
|
||||
|
||||
b.HasIndex("AcademicTermId", "Week", "DayOfWeek", "StartPeriod", "ClassroomId");
|
||||
|
||||
b.HasIndex("SchedulePlanId", "ClassroomId", "Week", "DayOfWeek", "StartPeriod");
|
||||
|
||||
b.ToTable("PublishedScheduleOccurrences");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ScheduleEntry", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -4162,21 +4102,6 @@ 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")
|
||||
@@ -4332,12 +4257,6 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
b.Property<int?>("EarliestPeriod")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid?>("ExperimentRequiredBuildingId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid?>("ExperimentRequiredCampusId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<int?>("LatestPeriod")
|
||||
.HasColumnType("int");
|
||||
|
||||
@@ -4358,10 +4277,6 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ExperimentRequiredBuildingId");
|
||||
|
||||
b.HasIndex("ExperimentRequiredCampusId");
|
||||
|
||||
b.HasIndex("RequiredBuildingId");
|
||||
|
||||
b.HasIndex("RequiredCampusId");
|
||||
@@ -6106,32 +6021,6 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
b.Navigation("Student");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.PublishedScheduleOccurrence", b =>
|
||||
{
|
||||
b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom")
|
||||
.WithMany()
|
||||
.HasForeignKey("ClassroomId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("Jiaowu.Api.Domain.Academic.ScheduleEntry", "ScheduleEntry")
|
||||
.WithMany()
|
||||
.HasForeignKey("ScheduleEntryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask")
|
||||
.WithMany()
|
||||
.HasForeignKey("TeachingTaskId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Classroom");
|
||||
|
||||
b.Navigation("ScheduleEntry");
|
||||
|
||||
b.Navigation("TeachingTask");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ScheduleEntry", b =>
|
||||
{
|
||||
b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom")
|
||||
@@ -6309,25 +6198,6 @@ 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")
|
||||
@@ -6391,16 +6261,6 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskScheduleConstraint", b =>
|
||||
{
|
||||
b.HasOne("Jiaowu.Api.Domain.Academic.Building", "ExperimentRequiredBuilding")
|
||||
.WithMany()
|
||||
.HasForeignKey("ExperimentRequiredBuildingId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.HasOne("Jiaowu.Api.Domain.Academic.Campus", "ExperimentRequiredCampus")
|
||||
.WithMany()
|
||||
.HasForeignKey("ExperimentRequiredCampusId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.HasOne("Jiaowu.Api.Domain.Academic.Building", "RequiredBuilding")
|
||||
.WithMany()
|
||||
.HasForeignKey("RequiredBuildingId")
|
||||
@@ -6417,10 +6277,6 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ExperimentRequiredBuilding");
|
||||
|
||||
b.Navigation("ExperimentRequiredCampus");
|
||||
|
||||
b.Navigation("RequiredBuilding");
|
||||
|
||||
b.Navigation("RequiredCampus");
|
||||
@@ -6729,8 +6585,6 @@ 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,7 +44,6 @@ 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)
|
||||
@@ -254,15 +253,8 @@ public sealed class AutomaticScheduleGenerator(AppDbContext db)
|
||||
x.TeachingTaskId == task.Id && x.DayOfWeek == day);
|
||||
var dayLoad = entries.Count(x => x.DayOfWeek == day);
|
||||
var roomWaste = room is null ? 0 : Math.Max(0, room.Capacity - task.Capacity);
|
||||
var experimentGeneralClassroomPenalty =
|
||||
kind == ScheduleEntryKind.Experiment &&
|
||||
constraint?.AllowedExperimentVenueNatures == 0 &&
|
||||
room?.TeachingVenueNature == TeachingVenueNature.GeneralClassroom
|
||||
? 100_000
|
||||
: 0;
|
||||
var score = sameTaskDay * 1000 + dayLoad * 10 + start +
|
||||
roomWaste / 10 + startWeek +
|
||||
experimentGeneralClassroomPenalty;
|
||||
roomWaste / 10 + startWeek;
|
||||
candidates.Add((proposed, score));
|
||||
}
|
||||
}
|
||||
@@ -287,9 +279,6 @@ 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 =>
|
||||
@@ -297,25 +286,16 @@ public sealed class AutomaticScheduleGenerator(AppDbContext db)
|
||||
student.Status == StudentStatus.Active) ?? 0));
|
||||
return classrooms.Where(room =>
|
||||
room.Capacity >= minimumCapacity &&
|
||||
(kind == ScheduleEntryKind.Experiment ||
|
||||
constraint?.RequiredCampusId is not Guid requiredCampusId ||
|
||||
(constraint?.RequiredCampusId is not Guid requiredCampusId ||
|
||||
room.Building!.CampusId == requiredCampusId) &&
|
||||
(kind == ScheduleEntryKind.Experiment ||
|
||||
constraint?.RequiredBuildingId is not Guid requiredBuildingId ||
|
||||
(constraint?.RequiredBuildingId is not Guid requiredBuildingId ||
|
||||
room.BuildingId == requiredBuildingId) &&
|
||||
(allowedRoomIds.Count == 0 || allowedRoomIds.Contains(room.Id)) &&
|
||||
(kind != ScheduleEntryKind.Experiment ||
|
||||
constraint?.ExperimentRequiredCampusId is not Guid experimentCampusId ||
|
||||
room.Building!.CampusId == experimentCampusId) &&
|
||||
(kind != ScheduleEntryKind.Experiment ||
|
||||
constraint?.ExperimentRequiredBuildingId is not Guid experimentBuildingId ||
|
||||
room.BuildingId == experimentBuildingId) &&
|
||||
(kind == ScheduleEntryKind.Experiment ||
|
||||
allowedRoomIds.Count == 0 || allowedRoomIds.Contains(room.Id)) &&
|
||||
TeachingVenueNatureRules.SupportsExperiment(room.TeachingVenueNature)) &&
|
||||
(kind != ScheduleEntryKind.Experiment || constraint is null ||
|
||||
constraint.AllowedExperimentVenueNatures == 0 ||
|
||||
(room.TeachingVenueNature & constraint.AllowedExperimentVenueNatures) != 0) &&
|
||||
(kind != ScheduleEntryKind.Experiment || allowedExperimentRoomIds.Count == 0 ||
|
||||
allowedExperimentRoomIds.Contains(room.Id)))
|
||||
(room.TeachingVenueNature & constraint.AllowedExperimentVenueNatures) != 0))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using Jiaowu.Api.Domain.Academic;
|
||||
using Jiaowu.Api.Infrastructure.Caching;
|
||||
using Jiaowu.Api.Infrastructure.Persistence;
|
||||
using Jiaowu.Api.Infrastructure.Timetables;
|
||||
using Jiaowu.Api.Infrastructure.Teaching;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
@@ -74,8 +73,6 @@ public sealed class SchedulePublishJobProcessor(
|
||||
foreach (var oldPlan in previous)
|
||||
oldPlan.Status = SchedulePlanStatus.Archived;
|
||||
|
||||
await new PublishedTimetableProjectionService(db)
|
||||
.RebuildAsync(publishPlan, stoppingToken);
|
||||
publishPlan.Status = SchedulePlanStatus.Published;
|
||||
publishPlan.PublishedAt = DateTime.UtcNow;
|
||||
publishJob.Status = SchedulePublishJobStatus.Succeeded;
|
||||
@@ -170,7 +167,6 @@ 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)
|
||||
@@ -274,40 +270,21 @@ public sealed class SchedulePlanPublisher(AppDbContext db)
|
||||
{
|
||||
if (classroom is null || !classroom.IsEnabled)
|
||||
Fail(entry, "所选教室不存在或已停用");
|
||||
if (entry.Kind != ScheduleEntryKind.Experiment &&
|
||||
constraint?.RequiredCampusId is Guid campusId &&
|
||||
if (entry.Kind == ScheduleEntryKind.Experiment &&
|
||||
!IsExperimentRoom(classroom.RoomType))
|
||||
Fail(entry, $"实验课不能安排在“{classroom.RoomType}”类型的场地");
|
||||
if (constraint?.RequiredCampusId is Guid campusId &&
|
||||
classroom.Building!.CampusId != campusId)
|
||||
Fail(entry, "所选教室不在指定校区");
|
||||
if (entry.Kind != ScheduleEntryKind.Experiment &&
|
||||
constraint?.RequiredBuildingId is Guid buildingId &&
|
||||
if (constraint?.RequiredBuildingId is Guid buildingId &&
|
||||
classroom.BuildingId != buildingId)
|
||||
Fail(entry, "所选教室不在指定教学楼");
|
||||
if (entry.Kind == ScheduleEntryKind.Experiment &&
|
||||
constraint?.ExperimentRequiredCampusId is Guid experimentCampusId &&
|
||||
classroom.Building!.CampusId != experimentCampusId)
|
||||
Fail(entry, "所选场地不在实验课指定校区");
|
||||
if (entry.Kind == ScheduleEntryKind.Experiment &&
|
||||
constraint?.ExperimentRequiredBuildingId is Guid experimentBuildingId &&
|
||||
classroom.BuildingId != experimentBuildingId)
|
||||
Fail(entry, "所选场地不在实验课指定教学楼");
|
||||
var allowedClassroomIds = constraint?.AllowedClassrooms
|
||||
.Select(x => x.ClassroomId)
|
||||
.ToHashSet() ?? [];
|
||||
if (entry.Kind != ScheduleEntryKind.Experiment && allowedClassroomIds.Count > 0 &&
|
||||
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 =>
|
||||
@@ -328,6 +305,12 @@ 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)
|
||||
{
|
||||
|
||||
+19
-20
@@ -17,33 +17,32 @@ public sealed class ClassroomReservationAvailabilityService(AppDbContext db)
|
||||
var occupiedIds = new HashSet<Guid>();
|
||||
var (week, dayOfWeek) = ResolveTeachingWeek(term, reservationDate);
|
||||
|
||||
var hasProjection = await db.PublishedScheduleOccurrences.AsNoTracking()
|
||||
.AnyAsync(entry => entry.AcademicTermId == term.Id, cancellationToken);
|
||||
if (hasProjection)
|
||||
{
|
||||
var projectedRoomIds = await db.PublishedScheduleOccurrences.AsNoTracking()
|
||||
.Where(entry => entry.AcademicTermId == term.Id && entry.Week == week &&
|
||||
entry.DayOfWeek == dayOfWeek && entry.ClassroomId.HasValue &&
|
||||
entry.StartPeriod < startPeriod + periodCount &&
|
||||
startPeriod < entry.StartPeriod + entry.PeriodCount)
|
||||
.Select(entry => entry.ClassroomId!.Value)
|
||||
.ToListAsync(cancellationToken);
|
||||
occupiedIds.UnionWith(projectedRoomIds);
|
||||
}
|
||||
else
|
||||
{
|
||||
var scheduleEntries = await db.ScheduleEntries.AsNoTracking()
|
||||
.Where(entry => entry.ClassroomId.HasValue &&
|
||||
.Where(entry =>
|
||||
entry.ClassroomId.HasValue &&
|
||||
entry.SchedulePlan!.AcademicTermId == term.Id &&
|
||||
entry.SchedulePlan.Status == SchedulePlanStatus.Published &&
|
||||
entry.DayOfWeek == dayOfWeek && entry.StartWeek <= week &&
|
||||
entry.EndWeek >= week && entry.StartPeriod < startPeriod + periodCount &&
|
||||
entry.DayOfWeek == dayOfWeek &&
|
||||
entry.StartWeek <= week &&
|
||||
entry.EndWeek >= week &&
|
||||
entry.StartPeriod < startPeriod + periodCount &&
|
||||
startPeriod < entry.StartPeriod + entry.PeriodCount)
|
||||
.Select(entry => new { entry.ClassroomId, entry.WeekPattern, entry.StartPeriod, entry.PeriodCount })
|
||||
.Select(entry => new
|
||||
{
|
||||
entry.ClassroomId,
|
||||
entry.WeekPattern,
|
||||
entry.StartPeriod,
|
||||
entry.PeriodCount
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
foreach (var entry in scheduleEntries.Where(entry =>
|
||||
FreeClassroomRules.MatchesWeek(entry.WeekPattern, week) &&
|
||||
FreeClassroomRules.PeriodsOverlap(startPeriod, periodCount, entry.StartPeriod, entry.PeriodCount)))
|
||||
FreeClassroomRules.PeriodsOverlap(
|
||||
startPeriod,
|
||||
periodCount,
|
||||
entry.StartPeriod,
|
||||
entry.PeriodCount)))
|
||||
{
|
||||
occupiedIds.Add(entry.ClassroomId!.Value);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
using Jiaowu.Api.Domain.Academic;
|
||||
using Jiaowu.Api.Infrastructure.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Jiaowu.Api.Infrastructure.Timetables;
|
||||
|
||||
public sealed class PublishedTimetableProjectionService(AppDbContext db)
|
||||
{
|
||||
private const int WriteBatchSize = 2_000;
|
||||
public async Task RebuildPublishedPlansForTaskAsync(Guid teachingTaskId, CancellationToken cancellationToken)
|
||||
{
|
||||
var plans = await db.SchedulePlans
|
||||
.Where(plan => plan.Status == SchedulePlanStatus.Published &&
|
||||
plan.Entries.Any(entry => entry.TeachingTaskId == teachingTaskId))
|
||||
.Include(plan => plan.Entries)
|
||||
.ToListAsync(cancellationToken);
|
||||
foreach (var plan in plans)
|
||||
await RebuildAsync(plan, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task RebuildAsync(SchedulePlan plan, CancellationToken cancellationToken)
|
||||
{
|
||||
await db.PublishedScheduleOccurrences
|
||||
.Where(x => x.SchedulePlanId == plan.Id)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
|
||||
var rows = new List<PublishedScheduleOccurrence>(WriteBatchSize);
|
||||
foreach (var entry in plan.Entries)
|
||||
for (var week = entry.StartWeek; week <= entry.EndWeek; week++)
|
||||
{
|
||||
if (entry.WeekPattern == WeekPattern.Odd && week % 2 == 0 ||
|
||||
entry.WeekPattern == WeekPattern.Even && week % 2 != 0) continue;
|
||||
rows.Add(new PublishedScheduleOccurrence
|
||||
{
|
||||
SchedulePlanId = plan.Id,
|
||||
AcademicTermId = plan.AcademicTermId,
|
||||
ScheduleEntryId = entry.Id,
|
||||
TeachingTaskId = entry.TeachingTaskId,
|
||||
ClassroomId = entry.ClassroomId,
|
||||
Week = week,
|
||||
DayOfWeek = entry.DayOfWeek,
|
||||
StartPeriod = entry.StartPeriod,
|
||||
PeriodCount = entry.PeriodCount,
|
||||
Kind = entry.Kind
|
||||
});
|
||||
if (rows.Count == WriteBatchSize)
|
||||
await WriteBatchAsync(rows, cancellationToken);
|
||||
}
|
||||
if (rows.Count > 0)
|
||||
await WriteBatchAsync(rows, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task WriteBatchAsync(
|
||||
List<PublishedScheduleOccurrence> rows,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
db.PublishedScheduleOccurrences.AddRange(rows);
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
foreach (var row in rows)
|
||||
db.Entry(row).State = EntityState.Detached;
|
||||
rows.Clear();
|
||||
}
|
||||
}
|
||||
@@ -422,7 +422,6 @@ builder.Services.AddScoped<DatabaseInitializer>();
|
||||
builder.Services.AddScoped<DemoDataSeeder>();
|
||||
builder.Services.AddScoped<DevelopmentSqliteMigrator>();
|
||||
builder.Services.AddScoped<TimetableDataService>();
|
||||
builder.Services.AddScoped<PublishedTimetableProjectionService>();
|
||||
builder.Services.AddScoped<AutomaticScheduleGenerator>();
|
||||
builder.Services.AddScoped<PersonalCalendarService>();
|
||||
builder.Services.AddScoped<ClassroomReservationAvailabilityService>();
|
||||
|
||||
@@ -205,79 +205,6 @@ public sealed class CourseSelectionsControllerTests
|
||||
x.Grade == 2026));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Student_options_include_an_administrator_assigned_offering_outside_the_students_class_scope()
|
||||
{
|
||||
await using var connection = new SqliteConnection("Data Source=:memory:");
|
||||
await connection.OpenAsync();
|
||||
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||
.UseSqlite(connection)
|
||||
.Options;
|
||||
await using var db = new AppDbContext(options);
|
||||
await db.Database.EnsureCreatedAsync();
|
||||
var data = await SeedFullOfferingAsync(db);
|
||||
|
||||
var originalClass = await db.AdministrativeClasses.SingleAsync();
|
||||
var otherClass = new AdministrativeClass
|
||||
{
|
||||
Code = "CS2026-02",
|
||||
Name = "计科 2026-2 班",
|
||||
MajorId = originalClass.MajorId,
|
||||
Grade = 2026
|
||||
};
|
||||
var taskId = await db.CourseSelectionOfferings
|
||||
.Where(x => x.Id == data.OfferingId)
|
||||
.Select(x => x.TeachingTaskId)
|
||||
.SingleAsync();
|
||||
var task = await db.TeachingTasks
|
||||
.Include(x => x.Classes)
|
||||
.SingleAsync(x => x.Id == taskId);
|
||||
task.Classes.Clear();
|
||||
task.Classes.Add(new TeachingTaskClass { AdministrativeClassId = otherClass.Id });
|
||||
task.SchedulingMode = TeachingTaskSchedulingMode.Standard;
|
||||
var termId = await db.CourseSelectionRounds
|
||||
.Where(x => x.Id == data.RoundId)
|
||||
.Select(x => x.AcademicTermId)
|
||||
.SingleAsync();
|
||||
var plan = new SchedulePlan
|
||||
{
|
||||
AcademicTermId = termId,
|
||||
Name = "正式课表",
|
||||
Version = "V1",
|
||||
Status = SchedulePlanStatus.Published,
|
||||
PublishedAt = DateTime.UtcNow
|
||||
};
|
||||
db.AddRange(otherClass, plan);
|
||||
await db.SaveChangesAsync();
|
||||
db.ScheduleEntries.Add(new ScheduleEntry
|
||||
{
|
||||
SchedulePlanId = plan.Id,
|
||||
TeachingTaskId = task.Id,
|
||||
DayOfWeek = 1,
|
||||
StartPeriod = 1,
|
||||
PeriodCount = 2,
|
||||
StartWeek = 1,
|
||||
EndWeek = 16,
|
||||
WeekPattern = WeekPattern.All
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
db.ChangeTracker.Clear();
|
||||
|
||||
var controller = new CourseSelectionsController(
|
||||
db,
|
||||
new StudentDataScope(data.EnrolledUserId));
|
||||
var result = Assert.IsType<OkObjectResult>(await controller.GetStudentOptions(
|
||||
data.RoundId,
|
||||
CancellationToken.None));
|
||||
var offerings = ReadProperty<IEnumerable<StudentOfferingDto>>(
|
||||
result.Value, "Offerings");
|
||||
var assignedOffering = Assert.Single(offerings);
|
||||
|
||||
Assert.Equal(data.OfferingId, assignedOffering.Id);
|
||||
Assert.Equal(CourseEnrollmentStatus.Enrolled, assignedOffering.EnrollmentStatus);
|
||||
Assert.Single(assignedOffering.Schedules);
|
||||
}
|
||||
|
||||
private static async Task<SeededSelection> SeedFullOfferingAsync(AppDbContext db)
|
||||
{
|
||||
var college = new College { Code = "CS", Name = "计算机学院" };
|
||||
@@ -431,14 +358,6 @@ public sealed class CourseSelectionsControllerTests
|
||||
return Assert.IsType<int>(property.GetValue(value));
|
||||
}
|
||||
|
||||
private static T ReadProperty<T>(object? value, string propertyName)
|
||||
{
|
||||
Assert.NotNull(value);
|
||||
var property = value.GetType().GetProperty(propertyName);
|
||||
Assert.NotNull(property);
|
||||
return Assert.IsAssignableFrom<T>(property.GetValue(value));
|
||||
}
|
||||
|
||||
private static Student CreateStudent(
|
||||
string number,
|
||||
string name,
|
||||
|
||||
@@ -85,19 +85,13 @@ public sealed class ScheduleSettingsControllerTests
|
||||
[classroom.Id],
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
UpdateExperimentClassroomScope: true,
|
||||
AllowedExperimentVenueNatures: TeachingVenueNature.Laboratory,
|
||||
AllowedExperimentClassroomIds: [classroom.Id],
|
||||
ExperimentRequiredCampusId: campus.Id,
|
||||
ExperimentRequiredBuildingId: building.Id),
|
||||
null),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.IsType<OkObjectResult>(result);
|
||||
db.ChangeTracker.Clear();
|
||||
var constraints = await db.TeachingTaskScheduleConstraints
|
||||
.Include(item => item.AllowedClassrooms)
|
||||
.Include(item => item.AllowedExperimentClassrooms)
|
||||
.OrderBy(item => item.TeachingTaskId)
|
||||
.ToListAsync();
|
||||
Assert.Equal(2, constraints.Count);
|
||||
@@ -109,11 +103,6 @@ public sealed class ScheduleSettingsControllerTests
|
||||
Assert.Equal(
|
||||
classroom.Id,
|
||||
Assert.Single(constraint.AllowedClassrooms).ClassroomId);
|
||||
Assert.Equal(campus.Id, constraint.ExperimentRequiredCampusId);
|
||||
Assert.Equal(building.Id, constraint.ExperimentRequiredBuildingId);
|
||||
Assert.Equal(
|
||||
classroom.Id,
|
||||
Assert.Single(constraint.AllowedExperimentClassrooms).ClassroomId);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<JiaowuBackendVersion>2.3.2-beta.4</JiaowuBackendVersion>
|
||||
<JiaowuFrontendVersion>2.3.2-beta.4</JiaowuFrontendVersion>
|
||||
<JiaowuSwaggerVersion>2.3.2-beta.4</JiaowuSwaggerVersion>
|
||||
<JiaowuBackendVersion>2.3.2-beta.2</JiaowuBackendVersion>
|
||||
<JiaowuFrontendVersion>2.3.2-beta.2</JiaowuFrontendVersion>
|
||||
<JiaowuSwaggerVersion>2.3.2-beta.2</JiaowuSwaggerVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -431,8 +431,6 @@ button { cursor: pointer; }
|
||||
.module-toolbar b, .module-toolbar span { display: block; }
|
||||
.module-toolbar b { font-size: 15px; }
|
||||
.module-toolbar span { margin-top: 4px; color: var(--muted); font-size: 10px; }
|
||||
.curriculum-view-actions { display: flex; align-items: center; gap: 10px; }
|
||||
.curriculum-view-actions .el-button + .el-button { margin-left: 0; }
|
||||
.curriculum-module { margin-top: 12px; border: 1px solid var(--line); }
|
||||
.curriculum-module > header { min-height: 72px; padding: 13px 16px; display: flex; align-items: center; gap: 18px; background: #f8fafb; border-bottom: 1px solid var(--line); }
|
||||
.curriculum-module > header > div:first-child { min-width: 160px; }
|
||||
@@ -441,24 +439,6 @@ button { cursor: pointer; }
|
||||
.curriculum-module > header p { margin: 0 auto 0 0; color: var(--muted); font-size: 11px; }
|
||||
.curriculum-module > header > div:last-child { display: flex; align-items: center; white-space: nowrap; }
|
||||
.curriculum-course-table { min-height: 80px; }
|
||||
.semester-view { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
|
||||
.semester-card { min-width: 0; border: 1px solid var(--line); background: #fff; }
|
||||
.semester-card > header { min-height: 70px; padding: 14px 16px; display: flex; align-items: center; justify-content: space-between; background: linear-gradient(135deg, #f4faf9, #f8fafb); border-bottom: 1px solid var(--line); }
|
||||
.semester-card > header span { color: var(--teal); font: 700 9px/1 Consolas, monospace; letter-spacing: .12em; }
|
||||
.semester-card h4 { margin: 7px 0 0; font-size: 15px; }
|
||||
.semester-card > header > b { color: #1d6b68; font: 700 18px/1 Consolas, monospace; }
|
||||
.semester-summary { display: flex; gap: 14px; padding: 9px 16px; color: var(--muted); font-size: 11px; border-bottom: 1px solid var(--line); }
|
||||
.semester-summary span + span { padding-left: 14px; border-left: 1px solid var(--line); }
|
||||
.semester-courses { margin: 0; padding: 0; list-style: none; }
|
||||
.semester-courses li { min-height: 68px; padding: 11px 16px; display: flex; align-items: center; justify-content: space-between; gap: 12px; border-bottom: 1px solid #edf0f2; }
|
||||
.semester-courses li:last-child { border-bottom: none; }
|
||||
.semester-courses li > div { min-width: 0; display: grid; gap: 3px; }
|
||||
.semester-courses li span { color: var(--teal); font-size: 10px; }
|
||||
.semester-courses li b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; }
|
||||
.semester-courses li small { color: var(--muted); font-size: 10px; }
|
||||
.semester-courses li > strong { flex: none; font: 700 15px/1 Consolas, monospace; }
|
||||
.semester-courses li > strong small { margin-left: 3px; font: 400 9px/1 inherit; }
|
||||
.semester-empty { margin: 0; padding: 25px 16px; color: var(--muted); font-size: 11px; text-align: center; }
|
||||
.task-summary { min-height: 82px; padding: 15px 22px; display: flex; align-items: center; gap: 28px; color: white; background: linear-gradient(108deg, #17295a, #263f80); }
|
||||
.task-summary > div { min-width: 170px; display: flex; align-items: baseline; gap: 8px; }
|
||||
.task-summary span, .task-summary small { color: #b8c1de; font-size: 10px; }
|
||||
@@ -532,8 +512,6 @@ button { cursor: pointer; }
|
||||
.constraint-batch-form { margin-top: 16px; display: grid; gap: 10px; }
|
||||
.constraint-batch-form > .el-checkbox { padding: 8px 10px; background: #f7f9fb; border-left: 3px solid #ccd8e1; }
|
||||
.batch-classroom-scope { padding: 12px 14px 2px; display: grid; gap: 12px; border: 1px solid #d8e2e8; background: #fbfcfd; }
|
||||
.experiment-classroom-scope { margin-bottom: 18px; padding: 12px 14px 2px; border: 1px solid #d8e2e8; background: #fbfcfd; }
|
||||
.experiment-classroom-scope__title { margin-bottom: 12px; color: #34435e; font-size: 13px; font-weight: 650; }
|
||||
.constraint-list article { min-width: 0; padding: 13px 15px; display: grid; grid-template-columns: minmax(230px, 1fr) minmax(170px, auto) auto; align-items: center; gap: 14px; border: 1px solid var(--line); background: #fff; }
|
||||
.constraint-list article > div:first-child { min-width: 0; display: grid; gap: 4px; }
|
||||
.constraint-list article span { color: var(--teal); font: 700 9px/1.2 Consolas, monospace; }
|
||||
@@ -1300,9 +1278,6 @@ button { cursor: pointer; }
|
||||
.plan-metrics > div:nth-child(-n+2) { border-bottom: 1px solid var(--line); }
|
||||
.curriculum-module > header { align-items: flex-start; flex-wrap: wrap; }
|
||||
.curriculum-module > header p { order: 3; width: 100%; }
|
||||
.module-toolbar { align-items: flex-start; flex-direction: column; gap: 12px; }
|
||||
.curriculum-view-actions { width: 100%; justify-content: space-between; }
|
||||
.semester-view { grid-template-columns: 1fr; }
|
||||
.task-summary { align-items: flex-start; flex-direction: column; gap: 12px; }
|
||||
.task-summary > div { width: 100%; }
|
||||
.task-summary p { padding: 12px 0 0; border-left: none; border-top: 1px solid rgba(255,255,255,.16); line-height: 1.6; }
|
||||
|
||||
@@ -18,7 +18,6 @@ const planDialog = ref(false)
|
||||
const moduleDialog = ref(false)
|
||||
const courseDialog = ref(false)
|
||||
const cloneDialog = ref(false)
|
||||
const detailView = ref<'structure' | 'semester'>('structure')
|
||||
const editingPlanId = ref('')
|
||||
const editingPlanStatus = ref('')
|
||||
const editingModuleId = ref('')
|
||||
@@ -75,34 +74,6 @@ const configuredCredits = computed(() =>
|
||||
0,
|
||||
) ?? 0,
|
||||
)
|
||||
const semesterGroups = computed(() => {
|
||||
if (!selected.value) return []
|
||||
|
||||
const courses = selected.value.modules.flatMap((module: any) =>
|
||||
module.courses.map((course: any) => ({
|
||||
...course,
|
||||
moduleCode: module.code,
|
||||
moduleName: module.name,
|
||||
})),
|
||||
)
|
||||
const semesterCount = Number(selected.value.schoolingYears) * 2
|
||||
|
||||
return Array.from({ length: semesterCount }, (_, index) => {
|
||||
const semester = index + 1
|
||||
const items = courses
|
||||
.filter((course: any) => Number(course.recommendedSemester) === semester)
|
||||
.sort((first: any, second: any) =>
|
||||
first.moduleCode.localeCompare(second.moduleCode) ||
|
||||
first.courseCode.localeCompare(second.courseCode),
|
||||
)
|
||||
return {
|
||||
semester,
|
||||
courses: items,
|
||||
credits: items.reduce((sum: number, course: any) => sum + Number(course.credits), 0),
|
||||
totalHours: items.reduce((sum: number, course: any) => sum + Number(course.totalHours), 0),
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
async function loadPlans(keepSelection = true) {
|
||||
loading.value = true
|
||||
@@ -479,21 +450,12 @@ onMounted(async () => {
|
||||
|
||||
<div class="module-toolbar">
|
||||
<div>
|
||||
<b>{{ detailView === 'structure' ? '课程结构' : '学期视图' }}</b>
|
||||
<span>{{ detailView === 'structure'
|
||||
? '指定必修须逐门通过;英语、体育等多选课程用“组内选修”,修满模块最低学分即可'
|
||||
: '按建议修读学期展示课程安排,包含每学期的课程数量、学分和学时。' }}</span>
|
||||
</div>
|
||||
<div class="curriculum-view-actions">
|
||||
<el-radio-group v-model="detailView" size="small" aria-label="培养方案展示方式">
|
||||
<el-radio-button value="structure">课程结构</el-radio-button>
|
||||
<el-radio-button value="semester">学期视图</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-button v-if="canEdit && detailView === 'structure'" :icon="Plus" @click="openModule()">新增模块</el-button>
|
||||
<b>课程结构</b>
|
||||
<span>指定必修须逐门通过;英语、体育等多选课程用“组内选修”,修满模块最低学分即可</span>
|
||||
</div>
|
||||
<el-button v-if="canEdit" :icon="Plus" @click="openModule()">新增模块</el-button>
|
||||
</div>
|
||||
|
||||
<template v-if="detailView === 'structure'">
|
||||
<section v-for="module in selected.modules" :key="module.id" class="curriculum-module">
|
||||
<header>
|
||||
<div>
|
||||
@@ -539,34 +501,6 @@ onMounted(async () => {
|
||||
</section>
|
||||
<el-empty v-if="selected.modules.length === 0" description="先建立课程模块,再添加课程" />
|
||||
</template>
|
||||
|
||||
<section v-else class="semester-view" aria-label="课程学期安排">
|
||||
<article v-for="group in semesterGroups" :key="group.semester" class="semester-card">
|
||||
<header>
|
||||
<div>
|
||||
<span>SEMESTER {{ String(group.semester).padStart(2, '0') }}</span>
|
||||
<h4>第 {{ group.semester }} 学期</h4>
|
||||
</div>
|
||||
<b>{{ group.courses.length }} 门</b>
|
||||
</header>
|
||||
<div class="semester-summary">
|
||||
<span>{{ group.credits }} 学分</span>
|
||||
<span>{{ group.totalHours }} 学时</span>
|
||||
</div>
|
||||
<ul v-if="group.courses.length" class="semester-courses">
|
||||
<li v-for="course in group.courses" :key="course.id">
|
||||
<div>
|
||||
<span>{{ course.moduleName }}</span>
|
||||
<b>{{ course.courseName }}</b>
|
||||
<small>{{ course.courseCode }} · {{ courseTypeLabels[course.type] }}</small>
|
||||
</div>
|
||||
<strong>{{ course.credits }}<small>学分</small></strong>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else class="semester-empty">本学期暂未安排课程</p>
|
||||
</article>
|
||||
</section>
|
||||
</template>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
|
||||
+14
-169
@@ -55,20 +55,9 @@ const weekdays = [
|
||||
{ value: 7, label: '星期日' },
|
||||
]
|
||||
const experimentVenueNatures = [
|
||||
{ value: 1, label: '普通教室' }, { value: 2, label: '实验室' },
|
||||
{ value: 4, label: '实训室' }, { value: 8, label: '计算机机房' },
|
||||
{ value: 16, label: '语音室' }, { value: 32, label: '体育场地' },
|
||||
{ value: 64, label: '艺术场地' },
|
||||
{ value: 2, label: '实验室' }, { value: 4, label: '实训室' },
|
||||
{ value: 8, label: '计算机机房' }, { value: 16, label: '语音室' },
|
||||
]
|
||||
const venueNatureValue = (value: unknown) => {
|
||||
if (typeof value === 'number') return value
|
||||
if (typeof value !== 'string') return 0
|
||||
const names: Record<string, number> = {
|
||||
GeneralClassroom: 1, Laboratory: 2, TrainingRoom: 4, ComputerLab: 8,
|
||||
LanguageLab: 16, SportsVenue: 32, ArtsVenue: 64,
|
||||
}
|
||||
return value.split(',').reduce((sum, name) => sum | (names[name.trim()] ?? 0), 0)
|
||||
}
|
||||
const periods = computed(() => {
|
||||
const configured = timeSlots.value
|
||||
.filter((item) => item.isEnabled)
|
||||
@@ -140,21 +129,17 @@ const publishStatusText = computed(() => {
|
||||
const selectedTaskConstraint = computed(() =>
|
||||
constraints.value.find((item) => item.id === entryForm.teachingTaskId),
|
||||
)
|
||||
const isExperimentRoom = (room: any) =>
|
||||
(Number(room.teachingVenueNature) & (2 | 4 | 8 | 16)) !== 0
|
||||
const entryClassrooms = computed(() =>
|
||||
classrooms.value.filter((room) =>
|
||||
(entryForm.kind === 'Experiment' || !selectedTaskConstraint.value?.requiredCampusId
|
||||
(!selectedTaskConstraint.value?.requiredCampusId
|
||||
|| room.campusId === selectedTaskConstraint.value.requiredCampusId) &&
|
||||
(entryForm.kind === 'Experiment' || !selectedTaskConstraint.value?.requiredBuildingId
|
||||
(!selectedTaskConstraint.value?.requiredBuildingId
|
||||
|| room.buildingId === selectedTaskConstraint.value.requiredBuildingId) &&
|
||||
(entryForm.kind === 'Experiment' || !selectedTaskConstraint.value?.allowedClassroomIds?.length
|
||||
(!selectedTaskConstraint.value?.allowedClassroomIds?.length
|
||||
|| selectedTaskConstraint.value.allowedClassroomIds.includes(room.id)) &&
|
||||
(entryForm.kind !== 'Experiment' || !selectedTaskConstraint.value?.experimentRequiredCampusId
|
||||
|| room.campusId === selectedTaskConstraint.value.experimentRequiredCampusId) &&
|
||||
(entryForm.kind !== 'Experiment' || !selectedTaskConstraint.value?.experimentRequiredBuildingId
|
||||
|| room.buildingId === selectedTaskConstraint.value.experimentRequiredBuildingId) &&
|
||||
(entryForm.kind !== 'Experiment' ||
|
||||
!selectedTaskConstraint.value?.allowedExperimentClassroomIds?.length ||
|
||||
selectedTaskConstraint.value.allowedExperimentClassroomIds.includes(room.id)),
|
||||
(entryForm.kind !== 'Experiment' || isExperimentRoom(room)),
|
||||
),
|
||||
)
|
||||
const entryWeekdays = computed(() => {
|
||||
@@ -207,22 +192,6 @@ const filteredClassrooms = computed(() =>
|
||||
(!constraintForm.requiredBuildingId || item.buildingId === constraintForm.requiredBuildingId),
|
||||
),
|
||||
)
|
||||
const filteredExperimentBuildings = computed(() =>
|
||||
constraintForm.experimentRequiredCampusId
|
||||
? buildings.value.filter((item) => item.campusId === constraintForm.experimentRequiredCampusId)
|
||||
: buildings.value,
|
||||
)
|
||||
const filteredExperimentClassrooms = computed(() =>
|
||||
classrooms.value.filter((item) =>
|
||||
(!constraintForm.experimentRequiredCampusId ||
|
||||
item.campusId === constraintForm.experimentRequiredCampusId) &&
|
||||
(!constraintForm.experimentRequiredBuildingId ||
|
||||
item.buildingId === constraintForm.experimentRequiredBuildingId) &&
|
||||
(!(constraintForm.allowedExperimentVenueNatures ?? []).length ||
|
||||
(venueNatureValue(item.teachingVenueNature) & (constraintForm.allowedExperimentVenueNatures ?? [])
|
||||
.reduce((value: number, nature: number) => value | nature, 0)) !== 0),
|
||||
),
|
||||
)
|
||||
const batchFilteredBuildings = computed(() =>
|
||||
constraintBatchForm.requiredCampusId
|
||||
? buildings.value.filter((item) => item.campusId === constraintBatchForm.requiredCampusId)
|
||||
@@ -236,23 +205,6 @@ const batchFilteredClassrooms = computed(() =>
|
||||
item.buildingId === constraintBatchForm.requiredBuildingId),
|
||||
),
|
||||
)
|
||||
const batchFilteredExperimentBuildings = computed(() =>
|
||||
constraintBatchForm.experimentRequiredCampusId
|
||||
? buildings.value.filter((item) => item.campusId === constraintBatchForm.experimentRequiredCampusId)
|
||||
: buildings.value,
|
||||
)
|
||||
const batchFilteredExperimentClassrooms = computed(() =>
|
||||
classrooms.value.filter((item) =>
|
||||
(!constraintBatchForm.experimentRequiredCampusId ||
|
||||
item.campusId === constraintBatchForm.experimentRequiredCampusId) &&
|
||||
(!constraintBatchForm.experimentRequiredBuildingId ||
|
||||
item.buildingId === constraintBatchForm.experimentRequiredBuildingId) &&
|
||||
(!(constraintBatchForm.allowedExperimentVenueNatures ?? []).length ||
|
||||
(venueNatureValue(item.teachingVenueNature) &
|
||||
(constraintBatchForm.allowedExperimentVenueNatures ?? [])
|
||||
.reduce((value: number, nature: number) => value | nature, 0)) !== 0),
|
||||
),
|
||||
)
|
||||
const filteredEntries = computed(() => {
|
||||
const text = keyword.value.trim().toLowerCase()
|
||||
if (!text) return selected.value?.entries ?? []
|
||||
@@ -341,15 +293,11 @@ function openConstraint(item: any) {
|
||||
Object.assign(constraintForm, {
|
||||
teachingTaskId: item.id,
|
||||
title: `${item.taskNumber} · ${item.name}`,
|
||||
coursePracticeHours: item.coursePracticeHours,
|
||||
schedulingMode: item.schedulingMode,
|
||||
requiresClassroom: item.requiresClassroom,
|
||||
requiredCampusId: item.requiredCampusId,
|
||||
requiredBuildingId: item.requiredBuildingId,
|
||||
experimentRequiredCampusId: item.experimentRequiredCampusId,
|
||||
experimentRequiredBuildingId: item.experimentRequiredBuildingId,
|
||||
allowedClassroomIds: [...item.allowedClassroomIds],
|
||||
allowedExperimentClassroomIds: [...item.allowedExperimentClassroomIds],
|
||||
allowedExperimentVenueNatures: experimentVenueNatures
|
||||
.filter((nature) => (Number(item.allowedExperimentVenueNatures) & nature.value) !== 0)
|
||||
.map((nature) => nature.value),
|
||||
@@ -369,10 +317,7 @@ async function saveConstraint() {
|
||||
requiresClassroom: constraintForm.requiresClassroom,
|
||||
requiredCampusId: constraintForm.requiredCampusId || null,
|
||||
requiredBuildingId: constraintForm.requiredBuildingId || null,
|
||||
experimentRequiredCampusId: constraintForm.experimentRequiredCampusId || null,
|
||||
experimentRequiredBuildingId: constraintForm.experimentRequiredBuildingId || null,
|
||||
allowedClassroomIds: constraintForm.allowedClassroomIds ?? [],
|
||||
allowedExperimentClassroomIds: constraintForm.allowedExperimentClassroomIds ?? [],
|
||||
allowedExperimentVenueNatures: (constraintForm.allowedExperimentVenueNatures ?? [])
|
||||
.reduce((value: number, nature: number) => value | nature, 0),
|
||||
allowedDayOfWeeks: constraintForm.allowedDayOfWeeks ?? [],
|
||||
@@ -413,11 +358,6 @@ function openConstraintBatch() {
|
||||
requiredCampusId: undefined,
|
||||
requiredBuildingId: undefined,
|
||||
allowedClassroomIds: [],
|
||||
updateExperimentClassroomScope: false,
|
||||
experimentRequiredCampusId: undefined,
|
||||
experimentRequiredBuildingId: undefined,
|
||||
allowedExperimentVenueNatures: [],
|
||||
allowedExperimentClassroomIds: [],
|
||||
updateDays: false,
|
||||
allowedDayOfWeeks: [1, 2, 3, 4, 5],
|
||||
updatePeriodRange: false,
|
||||
@@ -431,7 +371,6 @@ async function saveConstraintBatch() {
|
||||
if (!constraintBatchForm.updateSchedulingMode &&
|
||||
!constraintBatchForm.updateRequiresClassroom &&
|
||||
!constraintBatchForm.updateClassroomScope &&
|
||||
!constraintBatchForm.updateExperimentClassroomScope &&
|
||||
!constraintBatchForm.updateDays &&
|
||||
!constraintBatchForm.updatePeriodRange) {
|
||||
ElMessage.warning('请至少勾选一项需要批量修改的设置。')
|
||||
@@ -451,9 +390,6 @@ async function saveConstraintBatch() {
|
||||
!(constraintBatchForm.updateRequiresClassroom &&
|
||||
!constraintBatchForm.requiresClassroom) &&
|
||||
constraintBatchForm.updateClassroomScope
|
||||
const updateExperimentClassroomScope = !flexible &&
|
||||
!(constraintBatchForm.updateRequiresClassroom && !constraintBatchForm.requiresClassroom) &&
|
||||
constraintBatchForm.updateExperimentClassroomScope
|
||||
const { data } = await http.put('/schedules/constraints/batch', {
|
||||
academicTermId: termId.value,
|
||||
teachingTaskIds: targets.map((item) => item.id),
|
||||
@@ -473,20 +409,6 @@ async function saveConstraintBatch() {
|
||||
allowedClassroomIds: updateClassroomScope
|
||||
? constraintBatchForm.allowedClassroomIds
|
||||
: null,
|
||||
updateExperimentClassroomScope,
|
||||
experimentRequiredCampusId: updateExperimentClassroomScope
|
||||
? constraintBatchForm.experimentRequiredCampusId || null
|
||||
: null,
|
||||
experimentRequiredBuildingId: updateExperimentClassroomScope
|
||||
? constraintBatchForm.experimentRequiredBuildingId || null
|
||||
: null,
|
||||
allowedExperimentVenueNatures: updateExperimentClassroomScope
|
||||
? (constraintBatchForm.allowedExperimentVenueNatures ?? [])
|
||||
.reduce((value: number, nature: number) => value | nature, 0)
|
||||
: null,
|
||||
allowedExperimentClassroomIds: updateExperimentClassroomScope
|
||||
? constraintBatchForm.allowedExperimentClassroomIds
|
||||
: null,
|
||||
allowedDayOfWeeks: !flexible && constraintBatchForm.updateDays
|
||||
? constraintBatchForm.allowedDayOfWeeks
|
||||
: null,
|
||||
@@ -786,13 +708,10 @@ function changeEntryTask() {
|
||||
}
|
||||
const room = classrooms.value.find((item) => item.id === entryForm.classroomId)
|
||||
if (room && (
|
||||
(entryForm.kind !== 'Experiment' && task.requiredCampusId && room.campusId !== task.requiredCampusId) ||
|
||||
(entryForm.kind !== 'Experiment' && task.requiredBuildingId && room.buildingId !== task.requiredBuildingId) ||
|
||||
(entryForm.kind !== 'Experiment' && task.allowedClassroomIds?.length && !task.allowedClassroomIds.includes(room.id)) ||
|
||||
(entryForm.kind === 'Experiment' && task.experimentRequiredCampusId && room.campusId !== task.experimentRequiredCampusId) ||
|
||||
(entryForm.kind === 'Experiment' && task.experimentRequiredBuildingId && room.buildingId !== task.experimentRequiredBuildingId) ||
|
||||
(entryForm.kind === 'Experiment' && task.allowedExperimentClassroomIds?.length &&
|
||||
!task.allowedExperimentClassroomIds.includes(room.id))
|
||||
(task.requiredCampusId && room.campusId !== task.requiredCampusId) ||
|
||||
(task.requiredBuildingId && room.buildingId !== task.requiredBuildingId) ||
|
||||
(task.allowedClassroomIds?.length && !task.allowedClassroomIds.includes(room.id)) ||
|
||||
(entryForm.kind === 'Experiment' && !isExperimentRoom(room))
|
||||
)) {
|
||||
entryForm.classroomId = null
|
||||
}
|
||||
@@ -800,9 +719,7 @@ function changeEntryTask() {
|
||||
|
||||
function changeEntryKind() {
|
||||
const room = classrooms.value.find((item) => item.id === entryForm.classroomId)
|
||||
const task = selectedTaskConstraint.value
|
||||
if (entryForm.kind === 'Experiment' && room && task?.allowedExperimentClassroomIds?.length &&
|
||||
!task.allowedExperimentClassroomIds.includes(room.id)) {
|
||||
if (entryForm.kind === 'Experiment' && room && !isExperimentRoom(room)) {
|
||||
entryForm.classroomId = null
|
||||
}
|
||||
}
|
||||
@@ -1130,7 +1047,7 @@ onBeforeUnmount(() => {
|
||||
/>
|
||||
<el-form-item
|
||||
v-else
|
||||
:label="entryForm.kind === 'Experiment' ? '教学场地' : '教室'"
|
||||
:label="entryForm.kind === 'Experiment' ? '实验室 / 实训室 / 机房' : '教室'"
|
||||
required
|
||||
:hint="selectedTaskConstraint?.requiredBuildingId ? '仅显示约束范围内教室' : ''"
|
||||
>
|
||||
@@ -1316,32 +1233,6 @@ onBeforeUnmount(() => {
|
||||
</el-checkbox-group>
|
||||
<small class="field-hint">仅约束实验课;不勾选时可使用全部实验教学场地。</small>
|
||||
</el-form-item>
|
||||
<section v-if="constraintForm.coursePracticeHours" class="experiment-classroom-scope">
|
||||
<div class="experiment-classroom-scope__title">实验课指定可用场地</div>
|
||||
<div class="form-grid">
|
||||
<el-form-item label="实验课限定校区">
|
||||
<el-select v-model="constraintForm.experimentRequiredCampusId" clearable @change="constraintForm.experimentRequiredBuildingId = undefined; constraintForm.allowedExperimentClassroomIds = []">
|
||||
<el-option v-for="item in campuses" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="实验课限定教学楼">
|
||||
<el-select v-model="constraintForm.experimentRequiredBuildingId" clearable @change="constraintForm.allowedExperimentClassroomIds = []">
|
||||
<el-option v-for="item in filteredExperimentBuildings" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item 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>
|
||||
</section>
|
||||
</template>
|
||||
<el-form-item label="允许上课日">
|
||||
<el-checkbox-group v-model="constraintForm.allowedDayOfWeeks">
|
||||
@@ -1446,52 +1337,6 @@ onBeforeUnmount(() => {
|
||||
</el-form-item>
|
||||
</div>
|
||||
</template>
|
||||
<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>
|
||||
<div class="form-grid">
|
||||
<el-form-item label="统一实验课限定校区">
|
||||
<el-select
|
||||
v-model="constraintBatchForm.experimentRequiredCampusId"
|
||||
clearable
|
||||
@change="constraintBatchForm.experimentRequiredBuildingId = undefined; constraintBatchForm.allowedExperimentClassroomIds = []"
|
||||
>
|
||||
<el-option v-for="item in campuses" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="统一实验课限定教学楼">
|
||||
<el-select
|
||||
v-model="constraintBatchForm.experimentRequiredBuildingId"
|
||||
clearable
|
||||
@change="constraintBatchForm.allowedExperimentClassroomIds = []"
|
||||
>
|
||||
<el-option v-for="item in batchFilteredExperimentBuildings" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<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>
|
||||
<el-checkbox v-model="constraintBatchForm.updateDays">修改允许上课日</el-checkbox>
|
||||
<el-form-item v-if="constraintBatchForm.updateDays" label="统一允许上课日">
|
||||
<el-checkbox-group v-model="constraintBatchForm.allowedDayOfWeeks">
|
||||
|
||||
Reference in New Issue
Block a user