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#

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

// ============================================================================
// 【文件说明】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);
}
}
}