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); super(service);
} }
/**
* 获取 Agent 列表关键字搜索字段。
*
* @return Agent 名称和描述属性
*/
@Override
protected String[] getKeywordSearchProperties() {
return new String[]{"name", "description"};
}
/** /**
* 获取 Agent 详情。 * 获取 Agent 详情。
* *

View File

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

View File

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

View File

@@ -99,6 +99,16 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
super(service); super(service);
this.knowledgeService = knowledgeService; this.knowledgeService = knowledgeService;
} }
/**
* 获取知识库文档关键字搜索字段。
*
* @return 文件标题属性
*/
@Override
protected String[] getKeywordSearchProperties() {
return new String[]{"title"};
}
@PostMapping("removeDoc") @PostMapping("removeDoc")
@Transactional @Transactional
@SaCheckPermission("/api/v1/documentCollection/remove") @SaCheckPermission("/api/v1/documentCollection/remove")
@@ -148,13 +158,19 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
@GetMapping("documentList") @GetMapping("documentList")
@SaCheckPermission("/api/v1/documentCollection/query") @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"); String kbSlug = RequestUtil.getParamAsString("id");
if (StringUtil.noText(kbSlug)) { if (StringUtil.noText(kbSlug)) {
throw new BusinessException("知识库id不能为空"); throw new BusinessException("知识库id不能为空");
} }
DocumentCollection knowledge = getDocumentCollection(kbSlug, ResourceAction.READ, "无权限访问知识库"); 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); return Result.ok(documentList);
} }

View File

