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);
}
}
}
}