You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

389 lines
13 KiB
C#

// ============================================================================
// 【文件说明】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父菜单IDnull或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);
}
}
}