feat: 统一列表模糊搜索行为
- 统一管理端和用户中心搜索参数及多字段包含匹配 - 修复聊天搜索竞态并优化部门重名路径展示 - 补充部门展开、模型空白词和聊天查询回归测试
This commit is contained in:
@@ -101,6 +101,16 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
|
||||
super(service);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Agent 列表关键字搜索字段。
|
||||
*
|
||||
* @return Agent 名称和描述属性
|
||||
*/
|
||||
@Override
|
||||
protected String[] getKeywordSearchProperties() {
|
||||
return new String[]{"name", "description"};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Agent 详情。
|
||||
*
|
||||
|
||||
@@ -97,6 +97,16 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
||||
this.botMessageService = botMessageService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取智能体列表关键字搜索字段。
|
||||
*
|
||||
* @return 标题和描述属性
|
||||
*/
|
||||
@Override
|
||||
protected String[] getKeywordSearchProperties() {
|
||||
return new String[]{"title", "description"};
|
||||
}
|
||||
|
||||
@Resource
|
||||
private BotPluginService botPluginService;
|
||||
|
||||
|
||||
@@ -86,6 +86,16 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
||||
this.llmService = llmService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取知识库列表关键字搜索字段。
|
||||
*
|
||||
* @return 标题和描述属性
|
||||
*/
|
||||
@Override
|
||||
protected String[] getKeywordSearchProperties() {
|
||||
return new String[]{"title", "description"};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Result<?> onSaveOrUpdateBefore(DocumentCollection entity, boolean isSave) {
|
||||
normalizeVisibilityScope(entity, isSave);
|
||||
|
||||
@@ -99,6 +99,16 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
|
||||
super(service);
|
||||
this.knowledgeService = knowledgeService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取知识库文档关键字搜索字段。
|
||||
*
|
||||
* @return 文件标题属性
|
||||
*/
|
||||
@Override
|
||||
protected String[] getKeywordSearchProperties() {
|
||||
return new String[]{"title"};
|
||||
}
|
||||
@PostMapping("removeDoc")
|
||||
@Transactional
|
||||
@SaCheckPermission("/api/v1/documentCollection/remove")
|
||||
@@ -148,13 +158,19 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
|
||||
|
||||
@GetMapping("documentList")
|
||||
@SaCheckPermission("/api/v1/documentCollection/query")
|
||||
public Result<Page<Document>> documentList(@RequestParam(name="title", required = false) String fileName, @RequestParam(name="pageSize") int pageSize, @RequestParam(name = "pageNumber") int pageNumber) {
|
||||
public Result<Page<Document>> documentList(
|
||||
@RequestParam(name = "keyword", required = false) String keyword,
|
||||
@RequestParam(name = "title", required = false) String legacyTitle,
|
||||
@RequestParam(name = "pageSize") int pageSize,
|
||||
@RequestParam(name = "pageNumber") int pageNumber) {
|
||||
String kbSlug = RequestUtil.getParamAsString("id");
|
||||
if (StringUtil.noText(kbSlug)) {
|
||||
throw new BusinessException("知识库id不能为空");
|
||||
}
|
||||
DocumentCollection knowledge = getDocumentCollection(kbSlug, ResourceAction.READ, "无权限访问知识库");
|
||||
Page<Document> documentList = documentService.getDocumentList(knowledge.getId().toString(), pageSize, pageNumber,fileName);
|
||||
String effectiveKeyword = StringUtil.hasText(keyword) ? keyword : legacyTitle;
|
||||
Page<Document> documentList = documentService.getDocumentList(
|
||||
knowledge.getId().toString(), pageSize, pageNumber, effectiveKeyword);
|
||||
return Result.ok(documentList);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.query.QueryColumn;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
@@ -114,9 +115,14 @@ public class FaqItemController extends BaseCurdController<FaqItemService, FaqIte
|
||||
QueryWrapper queryWrapper = QueryWrapper.create()
|
||||
.eq(FaqItem::getCollectionId, collectionId);
|
||||
|
||||
String question = request.getParameter("question");
|
||||
if (question != null && !question.trim().isEmpty()) {
|
||||
queryWrapper.like(FaqItem::getQuestion, question.trim());
|
||||
String keyword = normalizeSearchKeyword(request.getParameter("keyword"));
|
||||
String question = normalizeSearchKeyword(request.getParameter("question"));
|
||||
if (StringUtils.hasText(keyword)) {
|
||||
queryWrapper.and(buildLiteralContainsCondition(
|
||||
keyword, new QueryColumn("question"), new QueryColumn("answer_text")));
|
||||
} else if (StringUtils.hasText(question)) {
|
||||
// 兼容旧客户端仅按问题字段搜索。
|
||||
queryWrapper.and(buildLiteralContainsCondition(question, new QueryColumn("question")));
|
||||
}
|
||||
|
||||
String categoryIdText = request.getParameter("categoryId");
|
||||
|
||||
@@ -36,6 +36,16 @@ public class McpController extends BaseCurdController<McpService, Mcp> {
|
||||
super(service);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 MCP 列表关键字搜索字段。
|
||||
*
|
||||
* @return 标题和描述属性
|
||||
*/
|
||||
@Override
|
||||
protected String[] getKeywordSearchProperties() {
|
||||
return new String[]{"title", "description"};
|
||||
}
|
||||
|
||||
@Resource
|
||||
private AgentResourceReferenceService agentResourceReferenceService;
|
||||
@Override
|
||||
|
||||
@@ -193,8 +193,10 @@ public class ModelController extends BaseCurdController<ModelService, Model> {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create();
|
||||
queryWrapper.eq(Model::getProviderId, providerId);
|
||||
queryWrapper.eq(Model::getModelType, modelType);
|
||||
if (StringUtils.hasLength(selectText)) {
|
||||
queryWrapper.and(ModelTableDef.MODEL.TITLE.like(selectText).or(ModelTableDef.MODEL.MODEL_NAME.like(selectText)));
|
||||
String keyword = normalizeSearchKeyword(selectText);
|
||||
if (StringUtils.hasText(keyword)) {
|
||||
queryWrapper.and(buildLiteralContainsCondition(
|
||||
keyword, ModelTableDef.MODEL.TITLE, ModelTableDef.MODEL.MODEL_NAME));
|
||||
}
|
||||
List<Model> totalList = service.getMapper().selectListWithRelationsByQuery(queryWrapper);
|
||||
Map<String, List<Model>> groupList = totalList.stream().collect(Collectors.groupingBy(Model::getGroupName));
|
||||
|
||||
@@ -2,6 +2,7 @@ package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.query.QueryColumn;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -58,6 +59,16 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
|
||||
this.pluginService = service;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件列表关键字搜索字段。
|
||||
*
|
||||
* @return 插件名称和描述属性
|
||||
*/
|
||||
@Override
|
||||
protected String[] getKeywordSearchProperties() {
|
||||
return new String[]{"name", "description"};
|
||||
}
|
||||
|
||||
@Resource
|
||||
PluginService pluginService;
|
||||
@Resource
|
||||
@@ -118,7 +129,7 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
|
||||
}
|
||||
|
||||
/**
|
||||
* 按分类分页查询插件,并支持按名称模糊查询。
|
||||
* 按分类分页查询插件,并支持按名称、描述模糊查询。
|
||||
*
|
||||
* @param request 当前请求
|
||||
* @param sortKey 排序字段
|
||||
@@ -126,13 +137,15 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
|
||||
* @param pageNumber 页码
|
||||
* @param pageSize 每页数量
|
||||
* @param category 分类 ID,0 表示全部分类
|
||||
* @param name 插件名称关键字
|
||||
* @param keyword 插件名称或描述关键字
|
||||
* @param name 兼容旧客户端的插件名称关键字
|
||||
* @return 插件分页结果
|
||||
*/
|
||||
@GetMapping("/pageByCategory")
|
||||
@SaCheckPermission("/api/v1/plugin/query")
|
||||
public Result<Page<Plugin>> pageByCategory(HttpServletRequest request, String sortKey, String sortType,
|
||||
Long pageNumber, Long pageSize, int category, String name) {
|
||||
Long pageNumber, Long pageSize, int category,
|
||||
String keyword, String name) {
|
||||
if (pageNumber == null || pageNumber < 1) {
|
||||
pageNumber = 1L;
|
||||
}
|
||||
@@ -145,7 +158,10 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
|
||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||
return Result.ok(queryPage(new Page<>(pageNumber, pageSize), queryWrapper));
|
||||
} else {
|
||||
Result<Page<Plugin>> result = pluginService.pageByCategory(pageNumber, pageSize, category, name);
|
||||
String effectiveKeyword = normalizeSearchKeyword(
|
||||
keyword == null || keyword.isBlank() ? name : keyword);
|
||||
Result<Page<Plugin>> result = pluginService.pageByCategory(
|
||||
pageNumber, pageSize, category, effectiveKeyword);
|
||||
if (result != null && result.getData() != null) {
|
||||
aiResourceCreatorNameSupport.fillPluginCreatorNames(result.getData().getRecords());
|
||||
}
|
||||
@@ -160,7 +176,7 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
|
||||
workflowVisibilityQueryHelper.applyReadableAccess(queryWrapper);
|
||||
queryWrapper.eq("publish_status", tech.easyflow.ai.enums.PublishStatus.PUBLISHED.getCode());
|
||||
if (keyword != null && !keyword.isBlank()) {
|
||||
queryWrapper.like("title", keyword.trim());
|
||||
queryWrapper.and(buildLiteralContainsCondition(keyword, new QueryColumn("title")));
|
||||
}
|
||||
queryWrapper.orderBy("modified desc");
|
||||
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
|
||||
|
||||
@@ -67,6 +67,16 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
|
||||
super(service);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件工具列表关键字搜索字段。
|
||||
*
|
||||
* @return 工具名称和描述属性
|
||||
*/
|
||||
@Override
|
||||
protected String[] getKeywordSearchProperties() {
|
||||
return new String[]{"name", "description"};
|
||||
}
|
||||
|
||||
@Resource
|
||||
private PluginItemService pluginItemService;
|
||||
|
||||
|
||||
@@ -45,6 +45,16 @@ public class ResourceController extends BaseCurdController<ResourceService, Reso
|
||||
super(service);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取资源列表关键字搜索字段。
|
||||
*
|
||||
* @return 资源名称属性
|
||||
*/
|
||||
@Override
|
||||
protected String[] getKeywordSearchProperties() {
|
||||
return new String[]{"resourceName"};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Result<?> onSaveOrUpdateBefore(Resource entity, boolean isSave) {
|
||||
LoginAccount loginUser = SaTokenUtil.getLoginAccount();
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.easyagents.core.store.DocumentStore;
|
||||
import com.easyagents.core.store.StoreOptions;
|
||||
import com.easyagents.core.store.StoreResult;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.query.QueryColumn;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
@@ -47,6 +48,7 @@ import tech.easyflow.ai.vo.FaqImportResultVo;
|
||||
import tech.easyflow.ai.vo.KnowledgeShareAuthContext;
|
||||
import tech.easyflow.ai.vo.KnowledgeShareViewDetail;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.util.SearchKeywordUtil;
|
||||
import tech.easyflow.common.filestorage.FileStorageService;
|
||||
import tech.easyflow.common.vo.UploadResVo;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
@@ -668,9 +670,15 @@ public class ShareKnowledgeController {
|
||||
faqCategoryService.ensureDefaultCategory(context.getKnowledge().getId());
|
||||
QueryWrapper queryWrapper = QueryWrapper.create()
|
||||
.eq(FaqItem::getCollectionId, context.getKnowledge().getId());
|
||||
String keyword = request.getParameter("keyword");
|
||||
String question = request.getParameter("question");
|
||||
if (StringUtils.hasText(question)) {
|
||||
queryWrapper.like(FaqItem::getQuestion, question.trim());
|
||||
if (StringUtils.hasText(keyword)) {
|
||||
String pattern = SearchKeywordUtil.literalContainsPattern(keyword);
|
||||
queryWrapper.and(new QueryColumn("question").likeRaw(pattern)
|
||||
.or(new QueryColumn("answer_text").likeRaw(pattern)));
|
||||
} else if (StringUtils.hasText(question)) {
|
||||
queryWrapper.and(new QueryColumn("question")
|
||||
.likeRaw(SearchKeywordUtil.literalContainsPattern(question)));
|
||||
}
|
||||
String categoryId = request.getParameter("categoryId");
|
||||
if (StringUtils.hasText(categoryId)) {
|
||||
|
||||
@@ -113,6 +113,16 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
this.modelService = modelService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工作流列表关键字搜索字段。
|
||||
*
|
||||
* @return 标题和描述属性
|
||||
*/
|
||||
@Override
|
||||
protected String[] getKeywordSearchProperties() {
|
||||
return new String[]{"title", "description"};
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询工作流设计器初始化所需的安全选项。
|
||||
*
|
||||
@@ -130,14 +140,16 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
*
|
||||
* @param pageNumber 页码
|
||||
* @param pageSize 每页数量
|
||||
* @param keyword 插件名称或描述关键字
|
||||
* @return 插件安全选项分页
|
||||
*/
|
||||
@GetMapping("/designer/plugins")
|
||||
@SaCheckPermission("/api/v1/workflow/query")
|
||||
public Result<Page<WorkflowDesignerOptionsView.PluginOption>> designerPlugins(
|
||||
Long pageNumber,
|
||||
Long pageSize) {
|
||||
return Result.ok(workflowDesignerOptionService.pagePlugins(pageNumber, pageSize));
|
||||
Long pageSize,
|
||||
String keyword) {
|
||||
return Result.ok(workflowDesignerOptionService.pagePlugins(pageNumber, pageSize, keyword));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -37,6 +37,16 @@ public class WorkflowExecResultController extends BaseCurdController<WorkflowExe
|
||||
super(service);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工作流执行记录关键字搜索字段。
|
||||
*
|
||||
* @return 执行 Key 属性
|
||||
*/
|
||||
@Override
|
||||
protected String[] getKeywordSearchProperties() {
|
||||
return new String[]{"execKey"};
|
||||
}
|
||||
|
||||
@GetMapping("/del")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@SaCheckPermission("/api/v1/workflow/remove")
|
||||
|
||||
@@ -37,6 +37,16 @@ public class WorkflowExecStepController extends BaseCurdController<WorkflowExecS
|
||||
super(service);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取执行步骤关键字搜索字段。
|
||||
*
|
||||
* @return 节点名称和节点 ID 属性
|
||||
*/
|
||||
@Override
|
||||
protected String[] getKeywordSearchProperties() {
|
||||
return new String[]{"nodeName", "nodeId"};
|
||||
}
|
||||
|
||||
@GetMapping("/getListByRecordId")
|
||||
public Result<List<WorkflowExecStep>> getListByRecordId(BigInteger recordId) {
|
||||
if (recordId == null) {
|
||||
@@ -60,4 +70,4 @@ public class WorkflowExecStepController extends BaseCurdController<WorkflowExecS
|
||||
}
|
||||
return Result.ok(list);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,16 @@ public class SysJobController extends BaseCurdController<SysJobService, SysJob>
|
||||
this.workflowRunningParameterResolver = workflowRunningParameterResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取定时任务关键字搜索字段。
|
||||
*
|
||||
* @return 任务名称和备注属性
|
||||
*/
|
||||
@Override
|
||||
protected String[] getKeywordSearchProperties() {
|
||||
return new String[]{"jobName", "remark"};
|
||||
}
|
||||
|
||||
@GetMapping("/start")
|
||||
@SaCheckPermission("/api/v1/sysJob/save")
|
||||
@LogRecord("启动定时任务")
|
||||
|
||||
@@ -10,6 +10,7 @@ import tech.easyflow.common.annotation.UsePermission;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.util.SearchKeywordUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.skill.entity.SkillCategory;
|
||||
@@ -69,9 +70,11 @@ public class SkillCategoryController {
|
||||
queryWrapper.eq(SkillCategory::getId, entity.getId(), entity.getId() != null)
|
||||
.eq(SkillCategory::getParentId, entity.getParentId(), entity.getParentId() != null)
|
||||
.eq(SkillCategory::getLevelNo, entity.getLevelNo(), entity.getLevelNo() != null)
|
||||
.eq(SkillCategory::getStatus, entity.getStatus(), entity.getStatus() != null)
|
||||
.like(SkillCategory::getCategoryName, entity.getCategoryName(),
|
||||
entity.getCategoryName() != null && !entity.getCategoryName().isBlank());
|
||||
.eq(SkillCategory::getStatus, entity.getStatus(), entity.getStatus() != null);
|
||||
if (entity.getCategoryName() != null && !entity.getCategoryName().isBlank()) {
|
||||
queryWrapper.and("category_name LIKE ?",
|
||||
SearchKeywordUtil.literalContainsPattern(entity.getCategoryName()));
|
||||
}
|
||||
}
|
||||
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess(CategoryResourceType.SKILL.getCode());
|
||||
if (access.isRestricted()) {
|
||||
|
||||
@@ -24,6 +24,7 @@ import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.util.SearchKeywordUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.skill.capability.SkillCapabilityBindingService;
|
||||
@@ -159,7 +160,7 @@ public class SkillController {
|
||||
.eq("source_type", sourceType, hasText(sourceType));
|
||||
String keyword = hasText(displayName) ? displayName : name;
|
||||
if (hasText(keyword)) {
|
||||
String pattern = "%" + keyword + "%";
|
||||
String pattern = SearchKeywordUtil.literalContainsPattern(keyword);
|
||||
query.and("(name LIKE ? OR display_name LIKE ? OR description LIKE ?)", pattern, pattern, pattern);
|
||||
}
|
||||
if (hasText(capabilityType)) {
|
||||
@@ -695,4 +696,5 @@ public class SkillController {
|
||||
private boolean hasText(String value) {
|
||||
return value != null && !value.isBlank();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ public class SysAccountController extends BaseCurdController<SysAccountService,
|
||||
return super.buildQueryWrapper(request);
|
||||
}
|
||||
|
||||
String likePattern = "%" + escapeLikeKeyword(keyword.trim()) + "%";
|
||||
String likePattern = buildLiteralContainsPattern(normalizeSearchKeyword(keyword));
|
||||
QueryWrapper roleExistsQuery = QueryMethods.selectOne()
|
||||
.from(SYS_ACCOUNT_ROLE)
|
||||
.innerJoin(SYS_ROLE)
|
||||
@@ -126,20 +126,7 @@ public class SysAccountController extends BaseCurdController<SysAccountService,
|
||||
.or(SYS_ACCOUNT.EMAIL.likeRaw(likePattern))
|
||||
.or(QueryMethods.exists(roleExistsQuery));
|
||||
|
||||
return QueryWrapper.create().and(keywordCondition);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转义 MySQL LIKE 模式中的特殊字符,使用户输入按普通文本匹配。
|
||||
*
|
||||
* @param keyword 已去除首尾空格的关键字
|
||||
* @return 可安全放入 LIKE 模式的文本
|
||||
*/
|
||||
private String escapeLikeKeyword(String keyword) {
|
||||
return keyword
|
||||
.replace("\\", "\\\\")
|
||||
.replace("%", "\\%")
|
||||
.replace("_", "\\_");
|
||||
return super.buildQueryWrapper(request).and(keywordCondition);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -58,6 +58,16 @@ public class SysApiKeyController extends BaseCurdController<SysApiKeyService, Sy
|
||||
super(service);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 API Key 列表关键字搜索字段。
|
||||
*
|
||||
* @return 名称和 Key 属性
|
||||
*/
|
||||
@Override
|
||||
protected String[] getKeywordSearchProperties() {
|
||||
return new String[]{"name", "apiKey"};
|
||||
}
|
||||
|
||||
@Resource
|
||||
private SysApiKeyResourceMappingService sysApiKeyResourceMappingService;
|
||||
@Resource
|
||||
|
||||
@@ -26,6 +26,16 @@ public class SysApiKeyResourceController extends BaseCurdController<SysApiKeyRes
|
||||
super(service);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取接口授权资源关键字搜索字段。
|
||||
*
|
||||
* @return 请求接口和标题属性
|
||||
*/
|
||||
@Override
|
||||
protected String[] getKeywordSearchProperties() {
|
||||
return new String[]{"requestInterface", "title"};
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询普通 API Key 接口授权资源。
|
||||
*
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package tech.easyflow.admin.controller.system;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.mybatisflex.core.query.QueryColumn;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -89,9 +90,49 @@ public class SysDeptController extends BaseCurdController<SysDeptService, SysDep
|
||||
@GetMapping("list")
|
||||
public Result<List<SysDept>> list(SysDept entity, Boolean asTree, String sortKey, String sortType) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||
String keyword = entity == null ? "" : normalizeSearchKeyword(entity.getKeyword());
|
||||
if (tech.easyflow.common.util.StringUtil.hasText(keyword)) {
|
||||
queryWrapper.and(buildLiteralContainsCondition(
|
||||
keyword, new QueryColumn("dept_name"), new QueryColumn("dept_code")));
|
||||
}
|
||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||
List<SysDept> sysMenus = service.list(queryWrapper);
|
||||
return Result.ok(Tree.tryToTree(sysMenus, "id", "parentId"));
|
||||
List<SysDept> matchedDepartments = service.list(queryWrapper);
|
||||
if (!tech.easyflow.common.util.StringUtil.hasText(keyword)) {
|
||||
return Result.ok(Tree.tryToTree(matchedDepartments, "id", "parentId"));
|
||||
}
|
||||
if (matchedDepartments.isEmpty()) {
|
||||
return Result.ok(List.of());
|
||||
}
|
||||
|
||||
// 搜索结果保留所有重名命中项,并补齐各自祖先节点以维持可定位的树结构。
|
||||
Set<BigInteger> visibleIds = new LinkedHashSet<>();
|
||||
for (SysDept department : matchedDepartments) {
|
||||
visibleIds.add(department.getId());
|
||||
addAncestorIds(visibleIds, department.getAncestors());
|
||||
}
|
||||
QueryWrapper visibleDepartmentQuery = QueryWrapper.create()
|
||||
.in(SysDept::getId, visibleIds)
|
||||
.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||
List<SysDept> visibleDepartments = service.list(visibleDepartmentQuery);
|
||||
return Result.ok(Tree.tryToTree(visibleDepartments, "id", "parentId"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将逗号分隔的祖先 ID 加入可见集合。
|
||||
*
|
||||
* @param visibleIds 可见部门 ID 集合
|
||||
* @param ancestors 祖先路径
|
||||
*/
|
||||
private void addAncestorIds(Set<BigInteger> visibleIds, String ancestors) {
|
||||
if (!tech.easyflow.common.util.StringUtil.hasText(ancestors)) {
|
||||
return;
|
||||
}
|
||||
for (String ancestor : ancestors.split(",")) {
|
||||
String normalized = ancestor.trim();
|
||||
if (!normalized.isEmpty() && !"0".equals(normalized)) {
|
||||
visibleIds.add(new BigInteger(normalized));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
package tech.easyflow.admin.controller.system;
|
||||
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.query.QueryCondition;
|
||||
import com.mybatisflex.core.query.QueryMethods;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.mybatisflex.core.relation.RelationManager;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.common.util.StringUtil;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.log.annotation.LogRecord;
|
||||
@@ -20,6 +24,9 @@ import java.time.format.DateTimeParseException;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
|
||||
import static tech.easyflow.system.entity.table.SysAccountTableDef.SYS_ACCOUNT;
|
||||
import static tech.easyflow.system.entity.table.SysLogTableDef.SYS_LOG;
|
||||
|
||||
/**
|
||||
* 操作日志表 控制层。
|
||||
*
|
||||
@@ -53,6 +60,22 @@ public class SysLogController extends BaseCurdController<SysLogService, SysLog>
|
||||
@Override
|
||||
protected QueryWrapper buildQueryWrapper(HttpServletRequest request) {
|
||||
QueryWrapper queryWrapper = super.buildQueryWrapper(request);
|
||||
String keyword = normalizeSearchKeyword(request.getParameter("keyword"));
|
||||
if (StringUtil.hasText(keyword)) {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
String pattern = buildLiteralContainsPattern(keyword);
|
||||
QueryCondition accountKeyword = SYS_ACCOUNT.LOGIN_NAME.likeRaw(pattern)
|
||||
.or(SYS_ACCOUNT.NICKNAME.likeRaw(pattern));
|
||||
QueryWrapper accountExists = QueryMethods.selectOne()
|
||||
.from(SYS_ACCOUNT)
|
||||
.where(SYS_ACCOUNT.ID.eq(SYS_LOG.ACCOUNT_ID))
|
||||
.and(SYS_ACCOUNT.TENANT_ID.eq(account.getTenantId()))
|
||||
.and(accountKeyword);
|
||||
QueryCondition keywordCondition = SYS_LOG.ACTION_NAME.likeRaw(pattern)
|
||||
.or(SYS_LOG.ACTION_IP.likeRaw(pattern))
|
||||
.or(QueryMethods.exists(accountExists));
|
||||
queryWrapper.and(keywordCondition);
|
||||
}
|
||||
Date createdStart = parseQueryTime(request.getParameter("createdStart"));
|
||||
Date createdEnd = parseQueryTime(request.getParameter("createdEnd"));
|
||||
if (createdStart != null && createdEnd != null && createdStart.after(createdEnd)) {
|
||||
|
||||
@@ -41,7 +41,7 @@ public class SysPositionController extends BaseCurdController<SysPositionService
|
||||
/**
|
||||
* 分页查询岗位列表
|
||||
* <p>
|
||||
* 支持按岗位名称模糊查询,按状态、编码精确查询。
|
||||
* 支持按岗位名称、岗位编码统一模糊查询,状态保持精确查询。
|
||||
* </p>
|
||||
*
|
||||
* @param request 请求对象
|
||||
@@ -67,17 +67,22 @@ public class SysPositionController extends BaseCurdController<SysPositionService
|
||||
.from(SYS_POSITION);
|
||||
|
||||
// 获取查询参数
|
||||
String keyword = normalizeSearchKeyword(request.getParameter("keyword"));
|
||||
String positionName = request.getParameter("positionName");
|
||||
String positionCode = request.getParameter("positionCode");
|
||||
String status = request.getParameter("status");
|
||||
|
||||
// 岗位名称 - 模糊查询
|
||||
if (StringUtil.hasText(positionName)) {
|
||||
queryWrapper.where(SYS_POSITION.POSITION_NAME.like(positionName));
|
||||
}
|
||||
// 岗位编码 - 精确查询
|
||||
if (StringUtil.hasText(positionCode)) {
|
||||
queryWrapper.where(SYS_POSITION.POSITION_CODE.eq(positionCode));
|
||||
if (StringUtil.hasText(keyword)) {
|
||||
queryWrapper.and(buildLiteralContainsCondition(
|
||||
keyword, SYS_POSITION.POSITION_NAME, SYS_POSITION.POSITION_CODE));
|
||||
} else {
|
||||
// 兼容仍按旧参数调用的客户端。
|
||||
if (StringUtil.hasText(positionName)) {
|
||||
queryWrapper.and(buildLiteralContainsCondition(positionName, SYS_POSITION.POSITION_NAME));
|
||||
}
|
||||
if (StringUtil.hasText(positionCode)) {
|
||||
queryWrapper.where(SYS_POSITION.POSITION_CODE.eq(positionCode));
|
||||
}
|
||||
}
|
||||
// 状态 - 精确查询
|
||||
if (StringUtil.hasText(status)) {
|
||||
@@ -129,4 +134,4 @@ public class SysPositionController extends BaseCurdController<SysPositionService
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,16 @@ public class SysRoleController extends BaseCurdController<SysRoleService, SysRol
|
||||
super(service);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色列表关键字搜索字段。
|
||||
*
|
||||
* @return 角色名称和角色标识属性
|
||||
*/
|
||||
@Override
|
||||
protected String[] getKeywordSearchProperties() {
|
||||
return new String[]{"roleName", "roleKey"};
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询角色表单所需的菜单和非 Bot 分类选项。
|
||||
*
|
||||
|
||||
@@ -24,6 +24,16 @@ public class SysUserFeedbackController extends BaseCurdController<SysUserFeedbac
|
||||
super(service);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户反馈关键字搜索字段。
|
||||
*
|
||||
* @return 反馈内容和联系方式属性
|
||||
*/
|
||||
@Override
|
||||
protected String[] getKeywordSearchProperties() {
|
||||
return new String[]{"feedbackContent", "contactInfo"};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Result<?> onSaveOrUpdateBefore(SysUserFeedback entity, boolean isSave) {
|
||||
if (!isSave) {
|
||||
@@ -33,4 +43,4 @@ public class SysUserFeedbackController extends BaseCurdController<SysUserFeedbac
|
||||
}
|
||||
return super.onSaveOrUpdateBefore(entity, isSave);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,15 +205,20 @@ public class WorkflowDesignerOptionService {
|
||||
*
|
||||
* @param pageNumber 页码
|
||||
* @param pageSize 每页数量
|
||||
* @param keyword 插件名称或描述关键字
|
||||
* @return 插件安全选项分页
|
||||
*/
|
||||
public Page<WorkflowDesignerOptionsView.PluginOption> pagePlugins(Long pageNumber, Long pageSize) {
|
||||
public Page<WorkflowDesignerOptionsView.PluginOption> pagePlugins(
|
||||
Long pageNumber, Long pageSize, String keyword) {
|
||||
LoginAccount account = requireAccount();
|
||||
QueryWrapper wrapper = QueryWrapper.create()
|
||||
.eq(Plugin::getTenantId, account.getTenantId().longValue())
|
||||
.orderBy(Plugin::getCreated, false);
|
||||
List<Plugin> plugins = pluginService.getMapper().selectListWithRelationsByQuery(wrapper);
|
||||
List<Plugin> availablePlugins = pluginService.preparePluginsForCurrentUser(plugins, false, true);
|
||||
String normalizedKeyword = keyword == null ? "" : keyword.trim().toLowerCase(java.util.Locale.ROOT);
|
||||
List<Plugin> availablePlugins = pluginService.preparePluginsForCurrentUser(plugins, false, true).stream()
|
||||
.filter(plugin -> matchesKeyword(normalizedKeyword, plugin.getName(), plugin.getDescription()))
|
||||
.toList();
|
||||
List<WorkflowDesignerOptionsView.PluginOption> options = availablePlugins.stream()
|
||||
.map(this::toPluginOption)
|
||||
.toList();
|
||||
@@ -229,6 +234,25 @@ public class WorkflowDesignerOptionService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断任一候选文本是否包含关键字。
|
||||
*
|
||||
* @param keyword 已归一化的小写关键字
|
||||
* @param values 候选文本
|
||||
* @return 空关键字或任一文本命中时返回 {@code true}
|
||||
*/
|
||||
private boolean matchesKeyword(String keyword, String... values) {
|
||||
if (keyword == null || keyword.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
for (String value : values) {
|
||||
if (value != null && value.toLowerCase(java.util.Locale.ROOT).contains(keyword)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询一个插件工具的工作流节点安全配置。
|
||||
*
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.testng.Assert;
|
||||
import org.testng.annotations.Test;
|
||||
import tech.easyflow.ai.entity.Model;
|
||||
import tech.easyflow.ai.mapper.ModelMapper;
|
||||
import tech.easyflow.ai.service.ModelService;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -38,4 +43,23 @@ public class ModelControllerTest {
|
||||
result.getData().get(0).getContextWindowTokens());
|
||||
verify(service).listSelectableModels(query, false, "id", "desc");
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证仅包含空白字符的选择文本不会生成模糊查询条件。
|
||||
*/
|
||||
@Test
|
||||
public void selectLlmShouldIgnoreBlankSearchText() {
|
||||
ModelService service = mock(ModelService.class);
|
||||
ModelMapper mapper = mock(ModelMapper.class);
|
||||
when(service.getMapper()).thenReturn(mapper);
|
||||
when(mapper.selectListWithRelationsByQuery(any(QueryWrapper.class)))
|
||||
.thenReturn(List.of());
|
||||
ModelController controller = new ModelController(service);
|
||||
|
||||
Result<Map<String, List<Model>>> result = controller.selectLlmByProviderAndModelType(
|
||||
"chat", BigInteger.ONE, " ");
|
||||
|
||||
Assert.assertTrue(result.getData().isEmpty());
|
||||
verify(mapper).selectListWithRelationsByQuery(any(QueryWrapper.class));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import tech.easyflow.common.constant.enums.EnumDataStatus;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.tree.TreeNode;
|
||||
import tech.easyflow.system.entity.SysDept;
|
||||
import tech.easyflow.system.mapper.SysDeptMapper;
|
||||
import tech.easyflow.system.service.SysAccountService;
|
||||
@@ -18,17 +19,21 @@ import tech.easyflow.system.service.SysDeptService;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyCollection;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.testng.Assert.assertEquals;
|
||||
import static org.testng.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* {@link SysDeptController} 部门状态保存测试。
|
||||
@@ -190,6 +195,44 @@ public class SysDeptControllerTest {
|
||||
assertEquals(entity.getStatus(), EnumDataStatus.UNAVAILABLE.getCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证名称或编码模糊搜索会保留所有重名部门及其祖先路径。
|
||||
*/
|
||||
@Test
|
||||
public void listShouldKeepAllDuplicateNameMatchesAndAncestors() {
|
||||
SysDeptService service = mock(SysDeptService.class);
|
||||
SysDeptController controller = createController(service);
|
||||
SysDept firstMatch = buildTreeDept(BigInteger.valueOf(3), BigInteger.valueOf(2),
|
||||
"1,2", "交易银行部", "A-01");
|
||||
SysDept secondMatch = buildTreeDept(BigInteger.valueOf(5), BigInteger.valueOf(4),
|
||||
"1,4", "交易银行部", "B-01");
|
||||
List<SysDept> allDepartments = List.of(
|
||||
buildTreeDept(BigInteger.ONE, BigInteger.ZERO, "0", "总行", "ROOT"),
|
||||
buildTreeDept(BigInteger.valueOf(2), BigInteger.ONE, "1", "分行一", "A"),
|
||||
firstMatch,
|
||||
buildTreeDept(BigInteger.valueOf(4), BigInteger.ONE, "1", "分行二", "B"),
|
||||
secondMatch);
|
||||
when(service.list(any(QueryWrapper.class)))
|
||||
.thenReturn(List.of(firstMatch, secondMatch), allDepartments);
|
||||
|
||||
SysDept query = new SysDept();
|
||||
query.setKeyword(" 银行 ");
|
||||
Result<List<SysDept>> result = controller.list(query, true, null, null);
|
||||
|
||||
List<SysDept> flattened = new ArrayList<>();
|
||||
flattenDepartments(result.getData(), flattened);
|
||||
assertEquals(flattened.stream()
|
||||
.filter(department -> "交易银行部".equals(department.getDeptName()))
|
||||
.count(), 2L);
|
||||
assertEquals(flattened.size(), 5);
|
||||
|
||||
ArgumentCaptor<QueryWrapper> queryCaptor = ArgumentCaptor.forClass(QueryWrapper.class);
|
||||
verify(service, times(2)).list(queryCaptor.capture());
|
||||
String ancestorQuerySql = queryCaptor.getAllValues().get(1).toSQL().toUpperCase(Locale.ROOT);
|
||||
assertTrue(ancestorQuerySql.contains("ID") && ancestorQuerySql.contains(" IN "),
|
||||
"祖先节点查询应使用可见部门 ID 集合限制范围: " + ancestorQuerySql);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造待保存的部门。
|
||||
*
|
||||
@@ -217,6 +260,41 @@ public class SysDeptControllerTest {
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造部门树节点。
|
||||
*
|
||||
* @param id 部门主键
|
||||
* @param parentId 父部门主键
|
||||
* @param ancestors 祖先路径
|
||||
* @param deptName 部门名称
|
||||
* @param deptCode 部门编码
|
||||
* @return 部门树节点
|
||||
*/
|
||||
private SysDept buildTreeDept(BigInteger id, BigInteger parentId, String ancestors,
|
||||
String deptName, String deptCode) {
|
||||
SysDept entity = buildDept(id, deptCode);
|
||||
entity.setParentId(parentId);
|
||||
entity.setAncestors(ancestors);
|
||||
entity.setDeptName(deptName);
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将部门树展开为列表。
|
||||
*
|
||||
* @param departments 当前层部门
|
||||
* @param result 展开结果
|
||||
*/
|
||||
private void flattenDepartments(List<? extends TreeNode> departments, List<SysDept> result) {
|
||||
for (TreeNode node : departments) {
|
||||
SysDept department = (SysDept) node;
|
||||
result.add(department);
|
||||
if (department.getChildren() != null) {
|
||||
flattenDepartments(department.getChildren(), result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用新增前置处理。
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user