补充客户分页 Id 字段与 customer-oms-login 接口,修复管理端删除/免密登录;公告通知增加接收人候选项与客户接收类型。 Co-authored-by: Cursor <cursoragent@cursor.com>
731 lines
26 KiB
C#
731 lines
26 KiB
C#
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.User;
|
||
using Fake.Rbac.Application.Jwt;
|
||
using Fake.Rbac.Application.Services;
|
||
using Fake.Rbac.Domain.Managers;
|
||
using Fake.Rbac.Domain.UserAggregate;
|
||
using Fake.Rbac.Infrastructure;
|
||
using Fake.Users;
|
||
using Microsoft.AspNetCore.Authorization;
|
||
using Microsoft.AspNetCore.Mvc;
|
||
using Microsoft.EntityFrameworkCore;
|
||
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,
|
||
UserService userService,
|
||
IJwtService jwtService,
|
||
IUserRepository userRepository,
|
||
IWebHostEnvironment webHostEnvironment,
|
||
IHttpContextAccessor httpContextAccessor,
|
||
FakeRbacDbContext rbacDbContext,
|
||
ICustomerRepository customerRepository,
|
||
IWeChatLoginSessionStore weChatLoginSessionStore)
|
||
: ApplicationService
|
||
{
|
||
private Guid CurrentUserGuid => CurrentUser.FindClaimValueOrNull(ClaimTypes.NameIdentifier)!.To<Guid>();
|
||
|
||
/// <summary>
|
||
/// 用户或客户登录。
|
||
/// </summary>
|
||
/// <param name="input">入参:登录类型、账号和密码。</param>
|
||
/// <param name="cancellationToken">取消令牌。</param>
|
||
/// <returns>登录令牌、刷新令牌和用户基础信息。</returns>
|
||
[AllowAnonymous]
|
||
[HttpPost("/rbac/auth/login")]
|
||
public async Task<LoginResultDto> 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<LoginResultDto> 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("客户未关联用户");
|
||
}
|
||
|
||
var claims = await BuildClaimsAsync(userGuid.Value, OmsLoginType.Customer, cancellationToken);
|
||
return BuildLoginResult(userGuid.Value, claims);
|
||
}
|
||
|
||
/// <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<LoginResultDto> 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);
|
||
|
||
var claims = await BuildClaimsAsync(userGuid.Value, OmsLoginType.Customer, cancellationToken);
|
||
return BuildLoginResult(userGuid.Value, claims);
|
||
}
|
||
|
||
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<LoginResultDto> 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("用户不存在");
|
||
}
|
||
|
||
var claims = await BuildAdminClaimsAsync(user.Id, cancellationToken);
|
||
return BuildLoginResult(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);
|
||
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);
|
||
return avatarUrl;
|
||
}
|
||
|
||
private async Task<LoginResultDto> 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);
|
||
}
|
||
|
||
private async Task<LoginResultDto> 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("客户未关联用户");
|
||
}
|
||
|
||
var claims = await BuildClaimsAsync(userGuid.Value, OmsLoginType.Customer, cancellationToken);
|
||
return BuildLoginResult(userGuid.Value, claims);
|
||
}
|
||
|
||
private async Task<LoginResultDto> 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 = await BuildClaimsAsync(userGuid.Value, OmsLoginType.Customer, cancellationToken);
|
||
return BuildLoginResult(userGuid.Value, refreshedClaims);
|
||
}
|
||
|
||
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
|
||
{
|
||
Id = userInfo.Id,
|
||
Name = userInfo.Name,
|
||
Account = userInfo.Account,
|
||
Email = userInfo.Email,
|
||
Avatar = userInfo.Avatar,
|
||
OrganizationId = userInfo.OrganizationId,
|
||
OrganizationName = userInfo.OrganizationName,
|
||
Roles = userInfo.Roles,
|
||
Permissions = permissions,
|
||
Menus = await BuildMenusByPermissionsAsync(permissions, cancellationToken),
|
||
CreatedAt = userInfo.CreatedAt,
|
||
CreatedBy = userInfo.CreatedBy,
|
||
UpdatedAt = userInfo.UpdatedAt,
|
||
UpdatedBy = userInfo.UpdatedBy
|
||
};
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
private async Task<List<Claim>> BuildAdminClaimsAsync(Guid userId, CancellationToken cancellationToken)
|
||
{
|
||
return await BuildClaimsAsync(userId, OmsLoginType.User, cancellationToken);
|
||
}
|
||
|
||
private async Task<List<Claim>> BuildClaimsAsync(Guid userId, OmsLoginType loginType, CancellationToken cancellationToken)
|
||
{
|
||
var claims = await jwtService.GenerateClaimsByUserIdAsync(userId, cancellationToken);
|
||
ReplaceOrAppendClaim(claims, SimpleOmsClaimTypes.LoginType, ((int)loginType).ToString());
|
||
return claims;
|
||
}
|
||
|
||
private LoginResultDto BuildLoginResult(Guid userId, IReadOnlyCollection<Claim> claims)
|
||
{
|
||
var accessClaims = CloneClaims(claims);
|
||
var refreshClaims = CloneClaims(claims);
|
||
|
||
return new LoginResultDto
|
||
{
|
||
AccessToken = jwtService.GenerateAccessToken(accessClaims),
|
||
RefreshToken = jwtService.GenerateRefreshToken(refreshClaims),
|
||
TokenType = "Bearer",
|
||
ExpiresIn = jwtService.GetExpiresInSeconds(),
|
||
UserId = userId
|
||
};
|
||
}
|
||
|
||
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 rbacDbContext.Menus
|
||
.Where(x => !x.IsDeleted)
|
||
.OrderBy(x => x.Order)
|
||
.ToListAsync(cancellationToken);
|
||
|
||
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))
|
||
.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);
|
||
}
|
||
}
|
||
|
||
var menuDtos = allMenus
|
||
.Where(x => selectedMenuIds.Contains(x.Id))
|
||
.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 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 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; }
|
||
}
|
||
|
||
}
|