多角色账号按 All > College > Class > Self 自动取最高权限。
学院管理员限制在所属学院。 辅导员通过稳定账号 ID 绑定行政班,避免重名串班。 教师只能访问本人档案、授课课程和所授课学生。 学生只能访问本人档案及所在班级课程。 教师/学生角色会自动校验并绑定工号或学号档案。 超级管理员可在用户页面调整角色、学院、工号/学号,并预览生效后的数据范围。
This commit is contained in:
1 parent
0b463fa4f2
commit
bcc4d33bd5
26 files changed
+2333
-88
No files matched your search
@@ -53,7 +53,8 @@ public sealed class AuthController(
|
||||
user.UserName!,
|
||||
user.DisplayName,
|
||||
roles,
|
||||
user.CollegeId));
|
||||
user.CollegeId,
|
||||
EffectiveDataScopeResolver.Resolve(roles).ToString()));
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
@@ -67,12 +68,14 @@ public sealed class AuthController(
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var roles = await userManager.GetRolesAsync(user);
|
||||
return new CurrentUserResponse(
|
||||
user.Id,
|
||||
user.UserName!,
|
||||
user.DisplayName,
|
||||
await userManager.GetRolesAsync(user),
|
||||
user.CollegeId);
|
||||
roles,
|
||||
user.CollegeId,
|
||||
EffectiveDataScopeResolver.Resolve(roles).ToString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,4 +90,5 @@ public sealed record CurrentUserResponse(
|
||||
string UserName,
|
||||
string DisplayName,
|
||||
IEnumerable<string> Roles,
|
||||
Guid? CollegeId);
|
||||
Guid? CollegeId,
|
||||
string EffectiveDataScope);
|
||||
@@ -168,7 +168,29 @@ public sealed class BaseDataController(AppDbContext db) : ControllerBase
|
||||
x.Id, x.Code, x.Name, x.MajorId,
|
||||
MajorName = x.Major!.Name,
|
||||
CollegeName = x.Major.College!.Name,
|
||||
x.Grade, x.CounselorName, x.IsEnabled, x.SortOrder
|
||||
x.Grade, x.CounselorUserId, x.CounselorName,
|
||||
x.IsEnabled, x.SortOrder
|
||||
})
|
||||
.ToListAsync(cancellationToken));
|
||||
|
||||
[HttpGet("counselors")]
|
||||
[Authorize(Roles = Administrators)]
|
||||
public async Task<ActionResult<object>> GetCounselors(CancellationToken cancellationToken) =>
|
||||
Ok(await db.Users.AsNoTracking()
|
||||
.Where(user =>
|
||||
user.IsEnabled &&
|
||||
db.UserRoles.Any(userRole =>
|
||||
userRole.UserId == user.Id &&
|
||||
db.Roles.Any(role =>
|
||||
role.Id == userRole.RoleId &&
|
||||
role.Name == SystemRoles.Counselor)))
|
||||
.OrderBy(x => x.DisplayName)
|
||||
.Select(x => new
|
||||
{
|
||||
x.Id,
|
||||
x.DisplayName,
|
||||
x.StaffNumber,
|
||||
x.CollegeId
|
||||
})
|
||||
.ToListAsync(cancellationToken));
|
||||
|
||||
@@ -180,6 +202,11 @@ public sealed class BaseDataController(AppDbContext db) : ControllerBase
|
||||
{
|
||||
if (!await db.Majors.AnyAsync(x => x.Id == request.MajorId, cancellationToken))
|
||||
return ValidationProblem("所选专业不存在。");
|
||||
var counselorName = await GetCounselorNameAsync(
|
||||
request.CounselorUserId,
|
||||
cancellationToken);
|
||||
if (request.CounselorUserId.HasValue && counselorName is null)
|
||||
return ValidationProblem("所选辅导员账号不存在、已停用或没有辅导员角色。");
|
||||
|
||||
var entity = new AdministrativeClass
|
||||
{
|
||||
@@ -187,7 +214,8 @@ public sealed class BaseDataController(AppDbContext db) : ControllerBase
|
||||
Name = request.Name.Trim(),
|
||||
MajorId = request.MajorId,
|
||||
Grade = request.Grade,
|
||||
CounselorName = request.CounselorName?.Trim(),
|
||||
CounselorUserId = request.CounselorUserId,
|
||||
CounselorName = counselorName,
|
||||
SortOrder = request.SortOrder,
|
||||
IsEnabled = request.IsEnabled
|
||||
};
|
||||
@@ -203,10 +231,16 @@ public sealed class BaseDataController(AppDbContext db) : ControllerBase
|
||||
{
|
||||
var entity = await db.AdministrativeClasses.FindAsync([id], cancellationToken);
|
||||
if (entity is null) return NotFound();
|
||||
var counselorName = await GetCounselorNameAsync(
|
||||
request.CounselorUserId,
|
||||
cancellationToken);
|
||||
if (request.CounselorUserId.HasValue && counselorName is null)
|
||||
return ValidationProblem("所选辅导员账号不存在、已停用或没有辅导员角色。");
|
||||
ApplyCatalog(entity, request);
|
||||
entity.MajorId = request.MajorId;
|
||||
entity.Grade = request.Grade;
|
||||
entity.CounselorName = request.CounselorName?.Trim();
|
||||
entity.CounselorUserId = request.CounselorUserId;
|
||||
entity.CounselorName = counselorName;
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
return entity;
|
||||
}
|
||||
@@ -409,6 +443,24 @@ public sealed class BaseDataController(AppDbContext db) : ControllerBase
|
||||
return CreatedAtAction(action, new { id = entity.Id }, entity);
|
||||
}
|
||||
|
||||
private async Task<string?> GetCounselorNameAsync(
|
||||
Guid? userId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!userId.HasValue) return null;
|
||||
return await db.Users.AsNoTracking()
|
||||
.Where(user =>
|
||||
user.Id == userId &&
|
||||
user.IsEnabled &&
|
||||
db.UserRoles.Any(userRole =>
|
||||
userRole.UserId == user.Id &&
|
||||
db.Roles.Any(role =>
|
||||
role.Id == userRole.RoleId &&
|
||||
role.Name == SystemRoles.Counselor)))
|
||||
.Select(x => x.DisplayName)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private static void ApplyCatalog(CatalogEntity entity, CatalogRequest request)
|
||||
{
|
||||
entity.Code = request.Code.Trim();
|
||||
@@ -442,7 +494,7 @@ public sealed record ClassRequest(
|
||||
string Code, string Name, int SortOrder, bool IsEnabled,
|
||||
Guid MajorId,
|
||||
[Range(2000, 2200)] int Grade,
|
||||
[MaxLength(50)] string? CounselorName)
|
||||
Guid? CounselorUserId)
|
||||
: CatalogRequest(Code, Name, SortOrder, IsEnabled);
|
||||
|
||||
public sealed record TermRequest(
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Security.Claims;
|
||||
using Jiaowu.Api.Contracts;
|
||||
using Jiaowu.Api.Domain.Academic;
|
||||
using Jiaowu.Api.Domain.Identity;
|
||||
using Jiaowu.Api.Infrastructure.Auth;
|
||||
using Jiaowu.Api.Infrastructure.Persistence;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -13,7 +13,9 @@ namespace Jiaowu.Api.Controllers;
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/courses")]
|
||||
public sealed class CoursesController(AppDbContext db) : ControllerBase
|
||||
public sealed class CoursesController(
|
||||
AppDbContext db,
|
||||
ICurrentUserDataScope currentUserDataScope) : ControllerBase
|
||||
{
|
||||
private const string WriteRoles =
|
||||
SystemRoles.SuperAdmin + "," +
|
||||
@@ -32,10 +34,7 @@ public sealed class CoursesController(AppDbContext db) : ControllerBase
|
||||
{
|
||||
page = Math.Max(1, page);
|
||||
pageSize = Math.Clamp(pageSize, 10, 100);
|
||||
var source = db.Courses.AsNoTracking().AsQueryable();
|
||||
var scopedCollegeId = GetScopedCollegeId();
|
||||
if (scopedCollegeId.HasValue)
|
||||
source = source.Where(x => x.CollegeId == scopedCollegeId.Value);
|
||||
var source = ScopedCourses().AsNoTracking();
|
||||
if (collegeId.HasValue)
|
||||
source = source.Where(x => x.CollegeId == collegeId.Value);
|
||||
if (nature.HasValue)
|
||||
@@ -162,17 +161,37 @@ public sealed class CoursesController(AppDbContext db) : ControllerBase
|
||||
return null;
|
||||
}
|
||||
|
||||
private Guid? GetScopedCollegeId()
|
||||
private IQueryable<Course> ScopedCourses()
|
||||
{
|
||||
if (!User.IsInRole(SystemRoles.CollegeAdmin))
|
||||
return null;
|
||||
return Guid.TryParse(User.FindFirstValue("college_id"), out var collegeId)
|
||||
? collegeId
|
||||
: Guid.Empty;
|
||||
var scope = currentUserDataScope.Current;
|
||||
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);
|
||||
if (scope.Scope == DataScope.Class)
|
||||
{
|
||||
return source.Where(course => db.TeachingTasks.Any(task =>
|
||||
task.CourseId == course.Id &&
|
||||
task.Classes.Any(item =>
|
||||
item.AdministrativeClass!.CounselorUserId == scope.UserId)));
|
||||
}
|
||||
|
||||
var userId = scope.UserId;
|
||||
var isTeacher = scope.IsInRole(SystemRoles.Teacher);
|
||||
var isStudent = scope.IsInRole(SystemRoles.Student);
|
||||
return source.Where(course =>
|
||||
isTeacher && db.TeachingTasks.Any(task =>
|
||||
task.CourseId == course.Id &&
|
||||
task.Teachers.Any(item => item.Teacher!.UserId == userId)) ||
|
||||
isStudent && db.TeachingTasks.Any(task =>
|
||||
task.CourseId == course.Id &&
|
||||
task.Classes.Any(item =>
|
||||
item.AdministrativeClass!.Students.Any(student =>
|
||||
student.UserId == userId))));
|
||||
}
|
||||
|
||||
private bool CanAccessCollege(Guid collegeId) =>
|
||||
!GetScopedCollegeId().HasValue || GetScopedCollegeId() == collegeId;
|
||||
currentUserDataScope.Current.CanAccessCollege(collegeId);
|
||||
|
||||
private async Task<ActionResult> SaveAsync(
|
||||
Guid id,
|
||||
|
||||
Loaded 3 of 26 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user