feat: 统一列表模糊搜索行为
- 统一管理端和用户中心搜索参数及多字段包含匹配 - 修复聊天搜索竞态并优化部门重名路径展示 - 补充部门展开、模型空白词和聊天查询回归测试
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
package tech.easyflow.common.util;
|
||||
|
||||
/**
|
||||
* 搜索关键词处理工具。
|
||||
*/
|
||||
public final class SearchKeywordUtil {
|
||||
|
||||
private SearchKeywordUtil() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 去除搜索关键词首尾空格,空值统一为空字符串。
|
||||
*
|
||||
* @param keyword 原始搜索关键词
|
||||
* @return 规范化后的关键词
|
||||
*/
|
||||
public static String normalize(String keyword) {
|
||||
return keyword == null ? "" : keyword.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造将 LIKE 通配符按普通字符处理的包含匹配模式。
|
||||
*
|
||||
* @param keyword 搜索关键词
|
||||
* @return 可参数化传入 LIKE 的匹配模式
|
||||
*/
|
||||
public static String literalContainsPattern(String keyword) {
|
||||
String normalizedKeyword = normalize(keyword);
|
||||
String escapedKeyword = normalizedKeyword
|
||||
.replace("\\", "\\\\")
|
||||
.replace("%", "\\%")
|
||||
.replace("_", "\\_");
|
||||
return "%" + escapedKeyword + "%";
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package tech.easyflow.common.web.controller;
|
||||
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.query.QueryColumn;
|
||||
import com.mybatisflex.core.query.QueryCondition;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.mybatisflex.core.query.SqlOperators;
|
||||
import com.mybatisflex.core.service.IService;
|
||||
@@ -15,6 +17,7 @@ import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.tree.Tree;
|
||||
import tech.easyflow.common.util.SearchKeywordUtil;
|
||||
import tech.easyflow.common.util.SqlOperatorsUtil;
|
||||
import tech.easyflow.common.util.SqlUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
@@ -30,6 +33,7 @@ import java.util.*;
|
||||
|
||||
public class BaseCurdController<S extends IService<M>, M> extends BaseController {
|
||||
|
||||
private static final String KEYWORD_PARAM = "keyword";
|
||||
|
||||
protected final S service;
|
||||
|
||||
@@ -198,6 +202,7 @@ public class BaseCurdController<S extends IService<M>, M> extends BaseController
|
||||
|
||||
Map<String, String> propertyColumnMapping = TableInfoFactory.ofEntityClass(getEntityClass())
|
||||
.getPropertyColumnMapping();
|
||||
QueryCondition legacyOrCondition = null;
|
||||
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
|
||||
String paramKey = entry.getKey();
|
||||
if (StringUtil.hasText(paramKey) && !paramKey.endsWith(OperatorBuilder.operatorSuffix) && propertyColumnMapping.containsKey(paramKey)) {
|
||||
@@ -216,9 +221,14 @@ public class BaseCurdController<S extends IService<M>, M> extends BaseController
|
||||
queryWrapper.eq(columnName, value);
|
||||
} else {
|
||||
if (isQueryOrBool) {
|
||||
queryWrapper.or(columnName + " like " + "'%" + value + "%' ");
|
||||
QueryCondition condition = buildLiteralContainsCondition(
|
||||
value, new QueryColumn(columnName));
|
||||
legacyOrCondition = legacyOrCondition == null
|
||||
? condition
|
||||
: legacyOrCondition.or(condition);
|
||||
} else {
|
||||
queryWrapper.like(columnName, value);
|
||||
queryWrapper.and(buildLiteralContainsCondition(
|
||||
value, new QueryColumn(columnName)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -227,9 +237,101 @@ public class BaseCurdController<S extends IService<M>, M> extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
if (legacyOrCondition != null) {
|
||||
queryWrapper.and(legacyOrCondition);
|
||||
}
|
||||
applyKeywordSearch(queryWrapper, request, propertyColumnMapping);
|
||||
|
||||
return queryWrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前控制器统一关键字需要匹配的实体属性。
|
||||
*
|
||||
* <p>属性名必须来自当前实体映射。分类、状态、租户等精确条件不应放入该数组。</p>
|
||||
*
|
||||
* @return 需要执行包含匹配的实体属性;默认不启用统一关键字
|
||||
*/
|
||||
protected String[] getKeywordSearchProperties() {
|
||||
return new String[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 归一化搜索关键字。
|
||||
*
|
||||
* @param keyword 原始关键字
|
||||
* @return 去除首尾空格后的关键字;空值返回空字符串
|
||||
*/
|
||||
protected String normalizeSearchKeyword(String keyword) {
|
||||
return SearchKeywordUtil.normalize(keyword);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造按普通文本解释的多列包含匹配条件。
|
||||
*
|
||||
* @param keyword 原始关键字
|
||||
* @param columns 允许搜索的受信任列
|
||||
* @return 已正确分组的 OR 条件
|
||||
* @throws IllegalArgumentException 关键字为空或未提供列时抛出
|
||||
*/
|
||||
protected QueryCondition buildLiteralContainsCondition(String keyword, QueryColumn... columns) {
|
||||
String normalizedKeyword = normalizeSearchKeyword(keyword);
|
||||
if (!StringUtil.hasText(normalizedKeyword) || columns == null || columns.length == 0) {
|
||||
throw new IllegalArgumentException("keyword and columns must not be empty");
|
||||
}
|
||||
String pattern = buildLiteralContainsPattern(normalizedKeyword);
|
||||
QueryCondition condition = null;
|
||||
for (QueryColumn column : columns) {
|
||||
if (column == null) {
|
||||
continue;
|
||||
}
|
||||
QueryCondition columnCondition = column.likeRaw(pattern);
|
||||
condition = condition == null ? columnCondition : condition.or(columnCondition);
|
||||
}
|
||||
if (condition == null) {
|
||||
throw new IllegalArgumentException("columns must contain at least one valid column");
|
||||
}
|
||||
return condition;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造将 LIKE 通配符按普通字符处理的包含匹配模式。
|
||||
*
|
||||
* @param keyword 已归一化的关键字
|
||||
* @return 可参数化传入 LIKE 的匹配模式
|
||||
*/
|
||||
protected String buildLiteralContainsPattern(String keyword) {
|
||||
return SearchKeywordUtil.literalContainsPattern(keyword);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将控制器声明的统一关键字字段追加到查询条件。
|
||||
*
|
||||
* @param queryWrapper 查询包装器
|
||||
* @param request 当前请求
|
||||
* @param propertyColumnMapping 实体属性与数据库列映射
|
||||
* @throws ProgramException 控制器声明了不存在的实体属性时抛出
|
||||
*/
|
||||
private void applyKeywordSearch(QueryWrapper queryWrapper,
|
||||
HttpServletRequest request,
|
||||
Map<String, String> propertyColumnMapping) {
|
||||
String keyword = normalizeSearchKeyword(request.getParameter(KEYWORD_PARAM));
|
||||
String[] properties = getKeywordSearchProperties();
|
||||
if (!StringUtil.hasText(keyword) || properties == null || properties.length == 0) {
|
||||
return;
|
||||
}
|
||||
QueryColumn[] columns = new QueryColumn[properties.length];
|
||||
for (int index = 0; index < properties.length; index++) {
|
||||
String property = properties[index];
|
||||
String column = propertyColumnMapping.get(property);
|
||||
if (!StringUtil.hasText(column)) {
|
||||
throw new ProgramException("搜索字段未映射到实体属性:" + property);
|
||||
}
|
||||
columns[index] = new QueryColumn(column);
|
||||
}
|
||||
queryWrapper.and(buildLiteralContainsCondition(keyword, columns));
|
||||
}
|
||||
|
||||
protected Class<?> getEntityClass() {
|
||||
Type type = getClass().getGenericSuperclass();
|
||||
if (type instanceof ParameterizedType) {
|
||||
|
||||
Reference in New Issue
Block a user