教学任务页面优化

This commit is contained in:
2026-07-24 21:10:59 +08:00 Unverified
parent 6619179acc
commit 49b550560a
3 changed files with 486 additions and 29 deletions
@@ -240,26 +240,8 @@ public sealed class TeachingTasksController(
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
if (task is null) return NotFound();
if (!CanManage(task.Course!)) return Forbid();
if (task.Status != TeachingTaskStatus.Draft)
return ConflictProblem("只有草稿教学任务可以发布。");
if (task.Teachers.Count == 0 || task.Teachers.Count(x => x.IsPrimary) != 1)
return ConflictProblem("发布前必须指定且只能指定一名主讲教师。");
if (task.Classes.Count == 0)
return ConflictProblem("发布前至少需要关联一个行政班。");
var teacherIds = task.Teachers.Select(x => x.TeacherId).ToArray();
if (await db.TeacherCourseApplications.CountAsync(
x =>
x.AcademicTermId == task.AcademicTermId &&
x.CourseId == task.CourseId &&
teacherIds.Contains(x.TeacherId) &&
x.Status == TeacherCourseApplicationStatus.Approved,
cancellationToken) != teacherIds.Length)
return ConflictProblem("授课教师尚未完成该课程申报,或学院审核尚未通过。");
var studentCount = task.Classes.Sum(x =>
x.AdministrativeClass!.Students.Count(student =>
student.Status == StudentStatus.Active));
if (studentCount > task.Capacity)
return ConflictProblem($"教学班容量不足:关联班级共有 {studentCount} 名在籍学生。");
var validation = await ValidatePublishingTasksAsync([task], cancellationToken);
if (validation is not null) return ConflictProblem(validation);
task.Status = TeachingTaskStatus.Published;
task.PublishedAt = DateTime.UtcNow;
@@ -280,6 +262,69 @@ public sealed class TeachingTasksController(
return await SaveAsync(id, false, cancellationToken);
}
[HttpPost("batch")]
public async Task<ActionResult> Batch(
TeachingTaskBatchRequest request,
CancellationToken cancellationToken)
{
var ids = request.Ids.Distinct().ToArray();
if (ids.Length == 0) return ValidationProblem("请至少选择一个教学任务。");
if (ids.Length > 100) return ValidationProblem("每次最多处理 100 个教学任务。");
var tasks = await ScopedTasks()
.Include(x => x.Course)
.Include(x => x.Teachers)
.Include(x => x.Classes)
.ThenInclude(x => x.AdministrativeClass)
.ThenInclude(x => x!.Students)
.Where(x => ids.Contains(x.Id))
.OrderBy(x => x.TaskNumber)
.ToListAsync(cancellationToken);
if (tasks.Count != ids.Length)
return ConflictProblem("部分教学任务不存在,或已超出当前账号的数据范围。");
if (tasks.Any(x => !CanManage(x.Course!))) return Forbid();
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;
}
break;
}
case TeachingTaskBatchOperation.Delete:
if (tasks.Any(x => x.Status != TeachingTaskStatus.Draft))
return ConflictProblem(
$"教学任务“{tasks.First(x => x.Status != TeachingTaskStatus.Draft).Name}”不是草稿,整批未删除。");
db.TeachingTasks.RemoveRange(tasks);
break;
case TeachingTaskBatchOperation.Archive:
if (tasks.Any(x => x.Status != TeachingTaskStatus.Published))
return ConflictProblem(
$"教学任务“{tasks.First(x => x.Status != TeachingTaskStatus.Published).Name}”不是已发布状态,整批未归档。");
foreach (var task in tasks) task.Status = TeachingTaskStatus.Closed;
break;
default:
return ValidationProblem("不支持的批量操作。");
}
try
{
await db.SaveChangesAsync(cancellationToken);
return Ok(new { AffectedCount = tasks.Count });
}
catch (DbUpdateException)
{
return ConflictProblem("批量操作未执行:教学任务仍被其他业务引用,或关联数据已失效。");
}
}
[HttpPost("generate-public-course")]
[Authorize(Roles = SchoolManagementRoles)]
public async Task<ActionResult> GeneratePublicCourseTasks(
@@ -511,6 +556,82 @@ public sealed class TeachingTasksController(
}
}
private async Task<string?> ValidatePublishingTasksAsync(
IReadOnlyCollection<TeachingTask> tasks,
CancellationToken cancellationToken)
{
var invalidStatus = tasks.FirstOrDefault(x => x.Status != TeachingTaskStatus.Draft);
if (invalidStatus is not null)
return PublishingProblem(
tasks,
invalidStatus,
"只有草稿教学任务可以发布。");
var invalidPrimaryTeacher = tasks.FirstOrDefault(x =>
x.Teachers.Count == 0 || x.Teachers.Count(item => item.IsPrimary) != 1);
if (invalidPrimaryTeacher is not null)
return PublishingProblem(
tasks,
invalidPrimaryTeacher,
"发布前必须指定且只能指定一名主讲教师。");
var missingClass = tasks.FirstOrDefault(x => x.Classes.Count == 0);
if (missingClass is not null)
return PublishingProblem(
tasks,
missingClass,
"发布前至少需要关联一个行政班。");
var termIds = tasks.Select(x => x.AcademicTermId).Distinct().ToArray();
var courseIds = tasks.Select(x => x.CourseId).Distinct().ToArray();
var teacherIds = tasks
.SelectMany(x => x.Teachers)
.Select(x => x.TeacherId)
.Distinct()
.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)
.Select(x => new { x.AcademicTermId, x.CourseId, x.TeacherId })
.ToListAsync(cancellationToken);
var approvedAssignments = approvedApplications
.Select(x => (x.AcademicTermId, x.CourseId, x.TeacherId))
.ToHashSet();
var unapprovedTeacher = tasks.FirstOrDefault(task =>
task.Teachers.Any(teacher =>
!approvedAssignments.Contains(
(task.AcademicTermId, task.CourseId, teacher.TeacherId))));
if (unapprovedTeacher is not null)
return PublishingProblem(
tasks,
unapprovedTeacher,
"授课教师尚未完成该课程申报,或学院审核尚未通过。");
foreach (var task in tasks)
{
var studentCount = task.Classes.Sum(x =>
x.AdministrativeClass!.Students.Count(student =>
student.Status == StudentStatus.Active));
if (studentCount > task.Capacity)
return PublishingProblem(
tasks,
task,
$"教学班容量不足:关联班级共有 {studentCount} 名在籍学生。");
}
return null;
}
private static string PublishingProblem(
IReadOnlyCollection<TeachingTask> tasks,
TeachingTask task,
string detail) =>
tasks.Count == 1 ? detail : $"教学任务“{task.Name}”:{detail}整批未发布。";
private Guid? ScopedCollegeId()
=> currentUserDataScope.Current.RestrictedCollegeId;
@@ -588,3 +709,14 @@ public sealed record PublicCourseTaskGenerationRequest(
[Range(1, 30)] int EndWeek,
[Range(1, 40)] int WeeklyHours,
IReadOnlyCollection<Guid> ClassIds);
public sealed record TeachingTaskBatchRequest(
TeachingTaskBatchOperation Operation,
[MinLength(1), MaxLength(100)] IReadOnlyCollection<Guid> Ids);
public enum TeachingTaskBatchOperation
{
Publish = 1,
Delete = 2,
Archive = 3
}