Browse Source

feat: 重构用户权限系统 - 修复角色Claims验证和用户管理功能

主要变更:
- 修复角色Claims验证问题:UserRoleRepository返回角色名称而非ID,解决JWT授权失败
- 重构用户DTO:创建UserSimpleDto简化用户数据传输,提升性能
- 修复用户状态切换:添加管理员保护机制,防止禁用管理员用户
- 优化导航属性加载:封装UserRole导航属性查询到仓储层
- 添加角色常量定义:RoleConstants统一管理角色名称,提高代码可维护性
- 完善前端适配:更新用户管理页面组件,修复主题适配和状态切换功能

技术改进:
- 增强JWT令牌Claims:同时支持角色名称和角色ID
- 改进错误处理:完善空值检查和异常处理
- 提升代码质量:使用DTO替代元组,遵循DDD设计原则
- 优化数据库查询:减少不必要的权限查询,提升性能

影响范围:
- 修复角色授权验证失败问题
- 提升用户管理功能稳定性
- 改善前端用户体验
- 增强系统安全性
refactor/permission-config
root 3 months ago
parent
commit
f163738e80
  1. 16
      src/X1.Application/Features/Auth/Commands/BaseLoginCommandHandler.cs
  2. 14
      src/X1.Application/Features/Users/Commands/ToggleUserStatus/ToggleUserStatusCommand.cs
  3. 121
      src/X1.Application/Features/Users/Commands/ToggleUserStatus/ToggleUserStatusCommandHandler.cs
  4. 11
      src/X1.Application/Features/Users/Commands/ToggleUserStatus/ToggleUserStatusResponse.cs
  5. 23
      src/X1.Application/Features/Users/Queries/Dtos/UserSimpleDto.cs
  6. 73
      src/X1.Application/Features/Users/Queries/GetAllUsers/GetAllUsersQueryHandler.cs
  7. 2
      src/X1.Application/Features/Users/Queries/GetAllUsers/GetAllUsersResponse.cs
  8. 95
      src/X1.Domain/Common/RoleConstants.cs
  9. 79
      src/X1.Domain/Models/UserRoleInfo.cs
  10. 16
      src/X1.Domain/Repositories/Identity/IUserRoleRepository.cs
  11. 46
      src/X1.Infrastructure/Repositories/Identity/UserRoleRepository.cs
  12. 3
      src/X1.Infrastructure/Services/UserManagement/UserRegistrationService.cs
  13. 2
      src/X1.Presentation/Controllers/NavigationMenuController.cs
  14. 2
      src/X1.Presentation/Controllers/PermissionsController.cs
  15. 2
      src/X1.Presentation/Controllers/RolesController.cs
  16. 56
      src/X1.Presentation/Controllers/UsersController.cs
  17. 3
      src/X1.WebUI/src/components/dashboard/StatsOverview.tsx
  18. 49
      src/X1.WebUI/src/components/ui/StatusSwitch.tsx
  19. 4
      src/X1.WebUI/src/pages/auth/LoginPage.tsx
  20. 1
      src/X1.WebUI/src/pages/task-execution-process/StepDetailsView.tsx
  21. 1
      src/X1.WebUI/src/pages/task-execution-process/TaskExecutionProcessView.tsx
  22. 26
      src/X1.WebUI/src/pages/users/UserForm.tsx
  23. 17
      src/X1.WebUI/src/pages/users/UserRolesForm.tsx
  24. 34
      src/X1.WebUI/src/pages/users/UserTable.tsx
  25. 6
      src/X1.WebUI/src/pages/users/UsersView.tsx
  26. 12
      src/X1.WebUI/src/services/userService.ts
  27. 129
      src/modify_20250121_role_claims_fix.md
  28. 80
      src/modify_20250121_toggleuserstatus_fix.md
  29. 61
      src/modify_20250121_userrole_navigation_fix.md
  30. 105
      src/modify_20250121_users_dto_refactor.md

16
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<TCommand, TResponse> : 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<Claim>
@ -228,14 +231,17 @@ public abstract class BaseLoginCommandHandler<TCommand, TResponse> : 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<string>();
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)

14
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;
/// <summary>
/// 切换用户状态命令
/// 用于切换用户激活/禁用状态的命令对象
/// </summary>
/// <param name="UserId">要切换状态的用户ID</param>
/// <param name="IsActive">新的激活状态</param>
public sealed record ToggleUserStatusCommand(
string UserId,
bool IsActive) : IRequest<OperationResult<ToggleUserStatusResponse>>;

121
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;
/// <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("切换用户状态失败");
}
}
}

11
src/X1.Application/Features/Users/Commands/ToggleUserStatus/ToggleUserStatusResponse.cs

@ -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);

23
src/X1.Application/Features/Users/Queries/Dtos/UserSimpleDto.cs

