refactor: 管理端去租户过滤并Portal化RBAC菜单

This commit is contained in:
2026-01-29 10:46:49 +00:00
parent ea9c20d8a9
commit b3639ff34b
115 changed files with 1106 additions and 1092 deletions

View File

@@ -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平台管理员为空

View File

@@ -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>

View File

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

View File

@@ -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。
/// 租户 IDPortal=Tenant 必填Portal=Admin 为空)
/// </summary>
public long TenantId { get; init; }
public long? TenantId { get; init; }
/// <summary>
/// 角色名称。

View File

@@ -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。
/// 租户 IDPortal=Tenant 必填Portal=Admin 为空)
/// </summary>
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
public long TenantId { get; init; }
[JsonConverter(typeof(NullableSnowflakeIdJsonConverter))]
public long? TenantId { get; init; }
/// <summary>
/// 角色名称。

View File

@@ -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。
/// 租户 IDPortal=Tenant 必填Portal=Admin 为空)
/// </summary>
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
public long TenantId { get; init; }
[JsonConverter(typeof(NullableSnowflakeIdJsonConverter))]
public long? TenantId { get; init; }
/// <summary>
/// 商户 ID。

View File

@@ -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。
/// 租户 IDPortal=Tenant 必填Portal=Admin 为空)
/// </summary>
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
public long TenantId { get; init; }
[JsonConverter(typeof(NullableSnowflakeIdJsonConverter))]
public long? TenantId { get; init; }
/// <summary>
/// 登录账号。

View File

@@ -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雪花序列化为字符串可空

View File

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

View File

@@ -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. 组装用户角色编码列表

View File

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

View File

@@ -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, "至少保留一个管理员");

View File

@@ -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,

View File

@@ -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,

View File

@@ -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(),

View File

@@ -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,

View File

@@ -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,

View File

@@ -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, "至少保留一个管理员");

View File

@@ -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);

View File

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

View File

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

View File

@@ -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,

View File

@@ -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();
}
}

View File

@@ -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();

View File

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

View File

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

View File

@@ -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,

View File

@@ -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);

View File

@@ -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, "用户不存在");

View File

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

View File

@@ -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,

View File

@@ -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. 组装用户角色编码列表

View File

@@ -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,

View File

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

View File

@@ -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

View File

@@ -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. 返回用户详情

View File

@@ -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. 更新字段

View File

@@ -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,

View File

@@ -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,

View File

@@ -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>

View File

@@ -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>

View File

@@ -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