培养方案下放学院维护,学院账号被后端限制在本学院范围;新增按年级、学院、专业联动筛选及维护范围提示。
课程库实行分级维护:学院只能维护本学院的专业必修、专业选修和实践课程;通识课程可查看、引用但不可修改。校级账号保留全校统筹权限。 “排课与课表”已从“教学建设”迁移到“教学运行”,原路由保持不变。 权限规则同时落实在前端和 API,并新增专项测试。
This commit is contained in:
1 parent
022e79c48d
commit
5a7a8d530b
8 files changed
+265
-41
No files matched your search
@@ -34,6 +34,11 @@ public sealed class CoursesController(
|
|||||||
{
|
{
|
||||||
page = Math.Max(1, page);
|
page = Math.Max(1, page);
|
||||||
pageSize = Math.Clamp(pageSize, 10, 100);
|
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();
|
var source = ScopedCourses().AsNoTracking();
|
||||||
if (collegeId.HasValue)
|
if (collegeId.HasValue)
|
||||||
source = source.Where(x => x.CollegeId == collegeId.Value);
|
source = source.Where(x => x.CollegeId == collegeId.Value);
|
||||||
@@ -73,7 +78,13 @@ public sealed class CoursesController(
|
|||||||
x.Description,
|
x.Description,
|
||||||
x.IsEnabled,
|
x.IsEnabled,
|
||||||
x.SortOrder,
|
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);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
@@ -118,7 +129,7 @@ public sealed class CoursesController(
|
|||||||
{
|
{
|
||||||
var entity = await db.Courses.FindAsync([id], cancellationToken);
|
var entity = await db.Courses.FindAsync([id], cancellationToken);
|
||||||
if (entity is null) return NotFound();
|
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);
|
var validation = await ValidateAsync(request, cancellationToken);
|
||||||
if (validation is not null) return validation;
|
if (validation is not null) return validation;
|
||||||
|
|
||||||
@@ -144,7 +155,7 @@ public sealed class CoursesController(
|
|||||||
{
|
{
|
||||||
var entity = await db.Courses.FindAsync([id], cancellationToken);
|
var entity = await db.Courses.FindAsync([id], cancellationToken);
|
||||||
if (entity is null) return NotFound();
|
if (entity is null) return NotFound();
|
||||||
if (!CanAccessCollege(entity.CollegeId)) return Forbid();
|
if (!CanManage(entity.CollegeId, entity.Nature)) return Forbid();
|
||||||
db.Courses.Remove(entity);
|
db.Courses.Remove(entity);
|
||||||
return await SaveAsync(id, false, cancellationToken);
|
return await SaveAsync(id, false, cancellationToken);
|
||||||
}
|
}
|
||||||
@@ -153,7 +164,7 @@ public sealed class CoursesController(
|
|||||||
CourseRequest request,
|
CourseRequest request,
|
||||||
CancellationToken cancellationToken)
|
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))
|
if (!await db.Colleges.AnyAsync(x => x.Id == request.CollegeId, cancellationToken))
|
||||||
return ValidationProblem("所选学院不存在。");
|
return ValidationProblem("所选学院不存在。");
|
||||||
if (request.LectureHours + request.PracticeHours > request.TotalHours)
|
if (request.LectureHours + request.PracticeHours > request.TotalHours)
|
||||||
@@ -167,7 +178,12 @@ public sealed class CoursesController(
|
|||||||
var source = db.Courses.AsQueryable();
|
var source = db.Courses.AsQueryable();
|
||||||
if (scope.Scope == DataScope.All) return source;
|
if (scope.Scope == DataScope.All) return source;
|
||||||
if (scope.Scope == DataScope.College)
|
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)
|
if (scope.Scope == DataScope.Class)
|
||||||
{
|
{
|
||||||
return source.Where(course => db.TeachingTasks.Any(task =>
|
return source.Where(course => db.TeachingTasks.Any(task =>
|
||||||
@@ -190,8 +206,11 @@ public sealed class CoursesController(
|
|||||||
student.UserId == userId))));
|
student.UserId == userId))));
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool CanAccessCollege(Guid collegeId) =>
|
private bool CanManage(Guid collegeId, CourseNature nature)
|
||||||
currentUserDataScope.Current.CanAccessCollege(collegeId);
|
=> CourseMaintenancePolicy.CanManage(
|
||||||
|
currentUserDataScope.Current,
|
||||||
|
collegeId,
|
||||||
|
nature);
|
||||||
|
|
||||||
private async Task<ActionResult> SaveAsync(
|
private async Task<ActionResult> SaveAsync(
|
||||||
Guid id,
|
Guid id,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ public sealed class CurriculumPlansController(
|
|||||||
int page = 1,
|
int page = 1,
|
||||||
int pageSize = 20,
|
int pageSize = 20,
|
||||||
string? keyword = null,
|
string? keyword = null,
|
||||||
|
Guid? collegeId = null,
|
||||||
Guid? majorId = null,
|
Guid? majorId = null,
|
||||||
int? effectiveGrade = null,
|
int? effectiveGrade = null,
|
||||||
CurriculumPlanStatus? status = null,
|
CurriculumPlanStatus? status = null,
|
||||||
@@ -35,6 +36,8 @@ public sealed class CurriculumPlansController(
|
|||||||
page = Math.Max(1, page);
|
page = Math.Max(1, page);
|
||||||
pageSize = Math.Clamp(pageSize, 10, 100);
|
pageSize = Math.Clamp(pageSize, 10, 100);
|
||||||
var source = ScopedPlans().AsNoTracking();
|
var source = ScopedPlans().AsNoTracking();
|
||||||
|
if (collegeId.HasValue)
|
||||||
|
source = source.Where(x => x.Major!.CollegeId == collegeId.Value);
|
||||||
if (majorId.HasValue) source = source.Where(x => x.MajorId == majorId);
|
if (majorId.HasValue) source = source.Where(x => x.MajorId == majorId);
|
||||||
if (effectiveGrade.HasValue)
|
if (effectiveGrade.HasValue)
|
||||||
source = source.Where(x => x.EffectiveGrade == effectiveGrade);
|
source = source.Where(x => x.EffectiveGrade == effectiveGrade);
|
||||||
@@ -76,6 +79,49 @@ public sealed class CurriculumPlansController(
|
|||||||
return Ok(new PagedResult<object>(items, total, page, pageSize));
|
return Ok(new PagedResult<object>(items, total, page, pageSize));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("filter-options")]
|
||||||
|
public async Task<ActionResult> GetFilterOptions(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var collegeId = ScopedCollegeId();
|
||||||
|
var majors = db.Majors.AsNoTracking().Where(x => x.IsEnabled);
|
||||||
|
if (collegeId.HasValue)
|
||||||
|
majors = majors.Where(x => x.CollegeId == collegeId.Value);
|
||||||
|
|
||||||
|
var majorOptions = await majors
|
||||||
|
.OrderBy(x => x.College!.SortOrder)
|
||||||
|
.ThenBy(x => x.SortOrder)
|
||||||
|
.ThenBy(x => x.Code)
|
||||||
|
.Select(x => new
|
||||||
|
{
|
||||||
|
x.Id,
|
||||||
|
x.Code,
|
||||||
|
x.Name,
|
||||||
|
x.CollegeId,
|
||||||
|
CollegeName = x.College!.Name,
|
||||||
|
x.SchoolingYears
|
||||||
|
})
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var accessibleCollegeIds = majorOptions
|
||||||
|
.Select(x => x.CollegeId)
|
||||||
|
.Distinct()
|
||||||
|
.ToArray();
|
||||||
|
var colleges = await db.Colleges.AsNoTracking()
|
||||||
|
.Where(x => x.IsEnabled && accessibleCollegeIds.Contains(x.Id))
|
||||||
|
.OrderBy(x => x.SortOrder)
|
||||||
|
.ThenBy(x => x.Code)
|
||||||
|
.Select(x => new { x.Id, x.Code, x.Name })
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
var grades = await ScopedPlans()
|
||||||
|
.AsNoTracking()
|
||||||
|
.Select(x => x.EffectiveGrade)
|
||||||
|
.Distinct()
|
||||||
|
.OrderByDescending(x => x)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Ok(new { Colleges = colleges, Majors = majorOptions, Grades = grades });
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("{id:guid}")]
|
[HttpGet("{id:guid}")]
|
||||||
public async Task<ActionResult> GetPlan(Guid id, CancellationToken cancellationToken)
|
public async Task<ActionResult> GetPlan(Guid id, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using Jiaowu.Api.Domain.Academic;
|
||||||
|
using Jiaowu.Api.Domain.Identity;
|
||||||
|
|
||||||
|
namespace Jiaowu.Api.Infrastructure.Auth;
|
||||||
|
|
||||||
|
public static class CourseMaintenancePolicy
|
||||||
|
{
|
||||||
|
public static bool CanManage(
|
||||||
|
CurrentUserScope scope,
|
||||||
|
Guid collegeId,
|
||||||
|
CourseNature nature)
|
||||||
|
{
|
||||||
|
if (scope.IsInRole(SystemRoles.SuperAdmin) ||
|
||||||
|
scope.IsInRole(SystemRoles.AcademicAdmin))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return scope.IsInRole(SystemRoles.CollegeAdmin) &&
|
||||||
|
scope.CollegeId == collegeId &&
|
||||||
|
IsCollegeMaintainedNature(nature);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsCollegeMaintainedNature(CourseNature nature) =>
|
||||||
|
nature is CourseNature.MajorRequired or
|
||||||
|
CourseNature.MajorElective or
|
||||||
|
CourseNature.Practice;
|
||||||
|
}
|
||||||
Loaded 3 of 8 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user