feat: 统一列表模糊搜索行为

- 统一管理端和用户中心搜索参数及多字段包含匹配

- 修复聊天搜索竞态并优化部门重名路径展示

- 补充部门展开、模型空白词和聊天查询回归测试
This commit is contained in:
2026-08-13 22:29:59 +08:00
parent 765006747a
commit 64a85c6a5b
83 changed files with 1134 additions and 208 deletions

View File

@@ -101,6 +101,16 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
super(service);
}
/**
* 获取 Agent 列表关键字搜索字段。
*
* @return Agent 名称和描述属性
*/
@Override
protected String[] getKeywordSearchProperties() {
return new String[]{"name", "description"};
}
/**
* 获取 Agent 详情。
*

View File

@@ -97,6 +97,16 @@ public class BotController extends BaseCurdController<BotService, Bot> {
this.botMessageService = botMessageService;
}
/**
* 获取智能体列表关键字搜索字段。
*
* @return 标题和描述属性
*/
@Override
protected String[] getKeywordSearchProperties() {
return new String[]{"title", "description"};
}
@Resource
private BotPluginService botPluginService;

View File

@@ -86,6 +86,16 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
this.llmService = llmService;
}
/**
* 获取知识库列表关键字搜索字段。
*
* @return 标题和描述属性
*/
@Override
protected String[] getKeywordSearchProperties() {
return new String[]{"title", "description"};
}
@Override
protected Result<?> onSaveOrUpdateBefore(DocumentCollection entity, boolean isSave) {
normalizeVisibilityScope(entity, isSave);

View File

@@ -99,6 +99,16 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
super(service);
this.knowledgeService = knowledgeService;
}
/**
* 获取知识库文档关键字搜索字段。
*
* @return 文件标题属性
*/
@Override
protected String[] getKeywordSearchProperties() {
return new String[]{"title"};
}
@PostMapping("removeDoc")
@Transactional
@SaCheckPermission("/api/v1/documentCollection/remove")
@@ -148,13 +158,19 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
@GetMapping("documentList")
@SaCheckPermission("/api/v1/documentCollection/query")
public Result<Page<Document>> documentList(@RequestParam(name="title", required = false) String fileName, @RequestParam(name="pageSize") int pageSize, @RequestParam(name = "pageNumber") int pageNumber) {
public Result<Page<Document>> documentList(
@RequestParam(name = "keyword", required = false) String keyword,
@RequestParam(name = "title", required = false) String legacyTitle,
@RequestParam(name = "pageSize") int pageSize,
@RequestParam(name = "pageNumber") int pageNumber) {
String kbSlug = RequestUtil.getParamAsString("id");
if (StringUtil.noText(kbSlug)) {
throw new BusinessException("知识库id不能为空");
}
DocumentCollection knowledge = getDocumentCollection(kbSlug, ResourceAction.READ, "无权限访问知识库");
Page<Document> documentList = documentService.getDocumentList(knowledge.getId().toString(), pageSize, pageNumber,fileName);
String effectiveKeyword = StringUtil.hasText(keyword) ? keyword : legacyTitle;
Page<Document> documentList = documentService.getDocumentList(
knowledge.getId().toString(), pageSize, pageNumber, effectiveKeyword);
return Result.ok(documentList);
}

View File

@@ -2,6 +2,7 @@ package tech.easyflow.admin.controller.ai;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryColumn;
import com.mybatisflex.core.query.QueryWrapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@@ -114,9 +115,14 @@ public class FaqItemController extends BaseCurdController<FaqItemService, FaqIte
QueryWrapper queryWrapper = QueryWrapper.create()
.eq(FaqItem::getCollectionId, collectionId);
String question = request.getParameter("question");
if (question != null && !question.trim().isEmpty()) {
queryWrapper.like(FaqItem::getQuestion, question.trim());
String keyword = normalizeSearchKeyword(request.getParameter("keyword"));
String question = normalizeSearchKeyword(request.getParameter("question"));
if (StringUtils.hasText(keyword)) {
queryWrapper.and(buildLiteralContainsCondition(
keyword, new QueryColumn("question"), new QueryColumn("answer_text")));
} else if (StringUtils.hasText(question)) {
// 兼容旧客户端仅按问题字段搜索。
queryWrapper.and(buildLiteralContainsCondition(question, new QueryColumn("question")));
}
String categoryIdText = request.getParameter("categoryId");

View File

@@ -36,6 +36,16 @@ public class McpController extends BaseCurdController<McpService, Mcp> {
super(service);
}
/**
* 获取 MCP 列表关键字搜索字段。
*
* @return 标题和描述属性
*/
@Override
protected String[] getKeywordSearchProperties() {
return new String[]{"title", "description"};
}
@Resource
private AgentResourceReferenceService agentResourceReferenceService;
@Override

View File

@@ -193,8 +193,10 @@ public class ModelController extends BaseCurdController<ModelService, Model> {
QueryWrapper queryWrapper = QueryWrapper.create();
queryWrapper.eq(Model::getProviderId, providerId);
queryWrapper.eq(Model::getModelType, modelType);
if (StringUtils.hasLength(selectText)) {
queryWrapper.and(ModelTableDef.MODEL.TITLE.like(selectText).or(ModelTableDef.MODEL.MODEL_NAME.like(selectText)));
String keyword = normalizeSearchKeyword(selectText);
if (StringUtils.hasText(keyword)) {
queryWrapper.and(buildLiteralContainsCondition(
keyword, ModelTableDef.MODEL.TITLE, ModelTableDef.MODEL.MODEL_NAME));
}
List<Model> totalList = service.getMapper().selectListWithRelationsByQuery(queryWrapper);
Map<String, List<Model>> groupList = totalList.stream().collect(Collectors.groupingBy(Model::getGroupName));

View File

@@ -2,6 +2,7 @@ package tech.easyflow.admin.controller.ai;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryColumn;
import com.mybatisflex.core.query.QueryWrapper;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.GetMapping;
@@ -58,6 +59,16 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
this.pluginService = service;
}
/**
* 获取插件列表关键字搜索字段。
*
* @return 插件名称和描述属性
*/
@Override
protected String[] getKeywordSearchProperties() {
return new String[]{"name", "description"};
}
@Resource
PluginService pluginService;
@Resource
@@ -118,7 +129,7 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
}
/**
* 按分类分页查询插件,并支持按名称模糊查询。
* 按分类分页查询插件,并支持按名称、描述模糊查询。
*
* @param request 当前请求
* @param sortKey 排序字段
@@ -126,13 +137,15 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
* @param pageNumber 页码
* @param pageSize 每页数量
* @param category 分类 ID0 表示全部分类
* @param name 插件名称关键字
* @param keyword 插件名称或描述关键字
* @param name 兼容旧客户端的插件名称关键字
* @return 插件分页结果
*/
@GetMapping("/pageByCategory")
@SaCheckPermission("/api/v1/plugin/query")
public Result<Page<Plugin>> pageByCategory(HttpServletRequest request, String sortKey, String sortType,
Long pageNumber, Long pageSize, int category, String name) {
Long pageNumber, Long pageSize, int category,
String keyword, String name) {
if (pageNumber == null || pageNumber < 1) {
pageNumber = 1L;
}
@@ -145,7 +158,10 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
return Result.ok(queryPage(new Page<>(pageNumber, pageSize), queryWrapper));
} else {
Result<Page<Plugin>> result = pluginService.pageByCategory(pageNumber, pageSize, category, name);
String effectiveKeyword = normalizeSearchKeyword(
keyword == null || keyword.isBlank() ? name : keyword);
Result<Page<Plugin>> result = pluginService.pageByCategory(
pageNumber, pageSize, category, effectiveKeyword);
if (result != null && result.getData() != null) {
aiResourceCreatorNameSupport.fillPluginCreatorNames(result.getData().getRecords());
}
@@ -160,7 +176,7 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
workflowVisibilityQueryHelper.applyReadableAccess(queryWrapper);
queryWrapper.eq("publish_status", tech.easyflow.ai.enums.PublishStatus.PUBLISHED.getCode());
if (keyword != null && !keyword.isBlank()) {
queryWrapper.like("title", keyword.trim());
queryWrapper.and(buildLiteralContainsCondition(keyword, new QueryColumn("title")));
}
queryWrapper.orderBy("modified desc");
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();

View File

@@ -67,6 +67,16 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
super(service);
}
/**
* 获取插件工具列表关键字搜索字段。
*
* @return 工具名称和描述属性
*/
@Override
protected String[] getKeywordSearchProperties() {
return new String[]{"name", "description"};
}
@Resource
private PluginItemService pluginItemService;

View File

@@ -45,6 +45,16 @@ public class ResourceController extends BaseCurdController<ResourceService, Reso
super(service);
}
/**
* 获取资源列表关键字搜索字段。
*
* @return 资源名称属性
*/
@Override
protected String[] getKeywordSearchProperties() {
return new String[]{"resourceName"};
}
@Override
protected Result<?> onSaveOrUpdateBefore(Resource entity, boolean isSave) {
LoginAccount loginUser = SaTokenUtil.getLoginAccount();

View File

@@ -6,6 +6,7 @@ import com.easyagents.core.store.DocumentStore;
import com.easyagents.core.store.StoreOptions;
import com.easyagents.core.store.StoreResult;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryColumn;
import com.mybatisflex.core.query.QueryWrapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@@ -47,6 +48,7 @@ import tech.easyflow.ai.vo.FaqImportResultVo;
import tech.easyflow.ai.vo.KnowledgeShareAuthContext;
import tech.easyflow.ai.vo.KnowledgeShareViewDetail;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.util.SearchKeywordUtil;
import tech.easyflow.common.filestorage.FileStorageService;
import tech.easyflow.common.vo.UploadResVo;
import tech.easyflow.common.web.exceptions.BusinessException;
@@ -668,9 +670,15 @@ public class ShareKnowledgeController {
faqCategoryService.ensureDefaultCategory(context.getKnowledge().getId());
QueryWrapper queryWrapper = QueryWrapper.create()
.eq(FaqItem::getCollectionId, context.getKnowledge().getId());
String keyword = request.getParameter("keyword");
String question = request.getParameter("question");
if (StringUtils.hasText(question)) {
queryWrapper.like(FaqItem::getQuestion, question.trim());
if (StringUtils.hasText(keyword)) {
String pattern = SearchKeywordUtil.literalContainsPattern(keyword);
queryWrapper.and(new QueryColumn("question").likeRaw(pattern)
.or(new QueryColumn("answer_text").likeRaw(pattern)));
} else if (StringUtils.hasText(question)) {
queryWrapper.and(new QueryColumn("question")
.likeRaw(SearchKeywordUtil.literalContainsPattern(question)));
}
String categoryId = request.getParameter("categoryId");
if (StringUtils.hasText(categoryId)) {

View File

@@ -113,6 +113,16 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
this.modelService = modelService;
}
/**
* 获取工作流列表关键字搜索字段。
*
* @return 标题和描述属性
*/
@Override
protected String[] getKeywordSearchProperties() {
return new String[]{"title", "description"};
}
/**
* 查询工作流设计器初始化所需的安全选项。
*
@@ -130,14 +140,16 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
*
* @param pageNumber 页码
* @param pageSize 每页数量
* @param keyword 插件名称或描述关键字
* @return 插件安全选项分页
*/
@GetMapping("/designer/plugins")
@SaCheckPermission("/api/v1/workflow/query")
public Result<Page<WorkflowDesignerOptionsView.PluginOption>> designerPlugins(
Long pageNumber,
Long pageSize) {
return Result.ok(workflowDesignerOptionService.pagePlugins(pageNumber, pageSize));
Long pageSize,
String keyword) {
return Result.ok(workflowDesignerOptionService.pagePlugins(pageNumber, pageSize, keyword));
}
/**

View File

@@ -37,6 +37,16 @@ public class WorkflowExecResultController extends BaseCurdController<WorkflowExe
super(service);
}
/**
* 获取工作流执行记录关键字搜索字段。
*
* @return 执行 Key 属性
*/
@Override
protected String[] getKeywordSearchProperties() {
return new String[]{"execKey"};
}
@GetMapping("/del")
@Transactional(rollbackFor = Exception.class)
@SaCheckPermission("/api/v1/workflow/remove")

View File

@@ -37,6 +37,16 @@ public class WorkflowExecStepController extends BaseCurdController<WorkflowExecS
super(service);
}
/**
* 获取执行步骤关键字搜索字段。
*
* @return 节点名称和节点 ID 属性
*/
@Override
protected String[] getKeywordSearchProperties() {
return new String[]{"nodeName", "nodeId"};
}
@GetMapping("/getListByRecordId")
public Result<List<WorkflowExecStep>> getListByRecordId(BigInteger recordId) {
if (recordId == null) {
@@ -60,4 +70,4 @@ public class WorkflowExecStepController extends BaseCurdController<WorkflowExecS
}
return Result.ok(list);
}
}
}

View File

@@ -82,6 +82,16 @@ public class SysJobController extends BaseCurdController<SysJobService, SysJob>
this.workflowRunningParameterResolver = workflowRunningParameterResolver;
}
/**
* 获取定时任务关键字搜索字段。
*
* @return 任务名称和备注属性
*/
@Override
protected String[] getKeywordSearchProperties() {
return new String[]{"jobName", "remark"};
}
@GetMapping("/start")
@SaCheckPermission("/api/v1/sysJob/save")
@LogRecord("启动定时任务")

