perf: 登录带回 userInfo,并加速 get-current-user

登录/刷令牌返回完整用户信息(含 menus);用户资料与权限并行查询并缓存 3 分钟,减少登录后二次慢请求。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-07-18 23:13:24 +08:00
parent e1f40ce36c
commit 2188a3ce6e
2 changed files with 100 additions and 27 deletions

View File

@ -14,6 +14,7 @@ using Fake.Users;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Oms.Application.Shared; using Oms.Application.Shared;
using Oms.Domain.CustomersAggregate.Entities; using Oms.Domain.CustomersAggregate.Entities;
using Oms.Domain.CustomersAggregate.Enums; using Oms.Domain.CustomersAggregate.Enums;
@ -39,20 +40,26 @@ public partial class AuthService(
IHttpContextAccessor httpContextAccessor, IHttpContextAccessor httpContextAccessor,
FakeRbacDbContext rbacDbContext, FakeRbacDbContext rbacDbContext,
ICustomerRepository customerRepository, ICustomerRepository customerRepository,
IWeChatLoginSessionStore weChatLoginSessionStore) IWeChatLoginSessionStore weChatLoginSessionStore,
IMemoryCache memoryCache)
: ApplicationService : 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 Guid CurrentUserGuid => CurrentUser.FindClaimValueOrNull(ClaimTypes.NameIdentifier)!.To<Guid>();
private static string UserInfoCacheKey(Guid userId) => UserInfoCacheKeyPrefix + userId;
/// <summary> /// <summary>
/// 用户或客户登录。 /// 用户或客户登录。
/// </summary> /// </summary>
/// <param name="input">入参:登录类型、账号和密码。</param> /// <param name="input">入参:登录类型、账号和密码。</param>
/// <param name="cancellationToken">取消令牌。</param> /// <param name="cancellationToken">取消令牌。</param>
/// <returns>登录令牌、刷新令牌和用户基础信息。</returns> /// <returns>登录令牌、刷新令牌和用户完整信息(含 menus)。</returns>
[AllowAnonymous] [AllowAnonymous]
[HttpPost("/rbac/auth/login")] [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 return input.Type switch
{ {
@ -69,7 +76,7 @@ public partial class AuthService(
/// <param name="cancellationToken">取消令牌。</param> /// <param name="cancellationToken">取消令牌。</param>
/// <returns>客户门户访问令牌。</returns> /// <returns>客户门户访问令牌。</returns>
[HttpPost("/rbac/auth/customer-oms-login")] [HttpPost("/rbac/auth/customer-oms-login")]
public async Task<LoginResultDto> CustomerOmsLoginAsync( public async Task<OmsLoginResultDto> CustomerOmsLoginAsync(
[FromBody] OmsCustomerOmsLoginInput input, [FromBody] OmsCustomerOmsLoginInput input,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
@ -106,7 +113,7 @@ public partial class AuthService(
} }
var claims = await BuildClaimsAsync(userGuid.Value, OmsLoginType.Customer, cancellationToken); var claims = await BuildClaimsAsync(userGuid.Value, OmsLoginType.Customer, cancellationToken);
return BuildLoginResult(userGuid.Value, claims); return await BuildLoginResultAsync(userGuid.Value, claims, cancellationToken);
} }
/// <summary> /// <summary>
@ -141,7 +148,7 @@ public partial class AuthService(
/// </summary> /// </summary>
[AllowAnonymous] [AllowAnonymous]
[HttpPost("/rbac/auth/wx-login")] [HttpPost("/rbac/auth/wx-login")]
public async Task<LoginResultDto> WxLoginAsync( public async Task<OmsLoginResultDto> WxLoginAsync(
[FromBody] OmsWxLoginInput input, [FromBody] OmsWxLoginInput input,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
@ -183,7 +190,7 @@ public partial class AuthService(
await weChatLoginSessionStore.RemoveAsync(loginKey, cancellationToken); await weChatLoginSessionStore.RemoveAsync(loginKey, cancellationToken);
var claims = await BuildClaimsAsync(userGuid.Value, OmsLoginType.Customer, 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( private async Task<Customer?> ResolveCustomerFromWxSessionAsync(
@ -213,7 +220,7 @@ public partial class AuthService(
/// <returns>新的访问令牌、刷新令牌和用户基础信息。</returns> /// <returns>新的访问令牌、刷新令牌和用户基础信息。</returns>
[AllowAnonymous] [AllowAnonymous]
[HttpPost("/rbac/auth/refresh-token")] [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 refreshToken = input.RefreshToken;
var userIdText = jwtService.ValidateRefreshToken(refreshToken); var userIdText = jwtService.ValidateRefreshToken(refreshToken);
@ -242,7 +249,7 @@ public partial class AuthService(
} }
var claims = await BuildAdminClaimsAsync(user.Id, cancellationToken); var claims = await BuildAdminClaimsAsync(user.Id, cancellationToken);
return BuildLoginResult(user.Id, claims); return await BuildLoginResultAsync(user.Id, claims, cancellationToken);
} }
/// <summary> /// <summary>
@ -300,6 +307,7 @@ public partial class AuthService(
user.Update(input.Name, input.Email); user.Update(input.Name, input.Email);
await userRepository.UpdateAsync(user, cancellationToken: cancellationToken); await userRepository.UpdateAsync(user, cancellationToken: cancellationToken);
await UnitOfWorkManager.Current!.SaveChangesAsync(cancellationToken); await UnitOfWorkManager.Current!.SaveChangesAsync(cancellationToken);
InvalidateUserInfoCache(userId);
return await BuildAdminUserInfoAsync(userId, cancellationToken); return await BuildAdminUserInfoAsync(userId, cancellationToken);
} }
@ -371,17 +379,18 @@ public partial class AuthService(
user.UpdateAvatar(avatarUrl); user.UpdateAvatar(avatarUrl);
await userRepository.UpdateAsync(user, cancellationToken: cancellationToken); await userRepository.UpdateAsync(user, cancellationToken: cancellationToken);
await UnitOfWorkManager.Current!.SaveChangesAsync(cancellationToken); await UnitOfWorkManager.Current!.SaveChangesAsync(cancellationToken);
InvalidateUserInfoCache(userId);
return avatarUrl; 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 user = await accountManager.ValidateCredentialsAsync(account, password, cancellationToken);
var claims = await BuildAdminClaimsAsync(user.Id, 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 normalizedAccount = account.Trim();
var customer = await FindCustomerByAccountAsync(normalizedAccount, cancellationToken); var customer = await FindCustomerByAccountAsync(normalizedAccount, cancellationToken);
@ -422,10 +431,10 @@ public partial class AuthService(
} }
var claims = await BuildClaimsAsync(userGuid.Value, OmsLoginType.Customer, 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<LoginResultDto> RefreshCustomerTokenAsync(IEnumerable<Claim> claims, CancellationToken cancellationToken) private async Task<OmsLoginResultDto> RefreshCustomerTokenAsync(IEnumerable<Claim> claims, CancellationToken cancellationToken)
{ {
var mapId = ParseIntClaim(claims, SimpleOmsClaimTypes.MapId); var mapId = ParseIntClaim(claims, SimpleOmsClaimTypes.MapId);
if (mapId <= 0) if (mapId <= 0)
@ -440,14 +449,27 @@ public partial class AuthService(
} }
var refreshedClaims = await BuildClaimsAsync(userGuid.Value, OmsLoginType.Customer, cancellationToken); 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) private async Task<UserInfoDto> BuildAdminUserInfoAsync(Guid userId, CancellationToken cancellationToken)
{ {
var userInfo = await userService.GetAsync(userId, cancellationToken); var cacheKey = UserInfoCacheKey(userId);
var permissions = await userService.GetUserPermissionsAsync(userId, cancellationToken); if (memoryCache.TryGetValue(cacheKey, out UserInfoDto? cached) && cached != null)
return new UserInfoDto {
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, Id = userInfo.Id,
Name = userInfo.Name, Name = userInfo.Name,
@ -458,12 +480,26 @@ public partial class AuthService(
OrganizationName = userInfo.OrganizationName, OrganizationName = userInfo.OrganizationName,
Roles = userInfo.Roles, Roles = userInfo.Roles,
Permissions = permissions, Permissions = permissions,
Menus = await BuildMenusByPermissionsAsync(permissions, cancellationToken), Menus = menus,
CreatedAt = userInfo.CreatedAt, CreatedAt = userInfo.CreatedAt,
CreatedBy = userInfo.CreatedBy, CreatedBy = userInfo.CreatedBy,
UpdatedAt = userInfo.UpdatedAt, UpdatedAt = userInfo.UpdatedAt,
UpdatedBy = userInfo.UpdatedBy 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) private async Task ChangeCustomerPasswordAsync(string oldPassword, string newPassword, CancellationToken cancellationToken)
@ -492,18 +528,22 @@ public partial class AuthService(
return claims; 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 accessClaims = CloneClaims(claims);
var refreshClaims = CloneClaims(claims); var refreshClaims = CloneClaims(claims);
return new LoginResultDto return new OmsLoginResultDto
{ {
AccessToken = jwtService.GenerateAccessToken(accessClaims), AccessToken = jwtService.GenerateAccessToken(accessClaims),
RefreshToken = jwtService.GenerateRefreshToken(refreshClaims), RefreshToken = jwtService.GenerateRefreshToken(refreshClaims),
TokenType = "Bearer", TokenType = "Bearer",
ExpiresIn = jwtService.GetExpiresInSeconds(), ExpiresIn = jwtService.GetExpiresInSeconds(),
UserId = userId UserId = userId,
UserInfo = await BuildAdminUserInfoAsync(userId, cancellationToken)
}; };
} }
@ -592,14 +632,13 @@ public partial class AuthService(
return []; return [];
} }
var allMenus = await rbacDbContext.Menus var allMenus = await GetAllActiveMenusCachedAsync(cancellationToken);
.Where(x => !x.IsDeleted) var permissionSet = permissionCodes as HashSet<string>
.OrderBy(x => x.Order) ?? new HashSet<string>(permissionCodes, StringComparer.OrdinalIgnoreCase);
.ToListAsync(cancellationToken);
var menuMap = allMenus.ToDictionary(x => x.Id); var menuMap = allMenus.ToDictionary(x => x.Id);
var selectedMenuIds = new HashSet<Guid>(allMenus 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)); .Select(x => x.Id));
var queue = new Queue<Guid>(selectedMenuIds); var queue = new Queue<Guid>(selectedMenuIds);
@ -656,6 +695,25 @@ public partial class AuthService(
return roots.OrderBy(x => x.Order).ToList(); 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) private static void ValidateMainCustomerStatus(Customer customer)
{ {
if (customer.Status == CustomerStatus.Disabled) if (customer.Status == CustomerStatus.Disabled)

View 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; }
}