Files
Academic-Affairs-System/src/Jiaowu.Api/Controllers/UsersController.cs
T
biss bcc4d33bd5 多角色账号按 All > College > Class > Self 自动取最高权限。
学院管理员限制在所属学院。
辅导员通过稳定账号 ID 绑定行政班,避免重名串班。
教师只能访问本人档案、授课课程和所授课学生。
学生只能访问本人档案及所在班级课程。
教师/学生角色会自动校验并绑定工号或学号档案。
超级管理员可在用户页面调整角色、学院、工号/学号,并预览生效后的数据范围。
2026-07-24 14:54:30 +08:00

236 lines
9.8 KiB
C#

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;
using Microsoft.EntityFrameworkCore;
namespace Jiaowu.Api.Controllers;
[ApiController]
[Authorize(Roles = SystemRoles.SuperAdmin)]
[Route("api/users")]
public sealed class UsersController(
AppDbContext db,
UserManager<ApplicationUser> userManager,
RoleManager<ApplicationRole> roleManager) : ControllerBase
{
[HttpGet]
public async Task<ActionResult<object>> GetUsers(CancellationToken cancellationToken)
{
var users = await userManager.Users.AsNoTracking()
.OrderBy(x => x.UserName)
.Select(x => new
{
x.Id, x.UserName, x.DisplayName, x.StaffNumber,
x.CollegeId, x.IsEnabled, x.LastLoginAt, x.CreatedAt
})
.ToListAsync(cancellationToken);
var result = new List<object>();
foreach (var user in users)
{
var identityUser = await userManager.FindByIdAsync(user.Id.ToString());
result.Add(new
{
user.Id, user.UserName, user.DisplayName, user.StaffNumber,
user.CollegeId, user.IsEnabled, user.LastLoginAt, user.CreatedAt,
Roles = identityUser is null
? []
: await userManager.GetRolesAsync(identityUser)
});
}
return Ok(result);
}
[HttpGet("roles")]
public async Task<ActionResult<object>> GetRoles(CancellationToken cancellationToken) =>
Ok(await roleManager.Roles.AsNoTracking()
.OrderBy(x => x.Name)
.Select(x => new { x.Name, x.Description, x.DataScope })
.ToListAsync(cancellationToken));
[HttpPost]
public async Task<ActionResult> Create(CreateUserRequest request)
{
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 = 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, 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 });
}
[HttpPut("{id:guid}/status")]
public async Task<IActionResult> SetStatus(Guid id, SetUserStatusRequest request)
{
var user = await userManager.FindByIdAsync(id.ToString());
if (user is null) return NotFound();
if (User.FindFirstValue(ClaimTypes.NameIdentifier) == id.ToString() &&
!request.IsEnabled)
return ValidationProblem("不能停用当前登录账号。");
user.IsEnabled = request.IsEnabled;
var result = await userManager.UpdateAsync(user);
return result.Succeeded ? NoContent() : IdentityValidationProblem(result);
}
[HttpPut("{id:guid}/roles")]
public async Task<IActionResult> SetRoles(Guid id, SetRolesRequest request)
{
var user = await userManager.FindByIdAsync(id.ToString());
if (user is null) return NotFound();
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) &&
!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(roles, StringComparer.OrdinalIgnoreCase));
if (!removeResult.Succeeded) return IdentityValidationProblem(removeResult);
var addResult = await userManager.AddToRolesAsync(
user,
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)
{
foreach (var error in result.Errors)
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(
[Required, MaxLength(50)] string UserName,
[Required, MaxLength(50)] string DisplayName,
[Required, MinLength(8), MaxLength(100)] string Password,
[MaxLength(30)] string? StaffNumber,
Guid? CollegeId,
[MinLength(1)] string[] Roles);
public sealed record SetUserStatusRequest(bool IsEnabled);
public sealed record SetRolesRequest(
[MaxLength(30)] string? StaffNumber,
Guid? CollegeId,
[MinLength(1)] string[] Roles);