admin/src/SimpleAdmin.Api/Auth/AuthService.cs
zzh a70f6f2a87 feat: 接入 9610 汇总申报,字段按业务含义命名并对齐运单
含清单/报关单拉取、ITFX 转发配置、迁移脚本与拣货单号扩展。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-30 17:46:04 +08:00

949 lines
34 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
/// <summary>
/// 接管 OMS 与 RBAC 共用的认证接口,补充客户登录和业务主体 claims 解析。
/// </summary>
[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<Guid>();
/// <summary>
/// 用户或客户登录。
/// </summary>
/// <param name="input">入参:登录类型、账号和密码。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>登录令牌、刷新令牌和用户完整信息(含 menus)。</returns>
[AllowAnonymous]
[HttpPost("/rbac/auth/login")]
public async Task<OmsLoginResultDto> 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("不支持的登录类型")
};
}
/// <summary>
/// 管理员代登客户门户(免密)。
/// </summary>
/// <param name="input">入参:目标客户主键。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>客户门户访问令牌。</returns>
[HttpPost("/rbac/auth/customer-oms-login")]
public async Task<OmsLoginResultDto> 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);
}
/// <summary>
/// 微信扫码登录轮询状态(key 是否已有绑定账号)。
/// </summary>
[AllowAnonymous]
[HttpGet("/rbac/auth/wx-login/status")]
public async Task<WxLoginStatusOutput> 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
};
}
/// <summary>
/// 微信扫码登录(轮询)。
/// </summary>
[AllowAnonymous]
[HttpPost("/rbac/auth/wx-login")]
public async Task<OmsLoginResultDto> 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);
}
/// <summary>
/// 微信网页授权登录(H5 内点按钮授权,无需扫码)。
/// </summary>
[AllowAnonymous]
[HttpPost("/rbac/auth/wx-oauth-login")]
public async Task<OmsLoginResultDto> 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<Customer?> 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;
}
/// <summary>
/// 刷新登录令牌。
/// </summary>
/// <param name="input">入参:刷新令牌。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>新的访问令牌、刷新令牌和用户基础信息。</returns>
[AllowAnonymous]
[HttpPost("/rbac/auth/refresh-token")]
public async Task<OmsLoginResultDto> 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);
}
/// <summary>
/// 获取当前登录用户信息。
/// </summary>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>当前登录用户信息、角色和菜单权限。</returns>
[HttpGet("/rbac/auth/get-current-user")]
public async Task<UserInfoDto> GetCurrentUserAsync(CancellationToken cancellationToken = default)
{
if (!CurrentUser.IsAuthenticated)
{
throw new UnauthorizedAccessException("用户未登录");
}
if (IsCustomerPrincipal())
{
return await BuildAdminUserInfoAsync(CurrentUserGuid, cancellationToken);
}
return await BuildAdminUserInfoAsync(CurrentUserGuid, cancellationToken);
}
/// <summary>
/// 修改当前登录账号密码。
/// </summary>
/// <param name="input">入参:旧密码和新密码。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>无返回体,仅完成密码修改。</returns>
[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);
}
/// <summary>
/// 更新当前管理员个人资料。
/// </summary>
/// <param name="input">入参:姓名和邮箱等个人资料。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>更新后的当前用户信息。</returns>
[HttpPut("/rbac/auth/update-profile")]
public async Task<UserInfoDto> 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);
}
/// <summary>
/// 上传当前管理员头像。
/// </summary>
/// <param name="file">入参:头像文件。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>头像访问路径。</returns>
[HttpPost("/rbac/auth/upload-avatar")]
public async Task<string> 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<OmsLoginResultDto> 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<OmsLoginResultDto> 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<OmsLoginResultDto> RefreshCustomerTokenAsync(IEnumerable<Claim> 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<UserInfoDto> 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<string> permissions;
List<MenuTreeDto> 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);
}
/// <summary>
/// 登录快照:一次构建 UserInfo,并从中派生 JWT claims,避免再走 Fake GetUserRoles/GetAsync。
/// </summary>
private async Task<OmsLoginResultDto> 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<Claim> claims)
{
return BuildTokenResult(userId, claims, userInfo: null);
}
private OmsLoginResultDto BuildTokenResult(Guid userId, IReadOnlyCollection<Claim> 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<List<Claim>> BuildClaimsFromUserInfoAsync(
Guid userId,
UserInfoDto userInfo,
OmsLoginType loginType,
CancellationToken cancellationToken)
{
var claims = new List<Claim>
{
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<Claim> ReuseClaimsFromRefreshToken(IEnumerable<Claim> source, OmsLoginType loginType)
{
var skip = new HashSet<string>(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<Claim> CloneClaims(IEnumerable<Claim> 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<CustomerPrincipalContext> GetCurrentCustomerContextAsync(CancellationToken cancellationToken)
{
var userId = CurrentUser.GetId<int>();
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<Customer?> 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<List<MenuTreeDto>> BuildMenusByPermissionsAsync(IReadOnlyCollection<string> permissionCodes, CancellationToken cancellationToken)
{
if (permissionCodes.Count == 0)
{
return [];
}
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) && permissionSet.Contains(x.PermissionCode!))
.Select(x => x.Id));
var queue = new Queue<Guid>(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<MenuTreeDto> BuildMenuTree(IEnumerable<Fake.Rbac.Domain.MenuAggregate.Menu> 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<MenuTreeDto>();
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<List<Fake.Rbac.Domain.MenuAggregate.Menu>> GetAllActiveMenusCachedAsync(
CancellationToken cancellationToken)
{
var cacheKey = rbacAuthCache.AllMenusKey();
if (memoryCache.TryGetValue(cacheKey, 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(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<Claim> 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<Guid?> GetRbacUserGuidByMapIdAsync(int userId, CancellationToken cancellationToken)
{
return await rbacDbContext.Database.SqlQuery<Guid?>(
$"select `Id` as `Value` from `user` where `IsDeleted` = 0 and `userid` = {userId}")
.FirstOrDefaultAsync(cancellationToken);
}
private async Task<int?> GetMapIdByUserGuidAsync(Guid userId, CancellationToken cancellationToken)
{
return await rbacDbContext.Database.SqlQuery<int?>(
$"select `userid` as `Value` from `user` where `Id` = {userId}")
.FirstOrDefaultAsync(cancellationToken);
}
private static int ParseIntClaim(IEnumerable<Claim> claims, string claimType)
{
return int.TryParse(claims.FirstOrDefault(x => x.Type == claimType)?.Value, out var value) ? value : 0;
}
private static void ReplaceOrAppendClaim(List<Claim> 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; }
}
}