多角色账号按 All > College > Class > Self 自动取最高权限。
学院管理员限制在所属学院。 辅导员通过稳定账号 ID 绑定行政班,避免重名串班。 教师只能访问本人档案、授课课程和所授课学生。 学生只能访问本人档案及所在班级课程。 教师/学生角色会自动校验并绑定工号或学号档案。 超级管理员可在用户页面调整角色、学院、工号/学号,并预览生效后的数据范围。
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Security.Claims;
|
||||
using Jiaowu.Api.Domain.Academic;
|
||||
using Jiaowu.Api.Domain.Identity;
|
||||
using Jiaowu.Api.Infrastructure.Persistence;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -12,6 +14,7 @@ namespace Jiaowu.Api.Controllers;
|
||||
[Authorize(Roles = SystemRoles.SuperAdmin)]
|
||||
[Route("api/users")]
|
||||
public sealed class UsersController(
|
||||
AppDbContext db,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
RoleManager<ApplicationRole> roleManager) : ControllerBase
|
||||
{
|
||||
@@ -53,29 +56,35 @@ public sealed class UsersController(
|
||||
[HttpPost]
|
||||
public async Task<ActionResult> Create(CreateUserRequest request)
|
||||
{
|
||||
var invalidRoles = request.Roles
|
||||
.Except(SystemRoles.All, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
if (invalidRoles.Length > 0)
|
||||
return ValidationProblem($"无效角色:{string.Join("、", invalidRoles)}");
|
||||
var roles = request.Roles.Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
var invalidRoles = ValidateRoles(roles);
|
||||
if (invalidRoles is not null) return invalidRoles;
|
||||
var staffNumber = Normalize(request.StaffNumber);
|
||||
var profiles = await ResolveProfilesAsync(staffNumber, roles);
|
||||
if (profiles.Error is not null) return profiles.Error;
|
||||
|
||||
var user = new ApplicationUser
|
||||
{
|
||||
UserName = request.UserName.Trim(),
|
||||
DisplayName = request.DisplayName.Trim(),
|
||||
StaffNumber = request.StaffNumber?.Trim(),
|
||||
StaffNumber = staffNumber,
|
||||
CollegeId = request.CollegeId,
|
||||
LockoutEnabled = true,
|
||||
IsEnabled = true
|
||||
};
|
||||
await using var transaction = await db.Database.BeginTransactionAsync();
|
||||
var result = await userManager.CreateAsync(user, request.Password);
|
||||
if (!result.Succeeded)
|
||||
return IdentityValidationProblem(result);
|
||||
|
||||
result = await userManager.AddToRolesAsync(user, request.Roles);
|
||||
result = await userManager.AddToRolesAsync(user, roles);
|
||||
if (!result.Succeeded)
|
||||
return IdentityValidationProblem(result);
|
||||
|
||||
if (profiles.Teacher is not null) profiles.Teacher.UserId = user.Id;
|
||||
if (profiles.Student is not null) profiles.Student.UserId = user.Id;
|
||||
await db.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
return CreatedAtAction(nameof(GetUsers), new { id = user.Id }, new { user.Id });
|
||||
}
|
||||
|
||||
@@ -99,28 +108,97 @@ public sealed class UsersController(
|
||||
var user = await userManager.FindByIdAsync(id.ToString());
|
||||
if (user is null) return NotFound();
|
||||
|
||||
var invalidRoles = request.Roles
|
||||
.Except(SystemRoles.All, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
if (invalidRoles.Length > 0)
|
||||
return ValidationProblem($"无效角色:{string.Join("、", invalidRoles)}");
|
||||
var roles = request.Roles.Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
var invalidRoles = ValidateRoles(roles);
|
||||
if (invalidRoles is not null) return invalidRoles;
|
||||
var staffNumber = Normalize(request.StaffNumber);
|
||||
var profiles = await ResolveProfilesAsync(staffNumber, roles, user.Id);
|
||||
if (profiles.Error is not null) return profiles.Error;
|
||||
|
||||
var existing = await userManager.GetRolesAsync(user);
|
||||
if (User.FindFirstValue(ClaimTypes.NameIdentifier) == id.ToString() &&
|
||||
existing.Contains(SystemRoles.SuperAdmin) &&
|
||||
!request.Roles.Contains(SystemRoles.SuperAdmin))
|
||||
!roles.Contains(SystemRoles.SuperAdmin, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return ValidationProblem("不能移除当前账号的超级管理员角色。");
|
||||
}
|
||||
|
||||
await using var transaction = await db.Database.BeginTransactionAsync();
|
||||
user.StaffNumber = staffNumber;
|
||||
user.CollegeId = request.CollegeId;
|
||||
var updateResult = await userManager.UpdateAsync(user);
|
||||
if (!updateResult.Succeeded) return IdentityValidationProblem(updateResult);
|
||||
var removeResult = await userManager.RemoveFromRolesAsync(
|
||||
user,
|
||||
existing.Except(request.Roles));
|
||||
existing.Except(roles, StringComparer.OrdinalIgnoreCase));
|
||||
if (!removeResult.Succeeded) return IdentityValidationProblem(removeResult);
|
||||
var addResult = await userManager.AddToRolesAsync(
|
||||
user,
|
||||
request.Roles.Except(existing));
|
||||
return addResult.Succeeded ? NoContent() : IdentityValidationProblem(addResult);
|
||||
roles.Except(existing, StringComparer.OrdinalIgnoreCase));
|
||||
if (!addResult.Succeeded) return IdentityValidationProblem(addResult);
|
||||
|
||||
var linkedTeachers = await db.Teachers.Where(x => x.UserId == id).ToListAsync();
|
||||
var linkedStudents = await db.Students.Where(x => x.UserId == id).ToListAsync();
|
||||
if (!roles.Contains(SystemRoles.Teacher, StringComparer.OrdinalIgnoreCase))
|
||||
foreach (var teacher in linkedTeachers) teacher.UserId = null;
|
||||
if (!roles.Contains(SystemRoles.Student, StringComparer.OrdinalIgnoreCase))
|
||||
foreach (var student in linkedStudents) student.UserId = null;
|
||||
if (profiles.Teacher is not null) profiles.Teacher.UserId = id;
|
||||
if (profiles.Student is not null) profiles.Student.UserId = id;
|
||||
await db.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
private ActionResult? ValidateRoles(IReadOnlyCollection<string> roles)
|
||||
{
|
||||
if (roles.Count == 0) return ValidationProblem("至少需要分配一个角色。");
|
||||
var invalidRoles = roles
|
||||
.Except(SystemRoles.All, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
return invalidRoles.Length == 0
|
||||
? null
|
||||
: ValidationProblem($"无效角色:{string.Join("、", invalidRoles)}");
|
||||
}
|
||||
|
||||
private async Task<(Teacher? Teacher, Student? Student, ActionResult? Error)>
|
||||
ResolveProfilesAsync(
|
||||
string? staffNumber,
|
||||
IReadOnlyCollection<string> roles,
|
||||
Guid? currentUserId = null)
|
||||
{
|
||||
var needsTeacher = roles.Contains(
|
||||
SystemRoles.Teacher,
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
var needsStudent = roles.Contains(
|
||||
SystemRoles.Student,
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
if (!needsTeacher && !needsStudent) return (null, null, null);
|
||||
if (string.IsNullOrWhiteSpace(staffNumber))
|
||||
return (null, null, ValidationProblem("教师或学生角色必须填写对应的工号/学号。"));
|
||||
|
||||
Teacher? teacher = null;
|
||||
Student? student = null;
|
||||
if (needsTeacher)
|
||||
{
|
||||
teacher = await db.Teachers.SingleOrDefaultAsync(
|
||||
x => x.TeacherNumber == staffNumber);
|
||||
if (teacher is null)
|
||||
return (null, null, ValidationProblem("未找到与该工号对应的教师档案。"));
|
||||
if (teacher.UserId.HasValue && teacher.UserId != currentUserId)
|
||||
return (null, null, ConflictProblem("该教师档案已经关联其他账号。"));
|
||||
}
|
||||
if (needsStudent)
|
||||
{
|
||||
student = await db.Students.SingleOrDefaultAsync(
|
||||
x => x.StudentNumber == staffNumber);
|
||||
if (student is null)
|
||||
return (null, null, ValidationProblem("未找到与该学号对应的学生档案。"));
|
||||
if (student.UserId.HasValue && student.UserId != currentUserId)
|
||||
return (null, null, ConflictProblem("该学生档案已经关联其他账号。"));
|
||||
}
|
||||
|
||||
return (teacher, student, null);
|
||||
}
|
||||
|
||||
private ActionResult IdentityValidationProblem(IdentityResult result)
|
||||
@@ -129,6 +207,17 @@ public sealed class UsersController(
|
||||
ModelState.AddModelError(error.Code, error.Description);
|
||||
return ValidationProblem(ModelState);
|
||||
}
|
||||
|
||||
private ActionResult ConflictProblem(string detail) =>
|
||||
Conflict(new ProblemDetails
|
||||
{
|
||||
Title = "账号关联冲突",
|
||||
Detail = detail,
|
||||
Status = StatusCodes.Status409Conflict
|
||||
});
|
||||
|
||||
private static string? Normalize(string? value) =>
|
||||
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
}
|
||||
|
||||
public sealed record CreateUserRequest(
|
||||
@@ -140,4 +229,7 @@ public sealed record CreateUserRequest(
|
||||
[MinLength(1)] string[] Roles);
|
||||
|
||||
public sealed record SetUserStatusRequest(bool IsEnabled);
|
||||
public sealed record SetRolesRequest([MinLength(1)] string[] Roles);
|
||||
public sealed record SetRolesRequest(
|
||||
[MaxLength(30)] string? StaffNumber,
|
||||
Guid? CollegeId,
|
||||
[MinLength(1)] string[] Roles);
|
||||
|
||||
Reference in New Issue
Block a user