diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/AuthController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/AuthController.cs index 32b4e5a..5b75885 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/AuthController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/AuthController.cs @@ -95,6 +95,27 @@ public sealed class AuthController(IAdminAuthService authService) : BaseApiContr return ApiResponse.Ok(profile); } + /// + /// 获取当前用户的菜单树(按权限过滤)。 + /// + /// 取消标记。 + /// 当前用户可见的菜单树。 + [HttpGet("menu")] + [PermissionAuthorize("identity:profile:read")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> GetMenuTree(CancellationToken cancellationToken) + { + // 1. 获取当前用户标识 + var userId = User.GetUserId(); + if (userId == 0) + { + return ApiResponse>.Error(ErrorCodes.Unauthorized, "Token 缺少有效的用户标识"); + } + // 2. 生成菜单树 + var menu = await authService.GetMenuTreeAsync(userId, cancellationToken); + return ApiResponse>.Ok(menu); + } + /// /// 查询指定用户的角色与权限概览(当前租户范围)。 /// diff --git a/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IAdminAuthService.cs b/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IAdminAuthService.cs index 8961c72..f49464c 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IAdminAuthService.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IAdminAuthService.cs @@ -8,9 +8,33 @@ namespace TakeoutSaaS.Application.Identity.Abstractions; /// public interface IAdminAuthService { + /// + /// 登录获取 Token。 + /// Task LoginAsync(AdminLoginRequest request, CancellationToken cancellationToken = default); + + /// + /// 刷新 Token。 + /// Task RefreshTokenAsync(RefreshTokenRequest request, CancellationToken cancellationToken = default); + + /// + /// 获取用户档案。 + /// Task GetProfileAsync(long userId, CancellationToken cancellationToken = default); + + /// + /// 获取用户权限概览。 + /// Task GetUserPermissionsAsync(long userId, CancellationToken cancellationToken = default); + + /// + /// 搜索用户权限概览列表。 + /// Task> SearchUserPermissionsAsync(string? keyword, int page, int pageSize, string? sortBy, bool sortDescending, CancellationToken cancellationToken = default); + + /// + /// 获取当前用户可见菜单树。 + /// + Task> GetMenuTreeAsync(long userId, CancellationToken cancellationToken = default); } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Contracts/MenuNodeDto.cs b/src/Application/TakeoutSaaS.Application/Identity/Contracts/MenuNodeDto.cs new file mode 100644 index 0000000..43f4f4d --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Contracts/MenuNodeDto.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; + +namespace TakeoutSaaS.Application.Identity.Contracts; + +/// +/// 管理端菜单节点 DTO。 +/// +public sealed record MenuNodeDto +{ + /// + /// 菜单编码(唯一标识)。 + /// + public string Code { get; init; } = string.Empty; + + /// + /// 展示名称(中文)。 + /// + public string Name { get; init; } = string.Empty; + + /// + /// 前端路由路径。 + /// + public string Path { get; init; } = string.Empty; + + /// + /// 可选图标标识。 + /// + public string? Icon { get; init; } + + /// + /// 访问该菜单所需的任一权限,留空表示公共可见。 + /// + public string[] RequiredPermissions { get; init; } = System.Array.Empty(); + + /// + /// 子菜单集合。 + /// + public IReadOnlyList Children { get; init; } = System.Array.Empty(); +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs b/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs index 72e8f2d..f8ee088 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs @@ -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); } + /// + /// 获取当前用户可见菜单树。 + /// + /// 用户 ID。 + /// 取消令牌。 + /// 菜单树。 + public async Task> GetMenuTreeAsync(long userId, CancellationToken cancellationToken = default) + { + // 1. 读取档案以获取权限 + var profile = await GetProfileAsync(userId, cancellationToken); + // 2. 生成菜单树 + var menu = AdminMenuProvider.BuildMenuTree(profile.Permissions); + return menu; + } + /// /// 获取指定用户的权限概览(校验当前租户)。 /// diff --git a/src/Application/TakeoutSaaS.Application/Identity/Services/AdminMenuProvider.cs b/src/Application/TakeoutSaaS.Application/Identity/Services/AdminMenuProvider.cs new file mode 100644 index 0000000..2738f4e --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Services/AdminMenuProvider.cs @@ -0,0 +1,265 @@ +using System.Collections.Generic; +using System.Linq; +using TakeoutSaaS.Application.Identity.Contracts; + +namespace TakeoutSaaS.Application.Identity.Services; + +/// +/// 管理端菜单生成器(基于权限过滤)。 +/// +public static class AdminMenuProvider +{ + /// + /// 按权限生成可见菜单树。 + /// + /// 当前用户拥有的权限集合。 + /// 过滤后的菜单树。 + public static IReadOnlyList BuildMenuTree(IEnumerable permissions) + { + // 1. 归一化权限集合 + var permissionSet = new HashSet(permissions ?? [], System.StringComparer.OrdinalIgnoreCase); + // 2. 过滤菜单 + var definitions = GetMenuDefinitions(); + return Filter(definitions, permissionSet); + } + + private static IReadOnlyList Filter(IEnumerable nodes, HashSet permissionSet) + { + // 1. 遍历节点并过滤子节点 + var result = new List(); + 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 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"] + } + ] + } + ]; + } +}