@ -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);

73
src/X1.Application/Features/Users/Queries/GetAllUsers/GetAllUsersQueryHandler.cs

@ -32,6 +32,7 @@ public sealed class GetAllUsersQueryHandler : IRequestHandler<GetAllUsersQuery,
/// <param name="userManager">用户管理器,用于管理用户身份</param>
/// <param name="roleManager">角色管理器,用于管理角色信息</param>
/// <param name="userRoleRepository">用户角色仓储</param>
/// <param name="rolePermissionRepository">角色权限仓储</param>
/// <param name="logger">日志记录器</param>
public GetAllUsersQueryHandler(
UserManager<AppUser> userManager,
@ -65,17 +66,17 @@ public sealed class GetAllUsersQueryHandler : IRequestHandler<GetAllUsersQuery,
// 应用条件过滤
if (!string.IsNullOrWhiteSpace(request.UserName))
{
query = query.Where(u => 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<GetAllUsersQuery,
.ToListAsync(cancellationToken);
// 批量获取所有用户的角色信息(优化性能)
var userDtos = await GetUsersWithRolesAsync(users, cancellationToken);
var userDtos = await GetUsersWithRolesSimpleAsync(users, cancellationToken);
// 构建响应对象
var response = new GetAllUsersResponse(
@ -157,7 +158,7 @@ public sealed class GetAllUsersQueryHandler : IRequestHandler<GetAllUsersQuery,
foreach (var roleId in userRoleIds)
{
if (allRoles.TryGetValue(roleId, out var role))
if (allRoles.TryGetValue(roleId, out var role) && role.Name != null)
{
roleNames.Add(role.Name);
roleIds.Add(role.Id);
@ -173,10 +174,10 @@ public sealed class GetAllUsersQueryHandler : IRequestHandler<GetAllUsersQuery,
var dto = new UserDto(
user.Id,
user.UserName,
user.RealName,
user.Email,
user.PhoneNumber,
user.UserName ?? string.Empty,
user.RealName ?? string.Empty,
user.Email ?? string.Empty,
user.PhoneNumber ?? string.Empty,
user.CreatedTime,
user.IsActive,
roleNames,
@ -187,4 +188,58 @@ public sealed class GetAllUsersQueryHandler : IRequestHandler<GetAllUsersQuery,
return userDtos;
}
/// <summary>
/// 批量获取用户角色信息(简化版本:不包含权限信息)
/// </summary>
/// <param name="users">用户列表</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>包含角色信息的用户简单DTO列表</returns>
private async Task<List<UserSimpleDto>> GetUsersWithRolesSimpleAsync(List<AppUser> users, CancellationToken cancellationToken)
{
if (!users.Any())
return new List<UserSimpleDto>();
// 一次性获取所有角色信息(角色数据不会特别多)
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<UserSimpleDto>();
foreach (var user in users)
{
// 从批量结果中获取当前用户的角色
var userRoleIds = allUserRoles.ContainsKey(user.Id) ? allUserRoles[user.Id] : new List<string>();
// 获取角色名称
var roleNames = new List<string>();
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;
}
}

2
src/X1.Application/Features/Users/Queries/GetAllUsers/GetAllUsersResponse.cs

@ -13,7 +13,7 @@ namespace X1.Application.Features.Users.Queries.GetAllUsers;
/// <param name="PageNumber">当前页码</param>
/// <param name="PageSize">每页记录数</param>
public sealed record GetAllUsersResponse(
List<UserDto> Users,
List<UserSimpleDto> Users,
int TotalCount,
int PageNumber,
int PageSize)

95
src/X1.Domain/Common/RoleConstants.cs

@ -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);
}
}

79
src/X1.Domain/Models/UserRoleInfo.cs

@ -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);
}
}

16
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<UserRole>
/// </summary>
Task<IList<string>> GetUserRolesAsync(string userId, CancellationToken cancellationToken = default);
/// <summary>
/// 获取用户的所有角色ID
/// </summary>
Task<IList<string>> GetUserRoleIdsAsync(string userId, CancellationToken cancellationToken = default);
/// <summary>
/// 获取用户角色信息(包含ID和名称)
/// </summary>
Task<IList<UserRoleInfo>> GetUserRoleInfoAsync(string userId, CancellationToken cancellationToken = default);
/// <summary>
/// 检查用户是否拥有指定角色
/// </summary>
@ -31,4 +42,9 @@ public interface IUserRoleRepository : IBaseRepository<UserRole>
/// 批量获取多个用户的角色信息
/// </summary>
Task<Dictionary<string, List<string>>> GetUsersRolesAsync(IEnumerable<string> userIds, CancellationToken cancellationToken = default);
/// <summary>
/// 获取用户角色关系实体(包含导航属性)
/// </summary>
Task<UserRole?> GetUserRoleWithNavigationAsync(string userId, CancellationToken cancellationToken = default);
}

