perf: 登录带回 userInfo,并加速 get-current-user
登录/刷令牌返回完整用户信息(含 menus);用户资料与权限并行查询并缓存 3 分钟,减少登录后二次慢请求。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
e1f40ce36c
commit
2188a3ce6e
@ -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<Guid>();
|
||||
private static string UserInfoCacheKey(Guid userId) => UserInfoCacheKeyPrefix + userId;
|
||||
|
||||
/// <summary>
|
||||
/// 用户或客户登录。
|
||||
/// </summary>
|
||||
/// <param name="input">入参:登录类型、账号和密码。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>登录令牌、刷新令牌和用户基础信息。</returns>
|
||||
/// <returns>登录令牌、刷新令牌和用户完整信息(含 menus)。</returns>
|
||||
[AllowAnonymous]
|
||||
[HttpPost("/rbac/auth/login")]
|
||||
public async Task<LoginResultDto> LoginAsync([FromBody] OmsLoginInput input, CancellationToken cancellationToken = default)
|
||||
public async Task<OmsLoginResultDto> LoginAsync([FromBody] OmsLoginInput input, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return input.Type switch
|
||||
{
|
||||
@ -69,7 +76,7 @@ public partial class AuthService(
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>客户门户访问令牌。</returns>
|
||||
[HttpPost("/rbac/auth/customer-oms-login")]
|
||||
public async Task<LoginResultDto> CustomerOmsLoginAsync(
|
||||
public async Task<OmsLoginResultDto> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -141,7 +148,7 @@ public partial class AuthService(
|
||||
/// </summary>
|
||||
[AllowAnonymous]
|
||||
[HttpPost("/rbac/auth/wx-login")]
|
||||
public async Task<LoginResultDto> WxLoginAsync(
|
||||
public async Task<OmsLoginResultDto> 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<Customer?> ResolveCustomerFromWxSessionAsync(
|
||||
@ -213,7 +220,7 @@ public partial class AuthService(
|
||||
/// <returns>新的访问令牌、刷新令牌和用户基础信息。</returns>
|
||||
[AllowAnonymous]
|
||||
[HttpPost("/rbac/auth/refresh-token")]
|
||||
public async Task<LoginResultDto> RefreshTokenAsync([FromBody] OmsRefreshTokenInput input, CancellationToken cancellationToken = default)
|
||||
public async Task<OmsLoginResultDto> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -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<LoginResultDto> LoginAdminAsync(string account, string password, CancellationToken cancellationToken)
|
||||
private async Task<OmsLoginResultDto> 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<LoginResultDto> LoginCustomerAsync(string account, string password, CancellationToken cancellationToken)
|
||||
private async Task<OmsLoginResultDto> 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<LoginResultDto> RefreshCustomerTokenAsync(IEnumerable<Claim> claims, CancellationToken cancellationToken)
|
||||
private async Task<OmsLoginResultDto> RefreshCustomerTokenAsync(IEnumerable<Claim> 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<UserInfoDto> 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<Claim> claims)
|
||||
private async Task<OmsLoginResultDto> BuildLoginResultAsync(
|
||||
Guid userId,
|
||||
IReadOnlyCollection<Claim> 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<string>
|
||||
?? new HashSet<string>(permissionCodes, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var menuMap = allMenus.ToDictionary(x => x.Id);
|
||||
var selectedMenuIds = new HashSet<Guid>(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<Guid>(selectedMenuIds);
|
||||
@ -656,6 +695,25 @@ public partial class AuthService(
|
||||
return roots.OrderBy(x => x.Order).ToList();
|
||||
}
|
||||
|
||||
private async Task<List<Fake.Rbac.Domain.MenuAggregate.Menu>> GetAllActiveMenusCachedAsync(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (memoryCache.TryGetValue(AllMenusCacheKey, out List<Fake.Rbac.Domain.MenuAggregate.Menu>? 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)
|
||||
|
||||
15
src/SimpleAdmin.Api/Auth/OmsLoginResultDto.cs
Normal file
15
src/SimpleAdmin.Api/Auth/OmsLoginResultDto.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using Fake.Rbac.Application.Dtos.Auth;
|
||||
using Fake.Rbac.Application.Dtos.User;
|
||||
|
||||
namespace SimpleAdmin.Api.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// OMS 登录结果:在 RBAC 令牌字段上扩展 UserInfo,避免登录后再打 get-current-user。
|
||||
/// </summary>
|
||||
public class OmsLoginResultDto : LoginResultDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 当前用户完整信息(含 permissions / menus)。
|
||||
/// </summary>
|
||||
public UserInfoDto? UserInfo { get; set; }
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user