using FoodLabeling.Application.Services.DbModels; using FoodLabeling.Th.Application.Contracts.Dtos.RbacMenu; using FoodLabeling.Th.Application.Contracts.IServices; using FoodLabeling.Th.Domain.Shared.Helpers; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using SqlSugar; using Volo.Abp; using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; using Yi.Framework.Rbac.Domain.Shared.Enums; using Yi.Framework.SqlSugarCore.Abstractions; namespace FoodLabeling.Th.Application.Services; /// /// 泰额版租户业务库 menu 表 CRUD(字符串 Id/ParentId,不使用 TreeHelper) /// [Authorize] public class ThRbacMenuAppService : ApplicationService, IThRbacMenuAppService { private readonly ISqlSugarDbContext _dbContext; public ThRbacMenuAppService(ISqlSugarDbContext dbContext) { _dbContext = dbContext; } /// /// 分页查询菜单列表 /// /// /// 在当前租户业务库 `menu` 表查询,过滤逻辑删除数据。 /// /// 示例请求: /// ```json /// { /// "skipCount": 0, /// "maxResultCount": 20, /// "menuName": "Label", /// "state": true, /// "menuSource": 1 /// } /// ``` /// /// 参数说明: /// - skipCount: 跳过条数 /// - maxResultCount: 每页条数 /// - menuName: 菜单名称模糊匹配 /// - state: 启用状态 /// - menuSource: 菜单来源(MenuSourceEnum) /// - menuType: 菜单类型(MenuTypeEnum) /// /// 分页与筛选条件 /// 分页菜单列表 /// 成功返回分页列表 /// 未登录 /// 服务器错误 [HttpGet("th-rbac-menu/list")] public virtual async Task> GetListAsync([FromQuery] ThRbacMenuGetListInputVo input) { RefAsync total = 0; var query = BuildFilteredQuery(input) .OrderBy(x => x.OrderNum, OrderByType.Desc); var entities = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total); var items = entities.Select(MapToListDto).ToList(); return new PagedResultDto(total, items); } /// /// 根据 Id 获取菜单详情 /// /// /// 在当前租户业务库按主键查询未删除菜单。 /// /// 菜单 Id(字符串) /// 菜单详情 /// 成功返回菜单 /// 菜单不存在 /// 未登录 /// 服务器错误 [HttpGet("th-rbac-menu/{id}")] public virtual async Task GetAsync(string id) { var entity = await FindActiveMenuAsync(id); return MapToListDto(entity); } /// /// 新增菜单 /// /// /// 写入当前租户业务库 `menu` 表;Id 使用雪花算法字符串。 /// /// 示例请求: /// ```json /// { /// "menuName": "Settings", /// "parentId": "0", /// "menuType": 1, /// "menuSource": 2, /// "router": "/settings", /// "orderNum": 10, /// "state": true /// } /// ``` /// /// 参数说明: /// - menuName: 菜单名称(必填) /// - parentId: 父级 Id,根节点为 0 /// - menuType: 菜单类型(MenuTypeEnum) /// - menuSource: 菜单来源(MenuSourceEnum) /// /// 新增菜单入参 /// 新建菜单详情 /// 创建成功 /// 参数校验失败 /// 未登录 /// 服务器错误 [HttpPost("th-rbac-menu")] public virtual async Task CreateAsync([FromBody] ThRbacMenuCreateInputVo input) { var name = input.MenuName?.Trim(); if (string.IsNullOrWhiteSpace(name)) { throw new UserFriendlyException("菜单名称不能为空"); } var parentId = NormalizeParentId(input.ParentId); await EnsureParentExistsAsync(parentId); var entity = new MenuDbEntity { Id = YitIdHelper.NextId().ToString(), MenuName = name, ParentId = parentId, MenuType = (int)input.MenuType, MenuSource = (int)input.MenuSource, PermissionCode = input.PermissionCode?.Trim(), Router = input.Router?.Trim(), RouterName = input.RouterName?.Trim(), Component = input.Component?.Trim(), MenuIcon = input.MenuIcon?.Trim(), OrderNum = input.OrderNum, State = input.State, IsDeleted = false, CreationTime = DateTime.Now, IsCache = false, IsLink = false, IsShow = input.IsShow, ConcurrencyStamp = string.Empty }; await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); return await GetAsync(entity.Id); } /// /// 编辑菜单 /// /// /// 更新当前租户业务库中指定 Id 的菜单;禁止将 ParentId 设为自身。 /// /// 示例请求: /// ```json /// { /// "menuName": "Settings", /// "parentId": "0", /// "menuType": 1, /// "menuSource": 2, /// "orderNum": 20, /// "state": true /// } /// ``` /// /// 菜单 Id /// 编辑入参 /// 更新后的菜单详情 /// 更新成功 /// 参数错误或菜单不存在 /// 未登录 /// 服务器错误 [HttpPut("th-rbac-menu/{id}")] public virtual async Task UpdateAsync(string id, [FromBody] ThRbacMenuUpdateInputVo input) { var entity = await FindActiveMenuAsync(id); var name = input.MenuName?.Trim(); if (string.IsNullOrWhiteSpace(name)) { throw new UserFriendlyException("菜单名称不能为空"); } var parentId = NormalizeParentId(input.ParentId); if (string.Equals(parentId, id, StringComparison.Ordinal)) { throw new UserFriendlyException("父级菜单不能为自身"); } await EnsureParentExistsAsync(parentId, id); entity.MenuName = name; entity.ParentId = parentId; entity.MenuType = (int)input.MenuType; entity.MenuSource = (int)input.MenuSource; entity.PermissionCode = input.PermissionCode?.Trim(); entity.Router = input.Router?.Trim(); entity.RouterName = input.RouterName?.Trim(); entity.Component = input.Component?.Trim(); entity.MenuIcon = input.MenuIcon?.Trim(); entity.OrderNum = input.OrderNum; entity.State = input.State; entity.IsShow = input.IsShow; entity.LastModificationTime = DateTime.Now; await _dbContext.SqlSugarClient.Updateable(entity) .Where(x => x.Id == entity.Id) .ExecuteCommandAsync(); return await GetAsync(entity.Id); } /// /// 批量删除菜单(逻辑删除) /// /// /// 将指定 Id 列表对应菜单标记为 IsDeleted=true。 /// /// 示例请求: /// ```json /// ["1234567890123456789", "9876543210987654321"] /// ``` /// /// 参数说明: /// - ids: 待删除菜单 Id 数组 /// /// 菜单 Id 列表 /// 无内容 /// 删除成功 /// 未登录 /// 服务器错误 [HttpDelete("th-rbac-menu")] public virtual async Task DeleteAsync([FromBody] List ids) { var idList = ids? .Where(x => !string.IsNullOrWhiteSpace(x)) .Select(x => x.Trim()) .Distinct(StringComparer.Ordinal) .ToList() ?? new List(); if (idList.Count == 0) { return; } await _dbContext.SqlSugarClient.Updateable() .SetColumns(x => new MenuDbEntity { IsDeleted = true, LastModificationTime = DateTime.Now }) .Where(x => idList.Contains(x.Id) && x.IsDeleted == false) .ExecuteCommandAsync(); } /// /// 获取全部菜单树 /// /// /// 返回当前租户业务库全部未删除菜单,按 ParentId 字符串组树(不使用 Guid TreeHelper)。 /// 树节点按 OrderNum 降序排序。 /// /// 根节点菜单树 /// 成功返回菜单树 /// 未登录 /// 服务器错误 [HttpGet("th-rbac-menu/tree")] public virtual async Task> GetTreeAsync() { var menus = await _dbContext.SqlSugarClient.Queryable() .Where(x => x.IsDeleted == false) .OrderBy(x => x.OrderNum, OrderByType.Desc) .ToListAsync(); var nodes = menus.Select(MapToTreeDto).ToList(); return BuildMenuTree(nodes); } private ISugarQueryable BuildFilteredQuery(ThRbacMenuGetListInputVo input) { return _dbContext.SqlSugarClient.Queryable() .Where(x => x.IsDeleted == false) .WhereIF(!string.IsNullOrWhiteSpace(input.MenuName), x => x.MenuName.Contains(input.MenuName!.Trim())) .WhereIF(input.State is not null, x => x.State == input.State) .WhereIF(input.MenuSource is not null, x => x.MenuSource == (int)input.MenuSource!.Value) .WhereIF(input.MenuType is not null, x => x.MenuType == (int)input.MenuType!.Value); } private async Task FindActiveMenuAsync(string id) { if (string.IsNullOrWhiteSpace(id)) { throw new UserFriendlyException("菜单 Id 不能为空"); } var entity = await _dbContext.SqlSugarClient.Queryable() .Where(x => x.Id == id.Trim() && x.IsDeleted == false) .SingleAsync(); if (entity is null) { throw new UserFriendlyException("菜单不存在"); } return entity; } private async Task EnsureParentExistsAsync(string parentId, string? currentId = null) { if (IsRootParentId(parentId)) { return; } var parent = await _dbContext.SqlSugarClient.Queryable() .Where(x => x.Id == parentId && x.IsDeleted == false) .SingleAsync(); if (parent is null) { throw new UserFriendlyException("父级菜单不存在"); } if (currentId is not null && string.Equals(parent.ParentId, currentId, StringComparison.Ordinal)) { throw new UserFriendlyException("不能将菜单移动到其子节点下"); } } private static string NormalizeParentId(string? parentId) { return string.IsNullOrWhiteSpace(parentId) ? "0" : parentId.Trim(); } private static bool IsRootParentId(string parentId) { return parentId == "0" || parentId == "00000000-0000-0000-0000-000000000000"; } private static ThRbacMenuGetListOutputDto MapToListDto(MenuDbEntity x) { return new ThRbacMenuGetListOutputDto { Id = x.Id, ParentId = x.ParentId, MenuName = x.MenuName ?? string.Empty, RouterName = x.RouterName, Router = x.Router, PermissionCode = x.PermissionCode, MenuType = (MenuTypeEnum)x.MenuType, MenuSource = (MenuSourceEnum)x.MenuSource, OrderNum = x.OrderNum, State = x.State }; } private static ThRbacMenuTreeDto MapToTreeDto(MenuDbEntity m) { return new ThRbacMenuTreeDto { Id = m.Id, IsDeleted = m.IsDeleted, CreationTime = m.CreationTime, CreatorId = m.CreatorId, LastModifierId = m.LastModifierId, LastModificationTime = m.LastModificationTime, OrderNum = m.OrderNum, State = m.State, MenuName = m.MenuName ?? string.Empty, RouterName = m.RouterName, MenuType = (MenuTypeEnum)m.MenuType, PermissionCode = m.PermissionCode, ParentId = m.ParentId, MenuIcon = m.MenuIcon, Router = m.Router, IsLink = m.IsLink, IsCache = m.IsCache, IsShow = m.IsShow, Remark = m.Remark, Component = m.Component, MenuSource = (MenuSourceEnum)m.MenuSource, Query = m.Query, ConcurrencyStamp = m.ConcurrencyStamp, Children = new List() }; } private static List BuildMenuTree(List nodes) { var nodeById = nodes .Where(x => !string.IsNullOrWhiteSpace(x.Id)) .GroupBy(x => x.Id) .ToDictionary(g => g.Key, g => g.First()); foreach (var node in nodes) { node.Children ??= new List(); var parentId = NormalizeParentId(node.ParentId); if (IsRootParentId(parentId)) { continue; } if (nodeById.TryGetValue(parentId, out var parent)) { parent.Children ??= new List(); parent.Children.Add(node); } } var roots = nodes .Where(n => { var pid = NormalizeParentId(n.ParentId); return IsRootParentId(pid) || !nodeById.ContainsKey(pid); }) .ToList(); SortTree(roots); return roots; } private static void SortTree(List nodes) { nodes.Sort((a, b) => b.OrderNum.CompareTo(a.OrderNum)); foreach (var node in nodes) { if (node.Children is { Count: > 0 }) { SortTree(node.Children); } } } }