View File

@@ -10,6 +10,7 @@ import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.util.SearchKeywordUtil;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.common.web.jsonbody.JsonBody;
import tech.easyflow.skill.entity.SkillCategory;
@@ -69,9 +70,11 @@ public class SkillCategoryController {
queryWrapper.eq(SkillCategory::getId, entity.getId(), entity.getId() != null)
.eq(SkillCategory::getParentId, entity.getParentId(), entity.getParentId() != null)
.eq(SkillCategory::getLevelNo, entity.getLevelNo(), entity.getLevelNo() != null)
.eq(SkillCategory::getStatus, entity.getStatus(), entity.getStatus() != null)
.like(SkillCategory::getCategoryName, entity.getCategoryName(),
entity.getCategoryName() != null && !entity.getCategoryName().isBlank());
.eq(SkillCategory::getStatus, entity.getStatus(), entity.getStatus() != null);
if (entity.getCategoryName() != null && !entity.getCategoryName().isBlank()) {
queryWrapper.and("category_name LIKE ?",
SearchKeywordUtil.literalContainsPattern(entity.getCategoryName()));
}
}
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess(CategoryResourceType.SKILL.getCode());
if (access.isRestricted()) {

View File

@@ -24,6 +24,7 @@ import tech.easyflow.approval.entity.vo.ApprovalActionResult;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.util.SearchKeywordUtil;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.common.web.jsonbody.JsonBody;
import tech.easyflow.skill.capability.SkillCapabilityBindingService;
@@ -159,7 +160,7 @@ public class SkillController {
.eq("source_type", sourceType, hasText(sourceType));
String keyword = hasText(displayName) ? displayName : name;
if (hasText(keyword)) {
String pattern = "%" + keyword + "%";
String pattern = SearchKeywordUtil.literalContainsPattern(keyword);
query.and("(name LIKE ? OR display_name LIKE ? OR description LIKE ?)", pattern, pattern, pattern);
}
if (hasText(capabilityType)) {
@@ -695,4 +696,5 @@ public class SkillController {
private boolean hasText(String value) {
return value != null && !value.isBlank();
}
}

View File

@@ -111,7 +111,7 @@ public class SysAccountController extends BaseCurdController<SysAccountService,
return super.buildQueryWrapper(request);
}
String likePattern = "%" + escapeLikeKeyword(keyword.trim()) + "%";
String likePattern = buildLiteralContainsPattern(normalizeSearchKeyword(keyword));
QueryWrapper roleExistsQuery = QueryMethods.selectOne()
.from(SYS_ACCOUNT_ROLE)
.innerJoin(SYS_ROLE)
@@ -126,20 +126,7 @@ public class SysAccountController extends BaseCurdController<SysAccountService,
.or(SYS_ACCOUNT.EMAIL.likeRaw(likePattern))
.or(QueryMethods.exists(roleExistsQuery));
return QueryWrapper.create().and(keywordCondition);
}
/**
* 转义 MySQL LIKE 模式中的特殊字符,使用户输入按普通文本匹配。
*
* @param keyword 已去除首尾空格的关键字
* @return 可安全放入 LIKE 模式的文本
*/
private String escapeLikeKeyword(String keyword) {
return keyword
.replace("\\", "\\\\")
.replace("%", "\\%")
.replace("_", "\\_");
return super.buildQueryWrapper(request).and(keywordCondition);
}
@Override

View File

@@ -58,6 +58,16 @@ public class SysApiKeyController extends BaseCurdController<SysApiKeyService, Sy
super(service);
}
/**
* 获取 API Key 列表关键字搜索字段。
*
* @return 名称和 Key 属性
*/
@Override
protected String[] getKeywordSearchProperties() {
return new String[]{"name", "apiKey"};
}
@Resource
private SysApiKeyResourceMappingService sysApiKeyResourceMappingService;
@Resource

