feat: add permission hierarchy tree

This commit is contained in:
2025-12-06 11:53:14 +08:00
parent d34f92ea1d
commit 37dc23f0c1
16 changed files with 1014 additions and 2 deletions

View File

@@ -8,6 +8,9 @@ namespace TakeoutSaaS.Application.Identity.Commands;
/// </summary>
public sealed record CreatePermissionCommand : IRequest<PermissionDto>
{
public long ParentId { get; init; } = 0;
public int SortOrder { get; init; } = 0;
public string Type { get; init; } = "leaf";
public string Name { get; init; } = string.Empty;
public string Code { get; init; } = string.Empty;
public string? Description { get; init; }

View File

@@ -9,6 +9,9 @@ namespace TakeoutSaaS.Application.Identity.Commands;
public sealed record UpdatePermissionCommand : IRequest<PermissionDto?>
{
public long PermissionId { get; init; }
public long ParentId { get; init; }
public int SortOrder { get; init; }
public string Type { get; init; } = "leaf";
public string Name { get; init; } = string.Empty;
public string? Description { get; init; }
}

View File

@@ -20,6 +20,22 @@ public sealed class PermissionDto
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
public long TenantId { get; init; }
/// <summary>
/// 父级权限 ID。
/// </summary>
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
public long ParentId { get; init; }
/// <summary>
/// 排序值,值越小越靠前。
/// </summary>
public int SortOrder { get; init; }
/// <summary>
/// 权限类型group/leaf
/// </summary>
public string Type { get; init; } = string.Empty;
/// <summary>
/// 权限名称。
/// </summary>

View File

@@ -0,0 +1,58 @@
using System.Text.Json.Serialization;
using TakeoutSaaS.Shared.Abstractions.Serialization;
namespace TakeoutSaaS.Application.Identity.Contracts;
/// <summary>
/// 权限树节点 DTO。
/// </summary>
public sealed record PermissionTreeDto
{
/// <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>
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
public long ParentId { get; init; }
/// <summary>
/// 排序值,值越小越靠前。
/// </summary>
public int SortOrder { get; init; }
/// <summary>
/// 权限类型group/leaf
/// </summary>
public string Type { get; init; } = string.Empty;
/// <summary>
/// 权限名称。
/// </summary>
public string Name { get; init; } = string.Empty;
/// <summary>
/// 权限编码(租户内唯一)。
/// </summary>
public string Code { get; init; } = string.Empty;
/// <summary>
/// 描述。
/// </summary>
public string? Description { get; init; }
/// <summary>
/// 子节点集合。
/// </summary>
public IReadOnlyList<PermissionTreeDto> Children { get; init; } = Array.Empty<PermissionTreeDto>();
}

View File

@@ -21,9 +21,18 @@ public sealed class CreatePermissionCommandHandler(
var tenantId = tenantProvider.GetCurrentTenantId();
// 2. 构建权限实体
var normalizedType = string.IsNullOrWhiteSpace(request.Type)
? "leaf"
: request.Type.Trim().ToLowerInvariant();
normalizedType = normalizedType is "group" or "leaf" ? normalizedType : "leaf";
var parentId = request.ParentId > 0 ? request.ParentId : 0;
var sortOrder = request.SortOrder < 0 ? 0 : request.SortOrder;
var permission = new Permission
{
TenantId = tenantId,
ParentId = parentId,
SortOrder = sortOrder,
Type = normalizedType,
Name = request.Name,
Code = request.Code,
Description = request.Description
@@ -38,6 +47,9 @@ public sealed class CreatePermissionCommandHandler(
{
Id = permission.Id,
TenantId = permission.TenantId,
ParentId = permission.ParentId,
SortOrder = permission.SortOrder,
Type = permission.Type,
Name = permission.Name,
Code = permission.Code,
Description = permission.Description

View File

@@ -0,0 +1,69 @@
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 PermissionTreeQueryHandler(
IPermissionRepository permissionRepository,
ITenantProvider tenantProvider)
: IRequestHandler<PermissionTreeQuery, IReadOnlyList<PermissionTreeDto>>
{
public async Task<IReadOnlyList<PermissionTreeDto>> Handle(PermissionTreeQuery request, CancellationToken cancellationToken)
{
// 1. 获取租户上下文并查询权限
var tenantId = tenantProvider.GetCurrentTenantId();
var permissions = await permissionRepository.SearchAsync(tenantId, request.Keyword, cancellationToken);
// 2. 构建节点映射与父子分组
var nodeMap = permissions.ToDictionary(
x => x.Id,
x => new PermissionTreeDto
{
Id = x.Id,
TenantId = x.TenantId,
ParentId = x.ParentId,
SortOrder = x.SortOrder,
Type = x.Type,
Name = x.Name,
Code = x.Code,
Description = x.Description,
Children = Array.Empty<PermissionTreeDto>()
});
var childrenLookup = permissions
.GroupBy(x => x.ParentId)
.ToDictionary(g => g.Key, g => g.OrderBy(c => c.SortOrder).ThenBy(c => c.Id).Select(c => c.Id).ToList());
// 3. 递归组装树,确保子节点引用最新
List<PermissionTreeDto> Build(long parentId)
{
if (!childrenLookup.TryGetValue(parentId, out var childIds))
{
return [];
}
var result = new List<PermissionTreeDto>(childIds.Count);
foreach (var childId in childIds)
{
if (!nodeMap.TryGetValue(childId, out var child))
{
continue;
}
var withChildren = child with { Children = Build(child.Id) };
result.Add(withChildren);
}
return result;
}
// 4. 返回根节点集合
var roots = Build(0);
return roots;
}
}

View File

@@ -41,6 +41,10 @@ public sealed class RoleDetailQueryHandler(
.Select(x => new PermissionDto
{
Id = x.Id,
TenantId = x.TenantId,
ParentId = x.ParentId,
SortOrder = x.SortOrder,
Type = x.Type,
Code = x.Code,
Name = x.Name,
Description = x.Description

View File

@@ -30,9 +30,18 @@ public sealed class SearchPermissionsQueryHandler(
"code" => request.SortDescending
? permissions.OrderByDescending(x => x.Code)
: permissions.OrderBy(x => x.Code),
"parentid" => request.SortDescending
? permissions.OrderByDescending(x => x.ParentId).ThenByDescending(x => x.SortOrder)
: permissions.OrderBy(x => x.ParentId).ThenBy(x => x.SortOrder),
"type" => request.SortDescending
? permissions.OrderByDescending(x => x.Type).ThenByDescending(x => x.SortOrder)
: permissions.OrderBy(x => x.Type).ThenBy(x => x.SortOrder),
"sortorder" => request.SortDescending
? permissions.OrderByDescending(x => x.SortOrder).ThenByDescending(x => x.CreatedAt)
: permissions.OrderBy(x => x.SortOrder).ThenBy(x => x.CreatedAt),
_ => request.SortDescending
? permissions.OrderByDescending(x => x.CreatedAt)
: permissions.OrderBy(x => x.CreatedAt)
? permissions.OrderByDescending(x => x.SortOrder).ThenByDescending(x => x.CreatedAt)
: permissions.OrderBy(x => x.SortOrder).ThenBy(x => x.CreatedAt)
};
// 3. 分页
@@ -46,6 +55,9 @@ public sealed class SearchPermissionsQueryHandler(
{
Id = permission.Id,
TenantId = permission.TenantId,
ParentId = permission.ParentId,
SortOrder = permission.SortOrder,
Type = permission.Type,
Name = permission.Name,
Code = permission.Code,
Description = permission.Description

View File

@@ -25,6 +25,17 @@ public sealed class UpdatePermissionCommandHandler(
}
// 2. 更新字段
var normalizedType = string.IsNullOrWhiteSpace(request.Type)
? "leaf"
: request.Type.Trim().ToLowerInvariant();
normalizedType = normalizedType is "group" or "leaf" ? normalizedType : "leaf";
var parentId = request.ParentId > 0 && request.ParentId != permission.Id
? request.ParentId
: 0;
var sortOrder = request.SortOrder < 0 ? 0 : request.SortOrder;
permission.ParentId = parentId;
permission.SortOrder = sortOrder;
permission.Type = normalizedType;
permission.Name = request.Name;
permission.Description = request.Description;
@@ -37,6 +48,9 @@ public sealed class UpdatePermissionCommandHandler(
{
Id = permission.Id,
TenantId = permission.TenantId,
ParentId = permission.ParentId,
SortOrder = permission.SortOrder,
Type = permission.Type,
Name = permission.Name,
Code = permission.Code,
Description = permission.Description

View File

@@ -0,0 +1,15 @@
using MediatR;
using TakeoutSaaS.Application.Identity.Contracts;
namespace TakeoutSaaS.Application.Identity.Queries;
/// <summary>
/// 权限树查询。
/// </summary>
public sealed class PermissionTreeQuery : IRequest<IReadOnlyList<PermissionTreeDto>>
{
/// <summary>
/// 关键字(可选)。
/// </summary>
public string? Keyword { get; init; }
}