feat: 重构 RBAC1 角色权限模型

This commit is contained in:
2025-12-02 16:21:46 +08:00
parent 3d69151426
commit b459c7edbe
21 changed files with 780 additions and 49 deletions

View File

@@ -1,3 +1,5 @@
using System;
using System.Linq;
using MediatR;
using TakeoutSaaS.Application.Identity.Contracts;
using TakeoutSaaS.Application.Identity.Queries;
@@ -11,10 +13,18 @@ namespace TakeoutSaaS.Application.Identity.Handlers;
/// </summary>
public sealed class GetUserPermissionsQueryHandler(
IIdentityUserRepository identityUserRepository,
IUserRoleRepository userRoleRepository,
IRoleRepository roleRepository,
IPermissionRepository permissionRepository,
IRolePermissionRepository rolePermissionRepository,
ITenantProvider tenantProvider)
: IRequestHandler<GetUserPermissionsQuery, UserPermissionDto?>
{
private readonly IIdentityUserRepository _identityUserRepository = identityUserRepository;
private readonly IUserRoleRepository _userRoleRepository = userRoleRepository;
private readonly IRoleRepository _roleRepository = roleRepository;
private readonly IPermissionRepository _permissionRepository = permissionRepository;
private readonly IRolePermissionRepository _rolePermissionRepository = rolePermissionRepository;
private readonly ITenantProvider _tenantProvider = tenantProvider;
/// <inheritdoc />
@@ -27,6 +37,9 @@ public sealed class GetUserPermissionsQueryHandler(
return null;
}
var roleCodes = await ResolveUserRolesAsync(tenantId, user.Id, cancellationToken);
var permissionCodes = await ResolveUserPermissionsAsync(tenantId, user.Id, cancellationToken);
return new UserPermissionDto
{
UserId = user.Id,
@@ -34,9 +47,42 @@ public sealed class GetUserPermissionsQueryHandler(
MerchantId = user.MerchantId,
Account = user.Account,
DisplayName = user.DisplayName,
Roles = user.Roles,
Permissions = user.Permissions,
Roles = roleCodes,
Permissions = permissionCodes,
CreatedAt = user.CreatedAt
};
}
private async Task<string[]> ResolveUserRolesAsync(long tenantId, long userId, CancellationToken cancellationToken)
{
var relations = await _userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken);
var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray();
if (roleIds.Length == 0)
{
return Array.Empty<string>();
}
var roles = await _roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken);
return roles.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
}
private async Task<string[]> ResolveUserPermissionsAsync(long tenantId, long userId, CancellationToken cancellationToken)
{
var relations = await _userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken);
var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray();
if (roleIds.Length == 0)
{
return Array.Empty<string>();
}
var rolePermissions = await _rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken);
var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray();
if (permissionIds.Length == 0)
{
return Array.Empty<string>();
}
var permissions = await _permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken);
return permissions.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
}
}

View File

