563 lines
22 KiB
C#
563 lines
22 KiB
C#
using Microsoft.AspNetCore.Identity;
|
||
using TakeoutSaaS.Application.Identity.Abstractions;
|
||
using TakeoutSaaS.Application.Identity.Contracts;
|
||
using TakeoutSaaS.Domain.Identity.Entities;
|
||
using TakeoutSaaS.Domain.Identity.Enums;
|
||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||
|
||
namespace TakeoutSaaS.Application.Identity.Services;
|
||
|
||
/// <summary>
|
||
/// 管理后台认证服务实现。
|
||
/// </summary>
|
||
public sealed class AdminAuthService(
|
||
IIdentityUserRepository userRepository,
|
||
IUserRoleRepository userRoleRepository,
|
||
IRoleRepository roleRepository,
|
||
IPermissionRepository permissionRepository,
|
||
IRolePermissionRepository rolePermissionRepository,
|
||
IMenuRepository menuRepository,
|
||
IPasswordHasher<IdentityUser> passwordHasher,
|
||
IJwtTokenService jwtTokenService,
|
||
IRefreshTokenStore refreshTokenStore,
|
||
ITenantProvider tenantProvider) : IAdminAuthService
|
||
{
|
||
/// <summary>
|
||
/// 管理后台登录:验证账号密码并生成令牌。
|
||
/// </summary>
|
||
/// <param name="request">登录请求</param>
|
||
/// <param name="cancellationToken">取消令牌</param>
|
||
/// <returns>令牌响应</returns>
|
||
/// <exception cref="BusinessException">账号或密码错误时抛出</exception>
|
||
public async Task<TokenResponse> LoginAsync(AdminLoginRequest request, CancellationToken cancellationToken = default)
|
||
{
|
||
// 1. 根据账号查找用户
|
||
var user = await userRepository.FindByAccountAsync(PortalType.Admin, null, request.Account, cancellationToken)
|
||
?? throw new BusinessException(ErrorCodes.Unauthorized, "账号或密码错误");
|
||
|
||
// 2. 校验账号状态
|
||
var now = DateTime.UtcNow;
|
||
if (user.Status == IdentityUserStatus.Disabled)
|
||
{
|
||
throw new BusinessException(ErrorCodes.Forbidden, "账号已被禁用,请联系管理员");
|
||
}
|
||
if (user.Status == IdentityUserStatus.Locked)
|
||
{
|
||
if (user.LockedUntil.HasValue && user.LockedUntil.Value <= now)
|
||
{
|
||
await ResetLockedUserAsync(user.Id, cancellationToken);
|
||
}
|
||
else
|
||
{
|
||
throw new BusinessException(ErrorCodes.Forbidden, "账号已被锁定,请稍后再试");
|
||
}
|
||
}
|
||
if (user.MustChangePassword)
|
||
{
|
||
throw new BusinessException(ErrorCodes.Forbidden, "账号需要重置密码,请通过重置链接设置新密码");
|
||
}
|
||
|
||
// 3. 验证密码(使用 ASP.NET Core Identity 的密码哈希器)
|
||
var result = passwordHasher.VerifyHashedPassword(user, user.PasswordHash, request.Password);
|
||
if (result == PasswordVerificationResult.Failed)
|
||
{
|
||
await IncreaseFailedLoginAsync(user.Id, now, cancellationToken);
|
||
throw new BusinessException(ErrorCodes.Unauthorized, "账号或密码错误");
|
||
}
|
||
|
||
// 4. 更新登录成功状态
|
||
await UpdateLoginSuccessAsync(user.Id, now, cancellationToken);
|
||
|
||
// 5. 构建用户档案并生成令牌
|
||
var profile = await BuildProfileAsync(user, cancellationToken);
|
||
return await jwtTokenService.CreateTokensAsync(profile, false, cancellationToken);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 简化登录:与标准登录一致(Admin Portal)。
|
||
/// </summary>
|
||
/// <param name="request">登录请求</param>
|
||
/// <param name="cancellationToken">取消令牌</param>
|
||
/// <returns>令牌响应</returns>
|
||
public async Task<TokenResponse> LoginSimpleAsync(AdminLoginRequest request, CancellationToken cancellationToken = default)
|
||
{
|
||
// 1. 参数校验
|
||
if (string.IsNullOrWhiteSpace(request.Account))
|
||
{
|
||
throw new BusinessException(ErrorCodes.BadRequest, "账号不能为空");
|
||
}
|
||
|
||
// 2. (空行后) 标准化账号
|
||
request.Account = request.Account.Trim();
|
||
|
||
// 3. (空行后) 走标准登录逻辑(Admin Portal)
|
||
return await LoginAsync(request, cancellationToken);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 刷新访问令牌:使用刷新令牌获取新的访问令牌和刷新令牌。
|
||
/// </summary>
|
||
/// <param name="request">刷新令牌请求</param>
|
||
/// <param name="cancellationToken">取消令牌</param>
|
||
/// <returns>新的令牌响应</returns>
|
||
/// <exception cref="BusinessException">刷新令牌无效、已过期或用户不存在时抛出</exception>
|
||
public async Task<TokenResponse> RefreshTokenAsync(RefreshTokenRequest request, CancellationToken cancellationToken = default)
|
||
{
|
||
// 1. 验证刷新令牌(检查是否存在、是否过期、是否已撤销)
|
||
var descriptor = await refreshTokenStore.GetAsync(request.RefreshToken, cancellationToken);
|
||
if (descriptor == null || descriptor.ExpiresAt <= DateTime.UtcNow || descriptor.Revoked)
|
||
{
|
||
throw new BusinessException(ErrorCodes.Unauthorized, "RefreshToken 无效或已过期");
|
||
}
|
||
|
||
// 2. 根据用户 ID 查找用户
|
||
var user = await userRepository.FindByIdAsync(descriptor.UserId, cancellationToken)
|
||
?? throw new BusinessException(ErrorCodes.Unauthorized, "用户不存在");
|
||
|
||
// 3. 撤销旧刷新令牌(防止重复使用)
|
||
await refreshTokenStore.RevokeAsync(descriptor.Token, cancellationToken);
|
||
|
||
// 4. 生成新的令牌对
|
||
var profile = await BuildProfileAsync(user, cancellationToken);
|
||
return await jwtTokenService.CreateTokensAsync(profile, false, cancellationToken);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取用户档案。
|
||
/// </summary>
|
||
/// <param name="userId">用户 ID</param>
|
||
/// <param name="cancellationToken">取消令牌</param>
|
||
/// <returns>用户档案</returns>
|
||
/// <exception cref="BusinessException">用户不存在时抛出</exception>
|
||
public async Task<CurrentUserProfile> GetProfileAsync(long userId, CancellationToken cancellationToken = default)
|
||
{
|
||
var user = await userRepository.FindByIdAsync(userId, cancellationToken)
|
||
?? throw new BusinessException(ErrorCodes.NotFound, "用户不存在");
|
||
// 1. 返回档案
|
||
return await BuildProfileAsync(user, cancellationToken);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前用户可见菜单树。
|
||
/// </summary>
|
||
/// <param name="userId">用户 ID。</param>
|
||
/// <param name="cancellationToken">取消令牌。</param>
|
||
/// <returns>菜单树。</returns>
|
||
public async Task<IReadOnlyList<MenuNodeDto>> GetMenuTreeAsync(long userId, CancellationToken cancellationToken = default)
|
||
{
|
||
// 1. 读取档案以获取权限
|
||
var profile = await GetProfileAsync(userId, cancellationToken);
|
||
// 2. 读取菜单定义
|
||
var definitions = await menuRepository.GetByPortalAsync(profile.Portal, cancellationToken);
|
||
|
||
// 3. 生成菜单树
|
||
var menu = BuildMenuTree(definitions, profile.Permissions);
|
||
return menu;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取指定用户的权限概览(校验当前租户)。
|
||
/// </summary>
|
||
public async Task<UserPermissionDto?> GetUserPermissionsAsync(long userId, CancellationToken cancellationToken = default)
|
||
{
|
||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||
var user = await userRepository.FindByIdAsync(userId, cancellationToken);
|
||
if (user == null || user.TenantId != tenantId)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
// 1. 解析角色集合
|
||
var roleCodes = await ResolveUserRolesAsync(user.Portal, user.TenantId, user.Id, cancellationToken);
|
||
// 2. (空行后) 解析权限集合
|
||
var permissionCodes = await ResolveUserPermissionsAsync(user.Portal, user.TenantId, user.Id, cancellationToken);
|
||
|
||
// 3. (空行后) 返回概览
|
||
return new UserPermissionDto
|
||
{
|
||
UserId = user.Id,
|
||
TenantId = user.TenantId,
|
||
MerchantId = user.MerchantId,
|
||
Account = user.Account,
|
||
DisplayName = user.DisplayName,
|
||
Roles = roleCodes,
|
||
Permissions = permissionCodes,
|
||
CreatedAt = user.CreatedAt
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 按租户分页查询用户权限概览。
|
||
/// </summary>
|
||
public async Task<PagedResult<UserPermissionDto>> SearchUserPermissionsAsync(
|
||
string? keyword,
|
||
int page,
|
||
int pageSize,
|
||
string? sortBy,
|
||
bool sortDescending,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
// 1. 获取当前租户
|
||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||
// 2. (空行后) 查询用户列表
|
||
var users = await userRepository.SearchAsync(tenantId, keyword, cancellationToken);
|
||
|
||
// 3. (空行后) 排序
|
||
var sorted = sortBy?.ToLowerInvariant() switch
|
||
{
|
||
"account" => sortDescending
|
||
? users.OrderByDescending(x => x.Account)
|
||
: users.OrderBy(x => x.Account),
|
||
"displayname" => sortDescending
|
||
? users.OrderByDescending(x => x.DisplayName)
|
||
: users.OrderBy(x => x.DisplayName),
|
||
_ => sortDescending
|
||
? users.OrderByDescending(x => x.CreatedAt)
|
||
: users.OrderBy(x => x.CreatedAt)
|
||
};
|
||
|
||
// 4. (空行后) 分页
|
||
var paged = sorted
|
||
.Skip((page - 1) * pageSize)
|
||
.Take(pageSize)
|
||
.ToList();
|
||
|
||
// 5. (空行后) 解析角色与权限
|
||
var resolved = await ResolveRolesAndPermissionsAsync(PortalType.Tenant, tenantId, paged, cancellationToken);
|
||
// 6. (空行后) 映射为 DTO
|
||
var items = paged.Select(user => new UserPermissionDto
|
||
{
|
||
UserId = user.Id,
|
||
TenantId = user.TenantId,
|
||
MerchantId = user.MerchantId,
|
||
Account = user.Account,
|
||
DisplayName = user.DisplayName,
|
||
Roles = resolved[user.Id].roles,
|
||
Permissions = resolved[user.Id].permissions,
|
||
CreatedAt = user.CreatedAt
|
||
}).ToList();
|
||
|
||
// 7. (空行后) 返回分页结果
|
||
return new PagedResult<UserPermissionDto>(items, page, pageSize, users.Count);
|
||
}
|
||
|
||
private async Task<CurrentUserProfile> BuildProfileAsync(IdentityUser user, CancellationToken cancellationToken)
|
||
{
|
||
// 1. 解析角色集合
|
||
var roles = await ResolveUserRolesAsync(user.Portal, user.TenantId, user.Id, cancellationToken);
|
||
// 2. (空行后) 解析权限集合
|
||
var permissions = await ResolveUserPermissionsAsync(user.Portal, user.TenantId, user.Id, cancellationToken);
|
||
|
||
// 3. (空行后) 组装档案
|
||
return new CurrentUserProfile
|
||
{
|
||
Portal = user.Portal,
|
||
UserId = user.Id,
|
||
Account = user.Account,
|
||
DisplayName = user.DisplayName,
|
||
TenantId = user.TenantId,
|
||
MerchantId = user.MerchantId,
|
||
Roles = roles,
|
||
Permissions = permissions,
|
||
Avatar = user.Avatar
|
||
};
|
||
}
|
||
|
||
private async Task ResetLockedUserAsync(long userId, CancellationToken cancellationToken)
|
||
{
|
||
// 1. 获取可更新实体
|
||
var tracked = await userRepository.GetForUpdateAsync(userId, cancellationToken);
|
||
if (tracked == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// 2. 解除锁定并清空失败次数
|
||
tracked.Status = IdentityUserStatus.Active;
|
||
tracked.LockedUntil = null;
|
||
tracked.FailedLoginCount = 0;
|
||
|
||
// 3. 保存变更
|
||
await userRepository.SaveChangesAsync(cancellationToken);
|
||
}
|
||
|
||
private async Task IncreaseFailedLoginAsync(long userId, DateTime now, CancellationToken cancellationToken)
|
||
{
|
||
// 1. 获取可更新实体
|
||
var tracked = await userRepository.GetForUpdateAsync(userId, cancellationToken);
|
||
if (tracked == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// 2. 累计失败次数并判断是否需要锁定
|
||
tracked.FailedLoginCount += 1;
|
||
if (tracked.FailedLoginCount >= 5)
|
||
{
|
||
tracked.Status = IdentityUserStatus.Locked;
|
||
tracked.LockedUntil = now.AddMinutes(15);
|
||
}
|
||
|
||
// 3. 保存变更
|
||
await userRepository.SaveChangesAsync(cancellationToken);
|
||
}
|
||
|
||
private async Task UpdateLoginSuccessAsync(long userId, DateTime now, CancellationToken cancellationToken)
|
||
{
|
||
// 1. 获取可更新实体
|
||
var tracked = await userRepository.GetForUpdateAsync(userId, cancellationToken);
|
||
if (tracked == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// 2. 重置失败次数并刷新登录时间
|
||
tracked.FailedLoginCount = 0;
|
||
tracked.LockedUntil = null;
|
||
tracked.LastLoginAt = now;
|
||
if (tracked.Status == IdentityUserStatus.Locked)
|
||
{
|
||
tracked.Status = IdentityUserStatus.Active;
|
||
}
|
||
|
||
// 3. 保存变更
|
||
await userRepository.SaveChangesAsync(cancellationToken);
|
||
}
|
||
|
||
private static bool IsLikelyPhone(string value)
|
||
{
|
||
// 1. 空值快速返回
|
||
if (string.IsNullOrWhiteSpace(value))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
// 2. 标准化前缀
|
||
var span = value.AsSpan();
|
||
if (span[0] == '+')
|
||
{
|
||
span = span[1..];
|
||
}
|
||
|
||
// 3. 长度校验
|
||
if (span.Length < 6 || span.Length > 32)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
// 4. 逐字符校验
|
||
foreach (var ch in span)
|
||
{
|
||
if (!char.IsDigit(ch))
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 5. 返回校验结果
|
||
return true;
|
||
}
|
||
|
||
private static IReadOnlyList<MenuNodeDto> BuildMenuTree(
|
||
IReadOnlyList<Domain.Identity.Entities.MenuDefinition> definitions,
|
||
IReadOnlyList<string> permissions)
|
||
{
|
||
// 1. 权限集合
|
||
var permissionSet = new HashSet<string>(permissions, StringComparer.OrdinalIgnoreCase);
|
||
|
||
// 2. 父子分组
|
||
var parentLookup = definitions
|
||
.GroupBy(x => x.ParentId)
|
||
.ToDictionary(g => g.Key, g => g.OrderBy(d => d.SortOrder).ToList());
|
||
|
||
// 3. 递归构造并过滤
|
||
IReadOnlyList<MenuNodeDto> BuildChildren(long parentId)
|
||
{
|
||
if (!parentLookup.TryGetValue(parentId, out var children))
|
||
{
|
||
return [];
|
||
}
|
||
|
||
// 1. 收集可见节点
|
||
var result = new List<MenuNodeDto>();
|
||
foreach (var def in children)
|
||
{
|
||
// 1.1 构造节点
|
||
var node = MapMenuNode(def);
|
||
|
||
// 1.2 递归子节点
|
||
var sub = BuildChildren(def.Id);
|
||
|
||
// 1.3 可见性(严格使用 RequiredPermissions,不做兜底)
|
||
var requiredPermissions = node.RequiredPermissions;
|
||
// 1.4 (空行后) 判断可见性
|
||
var isVisible = requiredPermissions.Length == 0 || requiredPermissions.Any(permissionSet.Contains);
|
||
|
||
// 1.5 (空行后) 收集
|
||
if (isVisible || sub.Count > 0)
|
||
{
|
||
result.Add(node with { Children = sub });
|
||
}
|
||
}
|
||
|
||
// 2. 返回本层节点
|
||
return result;
|
||
}
|
||
|
||
// 4. 返回根节点
|
||
return BuildChildren(0);
|
||
}
|
||
|
||
private static MenuNodeDto MapMenuNode(Domain.Identity.Entities.MenuDefinition definition)
|
||
{
|
||
// 1. 解析权限
|
||
var requiredPermissions = SplitCodes(definition.RequiredPermissions);
|
||
var metaPermissions = SplitCodes(definition.MetaPermissions);
|
||
var metaRoles = SplitCodes(definition.MetaRoles);
|
||
|
||
// 2. 解析按钮权限
|
||
var authList = string.IsNullOrWhiteSpace(definition.AuthListJson)
|
||
? []
|
||
: System.Text.Json.JsonSerializer.Deserialize<List<MenuAuthItemDto>>(definition.AuthListJson) ?? [];
|
||
|
||
// 3. 构造节点
|
||
return new MenuNodeDto
|
||
{
|
||
Name = definition.Name,
|
||
Path = definition.Path,
|
||
Component = definition.Component,
|
||
Meta = new MenuMetaDto
|
||
{
|
||
Title = definition.Title,
|
||
Icon = definition.Icon,
|
||
KeepAlive = definition.KeepAlive,
|
||
IsIframe = definition.IsIframe,
|
||
Link = definition.Link,
|
||
Roles = metaRoles,
|
||
Permissions = metaPermissions,
|
||
AuthList = authList
|
||
},
|
||
Children = [],
|
||
RequiredPermissions = requiredPermissions
|
||
};
|
||
}
|
||
|
||
private static string[] SplitCodes(string? value)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(value))
|
||
{
|
||
return Array.Empty<string>();
|
||
}
|
||
|
||
// 1. 拆分去重
|
||
return value
|
||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||
.ToArray();
|
||
}
|
||
|
||
private async Task<string[]> ResolveUserRolesAsync(PortalType portal, long? tenantId, long userId, CancellationToken cancellationToken)
|
||
{
|
||
var relations = await userRoleRepository.GetByUserIdAsync(portal, tenantId, userId, cancellationToken);
|
||
var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray();
|
||
if (roleIds.Length == 0)
|
||
{
|
||
return Array.Empty<string>();
|
||
}
|
||
|
||
// 2. (空行后) 查询角色定义并返回角色码
|
||
var roles = await roleRepository.GetByIdsAsync(portal, tenantId, roleIds, cancellationToken);
|
||
return roles.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||
}
|
||
|
||
private async Task<string[]> ResolveUserPermissionsAsync(PortalType portal, long? tenantId, long userId, CancellationToken cancellationToken)
|
||
{
|
||
var relations = await userRoleRepository.GetByUserIdAsync(portal, tenantId, userId, cancellationToken);
|
||
var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray();
|
||
if (roleIds.Length == 0)
|
||
{
|
||
return Array.Empty<string>();
|
||
}
|
||
|
||
// 2. (空行后) 查询角色权限关联
|
||
var rolePermissions = await rolePermissionRepository.GetByRoleIdsAsync(portal, tenantId, roleIds, cancellationToken);
|
||
var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray();
|
||
if (permissionIds.Length == 0)
|
||
{
|
||
return Array.Empty<string>();
|
||
}
|
||
|
||
// 3. (空行后) 查询权限定义并返回权限码
|
||
var permissions = await permissionRepository.GetByIdsAsync(permissionIds, cancellationToken);
|
||
return permissions.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||
}
|
||
|
||
private async Task<Dictionary<long, (string[] roles, string[] permissions)>> ResolveRolesAndPermissionsAsync(
|
||
PortalType portal,
|
||
long? tenantId,
|
||
IReadOnlyCollection<IdentityUser> users,
|
||
CancellationToken cancellationToken)
|
||
{
|
||
// 1. 读取用户-角色关系
|
||
var userIds = users.Select(x => x.Id).ToArray();
|
||
var userRoleRelations = await userRoleRepository.GetByUserIdsAsync(portal, tenantId, userIds, cancellationToken);
|
||
var roleIds = userRoleRelations.Select(x => x.RoleId).Distinct().ToArray();
|
||
|
||
// 2. (空行后) 读取角色定义
|
||
var roles = roleIds.Length == 0
|
||
? Array.Empty<Role>()
|
||
: await roleRepository.GetByIdsAsync(portal, tenantId, roleIds, cancellationToken);
|
||
var roleCodeMap = roles.ToDictionary(r => r.Id, r => r.Code, comparer: EqualityComparer<long>.Default);
|
||
|
||
// 3. (空行后) 读取角色-权限关系
|
||
var rolePermissions = roleIds.Length == 0
|
||
? Array.Empty<RolePermission>()
|
||
: await rolePermissionRepository.GetByRoleIdsAsync(portal, tenantId, roleIds, cancellationToken);
|
||
|
||
// 4. (空行后) 读取权限定义
|
||
var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray();
|
||
var permissions = permissionIds.Length == 0
|
||
? Array.Empty<Permission>()
|
||
: await permissionRepository.GetByIdsAsync(permissionIds, cancellationToken);
|
||
var permissionCodeMap = permissions.ToDictionary(p => p.Id, p => p.Code, comparer: EqualityComparer<long>.Default);
|
||
|
||
// 5. (空行后) 构建 Role -> PermissionId[] 映射
|
||
var rolePermissionsLookup = rolePermissions
|
||
.GroupBy(rp => rp.RoleId)
|
||
.ToDictionary(g => g.Key, g => g.Select(rp => rp.PermissionId).ToArray(), comparer: EqualityComparer<long>.Default);
|
||
|
||
// 6. (空行后) 按用户聚合角色码与权限码
|
||
var result = new Dictionary<long, (string[] roles, string[] permissions)>();
|
||
foreach (var userId in userIds)
|
||
{
|
||
// 6.1 解析用户角色码
|
||
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();
|
||
|
||
// 6.2 (空行后) 解析用户权限码
|
||
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();
|
||
|
||
// 6.3 (空行后) 写入结果
|
||
result[userId] = (roleCodes, permissionCodes);
|
||
}
|
||
|
||
// 7. (空行后) 返回聚合结果
|
||
return result;
|
||
}
|
||
}
|