修复Mysql

This commit is contained in:
2026-07-26 10:19:46 +08:00 Unverified
parent 634ef69a27
commit 72297aa23f
31 changed files with 1642 additions and 1192 deletions
@@ -76,11 +76,11 @@ public sealed class TeachingTasksController(
CollegeName = x.Course.College!.Name,
x.Capacity,
x.StartWeek,
x.EndWeek,
x.WeeklyHours,
x.SchedulingMode,
CourseTotalHours = x.Course.TotalHours,
x.GenerationBatchCode,
x.EndWeek,
x.WeeklyHours,
x.SchedulingMode,
CourseTotalHours = x.Course.TotalHours,
x.GenerationBatchCode,
x.Status,
TeacherNames = x.Teachers
.OrderByDescending(item => item.IsPrimary)
@@ -157,11 +157,11 @@ public sealed class TeachingTasksController(
CollegeName = x.Course.College!.Name,
x.Capacity,
x.StartWeek,
x.EndWeek,
x.WeeklyHours,
x.SchedulingMode,
CourseTotalHours = x.Course.TotalHours,
x.GenerationBatchCode,
x.EndWeek,
x.WeeklyHours,
x.SchedulingMode,
CourseTotalHours = x.Course.TotalHours,
x.GenerationBatchCode,
x.Status,
x.Notes,
x.PublishedAt,
@@ -215,10 +215,10 @@ public sealed class TeachingTasksController(
CourseId = request.CourseId,
Capacity = request.Capacity,
StartWeek = request.StartWeek,
EndWeek = request.EndWeek,
WeeklyHours = request.WeeklyHours,
SchedulingMode = request.SchedulingMode,
Notes = Normalize(request.Notes)
EndWeek = request.EndWeek,
WeeklyHours = request.WeeklyHours,
SchedulingMode = request.SchedulingMode,
Notes = Normalize(request.Notes)
};
SetAssignments(task, request);
db.TeachingTasks.Add(task);
@@ -249,10 +249,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.SchedulingMode = request.SchedulingMode;
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,7 +342,7 @@ public sealed class TeachingTasksController(
.Include(x => x.Classes)
.ThenInclude(x => x.AdministrativeClass)
.ThenInclude(x => x!.Students)
.Where(x => ids.Contains(x.Id))
.WhereIn(ids, x => x.Id)
.OrderBy(x => x.TaskNumber)
.ToListAsync(cancellationToken);
if (tasks.Count != ids.Length)
@@ -352,17 +352,17 @@ public sealed class TeachingTasksController(
switch (request.Operation)
{
case TeachingTaskBatchOperation.Publish:
{
var validation = await ValidatePublishingTasksAsync(tasks, cancellationToken);
if (validation is not null) return ConflictProblem(validation);
var publishedAt = DateTime.UtcNow;
foreach (var task in tasks)
{
task.Status = TeachingTaskStatus.Published;
task.PublishedAt = publishedAt;
var validation = await ValidatePublishingTasksAsync(tasks, cancellationToken);
if (validation is not null) return ConflictProblem(validation);
var publishedAt = DateTime.UtcNow;
foreach (var task in tasks)
{
task.Status = TeachingTaskStatus.Published;
task.PublishedAt = publishedAt;
}
break;
}
break;
}
case TeachingTaskBatchOperation.Delete:
if (tasks.Any(x =>
x.Status is not (TeachingTaskStatus.Draft or TeachingTaskStatus.Closed)))
@@ -414,14 +414,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();
var hoursProblem = TeachingTaskHours.Validate(
course,
request.StartWeek,
request.EndWeek,
request.WeeklyHours);
if (hoursProblem is not null) return ValidationProblem(hoursProblem);
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();
@@ -431,7 +431,8 @@ public sealed class TeachingTasksController(
var classIds = request.ClassIds.Distinct().ToArray();
if (classIds.Length == 0) return ValidationProblem("请至少选择一个行政班。");
var classesQuery = db.AdministrativeClasses
.Where(x => classIds.Contains(x.Id) && x.IsEnabled)
.Where(x => x.IsEnabled)
.WhereIn(classIds, x => x.Id)
.Include(x => x.Major)
.Include(x => x.Students)
.AsQueryable();
@@ -443,123 +444,127 @@ public sealed class TeachingTasksController(
if (classes.Count != classIds.Length)
return ValidationProblem("存在无效或不在当前数据范围内的行政班。");
await using var transaction = await db.Database.BeginTransactionAsync(
IsolationLevel.Serializable,
cancellationToken);
var assignedClassIds = await db.TeachingTaskClasses.AsNoTracking()
.Where(x =>
classIds.Contains(x.AdministrativeClassId) &&
x.TeachingTask!.AcademicTermId == request.AcademicTermId &&
x.TeachingTask.CourseId == request.CourseId)
.Select(x => x.AdministrativeClassId)
.Distinct()
.ToListAsync(cancellationToken);
if (assignedClassIds.Count > 0)
{
var names = classes
.Where(x => assignedClassIds.Contains(x.Id))
.Select(x => x.Name);
return ConflictProblem(
$"以下行政班已生成该课程教学任务:{string.Join('、', names)}。");
}
var eligibleTeachers = await db.TeacherCourseApplications.AsNoTracking()
.Where(x =>
x.AcademicTermId == request.AcademicTermId &&
x.CourseId == request.CourseId &&
x.Status == TeacherCourseApplicationStatus.Approved &&
x.Teacher!.Status == TeacherStatus.Active)
.Where(x => !scopedCollegeId.HasValue ||
x.Teacher!.CollegeId == scopedCollegeId.Value)
.Select(x => x.Teacher!)
.ToListAsync(cancellationToken);
if (eligibleTeachers.Count == 0)
return ConflictProblem("该课程没有学院审核通过的可授课教师,无法生成教学任务。");
var eligibleTeacherIds = eligibleTeachers.Select(x => x.Id).ToArray();
var existingLoads = await db.TeachingTaskTeachers.AsNoTracking()
.Where(x =>
eligibleTeacherIds.Contains(x.TeacherId) &&
x.TeachingTask!.AcademicTermId == request.AcademicTermId &&
x.TeachingTask.Status != TeachingTaskStatus.Closed)
.GroupBy(x => x.TeacherId)
.Select(group => new { TeacherId = group.Key, Count = group.Count() })
.ToDictionaryAsync(x => x.TeacherId, x => x.Count, cancellationToken);
var existingNumbers = await db.TeachingTasks.AsNoTracking()
.Where(x => x.AcademicTermId == request.AcademicTermId)
.Select(x => x.TaskNumber)
.ToHashSetAsync(cancellationToken);
var batchCode =
$"AUTO-{DateTime.UtcNow:yyyyMMddHHmmss}-{Guid.NewGuid():N}"[..31];
var groups = classes.Chunk(request.ClassesPerTask).ToList();
var teacherAssignments = PublicCourseTaskAssignmentPlanner.AssignTeachers(
eligibleTeacherIds,
existingLoads,
groups.Count);
var created = new List<TeachingTask>();
for (var index = 0; index < groups.Count; index++)
{
var teacher = eligibleTeachers.First(x => x.Id == teacherAssignments[index]);
var group = groups[index];
var taskNumber = NextTaskNumber(
term.Code,
course.Code,
index + 1,
existingNumbers);
existingNumbers.Add(taskNumber);
var studentCount = group.Sum(administrativeClass =>
administrativeClass.Students.Count(student =>
student.Status == StudentStatus.Active));
var task = new TeachingTask
return await db.ExecuteInRetriableTransactionAsync<ActionResult>(
async transaction =>
{
TaskNumber = taskNumber,
Name = $"{course.Name}教学班 {index + 1:D2}",
AcademicTermId = term.Id,
CourseId = course.Id,
Capacity = Math.Max(1, studentCount),
StartWeek = request.StartWeek,
EndWeek = request.EndWeek,
WeeklyHours = request.WeeklyHours,
GenerationBatchCode = batchCode,
Notes = "公共课合班自动生成,发布前可继续调整。",
Teachers =
[
new TeachingTaskTeacher
db.ChangeTracker.Clear();
var assignedClassIds = await db.TeachingTaskClasses.AsNoTracking()
.Where(x =>
x.TeachingTask!.AcademicTermId == request.AcademicTermId &&
x.TeachingTask.CourseId == request.CourseId)
.WhereIn(classIds, x => x.AdministrativeClassId)
.Select(x => x.AdministrativeClassId)
.Distinct()
.ToListAsync(cancellationToken);
if (assignedClassIds.Count > 0)
{
var names = classes
.Where(x => assignedClassIds.Contains(x.Id))
.Select(x => x.Name);
return ConflictProblem(
$"以下行政班已生成该课程教学任务:{string.Join('、', names)}。");
}
var eligibleTeachers = await db.TeacherCourseApplications.AsNoTracking()
.Where(x =>
x.AcademicTermId == request.AcademicTermId &&
x.CourseId == request.CourseId &&
x.Status == TeacherCourseApplicationStatus.Approved &&
x.Teacher!.Status == TeacherStatus.Active)
.Where(x => !scopedCollegeId.HasValue ||
x.Teacher!.CollegeId == scopedCollegeId.Value)
.Select(x => x.Teacher!)
.ToListAsync(cancellationToken);
if (eligibleTeachers.Count == 0)
return ConflictProblem("该课程没有学院审核通过的可授课教师,无法生成教学任务。");
var eligibleTeacherIds = eligibleTeachers.Select(x => x.Id).ToArray();
var existingLoads = await db.TeachingTaskTeachers.AsNoTracking()
.Where(x =>
x.TeachingTask!.AcademicTermId == request.AcademicTermId &&
x.TeachingTask.Status != TeachingTaskStatus.Closed)
.WhereIn(eligibleTeacherIds, x => x.TeacherId)
.GroupBy(x => x.TeacherId)
.Select(group => new { TeacherId = group.Key, Count = group.Count() })
.ToDictionaryAsync(x => x.TeacherId, x => x.Count, cancellationToken);
var existingNumbers = await db.TeachingTasks.AsNoTracking()
.Where(x => x.AcademicTermId == request.AcademicTermId)
.Select(x => x.TaskNumber)
.ToHashSetAsync(cancellationToken);
var batchCode =
$"AUTO-{DateTime.UtcNow:yyyyMMddHHmmss}-{Guid.NewGuid():N}"[..31];
var groups = classes.Chunk(request.ClassesPerTask).ToList();
var teacherAssignments = PublicCourseTaskAssignmentPlanner.AssignTeachers(
eligibleTeacherIds,
existingLoads,
groups.Count);
var created = new List<TeachingTask>();
for (var index = 0; index < groups.Count; index++)
{
var teacher = eligibleTeachers.First(x => x.Id == teacherAssignments[index]);
var group = groups[index];
var taskNumber = NextTaskNumber(
term.Code,
course.Code,
index + 1,
existingNumbers);
existingNumbers.Add(taskNumber);
var studentCount = group.Sum(administrativeClass =>
administrativeClass.Students.Count(student =>
student.Status == StudentStatus.Active));
var task = new TeachingTask
{
TaskNumber = taskNumber,
Name = $"{course.Name}教学班 {index + 1:D2}",
AcademicTermId = term.Id,
CourseId = course.Id,
Capacity = Math.Max(1, studentCount),
StartWeek = request.StartWeek,
EndWeek = request.EndWeek,
WeeklyHours = request.WeeklyHours,
GenerationBatchCode = batchCode,
Notes = "公共课合班自动生成,发布前可继续调整。",
Teachers =
[
new TeachingTaskTeacher
{
TeacherId = teacher.Id,
IsPrimary = true
}
],
Classes = group.Select(administrativeClass =>
new TeachingTaskClass
{
AdministrativeClassId = administrativeClass.Id
}).ToList()
};
created.Add(task);
}
],
Classes = group.Select(administrativeClass =>
new TeachingTaskClass
{
AdministrativeClassId = administrativeClass.Id
}).ToList()
};
created.Add(task);
}
db.TeachingTasks.AddRange(created);
await db.SaveChangesAsync(cancellationToken);
await transaction.CommitAsync(cancellationToken);
return Ok(new
{
BatchCode = batchCode,
CreatedCount = created.Count,
Tasks = created.Select(task => new
{
task.Id,
task.TaskNumber,
task.Name,
TeacherName = eligibleTeachers
.First(x => x.Id == task.Teachers.Single().TeacherId).Name,
ClassNames = classes
.Where(x => task.Classes.Any(item =>
item.AdministrativeClassId == x.Id))
.Select(x => x.Name),
task.Capacity
})
});
db.TeachingTasks.AddRange(created);
await db.SaveChangesAsync(cancellationToken);
await transaction.CommitAsync(cancellationToken);
return Ok(new
{
BatchCode = batchCode,
CreatedCount = created.Count,
Tasks = created.Select(task => new
{
task.Id,
task.TaskNumber,
task.Name,
TeacherName = eligibleTeachers
.First(x => x.Id == task.Teachers.Single().TeacherId).Name,
ClassNames = classes
.Where(x => task.Classes.Any(item =>
item.AdministrativeClassId == x.Id))
.Select(x => x.Name),
task.Capacity
})
});
},
cancellationToken,
IsolationLevel.Serializable);
}
private IQueryable<TeachingTask> ScopedTasks()
@@ -579,8 +584,8 @@ 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();
if (course is null) return ValidationProblem("所选课程不存在或已停用。");
if (!CanManage(course)) return Forbid();
if (!Enum.IsDefined(request.SchedulingMode))
return ValidationProblem("授课方式无效。");
var hoursProblem = TeachingTaskHours.Validate(
@@ -589,7 +594,7 @@ public sealed class TeachingTasksController(
request.EndWeek,
request.WeeklyHours);
if (hoursProblem is not null) return ValidationProblem(hoursProblem);
var collegeId = ScopedCollegeId();
var collegeId = ScopedCollegeId();
if (!await db.AcademicTerms.AnyAsync(
x => x.Id == request.AcademicTermId && x.IsEnabled,
cancellationToken))
@@ -599,25 +604,26 @@ public sealed class TeachingTasksController(
if (request.PrimaryTeacherId.HasValue &&
!teacherIds.Contains(request.PrimaryTeacherId.Value))
return ValidationProblem("主讲教师必须包含在授课教师中。");
if (await db.Teachers.CountAsync(
x => teacherIds.Contains(x.Id) && x.Status == TeacherStatus.Active,
cancellationToken) != teacherIds.Length)
if (await db.Teachers
.Where(x => x.Status == TeacherStatus.Active)
.WhereIn(teacherIds, x => x.Id)
.CountAsync(cancellationToken) != teacherIds.Length)
return ValidationProblem("存在无效或非在职授课教师。");
if (teacherIds.Length > 0 &&
await db.TeacherCourseApplications.CountAsync(
x =>
await db.TeacherCourseApplications
.Where(x =>
x.AcademicTermId == request.AcademicTermId &&
x.CourseId == request.CourseId &&
teacherIds.Contains(x.TeacherId) &&
x.Status == TeacherCourseApplicationStatus.Approved,
cancellationToken) != teacherIds.Length)
x.Status == TeacherCourseApplicationStatus.Approved)
.WhereIn(teacherIds, x => x.TeacherId)
.CountAsync(cancellationToken) != teacherIds.Length)
return ValidationProblem("授课教师必须已完成该课程申报并经学院审核通过。");
var classIds = request.ClassIds.Distinct().ToArray();
if (classIds.Length > 0)
{
var classes = db.AdministrativeClasses.AsNoTracking()
.Where(x => classIds.Contains(x.Id));
.WhereIn(classIds, x => x.Id);
if (collegeId.HasValue)
classes = classes.Where(x => x.Major!.CollegeId == collegeId.Value);
if (await classes.CountAsync(cancellationToken) != classIds.Length)
@@ -673,11 +679,10 @@ public sealed class TeachingTasksController(
.ToArray();
var approvedApplications = await db.TeacherCourseApplications
.AsNoTracking()
.Where(x =>
termIds.Contains(x.AcademicTermId) &&
courseIds.Contains(x.CourseId) &&
teacherIds.Contains(x.TeacherId) &&
x.Status == TeacherCourseApplicationStatus.Approved)
.Where(x => x.Status == TeacherCourseApplicationStatus.Approved)
.WhereIn(termIds, x => x.AcademicTermId)
.WhereIn(courseIds, x => x.CourseId)
.WhereIn(teacherIds, x => x.TeacherId)
.Select(x => new { x.AcademicTermId, x.CourseId, x.TeacherId })
.ToListAsync(cancellationToken);
var approvedAssignments = approvedApplications