教学任务按 授课周数 × 周学时 = 课程总学时 双端校验,不匹配会显示具体差额并阻止保存;公共课批量生成同样校验。
排课约束增加课程、教师、学院、授课方式、场地要求、约束状态筛选,并支持批量修改当前筛选结果。 新增“非排时课程”授课方式:不进入自动排课 不占星期、节次和教室 不阻塞课表发布 允许正常选课 在班级课表和学生个人课表中单独展示
This commit is contained in:
@@ -631,11 +631,12 @@ public sealed class CourseSelectionsController(
|
||||
x.Enrollments.Count(item =>
|
||||
item.Status == CourseEnrollmentStatus.Enrolled),
|
||||
x.IsOpenToAll,
|
||||
x.Enrollments
|
||||
.Where(item => item.StudentId == student.Id)
|
||||
.Select(item => (CourseEnrollmentStatus?)item.Status)
|
||||
.FirstOrDefault(),
|
||||
db.ScheduleEntries
|
||||
x.Enrollments
|
||||
.Where(item => item.StudentId == student.Id)
|
||||
.Select(item => (CourseEnrollmentStatus?)item.Status)
|
||||
.FirstOrDefault(),
|
||||
x.TeachingTask.SchedulingMode == TeachingTaskSchedulingMode.Flexible,
|
||||
db.ScheduleEntries
|
||||
.Where(entry =>
|
||||
entry.TeachingTaskId == x.TeachingTaskId &&
|
||||
entry.SchedulePlan!.AcademicTermId == round.AcademicTermId &&
|
||||
@@ -803,8 +804,9 @@ public sealed class CourseSelectionsController(
|
||||
round.AcademicTermId,
|
||||
[task.Id],
|
||||
cancellationToken);
|
||||
if (candidateEntries.Count == 0)
|
||||
return ConflictProblem("该教学班尚未发布课表,暂时不能选课。");
|
||||
if (task.SchedulingMode == TeachingTaskSchedulingMode.Standard &&
|
||||
candidateEntries.Count == 0)
|
||||
return ConflictProblem("该教学班尚未发布课表,暂时不能选课。");
|
||||
var selectedTaskIds = await db.CourseEnrollments
|
||||
.Where(x =>
|
||||
x.StudentId == student.Id &&
|
||||
@@ -1008,9 +1010,10 @@ public sealed record StudentOfferingDto(
|
||||
IEnumerable<string> TeacherNames,
|
||||
int Capacity,
|
||||
int EnrolledCount,
|
||||
bool IsOpenToAll,
|
||||
CourseEnrollmentStatus? EnrollmentStatus,
|
||||
IEnumerable<StudentScheduleDto> Schedules);
|
||||
bool IsOpenToAll,
|
||||
CourseEnrollmentStatus? EnrollmentStatus,
|
||||
bool IsFlexible,
|
||||
IEnumerable<StudentScheduleDto> Schedules);
|
||||
|
||||
public sealed record StudentScheduleDto(
|
||||
int DayOfWeek,
|
||||
|
||||
@@ -128,10 +128,11 @@ public sealed class CoursesController(
|
||||
x.Name,
|
||||
x.CollegeId,
|
||||
CollegeName = x.College!.Name,
|
||||
x.CourseCategoryId,
|
||||
CategoryName = x.CourseCategory != null ? x.CourseCategory.Name : null,
|
||||
x.Credits,
|
||||
x.Nature,
|
||||
x.CourseCategoryId,
|
||||
CategoryName = x.CourseCategory != null ? x.CourseCategory.Name : null,
|
||||
x.Credits,
|
||||
x.TotalHours,
|
||||
x.Nature,
|
||||
x.AssessmentMethod
|
||||
})
|
||||
.ToListAsync(cancellationToken));
|
||||
|
||||
@@ -82,13 +82,17 @@ public sealed class ScheduleSettingsController(AppDbContext db) : ControllerBase
|
||||
{
|
||||
x.Id,
|
||||
x.TaskNumber,
|
||||
x.Name,
|
||||
CourseName = x.Course!.Name,
|
||||
TeacherNames = x.Teachers
|
||||
x.Name,
|
||||
CourseCode = x.Course!.Code,
|
||||
CourseName = x.Course!.Name,
|
||||
CollegeId = x.Course.CollegeId,
|
||||
CollegeName = x.Course.College!.Name,
|
||||
TeacherNames = x.Teachers
|
||||
.OrderByDescending(item => item.IsPrimary)
|
||||
.Select(item => item.Teacher!.Name),
|
||||
x.Capacity,
|
||||
x.WeeklyHours
|
||||
x.Capacity,
|
||||
x.WeeklyHours,
|
||||
x.SchedulingMode
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
var taskIds = tasks.Select(x => x.Id).ToList();
|
||||
@@ -103,12 +107,19 @@ public sealed class ScheduleSettingsController(AppDbContext db) : ControllerBase
|
||||
{
|
||||
task.Id,
|
||||
task.TaskNumber,
|
||||
task.Name,
|
||||
task.CourseName,
|
||||
task.TeacherNames,
|
||||
task.Capacity,
|
||||
task.WeeklyHours,
|
||||
RequiresClassroom = constraint?.RequiresClassroom ?? true,
|
||||
task.Name,
|
||||
task.CourseCode,
|
||||
task.CourseName,
|
||||
task.CollegeId,
|
||||
task.CollegeName,
|
||||
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?.RequiredBuildingId,
|
||||
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))
|
||||
return ValidationProblem("允许上课日必须位于星期一至星期日。");
|
||||
if (request.EarliestPeriod.HasValue &&
|
||||
request.LatestPeriod.HasValue &&
|
||||
request.EarliestPeriod > request.LatestPeriod)
|
||||
return ValidationProblem("最早节次不能晚于最晚节次。");
|
||||
var task = await db.TeachingTasks
|
||||
.FirstOrDefaultAsync(x => x.Id == teachingTaskId, cancellationToken);
|
||||
if (task is null) return NotFound();
|
||||
if (request.EarliestPeriod.HasValue &&
|
||||
request.LatestPeriod.HasValue &&
|
||||
request.EarliestPeriod > request.LatestPeriod)
|
||||
return ValidationProblem("最早节次不能晚于最晚节次。");
|
||||
if (!Enum.IsDefined(request.SchedulingMode))
|
||||
return ValidationProblem("授课方式无效。");
|
||||
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)
|
||||
{
|
||||
building = await db.Buildings.AsNoTracking()
|
||||
@@ -191,10 +221,128 @@ public sealed class ScheduleSettingsController(AppDbContext db) : ControllerBase
|
||||
? request.AllowedClassroomIds.Distinct().Select(classroomId =>
|
||||
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);
|
||||
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) =>
|
||||
string.IsNullOrWhiteSpace(value)
|
||||
? []
|
||||
@@ -211,6 +359,7 @@ public sealed record ScheduleTimeSlotRequest(
|
||||
bool IsEnabled);
|
||||
|
||||
public sealed record TeachingTaskScheduleConstraintRequest(
|
||||
TeachingTaskSchedulingMode SchedulingMode,
|
||||
bool RequiresClassroom,
|
||||
Guid? RequiredCampusId,
|
||||
Guid? RequiredBuildingId,
|
||||
@@ -218,3 +367,13 @@ public sealed record TeachingTaskScheduleConstraintRequest(
|
||||
IReadOnlyList<int> AllowedDayOfWeeks,
|
||||
[Range(1, 30)] int? EarliestPeriod,
|
||||
[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);
|
||||
|
||||
@@ -232,9 +232,10 @@ public sealed class SchedulesController(
|
||||
}
|
||||
|
||||
var requiredTasks = await db.TeachingTasks.AsNoTracking()
|
||||
.Where(x =>
|
||||
x.AcademicTermId == plan.AcademicTermId &&
|
||||
x.Status == TeachingTaskStatus.Published)
|
||||
.Where(x =>
|
||||
x.AcademicTermId == plan.AcademicTermId &&
|
||||
x.Status == TeachingTaskStatus.Published &&
|
||||
x.SchedulingMode == TeachingTaskSchedulingMode.Standard)
|
||||
.Select(x => new { x.Id, x.TaskNumber, x.Name, x.WeeklyHours })
|
||||
.ToListAsync(cancellationToken);
|
||||
var scheduledHours = plan.Entries
|
||||
@@ -449,10 +450,12 @@ public sealed class SchedulesController(
|
||||
.ThenInclude(x => x.AdministrativeClass)
|
||||
.ThenInclude(x => x!.Students)
|
||||
.FirstOrDefaultAsync(x => x.Id == request.TeachingTaskId, cancellationToken);
|
||||
if (task is null ||
|
||||
task.Status != TeachingTaskStatus.Published ||
|
||||
task.AcademicTermId != plan.AcademicTermId)
|
||||
return ValidationProblem("只能安排同一学期内已发布的教学任务。");
|
||||
if (task is null ||
|
||||
task.Status != TeachingTaskStatus.Published ||
|
||||
task.AcademicTermId != plan.AcademicTermId)
|
||||
return ValidationProblem("只能安排同一学期内已发布的教学任务。");
|
||||
if (task.SchedulingMode == TeachingTaskSchedulingMode.Flexible)
|
||||
return ValidationProblem("非排时课程不进入正常课表,无需设置星期、节次或教室。");
|
||||
if (request.StartWeek < task.StartWeek || request.EndWeek > task.EndWeek)
|
||||
return ValidationProblem("排课周次必须位于教学任务的授课周次内。");
|
||||
|
||||
|
||||
@@ -76,9 +76,11 @@ public sealed class TeachingTasksController(
|
||||
CollegeName = x.Course.College!.Name,
|
||||
x.Capacity,
|
||||
x.StartWeek,
|
||||
x.EndWeek,
|
||||
x.WeeklyHours,
|
||||
x.GenerationBatchCode,
|
||||
x.EndWeek,
|
||||
x.WeeklyHours,
|
||||
x.SchedulingMode,
|
||||
CourseTotalHours = x.Course.TotalHours,
|
||||
x.GenerationBatchCode,
|
||||
x.Status,
|
||||
TeacherNames = x.Teachers
|
||||
.OrderByDescending(item => item.IsPrimary)
|
||||
@@ -114,9 +116,11 @@ public sealed class TeachingTasksController(
|
||||
CollegeName = x.Course.College!.Name,
|
||||
x.Capacity,
|
||||
x.StartWeek,
|
||||
x.EndWeek,
|
||||
x.WeeklyHours,
|
||||
x.GenerationBatchCode,
|
||||
x.EndWeek,
|
||||
x.WeeklyHours,
|
||||
x.SchedulingMode,
|
||||
CourseTotalHours = x.Course.TotalHours,
|
||||
x.GenerationBatchCode,
|
||||
x.Status,
|
||||
x.Notes,
|
||||
x.PublishedAt,
|
||||
@@ -170,9 +174,10 @@ public sealed class TeachingTasksController(
|
||||
CourseId = request.CourseId,
|
||||
Capacity = request.Capacity,
|
||||
StartWeek = request.StartWeek,
|
||||
EndWeek = request.EndWeek,
|
||||
WeeklyHours = request.WeeklyHours,
|
||||
Notes = Normalize(request.Notes)
|
||||
EndWeek = request.EndWeek,
|
||||
WeeklyHours = request.WeeklyHours,
|
||||
SchedulingMode = request.SchedulingMode,
|
||||
Notes = Normalize(request.Notes)
|
||||
};
|
||||
SetAssignments(task, request);
|
||||
db.TeachingTasks.Add(task);
|
||||
@@ -203,9 +208,10 @@ public sealed class TeachingTasksController(
|
||||
task.CourseId = request.CourseId;
|
||||
task.Capacity = request.Capacity;
|
||||
task.StartWeek = request.StartWeek;
|
||||
task.EndWeek = request.EndWeek;
|
||||
task.WeeklyHours = request.WeeklyHours;
|
||||
task.Notes = Normalize(request.Notes);
|
||||
task.EndWeek = request.EndWeek;
|
||||
task.WeeklyHours = request.WeeklyHours;
|
||||
task.SchedulingMode = request.SchedulingMode;
|
||||
task.Notes = Normalize(request.Notes);
|
||||
db.TeachingTaskTeachers.RemoveRange(task.Teachers);
|
||||
db.TeachingTaskClasses.RemoveRange(task.Classes);
|
||||
task.Teachers = [];
|
||||
@@ -342,8 +348,14 @@ public sealed class TeachingTasksController(
|
||||
.FirstOrDefaultAsync(
|
||||
x => x.Id == request.CourseId && x.IsEnabled,
|
||||
cancellationToken);
|
||||
if (course is null) return ValidationProblem("所选课程不存在或已停用。");
|
||||
if (!CanManage(course)) return Forbid();
|
||||
if (course is null) return ValidationProblem("所选课程不存在或已停用。");
|
||||
if (!CanManage(course)) return Forbid();
|
||||
var hoursProblem = TeachingTaskHours.Validate(
|
||||
course,
|
||||
request.StartWeek,
|
||||
request.EndWeek,
|
||||
request.WeeklyHours);
|
||||
if (hoursProblem is not null) return ValidationProblem(hoursProblem);
|
||||
if (course.Nature is not (CourseNature.GeneralRequired or CourseNature.GeneralElective))
|
||||
return ValidationProblem("批量合班生成仅用于公共必修课或公共选修课。");
|
||||
var scopedCollegeId = ScopedCollegeId();
|
||||
@@ -501,9 +513,17 @@ public sealed class TeachingTasksController(
|
||||
return ValidationProblem("开始周不能晚于结束周。");
|
||||
var course = await db.Courses.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.Id == request.CourseId && x.IsEnabled, cancellationToken);
|
||||
if (course is null) return ValidationProblem("所选课程不存在或已停用。");
|
||||
if (!CanManage(course)) return Forbid();
|
||||
var collegeId = ScopedCollegeId();
|
||||
if (course is null) return ValidationProblem("所选课程不存在或已停用。");
|
||||
if (!CanManage(course)) return Forbid();
|
||||
if (!Enum.IsDefined(request.SchedulingMode))
|
||||
return ValidationProblem("授课方式无效。");
|
||||
var hoursProblem = TeachingTaskHours.Validate(
|
||||
course,
|
||||
request.StartWeek,
|
||||
request.EndWeek,
|
||||
request.WeeklyHours);
|
||||
if (hoursProblem is not null) return ValidationProblem(hoursProblem);
|
||||
var collegeId = ScopedCollegeId();
|
||||
if (!await db.AcademicTerms.AnyAsync(
|
||||
x => x.Id == request.AcademicTermId && x.IsEnabled,
|
||||
cancellationToken))
|
||||
@@ -695,11 +715,12 @@ public sealed record TeachingTaskRequest(
|
||||
[Range(1, 10000)] int Capacity,
|
||||
[Range(1, 30)] int StartWeek,
|
||||
[Range(1, 30)] int EndWeek,
|
||||
[Range(1, 40)] int WeeklyHours,
|
||||
IReadOnlyCollection<Guid> TeacherIds,
|
||||
Guid? PrimaryTeacherId,
|
||||
IReadOnlyCollection<Guid> ClassIds,
|
||||
[MaxLength(500)] string? Notes);
|
||||
[Range(1, 40)] int WeeklyHours,
|
||||
IReadOnlyCollection<Guid> TeacherIds,
|
||||
Guid? PrimaryTeacherId,
|
||||
IReadOnlyCollection<Guid> ClassIds,
|
||||
[MaxLength(500)] string? Notes,
|
||||
TeachingTaskSchedulingMode SchedulingMode = TeachingTaskSchedulingMode.Standard);
|
||||
|
||||
public sealed record PublicCourseTaskGenerationRequest(
|
||||
Guid AcademicTermId,
|
||||
|
||||
@@ -28,9 +28,13 @@ public sealed class TimetablesController(AppDbContext db) : ControllerBase
|
||||
x.StartDate,
|
||||
x.EndDate,
|
||||
x.IsCurrent,
|
||||
HasPublishedTimetable = db.SchedulePlans.Any(plan =>
|
||||
plan.AcademicTermId == x.Id &&
|
||||
plan.Status == SchedulePlanStatus.Published)
|
||||
HasPublishedTimetable = db.SchedulePlans.Any(plan =>
|
||||
plan.AcademicTermId == x.Id &&
|
||||
plan.Status == SchedulePlanStatus.Published) ||
|
||||
db.TeachingTasks.Any(task =>
|
||||
task.AcademicTermId == x.Id &&
|
||||
task.Status == TeachingTaskStatus.Published &&
|
||||
task.SchedulingMode == TeachingTaskSchedulingMode.Flexible)
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
var defaultTermId = terms.FirstOrDefault(x => x.IsCurrent)?.Id
|
||||
@@ -47,12 +51,17 @@ public sealed class TimetablesController(AppDbContext db) : ControllerBase
|
||||
x.Grade,
|
||||
MajorName = x.Major!.Name,
|
||||
CollegeName = x.Major.College!.Name,
|
||||
HasPublishedTimetable = defaultTermId.HasValue &&
|
||||
db.ScheduleEntries.Any(entry =>
|
||||
entry.SchedulePlan!.AcademicTermId == defaultTermId.Value &&
|
||||
entry.SchedulePlan.Status == SchedulePlanStatus.Published &&
|
||||
entry.TeachingTask!.Classes.Any(item =>
|
||||
item.AdministrativeClassId == x.Id))
|
||||
HasPublishedTimetable = defaultTermId.HasValue &&
|
||||
(db.ScheduleEntries.Any(entry =>
|
||||
entry.SchedulePlan!.AcademicTermId == defaultTermId.Value &&
|
||||
entry.SchedulePlan.Status == SchedulePlanStatus.Published &&
|
||||
entry.TeachingTask!.Classes.Any(item =>
|
||||
item.AdministrativeClassId == x.Id)) ||
|
||||
db.TeachingTasks.Any(task =>
|
||||
task.AcademicTermId == defaultTermId.Value &&
|
||||
task.Status == TeachingTaskStatus.Published &&
|
||||
task.SchedulingMode == TeachingTaskSchedulingMode.Flexible &&
|
||||
task.Classes.Any(item => item.AdministrativeClassId == x.Id)))
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
return Ok(new { Terms = terms, Classes = classes });
|
||||
@@ -148,21 +157,67 @@ public sealed class TimetablesController(AppDbContext db) : ControllerBase
|
||||
.Select(x => new { x.Id, x.Name, x.Version, x.PublishedAt })
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
var slots = await db.ScheduleTimeSlots.AsNoTracking()
|
||||
var slots = await db.ScheduleTimeSlots.AsNoTracking()
|
||||
.Where(x => x.AcademicTermId == term.Id && x.IsEnabled)
|
||||
.OrderBy(x => x.PeriodNumber)
|
||||
.Select(x => new { x.PeriodNumber, x.Name, x.StartsAt, x.EndsAt })
|
||||
.Select(x => new { x.PeriodNumber, x.Name, x.StartsAt, x.EndsAt })
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var flexibleTasks = db.TeachingTasks.AsNoTracking()
|
||||
.Where(x =>
|
||||
x.AcademicTermId == term.Id &&
|
||||
x.Status == TeachingTaskStatus.Published &&
|
||||
x.SchedulingMode == TeachingTaskSchedulingMode.Flexible &&
|
||||
x.Classes.Any(item => item.AdministrativeClassId == classId));
|
||||
if (studentId.HasValue)
|
||||
{
|
||||
var selectedFlexibleTaskIds = db.CourseEnrollments.AsNoTracking()
|
||||
.Where(x =>
|
||||
x.StudentId == studentId.Value &&
|
||||
x.Status == CourseEnrollmentStatus.Enrolled &&
|
||||
x.CourseSelectionOffering!.CourseSelectionRound!.AcademicTermId == term.Id)
|
||||
.Select(x => x.CourseSelectionOffering!.TeachingTaskId);
|
||||
flexibleTasks = db.TeachingTasks.AsNoTracking()
|
||||
.Where(x =>
|
||||
x.AcademicTermId == term.Id &&
|
||||
x.Status == TeachingTaskStatus.Published &&
|
||||
x.SchedulingMode == TeachingTaskSchedulingMode.Flexible &&
|
||||
(x.Classes.Any(item => item.AdministrativeClassId == classId) ||
|
||||
selectedFlexibleTaskIds.Contains(x.Id)));
|
||||
}
|
||||
var flexibleCourses = await flexibleTasks
|
||||
.OrderBy(x => x.Course!.Code)
|
||||
.ThenBy(x => x.TaskNumber)
|
||||
.Select(x => new
|
||||
{
|
||||
x.Id,
|
||||
x.TaskNumber,
|
||||
x.Name,
|
||||
CourseCode = x.Course!.Code,
|
||||
CourseName = x.Course.Name,
|
||||
x.Course.Credits,
|
||||
x.Course.TotalHours,
|
||||
x.StartWeek,
|
||||
x.EndWeek,
|
||||
x.WeeklyHours,
|
||||
TeacherNames = x.Teachers
|
||||
.OrderByDescending(item => item.IsPrimary)
|
||||
.Select(item => item.Teacher!.Name),
|
||||
ClassNames = x.Classes.Select(item => item.AdministrativeClass!.Name),
|
||||
x.Notes
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (plan is null)
|
||||
return Ok(new
|
||||
if (plan is null)
|
||||
return Ok(new
|
||||
{
|
||||
Term = term,
|
||||
Class = administrativeClass,
|
||||
Student = student,
|
||||
Plan = (object?)null,
|
||||
Slots = slots,
|
||||
Entries = Array.Empty<object>()
|
||||
Plan = (object?)null,
|
||||
Slots = slots,
|
||||
Entries = Array.Empty<object>(),
|
||||
FlexibleCourses = flexibleCourses
|
||||
});
|
||||
|
||||
var entries = db.ScheduleEntries.AsNoTracking()
|
||||
@@ -223,9 +278,10 @@ public sealed class TimetablesController(AppDbContext db) : ControllerBase
|
||||
Term = term,
|
||||
Class = administrativeClass,
|
||||
Student = student,
|
||||
Plan = plan,
|
||||
Slots = slots,
|
||||
Entries = result
|
||||
});
|
||||
Plan = plan,
|
||||
Slots = slots,
|
||||
Entries = result,
|
||||
FlexibleCourses = flexibleCourses
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ public sealed class TeachingTask : EntityBase
|
||||
public int StartWeek { get; set; } = 1;
|
||||
public int EndWeek { get; set; } = 16;
|
||||
public int WeeklyHours { get; set; } = 2;
|
||||
public TeachingTaskSchedulingMode SchedulingMode { get; set; } =
|
||||
TeachingTaskSchedulingMode.Standard;
|
||||
public TeachingTaskStatus Status { get; set; } = TeachingTaskStatus.Draft;
|
||||
public string? GenerationBatchCode { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
@@ -63,6 +65,12 @@ public enum TeachingTaskStatus
|
||||
Closed = 3
|
||||
}
|
||||
|
||||
public enum TeachingTaskSchedulingMode
|
||||
{
|
||||
Standard = 1,
|
||||
Flexible = 2
|
||||
}
|
||||
|
||||
public enum TeacherCourseApplicationStatus
|
||||
{
|
||||
Pending = 1,
|
||||
|
||||
@@ -23,8 +23,10 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
"20260724_14_scheduling_optimization";
|
||||
private const string TeacherCourseApplicationsMigration =
|
||||
"20260724_15_teacher_course_applications";
|
||||
private const string AutomaticScheduleJobsMigration =
|
||||
"20260724_16_automatic_schedule_jobs";
|
||||
private const string AutomaticScheduleJobsMigration =
|
||||
"20260724_16_automatic_schedule_jobs";
|
||||
private const string TeachingTaskSchedulingModesMigration =
|
||||
"20260725_17_teaching_task_scheduling_modes";
|
||||
|
||||
public async Task MigrateAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -145,9 +147,21 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
WHERE type = 'table' AND name = 'AutomaticScheduleJobs'
|
||||
""")
|
||||
.AnyAsync(value => value > 0, cancellationToken);
|
||||
await ApplyMigrationAsync(
|
||||
AutomaticScheduleJobsMigration,
|
||||
automaticScheduleJobsExist ? [] : AutomaticScheduleJobStatements,
|
||||
cancellationToken);
|
||||
var teachingTaskSchedulingModeExists = await db.Database
|
||||
.SqlQueryRaw<int>(
|
||||
"""
|
||||
SELECT COUNT(*) AS "Value"
|
||||
FROM pragma_table_info('TeachingTasks')
|
||||
WHERE name = 'SchedulingMode'
|
||||
""")
|
||||
.AnyAsync(value => value > 0, cancellationToken);
|
||||
await ApplyMigrationAsync(
|
||||
AutomaticScheduleJobsMigration,
|
||||
automaticScheduleJobsExist ? [] : AutomaticScheduleJobStatements,
|
||||
TeachingTaskSchedulingModesMigration,
|
||||
teachingTaskSchedulingModeExists ? [] : TeachingTaskSchedulingModeStatements,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
@@ -384,8 +398,9 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
"Capacity" INTEGER NOT NULL,
|
||||
"StartWeek" INTEGER NOT NULL,
|
||||
"EndWeek" INTEGER NOT NULL,
|
||||
"WeeklyHours" INTEGER NOT NULL,
|
||||
"Status" INTEGER NOT NULL,
|
||||
"WeeklyHours" INTEGER NOT NULL,
|
||||
"SchedulingMode" INTEGER NOT NULL DEFAULT 1,
|
||||
"Status" INTEGER NOT NULL,
|
||||
"Notes" TEXT NULL,
|
||||
"PublishedAt" TEXT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
@@ -1081,4 +1096,12 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
ON "AutomaticScheduleJobs" ("RequestedByUserId");
|
||||
"""
|
||||
];
|
||||
|
||||
private static readonly string[] TeachingTaskSchedulingModeStatements =
|
||||
[
|
||||
"""
|
||||
ALTER TABLE "TeachingTasks"
|
||||
ADD COLUMN "SchedulingMode" INTEGER NOT NULL DEFAULT 1;
|
||||
"""
|
||||
];
|
||||
}
|
||||
|
||||
+2876
File diff suppressed because it is too large
Load Diff
+29
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class TeachingTaskSchedulingModes : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "SchedulingMode",
|
||||
table: "TeachingTasks",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 1);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SchedulingMode",
|
||||
table: "TeachingTasks");
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -1771,6 +1771,9 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
b.Property<DateTime?>("PublishedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<int>("SchedulingMode")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("StartWeek")
|
||||
.HasColumnType("int");
|
||||
|
||||
|
||||
@@ -26,9 +26,10 @@ public sealed class AutomaticScheduleGenerator(AppDbContext db)
|
||||
|
||||
var activePeriods = timeSlots.Select(x => x.PeriodNumber).ToHashSet();
|
||||
var tasks = await db.TeachingTasks
|
||||
.Where(x =>
|
||||
x.AcademicTermId == plan.AcademicTermId &&
|
||||
x.Status == TeachingTaskStatus.Published)
|
||||
.Where(x =>
|
||||
x.AcademicTermId == plan.AcademicTermId &&
|
||||
x.Status == TeachingTaskStatus.Published &&
|
||||
x.SchedulingMode == TeachingTaskSchedulingMode.Standard)
|
||||
.Include(x => x.Teachers)
|
||||
.Include(x => x.Classes)
|
||||
.ThenInclude(x => x.AdministrativeClass)
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using Jiaowu.Api.Domain.Academic;
|
||||
|
||||
namespace Jiaowu.Api.Infrastructure.Teaching;
|
||||
|
||||
public static class TeachingTaskHours
|
||||
{
|
||||
public static int Calculate(int startWeek, int endWeek, int weeklyHours) =>
|
||||
endWeek < startWeek ? 0 : (endWeek - startWeek + 1) * weeklyHours;
|
||||
|
||||
public static string? Validate(
|
||||
Course course,
|
||||
int startWeek,
|
||||
int endWeek,
|
||||
int weeklyHours)
|
||||
{
|
||||
var plannedHours = Calculate(startWeek, endWeek, weeklyHours);
|
||||
return plannedHours == course.TotalHours
|
||||
? null
|
||||
: $"课程“{course.Name}”总学时为 {course.TotalHours};当前第 {startWeek}—{endWeek} 周、每周 {weeklyHours} 学时,共 {plannedHours} 学时。请调整授课周次或周学时。";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user