diff --git a/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IAdminAuthService.cs b/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IAdminAuthService.cs index 15365df..0fd945f 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IAdminAuthService.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IAdminAuthService.cs @@ -14,7 +14,7 @@ public interface IAdminAuthService Task LoginAsync(AdminLoginRequest request, CancellationToken cancellationToken = default); /// - /// 简化登录:支持使用“账号@手机号”自动解析租户后登录。 + /// 简化登录:与标准登录一致(Admin Portal)。 /// Task LoginSimpleAsync(AdminLoginRequest request, CancellationToken cancellationToken = default); diff --git a/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs b/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs index 13008c3..0c782ff 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs @@ -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 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; - /// /// 管理后台登录:验证账号密码并生成令牌。 /// @@ -48,7 +36,7 @@ public sealed class AdminAuthService( public async Task 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( } /// - /// 简化登录:支持使用“账号@手机号”解析租户后登录。 + /// 简化登录:与标准登录一致(Admin Portal)。 /// /// 登录请求 /// 取消令牌 /// 令牌响应 public async Task 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); } /// @@ -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( /// public async Task 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(items, page, pageSize, users.Count); } private async Task 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 permissions) { // 1. 权限集合 - var permissionSet = new HashSet(permissions ?? [], StringComparer.OrdinalIgnoreCase); + var permissionSet = new HashSet(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 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(); } - 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 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(); } - 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(); } - 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 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() - : 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.Default); + // 3. (空行后) 读取角色-权限关系 var rolePermissions = roleIds.Length == 0 ? Array.Empty() - : 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() - : await _permissionRepository.GetByIdsAsync(permissionIds, cancellationToken); + : await permissionRepository.GetByIdsAsync(permissionIds, cancellationToken); var permissionCodeMap = permissions.ToDictionary(p => p.Id, p => p.Code, comparer: EqualityComparer.Default); + // 5. (空行后) 构建 Role -> PermissionId[] 映射 var rolePermissionsLookup = rolePermissions .GroupBy(rp => rp.RoleId) .ToDictionary(g => g.Key, g => g.Select(rp => rp.PermissionId).ToArray(), comparer: EqualityComparer.Default); + // 6. (空行后) 按用户聚合角色码与权限码 var result = new Dictionary(); 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()) .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; } } diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IIdentityUserRepository.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IIdentityUserRepository.cs index 7276268..74d73fe 100644 --- a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IIdentityUserRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IIdentityUserRepository.cs @@ -9,12 +9,18 @@ namespace TakeoutSaaS.Domain.Identity.Repositories; public interface IIdentityUserRepository { /// - /// 根据账号获取后台用户。 + /// 根据账号获取后台用户(按 Portal 与租户范围精确匹配)。 /// + /// Portal 类型。 + /// 租户 ID(Portal=Tenant 时必填;Portal=Admin 时必须为空)。 /// 账号。 /// 取消标记。 /// 后台用户或 null。 - Task FindByAccountAsync(string account, CancellationToken cancellationToken = default); + Task FindByAccountAsync( + PortalType portal, + long? tenantId, + string account, + CancellationToken cancellationToken = default); /// /// 判断账号是否存在。 diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs index ad0c58c..63f0aa7 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs @@ -1,5 +1,7 @@ +using System; using Microsoft.EntityFrameworkCore; using TakeoutSaaS.Domain.Identity.Entities; +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; namespace TakeoutSaaS.Infrastructure.Identity.Persistence; @@ -12,11 +14,40 @@ public sealed class EfIdentityUserRepository(IdentityDbContext dbContext) : IIde /// /// 根据账号获取后台用户。 /// + /// Portal 类型。 + /// 租户 ID(Portal=Tenant 时必填;Portal=Admin 时必须为空)。 /// 账号。 /// 取消标记。 /// 后台用户或 null。 - public Task FindByAccountAsync(string account, CancellationToken cancellationToken = default) - => dbContext.IdentityUsers.AsNoTracking().FirstOrDefaultAsync(x => x.Account == account, cancellationToken); + public Task FindByAccountAsync( + PortalType portal, + long? tenantId, + string account, + CancellationToken cancellationToken = default) + { + // 1. 参数校验 + if (string.IsNullOrWhiteSpace(account)) + { + throw new ArgumentException("账号不能为空。", nameof(account)); + } + + // 2. (空行后) 标准化账号 + var normalized = account.Trim(); + + // 3. (空行后) 按 Portal 进行精确匹配 + return portal switch + { + PortalType.Admin when tenantId is null => dbContext.IdentityUsers + .AsNoTracking() + .FirstOrDefaultAsync(x => x.Portal == PortalType.Admin && x.Account == normalized, cancellationToken), + PortalType.Admin => throw new ArgumentException("Portal=Admin 时 tenantId 必须为空。", nameof(tenantId)), + PortalType.Tenant when tenantId.HasValue && tenantId.Value > 0 => dbContext.IdentityUsers + .AsNoTracking() + .FirstOrDefaultAsync(x => x.Portal == PortalType.Tenant && x.TenantId == tenantId.Value && x.Account == normalized, cancellationToken), + PortalType.Tenant => throw new ArgumentException("Portal=Tenant 时必须指定 tenantId。", nameof(tenantId)), + _ => throw new ArgumentOutOfRangeException(nameof(portal), portal, "未知 Portal 类型。") + }; + } /// /// 判断账号是否存在。