refactor: 管理端去租户过滤并Portal化RBAC菜单
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Contracts;
|
||||
|
||||
/// <summary>
|
||||
@@ -5,6 +7,11 @@ namespace TakeoutSaaS.Application.Identity.Contracts;
|
||||
/// </summary>
|
||||
public sealed class CurrentUserProfile
|
||||
{
|
||||
/// <summary>
|
||||
/// 账号所属 Portal。
|
||||
/// </summary>
|
||||
public PortalType Portal { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户 ID。
|
||||
/// </summary>
|
||||
@@ -23,7 +30,7 @@ public sealed class CurrentUserProfile
|
||||
/// <summary>
|
||||
/// 所属租户 ID。
|
||||
/// </summary>
|
||||
public long TenantId { get; init; }
|
||||
public long? TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 所属商户 ID(平台管理员为空)。
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Contracts;
|
||||
@@ -8,18 +9,17 @@ namespace TakeoutSaaS.Application.Identity.Contracts;
|
||||
/// </summary>
|
||||
public sealed class PermissionDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 权限所属 Portal。
|
||||
/// </summary>
|
||||
public PortalType Portal { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 权限 ID(雪花,序列化为字符串)。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户 ID(固定权限时为基准租户)。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 父级权限 ID。
|
||||
/// </summary>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Contracts;
|
||||
@@ -8,18 +9,17 @@ namespace TakeoutSaaS.Application.Identity.Contracts;
|
||||
/// </summary>
|
||||
public sealed record PermissionTreeDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 权限所属 Portal。
|
||||
/// </summary>
|
||||
public PortalType Portal { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 权限 ID(雪花,序列化为字符串)。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户 ID(雪花,序列化为字符串)。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 父级权限 ID。
|
||||
/// </summary>
|
||||
@@ -42,7 +42,7 @@ public sealed record PermissionTreeDto
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 权限编码(租户内唯一)。
|
||||
/// 权限编码(全局唯一)。
|
||||
/// </summary>
|
||||
public string Code { get; init; } = string.Empty;
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Contracts;
|
||||
|
||||
/// <summary>
|
||||
@@ -5,15 +7,20 @@ namespace TakeoutSaaS.Application.Identity.Contracts;
|
||||
/// </summary>
|
||||
public sealed record RoleDetailDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 角色所属 Portal。
|
||||
/// </summary>
|
||||
public PortalType Portal { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色 ID。
|
||||
/// </summary>
|
||||
public long Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户 ID。
|
||||
/// 租户 ID(Portal=Tenant 必填;Portal=Admin 为空)。
|
||||
/// </summary>
|
||||
public long TenantId { get; init; }
|
||||
public long? TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色名称。
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Contracts;
|
||||
@@ -8,6 +9,11 @@ namespace TakeoutSaaS.Application.Identity.Contracts;
|
||||
/// </summary>
|
||||
public sealed class RoleDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 角色所属 Portal。
|
||||
/// </summary>
|
||||
public PortalType Portal { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色 ID(雪花,序列化为字符串)。
|
||||
/// </summary>
|
||||
@@ -15,10 +21,10 @@ public sealed class RoleDto
|
||||
public long Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户 ID。
|
||||
/// 租户 ID(Portal=Tenant 必填;Portal=Admin 为空)。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long TenantId { get; init; }
|
||||
[JsonConverter(typeof(NullableSnowflakeIdJsonConverter))]
|
||||
public long? TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色名称。
|
||||
|
||||
@@ -9,6 +9,11 @@ namespace TakeoutSaaS.Application.Identity.Contracts;
|
||||
/// </summary>
|
||||
public sealed record UserDetailDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 账号所属 Portal。
|
||||
/// </summary>
|
||||
public PortalType Portal { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户 ID。
|
||||
/// </summary>
|
||||
@@ -16,10 +21,10 @@ public sealed record UserDetailDto
|
||||
public long UserId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户 ID。
|
||||
/// 租户 ID(Portal=Tenant 必填;Portal=Admin 为空)。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long TenantId { get; init; }
|
||||
[JsonConverter(typeof(NullableSnowflakeIdJsonConverter))]
|
||||
public long? TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 商户 ID。
|
||||
|
||||
@@ -9,6 +9,11 @@ namespace TakeoutSaaS.Application.Identity.Contracts;
|
||||
/// </summary>
|
||||
public sealed record UserListItemDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 账号所属 Portal。
|
||||
/// </summary>
|
||||
public PortalType Portal { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户 ID。
|
||||
/// </summary>
|
||||
@@ -16,10 +21,10 @@ public sealed record UserListItemDto
|
||||
public long UserId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户 ID。
|
||||
/// 租户 ID(Portal=Tenant 必填;Portal=Admin 为空)。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long TenantId { get; init; }
|
||||
[JsonConverter(typeof(NullableSnowflakeIdJsonConverter))]
|
||||
public long? TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 登录账号。
|
||||
|
||||
@@ -15,10 +15,10 @@ public sealed class UserPermissionDto
|
||||
public long UserId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户 ID(雪花,序列化为字符串)。
|
||||
/// 租户 ID(雪花,序列化为字符串;平台管理员为空)。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long TenantId { get; init; }
|
||||
[JsonConverter(typeof(NullableSnowflakeIdJsonConverter))]
|
||||
public long? TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 商户 ID(雪花,序列化为字符串,可空)。
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.Identity.Commands;
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
@@ -21,14 +22,17 @@ public sealed class AssignUserRolesCommandHandler(
|
||||
/// <returns>执行结果。</returns>
|
||||
public async Task<bool> Handle(AssignUserRolesCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 获取租户上下文
|
||||
// 1. 固定为租户侧用户分配角色
|
||||
var portal = PortalType.Tenant;
|
||||
|
||||
// 2. 获取租户上下文
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
|
||||
// 2. 覆盖式绑定角色
|
||||
await userRoleRepository.ReplaceUserRolesAsync(tenantId, request.UserId, request.RoleIds, cancellationToken);
|
||||
// 3. 覆盖式绑定角色
|
||||
await userRoleRepository.ReplaceUserRolesAsync(portal, tenantId, request.UserId, request.RoleIds, cancellationToken);
|
||||
await userRoleRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 3. 返回执行结果
|
||||
// 4. 返回执行结果
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,14 +63,14 @@ public sealed class BatchIdentityUserOperationCommandHandler(
|
||||
|
||||
// 4. 查询目标用户集合
|
||||
var includeDeleted = request.Operation == IdentityUserBatchOperation.Restore;
|
||||
var users = await identityUserRepository.GetForUpdateByIdsAsync(tenantId, userIds, includeDeleted, isSuperAdmin, cancellationToken);
|
||||
var users = await identityUserRepository.GetForUpdateByIdsAsync(tenantId, userIds, includeDeleted, cancellationToken);
|
||||
var usersById = users.ToDictionary(user => user.Id, user => user, EqualityComparer<long>.Default);
|
||||
|
||||
// 5. 预计算租户管理员约束
|
||||
var tenantAdminRole = await roleRepository.FindByCodeAsync("tenant-admin", tenantId, cancellationToken);
|
||||
var tenantAdminRole = await roleRepository.FindByCodeAsync(PortalType.Tenant, tenantId, "tenant-admin", cancellationToken);
|
||||
var tenantAdminUserIds = tenantAdminRole == null
|
||||
? Array.Empty<long>()
|
||||
: (await userRoleRepository.GetByUserIdsAsync(tenantId, usersById.Keys, cancellationToken))
|
||||
: (await userRoleRepository.GetByUserIdsAsync(PortalType.Tenant, tenantId, usersById.Keys, cancellationToken))
|
||||
.Where(x => x.RoleId == tenantAdminRole.Id)
|
||||
.Select(x => x.UserId)
|
||||
.Distinct()
|
||||
@@ -85,7 +85,7 @@ public sealed class BatchIdentityUserOperationCommandHandler(
|
||||
IncludeDeleted = false,
|
||||
Page = 1,
|
||||
PageSize = 1
|
||||
}, isSuperAdmin, cancellationToken)).Total;
|
||||
}, cancellationToken)).Total;
|
||||
var remainingActiveAdmins = activeAdminCount;
|
||||
|
||||
// 6. 执行批量操作
|
||||
@@ -193,6 +193,7 @@ public sealed class BatchIdentityUserOperationCommandHandler(
|
||||
exportItems.AddRange(users.Select(user => new UserListItemDto
|
||||
{
|
||||
UserId = user.Id,
|
||||
Portal = user.Portal,
|
||||
TenantId = user.TenantId,
|
||||
Account = user.Account,
|
||||
DisplayName = user.DisplayName,
|
||||
@@ -276,9 +277,10 @@ public sealed class BatchIdentityUserOperationCommandHandler(
|
||||
var result = new Dictionary<long, string[]>(users.Count);
|
||||
|
||||
// 2. 按租户分组,降低角色查询次数
|
||||
foreach (var group in users.GroupBy(user => user.TenantId))
|
||||
foreach (var group in users.GroupBy(user => new { user.Portal, user.TenantId }))
|
||||
{
|
||||
var tenantId = group.Key;
|
||||
var portal = group.Key.Portal;
|
||||
var tenantId = group.Key.TenantId;
|
||||
var userIds = group.Select(user => user.Id).Distinct().ToArray();
|
||||
if (userIds.Length == 0)
|
||||
{
|
||||
@@ -286,7 +288,7 @@ public sealed class BatchIdentityUserOperationCommandHandler(
|
||||
}
|
||||
|
||||
// 3. 查询用户角色映射
|
||||
var relations = await userRoleRepository.GetByUserIdsAsync(tenantId, userIds, cancellationToken);
|
||||
var relations = await userRoleRepository.GetByUserIdsAsync(portal, tenantId, userIds, cancellationToken);
|
||||
if (relations.Count == 0)
|
||||
{
|
||||
continue;
|
||||
@@ -296,7 +298,7 @@ public sealed class BatchIdentityUserOperationCommandHandler(
|
||||
var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray();
|
||||
var roles = roleIds.Length == 0
|
||||
? Array.Empty<Role>()
|
||||
: await roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken);
|
||||
: await roleRepository.GetByIdsAsync(portal, tenantId, roleIds, cancellationToken);
|
||||
var roleCodeMap = roles.ToDictionary(role => role.Id, role => role.Code, EqualityComparer<long>.Default);
|
||||
|
||||
// 5. 组装用户角色编码列表
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.Identity.Commands;
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
@@ -21,19 +22,22 @@ public sealed class BindRolePermissionsCommandHandler(
|
||||
/// <returns>执行结果。</returns>
|
||||
public async Task<bool> Handle(BindRolePermissionsCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 获取租户上下文
|
||||
// 1. 固定绑定租户侧角色权限
|
||||
var portal = PortalType.Tenant;
|
||||
|
||||
// 2. 获取租户上下文
|
||||
var tenantId = request.TenantId ?? tenantProvider.GetCurrentTenantId();
|
||||
|
||||
// 2. 覆盖式绑定权限
|
||||
// 3. 覆盖式绑定权限
|
||||
var distinctPermissionIds = request.PermissionIds
|
||||
.Where(id => id > 0)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
await rolePermissionRepository.ReplaceRolePermissionsAsync(tenantId, request.RoleId, distinctPermissionIds, cancellationToken);
|
||||
await rolePermissionRepository.ReplaceRolePermissionsAsync(portal, tenantId, request.RoleId, distinctPermissionIds, cancellationToken);
|
||||
await rolePermissionRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 3. 返回执行结果
|
||||
// 4. 返回执行结果
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,9 +40,7 @@ public sealed class ChangeIdentityUserStatusCommandHandler(
|
||||
}
|
||||
|
||||
// 3. 查询用户实体
|
||||
var user = isSuperAdmin
|
||||
? await identityUserRepository.GetForUpdateIgnoringTenantAsync(request.UserId, cancellationToken)
|
||||
: await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken);
|
||||
var user = await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken);
|
||||
if (user == null)
|
||||
{
|
||||
return false;
|
||||
@@ -53,10 +51,13 @@ public sealed class ChangeIdentityUserStatusCommandHandler(
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. 校验租户管理员保留规则
|
||||
if (request.Status == IdentityUserStatus.Disabled && user.Status == IdentityUserStatus.Active)
|
||||
// 4. 校验租户管理员保留规则(仅租户侧用户适用)
|
||||
if (user.Portal == PortalType.Tenant
|
||||
&& request.Status == IdentityUserStatus.Disabled
|
||||
&& user.Status == IdentityUserStatus.Active
|
||||
&& user.TenantId.HasValue)
|
||||
{
|
||||
await EnsureNotLastActiveTenantAdminAsync(user.TenantId, user.Id, isSuperAdmin, cancellationToken);
|
||||
await EnsureNotLastActiveTenantAdminAsync(user.TenantId.Value, user.Id, cancellationToken);
|
||||
}
|
||||
|
||||
// 5. 更新状态
|
||||
@@ -114,17 +115,17 @@ public sealed class ChangeIdentityUserStatusCommandHandler(
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task EnsureNotLastActiveTenantAdminAsync(long tenantId, long userId, bool ignoreTenantFilter, CancellationToken cancellationToken)
|
||||
private async Task EnsureNotLastActiveTenantAdminAsync(long tenantId, long userId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 获取租户管理员角色
|
||||
var tenantAdminRole = await roleRepository.FindByCodeAsync("tenant-admin", tenantId, cancellationToken);
|
||||
var tenantAdminRole = await roleRepository.FindByCodeAsync(PortalType.Tenant, tenantId, "tenant-admin", cancellationToken);
|
||||
if (tenantAdminRole == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 判断用户是否为租户管理员
|
||||
var relations = await userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken);
|
||||
var relations = await userRoleRepository.GetByUserIdAsync(PortalType.Tenant, tenantId, userId, cancellationToken);
|
||||
if (!relations.Any(x => x.RoleId == tenantAdminRole.Id))
|
||||
{
|
||||
return;
|
||||
@@ -140,7 +141,7 @@ public sealed class ChangeIdentityUserStatusCommandHandler(
|
||||
Page = 1,
|
||||
PageSize = 1
|
||||
};
|
||||
var result = await identityUserRepository.SearchPagedAsync(filter, ignoreTenantFilter, cancellationToken);
|
||||
var result = await identityUserRepository.SearchPagedAsync(filter, cancellationToken);
|
||||
if (result.Total <= 1)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "至少保留一个管理员");
|
||||
|
||||
@@ -2,6 +2,7 @@ using MediatR;
|
||||
using TakeoutSaaS.Application.Identity.Commands;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
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;
|
||||
@@ -36,16 +37,20 @@ public sealed class CopyRoleTemplateCommandHandler(
|
||||
|
||||
// 2. 计算角色名称/编码与描述
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
|
||||
// 3. 固定复制为租户侧角色
|
||||
var portal = PortalType.Tenant;
|
||||
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;
|
||||
|
||||
// 1. 准备或更新角色主体(幂等创建)。
|
||||
var role = await roleRepository.FindByCodeAsync(roleCode, tenantId, cancellationToken);
|
||||
// 4. 准备或更新角色主体(幂等创建)。
|
||||
var role = await roleRepository.FindByCodeAsync(portal, tenantId, roleCode, cancellationToken);
|
||||
if (role is null)
|
||||
{
|
||||
role = new Role
|
||||
{
|
||||
Portal = portal,
|
||||
TenantId = tenantId,
|
||||
Name = roleName,
|
||||
Code = roleCode,
|
||||
@@ -68,8 +73,8 @@ public sealed class CopyRoleTemplateCommandHandler(
|
||||
await roleRepository.UpdateAsync(role, cancellationToken);
|
||||
}
|
||||
|
||||
// 3. 确保模板权限全部存在,不存在则按模板定义创建。
|
||||
var existingPermissions = await permissionRepository.GetByCodesAsync(tenantId, permissionCodes, cancellationToken);
|
||||
// 5. 确保模板权限全部存在,不存在则按模板定义创建。
|
||||
var existingPermissions = await permissionRepository.GetByCodesAsync(permissionCodes, cancellationToken);
|
||||
var permissionMap = existingPermissions.ToDictionary(x => x.Code, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var code in permissionCodes)
|
||||
@@ -81,7 +86,6 @@ public sealed class CopyRoleTemplateCommandHandler(
|
||||
|
||||
var permission = new Permission
|
||||
{
|
||||
TenantId = tenantId,
|
||||
Name = code,
|
||||
Code = code,
|
||||
Description = code
|
||||
@@ -93,8 +97,8 @@ public sealed class CopyRoleTemplateCommandHandler(
|
||||
|
||||
await roleRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 4. 绑定缺失的权限,保留租户自定义的已有授权。
|
||||
var rolePermissions = await rolePermissionRepository.GetByRoleIdsAsync(tenantId, new[] { role.Id }, cancellationToken);
|
||||
// 6. 绑定缺失的权限,保留租户自定义的已有授权。
|
||||
var rolePermissions = await rolePermissionRepository.GetByRoleIdsAsync(portal, tenantId, new[] { role.Id }, cancellationToken);
|
||||
var existingPermissionIds = rolePermissions
|
||||
.Select(x => x.PermissionId)
|
||||
.ToHashSet();
|
||||
@@ -108,6 +112,7 @@ public sealed class CopyRoleTemplateCommandHandler(
|
||||
{
|
||||
var relations = toAdd.Select(permissionId => new RolePermission
|
||||
{
|
||||
Portal = portal,
|
||||
TenantId = tenantId,
|
||||
RoleId = role.Id,
|
||||
PermissionId = permissionId
|
||||
@@ -121,6 +126,7 @@ public sealed class CopyRoleTemplateCommandHandler(
|
||||
return new RoleDto
|
||||
{
|
||||
Id = role.Id,
|
||||
Portal = role.Portal,
|
||||
TenantId = role.TenantId,
|
||||
Name = role.Name,
|
||||
Code = role.Code,
|
||||
|
||||
@@ -76,7 +76,8 @@ public sealed class CreateIdentityUserCommandHandler(
|
||||
// 5. 校验角色合法性
|
||||
if (roleIds.Length > 0)
|
||||
{
|
||||
var roles = await roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken);
|
||||
var portal = PortalType.Tenant;
|
||||
var roles = await roleRepository.GetByIdsAsync(portal, tenantId, roleIds, cancellationToken);
|
||||
if (roles.Count != roleIds.Length)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "角色列表包含无效项");
|
||||
@@ -84,9 +85,11 @@ public sealed class CreateIdentityUserCommandHandler(
|
||||
}
|
||||
|
||||
// 6. 创建用户实体
|
||||
var userPortal = PortalType.Tenant;
|
||||
var user = new IdentityUser
|
||||
{
|
||||
Id = idGenerator.NextId(),
|
||||
Portal = userPortal,
|
||||
TenantId = tenantId,
|
||||
Account = account,
|
||||
DisplayName = displayName,
|
||||
@@ -139,7 +142,7 @@ public sealed class CreateIdentityUserCommandHandler(
|
||||
// 9. 绑定角色
|
||||
if (roleIds.Length > 0)
|
||||
{
|
||||
await userRoleRepository.ReplaceUserRolesAsync(tenantId, user.Id, roleIds, cancellationToken);
|
||||
await userRoleRepository.ReplaceUserRolesAsync(userPortal, tenantId, user.Id, roleIds, cancellationToken);
|
||||
}
|
||||
|
||||
// 10. 返回用户详情
|
||||
@@ -147,6 +150,7 @@ public sealed class CreateIdentityUserCommandHandler(
|
||||
return detail ?? new UserDetailDto
|
||||
{
|
||||
UserId = user.Id,
|
||||
Portal = user.Portal,
|
||||
TenantId = user.TenantId,
|
||||
MerchantId = user.MerchantId,
|
||||
Account = user.Account,
|
||||
|
||||
@@ -3,10 +3,10 @@ using TakeoutSaaS.Application.Identity.Commands;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
using TakeoutSaaS.Application.Identity;
|
||||
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.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
|
||||
@@ -14,8 +14,7 @@ namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
/// 创建菜单处理器。
|
||||
/// </summary>
|
||||
public sealed class CreateMenuCommandHandler(
|
||||
IMenuRepository menuRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
IMenuRepository menuRepository)
|
||||
: IRequestHandler<CreateMenuCommand, MenuDefinitionDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
@@ -27,11 +26,11 @@ public sealed class CreateMenuCommandHandler(
|
||||
throw new BusinessException(ErrorCodes.Forbidden, "菜单已固定,禁止新增");
|
||||
}
|
||||
|
||||
// 2. 构造实体
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
// 2. 构造实体(仅允许维护管理端菜单)
|
||||
var portal = PortalType.Admin;
|
||||
var entity = new MenuDefinition
|
||||
{
|
||||
TenantId = tenantId,
|
||||
Portal = portal,
|
||||
ParentId = request.ParentId,
|
||||
Name = request.Name.Trim(),
|
||||
Path = request.Path.Trim(),
|
||||
|
||||
@@ -2,10 +2,10 @@ using MediatR;
|
||||
using TakeoutSaaS.Application.Identity.Commands;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
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.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
|
||||
@@ -13,8 +13,7 @@ namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
/// 创建权限处理器。
|
||||
/// </summary>
|
||||
public sealed class CreatePermissionCommandHandler(
|
||||
IPermissionRepository permissionRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
IPermissionRepository permissionRepository)
|
||||
: IRequestHandler<CreatePermissionCommand, PermissionDto>
|
||||
{
|
||||
/// <summary>
|
||||
@@ -31,10 +30,7 @@ public sealed class CreatePermissionCommandHandler(
|
||||
throw new BusinessException(ErrorCodes.Forbidden, "权限已固定,禁止新增");
|
||||
}
|
||||
|
||||
// 2. 获取租户上下文
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
|
||||
// 3. 构建权限实体
|
||||
// 2. 构建权限实体
|
||||
var normalizedType = string.IsNullOrWhiteSpace(request.Type)
|
||||
? "leaf"
|
||||
: request.Type.Trim().ToLowerInvariant();
|
||||
@@ -43,7 +39,7 @@ public sealed class CreatePermissionCommandHandler(
|
||||
var sortOrder = request.SortOrder < 0 ? 0 : request.SortOrder;
|
||||
var permission = new Permission
|
||||
{
|
||||
TenantId = tenantId,
|
||||
Portal = PortalType.Admin,
|
||||
ParentId = parentId,
|
||||
SortOrder = sortOrder,
|
||||
Type = normalizedType,
|
||||
@@ -52,15 +48,15 @@ public sealed class CreatePermissionCommandHandler(
|
||||
Description = request.Description
|
||||
};
|
||||
|
||||
// 4. 持久化
|
||||
// 3. 持久化
|
||||
await permissionRepository.AddAsync(permission, cancellationToken);
|
||||
await permissionRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 5. 返回 DTO
|
||||
// 4. 返回 DTO
|
||||
return new PermissionDto
|
||||
{
|
||||
Portal = permission.Portal,
|
||||
Id = permission.Id,
|
||||
TenantId = permission.TenantId,
|
||||
ParentId = permission.ParentId,
|
||||
SortOrder = permission.SortOrder,
|
||||
Type = permission.Type,
|
||||
|
||||
@@ -2,6 +2,7 @@ using MediatR;
|
||||
using TakeoutSaaS.Application.Identity.Commands;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
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;
|
||||
@@ -25,10 +26,13 @@ public sealed class CreateRoleCommandHandler(
|
||||
/// <returns>创建后的角色 DTO。</returns>
|
||||
public async Task<RoleDto> Handle(CreateRoleCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 获取租户上下文
|
||||
// 1. 固定创建租户侧角色
|
||||
var portal = PortalType.Tenant;
|
||||
|
||||
// 2. 获取租户上下文
|
||||
var tenantId = request.TenantId ?? tenantProvider.GetCurrentTenantId();
|
||||
|
||||
// 2. 归一化输入并校验唯一
|
||||
// 3. 归一化输入并校验唯一
|
||||
var name = request.Name?.Trim() ?? string.Empty;
|
||||
var code = request.Code?.Trim() ?? string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(code))
|
||||
@@ -36,29 +40,31 @@ public sealed class CreateRoleCommandHandler(
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "角色名称与编码不能为空");
|
||||
}
|
||||
|
||||
var exists = await roleRepository.FindByCodeAsync(code, tenantId, cancellationToken);
|
||||
var exists = await roleRepository.FindByCodeAsync(portal, tenantId, code, cancellationToken);
|
||||
if (exists is not null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "角色编码已存在");
|
||||
}
|
||||
|
||||
// 3. 构建角色实体
|
||||
// 4. 构建角色实体
|
||||
var role = new Role
|
||||
{
|
||||
Portal = portal,
|
||||
TenantId = tenantId,
|
||||
Name = name,
|
||||
Code = code,
|
||||
Description = request.Description
|
||||
};
|
||||
|
||||
// 4. 持久化
|
||||
// 5. 持久化
|
||||
await roleRepository.AddAsync(role, cancellationToken);
|
||||
await roleRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 5. 返回 DTO
|
||||
// 6. 返回 DTO
|
||||
return new RoleDto
|
||||
{
|
||||
Id = role.Id,
|
||||
Portal = role.Portal,
|
||||
TenantId = role.TenantId,
|
||||
Name = role.Name,
|
||||
Code = role.Code,
|
||||
|
||||
@@ -40,9 +40,7 @@ public sealed class DeleteIdentityUserCommandHandler(
|
||||
}
|
||||
|
||||
// 3. 查询用户实体
|
||||
var user = isSuperAdmin
|
||||
? await identityUserRepository.GetForUpdateIgnoringTenantAsync(request.UserId, cancellationToken)
|
||||
: await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken);
|
||||
var user = await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken);
|
||||
if (user == null)
|
||||
{
|
||||
return false;
|
||||
@@ -53,10 +51,10 @@ public sealed class DeleteIdentityUserCommandHandler(
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. 校验租户管理员保留规则
|
||||
if (user.Status == IdentityUserStatus.Active)
|
||||
// 4. 校验租户管理员保留规则(仅租户侧用户适用)
|
||||
if (user.Portal == PortalType.Tenant && user.Status == IdentityUserStatus.Active && user.TenantId.HasValue)
|
||||
{
|
||||
await EnsureNotLastActiveTenantAdminAsync(user.TenantId, user.Id, isSuperAdmin, cancellationToken);
|
||||
await EnsureNotLastActiveTenantAdminAsync(user.TenantId.Value, user.Id, cancellationToken);
|
||||
}
|
||||
|
||||
// 5. 构建操作日志消息
|
||||
@@ -88,17 +86,17 @@ public sealed class DeleteIdentityUserCommandHandler(
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task EnsureNotLastActiveTenantAdminAsync(long tenantId, long userId, bool ignoreTenantFilter, CancellationToken cancellationToken)
|
||||
private async Task EnsureNotLastActiveTenantAdminAsync(long tenantId, long userId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 获取租户管理员角色
|
||||
var tenantAdminRole = await roleRepository.FindByCodeAsync("tenant-admin", tenantId, cancellationToken);
|
||||
var tenantAdminRole = await roleRepository.FindByCodeAsync(PortalType.Tenant, tenantId, "tenant-admin", cancellationToken);
|
||||
if (tenantAdminRole == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 判断用户是否为租户管理员
|
||||
var relations = await userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken);
|
||||
var relations = await userRoleRepository.GetByUserIdAsync(PortalType.Tenant, tenantId, userId, cancellationToken);
|
||||
if (!relations.Any(x => x.RoleId == tenantAdminRole.Id))
|
||||
{
|
||||
return;
|
||||
@@ -114,7 +112,7 @@ public sealed class DeleteIdentityUserCommandHandler(
|
||||
Page = 1,
|
||||
PageSize = 1
|
||||
};
|
||||
var result = await identityUserRepository.SearchPagedAsync(filter, ignoreTenantFilter, cancellationToken);
|
||||
var result = await identityUserRepository.SearchPagedAsync(filter, cancellationToken);
|
||||
if (result.Total <= 1)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "至少保留一个管理员");
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.Identity;
|
||||
using TakeoutSaaS.Application.Identity.Commands;
|
||||
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;
|
||||
|
||||
@@ -12,8 +12,7 @@ namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
/// 删除菜单处理器。
|
||||
/// </summary>
|
||||
public sealed class DeleteMenuCommandHandler(
|
||||
IMenuRepository menuRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
IMenuRepository menuRepository)
|
||||
: IRequestHandler<DeleteMenuCommand, bool>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
@@ -25,9 +24,9 @@ public sealed class DeleteMenuCommandHandler(
|
||||
throw new BusinessException(ErrorCodes.Forbidden, "菜单已固定,禁止删除");
|
||||
}
|
||||
|
||||
// 2. 删除目标及可能的孤儿由外层保证
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
await menuRepository.DeleteAsync(request.Id, tenantId, cancellationToken);
|
||||
// 2. 删除目标及可能的孤儿由外层保证(仅维护管理端菜单)
|
||||
var portal = PortalType.Admin;
|
||||
await menuRepository.DeleteAsync(request.Id, portal, cancellationToken);
|
||||
|
||||
// 3. 持久化
|
||||
await menuRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
@@ -3,7 +3,6 @@ using TakeoutSaaS.Application.Identity.Commands;
|
||||
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;
|
||||
|
||||
@@ -11,8 +10,7 @@ namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
/// 删除权限处理器。
|
||||
/// </summary>
|
||||
public sealed class DeletePermissionCommandHandler(
|
||||
IPermissionRepository permissionRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
IPermissionRepository permissionRepository)
|
||||
: IRequestHandler<DeletePermissionCommand, bool>
|
||||
{
|
||||
/// <summary>
|
||||
@@ -29,14 +27,11 @@ public sealed class DeletePermissionCommandHandler(
|
||||
throw new BusinessException(ErrorCodes.Forbidden, "权限已固定,禁止删除");
|
||||
}
|
||||
|
||||
// 2. 获取租户上下文
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
|
||||
// 3. 删除权限
|
||||
await permissionRepository.DeleteAsync(request.PermissionId, tenantId, cancellationToken);
|
||||
// 2. 删除权限
|
||||
await permissionRepository.DeleteAsync(request.PermissionId, cancellationToken);
|
||||
await permissionRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 4. 返回执行结果
|
||||
// 3. 返回执行结果
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.Identity.Commands;
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
@@ -21,14 +22,17 @@ public sealed class DeleteRoleCommandHandler(
|
||||
/// <returns>执行结果。</returns>
|
||||
public async Task<bool> Handle(DeleteRoleCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 获取租户上下文
|
||||
// 1. 固定删除租户侧角色
|
||||
var portal = PortalType.Tenant;
|
||||
|
||||
// 2. 获取租户上下文
|
||||
var tenantId = request.TenantId ?? tenantProvider.GetCurrentTenantId();
|
||||
|
||||
// 2. 删除角色
|
||||
await roleRepository.DeleteAsync(request.RoleId, tenantId, cancellationToken);
|
||||
// 3. 删除角色
|
||||
await roleRepository.DeleteAsync(portal, tenantId, request.RoleId, cancellationToken);
|
||||
await roleRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 3. 返回执行结果
|
||||
// 4. 返回执行结果
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,17 +33,9 @@ public sealed class GetIdentityUserDetailQueryHandler(
|
||||
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
|
||||
|
||||
// 2. 查询用户实体
|
||||
IdentityUser? user;
|
||||
if (request.IncludeDeleted)
|
||||
{
|
||||
user = await identityUserRepository.GetForUpdateIncludingDeletedAsync(currentTenantId, request.UserId, isSuperAdmin, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
user = isSuperAdmin
|
||||
? await identityUserRepository.GetForUpdateIgnoringTenantAsync(request.UserId, cancellationToken)
|
||||
: await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken);
|
||||
}
|
||||
var user = request.IncludeDeleted
|
||||
? await identityUserRepository.GetForUpdateIncludingDeletedAsync(request.UserId, cancellationToken)
|
||||
: await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
@@ -56,34 +48,39 @@ public sealed class GetIdentityUserDetailQueryHandler(
|
||||
}
|
||||
|
||||
// 3. 加载角色与权限
|
||||
var roleRelations = await userRoleRepository.GetByUserIdAsync(user.TenantId, user.Id, cancellationToken);
|
||||
var portal = user.Portal;
|
||||
var tenantId = user.TenantId;
|
||||
|
||||
// 4. 查询用户角色关系
|
||||
var roleRelations = await userRoleRepository.GetByUserIdAsync(portal, tenantId, user.Id, cancellationToken);
|
||||
var roleIds = roleRelations.Select(x => x.RoleId).Distinct().ToArray();
|
||||
var roles = roleIds.Length == 0
|
||||
? Array.Empty<Role>()
|
||||
: await roleRepository.GetByIdsAsync(user.TenantId, roleIds, cancellationToken);
|
||||
: await roleRepository.GetByIdsAsync(portal, tenantId, roleIds, cancellationToken);
|
||||
var roleCodes = roles.Select(x => x.Code)
|
||||
.Where(code => !string.IsNullOrWhiteSpace(code))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
var permissionIds = roleIds.Length == 0
|
||||
? Array.Empty<long>()
|
||||
: (await rolePermissionRepository.GetByRoleIdsAsync(user.TenantId, roleIds, cancellationToken))
|
||||
: (await rolePermissionRepository.GetByRoleIdsAsync(portal, tenantId, roleIds, cancellationToken))
|
||||
.Select(x => x.PermissionId)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
var permissions = permissionIds.Length == 0
|
||||
? Array.Empty<string>()
|
||||
: (await permissionRepository.GetByIdsAsync(user.TenantId, permissionIds, cancellationToken))
|
||||
: (await permissionRepository.GetByIdsAsync(permissionIds, cancellationToken))
|
||||
.Select(x => x.Code)
|
||||
.Where(code => !string.IsNullOrWhiteSpace(code))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
// 4. 组装详情 DTO
|
||||
// 5. 组装详情 DTO
|
||||
var now = DateTime.UtcNow;
|
||||
return new UserDetailDto
|
||||
{
|
||||
UserId = user.Id,
|
||||
Portal = user.Portal,
|
||||
TenantId = user.TenantId,
|
||||
MerchantId = user.MerchantId,
|
||||
Account = user.Account,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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;
|
||||
|
||||
@@ -22,6 +23,7 @@ public sealed class GetUserPermissionsQueryHandler(
|
||||
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)
|
||||
@@ -30,8 +32,8 @@ public sealed class GetUserPermissionsQueryHandler(
|
||||
}
|
||||
|
||||
// 2. 解析角色与权限
|
||||
var roleCodes = await ResolveUserRolesAsync(tenantId, user.Id, cancellationToken);
|
||||
var permissionCodes = await ResolveUserPermissionsAsync(tenantId, user.Id, cancellationToken);
|
||||
var roleCodes = await ResolveUserRolesAsync(portal, tenantId, user.Id, cancellationToken);
|
||||
var permissionCodes = await ResolveUserPermissionsAsync(portal, tenantId, user.Id, cancellationToken);
|
||||
|
||||
// 3. 返回用户权限概览
|
||||
return new UserPermissionDto
|
||||
@@ -47,10 +49,10 @@ public sealed class GetUserPermissionsQueryHandler(
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<string[]> ResolveUserRolesAsync(long tenantId, long userId, CancellationToken cancellationToken)
|
||||
private async Task<string[]> ResolveUserRolesAsync(PortalType portal, long tenantId, long userId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询用户角色关系
|
||||
var relations = await userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken);
|
||||
var relations = await userRoleRepository.GetByUserIdAsync(portal, tenantId, userId, cancellationToken);
|
||||
var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray();
|
||||
if (roleIds.Length == 0)
|
||||
{
|
||||
@@ -58,14 +60,14 @@ public sealed class GetUserPermissionsQueryHandler(
|
||||
}
|
||||
|
||||
// 2. 查询角色编码
|
||||
var roles = await roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken);
|
||||
var roles = await roleRepository.GetByIdsAsync(portal, tenantId, roleIds, cancellationToken);
|
||||
return roles.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
}
|
||||
|
||||
private async Task<string[]> ResolveUserPermissionsAsync(long tenantId, long userId, CancellationToken cancellationToken)
|
||||
private async Task<string[]> ResolveUserPermissionsAsync(PortalType portal, long tenantId, long userId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询用户角色关系
|
||||
var relations = await userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken);
|
||||
var relations = await userRoleRepository.GetByUserIdAsync(portal, tenantId, userId, cancellationToken);
|
||||
var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray();
|
||||
if (roleIds.Length == 0)
|
||||
{
|
||||
@@ -73,7 +75,7 @@ public sealed class GetUserPermissionsQueryHandler(
|
||||
}
|
||||
|
||||
// 2. 查询角色-权限关系
|
||||
var rolePermissions = await rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken);
|
||||
var rolePermissions = await rolePermissionRepository.GetByRoleIdsAsync(portal, tenantId, roleIds, cancellationToken);
|
||||
var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray();
|
||||
if (permissionIds.Length == 0)
|
||||
{
|
||||
@@ -81,7 +83,7 @@ public sealed class GetUserPermissionsQueryHandler(
|
||||
}
|
||||
|
||||
// 3. 查询权限编码
|
||||
var permissions = await permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken);
|
||||
var permissions = await permissionRepository.GetByIdsAsync(permissionIds, cancellationToken);
|
||||
return permissions.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
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;
|
||||
|
||||
@@ -10,18 +10,17 @@ namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
/// 菜单列表查询处理器。
|
||||
/// </summary>
|
||||
public sealed class ListMenusQueryHandler(
|
||||
IMenuRepository menuRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
IMenuRepository menuRepository)
|
||||
: IRequestHandler<ListMenusQuery, IReadOnlyList<MenuDefinitionDto>>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<MenuDefinitionDto>> Handle(ListMenusQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 获取租户
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
// 1. 固定读取管理端菜单
|
||||
var portal = PortalType.Admin;
|
||||
|
||||
// 2. 查询列表
|
||||
var entities = await menuRepository.GetByTenantAsync(tenantId, cancellationToken);
|
||||
var entities = await menuRepository.GetByPortalAsync(portal, cancellationToken);
|
||||
|
||||
// 3. 映射 DTO
|
||||
var items = entities.Select(MenuMapper.ToDto).ToList();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
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;
|
||||
|
||||
@@ -10,18 +10,17 @@ namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
/// 菜单详情查询处理器。
|
||||
/// </summary>
|
||||
public sealed class MenuDetailQueryHandler(
|
||||
IMenuRepository menuRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
IMenuRepository menuRepository)
|
||||
: IRequestHandler<MenuDetailQuery, MenuDefinitionDto?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<MenuDefinitionDto?> Handle(MenuDetailQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 获取租户
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
// 1. 固定读取管理端菜单
|
||||
var portal = PortalType.Admin;
|
||||
|
||||
// 2. 查询实体
|
||||
var entity = await menuRepository.FindByIdAsync(request.Id, tenantId, cancellationToken);
|
||||
var entity = await menuRepository.FindByIdAsync(request.Id, portal, cancellationToken);
|
||||
if (entity is null)
|
||||
{
|
||||
return null;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
using TakeoutSaaS.Domain.Identity.Entities;
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
|
||||
@@ -79,23 +80,22 @@ internal static class MenuMapper
|
||||
/// 构建或更新菜单实体并返回 DTO。
|
||||
/// </summary>
|
||||
/// <param name="existing">已存在的菜单实体。</param>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="portal">Portal 类型。</param>
|
||||
/// <param name="name">菜单名称。</param>
|
||||
/// <param name="payload">菜单 DTO 载荷。</param>
|
||||
/// <returns>菜单定义 DTO。</returns>
|
||||
public static MenuDefinitionDto FromCommand(MenuDefinition? existing, long tenantId, string name, MenuDefinitionDto payload)
|
||||
public static MenuDefinitionDto FromCommand(MenuDefinition? existing, PortalType portal, string name, MenuDefinitionDto payload)
|
||||
{
|
||||
// 1. 构造实体
|
||||
var entity = existing ?? new MenuDefinition
|
||||
{
|
||||
TenantId = tenantId,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
Portal = portal
|
||||
};
|
||||
|
||||
// // 填充字段
|
||||
// 2. 填充字段
|
||||
FillEntity(entity, payload);
|
||||
|
||||
// 2. 返回 DTO 映射
|
||||
// 3. 返回 DTO 映射
|
||||
return ToDto(entity);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ using MediatR;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
using TakeoutSaaS.Application.Identity.Queries;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
|
||||
@@ -10,8 +9,7 @@ namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
/// 权限树查询处理器。
|
||||
/// </summary>
|
||||
public sealed class PermissionTreeQueryHandler(
|
||||
IPermissionRepository permissionRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
IPermissionRepository permissionRepository)
|
||||
: IRequestHandler<PermissionTreeQuery, IReadOnlyList<PermissionTreeDto>>
|
||||
{
|
||||
/// <summary>
|
||||
@@ -22,17 +20,16 @@ public sealed class PermissionTreeQueryHandler(
|
||||
/// <returns>权限树列表。</returns>
|
||||
public async Task<IReadOnlyList<PermissionTreeDto>> Handle(PermissionTreeQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 获取租户上下文并查询权限
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var permissions = await permissionRepository.SearchAsync(tenantId, request.Keyword, cancellationToken);
|
||||
// 1. 查询权限(按 Portal)
|
||||
var permissions = await permissionRepository.SearchAsync(request.Portal, request.Keyword, cancellationToken);
|
||||
|
||||
// 2. 构建节点映射与父子分组
|
||||
var nodeMap = permissions.ToDictionary(
|
||||
x => x.Id,
|
||||
x => new PermissionTreeDto
|
||||
{
|
||||
Portal = x.Portal,
|
||||
Id = x.Id,
|
||||
TenantId = x.TenantId,
|
||||
ParentId = x.ParentId,
|
||||
SortOrder = x.SortOrder,
|
||||
Type = x.Type,
|
||||
|
||||
@@ -47,8 +47,8 @@ public sealed class ResetAdminPasswordByTokenCommandHandler(
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "重置链接无效或已过期");
|
||||
}
|
||||
|
||||
// 3. 获取用户(可更新,忽略租户过滤器)并写入新密码哈希
|
||||
var user = await userRepository.GetForUpdateIgnoringTenantAsync(userId.Value, cancellationToken)
|
||||
// 3. 获取用户(可更新)并写入新密码哈希
|
||||
var user = await userRepository.GetForUpdateAsync(userId.Value, cancellationToken)
|
||||
?? throw new BusinessException(ErrorCodes.NotFound, "用户不存在");
|
||||
|
||||
user.PasswordHash = passwordHasher.HashPassword(user, password);
|
||||
|
||||
@@ -40,9 +40,7 @@ public sealed class ResetIdentityUserPasswordCommandHandler(
|
||||
}
|
||||
|
||||
// 3. 查询用户实体
|
||||
var user = isSuperAdmin
|
||||
? await identityUserRepository.GetForUpdateIgnoringTenantAsync(request.UserId, cancellationToken)
|
||||
: await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken);
|
||||
var user = await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken);
|
||||
if (user == null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "用户不存在");
|
||||
|
||||
@@ -37,7 +37,7 @@ public sealed class RestoreIdentityUserCommandHandler(
|
||||
}
|
||||
|
||||
// 3. 查询用户实体(包含已删除)
|
||||
var user = await identityUserRepository.GetForUpdateIncludingDeletedAsync(currentTenantId, request.UserId, isSuperAdmin, cancellationToken);
|
||||
var user = await identityUserRepository.GetForUpdateIncludingDeletedAsync(request.UserId, cancellationToken);
|
||||
if (user == null)
|
||||
{
|
||||
return false;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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;
|
||||
|
||||
@@ -19,29 +20,32 @@ public sealed class RoleDetailQueryHandler(
|
||||
/// <inheritdoc />
|
||||
public async Task<RoleDetailDto?> Handle(RoleDetailQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 获取租户上下文并查询角色
|
||||
// 1. 固定查询租户侧角色详情
|
||||
var portal = PortalType.Tenant;
|
||||
|
||||
// 2. 获取租户上下文并查询角色
|
||||
var tenantId = request.TenantId ?? tenantProvider.GetCurrentTenantId();
|
||||
var role = await roleRepository.FindByIdAsync(request.RoleId, tenantId, cancellationToken);
|
||||
var role = await roleRepository.FindByIdAsync(portal, tenantId, request.RoleId, cancellationToken);
|
||||
if (role is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. 查询角色权限关系
|
||||
var relations = await rolePermissionRepository.GetByRoleIdsAsync(tenantId, new[] { role.Id }, cancellationToken);
|
||||
// 3. 查询角色权限关系
|
||||
var relations = await rolePermissionRepository.GetByRoleIdsAsync(portal, tenantId, new[] { role.Id }, cancellationToken);
|
||||
var permissionIds = relations.Select(x => x.PermissionId).ToArray();
|
||||
|
||||
// 3. 拉取权限实体
|
||||
// 4. 拉取权限实体
|
||||
var permissions = permissionIds.Length == 0
|
||||
? Array.Empty<Domain.Identity.Entities.Permission>()
|
||||
: await permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken);
|
||||
: await permissionRepository.GetByIdsAsync(permissionIds, cancellationToken);
|
||||
|
||||
// 4. 映射 DTO
|
||||
// 5. 映射 DTO
|
||||
var permissionDtos = permissions
|
||||
.Select(x => new PermissionDto
|
||||
{
|
||||
Portal = x.Portal,
|
||||
Id = x.Id,
|
||||
TenantId = x.TenantId,
|
||||
ParentId = x.ParentId,
|
||||
SortOrder = x.SortOrder,
|
||||
Type = x.Type,
|
||||
@@ -54,6 +58,7 @@ public sealed class RoleDetailQueryHandler(
|
||||
return new RoleDetailDto
|
||||
{
|
||||
Id = role.Id,
|
||||
Portal = role.Portal,
|
||||
TenantId = role.TenantId,
|
||||
Name = role.Name,
|
||||
Code = role.Code,
|
||||
|
||||
@@ -58,7 +58,7 @@ public sealed class SearchIdentityUsersQueryHandler(
|
||||
};
|
||||
|
||||
// 4. 执行分页查询
|
||||
var (items, total) = await identityUserRepository.SearchPagedAsync(filter, isSuperAdmin, cancellationToken);
|
||||
var (items, total) = await identityUserRepository.SearchPagedAsync(filter, cancellationToken);
|
||||
if (items.Count == 0)
|
||||
{
|
||||
return new PagedResult<UserListItemDto>(Array.Empty<UserListItemDto>(), request.Page, request.PageSize, total);
|
||||
@@ -72,6 +72,7 @@ public sealed class SearchIdentityUsersQueryHandler(
|
||||
var dtos = items.Select(user => new UserListItemDto
|
||||
{
|
||||
UserId = user.Id,
|
||||
Portal = user.Portal,
|
||||
TenantId = user.TenantId,
|
||||
Account = user.Account,
|
||||
DisplayName = user.DisplayName,
|
||||
@@ -103,10 +104,11 @@ public sealed class SearchIdentityUsersQueryHandler(
|
||||
// 1. 预分配字典容量
|
||||
var result = new Dictionary<long, string[]>(users.Count);
|
||||
|
||||
// 2. 按租户分组,降低角色查询次数
|
||||
foreach (var group in users.GroupBy(user => user.TenantId))
|
||||
// 2. 按 Portal + TenantId 分组,降低角色查询次数
|
||||
foreach (var group in users.GroupBy(user => new { user.Portal, user.TenantId }))
|
||||
{
|
||||
var tenantId = group.Key;
|
||||
var portal = group.Key.Portal;
|
||||
var tenantId = group.Key.TenantId;
|
||||
var userIds = group.Select(user => user.Id).Distinct().ToArray();
|
||||
if (userIds.Length == 0)
|
||||
{
|
||||
@@ -114,7 +116,7 @@ public sealed class SearchIdentityUsersQueryHandler(
|
||||
}
|
||||
|
||||
// 3. 查询用户角色映射
|
||||
var relations = await userRoleRepository.GetByUserIdsAsync(tenantId, userIds, cancellationToken);
|
||||
var relations = await userRoleRepository.GetByUserIdsAsync(portal, tenantId, userIds, cancellationToken);
|
||||
if (relations.Count == 0)
|
||||
{
|
||||
continue;
|
||||
@@ -124,7 +126,7 @@ public sealed class SearchIdentityUsersQueryHandler(
|
||||
var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray();
|
||||
var roles = roleIds.Length == 0
|
||||
? Array.Empty<Role>()
|
||||
: await roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken);
|
||||
: await roleRepository.GetByIdsAsync(portal, tenantId, roleIds, cancellationToken);
|
||||
var roleCodeMap = roles.ToDictionary(role => role.Id, role => role.Code, EqualityComparer<long>.Default);
|
||||
|
||||
// 5. 组装用户角色编码列表
|
||||
|
||||
@@ -3,7 +3,6 @@ using TakeoutSaaS.Application.Identity.Contracts;
|
||||
using TakeoutSaaS.Application.Identity.Queries;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
|
||||
@@ -11,8 +10,7 @@ namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
/// 权限分页查询处理器。
|
||||
/// </summary>
|
||||
public sealed class SearchPermissionsQueryHandler(
|
||||
IPermissionRepository permissionRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
IPermissionRepository permissionRepository)
|
||||
: IRequestHandler<SearchPermissionsQuery, PagedResult<PermissionDto>>
|
||||
{
|
||||
/// <summary>
|
||||
@@ -23,9 +21,8 @@ public sealed class SearchPermissionsQueryHandler(
|
||||
/// <returns>分页结果。</returns>
|
||||
public async Task<PagedResult<PermissionDto>> Handle(SearchPermissionsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 获取租户上下文并查询权限
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var permissions = await permissionRepository.SearchAsync(tenantId, request.Keyword, cancellationToken);
|
||||
// 1. 查询权限(按 Portal)
|
||||
var permissions = await permissionRepository.SearchAsync(request.Portal, request.Keyword, cancellationToken);
|
||||
|
||||
// 2. 排序
|
||||
var sorted = request.SortBy?.ToLowerInvariant() switch
|
||||
@@ -59,8 +56,8 @@ public sealed class SearchPermissionsQueryHandler(
|
||||
// 4. 映射 DTO
|
||||
var items = paged.Select(permission => new PermissionDto
|
||||
{
|
||||
Portal = permission.Portal,
|
||||
Id = permission.Id,
|
||||
TenantId = permission.TenantId,
|
||||
ParentId = permission.ParentId,
|
||||
SortOrder = permission.SortOrder,
|
||||
Type = permission.Type,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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;
|
||||
@@ -23,11 +24,14 @@ public sealed class SearchRolesQueryHandler(
|
||||
/// <returns>分页结果。</returns>
|
||||
public async Task<PagedResult<RoleDto>> Handle(SearchRolesQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 获取租户上下文并查询角色
|
||||
var tenantId = request.TenantId ?? tenantProvider.GetCurrentTenantId();
|
||||
var roles = await roleRepository.SearchAsync(tenantId, request.Keyword, cancellationToken);
|
||||
// 1. 固定查询租户侧角色
|
||||
var portal = PortalType.Tenant;
|
||||
|
||||
// 2. 排序
|
||||
// 2. 获取租户上下文并查询角色
|
||||
var tenantId = request.TenantId ?? tenantProvider.GetCurrentTenantId();
|
||||
var roles = await roleRepository.SearchAsync(portal, tenantId, request.Keyword, cancellationToken);
|
||||
|
||||
// 3. 排序
|
||||
var sorted = request.SortBy?.ToLowerInvariant() switch
|
||||
{
|
||||
"name" => request.SortDescending
|
||||
@@ -38,23 +42,24 @@ public sealed class SearchRolesQueryHandler(
|
||||
: roles.OrderBy(x => x.CreatedAt)
|
||||
};
|
||||
|
||||
// 3. 分页
|
||||
// 4. 分页
|
||||
var paged = sorted
|
||||
.Skip((request.Page - 1) * request.PageSize)
|
||||
.Take(request.PageSize)
|
||||
.ToList();
|
||||
|
||||
// 4. 映射 DTO
|
||||
// 5. 映射 DTO
|
||||
var items = paged.Select(role => new RoleDto
|
||||
{
|
||||
Id = role.Id,
|
||||
Portal = role.Portal,
|
||||
TenantId = role.TenantId,
|
||||
Name = role.Name,
|
||||
Code = role.Code,
|
||||
Description = role.Description
|
||||
}).ToList();
|
||||
|
||||
// 5. 返回分页结果
|
||||
// 6. 返回分页结果
|
||||
return new PagedResult<RoleDto>(items, request.Page, request.PageSize, roles.Count);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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;
|
||||
@@ -23,6 +24,7 @@ public sealed class SearchUserPermissionsQueryHandler(
|
||||
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);
|
||||
|
||||
@@ -34,7 +36,7 @@ public sealed class SearchUserPermissionsQueryHandler(
|
||||
.ToList();
|
||||
|
||||
// 3. 解析角色与权限
|
||||
var resolved = await ResolveRolesAndPermissionsAsync(tenantId, paged, cancellationToken);
|
||||
var resolved = await ResolveRolesAndPermissionsAsync(portal, tenantId, paged, cancellationToken);
|
||||
var items = paged.Select(user => new UserPermissionDto
|
||||
{
|
||||
UserId = user.Id,
|
||||
@@ -70,31 +72,32 @@ public sealed class SearchUserPermissionsQueryHandler(
|
||||
}
|
||||
|
||||
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(tenantId, userIds, cancellationToken);
|
||||
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(tenantId, roleIds, cancellationToken);
|
||||
: 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(tenantId, roleIds, cancellationToken);
|
||||
: 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(tenantId, permissionIds, cancellationToken);
|
||||
: await permissionRepository.GetByIdsAsync(permissionIds, cancellationToken);
|
||||
var permissionCodeMap = permissions.ToDictionary(p => p.Id, p => p.Code, comparer: EqualityComparer<long>.Default);
|
||||
|
||||
var rolePermissionsLookup = rolePermissions
|
||||
|
||||
@@ -6,6 +6,7 @@ using TakeoutSaaS.Application.Identity.Contracts;
|
||||
using TakeoutSaaS.Application.Identity.Events;
|
||||
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;
|
||||
@@ -43,9 +44,7 @@ public sealed class UpdateIdentityUserCommandHandler(
|
||||
}
|
||||
|
||||
// 3. 获取用户实体
|
||||
var user = isSuperAdmin
|
||||
? await identityUserRepository.GetForUpdateIgnoringTenantAsync(request.UserId, cancellationToken)
|
||||
: await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken);
|
||||
var user = await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken);
|
||||
if (user == null)
|
||||
{
|
||||
return null;
|
||||
@@ -57,28 +56,37 @@ public sealed class UpdateIdentityUserCommandHandler(
|
||||
}
|
||||
|
||||
// 4. 规范化输入并校验唯一性
|
||||
var portal = user.Portal;
|
||||
var tenantId = user.TenantId;
|
||||
if (portal == PortalType.Tenant && (!tenantId.HasValue || tenantId.Value == 0))
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.InternalServerError, "用户缺少有效的租户标识");
|
||||
}
|
||||
|
||||
var displayName = request.DisplayName.Trim();
|
||||
var phone = string.IsNullOrWhiteSpace(request.Phone) ? null : request.Phone.Trim();
|
||||
var email = string.IsNullOrWhiteSpace(request.Email) ? null : request.Email.Trim();
|
||||
var roleIds = request.RoleIds == null ? null : ParseIds(request.RoleIds, "角色");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(phone)
|
||||
if (portal == PortalType.Tenant
|
||||
&& !string.IsNullOrWhiteSpace(phone)
|
||||
&& !string.Equals(phone, user.Phone, StringComparison.OrdinalIgnoreCase)
|
||||
&& await identityUserRepository.ExistsByPhoneAsync(user.TenantId, phone, user.Id, cancellationToken))
|
||||
&& await identityUserRepository.ExistsByPhoneAsync(tenantId!.Value, phone, user.Id, cancellationToken))
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "手机号已存在");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(email)
|
||||
if (portal == PortalType.Tenant
|
||||
&& !string.IsNullOrWhiteSpace(email)
|
||||
&& !string.Equals(email, user.Email, StringComparison.OrdinalIgnoreCase)
|
||||
&& await identityUserRepository.ExistsByEmailAsync(user.TenantId, email, user.Id, cancellationToken))
|
||||
&& await identityUserRepository.ExistsByEmailAsync(tenantId!.Value, email, user.Id, cancellationToken))
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "邮箱已存在");
|
||||
}
|
||||
|
||||
if (roleIds is { Length: > 0 })
|
||||
{
|
||||
var roles = await roleRepository.GetByIdsAsync(user.TenantId, roleIds, cancellationToken);
|
||||
var roles = await roleRepository.GetByIdsAsync(portal, tenantId, roleIds, cancellationToken);
|
||||
if (roles.Count != roleIds.Length)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "角色列表包含无效项");
|
||||
@@ -134,7 +142,7 @@ public sealed class UpdateIdentityUserCommandHandler(
|
||||
// 8. 覆盖角色绑定(仅当显式传入时)
|
||||
if (roleIds != null)
|
||||
{
|
||||
await userRoleRepository.ReplaceUserRolesAsync(user.TenantId, user.Id, roleIds, cancellationToken);
|
||||
await userRoleRepository.ReplaceUserRolesAsync(portal, tenantId, user.Id, roleIds, cancellationToken);
|
||||
}
|
||||
|
||||
// 9. 返回用户详情
|
||||
|
||||
@@ -2,10 +2,10 @@ using MediatR;
|
||||
using TakeoutSaaS.Application.Identity;
|
||||
using TakeoutSaaS.Application.Identity.Commands;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
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;
|
||||
|
||||
@@ -13,8 +13,7 @@ namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
/// 更新菜单处理器。
|
||||
/// </summary>
|
||||
public sealed class UpdateMenuCommandHandler(
|
||||
IMenuRepository menuRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
IMenuRepository menuRepository)
|
||||
: IRequestHandler<UpdateMenuCommand, MenuDefinitionDto?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
@@ -26,9 +25,9 @@ public sealed class UpdateMenuCommandHandler(
|
||||
throw new BusinessException(ErrorCodes.Forbidden, "菜单已固定,禁止修改");
|
||||
}
|
||||
|
||||
// 2. 校验存在
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var entity = await menuRepository.FindByIdAsync(request.Id, tenantId, cancellationToken)
|
||||
// 2. 校验存在(仅维护管理端菜单)
|
||||
var portal = PortalType.Admin;
|
||||
var entity = await menuRepository.FindByIdAsync(request.Id, portal, cancellationToken)
|
||||
?? throw new BusinessException(ErrorCodes.NotFound, "菜单不存在");
|
||||
|
||||
// 3. 更新字段
|
||||
|
||||
@@ -4,7 +4,6 @@ using TakeoutSaaS.Application.Identity.Contracts;
|
||||
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;
|
||||
|
||||
@@ -12,8 +11,7 @@ namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||
/// 更新权限处理器。
|
||||
/// </summary>
|
||||
public sealed class UpdatePermissionCommandHandler(
|
||||
IPermissionRepository permissionRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
IPermissionRepository permissionRepository)
|
||||
: IRequestHandler<UpdatePermissionCommand, PermissionDto?>
|
||||
{
|
||||
/// <summary>
|
||||
@@ -30,9 +28,8 @@ public sealed class UpdatePermissionCommandHandler(
|
||||
throw new BusinessException(ErrorCodes.Forbidden, "权限已固定,禁止修改");
|
||||
}
|
||||
|
||||
// 2. 获取租户上下文并查询权限
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var permission = await permissionRepository.FindByIdAsync(request.PermissionId, tenantId, cancellationToken);
|
||||
// 2. 查询权限
|
||||
var permission = await permissionRepository.FindByIdAsync(request.PermissionId, cancellationToken);
|
||||
if (permission == null)
|
||||
{
|
||||
return null;
|
||||
@@ -60,8 +57,8 @@ public sealed class UpdatePermissionCommandHandler(
|
||||
// 5. 返回 DTO
|
||||
return new PermissionDto
|
||||
{
|
||||
Portal = permission.Portal,
|
||||
Id = permission.Id,
|
||||
TenantId = permission.TenantId,
|
||||
ParentId = permission.ParentId,
|
||||
SortOrder = permission.SortOrder,
|
||||
Type = permission.Type,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using MediatR;
|
||||
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;
|
||||
|
||||
@@ -22,26 +23,30 @@ public sealed class UpdateRoleCommandHandler(
|
||||
/// <returns>更新后的角色 DTO 或 null。</returns>
|
||||
public async Task<RoleDto?> Handle(UpdateRoleCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 获取租户上下文并查询角色
|
||||
// 1. 固定更新租户侧角色
|
||||
var portal = PortalType.Tenant;
|
||||
|
||||
// 2. 获取租户上下文并查询角色
|
||||
var tenantId = request.TenantId ?? tenantProvider.GetCurrentTenantId();
|
||||
var role = await roleRepository.FindByIdAsync(request.RoleId, tenantId, cancellationToken);
|
||||
var role = await roleRepository.FindByIdAsync(portal, tenantId, request.RoleId, cancellationToken);
|
||||
if (role == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. 更新字段
|
||||
// 3. 更新字段
|
||||
role.Name = request.Name;
|
||||
role.Description = request.Description;
|
||||
|
||||
// 3. 持久化
|
||||
// 4. 持久化
|
||||
await roleRepository.UpdateAsync(role, cancellationToken);
|
||||
await roleRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 4. 返回 DTO
|
||||
// 5. 返回 DTO
|
||||
return new RoleDto
|
||||
{
|
||||
Id = role.Id,
|
||||
Portal = role.Portal,
|
||||
TenantId = role.TenantId,
|
||||
Name = role.Name,
|
||||
Code = role.Code,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Queries;
|
||||
|
||||
@@ -8,6 +9,11 @@ namespace TakeoutSaaS.Application.Identity.Queries;
|
||||
/// </summary>
|
||||
public sealed class PermissionTreeQuery : IRequest<IReadOnlyList<PermissionTreeDto>>
|
||||
{
|
||||
/// <summary>
|
||||
/// Portal 类型。
|
||||
/// </summary>
|
||||
public PortalType Portal { get; init; } = PortalType.Admin;
|
||||
|
||||
/// <summary>
|
||||
/// 关键字(可选)。
|
||||
/// </summary>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Queries;
|
||||
@@ -9,6 +10,11 @@ namespace TakeoutSaaS.Application.Identity.Queries;
|
||||
/// </summary>
|
||||
public sealed class SearchPermissionsQuery : IRequest<PagedResult<PermissionDto>>
|
||||
{
|
||||
/// <summary>
|
||||
/// Portal 类型。
|
||||
/// </summary>
|
||||
public PortalType Portal { get; init; } = PortalType.Admin;
|
||||
|
||||
/// <summary>
|
||||
/// 搜索关键字。
|
||||
/// </summary>
|
||||
|
||||
@@ -197,8 +197,7 @@ public sealed class AdminAuthService(
|
||||
// 1. 读取档案以获取权限
|
||||
var profile = await GetProfileAsync(userId, cancellationToken);
|
||||
// 2. 读取菜单定义
|
||||
var tenantId = _tenantProvider.GetCurrentTenantId();
|
||||
var definitions = await _menuRepository.GetByTenantAsync(tenantId, cancellationToken);
|
||||
var definitions = await _menuRepository.GetByPortalAsync(profile.Portal, cancellationToken);
|
||||
|
||||
// 3. 生成菜单树
|
||||
var menu = BuildMenuTree(definitions, profile.Permissions);
|
||||
@@ -217,8 +216,8 @@ public sealed class AdminAuthService(
|
||||
return null;
|
||||
}
|
||||
|
||||
var roleCodes = await ResolveUserRolesAsync(tenantId, user.Id, cancellationToken);
|
||||
var permissionCodes = await ResolveUserPermissionsAsync(tenantId, user.Id, cancellationToken);
|
||||
var roleCodes = await ResolveUserRolesAsync(user.Portal, user.TenantId, user.Id, cancellationToken);
|
||||
var permissionCodes = await ResolveUserPermissionsAsync(user.Portal, user.TenantId, user.Id, cancellationToken);
|
||||
|
||||
return new UserPermissionDto
|
||||
{
|
||||
@@ -265,7 +264,7 @@ public sealed class AdminAuthService(
|
||||
.Take(pageSize)
|
||||
.ToList();
|
||||
|
||||
var resolved = await ResolveRolesAndPermissionsAsync(tenantId, paged, cancellationToken);
|
||||
var resolved = await ResolveRolesAndPermissionsAsync(PortalType.Tenant, tenantId, paged, cancellationToken);
|
||||
var items = paged.Select(user => new UserPermissionDto
|
||||
{
|
||||
UserId = user.Id,
|
||||
@@ -283,12 +282,12 @@ public sealed class AdminAuthService(
|
||||
|
||||
private async Task<CurrentUserProfile> BuildProfileAsync(IdentityUser user, CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = user.TenantId;
|
||||
var roles = await ResolveUserRolesAsync(tenantId, user.Id, cancellationToken);
|
||||
var permissions = await ResolveUserPermissionsAsync(tenantId, user.Id, cancellationToken);
|
||||
var roles = await ResolveUserRolesAsync(user.Portal, user.TenantId, user.Id, cancellationToken);
|
||||
var permissions = await ResolveUserPermissionsAsync(user.Portal, user.TenantId, user.Id, cancellationToken);
|
||||
|
||||
return new CurrentUserProfile
|
||||
{
|
||||
Portal = user.Portal,
|
||||
UserId = user.Id,
|
||||
Account = user.Account,
|
||||
DisplayName = user.DisplayName,
|
||||
@@ -493,61 +492,62 @@ public sealed class AdminAuthService(
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private async Task<string[]> ResolveUserRolesAsync(long tenantId, long userId, CancellationToken cancellationToken)
|
||||
private async Task<string[]> ResolveUserRolesAsync(PortalType portal, long? tenantId, long userId, CancellationToken cancellationToken)
|
||||
{
|
||||
var relations = await _userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken);
|
||||
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>();
|
||||
}
|
||||
|
||||
var roles = await _roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken);
|
||||
var roles = await _roleRepository.GetByIdsAsync(portal, tenantId, roleIds, cancellationToken);
|
||||
return roles.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
}
|
||||
|
||||
private async Task<string[]> ResolveUserPermissionsAsync(long tenantId, long userId, CancellationToken cancellationToken)
|
||||
private async Task<string[]> ResolveUserPermissionsAsync(PortalType portal, long? tenantId, long userId, CancellationToken cancellationToken)
|
||||
{
|
||||
var relations = await _userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken);
|
||||
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>();
|
||||
}
|
||||
|
||||
var rolePermissions = await _rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken);
|
||||
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>();
|
||||
}
|
||||
|
||||
var permissions = await _permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken);
|
||||
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(
|
||||
long tenantId,
|
||||
PortalType portal,
|
||||
long? tenantId,
|
||||
IReadOnlyCollection<IdentityUser> users,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var userIds = users.Select(x => x.Id).ToArray();
|
||||
var userRoleRelations = await _userRoleRepository.GetByUserIdsAsync(tenantId, userIds, cancellationToken);
|
||||
var userRoleRelations = await _userRoleRepository.GetByUserIdsAsync(portal, tenantId, userIds, cancellationToken);
|
||||
var roleIds = userRoleRelations.Select(x => x.RoleId).Distinct().ToArray();
|
||||
|
||||
var roles = roleIds.Length == 0
|
||||
? Array.Empty<Role>()
|
||||
: await _roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken);
|
||||
: await _roleRepository.GetByIdsAsync(portal, tenantId, roleIds, cancellationToken);
|
||||
var roleCodeMap = roles.ToDictionary(r => r.Id, r => r.Code, comparer: EqualityComparer<long>.Default);
|
||||
|
||||
var rolePermissions = roleIds.Length == 0
|
||||
? Array.Empty<RolePermission>()
|
||||
: await _rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken);
|
||||
: await _rolePermissionRepository.GetByRoleIdsAsync(portal, tenantId, roleIds, cancellationToken);
|
||||
|
||||
var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray();
|
||||
var permissions = permissionIds.Length == 0
|
||||
? Array.Empty<Permission>()
|
||||
: await _permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken);
|
||||
: await _permissionRepository.GetByIdsAsync(permissionIds, cancellationToken);
|
||||
var permissionCodeMap = permissions.ToDictionary(p => p.Id, p => p.Code, comparer: EqualityComparer<long>.Default);
|
||||
|
||||
var rolePermissionsLookup = rolePermissions
|
||||
|
||||
Reference in New Issue
Block a user