diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/agent/AgentController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/agent/AgentController.java index 8894668f..6a35dbc2 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/agent/AgentController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/agent/AgentController.java @@ -101,6 +101,16 @@ public class AgentController extends BaseCurdController { super(service); } + /** + * 获取 Agent 列表关键字搜索字段。 + * + * @return Agent 名称和描述属性 + */ + @Override + protected String[] getKeywordSearchProperties() { + return new String[]{"name", "description"}; + } + /** * 获取 Agent 详情。 * diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/BotController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/BotController.java index 3ee2f0b4..37be4678 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/BotController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/BotController.java @@ -97,6 +97,16 @@ public class BotController extends BaseCurdController { this.botMessageService = botMessageService; } + /** + * 获取智能体列表关键字搜索字段。 + * + * @return 标题和描述属性 + */ + @Override + protected String[] getKeywordSearchProperties() { + return new String[]{"title", "description"}; + } + @Resource private BotPluginService botPluginService; diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/DocumentCollectionController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/DocumentCollectionController.java index a99aff78..70523874 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/DocumentCollectionController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/DocumentCollectionController.java @@ -86,6 +86,16 @@ public class DocumentCollectionController extends BaseCurdController onSaveOrUpdateBefore(DocumentCollection entity, boolean isSave) { normalizeVisibilityScope(entity, isSave); diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/DocumentController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/DocumentController.java index 2d81c131..ad92f16b 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/DocumentController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/DocumentController.java @@ -99,6 +99,16 @@ public class DocumentController extends BaseCurdController> documentList(@RequestParam(name="title", required = false) String fileName, @RequestParam(name="pageSize") int pageSize, @RequestParam(name = "pageNumber") int pageNumber) { + public Result> 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 documentList = documentService.getDocumentList(knowledge.getId().toString(), pageSize, pageNumber,fileName); + String effectiveKeyword = StringUtil.hasText(keyword) ? keyword : legacyTitle; + Page documentList = documentService.getDocumentList( + knowledge.getId().toString(), pageSize, pageNumber, effectiveKeyword); return Result.ok(documentList); } diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/FaqItemController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/FaqItemController.java index dbcdf93a..27580be0 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/FaqItemController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/FaqItemController.java @@ -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 { super(service); } + /** + * 获取 MCP 列表关键字搜索字段。 + * + * @return 标题和描述属性 + */ + @Override + protected String[] getKeywordSearchProperties() { + return new String[]{"title", "description"}; + } + @Resource private AgentResourceReferenceService agentResourceReferenceService; @Override diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/ModelController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/ModelController.java index 988a8f96..a7bf1633 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/ModelController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/ModelController.java @@ -193,8 +193,10 @@ public class ModelController extends BaseCurdController { 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 totalList = service.getMapper().selectListWithRelationsByQuery(queryWrapper); Map> groupList = totalList.stream().collect(Collectors.groupingBy(Model::getGroupName)); diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/PluginController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/PluginController.java index ebc3b78f..ca1ca71d 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/PluginController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/PluginController.java @@ -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 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 } /** - * 按分类分页查询插件,并支持按名称模糊查询。 + * 按分类分页查询插件,并支持按名称、描述模糊查询。 * * @param request 当前请求 * @param sortKey 排序字段 @@ -126,13 +137,15 @@ public class PluginController extends BaseCurdController * @param pageNumber 页码 * @param pageSize 每页数量 * @param category 分类 ID,0 表示全部分类 - * @param name 插件名称关键字 + * @param keyword 插件名称或描述关键字 + * @param name 兼容旧客户端的插件名称关键字 * @return 插件分页结果 */ @GetMapping("/pageByCategory") @SaCheckPermission("/api/v1/plugin/query") public Result> 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 queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy())); return Result.ok(queryPage(new Page<>(pageNumber, pageSize), queryWrapper)); } else { - Result> result = pluginService.pageByCategory(pageNumber, pageSize, category, name); + String effectiveKeyword = normalizeSearchKeyword( + keyword == null || keyword.isBlank() ? name : keyword); + Result> 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 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(); diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/PluginItemController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/PluginItemController.java index a3e43cac..30e898e6 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/PluginItemController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/PluginItemController.java @@ -67,6 +67,16 @@ public class PluginItemController extends BaseCurdController onSaveOrUpdateBefore(Resource entity, boolean isSave) { LoginAccount loginUser = SaTokenUtil.getLoginAccount(); diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/ShareKnowledgeController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/ShareKnowledgeController.java index 47155c46..f3d2f0ec 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/ShareKnowledgeController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/ShareKnowledgeController.java @@ -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)) { diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/WorkflowController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/WorkflowController.java index f3a8c955..45b6bfab 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/WorkflowController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/WorkflowController.java @@ -113,6 +113,16 @@ public class WorkflowController extends BaseCurdController> designerPlugins( Long pageNumber, - Long pageSize) { - return Result.ok(workflowDesignerOptionService.pagePlugins(pageNumber, pageSize)); + Long pageSize, + String keyword) { + return Result.ok(workflowDesignerOptionService.pagePlugins(pageNumber, pageSize, keyword)); } /** diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/WorkflowExecResultController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/WorkflowExecResultController.java index 1b5d4b82..de7ce6c7 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/WorkflowExecResultController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/WorkflowExecResultController.java @@ -37,6 +37,16 @@ public class WorkflowExecResultController extends BaseCurdController> getListByRecordId(BigInteger recordId) { if (recordId == null) { @@ -60,4 +70,4 @@ public class WorkflowExecStepController extends BaseCurdController this.workflowRunningParameterResolver = workflowRunningParameterResolver; } + /** + * 获取定时任务关键字搜索字段。 + * + * @return 任务名称和备注属性 + */ + @Override + protected String[] getKeywordSearchProperties() { + return new String[]{"jobName", "remark"}; + } + @GetMapping("/start") @SaCheckPermission("/api/v1/sysJob/save") @LogRecord("启动定时任务") diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/skill/SkillCategoryController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/skill/SkillCategoryController.java index d6a3873d..5418a52d 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/skill/SkillCategoryController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/skill/SkillCategoryController.java @@ -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()) { diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/skill/SkillController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/skill/SkillController.java index d7049fd7..d21bdc80 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/skill/SkillController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/skill/SkillController.java @@ -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(); } + } diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysAccountController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysAccountController.java index 67eb0802..c9e0a8e8 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysAccountController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysAccountController.java @@ -111,7 +111,7 @@ public class SysAccountController extends BaseCurdController> 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 sysMenus = service.list(queryWrapper); - return Result.ok(Tree.tryToTree(sysMenus, "id", "parentId")); + List 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 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 visibleDepartments = service.list(visibleDepartmentQuery); + return Result.ok(Tree.tryToTree(visibleDepartments, "id", "parentId")); + } + + /** + * 将逗号分隔的祖先 ID 加入可见集合。 + * + * @param visibleIds 可见部门 ID 集合 + * @param ancestors 祖先路径 + */ + private void addAncestorIds(Set 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)); + } + } } /** diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysLogController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysLogController.java index 8a6afe9d..5229e1ca 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysLogController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysLogController.java @@ -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 @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)) { diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysPositionController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysPositionController.java index 3f09f703..b1071527 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysPositionController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysPositionController.java @@ -41,7 +41,7 @@ public class SysPositionController extends BaseCurdController - * 支持按岗位名称模糊查询,按状态、编码精确查询。 + * 支持按岗位名称、岗位编码统一模糊查询,状态保持精确查询。 *

* * @param request 请求对象 @@ -67,17 +67,22 @@ public class SysPositionController extends BaseCurdController onSaveOrUpdateBefore(SysUserFeedback entity, boolean isSave) { if (!isSave) { @@ -33,4 +43,4 @@ public class SysUserFeedbackController extends BaseCurdController pagePlugins(Long pageNumber, Long pageSize) { + public Page 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 plugins = pluginService.getMapper().selectListWithRelationsByQuery(wrapper); - List availablePlugins = pluginService.preparePluginsForCurrentUser(plugins, false, true); + String normalizedKeyword = keyword == null ? "" : keyword.trim().toLowerCase(java.util.Locale.ROOT); + List availablePlugins = pluginService.preparePluginsForCurrentUser(plugins, false, true).stream() + .filter(plugin -> matchesKeyword(normalizedKeyword, plugin.getName(), plugin.getDescription())) + .toList(); List 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; + } + /** * 查询一个插件工具的工作流节点安全配置。 * diff --git a/easyflow-api/easyflow-api-admin/src/test/java/tech/easyflow/admin/controller/ai/ModelControllerTest.java b/easyflow-api/easyflow-api-admin/src/test/java/tech/easyflow/admin/controller/ai/ModelControllerTest.java index 16fac409..e15cf734 100644 --- a/easyflow-api/easyflow-api-admin/src/test/java/tech/easyflow/admin/controller/ai/ModelControllerTest.java +++ b/easyflow-api/easyflow-api-admin/src/test/java/tech/easyflow/admin/controller/ai/ModelControllerTest.java @@ -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>> result = controller.selectLlmByProviderAndModelType( + "chat", BigInteger.ONE, " "); + + Assert.assertTrue(result.getData().isEmpty()); + verify(mapper).selectListWithRelationsByQuery(any(QueryWrapper.class)); + } } diff --git a/easyflow-api/easyflow-api-admin/src/test/java/tech/easyflow/admin/controller/system/SysDeptControllerTest.java b/easyflow-api/easyflow-api-admin/src/test/java/tech/easyflow/admin/controller/system/SysDeptControllerTest.java index 353f13b6..1c51442a 100644 --- a/easyflow-api/easyflow-api-admin/src/test/java/tech/easyflow/admin/controller/system/SysDeptControllerTest.java +++ b/easyflow-api/easyflow-api-admin/src/test/java/tech/easyflow/admin/controller/system/SysDeptControllerTest.java @@ -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 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> result = controller.list(query, true, null, null); + + List flattened = new ArrayList<>(); + flattenDepartments(result.getData(), flattened); + assertEquals(flattened.stream() + .filter(department -> "交易银行部".equals(department.getDeptName())) + .count(), 2L); + assertEquals(flattened.size(), 5); + + ArgumentCaptor 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 departments, List result) { + for (TreeNode node : departments) { + SysDept department = (SysDept) node; + result.add(department); + if (department.getChildren() != null) { + flattenDepartments(department.getChildren(), result); + } + } + } + /** * 调用新增前置处理。 * diff --git a/easyflow-api/easyflow-api-usercenter/src/main/java/tech/easyflow/usercenter/controller/ai/UcBotController.java b/easyflow-api/easyflow-api-usercenter/src/main/java/tech/easyflow/usercenter/controller/ai/UcBotController.java index ea0a886a..4e2df9f7 100644 --- a/easyflow-api/easyflow-api-usercenter/src/main/java/tech/easyflow/usercenter/controller/ai/UcBotController.java +++ b/easyflow-api/easyflow-api-usercenter/src/main/java/tech/easyflow/usercenter/controller/ai/UcBotController.java @@ -80,6 +80,16 @@ public class UcBotController extends BaseCurdController { this.botDocumentCollectionService = botDocumentCollectionService; } + /** + * 获取用户中心智能体关键字搜索字段。 + * + * @return 标题和描述属性 + */ + @Override + protected String[] getKeywordSearchProperties() { + return new String[]{"title", "description"}; + } + @Resource private BotPluginService botPluginService; @Resource @@ -246,6 +256,10 @@ public class UcBotController extends BaseCurdController { @Override public Result> list(Bot entity, Boolean asTree, String sortKey, String sortType) { QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity)); + String keyword = entity == null ? "" : normalizeSearchKeyword(entity.getKeyword()); + if (!keyword.isEmpty()) { + queryWrapper.and(buildLiteralContainsCondition(keyword, BOT.TITLE, BOT.DESCRIPTION)); + } applyCategoryPermission(queryWrapper); queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy())); return Result.ok(service.list(queryWrapper)); diff --git a/easyflow-api/easyflow-api-usercenter/src/main/java/tech/easyflow/usercenter/controller/ai/UcWorkflowController.java b/easyflow-api/easyflow-api-usercenter/src/main/java/tech/easyflow/usercenter/controller/ai/UcWorkflowController.java index edd4097a..ca21718a 100644 --- a/easyflow-api/easyflow-api-usercenter/src/main/java/tech/easyflow/usercenter/controller/ai/UcWorkflowController.java +++ b/easyflow-api/easyflow-api-usercenter/src/main/java/tech/easyflow/usercenter/controller/ai/UcWorkflowController.java @@ -34,6 +34,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import static tech.easyflow.ai.entity.table.WorkflowTableDef.WORKFLOW; + /** * 工作流 */ @@ -57,6 +59,16 @@ public class UcWorkflowController extends BaseCurdController> list(Workflow entity, Boolean asTree, String sortKey, String sortType) { QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity)); + String keyword = entity == null ? "" : normalizeSearchKeyword(entity.getKeyword()); + if (!keyword.isEmpty()) { + queryWrapper.and(buildLiteralContainsCondition(keyword, WORKFLOW.TITLE, WORKFLOW.DESCRIPTION)); + } workflowVisibilityQueryHelper.applyReadableAccess(queryWrapper); queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy())); return Result.ok(service.list(queryWrapper)); diff --git a/easyflow-commons/easyflow-common-base/src/main/java/tech/easyflow/common/util/SearchKeywordUtil.java b/easyflow-commons/easyflow-common-base/src/main/java/tech/easyflow/common/util/SearchKeywordUtil.java new file mode 100644 index 00000000..dcab96fb --- /dev/null +++ b/easyflow-commons/easyflow-common-base/src/main/java/tech/easyflow/common/util/SearchKeywordUtil.java @@ -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 + "%"; + } +} diff --git a/easyflow-commons/easyflow-common-web/src/main/java/tech/easyflow/common/web/controller/BaseCurdController.java b/easyflow-commons/easyflow-common-web/src/main/java/tech/easyflow/common/web/controller/BaseCurdController.java index bac9fb9c..a54e1310 100644 --- a/easyflow-commons/easyflow-common-web/src/main/java/tech/easyflow/common/web/controller/BaseCurdController.java +++ b/easyflow-commons/easyflow-common-web/src/main/java/tech/easyflow/common/web/controller/BaseCurdController.java @@ -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, M> extends BaseController { + private static final String KEYWORD_PARAM = "keyword"; protected final S service; @@ -198,6 +202,7 @@ public class BaseCurdController, M> extends BaseController Map propertyColumnMapping = TableInfoFactory.ofEntityClass(getEntityClass()) .getPropertyColumnMapping(); + QueryCondition legacyOrCondition = null; for (Map.Entry 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, 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, M> extends BaseController } } + if (legacyOrCondition != null) { + queryWrapper.and(legacyOrCondition); + } + applyKeywordSearch(queryWrapper, request, propertyColumnMapping); + return queryWrapper; } + /** + * 获取当前控制器统一关键字需要匹配的实体属性。 + * + *

属性名必须来自当前实体映射。分类、状态、租户等精确条件不应放入该数组。

+ * + * @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 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) { diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/Bot.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/Bot.java index 2be82a4a..4ce6f565 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/Bot.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/Bot.java @@ -32,6 +32,9 @@ public class Bot extends BotBase { @Column(ignore = true) private String createdByName; + @Column(ignore = true) + private String keyword; + public boolean isAnonymousEnabled() { Map options = getOptions(); if (options == null) { @@ -83,4 +86,22 @@ public class Bot extends BotBase { this.createdByName = createdByName; } + /** + * 获取市场列表搜索关键字。 + * + * @return 标题或描述关键字 + */ + public String getKeyword() { + return keyword; + } + + /** + * 设置市场列表搜索关键字。 + * + * @param keyword 标题或描述关键字 + */ + public void setKeyword(String keyword) { + this.keyword = keyword; + } + } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/Workflow.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/Workflow.java index 01402136..d4232c16 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/Workflow.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/Workflow.java @@ -29,6 +29,9 @@ public class Workflow extends WorkflowBase implements VisibilityResource { @Column(ignore = true) private String createdByName; + @Column(ignore = true) + private String keyword; + public Tool toFunction(boolean needEnglishName) { return new WorkflowTool(this, needEnglishName); } @@ -78,4 +81,22 @@ public class Workflow extends WorkflowBase implements VisibilityResource { public void setCreatedByName(String createdByName) { this.createdByName = createdByName; } + + /** + * 获取市场列表搜索关键字。 + * + * @return 标题或描述关键字 + */ + public String getKeyword() { + return keyword; + } + + /** + * 设置市场列表搜索关键字。 + * + * @param keyword 标题或描述关键字 + */ + public void setKeyword(String keyword) { + this.keyword = keyword; + } } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/PluginService.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/PluginService.java index d38a7064..de025cb3 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/PluginService.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/PluginService.java @@ -27,10 +27,10 @@ public interface PluginService extends IService { * @param pageNumber 页码 * @param pageSize 每页数量 * @param category 分类 ID - * @param name 插件名称关键字 + * @param keyword 插件名称或描述关键字 * @return 插件分页结果 */ - Result> pageByCategory(Long pageNumber, Long pageSize, int category, String name); + Result> pageByCategory(Long pageNumber, Long pageSize, int category, String keyword); boolean updatePlugin(Plugin plugin); diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/DocumentServiceImpl.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/DocumentServiceImpl.java index f037a43e..0627e3e4 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/DocumentServiceImpl.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/DocumentServiceImpl.java @@ -31,6 +31,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import tech.easyflow.ai.config.SearcherFactory; +import tech.easyflow.common.util.SearchKeywordUtil; import tech.easyflow.ai.documentimport.DocumentImportDtos; import tech.easyflow.ai.documentimport.DocumentImportKeys; import tech.easyflow.ai.documentimport.DocumentImportPreviewService; @@ -183,7 +184,7 @@ public class DocumentServiceImpl extends ServiceImpl i .where(DOCUMENT.COLLECTION_ID.eq(knowledgeId)) .orderBy(DOCUMENT.ID, false); if (fileName != null && !fileName.trim().isEmpty()) { - queryWrapper.and(DOCUMENT.TITLE.like(fileName)); + queryWrapper.and(DOCUMENT.TITLE.likeRaw(SearchKeywordUtil.literalContainsPattern(fileName))); } if (documentId != null) { queryWrapper.and(DOCUMENT.ID.eq(documentId)); diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/PluginServiceImpl.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/PluginServiceImpl.java index 41bef8aa..04d358a5 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/PluginServiceImpl.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/PluginServiceImpl.java @@ -23,6 +23,7 @@ import tech.easyflow.common.domain.Result; import tech.easyflow.common.web.exceptions.BusinessException; import tech.easyflow.common.entity.LoginAccount; import tech.easyflow.common.satoken.util.SaTokenUtil; +import tech.easyflow.common.util.SearchKeywordUtil; import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot; import tech.easyflow.system.service.CategoryPermissionService; @@ -138,7 +139,7 @@ public class PluginServiceImpl extends ServiceImpl impleme } @Override - public Result> pageByCategory(Long pageNumber, Long pageSize, int category, String name) { + public Result> pageByCategory(Long pageNumber, Long pageSize, int category, String keyword) { RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("PLUGIN"); QueryWrapper queryWrapper = QueryWrapper.create().select(PluginCategoryMapping::getPluginId) .eq(PluginCategoryMapping::getCategoryId, category); @@ -165,7 +166,7 @@ public class PluginServiceImpl extends ServiceImpl impleme } List totalPlugins = preparePluginsForCurrentUser( - queryPluginsByIds(visiblePluginIds, name), true, false); + queryPluginsByIds(visiblePluginIds, keyword), true, false); int fromIndex = Math.max(0, Math.toIntExact((pageNumber - 1) * pageSize)); if (fromIndex >= totalPlugins.size()) { return Result.ok(new Page<>(Collections.emptyList(), pageNumber, pageSize, totalPlugins.size())); @@ -259,19 +260,20 @@ public class PluginServiceImpl extends ServiceImpl impleme } /** - * 按给定顺序查询插件,并按名称关键字过滤。 + * 按给定顺序查询插件,并按名称或描述关键字过滤。 * * @param pluginIds 插件 ID 列表 - * @param name 插件名称关键字 + * @param keyword 插件名称或描述关键字 * @return 保持输入 ID 顺序的插件列表 */ - private List queryPluginsByIds(List pluginIds, String name) { + private List queryPluginsByIds(List pluginIds, String keyword) { if (CollectionUtil.isEmpty(pluginIds)) { return Collections.emptyList(); } QueryWrapper queryPluginWrapper = QueryWrapper.create().select().in(Plugin::getId, pluginIds); - if (name != null && !name.isBlank()) { - queryPluginWrapper.like(Plugin::getName, name.trim()); + if (keyword != null && !keyword.isBlank()) { + String pattern = SearchKeywordUtil.literalContainsPattern(keyword); + queryPluginWrapper.and(PLUGIN.NAME.likeRaw(pattern).or(PLUGIN.DESCRIPTION.likeRaw(pattern))); } List plugins = pluginMapper.selectListWithRelationsByQuery(queryPluginWrapper); Map pluginMap = plugins.stream().collect(Collectors.toMap( diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalAssigneeServiceImpl.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalAssigneeServiceImpl.java index eb2e355e..24d42007 100644 --- a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalAssigneeServiceImpl.java +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalAssigneeServiceImpl.java @@ -19,6 +19,7 @@ import tech.easyflow.approval.mapper.ApprovalTaskAssigneeMapper; import tech.easyflow.approval.mapper.ApprovalTaskMapper; import tech.easyflow.approval.service.ApprovalAssigneeService; import tech.easyflow.common.constant.enums.EnumDataStatus; +import tech.easyflow.common.util.SearchKeywordUtil; import tech.easyflow.common.web.exceptions.BusinessException; import tech.easyflow.system.entity.SysAccount; import tech.easyflow.system.entity.SysAccountRole; @@ -365,7 +366,7 @@ public class ApprovalAssigneeServiceImpl implements ApprovalAssigneeService { .eq(SysAccount::getStatus, EnumDataStatus.AVAILABLE.getCode()) .orderBy("nickname asc, login_name asc, id asc"); if (StringUtils.hasText(keyword)) { - String likeKeyword = "%" + keyword.trim() + "%"; + String likeKeyword = SearchKeywordUtil.literalContainsPattern(keyword); queryWrapper.and("(`login_name` like ? or `nickname` like ?)", likeKeyword, likeKeyword); } Page page = sysAccountService.page(new Page<>(actualPageNumber, actualPageSize), queryWrapper); diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalFlowServiceImpl.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalFlowServiceImpl.java index 7f3c8f13..6331ba84 100644 --- a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalFlowServiceImpl.java +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalFlowServiceImpl.java @@ -7,6 +7,7 @@ import com.mybatisflex.spring.service.impl.ServiceImpl; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; +import tech.easyflow.common.util.SearchKeywordUtil; import tech.easyflow.common.web.exceptions.BusinessException; import tech.easyflow.approval.entity.ApprovalFlow; import tech.easyflow.approval.entity.ApprovalFlowScope; @@ -72,7 +73,7 @@ public class ApprovalFlowServiceImpl extends ServiceImpl params = new ArrayList<>(); StringBuilder sql = new StringBuilder("SELECT COUNT(1) FROM `").append(table) @@ -146,10 +162,31 @@ public class MySqlChatSessionRepository { sql.append(" AND assistant_code=?"); params.add(assistantCode); } + appendKeywordCondition(sql, params, query); Long count = jdbcTemplate.queryForObject(sql.toString(), Long.class, params.toArray()); return count == null ? 0L : count; } + /** + * 向会话 SQL 追加标题、最近消息和助手名称的普通文本包含匹配。 + * + * @param sql SQL 构造器 + * @param params SQL 参数 + * @param query 查询条件 + */ + private void appendKeywordCondition(StringBuilder sql, List params, ChatPageQuery query) { + String keyword = query == null ? null : query.getKeyword(); + if (keyword == null || keyword.isBlank()) { + return; + } + String pattern = SearchKeywordUtil.literalContainsPattern(keyword); + sql.append(" AND (title LIKE ? ESCAPE '\\\\' OR last_message_preview LIKE ? ESCAPE '\\\\'" + + " OR assistant_name LIKE ? ESCAPE '\\\\')"); + params.add(pattern); + params.add(pattern); + params.add(pattern); + } + public ChatSessionSummary findBySessionIdAndUserId(BigInteger sessionId, BigInteger userId) { String table = tableRouter.resolveSessionTable(); List list = jdbcTemplate.query( diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/ChatSessionQueryServiceImpl.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/ChatSessionQueryServiceImpl.java index d27d1518..39714890 100644 --- a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/ChatSessionQueryServiceImpl.java +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/ChatSessionQueryServiceImpl.java @@ -64,7 +64,7 @@ public class ChatSessionQueryServiceImpl implements ChatSessionQueryService { page.setPageNumber(query.getPageNumber()); page.setPageSize(query.getPageSize()); - page.setTotal(sessionRepository.countSessions(userId, assistantId, assistantCode)); + page.setTotal(sessionRepository.countSessions(userId, assistantId, assistantCode, query)); page.setRecords(listSessions(userId, assistantId, assistantCode, query)); return page; } diff --git a/easyflow-modules/easyflow-module-chatlog/src/test/java/tech/easyflow/chatlog/service/impl/ChatSessionQueryServiceImplTest.java b/easyflow-modules/easyflow-module-chatlog/src/test/java/tech/easyflow/chatlog/service/impl/ChatSessionQueryServiceImplTest.java index 01e6f344..2ce1068c 100644 --- a/easyflow-modules/easyflow-module-chatlog/src/test/java/tech/easyflow/chatlog/service/impl/ChatSessionQueryServiceImplTest.java +++ b/easyflow-modules/easyflow-module-chatlog/src/test/java/tech/easyflow/chatlog/service/impl/ChatSessionQueryServiceImplTest.java @@ -69,6 +69,27 @@ public class ChatSessionQueryServiceImplTest { Assert.assertEquals("AGENT", sessionRepository.capturedCountAssistantCode); } + /** + * 会话关键词必须同时下推到列表和计数查询,保证分页总数一致。 + */ + @Test + public void pageSessionsShouldPassKeywordToListAndCountQueries() { + FakeSessionRepository sessionRepository = new FakeSessionRepository(); + ChatSessionQueryServiceImpl service = new ChatSessionQueryServiceImpl( + sessionRepository, + new FakeLogRepository(), + new FakeTableManager(List.of()), + new FakeHotStateService() + ); + ChatPageQuery query = new ChatPageQuery(); + query.setKeyword("最近消息"); + + service.pageSessions(BigInteger.valueOf(7), null, query); + + Assert.assertSame(query, sessionRepository.capturedListQuery); + Assert.assertSame(query, sessionRepository.capturedCountQuery); + } + /** * 工作台消息分页必须走 MySQL 热表主线查询,并保持分页参数语义。 */ @@ -147,6 +168,8 @@ public class ChatSessionQueryServiceImplTest { private int listSessionsCalls; private String capturedListAssistantCode; private String capturedCountAssistantCode; + private ChatPageQuery capturedListQuery; + private ChatPageQuery capturedCountQuery; private ChatSessionSummary summary; private List sessions = new ArrayList<>(); @@ -163,6 +186,7 @@ public class ChatSessionQueryServiceImplTest { public List listSessions(BigInteger userId, BigInteger assistantId, String assistantCode, ChatPageQuery query) { listSessionsCalls += 1; capturedListAssistantCode = assistantCode; + capturedListQuery = query; return sessions; } @@ -173,8 +197,15 @@ public class ChatSessionQueryServiceImplTest { @Override public long countSessions(BigInteger userId, BigInteger assistantId, String assistantCode) { + return countSessions(userId, assistantId, assistantCode, null); + } + + @Override + public long countSessions(BigInteger userId, BigInteger assistantId, + String assistantCode, ChatPageQuery query) { countSessionsCalls += 1; capturedCountAssistantCode = assistantCode; + capturedCountQuery = query; return count; } diff --git a/easyflow-modules/easyflow-module-skill/src/main/java/tech/easyflow/skill/capability/SkillCapabilityTargetAccessServiceImpl.java b/easyflow-modules/easyflow-module-skill/src/main/java/tech/easyflow/skill/capability/SkillCapabilityTargetAccessServiceImpl.java index 5c5de9eb..a23a66df 100644 --- a/easyflow-modules/easyflow-module-skill/src/main/java/tech/easyflow/skill/capability/SkillCapabilityTargetAccessServiceImpl.java +++ b/easyflow-modules/easyflow-module-skill/src/main/java/tech/easyflow/skill/capability/SkillCapabilityTargetAccessServiceImpl.java @@ -19,6 +19,7 @@ import tech.easyflow.ai.service.PluginVisibilityService; import tech.easyflow.ai.service.WorkflowService; 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.skill.entity.SkillCapabilityBinding; import tech.easyflow.skill.enums.SkillCapabilityType; @@ -502,7 +503,7 @@ public class SkillCapabilityTargetAccessServiceImpl implements SkillCapabilityTa if (keyword == null || keyword.isBlank() || columns.length == 0) { return; } - String pattern = "%" + keyword.toLowerCase(Locale.ROOT) + "%"; + String pattern = SearchKeywordUtil.literalContainsPattern(keyword.toLowerCase(Locale.ROOT)); StringBuilder condition = new StringBuilder("("); Object[] arguments = new Object[columns.length]; for (int index = 0; index < columns.length; index++) { diff --git a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/entity/SysDept.java b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/entity/SysDept.java index 23a09001..e9d412f9 100644 --- a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/entity/SysDept.java +++ b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/entity/SysDept.java @@ -1,5 +1,6 @@ package tech.easyflow.system.entity; +import com.mybatisflex.annotation.Column; import tech.easyflow.system.entity.base.SysDeptBase; import com.mybatisflex.annotation.Table; @@ -12,4 +13,25 @@ import com.mybatisflex.annotation.Table; @Table(value = "tb_sys_dept", comment = "部门表") public class SysDept extends SysDeptBase { + + @Column(ignore = true) + private String keyword; + + /** + * 获取部门列表搜索关键字。 + * + * @return 部门名称或编码关键字 + */ + public String getKeyword() { + return keyword; + } + + /** + * 设置部门列表搜索关键字。 + * + * @param keyword 部门名称或编码关键字 + */ + public void setKeyword(String keyword) { + this.keyword = keyword; + } } diff --git a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/entity/SysDict.java b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/entity/SysDict.java index 14f582dc..5eb7dc16 100644 --- a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/entity/SysDict.java +++ b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/entity/SysDict.java @@ -1,5 +1,6 @@ package tech.easyflow.system.entity; +import tech.easyflow.common.util.SearchKeywordUtil; import tech.easyflow.common.util.SpringContextUtil; import tech.easyflow.common.dict.Dict; import tech.easyflow.common.dict.DictItem; @@ -81,7 +82,7 @@ public class SysDict extends SysDictBase { QueryWrapper qw = QueryWrapper.create() .eq(SysDictItem::getDictId, this.dictId) .eq(SysDictItem::getStatus, 0) - .like(SysDictItem::getText, keyword); + .and("`text` LIKE ?", SearchKeywordUtil.literalContainsPattern(keyword)); List sysDictItems = itemService.list(qw); Dict dict = new Dict(); diff --git a/easyflow-ui-admin/app/src/components/commonSelectModal/CommonSelectDataModal.vue b/easyflow-ui-admin/app/src/components/commonSelectModal/CommonSelectDataModal.vue index b1fa3f6d..8768453d 100644 --- a/easyflow-ui-admin/app/src/components/commonSelectModal/CommonSelectDataModal.vue +++ b/easyflow-ui-admin/app/src/components/commonSelectModal/CommonSelectDataModal.vue @@ -38,10 +38,6 @@ const props = defineProps({ title: { type: String, default: '' }, width: { type: String, default: '80%' }, extraQueryParams: { type: Object, default: () => ({}) }, - searchParams: { - type: Array as PropType, - default: () => [], - }, titleKey: { type: String, default: 'name' }, pageUrl: { type: String, default: '' }, hasParent: { type: Boolean, default: false }, @@ -183,14 +179,8 @@ const handleSubmitRun = () => { }; const handleSearch = (query: string) => { - const tempParams = {} as Record; - props.searchParams.forEach((paramName) => { - tempParams[paramName] = query; - }); - pageDataRef.value?.setQuery({ - isQueryOr: true, - ...tempParams, + keyword: query.trim() || undefined, }); }; diff --git a/easyflow-ui-admin/app/src/components/headerSearch/HeaderSearch.vue b/easyflow-ui-admin/app/src/components/headerSearch/HeaderSearch.vue index 9dffeb7e..f139cd05 100644 --- a/easyflow-ui-admin/app/src/components/headerSearch/HeaderSearch.vue +++ b/easyflow-ui-admin/app/src/components/headerSearch/HeaderSearch.vue @@ -76,9 +76,15 @@ const emitButtonEvent = (payload) => { }; const handleSearch = () => { + searchValue.value = searchValue.value.trim(); emit('search', searchValue.value); }; +const handleClear = () => { + searchValue.value = ''; + emit('search', ''); +}; + const handleReset = () => { searchValue.value = ''; emit('reset'); @@ -117,6 +123,7 @@ const handleDropdownClick = (button) => { :placeholder="searchPlaceholder" class="search-input" clearable + @clear="handleClear" @keyup.enter="handleSearch" >