diff --git a/src/SimpleAdmin.Api/Auth/AuthService.cs b/src/SimpleAdmin.Api/Auth/AuthService.cs index b64a73a..6be30a0 100644 --- a/src/SimpleAdmin.Api/Auth/AuthService.cs +++ b/src/SimpleAdmin.Api/Auth/AuthService.cs @@ -14,6 +14,7 @@ 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; @@ -39,20 +40,26 @@ public partial class AuthService( IHttpContextAccessor httpContextAccessor, FakeRbacDbContext rbacDbContext, ICustomerRepository customerRepository, - IWeChatLoginSessionStore weChatLoginSessionStore) + IWeChatLoginSessionStore weChatLoginSessionStore, + IMemoryCache memoryCache) : ApplicationService { + private const string AllMenusCacheKey = "rbac:menus:all:v1"; + private const string UserInfoCacheKeyPrefix = "rbac:userinfo:v2:"; + private static readonly TimeSpan UserInfoCacheTtl = TimeSpan.FromMinutes(3); + private Guid CurrentUserGuid => CurrentUser.FindClaimValueOrNull(ClaimTypes.NameIdentifier)!.To(); + private static string UserInfoCacheKey(Guid userId) => UserInfoCacheKeyPrefix + userId; /// /// 用户或客户登录。 /// /// 入参:登录类型、账号和密码。 /// 取消令牌。 - /// 登录令牌、刷新令牌和用户基础信息。 + /// 登录令牌、刷新令牌和用户完整信息(含 menus)。 [AllowAnonymous] [HttpPost("/rbac/auth/login")] - public async Task LoginAsync([FromBody] OmsLoginInput input, CancellationToken cancellationToken = default) + public async Task LoginAsync([FromBody] OmsLoginInput input, CancellationToken cancellationToken = default) { return input.Type switch { @@ -69,7 +76,7 @@ public partial class AuthService( /// 取消令牌。 /// 客户门户访问令牌。 [HttpPost("/rbac/auth/customer-oms-login")] - public async Task CustomerOmsLoginAsync( + public async Task CustomerOmsLoginAsync( [FromBody] OmsCustomerOmsLoginInput input, CancellationToken cancellationToken = default) { @@ -106,7 +113,7 @@ public partial class AuthService( } var claims = await BuildClaimsAsync(userGuid.Value, OmsLoginType.Customer, cancellationToken); - return BuildLoginResult(userGuid.Value, claims); + return await BuildLoginResultAsync(userGuid.Value, claims, cancellationToken); } /// @@ -141,7 +148,7 @@ public partial class AuthService( /// [AllowAnonymous] [HttpPost("/rbac/auth/wx-login")] - public async Task WxLoginAsync( + public async Task WxLoginAsync( [FromBody] OmsWxLoginInput input, CancellationToken cancellationToken = default) { @@ -183,7 +190,7 @@ public partial class AuthService( await weChatLoginSessionStore.RemoveAsync(loginKey, cancellationToken); var claims = await BuildClaimsAsync(userGuid.Value, OmsLoginType.Customer, cancellationToken); - return BuildLoginResult(userGuid.Value, claims); + return await BuildLoginResultAsync(userGuid.Value, claims, cancellationToken); } private async Task ResolveCustomerFromWxSessionAsync( @@ -213,7 +220,7 @@ public partial class AuthService( /// 新的访问令牌、刷新令牌和用户基础信息。 [AllowAnonymous] [HttpPost("/rbac/auth/refresh-token")] - public async Task RefreshTokenAsync([FromBody] OmsRefreshTokenInput input, CancellationToken cancellationToken = default) + public async Task RefreshTokenAsync([FromBody] OmsRefreshTokenInput input, CancellationToken cancellationToken = default) { var refreshToken = input.RefreshToken; var userIdText = jwtService.ValidateRefreshToken(refreshToken); @@ -242,7 +249,7 @@ public partial class AuthService( } var claims = await BuildAdminClaimsAsync(user.Id, cancellationToken); - return BuildLoginResult(user.Id, claims); + return await BuildLoginResultAsync(user.Id, claims, cancellationToken); } /// @@ -300,6 +307,7 @@ public partial class AuthService( user.Update(input.Name, input.Email); await userRepository.UpdateAsync(user, cancellationToken: cancellationToken); await UnitOfWorkManager.Current!.SaveChangesAsync(cancellationToken); + InvalidateUserInfoCache(userId); return await BuildAdminUserInfoAsync(userId, cancellationToken); } @@ -371,17 +379,18 @@ public partial class AuthService( 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) + private async Task LoginAdminAsync(string account, string password, CancellationToken cancellationToken) { var user = await accountManager.ValidateCredentialsAsync(account, password, cancellationToken); var claims = await BuildAdminClaimsAsync(user.Id, cancellationToken); - return BuildLoginResult(user.Id, claims); + return await BuildLoginResultAsync(user.Id, claims, cancellationToken); } - private async Task LoginCustomerAsync(string account, string password, CancellationToken cancellationToken) + private async Task LoginCustomerAsync(string account, string password, CancellationToken cancellationToken) { var normalizedAccount = account.Trim(); var customer = await FindCustomerByAccountAsync(normalizedAccount, cancellationToken); @@ -422,10 +431,10 @@ public partial class AuthService( } var claims = await BuildClaimsAsync(userGuid.Value, OmsLoginType.Customer, cancellationToken); - return BuildLoginResult(userGuid.Value, claims); + return await BuildLoginResultAsync(userGuid.Value, claims, cancellationToken); } - private async Task RefreshCustomerTokenAsync(IEnumerable claims, CancellationToken cancellationToken) + private async Task RefreshCustomerTokenAsync(IEnumerable claims, CancellationToken cancellationToken) { var mapId = ParseIntClaim(claims, SimpleOmsClaimTypes.MapId); if (mapId <= 0) @@ -440,14 +449,27 @@ public partial class AuthService( } var refreshedClaims = await BuildClaimsAsync(userGuid.Value, OmsLoginType.Customer, cancellationToken); - return BuildLoginResult(userGuid.Value, refreshedClaims); + return await BuildLoginResultAsync(userGuid.Value, refreshedClaims, cancellationToken); } private async Task BuildAdminUserInfoAsync(Guid userId, CancellationToken cancellationToken) { - var userInfo = await userService.GetAsync(userId, cancellationToken); - var permissions = await userService.GetUserPermissionsAsync(userId, cancellationToken); - return new UserInfoDto + var cacheKey = UserInfoCacheKey(userId); + if (memoryCache.TryGetValue(cacheKey, out UserInfoDto? cached) && cached != null) + { + return cached; + } + + // 用户资料与权限并行,避免串行打两次慢查询 + var userTask = userService.GetAsync(userId, cancellationToken); + var permissionsTask = userService.GetUserPermissionsAsync(userId, cancellationToken); + await Task.WhenAll(userTask, permissionsTask); + + var userInfo = await userTask; + var permissions = await permissionsTask; + var menus = await BuildMenusByPermissionsAsync(permissions, cancellationToken); + + var dto = new UserInfoDto { Id = userInfo.Id, Name = userInfo.Name, @@ -458,12 +480,26 @@ public partial class AuthService( OrganizationName = userInfo.OrganizationName, Roles = userInfo.Roles, Permissions = permissions, - Menus = await BuildMenusByPermissionsAsync(permissions, cancellationToken), + Menus = menus, CreatedAt = userInfo.CreatedAt, CreatedBy = userInfo.CreatedBy, UpdatedAt = userInfo.UpdatedAt, UpdatedBy = userInfo.UpdatedBy }; + + memoryCache.Set( + cacheKey, + dto, + new MemoryCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = UserInfoCacheTtl + }); + return dto; + } + + private void InvalidateUserInfoCache(Guid userId) + { + memoryCache.Remove(UserInfoCacheKey(userId)); } private async Task ChangeCustomerPasswordAsync(string oldPassword, string newPassword, CancellationToken cancellationToken) @@ -492,18 +528,22 @@ public partial class AuthService( return claims; } - private LoginResultDto BuildLoginResult(Guid userId, IReadOnlyCollection claims) + private async Task BuildLoginResultAsync( + Guid userId, + IReadOnlyCollection claims, + CancellationToken cancellationToken) { var accessClaims = CloneClaims(claims); var refreshClaims = CloneClaims(claims); - return new LoginResultDto + return new OmsLoginResultDto { AccessToken = jwtService.GenerateAccessToken(accessClaims), RefreshToken = jwtService.GenerateRefreshToken(refreshClaims), TokenType = "Bearer", ExpiresIn = jwtService.GetExpiresInSeconds(), - UserId = userId + UserId = userId, + UserInfo = await BuildAdminUserInfoAsync(userId, cancellationToken) }; } @@ -592,14 +632,13 @@ public partial class AuthService( return []; } - var allMenus = await rbacDbContext.Menus - .Where(x => !x.IsDeleted) - .OrderBy(x => x.Order) - .ToListAsync(cancellationToken); + 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) && permissionCodes.Contains(x.PermissionCode, StringComparer.OrdinalIgnoreCase)) + .Where(x => !string.IsNullOrWhiteSpace(x.PermissionCode) && permissionSet.Contains(x.PermissionCode!)) .Select(x => x.Id)); var queue = new Queue(selectedMenuIds); @@ -656,6 +695,25 @@ public partial class AuthService( return roots.OrderBy(x => x.Order).ToList(); } + private async Task> GetAllActiveMenusCachedAsync( + CancellationToken cancellationToken) + { + if (memoryCache.TryGetValue(AllMenusCacheKey, 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(AllMenusCacheKey, allMenus, TimeSpan.FromMinutes(5)); + return allMenus; + } + private static void ValidateMainCustomerStatus(Customer customer) { if (customer.Status == CustomerStatus.Disabled) diff --git a/src/SimpleAdmin.Api/Auth/OmsLoginResultDto.cs b/src/SimpleAdmin.Api/Auth/OmsLoginResultDto.cs new file mode 100644 index 0000000..01e0b6c --- /dev/null +++ b/src/SimpleAdmin.Api/Auth/OmsLoginResultDto.cs @@ -0,0 +1,15 @@ +using Fake.Rbac.Application.Dtos.Auth; +using Fake.Rbac.Application.Dtos.User; + +namespace SimpleAdmin.Api.Auth; + +/// +/// OMS 登录结果:在 RBAC 令牌字段上扩展 UserInfo,避免登录后再打 get-current-user。 +/// +public class OmsLoginResultDto : LoginResultDto +{ + /// + /// 当前用户完整信息(含 permissions / menus)。 + /// + public UserInfoDto? UserInfo { get; set; } +}