feat: 管理端返回权限过滤的菜单树
This commit is contained in:
@@ -95,6 +95,27 @@ public sealed class AuthController(IAdminAuthService authService) : BaseApiContr
|
||||
return ApiResponse<CurrentUserProfile>.Ok(profile);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前用户的菜单树(按权限过滤)。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>当前用户可见的菜单树。</returns>
|
||||
[HttpGet("menu")]
|
||||
[PermissionAuthorize("identity:profile:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<MenuNodeDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<MenuNodeDto>>> GetMenuTree(CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 获取当前用户标识
|
||||
var userId = User.GetUserId();
|
||||
if (userId == 0)
|
||||
{
|
||||
return ApiResponse<IReadOnlyList<MenuNodeDto>>.Error(ErrorCodes.Unauthorized, "Token 缺少有效的用户标识");
|
||||
}
|
||||
// 2. 生成菜单树
|
||||
var menu = await authService.GetMenuTreeAsync(userId, cancellationToken);
|
||||
return ApiResponse<IReadOnlyList<MenuNodeDto>>.Ok(menu);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询指定用户的角色与权限概览(当前租户范围)。
|
||||
/// </summary>
|
||||
|
||||
@@ -8,9 +8,33 @@ namespace TakeoutSaaS.Application.Identity.Abstractions;
|
||||
/// </summary>
|
||||
public interface IAdminAuthService
|
||||
{
|
||||
/// <summary>
|
||||
/// 登录获取 Token。
|
||||
/// </summary>
|
||||
Task<TokenResponse> LoginAsync(AdminLoginRequest request, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 刷新 Token。
|
||||
/// </summary>
|
||||
Task<TokenResponse> RefreshTokenAsync(RefreshTokenRequest request, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 获取用户档案。
|
||||
/// </summary>
|
||||
Task<CurrentUserProfile> GetProfileAsync(long userId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 获取用户权限概览。
|
||||
/// </summary>
|
||||
Task<UserPermissionDto?> GetUserPermissionsAsync(long userId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 搜索用户权限概览列表。
|
||||
/// </summary>
|
||||
Task<PagedResult<UserPermissionDto>> SearchUserPermissionsAsync(string? keyword, int page, int pageSize, string? sortBy, bool sortDescending, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前用户可见菜单树。
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<MenuNodeDto>> GetMenuTreeAsync(long userId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// 管理端菜单节点 DTO。
|
||||
/// </summary>
|
||||
public sealed record MenuNodeDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 菜单编码(唯一标识)。
|
||||
/// </summary>
|
||||
public string Code { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 展示名称(中文)。
|
||||
/// </summary>
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 前端路由路径。
|
||||
/// </summary>
|
||||
public string Path { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 可选图标标识。
|
||||
/// </summary>
|
||||
public string? Icon { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 访问该菜单所需的任一权限,留空表示公共可见。
|
||||
/// </summary>
|
||||
public string[] RequiredPermissions { get; init; } = System.Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// 子菜单集合。
|
||||
/// </summary>
|
||||
public IReadOnlyList<MenuNodeDto> Children { get; init; } = System.Array.Empty<MenuNodeDto>();
|
||||
}
|
||||
@@ -94,10 +94,25 @@ public sealed class AdminAuthService(
|
||||
{
|
||||
var user = await userRepository.FindByIdAsync(userId, cancellationToken)
|
||||
?? throw new BusinessException(ErrorCodes.NotFound, "用户不存在");
|
||||
|
||||
// 1. 返回档案
|
||||
return await BuildProfileAsync(user, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前用户可见菜单树。
|
||||
/// </summary>
|
||||
/// <param name="userId">用户 ID。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>菜单树。</returns>
|
||||
public async Task<IReadOnlyList<MenuNodeDto>> GetMenuTreeAsync(long userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 读取档案以获取权限
|
||||
var profile = await GetProfileAsync(userId, cancellationToken);
|
||||
// 2. 生成菜单树
|
||||
var menu = AdminMenuProvider.BuildMenuTree(profile.Permissions);
|
||||
return menu;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定用户的权限概览(校验当前租户)。
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 管理端菜单生成器(基于权限过滤)。
|
||||
/// </summary>
|
||||
public static class AdminMenuProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// 按权限生成可见菜单树。
|
||||
/// </summary>
|
||||
/// <param name="permissions">当前用户拥有的权限集合。</param>
|
||||
/// <returns>过滤后的菜单树。</returns>
|
||||
public static IReadOnlyList<MenuNodeDto> BuildMenuTree(IEnumerable<string> permissions)
|
||||
{
|
||||
// 1. 归一化权限集合
|
||||
var permissionSet = new HashSet<string>(permissions ?? [], System.StringComparer.OrdinalIgnoreCase);
|
||||
// 2. 过滤菜单
|
||||
var definitions = GetMenuDefinitions();
|
||||
return Filter(definitions, permissionSet);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<MenuNodeDto> Filter(IEnumerable<MenuNodeDto> nodes, HashSet<string> permissionSet)
|
||||
{
|
||||
// 1. 遍历节点并过滤子节点
|
||||
var result = new List<MenuNodeDto>();
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
var filteredChildren = Filter(node.Children, permissionSet);
|
||||
var visible = node.RequiredPermissions.Length == 0 || node.RequiredPermissions.Any(permissionSet.Contains);
|
||||
if (visible || filteredChildren.Count > 0)
|
||||
{
|
||||
result.Add(node with { Children = filteredChildren });
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 返回过滤结果
|
||||
return result;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<MenuNodeDto> GetMenuDefinitions()
|
||||
{
|
||||
// 1. 顶部菜单定义
|
||||
return
|
||||
[
|
||||
new MenuNodeDto
|
||||
{
|
||||
Code = "dashboard",
|
||||
Name = "仪表盘",
|
||||
Path = "/dashboard",
|
||||
Icon = "dashboard"
|
||||
},
|
||||
new MenuNodeDto
|
||||
{
|
||||
Code = "merchant",
|
||||
Name = "商户",
|
||||
Path = "/merchant",
|
||||
Icon = "store",
|
||||
RequiredPermissions = ["merchant:read"],
|
||||
Children =
|
||||
[
|
||||
new MenuNodeDto
|
||||
{
|
||||
Code = "merchant-list",
|
||||
Name = "商户管理",
|
||||
Path = "/merchant/list",
|
||||
RequiredPermissions = ["merchant:read"]
|
||||
},
|
||||
new MenuNodeDto
|
||||
{
|
||||
Code = "store",
|
||||
Name = "门店管理",
|
||||
Path = "/merchant/stores",
|
||||
RequiredPermissions = ["store:read"]
|
||||
},
|
||||
new MenuNodeDto
|
||||
{
|
||||
Code = "staff",
|
||||
Name = "员工管理",
|
||||
Path = "/merchant/staff",
|
||||
RequiredPermissions = ["store-staff:read"]
|
||||
}
|
||||
]
|
||||
},
|
||||
new MenuNodeDto
|
||||
{
|
||||
Code = "product",
|
||||
Name = "商品",
|
||||
Path = "/product",
|
||||
Icon = "goods",
|
||||
RequiredPermissions = ["product:read"],
|
||||
Children =
|
||||
[
|
||||
new MenuNodeDto
|
||||
{
|
||||
Code = "product-list",
|
||||
Name = "商品管理",
|
||||
Path = "/product/list",
|
||||
RequiredPermissions = ["product:read"]
|
||||
},
|
||||
new MenuNodeDto
|
||||
{
|
||||
Code = "category",
|
||||
Name = "品类管理",
|
||||
Path = "/product/category",
|
||||
RequiredPermissions = ["merchant_category:read"]
|
||||
}
|
||||
]
|
||||
},
|
||||
new MenuNodeDto
|
||||
{
|
||||
Code = "order",
|
||||
Name = "订单",
|
||||
Path = "/order",
|
||||
Icon = "order",
|
||||
RequiredPermissions = ["order:read"],
|
||||
Children =
|
||||
[
|
||||
new MenuNodeDto
|
||||
{
|
||||
Code = "order-list",
|
||||
Name = "订单管理",
|
||||
Path = "/order/list",
|
||||
RequiredPermissions = ["order:read"]
|
||||
},
|
||||
new MenuNodeDto
|
||||
{
|
||||
Code = "delivery",
|
||||
Name = "配送管理",
|
||||
Path = "/order/delivery",
|
||||
RequiredPermissions = ["delivery:read"]
|
||||
}
|
||||
]
|
||||
},
|
||||
new MenuNodeDto
|
||||
{
|
||||
Code = "inventory",
|
||||
Name = "库存",
|
||||
Path = "/inventory",
|
||||
Icon = "inventory",
|
||||
RequiredPermissions = ["inventory:read"],
|
||||
Children =
|
||||
[
|
||||
new MenuNodeDto
|
||||
{
|
||||
Code = "inventory-list",
|
||||
Name = "库存查询",
|
||||
Path = "/inventory/list",
|
||||
RequiredPermissions = ["inventory:read"]
|
||||
},
|
||||
new MenuNodeDto
|
||||
{
|
||||
Code = "inventory-batch",
|
||||
Name = "批次管理",
|
||||
Path = "/inventory/batch",
|
||||
RequiredPermissions = ["inventory:batch:read"]
|
||||
}
|
||||
]
|
||||
},
|
||||
new MenuNodeDto
|
||||
{
|
||||
Code = "payment",
|
||||
Name = "支付",
|
||||
Path = "/payment",
|
||||
Icon = "payment",
|
||||
RequiredPermissions = ["payment:read"],
|
||||
Children =
|
||||
[
|
||||
new MenuNodeDto
|
||||
{
|
||||
Code = "payment-list",
|
||||
Name = "支付记录",
|
||||
Path = "/payment/list",
|
||||
RequiredPermissions = ["payment:read"]
|
||||
}
|
||||
]
|
||||
},
|
||||
new MenuNodeDto
|
||||
{
|
||||
Code = "dictionary",
|
||||
Name = "字典",
|
||||
Path = "/dictionary",
|
||||
Icon = "dictionary",
|
||||
RequiredPermissions = ["dictionary:group:read", "dictionary:item:read"],
|
||||
Children =
|
||||
[
|
||||
new MenuNodeDto
|
||||
{
|
||||
Code = "dictionary-group",
|
||||
Name = "字典分组",
|
||||
Path = "/dictionary/group",
|
||||
RequiredPermissions = ["dictionary:group:read"]
|
||||
},
|
||||
new MenuNodeDto
|
||||
{
|
||||
Code = "dictionary-item",
|
||||
Name = "字典项",
|
||||
Path = "/dictionary/item",
|
||||
RequiredPermissions = ["dictionary:item:read"]
|
||||
}
|
||||
]
|
||||
},
|
||||
new MenuNodeDto
|
||||
{
|
||||
Code = "identity",
|
||||
Name = "权限",
|
||||
Path = "/identity",
|
||||
Icon = "shield",
|
||||
RequiredPermissions = ["identity:role:read", "identity:permission:read"],
|
||||
Children =
|
||||
[
|
||||
new MenuNodeDto
|
||||
{
|
||||
Code = "identity-user",
|
||||
Name = "用户权限",
|
||||
Path = "/identity/users",
|
||||
RequiredPermissions = ["identity:permission:read"]
|
||||
},
|
||||
new MenuNodeDto
|
||||
{
|
||||
Code = "identity-role",
|
||||
Name = "角色管理",
|
||||
Path = "/identity/roles",
|
||||
RequiredPermissions = ["identity:role:read"]
|
||||
},
|
||||
new MenuNodeDto
|
||||
{
|
||||
Code = "identity-permission",
|
||||
Name = "权限管理",
|
||||
Path = "/identity/permissions",
|
||||
RequiredPermissions = ["identity:permission:read"]
|
||||
}
|
||||
]
|
||||
},
|
||||
new MenuNodeDto
|
||||
{
|
||||
Code = "system",
|
||||
Name = "系统",
|
||||
Path = "/system",
|
||||
Icon = "settings",
|
||||
RequiredPermissions = ["system-parameter:read", "tenant-announcement:read"],
|
||||
Children =
|
||||
[
|
||||
new MenuNodeDto
|
||||
{
|
||||
Code = "system-parameter",
|
||||
Name = "系统参数",
|
||||
Path = "/system/parameters",
|
||||
RequiredPermissions = ["system-parameter:read"]
|
||||
},
|
||||
new MenuNodeDto
|
||||
{
|
||||
Code = "announcement",
|
||||
Name = "公告管理",
|
||||
Path = "/system/announcements",
|
||||
RequiredPermissions = ["tenant-announcement:read"]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user