// ============================================================================
// 【文件说明】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();
}
}