46
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<UserRole>, IUserRoleRepository
/// 获取用户的所有角色
/// </summary>
public async Task<IList<string>> 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();
}
/// <summary>
/// 获取用户的所有角色ID
/// </summary>
public async Task<IList<string>> GetUserRoleIdsAsync(string userId, CancellationToken cancellationToken = default)
{
var userRoles = await QueryRepository.FindAsync(
ur => ur.UserId == userId,
@ -51,6 +66,22 @@ public class UserRoleRepository : BaseRepository<UserRole>, IUserRoleRepository
return userRoles.Select(ur => ur.RoleId).ToList();
}
/// <summary>
/// 获取用户角色信息(包含ID和名称)
/// </summary>
public async Task<IList<UserRoleInfo>> 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();
}
/// <summary>
/// 检查用户是否拥有指定角色
/// </summary>
@ -68,13 +99,26 @@ public class UserRoleRepository : BaseRepository<UserRole>, 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());
}
/// <summary>
/// 获取用户角色关系实体(包含导航属性)
/// </summary>
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);
}
#endregion

3
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);

2
src/X1.Presentation/Controllers/NavigationMenuController.cs

@ -18,7 +18,7 @@ namespace X1.Presentation.Controllers;
/// 导航菜单管理控制器
/// 提供导航菜单管理相关的 API 接口,包括创建、更新、删除和查询导航菜单功能
/// </summary>
//[Authorize(Roles = "Admin")] // 只有管理员可以访问
[Authorize(Roles = RoleConstants.Admin)] // 只有管理员可以访问
[Route("api/navigation-menus")]
[ApiController]
public class NavigationMenuController : ApiController

2
src/X1.Presentation/Controllers/PermissionsController.cs

@ -20,7 +20,7 @@ namespace X1.Presentation.Controllers;
/// 权限管理控制器
/// 提供权限管理相关的 API 接口,包括创建、更新、删除和查询权限功能
/// </summary>
//[Authorize(Roles = "Admin")] // 只有管理员可以访问
[Authorize(Roles = RoleConstants.Admin)] // 只有管理员可以访问
[Route("api/permissions")]
[ApiController]
public class PermissionsController : ApiController

2
src/X1.Presentation/Controllers/RolesController.cs

@ -17,7 +17,7 @@ namespace X1.Presentation.Controllers;
/// 角色管理控制器
/// 提供角色管理相关的 API 接口,包括创建、删除和查询角色功能
/// </summary>
//[Authorize(Roles = "Admin")] // 只有管理员可以访问
[Authorize(Roles = RoleConstants.Admin)] // 只有管理员可以访问
[Route("api/roles")]
[ApiController]
public class RolesController : ApiController

56
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
}
}
/// <summary>
/// 切换用户状态
/// </summary>
/// <remarks>
/// 示例请求:
///
/// PUT /api/users/{id}/status
/// {
/// "isActive": true
/// }
///
/// </remarks>
/// <param name="id">用户ID</param>
/// <param name="isActive">新的激活状态</param>
/// <returns>
/// 切换结果,包含:
/// - 成功:返回切换后的状态信息
/// - 失败:返回错误信息
/// </returns>
/// <response code="200">切换成功,返回状态信息</response>
/// <response code="400">切换失败,返回错误信息</response>
/// <response code="404">用户不存在</response>
[HttpPut("{id}/status")]
[ProducesResponseType(typeof(OperationResult<ToggleUserStatusResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(OperationResult<object>), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(OperationResult<object>), StatusCodes.Status404NotFound)]
public async Task<OperationResult<ToggleUserStatusResponse>> 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<ToggleUserStatusResponse>.CreateFailure("系统错误,请稍后重试");
}
}
/// <summary>
/// 获取当前登录用户信息
/// </summary>

3
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';

49
src/X1.WebUI/src/components/ui/StatusSwitch.tsx

