From 22ceabff9661e18f12557926c649bb1d51272380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=AD=90=E9=BB=98?= <925456043@qq.com> Date: Sun, 29 Mar 2026 17:25:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E6=B5=81=E5=92=8C=E7=9F=A5=E8=AF=86=E5=BA=93=E4=B8=89=E7=BA=A7?= =?UTF-8?q?=E6=9D=83=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 抽取统一资源访问骨架与部门可见范围判断 - 接入工作流和知识库的 READ/MANAGE 权限校验 - 增加可见范围配置与只读态前端交互 --- .../ai/BotDocumentCollectionController.java | 40 +- .../controller/ai/BotWorkflowController.java | 41 +- .../ai/DocumentChunkController.java | 35 ++ .../ai/DocumentCollectionController.java | 117 ++++- .../controller/ai/DocumentController.java | 110 ++++- .../controller/ai/FaqCategoryController.java | 32 ++ .../controller/ai/FaqItemController.java | 78 +++ .../controller/ai/WorkflowController.java | 127 +++++ .../ai/entity/DocumentCollection.java | 7 +- .../tech/easyflow/ai/entity/Workflow.java | 3 +- .../ai/entity/base/DocumentChunkBase.java | 16 + .../entity/base/DocumentCollectionBase.java | 14 + .../easyflow/ai/entity/base/WorkflowBase.java | 14 + ...hunkIdKnowledgeResourceAccessResolver.java | 45 ++ ...mentIdKnowledgeResourceAccessResolver.java | 45 ++ ...goryIdKnowledgeResourceAccessResolver.java | 45 ++ ...ItemIdKnowledgeResourceAccessResolver.java | 45 ++ ...owledgeIdOrSlugResourceAccessResolver.java | 36 ++ .../KnowledgeIdResourceAccessResolver.java | 36 ++ .../KnowledgeReadAccessSnapshot.java | 41 ++ .../KnowledgeVisibilityQueryHelper.java | 101 ++++ ...WorkflowExecKeyResourceAccessResolver.java | 45 ++ .../WorkflowIdResourceAccessResolver.java | 36 ++ .../WorkflowReadAccessSnapshot.java | 41 ++ .../WorkflowVisibilityQueryHelper.java | 101 ++++ .../easyflow/ai/service/ModelService.java | 2 + .../ai/service/impl/ModelServiceImpl.java | 31 ++ .../KnowledgeVisibilityQueryHelperTest.java | 117 +++++ .../easyflow/system/enums/ResourceAction.java | 7 + .../easyflow/system/enums/ResourceLookup.java | 12 + .../system/enums/VisibilityScope.java | 29 ++ .../resource/RequireResourceAccess.java | 25 + .../resource/RequireResourceAccessAspect.java | 86 ++++ .../resource/ResolvedResourceAccess.java | 20 + .../resource/ResourceAccessResolver.java | 11 + .../resource/VisibilityResource.java | 14 + .../system/service/ResourceAccessService.java | 12 + .../system/service/SysDeptService.java | 6 + .../impl/ResourceAccessServiceImpl.java | 65 +++ .../service/impl/SysDeptServiceImpl.java | 40 ++ .../service/impl/SysDeptServiceImplTest.java | 51 ++ .../V10__knowledge_visibility_scope.sql | 6 + .../V9__workflow_visibility_scope.sql | 6 + .../app/src/components/page/CardList.vue | 43 ++ .../src/locales/langs/en-US/aiWorkflow.json | 7 + .../langs/en-US/documentCollection.json | 32 +- .../src/locales/langs/zh-CN/aiWorkflow.json | 7 + .../langs/zh-CN/documentCollection.json | 30 +- .../documentCollection/ChunkDocumentTable.vue | 24 +- .../views/ai/documentCollection/Document.vue | 50 +- .../documentCollection/DocumentCollection.vue | 467 +++++++++++++++++- .../DocumentCollectionDataConfig.vue | 44 +- .../DocumentCollectionModal.vue | 38 +- .../ai/documentCollection/DocumentTable.vue | 24 +- .../views/ai/documentCollection/FaqTable.vue | 105 +++- .../KnowledgeSearchConfig.vue | 37 +- .../src/views/ai/workflow/WorkflowList.vue | 387 ++++++++++++++- .../src/views/ai/workflow/WorkflowModal.vue | 52 +- 58 files changed, 3053 insertions(+), 85 deletions(-) create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/DocumentChunkIdKnowledgeResourceAccessResolver.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/DocumentIdKnowledgeResourceAccessResolver.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/FaqCategoryIdKnowledgeResourceAccessResolver.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/FaqItemIdKnowledgeResourceAccessResolver.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/KnowledgeIdOrSlugResourceAccessResolver.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/KnowledgeIdResourceAccessResolver.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/KnowledgeReadAccessSnapshot.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/KnowledgeVisibilityQueryHelper.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/WorkflowExecKeyResourceAccessResolver.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/WorkflowIdResourceAccessResolver.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/WorkflowReadAccessSnapshot.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/WorkflowVisibilityQueryHelper.java create mode 100644 easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/permission/KnowledgeVisibilityQueryHelperTest.java create mode 100644 easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/enums/ResourceAction.java create mode 100644 easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/enums/ResourceLookup.java create mode 100644 easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/enums/VisibilityScope.java create mode 100644 easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/permission/resource/RequireResourceAccess.java create mode 100644 easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/permission/resource/RequireResourceAccessAspect.java create mode 100644 easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/permission/resource/ResolvedResourceAccess.java create mode 100644 easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/permission/resource/ResourceAccessResolver.java create mode 100644 easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/permission/resource/VisibilityResource.java create mode 100644 easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/ResourceAccessService.java create mode 100644 easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/ResourceAccessServiceImpl.java create mode 100644 easyflow-modules/easyflow-module-system/src/test/java/tech/easyflow/system/service/impl/SysDeptServiceImplTest.java create mode 100644 easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/V10__knowledge_visibility_scope.sql create mode 100644 easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/V9__workflow_visibility_scope.sql diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/BotDocumentCollectionController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/BotDocumentCollectionController.java index 2fecb48..ac50067 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/BotDocumentCollectionController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/BotDocumentCollectionController.java @@ -2,7 +2,11 @@ package tech.easyflow.admin.controller.ai; import org.springframework.web.bind.annotation.PostMapping; import tech.easyflow.ai.entity.BotDocumentCollection; +import tech.easyflow.ai.entity.DocumentCollection; +import tech.easyflow.ai.permission.KnowledgeReadAccessSnapshot; +import tech.easyflow.ai.permission.KnowledgeVisibilityQueryHelper; import tech.easyflow.ai.service.BotDocumentCollectionService; +import tech.easyflow.ai.service.DocumentCollectionService; import tech.easyflow.common.annotation.UsePermission; import tech.easyflow.common.domain.Result; import tech.easyflow.common.web.controller.BaseCurdController; @@ -11,8 +15,13 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import tech.easyflow.common.web.jsonbody.JsonBody; +import tech.easyflow.system.enums.CategoryResourceType; +import tech.easyflow.system.enums.ResourceAction; +import tech.easyflow.system.service.ResourceAccessService; +import javax.annotation.Resource; import java.math.BigInteger; +import java.util.ArrayList; import java.util.List; /** @@ -25,6 +34,13 @@ import java.util.List; @RequestMapping("/api/v1/botKnowledge") @UsePermission(moduleName = "/api/v1/bot") public class BotDocumentCollectionController extends BaseCurdController { + @Resource + private DocumentCollectionService documentCollectionService; + @Resource + private KnowledgeVisibilityQueryHelper knowledgeVisibilityQueryHelper; + @Resource + private ResourceAccessService resourceAccessService; + public BotDocumentCollectionController(BotDocumentCollectionService service) { super(service); } @@ -35,12 +51,32 @@ public class BotDocumentCollectionController extends BaseCurdController botDocumentCollections = service.getMapper().selectListWithRelationsByQuery(queryWrapper); - return Result.ok(botDocumentCollections); + List visibleList = new ArrayList<>(); + KnowledgeReadAccessSnapshot snapshot = knowledgeVisibilityQueryHelper.getCurrentReadSnapshot(); + for (BotDocumentCollection relation : botDocumentCollections) { + DocumentCollection knowledge = relation.getKnowledge(); + if (knowledge == null || knowledgeVisibilityQueryHelper.canRead(knowledge, snapshot)) { + visibleList.add(relation); + } + } + return Result.ok(visibleList); } @PostMapping("updateBotKnowledgeIds") public Result save(@JsonBody("botId") BigInteger botId, @JsonBody("knowledgeIds") BigInteger [] knowledgeIds) { + if (knowledgeIds != null) { + for (BigInteger knowledgeId : knowledgeIds) { + if (knowledgeId == null) { + continue; + } + DocumentCollection collection = documentCollectionService.getById(knowledgeId); + if (collection == null) { + continue; + } + resourceAccessService.assertAccess(CategoryResourceType.KNOWLEDGE, collection, ResourceAction.READ, "无权限绑定知识库"); + } + } service.saveBotAndKnowledge(botId, knowledgeIds); return Result.ok(); } -} \ No newline at end of file +} diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/BotWorkflowController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/BotWorkflowController.java index 4cef75e..66345b2 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/BotWorkflowController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/BotWorkflowController.java @@ -2,7 +2,11 @@ package tech.easyflow.admin.controller.ai; import org.springframework.web.bind.annotation.PostMapping; import tech.easyflow.ai.entity.BotWorkflow; +import tech.easyflow.ai.entity.Workflow; +import tech.easyflow.ai.permission.WorkflowReadAccessSnapshot; +import tech.easyflow.ai.permission.WorkflowVisibilityQueryHelper; import tech.easyflow.ai.service.BotWorkflowService; +import tech.easyflow.ai.service.WorkflowService; import tech.easyflow.common.annotation.UsePermission; import tech.easyflow.common.domain.Result; import tech.easyflow.common.tree.Tree; @@ -12,10 +16,16 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import tech.easyflow.common.web.jsonbody.JsonBody; +import tech.easyflow.system.enums.CategoryResourceType; +import tech.easyflow.system.enums.ResourceAction; +import tech.easyflow.system.service.ResourceAccessService; import java.math.BigInteger; +import java.util.ArrayList; import java.util.List; +import javax.annotation.Resource; + /** * 控制层。 * @@ -26,6 +36,13 @@ import java.util.List; @RequestMapping("/api/v1/botWorkflow") @UsePermission(moduleName = "/api/v1/bot") public class BotWorkflowController extends BaseCurdController { + @Resource + private WorkflowService workflowService; + @Resource + private WorkflowVisibilityQueryHelper workflowVisibilityQueryHelper; + @Resource + private ResourceAccessService resourceAccessService; + public BotWorkflowController(BotWorkflowService service) { super(service); } @@ -36,13 +53,33 @@ public class BotWorkflowController extends BaseCurdController botWorkflows = service.getMapper().selectListWithRelationsByQuery(queryWrapper); - List list = Tree.tryToTree(botWorkflows, asTree); + List visibleList = new ArrayList<>(); + WorkflowReadAccessSnapshot snapshot = workflowVisibilityQueryHelper.getCurrentReadSnapshot(); + for (BotWorkflow botWorkflow : botWorkflows) { + Workflow workflow = botWorkflow.getWorkflow(); + if (workflow == null || workflowVisibilityQueryHelper.canRead(workflow, snapshot)) { + visibleList.add(botWorkflow); + } + } + List list = Tree.tryToTree(visibleList, asTree); return Result.ok(list); } @PostMapping("updateBotWorkflowIds") public Result save(@JsonBody("botId") BigInteger botId, @JsonBody("workflowIds") BigInteger [] workflowIds) { + if (workflowIds != null) { + for (BigInteger workflowId : workflowIds) { + if (workflowId == null) { + continue; + } + Workflow workflow = workflowService.getById(workflowId); + if (workflow == null) { + continue; + } + resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, workflow, ResourceAction.READ, "无权限绑定工作流"); + } + } service.saveBotAndWorkflowTool(botId, workflowIds); return Result.ok(); } -} \ No newline at end of file +} diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/DocumentChunkController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/DocumentChunkController.java index 92d1abf..176a4bc 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/DocumentChunkController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/DocumentChunkController.java @@ -2,6 +2,7 @@ package tech.easyflow.admin.controller.ai; import cn.dev33.satoken.annotation.SaCheckPermission; import com.easyagents.core.model.embedding.EmbeddingModel; +import com.mybatisflex.core.paginate.Page; import tech.easyflow.ai.entity.DocumentChunk; import tech.easyflow.ai.entity.DocumentCollection; import tech.easyflow.ai.entity.Model; @@ -16,9 +17,15 @@ import com.easyagents.core.document.Document; import com.easyagents.core.store.DocumentStore; import com.easyagents.core.store.StoreOptions; import com.easyagents.core.store.StoreResult; +import jakarta.servlet.http.HttpServletRequest; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import tech.easyflow.system.enums.CategoryResourceType; +import tech.easyflow.system.enums.ResourceAction; +import tech.easyflow.system.enums.ResourceLookup; +import tech.easyflow.system.permission.resource.RequireResourceAccess; import javax.annotation.Resource; import java.math.BigInteger; @@ -51,8 +58,29 @@ public class DocumentChunkController extends BaseCurdController> page(HttpServletRequest request, String sortKey, String sortType, Long pageNumber, Long pageSize) { + return super.page(request, sortKey, sortType, pageNumber, pageSize); + } + @PostMapping("update") @SaCheckPermission("/api/v1/documentCollection/save") + @RequireResourceAccess( + resource = CategoryResourceType.KNOWLEDGE, + action = ResourceAction.MANAGE, + lookup = ResourceLookup.DOCUMENT_CHUNK_ID, + idExpr = "#documentChunk.id", + denyMessage = "无权限管理知识库" + ) public Result update(@JsonBody DocumentChunk documentChunk) { boolean success = service.updateById(documentChunk); if (success){ @@ -87,6 +115,13 @@ public class DocumentChunkController extends BaseCurdController remove(@JsonBody(value = "id", required = true) BigInteger chunkId) { DocumentChunk docChunk = documentChunkService.getById(chunkId); if (docChunk == null) { diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/DocumentCollectionController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/DocumentCollectionController.java index 904592c..e9c9a8e 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/DocumentCollectionController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/DocumentCollectionController.java @@ -2,14 +2,19 @@ package tech.easyflow.admin.controller.ai; import cn.dev33.satoken.annotation.SaCheckPermission; import com.easyagents.core.document.Document; +import com.mybatisflex.core.paginate.Page; import com.mybatisflex.core.query.QueryWrapper; import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import tech.easyflow.ai.permission.KnowledgeVisibilityQueryHelper; +import tech.easyflow.ai.documentimport.DocumentImportDtos; import tech.easyflow.ai.entity.BotDocumentCollection; import tech.easyflow.ai.entity.DocumentCollection; +import tech.easyflow.ai.entity.Model; import tech.easyflow.ai.service.BotDocumentCollectionService; import tech.easyflow.ai.service.DocumentChunkService; import tech.easyflow.ai.service.DocumentCollectionService; @@ -17,6 +22,13 @@ import tech.easyflow.ai.service.ModelService; import tech.easyflow.common.domain.Result; import tech.easyflow.common.web.controller.BaseCurdController; import tech.easyflow.common.web.exceptions.BusinessException; +import tech.easyflow.common.web.jsonbody.JsonBody; +import tech.easyflow.system.enums.CategoryResourceType; +import tech.easyflow.system.enums.ResourceAction; +import tech.easyflow.system.enums.ResourceLookup; +import tech.easyflow.system.enums.VisibilityScope; +import tech.easyflow.system.permission.resource.RequireResourceAccess; +import tech.easyflow.system.service.ResourceAccessService; import javax.annotation.Resource; import java.io.Serializable; @@ -41,6 +53,10 @@ public class DocumentCollectionController extends BaseCurdController onSaveOrUpdateBefore(DocumentCollection entity, boolean isSave) { + normalizeVisibilityScope(entity, isSave); + if (!isSave && entity.getId() != null) { + DocumentCollection existed = requireKnowledge(String.valueOf(entity.getId())); + resourceAccessService.assertAccess(CategoryResourceType.KNOWLEDGE, existed, ResourceAction.MANAGE, "无权限管理知识库"); + } String alias = entity.getAlias(); String collectionType = entity.getCollectionType(); @@ -96,6 +117,13 @@ public class DocumentCollectionController extends BaseCurdController> search(@RequestParam BigInteger knowledgeId, @RequestParam String keyword) { return Result.ok(service.search(knowledgeId, keyword)); } @@ -103,6 +131,10 @@ public class DocumentCollectionController extends BaseCurdController onRemoveBefore(Collection ids) { + for (Serializable id : ids) { + DocumentCollection collection = requireKnowledge(String.valueOf(id)); + resourceAccessService.assertAccess(CategoryResourceType.KNOWLEDGE, collection, ResourceAction.MANAGE, "无权限管理知识库"); + } QueryWrapper queryWrapper = QueryWrapper.create(); queryWrapper.in(BotDocumentCollection::getDocumentCollectionId, ids); @@ -116,7 +148,90 @@ public class DocumentCollectionController extends BaseCurdController detail(String id) { - return Result.ok(service.getDetail(id)); + DocumentCollection detail = service.getDetail(id); + return Result.ok(detail); + } + + @GetMapping("modelList") + @SaCheckPermission("/api/v1/documentCollection/query") + public Result> modelList(Model entity, Boolean asTree, String sortKey, String sortType) { + return Result.ok(llmService.listSelectableModels(entity, asTree, sortKey, sortType)); + } + + @PostMapping("splitterProfile/save") + @SaCheckPermission("/api/v1/documentCollection/save") + @RequireResourceAccess( + resource = CategoryResourceType.KNOWLEDGE, + action = ResourceAction.MANAGE, + lookup = ResourceLookup.KNOWLEDGE_ID, + idExpr = "#request.knowledgeId", + denyMessage = "无权限管理知识库" + ) + public Result saveSplitterProfile(@JsonBody DocumentImportDtos.SplitterProfileSaveRequest request) { + if (request.getKnowledgeId() == null) { + throw new BusinessException("知识库ID不能为空"); + } + DocumentCollection collection = service.getById(request.getKnowledgeId()); + if (collection == null) { + throw new BusinessException("知识库不存在"); + } + if (collection.isFaqCollection()) { + throw new BusinessException("FAQ知识库不支持文档导入策略"); + } + Map options = collection.getOptions() == null + ? new HashMap<>() + : new HashMap<>(collection.getOptions()); + options.put(DocumentCollection.KEY_SPLITTER_DEFAULT_STRATEGY, request.getDefaultStrategyCode()); + options.put(DocumentCollection.KEY_SPLITTER_AUTO_RECOMMEND_ENABLED, request.getAutoRecommendEnabled()); + options.put(DocumentCollection.KEY_SPLITTER_FALLBACK_STRATEGY, request.getFallbackStrategyCode()); + options.put(DocumentCollection.KEY_SPLITTER_STRATEGY_PROFILES, request.getStrategyProfiles()); + + DocumentCollection update = new DocumentCollection(); + update.setId(collection.getId()); + update.setOptions(options); + return Result.ok(service.updateById(update)); + } + + @Override + public Result> list(DocumentCollection entity, Boolean asTree, String sortKey, String sortType) { + QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity)); + knowledgeVisibilityQueryHelper.applyReadableAccess(queryWrapper); + queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy())); + return Result.ok(service.list(queryWrapper)); + } + + @Override + protected Page queryPage(Page page, QueryWrapper queryWrapper) { + knowledgeVisibilityQueryHelper.applyReadableAccess(queryWrapper); + return super.queryPage(page, queryWrapper); + } + + private void normalizeVisibilityScope(DocumentCollection entity, boolean isSave) { + if (entity == null) { + return; + } + if (!StringUtils.hasLength(entity.getVisibilityScope())) { + if (isSave) { + entity.setVisibilityScope(VisibilityScope.PRIVATE.name()); + } + return; + } + entity.setVisibilityScope(VisibilityScope.from(entity.getVisibilityScope()).name()); + } + + private DocumentCollection requireKnowledge(String idOrAlias) { + DocumentCollection collection = service.getDetail(idOrAlias); + if (collection == null) { + throw new BusinessException("知识库不存在"); + } + return collection; } } diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/DocumentController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/DocumentController.java index db0d474..537e26e 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/DocumentController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/DocumentController.java @@ -1,13 +1,16 @@ package tech.easyflow.admin.controller.ai; import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.core.io.IoUtil; import com.mybatisflex.core.paginate.Page; import com.mybatisflex.core.query.QueryWrapper; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.ClassPathResource; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; +import tech.easyflow.ai.documentimport.DocumentImportDtos; import tech.easyflow.ai.entity.Document; import tech.easyflow.ai.entity.DocumentCollection; import tech.easyflow.ai.entity.DocumentCollectionSplitParams; @@ -24,11 +27,21 @@ import tech.easyflow.common.util.StringUtil; import tech.easyflow.common.web.controller.BaseCurdController; import tech.easyflow.common.web.exceptions.BusinessException; import tech.easyflow.common.web.jsonbody.JsonBody; +import tech.easyflow.common.filestorage.FileStorageService; +import tech.easyflow.system.enums.CategoryResourceType; +import tech.easyflow.system.enums.ResourceAction; +import tech.easyflow.system.enums.ResourceLookup; +import tech.easyflow.system.permission.resource.RequireResourceAccess; +import tech.easyflow.system.service.ResourceAccessService; +import javax.annotation.Resource; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.io.Serializable; import java.math.BigInteger; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.ArrayList; import java.util.Collections; @@ -58,6 +71,11 @@ public class DocumentController extends BaseCurdController remove(@JsonBody(value = "id", required = true) String id) { + Document document = requireDocument(new BigInteger(id)); + getDocumentCollection(document.getCollectionId().toString(), ResourceAction.MANAGE, "无权限管理知识库"); List ids = Collections.singletonList(id); Result result = onRemoveBefore(ids); if (result != null) return result; @@ -104,7 +124,7 @@ public class DocumentController extends BaseCurdController documentList = documentService.getDocumentList(knowledge.getId().toString(), pageSize, pageNumber,fileName); return Result.ok(documentList); } + @GetMapping("download") + @SaCheckPermission("/api/v1/documentCollection/query") + @RequireResourceAccess( + resource = CategoryResourceType.KNOWLEDGE, + action = ResourceAction.READ, + lookup = ResourceLookup.DOCUMENT_ID, + idExpr = "#documentId", + denyMessage = "无权限访问知识库" + ) + public void download(@RequestParam BigInteger documentId, HttpServletResponse response) throws IOException { + Document document = requireDocument(documentId); + String fileName = resolveDownloadFileName(document); + response.setContentType("application/octet-stream"); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replaceAll("\\+", "%20"); + response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + encodedFileName); + try (InputStream inputStream = storageService.readStream(document.getDocumentPath())) { + IoUtil.copy(inputStream, response.getOutputStream()); + response.flushBuffer(); + } + } + @Override protected String getDefaultOrderBy() { @@ -138,6 +180,11 @@ public class DocumentController extends BaseCurdController update(@JsonBody Document entity) { + if (entity.getId() == null) { + throw new BusinessException("文档不存在"); + } + Document current = requireDocument(entity.getId()); + getDocumentCollection(current.getCollectionId().toString(), ResourceAction.MANAGE, "无权限管理知识库"); super.update(entity); return Result.ok(updatePosition(entity)); } @@ -152,10 +199,40 @@ public class DocumentController extends BaseCurdController analyzeImport(@JsonBody DocumentImportDtos.AnalyzeRequest request) { + if (request.getKnowledgeId() == null) { + throw new BusinessException("知识库id不能为空"); + } + getDocumentCollection(request.getKnowledgeId().toString(), ResourceAction.MANAGE, "无权限管理知识库"); + return documentService.analyzeImport(request); + } + + @PostMapping("import/preview") + @SaCheckPermission("/api/v1/documentCollection/save") + public Result previewImport(@JsonBody DocumentImportDtos.PreviewRequest request) { + if (request.getKnowledgeId() == null) { + throw new BusinessException("知识库id不能为空"); + } + getDocumentCollection(request.getKnowledgeId().toString(), ResourceAction.MANAGE, "无权限管理知识库"); + return documentService.previewImport(request); + } + + @PostMapping("import/commit") + @SaCheckPermission("/api/v1/documentCollection/save") + public Result commitImport(@JsonBody DocumentImportDtos.CommitRequest request) { + if (request.getKnowledgeId() == null) { + throw new BusinessException("知识库id不能为空"); + } + getDocumentCollection(request.getKnowledgeId().toString(), ResourceAction.MANAGE, "无权限管理知识库"); + return documentService.commitImport(request); + } + /** * 更新 entity * @@ -219,17 +296,42 @@ public class DocumentController extends BaseCurdController= 0 ? path.substring(slashIndex + 1) : path; + } + return fileName; + } + } diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/FaqCategoryController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/FaqCategoryController.java index 7712c09..b4ef623 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/FaqCategoryController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/FaqCategoryController.java @@ -12,6 +12,10 @@ import tech.easyflow.common.domain.Result; import tech.easyflow.common.web.controller.BaseCurdController; import tech.easyflow.common.web.exceptions.BusinessException; import tech.easyflow.common.web.jsonbody.JsonBody; +import tech.easyflow.system.enums.CategoryResourceType; +import tech.easyflow.system.enums.ResourceAction; +import tech.easyflow.system.enums.ResourceLookup; +import tech.easyflow.system.permission.resource.RequireResourceAccess; import java.io.Serializable; import java.math.BigInteger; @@ -29,6 +33,13 @@ public class FaqCategoryController extends BaseCurdController> list(FaqCategory entity, Boolean asTree, String sortKey, String sortType) { BigInteger collectionId = entity == null ? null : entity.getCollectionId(); if (collectionId == null) { @@ -40,6 +51,13 @@ public class FaqCategoryController extends BaseCurdController save(@JsonBody FaqCategory entity) { return Result.ok(service.saveCategory(entity)); } @@ -47,6 +65,13 @@ public class FaqCategoryController extends BaseCurdController update(@JsonBody FaqCategory entity) { return Result.ok(service.updateCategory(entity)); } @@ -54,6 +79,13 @@ public class FaqCategoryController extends BaseCurdController remove(@JsonBody(value = "id", required = true) Serializable id) { return Result.ok(service.removeCategory(new BigInteger(String.valueOf(id)))); } diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/FaqItemController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/FaqItemController.java index 4d49bc9..70f1875 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/FaqItemController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/FaqItemController.java @@ -25,6 +25,10 @@ import tech.easyflow.common.vo.UploadResVo; import tech.easyflow.common.web.controller.BaseCurdController; import tech.easyflow.common.web.exceptions.BusinessException; import tech.easyflow.common.web.jsonbody.JsonBody; +import tech.easyflow.system.enums.CategoryResourceType; +import tech.easyflow.system.enums.ResourceAction; +import tech.easyflow.system.enums.ResourceLookup; +import tech.easyflow.system.permission.resource.RequireResourceAccess; import javax.annotation.Resource; import java.io.Serializable; @@ -67,13 +71,31 @@ public class FaqItemController extends BaseCurdController> list(FaqItem entity, Boolean asTree, String sortKey, String sortType) { + BigInteger collectionId = entity == null ? null : entity.getCollectionId(); + if (collectionId == null) { + throw new BusinessException("知识库ID不能为空"); + } return super.list(entity, asTree, sortKey, sortType); } @Override @GetMapping("page") @SaCheckPermission("/api/v1/documentCollection/query") + @RequireResourceAccess( + resource = CategoryResourceType.KNOWLEDGE, + action = ResourceAction.READ, + lookup = ResourceLookup.KNOWLEDGE_ID, + idExpr = "#request.getParameter('collectionId')", + denyMessage = "无权限访问知识库" + ) public Result> page(HttpServletRequest request, String sortKey, String sortType, Long pageNumber, Long pageSize) { if (pageNumber == null || pageNumber < 1) { pageNumber = 1L; @@ -123,6 +145,13 @@ public class FaqItemController extends BaseCurdController detail(String id) { return super.detail(id); } @@ -130,6 +159,13 @@ public class FaqItemController extends BaseCurdController save(@JsonBody FaqItem entity) { return Result.ok(service.saveFaqItem(entity)); } @@ -137,6 +173,13 @@ public class FaqItemController extends BaseCurdController update(@JsonBody FaqItem entity) { return Result.ok(service.updateFaqItem(entity)); } @@ -144,12 +187,26 @@ public class FaqItemController extends BaseCurdController remove(@JsonBody(value = "id", required = true) Serializable id) { return Result.ok(service.removeFaqItem(new java.math.BigInteger(String.valueOf(id)))); } @PostMapping(value = "uploadImage", produces = MediaType.APPLICATION_JSON_VALUE) @SaCheckPermission("/api/v1/documentCollection/save") + @RequireResourceAccess( + resource = CategoryResourceType.KNOWLEDGE, + action = ResourceAction.MANAGE, + lookup = ResourceLookup.KNOWLEDGE_ID, + idExpr = "#collectionId", + denyMessage = "无权限管理知识库" + ) public Result uploadImage(MultipartFile file, BigInteger collectionId) { if (collectionId == null) { throw new BusinessException("知识库ID不能为空"); @@ -180,12 +237,26 @@ public class FaqItemController extends BaseCurdController importExcel(MultipartFile file, BigInteger collectionId) { return Result.ok(service.importFromExcel(collectionId, file)); } @GetMapping("downloadImportTemplate") @SaCheckPermission("/api/v1/documentCollection/query") + @RequireResourceAccess( + resource = CategoryResourceType.KNOWLEDGE, + action = ResourceAction.READ, + lookup = ResourceLookup.KNOWLEDGE_ID, + idExpr = "#collectionId", + denyMessage = "无权限访问知识库" + ) public void downloadImportTemplate(BigInteger collectionId, HttpServletResponse response) throws Exception { if (collectionId == null) { throw new BusinessException("知识库ID不能为空"); @@ -206,6 +277,13 @@ public class FaqItemController extends BaseCurdController singleRun( @JsonBody(value = "workflowId", required = true) BigInteger workflowId, @JsonBody(value = "nodeId", required = true) String nodeId, @@ -96,6 +115,13 @@ public class WorkflowController extends BaseCurdController runAsync(@JsonBody(value = "id", required = true) BigInteger id, @JsonBody("variables") Map variables) { if (variables == null) { @@ -117,6 +143,13 @@ public class WorkflowController extends BaseCurdController getChainStatus(@JsonBody(value = "executeId") String executeId, @JsonBody("nodes") List nodes) { ChainInfo res = tinyFlowService.getChainStatus(executeId, nodes); @@ -128,6 +161,13 @@ public class WorkflowController extends BaseCurdController resume(@JsonBody(value = "executeId", required = true) String executeId, @JsonBody("confirmParams") Map confirmParams) { chainExecutor.resumeAsync(executeId, confirmParams); @@ -137,6 +177,10 @@ public class WorkflowController extends BaseCurdController importWorkFlow(Workflow workflow, MultipartFile jsonFile) throws Exception { + if (workflow.getId() != null) { + Workflow sourceWorkflow = requireWorkflow(String.valueOf(workflow.getId())); + resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, sourceWorkflow, ResourceAction.MANAGE, "无权限管理工作流"); + } InputStream is = jsonFile.getInputStream(); String content = IoUtil.read(is, StandardCharsets.UTF_8); workflow.setContent(content); @@ -147,13 +191,30 @@ public class WorkflowController extends BaseCurdController exportWorkFlow(BigInteger id) { Workflow workflow = service.getById(id); + if (workflow == null) { + throw new BusinessException("工作流不存在"); + } return Result.ok("", workflow.getContent()); } @GetMapping("getRunningParameters") @SaCheckPermission("/api/v1/workflow/query") + @RequireResourceAccess( + resource = CategoryResourceType.WORKFLOW, + action = ResourceAction.READ, + lookup = ResourceLookup.WORKFLOW_ID, + idExpr = "#id", + denyMessage = "无权限访问工作流" + ) public Result getRunningParameters(@RequestParam BigInteger id) { Workflow workflow = service.getById(id); @@ -186,6 +247,10 @@ public class WorkflowController extends BaseCurdController check(@JsonBody("id") BigInteger id, @JsonBody("content") String content, @JsonBody(value = "stage", required = true) String stage) { + if (id != null) { + Workflow workflow = requireWorkflow(String.valueOf(id)); + resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, workflow, ResourceAction.MANAGE, "无权限管理工作流"); + } WorkflowCheckStage checkStage = WorkflowCheckStage.from(stage); WorkflowCheckResult checkResult; if (StringUtils.hasLength(content)) { @@ -199,6 +264,14 @@ public class WorkflowController extends BaseCurdController detail(String id) { Workflow workflow = service.getDetail(id); return Result.ok(workflow); @@ -206,9 +279,19 @@ public class WorkflowController extends BaseCurdController copy(BigInteger id) { LoginAccount account = SaTokenUtil.getLoginAccount(); Workflow workflow = service.getById(id); + if (workflow == null) { + throw new BusinessException("工作流不存在"); + } workflow.setId(null); workflow.setAlias(IdUtil.fastSimpleUUID()); commonFiled(workflow, account.getId(), account.getTenantId(), account.getDeptId()); @@ -218,6 +301,11 @@ public class WorkflowController extends BaseCurdController> list(Workflow entity, Boolean asTree, String sortKey, String sortType) { + QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity)); + workflowVisibilityQueryHelper.applyReadableAccess(queryWrapper); + queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy())); + return Result.ok(service.list(queryWrapper)); + } + + @Override + protected Page queryPage(Page page, QueryWrapper queryWrapper) { + workflowVisibilityQueryHelper.applyReadableAccess(queryWrapper); + return super.queryPage(page, queryWrapper); + } + @Override protected Result onRemoveBefore(Collection ids) { + for (Serializable id : ids) { + Workflow workflow = requireWorkflow(String.valueOf(id)); + resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, workflow, ResourceAction.MANAGE, "无权限管理工作流"); + } QueryWrapper queryWrapper = QueryWrapper.create(); queryWrapper.in("workflow_id", ids); boolean exists = botWorkflowService.exists(queryWrapper); @@ -251,4 +357,25 @@ public class WorkflowController extends BaseCurdController options; + public BigInteger getId() { return id; } @@ -78,4 +86,12 @@ public class DocumentChunkBase implements Serializable { this.sorting = sorting; } + public Map getOptions() { + return options; + } + + public void setOptions(Map options) { + this.options = options; + } + } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/base/DocumentCollectionBase.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/base/DocumentCollectionBase.java index 4098a31..6072589 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/base/DocumentCollectionBase.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/base/DocumentCollectionBase.java @@ -160,6 +160,12 @@ public class DocumentCollectionBase extends DateEntity implements Serializable { @Column(comment = "分类ID") private BigInteger categoryId; + /** + * 可见范围 + */ + @Column(comment = "可见范围") + private String visibilityScope; + public BigInteger getId() { return id; } @@ -352,4 +358,12 @@ public class DocumentCollectionBase extends DateEntity implements Serializable { this.categoryId = categoryId; } + public String getVisibilityScope() { + return visibilityScope; + } + + public void setVisibilityScope(String visibilityScope) { + this.visibilityScope = visibilityScope; + } + } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/base/WorkflowBase.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/base/WorkflowBase.java index b40b93f..56134a9 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/base/WorkflowBase.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/base/WorkflowBase.java @@ -103,6 +103,12 @@ public class WorkflowBase extends DateEntity implements Serializable { @Column(comment = "分类ID") private BigInteger categoryId; + /** + * 可见范围 + */ + @Column(comment = "可见范围") + private String visibilityScope; + public BigInteger getId() { return id; } @@ -223,4 +229,12 @@ public class WorkflowBase extends DateEntity implements Serializable { this.categoryId = categoryId; } + public String getVisibilityScope() { + return visibilityScope; + } + + public void setVisibilityScope(String visibilityScope) { + this.visibilityScope = visibilityScope; + } + } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/DocumentChunkIdKnowledgeResourceAccessResolver.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/DocumentChunkIdKnowledgeResourceAccessResolver.java new file mode 100644 index 0000000..82901af --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/DocumentChunkIdKnowledgeResourceAccessResolver.java @@ -0,0 +1,45 @@ +package tech.easyflow.ai.permission; + +import org.springframework.stereotype.Component; +import tech.easyflow.ai.entity.DocumentChunk; +import tech.easyflow.ai.entity.DocumentCollection; +import tech.easyflow.ai.service.DocumentChunkService; +import tech.easyflow.ai.service.DocumentCollectionService; +import tech.easyflow.common.web.exceptions.BusinessException; +import tech.easyflow.system.enums.CategoryResourceType; +import tech.easyflow.system.enums.ResourceLookup; +import tech.easyflow.system.permission.resource.ResolvedResourceAccess; +import tech.easyflow.system.permission.resource.ResourceAccessResolver; + +import javax.annotation.Resource; + +@Component +public class DocumentChunkIdKnowledgeResourceAccessResolver implements ResourceAccessResolver { + + @Resource + private DocumentChunkService documentChunkService; + + @Resource + private DocumentCollectionService documentCollectionService; + + @Override + public boolean supports(CategoryResourceType resourceType, ResourceLookup lookup) { + return CategoryResourceType.KNOWLEDGE == resourceType && ResourceLookup.DOCUMENT_CHUNK_ID == lookup; + } + + @Override + public ResolvedResourceAccess resolve(Object identifier) { + if (identifier == null) { + throw new BusinessException("文档分段不存在"); + } + DocumentChunk documentChunk = documentChunkService.getById(String.valueOf(identifier)); + if (documentChunk == null) { + throw new BusinessException("文档分段不存在"); + } + DocumentCollection collection = documentCollectionService.getById(documentChunk.getDocumentCollectionId()); + if (collection == null) { + throw new BusinessException("知识库不存在"); + } + return new ResolvedResourceAccess(collection, null); + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/DocumentIdKnowledgeResourceAccessResolver.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/DocumentIdKnowledgeResourceAccessResolver.java new file mode 100644 index 0000000..5a52e7d --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/DocumentIdKnowledgeResourceAccessResolver.java @@ -0,0 +1,45 @@ +package tech.easyflow.ai.permission; + +import org.springframework.stereotype.Component; +import tech.easyflow.ai.entity.Document; +import tech.easyflow.ai.entity.DocumentCollection; +import tech.easyflow.ai.service.DocumentCollectionService; +import tech.easyflow.ai.service.DocumentService; +import tech.easyflow.common.web.exceptions.BusinessException; +import tech.easyflow.system.enums.CategoryResourceType; +import tech.easyflow.system.enums.ResourceLookup; +import tech.easyflow.system.permission.resource.ResolvedResourceAccess; +import tech.easyflow.system.permission.resource.ResourceAccessResolver; + +import javax.annotation.Resource; + +@Component +public class DocumentIdKnowledgeResourceAccessResolver implements ResourceAccessResolver { + + @Resource + private DocumentService documentService; + + @Resource + private DocumentCollectionService documentCollectionService; + + @Override + public boolean supports(CategoryResourceType resourceType, ResourceLookup lookup) { + return CategoryResourceType.KNOWLEDGE == resourceType && ResourceLookup.DOCUMENT_ID == lookup; + } + + @Override + public ResolvedResourceAccess resolve(Object identifier) { + if (identifier == null) { + throw new BusinessException("文档不存在"); + } + Document document = documentService.getById(String.valueOf(identifier)); + if (document == null) { + throw new BusinessException("文档不存在"); + } + DocumentCollection collection = documentCollectionService.getById(document.getCollectionId()); + if (collection == null) { + throw new BusinessException("知识库不存在"); + } + return new ResolvedResourceAccess(collection, null); + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/FaqCategoryIdKnowledgeResourceAccessResolver.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/FaqCategoryIdKnowledgeResourceAccessResolver.java new file mode 100644 index 0000000..5600d20 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/FaqCategoryIdKnowledgeResourceAccessResolver.java @@ -0,0 +1,45 @@ +package tech.easyflow.ai.permission; + +import org.springframework.stereotype.Component; +import tech.easyflow.ai.entity.DocumentCollection; +import tech.easyflow.ai.entity.FaqCategory; +import tech.easyflow.ai.service.DocumentCollectionService; +import tech.easyflow.ai.service.FaqCategoryService; +import tech.easyflow.common.web.exceptions.BusinessException; +import tech.easyflow.system.enums.CategoryResourceType; +import tech.easyflow.system.enums.ResourceLookup; +import tech.easyflow.system.permission.resource.ResolvedResourceAccess; +import tech.easyflow.system.permission.resource.ResourceAccessResolver; + +import javax.annotation.Resource; + +@Component +public class FaqCategoryIdKnowledgeResourceAccessResolver implements ResourceAccessResolver { + + @Resource + private FaqCategoryService faqCategoryService; + + @Resource + private DocumentCollectionService documentCollectionService; + + @Override + public boolean supports(CategoryResourceType resourceType, ResourceLookup lookup) { + return CategoryResourceType.KNOWLEDGE == resourceType && ResourceLookup.FAQ_CATEGORY_ID == lookup; + } + + @Override + public ResolvedResourceAccess resolve(Object identifier) { + if (identifier == null) { + throw new BusinessException("FAQ分类不存在"); + } + FaqCategory faqCategory = faqCategoryService.getById(String.valueOf(identifier)); + if (faqCategory == null) { + throw new BusinessException("FAQ分类不存在"); + } + DocumentCollection collection = documentCollectionService.getById(faqCategory.getCollectionId()); + if (collection == null) { + throw new BusinessException("知识库不存在"); + } + return new ResolvedResourceAccess(collection, null); + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/FaqItemIdKnowledgeResourceAccessResolver.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/FaqItemIdKnowledgeResourceAccessResolver.java new file mode 100644 index 0000000..df73a41 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/FaqItemIdKnowledgeResourceAccessResolver.java @@ -0,0 +1,45 @@ +package tech.easyflow.ai.permission; + +import org.springframework.stereotype.Component; +import tech.easyflow.ai.entity.DocumentCollection; +import tech.easyflow.ai.entity.FaqItem; +import tech.easyflow.ai.service.DocumentCollectionService; +import tech.easyflow.ai.service.FaqItemService; +import tech.easyflow.common.web.exceptions.BusinessException; +import tech.easyflow.system.enums.CategoryResourceType; +import tech.easyflow.system.enums.ResourceLookup; +import tech.easyflow.system.permission.resource.ResolvedResourceAccess; +import tech.easyflow.system.permission.resource.ResourceAccessResolver; + +import javax.annotation.Resource; + +@Component +public class FaqItemIdKnowledgeResourceAccessResolver implements ResourceAccessResolver { + + @Resource + private FaqItemService faqItemService; + + @Resource + private DocumentCollectionService documentCollectionService; + + @Override + public boolean supports(CategoryResourceType resourceType, ResourceLookup lookup) { + return CategoryResourceType.KNOWLEDGE == resourceType && ResourceLookup.FAQ_ITEM_ID == lookup; + } + + @Override + public ResolvedResourceAccess resolve(Object identifier) { + if (identifier == null) { + throw new BusinessException("FAQ不存在"); + } + FaqItem faqItem = faqItemService.getById(String.valueOf(identifier)); + if (faqItem == null) { + throw new BusinessException("FAQ不存在"); + } + DocumentCollection collection = documentCollectionService.getById(faqItem.getCollectionId()); + if (collection == null) { + throw new BusinessException("知识库不存在"); + } + return new ResolvedResourceAccess(collection, null); + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/KnowledgeIdOrSlugResourceAccessResolver.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/KnowledgeIdOrSlugResourceAccessResolver.java new file mode 100644 index 0000000..ad55c65 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/KnowledgeIdOrSlugResourceAccessResolver.java @@ -0,0 +1,36 @@ +package tech.easyflow.ai.permission; + +import org.springframework.stereotype.Component; +import tech.easyflow.ai.entity.DocumentCollection; +import tech.easyflow.ai.service.DocumentCollectionService; +import tech.easyflow.common.web.exceptions.BusinessException; +import tech.easyflow.system.enums.CategoryResourceType; +import tech.easyflow.system.enums.ResourceLookup; +import tech.easyflow.system.permission.resource.ResolvedResourceAccess; +import tech.easyflow.system.permission.resource.ResourceAccessResolver; + +import javax.annotation.Resource; + +@Component +public class KnowledgeIdOrSlugResourceAccessResolver implements ResourceAccessResolver { + + @Resource + private DocumentCollectionService documentCollectionService; + + @Override + public boolean supports(CategoryResourceType resourceType, ResourceLookup lookup) { + return CategoryResourceType.KNOWLEDGE == resourceType && ResourceLookup.KNOWLEDGE_ID_OR_SLUG == lookup; + } + + @Override + public ResolvedResourceAccess resolve(Object identifier) { + if (identifier == null) { + throw new BusinessException("知识库不存在"); + } + DocumentCollection collection = documentCollectionService.getDetail(String.valueOf(identifier)); + if (collection == null) { + throw new BusinessException("知识库不存在"); + } + return new ResolvedResourceAccess(collection, null); + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/KnowledgeIdResourceAccessResolver.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/KnowledgeIdResourceAccessResolver.java new file mode 100644 index 0000000..5513e52 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/KnowledgeIdResourceAccessResolver.java @@ -0,0 +1,36 @@ +package tech.easyflow.ai.permission; + +import org.springframework.stereotype.Component; +import tech.easyflow.ai.entity.DocumentCollection; +import tech.easyflow.ai.service.DocumentCollectionService; +import tech.easyflow.common.web.exceptions.BusinessException; +import tech.easyflow.system.enums.CategoryResourceType; +import tech.easyflow.system.enums.ResourceLookup; +import tech.easyflow.system.permission.resource.ResolvedResourceAccess; +import tech.easyflow.system.permission.resource.ResourceAccessResolver; + +import javax.annotation.Resource; + +@Component +public class KnowledgeIdResourceAccessResolver implements ResourceAccessResolver { + + @Resource + private DocumentCollectionService documentCollectionService; + + @Override + public boolean supports(CategoryResourceType resourceType, ResourceLookup lookup) { + return CategoryResourceType.KNOWLEDGE == resourceType && ResourceLookup.KNOWLEDGE_ID == lookup; + } + + @Override + public ResolvedResourceAccess resolve(Object identifier) { + if (identifier == null) { + throw new BusinessException("知识库不存在"); + } + DocumentCollection collection = documentCollectionService.getById(String.valueOf(identifier)); + if (collection == null) { + throw new BusinessException("知识库不存在"); + } + return new ResolvedResourceAccess(collection, null); + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/KnowledgeReadAccessSnapshot.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/KnowledgeReadAccessSnapshot.java new file mode 100644 index 0000000..0e6f6a6 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/KnowledgeReadAccessSnapshot.java @@ -0,0 +1,41 @@ +package tech.easyflow.ai.permission; + +import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot; + +import java.math.BigInteger; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +public class KnowledgeReadAccessSnapshot { + + private final RoleCategoryAccessSnapshot categoryAccess; + private final Set readableDeptIds; + + public KnowledgeReadAccessSnapshot(RoleCategoryAccessSnapshot categoryAccess, Set readableDeptIds) { + this.categoryAccess = categoryAccess; + this.readableDeptIds = readableDeptIds == null + ? Collections.emptySet() + : Collections.unmodifiableSet(new LinkedHashSet<>(readableDeptIds)); + } + + public BigInteger getAccountId() { + return categoryAccess == null ? null : categoryAccess.getAccountId(); + } + + public boolean isSuperAdmin() { + return categoryAccess != null && categoryAccess.isSuperAdmin(); + } + + public boolean isRestricted() { + return categoryAccess == null || categoryAccess.isRestricted(); + } + + public Set getCategoryIds() { + return categoryAccess == null ? Collections.emptySet() : categoryAccess.getCategoryIds(); + } + + public Set getReadableDeptIds() { + return readableDeptIds; + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/KnowledgeVisibilityQueryHelper.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/KnowledgeVisibilityQueryHelper.java new file mode 100644 index 0000000..f2d6bb8 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/KnowledgeVisibilityQueryHelper.java @@ -0,0 +1,101 @@ +package tech.easyflow.ai.permission; + +import com.mybatisflex.core.query.QueryCondition; +import com.mybatisflex.core.query.QueryWrapper; +import org.springframework.stereotype.Component; +import tech.easyflow.ai.entity.DocumentCollection; +import tech.easyflow.common.entity.LoginAccount; +import tech.easyflow.common.satoken.util.SaTokenUtil; +import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot; +import tech.easyflow.system.enums.CategoryResourceType; +import tech.easyflow.system.enums.VisibilityScope; +import tech.easyflow.system.service.CategoryPermissionService; +import tech.easyflow.system.service.SysDeptService; + +import javax.annotation.Resource; +import java.math.BigInteger; +import java.util.Collections; +import java.util.Set; + +import static tech.easyflow.ai.entity.table.DocumentCollectionTableDef.DOCUMENT_COLLECTION; + +@Component +public class KnowledgeVisibilityQueryHelper { + + @Resource + private CategoryPermissionService categoryPermissionService; + + @Resource + private SysDeptService sysDeptService; + + public KnowledgeReadAccessSnapshot getCurrentReadSnapshot() { + RoleCategoryAccessSnapshot categoryAccess = categoryPermissionService.getCurrentAccess(CategoryResourceType.KNOWLEDGE.getCode()); + if (categoryAccess.isSuperAdmin()) { + return new KnowledgeReadAccessSnapshot(categoryAccess, Collections.emptySet()); + } + LoginAccount loginAccount = SaTokenUtil.getLoginAccount(); + Set deptIds = loginAccount == null + ? Collections.emptySet() + : sysDeptService.getSelfAndAncestorDeptIds(loginAccount.getDeptId()); + return new KnowledgeReadAccessSnapshot(categoryAccess, deptIds); + } + + public void applyReadableAccess(QueryWrapper queryWrapper) { + applyReadableAccess(queryWrapper, getCurrentReadSnapshot()); + } + + public void applyReadableAccess(QueryWrapper queryWrapper, KnowledgeReadAccessSnapshot snapshot) { + if (snapshot.isSuperAdmin()) { + return; + } + BigInteger accountId = snapshot.getAccountId(); + if (accountId == null) { + queryWrapper.eq("id", BigInteger.valueOf(-1)); + return; + } + QueryCondition ownerCondition = DOCUMENT_COLLECTION.CREATED_BY.eq(accountId); + if (snapshot.isRestricted() && snapshot.getCategoryIds().isEmpty()) { + queryWrapper.and(ownerCondition); + return; + } + QueryCondition visibilityCondition = DOCUMENT_COLLECTION.VISIBILITY_SCOPE.eq(VisibilityScope.PUBLIC.name()); + if (!snapshot.getReadableDeptIds().isEmpty()) { + visibilityCondition = visibilityCondition.or( + DOCUMENT_COLLECTION.VISIBILITY_SCOPE.eq(VisibilityScope.DEPT.name()) + .and(DOCUMENT_COLLECTION.DEPT_ID.in(snapshot.getReadableDeptIds())) + ); + } + QueryCondition readableCondition = visibilityCondition; + if (snapshot.isRestricted()) { + readableCondition = DOCUMENT_COLLECTION.CATEGORY_ID.in(snapshot.getCategoryIds()).and(visibilityCondition); + } + queryWrapper.and(ownerCondition.or(readableCondition)); + } + + public boolean canRead(DocumentCollection collection) { + return canRead(collection, getCurrentReadSnapshot()); + } + + public boolean canRead(DocumentCollection collection, KnowledgeReadAccessSnapshot snapshot) { + if (collection == null) { + return false; + } + if (snapshot.isSuperAdmin()) { + return true; + } + BigInteger accountId = snapshot.getAccountId(); + if (accountId != null && accountId.equals(collection.getCreatedBy())) { + return true; + } + if (snapshot.isRestricted() && (collection.getCategoryId() == null || !snapshot.getCategoryIds().contains(collection.getCategoryId()))) { + return false; + } + VisibilityScope scope = VisibilityScope.fromOrDefault(collection.getVisibilityScope(), VisibilityScope.PRIVATE); + if (VisibilityScope.PUBLIC == scope) { + return true; + } + return VisibilityScope.DEPT == scope + && collection.getDeptId() != null + && snapshot.getReadableDeptIds().contains(collection.getDeptId()); + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/WorkflowExecKeyResourceAccessResolver.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/WorkflowExecKeyResourceAccessResolver.java new file mode 100644 index 0000000..80a41ad --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/WorkflowExecKeyResourceAccessResolver.java @@ -0,0 +1,45 @@ +package tech.easyflow.ai.permission; + +import org.springframework.stereotype.Component; +import tech.easyflow.ai.entity.Workflow; +import tech.easyflow.ai.entity.WorkflowExecResult; +import tech.easyflow.ai.service.WorkflowExecResultService; +import tech.easyflow.ai.service.WorkflowService; +import tech.easyflow.common.web.exceptions.BusinessException; +import tech.easyflow.system.enums.CategoryResourceType; +import tech.easyflow.system.enums.ResourceLookup; +import tech.easyflow.system.permission.resource.ResolvedResourceAccess; +import tech.easyflow.system.permission.resource.ResourceAccessResolver; + +import javax.annotation.Resource; + +@Component +public class WorkflowExecKeyResourceAccessResolver implements ResourceAccessResolver { + + @Resource + private WorkflowExecResultService workflowExecResultService; + + @Resource + private WorkflowService workflowService; + + @Override + public boolean supports(CategoryResourceType resourceType, ResourceLookup lookup) { + return CategoryResourceType.WORKFLOW == resourceType && ResourceLookup.EXEC_KEY == lookup; + } + + @Override + public ResolvedResourceAccess resolve(Object identifier) { + if (identifier == null) { + throw new BusinessException("工作流执行记录不存在"); + } + WorkflowExecResult execResult = workflowExecResultService.getByExecKey(String.valueOf(identifier)); + if (execResult == null) { + throw new BusinessException("工作流执行记录不存在"); + } + Workflow workflow = workflowService.getDetail(String.valueOf(execResult.getWorkflowId())); + if (workflow == null) { + throw new BusinessException("工作流不存在"); + } + return new ResolvedResourceAccess(workflow, execResult.getCreatedBy()); + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/WorkflowIdResourceAccessResolver.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/WorkflowIdResourceAccessResolver.java new file mode 100644 index 0000000..227ff9d --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/WorkflowIdResourceAccessResolver.java @@ -0,0 +1,36 @@ +package tech.easyflow.ai.permission; + +import org.springframework.stereotype.Component; +import tech.easyflow.ai.entity.Workflow; +import tech.easyflow.ai.service.WorkflowService; +import tech.easyflow.common.web.exceptions.BusinessException; +import tech.easyflow.system.enums.CategoryResourceType; +import tech.easyflow.system.enums.ResourceLookup; +import tech.easyflow.system.permission.resource.ResolvedResourceAccess; +import tech.easyflow.system.permission.resource.ResourceAccessResolver; + +import javax.annotation.Resource; + +@Component +public class WorkflowIdResourceAccessResolver implements ResourceAccessResolver { + + @Resource + private WorkflowService workflowService; + + @Override + public boolean supports(CategoryResourceType resourceType, ResourceLookup lookup) { + return CategoryResourceType.WORKFLOW == resourceType && ResourceLookup.WORKFLOW_ID == lookup; + } + + @Override + public ResolvedResourceAccess resolve(Object identifier) { + if (identifier == null) { + throw new BusinessException("工作流不存在"); + } + Workflow workflow = workflowService.getDetail(String.valueOf(identifier)); + if (workflow == null) { + throw new BusinessException("工作流不存在"); + } + return new ResolvedResourceAccess(workflow, null); + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/WorkflowReadAccessSnapshot.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/WorkflowReadAccessSnapshot.java new file mode 100644 index 0000000..df28b32 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/WorkflowReadAccessSnapshot.java @@ -0,0 +1,41 @@ +package tech.easyflow.ai.permission; + +import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot; + +import java.math.BigInteger; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +public class WorkflowReadAccessSnapshot { + + private final RoleCategoryAccessSnapshot categoryAccess; + private final Set readableDeptIds; + + public WorkflowReadAccessSnapshot(RoleCategoryAccessSnapshot categoryAccess, Set readableDeptIds) { + this.categoryAccess = categoryAccess; + this.readableDeptIds = readableDeptIds == null + ? Collections.emptySet() + : Collections.unmodifiableSet(new LinkedHashSet<>(readableDeptIds)); + } + + public BigInteger getAccountId() { + return categoryAccess == null ? null : categoryAccess.getAccountId(); + } + + public boolean isSuperAdmin() { + return categoryAccess != null && categoryAccess.isSuperAdmin(); + } + + public boolean isRestricted() { + return categoryAccess == null || categoryAccess.isRestricted(); + } + + public Set getCategoryIds() { + return categoryAccess == null ? Collections.emptySet() : categoryAccess.getCategoryIds(); + } + + public Set getReadableDeptIds() { + return readableDeptIds; + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/WorkflowVisibilityQueryHelper.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/WorkflowVisibilityQueryHelper.java new file mode 100644 index 0000000..119f7c3 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/permission/WorkflowVisibilityQueryHelper.java @@ -0,0 +1,101 @@ +package tech.easyflow.ai.permission; + +import com.mybatisflex.core.query.QueryCondition; +import com.mybatisflex.core.query.QueryWrapper; +import org.springframework.stereotype.Component; +import tech.easyflow.ai.entity.Workflow; +import tech.easyflow.common.entity.LoginAccount; +import tech.easyflow.common.satoken.util.SaTokenUtil; +import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot; +import tech.easyflow.system.enums.CategoryResourceType; +import tech.easyflow.system.enums.VisibilityScope; +import tech.easyflow.system.service.CategoryPermissionService; +import tech.easyflow.system.service.SysDeptService; + +import javax.annotation.Resource; +import java.math.BigInteger; +import java.util.Collections; +import java.util.Set; + +import static tech.easyflow.ai.entity.table.WorkflowTableDef.WORKFLOW; + +@Component +public class WorkflowVisibilityQueryHelper { + + @Resource + private CategoryPermissionService categoryPermissionService; + + @Resource + private SysDeptService sysDeptService; + + public WorkflowReadAccessSnapshot getCurrentReadSnapshot() { + RoleCategoryAccessSnapshot categoryAccess = categoryPermissionService.getCurrentAccess(CategoryResourceType.WORKFLOW.getCode()); + if (categoryAccess.isSuperAdmin()) { + return new WorkflowReadAccessSnapshot(categoryAccess, Collections.emptySet()); + } + LoginAccount loginAccount = SaTokenUtil.getLoginAccount(); + Set deptIds = loginAccount == null + ? Collections.emptySet() + : sysDeptService.getSelfAndAncestorDeptIds(loginAccount.getDeptId()); + return new WorkflowReadAccessSnapshot(categoryAccess, deptIds); + } + + public void applyReadableAccess(QueryWrapper queryWrapper) { + applyReadableAccess(queryWrapper, getCurrentReadSnapshot()); + } + + public void applyReadableAccess(QueryWrapper queryWrapper, WorkflowReadAccessSnapshot snapshot) { + if (snapshot.isSuperAdmin()) { + return; + } + BigInteger accountId = snapshot.getAccountId(); + if (accountId == null) { + queryWrapper.eq("id", BigInteger.valueOf(-1)); + return; + } + QueryCondition ownerCondition = WORKFLOW.CREATED_BY.eq(accountId); + if (snapshot.isRestricted() && snapshot.getCategoryIds().isEmpty()) { + queryWrapper.and(ownerCondition); + return; + } + QueryCondition visibilityCondition = WORKFLOW.VISIBILITY_SCOPE.eq(VisibilityScope.PUBLIC.name()); + if (!snapshot.getReadableDeptIds().isEmpty()) { + visibilityCondition = visibilityCondition.or( + WORKFLOW.VISIBILITY_SCOPE.eq(VisibilityScope.DEPT.name()) + .and(WORKFLOW.DEPT_ID.in(snapshot.getReadableDeptIds())) + ); + } + QueryCondition readableCondition = visibilityCondition; + if (snapshot.isRestricted()) { + readableCondition = WORKFLOW.CATEGORY_ID.in(snapshot.getCategoryIds()).and(visibilityCondition); + } + queryWrapper.and(ownerCondition.or(readableCondition)); + } + + public boolean canRead(Workflow workflow) { + return canRead(workflow, getCurrentReadSnapshot()); + } + + public boolean canRead(Workflow workflow, WorkflowReadAccessSnapshot snapshot) { + if (workflow == null) { + return false; + } + if (snapshot.isSuperAdmin()) { + return true; + } + BigInteger accountId = snapshot.getAccountId(); + if (accountId != null && accountId.equals(workflow.getCreatedBy())) { + return true; + } + if (snapshot.isRestricted() && (workflow.getCategoryId() == null || !snapshot.getCategoryIds().contains(workflow.getCategoryId()))) { + return false; + } + VisibilityScope scope = VisibilityScope.fromOrDefault(workflow.getVisibilityScope(), VisibilityScope.PRIVATE); + if (VisibilityScope.PUBLIC == scope) { + return true; + } + return VisibilityScope.DEPT == scope + && workflow.getDeptId() != null + && snapshot.getReadableDeptIds().contains(workflow.getDeptId()); + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/ModelService.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/ModelService.java index f07d502..cf38ffb 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/ModelService.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/ModelService.java @@ -31,6 +31,8 @@ public interface ModelService extends IService { List listInvokeModels(); + List listSelectableModels(Model entity, Boolean asTree, String sortKey, String sortType); + Model updateInvokeConfig(BigInteger id, String invokeCode, Boolean publishEnabled); List batchUpdateInvokePublishStatus(List ids, Boolean publishEnabled); diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/ModelServiceImpl.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/ModelServiceImpl.java index c10b18a..60a4a8c 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/ModelServiceImpl.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/ModelServiceImpl.java @@ -10,6 +10,7 @@ import com.easyagents.core.model.embedding.EmbeddingModel; import com.easyagents.core.model.rerank.RerankModel; import com.easyagents.core.store.VectorData; import com.mybatisflex.core.query.QueryWrapper; +import com.mybatisflex.core.util.StringUtil; import com.mybatisflex.spring.service.impl.ServiceImpl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,6 +23,9 @@ import tech.easyflow.ai.entity.ModelProvider; import tech.easyflow.ai.mapper.ModelMapper; import tech.easyflow.ai.service.ModelProviderService; import tech.easyflow.ai.service.ModelService; +import tech.easyflow.common.tree.Tree; +import tech.easyflow.common.util.SqlOperatorsUtil; +import tech.easyflow.common.util.SqlUtil; import tech.easyflow.common.web.exceptions.BusinessException; import javax.annotation.Resource; @@ -249,6 +253,18 @@ public class ModelServiceImpl extends ServiceImpl implements .collect(Collectors.toList()); } + @Override + public List listSelectableModels(Model entity, Boolean asTree, String sortKey, String sortType) { + QueryWrapper queryWrapper = QueryWrapper.create( + entity, + entity == null ? com.mybatisflex.core.query.SqlOperators.empty() : SqlOperatorsUtil.build(entity.getClass()) + ); + queryWrapper.orderBy(buildOrderBy(sortKey, sortType)); + List list = Tree.tryToTree(modelMapper.selectListWithRelationsByQuery(queryWrapper), asTree); + list.forEach(this::decorateModelTitle); + return list; + } + @Override public Model updateInvokeConfig(BigInteger id, String invokeCode, Boolean publishEnabled) { Model existing = getModelInstance(id); @@ -281,6 +297,21 @@ public class ModelServiceImpl extends ServiceImpl implements return updatedModels; } + private void decorateModelTitle(Model model) { + if (model == null) { + return; + } + String providerName = Optional.ofNullable(model.getModelProvider()) + .map(ModelProvider::getProviderName) + .orElse("-"); + model.setTitle(providerName + "/" + model.getTitle()); + } + + private String buildOrderBy(String sortKey, String sortType) { + sortKey = StringUtil.camelToUnderline(sortKey); + return SqlUtil.buildOrderBy(sortKey, sortType, "id desc"); + } + private String buildDefaultInvokeCode(String modelName) { if (StrUtil.isBlank(modelName)) { return null; diff --git a/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/permission/KnowledgeVisibilityQueryHelperTest.java b/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/permission/KnowledgeVisibilityQueryHelperTest.java new file mode 100644 index 0000000..3876765 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/permission/KnowledgeVisibilityQueryHelperTest.java @@ -0,0 +1,117 @@ +package tech.easyflow.ai.permission; + +import org.junit.Assert; +import org.junit.Test; +import tech.easyflow.ai.entity.DocumentCollection; +import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot; + +import java.math.BigInteger; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +public class KnowledgeVisibilityQueryHelperTest { + + private final KnowledgeVisibilityQueryHelper helper = new KnowledgeVisibilityQueryHelper(); + + @Test + public void canRead_shouldAllowCreatorForPrivateKnowledge() { + DocumentCollection collection = buildCollection(BigInteger.valueOf(11), BigInteger.valueOf(21), "PRIVATE"); + KnowledgeReadAccessSnapshot snapshot = buildSnapshot( + BigInteger.valueOf(11), + false, + false, + Collections.emptySet(), + Collections.emptySet() + ); + + Assert.assertTrue(helper.canRead(collection, snapshot)); + } + + @Test + public void canRead_shouldRejectWhenCategoryNotMatched() { + DocumentCollection collection = buildCollection(BigInteger.valueOf(11), BigInteger.valueOf(21), "PUBLIC"); + KnowledgeReadAccessSnapshot snapshot = buildSnapshot( + BigInteger.valueOf(12), + false, + false, + setOf(BigInteger.valueOf(99)), + Collections.emptySet() + ); + + Assert.assertFalse(helper.canRead(collection, snapshot)); + } + + @Test + public void canRead_shouldAllowDeptScopedKnowledgeForDescendantUser() { + DocumentCollection collection = buildCollection(BigInteger.valueOf(11), BigInteger.valueOf(21), "DEPT"); + collection.setDeptId(BigInteger.valueOf(3)); + KnowledgeReadAccessSnapshot snapshot = buildSnapshot( + BigInteger.valueOf(12), + false, + false, + setOf(BigInteger.valueOf(21)), + setOf(BigInteger.valueOf(1), BigInteger.valueOf(3), BigInteger.valueOf(9)) + ); + + Assert.assertTrue(helper.canRead(collection, snapshot)); + } + + @Test + public void canRead_shouldRejectDeptScopedKnowledgeWithoutDeptMatch() { + DocumentCollection collection = buildCollection(BigInteger.valueOf(11), BigInteger.valueOf(21), "DEPT"); + collection.setDeptId(BigInteger.valueOf(7)); + KnowledgeReadAccessSnapshot snapshot = buildSnapshot( + BigInteger.valueOf(12), + false, + false, + setOf(BigInteger.valueOf(21)), + setOf(BigInteger.valueOf(1), BigInteger.valueOf(3), BigInteger.valueOf(9)) + ); + + Assert.assertFalse(helper.canRead(collection, snapshot)); + } + + @Test + public void canRead_shouldAllowPublicKnowledgeWhenCategoryMatched() { + DocumentCollection collection = buildCollection(BigInteger.valueOf(11), BigInteger.valueOf(21), "PUBLIC"); + KnowledgeReadAccessSnapshot snapshot = buildSnapshot( + BigInteger.valueOf(12), + false, + false, + setOf(BigInteger.valueOf(21)), + Collections.emptySet() + ); + + Assert.assertTrue(helper.canRead(collection, snapshot)); + } + + private DocumentCollection buildCollection(BigInteger createdBy, BigInteger categoryId, String visibilityScope) { + DocumentCollection collection = new DocumentCollection(); + collection.setCreatedBy(createdBy); + collection.setCategoryId(categoryId); + collection.setVisibilityScope(visibilityScope); + return collection; + } + + private KnowledgeReadAccessSnapshot buildSnapshot(BigInteger accountId, + boolean superAdmin, + boolean allAccess, + Set categoryIds, + Set deptIds) { + RoleCategoryAccessSnapshot accessSnapshot = new RoleCategoryAccessSnapshot( + "KNOWLEDGE", + accountId, + superAdmin, + allAccess, + categoryIds + ); + return new KnowledgeReadAccessSnapshot(accessSnapshot, deptIds); + } + + private Set setOf(BigInteger... values) { + Set result = new LinkedHashSet<>(); + Collections.addAll(result, values); + return result; + } +} diff --git a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/enums/ResourceAction.java b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/enums/ResourceAction.java new file mode 100644 index 0000000..ef5d2af --- /dev/null +++ b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/enums/ResourceAction.java @@ -0,0 +1,7 @@ +package tech.easyflow.system.enums; + +public enum ResourceAction { + READ, + USE, + MANAGE +} diff --git a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/enums/ResourceLookup.java b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/enums/ResourceLookup.java new file mode 100644 index 0000000..386c056 --- /dev/null +++ b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/enums/ResourceLookup.java @@ -0,0 +1,12 @@ +package tech.easyflow.system.enums; + +public enum ResourceLookup { + WORKFLOW_ID, + EXEC_KEY, + KNOWLEDGE_ID, + KNOWLEDGE_ID_OR_SLUG, + DOCUMENT_ID, + DOCUMENT_CHUNK_ID, + FAQ_ITEM_ID, + FAQ_CATEGORY_ID +} diff --git a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/enums/VisibilityScope.java b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/enums/VisibilityScope.java new file mode 100644 index 0000000..96e3d3a --- /dev/null +++ b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/enums/VisibilityScope.java @@ -0,0 +1,29 @@ +package tech.easyflow.system.enums; + +import java.util.Arrays; +import java.util.Locale; + +public enum VisibilityScope { + + PRIVATE, + DEPT, + PUBLIC; + + public static VisibilityScope from(String code) { + if (code == null || code.isBlank()) { + throw new IllegalArgumentException("visibilityScope不能为空"); + } + String normalized = code.trim().toUpperCase(Locale.ROOT); + return Arrays.stream(values()) + .filter(item -> item.name().equals(normalized)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("不支持的visibilityScope: " + code)); + } + + public static VisibilityScope fromOrDefault(String code, VisibilityScope defaultValue) { + if (code == null || code.isBlank()) { + return defaultValue; + } + return from(code); + } +} diff --git a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/permission/resource/RequireResourceAccess.java b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/permission/resource/RequireResourceAccess.java new file mode 100644 index 0000000..25728b4 --- /dev/null +++ b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/permission/resource/RequireResourceAccess.java @@ -0,0 +1,25 @@ +package tech.easyflow.system.permission.resource; + +import tech.easyflow.system.enums.CategoryResourceType; +import tech.easyflow.system.enums.ResourceAction; +import tech.easyflow.system.enums.ResourceLookup; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface RequireResourceAccess { + + CategoryResourceType resource(); + + ResourceAction action(); + + ResourceLookup lookup(); + + String idExpr(); + + String denyMessage() default "无权限访问该资源"; +} diff --git a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/permission/resource/RequireResourceAccessAspect.java b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/permission/resource/RequireResourceAccessAspect.java new file mode 100644 index 0000000..9206080 --- /dev/null +++ b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/permission/resource/RequireResourceAccessAspect.java @@ -0,0 +1,86 @@ +package tech.easyflow.system.permission.resource; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.context.expression.MethodBasedEvaluationContext; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.stereotype.Component; +import tech.easyflow.common.entity.LoginAccount; +import tech.easyflow.common.satoken.util.SaTokenUtil; +import tech.easyflow.common.web.exceptions.BusinessException; +import tech.easyflow.system.enums.CategoryResourceType; +import tech.easyflow.system.enums.ResourceLookup; +import tech.easyflow.system.service.CategoryPermissionService; +import tech.easyflow.system.service.ResourceAccessService; + +import java.lang.reflect.Method; +import java.util.List; + +@Aspect +@Component +public class RequireResourceAccessAspect { + + private final List resolvers; + private final ResourceAccessService resourceAccessService; + private final CategoryPermissionService categoryPermissionService; + private final ExpressionParser expressionParser = new SpelExpressionParser(); + private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); + + public RequireResourceAccessAspect(List resolvers, + ResourceAccessService resourceAccessService, + CategoryPermissionService categoryPermissionService) { + this.resolvers = resolvers; + this.resourceAccessService = resourceAccessService; + this.categoryPermissionService = categoryPermissionService; + } + + @Around("@annotation(requireResourceAccess)") + public Object doAround(ProceedingJoinPoint joinPoint, RequireResourceAccess requireResourceAccess) throws Throwable { + Object identifier = resolveIdentifier(joinPoint, requireResourceAccess); + ResourceAccessResolver resolver = findResolver(requireResourceAccess.resource(), requireResourceAccess.lookup()); + ResolvedResourceAccess resolved = resolver.resolve(identifier); + assertExecutionOwner(resolved); + resourceAccessService.assertAccess( + requireResourceAccess.resource(), + resolved.getResource(), + requireResourceAccess.action(), + requireResourceAccess.denyMessage() + ); + return joinPoint.proceed(); + } + + private Object resolveIdentifier(ProceedingJoinPoint joinPoint, RequireResourceAccess requireResourceAccess) { + Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); + MethodBasedEvaluationContext context = new MethodBasedEvaluationContext( + joinPoint.getTarget(), + method, + joinPoint.getArgs(), + parameterNameDiscoverer + ); + return expressionParser.parseExpression(requireResourceAccess.idExpr()).getValue(context); + } + + private ResourceAccessResolver findResolver(CategoryResourceType resourceType, ResourceLookup lookup) { + return resolvers.stream() + .filter(item -> item.supports(resourceType, lookup)) + .findFirst() + .orElseThrow(() -> new IllegalStateException("未找到资源访问解析器: " + resourceType + "/" + lookup)); + } + + private void assertExecutionOwner(ResolvedResourceAccess resolved) { + String executionOwnerKey = resolved.getExecutionOwnerKey(); + if (executionOwnerKey == null || executionOwnerKey.isBlank() || categoryPermissionService.isCurrentSuperAdmin()) { + return; + } + LoginAccount loginAccount = SaTokenUtil.getLoginAccount(); + String accountId = loginAccount == null || loginAccount.getId() == null ? null : loginAccount.getId().toString(); + if (!executionOwnerKey.equals(accountId)) { + throw new BusinessException("无权限访问该执行记录"); + } + } +} diff --git a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/permission/resource/ResolvedResourceAccess.java b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/permission/resource/ResolvedResourceAccess.java new file mode 100644 index 0000000..4e183de --- /dev/null +++ b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/permission/resource/ResolvedResourceAccess.java @@ -0,0 +1,20 @@ +package tech.easyflow.system.permission.resource; + +public class ResolvedResourceAccess { + + private final VisibilityResource resource; + private final String executionOwnerKey; + + public ResolvedResourceAccess(VisibilityResource resource, String executionOwnerKey) { + this.resource = resource; + this.executionOwnerKey = executionOwnerKey; + } + + public VisibilityResource getResource() { + return resource; + } + + public String getExecutionOwnerKey() { + return executionOwnerKey; + } +} diff --git a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/permission/resource/ResourceAccessResolver.java b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/permission/resource/ResourceAccessResolver.java new file mode 100644 index 0000000..11ffdc4 --- /dev/null +++ b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/permission/resource/ResourceAccessResolver.java @@ -0,0 +1,11 @@ +package tech.easyflow.system.permission.resource; + +import tech.easyflow.system.enums.CategoryResourceType; +import tech.easyflow.system.enums.ResourceLookup; + +public interface ResourceAccessResolver { + + boolean supports(CategoryResourceType resourceType, ResourceLookup lookup); + + ResolvedResourceAccess resolve(Object identifier); +} diff --git a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/permission/resource/VisibilityResource.java b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/permission/resource/VisibilityResource.java new file mode 100644 index 0000000..e52f697 --- /dev/null +++ b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/permission/resource/VisibilityResource.java @@ -0,0 +1,14 @@ +package tech.easyflow.system.permission.resource; + +import java.math.BigInteger; + +public interface VisibilityResource { + + BigInteger getCreatedBy(); + + BigInteger getDeptId(); + + BigInteger getCategoryId(); + + String getVisibilityScope(); +} diff --git a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/ResourceAccessService.java b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/ResourceAccessService.java new file mode 100644 index 0000000..29a5cbb --- /dev/null +++ b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/ResourceAccessService.java @@ -0,0 +1,12 @@ +package tech.easyflow.system.service; + +import tech.easyflow.system.enums.CategoryResourceType; +import tech.easyflow.system.enums.ResourceAction; +import tech.easyflow.system.permission.resource.VisibilityResource; + +public interface ResourceAccessService { + + boolean canAccess(CategoryResourceType resourceType, VisibilityResource resource, ResourceAction action); + + void assertAccess(CategoryResourceType resourceType, VisibilityResource resource, ResourceAction action, String message); +} diff --git a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/SysDeptService.java b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/SysDeptService.java index cd85bf1..c6938aa 100644 --- a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/SysDeptService.java +++ b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/SysDeptService.java @@ -3,6 +3,9 @@ package tech.easyflow.system.service; import tech.easyflow.system.entity.SysDept; import com.mybatisflex.core.service.IService; +import java.math.BigInteger; +import java.util.Set; + /** * 部门表 服务层。 * @@ -11,4 +14,7 @@ import com.mybatisflex.core.service.IService; */ public interface SysDeptService extends IService { + Set getSelfAndAncestorDeptIds(BigInteger currentDeptId); + + boolean canUserAccessDeptScopedResource(BigInteger currentDeptId, BigInteger resourceDeptId); } diff --git a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/ResourceAccessServiceImpl.java b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/ResourceAccessServiceImpl.java new file mode 100644 index 0000000..e88a414 --- /dev/null +++ b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/ResourceAccessServiceImpl.java @@ -0,0 +1,65 @@ +package tech.easyflow.system.service.impl; + +import org.springframework.stereotype.Service; +import tech.easyflow.common.entity.LoginAccount; +import tech.easyflow.common.satoken.util.SaTokenUtil; +import tech.easyflow.common.web.exceptions.BusinessException; +import tech.easyflow.system.enums.CategoryResourceType; +import tech.easyflow.system.enums.ResourceAction; +import tech.easyflow.system.enums.VisibilityScope; +import tech.easyflow.system.permission.resource.VisibilityResource; +import tech.easyflow.system.service.CategoryPermissionService; +import tech.easyflow.system.service.ResourceAccessService; +import tech.easyflow.system.service.SysDeptService; + +import javax.annotation.Resource; +import java.math.BigInteger; + +@Service +public class ResourceAccessServiceImpl implements ResourceAccessService { + + @Resource + private CategoryPermissionService categoryPermissionService; + + @Resource + private SysDeptService sysDeptService; + + @Override + public boolean canAccess(CategoryResourceType resourceType, VisibilityResource resource, ResourceAction action) { + if (resource == null) { + return false; + } + LoginAccount loginAccount = SaTokenUtil.getLoginAccount(); + if (loginAccount == null || loginAccount.getId() == null) { + return false; + } + BigInteger accountId = loginAccount.getId(); + if (categoryPermissionService.isCurrentSuperAdmin()) { + return true; + } + if (accountId.equals(resource.getCreatedBy())) { + return true; + } + if (ResourceAction.MANAGE == action) { + return false; + } + if (!categoryPermissionService.canAccessCategory(resourceType.getCode(), resource.getCreatedBy(), resource.getCategoryId())) { + return false; + } + VisibilityScope scope = VisibilityScope.fromOrDefault(resource.getVisibilityScope(), VisibilityScope.PRIVATE); + if (VisibilityScope.PUBLIC == scope) { + return true; + } + if (VisibilityScope.DEPT == scope) { + return sysDeptService.canUserAccessDeptScopedResource(loginAccount.getDeptId(), resource.getDeptId()); + } + return false; + } + + @Override + public void assertAccess(CategoryResourceType resourceType, VisibilityResource resource, ResourceAction action, String message) { + if (!canAccess(resourceType, resource, action)) { + throw new BusinessException(message == null ? "无权限访问该资源" : message); + } + } +} diff --git a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysDeptServiceImpl.java b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysDeptServiceImpl.java index 7bdba9c..0485f8a 100644 --- a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysDeptServiceImpl.java +++ b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysDeptServiceImpl.java @@ -6,6 +6,11 @@ import tech.easyflow.system.service.SysDeptService; import com.mybatisflex.spring.service.impl.ServiceImpl; import org.springframework.stereotype.Service; +import java.math.BigInteger; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + /** * 部门表 服务层实现。 * @@ -15,4 +20,39 @@ import org.springframework.stereotype.Service; @Service public class SysDeptServiceImpl extends ServiceImpl implements SysDeptService { + @Override + public Set getSelfAndAncestorDeptIds(BigInteger currentDeptId) { + if (currentDeptId == null) { + return Collections.emptySet(); + } + SysDept currentDept = getById(currentDeptId); + if (currentDept == null) { + return Collections.emptySet(); + } + Set deptIds = new LinkedHashSet<>(); + String ancestors = currentDept.getAncestors(); + if (ancestors != null && !ancestors.isBlank()) { + String[] items = ancestors.split(","); + for (String item : items) { + if (item == null || item.isBlank()) { + continue; + } + BigInteger deptId = new BigInteger(item.trim()); + if (BigInteger.ZERO.equals(deptId)) { + continue; + } + deptIds.add(deptId); + } + } + deptIds.add(currentDeptId); + return deptIds; + } + + @Override + public boolean canUserAccessDeptScopedResource(BigInteger currentDeptId, BigInteger resourceDeptId) { + if (currentDeptId == null || resourceDeptId == null) { + return false; + } + return getSelfAndAncestorDeptIds(currentDeptId).contains(resourceDeptId); + } } diff --git a/easyflow-modules/easyflow-module-system/src/test/java/tech/easyflow/system/service/impl/SysDeptServiceImplTest.java b/easyflow-modules/easyflow-module-system/src/test/java/tech/easyflow/system/service/impl/SysDeptServiceImplTest.java new file mode 100644 index 0000000..f29bf0e --- /dev/null +++ b/easyflow-modules/easyflow-module-system/src/test/java/tech/easyflow/system/service/impl/SysDeptServiceImplTest.java @@ -0,0 +1,51 @@ +package tech.easyflow.system.service.impl; + +import org.junit.Assert; +import org.junit.Test; +import tech.easyflow.system.entity.SysDept; + +import java.io.Serializable; +import java.math.BigInteger; +import java.util.Set; + +public class SysDeptServiceImplTest { + + @Test + public void shouldReturnSelfAndAncestors() { + SysDept dept = new SysDept(); + dept.setId(BigInteger.valueOf(5)); + dept.setAncestors("0,1,3"); + + SysDeptServiceImpl service = new SysDeptServiceImpl() { + @Override + public SysDept getById(Serializable id) { + return dept; + } + }; + + Set deptIds = service.getSelfAndAncestorDeptIds(BigInteger.valueOf(5)); + Assert.assertTrue(deptIds.contains(BigInteger.valueOf(1))); + Assert.assertTrue(deptIds.contains(BigInteger.valueOf(3))); + Assert.assertTrue(deptIds.contains(BigInteger.valueOf(5))); + Assert.assertFalse(deptIds.contains(BigInteger.ZERO)); + } + + @Test + public void shouldMatchDeptScopedResourceByAncestorChain() { + SysDept dept = new SysDept(); + dept.setId(BigInteger.valueOf(9)); + dept.setAncestors("1,5"); + + SysDeptServiceImpl service = new SysDeptServiceImpl() { + @Override + public SysDept getById(Serializable id) { + return dept; + } + }; + + Assert.assertTrue(service.canUserAccessDeptScopedResource(BigInteger.valueOf(9), BigInteger.valueOf(1))); + Assert.assertTrue(service.canUserAccessDeptScopedResource(BigInteger.valueOf(9), BigInteger.valueOf(5))); + Assert.assertTrue(service.canUserAccessDeptScopedResource(BigInteger.valueOf(9), BigInteger.valueOf(9))); + Assert.assertFalse(service.canUserAccessDeptScopedResource(BigInteger.valueOf(9), BigInteger.valueOf(7))); + } +} diff --git a/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/V10__knowledge_visibility_scope.sql b/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/V10__knowledge_visibility_scope.sql new file mode 100644 index 0000000..5c09198 --- /dev/null +++ b/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/V10__knowledge_visibility_scope.sql @@ -0,0 +1,6 @@ +ALTER TABLE tb_document_collection + ADD COLUMN visibility_scope VARCHAR(32) NULL COMMENT '可见范围'; + +UPDATE tb_document_collection +SET visibility_scope = 'PUBLIC' +WHERE visibility_scope IS NULL OR visibility_scope = ''; diff --git a/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/V9__workflow_visibility_scope.sql b/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/V9__workflow_visibility_scope.sql new file mode 100644 index 0000000..822335c --- /dev/null +++ b/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/V9__workflow_visibility_scope.sql @@ -0,0 +1,6 @@ +ALTER TABLE tb_workflow + ADD COLUMN visibility_scope VARCHAR(32) NULL COMMENT '可见范围'; + +UPDATE tb_workflow +SET visibility_scope = 'PUBLIC' +WHERE visibility_scope IS NULL OR visibility_scope = ''; diff --git a/easyflow-ui-admin/app/src/components/page/CardList.vue b/easyflow-ui-admin/app/src/components/page/CardList.vue index 0e97ef4..7b6e09a 100644 --- a/easyflow-ui-admin/app/src/components/page/CardList.vue +++ b/easyflow-ui-admin/app/src/components/page/CardList.vue @@ -46,6 +46,9 @@ export interface CardListProps { titleField?: string; descField?: string; actions?: ActionButton[]; + cornerTagField?: string; + cornerTagMap?: Record; + cornerTagTypeMap?: Record; defaultIcon: any; data: any[]; primaryAction?: CardPrimaryAction; @@ -58,6 +61,9 @@ const props = withDefaults(defineProps(), { titleField: 'title', descField: 'description', actions: () => [], + cornerTagField: '', + cornerTagMap: () => ({}), + cornerTagTypeMap: () => ({}), primaryAction: undefined, tagField: '', tagMap: () => ({}), @@ -154,6 +160,23 @@ function handleActionClick(event: Event, action: ActionButton, item: any) { {{ item[descField] }} +
+ + + {{ + cornerTagMap[item[cornerTagField]] || item[cornerTagField] + }} + + +
@@ -343,6 +366,26 @@ function handleActionClick(event: Event, action: ActionButton, item: any) { color: hsl(var(--text-muted)); } +.card-corner-tag { + display: flex; + flex-shrink: 0; + align-items: flex-start; + justify-content: flex-end; + min-height: 28px; + margin-left: auto; +} + +.card-corner-tag :deep(.el-tag) { + --el-tag-border-radius: 999px; + --el-tag-font-size: 12px; + --el-tag-border-color: transparent; + + padding: 0 10px; + font-weight: 600; + letter-spacing: 0.01em; + backdrop-filter: blur(6px); +} + .card-footer { display: flex; gap: 12px; diff --git a/easyflow-ui-admin/app/src/locales/langs/en-US/aiWorkflow.json b/easyflow-ui-admin/app/src/locales/langs/en-US/aiWorkflow.json index 764394f..d1d859b 100644 --- a/easyflow-ui-admin/app/src/locales/langs/en-US/aiWorkflow.json +++ b/easyflow-ui-admin/app/src/locales/langs/en-US/aiWorkflow.json @@ -14,6 +14,13 @@ "englishName": "EnglishName", "status": "ShowInUserCenter", "categoryId": "Category", + "visibilityScope": "Visibility Scope", + "visibilityScopePrivate": "Personal", + "visibilityScopePrivateDesc": "Only the creator can access it", + "visibilityScopeDept": "Dept", + "visibilityScopeDeptDesc": "Available to the dept and descendants", + "visibilityScopePublic": "Public", + "visibilityScopePublicDesc": "Available to internal users matched by category", "params": "Params", "steps": "Steps", "result": "Result", diff --git a/easyflow-ui-admin/app/src/locales/langs/en-US/documentCollection.json b/easyflow-ui-admin/app/src/locales/langs/en-US/documentCollection.json index 0ce26d9..dab2314 100644 --- a/easyflow-ui-admin/app/src/locales/langs/en-US/documentCollection.json +++ b/easyflow-ui-admin/app/src/locales/langs/en-US/documentCollection.json @@ -10,6 +10,13 @@ "collectionTypeDocument": "Document", "collectionTypeFaq": "FAQ", "description": "Description", + "visibilityScope": "Visibility Scope", + "visibilityScopePrivate": "Personal", + "visibilityScopePrivateDesc": "Only the creator can access it", + "visibilityScopeDept": "Dept", + "visibilityScopeDeptDesc": "Available to the dept and descendants", + "visibilityScopePublic": "Public", + "visibilityScopePublicDesc": "Available to internal users matched by category", "slug": "Slug", "vectorStoreEnable": "VectorStoreEnable", "vectorStoreType": "VectorStoreType", @@ -46,12 +53,24 @@ }, "importDoc": { "fileUpload": "File upload", - "parameterSettings": "ParameterSettings", + "parameterSettings": "Parameter settings", + "strategyAnalysis": "Strategy analysis", "segmentedPreview": "SegmentedPreview", "confirmImport": "ConfirmImport", "fileName": "File Name", "progressUpload": "Progress of file upload", - "fileSize": "File size" + "fileSize": "File size", + "analysisTip": "The system analyzes multilingual structure first and recommends a splitting strategy. You can still adjust each file manually.", + "confidence": "Confidence", + "recommendReason": "Reasons", + "candidateStrategies": "Candidates", + "strategySelection": "Strategy", + "previewTip": "The preview result is the final import basis. Confirm it before committing.", + "previewEmpty": "No preview data", + "warningCount": "Warnings", + "chunkCount": "Chunks", + "resultEmpty": "No import result", + "importFailed": "Import failed" }, "splitterDoc": { "fileType": "FileType", @@ -64,6 +83,12 @@ "simpleTokenizeSplitter": "SimpleTokenizeSplitter", "regexDocumentSplitter": "RegexDocumentSplitter", "markdownHeaderSplitter": "MarkdownHeaderSplitter", + "autoStrategy": "Auto recommendation", + "markdownSection": "Markdown headings", + "outlineSection": "Outline sections", + "qaPair": "Q&A pairs", + "paragraphLength": "Paragraph length", + "customRegex": "Custom regex", "mdSplitterLevel": "MarkdownSplitterLevel", "uploadStatus": "UploadStatus", "pendingUpload": "PendingUpload", @@ -139,5 +164,6 @@ "tencentCloud": "tencentCloud", "vectorEmbedModelTips": "After successful vector data, it is not allowed to modify the vector model", "dimensionOfVectorModelTips": "After successful vector data, it is not allowed to modify the dimensions of the vector model", - "dimensionOfVectorModel": "Dimension of vector model" + "dimensionOfVectorModel": "Dimension of vector model", + "managePermissionHint": "Only the creator or super admin can modify this knowledge base" } diff --git a/easyflow-ui-admin/app/src/locales/langs/zh-CN/aiWorkflow.json b/easyflow-ui-admin/app/src/locales/langs/zh-CN/aiWorkflow.json index a386268..c29f0ea 100644 --- a/easyflow-ui-admin/app/src/locales/langs/zh-CN/aiWorkflow.json +++ b/easyflow-ui-admin/app/src/locales/langs/zh-CN/aiWorkflow.json @@ -14,6 +14,13 @@ "englishName": "英文名称", "status": "在用户中心显示", "categoryId": "分类", + "visibilityScope": "可见范围", + "visibilityScopePrivate": "个人", + "visibilityScopePrivateDesc": "仅创建者可访问", + "visibilityScopeDept": "部门", + "visibilityScopeDeptDesc": "本部门及下级部门可访问", + "visibilityScopePublic": "公开", + "visibilityScopePublicDesc": "分类命中的内部用户可访问", "params": "执行参数", "steps": "执行步骤", "result": "执行结果", diff --git a/easyflow-ui-admin/app/src/locales/langs/zh-CN/documentCollection.json b/easyflow-ui-admin/app/src/locales/langs/zh-CN/documentCollection.json index a99a814..8be603f 100644 --- a/easyflow-ui-admin/app/src/locales/langs/zh-CN/documentCollection.json +++ b/easyflow-ui-admin/app/src/locales/langs/zh-CN/documentCollection.json @@ -10,6 +10,13 @@ "collectionTypeDocument": "文档", "collectionTypeFaq": "FAQ", "description": "描述", + "visibilityScope": "可见范围", + "visibilityScopePrivate": "个人", + "visibilityScopePrivateDesc": "仅创建者可访问", + "visibilityScopeDept": "部门", + "visibilityScopeDeptDesc": "本部门及下级部门可访问", + "visibilityScopePublic": "公开", + "visibilityScopePublicDesc": "分类命中的内部用户可访问", "slug": "URL 别名", "vectorStoreEnable": "是否启用向量数据库", "vectorStoreType": "向量数据库类型", @@ -47,11 +54,23 @@ "importDoc": { "fileUpload": "文件上传", "parameterSettings": "参数设置", + "strategyAnalysis": "策略分析", "segmentedPreview": "分段预览", "confirmImport": "确认导入", "fileName": "文件名称", "progressUpload": "文件上传进度", - "fileSize": "文件大小" + "fileSize": "文件大小", + "analysisTip": "系统会先基于文档结构做中英文规则分析,再推荐拆分策略,你也可以逐个文件手动调整。", + "confidence": "置信度", + "recommendReason": "推荐理由", + "candidateStrategies": "备选策略", + "strategySelection": "拆分策略", + "previewTip": "预览结果就是最终入库依据,确认无误后再执行导入。", + "previewEmpty": "暂无可预览内容", + "warningCount": "警告数", + "chunkCount": "分块数", + "resultEmpty": "暂无导入结果", + "importFailed": "导入失败" }, "splitterDoc": { "fileType": "文件类型", @@ -64,6 +83,12 @@ "simpleTokenizeSplitter": "简单分词器", "regexDocumentSplitter": "正则文档分割器", "markdownHeaderSplitter": "Markdown标题层级拆分器", + "autoStrategy": "自动推荐", + "markdownSection": "Markdown 标题拆分", + "outlineSection": "章节标题拆分", + "qaPair": "问答对拆分", + "paragraphLength": "自然段长度拆分", + "customRegex": "自定义正则拆分", "mdSplitterLevel": "Markdown标题等级", "uploadStatus": "上传状态", "pendingUpload": "待上传", @@ -139,5 +164,6 @@ "tencentCloud": "腾讯云", "vectorEmbedModelTips": "成功向量数据之后不允许修改向量模型", "dimensionOfVectorModelTips": "成功向量数据之后不允许修改向量模型维度", - "dimensionOfVectorModel": "向量模型维度" + "dimensionOfVectorModel": "向量模型维度", + "managePermissionHint": "仅创建者或超级管理员可修改当前知识库" } diff --git a/easyflow-ui-admin/app/src/views/ai/documentCollection/ChunkDocumentTable.vue b/easyflow-ui-admin/app/src/views/ai/documentCollection/ChunkDocumentTable.vue index ed9f25b..21c5a31 100644 --- a/easyflow-ui-admin/app/src/views/ai/documentCollection/ChunkDocumentTable.vue +++ b/easyflow-ui-admin/app/src/views/ai/documentCollection/ChunkDocumentTable.vue @@ -27,14 +27,26 @@ const props = defineProps({ type: String, default: '', }, + manageable: { + type: Boolean, + default: true, + }, }); const dialogVisible = ref(false); const pageDataRef = ref(); const handleEdit = (row: any) => { + if (!props.manageable) { + ElMessage.warning($t('documentCollection.managePermissionHint')); + return; + } form.value = { id: row.id, content: row.content }; openDialog(); }; const handleDelete = (row: any) => { + if (!props.manageable) { + ElMessage.warning($t('documentCollection.managePermissionHint')); + return; + } ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), { confirmButtonText: $t('message.ok'), cancelButtonText: $t('message.cancel'), @@ -68,6 +80,10 @@ const queryParams = ref({ sortType: 'asc', }); const save = () => { + if (!props.manageable) { + ElMessage.warning($t('documentCollection.managePermissionHint')); + return; + } btnLoading.value = true; api.post('/api/v1/documentChunk/update', form.value).then((res: any) => { btnLoading.value = false; @@ -103,7 +119,12 @@ const form = ref({ :label="$t('documentCollection.content')" min-width="240" /> - + ((route.query.id as string) || ''); const activeMenu = ref((route.query.activeMenu as string) || ''); const knowledgeInfo = ref({}); const selectedCategory = ref(''); +const canManageKnowledgePermission = computed(() => + hasAccessByCodes(['/api/v1/documentCollection/save']), +); + +const isSuperAdmin = computed(() => { + return ( + String(userStore.userInfo?.id || '') === '1' || + (userStore.userRoles || []).includes('super_admin') + ); +}); + +const canManageCurrentKnowledge = computed(() => { + if (!knowledgeInfo.value?.id || !canManageKnowledgePermission.value) { + return false; + } + return ( + isSuperAdmin.value || + String(userStore.userInfo?.id || '') === + String(knowledgeInfo.value.createdBy || '') + ); +}); const syncNavTitle = (title: string) => { if (!title) { @@ -163,6 +188,9 @@ const backDoc = () => {
{{ knowledgeInfo.description || '' }}
+
+ {{ $t('documentCollection.managePermissionHint') }} +