已完成“专业课教学任务下放学院管理”。
专业必修、专业选修、实践课程:由课程所属学院创建、修改、删除、发布和关闭。 公共必修、公共选修:仍由校级教务管理。 校级教务可以查看全校专业课任务,但不能代替学院维护。 超级管理员保留跨学院纠错权限。 教师分配统一要求:教师已申报该课程,并通过学院审核。 兼岗账号同时拥有校级公共课和本学院专业课职责。
This commit is contained in:
@@ -23,6 +23,9 @@ public sealed class TeachingTasksController(
|
||||
SystemRoles.SuperAdmin + "," +
|
||||
SystemRoles.AcademicAdmin + "," +
|
||||
SystemRoles.CollegeAdmin;
|
||||
private const string SchoolManagementRoles =
|
||||
SystemRoles.SuperAdmin + "," +
|
||||
SystemRoles.AcademicAdmin;
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<PagedResult<object>>> Get(
|
||||
@@ -68,6 +71,7 @@ public sealed class TeachingTasksController(
|
||||
x.CourseId,
|
||||
CourseCode = x.Course!.Code,
|
||||
CourseName = x.Course.Name,
|
||||
CourseNature = x.Course.Nature,
|
||||
CollegeId = x.Course.CollegeId,
|
||||
CollegeName = x.Course.College!.Name,
|
||||
x.Capacity,
|
||||
@@ -105,6 +109,7 @@ public sealed class TeachingTasksController(
|
||||
x.CourseId,
|
||||
CourseCode = x.Course!.Code,
|
||||
CourseName = x.Course.Name,
|
||||
CourseNature = x.Course.Nature,
|
||||
CollegeId = x.Course.CollegeId,
|
||||
CollegeName = x.Course.College!.Name,
|
||||
x.Capacity,
|
||||
@@ -181,10 +186,12 @@ public sealed class TeachingTasksController(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var task = await ScopedTasks()
|
||||
.Include(x => x.Course)
|
||||
.Include(x => x.Teachers)
|
||||
.Include(x => x.Classes)
|
||||
.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("已发布或已结课的教学任务不可直接修改。");
|
||||
var validation = await ValidateRequestAsync(request, cancellationToken);
|
||||
@@ -210,8 +217,11 @@ public sealed class TeachingTasksController(
|
||||
[HttpDelete("{id:guid}")]
|
||||
public async Task<ActionResult> Delete(Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
var task = await ScopedTasks().FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
var task = await ScopedTasks()
|
||||
.Include(x => x.Course)
|
||||
.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("仅草稿教学任务可以删除。");
|
||||
db.TeachingTasks.Remove(task);
|
||||
@@ -222,18 +232,29 @@ public sealed class TeachingTasksController(
|
||||
public async Task<ActionResult> Publish(Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
var task = await ScopedTasks()
|
||||
.Include(x => x.Course)
|
||||
.Include(x => x.Teachers)
|
||||
.Include(x => x.Classes)
|
||||
.ThenInclude(x => x.AdministrativeClass)
|
||||
.ThenInclude(x => x!.Students)
|
||||
.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));
|
||||
@@ -248,8 +269,11 @@ public sealed class TeachingTasksController(
|
||||
[HttpPost("{id:guid}/close")]
|
||||
public async Task<ActionResult> Close(Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
var task = await ScopedTasks().FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
var task = await ScopedTasks()
|
||||
.Include(x => x.Course)
|
||||
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
if (task is null) return NotFound();
|
||||
if (!CanManage(task.Course!)) return Forbid();
|
||||
if (task.Status != TeachingTaskStatus.Published)
|
||||
return ConflictProblem("只有已发布的教学任务可以结课。");
|
||||
task.Status = TeachingTaskStatus.Closed;
|
||||
@@ -257,6 +281,7 @@ public sealed class TeachingTasksController(
|
||||
}
|
||||
|
||||
[HttpPost("generate-public-course")]
|
||||
[Authorize(Roles = SchoolManagementRoles)]
|
||||
public async Task<ActionResult> GeneratePublicCourseTasks(
|
||||
PublicCourseTaskGenerationRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -273,6 +298,7 @@ public sealed class TeachingTasksController(
|
||||
x => x.Id == request.CourseId && x.IsEnabled,
|
||||
cancellationToken);
|
||||
if (course is null) return ValidationProblem("所选课程不存在或已停用。");
|
||||
if (!CanManage(course)) return Forbid();
|
||||
if (course.Nature is not (CourseNature.GeneralRequired or CourseNature.GeneralElective))
|
||||
return ValidationProblem("批量合班生成仅用于公共必修课或公共选修课。");
|
||||
var scopedCollegeId = ScopedCollegeId();
|
||||
@@ -431,8 +457,8 @@ public sealed class TeachingTasksController(
|
||||
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 (collegeId.HasValue && course.CollegeId != collegeId.Value) return Forbid();
|
||||
if (!await db.AcademicTerms.AnyAsync(
|
||||
x => x.Id == request.AcademicTermId && x.IsEnabled,
|
||||
cancellationToken))
|
||||
@@ -447,7 +473,6 @@ public sealed class TeachingTasksController(
|
||||
cancellationToken) != teacherIds.Length)
|
||||
return ValidationProblem("存在无效或非在职授课教师。");
|
||||
if (teacherIds.Length > 0 &&
|
||||
course.Nature is CourseNature.GeneralRequired or CourseNature.GeneralElective &&
|
||||
await db.TeacherCourseApplications.CountAsync(
|
||||
x =>
|
||||
x.AcademicTermId == request.AcademicTermId &&
|
||||
@@ -455,7 +480,7 @@ public sealed class TeachingTasksController(
|
||||
teacherIds.Contains(x.TeacherId) &&
|
||||
x.Status == TeacherCourseApplicationStatus.Approved,
|
||||
cancellationToken) != teacherIds.Length)
|
||||
return ValidationProblem("公共课授课教师必须已完成课程申报并经学院审核通过。");
|
||||
return ValidationProblem("授课教师必须已完成该课程申报并经学院审核通过。");
|
||||
|
||||
var classIds = request.ClassIds.Distinct().ToArray();
|
||||
var classes = db.AdministrativeClasses.AsNoTracking()
|
||||
@@ -489,6 +514,12 @@ public sealed class TeachingTasksController(
|
||||
private Guid? ScopedCollegeId()
|
||||
=> currentUserDataScope.Current.RestrictedCollegeId;
|
||||
|
||||
private bool CanManage(Course course) =>
|
||||
TeachingTaskMaintenancePolicy.CanManage(
|
||||
currentUserDataScope.Current,
|
||||
course.CollegeId,
|
||||
course.Nature);
|
||||
|
||||
private async Task<ActionResult> SaveAsync(
|
||||
Guid id,
|
||||
bool created,
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
using Jiaowu.Api.Domain.Academic;
|
||||
using Jiaowu.Api.Domain.Identity;
|
||||
|
||||
namespace Jiaowu.Api.Infrastructure.Auth;
|
||||
|
||||
public static class TeachingTaskMaintenancePolicy
|
||||
{
|
||||
public static bool CanManage(
|
||||
CurrentUserScope scope,
|
||||
Guid courseCollegeId,
|
||||
CourseNature nature)
|
||||
{
|
||||
if (scope.IsInRole(SystemRoles.SuperAdmin)) return true;
|
||||
if (scope.IsInRole(SystemRoles.AcademicAdmin) &&
|
||||
IsSchoolManagedNature(nature))
|
||||
return true;
|
||||
return scope.IsInRole(SystemRoles.CollegeAdmin) &&
|
||||
scope.CollegeId == courseCollegeId &&
|
||||
IsCollegeManagedNature(nature);
|
||||
}
|
||||
|
||||
public static bool IsCollegeManagedNature(CourseNature nature) =>
|
||||
nature is CourseNature.MajorRequired or
|
||||
CourseNature.MajorElective or
|
||||
CourseNature.Practice;
|
||||
|
||||
public static bool IsSchoolManagedNature(CourseNature nature) =>
|
||||
nature is CourseNature.GeneralRequired or
|
||||
CourseNature.GeneralElective;
|
||||
}
|
||||
Reference in New Issue
Block a user