feat(搜索): 添加详细注释
重构搜索服务核心逻辑,新增ES索引支持与路由解析器,优化搜索结果处理流程 1. 新增PortalSearchRouteResolver路由解析器,实现配置分类到菜单路由的智能映射 2. 重构PortalSearchEsServiceImpl,增加索引自愈机制和查询结果归一化处理 3. 优化PortalSearchDocConverter文档转换逻辑,完善各来源类型的字段映射规则 4. 增强HwSearchServiceImpl的MySQL兜底查询,保持与ES结果结构一致性 5. 为所有核心类添加详细注释,明确各字段用途和业务逻辑 6. 实现关键词高亮、摘要生成和分页查询等基础功能 7. 支持编辑模式特殊路由,便于后台快速定位配置项main
parent
6d8d0cc5fb
commit
e4548f1d5c
@ -0,0 +1,178 @@
|
||||
package com.ruoyi.portal.search.support;
|
||||
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.portal.domain.HwPortalConfigType;
|
||||
import com.ruoyi.portal.domain.HwWebMenu;
|
||||
import com.ruoyi.portal.mapper.HwWebMenuMapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 门户搜索路由解析器
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Component
|
||||
public class PortalSearchRouteResolver
|
||||
{
|
||||
// 配置分类编码到根菜单 ID 的映射;解析配置分类路由时先约束菜单树范围以减少误匹配。
|
||||
private static final Map<String, Long> CONFIG_TYPE_ROOT_MENU_MAP = Map.of(
|
||||
"3", 2L,
|
||||
"4", 7L,
|
||||
"5", 4L,
|
||||
"6", 24L
|
||||
);
|
||||
|
||||
private final HwWebMenuMapper hwWebMenuMapper;
|
||||
|
||||
public PortalSearchRouteResolver(HwWebMenuMapper hwWebMenuMapper)
|
||||
{
|
||||
// 保存菜单 Mapper 依赖;后续解析路由时需要实时读取菜单树,避免使用过期缓存数据。
|
||||
this.hwWebMenuMapper = hwWebMenuMapper;
|
||||
}
|
||||
|
||||
public String resolveConfigTypeWebCode(HwPortalConfigType configType, List<HwWebMenu> menus)
|
||||
{
|
||||
// 先做空值保护;避免上游未传配置对象时触发空指针并中断搜索流程。
|
||||
if (configType == null)
|
||||
{
|
||||
// 无法解析时返回空;让调用方按兜底逻辑处理比抛异常更稳妥。
|
||||
return null;
|
||||
}
|
||||
// 委托到统一解析入口;集中匹配规则便于维护并保证两种入口行为一致。
|
||||
return resolveConfigTypeWebCode(
|
||||
// 组装候选名称集合;兼容配置名称和首页展示名称两种命名来源。
|
||||
buildCandidateNames(configType.getConfigTypeName(), configType.getHomeConfigTypeName()),
|
||||
// 传入分类编码;用于约束应命中的根菜单范围,降低误匹配。
|
||||
configType.getConfigTypeClassfication(),
|
||||
// 传入菜单列表;避免在同一请求中重复查库。
|
||||
menus
|
||||
);
|
||||
}
|
||||
|
||||
public String resolveConfigTypeWebCode(String title)
|
||||
{
|
||||
// 标题直查场景走同一套候选名解析;确保与配置对象场景的匹配逻辑一致。
|
||||
return resolveConfigTypeWebCode(buildCandidateNames(title), null, loadMenus());
|
||||
}
|
||||
|
||||
private String resolveConfigTypeWebCode(List<String> candidateNames, String configTypeClassfication, List<HwWebMenu> menus)
|
||||
{
|
||||
// 前置校验候选名与菜单集;缺任一关键输入时直接返回空,避免无意义遍历。
|
||||
if (candidateNames.isEmpty() || menus == null || menus.isEmpty())
|
||||
{
|
||||
// 返回空表示未命中;让上层决定是否走默认路由。
|
||||
return null;
|
||||
}
|
||||
// 根据分类编码映射期望根菜单;后续优先在该根菜单子树中匹配以提升准确率。
|
||||
Long expectedRootMenuId = configTypeClassfication != null ? CONFIG_TYPE_ROOT_MENU_MAP.get(configTypeClassfication) : null;
|
||||
// 这里优先按“分类所属根菜单 + 精确名称”匹配,避免把配置分类主键错误地当成页面 web_code 返回给前端。
|
||||
// 在菜单列表中按候选名筛选并排序取首个;优先更接近业务期望的目标菜单。
|
||||
HwWebMenu matchedMenu = menus.stream()
|
||||
// 先按标准化后的菜单名做精确命中;减少模糊匹配带来的串线风险。
|
||||
.filter(menu -> candidateNames.contains(normalize(menu.getWebMenuName())))
|
||||
// 再按根菜单关系过滤;保证命中的菜单属于对应业务分类。
|
||||
.filter(menu -> matchesRootMenu(menu, expectedRootMenuId))
|
||||
// 排序时先让直系子菜单排前;因为它通常是前端实际跳转入口。
|
||||
.sorted(Comparator
|
||||
.comparing((HwWebMenu menu) -> !isDirectChild(menu, expectedRootMenuId))
|
||||
// 同优先级再按菜单 ID 升序;保证结果稳定,避免同输入返回不同菜单。
|
||||
.thenComparing(HwWebMenu::getWebMenuId, Comparator.nullsLast(Long::compareTo)))
|
||||
// 取首个最佳匹配;减少后续处理复杂度。
|
||||
.findFirst()
|
||||
// 若根菜单约束无命中则降级为全局同名匹配;兼容历史数据不规范场景。
|
||||
.orElseGet(() -> menus.stream()
|
||||
// 继续使用相同标准化名称匹配;确保降级策略仍可解释。
|
||||
.filter(menu -> candidateNames.contains(normalize(menu.getWebMenuName())))
|
||||
// 按 ID 升序取第一个;保证降级结果可预测。
|
||||
.sorted(Comparator.comparing(HwWebMenu::getWebMenuId, Comparator.nullsLast(Long::compareTo)))
|
||||
// 获取降级匹配结果。
|
||||
.findFirst()
|
||||
// 仍未命中则返回空。
|
||||
.orElse(null));
|
||||
// 返回命中菜单 ID 字符串;前端路由参数以字符串传递可避免类型歧义。
|
||||
return matchedMenu == null || matchedMenu.getWebMenuId() == null ? null : String.valueOf(matchedMenu.getWebMenuId());
|
||||
}
|
||||
|
||||
private List<HwWebMenu> loadMenus()
|
||||
{
|
||||
// 查询全量菜单;标题直查场景没有外部传入菜单时需要自行加载。
|
||||
return hwWebMenuMapper.selectHwWebMenuList(new HwWebMenu());
|
||||
}
|
||||
|
||||
private List<String> buildCandidateNames(String... values)
|
||||
{
|
||||
// 使用有序去重集合存候选名;既要去重又要保留输入优先级。
|
||||
Set<String> candidates = new LinkedHashSet<>();
|
||||
// 遍历所有输入名称;统一做清洗和扩展以提升匹配命中率。
|
||||
Arrays.stream(values)
|
||||
// 过滤空白值;避免无效候选污染匹配集合。
|
||||
.filter(StringUtils::isNotBlank)
|
||||
// 统一标准化(去首尾空格与多空白);减少命名差异影响。
|
||||
.map(this::normalize)
|
||||
// 写入候选集合并按规则补充变体;兼容“案例”后缀命名。
|
||||
.forEach(value ->
|
||||
{
|
||||
// 加入标准化名称本身;这是最直接的匹配键。
|
||||
candidates.add(value);
|
||||
// 若名称以“案例”结尾则补充去后缀版本;兼容菜单与配置命名不一致。
|
||||
if (value.endsWith("案例"))
|
||||
{
|
||||
// 去掉“案例”后再次标准化后加入;避免截断后残留空白影响匹配。
|
||||
candidates.add(normalize(StringUtils.substring(value, 0, value.length() - 2)));
|
||||
}
|
||||
});
|
||||
// 转为列表返回;方便后续 contains 判断并保持候选顺序。
|
||||
return new ArrayList<>(candidates);
|
||||
}
|
||||
|
||||
private boolean matchesRootMenu(HwWebMenu menu, Long expectedRootMenuId)
|
||||
{
|
||||
// 菜单为空直接不匹配;防止访问空对象属性。
|
||||
if (menu == null)
|
||||
{
|
||||
// 返回 false 明确告诉调用方当前记录不可用。
|
||||
return false;
|
||||
}
|
||||
// 未指定期望根菜单时默认放行;用于标题直查等无分类上下文场景。
|
||||
if (expectedRootMenuId == null)
|
||||
{
|
||||
// 返回 true 表示不做根菜单限制。
|
||||
return true;
|
||||
}
|
||||
// 命中条件:直系子节点或祖先链包含目标根;兼容多级菜单结构。
|
||||
return isDirectChild(menu, expectedRootMenuId) || containsAncestor(menu.getAncestors(), expectedRootMenuId);
|
||||
}
|
||||
|
||||
private boolean isDirectChild(HwWebMenu menu, Long expectedRootMenuId)
|
||||
{
|
||||
// 通过 parent 与期望根菜单比较判断是否直系;这是最高优先级的匹配关系。
|
||||
return menu != null && expectedRootMenuId != null && Objects.equals(menu.getParent(), expectedRootMenuId);
|
||||
}
|
||||
|
||||
private boolean containsAncestor(String ancestors, Long expectedRootMenuId)
|
||||
{
|
||||
// 祖先串为空或目标根为空时无法判断包含关系;直接返回 false。
|
||||
if (StringUtils.isBlank(ancestors) || expectedRootMenuId == null)
|
||||
{
|
||||
// 返回 false 避免误判菜单归属。
|
||||
return false;
|
||||
}
|
||||
// 拆分祖先链并去空白后转集合;便于 O(1) 判断目标根是否存在。
|
||||
return Arrays.stream(ancestors.split(","))
|
||||
// 去除每段两端空格;兼容存储中可能存在的格式噪声。
|
||||
.map(String::trim)
|
||||
// 收集为集合提升 contains 查询效率。
|
||||
.collect(Collectors.toSet())
|
||||
// 判断目标根 ID 字符串是否在祖先链中;用于识别间接子孙菜单。
|
||||
.contains(String.valueOf(expectedRootMenuId));
|
||||
}
|
||||
|
||||
private String normalize(String value)
|
||||
{
|
||||
// 统一去首尾空白并压缩内部空白;保证不同来源文本可以稳定比对。
|
||||
return StringUtils.trimToEmpty(value).replaceAll("\\s+", "");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue