|
|
// ============================================================================
|
|
|
// 【文件说明】HwWebMenuService.cs - 网站菜单服务类
|
|
|
// ============================================================================
|
|
|
// 这个服务类负责处理网站菜单的业务逻辑,包括:
|
|
|
// - 菜单的 CRUD 操作
|
|
|
// - 树形菜单结构构建
|
|
|
// - 搜索索引重建
|
|
|
//
|
|
|
// 【业务背景】
|
|
|
// 网站菜单模块用于管理网站的前端导航菜单,支持多级树形结构。
|
|
|
// 菜单变更时会自动触发搜索索引重建。
|
|
|
//
|
|
|
// 【与 Java Spring Boot 的对比】
|
|
|
// Java Spring Boot:
|
|
|
// @Service
|
|
|
// public class HwWebMenuServiceImpl implements HwWebMenuService { ... }
|
|
|
//
|
|
|
// ASP.NET Core + Furion:
|
|
|
// public class HwWebMenuService : ITransient { ... }
|
|
|
// ============================================================================
|
|
|
|
|
|
namespace Admin.NET.Plugin.HwPortal;
|
|
|
|
|
|
/// <summary>
|
|
|
/// 网站菜单服务类。
|
|
|
/// <para>
|
|
|
/// 【服务职责】
|
|
|
/// 1. 管理网站菜单的增删改查
|
|
|
/// 2. 构建树形菜单结构
|
|
|
/// 3. 菜单变更时自动重建搜索索引
|
|
|
/// </para>
|
|
|
/// <para>
|
|
|
/// 【树形结构说明】
|
|
|
/// - Parent:父菜单ID,null或0表示顶级菜单
|
|
|
/// - Ancestors:祖先路径,格式为"0,1,2"
|
|
|
/// - Order:排序号,用于控制菜单显示顺序
|
|
|
/// </para>
|
|
|
/// </summary>
|
|
|
public class HwWebMenuService : ITransient
|
|
|
{
|
|
|
/// <summary>
|
|
|
/// MyBatis 映射器名称(保留用于回滚)。
|
|
|
/// </summary>
|
|
|
private const string Mapper = "HwWebMenuMapper";
|
|
|
|
|
|
/// <summary>
|
|
|
/// MyBatis 执行器(保留用于回滚)。
|
|
|
/// </summary>
|
|
|
private readonly HwPortalMyBatisExecutor _executor;
|
|
|
|
|
|
/// <summary>
|
|
|
/// SqlSugar 数据访问对象。
|
|
|
/// </summary>
|
|
|
private readonly ISqlSugarClient _db;
|
|
|
|
|
|
/// <summary>
|
|
|
/// 搜索索引重建服务。
|
|
|
/// </summary>
|
|
|
private readonly IHwSearchRebuildService _searchRebuildService;
|
|
|
|
|
|
/// <summary>
|
|
|
/// 日志记录器。
|
|
|
/// </summary>
|
|
|
private readonly ILogger<HwWebMenuService> _logger;
|
|
|
|
|
|
/// <summary>
|
|
|
/// 构造函数(依赖注入)。
|
|
|
/// </summary>
|
|
|
/// <param name="executor">MyBatis 执行器</param>
|
|
|
/// <param name="db">SqlSugar 数据访问对象</param>
|
|
|
/// <param name="searchRebuildService">搜索索引重建服务</param>
|
|
|
/// <param name="logger">日志记录器</param>
|
|
|
public HwWebMenuService(HwPortalMyBatisExecutor executor, ISqlSugarClient db, IHwSearchRebuildService searchRebuildService, ILogger<HwWebMenuService> logger)
|
|
|
{
|
|
|
_executor = executor;
|
|
|
_db = db;
|
|
|
_searchRebuildService = searchRebuildService;
|
|
|
_logger = logger;
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
/// 根据菜单ID查询菜单信息。
|
|
|
/// <para>
|
|
|
/// 【软删除过滤】
|
|
|
/// 查询时自动过滤已删除的记录(IsDelete == "0")。
|
|
|
/// </para>
|
|
|
/// </summary>
|
|
|
/// <param name="webMenuId">菜单ID</param>
|
|
|
/// <returns>菜单实体</returns>
|
|
|
public async Task<HwWebMenu> SelectHwWebMenuByWebMenuId(long webMenuId)
|
|
|
{
|
|
|
// 回滚到 XML 方案时可直接恢复:
|
|
|
// return await _executor.QuerySingleAsync<HwWebMenu>(Mapper, "selectHwWebMenuByWebMenuId", new { webMenuId });
|
|
|
return await _db.Queryable<HwWebMenu>()
|
|
|
.Where(item => item.IsDelete == "0" && item.WebMenuId == webMenuId)
|
|
|
.FirstAsync();
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
/// 查询菜单列表。
|
|
|
/// <para>
|
|
|
/// 【动态查询条件】
|
|
|
/// 支持按父ID、祖先路径、状态、菜单名、租户ID、图片、类型、排序号、英文名等条件筛选。
|
|
|
/// </para>
|
|
|
/// <para>
|
|
|
/// 【排序】
|
|
|
/// 按 Parent、Order、WebMenuId 排序,确保菜单按层级和顺序显示。
|
|
|
/// </para>
|
|
|
/// </summary>
|
|
|
/// <param name="input">查询条件</param>
|
|
|
/// <returns>菜单列表</returns>
|
|
|
public async Task<List<HwWebMenu>> SelectHwWebMenuList(HwWebMenu input)
|
|
|
{
|
|
|
HwWebMenu query = input ?? new HwWebMenu();
|
|
|
// 回滚到 XML 方案时可直接恢复:
|
|
|
// return await _executor.QueryListAsync<HwWebMenu>(Mapper, "selectHwWebMenuList", query);
|
|
|
return await _db.Queryable<HwWebMenu>()
|
|
|
.Where(item => item.IsDelete == "0")
|
|
|
.WhereIF(query.Parent.HasValue, item => item.Parent == query.Parent)
|
|
|
.WhereIF(!string.IsNullOrWhiteSpace(query.Ancestors), item => item.Ancestors == query.Ancestors)
|
|
|
.WhereIF(!string.IsNullOrWhiteSpace(query.Status), item => item.Status == query.Status)
|
|
|
.WhereIF(!string.IsNullOrWhiteSpace(query.WebMenuName), item => item.WebMenuName.Contains(query.WebMenuName))
|
|
|
.WhereIF(query.TenantId.HasValue, item => item.TenantId == query.TenantId)
|
|
|
.WhereIF(!string.IsNullOrWhiteSpace(query.WebMenuPic), item => item.WebMenuPic == query.WebMenuPic)
|
|
|
.WhereIF(query.WebMenuType.HasValue, item => item.WebMenuType == query.WebMenuType)
|
|
|
.WhereIF(query.Order.HasValue, item => item.Order == query.Order)
|
|
|
.WhereIF(!string.IsNullOrWhiteSpace(query.WebMenuNameEnglish), item => item.WebMenuNameEnglish == query.WebMenuNameEnglish)
|
|
|
.OrderBy(item => item.Parent)
|
|
|
.OrderBy(item => item.Order)
|
|
|
.OrderBy(item => item.WebMenuId)
|
|
|
.ToListAsync();
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
/// 查询菜单树。
|
|
|
/// <para>
|
|
|
/// 【树形结构构建】
|
|
|
/// 先查询菜单列表,然后调用 BuildWebMenuTree 构建树形结构。
|
|
|
/// </para>
|
|
|
/// </summary>
|
|
|
/// <param name="input">查询条件</param>
|
|
|
/// <returns>树形菜单列表</returns>
|
|
|
public async Task<List<HwWebMenu>> SelectMenuTree(HwWebMenu input)
|
|
|
{
|
|
|
List<HwWebMenu> menus = await SelectHwWebMenuList(input);
|
|
|
return BuildWebMenuTree(menus);
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
/// 新增菜单。
|
|
|
/// <para>
|
|
|
/// 【搜索索引重建】
|
|
|
/// 菜单新增后,自动触发搜索索引重建。
|
|
|
/// </para>
|
|
|
/// </summary>
|
|
|
/// <param name="input">菜单数据</param>
|
|
|
/// <returns>影响行数</returns>
|
|
|
public async Task<int> InsertHwWebMenu(HwWebMenu input)
|
|
|
{
|
|
|
// 【软删除标记初始化】
|
|
|
input.IsDelete = string.IsNullOrWhiteSpace(input.IsDelete) ? "0" : input.IsDelete;
|
|
|
|
|
|
// 回滚到 XML 方案时可直接恢复:
|
|
|
// int rows = await _executor.ExecuteAsync(Mapper, "insertHwWebMenu", input);
|
|
|
int rows = await _db.Insertable(input).ExecuteCommandAsync();
|
|
|
|
|
|
// 【搜索索引重建】
|
|
|
if (rows > 0)
|
|
|
{
|
|
|
await RebuildSearchIndexQuietly("hw_web_menu");
|
|
|
}
|
|
|
|
|
|
return rows;
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
/// 更新菜单信息。
|
|
|
/// <para>
|
|
|
/// 【字段级更新策略】
|
|
|
/// 只更新输入对象中不为 null 的字段。
|
|
|
/// </para>
|
|
|
/// <para>
|
|
|
/// 【搜索索引重建】
|
|
|
/// 菜单更新后,自动触发搜索索引重建。
|
|
|
/// </para>
|
|
|
/// </summary>
|
|
|
/// <param name="input">更新的数据</param>
|
|
|
/// <returns>影响行数</returns>
|
|
|
public async Task<int> UpdateHwWebMenu(HwWebMenu input)
|
|
|
{
|
|
|
// 回滚到 XML 方案时可直接恢复:
|
|
|
// int rows = await _executor.ExecuteAsync(Mapper, "updateHwWebMenu", input);
|
|
|
HwWebMenu current = await SelectHwWebMenuByWebMenuId(input.WebMenuId ?? 0);
|
|
|
if (current == null)
|
|
|
{
|
|
|
return 0;
|
|
|
}
|
|
|
|
|
|
// 【父菜单ID】
|
|
|
if (input.Parent.HasValue)
|
|
|
{
|
|
|
current.Parent = input.Parent;
|
|
|
}
|
|
|
|
|
|
// 【祖先路径】
|
|
|
if (input.Ancestors != null)
|
|
|
{
|
|
|
current.Ancestors = input.Ancestors;
|
|
|
}
|
|
|
|
|
|
// 【状态】
|
|
|
if (input.Status != null)
|
|
|
{
|
|
|
current.Status = input.Status;
|
|
|
}
|
|
|
|
|
|
// 【菜单名】
|
|
|
if (input.WebMenuName != null)
|
|
|
{
|
|
|
current.WebMenuName = input.WebMenuName;
|
|
|
}
|
|
|
|
|
|
// 【租户ID】
|
|
|
if (input.TenantId.HasValue)
|
|
|
{
|
|
|
current.TenantId = input.TenantId;
|
|
|
}
|
|
|
|
|
|
// 【菜单图片】
|
|
|
if (input.WebMenuPic != null)
|
|
|
{
|
|
|
current.WebMenuPic = input.WebMenuPic;
|
|
|
}
|
|
|
|
|
|
// 【菜单类型】
|
|
|
if (input.WebMenuType.HasValue)
|
|
|
{
|
|
|
current.WebMenuType = input.WebMenuType;
|
|
|
}
|
|
|
|
|
|
// 【排序号】
|
|
|
if (input.Order.HasValue)
|
|
|
{
|
|
|
current.Order = input.Order;
|
|
|
}
|
|
|
|
|
|
// 【英文名】
|
|
|
if (input.WebMenuNameEnglish != null)
|
|
|
{
|
|
|
current.WebMenuNameEnglish = input.WebMenuNameEnglish;
|
|
|
}
|
|
|
|
|
|
int rows = await _db.Updateable(current).ExecuteCommandAsync();
|
|
|
|
|
|
// 【搜索索引重建】
|
|
|
if (rows > 0)
|
|
|
{
|
|
|
await RebuildSearchIndexQuietly("hw_web_menu");
|
|
|
}
|
|
|
|
|
|
return rows;
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
/// 批量删除菜单(软删除)。
|
|
|
/// <para>
|
|
|
/// 【软删除实现】
|
|
|
/// 将 IsDelete 字段更新为"1",而不是物理删除。
|
|
|
/// </para>
|
|
|
/// <para>
|
|
|
/// 【搜索索引重建】
|
|
|
/// 菜单删除后,自动触发搜索索引重建。
|
|
|
/// </para>
|
|
|
/// </summary>
|
|
|
/// <param name="webMenuIds">菜单ID数组</param>
|
|
|
/// <returns>影响行数</returns>
|
|
|
public async Task<int> DeleteHwWebMenuByWebMenuIds(long[] webMenuIds)
|
|
|
{
|
|
|
// 回滚到 XML 方案时可直接恢复:
|
|
|
// int rows = await _executor.ExecuteAsync(Mapper, "deleteHwWebMenuByWebMenuIds", new { array = webMenuIds });
|
|
|
|
|
|
// 【查询待删除的菜单】
|
|
|
List<HwWebMenu> menus = await _db.Queryable<HwWebMenu>()
|
|
|
.Where(item => item.WebMenuId.HasValue && webMenuIds.Contains(item.WebMenuId.Value))
|
|
|
.ToListAsync();
|
|
|
|
|
|
// 【软删除标记】
|
|
|
foreach (HwWebMenu menu in menus)
|
|
|
{
|
|
|
menu.IsDelete = "1";
|
|
|
}
|
|
|
|
|
|
// 【批量更新】
|
|
|
int rows = menus.Count == 0 ? 0 : await _db.Updateable(menus)
|
|
|
.UpdateColumns(item => new { item.IsDelete })
|
|
|
.ExecuteCommandAsync();
|
|
|
|
|
|
// 【搜索索引重建】
|
|
|
if (rows > 0)
|
|
|
{
|
|
|
await RebuildSearchIndexQuietly("hw_web_menu");
|
|
|
}
|
|
|
|
|
|
return rows;
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
/// 构建菜单树形结构。
|
|
|
/// <para>
|
|
|
/// 【树形结构构建算法】
|
|
|
/// 1. 收集所有节点ID
|
|
|
/// 2. 遍历找出顶级节点(Parent为null/0或不在列表中)
|
|
|
/// 3. 对每个顶级节点递归构建子树
|
|
|
/// 4. 返回树形结构列表
|
|
|
/// </para>
|
|
|
/// </summary>
|
|
|
/// <param name="menus">扁平菜单列表</param>
|
|
|
/// <returns>树形菜单列表</returns>
|
|
|
public List<HwWebMenu> BuildWebMenuTree(List<HwWebMenu> menus)
|
|
|
{
|
|
|
List<HwWebMenu> returnList = new();
|
|
|
List<long?> tempList = menus.Select(item => item.WebMenuId).ToList();
|
|
|
foreach (HwWebMenu menu in menus)
|
|
|
{
|
|
|
// 【顶级节点判定】
|
|
|
if (!menu.Parent.HasValue || menu.Parent == 0 || !tempList.Contains(menu.Parent))
|
|
|
{
|
|
|
RecursionFn(menus, menu);
|
|
|
returnList.Add(menu);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return returnList.Count == 0 ? menus : returnList;
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
/// 递归构建子树。
|
|
|
/// </summary>
|
|
|
/// <param name="list">所有节点列表</param>
|
|
|
/// <param name="current">当前节点</param>
|
|
|
private static void RecursionFn(List<HwWebMenu> list, HwWebMenu current)
|
|
|
{
|
|
|
List<HwWebMenu> childList = GetChildList(list, current);
|
|
|
current.Children = childList;
|
|
|
foreach (HwWebMenu child in childList.Where(child => HasChild(list, child)))
|
|
|
{
|
|
|
RecursionFn(list, child);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
/// 获取子节点列表。
|
|
|
/// </summary>
|
|
|
/// <param name="list">所有节点列表</param>
|
|
|
/// <param name="current">当前节点</param>
|
|
|
/// <returns>子节点列表</returns>
|
|
|
private static List<HwWebMenu> GetChildList(List<HwWebMenu> list, HwWebMenu current)
|
|
|
{
|
|
|
return list.Where(item => item.Parent.HasValue && current.WebMenuId.HasValue && item.Parent.Value == current.WebMenuId.Value).ToList();
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
/// 判断是否有子节点。
|
|
|
/// </summary>
|
|
|
/// <param name="list">所有节点列表</param>
|
|
|
/// <param name="current">当前节点</param>
|
|
|
/// <returns>是否有子节点</returns>
|
|
|
private static bool HasChild(List<HwWebMenu> list, HwWebMenu current)
|
|
|
{
|
|
|
return GetChildList(list, current).Count > 0;
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
/// 静默重建搜索索引。
|
|
|
/// </summary>
|
|
|
/// <param name="source">数据来源</param>
|
|
|
private async Task RebuildSearchIndexQuietly(string source)
|
|
|
{
|
|
|
try
|
|
|
{
|
|
|
await _searchRebuildService.RebuildAllAsync();
|
|
|
}
|
|
|
catch (Exception ex)
|
|
|
{
|
|
|
_logger.LogError(ex, "rebuild portal search index failed after {Source} changed", source);
|
|
|
}
|
|
|
}
|
|
|
}
|