View File

@@ -26,6 +26,16 @@ public class SysApiKeyResourceController extends BaseCurdController<SysApiKeyRes
super(service);
}
/**
* 获取接口授权资源关键字搜索字段。
*
* @return 请求接口和标题属性
*/
@Override
protected String[] getKeywordSearchProperties() {
return new String[]{"requestInterface", "title"};
}
/**
* 查询普通 API Key 接口授权资源。
*

View File

@@ -1,6 +1,7 @@
package tech.easyflow.admin.controller.system;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.mybatisflex.core.query.QueryColumn;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
@@ -89,9 +90,49 @@ public class SysDeptController extends BaseCurdController<SysDeptService, SysDep
@GetMapping("list")
public Result<List<SysDept>> list(SysDept entity, Boolean asTree, String sortKey, String sortType) {
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
String keyword = entity == null ? "" : normalizeSearchKeyword(entity.getKeyword());
if (tech.easyflow.common.util.StringUtil.hasText(keyword)) {
queryWrapper.and(buildLiteralContainsCondition(
keyword, new QueryColumn("dept_name"), new QueryColumn("dept_code")));
}
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
List<SysDept> sysMenus = service.list(queryWrapper);
return Result.ok(Tree.tryToTree(sysMenus, "id", "parentId"));
List<SysDept> matchedDepartments = service.list(queryWrapper);
if (!tech.easyflow.common.util.StringUtil.hasText(keyword)) {
return Result.ok(Tree.tryToTree(matchedDepartments, "id", "parentId"));
}
if (matchedDepartments.isEmpty()) {
return Result.ok(List.of());
}
// 搜索结果保留所有重名命中项,并补齐各自祖先节点以维持可定位的树结构。
Set<BigInteger> visibleIds = new LinkedHashSet<>();
for (SysDept department : matchedDepartments) {
visibleIds.add(department.getId());
addAncestorIds(visibleIds, department.getAncestors());
}
QueryWrapper visibleDepartmentQuery = QueryWrapper.create()
.in(SysDept::getId, visibleIds)
.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
List<SysDept> visibleDepartments = service.list(visibleDepartmentQuery);
return Result.ok(Tree.tryToTree(visibleDepartments, "id", "parentId"));
}
/**
* 将逗号分隔的祖先 ID 加入可见集合。
*
* @param visibleIds 可见部门 ID 集合
* @param ancestors 祖先路径
*/
private void addAncestorIds(Set<BigInteger> visibleIds, String ancestors) {
if (!tech.easyflow.common.util.StringUtil.hasText(ancestors)) {
return;
}
for (String ancestor : ancestors.split(",")) {
String normalized = ancestor.trim();
if (!normalized.isEmpty() && !"0".equals(normalized)) {
visibleIds.add(new BigInteger(normalized));
}
}
}
/**

View File

@@ -1,12 +1,16 @@
package tech.easyflow.admin.controller.system;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryCondition;
import com.mybatisflex.core.query.QueryMethods;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.core.relation.RelationManager;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.common.util.StringUtil;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.log.annotation.LogRecord;
@@ -20,6 +24,9 @@ import java.time.format.DateTimeParseException;
import java.util.Collections;
import java.util.Date;
import static tech.easyflow.system.entity.table.SysAccountTableDef.SYS_ACCOUNT;
import static tech.easyflow.system.entity.table.SysLogTableDef.SYS_LOG;
/**
* 操作日志表 控制层。
*
@@ -53,6 +60,22 @@ public class SysLogController extends BaseCurdController<SysLogService, SysLog>
@Override
protected QueryWrapper buildQueryWrapper(HttpServletRequest request) {
QueryWrapper queryWrapper = super.buildQueryWrapper(request);
String keyword = normalizeSearchKeyword(request.getParameter("keyword"));
if (StringUtil.hasText(keyword)) {
LoginAccount account = SaTokenUtil.getLoginAccount();
String pattern = buildLiteralContainsPattern(keyword);
QueryCondition accountKeyword = SYS_ACCOUNT.LOGIN_NAME.likeRaw(pattern)
.or(SYS_ACCOUNT.NICKNAME.likeRaw(pattern));
QueryWrapper accountExists = QueryMethods.selectOne()
.from(SYS_ACCOUNT)
.where(SYS_ACCOUNT.ID.eq(SYS_LOG.ACCOUNT_ID))
.and(SYS_ACCOUNT.TENANT_ID.eq(account.getTenantId()))
.and(accountKeyword);
QueryCondition keywordCondition = SYS_LOG.ACTION_NAME.likeRaw(pattern)
.or(SYS_LOG.ACTION_IP.likeRaw(pattern))
.or(QueryMethods.exists(accountExists));
queryWrapper.and(keywordCondition);
}
Date createdStart = parseQueryTime(request.getParameter("createdStart"));
Date createdEnd = parseQueryTime(request.getParameter("createdEnd"));
if (createdStart != null && createdEnd != null && createdStart.after(createdEnd)) {

View File

@@ -41,7 +41,7 @@ public class SysPositionController extends BaseCurdController<SysPositionService
/**
* 分页查询岗位列表
* <p>
* 支持按岗位名称模糊查询,状态、编码精确查询。
* 支持按岗位名称、岗位编码统一模糊查询,状态保持精确查询。
* </p>
*
* @param request 请求对象
@@ -67,17 +67,22 @@ public class SysPositionController extends BaseCurdController<SysPositionService
.from(SYS_POSITION);
// 获取查询参数
String keyword = normalizeSearchKeyword(request.getParameter("keyword"));
String positionName = request.getParameter("positionName");
String positionCode = request.getParameter("positionCode");
String status = request.getParameter("status");
// 岗位名称 - 模糊查询
if (StringUtil.hasText(positionName)) {
queryWrapper.where(SYS_POSITION.POSITION_NAME.like(positionName));
}
// 岗位编码 - 精确查询
if (StringUtil.hasText(positionCode)) {
queryWrapper.where(SYS_POSITION.POSITION_CODE.eq(positionCode));
if (StringUtil.hasText(keyword)) {
queryWrapper.and(buildLiteralContainsCondition(
keyword, SYS_POSITION.POSITION_NAME, SYS_POSITION.POSITION_CODE));
} else {
// 兼容仍按旧参数调用的客户端。
if (StringUtil.hasText(positionName)) {
queryWrapper.and(buildLiteralContainsCondition(positionName, SYS_POSITION.POSITION_NAME));
}
if (StringUtil.hasText(positionCode)) {
queryWrapper.where(SYS_POSITION.POSITION_CODE.eq(positionCode));
}
}
// 状态 - 精确查询
if (StringUtil.hasText(status)) {
@@ -129,4 +134,4 @@ public class SysPositionController extends BaseCurdController<SysPositionService
}
return null;
}
}
}

View File

@@ -47,6 +47,16 @@ public class SysRoleController extends BaseCurdController<SysRoleService, SysRol
super(service);
}
/**
* 获取角色列表关键字搜索字段。
*
* @return 角色名称和角色标识属性
*/
@Override
protected String[] getKeywordSearchProperties() {
return new String[]{"roleName", "roleKey"};
}
/**
* 查询角色表单所需的菜单和非 Bot 分类选项。
*

View File

@@ -24,6 +24,16 @@ public class SysUserFeedbackController extends BaseCurdController<SysUserFeedbac
super(service);
}
/**
* 获取用户反馈关键字搜索字段。
*
* @return 反馈内容和联系方式属性
*/
@Override
protected String[] getKeywordSearchProperties() {
return new String[]{"feedbackContent", "contactInfo"};
}
@Override
protected Result<?> onSaveOrUpdateBefore(SysUserFeedback entity, boolean isSave) {
if (!isSave) {
@@ -33,4 +43,4 @@ public class SysUserFeedbackController extends BaseCurdController<SysUserFeedbac
}
return super.onSaveOrUpdateBefore(entity, isSave);
}
}
}