@@ -2,6 +2,7 @@ package tech.easyflow.admin.controller.ai;
import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckPermission;
import com.mybatisflex.core.paginate.Page; import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryColumn;
import com.mybatisflex.core.query.QueryWrapper; import com.mybatisflex.core.query.QueryWrapper;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
@@ -114,9 +115,14 @@ public class FaqItemController extends BaseCurdController<FaqItemService, FaqIte
QueryWrapper queryWrapper = QueryWrapper.create() QueryWrapper queryWrapper = QueryWrapper.create()
.eq(FaqItem::getCollectionId, collectionId); .eq(FaqItem::getCollectionId, collectionId);
String question = request.getParameter("question"); String keyword = normalizeSearchKeyword(request.getParameter("keyword"));
if (question != null && !question.trim().isEmpty()) { String question = normalizeSearchKeyword(request.getParameter("question"));
queryWrapper.like(FaqItem::getQuestion, question.trim()); 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"); String categoryIdText = request.getParameter("categoryId");

View File

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

View File

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

View File

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

View File

@@ -45,6 +45,16 @@ public class ResourceController extends BaseCurdController<ResourceService, Reso
super(service); super(service);
} }
/**
* 获取资源列表关键字搜索字段。
*
* @return 资源名称属性
*/
@Override
protected String[] getKeywordSearchProperties() {
return new String[]{"resourceName"};
}
@Override @Override
protected Result<?> onSaveOrUpdateBefore(Resource entity, boolean isSave) { protected Result<?> onSaveOrUpdateBefore(Resource entity, boolean isSave) {
LoginAccount loginUser = SaTokenUtil.getLoginAccount(); 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.StoreOptions;
import com.easyagents.core.store.StoreResult; import com.easyagents.core.store.StoreResult;
import com.mybatisflex.core.paginate.Page; import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryColumn;
import com.mybatisflex.core.query.QueryWrapper; import com.mybatisflex.core.query.QueryWrapper;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; 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.KnowledgeShareAuthContext;
import tech.easyflow.ai.vo.KnowledgeShareViewDetail; import tech.easyflow.ai.vo.KnowledgeShareViewDetail;
import tech.easyflow.common.domain.Result; import tech.easyflow.common.domain.Result;
import tech.easyflow.common.util.SearchKeywordUtil;
import tech.easyflow.common.filestorage.FileStorageService; import tech.easyflow.common.filestorage.FileStorageService;
import tech.easyflow.common.vo.UploadResVo; import tech.easyflow.common.vo.UploadResVo;
import tech.easyflow.common.web.exceptions.BusinessException; import tech.easyflow.common.web.exceptions.BusinessException;
@@ -668,9 +670,15 @@ public class ShareKnowledgeController {
faqCategoryService.ensureDefaultCategory(context.getKnowledge().getId()); faqCategoryService.ensureDefaultCategory(context.getKnowledge().getId());
QueryWrapper queryWrapper = QueryWrapper.create() QueryWrapper queryWrapper = QueryWrapper.create()
.eq(FaqItem::getCollectionId, context.getKnowledge().getId()); .eq(FaqItem::getCollectionId, context.getKnowledge().getId());
String keyword = request.getParameter("keyword");
String question = request.getParameter("question"); String question = request.getParameter("question");
if (StringUtils.hasText(question)) { if (StringUtils.hasText(keyword)) {
queryWrapper.like(FaqItem::getQuestion, question.trim()); 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"); String categoryId = request.getParameter("categoryId");
if (StringUtils.hasText(categoryId)) { if (StringUtils.hasText(categoryId)) {

View File

@@ -113,6 +113,16 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
this.modelService = modelService; 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 pageNumber 页码
* @param pageSize 每页数量 * @param pageSize 每页数量
* @param keyword 插件名称或描述关键字
* @return 插件安全选项分页 * @return 插件安全选项分页
*/ */
@GetMapping("/designer/plugins") @GetMapping("/designer/plugins")
@SaCheckPermission("/api/v1/workflow/query") @SaCheckPermission("/api/v1/workflow/query")
public Result<Page<WorkflowDesignerOptionsView.PluginOption>> designerPlugins( public Result<Page<WorkflowDesignerOptionsView.PluginOption>> designerPlugins(
Long pageNumber, Long pageNumber,
Long pageSize) { Long pageSize,
return Result.ok(workflowDesignerOptionService.pagePlugins(pageNumber, 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); super(service);
} }
/**
* 获取工作流执行记录关键字搜索字段。
*
* @return 执行 Key 属性
*/
@Override
protected String[] getKeywordSearchProperties() {
return new String[]{"execKey"};
}
@GetMapping("/del") @GetMapping("/del")
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
@SaCheckPermission("/api/v1/workflow/remove") @SaCheckPermission("/api/v1/workflow/remove")

View File

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

View File

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

View File

@@ -10,6 +10,7 @@ import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.domain.Result; import tech.easyflow.common.domain.Result;
import tech.easyflow.common.entity.LoginAccount; import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil; 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.exceptions.BusinessException;
import tech.easyflow.common.web.jsonbody.JsonBody; import tech.easyflow.common.web.jsonbody.JsonBody;
import tech.easyflow.skill.entity.SkillCategory; import tech.easyflow.skill.entity.SkillCategory;
@@ -69,9 +70,11 @@ public class SkillCategoryController {
queryWrapper.eq(SkillCategory::getId, entity.getId(), entity.getId() != null) queryWrapper.eq(SkillCategory::getId, entity.getId(), entity.getId() != null)
.eq(SkillCategory::getParentId, entity.getParentId(), entity.getParentId() != null) .eq(SkillCategory::getParentId, entity.getParentId(), entity.getParentId() != null)
.eq(SkillCategory::getLevelNo, entity.getLevelNo(), entity.getLevelNo() != null) .eq(SkillCategory::getLevelNo, entity.getLevelNo(), entity.getLevelNo() != null)
.eq(SkillCategory::getStatus, entity.getStatus(), entity.getStatus() != null) .eq(SkillCategory::getStatus, entity.getStatus(), entity.getStatus() != null);
.like(SkillCategory::getCategoryName, entity.getCategoryName(), if (entity.getCategoryName() != null && !entity.getCategoryName().isBlank()) {
entity.getCategoryName() != null && !entity.getCategoryName().isBlank()); queryWrapper.and("category_name LIKE ?",
SearchKeywordUtil.literalContainsPattern(entity.getCategoryName()));
}
} }
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess(CategoryResourceType.SKILL.getCode()); RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess(CategoryResourceType.SKILL.getCode());
if (access.isRestricted()) { 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.entity.LoginAccount;
import tech.easyflow.common.domain.Result; import tech.easyflow.common.domain.Result;
import tech.easyflow.common.satoken.util.SaTokenUtil; 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.exceptions.BusinessException;
import tech.easyflow.common.web.jsonbody.JsonBody; import tech.easyflow.common.web.jsonbody.JsonBody;
import tech.easyflow.skill.capability.SkillCapabilityBindingService; import tech.easyflow.skill.capability.SkillCapabilityBindingService;
@@ -159,7 +160,7 @@ public class SkillController {
.eq("source_type", sourceType, hasText(sourceType)); .eq("source_type", sourceType, hasText(sourceType));
String keyword = hasText(displayName) ? displayName : name; String keyword = hasText(displayName) ? displayName : name;
if (hasText(keyword)) { 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); query.and("(name LIKE ? OR display_name LIKE ? OR description LIKE ?)", pattern, pattern, pattern);
} }
if (hasText(capabilityType)) { if (hasText(capabilityType)) {
@@ -695,4 +696,5 @@ public class SkillController {
private boolean hasText(String value) { private boolean hasText(String value) {
return value != null && !value.isBlank(); return value != null && !value.isBlank();
} }
} }

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
package tech.easyflow.admin.controller.system; package tech.easyflow.admin.controller.system;
import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckPermission;
import com.mybatisflex.core.query.QueryColumn;
import com.mybatisflex.core.query.QueryWrapper; import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -89,9 +90,49 @@ public class SysDeptController extends BaseCurdController<SysDeptService, SysDep
@GetMapping("list") @GetMapping("list")
public Result<List<SysDept>> list(SysDept entity, Boolean asTree, String sortKey, String sortType) { public Result<List<SysDept>> list(SysDept entity, Boolean asTree, String sortKey, String sortType) {
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity)); 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())); queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
List<SysDept> sysMenus = service.list(queryWrapper); List<SysDept> matchedDepartments = service.list(queryWrapper);
return Result.ok(Tree.tryToTree(sysMenus, "id", "parentId")); 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; package tech.easyflow.admin.controller.system;
import com.mybatisflex.core.paginate.Page; 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.query.QueryWrapper;
import com.mybatisflex.core.relation.RelationManager; import com.mybatisflex.core.relation.RelationManager;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.common.util.StringUtil; 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.controller.BaseCurdController;
import tech.easyflow.common.web.exceptions.BusinessException; import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.log.annotation.LogRecord; import tech.easyflow.log.annotation.LogRecord;
@@ -20,6 +24,9 @@ import java.time.format.DateTimeParseException;
import java.util.Collections; import java.util.Collections;
import java.util.Date; 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 @Override
protected QueryWrapper buildQueryWrapper(HttpServletRequest request) { protected QueryWrapper buildQueryWrapper(HttpServletRequest request) {
QueryWrapper queryWrapper = super.buildQueryWrapper(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 createdStart = parseQueryTime(request.getParameter("createdStart"));
Date createdEnd = parseQueryTime(request.getParameter("createdEnd")); Date createdEnd = parseQueryTime(request.getParameter("createdEnd"));
if (createdStart != null && createdEnd != null && createdStart.after(createdEnd)) { if (createdStart != null && createdEnd != null && createdStart.after(createdEnd)) {

View File

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

View File

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

View File

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

View File

@@ -205,15 +205,20 @@ public class WorkflowDesignerOptionService {
* *
* @param pageNumber 页码 * @param pageNumber 页码
* @param pageSize 每页数量 * @param pageSize 每页数量
* @param keyword 插件名称或描述关键字
* @return 插件安全选项分页 * @return 插件安全选项分页
*/ */
public Page<WorkflowDesignerOptionsView.PluginOption> pagePlugins(Long pageNumber, Long pageSize) { public Page<WorkflowDesignerOptionsView.PluginOption> pagePlugins(
Long pageNumber, Long pageSize, String keyword) {
LoginAccount account = requireAccount(); LoginAccount account = requireAccount();
QueryWrapper wrapper = QueryWrapper.create() QueryWrapper wrapper = QueryWrapper.create()
.eq(Plugin::getTenantId, account.getTenantId().longValue()) .eq(Plugin::getTenantId, account.getTenantId().longValue())
.orderBy(Plugin::getCreated, false); .orderBy(Plugin::getCreated, false);
List<Plugin> plugins = pluginService.getMapper().selectListWithRelationsByQuery(wrapper); 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() List<WorkflowDesignerOptionsView.PluginOption> options = availablePlugins.stream()
.map(this::toPluginOption) .map(this::toPluginOption)
.toList(); .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; package tech.easyflow.admin.controller.ai;
import com.mybatisflex.core.query.QueryWrapper;
import org.testng.Assert; import org.testng.Assert;
import org.testng.annotations.Test; import org.testng.annotations.Test;
import tech.easyflow.ai.entity.Model; import tech.easyflow.ai.entity.Model;
import tech.easyflow.ai.mapper.ModelMapper;
import tech.easyflow.ai.service.ModelService; import tech.easyflow.ai.service.ModelService;
import tech.easyflow.common.domain.Result; import tech.easyflow.common.domain.Result;
import java.math.BigInteger;
import java.util.List; 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.mock;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -38,4 +43,23 @@ public class ModelControllerTest {
result.getData().get(0).getContextWindowTokens()); result.getData().get(0).getContextWindowTokens());
verify(service).listSelectableModels(query, false, "id", "desc"); 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.domain.Result;
import tech.easyflow.common.entity.LoginAccount; import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil; import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.tree.TreeNode;
import tech.easyflow.system.entity.SysDept; import tech.easyflow.system.entity.SysDept;
import tech.easyflow.system.mapper.SysDeptMapper; import tech.easyflow.system.mapper.SysDeptMapper;
import tech.easyflow.system.service.SysAccountService; import tech.easyflow.system.service.SysAccountService;
@@ -18,17 +19,21 @@ import tech.easyflow.system.service.SysDeptService;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Locale;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyCollection; import static org.mockito.ArgumentMatchers.anyCollection;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
/** /**
* {@link SysDeptController} 部门状态保存测试。 * {@link SysDeptController} 部门状态保存测试。
@@ -190,6 +195,44 @@ public class SysDeptControllerTest {
assertEquals(entity.getStatus(), EnumDataStatus.UNAVAILABLE.getCode()); 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; 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);
}
}
}
/** /**
* 调用新增前置处理。 * 调用新增前置处理。
* *

View File

@@ -80,6 +80,16 @@ public class UcBotController extends BaseCurdController<BotService, Bot> {
this.botDocumentCollectionService = botDocumentCollectionService; this.botDocumentCollectionService = botDocumentCollectionService;
} }
/**
* 获取用户中心智能体关键字搜索字段。
*
* @return 标题和描述属性
*/
@Override
protected String[] getKeywordSearchProperties() {
return new String[]{"title", "description"};
}
@Resource @Resource
private BotPluginService botPluginService; private BotPluginService botPluginService;
@Resource @Resource
@@ -246,6 +256,10 @@ public class UcBotController extends BaseCurdController<BotService, Bot> {
@Override @Override
public Result<List<Bot>> list(Bot entity, Boolean asTree, String sortKey, String sortType) { public Result<List<Bot>> list(Bot entity, Boolean asTree, String sortKey, String sortType) {
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity)); 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); applyCategoryPermission(queryWrapper);
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy())); queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
return Result.ok(service.list(queryWrapper)); return Result.ok(service.list(queryWrapper));

View File

@@ -34,6 +34,8 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import static tech.easyflow.ai.entity.table.WorkflowTableDef.WORKFLOW;
/** /**
* 工作流 * 工作流
*/ */
@@ -57,6 +59,16 @@ public class UcWorkflowController extends BaseCurdController<WorkflowService, Wo
super(service); super(service);
} }
/**
* 获取用户中心工作流关键字搜索字段。
*
* @return 标题和描述属性
*/
@Override
protected String[] getKeywordSearchProperties() {
return new String[]{"title", "description"};
}
/** /**
* 节点单独运行 * 节点单独运行
*/ */
@@ -193,6 +205,10 @@ public class UcWorkflowController extends BaseCurdController<WorkflowService, Wo
@Override @Override
public Result<List<Workflow>> list(Workflow entity, Boolean asTree, String sortKey, String sortType) { public Result<List<Workflow>> list(Workflow entity, Boolean asTree, String sortKey, String sortType) {
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity)); 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); workflowVisibilityQueryHelper.applyReadableAccess(queryWrapper);
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy())); queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
return Result.ok(service.list(queryWrapper)); return Result.ok(service.list(queryWrapper));

View File

@@ -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 + "%";
}
}

View File

@@ -1,6 +1,8 @@
package tech.easyflow.common.web.controller; package tech.easyflow.common.web.controller;
import com.mybatisflex.core.paginate.Page; 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.QueryWrapper;
import com.mybatisflex.core.query.SqlOperators; import com.mybatisflex.core.query.SqlOperators;
import com.mybatisflex.core.service.IService; 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.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil; import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.tree.Tree; import tech.easyflow.common.tree.Tree;
import tech.easyflow.common.util.SearchKeywordUtil;
import tech.easyflow.common.util.SqlOperatorsUtil; import tech.easyflow.common.util.SqlOperatorsUtil;
import tech.easyflow.common.util.SqlUtil; import tech.easyflow.common.util.SqlUtil;
import tech.easyflow.common.web.exceptions.BusinessException; import tech.easyflow.common.web.exceptions.BusinessException;
@@ -30,6 +33,7 @@ import java.util.*;
public class BaseCurdController<S extends IService<M>, M> extends BaseController { public class BaseCurdController<S extends IService<M>, M> extends BaseController {
private static final String KEYWORD_PARAM = "keyword";
protected final S service; 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()) Map<String, String> propertyColumnMapping = TableInfoFactory.ofEntityClass(getEntityClass())
.getPropertyColumnMapping(); .getPropertyColumnMapping();
QueryCondition legacyOrCondition = null;
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) { for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
String paramKey = entry.getKey(); String paramKey = entry.getKey();
if (StringUtil.hasText(paramKey) && !paramKey.endsWith(OperatorBuilder.operatorSuffix) && propertyColumnMapping.containsKey(paramKey)) { 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); queryWrapper.eq(columnName, value);
} else { } else {
if (isQueryOrBool) { if (isQueryOrBool) {
queryWrapper.or(columnName + " like " + "'%" + value + "%' "); QueryCondition condition = buildLiteralContainsCondition(
value, new QueryColumn(columnName));
legacyOrCondition = legacyOrCondition == null
? condition
: legacyOrCondition.or(condition);
} else { } 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; 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() { protected Class<?> getEntityClass() {
Type type = getClass().getGenericSuperclass(); Type type = getClass().getGenericSuperclass();
if (type instanceof ParameterizedType) { if (type instanceof ParameterizedType) {

View File

@@ -32,6 +32,9 @@ public class Bot extends BotBase {
@Column(ignore = true) @Column(ignore = true)
private String createdByName; private String createdByName;
@Column(ignore = true)
private String keyword;
public boolean isAnonymousEnabled() { public boolean isAnonymousEnabled() {
Map<String, Object> options = getOptions(); Map<String, Object> options = getOptions();
if (options == null) { if (options == null) {
@@ -83,4 +86,22 @@ public class Bot extends BotBase {
this.createdByName = createdByName; this.createdByName = createdByName;
} }
/**
* 获取市场列表搜索关键字。
*
* @return 标题或描述关键字
*/
public String getKeyword() {
return keyword;
}
/**
* 设置市场列表搜索关键字。
*
* @param keyword 标题或描述关键字
*/
public void setKeyword(String keyword) {
this.keyword = keyword;
}
} }

View File

@@ -29,6 +29,9 @@ public class Workflow extends WorkflowBase implements VisibilityResource {
@Column(ignore = true) @Column(ignore = true)
private String createdByName; private String createdByName;
@Column(ignore = true)
private String keyword;
public Tool toFunction(boolean needEnglishName) { public Tool toFunction(boolean needEnglishName) {
return new WorkflowTool(this, needEnglishName); return new WorkflowTool(this, needEnglishName);
} }
@@ -78,4 +81,22 @@ public class Workflow extends WorkflowBase implements VisibilityResource {
public void setCreatedByName(String createdByName) { public void setCreatedByName(String createdByName) {
this.createdByName = createdByName; this.createdByName = createdByName;
} }
/**
* 获取市场列表搜索关键字。
*
* @return 标题或描述关键字
*/
public String getKeyword() {
return keyword;
}
/**
* 设置市场列表搜索关键字。
*
* @param keyword 标题或描述关键字
*/
public void setKeyword(String keyword) {
this.keyword = keyword;
}
} }

View File

@@ -27,10 +27,10 @@ public interface PluginService extends IService<Plugin> {
* @param pageNumber 页码 * @param pageNumber 页码
* @param pageSize 每页数量 * @param pageSize 每页数量
* @param category 分类 ID * @param category 分类 ID
* @param name 插件名称关键字 * @param keyword 插件名称或描述关键字
* @return 插件分页结果 * @return 插件分页结果
*/ */
Result<Page<Plugin>> pageByCategory(Long pageNumber, Long pageSize, int category, String name); Result<Page<Plugin>> pageByCategory(Long pageNumber, Long pageSize, int category, String keyword);
boolean updatePlugin(Plugin plugin); boolean updatePlugin(Plugin plugin);

View File

@@ -31,6 +31,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import tech.easyflow.ai.config.SearcherFactory; import tech.easyflow.ai.config.SearcherFactory;
import tech.easyflow.common.util.SearchKeywordUtil;
import tech.easyflow.ai.documentimport.DocumentImportDtos; import tech.easyflow.ai.documentimport.DocumentImportDtos;
import tech.easyflow.ai.documentimport.DocumentImportKeys; import tech.easyflow.ai.documentimport.DocumentImportKeys;
import tech.easyflow.ai.documentimport.DocumentImportPreviewService; import tech.easyflow.ai.documentimport.DocumentImportPreviewService;
@@ -183,7 +184,7 @@ public class DocumentServiceImpl extends ServiceImpl<DocumentMapper, Document> i
.where(DOCUMENT.COLLECTION_ID.eq(knowledgeId)) .where(DOCUMENT.COLLECTION_ID.eq(knowledgeId))
.orderBy(DOCUMENT.ID, false); .orderBy(DOCUMENT.ID, false);
if (fileName != null && !fileName.trim().isEmpty()) { if (fileName != null && !fileName.trim().isEmpty()) {
queryWrapper.and(DOCUMENT.TITLE.like(fileName)); queryWrapper.and(DOCUMENT.TITLE.likeRaw(SearchKeywordUtil.literalContainsPattern(fileName)));
} }
if (documentId != null) { if (documentId != null) {
queryWrapper.and(DOCUMENT.ID.eq(documentId)); queryWrapper.and(DOCUMENT.ID.eq(documentId));

View File

@@ -23,6 +23,7 @@ import tech.easyflow.common.domain.Result;
import tech.easyflow.common.web.exceptions.BusinessException; import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.common.entity.LoginAccount; import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil; import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.util.SearchKeywordUtil;
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot; import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
import tech.easyflow.system.service.CategoryPermissionService; import tech.easyflow.system.service.CategoryPermissionService;
@@ -138,7 +139,7 @@ public class PluginServiceImpl extends ServiceImpl<PluginMapper, Plugin> impleme
} }
@Override @Override
public Result<Page<Plugin>> pageByCategory(Long pageNumber, Long pageSize, int category, String name) { public Result<Page<Plugin>> pageByCategory(Long pageNumber, Long pageSize, int category, String keyword) {
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("PLUGIN"); RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("PLUGIN");
QueryWrapper queryWrapper = QueryWrapper.create().select(PluginCategoryMapping::getPluginId) QueryWrapper queryWrapper = QueryWrapper.create().select(PluginCategoryMapping::getPluginId)
.eq(PluginCategoryMapping::getCategoryId, category); .eq(PluginCategoryMapping::getCategoryId, category);
@@ -165,7 +166,7 @@ public class PluginServiceImpl extends ServiceImpl<PluginMapper, Plugin> impleme
} }
List<Plugin> totalPlugins = preparePluginsForCurrentUser( List<Plugin> totalPlugins = preparePluginsForCurrentUser(
queryPluginsByIds(visiblePluginIds, name), true, false); queryPluginsByIds(visiblePluginIds, keyword), true, false);
int fromIndex = Math.max(0, Math.toIntExact((pageNumber - 1) * pageSize)); int fromIndex = Math.max(0, Math.toIntExact((pageNumber - 1) * pageSize));
if (fromIndex >= totalPlugins.size()) { if (fromIndex >= totalPlugins.size()) {
return Result.ok(new Page<>(Collections.emptyList(), pageNumber, pageSize, totalPlugins.size())); return Result.ok(new Page<>(Collections.emptyList(), pageNumber, pageSize, totalPlugins.size()));
@@ -259,19 +260,20 @@ public class PluginServiceImpl extends ServiceImpl<PluginMapper, Plugin> impleme
} }
/** /**
* 按给定顺序查询插件,并按名称关键字过滤。 * 按给定顺序查询插件,并按名称或描述关键字过滤。
* *
* @param pluginIds 插件 ID 列表 * @param pluginIds 插件 ID 列表
* @param name 插件名称关键字 * @param keyword 插件名称或描述关键字
* @return 保持输入 ID 顺序的插件列表 * @return 保持输入 ID 顺序的插件列表
*/ */
private List<Plugin> queryPluginsByIds(List<BigInteger> pluginIds, String name) { private List<Plugin> queryPluginsByIds(List<BigInteger> pluginIds, String keyword) {
if (CollectionUtil.isEmpty(pluginIds)) { if (CollectionUtil.isEmpty(pluginIds)) {
return Collections.emptyList(); return Collections.emptyList();
} }
QueryWrapper queryPluginWrapper = QueryWrapper.create().select().in(Plugin::getId, pluginIds); QueryWrapper queryPluginWrapper = QueryWrapper.create().select().in(Plugin::getId, pluginIds);
if (name != null && !name.isBlank()) { if (keyword != null && !keyword.isBlank()) {
queryPluginWrapper.like(Plugin::getName, name.trim()); String pattern = SearchKeywordUtil.literalContainsPattern(keyword);
queryPluginWrapper.and(PLUGIN.NAME.likeRaw(pattern).or(PLUGIN.DESCRIPTION.likeRaw(pattern)));
} }
List<Plugin> plugins = pluginMapper.selectListWithRelationsByQuery(queryPluginWrapper); List<Plugin> plugins = pluginMapper.selectListWithRelationsByQuery(queryPluginWrapper);
Map<BigInteger, Plugin> pluginMap = plugins.stream().collect(Collectors.toMap( Map<BigInteger, Plugin> pluginMap = plugins.stream().collect(Collectors.toMap(

View File

@@ -19,6 +19,7 @@ import tech.easyflow.approval.mapper.ApprovalTaskAssigneeMapper;
import tech.easyflow.approval.mapper.ApprovalTaskMapper; import tech.easyflow.approval.mapper.ApprovalTaskMapper;
import tech.easyflow.approval.service.ApprovalAssigneeService; import tech.easyflow.approval.service.ApprovalAssigneeService;
import tech.easyflow.common.constant.enums.EnumDataStatus; import tech.easyflow.common.constant.enums.EnumDataStatus;
import tech.easyflow.common.util.SearchKeywordUtil;
import tech.easyflow.common.web.exceptions.BusinessException; import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.entity.SysAccount; import tech.easyflow.system.entity.SysAccount;
import tech.easyflow.system.entity.SysAccountRole; import tech.easyflow.system.entity.SysAccountRole;
@@ -365,7 +366,7 @@ public class ApprovalAssigneeServiceImpl implements ApprovalAssigneeService {
.eq(SysAccount::getStatus, EnumDataStatus.AVAILABLE.getCode()) .eq(SysAccount::getStatus, EnumDataStatus.AVAILABLE.getCode())
.orderBy("nickname asc, login_name asc, id asc"); .orderBy("nickname asc, login_name asc, id asc");
if (StringUtils.hasText(keyword)) { if (StringUtils.hasText(keyword)) {
String likeKeyword = "%" + keyword.trim() + "%"; String likeKeyword = SearchKeywordUtil.literalContainsPattern(keyword);
queryWrapper.and("(`login_name` like ? or `nickname` like ?)", likeKeyword, likeKeyword); queryWrapper.and("(`login_name` like ? or `nickname` like ?)", likeKeyword, likeKeyword);
} }
Page<SysAccount> page = sysAccountService.page(new Page<>(actualPageNumber, actualPageSize), queryWrapper); Page<SysAccount> page = sysAccountService.page(new Page<>(actualPageNumber, actualPageSize), queryWrapper);

View File

@@ -7,6 +7,7 @@ import com.mybatisflex.spring.service.impl.ServiceImpl;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import tech.easyflow.common.util.SearchKeywordUtil;
import tech.easyflow.common.web.exceptions.BusinessException; import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.approval.entity.ApprovalFlow; import tech.easyflow.approval.entity.ApprovalFlow;
import tech.easyflow.approval.entity.ApprovalFlowScope; import tech.easyflow.approval.entity.ApprovalFlowScope;
@@ -72,7 +73,7 @@ public class ApprovalFlowServiceImpl extends ServiceImpl<ApprovalFlowMapper, App
long actualPageSize = pageSize == null || pageSize < 1 ? 10L : pageSize; long actualPageSize = pageSize == null || pageSize < 1 ? 10L : pageSize;
QueryWrapper queryWrapper = QueryWrapper.create(); QueryWrapper queryWrapper = QueryWrapper.create();
if (StringUtils.hasText(name)) { if (StringUtils.hasText(name)) {
queryWrapper.like(ApprovalFlow::getName, name.trim()); queryWrapper.and("name LIKE ?", SearchKeywordUtil.literalContainsPattern(name));
} }
if (StringUtils.hasText(resourceType)) { if (StringUtils.hasText(resourceType)) {
queryWrapper.eq(ApprovalFlow::getResourceType, ApprovalResourceType.from(resourceType).getCode()); queryWrapper.eq(ApprovalFlow::getResourceType, ApprovalResourceType.from(resourceType).getCode());

View File

@@ -7,6 +7,7 @@ import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import tech.easyflow.common.entity.LoginAccount; import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil; 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.exceptions.BusinessException;
import tech.easyflow.approval.entity.ApprovalFlowStep; import tech.easyflow.approval.entity.ApprovalFlowStep;
import tech.easyflow.approval.entity.ApprovalInstance; import tech.easyflow.approval.entity.ApprovalInstance;
@@ -310,7 +311,7 @@ public class ApprovalQueryServiceImpl implements ApprovalQueryService {
queryWrapper.eq(ApprovalInstance::getActionType, ApprovalActionType.from(actionType).getCode()); queryWrapper.eq(ApprovalInstance::getActionType, ApprovalActionType.from(actionType).getCode());
} }
if (StringUtils.hasText(keyword)) { if (StringUtils.hasText(keyword)) {
queryWrapper.like(ApprovalInstance::getSummary, keyword.trim()); queryWrapper.and("summary LIKE ?", SearchKeywordUtil.literalContainsPattern(keyword));
} }
return queryWrapper; return queryWrapper;
} }

View File

@@ -6,6 +6,7 @@ public class ChatPageQuery implements Serializable {
private long pageNumber = 1; private long pageNumber = 1;
private long pageSize = 20; private long pageSize = 20;
private String keyword;
public long getPageNumber() { public long getPageNumber() {
return pageNumber; return pageNumber;
@@ -26,4 +27,22 @@ public class ChatPageQuery implements Serializable {
public long getOffset() { public long getOffset() {
return (pageNumber - 1) * pageSize; return (pageNumber - 1) * pageSize;
} }
/**
* 获取会话搜索关键字。
*
* @return 会话标题、最近消息或助手名称关键字
*/
public String getKeyword() {
return keyword;
}
/**
* 设置会话搜索关键字。
*
* @param keyword 会话标题、最近消息或助手名称关键字
*/
public void setKeyword(String keyword) {
this.keyword = keyword == null ? null : keyword.trim();
}
} }

View File

@@ -20,6 +20,7 @@ import tech.easyflow.common.analyticaldb.core.AnalyticalDBOperations;
import tech.easyflow.common.analyticaldb.page.AnalyticalDBPageRequest; import tech.easyflow.common.analyticaldb.page.AnalyticalDBPageRequest;
import tech.easyflow.common.analyticaldb.page.AnalyticalDBPageResult; import tech.easyflow.common.analyticaldb.page.AnalyticalDBPageResult;
import tech.easyflow.common.analyticaldb.support.AnalyticalDBHealthSupport; import tech.easyflow.common.analyticaldb.support.AnalyticalDBHealthSupport;
import tech.easyflow.common.util.SearchKeywordUtil;
import java.math.BigInteger; import java.math.BigInteger;
import java.sql.Timestamp; import java.sql.Timestamp;
@@ -746,7 +747,7 @@ public class ChatAnalyticalDBRepository {
} }
if (query.getUserAccount() != null && !query.getUserAccount().isBlank()) { if (query.getUserAccount() != null && !query.getUserAccount().isBlank()) {
sql.append(" AND user_account LIKE ?"); sql.append(" AND user_account LIKE ?");
args.add("%" + query.getUserAccount().trim() + "%"); args.add(SearchKeywordUtil.literalContainsPattern(query.getUserAccount()));
} }
if (query.getStartTime() != null) { if (query.getStartTime() != null) {
String startTime = formatDateTime(query.getStartTime()); String startTime = formatDateTime(query.getStartTime());

View File

@@ -10,6 +10,7 @@ import tech.easyflow.chatlog.domain.event.payload.ChatSessionDeletePayload;
import tech.easyflow.chatlog.domain.event.payload.ChatSessionRenamePayload; import tech.easyflow.chatlog.domain.event.payload.ChatSessionRenamePayload;
import tech.easyflow.chatlog.domain.query.ChatPageQuery; import tech.easyflow.chatlog.domain.query.ChatPageQuery;
import tech.easyflow.chatlog.support.ChatTableRouter; import tech.easyflow.chatlog.support.ChatTableRouter;
import tech.easyflow.common.util.SearchKeywordUtil;
import java.math.BigInteger; import java.math.BigInteger;
import java.sql.ResultSet; import java.sql.ResultSet;
@@ -122,6 +123,7 @@ public class MySqlChatSessionRepository {
sql.append(" AND assistant_code=?"); sql.append(" AND assistant_code=?");
params.add(assistantCode); params.add(assistantCode);
} }
appendKeywordCondition(sql, params, query);
sql.append(" ORDER BY last_message_at DESC, id DESC LIMIT ? OFFSET ?"); sql.append(" ORDER BY last_message_at DESC, id DESC LIMIT ? OFFSET ?");
params.add(query.getPageSize()); params.add(query.getPageSize());
params.add(query.getOffset()); params.add(query.getOffset());
@@ -133,6 +135,20 @@ public class MySqlChatSessionRepository {
} }
public long countSessions(BigInteger userId, BigInteger assistantId, String assistantCode) { public long countSessions(BigInteger userId, BigInteger assistantId, String assistantCode) {
return countSessions(userId, assistantId, assistantCode, null);
}
/**
* 按用户、助手和关键字统计会话数量。
*
* @param userId 用户 ID
* @param assistantId 助手 ID可为空
* @param assistantCode 助手编码,可为空
* @param query 分页与关键字条件,可为空
* @return 符合条件的会话数量
*/
public long countSessions(BigInteger userId, BigInteger assistantId,
String assistantCode, ChatPageQuery query) {
String table = tableRouter.resolveSessionTable(); String table = tableRouter.resolveSessionTable();
List<Object> params = new ArrayList<>(); List<Object> params = new ArrayList<>();
StringBuilder sql = new StringBuilder("SELECT COUNT(1) FROM `").append(table) StringBuilder sql = new StringBuilder("SELECT COUNT(1) FROM `").append(table)
@@ -146,10 +162,31 @@ public class MySqlChatSessionRepository {
sql.append(" AND assistant_code=?"); sql.append(" AND assistant_code=?");
params.add(assistantCode); params.add(assistantCode);
} }
appendKeywordCondition(sql, params, query);
Long count = jdbcTemplate.queryForObject(sql.toString(), Long.class, params.toArray()); Long count = jdbcTemplate.queryForObject(sql.toString(), Long.class, params.toArray());
return count == null ? 0L : count; return count == null ? 0L : count;
} }
/**
* 向会话 SQL 追加标题、最近消息和助手名称的普通文本包含匹配。
*
* @param sql SQL 构造器
* @param params SQL 参数
* @param query 查询条件
*/
private void appendKeywordCondition(StringBuilder sql, List<Object> 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) { public ChatSessionSummary findBySessionIdAndUserId(BigInteger sessionId, BigInteger userId) {
String table = tableRouter.resolveSessionTable(); String table = tableRouter.resolveSessionTable();
List<ChatSessionSummary> list = jdbcTemplate.query( List<ChatSessionSummary> list = jdbcTemplate.query(

View File

@@ -64,7 +64,7 @@ public class ChatSessionQueryServiceImpl implements ChatSessionQueryService {
page.setPageNumber(query.getPageNumber()); page.setPageNumber(query.getPageNumber());
page.setPageSize(query.getPageSize()); 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)); page.setRecords(listSessions(userId, assistantId, assistantCode, query));
return page; return page;
} }

View File

@@ -69,6 +69,27 @@ public class ChatSessionQueryServiceImplTest {
Assert.assertEquals("AGENT", sessionRepository.capturedCountAssistantCode); 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 热表主线查询,并保持分页参数语义。 * 工作台消息分页必须走 MySQL 热表主线查询,并保持分页参数语义。
*/ */
@@ -147,6 +168,8 @@ public class ChatSessionQueryServiceImplTest {
private int listSessionsCalls; private int listSessionsCalls;
private String capturedListAssistantCode; private String capturedListAssistantCode;
private String capturedCountAssistantCode; private String capturedCountAssistantCode;
private ChatPageQuery capturedListQuery;
private ChatPageQuery capturedCountQuery;
private ChatSessionSummary summary; private ChatSessionSummary summary;
private List<ChatSessionSummary> sessions = new ArrayList<>(); private List<ChatSessionSummary> sessions = new ArrayList<>();
@@ -163,6 +186,7 @@ public class ChatSessionQueryServiceImplTest {
public List<ChatSessionSummary> listSessions(BigInteger userId, BigInteger assistantId, String assistantCode, ChatPageQuery query) { public List<ChatSessionSummary> listSessions(BigInteger userId, BigInteger assistantId, String assistantCode, ChatPageQuery query) {
listSessionsCalls += 1; listSessionsCalls += 1;
capturedListAssistantCode = assistantCode; capturedListAssistantCode = assistantCode;
capturedListQuery = query;
return sessions; return sessions;
} }
@@ -173,8 +197,15 @@ public class ChatSessionQueryServiceImplTest {
@Override @Override
public long countSessions(BigInteger userId, BigInteger assistantId, String assistantCode) { 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; countSessionsCalls += 1;
capturedCountAssistantCode = assistantCode; capturedCountAssistantCode = assistantCode;
capturedCountQuery = query;
return count; return count;
} }

View File

@@ -19,6 +19,7 @@ import tech.easyflow.ai.service.PluginVisibilityService;
import tech.easyflow.ai.service.WorkflowService; import tech.easyflow.ai.service.WorkflowService;
import tech.easyflow.common.entity.LoginAccount; import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil; 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.exceptions.BusinessException;
import tech.easyflow.skill.entity.SkillCapabilityBinding; import tech.easyflow.skill.entity.SkillCapabilityBinding;
import tech.easyflow.skill.enums.SkillCapabilityType; import tech.easyflow.skill.enums.SkillCapabilityType;
@@ -502,7 +503,7 @@ public class SkillCapabilityTargetAccessServiceImpl implements SkillCapabilityTa
if (keyword == null || keyword.isBlank() || columns.length == 0) { if (keyword == null || keyword.isBlank() || columns.length == 0) {
return; return;
} }
String pattern = "%" + keyword.toLowerCase(Locale.ROOT) + "%"; String pattern = SearchKeywordUtil.literalContainsPattern(keyword.toLowerCase(Locale.ROOT));
StringBuilder condition = new StringBuilder("("); StringBuilder condition = new StringBuilder("(");
Object[] arguments = new Object[columns.length]; Object[] arguments = new Object[columns.length];
for (int index = 0; index < columns.length; index++) { for (int index = 0; index < columns.length; index++) {

View File

@@ -1,5 +1,6 @@
package tech.easyflow.system.entity; package tech.easyflow.system.entity;
import com.mybatisflex.annotation.Column;
import tech.easyflow.system.entity.base.SysDeptBase; import tech.easyflow.system.entity.base.SysDeptBase;
import com.mybatisflex.annotation.Table; import com.mybatisflex.annotation.Table;
@@ -12,4 +13,25 @@ import com.mybatisflex.annotation.Table;
@Table(value = "tb_sys_dept", comment = "部门表") @Table(value = "tb_sys_dept", comment = "部门表")
public class SysDept extends SysDeptBase { 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;
}
} }

View File

@@ -1,5 +1,6 @@
package tech.easyflow.system.entity; package tech.easyflow.system.entity;
import tech.easyflow.common.util.SearchKeywordUtil;
import tech.easyflow.common.util.SpringContextUtil; import tech.easyflow.common.util.SpringContextUtil;
import tech.easyflow.common.dict.Dict; import tech.easyflow.common.dict.Dict;
import tech.easyflow.common.dict.DictItem; import tech.easyflow.common.dict.DictItem;
@@ -81,7 +82,7 @@ public class SysDict extends SysDictBase {
QueryWrapper qw = QueryWrapper.create() QueryWrapper qw = QueryWrapper.create()
.eq(SysDictItem::getDictId, this.dictId) .eq(SysDictItem::getDictId, this.dictId)
.eq(SysDictItem::getStatus, 0) .eq(SysDictItem::getStatus, 0)
.like(SysDictItem::getText, keyword); .and("`text` LIKE ?", SearchKeywordUtil.literalContainsPattern(keyword));
List<SysDictItem> sysDictItems = itemService.list(qw); List<SysDictItem> sysDictItems = itemService.list(qw);
Dict dict = new Dict(); Dict dict = new Dict();

View File

@@ -38,10 +38,6 @@ const props = defineProps({
title: { type: String, default: '' }, title: { type: String, default: '' },
width: { type: String, default: '80%' }, width: { type: String, default: '80%' },
extraQueryParams: { type: Object, default: () => ({}) }, extraQueryParams: { type: Object, default: () => ({}) },
searchParams: {
type: Array as PropType<string[]>,
default: () => [],
},
titleKey: { type: String, default: 'name' }, titleKey: { type: String, default: 'name' },
pageUrl: { type: String, default: '' }, pageUrl: { type: String, default: '' },
hasParent: { type: Boolean, default: false }, hasParent: { type: Boolean, default: false },
@@ -183,14 +179,8 @@ const handleSubmitRun = () => {
}; };
const handleSearch = (query: string) => { const handleSearch = (query: string) => {
const tempParams = {} as Record<string, string>;
props.searchParams.forEach((paramName) => {
tempParams[paramName] = query;
});
pageDataRef.value?.setQuery({ pageDataRef.value?.setQuery({
isQueryOr: true, keyword: query.trim() || undefined,
...tempParams,
}); });
}; };
</script> </script>

View File

@@ -76,9 +76,15 @@ const emitButtonEvent = (payload) => {
}; };
const handleSearch = () => { const handleSearch = () => {
searchValue.value = searchValue.value.trim();
emit('search', searchValue.value); emit('search', searchValue.value);
}; };
const handleClear = () => {
searchValue.value = '';
emit('search', '');
};
const handleReset = () => { const handleReset = () => {
searchValue.value = ''; searchValue.value = '';
emit('reset'); emit('reset');
@@ -117,6 +123,7 @@ const handleDropdownClick = (button) => {
:placeholder="searchPlaceholder" :placeholder="searchPlaceholder"
class="search-input" class="search-input"
clearable clearable
@clear="handleClear"
@keyup.enter="handleSearch" @keyup.enter="handleSearch"
> >
<template #prefix> <template #prefix>

View File

@@ -33,7 +33,7 @@
"tools": "tools" "tools": "tools"
}, },
"toolsManagement": "Tools Management", "toolsManagement": "Tools Management",
"searchUsers": "Search Users", "searchUsers": "Search plugin name or description",
"parameterValue": "ParameterValue", "parameterValue": "ParameterValue",
"workflow": "Workflow", "workflow": "Workflow",
"workflowPluginHint": "Workflow plugins mirror the published snapshot of the target workflow. Availability is evaluated in real time against workflow permissions and approval status.", "workflowPluginHint": "Workflow plugins mirror the published snapshot of the target workflow. Availability is evaluated in real time against workflow permissions and approval status.",

View File

@@ -6,6 +6,7 @@
"ancestors": "Ancestors", "ancestors": "Ancestors",
"deptName": "DeptName", "deptName": "DeptName",
"deptCode": "DeptCode", "deptCode": "DeptCode",
"searchPlaceholder": "Search department name or code",
"sortNo": "SortNo", "sortNo": "SortNo",
"status": "Status", "status": "Status",
"created": "Created", "created": "Created",

View File

@@ -11,7 +11,7 @@
"actionBody": "ActionBody", "actionBody": "ActionBody",
"status": "Status", "status": "Status",
"created": "Created", "created": "Created",
"searchPlaceholder": "Search action name", "searchPlaceholder": "Search action, user, or IP",
"startDate": "Start date", "startDate": "Start date",
"endDate": "End date" "endDate": "End date"
} }

View File

@@ -33,7 +33,7 @@
"tools": "工具" "tools": "工具"
}, },
"toolsManagement": "工具管理", "toolsManagement": "工具管理",
"searchUsers": "搜索用户", "searchUsers": "搜索插件名称或描述",
"parameterValue": "参数值", "parameterValue": "参数值",
"workflow": "工作流", "workflow": "工作流",
"workflowPluginHint": "工作流插件会自动镜像目标工作流的已发布快照,插件可用性会实时跟随工作流权限和审批状态变化。", "workflowPluginHint": "工作流插件会自动镜像目标工作流的已发布快照,插件可用性会实时跟随工作流权限和审批状态变化。",

View File

@@ -6,6 +6,7 @@
"ancestors": "父级部门ID集合", "ancestors": "父级部门ID集合",
"deptName": "部门名称", "deptName": "部门名称",
"deptCode": "部门编码", "deptCode": "部门编码",
"searchPlaceholder": "搜索部门名称或编码",
"sortNo": "排序", "sortNo": "排序",
"status": "数据状态", "status": "数据状态",
"created": "创建时间", "created": "创建时间",

View File

@@ -11,7 +11,7 @@
"actionBody": "操作请求body", "actionBody": "操作请求body",
"status": "操作状态 1 成功 9 失败", "status": "操作状态 1 成功 9 失败",
"created": "操作时间", "created": "操作时间",
"searchPlaceholder": "搜索操作名称", "searchPlaceholder": "搜索操作、用户或 IP",
"startDate": "开始日期", "startDate": "开始日期",
"endDate": "结束日期" "endDate": "结束日期"
} }

View File

@@ -79,9 +79,7 @@ watchListRouteState(route, agentListRouteSchema, (state) => {
pageSize: state.pageSize, pageSize: state.pageSize,
queryParams: { queryParams: {
categoryId: state.categoryId || undefined, categoryId: state.categoryId || undefined,
description: state.keyword || undefined, keyword: state.keyword || undefined,
isQueryOr: true,
name: state.keyword || undefined,
}, },
}); });
}); });
@@ -187,9 +185,7 @@ function handleSearch(keyword: string) {
searchKeyword.value = keyword; searchKeyword.value = keyword;
pageDataRef.value?.setQuery({ pageDataRef.value?.setQuery({
categoryId: selectedCategoryId.value || undefined, categoryId: selectedCategoryId.value || undefined,
isQueryOr: true, keyword: keyword || undefined,
name: keyword || undefined,
description: keyword || undefined,
}); });
} }
@@ -268,9 +264,7 @@ function changeCategory(category: any) {
selectedCategoryId.value = categoryId; selectedCategoryId.value = categoryId;
pageDataRef.value?.setQuery({ pageDataRef.value?.setQuery({
categoryId: categoryId || undefined, categoryId: categoryId || undefined,
isQueryOr: true, keyword: searchKeyword.value || undefined,
name: searchKeyword.value || undefined,
description: searchKeyword.value || undefined,
}); });
} }
@@ -290,9 +284,7 @@ async function loadCategories() {
selectedCategoryId.value = ''; selectedCategoryId.value = '';
initialCategoryChangePending = false; initialCategoryChangePending = false;
pageDataRef.value?.setQuery?.({ pageDataRef.value?.setQuery?.({
isQueryOr: true, keyword: searchKeyword.value || undefined,
name: searchKeyword.value || undefined,
description: searchKeyword.value || undefined,
}); });
} }
} }
@@ -452,9 +444,7 @@ async function handleDeleteAction(row: AgentInfo) {
:initial-page-number="initialListState.pageNumber" :initial-page-number="initialListState.pageNumber"
:initial-query-params="{ :initial-query-params="{
categoryId: initialListState.categoryId || undefined, categoryId: initialListState.categoryId || undefined,
isQueryOr: true, keyword: initialListState.keyword || undefined,
name: initialListState.keyword || undefined,
description: initialListState.keyword || undefined,
}" }"
@state-change="handlePageStateChange" @state-change="handlePageStateChange"
> >

View File

@@ -100,8 +100,7 @@ watchListRouteState(route, botListRouteSchema, (state) => {
pageSize: state.pageSize, pageSize: state.pageSize,
queryParams: { queryParams: {
categoryId: state.categoryId || undefined, categoryId: state.categoryId || undefined,
isQueryOr: true, keyword: state.keyword || undefined,
title: state.keyword || undefined,
}, },
}); });
}); });
@@ -310,8 +309,7 @@ const handleSearch = (params: string) => {
searchKeyword.value = params; searchKeyword.value = params;
pageDataRef.value.setQuery({ pageDataRef.value.setQuery({
categoryId: selectedCategoryId.value || undefined, categoryId: selectedCategoryId.value || undefined,
title: params || undefined, keyword: params || undefined,
isQueryOr: true,
}); });
}; };
const handleButtonClick = () => { const handleButtonClick = () => {
@@ -395,8 +393,7 @@ function changeCategory(category: any) {
selectedCategoryId.value = categoryId; selectedCategoryId.value = categoryId;
pageDataRef.value.setQuery({ pageDataRef.value.setQuery({
categoryId: categoryId || undefined, categoryId: categoryId || undefined,
title: searchKeyword.value || undefined, keyword: searchKeyword.value || undefined,
isQueryOr: true,
}); });
} }
function showControlDialog(item: any) { function showControlDialog(item: any) {
@@ -471,8 +468,7 @@ const getSideList = async () => {
selectedCategoryId.value = ''; selectedCategoryId.value = '';
initialCategoryChangePending = false; initialCategoryChangePending = false;
pageDataRef.value?.setQuery?.({ pageDataRef.value?.setQuery?.({
title: searchKeyword.value || undefined, keyword: searchKeyword.value || undefined,
isQueryOr: true,
}); });
} }
} }
@@ -538,8 +534,7 @@ function handlePageStateChange(state: {
:initial-page-number="initialListState.pageNumber" :initial-page-number="initialListState.pageNumber"
:initial-query-params="{ :initial-query-params="{
categoryId: initialListState.categoryId || undefined, categoryId: initialListState.categoryId || undefined,
title: initialListState.keyword || undefined, keyword: initialListState.keyword || undefined,
isQueryOr: true,
}" }"
@state-change="handlePageStateChange" @state-change="handlePageStateChange"
> >

View File

@@ -101,6 +101,7 @@ const loadingAssistants = ref(false);
const loadingKnowledgeOptions = ref(false); const loadingKnowledgeOptions = ref(false);
const sending = ref(false); const sending = ref(false);
const sessionSearchKeyword = ref(''); const sessionSearchKeyword = ref('');
const appliedSessionSearchKeyword = ref('');
const currentAssistantId = ref<string>(); const currentAssistantId = ref<string>();
const currentAssistantDetail = ref<any>(); const currentAssistantDetail = ref<any>();
const currentSession = ref<any>(); const currentSession = ref<any>();
@@ -122,6 +123,7 @@ const sessionContextMenuVisible = ref(false);
const sessionContextMenuTarget = ref<null | SessionItem>(null); const sessionContextMenuTarget = ref<null | SessionItem>(null);
const sessionContextMenuX = ref(0); const sessionContextMenuX = ref(0);
const sessionContextMenuY = ref(0); const sessionContextMenuY = ref(0);
let latestSessionRequestId = 0;
const variantSwitchController = createChatVariantSwitchController< const variantSwitchController = createChatVariantSwitchController<
ChatTimeHistoryRecord, ChatTimeHistoryRecord,
@@ -242,17 +244,7 @@ const extraKnowledgeOptionList = computed(() => {
return knowledgeOptions.value.filter((item) => !excluded.has(String(item.value))); return knowledgeOptions.value.filter((item) => !excluded.has(String(item.value)));
}); });
const filteredSessions = computed(() => { const filteredSessions = computed(() => {
const keyword = sessionSearchKeyword.value.trim().toLowerCase(); return sessions.value;
if (!keyword) {
return sessions.value;
}
return sessions.value.filter((session) => {
return [
resolveSessionLabel(session),
session.lastMessagePreview,
session.assistantName,
].some((value) => String(value || '').toLowerCase().includes(keyword));
});
}); });
const groupedSessions = computed(() => { const groupedSessions = computed(() => {
const today: SessionItem[] = []; const today: SessionItem[] = [];
@@ -432,14 +424,21 @@ async function fetchSessions(options: { append?: boolean } = {}) {
loadingMoreSessions.value = true; loadingMoreSessions.value = true;
} else { } else {
loadingSessions.value = true; loadingSessions.value = true;
loadingMoreSessions.value = false;
} }
const requestId = ++latestSessionRequestId;
const nextPageNumber = append ? sessionPage.value.pageNumber + 1 : 1; const nextPageNumber = append ? sessionPage.value.pageNumber + 1 : 1;
const requestKeyword = appliedSessionSearchKeyword.value;
const [, res] = await tryit(api.get)('/api/v1/chatWorkspace/sessions', { const [, res] = await tryit(api.get)('/api/v1/chatWorkspace/sessions', {
params: { params: {
keyword: requestKeyword || undefined,
pageNumber: nextPageNumber, pageNumber: nextPageNumber,
pageSize: sessionPage.value.pageSize, pageSize: sessionPage.value.pageSize,
}, },
}); });
if (requestId !== latestSessionRequestId) {
return;
}
if (append) { if (append) {
loadingMoreSessions.value = false; loadingMoreSessions.value = false;
} else { } else {
@@ -458,6 +457,13 @@ async function fetchSessions(options: { append?: boolean } = {}) {
} }
} }
function handleSessionSearch() {
const keyword = sessionSearchKeyword.value.trim();
sessionSearchKeyword.value = keyword;
appliedSessionSearchKeyword.value = keyword;
void fetchSessions();
}
function mergeFreshSessionRecords(records: SessionItem[]) { function mergeFreshSessionRecords(records: SessionItem[]) {
const freshIds = new Set(records.map((item) => String(item.sessionId))); const freshIds = new Set(records.map((item) => String(item.sessionId)));
const optimisticIds = new Set(optimisticSessionIds.value); const optimisticIds = new Set(optimisticSessionIds.value);
@@ -1581,6 +1587,8 @@ async function deleteSession(targetSession?: SessionItem) {
clearable clearable
:prefix-icon="Search" :prefix-icon="Search"
placeholder="搜索会话" placeholder="搜索会话"
@clear="handleSessionSearch"
@keyup.enter="handleSessionSearch"
/> />
</div> </div>
@@ -1661,7 +1669,7 @@ async function deleteSession(targetSession?: SessionItem) {
class="chat-workspace__empty" class="chat-workspace__empty"
/> />
<ElEmpty <ElEmpty
v-else-if="sessionSearchKeyword" v-else-if="appliedSessionSearchKeyword"
description="未找到匹配会话" description="未找到匹配会话"
class="chat-workspace__empty" class="chat-workspace__empty"
/> />

View File

@@ -405,9 +405,11 @@ function closeDetail() {
<ElInput <ElInput
v-if="isSuperAdmin" v-if="isSuperAdmin"
v-model="query.userAccount" v-model="query.userAccount"
clearable
class="chat-history-page__filter-control is-input" class="chat-history-page__filter-control is-input"
placeholder="搜索聊天用户" placeholder="搜索用户账号"
:prefix-icon="Search" :prefix-icon="Search"
@clear="handleSearch"
@keyup.enter="handleSearch" @keyup.enter="handleSearch"
/> />
</div> </div>

View File

@@ -91,8 +91,7 @@ watchListRouteState(route, documentCollectionListRouteSchema, (state) => {
pageSize: state.pageSize, pageSize: state.pageSize,
queryParams: { queryParams: {
categoryId: state.categoryId || undefined, categoryId: state.categoryId || undefined,
isQueryOr: true, keyword: state.keyword || undefined,
title: state.keyword || undefined,
}, },
}); });
}); });
@@ -590,8 +589,7 @@ const handleSearch = (params: any) => {
searchKeyword.value = String(params || ''); searchKeyword.value = String(params || '');
pageDataRef.value.setQuery({ pageDataRef.value.setQuery({
categoryId: selectedCategoryId.value || undefined, categoryId: selectedCategoryId.value || undefined,
title: searchKeyword.value || undefined, keyword: searchKeyword.value || undefined,
isQueryOr: true,
}); });
}; };
const reloadKnowledgeList = () => { const reloadKnowledgeList = () => {
@@ -631,8 +629,7 @@ const getCategoryList = async () => {
selectedCategoryId.value = ''; selectedCategoryId.value = '';
initialCategoryChangePending = false; initialCategoryChangePending = false;
pageDataRef.value?.setQuery?.({ pageDataRef.value?.setQuery?.({
title: searchKeyword.value || undefined, keyword: searchKeyword.value || undefined,
isQueryOr: true,
}); });
} }
} }
@@ -763,8 +760,7 @@ function changeCategory(category: any) {
selectedCategoryId.value = categoryId; selectedCategoryId.value = categoryId;
pageDataRef.value.setQuery({ pageDataRef.value.setQuery({
categoryId: categoryId || undefined, categoryId: categoryId || undefined,
title: searchKeyword.value || undefined, keyword: searchKeyword.value || undefined,
isQueryOr: true,
}); });
} }
function handlePageStateChange(state: { function handlePageStateChange(state: {
@@ -822,8 +818,7 @@ function handlePageStateChange(state: {
:initial-page-number="initialListState.pageNumber" :initial-page-number="initialListState.pageNumber"
:initial-query-params="{ :initial-query-params="{
categoryId: initialListState.categoryId || undefined, categoryId: initialListState.categoryId || undefined,
title: initialListState.keyword || undefined, keyword: initialListState.keyword || undefined,
isQueryOr: true,
}" }"
@state-change="handlePageStateChange" @state-change="handlePageStateChange"
> >

View File

@@ -100,7 +100,7 @@ defineExpose({
}, },
search(searchText: string) { search(searchText: string) {
pageDataRef.value?.setQuery?.({ pageDataRef.value?.setQuery?.({
title: searchText, keyword: searchText.trim() || undefined,
}); });
}, },
}); });

View File

@@ -91,7 +91,7 @@ const treeData = computed(() => [
const refreshList = () => { const refreshList = () => {
const query: Record<string, any> = {}; const query: Record<string, any> = {};
if (searchKeyword.value.trim()) { if (searchKeyword.value.trim()) {
query.question = searchKeyword.value.trim(); query.keyword = searchKeyword.value.trim();
} }
if (selectedCategoryId.value !== 'all') { if (selectedCategoryId.value !== 'all') {
query.categoryId = selectedCategoryId.value; query.categoryId = selectedCategoryId.value;

View File

@@ -117,7 +117,7 @@ const headerButtons = [
}, },
]; ];
const handleSearch = (params: string) => { const handleSearch = (params: string) => {
pageDataRef.value.setQuery({ packageName: params, isQueryOr: true }); pageDataRef.value.setQuery({ keyword: params || undefined });
}; };
const handleHeaderButtonClick = (button: any) => { const handleHeaderButtonClick = (button: any) => {
if (button.key === 'create') { if (button.key === 'create') {

View File

@@ -6,26 +6,23 @@ import {
} from './plugin-query'; } from './plugin-query';
describe('plugin query params', () => { describe('plugin query params', () => {
it('uses the persisted name field and an explicit like operator', () => { it('uses the unified keyword for plugin name and description search', () => {
expect(buildPluginPageQueryParams('7', ' 1213 ')).toEqual({ expect(buildPluginPageQueryParams('7', ' 1213 ')).toEqual({
category: '7', category: '7',
name: '1213', keyword: '1213',
name__op: 'like',
}); });
}); });
it('retains category while clearing the keyword', () => { it('retains category while clearing the keyword', () => {
expect(buildPluginPageQueryParams('7', ' ')).toEqual({ expect(buildPluginPageQueryParams('7', ' ')).toEqual({
category: '7', category: '7',
name: undefined, keyword: undefined,
name__op: undefined,
}); });
}); });
it('uses contains matching for plugin tool names', () => { it('uses the unified keyword for plugin tool name and description search', () => {
expect(buildPluginToolPageQueryParams(' HTTP ')).toEqual({ expect(buildPluginToolPageQueryParams(' HTTP ')).toEqual({
name: 'HTTP', keyword: 'HTTP',
name__op: 'like',
}); });
}); });
}); });

View File

@@ -3,19 +3,17 @@ function normalizeKeyword(keyword?: string): string {
} }
function buildPluginPageQueryParams(categoryId: string, keyword?: string) { function buildPluginPageQueryParams(categoryId: string, keyword?: string) {
const name = normalizeKeyword(keyword); const normalizedKeyword = normalizeKeyword(keyword);
return { return {
category: categoryId || '0', category: categoryId || '0',
name: name || undefined, keyword: normalizedKeyword || undefined,
name__op: name ? 'like' : undefined,
}; };
} }
function buildPluginToolPageQueryParams(keyword?: string) { function buildPluginToolPageQueryParams(keyword?: string) {
const name = normalizeKeyword(keyword); const normalizedKeyword = normalizeKeyword(keyword);
return { return {
name: name || undefined, keyword: normalizedKeyword || undefined,
name__op: name ? 'like' : undefined,
}; };
} }

View File

@@ -56,6 +56,7 @@ const formRef = ref<FormInstance>();
const pageDataRef = ref(); const pageDataRef = ref();
const saveDialog = ref(); const saveDialog = ref();
const previewDialog = ref(); const previewDialog = ref();
const selectedCategoryId = ref('');
const formInline = ref({ const formInline = ref({
resourceName: '', resourceName: '',
resourceType: '', resourceType: '',
@@ -68,13 +69,19 @@ function initDict() {
function search(formEl: FormInstance | undefined) { function search(formEl: FormInstance | undefined) {
formEl?.validate((valid) => { formEl?.validate((valid) => {
if (valid) { if (valid) {
pageDataRef.value.setQuery(formInline.value); pageDataRef.value.setQuery({
categoryId: selectedCategoryId.value || undefined,
keyword: formInline.value.resourceName.trim() || undefined,
resourceType: formInline.value.resourceType || undefined,
});
} }
}); });
} }
function reset(formEl: FormInstance | undefined) { function reset(formEl: FormInstance | undefined) {
formEl?.resetFields(); formEl?.resetFields();
pageDataRef.value.setQuery({}); pageDataRef.value.setQuery({
categoryId: selectedCategoryId.value || undefined,
});
} }
function showDialog(row: any) { function showDialog(row: any) {
saveDialog.value.openDialog({ ...row }); saveDialog.value.openDialog({ ...row });
@@ -177,7 +184,12 @@ const sideFormRules = computed(() => {
const sideSaveLoading = ref(false); const sideSaveLoading = ref(false);
function changeCategory(category: any) { function changeCategory(category: any) {
pageDataRef.value.setQuery({ categoryId: category.id }); selectedCategoryId.value = String(category.id || '');
pageDataRef.value.setQuery({
categoryId: selectedCategoryId.value || undefined,
keyword: formInline.value.resourceName.trim() || undefined,
resourceType: formInline.value.resourceType || undefined,
});
} }
function showControlDialog(item: any) { function showControlDialog(item: any) {
sideFormRef.value?.resetFields(); sideFormRef.value?.resetFields();

View File

@@ -310,7 +310,7 @@ const currentPageSize = ref(initialListState.pageSize);
const suppressInitialCategoryChange = ref(Boolean(initialListState.categoryId)); const suppressInitialCategoryChange = ref(Boolean(initialListState.categoryId));
const initialQueryParams = { const initialQueryParams = {
categoryId: initialListState.categoryId || undefined, categoryId: initialListState.categoryId || undefined,
title: initialListState.keyword || undefined, keyword: initialListState.keyword || undefined,
}; };
watchListRouteState(route, workflowListRouteSchema, (state) => { watchListRouteState(route, workflowListRouteSchema, (state) => {
if (String(state.categoryId) !== String(selectedCategoryId.value)) { if (String(state.categoryId) !== String(selectedCategoryId.value)) {
@@ -325,7 +325,7 @@ watchListRouteState(route, workflowListRouteSchema, (state) => {
pageSize: state.pageSize, pageSize: state.pageSize,
queryParams: { queryParams: {
categoryId: state.categoryId || undefined, categoryId: state.categoryId || undefined,
title: state.keyword || undefined, keyword: state.keyword || undefined,
}, },
}); });
}); });
@@ -401,7 +401,7 @@ async function updateVisibilityScope(
function applyFilters() { function applyFilters() {
pageDataRef.value?.setQuery({ pageDataRef.value?.setQuery({
categoryId: selectedCategoryId.value || undefined, categoryId: selectedCategoryId.value || undefined,
title: searchKeyword.value || undefined, keyword: searchKeyword.value || undefined,
}); });
} }
function currentListState(): WorkflowListRouteState { function currentListState(): WorkflowListRouteState {

View File

@@ -66,7 +66,7 @@ watchListRouteState($route, workflowExecListRouteSchema, (state) => {
const restored = pageDataRef.value?.restoreState?.({ const restored = pageDataRef.value?.restoreState?.({
pageNumber: state.pageNumber, pageNumber: state.pageNumber,
pageSize: state.pageSize, pageSize: state.pageSize,
queryParams: { execKey: state.execKey || undefined }, queryParams: { keyword: state.execKey || undefined },
}); });
if (workflowChanged && !restored) pageDataRef.value?.reload?.(); if (workflowChanged && !restored) pageDataRef.value?.reload?.();
}); });
@@ -79,7 +79,7 @@ function search(formEl: FormInstance | undefined) {
if (valid) { if (valid) {
appliedExecKey.value = formInline.value.execKey; appliedExecKey.value = formInline.value.execKey;
pageDataRef.value.setQuery({ pageDataRef.value.setQuery({
execKey: appliedExecKey.value || undefined, keyword: appliedExecKey.value || undefined,
}); });
} }
}); });
@@ -222,7 +222,7 @@ function getTagType(row: any) {
:page-size="initialListState.pageSize" :page-size="initialListState.pageSize"
:initial-page-number="initialListState.pageNumber" :initial-page-number="initialListState.pageNumber"
:initial-query-params="{ :initial-query-params="{
execKey: initialListState.execKey || undefined, keyword: initialListState.execKey || undefined,
}" }"
:extra-query-params="{ :extra-query-params="{
workflowId: $route.query.workflowId, workflowId: $route.query.workflowId,

View File

@@ -37,7 +37,9 @@ function initDict() {
function search(formEl: FormInstance | undefined) { function search(formEl: FormInstance | undefined) {
formEl?.validate((valid) => { formEl?.validate((valid) => {
if (valid) { if (valid) {
pageDataRef.value.setQuery(formInline.value); pageDataRef.value.setQuery({
keyword: formInline.value.nodeName.trim() || undefined,
});
} }
}); });
} }

View File

@@ -42,7 +42,7 @@ const headerButtons = [
]; ];
const handleSearch = (params: string) => { const handleSearch = (params: string) => {
pageDataRef.value.setQuery({ apiKey: params, isQueryOr: true }); pageDataRef.value.setQuery({ keyword: params || undefined });
}; };
const headerButtonClick = (action: any) => { const headerButtonClick = (action: any) => {
if (action.key === 'addApiKey') { if (action.key === 'addApiKey') {

View File

@@ -35,7 +35,9 @@ const formInline = ref({
function search(formEl: FormInstance | undefined) { function search(formEl: FormInstance | undefined) {
formEl?.validate((valid) => { formEl?.validate((valid) => {
if (valid) { if (valid) {
pageDataRef.value.setQuery(formInline.value); pageDataRef.value.setQuery({
keyword: formInline.value.title.trim() || undefined,
});
} }
}); });
} }

View File

@@ -0,0 +1,164 @@
/* eslint-disable vue/one-component-per-file -- Inline stubs keep this interaction test isolated. */
import { flushPromises, mount } from '@vue/test-utils';
import { defineComponent, h } from 'vue';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import SysDeptList from './SysDeptList.vue';
const apiMocks = vi.hoisted(() => ({
get: vi.fn(),
post: vi.fn(),
}));
const tableMocks = vi.hoisted(() => ({
clearSelection: vi.fn(),
toggleRowExpansion: vi.fn(),
}));
const dictStoreMocks = vi.hoisted(() => ({
fetchDictionary: vi.fn(),
getDictLabel: vi.fn(),
}));
vi.mock('#/api/request', () => ({ api: apiMocks }));
vi.mock('#/locales', () => ({ $t: (key: string) => key }));
vi.mock('#/store', () => ({ useDictStore: () => dictStoreMocks }));
vi.mock('./SysDeptModal.vue', () => ({
default: defineComponent({
name: 'SysDeptModal',
setup: () => () => h('div'),
}),
}));
vi.mock('#/components/dict/DictSelect.vue', () => ({
default: defineComponent({
name: 'DictSelect',
setup: () => () => h('div'),
}),
}));
vi.mock('element-plus', () => ({
ElButton: defineComponent({
name: 'ElButton',
setup(_, { attrs, slots }) {
return () => h('button', attrs, slots.default?.());
},
}),
ElForm: defineComponent({
name: 'ElForm',
setup(_, { expose, slots }) {
expose({
resetFields: vi.fn(),
validate: (callback: (valid: boolean) => void) => callback(true),
});
return () => h('form', slots.default?.());
},
}),
ElFormItem: defineComponent({
name: 'ElFormItem',
setup(_, { slots }) {
return () => h('div', slots.default?.());
},
}),
ElIcon: defineComponent({
name: 'ElIcon',
setup(_, { slots }) {
return () => h('span', slots.default?.());
},
}),
ElInput: defineComponent({
name: 'ElInput',
inheritAttrs: false,
props: { modelValue: { default: '', type: String } },
emits: ['update:modelValue'],
setup(props, { attrs, emit }) {
return () =>
h('input', {
...attrs,
value: props.modelValue,
onInput: (event: Event) =>
emit('update:modelValue', (event.target as HTMLInputElement).value),
});
},
}),
ElMessage: {
error: vi.fn(),
success: vi.fn(),
warning: vi.fn(),
},
ElMessageBox: { confirm: vi.fn() },
ElTable: defineComponent({
name: 'ElTable',
setup(_, { expose, slots }) {
expose(tableMocks);
return () => h('div', slots.default?.());
},
}),
ElTableColumn: defineComponent({
name: 'ElTableColumn',
setup: () => () => h('div'),
}),
}));
describe('sysDeptList search expansion', () => {
beforeEach(() => {
vi.clearAllMocks();
apiMocks.get.mockResolvedValue({ data: [] });
});
it('expands every matched department path after keyword search', async () => {
const matchedTree = [
{
children: [
{
children: [{ id: 'matched', deptName: '研发一部' }],
id: 'branch',
deptName: '技术部',
},
],
id: 'root',
deptName: '总行',
},
];
const rootDepartment = matchedTree[0];
const branchDepartment = rootDepartment?.children[0];
if (!rootDepartment || !branchDepartment) {
throw new Error('部门搜索测试数据缺少祖先节点');
}
apiMocks.get
.mockResolvedValueOnce({ data: [] })
.mockResolvedValueOnce({ data: matchedTree });
const wrapper = mount(SysDeptList, {
global: {
directives: {
access: {},
loading: {},
},
},
});
await flushPromises();
tableMocks.toggleRowExpansion.mockClear();
const input = wrapper.get('input');
await input.setValue(' 研发 ');
await input.trigger('keyup', { key: 'Enter' });
await flushPromises();
expect(apiMocks.get).toHaveBeenLastCalledWith('/api/v1/sysDept/list', {
params: {
asTree: true,
keyword: '研发',
status: undefined,
},
});
expect(tableMocks.toggleRowExpansion).toHaveBeenCalledTimes(2);
expect(tableMocks.toggleRowExpansion).toHaveBeenNthCalledWith(
1,
rootDepartment,
true,
);
expect(tableMocks.toggleRowExpansion).toHaveBeenNthCalledWith(
2,
branchDepartment,
true,
);
wrapper.unmount();
});
});

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { FormInstance } from 'element-plus'; import type { FormInstance } from 'element-plus';
import { computed, onMounted, ref } from 'vue'; import { computed, nextTick, onMounted, ref } from 'vue';
import { Plus } from '@element-plus/icons-vue'; import { Plus } from '@element-plus/icons-vue';
import { import {
@@ -23,6 +23,12 @@ import { useDictStore } from '#/store';
import SysDeptModal from './SysDeptModal.vue'; import SysDeptModal from './SysDeptModal.vue';
interface DepartmentTreeNode {
children?: DepartmentTreeNode[];
id: number | string;
[key: string]: any;
}
onMounted(() => { onMounted(() => {
void getTree(); void getTree();
initDict(); initDict();
@@ -30,7 +36,7 @@ onMounted(() => {
const formRef = ref<FormInstance>(); const formRef = ref<FormInstance>();
const tableRef = ref<InstanceType<typeof ElTable>>(); const tableRef = ref<InstanceType<typeof ElTable>>();
const treeData = ref([]); const treeData = ref<DepartmentTreeNode[]>([]);
const selectedRows = ref<any[]>([]); const selectedRows = ref<any[]>([]);
const loading = ref(false); const loading = ref(false);
const batchActionType = ref<'' | 'delete' | 'disable' | 'enable'>(''); const batchActionType = ref<'' | 'delete' | 'disable' | 'enable'>('');
@@ -83,6 +89,16 @@ function clearSelection() {
tableRef.value?.clearSelection(); tableRef.value?.clearSelection();
} }
function setTreeExpanded(rows: DepartmentTreeNode[], expanded: boolean) {
for (const row of rows) {
if (!row.children?.length) {
continue;
}
tableRef.value?.toggleRowExpansion(row, expanded);
setTreeExpanded(row.children, expanded);
}
}
function getSelectedIds() { function getSelectedIds() {
return selectedRows.value return selectedRows.value
.map((row) => row?.id) .map((row) => row?.id)
@@ -235,14 +251,18 @@ function removeSelected() {
async function getTree() { async function getTree() {
loading.value = true; loading.value = true;
try { try {
const keyword = formInline.value.deptName.trim();
const res = await api.get('/api/v1/sysDept/list', { const res = await api.get('/api/v1/sysDept/list', {
params: { params: {
asTree: true, asTree: true,
...formInline.value, keyword: keyword || undefined,
status: formInline.value.status || undefined,
}, },
}); });
treeData.value = res.data || []; treeData.value = res.data || [];
clearSelection(); clearSelection();
await nextTick();
setTreeExpanded(treeData.value, keyword.length > 0);
} finally { } finally {
loading.value = false; loading.value = false;
} }
@@ -258,7 +278,10 @@ async function getTree() {
<ElInput <ElInput
class="search-input" class="search-input"
v-model="formInline.deptName" v-model="formInline.deptName"
:placeholder="$t('sysDept.deptName')" clearable
:placeholder="$t('sysDept.searchPlaceholder')"
@clear="search(formRef)"
@keyup.enter="search(formRef)"
/> />
</ElFormItem> </ElFormItem>
<ElFormItem prop="status" class="!mr-3"> <ElFormItem prop="status" class="!mr-3">

View File

@@ -108,7 +108,7 @@ watchListRouteState(route, sysFeedbackListRouteSchema, (state) => {
pageNumber: state.pageNumber, pageNumber: state.pageNumber,
pageSize: state.pageSize, pageSize: state.pageSize,
queryParams: { queryParams: {
feedbackContent: state.feedbackContent || undefined, keyword: state.feedbackContent || undefined,
feedbackType: state.feedbackType || undefined, feedbackType: state.feedbackType || undefined,
status: state.status || undefined, status: state.status || undefined,
}, },
@@ -119,7 +119,11 @@ function search(formEl?: FormInstance) {
formEl?.validate((valid) => { formEl?.validate((valid) => {
if (valid) { if (valid) {
appliedFormData.value = { ...formData.value }; appliedFormData.value = { ...formData.value };
pageDataRef.value.setQuery(appliedFormData.value); pageDataRef.value.setQuery({
feedbackType: appliedFormData.value.feedbackType || undefined,
keyword: appliedFormData.value.feedbackContent.trim() || undefined,
status: appliedFormData.value.status || undefined,
});
} }
}); });
} }
@@ -242,7 +246,7 @@ function openFeedbackDetail(row: any) {
:page-size="initialListState.pageSize" :page-size="initialListState.pageSize"
:initial-page-number="initialListState.pageNumber" :initial-page-number="initialListState.pageNumber"
:initial-query-params="{ :initial-query-params="{
feedbackContent: initialListState.feedbackContent || undefined, keyword: initialListState.feedbackContent || undefined,
feedbackType: initialListState.feedbackType || undefined, feedbackType: initialListState.feedbackType || undefined,
status: initialListState.status || undefined, status: initialListState.status || undefined,
}" }"

View File

@@ -61,8 +61,7 @@ watchListRouteState(route, sysJobListRouteSchema, (state) => {
pageNumber: state.pageNumber, pageNumber: state.pageNumber,
pageSize: state.pageSize, pageSize: state.pageSize,
queryParams: { queryParams: {
isQueryOr: true, keyword: state.keyword || undefined,
jobName: state.keyword || undefined,
}, },
}); });
}); });
@@ -88,8 +87,7 @@ function initDict() {
const handleSearch = (params: string) => { const handleSearch = (params: string) => {
searchKeyword.value = params; searchKeyword.value = params;
pageDataRef.value.setQuery({ pageDataRef.value.setQuery({
jobName: params || undefined, keyword: params || undefined,
isQueryOr: true,
}); });
}; };
function reloadCurrentList() { function reloadCurrentList() {
@@ -230,8 +228,7 @@ function handlePageStateChange(state: {
:page-size="initialListState.pageSize" :page-size="initialListState.pageSize"
:initial-page-number="initialListState.pageNumber" :initial-page-number="initialListState.pageNumber"
:initial-query-params="{ :initial-query-params="{
jobName: initialListState.keyword || undefined, keyword: initialListState.keyword || undefined,
isQueryOr: true,
}" }"
@state-change="handlePageStateChange" @state-change="handlePageStateChange"
> >

View File

@@ -37,7 +37,7 @@ function buildTimeQuery(range: string[]) {
function applyQuery() { function applyQuery() {
pageDataRef.value?.setQuery({ pageDataRef.value?.setQuery({
actionName: actionName.value || undefined, keyword: actionName.value || undefined,
...buildTimeQuery(timeRange.value), ...buildTimeQuery(timeRange.value),
}); });
} }

View File

@@ -47,8 +47,7 @@ function initDict() {
} }
const handleSearch = (params: string) => { const handleSearch = (params: string) => {
// 后端支持 positionName 模糊查询 pageDataRef.value.setQuery({ keyword: params || undefined });
pageDataRef.value.setQuery({ positionName: params });
}; };
function reset(formEl?: FormInstance) { function reset(formEl?: FormInstance) {

View File

@@ -49,7 +49,7 @@ function isAdminRole(data: any) {
return data?.roleKey === 'super_admin'; return data?.roleKey === 'super_admin';
} }
const handleSearch = (params: string) => { const handleSearch = (params: string) => {
pageDataRef.value.setQuery({ roleName: params, isQueryOr: true }); pageDataRef.value.setQuery({ keyword: params || undefined });
}; };
function reset(formEl?: FormInstance) { function reset(formEl?: FormInstance) {
formEl?.resetFields(); formEl?.resetFields();

View File

@@ -1,5 +1,5 @@
<script setup> <script setup>
import { computed, ref } from 'vue'; import { computed, ref, watch } from 'vue';
import { ArrowDown } from '@element-plus/icons-vue'; import { ArrowDown } from '@element-plus/icons-vue';
import { import {
@@ -31,6 +31,10 @@ const props = defineProps({
type: Number, type: Number,
default: 3, default: 3,
}, },
initialValue: {
type: String,
default: '',
},
// 搜索框占位符 // 搜索框占位符
searchPlaceholder: { searchPlaceholder: {
type: String, type: String,
@@ -38,10 +42,17 @@ const props = defineProps({
}, },
}); });
const emit = defineEmits(['search', 'button-click', 'buttonClick']); const emit = defineEmits(['search', 'button-click', 'buttonClick', 'reset']);
// 搜索值 // 搜索值
const searchValue = ref(''); const searchValue = ref(props.initialValue);
watch(
() => props.initialValue,
(value) => {
searchValue.value = value;
},
);
// 计算显示的按钮 // 计算显示的按钮
const visibleButtons = computed(() => { const visibleButtons = computed(() => {
@@ -55,11 +66,19 @@ const dropdownButtons = computed(() => {
// 处理搜索 // 处理搜索
const handleSearch = () => { const handleSearch = () => {
searchValue.value = searchValue.value.trim();
emit('search', searchValue.value); emit('search', searchValue.value);
}; };
const handleClear = () => {
searchValue.value = '';
emit('search', '');
};
const handleReset = () => { const handleReset = () => {
searchValue.value = ''; searchValue.value = '';
emit('reset');
emit('search', '');
}; };
// 处理按钮点击 // 处理按钮点击
@@ -91,10 +110,11 @@ const handleDropdownClick = (button) => {
<div> <div>
<ElInput <ElInput
v-model="searchValue" v-model="searchValue"
:placeholder="$t('common.searchPlaceholder')" :placeholder="searchPlaceholder"
class="search-input" class="search-input"
@keyup.enter="handleSearch"
clearable clearable
@clear="handleClear"
@keyup.enter="handleSearch"
/> />
</div> </div>
<div> <div>

View File

@@ -113,8 +113,10 @@ function removeBotFromRecentlyUsed(botId: any) {
<h1 class="text-2xl font-medium">助理市场</h1> <h1 class="text-2xl font-medium">助理市场</h1>
<ElSpace :size="20"> <ElSpace :size="20">
<ElInput <ElInput
placeholder="搜索" v-model="queryParams.keyword"
v-model="queryParams.title" clearable
placeholder="搜索助理名称或描述"
@clear="getBotList"
@keyup.enter="getBotList" @keyup.enter="getBotList"
:prefix-icon="Search" :prefix-icon="Search"
/> />

View File

@@ -61,8 +61,10 @@ function handleTagClick(tag: any) {
<h1 class="text-2xl font-medium">智能体</h1> <h1 class="text-2xl font-medium">智能体</h1>
<ElSpace :size="20"> <ElSpace :size="20">
<ElInput <ElInput
placeholder="搜索" v-model="queryParams.keyword"
v-model="queryParams.title" clearable
placeholder="搜索工作流名称或描述"
@clear="getWorkflowList"
@keyup.enter="getWorkflowList" @keyup.enter="getWorkflowList"
:prefix-icon="Search" :prefix-icon="Search"
/> />

View File

@@ -42,6 +42,8 @@ const queryParams = ref({
pageNumber: 1, pageNumber: 1,
pageSize: 20, pageSize: 20,
}); });
const appliedSessionKeyword = ref('');
let latestSessionRequestId = 0;
const pageState = ref({ const pageState = ref({
total: 0, total: 0,
@@ -73,20 +75,7 @@ const variantSwitchController = createChatVariantSwitchController<
}); });
const filteredSessions = computed(() => { const filteredSessions = computed(() => {
const keyword = queryParams.value.keyword.trim().toLowerCase(); return sessions.value;
if (!keyword) {
return sessions.value;
}
return sessions.value.filter((item) => {
const title = String(item.title || '').toLowerCase();
const preview = String(item.lastMessagePreview || '').toLowerCase();
const assistantName = String(item.assistantName || '').toLowerCase();
return (
title.includes(keyword) ||
preview.includes(keyword) ||
assistantName.includes(keyword)
);
});
}); });
onMounted(async () => { onMounted(async () => {
@@ -127,13 +116,19 @@ async function fetchAssistants() {
async function fetchSessions() { async function fetchSessions() {
loading.value = true; loading.value = true;
const requestId = ++latestSessionRequestId;
const requestParams = {
assistantId: queryParams.value.assistantId,
keyword: appliedSessionKeyword.value || undefined,
pageNumber: queryParams.value.pageNumber,
pageSize: queryParams.value.pageSize,
};
const [, res] = await tryit(api.get)('/userCenter/chatHistory/sessions', { const [, res] = await tryit(api.get)('/userCenter/chatHistory/sessions', {
params: { params: requestParams,
assistantId: queryParams.value.assistantId,
pageNumber: queryParams.value.pageNumber,
pageSize: queryParams.value.pageSize,
},
}); });
if (requestId !== latestSessionRequestId) {
return;
}
loading.value = false; loading.value = false;
if (res?.errorCode === 0) { if (res?.errorCode === 0) {
sessions.value = res.data?.records || []; sessions.value = res.data?.records || [];
@@ -141,6 +136,19 @@ async function fetchSessions() {
} }
} }
function handleSessionSearch() {
const keyword = queryParams.value.keyword.trim();
queryParams.value.keyword = keyword;
appliedSessionKeyword.value = keyword;
queryParams.value.pageNumber = 1;
void fetchSessions();
}
function handleAssistantChange() {
queryParams.value.pageNumber = 1;
void fetchSessions();
}
async function openSession(sessionId: string | number) { async function openSession(sessionId: string | number) {
drawerLoading.value = true; drawerLoading.value = true;
const [, summaryRes] = await tryit(api.get)( const [, summaryRes] = await tryit(api.get)(
@@ -387,13 +395,16 @@ function formatTime(value?: string) {
placeholder="筛选聊天助理" placeholder="筛选聊天助理"
:options="assistantList" :options="assistantList"
class="!w-[220px]" class="!w-[220px]"
@change="fetchSessions" @change="handleAssistantChange"
/> />
<ElInput <ElInput
v-model="queryParams.keyword" v-model="queryParams.keyword"
class="max-w-[320px]" class="max-w-[320px]"
placeholder="搜索标题或最近消息" clearable
placeholder="搜索标题、最近消息或助理"
:prefix-icon="Search" :prefix-icon="Search"
@clear="handleSessionSearch"
@keyup.enter="handleSessionSearch"
/> />
</div> </div>
</ElSpace> </ElSpace>