feat: add permission hierarchy tree
This commit is contained in:
@@ -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; }
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
Reference in New Issue
Block a user