@@ -1,3 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using MediatR;
using TakeoutSaaS.Application.Identity.Contracts;
@@ -13,10 +15,18 @@ namespace TakeoutSaaS.Application.Identity.Handlers;
/// </summary>
public sealed class SearchUserPermissionsQueryHandler(
IIdentityUserRepository identityUserRepository,
IUserRoleRepository userRoleRepository,
IRoleRepository roleRepository,
IPermissionRepository permissionRepository,
IRolePermissionRepository rolePermissionRepository,
ITenantProvider tenantProvider)
: IRequestHandler<SearchUserPermissionsQuery, PagedResult<UserPermissionDto>>
{
private readonly IIdentityUserRepository _identityUserRepository = identityUserRepository;
private readonly IUserRoleRepository _userRoleRepository = userRoleRepository;
private readonly IRoleRepository _roleRepository = roleRepository;
private readonly IPermissionRepository _permissionRepository = permissionRepository;
private readonly IRolePermissionRepository _rolePermissionRepository = rolePermissionRepository;
private readonly ITenantProvider _tenantProvider = tenantProvider;
/// <inheritdoc />
@@ -31,6 +41,7 @@ public sealed class SearchUserPermissionsQueryHandler(
.Take(request.PageSize)
.ToList();
var resolved = await ResolveRolesAndPermissionsAsync(tenantId, paged, cancellationToken);
var items = paged.Select(user => new UserPermissionDto
{
UserId = user.Id,
@@ -38,8 +49,8 @@ public sealed class SearchUserPermissionsQueryHandler(
MerchantId = user.MerchantId,
Account = user.Account,
DisplayName = user.DisplayName,
Roles = user.Roles,
Permissions = user.Permissions,
Roles = resolved[user.Id].roles,
Permissions = resolved[user.Id].permissions,
CreatedAt = user.CreatedAt
}).ToList();
@@ -64,4 +75,57 @@ public sealed class SearchUserPermissionsQueryHandler(
: users.OrderBy(x => x.CreatedAt)
};
}
private async Task<Dictionary<long, (string[] roles, string[] permissions)>> ResolveRolesAndPermissionsAsync(
long tenantId,
IReadOnlyCollection<Domain.Identity.Entities.IdentityUser> users,
CancellationToken cancellationToken)
{
var userIds = users.Select(x => x.Id).ToArray();
var userRoleRelations = await _userRoleRepository.GetByUserIdsAsync(tenantId, userIds, cancellationToken);
var roleIds = userRoleRelations.Select(x => x.RoleId).Distinct().ToArray();
var roles = roleIds.Length == 0
? Array.Empty<Domain.Identity.Entities.Role>()
: await _roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken);
var roleCodeMap = roles.ToDictionary(r => r.Id, r => r.Code, comparer: EqualityComparer<long>.Default);
var rolePermissions = roleIds.Length == 0
? Array.Empty<Domain.Identity.Entities.RolePermission>()
: await _rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken);
var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray();
var permissions = permissionIds.Length == 0
? Array.Empty<Domain.Identity.Entities.Permission>()
: await _permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken);
var permissionCodeMap = permissions.ToDictionary(p => p.Id, p => p.Code, comparer: EqualityComparer<long>.Default);
var rolePermissionsLookup = rolePermissions
.GroupBy(rp => rp.RoleId)
.ToDictionary(g => g.Key, g => g.Select(rp => rp.PermissionId).ToArray(), comparer: EqualityComparer<long>.Default);
var result = new Dictionary<long, (string[] roles, string[] permissions)>();
foreach (var userId in userIds)
{
var rolesForUser = userRoleRelations.Where(ur => ur.UserId == userId).Select(ur => ur.RoleId).Distinct().ToArray();
var roleCodes = rolesForUser
.Select(rid => roleCodeMap.GetValueOrDefault(rid))
.Where(c => !string.IsNullOrWhiteSpace(c))
.Select(c => c!)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
var permissionCodes = rolesForUser
.SelectMany(rid => rolePermissionsLookup.GetValueOrDefault(rid) ?? Array.Empty<long>())
.Select(pid => permissionCodeMap.GetValueOrDefault(pid))
.Where(code => !string.IsNullOrWhiteSpace(code))
.Select(code => code!)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
result[userId] = (roleCodes, permissionCodes);
}
return result;
}
}

View File

