From f163738e806c3e22331119d7fae37d467f405a00 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 8 Sep 2025 11:03:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E6=9D=83=E9=99=90=E7=B3=BB=E7=BB=9F=20-=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E8=A7=92=E8=89=B2Claims=E9=AA=8C=E8=AF=81=E5=92=8C=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主要变更: - 修复角色Claims验证问题:UserRoleRepository返回角色名称而非ID,解决JWT授权失败 - 重构用户DTO:创建UserSimpleDto简化用户数据传输,提升性能 - 修复用户状态切换:添加管理员保护机制,防止禁用管理员用户 - 优化导航属性加载:封装UserRole导航属性查询到仓储层 - 添加角色常量定义:RoleConstants统一管理角色名称,提高代码可维护性 - 完善前端适配:更新用户管理页面组件,修复主题适配和状态切换功能 技术改进: - 增强JWT令牌Claims:同时支持角色名称和角色ID - 改进错误处理:完善空值检查和异常处理 - 提升代码质量:使用DTO替代元组,遵循DDD设计原则 - 优化数据库查询:减少不必要的权限查询,提升性能 影响范围: - 修复角色授权验证失败问题 - 提升用户管理功能稳定性 - 改善前端用户体验 - 增强系统安全性 --- .../Auth/Commands/BaseLoginCommandHandler.cs | 16 ++- .../ToggleUserStatusCommand.cs | 14 ++ .../ToggleUserStatusCommandHandler.cs | 121 ++++++++++++++++ .../ToggleUserStatusResponse.cs | 11 ++ .../Users/Queries/Dtos/UserSimpleDto.cs | 23 ++++ .../GetAllUsers/GetAllUsersQueryHandler.cs | 73 ++++++++-- .../GetAllUsers/GetAllUsersResponse.cs | 2 +- src/X1.Domain/Common/RoleConstants.cs | 95 +++++++++++++ src/X1.Domain/Models/UserRoleInfo.cs | 79 +++++++++++ .../Identity/IUserRoleRepository.cs | 16 +++ .../Identity/UserRoleRepository.cs | 46 ++++++- .../UserManagement/UserRegistrationService.cs | 3 +- .../Controllers/NavigationMenuController.cs | 2 +- .../Controllers/PermissionsController.cs | 2 +- .../Controllers/RolesController.cs | 2 +- .../Controllers/UsersController.cs | 56 ++++++++ .../components/dashboard/StatsOverview.tsx | 3 +- .../src/components/ui/StatusSwitch.tsx | 49 +++---- src/X1.WebUI/src/pages/auth/LoginPage.tsx | 4 +- .../StepDetailsView.tsx | 1 - .../TaskExecutionProcessView.tsx | 1 - src/X1.WebUI/src/pages/users/UserForm.tsx | 26 +++- .../src/pages/users/UserRolesForm.tsx | 17 ++- src/X1.WebUI/src/pages/users/UserTable.tsx | 34 ++++- src/X1.WebUI/src/pages/users/UsersView.tsx | 6 +- src/X1.WebUI/src/services/userService.ts | 12 +- src/modify_20250121_role_claims_fix.md | 129 ++++++++++++++++++ src/modify_20250121_toggleuserstatus_fix.md | 80 +++++++++++ ...modify_20250121_userrole_navigation_fix.md | 61 +++++++++ src/modify_20250121_users_dto_refactor.md | 105 ++++++++++++++ 30 files changed, 1015 insertions(+), 74 deletions(-) create mode 100644 src/X1.Application/Features/Users/Commands/ToggleUserStatus/ToggleUserStatusCommand.cs create mode 100644 src/X1.Application/Features/Users/Commands/ToggleUserStatus/ToggleUserStatusCommandHandler.cs create mode 100644 src/X1.Application/Features/Users/Commands/ToggleUserStatus/ToggleUserStatusResponse.cs create mode 100644 src/X1.Application/Features/Users/Queries/Dtos/UserSimpleDto.cs create mode 100644 src/X1.Domain/Common/RoleConstants.cs create mode 100644 src/X1.Domain/Models/UserRoleInfo.cs create mode 100644 src/modify_20250121_role_claims_fix.md create mode 100644 src/modify_20250121_toggleuserstatus_fix.md create mode 100644 src/modify_20250121_userrole_navigation_fix.md create mode 100644 src/modify_20250121_users_dto_refactor.md diff --git a/src/X1.Application/Features/Auth/Commands/BaseLoginCommandHandler.cs b/src/X1.Application/Features/Auth/Commands/BaseLoginCommandHandler.cs index 62501fa..cb6fe54 100644 --- a/src/X1.Application/Features/Auth/Commands/BaseLoginCommandHandler.cs +++ b/src/X1.Application/Features/Auth/Commands/BaseLoginCommandHandler.cs @@ -19,6 +19,7 @@ using X1.Domain.Repositories.Identity; using X1.Domain.Repositories.Base; using X1.Domain.Options; using Microsoft.Extensions.Options; +using X1.Domain.Models; namespace X1.Application.Features.Auth.Commands; @@ -211,8 +212,10 @@ public abstract class BaseLoginCommandHandler : IRequestHan } } - // 获取用户角色 - var roles = await _userRoleRepository.GetUserRolesAsync(user.Id, cancellationToken); + // 获取用户角色信息(包含ID和名称) + var roleInfo = await _userRoleRepository.GetUserRoleInfoAsync(user.Id, cancellationToken); + var roles = roleInfo.Select(r => r.RoleName).ToList(); + var roleIds = roleInfo.Select(r => r.RoleId).ToList(); // 创建用户声明 var claims = new List @@ -228,14 +231,17 @@ public abstract class BaseLoginCommandHandler : IRequestHan claims.Add(new Claim("LastLoginTime", user.LastLoginTime.Value.ToUniversalTime().ToString("o"))); } - // 添加角色声明 + // 添加角色名称声明(用于授权验证) claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role))); + // 添加角色ID声明(用于业务逻辑) + claims.AddRange(roleIds.Select(roleId => new Claim("RoleId", roleId))); + // 获取所有角色的权限 var permissionCodes = new HashSet(); - if (roles.Any()) + if (roleIds.Any()) { - var allRolePermissions = await _rolePermissionRepository.GetRolePermissionsByRolesAsync(roles, cancellationToken); + var allRolePermissions = await _rolePermissionRepository.GetRolePermissionsByRolesAsync(roleIds, cancellationToken); foreach (var rolePermission in allRolePermissions) { if (rolePermission.Permission != null) diff --git a/src/X1.Application/Features/Users/Commands/ToggleUserStatus/ToggleUserStatusCommand.cs b/src/X1.Application/Features/Users/Commands/ToggleUserStatus/ToggleUserStatusCommand.cs new file mode 100644 index 0000000..63a132b --- /dev/null +++ b/src/X1.Application/Features/Users/Commands/ToggleUserStatus/ToggleUserStatusCommand.cs @@ -0,0 +1,14 @@ +using X1.Domain.Common; +using MediatR; + +namespace X1.Application.Features.Users.Commands.ToggleUserStatus; + +/// +/// 切换用户状态命令 +/// 用于切换用户激活/禁用状态的命令对象 +/// +/// 要切换状态的用户ID +/// 新的激活状态 +public sealed record ToggleUserStatusCommand( + string UserId, + bool IsActive) : IRequest>; diff --git a/src/X1.Application/Features/Users/Commands/ToggleUserStatus/ToggleUserStatusCommandHandler.cs b/src/X1.Application/Features/Users/Commands/ToggleUserStatus/ToggleUserStatusCommandHandler.cs new file mode 100644 index 0000000..8214d9c --- /dev/null +++ b/src/X1.Application/Features/Users/Commands/ToggleUserStatus/ToggleUserStatusCommandHandler.cs @@ -0,0 +1,121 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using MediatR; +using X1.Domain.Entities; +using X1.Domain.Repositories; +using X1.Domain.Common; +using System.Threading.Tasks; +using System.Threading; +using System.Linq; +using System; +using X1.Domain.Repositories.Base; +using X1.Domain.Repositories.Identity; + +namespace X1.Application.Features.Users.Commands.ToggleUserStatus; + +/// +/// 切换用户状态命令处理器 +/// 处理用户激活/禁用状态的切换逻辑 +/// +public sealed class ToggleUserStatusCommandHandler : IRequestHandler> +{ + private readonly UserManager _userManager; + private readonly ILogger _logger; + private readonly IUnitOfWork _unitOfWork; + private readonly IUserRoleRepository _userRoleRepository; + + /// + /// 构造函数 + /// + /// 用户管理器 + /// 日志记录器 + /// 工作单元 + public ToggleUserStatusCommandHandler( + UserManager userManager, + ILogger logger, + IUnitOfWork unitOfWork, + IUserRoleRepository userRoleRepository) + { + _userManager = userManager; + _logger = logger; + _unitOfWork = unitOfWork; + _userRoleRepository = userRoleRepository; + } + + /// + /// 处理切换用户状态命令 + /// + /// 切换状态命令 + /// 取消令牌 + /// 操作结果,包含切换后的状态信息 + public async Task> Handle( + ToggleUserStatusCommand request, + CancellationToken cancellationToken) + { + try + { + _logger.LogInformation("开始切换用户 {UserId} 状态为 {IsActive}", request.UserId, request.IsActive); + + var user = await _userManager.FindByIdAsync(request.UserId); + if (user == null) + { + _logger.LogWarning("用户 {UserId} 不存在", request.UserId); + return OperationResult.CreateFailure("用户不存在"); + } + + // 检查是否为管理员用户,如果是管理员且要禁用,则不允许 + var userRoles = await _userRoleRepository.GetUserRoleWithNavigationAsync(user.Id); + + if (userRoles?.Role == null) + { + _logger.LogWarning("用户 {UserId} 没有分配角色", request.UserId); + return OperationResult.CreateFailure("用户没有分配角色"); + } + + var isAdmin = RoleConstants.IsAdminRole(userRoles.Role.Name ?? string.Empty); + + // 如果要禁用用户且该用户是管理员,则不允许 + if (!request.IsActive && isAdmin) + { + _logger.LogWarning("尝试禁用管理员用户 {UserId},操作被拒绝", request.UserId); + return OperationResult.CreateFailure("不能禁用管理员用户"); + } + + // 检查状态是否已经是指定状态 + if (user.IsActive == request.IsActive) + { + _logger.LogInformation("用户 {UserId} 状态已经是 {IsActive},无需切换", request.UserId, request.IsActive); + return OperationResult.CreateSuccess( + new ToggleUserStatusResponse(user.Id, user.IsActive)); + } + + // 更新用户状态 + user.IsActive = request.IsActive; + + // 在事务中更新用户状态 + await _unitOfWork.ExecuteTransactionAsync(async () => + { + var result = await _userManager.UpdateAsync(user); + if (!result.Succeeded) + { + var errors = result.Errors.Select(e => e.Description).ToList(); + _logger.LogWarning("切换用户 {UserId} 状态失败: {Errors}", request.UserId, string.Join(", ", errors)); + throw new InvalidOperationException(string.Join(", ", errors)); + } + + // 保存所有更改到数据库 + await _unitOfWork.SaveChangesAsync(cancellationToken); + }, cancellationToken: cancellationToken); + + _logger.LogInformation("用户 {UserId} 状态切换成功,新状态: {IsActive}", user.Id, user.IsActive); + + return OperationResult.CreateSuccess( + new ToggleUserStatusResponse(user.Id, user.IsActive)); + } + catch (Exception ex) + { + _logger.LogError(ex, "切换用户 {UserId} 状态时发生错误", request.UserId); + return OperationResult.CreateFailure("切换用户状态失败"); + } + } +} diff --git a/src/X1.Application/Features/Users/Commands/ToggleUserStatus/ToggleUserStatusResponse.cs b/src/X1.Application/Features/Users/Commands/ToggleUserStatus/ToggleUserStatusResponse.cs new file mode 100644 index 0000000..0302e53 --- /dev/null +++ b/src/X1.Application/Features/Users/Commands/ToggleUserStatus/ToggleUserStatusResponse.cs @@ -0,0 +1,11 @@ +namespace X1.Application.Features.Users.Commands.ToggleUserStatus; + +/// +/// 切换用户状态响应 +/// 包含切换状态后的用户信息 +/// +/// 用户ID +/// 新的激活状态 +public sealed record ToggleUserStatusResponse( + string UserId, + bool IsActive); diff --git a/src/X1.Application/Features/Users/Queries/Dtos/UserSimpleDto.cs b/src/X1.Application/Features/Users/Queries/Dtos/UserSimpleDto.cs new file mode 100644 index 0000000..48abc1e --- /dev/null +++ b/src/X1.Application/Features/Users/Queries/Dtos/UserSimpleDto.cs @@ -0,0 +1,23 @@ +namespace X1.Application.Features.Users.Queries.Dtos; + +/// +/// 用户简单数据传输对象 +/// 用于在API层和领域层之间传输用户数据(不包含权限信息) +/// +/// 用户ID +/// 用户名 +/// 真实姓名 +/// 电子邮箱 +/// 电话号码 +/// 创建时间 +/// 是否激活 +/// 角色名称列表 +public sealed record UserSimpleDto( + string Id, + string UserName, + string RealName, + string Email, + string PhoneNumber, + DateTime CreatedAt, + bool IsActive, + List Roles); diff --git a/src/X1.Application/Features/Users/Queries/GetAllUsers/GetAllUsersQueryHandler.cs b/src/X1.Application/Features/Users/Queries/GetAllUsers/GetAllUsersQueryHandler.cs index 95342d9..de7991b 100644 --- a/src/X1.Application/Features/Users/Queries/GetAllUsers/GetAllUsersQueryHandler.cs +++ b/src/X1.Application/Features/Users/Queries/GetAllUsers/GetAllUsersQueryHandler.cs @@ -32,6 +32,7 @@ public sealed class GetAllUsersQueryHandler : IRequestHandler用户管理器,用于管理用户身份 /// 角色管理器,用于管理角色信息 /// 用户角色仓储 + /// 角色权限仓储 /// 日志记录器 public GetAllUsersQueryHandler( UserManager userManager, @@ -65,17 +66,17 @@ public sealed class GetAllUsersQueryHandler : IRequestHandler u.UserName.Contains(request.UserName)); + query = query.Where(u => u.UserName != null && u.UserName.Contains(request.UserName)); } if (!string.IsNullOrWhiteSpace(request.Email)) { - query = query.Where(u => u.Email.Contains(request.Email)); + query = query.Where(u => u.Email != null && u.Email.Contains(request.Email)); } if (!string.IsNullOrWhiteSpace(request.PhoneNumber)) { - query = query.Where(u => u.PhoneNumber.Contains(request.PhoneNumber)); + query = query.Where(u => u.PhoneNumber != null && u.PhoneNumber.Contains(request.PhoneNumber)); } // 获取总记录数 @@ -88,7 +89,7 @@ public sealed class GetAllUsersQueryHandler : IRequestHandler + /// 批量获取用户角色信息(简化版本:不包含权限信息) + /// + /// 用户列表 + /// 取消令牌 + /// 包含角色信息的用户简单DTO列表 + private async Task> GetUsersWithRolesSimpleAsync(List users, CancellationToken cancellationToken) + { + if (!users.Any()) + return new List(); + + // 一次性获取所有角色信息(角色数据不会特别多) + var allRoles = await _roleManager.Roles.ToDictionaryAsync(r => r.Id, r => r, cancellationToken); + + // 获取所有用户的ID + var userIds = users.Select(u => u.Id).ToList(); + + // 批量获取所有用户的角色信息(避免循环查询) + var allUserRoles = await _userRoleRepository.GetUsersRolesAsync(userIds, cancellationToken); + + var userDtos = new List(); + + foreach (var user in users) + { + // 从批量结果中获取当前用户的角色 + var userRoleIds = allUserRoles.ContainsKey(user.Id) ? allUserRoles[user.Id] : new List(); + + // 获取角色名称 + var roleNames = new List(); + + foreach (var roleId in userRoleIds) + { + if (allRoles.TryGetValue(roleId, out var role) && role.Name != null) + { + roleNames.Add(role.Name); + } + } + + var dto = new UserSimpleDto( + user.Id, + user.UserName ?? string.Empty, + user.RealName ?? string.Empty, + user.Email ?? string.Empty, + user.PhoneNumber ?? string.Empty, + user.CreatedTime, + user.IsActive, + roleNames); + + userDtos.Add(dto); + } + + return userDtos; + } } \ No newline at end of file diff --git a/src/X1.Application/Features/Users/Queries/GetAllUsers/GetAllUsersResponse.cs b/src/X1.Application/Features/Users/Queries/GetAllUsers/GetAllUsersResponse.cs index a6ccc2f..ecbd082 100644 --- a/src/X1.Application/Features/Users/Queries/GetAllUsers/GetAllUsersResponse.cs +++ b/src/X1.Application/Features/Users/Queries/GetAllUsers/GetAllUsersResponse.cs @@ -13,7 +13,7 @@ namespace X1.Application.Features.Users.Queries.GetAllUsers; /// 当前页码 /// 每页记录数 public sealed record GetAllUsersResponse( - List Users, + List Users, int TotalCount, int PageNumber, int PageSize) diff --git a/src/X1.Domain/Common/RoleConstants.cs b/src/X1.Domain/Common/RoleConstants.cs new file mode 100644 index 0000000..84bbcca --- /dev/null +++ b/src/X1.Domain/Common/RoleConstants.cs @@ -0,0 +1,95 @@ +namespace X1.Domain.Common; + +/// +/// 系统角色常量定义 +/// 提供系统中所有预定义角色的常量值 +/// +public static class RoleConstants +{ + /// + /// 系统管理员角色 + /// + public const string Admin = "Admin"; + + /// + /// 部门经理角色 + /// + public const string Manager = "Manager"; + + /// + /// 普通用户角色 + /// + public const string User = "User"; + + /// + /// 操作员角色 + /// + public const string Operator = "Operator"; + + /// + /// 财务人员角色 + /// + public const string Finance = "Finance"; + + /// + /// 人力资源角色 + /// + public const string HR = "HR"; + + /// + /// 客服人员角色 + /// + public const string CustomerService = "CustomerService"; + + /// + /// 技术支持角色 + /// + public const string TechnicalSupport = "TechnicalSupport"; + + /// + /// 数据分析师角色 + /// + public const string DataAnalyst = "DataAnalyst"; + + /// + /// 审计人员角色 + /// + public const string Auditor = "Auditor"; + + /// + /// 获取所有系统角色 + /// + public static readonly string[] AllRoles = + { + Admin, + Manager, + User, + Operator, + Finance, + HR, + CustomerService, + TechnicalSupport, + DataAnalyst, + Auditor + }; + + /// + /// 检查是否为管理员角色 + /// + /// 角色名称 + /// 如果是管理员角色返回true,否则返回false + public static bool IsAdminRole(string roleName) + { + return string.Equals(roleName, Admin, StringComparison.OrdinalIgnoreCase); + } + + /// + /// 检查是否为系统关键角色(不允许禁用的角色) + /// + /// 角色名称 + /// 如果是关键角色返回true,否则返回false + public static bool IsCriticalRole(string roleName) + { + return IsAdminRole(roleName); + } +} diff --git a/src/X1.Domain/Models/UserRoleInfo.cs b/src/X1.Domain/Models/UserRoleInfo.cs new file mode 100644 index 0000000..9f9ccc3 --- /dev/null +++ b/src/X1.Domain/Models/UserRoleInfo.cs @@ -0,0 +1,79 @@ +namespace X1.Domain.Models; + +/// +/// 用户角色信息DTO +/// 包含用户角色的ID和名称信息 +/// +public class UserRoleInfo +{ + /// + /// 角色ID + /// + public string RoleId { get; set; } = string.Empty; + + /// + /// 角色名称 + /// + public string RoleName { get; set; } = string.Empty; + + /// + /// 构造函数 + /// + public UserRoleInfo() + { + } + + /// + /// 构造函数 + /// + /// 角色ID + /// 角色名称 + public UserRoleInfo(string roleId, string roleName) + { + RoleId = roleId; + RoleName = roleName; + } + + /// + /// 创建UserRoleInfo实例 + /// + /// 角色ID + /// 角色名称 + /// UserRoleInfo实例 + public static UserRoleInfo Create(string roleId, string roleName) + { + return new UserRoleInfo(roleId, roleName); + } + + /// + /// 转换为字符串表示 + /// + /// 字符串表示 + public override string ToString() + { + return $"RoleId: {RoleId}, RoleName: {RoleName}"; + } + + /// + /// 比较两个UserRoleInfo是否相等 + /// + /// 要比较的对象 + /// 是否相等 + public override bool Equals(object? obj) + { + if (obj is UserRoleInfo other) + { + return RoleId == other.RoleId && RoleName == other.RoleName; + } + return false; + } + + /// + /// 获取哈希码 + /// + /// 哈希码 + public override int GetHashCode() + { + return HashCode.Combine(RoleId, RoleName); + } +} diff --git a/src/X1.Domain/Repositories/Identity/IUserRoleRepository.cs b/src/X1.Domain/Repositories/Identity/IUserRoleRepository.cs index 9f9a353..de472ac 100644 --- a/src/X1.Domain/Repositories/Identity/IUserRoleRepository.cs +++ b/src/X1.Domain/Repositories/Identity/IUserRoleRepository.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using X1.Domain.Entities; +using X1.Domain.Models; using X1.Domain.Repositories.Base; namespace X1.Domain.Repositories.Identity; @@ -22,6 +23,16 @@ public interface IUserRoleRepository : IBaseRepository /// Task> GetUserRolesAsync(string userId, CancellationToken cancellationToken = default); + /// + /// 获取用户的所有角色ID + /// + Task> GetUserRoleIdsAsync(string userId, CancellationToken cancellationToken = default); + + /// + /// 获取用户角色信息(包含ID和名称) + /// + Task> GetUserRoleInfoAsync(string userId, CancellationToken cancellationToken = default); + /// /// 检查用户是否拥有指定角色 /// @@ -31,4 +42,9 @@ public interface IUserRoleRepository : IBaseRepository /// 批量获取多个用户的角色信息 /// Task>> GetUsersRolesAsync(IEnumerable userIds, CancellationToken cancellationToken = default); + + /// + /// 获取用户角色关系实体(包含导航属性) + /// + Task GetUserRoleWithNavigationAsync(string userId, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/X1.Infrastructure/Repositories/Identity/UserRoleRepository.cs b/src/X1.Infrastructure/Repositories/Identity/UserRoleRepository.cs index bcd9710..2f94d10 100644 --- a/src/X1.Infrastructure/Repositories/Identity/UserRoleRepository.cs +++ b/src/X1.Infrastructure/Repositories/Identity/UserRoleRepository.cs @@ -3,7 +3,9 @@ using System.Threading; using System.Threading.Tasks; using System.Linq; using Microsoft.Extensions.Logging; +using Microsoft.EntityFrameworkCore; using X1.Domain.Entities; +using X1.Domain.Models; using X1.Domain.Repositories; using X1.Infrastructure.Repositories.Base; using X1.Domain.Repositories.Base; @@ -43,6 +45,19 @@ public class UserRoleRepository : BaseRepository, IUserRoleRepository /// 获取用户的所有角色 /// public async Task> GetUserRolesAsync(string userId, CancellationToken cancellationToken = default) + { + var userRoles = await QueryRepository.FindAsync( + ur => ur.UserId == userId, + include: q => q.Include(ur => ur.Role), + cancellationToken: cancellationToken); + + return userRoles.Where(ur => ur.Role != null && !string.IsNullOrEmpty(ur.Role.Name)).Select(ur => ur.Role!.Name!).ToList(); + } + + /// + /// 获取用户的所有角色ID + /// + public async Task> GetUserRoleIdsAsync(string userId, CancellationToken cancellationToken = default) { var userRoles = await QueryRepository.FindAsync( ur => ur.UserId == userId, @@ -51,6 +66,22 @@ public class UserRoleRepository : BaseRepository, IUserRoleRepository return userRoles.Select(ur => ur.RoleId).ToList(); } + /// + /// 获取用户角色信息(包含ID和名称) + /// + public async Task> GetUserRoleInfoAsync(string userId, CancellationToken cancellationToken = default) + { + var userRoles = await QueryRepository.FindAsync( + ur => ur.UserId == userId, + include: q => q.Include(ur => ur.Role), + cancellationToken: cancellationToken); + + return userRoles + .Where(ur => ur.Role != null && !string.IsNullOrEmpty(ur.Role.Name)) + .Select(ur => UserRoleInfo.Create(ur.RoleId, ur.Role!.Name!)) + .ToList(); + } + /// /// 检查用户是否拥有指定角色 /// @@ -68,13 +99,26 @@ public class UserRoleRepository : BaseRepository, IUserRoleRepository { var userRoles = await QueryRepository.FindAsync( ur => userIds.Contains(ur.UserId), + include: q => q.Include(ur => ur.Role), cancellationToken: cancellationToken); return userRoles + .Where(ur => ur.Role != null && !string.IsNullOrEmpty(ur.Role.Name)) .GroupBy(ur => ur.UserId) .ToDictionary( g => g.Key, - g => g.Select(ur => ur.RoleId).ToList()); + g => g.Select(ur => ur.Role!.Name!).ToList()); + } + + /// + /// 获取用户角色关系实体(包含导航属性) + /// + public async Task GetUserRoleWithNavigationAsync(string userId, CancellationToken cancellationToken = default) + { + return await QueryRepository.FirstOrDefaultAsync( + ur => ur.UserId == userId, + include: q => q.Include(ur => ur.User).Include(ur => ur.Role), + cancellationToken: cancellationToken); } #endregion diff --git a/src/X1.Infrastructure/Services/UserManagement/UserRegistrationService.cs b/src/X1.Infrastructure/Services/UserManagement/UserRegistrationService.cs index 21a84b2..084b89a 100644 --- a/src/X1.Infrastructure/Services/UserManagement/UserRegistrationService.cs +++ b/src/X1.Infrastructure/Services/UserManagement/UserRegistrationService.cs @@ -10,6 +10,7 @@ using Microsoft.EntityFrameworkCore; using X1.Domain.Repositories; using System.Threading; using X1.Domain.Repositories.Identity; +using X1.Domain.Common; namespace X1.Infrastructure.Services.UserManagement; @@ -108,7 +109,7 @@ public class UserRegistrationService : IUserRegistrationService // 检查是否是第一个用户 var isFirstUser = !await _userManager.Users.AnyAsync(); - string roleName = isFirstUser ? "User" : "Admin"; + string roleName = isFirstUser ? RoleConstants.User : RoleConstants.Admin; // 获取或创建角色 var role = await _roleManager.FindByNameAsync(roleName); diff --git a/src/X1.Presentation/Controllers/NavigationMenuController.cs b/src/X1.Presentation/Controllers/NavigationMenuController.cs index 15ed208..7424f96 100644 --- a/src/X1.Presentation/Controllers/NavigationMenuController.cs +++ b/src/X1.Presentation/Controllers/NavigationMenuController.cs @@ -18,7 +18,7 @@ namespace X1.Presentation.Controllers; /// 导航菜单管理控制器 /// 提供导航菜单管理相关的 API 接口,包括创建、更新、删除和查询导航菜单功能 /// -//[Authorize(Roles = "Admin")] // 只有管理员可以访问 +[Authorize(Roles = RoleConstants.Admin)] // 只有管理员可以访问 [Route("api/navigation-menus")] [ApiController] public class NavigationMenuController : ApiController diff --git a/src/X1.Presentation/Controllers/PermissionsController.cs b/src/X1.Presentation/Controllers/PermissionsController.cs index 3491e48..4377c84 100644 --- a/src/X1.Presentation/Controllers/PermissionsController.cs +++ b/src/X1.Presentation/Controllers/PermissionsController.cs @@ -20,7 +20,7 @@ namespace X1.Presentation.Controllers; /// 权限管理控制器 /// 提供权限管理相关的 API 接口,包括创建、更新、删除和查询权限功能 /// -//[Authorize(Roles = "Admin")] // 只有管理员可以访问 +[Authorize(Roles = RoleConstants.Admin)] // 只有管理员可以访问 [Route("api/permissions")] [ApiController] public class PermissionsController : ApiController diff --git a/src/X1.Presentation/Controllers/RolesController.cs b/src/X1.Presentation/Controllers/RolesController.cs index 3316111..72e84d4 100644 --- a/src/X1.Presentation/Controllers/RolesController.cs +++ b/src/X1.Presentation/Controllers/RolesController.cs @@ -17,7 +17,7 @@ namespace X1.Presentation.Controllers; /// 角色管理控制器 /// 提供角色管理相关的 API 接口,包括创建、删除和查询角色功能 /// -//[Authorize(Roles = "Admin")] // 只有管理员可以访问 +[Authorize(Roles = RoleConstants.Admin)] // 只有管理员可以访问 [Route("api/roles")] [ApiController] public class RolesController : ApiController diff --git a/src/X1.Presentation/Controllers/UsersController.cs b/src/X1.Presentation/Controllers/UsersController.cs index ebee1c6..abd1c8e 100644 --- a/src/X1.Presentation/Controllers/UsersController.cs +++ b/src/X1.Presentation/Controllers/UsersController.cs @@ -3,6 +3,7 @@ using MediatR; using X1.Application.Features.Users.Commands.CreateUser; using X1.Application.Features.Users.Commands.UpdateUser; using X1.Application.Features.Users.Commands.DeleteUser; +using X1.Application.Features.Users.Commands.ToggleUserStatus; using X1.Application.Features.Users.Queries.GetUserById; using X1.Application.Features.Users.Queries.GetAllUsers; using X1.Application.Features.Users.Queries.GetCurrentUser; @@ -306,6 +307,61 @@ public class UsersController : ApiController } } + /// + /// 切换用户状态 + /// + /// + /// 示例请求: + /// + /// PUT /api/users/{id}/status + /// { + /// "isActive": true + /// } + /// + /// + /// 用户ID + /// 新的激活状态 + /// + /// 切换结果,包含: + /// - 成功:返回切换后的状态信息 + /// - 失败:返回错误信息 + /// + /// 切换成功,返回状态信息 + /// 切换失败,返回错误信息 + /// 用户不存在 + [HttpPut("{id}/status")] + [ProducesResponseType(typeof(OperationResult), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(OperationResult), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(OperationResult), StatusCodes.Status404NotFound)] + public async Task> ToggleUserStatus( + [FromRoute] string id, + [FromBody] bool isActive) + { + try + { + var command = new ToggleUserStatusCommand(id, isActive); + var result = await mediator.Send(command); + + if (result.IsSuccess) + { + _logger.LogInformation("切换用户 {UserId} 状态成功,新状态: {IsActive}", id, isActive); + } + else + { + _logger.LogWarning("切换用户 {UserId} 状态失败: {Error}", + id, + result.ErrorMessages?.FirstOrDefault()); + } + + return result; + } + catch (Exception ex) + { + _logger.LogError(ex, "切换用户 {UserId} 状态时发生异常", id); + return OperationResult.CreateFailure("系统错误,请稍后重试"); + } + } + /// /// 获取当前登录用户信息 /// diff --git a/src/X1.WebUI/src/components/dashboard/StatsOverview.tsx b/src/X1.WebUI/src/components/dashboard/StatsOverview.tsx index 7064366..02a6b27 100644 --- a/src/X1.WebUI/src/components/dashboard/StatsOverview.tsx +++ b/src/X1.WebUI/src/components/dashboard/StatsOverview.tsx @@ -7,8 +7,7 @@ import { GitBranch, Activity, CheckCircle, - XCircle, - Clock + XCircle } from 'lucide-react'; import { DashboardStatistics } from '@/services/dashboardService'; diff --git a/src/X1.WebUI/src/components/ui/StatusSwitch.tsx b/src/X1.WebUI/src/components/ui/StatusSwitch.tsx index 9337829..5b601b2 100644 --- a/src/X1.WebUI/src/components/ui/StatusSwitch.tsx +++ b/src/X1.WebUI/src/components/ui/StatusSwitch.tsx @@ -30,60 +30,49 @@ const StatusSwitch: React.FC = ({ return (
{/* 文字 */} {checked ? activeText : inactiveText} {/* 圆点 */}
diff --git a/src/X1.WebUI/src/pages/auth/LoginPage.tsx b/src/X1.WebUI/src/pages/auth/LoginPage.tsx index 71e4cf2..290e7eb 100644 --- a/src/X1.WebUI/src/pages/auth/LoginPage.tsx +++ b/src/X1.WebUI/src/pages/auth/LoginPage.tsx @@ -50,7 +50,7 @@ export function LoginPage() {
-
+ {/*
还没有账号? -
+
*/}
diff --git a/src/X1.WebUI/src/pages/task-execution-process/StepDetailsView.tsx b/src/X1.WebUI/src/pages/task-execution-process/StepDetailsView.tsx index 62cf2ff..c8b9803 100644 --- a/src/X1.WebUI/src/pages/task-execution-process/StepDetailsView.tsx +++ b/src/X1.WebUI/src/pages/task-execution-process/StepDetailsView.tsx @@ -16,7 +16,6 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; -import { ScrollArea } from '@/components/ui/scroll-area'; import { useToast } from '@/components/ui/use-toast'; import { Play, diff --git a/src/X1.WebUI/src/pages/task-execution-process/TaskExecutionProcessView.tsx b/src/X1.WebUI/src/pages/task-execution-process/TaskExecutionProcessView.tsx index e4304de..ea2bbb2 100644 --- a/src/X1.WebUI/src/pages/task-execution-process/TaskExecutionProcessView.tsx +++ b/src/X1.WebUI/src/pages/task-execution-process/TaskExecutionProcessView.tsx @@ -9,7 +9,6 @@ import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { useToast } from '@/components/ui/use-toast'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Separator } from '@/components/ui/separator'; import { ScrollArea } from '@/components/ui/scroll-area'; import TaskExecutionTree from './TaskExecutionTree'; import StepDetailsView from './StepDetailsView'; diff --git a/src/X1.WebUI/src/pages/users/UserForm.tsx b/src/X1.WebUI/src/pages/users/UserForm.tsx index f6d90dc..da6a3cd 100644 --- a/src/X1.WebUI/src/pages/users/UserForm.tsx +++ b/src/X1.WebUI/src/pages/users/UserForm.tsx @@ -17,7 +17,7 @@ export default function UserForm({ onSubmit, initialData }: UserFormProps) { email: initialData?.email || '', phoneNumber: initialData?.phoneNumber || '', password: '', - roleIds: initialData?.roleIds || [] + roleIds: [] }); const [roles, setRoles] = useState([]); @@ -28,12 +28,32 @@ export default function UserForm({ onSubmit, initialData }: UserFormProps) { setLoading(true); const result = await roleService.getAllRoles(); if (result.isSuccess && result.data) { - setRoles(result.data.roles || []); + const allRoles = result.data.roles || []; + setRoles(allRoles); + + // 如果有初始数据且包含角色信息,需要将角色名称转换为角色ID + if (initialData?.roleIds && Array.isArray(initialData.roleIds)) { + // 如果 roleIds 是角色名称数组(来自 User.roles),需要转换为角色ID + const roleIds = allRoles + .filter(role => initialData.roleIds!.includes(role.name)) + .map(role => role.id); + + setFormData(prev => ({ + ...prev, + roleIds: roleIds + })); + } else if (initialData?.roleIds) { + // 如果 roleIds 已经是角色ID数组,直接使用 + setFormData(prev => ({ + ...prev, + roleIds: initialData.roleIds! + })); + } } setLoading(false); }; fetchRoles(); - }, []); + }, [initialData]); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); diff --git a/src/X1.WebUI/src/pages/users/UserRolesForm.tsx b/src/X1.WebUI/src/pages/users/UserRolesForm.tsx index 913505a..8adb396 100644 --- a/src/X1.WebUI/src/pages/users/UserRolesForm.tsx +++ b/src/X1.WebUI/src/pages/users/UserRolesForm.tsx @@ -25,7 +25,7 @@ export default function UserRolesForm({ user, onSubmit }: UserRolesFormProps) { const form = useForm({ defaultValues: { - roleIds: user.roleIds || [], + roleIds: [] as string[], }, }); @@ -34,12 +34,23 @@ export default function UserRolesForm({ user, onSubmit }: UserRolesFormProps) { setLoading(true); const result = await roleService.getAllRoles(); if (result.isSuccess && result.data) { - setRoles(result.data.roles || []); + const allRoles = result.data.roles || []; + setRoles(allRoles); + + // 根据用户当前的角色名称,找到对应的角色ID + const userRoleIds = allRoles + .filter(role => user.roles?.includes(role.name)) + .map(role => role.id); + + // 设置表单的默认值 + form.reset({ + roleIds: userRoleIds, + }); } setLoading(false); }; fetchRoles(); - }, []); + }, [user.roles, form]); const handleSubmit = (data: { roleIds: string[] }) => { onSubmit(data.roleIds); diff --git a/src/X1.WebUI/src/pages/users/UserTable.tsx b/src/X1.WebUI/src/pages/users/UserTable.tsx index 75ba991..87bb846 100644 --- a/src/X1.WebUI/src/pages/users/UserTable.tsx +++ b/src/X1.WebUI/src/pages/users/UserTable.tsx @@ -12,6 +12,7 @@ import { User } from '@/services/userService'; import { formatToBeijingTime } from '@/lib/utils'; import { DensityType } from '@/components/ui/TableToolbar'; import { userService } from '@/services/userService'; +import { useToast } from '@/components/ui/use-toast'; import StatusSwitch from '@/components/ui/StatusSwitch'; @@ -49,6 +50,7 @@ export default function UserTable({ columns = [], onRefresh, }: UserTableProps) { + const { toast } = useToast(); const Wrapper = hideCard ? React.Fragment : 'div'; const wrapperProps = hideCard ? {} : { className: 'rounded-md border bg-background' }; @@ -96,7 +98,7 @@ export default function UserTable({ ) : ( users.map((user) => ( - + {visibleColumns.map(col => { switch (col.key) { case 'userName': @@ -112,8 +114,30 @@ export default function UserTable({ { - await userService.updateUser(user.userId, { ...user, isActive: !user.isActive }); - if (onRefresh) onRefresh(); + try { + const result = await userService.toggleUserStatus(user.id, !user.isActive); + if (result.isSuccess) { + toast({ + title: "状态切换成功", + description: `用户 ${user.userName} 状态已更新为 ${!user.isActive ? '正常' : '禁用'}`, + }); + if (onRefresh) { + onRefresh(); + } + } else { + toast({ + title: "状态切换失败", + description: result.errorMessages?.join(', ') || '操作失败', + variant: "destructive", + }); + } + } catch (error) { + toast({ + title: "状态切换失败", + description: "网络错误,请稍后重试", + variant: "destructive", + }); + } }} disabled={loading} /> @@ -121,7 +145,7 @@ export default function UserTable({ ); case 'roles': - return {Array.isArray(user.roleNames) && user.roleNames.length > 0 ? user.roleNames.join(', ') : '-'}; + return {Array.isArray(user.roles) && user.roles.length > 0 ? user.roles.join(', ') : '-'}; case 'createdAt': return {user.createdAt ? formatToBeijingTime(user.createdAt) : '-'}; case 'actions': @@ -146,7 +170,7 @@ export default function UserTable({ )} onDelete(user.userId)} + onClick={() => onDelete(user.id)} > 删除 diff --git a/src/X1.WebUI/src/pages/users/UsersView.tsx b/src/X1.WebUI/src/pages/users/UsersView.tsx index a1ec40d..207478b 100644 --- a/src/X1.WebUI/src/pages/users/UsersView.tsx +++ b/src/X1.WebUI/src/pages/users/UsersView.tsx @@ -68,7 +68,7 @@ export default function UsersView() { const handleEdit = async (data: { userName: string; email: string; phoneNumber?: string; roles?: string[] }) => { if (!selectedUser) return; - const result = await userService.updateUser(selectedUser.userId, data); + const result = await userService.updateUser(selectedUser.id, data); if (result.isSuccess) { setEditOpen(false); setSelectedUser(null); @@ -78,7 +78,7 @@ export default function UsersView() { const handleSetRoles = async (roleIds: string[]) => { if (!selectedUser) return; - const result = await userService.updateUserRoles(selectedUser.userId, roleIds); + const result = await userService.updateUserRoles(selectedUser.id, roleIds); if (result.isSuccess) { setRolesOpen(false); setSelectedUser(null); @@ -223,7 +223,7 @@ export default function UsersView() { userName: selectedUser.userName, email: selectedUser.email, phoneNumber: selectedUser.phoneNumber, - roleIds: selectedUser.roleIds + roleIds: selectedUser.roles }} /> )} diff --git a/src/X1.WebUI/src/services/userService.ts b/src/X1.WebUI/src/services/userService.ts index ab393c5..df64ac1 100644 --- a/src/X1.WebUI/src/services/userService.ts +++ b/src/X1.WebUI/src/services/userService.ts @@ -3,15 +3,14 @@ import { OperationResult } from '@/types/auth'; import { API_PATHS } from '@/constants/api'; export interface User { - userId: string; + id: string; userName: string; realName: string; email: string; - phoneNumber?: string; + phoneNumber: string; createdAt: string; isActive: boolean; - roleIds: string[]; - roleNames: string[]; + roles: string[]; } // 获取用户列表请求接口 @@ -89,6 +88,11 @@ class UserService { async deleteUser(userId: string): Promise> { return httpClient.delete(`${this.baseUrl}/${userId}`); } + + // 切换用户状态 + async toggleUserStatus(userId: string, isActive: boolean): Promise> { + return httpClient.put<{ userId: string; isActive: boolean }>(`${this.baseUrl}/${userId}/status`, isActive); + } } export const userService = new UserService(); \ No newline at end of file diff --git a/src/modify_20250121_role_claims_fix.md b/src/modify_20250121_role_claims_fix.md new file mode 100644 index 0000000..1d3aafb --- /dev/null +++ b/src/modify_20250121_role_claims_fix.md @@ -0,0 +1,129 @@ +# 2025-01-21 修复角色Claims问题 - RolesAuthorizationRequirement验证失败 + +## 概述 +修复了 `BaseLoginCommandHandler` 中角色Claims配置问题,解决了 `RolesAuthorizationRequirement:User.IsInRole must be true for one of the following roles: (Admin)` 错误。 + +## 问题分析 +- **根本原因**: `UserRoleRepository.GetUserRolesAsync` 方法返回的是角色ID(GUID字符串)而不是角色名称 +- **影响范围**: 导致JWT令牌中的角色Claims包含角色ID而非角色名称,使得基于角色的授权验证失败 +- **错误表现**: 系统期望角色名称为"Admin",但实际Claims中包含的是角色ID + +## 主要变更 + +### 1. 修复 UserRoleRepository.GetUserRolesAsync 方法 +- **文件**: `X1.Infrastructure/Repositories/Identity/UserRoleRepository.cs` +- **问题**: 原方法返回 `ur.RoleId`(角色ID) +- **修复**: 改为返回 `ur.Role.Name`(角色名称) +- **技术实现**: + - 添加 `include: q => q.Include(ur => ur.Role)` 加载导航属性 + - 添加空值检查 `Where(ur => ur.Role != null)` + - 返回角色名称 `Select(ur => ur.Role!.Name)` + +### 2. 修复 UserRoleRepository.GetUsersRolesAsync 方法 +- **文件**: `X1.Infrastructure/Repositories/Identity/UserRoleRepository.cs` +- **问题**: 批量获取用户角色时也返回角色ID +- **修复**: 同样改为返回角色名称 +- **一致性**: 确保所有角色获取方法都返回角色名称 + +## 技术实现 + +### 修复前的问题代码 +```csharp +// 返回角色ID(GUID字符串) +return userRoles.Select(ur => ur.RoleId).ToList(); +``` + +### 修复后的正确代码 +```csharp +// 返回角色名称(如"Admin") +var userRoles = await QueryRepository.FindAsync( + ur => ur.UserId == userId, + include: q => q.Include(ur => ur.Role), + cancellationToken: cancellationToken); + +return userRoles.Where(ur => ur.Role != null).Select(ur => ur.Role!.Name).ToList(); +``` + +## 影响范围 +- **JWT令牌生成**: 现在正确包含角色名称而不是角色ID +- **角色授权验证**: `[Authorize(Roles = "Admin")]` 现在能正确工作 +- **用户权限检查**: 基于角色的权限验证恢复正常 +- **前端权限控制**: 前端基于角色的UI控制恢复正常 + +## 验证方法 +1. 用户登录后检查JWT令牌中的角色Claims +2. 访问需要Admin角色的API端点 +3. 验证角色授权是否正常工作 +4. 检查前端基于角色的UI显示 + +## 增强功能 - 同时支持角色ID和角色名称 + +### 新增功能 +为了提供更完整的角色信息支持,在原有修复基础上增加了角色ID的Claims支持: + +#### 1. 新增 UserRoleRepository 方法 +- **GetUserRoleIdsAsync**: 获取用户的所有角色ID +- **GetUserRoleInfoAsync**: 获取用户角色信息(同时包含ID和名称) + +#### 2. 增强 BaseLoginCommandHandler Claims +- **角色名称Claims**: `ClaimTypes.Role` - 用于授权验证(如 `[Authorize(Roles = "Admin")]`) +- **角色ID Claims**: `"RoleId"` - 用于业务逻辑处理 + +#### 3. 技术实现 +```csharp +// 获取用户角色信息(包含ID和名称) +var roleInfo = await _userRoleRepository.GetUserRoleInfoAsync(user.Id, cancellationToken); +var roles = roleInfo.Select(r => r.RoleName).ToList(); +var roleIds = roleInfo.Select(r => r.RoleId).ToList(); + +// 添加角色名称声明(用于授权验证) +claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role))); + +// 添加角色ID声明(用于业务逻辑) +claims.AddRange(roleIds.Select(roleId => new Claim("RoleId", roleId))); +``` + +### 代码重构 - 使用DTO替代元组 + +为了提高代码可维护性,将元组 `(string RoleId, string RoleName)` 替换为专门的DTO类: + +#### 1. 创建UserRoleInfo DTO类 +- **文件**: `X1.Domain/Models/UserRoleInfo.cs` +- **功能**: 封装用户角色信息,包含RoleId和RoleName属性 +- **优势**: + - 更好的类型安全性 + - 更清晰的代码可读性 + - 便于扩展和维护 + - 符合面向对象设计原则 + +#### 2. 更新接口和实现 +- **IUserRoleRepository**: 方法签名改为返回 `IList` +- **UserRoleRepository**: 实现改为使用 `UserRoleInfo.Create()` 方法 +- **BaseLoginCommandHandler**: 使用DTO的属性访问方式 + +#### 3. DTO类特性 +```csharp +public class UserRoleInfo +{ + public string RoleId { get; set; } = string.Empty; + public string RoleName { get; set; } = string.Empty; + + public static UserRoleInfo Create(string roleId, string roleName) + { + return new UserRoleInfo(roleId, roleName); + } + + // 包含ToString、Equals、GetHashCode等方法 +} +``` + +### 使用场景 +- **角色名称**: 用于 `[Authorize(Roles = "Admin")]` 等授权验证 +- **角色ID**: 用于业务逻辑中的角色比较、权限检查等 + +## 相关文件 +- `X1.Domain/Models/UserRoleInfo.cs` - 新增DTO类 +- `X1.Infrastructure/Repositories/Identity/UserRoleRepository.cs` - 主要修复文件 +- `X1.Domain/Repositories/Identity/IUserRoleRepository.cs` - 接口定义 +- `X1.Application/Features/Auth/Commands/BaseLoginCommandHandler.cs` - 使用修复后的角色数据 +- `X1.Domain/Common/RoleConstants.cs` - 角色常量定义 diff --git a/src/modify_20250121_toggleuserstatus_fix.md b/src/modify_20250121_toggleuserstatus_fix.md new file mode 100644 index 0000000..90cecb7 --- /dev/null +++ b/src/modify_20250121_toggleuserstatus_fix.md @@ -0,0 +1,80 @@ +# 2025-01-21 修复 ToggleUserStatusCommandHandler 管理员检查逻辑 + +## 概述 +修复了 `ToggleUserStatusCommandHandler` 中管理员用户状态切换的逻辑错误,并添加了角色常量定义以提高代码可维护性。 + +## 主要变更 + +### 1. 修复逻辑错误 +- **文件**: `X1.Application/Features/Users/Commands/ToggleUserStatus/ToggleUserStatusCommandHandler.cs` +- **问题**: 原逻辑 `if (!isAdmin)` 错误,应该是 `if (isAdmin && !request.IsActive)` +- **修复**: 正确实现"如果要禁用用户且该用户是管理员,则不允许"的逻辑 + +### 2. 改进角色验证 +- **原逻辑**: 使用 `userRoles.Role.Name.Contains("Admin")` 可能匹配到其他包含"Admin"的角色 +- **新逻辑**: 使用 `RoleConstants.IsAdminRole()` 进行精确的角色名称匹配 +- **优势**: 更严谨的角色检查,避免误判 + +### 3. 添加角色常量定义 +- **文件**: `X1.Domain/Common/RoleConstants.cs` +- **功能**: 定义系统中所有预定义角色的常量 +- **包含角色**: Admin, Manager, User, Operator, Finance, HR, CustomerService, TechnicalSupport, DataAnalyst, Auditor +- **工具方法**: + - `IsAdminRole()` - 检查是否为管理员角色 + - `IsCriticalRole()` - 检查是否为系统关键角色 + - `AllRoles` - 获取所有系统角色数组 + +### 4. 增强错误处理 +- **空值检查**: 添加对 `userRoles?.Role` 的空值检查 +- **角色验证**: 确保用户已分配角色,未分配角色的用户返回错误 +- **日志记录**: 完善日志记录,包括用户没有分配角色的情况 + +## 技术实现 + +### 修复后的核心逻辑 +```csharp +// 检查是否为管理员用户,如果是管理员且要禁用,则不允许 +var userRoles = await _userRoleRepository.GetUserRoleWithNavigationAsync(user.Id); + +if (userRoles?.Role == null) +{ + _logger.LogWarning("用户 {UserId} 没有分配角色", request.UserId); + return OperationResult.CreateFailure("用户没有分配角色"); +} + +var isAdmin = RoleConstants.IsAdminRole(userRoles.Role.Name ?? string.Empty); + +// 如果要禁用用户且该用户是管理员,则不允许 +if (!request.IsActive && isAdmin) +{ + _logger.LogWarning("尝试禁用管理员用户 {UserId},操作被拒绝", request.UserId); + return OperationResult.CreateFailure("不能禁用管理员用户"); +} +``` + +### 角色常量定义 +```csharp +public static class RoleConstants +{ + public const string Admin = "Admin"; + public const string Manager = "Manager"; + // ... 其他角色常量 + + public static bool IsAdminRole(string roleName) + { + return string.Equals(roleName, Admin, StringComparison.OrdinalIgnoreCase); + } +} +``` + +## 影响范围 +- **安全性提升**: 确保管理员用户不能被意外禁用 +- **代码质量**: 使用常量替代硬编码字符串,提高可维护性 +- **错误处理**: 更完善的错误处理和用户反馈 +- **向后兼容**: 不影响现有功能,仅修复逻辑错误 + +## 测试建议 +1. 测试禁用管理员用户应该被拒绝 +2. 测试启用管理员用户应该成功 +3. 测试禁用普通用户应该成功 +4. 测试未分配角色的用户应该返回错误 diff --git a/src/modify_20250121_userrole_navigation_fix.md b/src/modify_20250121_userrole_navigation_fix.md new file mode 100644 index 0000000..1a0622d --- /dev/null +++ b/src/modify_20250121_userrole_navigation_fix.md @@ -0,0 +1,61 @@ +# 2025-01-21 修复 UserRole 实体导航属性加载问题 + +## 问题描述 +在 `ToggleUserStatusCommandHandler.cs` 中,使用 `FirstOrDefaultAsync` 查询 `UserRole` 实体时,`AppUser User` 属性有值,但 `AppRole Role` 属性为 `null`。 + +## 问题原因 +EF Core 默认不加载导航属性,`FirstOrDefaultAsync` 方法只加载实体的基本属性,不会自动加载导航属性(如 `User` 和 `Role`)。 + +## 解决方案 +按照领域驱动设计原则,将导航属性加载逻辑封装到仓储层,而不是在业务逻辑层处理。 + +### 修改文件 + +#### 1. 扩展 IUserRoleRepository 接口 +- **文件**: `X1.Domain/Repositories/Identity/IUserRoleRepository.cs` +- **新增方法**: + ```csharp + /// + /// 获取用户角色关系实体(包含导航属性) + /// + Task GetUserRoleWithNavigationAsync(string userId, CancellationToken cancellationToken = default); + ``` + +#### 2. 实现 UserRoleRepository 方法 +- **文件**: `X1.Infrastructure/Repositories/Identity/UserRoleRepository.cs` +- **添加 using 语句**: + ```csharp + using Microsoft.EntityFrameworkCore; + ``` +- **实现方法**: + ```csharp + public async Task GetUserRoleWithNavigationAsync(string userId, CancellationToken cancellationToken = default) + { + return await QueryRepository.FirstOrDefaultAsync( + ur => ur.UserId == userId, + include: q => q.Include(ur => ur.User).Include(ur => ur.Role), + cancellationToken: cancellationToken); + } + ``` + +#### 3. 修改业务逻辑层 +- **文件**: `X1.Application/Features/Users/Commands/ToggleUserStatus/ToggleUserStatusCommandHandler.cs` +- **修改代码**: + ```csharp + // 修改前 + var data = await _userRoleRepository.FirstOrDefaultAsync(s => s.UserId == user.Id); + + // 修改后 + var userRoleWithNavigation = await _userRoleRepository.GetUserRoleWithNavigationAsync(user.Id); + ``` + +## 技术说明 +- **关注点分离**: 将数据访问逻辑封装在仓储层,业务逻辑层只关注业务规则 +- **可重用性**: 新的仓储方法可以在其他地方复用 +- **可测试性**: 更容易进行单元测试和模拟 +- **维护性**: 导航属性加载逻辑集中管理,便于维护 + +## 影响范围 +- 修复了 `ToggleUserStatusCommandHandler` 中角色信息无法正确获取的问题 +- 确保用户角色切换功能能够正确识别管理员角色 +- 提高了代码的可维护性和可重用性 diff --git a/src/modify_20250121_users_dto_refactor.md b/src/modify_20250121_users_dto_refactor.md new file mode 100644 index 0000000..f641c4b --- /dev/null +++ b/src/modify_20250121_users_dto_refactor.md @@ -0,0 +1,105 @@ +## 2025-01-21 重构 GetAllUsersQueryHandler - 创建简化版用户DTO + +### 概述 + +为 `GetAllUsersResponse` 创建新的简化版用户DTO,去除权限信息,同时保留原有的完整功能。 + +### 主要变更 + +#### 1. 创建新的用户简单DTO +- **文件**: `X1.Application/Features/Users/Queries/Dtos/UserSimpleDto.cs` +- **功能**: 用户数据传输对象,不包含权限信息 +- **字段**: Id, UserName, RealName, Email, PhoneNumber, CreatedAt, IsActive, Roles +- **特点**: 去除了原有的 `Permissions` 字段 + +#### 2. 修改 GetAllUsersResponse +- **文件**: `X1.Application/Features/Users/Queries/GetAllUsers/GetAllUsersResponse.cs` +- **变更**: 将 `List` 改为 `List` +- **影响**: 响应中不再包含用户权限信息 + +#### 3. 重构 GetAllUsersQueryHandler +- **文件**: `X1.Application/Features/Users/Queries/GetAllUsers/GetAllUsersQueryHandler.cs` +- **保留**: 原有的 `GetUsersWithRolesAsync` 方法(包含权限信息) +- **新增**: `GetUsersWithRolesSimpleAsync` 方法(不包含权限信息) +- **调用**: 主处理方法调用新的简化版方法 + +#### 4. 技术实现细节 +- **性能优化**: 简化版方法不查询权限信息,减少数据库查询 +- **代码复用**: 两个方法共享角色查询逻辑 +- **空值处理**: 添加了空值检查,避免编译警告 +- **依赖管理**: 保留了所有必要的依赖注入 + +### 文件变更总结 + +1. **新增文件**: + - `UserSimpleDto.cs` - 简化版用户DTO + +2. **修改文件**: + - `GetAllUsersResponse.cs` - 使用新的DTO类型 + - `GetAllUsersQueryHandler.cs` - 添加新方法,保留原方法 + +3. **保留文件**: + - `UserDto.cs` - 原有完整版DTO保持不变 + +### 影响分析 + +- **向后兼容**: 原有的 `UserDto` 和 `GetUsersWithRolesAsync` 方法完全保留 +- **性能提升**: 简化版查询减少了权限相关的数据库查询 +- **API变更**: `GetAllUsersResponse` 的响应结构发生变化,不再包含权限信息 +- **代码维护**: 两个方法可以独立维护和优化 + +### 前端适配变更 + +#### 1. 更新 userService.ts 中的 User 接口 +- **文件**: `X1.WebUI/src/services/userService.ts` +- **变更**: 更新 `User` 接口以匹配后端的 `UserSimpleDto` +- **字段变更**: + - `userId` → `id` + - `roleIds` + `roleNames` → `roles` (角色名称数组) + - 去除 `permissions` 字段 + - `phoneNumber` 改为必填字段 + +#### 2. 更新用户相关页面组件 +- **UserTable.tsx**: 更新字段引用 (`user.userId` → `user.id`, `user.roleNames` → `user.roles`) +- **UsersView.tsx**: 更新字段引用 (`selectedUser.userId` → `selectedUser.id`, `selectedUser.roleIds` → `selectedUser.roles`) +- **UserRolesForm.tsx**: + - 更新字段引用 (`user.roleIds` → `user.roles`) + - 修复角色设置功能:根据用户当前角色名称匹配角色ID,正确显示已选中的角色 +- **UserForm.tsx**: + - 修复用户编辑功能:正确处理角色名称到角色ID的转换,确保编辑时显示用户当前角色 + +#### 3. 前端类型一致性 +- 确保前端 `User` 接口与后端 `UserSimpleDto` 完全一致 +- 保持与认证相关的 `User` 接口(`types/auth.ts`)分离,避免混淆 +- 用户管理功能不再依赖权限信息,简化了前端逻辑 + +#### 4. 主题适配优化 +- **StatusSwitch.tsx**: 修复状态开关组件的主题适配问题 + - 移除硬编码的颜色值(`#409eff`, `#d9d9d9`) + - 使用 Tailwind CSS 主题颜色类(`bg-primary`, `bg-muted`, `text-primary-foreground`, `text-muted-foreground`) + - 确保在明暗主题下都有良好的视觉效果和对比度 + +#### 5. 修复用户状态切换问题 +- **ToggleUserStatusCommand.cs**: 创建专门的状态切换命令 + - 只包含 `UserId` 和 `IsActive` 字段 + - 避免传递完整用户信息的复杂性 +- **ToggleUserStatusCommandHandler.cs**: 创建状态切换命令处理器 + - 专门处理用户状态切换逻辑 + - 包含完整的错误处理和日志记录 +- **UsersController.cs**: 添加状态切换接口 + - 新增 `PUT /api/users/{id}/status` 接口 + - 只接收 `isActive` 布尔值参数 +- **userService.ts**: 添加状态切换方法 + - 新增 `toggleUserStatus` 方法 + - 调用专门的状态切换接口 +- **UserTable.tsx**: 更新状态切换逻辑 + - 使用新的状态切换方法 + - 优化错误处理和刷新逻辑 + - 添加 toast 提示功能,显示操作结果 +- **ToggleUserStatusCommandHandler.cs**: 添加管理员保护机制 + - 检查用户是否为管理员角色 + - 防止禁用管理员用户 + - 返回明确的错误信息 + - 修复 Entity Framework 角色检查异常问题 + - 使用 `GetRolesAsync` 替代 `IsInRoleAsync` 避免数据库查询异常 + - 添加异常处理,确保角色检查失败时不影响正常操作