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:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -81,7 +81,6 @@ public sealed class CopyRoleTemplateCommandHandler(
|
||||
|
||||
var permission = new Permission
|
||||
{
|
||||
TenantId = tenantId,
|
||||
Name = code,
|
||||
Code = code,
|
||||
Description = code
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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. 更新字段
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Identity.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 管理端菜单定义。
|
||||
/// 管理端菜单定义(系统级数据,不按租户隔离)。
|
||||
/// </summary>
|
||||
public sealed class MenuDefinition : MultiTenantEntityBase
|
||||
public sealed class MenuDefinition : AuditableEntityBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 门户类型(Admin=平台端,Tenant=租户端)。
|
||||
/// </summary>
|
||||
public PortalType Portal { get; set; }
|
||||
/// <summary>
|
||||
/// 父级菜单 ID,根节点为 0。
|
||||
/// </summary>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Identity.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 权限定义。
|
||||
/// 权限定义(系统级数据,不按租户隔离)。
|
||||
/// </summary>
|
||||
public sealed class Permission : MultiTenantEntityBase
|
||||
public sealed class Permission : AuditableEntityBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 父级权限 ID,根节点为 0。
|
||||
@@ -36,4 +37,9 @@ public sealed class Permission : MultiTenantEntityBase
|
||||
/// 描述。
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 门户类型(Admin=平台端,Tenant=租户端)。
|
||||
/// </summary>
|
||||
public PortalType Portal { get; set; }
|
||||
}
|
||||
|
||||
17
src/Domain/TakeoutSaaS.Domain/Identity/Enums/PortalType.cs
Normal file
17
src/Domain/TakeoutSaaS.Domain/Identity/Enums/PortalType.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace TakeoutSaaS.Domain.Identity.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// 后台端类型(用于区分平台管理端与租户管理端)。
|
||||
/// </summary>
|
||||
public enum PortalType
|
||||
{
|
||||
/// <summary>
|
||||
/// 平台管理端(Admin)。
|
||||
/// </summary>
|
||||
Admin = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 租户管理端(Tenant)。
|
||||
/// </summary>
|
||||
Tenant = 1
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using TakeoutSaaS.Domain.Identity.Entities;
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Identity.Repositories;
|
||||
|
||||
@@ -8,14 +9,14 @@ namespace TakeoutSaaS.Domain.Identity.Repositories;
|
||||
public interface IMenuRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// 按租户获取菜单列表。
|
||||
/// 按门户类型获取菜单列表。
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<MenuDefinition>> GetByTenantAsync(long tenantId, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<MenuDefinition>> GetByPortalAsync(PortalType portal, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 根据 ID 查询菜单。
|
||||
/// </summary>
|
||||
Task<MenuDefinition?> FindByIdAsync(long id, long tenantId, CancellationToken cancellationToken = default);
|
||||
Task<MenuDefinition?> FindByIdAsync(long id, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 新增菜单。
|
||||
@@ -30,7 +31,7 @@ public interface IMenuRepository
|
||||
/// <summary>
|
||||
/// 删除菜单。
|
||||
/// </summary>
|
||||
Task DeleteAsync(long id, long tenantId, CancellationToken cancellationToken = default);
|
||||
Task DeleteAsync(long id, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 持久化变更。
|
||||
|
||||
@@ -7,36 +7,41 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
|
||||
/// <summary>
|
||||
/// EF 权限仓储。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 权限是系统级数据,使用 IgnoreQueryFilters 忽略多租户过滤。
|
||||
/// </remarks>
|
||||
public sealed class EfPermissionRepository(IdentityDbContext dbContext) : IPermissionRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// 根据权限 ID 获取权限。
|
||||
/// </summary>
|
||||
/// <param name="permissionId">权限 ID。</param>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="tenantId">租户 ID(保留参数,实际不使用)。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>权限实体或 null。</returns>
|
||||
public Task<Permission?> FindByIdAsync(long permissionId, long tenantId, CancellationToken cancellationToken = default)
|
||||
=> dbContext.Permissions
|
||||
.IgnoreQueryFilters()
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.Id == permissionId && x.TenantId == tenantId && x.DeletedAt == null, cancellationToken);
|
||||
.FirstOrDefaultAsync(x => x.Id == permissionId && x.DeletedAt == null, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// 根据权限编码获取权限。
|
||||
/// </summary>
|
||||
/// <param name="code">权限编码。</param>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="tenantId">租户 ID(保留参数,实际不使用)。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>权限实体或 null。</returns>
|
||||
public Task<Permission?> FindByCodeAsync(string code, long tenantId, CancellationToken cancellationToken = default)
|
||||
=> dbContext.Permissions
|
||||
.IgnoreQueryFilters()
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.Code == code && x.TenantId == tenantId && x.DeletedAt == null, cancellationToken);
|
||||
.FirstOrDefaultAsync(x => x.Code == code && x.DeletedAt == null, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// 根据权限编码集合批量获取权限。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="tenantId">租户 ID(保留参数,实际不使用)。</param>
|
||||
/// <param name="codes">权限编码集合。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>权限列表。</returns>
|
||||
@@ -49,10 +54,11 @@ public sealed class EfPermissionRepository(IdentityDbContext dbContext) : IPermi
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
// 2. 读取租户权限
|
||||
// 2. 读取权限(忽略租户过滤)
|
||||
return dbContext.Permissions
|
||||
.IgnoreQueryFilters()
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.DeletedAt == null && normalizedCodes.Contains(x.Code))
|
||||
.Where(x => x.DeletedAt == null && normalizedCodes.Contains(x.Code))
|
||||
.ToListAsync(cancellationToken)
|
||||
.ContinueWith(t => (IReadOnlyList<Permission>)t.Result, cancellationToken);
|
||||
}
|
||||
@@ -60,30 +66,32 @@ public sealed class EfPermissionRepository(IdentityDbContext dbContext) : IPermi
|
||||
/// <summary>
|
||||
/// 根据权限 ID 集合批量获取权限。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="tenantId">租户 ID(保留参数,实际不使用)。</param>
|
||||
/// <param name="permissionIds">权限 ID 集合。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>权限列表。</returns>
|
||||
public Task<IReadOnlyList<Permission>> GetByIdsAsync(long tenantId, IEnumerable<long> permissionIds, CancellationToken cancellationToken = default)
|
||||
=> dbContext.Permissions
|
||||
.IgnoreQueryFilters()
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.DeletedAt == null && permissionIds.Contains(x.Id))
|
||||
.Where(x => x.DeletedAt == null && permissionIds.Contains(x.Id))
|
||||
.ToListAsync(cancellationToken)
|
||||
.ContinueWith(t => (IReadOnlyList<Permission>)t.Result, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// 按关键字搜索权限。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="tenantId">租户 ID(保留参数,实际不使用)。</param>
|
||||
/// <param name="keyword">搜索关键字。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>权限列表。</returns>
|
||||
public Task<IReadOnlyList<Permission>> SearchAsync(long tenantId, string? keyword, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 构建基础查询
|
||||
// 1. 构建基础查询(忽略租户过滤)
|
||||
var query = dbContext.Permissions
|
||||
.IgnoreQueryFilters()
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.DeletedAt == null);
|
||||
.Where(x => x.DeletedAt == null);
|
||||
if (!string.IsNullOrWhiteSpace(keyword))
|
||||
{
|
||||
// 2. 追加关键字过滤
|
||||
@@ -128,13 +136,15 @@ public sealed class EfPermissionRepository(IdentityDbContext dbContext) : IPermi
|
||||
/// 删除指定权限。
|
||||
/// </summary>
|
||||
/// <param name="permissionId">权限 ID。</param>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="tenantId">租户 ID(保留参数,实际不使用)。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>异步任务。</returns>
|
||||
public async Task DeleteAsync(long permissionId, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 查询目标权限
|
||||
var entity = await dbContext.Permissions.FirstOrDefaultAsync(x => x.Id == permissionId && x.TenantId == tenantId, cancellationToken);
|
||||
var entity = await dbContext.Permissions
|
||||
.IgnoreQueryFilters()
|
||||
.FirstOrDefaultAsync(x => x.Id == permissionId, cancellationToken);
|
||||
if (entity != null)
|
||||
{
|
||||
// 2. 删除实体
|
||||
|
||||
@@ -160,7 +160,6 @@ public sealed class IdentityDbContext(
|
||||
{
|
||||
builder.ToTable("permissions");
|
||||
builder.HasKey(x => x.Id);
|
||||
builder.Property(x => x.TenantId).IsRequired();
|
||||
builder.Property(x => x.ParentId).IsRequired();
|
||||
builder.Property(x => x.SortOrder).IsRequired();
|
||||
builder.Property(x => x.Type).HasMaxLength(16).IsRequired();
|
||||
@@ -169,9 +168,8 @@ public sealed class IdentityDbContext(
|
||||
builder.Property(x => x.Description).HasMaxLength(256);
|
||||
ConfigureAuditableEntity(builder);
|
||||
ConfigureSoftDeleteEntity(builder);
|
||||
builder.HasIndex(x => x.TenantId);
|
||||
builder.HasIndex(x => new { x.TenantId, x.ParentId, x.SortOrder });
|
||||
builder.HasIndex(x => new { x.TenantId, x.Code }).IsUnique();
|
||||
builder.HasIndex(x => x.Code).IsUnique();
|
||||
builder.HasIndex(x => new { x.Portal, x.ParentId, x.SortOrder });
|
||||
}
|
||||
|
||||
private static void ConfigureRoleTemplate(EntityTypeBuilder<RoleTemplate> builder)
|
||||
@@ -226,7 +224,7 @@ public sealed class IdentityDbContext(
|
||||
{
|
||||
builder.ToTable("menu_definitions");
|
||||
builder.HasKey(x => x.Id);
|
||||
builder.Property(x => x.TenantId).IsRequired();
|
||||
builder.Property(x => x.Portal).HasConversion<int>().IsRequired();
|
||||
builder.Property(x => x.ParentId).IsRequired();
|
||||
builder.Property(x => x.Name).HasMaxLength(64).IsRequired();
|
||||
builder.Property(x => x.Path).HasMaxLength(256).IsRequired();
|
||||
@@ -241,6 +239,6 @@ public sealed class IdentityDbContext(
|
||||
builder.Property(x => x.AuthListJson).HasColumnType("text");
|
||||
ConfigureAuditableEntity(builder);
|
||||
ConfigureSoftDeleteEntity(builder);
|
||||
builder.HasIndex(x => new { x.TenantId, x.ParentId, x.SortOrder });
|
||||
builder.HasIndex(x => new { x.Portal, x.ParentId, x.SortOrder });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TakeoutSaaS.Domain.Identity.Entities;
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Infrastructure.Identity.Persistence;
|
||||
|
||||
@@ -11,28 +12,28 @@ namespace TakeoutSaaS.Infrastructure.Identity.Repositories;
|
||||
public sealed class EfMenuRepository(IdentityDbContext dbContext) : IMenuRepository
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<MenuDefinition>> GetByTenantAsync(long tenantId, CancellationToken cancellationToken = default)
|
||||
public async Task<IReadOnlyList<MenuDefinition>> GetByPortalAsync(PortalType portal, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 仅返回该租户的菜单,无回退逻辑
|
||||
var tenantMenus = await dbContext.MenuDefinitions
|
||||
// 1. 按门户类型查询菜单(忽略租户过滤器)
|
||||
var menus = await dbContext.MenuDefinitions
|
||||
.IgnoreQueryFilters()
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.DeletedAt == null)
|
||||
.Where(x => x.Portal == portal && x.DeletedAt == null)
|
||||
.OrderBy(x => x.ParentId)
|
||||
.ThenBy(x => x.SortOrder)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return tenantMenus;
|
||||
return menus;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<MenuDefinition?> FindByIdAsync(long id, long tenantId, CancellationToken cancellationToken = default)
|
||||
public async Task<MenuDefinition?> FindByIdAsync(long id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 仅查询该租户的菜单,无回退逻辑
|
||||
// 1. 按 ID 查询菜单(忽略租户过滤器)
|
||||
return await dbContext.MenuDefinitions
|
||||
.IgnoreQueryFilters()
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(
|
||||
x => x.Id == id && x.TenantId == tenantId && x.DeletedAt == null,
|
||||
cancellationToken);
|
||||
.FirstOrDefaultAsync(x => x.Id == id && x.DeletedAt == null, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -49,11 +50,12 @@ public sealed class EfMenuRepository(IdentityDbContext dbContext) : IMenuReposit
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteAsync(long id, long tenantId, CancellationToken cancellationToken = default)
|
||||
public async Task DeleteAsync(long id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 查询目标
|
||||
// 1. 查询目标(忽略租户过滤器)
|
||||
var entity = await dbContext.MenuDefinitions
|
||||
.FirstOrDefaultAsync(x => x.Id == id && x.TenantId == tenantId, cancellationToken);
|
||||
.IgnoreQueryFilters()
|
||||
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
|
||||
// 2. 存在则删除
|
||||
if (entity is not null)
|
||||
|
||||
Reference in New Issue
Block a user