View File

@@ -205,15 +205,20 @@ public class WorkflowDesignerOptionService {
*
* @param pageNumber 页码
* @param pageSize 每页数量
* @param keyword 插件名称或描述关键字
* @return 插件安全选项分页
*/
public Page<WorkflowDesignerOptionsView.PluginOption> pagePlugins(Long pageNumber, Long pageSize) {
public Page<WorkflowDesignerOptionsView.PluginOption> pagePlugins(
Long pageNumber, Long pageSize, String keyword) {
LoginAccount account = requireAccount();
QueryWrapper wrapper = QueryWrapper.create()
.eq(Plugin::getTenantId, account.getTenantId().longValue())
.orderBy(Plugin::getCreated, false);
List<Plugin> plugins = pluginService.getMapper().selectListWithRelationsByQuery(wrapper);
List<Plugin> availablePlugins = pluginService.preparePluginsForCurrentUser(plugins, false, true);
String normalizedKeyword = keyword == null ? "" : keyword.trim().toLowerCase(java.util.Locale.ROOT);
List<Plugin> availablePlugins = pluginService.preparePluginsForCurrentUser(plugins, false, true).stream()
.filter(plugin -> matchesKeyword(normalizedKeyword, plugin.getName(), plugin.getDescription()))
.toList();
List<WorkflowDesignerOptionsView.PluginOption> options = availablePlugins.stream()
.map(this::toPluginOption)
.toList();
@@ -229,6 +234,25 @@ public class WorkflowDesignerOptionService {
);
}
/**
* 判断任一候选文本是否包含关键字。
*
* @param keyword 已归一化的小写关键字
* @param values 候选文本
* @return 空关键字或任一文本命中时返回 {@code true}
*/
private boolean matchesKeyword(String keyword, String... values) {
if (keyword == null || keyword.isEmpty()) {
return true;
}
for (String value : values) {
if (value != null && value.toLowerCase(java.util.Locale.ROOT).contains(keyword)) {
return true;
}
}
return false;
}
/**
* 查询一个插件工具的工作流节点安全配置。
*

View File

@@ -1,13 +1,18 @@
package tech.easyflow.admin.controller.ai;
import com.mybatisflex.core.query.QueryWrapper;
import org.testng.Assert;
import org.testng.annotations.Test;
import tech.easyflow.ai.entity.Model;
import tech.easyflow.ai.mapper.ModelMapper;
import tech.easyflow.ai.service.ModelService;
import tech.easyflow.common.domain.Result;
import java.math.BigInteger;
import java.util.List;
import java.util.Map;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -38,4 +43,23 @@ public class ModelControllerTest {
result.getData().get(0).getContextWindowTokens());
verify(service).listSelectableModels(query, false, "id", "desc");
}
/**
* 验证仅包含空白字符的选择文本不会生成模糊查询条件。
*/
@Test
public void selectLlmShouldIgnoreBlankSearchText() {
ModelService service = mock(ModelService.class);
ModelMapper mapper = mock(ModelMapper.class);
when(service.getMapper()).thenReturn(mapper);
when(mapper.selectListWithRelationsByQuery(any(QueryWrapper.class)))
.thenReturn(List.of());
ModelController controller = new ModelController(service);
Result<Map<String, List<Model>>> result = controller.selectLlmByProviderAndModelType(
"chat", BigInteger.ONE, " ");
Assert.assertTrue(result.getData().isEmpty());
verify(mapper).selectListWithRelationsByQuery(any(QueryWrapper.class));
}
}

View File

@@ -11,6 +11,7 @@ import tech.easyflow.common.constant.enums.EnumDataStatus;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.tree.TreeNode;
import tech.easyflow.system.entity.SysDept;
import tech.easyflow.system.mapper.SysDeptMapper;
import tech.easyflow.system.service.SysAccountService;
@@ -18,17 +19,21 @@ import tech.easyflow.system.service.SysDeptService;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyCollection;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
/**
* {@link SysDeptController} 部门状态保存测试。
@@ -190,6 +195,44 @@ public class SysDeptControllerTest {
assertEquals(entity.getStatus(), EnumDataStatus.UNAVAILABLE.getCode());
}
/**
* 验证名称或编码模糊搜索会保留所有重名部门及其祖先路径。
*/
@Test
public void listShouldKeepAllDuplicateNameMatchesAndAncestors() {
SysDeptService service = mock(SysDeptService.class);
SysDeptController controller = createController(service);
SysDept firstMatch = buildTreeDept(BigInteger.valueOf(3), BigInteger.valueOf(2),
"1,2", "交易银行部", "A-01");
SysDept secondMatch = buildTreeDept(BigInteger.valueOf(5), BigInteger.valueOf(4),
"1,4", "交易银行部", "B-01");
List<SysDept> allDepartments = List.of(
buildTreeDept(BigInteger.ONE, BigInteger.ZERO, "0", "总行", "ROOT"),
buildTreeDept(BigInteger.valueOf(2), BigInteger.ONE, "1", "分行一", "A"),
firstMatch,
buildTreeDept(BigInteger.valueOf(4), BigInteger.ONE, "1", "分行二", "B"),
secondMatch);
when(service.list(any(QueryWrapper.class)))
.thenReturn(List.of(firstMatch, secondMatch), allDepartments);
SysDept query = new SysDept();
query.setKeyword(" 银行 ");
Result<List<SysDept>> result = controller.list(query, true, null, null);
List<SysDept> flattened = new ArrayList<>();
flattenDepartments(result.getData(), flattened);
assertEquals(flattened.stream()
.filter(department -> "交易银行部".equals(department.getDeptName()))
.count(), 2L);
assertEquals(flattened.size(), 5);
ArgumentCaptor<QueryWrapper> queryCaptor = ArgumentCaptor.forClass(QueryWrapper.class);
verify(service, times(2)).list(queryCaptor.capture());
String ancestorQuerySql = queryCaptor.getAllValues().get(1).toSQL().toUpperCase(Locale.ROOT);
assertTrue(ancestorQuerySql.contains("ID") && ancestorQuerySql.contains(" IN "),
"祖先节点查询应使用可见部门 ID 集合限制范围: " + ancestorQuerySql);
}
/**
* 构造待保存的部门。
*
@@ -217,6 +260,41 @@ public class SysDeptControllerTest {
return entity;
}
/**
* 构造部门树节点。
*
* @param id 部门主键
* @param parentId 父部门主键
* @param ancestors 祖先路径
* @param deptName 部门名称
* @param deptCode 部门编码
* @return 部门树节点
*/
private SysDept buildTreeDept(BigInteger id, BigInteger parentId, String ancestors,
String deptName, String deptCode) {
SysDept entity = buildDept(id, deptCode);
entity.setParentId(parentId);
entity.setAncestors(ancestors);
entity.setDeptName(deptName);
return entity;
}
/**
* 将部门树展开为列表。
*
* @param departments 当前层部门
* @param result 展开结果
*/
private void flattenDepartments(List<? extends TreeNode> departments, List<SysDept> result) {
for (TreeNode node : departments) {
SysDept department = (SysDept) node;
result.add(department);
if (department.getChildren() != null) {
flattenDepartments(department.getChildren(), result);
}
}
}
/**
* 调用新增前置处理。
*