// ============================================================================ // 【文件说明】PortalSearchDocConverter.cs - 搜索文档转换器 // ============================================================================ // 这个类负责把业务数据转换成搜索索引文档格式。 // // 【核心功能】 // 1. 从 JSON 配置中提取可搜索文本 // 2. 过滤掉不需要搜索的字段(如 URL、图片路径) // 3. 清理 HTML 标签和多余空白 // // 【为什么需要转换器?】 // 业务数据存储格式和搜索需求不同: // - 业务数据:结构化 JSON,包含配置、URL、图片等 // - 搜索需求:纯文本,便于全文检索 // // 例如: // - 业务数据:{ "title": "产品介绍", "icon": "/img/icon.png", "desc": "

产品描述

" } // - 搜索文本:产品介绍 产品描述 // // 【与 Java Spring Boot 的对比】 // Java 通常用工具类实现类似功能: // public class SearchTextExtractor { // public static String extract(String json) { ... } // } // // C# 这里用实例类,便于依赖注入和测试。 // ============================================================================ namespace Admin.NET.Plugin.HwPortal; /// /// 搜索文档转换器。 /// /// 【服务职责】 /// 把业务实体转换成搜索索引文档: /// 1. 提取 JSON 中的可搜索文本 /// 2. 过滤掉 URL、图片等非搜索字段 /// 3. 清理 HTML 标签,保留纯文本 /// /// /// 【C# 语法知识点 - sealed 密封类】 /// sealed 表示不能被继承。转换器类通常不需要扩展。 /// /// 注意:这个类没有实现 ITransient 等接口。 /// 这意味着它不会被自动注册到 DI 容器。 /// 使用时需要手动创建实例或手动注册。 /// /// public sealed class PortalSearchDocConverter { /// /// 来源类型常量 - 菜单。 /// /// 【C# 语法知识点 - const 常量】 /// const 是编译期常量,值在编译时就确定了。 /// 这些常量用于标识搜索文档的来源类型。 /// /// 对比 Java: /// Java: public static final String SOURCE_MENU = "menu"; /// C#: public const string SourceMenu = "menu"; /// /// C# 的 const 隐式是 static 的,不能加 static 修饰符。 /// /// public const string SourceMenu = "menu"; /// /// 来源类型常量 - 页面。 /// public const string SourceWeb = "web"; /// /// 来源类型常量 - 页面1。 /// public const string SourceWeb1 = "web1"; /// /// 来源类型常量 - 文档。 /// public const string SourceDocument = "document"; /// /// 来源类型常量 - 配置类型。 /// public const string SourceConfigType = "configType"; /// /// 需要跳过的 JSON 字段名集合。 /// /// 【C# 语法知识点 - HashSet<T> 哈希集合】 /// HashSet<T> 是不包含重复元素的集合: /// - 查找速度快(O(1)) /// - 自动去重 /// - 适合存储需要快速判断是否存在的元素 /// /// 对比 Java: /// Java: private static final Set<String> SKIP_KEYS = new HashSet<>(Arrays.asList(...)); /// C#: private static readonly HashSet<string> SkipJsonKeys = new(...) { ... }; /// /// 【StringComparer.OrdinalIgnoreCase 参数】 /// 创建集合时传入比较器,实现忽略大小写的比较: /// - "Icon" 和 "icon" 被视为相同 /// - 查找时自动忽略大小写 /// /// 对比 Java: /// Java 需要用 TreeSet 或自定义 HashSet: /// new TreeSet<>(String.CASE_INSENSITIVE_ORDER) /// /// /// 【为什么跳过这些字段?】 /// 这些字段不适合全文搜索: /// - icon, img, banner:图片路径 /// - url, route:URL 路径 /// - uuid, id:技术标识符 /// - secretkey:敏感信息 /// /// private static readonly HashSet SkipJsonKeys = new(StringComparer.OrdinalIgnoreCase) { "icon", "url", "banner", "banner1", "img", "imglist", "type", "uuid", "filename", "documentaddress", "secretkey", "route", "routequery", "webcode", "deviceid", "typeid", "configtypeid", "id" }; /// /// 从文本中提取可搜索文本。 /// /// 【处理流程】 /// 1. 如果文本为空,返回空字符串 /// 2. 先尝试作为 JSON 解析 /// 3. 如果 JSON 解析成功,提取所有文本节点 /// 4. 如果 JSON 解析失败,清理 HTML 标签后返回 /// /// /// 【C# 语法知识点 - string.IsNullOrWhiteSpace】 /// string.IsNullOrWhiteSpace(text) 检查字符串是否: /// - null /// - 空字符串 "" /// - 只包含空白字符(空格、制表符、换行等) /// /// 对比 Java: /// Java: StringUtils.isBlank(text)(Apache Commons) /// 或: text == null || text.trim().isEmpty() /// /// /// 输入文本(可能是 JSON 或 HTML) /// 提取的可搜索文本 public string ExtractSearchableText(string text) { // 【空值检查】 if (string.IsNullOrWhiteSpace(text)) { return string.Empty; } // 【HTML 标签清理】 // 先清理 HTML,作为备用结果。 string stripped = StripHtml(text); try { // 【JSON 解析】 // JsonDocument 是 System.Text.Json 的高性能 JSON 解析器。 // using 语句确保资源正确释放。 using JsonDocument document = JsonDocument.Parse(text); // 【StringBuilder 文本拼接】 // StringBuilder 用于高效拼接大量字符串。 // 对比:string + string 每次都创建新对象,性能差。 StringBuilder builder = new(); // 【递归提取文本节点】 CollectNodeText(document.RootElement, null, builder); // 【规范化空白】 string extracted = NormalizeWhitespace(builder.ToString()); // 【返回结果】 // 如果提取的文本为空,返回清理后的 HTML 文本。 return string.IsNullOrWhiteSpace(extracted) ? stripped : extracted; } catch { // 【JSON 解析失败】 // 如果不是有效的 JSON,返回清理后的 HTML 文本。 return stripped; } } /// /// 递归收集 JSON 节点的文本内容。 /// /// 【递归遍历 JSON】 /// JSON 可能是嵌套结构,需要递归遍历: /// - Object:遍历每个属性 /// - Array:遍历每个元素 /// - String:提取文本值 /// - Number/Boolean/Null:跳过(不适合搜索) /// /// /// 【C# 语法知识点 - switch 表达式】 /// switch (element.ValueKind) 根据 JSON 值类型分支处理: /// - JsonValueKind.Object:JSON 对象 /// - JsonValueKind.Array:JSON 数组 /// - JsonValueKind.String:字符串 /// /// 对比 Java: /// Java 需要用 if-else 或 switch 语句: /// switch (element.getValueKind()) { /// case OBJECT: ... /// case ARRAY: ... /// } /// /// /// JSON 元素 /// 字段名(用于判断是否跳过) /// 输出 StringBuilder private void CollectNodeText(JsonElement element, string fieldName, StringBuilder output) { switch (element.ValueKind) { case JsonValueKind.Object: // 【遍历对象属性】 // EnumerateObject() 返回对象的所有属性。 foreach (JsonProperty property in element.EnumerateObject()) { // 【过滤跳过字段】 // 如果字段名在跳过列表中,不处理。 if (!ShouldSkip(property.Name)) { CollectNodeText(property.Value, property.Name, output); } } break; case JsonValueKind.Array: // 【遍历数组元素】 // EnumerateArray() 返回数组的所有元素。 foreach (JsonElement child in element.EnumerateArray()) { CollectNodeText(child, fieldName, output); } break; case JsonValueKind.String: // 【提取字符串值】 // 如果字段需要跳过,直接返回。 if (ShouldSkip(fieldName)) { return; } // 【获取字符串值】 // element.GetString() 获取 JSON 字符串的值。 string value = NormalizeWhitespace(StripHtml(element.GetString())); // 【过滤 URL 和空值】 // URL 不适合搜索,跳过。 if (string.IsNullOrWhiteSpace(value) || value.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || value.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) { return; } // 【追加到输出】 // StringBuilder.Append() 追加文本。 // .Append(' ') 追加空格分隔。 output.Append(value).Append(' '); break; } } /// /// 判断字段是否应该跳过。 /// /// 【跳过规则】 /// 1. 字段名在 SkipJsonKeys 集合中 /// 2. 字段名以 "url" 结尾(如 imageUrl, videoUrl) /// 3. 字段名以 "icon" 结尾(如 menuIcon) /// /// /// 字段名 /// 是否跳过 private static bool ShouldSkip(string fieldName) { // 【空字段名检查】 if (string.IsNullOrWhiteSpace(fieldName)) { return false; } // 【多条件判断】 // Contains 检查是否在跳过集合中。 // EndsWith 检查是否以特定后缀结尾。 return SkipJsonKeys.Contains(fieldName) || fieldName.EndsWith("url", StringComparison.OrdinalIgnoreCase) || fieldName.EndsWith("icon", StringComparison.OrdinalIgnoreCase); } /// /// 清理 HTML 标签。 /// /// 【C# 语法知识点 - 正则表达式】 /// Regex.Replace(input, pattern, replacement) /// - input:输入字符串 /// - pattern:正则表达式模式 /// - replacement:替换字符串 /// /// "<[^>]+>" 匹配所有 HTML 标签: /// - <:匹配 < /// - [^>]+:匹配一个或多个非 > 的字符 /// - >:匹配 > /// /// 对比 Java: /// Java: text.replaceAll("<[^>]+>", " ") /// /// C# 的 Regex.Replace 性能更好,支持编译正则。 /// /// /// 输入文本 /// 清理后的文本 private static string StripHtml(string text) { // 用空格替换 HTML 标签,避免标签内容粘连。 return NormalizeWhitespace(Regex.Replace(text ?? string.Empty, "<[^>]+>", " ")); } /// /// 规范化空白字符。 /// /// 【处理说明】 /// 把连续的空白字符(空格、制表符、换行等)替换为单个空格: /// - "a b\n\tc" → "a b c" /// - 去除首尾空白 /// /// /// 【C# 语法知识点 - 正则表达式 @"\s+"】 /// @ 符号表示"逐字字符串"(Verbatim String): /// - 不需要转义反斜杠 /// - @"\s+" 等价于 "\\s+" /// /// \s+ 匹配一个或多个空白字符。 /// /// /// 输入文本 /// 规范化后的文本 private static string NormalizeWhitespace(string text) { // 把连续空白替换为单个空格,并去除首尾空白。 return Regex.Replace(text ?? string.Empty, @"\s+", " ").Trim(); } }