// ============================================================================ // 【文件说明】LegacyHwSearchQueryService.cs - 旧版搜索查询服务 // ============================================================================ // 这是一个"遗留系统"(Legacy)的搜索服务,使用原始 SQL 实现搜索。 // // 【什么是遗留系统?】 // 遗留系统是指: // - 旧版本的功能实现 // - 新版本保留它作为兼容或降级方案 // - 通常在特定场景下使用 // // 【为什么保留旧版搜索?】 // 1. 兼容性:新搜索可能依赖额外的索引表,旧环境可能没有 // 2. 降级方案:新搜索失败时,可以回退到旧版 // 3. 平滑迁移:逐步切换到新搜索,而不是一刀切 // // 【与新版搜索的区别】 // - 新版:使用 EF Core 查询索引表,支持复杂查询和排序 // - 旧版:使用 MyBatis 风格的 SQL,直接查询业务表 // // 【与 Java Spring Boot 的对比】 // Java 若依的搜索通常是直接 SQL: // @Select("SELECT * FROM hw_web WHERE title LIKE CONCAT('%', #{keyword}, '%')") // List searchByKeyword(@Param("keyword") String keyword); // // 这个类就是类似的实现方式。 // ============================================================================ namespace Admin.NET.Plugin.HwPortal; /// /// 旧版搜索查询服务。 /// /// 【设计模式 - 适配器模式】 /// 这个类把旧版 SQL 搜索适配到新的服务架构中: /// - 对外提供统一的搜索接口 /// - 内部使用旧的 SQL 实现 /// /// 这样可以在不修改调用方的情况下,平滑切换搜索实现。 /// /// /// 【C# 语法知识点 - sealed 密封类】 /// sealed 表示不能被继承。遗留服务通常不需要扩展。 /// /// /// 【C# 语法知识点 - ITransient 瞬态服务】 /// ITransient 表示每次请求都创建新实例。 /// 搜索服务通常是无状态的,用 ITransient 或 IScoped 都可以。 /// /// public sealed class LegacyHwSearchQueryService : ITransient { /// /// MyBatis 映射器名称。 /// private const string Mapper = "HwSearchMapper"; private const string SearchByKeywordSql = """ SELECT * FROM ( SELECT CONVERT('web' USING utf8mb4) COLLATE utf8mb4_general_ci AS source_type, CONVERT(CAST(w.web_id AS CHAR) USING utf8mb4) COLLATE utf8mb4_general_ci AS biz_id, CONVERT(CONCAT('页面#', w.web_code) USING utf8mb4) COLLATE utf8mb4_general_ci AS title, CONVERT(IFNULL(w.web_json_string, '') USING utf8mb4) COLLATE utf8mb4_general_ci AS content, CONVERT(CAST(w.web_code AS CHAR) USING utf8mb4) COLLATE utf8mb4_general_ci AS web_code, CAST(NULL AS CHAR CHARACTER SET utf8mb4) COLLATE utf8mb4_general_ci AS type_id, CAST(NULL AS CHAR CHARACTER SET utf8mb4) COLLATE utf8mb4_general_ci AS device_id, CAST(NULL AS CHAR CHARACTER SET utf8mb4) COLLATE utf8mb4_general_ci AS menu_id, CAST(NULL AS CHAR CHARACTER SET utf8mb4) COLLATE utf8mb4_general_ci AS document_id, 80 AS score, w.update_time AS updated_at FROM hw_web w WHERE w.is_delete = '0' AND w.web_json_string LIKE CONCAT('%', @keyword, '%') UNION ALL SELECT CONVERT('web1' USING utf8mb4) COLLATE utf8mb4_general_ci AS source_type, CONVERT(CAST(w1.web_id AS CHAR) USING utf8mb4) COLLATE utf8mb4_general_ci AS biz_id, CONVERT(CONCAT('详情#', w1.web_code, '-', w1.typeId, '-', w1.device_id) USING utf8mb4) COLLATE utf8mb4_general_ci AS title, CONVERT(IFNULL(w1.web_json_string, '') USING utf8mb4) COLLATE utf8mb4_general_ci AS content, CONVERT(CAST(w1.web_code AS CHAR) USING utf8mb4) COLLATE utf8mb4_general_ci AS web_code, CONVERT(CAST(w1.typeId AS CHAR) USING utf8mb4) COLLATE utf8mb4_general_ci AS type_id, CONVERT(CAST(w1.device_id AS CHAR) USING utf8mb4) COLLATE utf8mb4_general_ci AS device_id, CAST(NULL AS CHAR CHARACTER SET utf8mb4) COLLATE utf8mb4_general_ci AS menu_id, CAST(NULL AS CHAR CHARACTER SET utf8mb4) COLLATE utf8mb4_general_ci AS document_id, 90 AS score, w1.update_time AS updated_at FROM hw_web1 w1 WHERE w1.is_delete = '0' AND w1.web_json_string LIKE CONCAT('%', @keyword, '%') UNION ALL SELECT CONVERT('document' USING utf8mb4) COLLATE utf8mb4_general_ci AS source_type, CONVERT(IFNULL(d.document_id, '') USING utf8mb4) COLLATE utf8mb4_general_ci AS biz_id, CONVERT(IFNULL(NULLIF(d.json, ''), d.document_id) USING utf8mb4) COLLATE utf8mb4_general_ci AS title, CONVERT(IFNULL(d.json, '') USING utf8mb4) COLLATE utf8mb4_general_ci AS content, CONVERT(IFNULL(d.web_code, '') USING utf8mb4) COLLATE utf8mb4_general_ci AS web_code, CONVERT(IFNULL(d.type, '') USING utf8mb4) COLLATE utf8mb4_general_ci AS type_id, CAST(NULL AS CHAR CHARACTER SET utf8mb4) COLLATE utf8mb4_general_ci AS device_id, CAST(NULL AS CHAR CHARACTER SET utf8mb4) COLLATE utf8mb4_general_ci AS menu_id, CONVERT(IFNULL(d.document_id, '') USING utf8mb4) COLLATE utf8mb4_general_ci AS document_id, 70 AS score, d.update_time AS updated_at FROM hw_web_document d WHERE d.is_delete = '0' AND ( d.json LIKE CONCAT('%', @keyword, '%') OR d.document_id LIKE CONCAT('%', @keyword, '%') ) ) s ORDER BY s.score DESC, s.updated_at DESC LIMIT 500 """; /// /// MyBatis 执行器。 /// /// 【依赖注入】 /// 通过构造函数注入 HwPortalMyBatisExecutor。 /// 这个执行器负责解析 XML 中的 SQL 并执行。 /// /// private readonly HwPortalMyBatisExecutor _executor; private readonly ISqlSugarClient _db; /// /// 构造函数(依赖注入)。 /// /// MyBatis 执行器 public LegacyHwSearchQueryService(HwPortalMyBatisExecutor executor, ISqlSugarClient db) { _executor = executor; _db = db; } /// /// 根据关键词搜索。 /// /// 【搜索逻辑】 /// 调用 HwSearchMapper.xml 中定义的 searchByKeyword SQL。 /// SQL 通常是 LIKE 查询: /// SELECT * FROM hw_web WHERE title LIKE '%keyword%' /// UNION ALL /// SELECT * FROM hw_product WHERE title LIKE '%keyword%' /// /// 【性能说明】 /// LIKE '%keyword%' 无法使用索引,性能较差。 /// 这就是为什么需要新版搜索(使用索引表)。 /// /// /// 【C# 语法知识点 - 匿名对象传参】 /// new { keyword } 创建匿名对象,作为 SQL 参数。 /// SQL 中的 #{keyword} 会被替换为参数值。 /// /// 对比 Java MyBatis: /// Java: @Param("keyword") String keyword /// C#: new { keyword } /// /// /// 搜索关键词 /// 搜索结果列表 public Task> SearchAsync(string keyword) { // QueryListAsync<T> 执行查询并返回列表。 // T 是结果映射类型,SearchRawRecord 是原始搜索记录。 // 回滚到 XML 方案时可直接恢复: // return _executor.QueryListAsync(Mapper, "searchByKeyword", new { keyword }); return _db.Ado.SqlQueryAsync(SearchByKeywordSql, new[] { new SugarParameter("@keyword", keyword) }); } }