refactor: 将 Permission 和 MenuDefinition 改为系统级实体

- Permission 和 MenuDefinition 改为继承 AuditableEntityBase(移除 TenantId)
- 添加 PortalType 枚举区分平台端/租户端
- Repository 使用 IgnoreQueryFilters() 查询系统级数据
- 更新所有相关 Handler 和 DTO,移除 TenantId 引用
- 与 AdminApi 保持一致的设计

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
MSuMshk
2026-02-03 14:49:27 +08:00
parent e88c41c11e
commit 5a26f82628
22 changed files with 108 additions and 110 deletions

View File

@@ -14,12 +14,6 @@ public sealed class PermissionDto
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
public long Id { get; init; }
/// <summary>
/// 租户 ID固定权限时为基准租户
/// </summary>
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
public long TenantId { get; init; }
/// <summary>
/// 父级权限 ID。
/// </summary>

View File

@@ -14,12 +14,6 @@ public sealed record PermissionTreeDto
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
public long Id { get; init; }
/// <summary>
/// 租户 ID雪花序列化为字符串
/// </summary>
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
public long TenantId { get; init; }
/// <summary>
/// 父级权限 ID。
/// </summary>
@@ -42,7 +36,7 @@ public sealed record PermissionTreeDto
public string Name { get; init; } = string.Empty;
/// <summary>
/// 权限编码(租户内唯一)。
/// 权限编码(全局唯一)。
/// </summary>
public string Code { get; init; } = string.Empty;

View File

