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 userManager, RoleManager roleManager) : ControllerBase { [HttpGet] public async Task> 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(); 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> 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 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 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 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 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 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);