@ -30,60 +30,49 @@ const StatusSwitch: React.FC<StatusSwitchProps> = ({
return (
<div
className={`
relative flex items-center box-border font-medium p-0 select-none transition-all duration-200
${checked
? 'bg-primary border-primary'
: 'bg-muted border-muted-foreground/20'
}
${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}
`}
style={{
width: SWITCH_WIDTH,
height: SWITCH_HEIGHT,
borderRadius: SWITCH_HEIGHT / 2,
background: checked ? '#409eff' : '#d9d9d9',
cursor: disabled ? 'not-allowed' : 'pointer',
opacity: disabled ? 0.5 : 1,
userSelect: 'none',
transition: 'background 0.2s',
border: checked ? '1px solid #409eff' : '1px solid #d9d9d9',
position: 'relative',
display: 'flex',
alignItems: 'center',
boxSizing: 'border-box',
fontWeight: 500,
padding: 0,
border: '1px solid',
}}
onClick={handleClick}
>
{/* 文字 */}
<span
className={`
absolute text-xs font-medium pointer-events-none whitespace-nowrap z-10
transition-all duration-200
${checked
? 'text-primary-foreground left-3 text-left'
: 'text-muted-foreground right-3 text-right'
}
`}
style={{
color: checked ? '#fff' : '#888',
fontSize: 13,
letterSpacing: 1,
position: 'absolute',
left: checked ? 12 : undefined,
right: !checked ? 12 : undefined,
transition: 'left 0.2s, right 0.2s, color 0.2s',
minWidth: 28,
textAlign: checked ? 'left' : 'right',
pointerEvents: 'none',
whiteSpace: 'nowrap',
zIndex: 2,
}}
>
{checked ? activeText : inactiveText}
</span>
{/* 圆点 */}
<div
className="absolute bg-background border border-border shadow-sm z-20 transition-all duration-200 ease-out"
style={{
width: THUMB_SIZE,
height: THUMB_SIZE,
borderRadius: '50%',
background: '#fff',
boxShadow: '0 2px 8px rgba(0,0,0,0.10)',
position: 'absolute',
top: (SWITCH_HEIGHT - THUMB_SIZE) / 2-1,
top: (SWITCH_HEIGHT - THUMB_SIZE) / 2 - 1,
left: checked
? SWITCH_WIDTH - THUMB_SIZE - 4
: 4,
transition: 'left 0.2s cubic-bezier(.4,0,.2,1)',
border: '1px solid #f0f0f0',
zIndex: 3,
}}
/>
</div>

4
src/X1.WebUI/src/pages/auth/LoginPage.tsx

@ -50,7 +50,7 @@ export function LoginPage() {
<div className={`${theme === 'dark' ? 'bg-gray-800/80 backdrop-blur-sm border-gray-700' : 'bg-white'} rounded-2xl shadow-xl p-8 sm:p-10 space-y-8 ${theme === 'dark' ? 'border' : ''}`}>
<LoginForm onSubmit={handleLogin} />
<div className={`text-center text-base ${theme === 'dark' ? 'text-gray-300' : 'text-gray-600'}`}>
{/* <div className={`text-center text-base ${theme === 'dark' ? 'text-gray-300' : 'text-gray-600'}`}>
<span></span>
<button
onClick={() => navigate('/register')}
@ -58,7 +58,7 @@ export function LoginPage() {
>
</button>
</div>
</div> */}
</div>
</div>
</div>

1
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,

1
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';

26
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<Role[]>([]);
@ -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();

17
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);

34
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({
</TableRow>
) : (
users.map((user) => (
<TableRow key={user.userId} className={rowClass}>
<TableRow key={user.id} className={rowClass}>
{visibleColumns.map(col => {
switch (col.key) {
case 'userName':
@ -112,8 +114,30 @@ export default function UserTable({
<StatusSwitch
checked={!!user.isActive}
onChange={async () => {
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({
</TableCell>
);
case 'roles':
return <TableCell key={col.key} className={`text-foreground text-center ${cellPadding}`}>{Array.isArray(user.roleNames) && user.roleNames.length > 0 ? user.roleNames.join(', ') : '-'}</TableCell>;
return <TableCell key={col.key} className={`text-foreground text-center ${cellPadding}`}>{Array.isArray(user.roles) && user.roles.length > 0 ? user.roles.join(', ') : '-'}</TableCell>;
case 'createdAt':
return <TableCell key={col.key} className={`text-foreground text-center ${cellPadding}`}>{user.createdAt ? formatToBeijingTime(user.createdAt) : '-'}</TableCell>;
case 'actions':
@ -146,7 +170,7 @@ export default function UserTable({
)}
<span
className="cursor-pointer text-red-500 hover:underline select-none"
onClick={() => onDelete(user.userId)}
onClick={() => onDelete(user.id)}
>
</span>

6
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
}}
/>
)}

12
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<OperationResult<boolean>> {
return httpClient.delete<boolean>(`${this.baseUrl}/${userId}`);
}
// 切换用户状态
async toggleUserStatus(userId: string, isActive: boolean): Promise<OperationResult<{ userId: string; isActive: boolean }>> {
return httpClient.put<{ userId: string; isActive: boolean }>(`${this.baseUrl}/${userId}/status`, isActive);
}
}
export const userService = new UserService();

129
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<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` - 角色常量定义

80
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<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. 测试未分配角色的用户应该返回错误

61
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
/// <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` 中角色信息无法正确获取的问题
- 确保用户角色切换功能能够正确识别管理员角色
- 提高了代码的可维护性和可重用性

105
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<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…
Cancel
Save