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.

176 lines
8.0 KiB
C#

// ============================================================================
// 【文件说明】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<HwWeb> searchByKeyword(@Param("keyword") String keyword);
//
// 这个类就是类似的实现方式。
// ============================================================================
namespace Admin.NET.Plugin.HwPortal;
/// <summary>
/// 旧版搜索查询服务。
/// <para>
/// 【设计模式 - 适配器模式】
/// 这个类把旧版 SQL 搜索适配到新的服务架构中:
/// - 对外提供统一的搜索接口
/// - 内部使用旧的 SQL 实现
///
/// 这样可以在不修改调用方的情况下,平滑切换搜索实现。
/// </para>
/// <para>
/// 【C# 语法知识点 - sealed 密封类】
/// sealed 表示不能被继承。遗留服务通常不需要扩展。
/// </para>
/// <para>
/// 【C# 语法知识点 - ITransient 瞬态服务】
/// ITransient 表示每次请求都创建新实例。
/// 搜索服务通常是无状态的,用 ITransient 或 IScoped 都可以。
/// </para>
/// </summary>
public sealed class LegacyHwSearchQueryService : ITransient
{
/// <summary>
/// MyBatis 映射器名称。
/// </summary>
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
""";
/// <summary>
/// MyBatis 执行器。
/// <para>
/// 【依赖注入】
/// 通过构造函数注入 HwPortalMyBatisExecutor。
/// 这个执行器负责解析 XML 中的 SQL 并执行。
/// </para>
/// </summary>
private readonly HwPortalMyBatisExecutor _executor;
private readonly ISqlSugarClient _db;
/// <summary>
/// 构造函数(依赖注入)。
/// </summary>
/// <param name="executor">MyBatis 执行器</param>
public LegacyHwSearchQueryService(HwPortalMyBatisExecutor executor, ISqlSugarClient db)
{
_executor = executor;
_db = db;
}
/// <summary>
/// 根据关键词搜索。
/// <para>
/// 【搜索逻辑】
/// 调用 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%' 无法使用索引,性能较差。
/// 这就是为什么需要新版搜索(使用索引表)。
/// </para>
/// <para>
/// 【C# 语法知识点 - 匿名对象传参】
/// new { keyword } 创建匿名对象,作为 SQL 参数。
/// SQL 中的 #{keyword} 会被替换为参数值。
///
/// 对比 Java MyBatis
/// Java: @Param("keyword") String keyword
/// C#: new { keyword }
/// </para>
/// </summary>
/// <param name="keyword">搜索关键词</param>
/// <returns>搜索结果列表</returns>
public Task<List<SearchRawRecord>> SearchAsync(string keyword)
{
// QueryListAsync&lt;T&gt; 执行查询并返回列表。
// T 是结果映射类型SearchRawRecord 是原始搜索记录。
// 回滚到 XML 方案时可直接恢复:
// return _executor.QueryListAsync<SearchRawRecord>(Mapper, "searchByKeyword", new { keyword });
return _db.Ado.SqlQueryAsync<SearchRawRecord>(SearchByKeywordSql, new[]
{
new SugarParameter("@keyword", keyword)
});
}
}