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.

527 lines
20 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.

// ============================================================================
// 【文件说明】HwWebMenu1Service.cs - 网站菜单服务类(变体版本)
// ============================================================================
// 这个服务类负责处理网站菜单的业务逻辑(变体版本),包括:
// - 菜单的 CRUD 操作
// - 树形菜单结构构建
// - 软删除支持
//
// 【业务背景】
// 网站菜单模块用于管理网站的前端导航菜单,支持多级树形结构。
// 这个服务是 HwWebMenuService 的变体版本,可能用于不同的菜单场景。
//
// 【与 Java Spring Boot 的对比】
// Java Spring Boot:
// @Service
// public class HwWebMenu1ServiceImpl implements HwWebMenu1Service { ... }
//
// ASP.NET Core + Furion:
// public class HwWebMenu1Service : ITransient { ... }
// ============================================================================
namespace Admin.NET.Plugin.HwPortal;
/// <summary>
/// 网站菜单服务类(变体版本)。
/// <para>
/// 【服务职责】
/// 1. 管理网站菜单的增删改查
/// 2. 构建树形菜单结构
/// 3. 支持软删除IsDelete 字段标记)
/// </para>
/// <para>
/// 【树形结构说明】
/// - Parent父菜单IDnull或0表示顶级菜单
/// - Ancestors祖先路径格式为"0,1,2"
/// - 支持多级嵌套菜单结构
/// </para>
/// <para>
/// 【与 HwWebMenuService 的区别】
/// 这个服务是菜单服务的变体版本,可能用于:
/// - 不同的菜单类型(如前台菜单 vs 后台菜单)
/// - 不同的租户或站点
/// - 不同的业务场景
/// </para>
/// </summary>
public class HwWebMenu1Service : ITransient
{
/// <summary>
/// MyBatis 映射器名称(保留用于回滚)。
/// <para>
/// 【C# 语法知识点 - const 常量】
/// const 是"编译期常量",值在编译时就确定了。
///
/// 对比 Java
/// Java: private static final String MAPPER = "HwWebMenuMapper1";
/// C#: private const string Mapper = "HwWebMenuMapper1";
///
/// C# 的命名约定:
/// - const 通常用 PascalCase首字母大写
/// - Java 的 static final 通常用 UPPER_SNAKE_CASE
/// </para>
/// </summary>
private const string Mapper = "HwWebMenuMapper1";
/// <summary>
/// MyBatis 执行器(保留用于回滚)。
/// <para>
/// 【依赖注入】
/// 通过构造函数注入,和控制器注入服务的方式一样。
///
/// HwPortalMyBatisExecutor 是自定义的执行器,封装了 MyBatis 风格的 SQL 执行逻辑。
/// 当前代码中已注释掉使用,但保留以备回滚。
/// </para>
/// </summary>
private readonly HwPortalMyBatisExecutor _executor;
/// <summary>
/// SqlSugar 数据访问对象。
/// <para>
/// 【ISqlSugarClient 接口】
/// 这是 SqlSugar 的核心接口,提供数据库访问能力。
///
/// 对比 Java
/// Java MyBatis: SqlSession 或 Mapper 接口
/// C# SqlSugar: ISqlSugarClient
///
/// 通过依赖注入获取,由框架在启动时配置并注册到 DI 容器。
/// </para>
/// </summary>
private readonly ISqlSugarClient _db;
/// <summary>
/// 构造函数(依赖注入)。
/// <para>
/// 【C# 语法知识点 - 构造函数】
/// public HwWebMenu1Service(HwPortalMyBatisExecutor executor, ISqlSugarClient db)
///
/// 对比 Java
/// Java:
/// @Autowired
/// public HwWebMenu1Service(HwPortalMyBatisExecutor executor, ISqlSugarClient db) {
/// this.executor = executor;
/// this.db = db;
/// }
///
/// C#:
/// public HwWebMenu1Service(HwPortalMyBatisExecutor executor, ISqlSugarClient db)
/// {
/// _executor = executor;
/// _db = db;
/// }
///
/// C# 的改进:
/// 1. 不需要 @Autowired 注解,框架自动识别构造函数
/// 2. 使用 _executor 命名约定表示私有字段
/// 3. 可以使用主构造函数C# 12+)进一步简化
/// </para>
/// </summary>
/// <param name="executor">MyBatis 执行器(保留用于回滚)</param>
/// <param name="db">SqlSugar 数据访问对象</param>
public HwWebMenu1Service(HwPortalMyBatisExecutor executor, ISqlSugarClient db)
{
_executor = executor;
_db = db;
}
/// <summary>
/// 根据菜单ID查询菜单信息。
/// <para>
/// 【方法命名约定】
/// Select + 实体名 + By + 主键名:根据主键查询单条记录
///
/// 对比 Java 若依:
/// 若依通常使用selectXxxById、getXxxById
/// 这里使用SelectXxxByWebMenuId更符合 C# 的 PascalCase 命名规范
/// </para>
/// <para>
/// 【软删除过滤】
/// 查询时自动过滤已删除的记录IsDelete == "0")。
/// 这是软删除的标准做法。
/// </para>
/// </summary>
/// <param name="webMenuId">菜单ID</param>
/// <returns>菜单实体</returns>
public async Task<HwWebMenu1> SelectHwWebMenuByWebMenuId(long webMenuId)
{
// 回滚到 XML 方案时可直接恢复:
// return await _executor.QuerySingleAsync<HwWebMenu1>(Mapper, "selectHwWebMenuByWebMenuId", new { webMenuId });
// 【SqlSugar 查询语法】
// _db.Queryable<HwWebMenu1>():创建一个针对 HwWebMenu1 表的查询
// .Where(item => item.IsDelete == "0" && item.WebMenuId == webMenuId):添加 WHERE 条件
// .FirstAsync():执行查询,返回第一条记录,如果没有则返回 null
//
// 生成的 SQL 类似于:
// SELECT * FROM hw_web_menu1 WHERE IsDelete = '0' AND WebMenuId = @webMenuId LIMIT 1
//
// 对比 Java MyBatis
// Java: mapper.selectOne(new QueryWrapper<HwWebMenu1>().eq("IsDelete", "0").eq("WebMenuId", webMenuId));
// C#: _db.Queryable<HwWebMenu1>().Where(item => item.IsDelete == "0" && item.WebMenuId == webMenuId).FirstAsync();
//
// C# 的优势Lambda 表达式是强类型的,拼写错误在编译期就能发现
return await _db.Queryable<HwWebMenu1>()
.Where(item => item.IsDelete == "0" && item.WebMenuId == webMenuId)
.FirstAsync();
}
/// <summary>
/// 查询菜单列表。
/// <para>
/// 【动态查询条件】
/// 支持按父ID、祖先路径、状态、菜单名、租户ID、图片、类型、值、英文名等条件筛选。
/// 所有条件都是可选的,有值时才添加到 WHERE 子句。
/// </para>
/// <para>
/// 【软删除过滤】
/// 默认只查询未删除的记录IsDelete == "0")。
/// </para>
/// </summary>
/// <param name="input">查询条件</param>
/// <returns>菜单列表</returns>
public async Task<List<HwWebMenu1>> SelectHwWebMenuList(HwWebMenu1 input)
{
// 【防御性编程】
// 确保 query 不为 null避免后续空引用异常
HwWebMenu1 query = input ?? new HwWebMenu1();
// 回滚到 XML 方案时可直接恢复:
// return await _executor.QueryListAsync<HwWebMenu1>(Mapper, "selectHwWebMenuList", query);
// 【SqlSugar WhereIF 动态条件】
// .WhereIF(condition, expression):只有当 condition 为 true 时,才添加 WHERE 条件
//
// 对比 Java MyBatis 的 XML
// <if test="parent != null">
// AND Parent = #{parent}
// </if>
//
// C# SqlSugar 的 WhereIF 更直观,而且类型安全
//
// 【string.IsNullOrWhiteSpace】
// 检查字符串是否为 null、空字符串 "" 或仅包含空白字符
//
// 对比 Java
// Java: StringUtils.isBlank(str)(需要 Apache Commons Lang
// C#: string.IsNullOrWhiteSpace(str)(内置)
//
// 【Contains 模糊查询】
// item.WebMenuName.Contains(query.WebMenuName)
// 生成 SQLWHERE WebMenuName LIKE '%xxx%'
//
// 【HasValue 可空类型检查】
// query.Parent.HasValue 检查可空 long? 是否有值
//
// 对比 Java
// Java: query.getParent() != null
// C#: query.Parent.HasValue
//
// 【ToListAsync】
// 执行查询并返回列表,异步版本
return await _db.Queryable<HwWebMenu1>()
.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(!string.IsNullOrWhiteSpace(query.Value), item => item.Value.Contains(query.Value))
.WhereIF(!string.IsNullOrWhiteSpace(query.WebMenuNameEnglish), item => item.WebMenuNameEnglish.Contains(query.WebMenuNameEnglish))
.ToListAsync();
}
/// <summary>
/// 查询菜单树。
/// <para>
/// 【树形结构构建】
/// 先查询菜单列表,然后调用 BuildWebMenuTree 构建树形结构。
/// </para>
/// </summary>
/// <param name="input">查询条件</param>
/// <returns>树形菜单列表</returns>
public async Task<List<HwWebMenu1>> SelectMenuTree(HwWebMenu1 input)
{
// 查询菜单列表
List<HwWebMenu1> menus = await SelectHwWebMenuList(input);
// 构建并返回树形结构
return BuildWebMenuTree(menus);
}
/// <summary>
/// 新增菜单。
/// <para>
/// 【软删除标记初始化】
/// 如果未指定删除标记,默认为"0"(未删除)。
/// 这是软删除的标准做法。
/// </para>
/// </summary>
/// <param name="input">菜单数据</param>
/// <returns>影响行数</returns>
public async Task<int> InsertHwWebMenu(HwWebMenu1 input)
{
// 【软删除标记初始化】
// string.IsNullOrWhiteSpace 检查是否为 null、空字符串或仅包含空白字符
// ?? 是空合并运算符:如果左边为 null返回右边
input.IsDelete = string.IsNullOrWhiteSpace(input.IsDelete) ? "0" : input.IsDelete;
// 回滚到 XML 方案时可直接恢复:
// return await _executor.ExecuteAsync(Mapper, "insertHwWebMenu", input);
// 【SqlSugar 插入语法】
// _db.Insertable(input):创建一个插入操作
// .ExecuteCommandAsync():执行插入,返回影响行数
//
// 生成的 SQL 类似于:
// INSERT INTO hw_web_menu1 (Parent, Ancestors, Status, WebMenuName, ...) VALUES (@Parent, @Ancestors, ...)
return await _db.Insertable(input).ExecuteCommandAsync();
}
/// <summary>
/// 更新菜单信息。
/// <para>
/// 【字段级更新策略】
/// 1. 先查询现有记录
/// 2. 对非空字段进行更新
/// 3. null 值表示"不更新此字段"
///
/// 这种设计支持部分更新PATCH 语义)。
/// </para>
/// </summary>
/// <param name="input">更新的数据</param>
/// <returns>影响行数</returns>
public async Task<int> UpdateHwWebMenu(HwWebMenu1 input)
{
// 回滚到 XML 方案时可直接恢复:
// return await _executor.ExecuteAsync(Mapper, "updateHwWebMenu", input);
// 查询现有记录
HwWebMenu1 current = await SelectHwWebMenuByWebMenuId(input.WebMenuId ?? 0);
if (current == null)
{
return 0;
}
// 【父菜单ID】
// HasValue 检查可空类型是否有值
if (input.Parent.HasValue)
{
current.Parent = input.Parent;
}
// 【祖先路径】
// != null 检查引用类型是否为 null
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.Value != null)
{
current.Value = input.Value;
}
// 【英文名】
if (input.WebMenuNameEnglish != null)
{
current.WebMenuNameEnglish = input.WebMenuNameEnglish;
}
// 【执行更新】
// _db.Updateable(current):创建更新操作
// .ExecuteCommandAsync():执行更新,返回影响行数
return await _db.Updateable(current).ExecuteCommandAsync();
}
/// <summary>
/// 批量删除菜单(软删除)。
/// <para>
/// 【软删除实现】
/// 不是物理删除,而是将 IsDelete 字段更新为"1"。
/// 这样可以保留数据用于审计,也支持恢复操作。
///
/// 对比 Java MyBatis
/// Java 通常也是 UPDATE table SET is_delete = '1' WHERE id IN (...)
/// </para>
/// </summary>
/// <param name="webMenuIds">菜单ID数组</param>
/// <returns>影响行数</returns>
public async Task<int> DeleteHwWebMenuByWebMenuIds(long[] webMenuIds)
{
// 回滚到 XML 方案时可直接恢复:
// return await _executor.ExecuteAsync(Mapper, "deleteHwWebMenuByWebMenuIds", new { array = webMenuIds });
// 【查询待删除的菜单】
// .Where(item => item.WebMenuId.HasValue && webMenuIds.Contains(item.WebMenuId.Value))
// 条件WebMenuId 有值,且在传入的 ID 数组中
List<HwWebMenu1> menus = await _db.Queryable<HwWebMenu1>()
.Where(item => item.WebMenuId.HasValue && webMenuIds.Contains(item.WebMenuId.Value))
.ToListAsync();
// 【软删除标记】
// 将 IsDelete 字段设置为 "1",表示已删除
foreach (HwWebMenu1 menu in menus)
{
menu.IsDelete = "1";
}
// 【批量更新】
// 只更新 IsDelete 字段,使用 UpdateColumns 指定更新字段
// 如果 menus 为空,返回 0否则执行批量更新
return menus.Count == 0 ? 0 : await _db.Updateable(menus)
.UpdateColumns(item => new { item.IsDelete })
.ExecuteCommandAsync();
}
/// <summary>
/// 构建菜单树形结构。
/// <para>
/// 【树形结构构建算法】
/// 这是一个经典的"扁平列表转树形结构"算法:
///
/// 输入:扁平的节点列表,每个节点有 parentId 指向父节点
/// 输出:树形结构,顶级节点包含 children 子节点
///
/// 算法步骤:
/// 1. 收集所有节点ID到 tempList
/// 2. 遍历节点列表找出所有顶级节点parentId 为 null、0 或不在列表中)
/// 3. 对每个顶级节点,调用 RecursionFn 递归构建子树
/// 4. 返回树形结构列表
///
/// 对比 Java 若依:
/// 若依的 TreeBuildUtils.buildTree() 做同样的事情。
/// 这是若依框架的标准树构建算法。
/// </para>
/// </summary>
/// <param name="menus">扁平菜单列表</param>
/// <returns>树形菜单列表</returns>
public List<HwWebMenu1> BuildWebMenuTree(List<HwWebMenu1> menus)
{
// 结果列表
List<HwWebMenu1> returnList = new();
// 【收集所有节点ID】
// .Select(item => item.WebMenuId) 提取所有 ID
// .ToList() 转换为列表
List<long?> tempList = menus.Select(item => item.WebMenuId).ToList();
// 【遍历查找顶级节点】
foreach (HwWebMenu1 menu in menus)
{
// 【顶级节点判定】
// 满足以下任一条件即为顶级节点:
// 1. !menu.Parent.HasValueparent 为 null
// 2. menu.Parent == 0parent 为 0
// 3. !tempList.Contains(menu.Parent)parent 不在列表中(父节点不存在)
if (!menu.Parent.HasValue || menu.Parent == 0 || !tempList.Contains(menu.Parent))
{
// 递归构建子树
RecursionFn(menus, menu);
returnList.Add(menu);
}
}
// 如果没找到顶级节点,返回原列表(兜底处理)
return returnList.Count == 0 ? menus : returnList;
}
/// <summary>
/// 递归构建子树。
/// <para>
/// 【递归算法说明】
/// 递归是一种"自己调用自己"的编程技巧。
///
/// 这里的递归逻辑:
/// 1. 找出当前节点的所有直接子节点
/// 2. 把子节点挂到当前节点的 Children 属性
/// 3. 对每个子节点,递归执行步骤 1-2
///
/// 终止条件节点没有子节点HasChild 返回 false
/// </para>
/// </summary>
/// <param name="list">所有节点列表</param>
/// <param name="current">当前节点</param>
private static void RecursionFn(List<HwWebMenu1> list, HwWebMenu1 current)
{
// 找出当前节点的所有直接子节点
List<HwWebMenu1> childList = GetChildList(list, current);
// 把子节点挂到当前节点
current.Children = childList;
// 【递归调用】
// 对每个有子节点的子节点,继续递归构建子树
// .Where(child => HasChild(list, child)) 筛选有子节点的节点
foreach (HwWebMenu1 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<HwWebMenu1> GetChildList(List<HwWebMenu1> list, HwWebMenu1 current)
{
// 【LINQ Where 过滤】
// item.Parent.Value == current.WebMenuId.Value
// 条件item 的 parent 等于 current 的 id
//
// .HasValue 检查可空类型是否有值(不为 null
// .Value 获取可空类型的实际值
//
// 对比 Java
// Java: list.stream().filter(item -> item.getParent() != null && current.getWebMenuId() != null && item.getParent().equals(current.getWebMenuId())).collect(Collectors.toList());
//
// C# 的可空类型处理更优雅
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<HwWebMenu1> list, HwWebMenu1 current)
{
return GetChildList(list, current).Count > 0;
}
}