培养方案下放学院维护,学院账号被后端限制在本学院范围;新增按年级、学院、专业联动筛选及维护范围提示。

课程库实行分级维护:学院只能维护本学院的专业必修、专业选修和实践课程;通识课程可查看、引用但不可修改。校级账号保留全校统筹权限。
“排课与课表”已从“教学建设”迁移到“教学运行”,原路由保持不变。
权限规则同时落实在前端和 API,并新增专项测试。
This commit is contained in:
2026-07-24 18:31:33 +08:00 Unverified
parent 022e79c48d
commit 5a7a8d530b
8 changed files with 265 additions and 41 deletions
@@ -34,6 +34,11 @@ public sealed class CoursesController(
{
page = Math.Max(1, page);
pageSize = Math.Clamp(pageSize, 10, 100);
var scope = currentUserDataScope.Current;
var canManageAll = scope.IsInRole(SystemRoles.SuperAdmin) ||
scope.IsInRole(SystemRoles.AcademicAdmin);
var canManageCollegeCourses = scope.IsInRole(SystemRoles.CollegeAdmin);
var managedCollegeId = scope.CollegeId;
var source = ScopedCourses().AsNoTracking();
if (collegeId.HasValue)
source = source.Where(x => x.CollegeId == collegeId.Value);
@@ -73,7 +78,13 @@ public sealed class CoursesController(
x.Description,
x.IsEnabled,
x.SortOrder,
x.CreatedAt
x.CreatedAt,
CanManage = canManageAll ||
canManageCollegeCourses &&
x.CollegeId == managedCollegeId &&
(x.Nature == CourseNature.MajorRequired ||
x.Nature == CourseNature.MajorElective ||
x.Nature == CourseNature.Practice)
})
.ToListAsync(cancellationToken);
@@ -118,7 +129,7 @@ public sealed class CoursesController(
{
var entity = await db.Courses.FindAsync([id], cancellationToken);
if (entity is null) return NotFound();
if (!CanAccessCollege(entity.CollegeId)) return Forbid();
if (!CanManage(entity.CollegeId, entity.Nature)) return Forbid();
var validation = await ValidateAsync(request, cancellationToken);
if (validation is not null) return validation;
@@ -144,7 +155,7 @@ public sealed class CoursesController(
{
var entity = await db.Courses.FindAsync([id], cancellationToken);
if (entity is null) return NotFound();
if (!CanAccessCollege(entity.CollegeId)) return Forbid();
if (!CanManage(entity.CollegeId, entity.Nature)) return Forbid();
db.Courses.Remove(entity);
return await SaveAsync(id, false, cancellationToken);
}
@@ -153,7 +164,7 @@ public sealed class CoursesController(
CourseRequest request,
CancellationToken cancellationToken)
{
if (!CanAccessCollege(request.CollegeId)) return Forbid();
if (!CanManage(request.CollegeId, request.Nature)) return Forbid();
if (!await db.Colleges.AnyAsync(x => x.Id == request.CollegeId, cancellationToken))
return ValidationProblem("所选学院不存在。");
if (request.LectureHours + request.PracticeHours > request.TotalHours)
@@ -167,7 +178,12 @@ public sealed class CoursesController(
var source = db.Courses.AsQueryable();
if (scope.Scope == DataScope.All) return source;
if (scope.Scope == DataScope.College)
return source.Where(x => x.CollegeId == scope.RestrictedCollegeId);
{
return source.Where(x =>
x.Nature == CourseNature.GeneralRequired ||
x.Nature == CourseNature.GeneralElective ||
x.CollegeId == scope.RestrictedCollegeId);
}
if (scope.Scope == DataScope.Class)
{
return source.Where(course => db.TeachingTasks.Any(task =>
@@ -190,8 +206,11 @@ public sealed class CoursesController(
student.UserId == userId))));
}
private bool CanAccessCollege(Guid collegeId) =>
currentUserDataScope.Current.CanAccessCollege(collegeId);
private bool CanManage(Guid collegeId, CourseNature nature)
=> CourseMaintenancePolicy.CanManage(
currentUserDataScope.Current,
collegeId,
nature);
private async Task<ActionResult> SaveAsync(
Guid id,