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