// ============================================================================ // 【文件说明】HwWebService.cs - 官网页面业务服务类 // ============================================================================ // 这个服务类负责处理官网页面的业务逻辑。 // // 【分层架构说明】 // Controller(控制器层)-> Service(服务层)-> Repository(数据访问层) // // 控制器只负责:接收请求、调用服务、返回响应 // 服务层负责:业务逻辑、数据组装、事务控制 // 数据访问层负责:数据库 CRUD 操作 // // 【与 Java Spring Boot 的对比】 // Java Spring Boot: // @Service // public class HwWebServiceImpl implements HwWebService { ... } // // ASP.NET Core + Furion: // public class HwWebService : ITransient { ... } // // ITransient 是 Furion 的"生命周期接口",表示"瞬态服务": // - 每次请求都创建新实例 // - 类似 Java Spring 的 @Scope("prototype") // // Furion 支持三种生命周期: // - ITransient:瞬态,每次请求新实例 // - IScoped:作用域,同一请求内共享实例 // - ISingleton:单例,全局共享一个实例 // ============================================================================ namespace Admin.NET.Plugin.HwPortal; /// /// 官网页面业务服务类。 /// /// 【C# 语法知识点 - 接口实现】 /// public class HwWebService : ITransient /// /// ITransient 是 Furion 框架的"标记接口"(Marker Interface)。 /// 它没有任何方法,只是用来标记服务的生命周期。 /// /// 对比 Java Spring: /// Java Spring 用 @Service + @Scope 注解来定义服务: /// @Service /// @Scope("prototype") // 等价于 ITransient /// public class HwWebService { ... } /// /// C# Furion 用接口来标记,更符合"接口隔离原则"。 /// /// /// 【MyBatis 风格的数据访问】 /// 这个服务使用 HwPortalMyBatisExecutor 执行 SQL。 // 这是一种"类 MyBatis"的设计: /// - SQL 写在 XML 文件中(Sql/HwWebMapper.xml) /// - 通过执行器调用 XML 中定义的 SQL /// - 参数通过匿名对象传递 /// /// 对比 Java MyBatis: /// Java MyBatis: /// @Mapper /// public interface HwWebMapper { /// HwWeb selectHwWebByWebcode(@Param("webCode") Long webCode); /// } /// /// C# 这里用执行器模式,更灵活但需要手动调用。 /// /// public class HwWebService : ITransient { /// /// MyBatis 映射器名称。 /// /// 【C# 语法知识点 - const 常量】 /// const 是"编译期常量",值在编译时就确定了。 /// /// 对比 Java: /// Java: private static final String MAPPER = "HwWebMapper"; /// C#: private const string Mapper = "HwWebMapper"; /// /// C# 的命名约定: /// - const 通常用 PascalCase(首字母大写) /// - Java 的 static final 通常用 UPPER_SNAKE_CASE /// /// private const string Mapper = "HwWebMapper"; /// /// MyBatis 执行器。 /// /// 【依赖注入】 /// 通过构造函数注入,和控制器注入服务的方式一样。 /// /// HwPortalMyBatisExecutor 是自定义的执行器,封装了 MyBatis 风格的 SQL 执行逻辑。 /// /// private readonly HwPortalMyBatisExecutor _executor; /// /// SqlSugar 数据访问对象。 /// private readonly ISqlSugarClient _db; /// /// 搜索索引重建服务。 /// private readonly IHwSearchRebuildService _searchRebuildService; /// /// 日志记录器。 /// /// 【C# 语法知识点 - 泛型日志接口】 /// ILogger<HwWebService> 是 Microsoft.Extensions.Logging 提供的日志接口。 /// 泛型参数 T 用于标识日志的类别,通常传入当前类。 /// /// 使用方式: /// _logger.LogInformation("信息日志"); /// _logger.LogWarning("警告日志"); /// _logger.LogError("错误日志"); /// _logger.LogError(ex, "错误日志,带异常信息"); /// /// 对比 Java SLF4J: /// Java: private static final Logger log = LoggerFactory.getLogger(HwWebService.class); /// C#: private readonly ILogger<HwWebService> _logger; /// /// 两者的用法类似,但 C# 的 ILogger 是接口,通过 DI 注入。 /// /// private readonly ILogger _logger; /// /// 构造函数(依赖注入)。 /// /// MyBatis 执行器 /// 搜索索引重建服务 /// 日志记录器 public HwWebService(HwPortalMyBatisExecutor executor, ISqlSugarClient db, IHwSearchRebuildService searchRebuildService, ILogger logger) { _executor = executor; _db = db; _searchRebuildService = searchRebuildService; _logger = logger; } /// /// 根据页面编码查询页面信息。 /// /// 页面编码 /// 页面实体 public async Task SelectHwWebByWebcode(long webCode) { // 【匿名对象传参】 // new { webCode } 是 C# 的"匿名对象"语法。 // 编译器会自动创建一个包含 webCode 属性的临时类。 // // 对比 Java: // Java 没有匿名对象语法,通常用: // - Map<String, Object> params = new HashMap<>(); // - params.put("webCode", webCode); // 或者: // - @Param("webCode") Long webCode(MyBatis 注解) // // C# 的匿名对象更简洁,编译器自动推断类型。 // 回滚到 XML 方案时可直接恢复: // return await _executor.QuerySingleAsync(Mapper, "selectHwWebByWebcode", new { webCode }); return await _db.Queryable() .Where(item => item.IsDelete == "0" && item.WebCode == webCode) .FirstAsync(); } /// /// 查询页面列表。 /// /// 查询条件 /// 页面列表 public async Task> SelectHwWebList(HwWeb input) { // 【空合并运算符 ??】 // input ?? new HwWeb() 含义: // 如果 input 不为 null,就用 input; // 否则 new 一个默认的 HwWeb 对象。 // // 对比 Java: // Java 需要手写: // if (input == null) input = new HwWeb(); // 或者用 Optional: // input = Optional.ofNullable(input).orElse(new HwWeb()); HwWeb query = input ?? new HwWeb(); // 回滚到 XML 方案时可直接恢复: // return await _executor.QueryListAsync(Mapper, "selectHwWebList", query); return await _db.Queryable() .Where(item => item.IsDelete == "0") .WhereIF(query.WebId.HasValue, item => item.WebId == query.WebId) .WhereIF(query.WebJson != null && query.WebJson != string.Empty, item => item.WebJson == query.WebJson) .WhereIF(query.WebJsonString != null && query.WebJsonString != string.Empty, item => item.WebJsonString == query.WebJsonString) .WhereIF(query.WebCode.HasValue, item => item.WebCode == query.WebCode) .WhereIF(query.WebJsonEnglish != null, item => item.WebJsonEnglish == query.WebJsonEnglish) .ToListAsync(); } /// /// 新增页面。 /// /// 页面数据 /// 影响行数(1=成功,0=失败) public async Task InsertHwWeb(HwWeb input) { // 【async/await 异步编程】 // await 会等待异步操作完成,然后继续执行后续代码。 // // InsertReturnIdentityAsync 返回自增主键值。 // 对比 Java MyBatis: // Java MyBatis 需要配置 useGeneratedKeys="true" keyProperty="webId" // 然后通过 input.getWebId() 获取主键。 input.IsDelete = string.IsNullOrWhiteSpace(input.IsDelete) ? "0" : input.IsDelete; // 回滚到 XML 方案时可直接恢复: // long identity = await _executor.InsertReturnIdentityAsync(Mapper, "insertHwWeb", input); HwWeb entity = await _db.Insertable(input).ExecuteReturnEntityAsync(); // 把自增主键回填到实体对象。 // 这样调用方可以获取到新插入记录的 ID。 input.WebId = entity.WebId; if (input.WebId > 0) { // 【静默重建搜索索引】 // 新增成功后,触发搜索索引重建。 // "静默"表示:索引重建失败不影响主业务,只记录日志。 await RebuildSearchIndexQuietly("hw_web"); } // 返回 1 表示成功,0 表示失败。 return input.WebId > 0 ? 1 : 0; } /// /// 更新页面。 /// /// 【版本化更新策略】 /// 这里的更新不是传统 SQL UPDATE,而是: /// 1. 按 webCode 找旧记录 /// 2. 逻辑删旧记录 /// 3. 插入新记录 /// /// 为什么这样做? /// 1. 保留历史版本:旧记录还在,可以回滚 /// 2. 避免缓存失效:旧 ID 的缓存自然过期 /// 3. 保持引用稳定:其他表引用的是 webCode,不是 webId /// /// 这是一种"追加写入"的设计模式,适合需要审计追踪的场景。 /// /// /// 页面数据 /// 影响行数 public async Task UpdateHwWeb(HwWeb input) { // 【对象初始化器】 // HwWeb query = new() { WebCode = input.WebCode }; // 等价于: // HwWeb query = new HwWeb(); // query.WebCode = input.WebCode; // // new() 是 C# 9.0 的"目标类型 new"语法,编译器自动推断类型。 HwWeb query = new() { WebCode = input.WebCode }; // 查询现有记录。 List exists = await SelectHwWebList(query); if (exists.Count > 0) { // 【LINQ 链式操作】 // exists.Where(...).Select(...).ToArray() // // Where:筛选符合条件的元素 // Select:转换元素(这里是提取 WebId 值) // ToArray:转换为数组 // // 对比 Java Stream: // exists.stream() // .filter(u -> u.getWebId() != null) // .map(u -> u.getWebId()) // .toArray(Long[]::new); // // C# 的 LINQ 和 Java Stream 非常相似。 // // 【! 非空断言】 // u.WebId!.Value 中的 ! 是"非空断言"。 // 告诉编译器:我确定这里不为 null,不要警告我。 // 因为 Where 条件已经过滤了 HasValue 为 false 的项。 await DeleteHwWebByWebIds(exists.Where(u => u.WebId.HasValue).Select(u => u.WebId!.Value).ToArray(), false); } // 确保新记录是有效状态(is_delete = "0")。 input.IsDelete = "0"; // 回滚到 XML 方案时可直接恢复: // long identity = await _executor.InsertReturnIdentityAsync(Mapper, "insertHwWeb", input); HwWeb entity = await _db.Insertable(input).ExecuteReturnEntityAsync(); input.WebId = entity.WebId; if (input.WebId > 0) { await RebuildSearchIndexQuietly("hw_web"); } return input.WebId > 0 ? 1 : 0; } /// /// 批量删除页面(公开方法,默认重建索引)。 /// /// 页面ID数组 /// 影响行数 public Task DeleteHwWebByWebIds(long[] webIds) { // 这里故意再包一层公开方法,把"是否重建索引"的内部开关藏起来。 // 对外调用统一默认 true,只有内部特殊场景才传 false。 return DeleteHwWebByWebIds(webIds, true); } /// /// 批量删除页面(私有方法,可控制是否重建索引)。 /// /// 页面ID数组 /// 是否重建搜索索引 /// 影响行数 private async Task DeleteHwWebByWebIds(long[] webIds, bool rebuild) { // 执行删除 SQL。 // 回滚到 XML 方案时可直接恢复: // int rows = await _executor.ExecuteAsync(Mapper, "deleteHwWebByWebIds", new { array = webIds }); List pages = await _db.Queryable() .Where(item => item.WebId.HasValue && webIds.Contains(item.WebId.Value)) .ToListAsync(); foreach (HwWeb page in pages) { page.IsDelete = "1"; } int rows = pages.Count == 0 ? 0 : await _db.Updateable(pages) .UpdateColumns(item => new { item.IsDelete }) .ExecuteCommandAsync(); if (rows > 0 && rebuild) { // rebuild=false 的场景只出现在"更新前先删旧版本"这条内部链路。 // 那里最后会统一在新记录插入成功后重建一次索引,所以这里不能重复触发。 await RebuildSearchIndexQuietly("hw_web"); } return rows; } /// /// 静默重建搜索索引。 /// /// 【"静默"的含义】 /// "静默"(Quietly)表示:索引重建失败不抛异常,只记录日志。 /// /// 为什么静默? /// 1. 搜索索引是附属能力:主业务(增删改)成功就够了 /// 2. 避免事务回滚:如果索引重建失败导致事务回滚,用户体验不好 /// 3. 可以后续修复:索引问题可以后台修复,不影响用户操作 /// /// 这是"最终一致性"的设计思想: /// 主业务强一致,附属能力最终一致。 /// /// /// 触发重建的数据源(用于日志记录) private async Task RebuildSearchIndexQuietly(string source) { try { // 尝试重建所有搜索索引。 await _searchRebuildService.RebuildAllAsync(); } catch (Exception ex) { // 【异常处理】 // 这里用 ILogger 记录错误日志,而不是 throw。 // // _logger.LogError(ex, "message", args...) 参数说明: // - ex:异常对象,会自动提取堆栈信息 // - "message":日志消息模板 // - args:模板参数(类似 String.Format) // // 对比 Java SLF4J: // Java: log.error("rebuild portal search index failed after {} changed", source, ex); // C#: _logger.LogError(ex, "rebuild portal search index failed after {Source} changed", source); // // C# 的消息模板用 {Source} 占位符,Java 用 {}。 _logger.LogError(ex, "rebuild portal search index failed after {Source} changed", source); } } }