using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using Fake.Application; using Fake.Domain.Exceptions; using Fake.Rbac.Application.Dtos.Auth; using Fake.Rbac.Application.Dtos.Menu; using Fake.Rbac.Application.Dtos.Role; using Fake.Rbac.Application.Dtos.User; using Fake.Rbac.Application.Jwt; using Fake.Rbac.Domain.Managers; using Fake.Rbac.Domain.UserAggregate; using Fake.Rbac.Infrastructure; using Fake.Security.Claims; using Fake.Users; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using Oms.Application.Shared; using Oms.Domain.CustomersAggregate.Entities; using Oms.Domain.CustomersAggregate.Enums; using Oms.Domain.CustomersAggregate.Repositories; using SimpleAdmin.Api.WeChat.Dtos; using SimpleAdmin.Api.WeChat.Services; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Processing; namespace SimpleAdmin.Api.Auth; /// /// 接管 OMS 与 RBAC 共用的认证接口,补充客户登录和业务主体 claims 解析。 /// [Authorize] [ApiExplorerSettings(GroupName = "RBAC")] public partial class AuthService( AccountManager accountManager, IJwtService jwtService, IUserRepository userRepository, IWebHostEnvironment webHostEnvironment, IHttpContextAccessor httpContextAccessor, FakeRbacDbContext rbacDbContext, ICustomerRepository customerRepository, IWeChatLoginSessionStore weChatLoginSessionStore, IMemoryCache memoryCache, IRbacAuthCache rbacAuthCache) : ApplicationService { private static readonly TimeSpan UserInfoCacheTtl = TimeSpan.FromMinutes(3); private static readonly TimeSpan AllMenusCacheTtl = TimeSpan.FromMinutes(5); private Guid CurrentUserGuid => CurrentUser.FindClaimValueOrNull(ClaimTypes.NameIdentifier)!.To(); /// /// 用户或客户登录。 /// /// 入参:登录类型、账号和密码。 /// 取消令牌。 /// 登录令牌、刷新令牌和用户完整信息(含 menus)。 [AllowAnonymous] [HttpPost("/rbac/auth/login")] public async Task LoginAsync([FromBody] OmsLoginInput input, CancellationToken cancellationToken = default) { return input.Type switch { OmsLoginType.User => await LoginAdminAsync(input.Account, input.Password, cancellationToken), OmsLoginType.Customer => await LoginCustomerAsync(input.Account, input.Password, cancellationToken), _ => throw new BusinessException("不支持的登录类型") }; } /// /// 管理员代登客户门户(免密)。 /// /// 入参:目标客户主键。 /// 取消令牌。 /// 客户门户访问令牌。 [HttpPost("/rbac/auth/customer-oms-login")] public async Task CustomerOmsLoginAsync( [FromBody] OmsCustomerOmsLoginInput input, CancellationToken cancellationToken = default) { EnsureAdminPrincipal(); var customer = await customerRepository.FirstOrDefaultAsync(x => x.Id == input.CustomerId, cancellationToken); if (customer == null) { throw new BusinessException("客户不存在"); } if (customer.ParentCustomerId.HasValue) { ValidateSubCustomerStatus(customer); var mainCustomer = await customerRepository.FirstOrDefaultAsync( x => x.Id == customer.ParentCustomerId.Value, cancellationToken); if (mainCustomer == null) { throw new BusinessException("主客户不存在"); } ValidateMainCustomerStatus(mainCustomer); } else { ValidateMainCustomerStatus(customer); } var userGuid = await GetRbacUserGuidByMapIdAsync(customer.UserId, cancellationToken); if (!userGuid.HasValue) { throw new BusinessException("客户未关联用户"); } return await BuildLoginResultAsync(userGuid.Value, OmsLoginType.Customer, cancellationToken); } /// /// 微信扫码登录轮询状态(key 是否已有绑定账号)。 /// [AllowAnonymous] [HttpGet("/rbac/auth/wx-login/status")] public async Task WxLoginStatusAsync( [FromQuery] string loginUuid, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(loginUuid)) { throw new BusinessException("缺少 loginUuid"); } var session = await weChatLoginSessionStore.GetSessionAsync(loginUuid.Trim(), cancellationToken); if (session == null) { return new WxLoginStatusOutput { Ready = false }; } return new WxLoginStatusOutput { Ready = true, Account = string.IsNullOrWhiteSpace(session.CustomerCode) ? session.OpenId : session.CustomerCode }; } /// /// 微信扫码登录(轮询)。 /// [AllowAnonymous] [HttpPost("/rbac/auth/wx-login")] public async Task WxLoginAsync( [FromBody] OmsWxLoginInput input, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(input.LoginUuid)) { throw new BusinessException("缺少 loginUuid"); } var loginKey = input.LoginUuid.Trim(); var session = await weChatLoginSessionStore.GetSessionAsync(loginKey, cancellationToken); if (session == null) { throw new BusinessException("请扫码登录"); } var customer = await ResolveCustomerFromWxSessionAsync(session, cancellationToken); if (customer == null) { throw new BusinessException("未绑定微信,请先注册或绑定账号"); } if (customer.ParentCustomerId.HasValue) { throw new BusinessException("子客户不支持微信扫码登录"); } ValidateMainCustomerStatus(customer); customer.LastLoginIp = httpContextAccessor.HttpContext?.Connection.RemoteIpAddress?.ToString(); customer.LastLoginTime = DateTime.Now; await customerRepository.UpdateAsync(customer, cancellationToken); var userGuid = await GetRbacUserGuidByMapIdAsync(customer.UserId, cancellationToken); if (!userGuid.HasValue) { throw new BusinessException("客户未关联用户"); } await weChatLoginSessionStore.RemoveAsync(loginKey, cancellationToken); return await BuildLoginResultAsync(userGuid.Value, OmsLoginType.Customer, cancellationToken); } /// /// 微信网页授权登录(H5 内点按钮授权,无需扫码)。 /// [AllowAnonymous] [HttpPost("/rbac/auth/wx-oauth-login")] public async Task WxOAuthLoginAsync( [FromBody] OmsWxOAuthLoginInput input, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(input.Code)) { throw new BusinessException("缺少微信授权 code"); } var token = await WeChatMpApiClient.GetOAuthAccessTokenAsync(input.Code.Trim(), cancellationToken); if (token == null || string.IsNullOrWhiteSpace(token.OpenId)) { throw new BusinessException("微信授权失败,请重试"); } var customer = await customerRepository.GetByWechatOpenidAsync(token.OpenId, cancellationToken); if (customer == null) { throw new BusinessException("未绑定微信,请先注册或绑定账号"); } if (customer.ParentCustomerId.HasValue) { throw new BusinessException("子客户不支持微信授权登录"); } ValidateMainCustomerStatus(customer); customer.LastLoginIp = httpContextAccessor.HttpContext?.Connection.RemoteIpAddress?.ToString(); customer.LastLoginTime = DateTime.Now; await customerRepository.UpdateAsync(customer, cancellationToken); var userGuid = await GetRbacUserGuidByMapIdAsync(customer.UserId, cancellationToken); if (!userGuid.HasValue) { throw new BusinessException("客户未关联用户"); } return await BuildLoginResultAsync(userGuid.Value, OmsLoginType.Customer, cancellationToken); } private async Task ResolveCustomerFromWxSessionAsync( WeChatLoginSessionPayload session, CancellationToken cancellationToken) { if (session.CustomerId > 0) { return await customerRepository.FirstOrDefaultAsync( x => x.Id == session.CustomerId, cancellationToken: cancellationToken); } if (!string.IsNullOrWhiteSpace(session.OpenId)) { return await customerRepository.GetByWechatOpenidAsync(session.OpenId, cancellationToken); } return null; } /// /// 刷新登录令牌。 /// /// 入参:刷新令牌。 /// 取消令牌。 /// 新的访问令牌、刷新令牌和用户基础信息。 [AllowAnonymous] [HttpPost("/rbac/auth/refresh-token")] public async Task RefreshTokenAsync([FromBody] OmsRefreshTokenInput input, CancellationToken cancellationToken = default) { var refreshToken = input.RefreshToken; var userIdText = jwtService.ValidateRefreshToken(refreshToken); if (string.IsNullOrWhiteSpace(userIdText)) { throw new BusinessException("无效的刷新令牌"); } var token = new JwtSecurityTokenHandler().ReadJwtToken(refreshToken); var loginType = ParseLoginType(token.Claims); if (loginType == OmsLoginType.Customer) { return await RefreshCustomerTokenAsync(token.Claims, cancellationToken); } if (!Guid.TryParse(userIdText, out var userId)) { throw new BusinessException("无效的刷新令牌"); } var user = await userRepository.FirstOrDefaultAsync(x => x.Id == userId, cancellationToken: cancellationToken); if (user == null) { throw new BusinessException("用户不存在"); } // 刷新令牌只发新 token,不重建 UserInfo(前端忽略该字段) var claims = ReuseClaimsFromRefreshToken(token.Claims, OmsLoginType.User); return BuildTokenOnlyResult(user.Id, claims); } /// /// 获取当前登录用户信息。 /// /// 取消令牌。 /// 当前登录用户信息、角色和菜单权限。 [HttpGet("/rbac/auth/get-current-user")] public async Task GetCurrentUserAsync(CancellationToken cancellationToken = default) { if (!CurrentUser.IsAuthenticated) { throw new UnauthorizedAccessException("用户未登录"); } if (IsCustomerPrincipal()) { return await BuildAdminUserInfoAsync(CurrentUserGuid, cancellationToken); } return await BuildAdminUserInfoAsync(CurrentUserGuid, cancellationToken); } /// /// 修改当前登录账号密码。 /// /// 入参:旧密码和新密码。 /// 取消令牌。 /// 无返回体,仅完成密码修改。 [HttpPost("/rbac/auth/change-password")] public async Task ChangePasswordAsync([FromBody] OmsChangePasswordInput input, CancellationToken cancellationToken = default) { if (IsCustomerPrincipal()) { await ChangeCustomerPasswordAsync(input.OldPassword, input.NewPassword, cancellationToken); return; } await accountManager.UpdatePasswordAsync(CurrentUserGuid, input.OldPassword, input.NewPassword, cancellationToken); } /// /// 更新当前管理员个人资料。 /// /// 入参:姓名和邮箱等个人资料。 /// 取消令牌。 /// 更新后的当前用户信息。 [HttpPut("/rbac/auth/update-profile")] public async Task UpdateProfileAsync([FromBody] OmsUpdateProfileInput input, CancellationToken cancellationToken = default) { EnsureAdminPrincipal(); var userId = CurrentUserGuid; var user = await userRepository.FirstAsync(u => u.Id == userId, cancellationToken: cancellationToken); user.Update(input.Name, input.Email); await userRepository.UpdateAsync(user, cancellationToken: cancellationToken); await UnitOfWorkManager.Current!.SaveChangesAsync(cancellationToken); InvalidateUserInfoCache(userId); return await BuildAdminUserInfoAsync(userId, cancellationToken); } /// /// 上传当前管理员头像。 /// /// 入参:头像文件。 /// 取消令牌。 /// 头像访问路径。 [HttpPost("/rbac/auth/upload-avatar")] public async Task UploadAvatarAsync(IFormFile file, CancellationToken cancellationToken = default) { EnsureAdminPrincipal(); var userId = CurrentUserGuid; if (file == null || file.Length == 0) { throw new DomainException("请选择要上传的文件"); } var allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".gif", ".webp" }; var extension = Path.GetExtension(file.FileName).ToLowerInvariant(); if (!allowedExtensions.Contains(extension)) { throw new DomainException("只支持 jpg、jpeg、png、gif、webp 格式的图片"); } if (file.Length > 10 * 1024 * 1024) { throw new DomainException("文件大小不能超过10MB"); } var uploadPath = Path.Combine(webHostEnvironment.WebRootPath ?? webHostEnvironment.ContentRootPath, "uploads", "avatars"); Directory.CreateDirectory(uploadPath); var fileName = $"{userId}_{DateTime.Now:yyyyMMddHHmmss}{extension}"; var filePath = Path.Combine(uploadPath, fileName); using (var stream = file.OpenReadStream()) using (var image = await Image.LoadAsync(stream, cancellationToken)) { var maxSize = 200; var width = image.Width; var height = image.Height; if (width > maxSize || height > maxSize) { if (width > height) { height = (int)(height * ((float)maxSize / width)); width = maxSize; } else { width = (int)(width * ((float)maxSize / height)); height = maxSize; } } image.Mutate(x => x.Resize(width, height)); await image.SaveAsJpegAsync(filePath, new SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder { Quality = 80 }, cancellationToken); } var avatarUrl = $"/uploads/avatars/{fileName}"; var user = await userRepository.FirstAsync(u => u.Id == userId, cancellationToken: cancellationToken); user.UpdateAvatar(avatarUrl); await userRepository.UpdateAsync(user, cancellationToken: cancellationToken); await UnitOfWorkManager.Current!.SaveChangesAsync(cancellationToken); InvalidateUserInfoCache(userId); return avatarUrl; } private async Task LoginAdminAsync(string account, string password, CancellationToken cancellationToken) { var user = await accountManager.ValidateCredentialsAsync(account, password, cancellationToken); return await BuildLoginResultAsync(user.Id, OmsLoginType.User, cancellationToken); } private async Task LoginCustomerAsync(string account, string password, CancellationToken cancellationToken) { var normalizedAccount = account.Trim(); var customer = await FindCustomerByAccountAsync(normalizedAccount, cancellationToken); if (customer == null) { throw new BusinessException("客户账号或密码错误"); } if (customer.ParentCustomerId.HasValue) { ValidateSubCustomerStatus(customer); var mainCustomer = await customerRepository.FirstOrDefaultAsync(x => x.Id == customer.ParentCustomerId.Value, cancellationToken); if (mainCustomer == null) { throw new BusinessException("主客户不存在"); } ValidateMainCustomerStatus(mainCustomer); } else { ValidateMainCustomerStatus(customer); } if (!string.Equals(customer.Password, OmsPasswordHelper.ToMd5(password), StringComparison.OrdinalIgnoreCase)) { throw new BusinessException("客户账号或密码错误"); } customer.LastLoginIp = httpContextAccessor.HttpContext?.Connection.RemoteIpAddress?.ToString(); customer.LastLoginTime = DateTime.Now; await customerRepository.UpdateAsync(customer, cancellationToken); var userGuid = await GetRbacUserGuidByMapIdAsync(customer.UserId, cancellationToken); if (!userGuid.HasValue) { throw new BusinessException("客户未关联用户"); } return await BuildLoginResultAsync(userGuid.Value, OmsLoginType.Customer, cancellationToken); } private async Task RefreshCustomerTokenAsync(IEnumerable claims, CancellationToken cancellationToken) { var mapId = ParseIntClaim(claims, SimpleOmsClaimTypes.MapId); if (mapId <= 0) { throw new BusinessException("无效的刷新令牌"); } var userGuid = await GetRbacUserGuidByMapIdAsync(mapId, cancellationToken); if (!userGuid.HasValue) { throw new BusinessException("用户不存在"); } var refreshedClaims = ReuseClaimsFromRefreshToken(claims, OmsLoginType.Customer); return BuildTokenOnlyResult(userGuid.Value, refreshedClaims); } private async Task BuildAdminUserInfoAsync(Guid userId, CancellationToken cancellationToken) { var cacheKey = rbacAuthCache.UserInfoKey(userId); if (memoryCache.TryGetValue(cacheKey, out UserInfoDto? cached) && cached != null) { return cached; } // 一次加载:用户 + 角色 + 权限,避免 Fake UserService 多次 GetWithRoles / GetRolesWithPermissions var user = await rbacDbContext.Users .AsNoTracking() .Include(x => x.Roles) .FirstOrDefaultAsync(x => x.Id == userId && !x.IsDeleted, cancellationToken); if (user == null) { throw new BusinessException("用户不存在"); } var roleIds = user.Roles.Select(x => x.RoleId).Distinct().ToList(); var roles = roleIds.Count == 0 ? [] : await rbacDbContext.Roles .AsNoTracking() .Include(x => x.Permissions) .Where(x => roleIds.Contains(x.Id) && !x.IsDeleted) .ToListAsync(cancellationToken); string? organizationName = null; if (user.OrganizationId.HasValue) { organizationName = await rbacDbContext.Organizations .AsNoTracking() .Where(x => x.Id == user.OrganizationId.Value) .Select(x => x.Name) .FirstOrDefaultAsync(cancellationToken); } var isAdminRole = roles.Any(x => string.Equals(x.Code, "Admin", StringComparison.OrdinalIgnoreCase)); List permissions; List menus; if (isAdminRole) { // Admin:默认全菜单/全权限码,无需在角色管理里逐项勾选 var allMenus = await GetAllActiveMenusCachedAsync(cancellationToken); permissions = allMenus .Select(x => x.PermissionCode) .Where(x => !string.IsNullOrWhiteSpace(x)) .Distinct(StringComparer.OrdinalIgnoreCase) .Select(x => x!) .ToList(); menus = BuildMenuTree(allMenus); } else { permissions = roles .SelectMany(x => x.Permissions.Select(p => p.PermissionCode)) .Where(x => !string.IsNullOrWhiteSpace(x)) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); menus = await BuildMenusByPermissionsAsync(permissions, cancellationToken); } var dto = new UserInfoDto { Id = user.Id, Name = user.Name, Account = user.Account, Email = user.Email, Avatar = user.Avatar, OrganizationId = user.OrganizationId, OrganizationName = organizationName, Roles = roles .Select(x => new RoleSimpleDto { Id = x.Id, Name = x.Name, Code = x.Code }) .ToList(), Permissions = permissions, Menus = menus, CreatedAt = user.CreateTime, CreatedBy = user.CreateUserId, UpdatedAt = user.UpdateTime == default ? null : user.UpdateTime, UpdatedBy = user.UpdateUserId }; memoryCache.Set( cacheKey, dto, new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = UserInfoCacheTtl }); return dto; } private void InvalidateUserInfoCache(Guid userId) { rbacAuthCache.InvalidateUser(userId); } private async Task ChangeCustomerPasswordAsync(string oldPassword, string newPassword, CancellationToken cancellationToken) { accountManager.ValidatePasswordStrength(newPassword); var context = await GetCurrentCustomerContextAsync(cancellationToken); if (!string.Equals(context.Customer.Password, OmsPasswordHelper.ToMd5(oldPassword), StringComparison.OrdinalIgnoreCase)) { throw new BusinessException("旧密码不正确"); } context.Customer.Password = OmsPasswordHelper.ToMd5(newPassword); await customerRepository.UpdateAsync(context.Customer, cancellationToken); } /// /// 登录快照:一次构建 UserInfo,并从中派生 JWT claims,避免再走 Fake GetUserRoles/GetAsync。 /// private async Task BuildLoginResultAsync( Guid userId, OmsLoginType loginType, CancellationToken cancellationToken) { var userInfo = await BuildAdminUserInfoAsync(userId, cancellationToken); var claims = await BuildClaimsFromUserInfoAsync(userId, userInfo, loginType, cancellationToken); return BuildTokenResult(userId, claims, userInfo); } private OmsLoginResultDto BuildTokenOnlyResult(Guid userId, List claims) { return BuildTokenResult(userId, claims, userInfo: null); } private OmsLoginResultDto BuildTokenResult(Guid userId, IReadOnlyCollection claims, UserInfoDto? userInfo) { var accessClaims = CloneClaims(claims); var refreshClaims = CloneClaims(claims); return new OmsLoginResultDto { AccessToken = jwtService.GenerateAccessToken(accessClaims), RefreshToken = jwtService.GenerateRefreshToken(refreshClaims), TokenType = "Bearer", ExpiresIn = jwtService.GetExpiresInSeconds(), UserId = userId, UserInfo = userInfo }; } private async Task> BuildClaimsFromUserInfoAsync( Guid userId, UserInfoDto userInfo, OmsLoginType loginType, CancellationToken cancellationToken) { var claims = new List { new(FakeClaimTypes.UserName, userInfo.Name ?? string.Empty) }; foreach (var roleCode in (userInfo.Roles ?? []) .Select(x => x.Code) .Where(x => !string.IsNullOrWhiteSpace(x))) { claims.Add(new Claim(FakeClaimTypes.Role, roleCode!)); } var mapId = await GetMapIdByUserGuidAsync(userId, cancellationToken); if (mapId.HasValue) { claims.Add(new Claim(FakeClaimTypes.UserId, mapId.Value.ToString())); claims.Add(new Claim(ClaimTypes.NameIdentifier, userId.ToString())); } else { claims.Add(new Claim(FakeClaimTypes.UserId, userId.ToString())); claims.Add(new Claim(ClaimTypes.NameIdentifier, userId.ToString())); } ReplaceOrAppendClaim(claims, SimpleOmsClaimTypes.LoginType, ((int)loginType).ToString()); return claims; } private static List ReuseClaimsFromRefreshToken(IEnumerable source, OmsLoginType loginType) { var skip = new HashSet(StringComparer.OrdinalIgnoreCase) { "jti", "iat", "nbf", "exp", "iss", "aud", "token_type" }; var claims = source .Where(x => !skip.Contains(x.Type)) .Select(x => new Claim(x.Type, x.Value, x.ValueType, x.Issuer, x.OriginalIssuer)) .ToList(); ReplaceOrAppendClaim(claims, SimpleOmsClaimTypes.LoginType, ((int)loginType).ToString()); return claims; } private static List CloneClaims(IEnumerable claims) { return claims.Select(x => new Claim(x.Type, x.Value, x.ValueType, x.Issuer, x.OriginalIssuer)).ToList(); } private bool IsCustomerPrincipal() { return CurrentUser.FindClaimOrNull(SimpleOmsClaimTypes.LoginType)?.Value == ((int)OmsLoginType.Customer).ToString(); } private void EnsureAdminPrincipal() { if (IsCustomerPrincipal()) { throw new BusinessException("当前登录主体暂不支持此操作"); } } private async Task GetCurrentCustomerContextAsync(CancellationToken cancellationToken) { var userId = CurrentUser.GetId(); if (userId <= 0) { throw new UnauthorizedAccessException("客户未登录"); } var customer = await customerRepository.FirstOrDefaultAsync(x => x.UserId == userId, cancellationToken); if (customer == null) { throw new BusinessException("客户不存在"); } if (customer.ParentCustomerId.HasValue) { ValidateSubCustomerStatus(customer); var mainCustomer = await customerRepository.FirstOrDefaultAsync(x => x.Id == customer.ParentCustomerId.Value, cancellationToken); if (mainCustomer == null) { throw new BusinessException("主客户不存在"); } ValidateMainCustomerStatus(mainCustomer); } else { ValidateMainCustomerStatus(customer); } return new CustomerPrincipalContext { PrincipalId = CurrentUserGuid, Customer = customer, DisplayName = customer.WechatNickname ?? customer.CustomerName, AccountCode = customer.CustomerCode, Email = customer.Email, Avatar = customer.WechatAvatar, CreateTime = customer.CreateTime, UpdateTime = customer.UpdateTime }; } private async Task FindCustomerByAccountAsync(string account, CancellationToken cancellationToken) { var customer = await customerRepository.GetByPhoneAsync(account, cancellationToken); if (customer != null) { return customer; } customer = await customerRepository.GetByEmailAsync(account, cancellationToken); if (customer != null) { return customer; } return await customerRepository.GetByCustomerCodeAsync(account, cancellationToken); } private async Task> BuildMenusByPermissionsAsync(IReadOnlyCollection permissionCodes, CancellationToken cancellationToken) { if (permissionCodes.Count == 0) { return []; } var allMenus = await GetAllActiveMenusCachedAsync(cancellationToken); var permissionSet = permissionCodes as HashSet ?? new HashSet(permissionCodes, StringComparer.OrdinalIgnoreCase); var menuMap = allMenus.ToDictionary(x => x.Id); var selectedMenuIds = new HashSet(allMenus .Where(x => !string.IsNullOrWhiteSpace(x.PermissionCode) && permissionSet.Contains(x.PermissionCode!)) .Select(x => x.Id)); var queue = new Queue(selectedMenuIds); while (queue.Count > 0) { var menuId = queue.Dequeue(); if (!menuMap.TryGetValue(menuId, out var currentMenu)) { continue; } if (currentMenu.PId != Guid.Empty && selectedMenuIds.Add(currentMenu.PId)) { queue.Enqueue(currentMenu.PId); } } return BuildMenuTree(allMenus.Where(x => selectedMenuIds.Contains(x.Id))); } private static List BuildMenuTree(IEnumerable menus) { var menuDtos = menus .OrderBy(x => x.Order) .ThenBy(x => x.CreateTime) .Select(x => new MenuTreeDto { Id = x.Id, PId = x.PId, Name = x.Name, PermissionCode = x.PermissionCode, Type = x.Type, Icon = x.Icon, Route = x.Route, Component = x.Component, Order = x.Order, IsHidden = x.IsHidden, IsCached = x.IsCached, Description = x.Description, CreatedAt = x.CreateTime, UpdatedAt = x.UpdateTime == default ? null : x.UpdateTime }) .ToList(); var dtoMap = menuDtos.ToDictionary(x => x.Id); var roots = new List(); foreach (var menu in menuDtos) { if (menu.PId == Guid.Empty || !dtoMap.TryGetValue(menu.PId, out var parentMenu)) { roots.Add(menu); continue; } parentMenu.Children.Add(menu); } return roots.OrderBy(x => x.Order).ToList(); } private async Task> GetAllActiveMenusCachedAsync( CancellationToken cancellationToken) { var cacheKey = rbacAuthCache.AllMenusKey(); if (memoryCache.TryGetValue(cacheKey, out List? cached) && cached is { Count: > 0 }) { return cached; } var allMenus = await rbacDbContext.Menus .AsNoTracking() .Where(x => !x.IsDeleted) .OrderBy(x => x.Order) .ToListAsync(cancellationToken); memoryCache.Set(cacheKey, allMenus, AllMenusCacheTtl); return allMenus; } private static void ValidateMainCustomerStatus(Customer customer) { if (customer.Status == CustomerStatus.Disabled) { throw new BusinessException("客户已禁用"); } if (customer.Status == CustomerStatus.Frozen) { throw new BusinessException("客户已冻结"); } } private static void ValidateSubCustomerStatus(Customer customer) { if (customer.Status == CustomerStatus.Disabled) { throw new BusinessException("子客户已禁用"); } if (customer.Status == CustomerStatus.Frozen) { throw new BusinessException("子客户已冻结"); } } private static OmsLoginType ParseLoginType(IEnumerable claims) { return int.TryParse(claims.FirstOrDefault(x => x.Type == SimpleOmsClaimTypes.LoginType)?.Value, out var loginType) && Enum.IsDefined(typeof(OmsLoginType), loginType) ? (OmsLoginType)loginType : OmsLoginType.User; } private async Task GetRbacUserGuidByMapIdAsync(int userId, CancellationToken cancellationToken) { return await rbacDbContext.Database.SqlQuery( $"select `Id` as `Value` from `user` where `IsDeleted` = 0 and `userid` = {userId}") .FirstOrDefaultAsync(cancellationToken); } private async Task GetMapIdByUserGuidAsync(Guid userId, CancellationToken cancellationToken) { return await rbacDbContext.Database.SqlQuery( $"select `userid` as `Value` from `user` where `Id` = {userId}") .FirstOrDefaultAsync(cancellationToken); } private static int ParseIntClaim(IEnumerable claims, string claimType) { return int.TryParse(claims.FirstOrDefault(x => x.Type == claimType)?.Value, out var value) ? value : 0; } private static void ReplaceOrAppendClaim(List claims, string claimType, string value) { claims.RemoveAll(x => x.Type == claimType); claims.Add(new Claim(claimType, value)); } private sealed class CustomerPrincipalContext { public Guid PrincipalId { get; set; } public required Customer Customer { get; init; } public required string DisplayName { get; init; } public required string AccountCode { get; init; } public string? Email { get; init; } public string? Avatar { get; init; } public DateTime CreateTime { get; init; } public DateTime UpdateTime { get; init; } } }