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; /// /// 管理后台认证服务实现。 /// public sealed class AdminAuthService( IIdentityUserRepository userRepository, IUserRoleRepository userRoleRepository, IRoleRepository roleRepository, IPermissionRepository permissionRepository, IRolePermissionRepository rolePermissionRepository, IMenuRepository menuRepository, IPasswordHasher passwordHasher, IJwtTokenService jwtTokenService, IRefreshTokenStore refreshTokenStore, ITenantProvider tenantProvider) : IAdminAuthService { /// /// 管理后台登录:验证账号密码并生成令牌。 /// /// 登录请求 /// 取消令牌 /// 令牌响应 /// 账号或密码错误时抛出 public async Task 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); } /// /// 简化登录:与标准登录一致(Admin Portal)。 /// /// 登录请求 /// 取消令牌 /// 令牌响应 public async Task 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); } /// /// 刷新访问令牌:使用刷新令牌获取新的访问令牌和刷新令牌。 /// /// 刷新令牌请求 /// 取消令牌 /// 新的令牌响应 /// 刷新令牌无效、已过期或用户不存在时抛出 public async Task 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); } /// /// 获取用户档案。 /// /// 用户 ID /// 取消令牌 /// 用户档案 /// 用户不存在时抛出 public async Task GetProfileAsync(long userId, CancellationToken cancellationToken = default) { var user = await userRepository.FindByIdAsync(userId, cancellationToken) ?? throw new BusinessException(ErrorCodes.NotFound, "用户不存在"); // 1. 返回档案 return await BuildProfileAsync(user, cancellationToken); } /// /// 获取当前用户可见菜单树。 /// /// 用户 ID。 /// 取消令牌。 /// 菜单树。 public async Task> 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; } /// /// 获取指定用户的权限概览(校验当前租户)。 /// public async Task 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 }; } /// /// 按租户分页查询用户权限概览。 /// public async Task> 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(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, 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 BuildMenuTree( IReadOnlyList definitions, IReadOnlyList permissions) { // 1. 权限集合 var permissionSet = new HashSet(permissions, StringComparer.OrdinalIgnoreCase); // 2. 父子分组 var parentLookup = definitions .GroupBy(x => x.ParentId) .ToDictionary(g => g.Key, g => g.OrderBy(d => d.SortOrder).ToList()); // 3. 递归构造并过滤 IReadOnlyList BuildChildren(long parentId) { if (!parentLookup.TryGetValue(parentId, out var children)) { return []; } // 1. 收集可见节点 var result = new List(); 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>(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(); } // 1. 拆分去重 return value .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); } private async Task 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(); } // 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 roleIds = relations.Select(x => x.RoleId).Distinct().ToArray(); if (roleIds.Length == 0) { return Array.Empty(); } // 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(); } // 3. (空行后) 查询权限定义并返回权限码 var permissions = await permissionRepository.GetByIdsAsync(permissionIds, cancellationToken); return permissions.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } private async Task> ResolveRolesAndPermissionsAsync( PortalType portal, long? tenantId, IReadOnlyCollection 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() : 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); // 4. (空行后) 读取权限定义 var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray(); var permissions = permissionIds.Length == 0 ? Array.Empty() : 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)) .Where(c => !string.IsNullOrWhiteSpace(c)) .Select(c => c!) .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); // 6.2 (空行后) 解析用户权限码 var permissionCodes = rolesForUser .SelectMany(rid => rolePermissionsLookup.GetValueOrDefault(rid) ?? Array.Empty()) .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; } }