@@ -81,7 +81,6 @@ public sealed class CopyRoleTemplateCommandHandler(
var permission = new Permission
{
TenantId = tenantId,
Name = code,
Code = code,
Description = code

View File

@@ -3,19 +3,17 @@ 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;
/// <summary>
/// 创建菜单处理器。
/// </summary>
public sealed class CreateMenuCommandHandler(
IMenuRepository menuRepository,
ITenantProvider tenantProvider)
public sealed class CreateMenuCommandHandler(IMenuRepository menuRepository)
: IRequestHandler<CreateMenuCommand, MenuDefinitionDto>
{
/// <inheritdoc />
@@ -28,10 +26,9 @@ public sealed class CreateMenuCommandHandler(
}
// 2. 构造实体
var tenantId = tenantProvider.GetCurrentTenantId();
var entity = new MenuDefinition
{
TenantId = tenantId,
Portal = PortalType.Tenant,
ParentId = request.ParentId,
Name = request.Name.Trim(),
Path = request.Path.Trim(),

View File

@@ -43,7 +43,6 @@ public sealed class CreatePermissionCommandHandler(
var sortOrder = request.SortOrder < 0 ? 0 : request.SortOrder;
var permission = new Permission
{
TenantId = tenantId,
ParentId = parentId,
SortOrder = sortOrder,
Type = normalizedType,
@@ -60,7 +59,6 @@ public sealed class CreatePermissionCommandHandler(
return new PermissionDto
{
Id = permission.Id,
TenantId = permission.TenantId,
ParentId = permission.ParentId,
SortOrder = permission.SortOrder,
Type = permission.Type,

View File

@@ -4,16 +4,13 @@ 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;
/// <summary>
/// 删除菜单处理器。
/// </summary>
public sealed class DeleteMenuCommandHandler(
IMenuRepository menuRepository,
ITenantProvider tenantProvider)
public sealed class DeleteMenuCommandHandler(IMenuRepository menuRepository)
: IRequestHandler<DeleteMenuCommand, bool>
{
/// <inheritdoc />
@@ -25,9 +22,8 @@ public sealed class DeleteMenuCommandHandler(
throw new BusinessException(ErrorCodes.Forbidden, "菜单已固定,禁止删除");
}
// 2. 删除目标及可能的孤儿由外层保证
var tenantId = tenantProvider.GetCurrentTenantId();
await menuRepository.DeleteAsync(request.Id, tenantId, cancellationToken);
// 2. 删除目标
await menuRepository.DeleteAsync(request.Id, cancellationToken);
// 3. 持久化
await menuRepository.SaveChangesAsync(cancellationToken);

View File

@@ -1,32 +1,27 @@
using MediatR;
using TakeoutSaaS.Application.Identity.Contracts;
using TakeoutSaaS.Application.Identity.Queries;
using TakeoutSaaS.Domain.Identity.Enums;
using TakeoutSaaS.Domain.Identity.Repositories;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.Identity.Handlers;
/// <summary>
/// 菜单列表查询处理器。
/// </summary>
public sealed class ListMenusQueryHandler(
IMenuRepository menuRepository,
ITenantProvider tenantProvider)
public sealed class ListMenusQueryHandler(IMenuRepository menuRepository)
: IRequestHandler<ListMenusQuery, IReadOnlyList<MenuDefinitionDto>>
{
/// <inheritdoc />
public async Task<IReadOnlyList<MenuDefinitionDto>> Handle(ListMenusQuery request, CancellationToken cancellationToken)
{
// 1. 获取租户
var tenantId = tenantProvider.GetCurrentTenantId();
// 1. 查询租户端菜单
var entities = await menuRepository.GetByPortalAsync(PortalType.Tenant, cancellationToken);
// 2. 查询列表
var entities = await menuRepository.GetByTenantAsync(tenantId, cancellationToken);
// 3. 映射 DTO
// 2. 映射 DTO
var items = entities.Select(MenuMapper.ToDto).ToList();
// 4. 返回结果
// 3. 返回结果
return items;
}
}

View File

@@ -2,32 +2,26 @@ 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;
/// <summary>
/// 菜单详情查询处理器。
/// </summary>
public sealed class MenuDetailQueryHandler(
IMenuRepository menuRepository,
ITenantProvider tenantProvider)
public sealed class MenuDetailQueryHandler(IMenuRepository menuRepository)
: IRequestHandler<MenuDetailQuery, MenuDefinitionDto?>
{
/// <inheritdoc />
public async Task<MenuDefinitionDto?> Handle(MenuDetailQuery request, CancellationToken cancellationToken)
{
// 1. 获取租户
var tenantId = tenantProvider.GetCurrentTenantId();
// 2. 查询实体
var entity = await menuRepository.FindByIdAsync(request.Id, tenantId, cancellationToken);
// 1. 查询实体
var entity = await menuRepository.FindByIdAsync(request.Id, cancellationToken);
if (entity is null)
{
return null;
}
// 3. 映射并返回
// 2. 映射并返回
return MenuMapper.ToDto(entity);
}
}

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="name">菜单名称。</param>
/// <param name="portal">门户类型。</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, MenuDefinitionDto payload)
{
// 1. 构造实体
var entity = existing ?? new MenuDefinition
{
TenantId = tenantId,
Portal = portal,
CreatedAt = DateTime.UtcNow
};
// // 填充字段
// 2. 填充字段
FillEntity(entity, payload);
// 2. 返回 DTO 映射
// 3. 返回 DTO 映射
return ToDto(entity);
}

View File

@@ -32,7 +32,6 @@ public sealed class PermissionTreeQueryHandler(
x => new PermissionTreeDto
{
Id = x.Id,
TenantId = x.TenantId,
ParentId = x.ParentId,
SortOrder = x.SortOrder,
Type = x.Type,

View File

@@ -56,7 +56,6 @@ public sealed class RoleDetailQueryHandler(
.Select(x => new PermissionDto
{
Id = x.Id,
TenantId = x.TenantId,
ParentId = x.ParentId,
SortOrder = x.SortOrder,
Type = x.Type,

View File

@@ -60,7 +60,6 @@ public sealed class SearchPermissionsQueryHandler(
var items = paged.Select(permission => new PermissionDto
{
Id = permission.Id,
TenantId = permission.TenantId,
ParentId = permission.ParentId,
SortOrder = permission.SortOrder,
Type = permission.Type,

View File

@@ -5,16 +5,13 @@ 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;
/// <summary>
/// 更新菜单处理器。
/// </summary>
public sealed class UpdateMenuCommandHandler(
IMenuRepository menuRepository,
ITenantProvider tenantProvider)
public sealed class UpdateMenuCommandHandler(IMenuRepository menuRepository)
: IRequestHandler<UpdateMenuCommand, MenuDefinitionDto?>
{
/// <inheritdoc />
@@ -27,8 +24,7 @@ public sealed class UpdateMenuCommandHandler(
}
// 2. 校验存在
var tenantId = tenantProvider.GetCurrentTenantId();
var entity = await menuRepository.FindByIdAsync(request.Id, tenantId, cancellationToken)
var entity = await menuRepository.FindByIdAsync(request.Id, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, "菜单不存在");
// 3. 更新字段

View File

@@ -61,7 +61,6 @@ public sealed class UpdatePermissionCommandHandler(
return new PermissionDto
{
Id = permission.Id,
TenantId = permission.TenantId,
ParentId = permission.ParentId,
SortOrder = permission.SortOrder,
Type = permission.Type,

View File

@@ -142,9 +142,9 @@ public sealed class AdminAuthService(
{
// 1. 读取档案以获取权限
var profile = await GetProfileAsync(userId, cancellationToken);
// 2. 读取菜单定义
var tenantId = tenantProvider.GetCurrentTenantId();
var definitions = await menuRepository.GetByTenantAsync(tenantId, cancellationToken);
// 2. 读取租户端菜单定义
var definitions = await menuRepository.GetByPortalAsync(PortalType.Tenant, cancellationToken);
// 3. 生成菜单树
var menu = BuildMenuTree(definitions, profile.Permissions);