fix: 管理后台登录按Portal精确匹配
This commit is contained in:
@@ -14,7 +14,7 @@ public interface IAdminAuthService
|
||||
Task<TokenResponse> LoginAsync(AdminLoginRequest request, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 简化登录:支持使用“账号@手机号”自动解析租户后登录。
|
||||
/// 简化登录:与标准登录一致(Admin Portal)。
|
||||
/// </summary>
|
||||
Task<TokenResponse> LoginSimpleAsync(AdminLoginRequest request, CancellationToken cancellationToken = default);
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ using TakeoutSaaS.Application.Identity.Contracts;
|
||||
using TakeoutSaaS.Domain.Identity.Entities;
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Domain.Tenants.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
@@ -25,19 +24,8 @@ public sealed class AdminAuthService(
|
||||
IPasswordHasher<IdentityUser> passwordHasher,
|
||||
IJwtTokenService jwtTokenService,
|
||||
IRefreshTokenStore refreshTokenStore,
|
||||
ITenantProvider tenantProvider,
|
||||
ITenantContextAccessor tenantContextAccessor,
|
||||
ITenantRepository tenantRepository) : IAdminAuthService
|
||||
ITenantProvider tenantProvider) : IAdminAuthService
|
||||
{
|
||||
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||
private readonly ITenantContextAccessor _tenantContextAccessor = tenantContextAccessor;
|
||||
private readonly ITenantRepository _tenantRepository = tenantRepository;
|
||||
private readonly IUserRoleRepository _userRoleRepository = userRoleRepository;
|
||||
private readonly IRoleRepository _roleRepository = roleRepository;
|
||||
private readonly IPermissionRepository _permissionRepository = permissionRepository;
|
||||
private readonly IRolePermissionRepository _rolePermissionRepository = rolePermissionRepository;
|
||||
private readonly IMenuRepository _menuRepository = menuRepository;
|
||||
|
||||
/// <summary>
|
||||
/// 管理后台登录:验证账号密码并生成令牌。
|
||||
/// </summary>
|
||||
@@ -48,7 +36,7 @@ public sealed class AdminAuthService(
|
||||
public async Task<TokenResponse> LoginAsync(AdminLoginRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 根据账号查找用户
|
||||
var user = await userRepository.FindByAccountAsync(request.Account, cancellationToken)
|
||||
var user = await userRepository.FindByAccountAsync(PortalType.Admin, null, request.Account, cancellationToken)
|
||||
?? throw new BusinessException(ErrorCodes.Unauthorized, "账号或密码错误");
|
||||
|
||||
// 2. 校验账号状态
|
||||
@@ -90,57 +78,24 @@ public sealed class AdminAuthService(
|
||||
}
|
||||
|
||||
/// <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. 标准化输入
|
||||
var rawAccount = request.Account?.Trim() ?? string.Empty;
|
||||
|
||||
// 2. 尝试解析 “账号@手机号”
|
||||
var atIndex = rawAccount.LastIndexOf('@');
|
||||
if (atIndex > 0 && atIndex < rawAccount.Length - 1)
|
||||
// 1. 参数校验
|
||||
if (string.IsNullOrWhiteSpace(request.Account))
|
||||
{
|
||||
var accountPart = rawAccount[..atIndex].Trim();
|
||||
var phonePart = rawAccount[(atIndex + 1)..].Trim();
|
||||
|
||||
if (IsLikelyPhone(phonePart))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(accountPart))
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "账号格式错误,应为 账号@手机号");
|
||||
}
|
||||
|
||||
var tenantId = await _tenantRepository.FindTenantIdByContactPhoneAsync(phonePart, cancellationToken);
|
||||
if (!tenantId.HasValue || tenantId.Value == 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Unauthorized, "账号或密码错误");
|
||||
}
|
||||
|
||||
var originalTenant = _tenantContextAccessor.Current;
|
||||
_tenantContextAccessor.Current = new TenantContext(tenantId.Value, null, "login:simple:contact_phone");
|
||||
try
|
||||
{
|
||||
return await LoginAsync(new AdminLoginRequest { Account = accountPart, Password = request.Password }, cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_tenantContextAccessor.Current = originalTenant;
|
||||
}
|
||||
}
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "账号不能为空");
|
||||
}
|
||||
|
||||
// 3. 未携带手机号时,要求外部已解析租户(Header/Host 等)
|
||||
if (_tenantProvider.GetCurrentTenantId() == 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识,请使用 账号@手机号 登录");
|
||||
}
|
||||
// 2. (空行后) 标准化账号
|
||||
request.Account = request.Account.Trim();
|
||||
|
||||
// 4. 走原有登录逻辑
|
||||
return await LoginAsync(new AdminLoginRequest { Account = rawAccount, Password = request.Password }, cancellationToken);
|
||||
// 3. (空行后) 走标准登录逻辑(Admin Portal)
|
||||
return await LoginAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -197,7 +152,7 @@ public sealed class AdminAuthService(
|
||||
// 1. 读取档案以获取权限
|
||||
var profile = await GetProfileAsync(userId, cancellationToken);
|
||||
// 2. 读取菜单定义
|
||||
var definitions = await _menuRepository.GetByPortalAsync(profile.Portal, cancellationToken);
|
||||
var definitions = await menuRepository.GetByPortalAsync(profile.Portal, cancellationToken);
|
||||
|
||||
// 3. 生成菜单树
|
||||
var menu = BuildMenuTree(definitions, profile.Permissions);
|
||||
@@ -209,16 +164,19 @@ public sealed class AdminAuthService(
|
||||
/// </summary>
|
||||
public async Task<UserPermissionDto?> GetUserPermissionsAsync(long userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tenantId = _tenantProvider.GetCurrentTenantId();
|
||||
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,
|
||||
@@ -243,9 +201,12 @@ public sealed class AdminAuthService(
|
||||
bool sortDescending,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tenantId = _tenantProvider.GetCurrentTenantId();
|
||||
// 1. 获取当前租户
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
// 2. (空行后) 查询用户列表
|
||||
var users = await userRepository.SearchAsync(tenantId, keyword, cancellationToken);
|
||||
|
||||
// 3. (空行后) 排序
|
||||
var sorted = sortBy?.ToLowerInvariant() switch
|
||||
{
|
||||
"account" => sortDescending
|
||||
@@ -259,12 +220,15 @@ public sealed class AdminAuthService(
|
||||
: 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,
|
||||
@@ -277,14 +241,18 @@ public sealed class AdminAuthService(
|
||||
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,
|
||||
@@ -362,22 +330,26 @@ public sealed class AdminAuthService(
|
||||
|
||||
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))
|
||||
@@ -386,6 +358,7 @@ public sealed class AdminAuthService(
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 返回校验结果
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -394,7 +367,7 @@ public sealed class AdminAuthService(
|
||||
IReadOnlyList<string> permissions)
|
||||
{
|
||||
// 1. 权限集合
|
||||
var permissionSet = new HashSet<string>(permissions ?? [], StringComparer.OrdinalIgnoreCase);
|
||||
var permissionSet = new HashSet<string>(permissions, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// 2. 父子分组
|
||||
var parentLookup = definitions
|
||||
@@ -419,18 +392,13 @@ public sealed class AdminAuthService(
|
||||
// 1.2 递归子节点
|
||||
var sub = BuildChildren(def.Id);
|
||||
|
||||
// 1.3 可见性
|
||||
var required = node.RequiredPermissions ?? [];
|
||||
if (required.Length == 0 && node.Meta.Permissions.Length > 0)
|
||||
{
|
||||
// Fall back to meta permissions when explicit required permissions are missing.
|
||||
required = node.Meta.Permissions;
|
||||
}
|
||||
// 1.3 可见性(严格使用 RequiredPermissions,不做兜底)
|
||||
var requiredPermissions = node.RequiredPermissions;
|
||||
// 1.4 (空行后) 判断可见性
|
||||
var isVisible = requiredPermissions.Length == 0 || requiredPermissions.Any(permissionSet.Contains);
|
||||
|
||||
var visible = required.Length == 0 || required.Any(permissionSet.Contains);
|
||||
|
||||
// 1.4 收集
|
||||
if (visible || sub.Count > 0)
|
||||
// 1.5 (空行后) 收集
|
||||
if (isVisible || sub.Count > 0)
|
||||
{
|
||||
result.Add(node with { Children = sub });
|
||||
}
|
||||
@@ -494,34 +462,37 @@ public sealed class AdminAuthService(
|
||||
|
||||
private async Task<string[]> ResolveUserRolesAsync(PortalType portal, long? tenantId, long userId, CancellationToken cancellationToken)
|
||||
{
|
||||
var relations = await _userRoleRepository.GetByUserIdAsync(portal, tenantId, userId, 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>();
|
||||
}
|
||||
|
||||
var roles = await _roleRepository.GetByIdsAsync(portal, tenantId, roleIds, cancellationToken);
|
||||
// 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 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>();
|
||||
}
|
||||
|
||||
var rolePermissions = await _rolePermissionRepository.GetByRoleIdsAsync(portal, tenantId, roleIds, cancellationToken);
|
||||
// 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>();
|
||||
}
|
||||
|
||||
var permissions = await _permissionRepository.GetByIdsAsync(permissionIds, cancellationToken);
|
||||
// 3. (空行后) 查询权限定义并返回权限码
|
||||
var permissions = await permissionRepository.GetByIdsAsync(permissionIds, cancellationToken);
|
||||
return permissions.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
}
|
||||
|
||||
@@ -531,32 +502,39 @@ public sealed class AdminAuthService(
|
||||
IReadOnlyCollection<IdentityUser> users,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 读取用户-角色关系
|
||||
var userIds = users.Select(x => x.Id).ToArray();
|
||||
var userRoleRelations = await _userRoleRepository.GetByUserIdsAsync(portal, tenantId, userIds, cancellationToken);
|
||||
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);
|
||||
: 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);
|
||||
: 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);
|
||||
: 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))
|
||||
@@ -565,6 +543,7 @@ public sealed class AdminAuthService(
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
// 6.2 (空行后) 解析用户权限码
|
||||
var permissionCodes = rolesForUser
|
||||
.SelectMany(rid => rolePermissionsLookup.GetValueOrDefault(rid) ?? Array.Empty<long>())
|
||||
.Select(pid => permissionCodeMap.GetValueOrDefault(pid))
|
||||
@@ -573,9 +552,11 @@ public sealed class AdminAuthService(
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
// 6.3 (空行后) 写入结果
|
||||
result[userId] = (roleCodes, permissionCodes);
|
||||
}
|
||||
|
||||
// 7. (空行后) 返回聚合结果
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user