refactor: AdminApi 剔除租户侧能力
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Abstractions;
|
||||
|
||||
@@ -28,16 +27,6 @@ public interface IAdminAuthService
|
||||
/// </summary>
|
||||
Task<CurrentUserProfile> GetProfileAsync(long userId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 获取用户权限概览。
|
||||
/// </summary>
|
||||
Task<UserPermissionDto?> GetUserPermissionsAsync(long userId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 搜索用户权限概览列表。
|
||||
/// </summary>
|
||||
Task<PagedResult<UserPermissionDto>> SearchUserPermissionsAsync(string? keyword, int page, int pageSize, string? sortBy, bool sortDescending, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前用户可见菜单树。
|
||||
/// </summary>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// 小程序认证服务。
|
||||
/// </summary>
|
||||
public interface IMiniAuthService
|
||||
{
|
||||
Task<TokenResponse> LoginWithWeChatAsync(WeChatLoginRequest request, CancellationToken cancellationToken = default);
|
||||
Task<TokenResponse> RefreshTokenAsync(RefreshTokenRequest request, CancellationToken cancellationToken = default);
|
||||
Task<CurrentUserProfile> GetProfileAsync(long userId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
namespace TakeoutSaaS.Application.Identity.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// 微信 code2Session 服务契约。
|
||||
/// </summary>
|
||||
public interface IWeChatAuthService
|
||||
{
|
||||
/// <summary>
|
||||
/// 调用微信接口完成 code2Session 交换。
|
||||
/// </summary>
|
||||
/// <param name="code">临时登录凭证 code。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>会话信息。</returns>
|
||||
Task<WeChatSessionInfo> Code2SessionAsync(string code, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 微信会话信息。
|
||||
/// </summary>
|
||||
public sealed class WeChatSessionInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// OpenId。
|
||||
/// </summary>
|
||||
public string OpenId { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// UnionId。
|
||||
/// </summary>
|
||||
public string? UnionId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 会话密钥。
|
||||
/// </summary>
|
||||
public string SessionKey { get; init; } = string.Empty;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
using MediatR;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 为用户分配角色(覆盖式)。
|
||||
/// </summary>
|
||||
public sealed record AssignUserRolesCommand : IRequest<bool>
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户 ID。
|
||||
/// </summary>
|
||||
public long UserId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色 ID 集合。
|
||||
/// </summary>
|
||||
public long[] RoleIds { get; init; } = Array.Empty<long>();
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Commands;
|
||||
|
||||
@@ -8,6 +9,16 @@ namespace TakeoutSaaS.Application.Identity.Commands;
|
||||
/// </summary>
|
||||
public sealed record CopyRoleTemplateCommand : IRequest<RoleDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 目标 Portal。
|
||||
/// </summary>
|
||||
public PortalType Portal { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 目标租户 ID(Portal=Tenant 时必填;Portal=Admin 时必须为空)。
|
||||
/// </summary>
|
||||
public long? TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 模板编码。
|
||||
/// </summary>
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 批量为当前租户初始化角色模板。
|
||||
/// </summary>
|
||||
public sealed record InitializeRoleTemplatesCommand : IRequest<IReadOnlyList<RoleDto>>
|
||||
{
|
||||
/// <summary>
|
||||
/// 需要初始化的模板编码列表(为空则全部)。
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<string>? TemplateCodes { get; init; }
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// 微信小程序登录请求。
|
||||
/// </summary>
|
||||
public sealed class WeChatLoginRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// wx.login 返回的临时 code。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[MaxLength(128)]
|
||||
public string Code { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 用户昵称。
|
||||
/// </summary>
|
||||
[MaxLength(64)]
|
||||
public string? Nickname { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 头像地址。
|
||||
/// </summary>
|
||||
[MaxLength(256)]
|
||||
public string? Avatar { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 加密用户数据。
|
||||
/// </summary>
|
||||
public string? EncryptedData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 加密向量。
|
||||
/// </summary>
|
||||
public string? Iv { get; set; }
|
||||
}
|
||||
@@ -13,16 +13,10 @@ public static class IdentityServiceCollectionExtensions
|
||||
/// 注册身份认证相关应用服务
|
||||
/// </summary>
|
||||
/// <param name="services">服务集合</param>
|
||||
/// <param name="enableMiniSupport">是否注册小程序认证服务</param>
|
||||
public static IServiceCollection AddIdentityApplication(this IServiceCollection services, bool enableMiniSupport = false)
|
||||
public static IServiceCollection AddIdentityApplication(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IAdminAuthService, AdminAuthService>();
|
||||
|
||||
if (enableMiniSupport)
|
||||
{
|
||||
services.AddScoped<IMiniAuthService, MiniAuthService>();
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.Identity.Commands;
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 用户角色分配处理器。
|
||||
/// </summary>
|
||||
public sealed class AssignUserRolesCommandHandler(
|
||||
IUserRoleRepository userRoleRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<AssignUserRolesCommand, bool>
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理用户角色分配请求。
|
||||
/// </summary>
|
||||
/// <param name="request">分配命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>执行结果。</returns>
|
||||
public async Task<bool> Handle(AssignUserRolesCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 固定为租户侧用户分配角色
|
||||
var portal = PortalType.Tenant;
|
||||
|
||||
// 2. 获取租户上下文
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
|
||||
// 3. 覆盖式绑定角色
|
||||
await userRoleRepository.ReplaceUserRolesAsync(portal, tenantId, request.UserId, request.RoleIds, cancellationToken);
|
||||
await userRoleRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 4. 返回执行结果
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
|
||||
@@ -22,7 +21,6 @@ public sealed class BatchIdentityUserOperationCommandHandler(
|
||||
IIdentityUserRepository identityUserRepository,
|
||||
IUserRoleRepository userRoleRepository,
|
||||
IRoleRepository roleRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ICurrentUserAccessor currentUserAccessor,
|
||||
IAdminAuthService adminAuthService,
|
||||
IIdentityOperationLogPublisher operationLogPublisher)
|
||||
@@ -31,24 +29,17 @@ public sealed class BatchIdentityUserOperationCommandHandler(
|
||||
/// <inheritdoc />
|
||||
public async Task<BatchIdentityUserOperationResult> Handle(BatchIdentityUserOperationCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 获取操作者档案并判断权限
|
||||
var currentTenantId = tenantProvider.GetCurrentTenantId();
|
||||
// 1. 获取操作者档案(用于操作日志)
|
||||
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
||||
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
|
||||
|
||||
// 2. 校验跨租户访问权限
|
||||
if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
|
||||
// 2. (空行后) 校验租户参数
|
||||
if (!request.TenantId.HasValue || request.TenantId.Value <= 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户批量操作用户");
|
||||
throw new BusinessException(ErrorCodes.ValidationFailed, "TenantId 不能为空");
|
||||
}
|
||||
|
||||
if (isSuperAdmin && !request.TenantId.HasValue)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "批量操作必须指定租户");
|
||||
}
|
||||
|
||||
// 3. 解析用户 ID 列表
|
||||
var tenantId = request.TenantId ?? currentTenantId;
|
||||
// 3. (空行后) 解析用户 ID 列表
|
||||
var tenantId = request.TenantId.Value;
|
||||
var userIds = ParseIds(request.UserIds, "用户");
|
||||
if (userIds.Length == 0)
|
||||
{
|
||||
|
||||
@@ -2,7 +2,8 @@ using MediatR;
|
||||
using TakeoutSaaS.Application.Identity.Commands;
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
|
||||
@@ -10,8 +11,7 @@ namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
/// 绑定角色权限处理器。
|
||||
/// </summary>
|
||||
public sealed class BindRolePermissionsCommandHandler(
|
||||
IRolePermissionRepository rolePermissionRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
IRolePermissionRepository rolePermissionRepository)
|
||||
: IRequestHandler<BindRolePermissionsCommand, bool>
|
||||
{
|
||||
/// <summary>
|
||||
@@ -25,10 +25,16 @@ public sealed class BindRolePermissionsCommandHandler(
|
||||
// 1. 固定绑定租户侧角色权限
|
||||
var portal = PortalType.Tenant;
|
||||
|
||||
// 2. 获取租户上下文
|
||||
var tenantId = request.TenantId ?? tenantProvider.GetCurrentTenantId();
|
||||
// 2. (空行后) 校验租户参数
|
||||
if (!request.TenantId.HasValue || request.TenantId.Value <= 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.ValidationFailed, "TenantId 不能为空");
|
||||
}
|
||||
|
||||
// 3. 覆盖式绑定权限
|
||||
// 3. (空行后) 获取租户标识
|
||||
var tenantId = request.TenantId.Value;
|
||||
|
||||
// 4. (空行后) 覆盖式绑定权限
|
||||
var distinctPermissionIds = request.PermissionIds
|
||||
.Where(id => id > 0)
|
||||
.Distinct()
|
||||
@@ -37,7 +43,7 @@ public sealed class BindRolePermissionsCommandHandler(
|
||||
await rolePermissionRepository.ReplaceRolePermissionsAsync(portal, tenantId, request.RoleId, distinctPermissionIds, cancellationToken);
|
||||
await rolePermissionRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 4. 返回执行结果
|
||||
// 5. 返回执行结果
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
|
||||
@@ -19,7 +18,6 @@ public sealed class ChangeIdentityUserStatusCommandHandler(
|
||||
IIdentityUserRepository identityUserRepository,
|
||||
IUserRoleRepository userRoleRepository,
|
||||
IRoleRepository roleRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ICurrentUserAccessor currentUserAccessor,
|
||||
IAdminAuthService adminAuthService,
|
||||
IIdentityOperationLogPublisher operationLogPublisher)
|
||||
@@ -28,30 +26,17 @@ public sealed class ChangeIdentityUserStatusCommandHandler(
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> Handle(ChangeIdentityUserStatusCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 获取操作者档案并判断权限
|
||||
var currentTenantId = tenantProvider.GetCurrentTenantId();
|
||||
// 1. 获取操作者档案(用于操作日志)
|
||||
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
||||
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
|
||||
|
||||
// 2. 校验跨租户访问权限
|
||||
if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户修改用户状态");
|
||||
}
|
||||
|
||||
// 3. 查询用户实体
|
||||
// 2. (空行后) 查询用户实体
|
||||
var user = await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken);
|
||||
if (user == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isSuperAdmin && user.TenantId != currentTenantId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. 校验租户管理员保留规则(仅租户侧用户适用)
|
||||
// 3. 校验租户管理员保留规则(仅租户侧用户适用)
|
||||
if (user.Portal == PortalType.Tenant
|
||||
&& request.Status == IdentityUserStatus.Disabled
|
||||
&& user.Status == IdentityUserStatus.Active
|
||||
@@ -60,7 +45,7 @@ public sealed class ChangeIdentityUserStatusCommandHandler(
|
||||
await EnsureNotLastActiveTenantAdminAsync(user.TenantId.Value, user.Id, cancellationToken);
|
||||
}
|
||||
|
||||
// 5. 更新状态
|
||||
// 4. 更新状态
|
||||
var previousStatus = user.Status;
|
||||
switch (request.Status)
|
||||
{
|
||||
@@ -81,7 +66,7 @@ public sealed class ChangeIdentityUserStatusCommandHandler(
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "无效的用户状态");
|
||||
}
|
||||
|
||||
// 6. 构建操作日志消息
|
||||
// 5. 构建操作日志消息
|
||||
var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName)
|
||||
? operatorProfile.Account
|
||||
: operatorProfile.DisplayName;
|
||||
@@ -108,7 +93,7 @@ public sealed class ChangeIdentityUserStatusCommandHandler(
|
||||
Success = true
|
||||
};
|
||||
|
||||
// 7. 写入 Outbox 并保存变更
|
||||
// 6. 写入 Outbox 并保存变更
|
||||
await operationLogPublisher.PublishAsync(logMessage, cancellationToken);
|
||||
await identityUserRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ using TakeoutSaaS.Domain.Identity.Enums;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
|
||||
@@ -17,14 +16,24 @@ public sealed class CopyRoleTemplateCommandHandler(
|
||||
IRoleTemplateRepository roleTemplateRepository,
|
||||
IRoleRepository roleRepository,
|
||||
IPermissionRepository permissionRepository,
|
||||
IRolePermissionRepository rolePermissionRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
IRolePermissionRepository rolePermissionRepository)
|
||||
: IRequestHandler<CopyRoleTemplateCommand, RoleDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<RoleDto> Handle(CopyRoleTemplateCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询模板与模板权限
|
||||
// 1. 校验 Portal 与 TenantId 参数
|
||||
if (request.Portal == PortalType.Admin && request.TenantId is not null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.ValidationFailed, "Portal=Admin 时 TenantId 必须为空");
|
||||
}
|
||||
|
||||
if (request.Portal == PortalType.Tenant && (!request.TenantId.HasValue || request.TenantId.Value <= 0))
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.ValidationFailed, "Portal=Tenant 时 TenantId 必须大于 0");
|
||||
}
|
||||
|
||||
// 2. 查询模板与模板权限
|
||||
var template = await roleTemplateRepository.FindByCodeAsync(request.TemplateCode, cancellationToken)
|
||||
?? throw new BusinessException(ErrorCodes.NotFound, $"角色模板 {request.TemplateCode} 不存在");
|
||||
|
||||
@@ -35,16 +44,16 @@ public sealed class CopyRoleTemplateCommandHandler(
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
// 2. 计算角色名称/编码与描述
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
// 3. 计算角色名称/编码与描述
|
||||
var portal = request.Portal;
|
||||
var tenantId = request.TenantId;
|
||||
|
||||
// 3. 固定复制为租户侧角色
|
||||
var portal = PortalType.Tenant;
|
||||
// 4. (空行后) 解析目标角色信息
|
||||
var roleCode = string.IsNullOrWhiteSpace(request.RoleCode) ? template.TemplateCode : request.RoleCode.Trim();
|
||||
var roleName = string.IsNullOrWhiteSpace(request.RoleName) ? template.Name : request.RoleName.Trim();
|
||||
var roleDescription = request.Description ?? template.Description;
|
||||
|
||||
// 4. 准备或更新角色主体(幂等创建)。
|
||||
// 5. 准备或更新角色主体(幂等创建)。
|
||||
var role = await roleRepository.FindByCodeAsync(portal, tenantId, roleCode, cancellationToken);
|
||||
if (role is null)
|
||||
{
|
||||
@@ -73,7 +82,7 @@ public sealed class CopyRoleTemplateCommandHandler(
|
||||
await roleRepository.UpdateAsync(role, cancellationToken);
|
||||
}
|
||||
|
||||
// 5. 确保模板权限全部存在,不存在则按模板定义创建。
|
||||
// 6. 确保模板权限全部存在,不存在则按模板定义创建。
|
||||
var existingPermissions = await permissionRepository.GetByCodesAsync(permissionCodes, cancellationToken);
|
||||
var permissionMap = existingPermissions.ToDictionary(x => x.Code, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
@@ -97,7 +106,7 @@ public sealed class CopyRoleTemplateCommandHandler(
|
||||
|
||||
await roleRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 6. 绑定缺失的权限,保留租户自定义的已有授权。
|
||||
// 7. 绑定缺失的权限,保留租户自定义的已有授权。
|
||||
var rolePermissions = await rolePermissionRepository.GetByRoleIdsAsync(portal, tenantId, new[] { role.Id }, cancellationToken);
|
||||
var existingPermissionIds = rolePermissions
|
||||
.Select(x => x.PermissionId)
|
||||
|
||||
@@ -13,7 +13,6 @@ using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Ids;
|
||||
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
|
||||
@@ -25,7 +24,6 @@ public sealed class CreateIdentityUserCommandHandler(
|
||||
IUserRoleRepository userRoleRepository,
|
||||
IRoleRepository roleRepository,
|
||||
IPasswordHasher<IdentityUser> passwordHasher,
|
||||
ITenantProvider tenantProvider,
|
||||
ICurrentUserAccessor currentUserAccessor,
|
||||
IAdminAuthService adminAuthService,
|
||||
IIdentityOperationLogPublisher operationLogPublisher,
|
||||
@@ -36,19 +34,17 @@ public sealed class CreateIdentityUserCommandHandler(
|
||||
/// <inheritdoc />
|
||||
public async Task<UserDetailDto> Handle(CreateIdentityUserCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 获取操作者档案并判断权限
|
||||
var currentTenantId = tenantProvider.GetCurrentTenantId();
|
||||
// 1. 获取操作者档案(用于操作日志)
|
||||
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
||||
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
|
||||
|
||||
// 2. 校验跨租户访问权限
|
||||
if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
|
||||
// 2. (空行后) 校验租户参数
|
||||
if (!request.TenantId.HasValue || request.TenantId.Value <= 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户创建用户");
|
||||
throw new BusinessException(ErrorCodes.ValidationFailed, "TenantId 不能为空");
|
||||
}
|
||||
|
||||
// 3. 规范化输入并准备校验
|
||||
var tenantId = isSuperAdmin ? request.TenantId ?? currentTenantId : currentTenantId;
|
||||
// 3. (空行后) 规范化输入并准备校验
|
||||
var tenantId = request.TenantId.Value;
|
||||
var account = request.Account.Trim();
|
||||
var displayName = request.DisplayName.Trim();
|
||||
var phone = string.IsNullOrWhiteSpace(request.Phone) ? null : request.Phone.Trim();
|
||||
|
||||
@@ -6,7 +6,6 @@ using TakeoutSaaS.Domain.Identity.Enums;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
|
||||
@@ -14,8 +13,7 @@ namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
/// 创建角色处理器。
|
||||
/// </summary>
|
||||
public sealed class CreateRoleCommandHandler(
|
||||
IRoleRepository roleRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
IRoleRepository roleRepository)
|
||||
: IRequestHandler<CreateRoleCommand, RoleDto>
|
||||
{
|
||||
/// <summary>
|
||||
@@ -28,11 +26,17 @@ public sealed class CreateRoleCommandHandler(
|
||||
{
|
||||
// 1. 固定创建租户侧角色
|
||||
var portal = PortalType.Tenant;
|
||||
|
||||
// 2. 获取租户上下文
|
||||
var tenantId = request.TenantId ?? tenantProvider.GetCurrentTenantId();
|
||||
|
||||
// 3. 归一化输入并校验唯一
|
||||
// 2. (空行后) 校验租户参数
|
||||
if (!request.TenantId.HasValue || request.TenantId.Value <= 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.ValidationFailed, "TenantId 不能为空");
|
||||
}
|
||||
|
||||
// 3. (空行后) 获取租户标识
|
||||
var tenantId = request.TenantId.Value;
|
||||
|
||||
// 4. (空行后) 归一化输入并校验唯一
|
||||
var name = request.Name?.Trim() ?? string.Empty;
|
||||
var code = request.Code?.Trim() ?? string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(code))
|
||||
@@ -46,7 +50,7 @@ public sealed class CreateRoleCommandHandler(
|
||||
throw new BusinessException(ErrorCodes.Conflict, "角色编码已存在");
|
||||
}
|
||||
|
||||
// 4. 构建角色实体
|
||||
// 5. 构建角色实体
|
||||
var role = new Role
|
||||
{
|
||||
Portal = portal,
|
||||
@@ -56,11 +60,11 @@ public sealed class CreateRoleCommandHandler(
|
||||
Description = request.Description
|
||||
};
|
||||
|
||||
// 5. 持久化
|
||||
// 6. 持久化
|
||||
await roleRepository.AddAsync(role, cancellationToken);
|
||||
await roleRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 6. 返回 DTO
|
||||
// 7. 返回 DTO
|
||||
return new RoleDto
|
||||
{
|
||||
Id = role.Id,
|
||||
|
||||
@@ -8,7 +8,6 @@ using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
|
||||
@@ -19,7 +18,6 @@ public sealed class DeleteIdentityUserCommandHandler(
|
||||
IIdentityUserRepository identityUserRepository,
|
||||
IUserRoleRepository userRoleRepository,
|
||||
IRoleRepository roleRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ICurrentUserAccessor currentUserAccessor,
|
||||
IAdminAuthService adminAuthService,
|
||||
IIdentityOperationLogPublisher operationLogPublisher)
|
||||
@@ -28,36 +26,23 @@ public sealed class DeleteIdentityUserCommandHandler(
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> Handle(DeleteIdentityUserCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 获取操作者档案并判断权限
|
||||
var currentTenantId = tenantProvider.GetCurrentTenantId();
|
||||
// 1. 获取操作者档案(用于操作日志)
|
||||
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
||||
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
|
||||
|
||||
// 2. 校验跨租户访问权限
|
||||
if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户删除用户");
|
||||
}
|
||||
|
||||
// 3. 查询用户实体
|
||||
// 2. (空行后) 查询用户实体
|
||||
var user = await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken);
|
||||
if (user == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isSuperAdmin && user.TenantId != currentTenantId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. 校验租户管理员保留规则(仅租户侧用户适用)
|
||||
// 3. 校验租户管理员保留规则(仅租户侧用户适用)
|
||||
if (user.Portal == PortalType.Tenant && user.Status == IdentityUserStatus.Active && user.TenantId.HasValue)
|
||||
{
|
||||
await EnsureNotLastActiveTenantAdminAsync(user.TenantId.Value, user.Id, cancellationToken);
|
||||
}
|
||||
|
||||
// 5. 构建操作日志消息
|
||||
// 4. 构建操作日志消息
|
||||
var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName)
|
||||
? operatorProfile.Account
|
||||
: operatorProfile.DisplayName;
|
||||
@@ -78,7 +63,7 @@ public sealed class DeleteIdentityUserCommandHandler(
|
||||
Success = true
|
||||
};
|
||||
|
||||
// 6. 软删除用户并写入 Outbox
|
||||
// 5. 软删除用户并写入 Outbox
|
||||
await identityUserRepository.RemoveAsync(user, cancellationToken);
|
||||
await operationLogPublisher.PublishAsync(logMessage, cancellationToken);
|
||||
await identityUserRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
@@ -2,7 +2,8 @@ using MediatR;
|
||||
using TakeoutSaaS.Application.Identity.Commands;
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
|
||||
@@ -10,8 +11,7 @@ namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
/// 删除角色处理器。
|
||||
/// </summary>
|
||||
public sealed class DeleteRoleCommandHandler(
|
||||
IRoleRepository roleRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
IRoleRepository roleRepository)
|
||||
: IRequestHandler<DeleteRoleCommand, bool>
|
||||
{
|
||||
/// <summary>
|
||||
@@ -25,14 +25,19 @@ public sealed class DeleteRoleCommandHandler(
|
||||
// 1. 固定删除租户侧角色
|
||||
var portal = PortalType.Tenant;
|
||||
|
||||
// 2. 获取租户上下文
|
||||
var tenantId = request.TenantId ?? tenantProvider.GetCurrentTenantId();
|
||||
// 2. (空行后) 校验租户参数
|
||||
if (!request.TenantId.HasValue || request.TenantId.Value <= 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.ValidationFailed, "TenantId 不能为空");
|
||||
}
|
||||
|
||||
// 3. (空行后) 获取租户标识并删除角色
|
||||
var tenantId = request.TenantId.Value;
|
||||
|
||||
// 3. 删除角色
|
||||
await roleRepository.DeleteAsync(portal, tenantId, request.RoleId, cancellationToken);
|
||||
await roleRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 4. 返回执行结果
|
||||
// 4. (空行后) 返回执行结果
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.Identity.Abstractions;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
using TakeoutSaaS.Application.Identity.Queries;
|
||||
using TakeoutSaaS.Domain.Identity.Entities;
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
|
||||
@@ -18,21 +15,13 @@ public sealed class GetIdentityUserDetailQueryHandler(
|
||||
IUserRoleRepository userRoleRepository,
|
||||
IRoleRepository roleRepository,
|
||||
IRolePermissionRepository rolePermissionRepository,
|
||||
IPermissionRepository permissionRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ICurrentUserAccessor currentUserAccessor,
|
||||
IAdminAuthService adminAuthService)
|
||||
IPermissionRepository permissionRepository)
|
||||
: IRequestHandler<GetIdentityUserDetailQuery, UserDetailDto?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<UserDetailDto?> Handle(GetIdentityUserDetailQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 获取操作者档案并判断权限
|
||||
var currentTenantId = tenantProvider.GetCurrentTenantId();
|
||||
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
||||
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
|
||||
|
||||
// 2. 查询用户实体
|
||||
// 1. 查询用户实体
|
||||
var user = request.IncludeDeleted
|
||||
? await identityUserRepository.GetForUpdateIncludingDeletedAsync(request.UserId, cancellationToken)
|
||||
: await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken);
|
||||
@@ -42,16 +31,11 @@ public sealed class GetIdentityUserDetailQueryHandler(
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isSuperAdmin && user.TenantId != currentTenantId)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 3. 加载角色与权限
|
||||
// 2. 加载角色与权限
|
||||
var portal = user.Portal;
|
||||
var tenantId = user.TenantId;
|
||||
|
||||
// 4. 查询用户角色关系
|
||||
// 3. 查询用户角色关系
|
||||
var roleRelations = await userRoleRepository.GetByUserIdAsync(portal, tenantId, user.Id, cancellationToken);
|
||||
var roleIds = roleRelations.Select(x => x.RoleId).Distinct().ToArray();
|
||||
var roles = roleIds.Length == 0
|
||||
@@ -75,7 +59,7 @@ public sealed class GetIdentityUserDetailQueryHandler(
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
// 5. 组装详情 DTO
|
||||
// 4. 组装详情 DTO
|
||||
var now = DateTime.UtcNow;
|
||||
return new UserDetailDto
|
||||
{
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
using TakeoutSaaS.Application.Identity.Queries;
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 按用户 ID 获取权限概览处理器。
|
||||
/// </summary>
|
||||
public sealed class GetUserPermissionsQueryHandler(
|
||||
IIdentityUserRepository identityUserRepository,
|
||||
IUserRoleRepository userRoleRepository,
|
||||
IRoleRepository roleRepository,
|
||||
IPermissionRepository permissionRepository,
|
||||
IRolePermissionRepository rolePermissionRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<GetUserPermissionsQuery, UserPermissionDto?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<UserPermissionDto?> Handle(GetUserPermissionsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 获取租户并查询用户
|
||||
var portal = PortalType.Tenant;
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var user = await identityUserRepository.FindByIdAsync(request.UserId, cancellationToken);
|
||||
if (user == null || user.TenantId != tenantId)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. 解析角色与权限
|
||||
var roleCodes = await ResolveUserRolesAsync(portal, tenantId, user.Id, cancellationToken);
|
||||
var permissionCodes = await ResolveUserPermissionsAsync(portal, tenantId, user.Id, cancellationToken);
|
||||
|
||||
// 3. 返回用户权限概览
|
||||
return new UserPermissionDto
|
||||
{
|
||||
UserId = user.Id,
|
||||
TenantId = user.TenantId,
|
||||
MerchantId = user.MerchantId,
|
||||
Account = user.Account,
|
||||
DisplayName = user.DisplayName,
|
||||
Roles = roleCodes,
|
||||
Permissions = permissionCodes,
|
||||
CreatedAt = user.CreatedAt
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<string[]> ResolveUserRolesAsync(PortalType portal, long tenantId, long userId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询用户角色关系
|
||||
var relations = await userRoleRepository.GetByUserIdAsync(portal, tenantId, userId, cancellationToken);
|
||||
var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray();
|
||||
if (roleIds.Length == 0)
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
// 2. 查询角色编码
|
||||
var roles = await roleRepository.GetByIdsAsync(portal, tenantId, roleIds, cancellationToken);
|
||||
return roles.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
}
|
||||
|
||||
private async Task<string[]> ResolveUserPermissionsAsync(PortalType portal, long tenantId, long userId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询用户角色关系
|
||||
var relations = await userRoleRepository.GetByUserIdAsync(portal, tenantId, userId, cancellationToken);
|
||||
var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray();
|
||||
if (roleIds.Length == 0)
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
// 2. 查询角色-权限关系
|
||||
var rolePermissions = await rolePermissionRepository.GetByRoleIdsAsync(portal, tenantId, roleIds, cancellationToken);
|
||||
var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray();
|
||||
if (permissionIds.Length == 0)
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
// 3. 查询权限编码
|
||||
var permissions = await permissionRepository.GetByIdsAsync(permissionIds, cancellationToken);
|
||||
return permissions.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.Identity.Commands;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 租户角色模板批量初始化处理器。
|
||||
/// </summary>
|
||||
public sealed class InitializeRoleTemplatesCommandHandler(
|
||||
IRoleTemplateRepository roleTemplateRepository,
|
||||
IMediator mediator)
|
||||
: IRequestHandler<InitializeRoleTemplatesCommand, IReadOnlyList<RoleDto>>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<RoleDto>> Handle(InitializeRoleTemplatesCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 解析需要初始化的模板编码,默认取全部模板。
|
||||
var requestedCodes = request.TemplateCodes?
|
||||
.Where(code => !string.IsNullOrWhiteSpace(code))
|
||||
.Select(code => code.Trim())
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
var availableTemplates = await roleTemplateRepository.GetAllAsync(true, cancellationToken);
|
||||
var availableCodes = availableTemplates.Select(t => t.TemplateCode).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var targetCodes = requestedCodes?.Length > 0
|
||||
? requestedCodes
|
||||
: availableTemplates.Select(template => template.TemplateCode).ToArray();
|
||||
|
||||
if (targetCodes.Length == 0)
|
||||
{
|
||||
return Array.Empty<RoleDto>();
|
||||
}
|
||||
|
||||
foreach (var code in targetCodes)
|
||||
{
|
||||
if (!availableCodes.Contains(code))
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, $"角色模板 {code} 不存在或未启用");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 逐个复制模板,幂等写入角色与权限。
|
||||
var roles = new List<RoleDto>(targetCodes.Length);
|
||||
foreach (var templateCode in targetCodes)
|
||||
{
|
||||
var role = await mediator.Send(new CopyRoleTemplateCommand
|
||||
{
|
||||
TemplateCode = templateCode
|
||||
}, cancellationToken);
|
||||
|
||||
roles.Add(role);
|
||||
}
|
||||
|
||||
return roles;
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
|
||||
@@ -19,7 +18,6 @@ namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
public sealed class ResetIdentityUserPasswordCommandHandler(
|
||||
IAdminPasswordResetTokenStore tokenStore,
|
||||
IIdentityUserRepository identityUserRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ICurrentUserAccessor currentUserAccessor,
|
||||
IAdminAuthService adminAuthService,
|
||||
IIdentityOperationLogPublisher operationLogPublisher)
|
||||
@@ -28,34 +26,21 @@ public sealed class ResetIdentityUserPasswordCommandHandler(
|
||||
/// <inheritdoc />
|
||||
public async Task<ResetIdentityUserPasswordResult> Handle(ResetIdentityUserPasswordCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 获取操作者档案并判断权限
|
||||
var currentTenantId = tenantProvider.GetCurrentTenantId();
|
||||
// 1. 获取操作者档案(用于操作日志)
|
||||
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
||||
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
|
||||
|
||||
// 2. 校验跨租户访问权限
|
||||
if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户重置密码");
|
||||
}
|
||||
|
||||
// 3. 查询用户实体
|
||||
// 2. (空行后) 查询用户实体
|
||||
var user = await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken);
|
||||
if (user == null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "用户不存在");
|
||||
}
|
||||
|
||||
if (!isSuperAdmin && user.TenantId != currentTenantId)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户重置密码");
|
||||
}
|
||||
|
||||
// 4. 签发重置令牌(1 小时有效)
|
||||
// 3. 签发重置令牌(1 小时有效)
|
||||
var expiresAt = DateTime.UtcNow.AddHours(1);
|
||||
var token = await tokenStore.IssueAsync(user.Id, expiresAt, cancellationToken);
|
||||
|
||||
// 5. 标记用户需重置密码
|
||||
// 4. 标记用户需重置密码
|
||||
user.MustChangePassword = true;
|
||||
user.FailedLoginCount = 0;
|
||||
user.LockedUntil = null;
|
||||
@@ -64,7 +49,7 @@ public sealed class ResetIdentityUserPasswordCommandHandler(
|
||||
user.Status = IdentityUserStatus.Active;
|
||||
}
|
||||
|
||||
// 6. 构建操作日志消息
|
||||
// 5. 构建操作日志消息
|
||||
var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName)
|
||||
? operatorProfile.Account
|
||||
: operatorProfile.DisplayName;
|
||||
@@ -85,7 +70,7 @@ public sealed class ResetIdentityUserPasswordCommandHandler(
|
||||
Success = true
|
||||
};
|
||||
|
||||
// 7. 写入 Outbox 并保存变更
|
||||
// 6. 写入 Outbox 并保存变更
|
||||
await operationLogPublisher.PublishAsync(logMessage, cancellationToken);
|
||||
await identityUserRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
|
||||
@@ -16,7 +15,6 @@ namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
/// </summary>
|
||||
public sealed class RestoreIdentityUserCommandHandler(
|
||||
IIdentityUserRepository identityUserRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ICurrentUserAccessor currentUserAccessor,
|
||||
IAdminAuthService adminAuthService,
|
||||
IIdentityOperationLogPublisher operationLogPublisher)
|
||||
@@ -25,35 +23,22 @@ public sealed class RestoreIdentityUserCommandHandler(
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> Handle(RestoreIdentityUserCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 获取操作者档案并判断权限
|
||||
var currentTenantId = tenantProvider.GetCurrentTenantId();
|
||||
// 1. 获取操作者档案(用于操作日志)
|
||||
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
||||
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
|
||||
|
||||
// 2. 校验跨租户访问权限
|
||||
if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户恢复用户");
|
||||
}
|
||||
|
||||
// 3. 查询用户实体(包含已删除)
|
||||
// 2. (空行后) 查询用户实体(包含已删除)
|
||||
var user = await identityUserRepository.GetForUpdateIncludingDeletedAsync(request.UserId, cancellationToken);
|
||||
if (user == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isSuperAdmin && user.TenantId != currentTenantId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!user.DeletedAt.HasValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. 构建操作日志消息
|
||||
// 3. (空行后) 构建操作日志消息
|
||||
var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName)
|
||||
? operatorProfile.Account
|
||||
: operatorProfile.DisplayName;
|
||||
@@ -74,7 +59,7 @@ public sealed class RestoreIdentityUserCommandHandler(
|
||||
Success = true
|
||||
};
|
||||
|
||||
// 5. 恢复软删除状态并写入 Outbox
|
||||
// 4. 恢复软删除状态并写入 Outbox
|
||||
user.DeletedAt = null;
|
||||
user.DeletedBy = null;
|
||||
await operationLogPublisher.PublishAsync(logMessage, cancellationToken);
|
||||
|
||||
@@ -3,7 +3,8 @@ using TakeoutSaaS.Application.Identity.Contracts;
|
||||
using TakeoutSaaS.Application.Identity.Queries;
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
|
||||
@@ -13,8 +14,7 @@ namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
public sealed class RoleDetailQueryHandler(
|
||||
IRoleRepository roleRepository,
|
||||
IRolePermissionRepository rolePermissionRepository,
|
||||
IPermissionRepository permissionRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
IPermissionRepository permissionRepository)
|
||||
: IRequestHandler<RoleDetailQuery, RoleDetailDto?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
@@ -23,24 +23,30 @@ public sealed class RoleDetailQueryHandler(
|
||||
// 1. 固定查询租户侧角色详情
|
||||
var portal = PortalType.Tenant;
|
||||
|
||||
// 2. 获取租户上下文并查询角色
|
||||
var tenantId = request.TenantId ?? tenantProvider.GetCurrentTenantId();
|
||||
// 2. (空行后) 校验租户参数
|
||||
if (!request.TenantId.HasValue || request.TenantId.Value <= 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.ValidationFailed, "TenantId 不能为空");
|
||||
}
|
||||
|
||||
// 3. (空行后) 获取租户标识并查询角色
|
||||
var tenantId = request.TenantId.Value;
|
||||
var role = await roleRepository.FindByIdAsync(portal, tenantId, request.RoleId, cancellationToken);
|
||||
if (role is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 3. 查询角色权限关系
|
||||
// 4. 查询角色权限关系
|
||||
var relations = await rolePermissionRepository.GetByRoleIdsAsync(portal, tenantId, new[] { role.Id }, cancellationToken);
|
||||
var permissionIds = relations.Select(x => x.PermissionId).ToArray();
|
||||
|
||||
// 4. 拉取权限实体
|
||||
// 5. 拉取权限实体
|
||||
var permissions = permissionIds.Length == 0
|
||||
? Array.Empty<Domain.Identity.Entities.Permission>()
|
||||
: await permissionRepository.GetByIdsAsync(permissionIds, cancellationToken);
|
||||
|
||||
// 5. 映射 DTO
|
||||
// 6. 映射 DTO
|
||||
var permissionDtos = permissions
|
||||
.Select(x => new PermissionDto
|
||||
{
|
||||
@@ -55,6 +61,7 @@ public sealed class RoleDetailQueryHandler(
|
||||
})
|
||||
.ToList();
|
||||
|
||||
// 7. (空行后) 返回角色详情
|
||||
return new RoleDetailDto
|
||||
{
|
||||
Id = role.Id,
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.Identity.Abstractions;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
using TakeoutSaaS.Application.Identity.Queries;
|
||||
using TakeoutSaaS.Domain.Identity.Entities;
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
|
||||
@@ -19,30 +14,16 @@ namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
public sealed class SearchIdentityUsersQueryHandler(
|
||||
IIdentityUserRepository identityUserRepository,
|
||||
IUserRoleRepository userRoleRepository,
|
||||
IRoleRepository roleRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ICurrentUserAccessor currentUserAccessor,
|
||||
IAdminAuthService adminAuthService)
|
||||
IRoleRepository roleRepository)
|
||||
: IRequestHandler<SearchIdentityUsersQuery, PagedResult<UserListItemDto>>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<PagedResult<UserListItemDto>> Handle(SearchIdentityUsersQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 获取操作者档案并判断权限
|
||||
var currentTenantId = tenantProvider.GetCurrentTenantId();
|
||||
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
||||
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
|
||||
|
||||
// 2. 校验跨租户访问权限
|
||||
if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户查询用户");
|
||||
}
|
||||
|
||||
// 3. 组装查询过滤条件
|
||||
// 1. 组装查询过滤条件
|
||||
var filter = new IdentityUserSearchFilter
|
||||
{
|
||||
TenantId = isSuperAdmin ? request.TenantId : currentTenantId,
|
||||
TenantId = request.TenantId,
|
||||
Keyword = request.Keyword,
|
||||
Status = request.Status,
|
||||
RoleId = request.RoleId,
|
||||
@@ -57,17 +38,17 @@ public sealed class SearchIdentityUsersQueryHandler(
|
||||
SortDescending = request.SortDescending
|
||||
};
|
||||
|
||||
// 4. 执行分页查询
|
||||
// 2. 执行分页查询
|
||||
var (items, total) = await identityUserRepository.SearchPagedAsync(filter, cancellationToken);
|
||||
if (items.Count == 0)
|
||||
{
|
||||
return new PagedResult<UserListItemDto>(Array.Empty<UserListItemDto>(), request.Page, request.PageSize, total);
|
||||
}
|
||||
|
||||
// 5. 加载角色编码映射
|
||||
// 3. 加载角色编码映射
|
||||
var roleCodesLookup = await ResolveRoleCodesAsync(items, userRoleRepository, roleRepository, cancellationToken);
|
||||
|
||||
// 6. 组装 DTO
|
||||
// 4. 组装 DTO
|
||||
var now = DateTime.UtcNow;
|
||||
var dtos = items.Select(user => new UserListItemDto
|
||||
{
|
||||
@@ -87,7 +68,7 @@ public sealed class SearchIdentityUsersQueryHandler(
|
||||
LastLoginAt = user.LastLoginAt
|
||||
}).ToList();
|
||||
|
||||
// 7. 返回分页结果
|
||||
// 5. 返回分页结果
|
||||
return new PagedResult<UserListItemDto>(dtos, request.Page, request.PageSize, total);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,9 @@ using TakeoutSaaS.Application.Identity.Contracts;
|
||||
using TakeoutSaaS.Application.Identity.Queries;
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
|
||||
@@ -12,8 +13,7 @@ namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
/// 角色分页查询处理器。
|
||||
/// </summary>
|
||||
public sealed class SearchRolesQueryHandler(
|
||||
IRoleRepository roleRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
IRoleRepository roleRepository)
|
||||
: IRequestHandler<SearchRolesQuery, PagedResult<RoleDto>>
|
||||
{
|
||||
/// <summary>
|
||||
@@ -27,11 +27,17 @@ public sealed class SearchRolesQueryHandler(
|
||||
// 1. 固定查询租户侧角色
|
||||
var portal = PortalType.Tenant;
|
||||
|
||||
// 2. 获取租户上下文并查询角色
|
||||
var tenantId = request.TenantId ?? tenantProvider.GetCurrentTenantId();
|
||||
// 2. (空行后) 校验租户参数
|
||||
if (!request.TenantId.HasValue || request.TenantId.Value <= 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.ValidationFailed, "TenantId 不能为空");
|
||||
}
|
||||
|
||||
// 3. (空行后) 获取租户标识并查询角色
|
||||
var tenantId = request.TenantId.Value;
|
||||
var roles = await roleRepository.SearchAsync(portal, tenantId, request.Keyword, cancellationToken);
|
||||
|
||||
// 3. 排序
|
||||
// 4. 排序
|
||||
var sorted = request.SortBy?.ToLowerInvariant() switch
|
||||
{
|
||||
"name" => request.SortDescending
|
||||
@@ -42,13 +48,13 @@ public sealed class SearchRolesQueryHandler(
|
||||
: roles.OrderBy(x => x.CreatedAt)
|
||||
};
|
||||
|
||||
// 4. 分页
|
||||
// 5. 分页
|
||||
var paged = sorted
|
||||
.Skip((request.Page - 1) * request.PageSize)
|
||||
.Take(request.PageSize)
|
||||
.ToList();
|
||||
|
||||
// 5. 映射 DTO
|
||||
// 6. 映射 DTO
|
||||
var items = paged.Select(role => new RoleDto
|
||||
{
|
||||
Id = role.Id,
|
||||
@@ -59,7 +65,7 @@ public sealed class SearchRolesQueryHandler(
|
||||
Description = role.Description
|
||||
}).ToList();
|
||||
|
||||
// 6. 返回分页结果
|
||||
// 7. 返回分页结果
|
||||
return new PagedResult<RoleDto>(items, request.Page, request.PageSize, roles.Count);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
using TakeoutSaaS.Application.Identity.Queries;
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 租户用户权限分页查询处理器。
|
||||
/// </summary>
|
||||
public sealed class SearchUserPermissionsQueryHandler(
|
||||
IIdentityUserRepository identityUserRepository,
|
||||
IUserRoleRepository userRoleRepository,
|
||||
IRoleRepository roleRepository,
|
||||
IPermissionRepository permissionRepository,
|
||||
IRolePermissionRepository rolePermissionRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<SearchUserPermissionsQuery, PagedResult<UserPermissionDto>>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<PagedResult<UserPermissionDto>> Handle(SearchUserPermissionsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 获取租户并查询用户
|
||||
var portal = PortalType.Tenant;
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var users = await identityUserRepository.SearchAsync(tenantId, request.Keyword, cancellationToken);
|
||||
|
||||
// 2. 排序与分页
|
||||
var sorted = SortUsers(users, request.SortBy, request.SortDescending);
|
||||
var paged = sorted
|
||||
.Skip((request.Page - 1) * request.PageSize)
|
||||
.Take(request.PageSize)
|
||||
.ToList();
|
||||
|
||||
// 3. 解析角色与权限
|
||||
var resolved = await ResolveRolesAndPermissionsAsync(portal, tenantId, paged, cancellationToken);
|
||||
var items = paged.Select(user => new UserPermissionDto
|
||||
{
|
||||
UserId = user.Id,
|
||||
TenantId = user.TenantId,
|
||||
MerchantId = user.MerchantId,
|
||||
Account = user.Account,
|
||||
DisplayName = user.DisplayName,
|
||||
Roles = resolved[user.Id].roles,
|
||||
Permissions = resolved[user.Id].permissions,
|
||||
CreatedAt = user.CreatedAt
|
||||
}).ToList();
|
||||
|
||||
return new PagedResult<UserPermissionDto>(items, request.Page, request.PageSize, users.Count);
|
||||
}
|
||||
|
||||
private static IOrderedEnumerable<Domain.Identity.Entities.IdentityUser> SortUsers(
|
||||
IReadOnlyCollection<Domain.Identity.Entities.IdentityUser> users,
|
||||
string? sortBy,
|
||||
bool sortDescending)
|
||||
{
|
||||
return sortBy?.ToLowerInvariant() switch
|
||||
{
|
||||
"account" => sortDescending
|
||||
? users.OrderByDescending(x => x.Account)
|
||||
: users.OrderBy(x => x.Account),
|
||||
"displayname" => sortDescending
|
||||
? users.OrderByDescending(x => x.DisplayName)
|
||||
: users.OrderBy(x => x.DisplayName),
|
||||
_ => sortDescending
|
||||
? users.OrderByDescending(x => x.CreatedAt)
|
||||
: users.OrderBy(x => x.CreatedAt)
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<Dictionary<long, (string[] roles, string[] permissions)>> ResolveRolesAndPermissionsAsync(
|
||||
PortalType portal,
|
||||
long tenantId,
|
||||
IReadOnlyCollection<Domain.Identity.Entities.IdentityUser> users,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询用户角色关系
|
||||
var userIds = users.Select(x => x.Id).ToArray();
|
||||
var userRoleRelations = await userRoleRepository.GetByUserIdsAsync(portal, tenantId, userIds, cancellationToken);
|
||||
var roleIds = userRoleRelations.Select(x => x.RoleId).Distinct().ToArray();
|
||||
|
||||
// 2. 查询角色信息
|
||||
var roles = roleIds.Length == 0
|
||||
? Array.Empty<Domain.Identity.Entities.Role>()
|
||||
: await roleRepository.GetByIdsAsync(portal, tenantId, roleIds, cancellationToken);
|
||||
var roleCodeMap = roles.ToDictionary(r => r.Id, r => r.Code, comparer: EqualityComparer<long>.Default);
|
||||
|
||||
// 3. 查询角色-权限关系
|
||||
var rolePermissions = roleIds.Length == 0
|
||||
? Array.Empty<Domain.Identity.Entities.RolePermission>()
|
||||
: await rolePermissionRepository.GetByRoleIdsAsync(portal, tenantId, roleIds, cancellationToken);
|
||||
var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray();
|
||||
|
||||
// 4. 查询权限详情
|
||||
var permissions = permissionIds.Length == 0
|
||||
? Array.Empty<Domain.Identity.Entities.Permission>()
|
||||
: await permissionRepository.GetByIdsAsync(permissionIds, cancellationToken);
|
||||
var permissionCodeMap = permissions.ToDictionary(p => p.Id, p => p.Code, comparer: EqualityComparer<long>.Default);
|
||||
|
||||
var rolePermissionsLookup = rolePermissions
|
||||
.GroupBy(rp => rp.RoleId)
|
||||
.ToDictionary(g => g.Key, g => g.Select(rp => rp.PermissionId).ToArray(), comparer: EqualityComparer<long>.Default);
|
||||
|
||||
var result = new Dictionary<long, (string[] roles, string[] permissions)>();
|
||||
foreach (var userId in userIds)
|
||||
{
|
||||
// 5. 聚合用户角色与权限编码
|
||||
var rolesForUser = userRoleRelations.Where(ur => ur.UserId == userId).Select(ur => ur.RoleId).Distinct().ToArray();
|
||||
var roleCodes = rolesForUser
|
||||
.Select(rid => roleCodeMap.GetValueOrDefault(rid))
|
||||
.Where(c => !string.IsNullOrWhiteSpace(c))
|
||||
.Select(c => c!)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
var permissionCodes = rolesForUser
|
||||
.SelectMany(rid => rolePermissionsLookup.GetValueOrDefault(rid) ?? Array.Empty<long>())
|
||||
.Select(pid => permissionCodeMap.GetValueOrDefault(pid))
|
||||
.Where(code => !string.IsNullOrWhiteSpace(code))
|
||||
.Select(code => code!)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
result[userId] = (roleCodes, permissionCodes);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
|
||||
@@ -22,7 +21,6 @@ public sealed class UpdateIdentityUserCommandHandler(
|
||||
IIdentityUserRepository identityUserRepository,
|
||||
IUserRoleRepository userRoleRepository,
|
||||
IRoleRepository roleRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ICurrentUserAccessor currentUserAccessor,
|
||||
IAdminAuthService adminAuthService,
|
||||
IIdentityOperationLogPublisher operationLogPublisher,
|
||||
@@ -32,30 +30,17 @@ public sealed class UpdateIdentityUserCommandHandler(
|
||||
/// <inheritdoc />
|
||||
public async Task<UserDetailDto?> Handle(UpdateIdentityUserCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 获取操作者档案并判断权限
|
||||
var currentTenantId = tenantProvider.GetCurrentTenantId();
|
||||
// 1. 获取操作者档案(用于操作日志)
|
||||
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
||||
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
|
||||
|
||||
// 2. 校验跨租户访问权限
|
||||
if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户更新用户");
|
||||
}
|
||||
|
||||
// 3. 获取用户实体
|
||||
// 2. (空行后) 获取用户实体
|
||||
var user = await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken);
|
||||
if (user == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isSuperAdmin && user.TenantId != currentTenantId)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 4. 规范化输入并校验唯一性
|
||||
// 3. (空行后) 规范化输入并校验唯一性
|
||||
var portal = user.Portal;
|
||||
var tenantId = user.TenantId;
|
||||
if (portal == PortalType.Tenant && (!tenantId.HasValue || tenantId.Value == 0))
|
||||
@@ -93,14 +78,14 @@ public sealed class UpdateIdentityUserCommandHandler(
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 更新用户字段
|
||||
// 4. 更新用户字段
|
||||
user.DisplayName = displayName;
|
||||
user.Phone = phone;
|
||||
user.Email = email;
|
||||
user.Avatar = string.IsNullOrWhiteSpace(request.Avatar) ? null : request.Avatar.Trim();
|
||||
user.RowVersion = request.RowVersion;
|
||||
|
||||
// 6. 构建操作日志消息
|
||||
// 5. 构建操作日志消息
|
||||
var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName)
|
||||
? operatorProfile.Account
|
||||
: operatorProfile.DisplayName;
|
||||
@@ -128,7 +113,7 @@ public sealed class UpdateIdentityUserCommandHandler(
|
||||
Success = true
|
||||
};
|
||||
|
||||
// 7. 持久化用户更新并写入 Outbox
|
||||
// 6. 持久化用户更新并写入 Outbox
|
||||
try
|
||||
{
|
||||
await operationLogPublisher.PublishAsync(logMessage, cancellationToken);
|
||||
@@ -139,13 +124,13 @@ public sealed class UpdateIdentityUserCommandHandler(
|
||||
throw new BusinessException(ErrorCodes.Conflict, "用户数据已被修改,请刷新后重试");
|
||||
}
|
||||
|
||||
// 8. 覆盖角色绑定(仅当显式传入时)
|
||||
// 7. 覆盖角色绑定(仅当显式传入时)
|
||||
if (roleIds != null)
|
||||
{
|
||||
await userRoleRepository.ReplaceUserRolesAsync(portal, tenantId, user.Id, roleIds, cancellationToken);
|
||||
}
|
||||
|
||||
// 9. 返回用户详情
|
||||
// 8. 返回用户详情
|
||||
return await mediator.Send(new GetIdentityUserDetailQuery { UserId = user.Id }, cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@ using TakeoutSaaS.Application.Identity.Commands;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
|
||||
@@ -11,8 +12,7 @@ namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
/// 更新角色处理器。
|
||||
/// </summary>
|
||||
public sealed class UpdateRoleCommandHandler(
|
||||
IRoleRepository roleRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
IRoleRepository roleRepository)
|
||||
: IRequestHandler<UpdateRoleCommand, RoleDto?>
|
||||
{
|
||||
/// <summary>
|
||||
@@ -26,23 +26,29 @@ public sealed class UpdateRoleCommandHandler(
|
||||
// 1. 固定更新租户侧角色
|
||||
var portal = PortalType.Tenant;
|
||||
|
||||
// 2. 获取租户上下文并查询角色
|
||||
var tenantId = request.TenantId ?? tenantProvider.GetCurrentTenantId();
|
||||
// 2. (空行后) 校验租户参数
|
||||
if (!request.TenantId.HasValue || request.TenantId.Value <= 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.ValidationFailed, "TenantId 不能为空");
|
||||
}
|
||||
|
||||
// 3. (空行后) 获取租户标识并查询角色
|
||||
var tenantId = request.TenantId.Value;
|
||||
var role = await roleRepository.FindByIdAsync(portal, tenantId, request.RoleId, cancellationToken);
|
||||
if (role == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 3. 更新字段
|
||||
// 4. 更新字段
|
||||
role.Name = request.Name;
|
||||
role.Description = request.Description;
|
||||
|
||||
// 4. 持久化
|
||||
// 5. 持久化
|
||||
await roleRepository.UpdateAsync(role, cancellationToken);
|
||||
await roleRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 5. 返回 DTO
|
||||
// 6. 返回 DTO
|
||||
return new RoleDto
|
||||
{
|
||||
Id = role.Id,
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 按用户 ID 获取角色/权限概览。
|
||||
/// </summary>
|
||||
public sealed class GetUserPermissionsQuery : IRequest<UserPermissionDto?>
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户 ID(雪花)。
|
||||
/// </summary>
|
||||
public long UserId { get; init; }
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 按租户分页查询用户的角色/权限概览。
|
||||
/// </summary>
|
||||
public sealed class SearchUserPermissionsQuery : IRequest<PagedResult<UserPermissionDto>>
|
||||
{
|
||||
/// <summary>
|
||||
/// 关键字(账号或展示名称)。
|
||||
/// </summary>
|
||||
public string? Keyword { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 页码,从 1 开始。
|
||||
/// </summary>
|
||||
public int Page { get; init; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 每页条数。
|
||||
/// </summary>
|
||||
public int PageSize { get; init; } = 20;
|
||||
|
||||
/// <summary>
|
||||
/// 排序字段(account/displayName/createdAt)。
|
||||
/// </summary>
|
||||
public string? SortBy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否倒序。
|
||||
/// </summary>
|
||||
public bool SortDescending { get; init; } = true;
|
||||
}
|
||||
@@ -6,8 +6,6 @@ using TakeoutSaaS.Domain.Identity.Enums;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Services;
|
||||
|
||||
@@ -23,8 +21,7 @@ public sealed class AdminAuthService(
|
||||
IMenuRepository menuRepository,
|
||||
IPasswordHasher<IdentityUser> passwordHasher,
|
||||
IJwtTokenService jwtTokenService,
|
||||
IRefreshTokenStore refreshTokenStore,
|
||||
ITenantProvider tenantProvider) : IAdminAuthService
|
||||
IRefreshTokenStore refreshTokenStore) : IAdminAuthService
|
||||
{
|
||||
/// <summary>
|
||||
/// 管理后台登录:验证账号密码并生成令牌。
|
||||
@@ -159,92 +156,6 @@ public sealed class AdminAuthService(
|
||||
return menu;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定用户的权限概览(校验当前租户)。
|
||||
/// </summary>
|
||||
public async Task<UserPermissionDto?> GetUserPermissionsAsync(long userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var user = await userRepository.FindByIdAsync(userId, cancellationToken);
|
||||
if (user == null || user.TenantId != tenantId)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 1. 解析角色集合
|
||||
var roleCodes = await ResolveUserRolesAsync(user.Portal, user.TenantId, user.Id, cancellationToken);
|
||||
// 2. (空行后) 解析权限集合
|
||||
var permissionCodes = await ResolveUserPermissionsAsync(user.Portal, user.TenantId, user.Id, cancellationToken);
|
||||
|
||||
// 3. (空行后) 返回概览
|
||||
return new UserPermissionDto
|
||||
{
|
||||
UserId = user.Id,
|
||||
TenantId = user.TenantId,
|
||||
MerchantId = user.MerchantId,
|
||||
Account = user.Account,
|
||||
DisplayName = user.DisplayName,
|
||||
Roles = roleCodes,
|
||||
Permissions = permissionCodes,
|
||||
CreatedAt = user.CreatedAt
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按租户分页查询用户权限概览。
|
||||
/// </summary>
|
||||
public async Task<PagedResult<UserPermissionDto>> SearchUserPermissionsAsync(
|
||||
string? keyword,
|
||||
int page,
|
||||
int pageSize,
|
||||
string? sortBy,
|
||||
bool sortDescending,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 获取当前租户
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
// 2. (空行后) 查询用户列表
|
||||
var users = await userRepository.SearchAsync(tenantId, keyword, cancellationToken);
|
||||
|
||||
// 3. (空行后) 排序
|
||||
var sorted = sortBy?.ToLowerInvariant() switch
|
||||
{
|
||||
"account" => sortDescending
|
||||
? users.OrderByDescending(x => x.Account)
|
||||
: users.OrderBy(x => x.Account),
|
||||
"displayname" => sortDescending
|
||||
? users.OrderByDescending(x => x.DisplayName)
|
||||
: users.OrderBy(x => x.DisplayName),
|
||||
_ => sortDescending
|
||||
? users.OrderByDescending(x => x.CreatedAt)
|
||||
: users.OrderBy(x => x.CreatedAt)
|
||||
};
|
||||
|
||||
// 4. (空行后) 分页
|
||||
var paged = sorted
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToList();
|
||||
|
||||
// 5. (空行后) 解析角色与权限
|
||||
var resolved = await ResolveRolesAndPermissionsAsync(PortalType.Tenant, tenantId, paged, cancellationToken);
|
||||
// 6. (空行后) 映射为 DTO
|
||||
var items = paged.Select(user => new UserPermissionDto
|
||||
{
|
||||
UserId = user.Id,
|
||||
TenantId = user.TenantId,
|
||||
MerchantId = user.MerchantId,
|
||||
Account = user.Account,
|
||||
DisplayName = user.DisplayName,
|
||||
Roles = resolved[user.Id].roles,
|
||||
Permissions = resolved[user.Id].permissions,
|
||||
CreatedAt = user.CreatedAt
|
||||
}).ToList();
|
||||
|
||||
// 7. (空行后) 返回分页结果
|
||||
return new PagedResult<UserPermissionDto>(items, page, pageSize, users.Count);
|
||||
}
|
||||
|
||||
private async Task<CurrentUserProfile> BuildProfileAsync(IdentityUser user, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 解析角色集合
|
||||
@@ -495,68 +406,4 @@ public sealed class AdminAuthService(
|
||||
var permissions = await permissionRepository.GetByIdsAsync(permissionIds, cancellationToken);
|
||||
return permissions.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
}
|
||||
|
||||
private async Task<Dictionary<long, (string[] roles, string[] permissions)>> ResolveRolesAndPermissionsAsync(
|
||||
PortalType portal,
|
||||
long? tenantId,
|
||||
IReadOnlyCollection<IdentityUser> users,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 读取用户-角色关系
|
||||
var userIds = users.Select(x => x.Id).ToArray();
|
||||
var userRoleRelations = await userRoleRepository.GetByUserIdsAsync(portal, tenantId, userIds, cancellationToken);
|
||||
var roleIds = userRoleRelations.Select(x => x.RoleId).Distinct().ToArray();
|
||||
|
||||
// 2. (空行后) 读取角色定义
|
||||
var roles = roleIds.Length == 0
|
||||
? Array.Empty<Role>()
|
||||
: await roleRepository.GetByIdsAsync(portal, tenantId, roleIds, cancellationToken);
|
||||
var roleCodeMap = roles.ToDictionary(r => r.Id, r => r.Code, comparer: EqualityComparer<long>.Default);
|
||||
|
||||
// 3. (空行后) 读取角色-权限关系
|
||||
var rolePermissions = roleIds.Length == 0
|
||||
? Array.Empty<RolePermission>()
|
||||
: await rolePermissionRepository.GetByRoleIdsAsync(portal, tenantId, roleIds, cancellationToken);
|
||||
|
||||
// 4. (空行后) 读取权限定义
|
||||
var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray();
|
||||
var permissions = permissionIds.Length == 0
|
||||
? Array.Empty<Permission>()
|
||||
: await permissionRepository.GetByIdsAsync(permissionIds, cancellationToken);
|
||||
var permissionCodeMap = permissions.ToDictionary(p => p.Id, p => p.Code, comparer: EqualityComparer<long>.Default);
|
||||
|
||||
// 5. (空行后) 构建 Role -> PermissionId[] 映射
|
||||
var rolePermissionsLookup = rolePermissions
|
||||
.GroupBy(rp => rp.RoleId)
|
||||
.ToDictionary(g => g.Key, g => g.Select(rp => rp.PermissionId).ToArray(), comparer: EqualityComparer<long>.Default);
|
||||
|
||||
// 6. (空行后) 按用户聚合角色码与权限码
|
||||
var result = new Dictionary<long, (string[] roles, string[] permissions)>();
|
||||
foreach (var userId in userIds)
|
||||
{
|
||||
// 6.1 解析用户角色码
|
||||
var rolesForUser = userRoleRelations.Where(ur => ur.UserId == userId).Select(ur => ur.RoleId).Distinct().ToArray();
|
||||
var roleCodes = rolesForUser
|
||||
.Select(rid => roleCodeMap.GetValueOrDefault(rid))
|
||||
.Where(c => !string.IsNullOrWhiteSpace(c))
|
||||
.Select(c => c!)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
// 6.2 (空行后) 解析用户权限码
|
||||
var permissionCodes = rolesForUser
|
||||
.SelectMany(rid => rolePermissionsLookup.GetValueOrDefault(rid) ?? Array.Empty<long>())
|
||||
.Select(pid => permissionCodeMap.GetValueOrDefault(pid))
|
||||
.Where(code => !string.IsNullOrWhiteSpace(code))
|
||||
.Select(code => code!)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
// 6.3 (空行后) 写入结果
|
||||
result[userId] = (roleCodes, permissionCodes);
|
||||
}
|
||||
|
||||
// 7. (空行后) 返回聚合结果
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using System.Net;
|
||||
using TakeoutSaaS.Application.Identity.Abstractions;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
using TakeoutSaaS.Domain.Identity.Entities;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 小程序认证服务实现。
|
||||
/// </summary>
|
||||
public sealed class MiniAuthService(
|
||||
IWeChatAuthService weChatAuthService,
|
||||
IMiniUserRepository miniUserRepository,
|
||||
IJwtTokenService jwtTokenService,
|
||||
IRefreshTokenStore refreshTokenStore,
|
||||
ILoginRateLimiter rateLimiter,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
ITenantProvider tenantProvider) : IMiniAuthService
|
||||
{
|
||||
/// <summary>
|
||||
/// 微信小程序登录:通过微信 code 获取用户信息并生成令牌。
|
||||
/// </summary>
|
||||
/// <param name="request">微信登录请求</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>令牌响应</returns>
|
||||
/// <exception cref="BusinessException">获取微信用户信息失败、缺少租户标识时抛出</exception>
|
||||
public async Task<TokenResponse> LoginWithWeChatAsync(WeChatLoginRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 限流检查(基于 IP 地址)
|
||||
var throttleKey = BuildThrottleKey();
|
||||
await rateLimiter.EnsureAllowedAsync(throttleKey, cancellationToken);
|
||||
|
||||
// 2. 通过微信 code 获取 session(OpenId、UnionId、SessionKey)
|
||||
var session = await weChatAuthService.Code2SessionAsync(request.Code, cancellationToken);
|
||||
if (string.IsNullOrWhiteSpace(session.OpenId))
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Unauthorized, "获取微信用户信息失败");
|
||||
}
|
||||
|
||||
// 3. 获取当前租户 ID(多租户支持)
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
if (tenantId == 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识");
|
||||
}
|
||||
|
||||
// 4. 获取或创建小程序用户(如果 OpenId 已存在则返回现有用户,否则创建新用户)
|
||||
var (user, isNew) = await GetOrBindMiniUserAsync(session.OpenId, session.UnionId, request.Nickname, request.Avatar, tenantId, cancellationToken);
|
||||
|
||||
// 5. 登录成功后重置限流计数
|
||||
await rateLimiter.ResetAsync(throttleKey, cancellationToken);
|
||||
|
||||
// 6. 构建用户档案并生成令牌
|
||||
var profile = BuildProfile(user);
|
||||
return await jwtTokenService.CreateTokensAsync(profile, isNew, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新访问令牌:使用刷新令牌获取新的访问令牌和刷新令牌。
|
||||
/// </summary>
|
||||
/// <param name="request">刷新令牌请求</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>新的令牌响应</returns>
|
||||
/// <exception cref="BusinessException">刷新令牌无效、已过期或用户不存在时抛出</exception>
|
||||
public async Task<TokenResponse> RefreshTokenAsync(RefreshTokenRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 验证刷新令牌(检查是否存在、是否过期、是否已撤销)
|
||||
var descriptor = await refreshTokenStore.GetAsync(request.RefreshToken, cancellationToken);
|
||||
if (descriptor == null || descriptor.ExpiresAt <= DateTime.UtcNow || descriptor.Revoked)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Unauthorized, "RefreshToken 无效或已过期");
|
||||
}
|
||||
|
||||
// 2. 根据用户 ID 查找用户
|
||||
var user = await miniUserRepository.FindByIdAsync(descriptor.UserId, cancellationToken)
|
||||
?? throw new BusinessException(ErrorCodes.Unauthorized, "用户不存在");
|
||||
|
||||
// 3. 撤销旧刷新令牌(防止重复使用)
|
||||
await refreshTokenStore.RevokeAsync(descriptor.Token, cancellationToken);
|
||||
|
||||
// 4. 生成新的令牌对
|
||||
var profile = BuildProfile(user);
|
||||
return await jwtTokenService.CreateTokensAsync(profile, false, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取用户档案。
|
||||
/// </summary>
|
||||
/// <param name="userId">用户 ID</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>用户档案</returns>
|
||||
/// <exception cref="BusinessException">用户不存在时抛出</exception>
|
||||
public async Task<CurrentUserProfile> GetProfileAsync(long userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var user = await miniUserRepository.FindByIdAsync(userId, cancellationToken)
|
||||
?? throw new BusinessException(ErrorCodes.NotFound, "用户不存在");
|
||||
|
||||
return BuildProfile(user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取或绑定小程序用户:如果 OpenId 已存在则返回现有用户,否则创建新用户。
|
||||
/// </summary>
|
||||
/// <param name="openId">微信 OpenId</param>
|
||||
/// <param name="unionId">微信 UnionId(可选)</param>
|
||||
/// <param name="nickname">昵称</param>
|
||||
/// <param name="avatar">头像地址(可选)</param>
|
||||
/// <param name="tenantId">租户 ID</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>用户实体和是否为新用户的元组</returns>
|
||||
private async Task<(MiniUser user, bool isNew)> GetOrBindMiniUserAsync(string openId, string? unionId, string? nickname, string? avatar, long tenantId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 检查用户是否已存在
|
||||
var existing = await miniUserRepository.FindByOpenIdAsync(openId, cancellationToken);
|
||||
if (existing != null)
|
||||
{
|
||||
return (existing, false);
|
||||
}
|
||||
|
||||
// 创建新用户
|
||||
var created = await miniUserRepository.CreateOrUpdateAsync(openId, unionId, nickname, avatar, tenantId, cancellationToken);
|
||||
return (created, true);
|
||||
}
|
||||
|
||||
private static CurrentUserProfile BuildProfile(MiniUser user)
|
||||
=> new()
|
||||
{
|
||||
UserId = user.Id,
|
||||
Account = user.OpenId,
|
||||
DisplayName = user.Nickname,
|
||||
TenantId = user.TenantId,
|
||||
MerchantId = null,
|
||||
Roles = Array.Empty<string>(),
|
||||
Permissions = Array.Empty<string>(),
|
||||
Avatar = user.Avatar
|
||||
};
|
||||
|
||||
private string BuildThrottleKey()
|
||||
{
|
||||
var ip = httpContextAccessor.HttpContext?.Connection.RemoteIpAddress ?? IPAddress.Loopback;
|
||||
return $"mini-login:{ip}";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user