@@ -1,3 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Identity;
using TakeoutSaaS.Application.Identity.Abstractions;
using TakeoutSaaS.Application.Identity.Contracts;
@@ -15,12 +18,20 @@ namespace TakeoutSaaS.Application.Identity.Services;
/// </summary>
public sealed class AdminAuthService(
IIdentityUserRepository userRepository,
IUserRoleRepository userRoleRepository,
IRoleRepository roleRepository,
IPermissionRepository permissionRepository,
IRolePermissionRepository rolePermissionRepository,
IPasswordHasher<IdentityUser> passwordHasher,
IJwtTokenService jwtTokenService,
IRefreshTokenStore refreshTokenStore,
ITenantProvider tenantProvider) : IAdminAuthService
{
private readonly ITenantProvider _tenantProvider = tenantProvider;
private readonly IUserRoleRepository _userRoleRepository = userRoleRepository;
private readonly IRoleRepository _roleRepository = roleRepository;
private readonly IPermissionRepository _permissionRepository = permissionRepository;
private readonly IRolePermissionRepository _rolePermissionRepository = rolePermissionRepository;
/// <summary>
/// 管理后台登录:验证账号密码并生成令牌。
@@ -43,7 +54,7 @@ public sealed class AdminAuthService(
}
// 3. 构建用户档案并生成令牌
var profile = BuildProfile(user);
var profile = await BuildProfileAsync(user, cancellationToken);
return await jwtTokenService.CreateTokensAsync(profile, false, cancellationToken);
}
@@ -71,7 +82,7 @@ public sealed class AdminAuthService(
await refreshTokenStore.RevokeAsync(descriptor.Token, cancellationToken);
// 4. 生成新的令牌对
var profile = BuildProfile(user);
var profile = await BuildProfileAsync(user, cancellationToken);
return await jwtTokenService.CreateTokensAsync(profile, false, cancellationToken);
}
@@ -87,7 +98,7 @@ public sealed class AdminAuthService(
var user = await userRepository.FindByIdAsync(userId, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, "用户不存在");
return BuildProfile(user);
return await BuildProfileAsync(user, cancellationToken);
}
/// <summary>
@@ -102,6 +113,9 @@ public sealed class AdminAuthService(
return null;
}
var roleCodes = await ResolveUserRolesAsync(tenantId, user.Id, cancellationToken);
var permissionCodes = await ResolveUserPermissionsAsync(tenantId, user.Id, cancellationToken);
return new UserPermissionDto
{
UserId = user.Id,
@@ -109,8 +123,8 @@ public sealed class AdminAuthService(
MerchantId = user.MerchantId,
Account = user.Account,
DisplayName = user.DisplayName,
Roles = user.Roles,
Permissions = user.Permissions,
Roles = roleCodes,
Permissions = permissionCodes,
CreatedAt = user.CreatedAt
};
}
@@ -147,6 +161,7 @@ public sealed class AdminAuthService(
.Take(pageSize)
.ToList();
var resolved = await ResolveRolesAndPermissionsAsync(tenantId, paged, cancellationToken);
var items = paged.Select(user => new UserPermissionDto
{
UserId = user.Id,
@@ -154,24 +169,116 @@ public sealed class AdminAuthService(
MerchantId = user.MerchantId,
Account = user.Account,
DisplayName = user.DisplayName,
Roles = user.Roles,
Permissions = user.Permissions,
Roles = resolved[user.Id].roles,
Permissions = resolved[user.Id].permissions,
CreatedAt = user.CreatedAt
}).ToList();
return new PagedResult<UserPermissionDto>(items, page, pageSize, users.Count);
}
private static CurrentUserProfile BuildProfile(IdentityUser user)
=> new()
private async Task<CurrentUserProfile> BuildProfileAsync(IdentityUser user, CancellationToken cancellationToken)
{
var tenantId = user.TenantId;
var roles = await ResolveUserRolesAsync(tenantId, user.Id, cancellationToken);
var permissions = await ResolveUserPermissionsAsync(tenantId, user.Id, cancellationToken);
return new CurrentUserProfile
{
UserId = user.Id,
Account = user.Account,
DisplayName = user.DisplayName,
TenantId = user.TenantId,
MerchantId = user.MerchantId,
Roles = user.Roles,
Permissions = user.Permissions,
Roles = roles,
Permissions = permissions,
Avatar = user.Avatar
};
}
private async Task<string[]> ResolveUserRolesAsync(long tenantId, long userId, CancellationToken cancellationToken)
{
var relations = await _userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken);
var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray();
if (roleIds.Length == 0)
{
return Array.Empty<string>();
}
var roles = await _roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken);
return roles.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
}
private async Task<string[]> ResolveUserPermissionsAsync(long tenantId, long userId, CancellationToken cancellationToken)
{
var relations = await _userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken);
var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray();
if (roleIds.Length == 0)
{
return Array.Empty<string>();
}
var rolePermissions = await _rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken);
var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray();
if (permissionIds.Length == 0)
{
return Array.Empty<string>();
}
var permissions = await _permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken);
return permissions.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
}
private async Task<Dictionary<long, (string[] roles, string[] permissions)>> ResolveRolesAndPermissionsAsync(
long tenantId,
IReadOnlyCollection<IdentityUser> users,
CancellationToken cancellationToken)
{
var userIds = users.Select(x => x.Id).ToArray();
var userRoleRelations = await _userRoleRepository.GetByUserIdsAsync(tenantId, userIds, cancellationToken);
var roleIds = userRoleRelations.Select(x => x.RoleId).Distinct().ToArray();
var roles = roleIds.Length == 0
? Array.Empty<Role>()
: await _roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken);
var roleCodeMap = roles.ToDictionary(r => r.Id, r => r.Code, comparer: EqualityComparer<long>.Default);
var rolePermissions = roleIds.Length == 0
? Array.Empty<RolePermission>()
: await _rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken);
var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray();
var permissions = permissionIds.Length == 0
? Array.Empty<Permission>()
: await _permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken);
var permissionCodeMap = permissions.ToDictionary(p => p.Id, p => p.Code, comparer: EqualityComparer<long>.Default);
var rolePermissionsLookup = rolePermissions
.GroupBy(rp => rp.RoleId)
.ToDictionary(g => g.Key, g => g.Select(rp => rp.PermissionId).ToArray(), comparer: EqualityComparer<long>.Default);
var result = new Dictionary<long, (string[] roles, string[] permissions)>();
foreach (var userId in userIds)
{
var rolesForUser = userRoleRelations.Where(ur => ur.UserId == userId).Select(ur => ur.RoleId).Distinct().ToArray();
var roleCodes = rolesForUser
.Select(rid => roleCodeMap.GetValueOrDefault(rid))
.Where(c => !string.IsNullOrWhiteSpace(c))
.Select(c => c!)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
var permissionCodes = rolesForUser
.SelectMany(rid => rolePermissionsLookup.GetValueOrDefault(rid) ?? Array.Empty<long>())
.Select(pid => permissionCodeMap.GetValueOrDefault(pid))
.Where(code => !string.IsNullOrWhiteSpace(code))
.Select(code => code!)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
result[userId] = (roleCodes, permissionCodes);
}
return result;
}
}