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