Browse Source
主要变更: - 修复角色Claims验证问题:UserRoleRepository返回角色名称而非ID,解决JWT授权失败 - 重构用户DTO:创建UserSimpleDto简化用户数据传输,提升性能 - 修复用户状态切换:添加管理员保护机制,防止禁用管理员用户 - 优化导航属性加载:封装UserRole导航属性查询到仓储层 - 添加角色常量定义:RoleConstants统一管理角色名称,提高代码可维护性 - 完善前端适配:更新用户管理页面组件,修复主题适配和状态切换功能 技术改进: - 增强JWT令牌Claims:同时支持角色名称和角色ID - 改进错误处理:完善空值检查和异常处理 - 提升代码质量:使用DTO替代元组,遵循DDD设计原则 - 优化数据库查询:减少不必要的权限查询,提升性能 影响范围: - 修复角色授权验证失败问题 - 提升用户管理功能稳定性 - 改善前端用户体验 - 增强系统安全性refactor/permission-config
30 changed files with 1015 additions and 74 deletions
@ -0,0 +1,14 @@ |
|||
using X1.Domain.Common; |
|||
using MediatR; |
|||
|
|||
namespace X1.Application.Features.Users.Commands.ToggleUserStatus; |
|||
|
|||
/// <summary>
|
|||
/// 切换用户状态命令
|
|||
/// 用于切换用户激活/禁用状态的命令对象
|
|||
/// </summary>
|
|||
/// <param name="UserId">要切换状态的用户ID</param>
|
|||
/// <param name="IsActive">新的激活状态</param>
|
|||
public sealed record ToggleUserStatusCommand( |
|||
string UserId, |
|||
bool IsActive) : IRequest<OperationResult<ToggleUserStatusResponse>>; |
|||
@ -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; |
|||
|
|||
/// <summary>
|
|||
/// 切换用户状态命令处理器
|
|||
/// 处理用户激活/禁用状态的切换逻辑
|
|||
/// </summary>
|
|||
public sealed class ToggleUserStatusCommandHandler : IRequestHandler<ToggleUserStatusCommand, OperationResult<ToggleUserStatusResponse>> |
|||
{ |
|||
private readonly UserManager<AppUser> _userManager; |
|||
private readonly ILogger<ToggleUserStatusCommandHandler> _logger; |
|||
private readonly IUnitOfWork _unitOfWork; |
|||
private readonly IUserRoleRepository _userRoleRepository; |
|||
|
|||
/// <summary>
|
|||
/// 构造函数
|
|||
/// </summary>
|
|||
/// <param name="userManager">用户管理器</param>
|
|||
/// <param name="logger">日志记录器</param>
|
|||
/// <param name="unitOfWork">工作单元</param>
|
|||
public ToggleUserStatusCommandHandler( |
|||
UserManager<AppUser> userManager, |
|||
ILogger<ToggleUserStatusCommandHandler> logger, |
|||
IUnitOfWork unitOfWork, |
|||
IUserRoleRepository userRoleRepository) |
|||
{ |
|||
_userManager = userManager; |
|||
_logger = logger; |
|||
_unitOfWork = unitOfWork; |
|||
_userRoleRepository = userRoleRepository; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 处理切换用户状态命令
|
|||
/// </summary>
|
|||
/// <param name="request">切换状态命令</param>
|
|||
/// <param name="cancellationToken">取消令牌</param>
|
|||
/// <returns>操作结果,包含切换后的状态信息</returns>
|
|||
public async Task<OperationResult<ToggleUserStatusResponse>> 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<ToggleUserStatusResponse>.CreateFailure("用户不存在"); |
|||
} |
|||
|
|||
// 检查是否为管理员用户,如果是管理员且要禁用,则不允许
|
|||
var userRoles = await _userRoleRepository.GetUserRoleWithNavigationAsync(user.Id); |
|||
|
|||
if (userRoles?.Role == null) |
|||
{ |
|||
_logger.LogWarning("用户 {UserId} 没有分配角色", request.UserId); |
|||
return OperationResult<ToggleUserStatusResponse>.CreateFailure("用户没有分配角色"); |
|||
} |
|||
|
|||
var isAdmin = RoleConstants.IsAdminRole(userRoles.Role.Name ?? string.Empty); |
|||
|
|||
// 如果要禁用用户且该用户是管理员,则不允许
|
|||
if (!request.IsActive && isAdmin) |
|||
{ |
|||
_logger.LogWarning("尝试禁用管理员用户 {UserId},操作被拒绝", request.UserId); |
|||
return OperationResult<ToggleUserStatusResponse>.CreateFailure("不能禁用管理员用户"); |
|||
} |
|||
|
|||
// 检查状态是否已经是指定状态
|
|||
if (user.IsActive == request.IsActive) |
|||
{ |
|||
_logger.LogInformation("用户 {UserId} 状态已经是 {IsActive},无需切换", request.UserId, request.IsActive); |
|||
return OperationResult<ToggleUserStatusResponse>.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<ToggleUserStatusResponse>.CreateSuccess( |
|||
new ToggleUserStatusResponse(user.Id, user.IsActive)); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "切换用户 {UserId} 状态时发生错误", request.UserId); |
|||
return OperationResult<ToggleUserStatusResponse>.CreateFailure("切换用户状态失败"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
namespace X1.Application.Features.Users.Commands.ToggleUserStatus; |
|||
|
|||
/// <summary>
|
|||
/// 切换用户状态响应
|
|||
/// 包含切换状态后的用户信息
|
|||
/// </summary>
|
|||
/// <param name="UserId">用户ID</param>
|
|||
/// <param name="IsActive">新的激活状态</param>
|
|||
public sealed record ToggleUserStatusResponse( |
|||
string UserId, |
|||
bool IsActive); |
|||
@ -0,0 +1,23 @@ |
|||
namespace X1.Application.Features.Users.Queries.Dtos; |
|||
|
|||
/// <summary>
|
|||
/// 用户简单数据传输对象
|
|||
/// 用于在API层和领域层之间传输用户数据(不包含权限信息)
|
|||
/// </summary>
|
|||
/// <param name="Id">用户ID</param>
|
|||
/// <param name="UserName">用户名</param>
|
|||
/// <param name="RealName">真实姓名</param>
|
|||
/// <param name="Email">电子邮箱</param>
|
|||
/// <param name="PhoneNumber">电话号码</param>
|
|||
/// <param name="CreatedAt">创建时间</param>
|
|||
/// <param name="IsActive">是否激活</param>
|
|||
/// <param name="Roles">角色名称列表</param>
|
|||
public sealed record UserSimpleDto( |
|||
string Id, |
|||
string UserName, |
|||
string RealName, |
|||
string Email, |
|||
string PhoneNumber, |
|||
DateTime CreatedAt, |
|||
bool IsActive, |
|||
List<string> Roles); |
|||
@ -0,0 +1,95 @@ |
|||
namespace X1.Domain.Common; |
|||
|
|||
/// <summary>
|
|||
/// 系统角色常量定义
|
|||
/// 提供系统中所有预定义角色的常量值
|
|||
/// </summary>
|
|||
public static class RoleConstants |
|||
{ |
|||
/// <summary>
|
|||
/// 系统管理员角色
|
|||
/// </summary>
|
|||
public const string Admin = "Admin"; |
|||
|
|||
/// <summary>
|
|||
/// 部门经理角色
|
|||
/// </summary>
|
|||
public const string Manager = "Manager"; |
|||
|
|||
/// <summary>
|
|||
/// 普通用户角色
|
|||
/// </summary>
|
|||
public const string User = "User"; |
|||
|
|||
/// <summary>
|
|||
/// 操作员角色
|
|||
/// </summary>
|
|||
public const string Operator = "Operator"; |
|||
|
|||
/// <summary>
|
|||
/// 财务人员角色
|
|||
/// </summary>
|
|||
public const string Finance = "Finance"; |
|||
|
|||
/// <summary>
|
|||
/// 人力资源角色
|
|||
/// </summary>
|
|||
public const string HR = "HR"; |
|||
|
|||
/// <summary>
|
|||
/// 客服人员角色
|
|||
/// </summary>
|
|||
public const string CustomerService = "CustomerService"; |
|||
|
|||
/// <summary>
|
|||
/// 技术支持角色
|
|||
/// </summary>
|
|||
public const string TechnicalSupport = "TechnicalSupport"; |
|||
|
|||
/// <summary>
|
|||
/// 数据分析师角色
|
|||
/// </summary>
|
|||
public const string DataAnalyst = "DataAnalyst"; |
|||
|
|||
/// <summary>
|
|||
/// 审计人员角色
|
|||
/// </summary>
|
|||
public const string Auditor = "Auditor"; |
|||
|
|||
/// <summary>
|
|||
/// 获取所有系统角色
|
|||
/// </summary>
|
|||
public static readonly string[] AllRoles = |
|||
{ |
|||
Admin, |
|||
Manager, |
|||
User, |
|||
Operator, |
|||
Finance, |
|||
HR, |
|||
CustomerService, |
|||
TechnicalSupport, |
|||
DataAnalyst, |
|||
Auditor |
|||
}; |
|||
|
|||
/// <summary>
|
|||
/// 检查是否为管理员角色
|
|||
/// </summary>
|
|||
/// <param name="roleName">角色名称</param>
|
|||
/// <returns>如果是管理员角色返回true,否则返回false</returns>
|
|||
public static bool IsAdminRole(string roleName) |
|||
{ |
|||
return string.Equals(roleName, Admin, StringComparison.OrdinalIgnoreCase); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 检查是否为系统关键角色(不允许禁用的角色)
|
|||
/// </summary>
|
|||
/// <param name="roleName">角色名称</param>
|
|||
/// <returns>如果是关键角色返回true,否则返回false</returns>
|
|||
public static bool IsCriticalRole(string roleName) |
|||
{ |
|||
return IsAdminRole(roleName); |
|||
} |
|||
} |
|||
@ -0,0 +1,79 @@ |
|||
namespace X1.Domain.Models; |
|||
|
|||
/// <summary>
|
|||
/// 用户角色信息DTO
|
|||
/// 包含用户角色的ID和名称信息
|
|||
/// </summary>
|
|||
public class UserRoleInfo |
|||
{ |
|||
/// <summary>
|
|||
/// 角色ID
|
|||
/// </summary>
|
|||
public string RoleId { get; set; } = string.Empty; |
|||
|
|||
/// <summary>
|
|||
/// 角色名称
|
|||
/// </summary>
|
|||
public string RoleName { get; set; } = string.Empty; |
|||
|
|||
/// <summary>
|
|||
/// 构造函数
|
|||
/// </summary>
|
|||
public UserRoleInfo() |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 构造函数
|
|||
/// </summary>
|
|||
/// <param name="roleId">角色ID</param>
|
|||
/// <param name="roleName">角色名称</param>
|
|||
public UserRoleInfo(string roleId, string roleName) |
|||
{ |
|||
RoleId = roleId; |
|||
RoleName = roleName; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 创建UserRoleInfo实例
|
|||
/// </summary>
|
|||
/// <param name="roleId">角色ID</param>
|
|||
/// <param name="roleName">角色名称</param>
|
|||
/// <returns>UserRoleInfo实例</returns>
|
|||
public static UserRoleInfo Create(string roleId, string roleName) |
|||
{ |
|||
return new UserRoleInfo(roleId, roleName); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 转换为字符串表示
|
|||
/// </summary>
|
|||
/// <returns>字符串表示</returns>
|
|||
public override string ToString() |
|||
{ |
|||
return $"RoleId: {RoleId}, RoleName: {RoleName}"; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 比较两个UserRoleInfo是否相等
|
|||
/// </summary>
|
|||
/// <param name="obj">要比较的对象</param>
|
|||
/// <returns>是否相等</returns>
|
|||
public override bool Equals(object? obj) |
|||
{ |
|||
if (obj is UserRoleInfo other) |
|||
{ |
|||
return RoleId == other.RoleId && RoleName == other.RoleName; |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取哈希码
|
|||
/// </summary>
|
|||
/// <returns>哈希码</returns>
|
|||
public override int GetHashCode() |
|||
{ |
|||
return HashCode.Combine(RoleId, RoleName); |
|||
} |
|||
} |
|||
@ -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<UserRoleInfo>` |
|||
- **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` - 角色常量定义 |
|||
@ -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<ToggleUserStatusResponse>.CreateFailure("用户没有分配角色"); |
|||
} |
|||
|
|||
var isAdmin = RoleConstants.IsAdminRole(userRoles.Role.Name ?? string.Empty); |
|||
|
|||
// 如果要禁用用户且该用户是管理员,则不允许 |
|||
if (!request.IsActive && isAdmin) |
|||
{ |
|||
_logger.LogWarning("尝试禁用管理员用户 {UserId},操作被拒绝", request.UserId); |
|||
return OperationResult<ToggleUserStatusResponse>.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. 测试未分配角色的用户应该返回错误 |
|||
@ -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 |
|||
/// <summary> |
|||
/// 获取用户角色关系实体(包含导航属性) |
|||
/// </summary> |
|||
Task<UserRole?> GetUserRoleWithNavigationAsync(string userId, CancellationToken cancellationToken = default); |
|||
``` |
|||
|
|||
#### 2. 实现 UserRoleRepository 方法 |
|||
- **文件**: `X1.Infrastructure/Repositories/Identity/UserRoleRepository.cs` |
|||
- **添加 using 语句**: |
|||
```csharp |
|||
using Microsoft.EntityFrameworkCore; |
|||
``` |
|||
- **实现方法**: |
|||
```csharp |
|||
public async Task<UserRole?> 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` 中角色信息无法正确获取的问题 |
|||
- 确保用户角色切换功能能够正确识别管理员角色 |
|||
- 提高了代码的可维护性和可重用性 |
|||
@ -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<UserDto>` 改为 `List<UserSimpleDto>` |
|||
- **影响**: 响应中不再包含用户权限信息 |
|||
|
|||
#### 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` 避免数据库查询异常 |
|||
- 添加异常处理,确保角色检查失败时不影响正常操作 |
|||
Loading…
Reference in new issue