教学任务按 授课周数 × 周学时 = 课程总学时 双端校验,不匹配会显示具体差额并阻止保存;公共课批量生成同样校验。
排课约束增加课程、教师、学院、授课方式、场地要求、约束状态筛选,并支持批量修改当前筛选结果。 新增“非排时课程”授课方式:不进入自动排课 不占星期、节次和教室 不阻塞课表发布 允许正常选课 在班级课表和学生个人课表中单独展示
This commit is contained in:
1 parent
30c15f89e5
commit
fe508054d0
20 files changed
+3785
-136
No files matched your search
@@ -631,11 +631,12 @@ public sealed class CourseSelectionsController(
|
|||||||
x.Enrollments.Count(item =>
|
x.Enrollments.Count(item =>
|
||||||
item.Status == CourseEnrollmentStatus.Enrolled),
|
item.Status == CourseEnrollmentStatus.Enrolled),
|
||||||
x.IsOpenToAll,
|
x.IsOpenToAll,
|
||||||
x.Enrollments
|
x.Enrollments
|
||||||
.Where(item => item.StudentId == student.Id)
|
.Where(item => item.StudentId == student.Id)
|
||||||
.Select(item => (CourseEnrollmentStatus?)item.Status)
|
.Select(item => (CourseEnrollmentStatus?)item.Status)
|
||||||
.FirstOrDefault(),
|
.FirstOrDefault(),
|
||||||
db.ScheduleEntries
|
x.TeachingTask.SchedulingMode == TeachingTaskSchedulingMode.Flexible,
|
||||||
|
db.ScheduleEntries
|
||||||
.Where(entry =>
|
.Where(entry =>
|
||||||
entry.TeachingTaskId == x.TeachingTaskId &&
|
entry.TeachingTaskId == x.TeachingTaskId &&
|
||||||
entry.SchedulePlan!.AcademicTermId == round.AcademicTermId &&
|
entry.SchedulePlan!.AcademicTermId == round.AcademicTermId &&
|
||||||
@@ -803,8 +804,9 @@ public sealed class CourseSelectionsController(
|
|||||||
round.AcademicTermId,
|
round.AcademicTermId,
|
||||||
[task.Id],
|
[task.Id],
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
if (candidateEntries.Count == 0)
|
if (task.SchedulingMode == TeachingTaskSchedulingMode.Standard &&
|
||||||
return ConflictProblem("该教学班尚未发布课表,暂时不能选课。");
|
candidateEntries.Count == 0)
|
||||||
|
return ConflictProblem("该教学班尚未发布课表,暂时不能选课。");
|
||||||
var selectedTaskIds = await db.CourseEnrollments
|
var selectedTaskIds = await db.CourseEnrollments
|
||||||
.Where(x =>
|
.Where(x =>
|
||||||
x.StudentId == student.Id &&
|
x.StudentId == student.Id &&
|
||||||
@@ -1008,9 +1010,10 @@ public sealed record StudentOfferingDto(
|
|||||||
IEnumerable<string> TeacherNames,
|
IEnumerable<string> TeacherNames,
|
||||||
int Capacity,
|
int Capacity,
|
||||||
int EnrolledCount,
|
int EnrolledCount,
|
||||||
bool IsOpenToAll,
|
bool IsOpenToAll,
|
||||||
CourseEnrollmentStatus? EnrollmentStatus,
|
CourseEnrollmentStatus? EnrollmentStatus,
|
||||||
IEnumerable<StudentScheduleDto> Schedules);
|
bool IsFlexible,
|
||||||
|
IEnumerable<StudentScheduleDto> Schedules);
|
||||||
|
|
||||||
public sealed record StudentScheduleDto(
|
public sealed record StudentScheduleDto(
|
||||||
int DayOfWeek,
|
int DayOfWeek,
|
||||||
|
|||||||
@@ -128,10 +128,11 @@ public sealed class CoursesController(
|
|||||||
x.Name,
|
x.Name,
|
||||||
x.CollegeId,
|
x.CollegeId,
|
||||||
CollegeName = x.College!.Name,
|
CollegeName = x.College!.Name,
|
||||||
x.CourseCategoryId,
|
x.CourseCategoryId,
|
||||||
CategoryName = x.CourseCategory != null ? x.CourseCategory.Name : null,
|
CategoryName = x.CourseCategory != null ? x.CourseCategory.Name : null,
|
||||||
x.Credits,
|
x.Credits,
|
||||||
x.Nature,
|
x.TotalHours,
|
||||||
|
x.Nature,
|
||||||
x.AssessmentMethod
|
x.AssessmentMethod
|
||||||
})
|
})
|
||||||
.ToListAsync(cancellationToken));
|
.ToListAsync(cancellationToken));
|
||||||
|
|||||||
@@ -82,13 +82,17 @@ public sealed class ScheduleSettingsController(AppDbContext db) : ControllerBase
|
|||||||
{
|
{
|
||||||
x.Id,
|
x.Id,
|
||||||
x.TaskNumber,
|
x.TaskNumber,
|
||||||
x.Name,
|
x.Name,
|
||||||
CourseName = x.Course!.Name,
|
CourseCode = x.Course!.Code,
|
||||||
TeacherNames = x.Teachers
|
CourseName = x.Course!.Name,
|
||||||
|
CollegeId = x.Course.CollegeId,
|
||||||
|
CollegeName = x.Course.College!.Name,
|
||||||
|
TeacherNames = x.Teachers
|
||||||
.OrderByDescending(item => item.IsPrimary)
|
.OrderByDescending(item => item.IsPrimary)
|
||||||
.Select(item => item.Teacher!.Name),
|
.Select(item => item.Teacher!.Name),
|
||||||
x.Capacity,
|
x.Capacity,
|
||||||
x.WeeklyHours
|
x.WeeklyHours,
|
||||||
|
x.SchedulingMode
|
||||||
})
|
})
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
var taskIds = tasks.Select(x => x.Id).ToList();
|
var taskIds = tasks.Select(x => x.Id).ToList();
|
||||||
@@ -103,12 +107,19 @@ public sealed class ScheduleSettingsController(AppDbContext db) : ControllerBase
|
|||||||
{
|
{
|
||||||
task.Id,
|
task.Id,
|
||||||
task.TaskNumber,
|
task.TaskNumber,
|
||||||
task.Name,
|
task.Name,
|
||||||
task.CourseName,
|
task.CourseCode,
|
||||||
task.TeacherNames,
|
task.CourseName,
|
||||||
task.Capacity,
|
task.CollegeId,
|
||||||
task.WeeklyHours,
|
task.CollegeName,
|
||||||
RequiresClassroom = constraint?.RequiresClassroom ?? true,
|
task.TeacherNames,
|
||||||
|
task.Capacity,
|
||||||
|
task.WeeklyHours,
|
||||||
|
task.SchedulingMode,
|
||||||
|
HasCustomConstraint = constraint is not null,
|
||||||
|
RequiresClassroom = task.SchedulingMode == TeachingTaskSchedulingMode.Flexible
|
||||||
|
? false
|
||||||
|
: constraint?.RequiresClassroom ?? true,
|
||||||
constraint?.RequiredCampusId,
|
constraint?.RequiredCampusId,
|
||||||
constraint?.RequiredBuildingId,
|
constraint?.RequiredBuildingId,
|
||||||
AllowedDayOfWeeks = ParseDays(constraint?.AllowedDayOfWeeks),
|
AllowedDayOfWeeks = ParseDays(constraint?.AllowedDayOfWeeks),
|
||||||
@@ -128,15 +139,34 @@ public sealed class ScheduleSettingsController(AppDbContext db) : ControllerBase
|
|||||||
{
|
{
|
||||||
if (request.AllowedDayOfWeeks.Any(day => day is < 1 or > 7))
|
if (request.AllowedDayOfWeeks.Any(day => day is < 1 or > 7))
|
||||||
return ValidationProblem("允许上课日必须位于星期一至星期日。");
|
return ValidationProblem("允许上课日必须位于星期一至星期日。");
|
||||||
if (request.EarliestPeriod.HasValue &&
|
if (request.EarliestPeriod.HasValue &&
|
||||||
request.LatestPeriod.HasValue &&
|
request.LatestPeriod.HasValue &&
|
||||||
request.EarliestPeriod > request.LatestPeriod)
|
request.EarliestPeriod > request.LatestPeriod)
|
||||||
return ValidationProblem("最早节次不能晚于最晚节次。");
|
return ValidationProblem("最早节次不能晚于最晚节次。");
|
||||||
var task = await db.TeachingTasks
|
if (!Enum.IsDefined(request.SchedulingMode))
|
||||||
.FirstOrDefaultAsync(x => x.Id == teachingTaskId, cancellationToken);
|
return ValidationProblem("授课方式无效。");
|
||||||
if (task is null) return NotFound();
|
var task = await db.TeachingTasks
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == teachingTaskId, cancellationToken);
|
||||||
|
if (task is null) return NotFound();
|
||||||
|
if (request.SchedulingMode == TeachingTaskSchedulingMode.Flexible &&
|
||||||
|
await HasScheduleEntriesAsync([teachingTaskId], cancellationToken))
|
||||||
|
return ConflictProblem("该教学任务已有正常排课记录,请先删除排课记录后再改为非排时课程。");
|
||||||
|
|
||||||
Building? building = null;
|
task.SchedulingMode = request.SchedulingMode;
|
||||||
|
if (request.SchedulingMode == TeachingTaskSchedulingMode.Flexible)
|
||||||
|
{
|
||||||
|
var flexibleConstraint = await db.TeachingTaskScheduleConstraints
|
||||||
|
.Include(x => x.AllowedClassrooms)
|
||||||
|
.FirstOrDefaultAsync(x => x.TeachingTaskId == teachingTaskId, cancellationToken);
|
||||||
|
if (flexibleConstraint is not null)
|
||||||
|
{
|
||||||
|
ClearConstraint(flexibleConstraint);
|
||||||
|
}
|
||||||
|
await db.SaveChangesAsync(cancellationToken);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
Building? building = null;
|
||||||
if (request.RequiredBuildingId.HasValue)
|
if (request.RequiredBuildingId.HasValue)
|
||||||
{
|
{
|
||||||
building = await db.Buildings.AsNoTracking()
|
building = await db.Buildings.AsNoTracking()
|
||||||
@@ -191,10 +221,128 @@ public sealed class ScheduleSettingsController(AppDbContext db) : ControllerBase
|
|||||||
? request.AllowedClassroomIds.Distinct().Select(classroomId =>
|
? request.AllowedClassroomIds.Distinct().Select(classroomId =>
|
||||||
new TeachingTaskAllowedClassroom { ClassroomId = classroomId }).ToList()
|
new TeachingTaskAllowedClassroom { ClassroomId = classroomId }).ToList()
|
||||||
: [];
|
: [];
|
||||||
|
await db.SaveChangesAsync(cancellationToken);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("constraints/batch")]
|
||||||
|
public async Task<ActionResult> SaveConstraintsBatch(
|
||||||
|
TeachingTaskScheduleConstraintBatchRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var taskIds = request.TeachingTaskIds.Distinct().ToArray();
|
||||||
|
if (taskIds.Length == 0)
|
||||||
|
return ValidationProblem("请至少选择一个教学任务。");
|
||||||
|
if (request.AllowedDayOfWeeks?.Any(day => day is < 1 or > 7) == true)
|
||||||
|
return ValidationProblem("允许上课日必须位于星期一至星期日。");
|
||||||
|
if (request.EarliestPeriod.HasValue &&
|
||||||
|
request.LatestPeriod.HasValue &&
|
||||||
|
request.EarliestPeriod > request.LatestPeriod)
|
||||||
|
return ValidationProblem("最早节次不能晚于最晚节次。");
|
||||||
|
if (request.SchedulingMode.HasValue &&
|
||||||
|
!Enum.IsDefined(request.SchedulingMode.Value))
|
||||||
|
return ValidationProblem("授课方式无效。");
|
||||||
|
if (!request.SchedulingMode.HasValue &&
|
||||||
|
!request.RequiresClassroom.HasValue &&
|
||||||
|
request.AllowedDayOfWeeks is null &&
|
||||||
|
!request.UpdatePeriodRange &&
|
||||||
|
!request.EarliestPeriod.HasValue &&
|
||||||
|
!request.LatestPeriod.HasValue)
|
||||||
|
return ValidationProblem("请至少选择一项需要批量修改的设置。");
|
||||||
|
|
||||||
|
var tasks = await db.TeachingTasks
|
||||||
|
.Where(x =>
|
||||||
|
taskIds.Contains(x.Id) &&
|
||||||
|
x.AcademicTermId == request.AcademicTermId &&
|
||||||
|
x.Status == TeachingTaskStatus.Published)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
if (tasks.Count != taskIds.Length)
|
||||||
|
return ValidationProblem("部分教学任务不存在、未发布或不属于当前学期。");
|
||||||
|
if (request.SchedulingMode == TeachingTaskSchedulingMode.Flexible &&
|
||||||
|
await HasScheduleEntriesAsync(taskIds, cancellationToken))
|
||||||
|
return ConflictProblem("所选教学任务中存在已有正常排课记录的课程,请先删除这些排课记录。");
|
||||||
|
|
||||||
|
var constraints = await db.TeachingTaskScheduleConstraints
|
||||||
|
.Where(x => taskIds.Contains(x.TeachingTaskId))
|
||||||
|
.Include(x => x.AllowedClassrooms)
|
||||||
|
.ToDictionaryAsync(x => x.TeachingTaskId, cancellationToken);
|
||||||
|
foreach (var task in tasks)
|
||||||
|
{
|
||||||
|
if (request.SchedulingMode.HasValue)
|
||||||
|
task.SchedulingMode = request.SchedulingMode.Value;
|
||||||
|
if (task.SchedulingMode == TeachingTaskSchedulingMode.Flexible)
|
||||||
|
{
|
||||||
|
if (constraints.TryGetValue(task.Id, out var flexibleConstraint))
|
||||||
|
ClearConstraint(flexibleConstraint);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!constraints.TryGetValue(task.Id, out var constraint))
|
||||||
|
{
|
||||||
|
var changesConstraint =
|
||||||
|
request.RequiresClassroom.HasValue ||
|
||||||
|
request.AllowedDayOfWeeks is not null ||
|
||||||
|
request.UpdatePeriodRange;
|
||||||
|
if (!changesConstraint) continue;
|
||||||
|
constraint = new TeachingTaskScheduleConstraint { TeachingTaskId = task.Id };
|
||||||
|
constraints[task.Id] = constraint;
|
||||||
|
db.TeachingTaskScheduleConstraints.Add(constraint);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.RequiresClassroom.HasValue)
|
||||||
|
{
|
||||||
|
constraint.RequiresClassroom = request.RequiresClassroom.Value;
|
||||||
|
if (!request.RequiresClassroom.Value)
|
||||||
|
{
|
||||||
|
constraint.RequiredCampusId = null;
|
||||||
|
constraint.RequiredBuildingId = null;
|
||||||
|
db.TeachingTaskAllowedClassrooms.RemoveRange(constraint.AllowedClassrooms);
|
||||||
|
constraint.AllowedClassrooms = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (request.AllowedDayOfWeeks is not null)
|
||||||
|
{
|
||||||
|
constraint.AllowedDayOfWeeks = request.AllowedDayOfWeeks.Count == 0
|
||||||
|
? null
|
||||||
|
: string.Join(',', request.AllowedDayOfWeeks.Distinct().Order());
|
||||||
|
}
|
||||||
|
if (request.UpdatePeriodRange)
|
||||||
|
{
|
||||||
|
constraint.EarliestPeriod = request.EarliestPeriod;
|
||||||
|
constraint.LatestPeriod = request.LatestPeriod;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await db.SaveChangesAsync(cancellationToken);
|
await db.SaveChangesAsync(cancellationToken);
|
||||||
return NoContent();
|
return Ok(new { AffectedCount = tasks.Count });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Task<bool> HasScheduleEntriesAsync(
|
||||||
|
IReadOnlyCollection<Guid> taskIds,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
db.ScheduleEntries.AsNoTracking()
|
||||||
|
.AnyAsync(x => taskIds.Contains(x.TeachingTaskId), cancellationToken);
|
||||||
|
|
||||||
|
private void ClearConstraint(TeachingTaskScheduleConstraint constraint)
|
||||||
|
{
|
||||||
|
constraint.RequiresClassroom = false;
|
||||||
|
constraint.RequiredCampusId = null;
|
||||||
|
constraint.RequiredBuildingId = null;
|
||||||
|
constraint.AllowedDayOfWeeks = null;
|
||||||
|
constraint.EarliestPeriod = null;
|
||||||
|
constraint.LatestPeriod = null;
|
||||||
|
db.TeachingTaskAllowedClassrooms.RemoveRange(constraint.AllowedClassrooms);
|
||||||
|
constraint.AllowedClassrooms = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private ActionResult ConflictProblem(string detail) =>
|
||||||
|
Conflict(new ProblemDetails
|
||||||
|
{
|
||||||
|
Title = "操作冲突",
|
||||||
|
Detail = detail,
|
||||||
|
Status = StatusCodes.Status409Conflict
|
||||||
|
});
|
||||||
|
|
||||||
private static int[] ParseDays(string? value) =>
|
private static int[] ParseDays(string? value) =>
|
||||||
string.IsNullOrWhiteSpace(value)
|
string.IsNullOrWhiteSpace(value)
|
||||||
? []
|
? []
|
||||||
@@ -211,6 +359,7 @@ public sealed record ScheduleTimeSlotRequest(
|
|||||||
bool IsEnabled);
|
bool IsEnabled);
|
||||||
|
|
||||||
public sealed record TeachingTaskScheduleConstraintRequest(
|
public sealed record TeachingTaskScheduleConstraintRequest(
|
||||||
|
TeachingTaskSchedulingMode SchedulingMode,
|
||||||
bool RequiresClassroom,
|
bool RequiresClassroom,
|
||||||
Guid? RequiredCampusId,
|
Guid? RequiredCampusId,
|
||||||
Guid? RequiredBuildingId,
|
Guid? RequiredBuildingId,
|
||||||
@@ -218,3 +367,13 @@ 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);
|
||||||
|
|
||||||
|
public sealed record TeachingTaskScheduleConstraintBatchRequest(
|
||||||
|
Guid AcademicTermId,
|
||||||
|
[MinLength(1), MaxLength(500)] IReadOnlyCollection<Guid> TeachingTaskIds,
|
||||||
|
TeachingTaskSchedulingMode? SchedulingMode,
|
||||||
|
bool? RequiresClassroom,
|
||||||
|
IReadOnlyList<int>? AllowedDayOfWeeks,
|
||||||
|
bool UpdatePeriodRange,
|
||||||
|
[Range(1, 30)] int? EarliestPeriod,
|
||||||
|
[Range(1, 30)] int? LatestPeriod);
|
||||||
Loaded 3 of 20 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user