diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/BotController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/BotController.java index 4d8124e..da12346 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/BotController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/BotController.java @@ -19,6 +19,7 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import tech.easyflow.ai.easyagents.listener.PromptChoreChatStreamListener; import tech.easyflow.ai.entity.*; import tech.easyflow.ai.publish.BotPublishAppService; +import tech.easyflow.approval.entity.vo.ApprovalActionResult; import tech.easyflow.ai.service.*; import tech.easyflow.ai.service.impl.BotServiceImpl; import tech.easyflow.common.audio.core.AudioServiceManager; @@ -70,6 +71,8 @@ public class BotController extends BaseCurdController { private CategoryPermissionService categoryPermissionService; @Resource private BotPublishAppService botPublishAppService; + @Resource + private AiResourceApprovalStateService aiResourceApprovalStateService; public BotController(BotService service, ModelService modelService, BotWorkflowService botWorkflowService, BotDocumentCollectionService botDocumentCollectionService, BotMessageService botMessageService) { @@ -197,6 +200,9 @@ public class BotController extends BaseCurdController { if (!StpUtil.isLogin() && !tech.easyflow.ai.enums.PublishStatus.from(bot.getPublishStatus()).isExternallyVisible()) { throw new BusinessException("聊天助手尚未发布"); } + if (StpUtil.isLogin()) { + aiResourceApprovalStateService.fillBotApprovalState(bot); + } return Result.ok(bot); } @@ -221,6 +227,9 @@ public class BotController extends BaseCurdController { } if (data.getModelId() == null) { + if (StpUtil.isLogin()) { + aiResourceApprovalStateService.fillBotApprovalState(data); + } return Result.ok(data); } @@ -229,6 +238,9 @@ public class BotController extends BaseCurdController { if (llm == null) { data.setModelId(null); + if (StpUtil.isLogin()) { + aiResourceApprovalStateService.fillBotApprovalState(data); + } return Result.ok(data); } @@ -242,19 +254,40 @@ public class BotController extends BaseCurdController { } + if (StpUtil.isLogin()) { + aiResourceApprovalStateService.fillBotApprovalState(data); + } return Result.ok(data); } @PostMapping("/submitPublishApproval") @SaCheckPermission("/api/v1/bot/save") public Result submitPublishApproval(@JsonBody("id") BigInteger id) { - return Result.ok(botPublishAppService.submitPublishApproval(id)); + return buildApprovalActionResult( + botPublishAppService.submitPublishApproval(id), + "已提交发布审批", + "已直接发布" + ); + } + + @PostMapping("/submitOfflineApproval") + @SaCheckPermission("/api/v1/bot/save") + public Result submitOfflineApproval(@JsonBody("id") BigInteger id) { + return buildApprovalActionResult( + botPublishAppService.submitOfflineApproval(id), + "已提交下线审批", + "已直接下线" + ); } @PostMapping("/submitDeleteApproval") @SaCheckPermission("/api/v1/bot/remove") public Result submitDeleteApproval(@JsonBody("id") BigInteger id) { - return Result.ok(botPublishAppService.submitDeleteApproval(id)); + return buildApprovalActionResult( + botPublishAppService.submitDeleteApproval(id), + "已提交删除审批", + "已直接删除" + ); } @Override @@ -262,13 +295,26 @@ public class BotController extends BaseCurdController { QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity)); applyCategoryPermission(queryWrapper); queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy())); - return Result.ok(service.list(queryWrapper)); + List bots = service.list(queryWrapper); + aiResourceApprovalStateService.fillBotApprovalState(bots); + return Result.ok(bots); } @Override protected Page queryPage(Page page, QueryWrapper queryWrapper) { applyCategoryPermission(queryWrapper); - return super.queryPage(page, queryWrapper); + Page result = super.queryPage(page, queryWrapper); + aiResourceApprovalStateService.fillBotApprovalState(result.getRecords()); + return result; + } + + private Result buildApprovalActionResult(ApprovalActionResult actionResult, + String approvalMessage, + String directMessage) { + return Result.ok( + actionResult.isApprovalRequired() ? approvalMessage : directMessage, + actionResult.getInstanceId() + ); } private void applyCategoryPermission(QueryWrapper queryWrapper) { @@ -388,7 +434,7 @@ public class BotController extends BaseCurdController { @Override @PostMapping("remove") public Result remove(@JsonBody(value = "id", required = true) Serializable id) { - return Result.fail(1, "请提交删除审批"); + return Result.fail(1, "请使用发布状态操作删除聊天助手"); } /** 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 93a8ba2..1ba5821 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 @@ -21,6 +21,9 @@ import tech.easyflow.ai.entity.DocumentCollection; import tech.easyflow.ai.entity.Model; import tech.easyflow.ai.enums.PublishStatus; import tech.easyflow.ai.publish.KnowledgePublishAppService; +import tech.easyflow.ai.service.AiResourceApprovalStateService; +import tech.easyflow.ai.vo.OfflineImpactCheckVo; +import tech.easyflow.approval.entity.vo.ApprovalActionResult; import tech.easyflow.ai.rag.KnowledgeRetrievalRequest; import tech.easyflow.ai.rag.KnowledgeRetrievalModes; import tech.easyflow.ai.service.BotDocumentCollectionService; @@ -71,6 +74,8 @@ public class DocumentCollectionController extends BaseCurdController detail(String id) { DocumentCollection detail = service.getDetail(id); + aiResourceApprovalStateService.fillKnowledgeApprovalState(detail); return Result.ok(detail); } @@ -195,7 +201,46 @@ public class DocumentCollectionController extends BaseCurdController submitPublishApproval(@JsonBody("id") BigInteger id) { - return Result.ok(knowledgePublishAppService.submitPublishApproval(id)); + return buildApprovalActionResult( + knowledgePublishAppService.submitPublishApproval(id), + "已提交发布审批", + "已直接发布" + ); + } + + /** + * 提交下线审批。 + * + * @param id 知识库 ID + * @return 审批实例 ID + */ + @PostMapping("/submitOfflineApproval") + @SaCheckPermission("/api/v1/documentCollection/save") + public Result submitOfflineApproval(@JsonBody("id") BigInteger id) { + return buildApprovalActionResult( + knowledgePublishAppService.submitOfflineApproval(id), + "已提交下线审批", + "已直接下线" + ); + } + + /** + * 检查知识库下线影响。 + * + * @param id 知识库 ID + * @return 下线影响结果 + */ + @GetMapping("/offlineImpactCheck") + @SaCheckPermission("/api/v1/documentCollection/save") + @RequireResourceAccess( + resource = CategoryResourceType.KNOWLEDGE, + action = ResourceAction.MANAGE, + lookup = ResourceLookup.KNOWLEDGE_ID, + idExpr = "#id", + denyMessage = "无权限管理知识库" + ) + public Result offlineImpactCheck(@RequestParam BigInteger id) { + return Result.ok(knowledgePublishAppService.checkOfflineImpact(id)); } /** @@ -207,7 +252,11 @@ public class DocumentCollectionController extends BaseCurdController submitDeleteApproval(@JsonBody("id") BigInteger id) { - return Result.ok(knowledgePublishAppService.submitDeleteApproval(id)); + return buildApprovalActionResult( + knowledgePublishAppService.submitDeleteApproval(id), + "已提交删除审批", + "已直接删除" + ); } @PostMapping("splitterProfile/save") @@ -250,20 +299,33 @@ public class DocumentCollectionController extends BaseCurdController collections = service.list(queryWrapper); + aiResourceApprovalStateService.fillKnowledgeApprovalState(collections); + return Result.ok(collections); } @Override protected Page queryPage(Page page, QueryWrapper queryWrapper) { knowledgeVisibilityQueryHelper.applyReadableAccess(queryWrapper); applyPublishedOnlyFilter(queryWrapper); - return super.queryPage(page, queryWrapper); + Page result = super.queryPage(page, queryWrapper); + aiResourceApprovalStateService.fillKnowledgeApprovalState(result.getRecords()); + return result; } @Override @PostMapping("remove") public Result remove(@JsonBody(value = "id", required = true) Serializable id) { - return Result.fail(1, "请提交删除审批"); + return Result.fail(1, "请使用发布状态操作删除知识库"); + } + + private Result buildApprovalActionResult(ApprovalActionResult actionResult, + String approvalMessage, + String directMessage) { + return Result.ok( + actionResult.isApprovalRequired() ? approvalMessage : directMessage, + actionResult.getInstanceId() + ); } private void normalizeVisibilityScope(DocumentCollection entity, boolean isSave) { diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/ResourceController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/ResourceController.java index d5f26f8..a885ced 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/ResourceController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/ResourceController.java @@ -4,14 +4,19 @@ import cn.hutool.core.io.FileTypeUtil; import cn.hutool.http.HttpUtil; import com.mybatisflex.core.paginate.Page; import com.mybatisflex.core.query.QueryWrapper; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import tech.easyflow.admin.model.ai.ResourcePreviewVo; import tech.easyflow.ai.entity.Resource; import tech.easyflow.ai.service.ResourceService; +import tech.easyflow.ai.utils.DocUtil; import tech.easyflow.common.domain.Result; import tech.easyflow.common.entity.LoginAccount; import tech.easyflow.common.satoken.util.SaTokenUtil; import tech.easyflow.common.web.controller.BaseCurdController; +import tech.easyflow.common.web.exceptions.BusinessException; import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot; import tech.easyflow.system.service.CategoryPermissionService; @@ -31,6 +36,8 @@ import static tech.easyflow.ai.entity.table.ResourceTableDef.RESOURCE; @RestController @RequestMapping("/api/v1/resource") public class ResourceController extends BaseCurdController { + private static final int RESOURCE_PREVIEW_CONTENT_LIMIT = 20_000; + @javax.annotation.Resource private CategoryPermissionService categoryPermissionService; @@ -79,6 +86,26 @@ public class ResourceController extends BaseCurdController previewContent(String id) { + Resource resource = service.getById(id); + if (resource == null) { + throw new BusinessException("素材不存在"); + } + categoryPermissionService.assertCategoryResourceVisible("RESOURCE", resource.getCreatedBy(), resource.getCategoryId(), "无权限访问素材"); + + byte[] bytes = DocUtil.downloadFile(resource.getResourceUrl()); + String suffix = resolvePreviewSuffix(resource, bytes); + String content = DocUtil.readPreviewContent(suffix, new ByteArrayInputStream(bytes)); + return Result.ok(buildPreviewVo(content)); + } + private void applyCategoryPermission(QueryWrapper queryWrapper) { RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("RESOURCE"); if (!access.isRestricted()) { @@ -90,4 +117,37 @@ public class ResourceController extends BaseCurdController RESOURCE_PREVIEW_CONTENT_LIMIT; + ResourcePreviewVo vo = new ResourcePreviewVo(); + vo.setContent(truncated ? safeContent.substring(0, RESOURCE_PREVIEW_CONTENT_LIMIT) : safeContent); + vo.setTruncated(truncated); + return vo; + } } diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/WorkflowController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/WorkflowController.java index 5227779..763db72 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/WorkflowController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/WorkflowController.java @@ -27,6 +27,9 @@ import tech.easyflow.ai.easyagentsflow.service.WorkflowDatacenterContentService; import tech.easyflow.ai.entity.Workflow; import tech.easyflow.ai.enums.PublishStatus; import tech.easyflow.ai.publish.WorkflowPublishAppService; +import tech.easyflow.ai.service.AiResourceApprovalStateService; +import tech.easyflow.ai.vo.OfflineImpactCheckVo; +import tech.easyflow.approval.entity.vo.ApprovalActionResult; import tech.easyflow.ai.service.BotWorkflowService; import tech.easyflow.ai.service.ModelService; import tech.easyflow.ai.service.WorkflowService; @@ -88,6 +91,8 @@ public class WorkflowController extends BaseCurdController submitPublishApproval(@JsonBody("id") BigInteger id) { - return Result.ok(workflowPublishAppService.submitPublishApproval(id)); + return buildApprovalActionResult( + workflowPublishAppService.submitPublishApproval(id), + "已提交发布审批", + "已直接发布" + ); + } + + /** + * 提交下线审批。 + * + * @param id 工作流 ID + * @return 审批实例 ID + */ + @PostMapping("/submitOfflineApproval") + @SaCheckPermission("/api/v1/workflow/save") + public Result submitOfflineApproval(@JsonBody("id") BigInteger id) { + return buildApprovalActionResult( + workflowPublishAppService.submitOfflineApproval(id), + "已提交下线审批", + "已直接下线" + ); + } + + /** + * 检查工作流下线影响。 + * + * @param id 工作流 ID + * @return 下线影响结果 + */ + @GetMapping("/offlineImpactCheck") + @SaCheckPermission("/api/v1/workflow/save") + @RequireResourceAccess( + resource = CategoryResourceType.WORKFLOW, + action = ResourceAction.MANAGE, + lookup = ResourceLookup.WORKFLOW_ID, + idExpr = "#id", + denyMessage = "无权限管理工作流" + ) + public Result offlineImpactCheck(@RequestParam BigInteger id) { + return Result.ok(workflowPublishAppService.checkOfflineImpact(id)); } /** @@ -266,7 +310,11 @@ public class WorkflowController extends BaseCurdController submitDeleteApproval(@JsonBody("id") BigInteger id) { - return Result.ok(workflowPublishAppService.submitDeleteApproval(id)); + return buildApprovalActionResult( + workflowPublishAppService.submitDeleteApproval(id), + "已提交删除审批", + "已直接删除" + ); } @GetMapping("/supportedCodeEngines") @@ -307,6 +355,7 @@ public class WorkflowController extends BaseCurdController detail(String id) { Workflow workflow = service.getDetail(id); + aiResourceApprovalStateService.fillWorkflowApprovalState(workflow); return Result.ok(workflow); } @@ -368,20 +417,33 @@ public class WorkflowController extends BaseCurdController workflows = service.list(queryWrapper); + aiResourceApprovalStateService.fillWorkflowApprovalState(workflows); + return Result.ok(workflows); } @Override protected Page queryPage(Page page, QueryWrapper queryWrapper) { workflowVisibilityQueryHelper.applyReadableAccess(queryWrapper); applyPublishedOnlyFilter(queryWrapper); - return super.queryPage(page, queryWrapper); + Page result = super.queryPage(page, queryWrapper); + aiResourceApprovalStateService.fillWorkflowApprovalState(result.getRecords()); + return result; } @Override @PostMapping("remove") public Result remove(@JsonBody(value = "id", required = true) Serializable id) { - return Result.fail(1, "请提交删除审批"); + return Result.fail(1, "请使用发布状态操作删除工作流"); + } + + private Result buildApprovalActionResult(ApprovalActionResult actionResult, + String approvalMessage, + String directMessage) { + return Result.ok( + actionResult.isApprovalRequired() ? approvalMessage : directMessage, + actionResult.getInstanceId() + ); } @Override diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/model/ai/ResourcePreviewVo.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/model/ai/ResourcePreviewVo.java new file mode 100644 index 0000000..f176a4d --- /dev/null +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/model/ai/ResourcePreviewVo.java @@ -0,0 +1,27 @@ +package tech.easyflow.admin.model.ai; + +/** + * 素材预览内容返回对象。 + */ +public class ResourcePreviewVo { + + private String content; + + private boolean truncated; + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public boolean isTruncated() { + return truncated; + } + + public void setTruncated(boolean truncated) { + this.truncated = truncated; + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/Bot.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/Bot.java index b93667c..f6471d6 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/Bot.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/Bot.java @@ -1,5 +1,6 @@ package tech.easyflow.ai.entity; +import com.mybatisflex.annotation.Column; import tech.easyflow.ai.entity.base.BotBase; import com.mybatisflex.annotation.Table; @@ -19,6 +20,15 @@ public class Bot extends BotBase { public static final String KEY_MAX_MESSAGE_COUNT = "maxMessageCount"; public static final String KEY_ENABLE_DEEP_THINKING = "enableDeepThinking"; + @Column(ignore = true) + private Boolean approvalPending; + + @Column(ignore = true) + private String currentApprovalActionType; + + @Column(ignore = true) + private String displayPublishStatus; + public boolean isAnonymousEnabled() { Map options = getOptions(); if (options == null) { @@ -28,4 +38,28 @@ public class Bot extends BotBase { return o != null && (boolean) o; } + public Boolean getApprovalPending() { + return approvalPending; + } + + public void setApprovalPending(Boolean approvalPending) { + this.approvalPending = approvalPending; + } + + public String getCurrentApprovalActionType() { + return currentApprovalActionType; + } + + public void setCurrentApprovalActionType(String currentApprovalActionType) { + this.currentApprovalActionType = currentApprovalActionType; + } + + public String getDisplayPublishStatus() { + return displayPublishStatus; + } + + public void setDisplayPublishStatus(String displayPublishStatus) { + this.displayPublishStatus = displayPublishStatus; + } + } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/DocumentCollection.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/DocumentCollection.java index 1cee4d8..3e40483 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/DocumentCollection.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/DocumentCollection.java @@ -3,6 +3,7 @@ package tech.easyflow.ai.entity; import com.easyagents.core.model.chat.tool.Tool; import com.easyagents.core.store.DocumentStore; import com.easyagents.rag.retrieval.RetrievalMode; +import com.mybatisflex.annotation.Column; import com.easyagents.store.milvus.MilvusVectorStore; import com.easyagents.store.milvus.MilvusVectorStoreConfig; import com.mybatisflex.annotation.Table; @@ -27,6 +28,15 @@ import java.util.Map; @Table("tb_document_collection") public class DocumentCollection extends DocumentCollectionBase implements VisibilityResource { + @Column(ignore = true) + private Boolean approvalPending; + + @Column(ignore = true) + private String currentApprovalActionType; + + @Column(ignore = true) + private String displayPublishStatus; + public static final String TYPE_DOCUMENT = "DOCUMENT"; public static final String TYPE_FAQ = "FAQ"; @@ -135,4 +145,28 @@ public class DocumentCollection extends DocumentCollectionBase implements Visibi } return options.get(key); } + + public Boolean getApprovalPending() { + return approvalPending; + } + + public void setApprovalPending(Boolean approvalPending) { + this.approvalPending = approvalPending; + } + + public String getCurrentApprovalActionType() { + return currentApprovalActionType; + } + + public void setCurrentApprovalActionType(String currentApprovalActionType) { + this.currentApprovalActionType = currentApprovalActionType; + } + + public String getDisplayPublishStatus() { + return displayPublishStatus; + } + + public void setDisplayPublishStatus(String displayPublishStatus) { + this.displayPublishStatus = displayPublishStatus; + } } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/Workflow.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/Workflow.java index dd10c75..919b387 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/Workflow.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/Workflow.java @@ -1,6 +1,7 @@ package tech.easyflow.ai.entity; import com.easyagents.core.model.chat.tool.Tool; +import com.mybatisflex.annotation.Column; import com.mybatisflex.annotation.Table; import tech.easyflow.ai.easyagents.tool.WorkflowTool; import tech.easyflow.ai.entity.base.WorkflowBase; @@ -16,6 +17,15 @@ import tech.easyflow.system.permission.resource.VisibilityResource; @Table("tb_workflow") public class Workflow extends WorkflowBase implements VisibilityResource { + @Column(ignore = true) + private Boolean approvalPending; + + @Column(ignore = true) + private String currentApprovalActionType; + + @Column(ignore = true) + private String displayPublishStatus; + public Tool toFunction(boolean needEnglishName) { return new WorkflowTool(this, needEnglishName); } @@ -23,4 +33,28 @@ public class Workflow extends WorkflowBase implements VisibilityResource { public Tool toFunction(boolean needEnglishName, String definitionId) { return new WorkflowTool(this, needEnglishName, definitionId); } + + public Boolean getApprovalPending() { + return approvalPending; + } + + public void setApprovalPending(Boolean approvalPending) { + this.approvalPending = approvalPending; + } + + public String getCurrentApprovalActionType() { + return currentApprovalActionType; + } + + public void setCurrentApprovalActionType(String currentApprovalActionType) { + this.currentApprovalActionType = currentApprovalActionType; + } + + public String getDisplayPublishStatus() { + return displayPublishStatus; + } + + public void setDisplayPublishStatus(String displayPublishStatus) { + this.displayPublishStatus = displayPublishStatus; + } } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/enums/PublishStatus.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/enums/PublishStatus.java index 24c5d96..fe60675 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/enums/PublishStatus.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/enums/PublishStatus.java @@ -11,6 +11,8 @@ public enum PublishStatus { DRAFT("DRAFT"), PUBLISH_PENDING("PUBLISH_PENDING"), PUBLISHED("PUBLISHED"), + OFFLINE_PENDING("OFFLINE_PENDING"), + OFFLINE("OFFLINE"), DELETE_PENDING("DELETE_PENDING"); private final String code; @@ -34,7 +36,16 @@ public enum PublishStatus { * @return 允许外部访问或线上运行时返回 true */ public boolean isExternallyVisible() { - return this == PUBLISHED || this == DELETE_PENDING; + return this == PUBLISHED || this == DELETE_PENDING || this == OFFLINE_PENDING; + } + + /** + * 是否允许作为 Bot 新绑定候选。 + * + * @return 仅已发布状态返回 true + */ + public boolean isSelectableForBot() { + return this == PUBLISHED; } /** diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/AbstractAiResourceLifecycleHandler.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/AbstractAiResourceLifecycleHandler.java new file mode 100644 index 0000000..197f7d3 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/AbstractAiResourceLifecycleHandler.java @@ -0,0 +1,366 @@ +package tech.easyflow.ai.publish; + +import com.fasterxml.jackson.databind.ObjectMapper; +import tech.easyflow.ai.enums.PublishStatus; +import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest; +import tech.easyflow.approval.enums.ApprovalActionType; +import tech.easyflow.approval.service.ApprovalInstanceService; +import tech.easyflow.approval.service.ApprovalSubjectHandler; +import tech.easyflow.common.web.exceptions.BusinessException; + +import java.math.BigInteger; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +/** + * AI 资源生命周期处理器抽象基类。 + * + * @param 资源类型 + */ +public abstract class AbstractAiResourceLifecycleHandler implements ApprovalSubjectHandler, AiResourceLifecycleHandler { + + protected static final String SNAPSHOT_KEY = "resourceSnapshot"; + protected static final String PREVIOUS_STATUS_KEY = "previousPublishStatus"; + + private final ApprovalInstanceService approvalInstanceService; + protected final ObjectMapper objectMapper; + + protected AbstractAiResourceLifecycleHandler(ApprovalInstanceService approvalInstanceService, + ObjectMapper objectMapper) { + this.approvalInstanceService = approvalInstanceService; + this.objectMapper = objectMapper; + } + + /** + * 加载资源,不存在时抛异常。 + * + * @param resourceId 资源 ID + * @return 资源 + */ + protected abstract T requireResource(BigInteger resourceId); + + /** + * 校验资源管理权限。 + * + * @param resource 资源 + */ + protected abstract void assertManagePermission(T resource); + + /** + * 获取资源分类 ID。 + * + * @param resource 资源 + * @return 分类 ID + */ + protected abstract BigInteger getCategoryId(T resource); + + /** + * 获取资源部门 ID。 + * + * @param resource 资源 + * @return 部门 ID + */ + protected abstract BigInteger getDeptId(T resource); + + /** + * 获取资源标题。 + * + * @param resource 资源 + * @return 资源标题 + */ + protected abstract String getTitle(T resource); + + /** + * 获取当前发布状态。 + * + * @param resource 资源 + * @return 发布状态 + */ + protected abstract PublishStatus getCurrentStatus(T resource); + + /** + * 获取当前已发布快照。 + * + * @param resource 资源 + * @return 已发布快照 + */ + protected abstract Map getPublishedSnapshot(T resource); + + /** + * 构建当前草稿快照。 + * + * @param resource 资源 + * @return 草稿快照 + */ + protected abstract Map buildResourceSnapshot(T resource); + + /** + * 更新资源待审批状态。 + * + * @param resourceId 资源 ID + * @param publishStatus 发布状态 + * @param currentApprovalInstanceId 审批实例 ID + */ + protected abstract void persistResourceState(BigInteger resourceId, PublishStatus publishStatus, BigInteger currentApprovalInstanceId); + + /** + * 发布成功后写入已发布状态。 + * + * @param resourceId 资源 ID + * @param resourceSnapshot 已发布快照 + * @param operatorId 操作人 ID + */ + protected abstract void publishResource(BigInteger resourceId, Map resourceSnapshot, BigInteger operatorId); + + /** + * 下线成功后写入下线状态。 + * + * @param resourceId 资源 ID + */ + protected abstract void markResourceOffline(BigInteger resourceId); + + /** + * 删除资源。 + * + * @param resourceId 资源 ID + */ + protected abstract void removeResource(BigInteger resourceId); + + /** + * 返回资源中文名称。 + * + * @return 资源名称 + */ + protected abstract String resourceLabel(); + + /** + * 返回当前动作摘要前缀。 + * + * @param actionType 动作类型 + * @return 摘要前缀 + */ + protected String resolveActionLabel(ApprovalActionType actionType) { + return switch (actionType) { + case PUBLISH -> "发布"; + case OFFLINE -> "下线"; + case DELETE -> "删除"; + }; + } + + /** + * 扩展下线快照内容。 + * + * @param resource 资源 + * @param snapshot 当前快照副本 + */ + protected void enrichOfflineSnapshot(T resource, Map snapshot) { + } + + /** + * 删除前额外校验。 + * + * @param resource 资源 + * @param currentStatus 当前状态 + */ + protected void validateDelete(T resource, PublishStatus currentStatus) { + } + + /** + * 下线成功后的额外副作用。 + * + * @param resourceId 资源 ID + */ + protected void afterOffline(BigInteger resourceId) { + } + + /** + * 删除成功前的额外副作用。 + * + * @param resourceId 资源 ID + */ + protected void beforeRemove(BigInteger resourceId) { + } + + /** + * {@inheritDoc} + */ + @Override + public ApprovalSubmitRequest buildSubmitRequest(BigInteger resourceId, String actionType, BigInteger operatorId) { + T resource = requireResource(resourceId); + assertManagePermission(resource); + if (approvalInstanceService.existsActiveInstance(resourceType(), resourceId)) { + throw new BusinessException("当前" + resourceLabel() + "存在未结束审批,请先处理完成"); + } + ApprovalActionType approvalActionType = ApprovalActionType.from(actionType); + PublishStatus currentStatus = getCurrentStatus(resource); + Map resourceSnapshot = resolveSubmitSnapshot(resource, approvalActionType, currentStatus); + + ApprovalSubmitRequest request = new ApprovalSubmitRequest(); + request.setResourceType(resourceType()); + request.setResourceId(resourceId); + request.setActionType(approvalActionType.getCode()); + request.setApplicantId(operatorId); + request.setCategoryId(getCategoryId(resource)); + request.setDeptId(getDeptId(resource)); + request.setSummary(resolveActionLabel(approvalActionType) + resourceLabel() + ":" + getTitle(resource)); + Map snapshot = new LinkedHashMap<>(); + snapshot.put(SNAPSHOT_KEY, resourceSnapshot); + snapshot.put(PREVIOUS_STATUS_KEY, currentStatus.getCode()); + request.setSnapshotJson(snapshot); + return request; + } + + /** + * 解析动作对应的提交快照。 + * + * @param resource 资源 + * @param actionType 动作类型 + * @param currentStatus 当前状态 + * @return 提交快照 + */ + protected Map resolveSubmitSnapshot(T resource, + ApprovalActionType actionType, + PublishStatus currentStatus) { + return switch (actionType) { + case PUBLISH -> buildPublishSnapshot(resource, currentStatus); + case OFFLINE -> buildOfflineSnapshot(resource, currentStatus); + case DELETE -> buildDeleteSnapshot(resource, currentStatus); + }; + } + + /** + * 构建发布快照。 + * + * @param resource 资源 + * @param currentStatus 当前状态 + * @return 发布快照 + */ + protected Map buildPublishSnapshot(T resource, PublishStatus currentStatus) { + if (currentStatus != PublishStatus.DRAFT + && currentStatus != PublishStatus.OFFLINE + && currentStatus != PublishStatus.PUBLISHED) { + throw new BusinessException("当前" + resourceLabel() + "状态不允许发布"); + } + Map snapshot = buildResourceSnapshot(resource); + if (currentStatus == PublishStatus.PUBLISHED && isSameSnapshot(snapshot, getPublishedSnapshot(resource))) { + throw new BusinessException("当前内容与已发布版本一致,无需重新发布"); + } + return snapshot; + } + + /** + * 构建下线快照。 + * + * @param resource 资源 + * @param currentStatus 当前状态 + * @return 下线快照 + */ + protected Map buildOfflineSnapshot(T resource, PublishStatus currentStatus) { + if (currentStatus != PublishStatus.PUBLISHED) { + throw new BusinessException("当前" + resourceLabel() + "尚未发布,无法下线"); + } + Map publishedSnapshot = getPublishedSnapshot(resource); + if (publishedSnapshot == null || publishedSnapshot.isEmpty()) { + throw new BusinessException("当前" + resourceLabel() + "缺少已发布快照,无法下线"); + } + Map snapshot = new LinkedHashMap<>(publishedSnapshot); + enrichOfflineSnapshot(resource, snapshot); + return snapshot; + } + + /** + * 构建删除快照。 + * + * @param resource 资源 + * @param currentStatus 当前状态 + * @return 删除快照 + */ + protected Map buildDeleteSnapshot(T resource, PublishStatus currentStatus) { + if (currentStatus == PublishStatus.PUBLISHED) { + throw new BusinessException("当前" + resourceLabel() + "已发布,请先下线后再删除"); + } + if (currentStatus == PublishStatus.PUBLISH_PENDING + || currentStatus == PublishStatus.OFFLINE_PENDING + || currentStatus == PublishStatus.DELETE_PENDING) { + throw new BusinessException("当前" + resourceLabel() + "存在进行中的审批,请先处理完成"); + } + validateDelete(resource, currentStatus); + return buildResourceSnapshot(resource); + } + + /** + * 执行审批通过后的真实动作。 + * + * @param actionType 动作类型 + * @param resourceId 资源 ID + * @param resourceSnapshot 快照 + * @param operatorId 操作人 ID + */ + @Override + public void applyApprovedAction(String actionType, + BigInteger resourceId, + Map resourceSnapshot, + BigInteger operatorId) { + ApprovalActionType normalizedAction = ApprovalActionType.from(actionType); + if (normalizedAction == ApprovalActionType.PUBLISH) { + publishResource(resourceId, resourceSnapshot, operatorId); + return; + } + if (normalizedAction == ApprovalActionType.OFFLINE) { + markResourceOffline(resourceId); + afterOffline(resourceId); + return; + } + beforeRemove(resourceId); + removeResource(resourceId); + } + + /** + * {@inheritDoc} + */ + @Override + public void updatePendingState(BigInteger resourceId, PublishStatus publishStatus, BigInteger instanceId) { + persistResourceState(resourceId, publishStatus, instanceId); + } + + /** + * {@inheritDoc} + */ + @Override + public void restoreState(BigInteger resourceId, PublishStatus previousStatus) { + persistResourceState(resourceId, previousStatus, null); + } + + /** + * 从请求快照读取资源内容。 + * + * @param snapshotJson 请求快照 + * @return 资源快照 + */ + @SuppressWarnings("unchecked") + protected Map readResourceSnapshot(Map snapshotJson) { + Object snapshot = snapshotJson == null ? null : snapshotJson.get(SNAPSHOT_KEY); + if (!(snapshot instanceof Map map)) { + throw new BusinessException("审批快照缺少" + resourceLabel() + "内容"); + } + return new LinkedHashMap<>((Map) map); + } + + /** + * 判断当前草稿快照与已发布快照是否一致。 + * + * @param currentSnapshot 当前快照 + * @param publishedSnapshot 已发布快照 + * @return 一致返回 true + */ + protected boolean isSameSnapshot(Map currentSnapshot, Map publishedSnapshot) { + if (publishedSnapshot == null || publishedSnapshot.isEmpty()) { + return false; + } + return Objects.equals( + objectMapper.valueToTree(currentSnapshot), + objectMapper.valueToTree(publishedSnapshot) + ); + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/AiResourceLifecycleHandler.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/AiResourceLifecycleHandler.java new file mode 100644 index 0000000..14d33b6 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/AiResourceLifecycleHandler.java @@ -0,0 +1,57 @@ +package tech.easyflow.ai.publish; + +import tech.easyflow.ai.enums.PublishStatus; +import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest; + +import java.math.BigInteger; +import java.util.Map; + +/** + * AI 资源生命周期处理器。 + */ +public interface AiResourceLifecycleHandler { + + /** + * 当前处理器支持的资源类型。 + * + * @return 资源类型编码 + */ + String resourceType(); + + /** + * 构建动作提交请求。 + * + * @param resourceId 资源 ID + * @param actionType 动作类型 + * @param operatorId 操作人 ID + * @return 提交请求 + */ + ApprovalSubmitRequest buildSubmitRequest(BigInteger resourceId, String actionType, BigInteger operatorId); + + /** + * 写入待审批状态。 + * + * @param resourceId 资源 ID + * @param publishStatus 待写入的发布状态 + * @param instanceId 审批实例 ID + */ + void updatePendingState(BigInteger resourceId, PublishStatus publishStatus, BigInteger instanceId); + + /** + * 执行动作真正生效后的资源更新与副作用。 + * + * @param actionType 动作类型 + * @param resourceId 资源 ID + * @param resourceSnapshot 审批冻结快照 + * @param operatorId 操作人 ID + */ + void applyApprovedAction(String actionType, BigInteger resourceId, Map resourceSnapshot, BigInteger operatorId); + + /** + * 按提交前真实状态恢复资源状态。 + * + * @param resourceId 资源 ID + * @param previousStatus 提交前状态 + */ + void restoreState(BigInteger resourceId, PublishStatus previousStatus); +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/AiResourceLifecycleService.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/AiResourceLifecycleService.java new file mode 100644 index 0000000..3933c37 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/AiResourceLifecycleService.java @@ -0,0 +1,22 @@ +package tech.easyflow.ai.publish; + +import tech.easyflow.approval.entity.vo.ApprovalActionResult; + +import java.math.BigInteger; + +/** + * AI 资源生命周期统一状态机服务。 + */ +public interface AiResourceLifecycleService { + + /** + * 提交资源动作。 + * + * @param resourceType 资源类型 + * @param resourceId 资源 ID + * @param actionType 动作类型 + * @param operatorId 操作人 ID + * @return 执行结果 + */ + ApprovalActionResult submitAction(String resourceType, BigInteger resourceId, String actionType, BigInteger operatorId); +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/AiResourceLifecycleServiceImpl.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/AiResourceLifecycleServiceImpl.java new file mode 100644 index 0000000..872e167 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/AiResourceLifecycleServiceImpl.java @@ -0,0 +1,153 @@ +package tech.easyflow.ai.publish; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import tech.easyflow.ai.enums.PublishStatus; +import tech.easyflow.approval.entity.ApprovalInstance; +import tech.easyflow.approval.entity.vo.ApprovalActionResult; +import tech.easyflow.approval.entity.vo.ApprovalFlowDetailVo; +import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest; +import tech.easyflow.approval.enums.ApprovalActionType; +import tech.easyflow.approval.service.ApprovalInstanceService; +import tech.easyflow.approval.service.ApprovalMatchService; +import tech.easyflow.approval.service.ApprovalResultHandler; +import tech.easyflow.common.web.exceptions.BusinessException; + +import java.math.BigInteger; +import java.util.List; +import java.util.Map; + +/** + * AI 资源生命周期统一状态机实现。 + */ +@Service +public class AiResourceLifecycleServiceImpl implements AiResourceLifecycleService, ApprovalResultHandler { + + private static final String SNAPSHOT_KEY = "resourceSnapshot"; + private static final String PREVIOUS_STATUS_KEY = "previousPublishStatus"; + + private final List handlers; + private final ApprovalMatchService approvalMatchService; + private final ApprovalInstanceService approvalInstanceService; + + public AiResourceLifecycleServiceImpl(List handlers, + ApprovalMatchService approvalMatchService, + ApprovalInstanceService approvalInstanceService) { + this.handlers = handlers; + this.approvalMatchService = approvalMatchService; + this.approvalInstanceService = approvalInstanceService; + } + + /** + * {@inheritDoc} + */ + @Override + @Transactional(rollbackFor = Exception.class) + public ApprovalActionResult submitAction(String resourceType, BigInteger resourceId, String actionType, BigInteger operatorId) { + AiResourceLifecycleHandler handler = getHandler(resourceType); + ApprovalSubmitRequest request = handler.buildSubmitRequest(resourceId, actionType, operatorId); + ApprovalFlowDetailVo flow = approvalMatchService.matchFlowOrNull(request); + if (flow == null) { + handler.applyApprovedAction(actionType, resourceId, readResourceSnapshot(request.getSnapshotJson()), operatorId); + return ApprovalActionResult.direct(); + } + BigInteger instanceId = approvalInstanceService.submitApproval(request); + handler.updatePendingState( + resourceId, + resolveSubmittedStatus(actionType, resolvePreviousStatus(request.getSnapshotJson())), + instanceId + ); + return ApprovalActionResult.required(instanceId); + } + + /** + * {@inheritDoc} + */ + @Override + public void handleApproved(ApprovalInstance instance, BigInteger operatorId, String comment) { + getHandler(instance.getResourceType()).applyApprovedAction( + instance.getActionType(), + instance.getResourceId(), + readResourceSnapshot(instance.getSnapshotJson()), + operatorId + ); + } + + /** + * {@inheritDoc} + */ + @Override + public void handleRejected(ApprovalInstance instance, BigInteger operatorId, String comment) { + getHandler(instance.getResourceType()).restoreState( + instance.getResourceId(), + resolvePreviousStatus(instance.getSnapshotJson()) + ); + } + + /** + * {@inheritDoc} + */ + @Override + public void handleRevoked(ApprovalInstance instance, BigInteger operatorId, String comment) { + getHandler(instance.getResourceType()).restoreState( + instance.getResourceId(), + resolvePreviousStatus(instance.getSnapshotJson()) + ); + } + + private AiResourceLifecycleHandler getHandler(String resourceType) { + return handlers.stream() + .filter(item -> item.resourceType().equals(resourceType)) + .findFirst() + .orElseThrow(() -> new BusinessException("未找到资源生命周期处理器: " + resourceType)); + } + + /** + * 解析提交动作后的真实持久化状态。 + * + * @param actionType 动作类型 + * @param previousStatus 提交前真实状态 + * @return 提交后应写入的状态 + */ + private PublishStatus resolveSubmittedStatus(String actionType, PublishStatus previousStatus) { + if (ApprovalActionType.PUBLISH.getCode().equals(actionType) && previousStatus == PublishStatus.PUBLISHED) { + return PublishStatus.PUBLISHED; + } + if (ApprovalActionType.DELETE.getCode().equals(actionType)) { + return PublishStatus.DELETE_PENDING; + } + if (ApprovalActionType.OFFLINE.getCode().equals(actionType)) { + return PublishStatus.OFFLINE_PENDING; + } + return PublishStatus.PUBLISH_PENDING; + } + + /** + * 从审批快照解析提交前真实状态。 + * + * @param snapshotJson 审批冻结快照 + * @return 提交前真实状态 + */ + private PublishStatus resolvePreviousStatus(Map snapshotJson) { + Object status = snapshotJson == null ? null : snapshotJson.get(PREVIOUS_STATUS_KEY); + if (status instanceof String value && !value.isBlank()) { + return PublishStatus.from(value); + } + return PublishStatus.DRAFT; + } + + /** + * 从审批快照中提取冻结资源内容。 + * + * @param snapshotJson 审批冻结快照 + * @return 冻结资源内容 + */ + @SuppressWarnings("unchecked") + private Map readResourceSnapshot(Map snapshotJson) { + Object snapshot = snapshotJson == null ? null : snapshotJson.get(SNAPSHOT_KEY); + if (!(snapshot instanceof Map map)) { + throw new BusinessException("审批快照缺少资源内容"); + } + return (Map) map; + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/BotApprovalSubjectHandler.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/BotApprovalSubjectHandler.java index fa24d84..0791fa9 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/BotApprovalSubjectHandler.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/BotApprovalSubjectHandler.java @@ -1,11 +1,11 @@ package tech.easyflow.ai.publish; -import com.alibaba.fastjson2.JSON; +import com.fasterxml.jackson.databind.ObjectMapper; import com.mybatisflex.core.query.QueryWrapper; import org.springframework.stereotype.Component; import tech.easyflow.ai.entity.Bot; -import tech.easyflow.ai.entity.BotDocumentCollection; import tech.easyflow.ai.entity.BotCategory; +import tech.easyflow.ai.entity.BotDocumentCollection; import tech.easyflow.ai.entity.BotMcp; import tech.easyflow.ai.entity.BotPlugin; import tech.easyflow.ai.entity.BotWorkflow; @@ -26,35 +26,27 @@ import tech.easyflow.ai.service.McpService; import tech.easyflow.ai.service.ModelService; import tech.easyflow.ai.service.PluginItemService; import tech.easyflow.ai.service.WorkflowService; -import tech.easyflow.approval.entity.ApprovalInstance; -import tech.easyflow.approval.entity.vo.ApprovalCallbackContext; -import tech.easyflow.approval.entity.vo.ApprovalSubmitCallbackContext; -import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest; -import tech.easyflow.approval.enums.ApprovalActionType; -import tech.easyflow.approval.enums.ApprovalResourceType; import tech.easyflow.approval.service.ApprovalInstanceService; -import tech.easyflow.approval.service.ApprovalSubjectHandler; +import tech.easyflow.approval.enums.ApprovalResourceType; import tech.easyflow.common.entity.LoginAccount; import tech.easyflow.common.satoken.util.SaTokenUtil; import tech.easyflow.common.web.exceptions.BusinessException; -import tech.easyflow.system.service.CategoryPermissionService; import tech.easyflow.system.entity.SysDept; +import tech.easyflow.system.service.CategoryPermissionService; import tech.easyflow.system.service.SysDeptService; import java.math.BigInteger; import java.util.ArrayList; -import java.util.Date; +import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** - * 聊天助手审批处理器。 + * 聊天助手生命周期处理器。 */ @Component -public class BotApprovalSubjectHandler implements ApprovalSubjectHandler { - - private static final String SNAPSHOT_KEY = "resourceSnapshot"; +public class BotApprovalSubjectHandler extends AbstractAiResourceLifecycleHandler { private final BotService botService; private final BotWorkflowService botWorkflowService; @@ -63,7 +55,6 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler { private final BotMcpService botMcpService; private final WorkflowService workflowService; private final DocumentCollectionService documentCollectionService; - private final ApprovalInstanceService approvalInstanceService; private final CategoryPermissionService categoryPermissionService; private final ModelService modelService; private final BotCategoryService botCategoryService; @@ -84,7 +75,9 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler { BotCategoryService botCategoryService, SysDeptService sysDeptService, PluginItemService pluginItemService, - McpService mcpService) { + McpService mcpService, + ObjectMapper objectMapper) { + super(approvalInstanceService, objectMapper); this.botService = botService; this.botWorkflowService = botWorkflowService; this.botDocumentCollectionService = botDocumentCollectionService; @@ -92,7 +85,6 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler { this.botMcpService = botMcpService; this.workflowService = workflowService; this.documentCollectionService = documentCollectionService; - this.approvalInstanceService = approvalInstanceService; this.categoryPermissionService = categoryPermissionService; this.modelService = modelService; this.botCategoryService = botCategoryService; @@ -106,69 +98,6 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler { return ApprovalResourceType.BOT.getCode(); } - @Override - public ApprovalSubmitRequest buildSubmitRequest(BigInteger resourceId, String actionType, BigInteger operatorId) { - Bot bot = requireBot(resourceId); - assertManagePermission(bot); - if (approvalInstanceService.existsActiveInstance(resourceType(), resourceId)) { - throw new BusinessException("当前聊天助手存在未结束审批,请先处理完成"); - } - ApprovalActionType approvalActionType = ApprovalActionType.from(actionType); - Map resourceSnapshot = buildResourceSnapshot(bot); - if (approvalActionType == ApprovalActionType.PUBLISH - && JSON.toJSONString(resourceSnapshot).equals(JSON.toJSONString(bot.getPublishedSnapshotJson()))) { - throw new BusinessException("当前聊天助手没有变更,无需重复发布"); - } - - ApprovalSubmitRequest request = new ApprovalSubmitRequest(); - request.setResourceType(resourceType()); - request.setResourceId(resourceId); - request.setActionType(approvalActionType.getCode()); - request.setApplicantId(operatorId); - request.setCategoryId(bot.getCategoryId()); - request.setDeptId(bot.getDeptId()); - request.setSummary((approvalActionType == ApprovalActionType.PUBLISH ? "发布" : "删除") + "聊天助手:" + bot.getTitle()); - request.setSnapshotJson(Map.of(SNAPSHOT_KEY, resourceSnapshot)); - return request; - } - - @Override - public void onSubmitted(ApprovalSubmitCallbackContext context) { - Bot update = new Bot(); - update.setId(context.getResourceId()); - update.setCurrentApprovalInstanceId(context.getInstanceId()); - update.setPublishStatus(resolvePendingStatus(context.getActionType()).getCode()); - botService.updateById(update); - } - - @Override - public void onApproved(ApprovalCallbackContext context) { - ApprovalInstance instance = context.getInstance(); - if (ApprovalActionType.PUBLISH.getCode().equals(instance.getActionType())) { - Bot update = new Bot(); - update.setId(instance.getResourceId()); - update.setPublishStatus(PublishStatus.PUBLISHED.getCode()); - update.setCurrentApprovalInstanceId(null); - update.setPublishedSnapshotJson(readResourceSnapshot(instance)); - update.setPublishedAt(new Date()); - update.setPublishedBy(context.getOperatorId()); - botService.updateById(update); - return; - } - removeBotRelations(instance.getResourceId()); - botService.removeById(instance.getResourceId()); - } - - @Override - public void onRejected(ApprovalCallbackContext context) { - clearPendingStatus(context.getInstance().getResourceId()); - } - - @Override - public void onRevoked(ApprovalCallbackContext context) { - clearPendingStatus(context.getInstance().getResourceId()); - } - @Override public void assertPublishedAccess(Object identifier, String denyMessage) { Bot bot = botService.getDetail(String.valueOf(identifier)); @@ -178,24 +107,52 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler { } } - private Bot requireBot(BigInteger id) { - Bot bot = botService.getById(id); + @Override + protected Bot requireResource(BigInteger resourceId) { + Bot bot = botService.getById(resourceId); if (bot == null) { throw new BusinessException("聊天助手不存在"); } return bot; } - private void assertManagePermission(Bot bot) { + @Override + protected void assertManagePermission(Bot resource) { LoginAccount account = SaTokenUtil.getLoginAccount(); boolean superAdmin = categoryPermissionService.isCurrentSuperAdmin(); - boolean creator = account != null && account.getId() != null && account.getId().equals(bot.getCreatedBy()); + boolean creator = account != null && account.getId() != null && account.getId().equals(resource.getCreatedBy()); if (!superAdmin && !creator) { throw new BusinessException("仅创建者或超级管理员可管理聊天助手"); } } - private Map buildResourceSnapshot(Bot bot) { + @Override + protected BigInteger getCategoryId(Bot resource) { + return resource.getCategoryId(); + } + + @Override + protected BigInteger getDeptId(Bot resource) { + return resource.getDeptId(); + } + + @Override + protected String getTitle(Bot resource) { + return resource.getTitle(); + } + + @Override + protected PublishStatus getCurrentStatus(Bot resource) { + return PublishStatus.from(resource.getPublishStatus()); + } + + @Override + protected Map getPublishedSnapshot(Bot resource) { + return resource.getPublishedSnapshotJson(); + } + + @Override + protected Map buildResourceSnapshot(Bot bot) { Model model = resolveModel(bot.getModelId()); BotCategory category = resolveCategory(bot.getCategoryId()); SysDept dept = resolveDept(bot.getDeptId()); @@ -228,13 +185,59 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler { return snapshot; } + @Override + protected void persistResourceState(BigInteger resourceId, PublishStatus publishStatus, BigInteger currentApprovalInstanceId) { + Bot update = new Bot(); + update.setId(resourceId); + update.setPublishStatus(publishStatus.getCode()); + update.setCurrentApprovalInstanceId(currentApprovalInstanceId); + botService.updateById(update); + } + + @Override + protected void publishResource(BigInteger resourceId, Map resourceSnapshot, BigInteger operatorId) { + Bot update = new Bot(); + update.setId(resourceId); + update.setPublishStatus(PublishStatus.PUBLISHED.getCode()); + update.setCurrentApprovalInstanceId(null); + update.setPublishedSnapshotJson(resourceSnapshot); + update.setPublishedAt(new java.util.Date()); + update.setPublishedBy(operatorId); + botService.updateById(update); + } + + @Override + protected void markResourceOffline(BigInteger resourceId) { + Bot update = new Bot(); + update.setId(resourceId); + update.setPublishStatus(PublishStatus.OFFLINE.getCode()); + update.setCurrentApprovalInstanceId(null); + botService.updateById(update); + } + + @Override + protected void removeResource(BigInteger resourceId) { + botService.removeById(resourceId); + } + + @Override + protected String resourceLabel() { + return "聊天助手"; + } + + @Override + protected void beforeRemove(BigInteger resourceId) { + removeBotRelations(resourceId); + } + private List> buildWorkflowBindings(BigInteger botId) { QueryWrapper queryWrapper = QueryWrapper.create().eq(BotWorkflow::getBotId, botId); List relations = botWorkflowService.getMapper().selectListWithRelationsByQuery(queryWrapper); + relations.sort(Comparator.comparing(BotWorkflow::getWorkflowId, Comparator.nullsLast(BigInteger::compareTo))); List> result = new ArrayList<>(); for (BotWorkflow relation : relations) { Workflow workflow = relation.getWorkflow(); - if (workflow == null || !PublishStatus.from(workflow.getPublishStatus()).isExternallyVisible()) { + if (workflow == null || !PublishStatus.from(workflow.getPublishStatus()).isSelectableForBot()) { throw new BusinessException("聊天助手绑定的工作流未发布,无法发布聊天助手"); } Map item = new LinkedHashMap<>(); @@ -248,10 +251,11 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler { private List> buildKnowledgeBindings(BigInteger botId) { QueryWrapper queryWrapper = QueryWrapper.create().eq(BotDocumentCollection::getBotId, botId); List relations = botDocumentCollectionService.getMapper().selectListWithRelationsByQuery(queryWrapper); + relations.sort(Comparator.comparing(BotDocumentCollection::getDocumentCollectionId, Comparator.nullsLast(BigInteger::compareTo))); List> result = new ArrayList<>(); for (BotDocumentCollection relation : relations) { DocumentCollection knowledge = relation.getKnowledge(); - if (knowledge == null || !PublishStatus.from(knowledge.getPublishStatus()).isExternallyVisible()) { + if (knowledge == null || !PublishStatus.from(knowledge.getPublishStatus()).isSelectableForBot()) { throw new BusinessException("聊天助手绑定的知识库未发布,无法发布聊天助手"); } Map item = new LinkedHashMap<>(); @@ -266,6 +270,7 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler { private List> buildPluginBindings(BigInteger botId) { QueryWrapper queryWrapper = QueryWrapper.create().eq(BotPlugin::getBotId, botId); List relations = botPluginService.list(queryWrapper); + relations.sort(Comparator.comparing(BotPlugin::getPluginItemId, Comparator.nullsLast(BigInteger::compareTo))); List> result = new ArrayList<>(); for (BotPlugin relation : relations) { PluginItem pluginItem = pluginItemService.getById(relation.getPluginItemId()); @@ -274,7 +279,7 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler { } Map item = new LinkedHashMap<>(); item.put("pluginItemId", relation.getPluginItemId()); - item.put("pluginItemName", resolvePluginName(pluginItem)); + item.put("pluginItemName", pluginItem.getName()); result.add(item); } return result; @@ -283,156 +288,85 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler { private List> buildMcpBindings(BigInteger botId) { QueryWrapper queryWrapper = QueryWrapper.create().eq(BotMcp::getBotId, botId); List relations = botMcpService.list(queryWrapper); + relations.sort(Comparator.comparing(BotMcp::getMcpId, Comparator.nullsLast(BigInteger::compareTo))); List> result = new ArrayList<>(); for (BotMcp relation : relations) { Mcp mcp = mcpService.getById(relation.getMcpId()); if (mcp == null) { - throw new BusinessException("聊天助手绑定的MCP不存在,无法发布聊天助手"); + throw new BusinessException("聊天助手绑定的 MCP 不存在,无法发布聊天助手"); } Map item = new LinkedHashMap<>(); item.put("mcpId", relation.getMcpId()); item.put("mcpName", mcp.getTitle()); item.put("mcpToolName", relation.getMcpToolName()); - item.put("mcpToolDescription", relation.getMcpToolDescription()); result.add(item); } return result; } - /** - * 解析聊天助手分类。 - * - * @param categoryId 分类 ID - * @return 分类实体,不存在时返回 {@code null} - */ - private BotCategory resolveCategory(BigInteger categoryId) { - if (categoryId == null) { - return null; - } - return botCategoryService.getById(categoryId); - } - - /** - * 解析部门信息。 - * - * @param deptId 部门 ID - * @return 部门实体,不存在时返回 {@code null} - */ - private SysDept resolveDept(BigInteger deptId) { - if (deptId == null) { - return null; - } - return sysDeptService.getById(deptId); - } - - /** - * 解析聊天模型。 - * - * @param modelId 模型 ID - * @return 模型实体 - */ - private Model resolveModel(BigInteger modelId) { - if (modelId == null) { - throw new BusinessException("聊天助手未配置模型,无法提交审批"); - } - Model model = modelService.getById(modelId); - if (model == null) { - throw new BusinessException("聊天助手绑定的模型不存在,无法提交审批"); - } - return model; - } - - /** - * 生成模型展示名称。 - * - * @param model 模型实体 - * @return 模型名称 - */ - private String resolveModelName(Model model) { - if (model.getTitle() != null && !model.getTitle().isBlank()) { - return model.getTitle(); - } - return model.getModelName(); - } - - /** - * 提取系统提示词。 - * - * @param bot 聊天助手 - * @return 系统提示词 - */ - private String resolveSystemPrompt(Bot bot) { - if (bot.getModelOptions() == null) { - return null; - } - Object prompt = bot.getModelOptions().get(Bot.KEY_SYSTEM_PROMPT); - return prompt == null ? null : String.valueOf(prompt); - } - - /** - * 读取数值配置项。 - * - * @param options 配置 map - * @param key 配置键 - * @return 数值配置,不存在时返回 {@code null} - */ - private Number readNumberOption(Map options, String key) { - if (options == null) { - return null; - } - Object value = options.get(key); - if (value instanceof Number number) { - return number; - } - return null; - } - - /** - * 生成插件工具展示名称。 - * - * @param pluginItem 插件工具实体 - * @return 展示名称 - */ - private String resolvePluginName(PluginItem pluginItem) { - if (pluginItem.getName() != null && !pluginItem.getName().isBlank()) { - return pluginItem.getName(); - } - return pluginItem.getEnglishName(); - } - - @SuppressWarnings("unchecked") - private Map readResourceSnapshot(ApprovalInstance instance) { - Object snapshot = instance.getSnapshotJson() == null ? null : instance.getSnapshotJson().get(SNAPSHOT_KEY); - if (!(snapshot instanceof Map map)) { - throw new BusinessException("审批快照缺少聊天助手发布内容"); - } - return (Map) map; - } - - private PublishStatus resolvePendingStatus(String actionType) { - return ApprovalActionType.DELETE.getCode().equals(actionType) - ? PublishStatus.DELETE_PENDING - : PublishStatus.PUBLISH_PENDING; - } - - private void clearPendingStatus(BigInteger botId) { - Bot bot = botService.getById(botId); - if (bot == null) { - return; - } - Bot update = new Bot(); - update.setId(botId); - update.setCurrentApprovalInstanceId(null); - update.setPublishStatus(bot.getPublishedSnapshotJson() == null || bot.getPublishedSnapshotJson().isEmpty() - ? PublishStatus.DRAFT.getCode() - : PublishStatus.PUBLISHED.getCode()); - botService.updateById(update); - } - private void removeBotRelations(BigInteger botId) { botDocumentCollectionService.remove(QueryWrapper.create().eq(BotDocumentCollection::getBotId, botId)); botWorkflowService.remove(QueryWrapper.create().eq(BotWorkflow::getBotId, botId)); botPluginService.remove(QueryWrapper.create().eq(BotPlugin::getBotId, botId)); botMcpService.remove(QueryWrapper.create().eq(BotMcp::getBotId, botId)); } + + private BotCategory resolveCategory(BigInteger categoryId) { + if (categoryId == null) { + return null; + } + return botCategoryService.getById(categoryId); + } + + private SysDept resolveDept(BigInteger deptId) { + if (deptId == null) { + return null; + } + return sysDeptService.getById(deptId); + } + + private Model resolveModel(BigInteger modelId) { + if (modelId == null) { + throw new BusinessException("聊天助手未配置模型,无法提交审批"); + } + Model model = modelService.getById(modelId); + if (model == null) { + throw new BusinessException("聊天助手关联模型不存在,无法提交审批"); + } + return model; + } + + private String resolveModelName(Model model) { + String providerName = model.getModelProvider() == null ? null : model.getModelProvider().getProviderName(); + if (providerName == null || providerName.isBlank()) { + return model.getModelName(); + } + return providerName + " / " + model.getModelName(); + } + + private String resolveSystemPrompt(Bot bot) { + if (bot.getModelOptions() == null) { + return null; + } + Object value = bot.getModelOptions().get("systemPrompt"); + return value == null ? null : String.valueOf(value); + } + + private Number readNumberOption(Map options, String key) { + if (options == null || !options.containsKey(key)) { + return null; + } + Object value = options.get(key); + if (value instanceof Number number) { + return number; + } + if (value instanceof String string && !string.isBlank()) { + try { + return Double.valueOf(string); + } catch (NumberFormatException ignore) { + return null; + } + } + return null; + } } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/BotPublishAppService.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/BotPublishAppService.java index 5e8bccd..6cabde7 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/BotPublishAppService.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/BotPublishAppService.java @@ -1,8 +1,11 @@ package tech.easyflow.ai.publish; import org.springframework.stereotype.Service; -import tech.easyflow.approval.annotation.ApprovalAction; +import tech.easyflow.approval.entity.vo.ApprovalActionResult; +import tech.easyflow.approval.enums.ApprovalActionType; +import tech.easyflow.approval.enums.ApprovalResourceType; import tech.easyflow.common.web.exceptions.BusinessException; +import tech.easyflow.common.satoken.util.SaTokenUtil; import java.math.BigInteger; @@ -12,36 +15,58 @@ import java.math.BigInteger; @Service public class BotPublishAppService { + private final AiResourceLifecycleService aiResourceLifecycleService; + + public BotPublishAppService(AiResourceLifecycleService aiResourceLifecycleService) { + this.aiResourceLifecycleService = aiResourceLifecycleService; + } + /** * 提交聊天助手发布审批。 * * @param id 助手 ID - * @return 助手 ID + * @return 动作执行结果 */ - @ApprovalAction( - resourceType = "BOT", - actionType = "PUBLISH", - idExpr = "#id" - ) - public BigInteger submitPublishApproval(BigInteger id) { + public ApprovalActionResult submitPublishApproval(BigInteger id) { assertId(id); - return id; + return aiResourceLifecycleService.submitAction( + ApprovalResourceType.BOT.getCode(), + id, + ApprovalActionType.PUBLISH.getCode(), + SaTokenUtil.getLoginAccount().getId() + ); + } + + /** + * 提交聊天助手下线审批。 + * + * @param id 助手 ID + * @return 动作执行结果 + */ + public ApprovalActionResult submitOfflineApproval(BigInteger id) { + assertId(id); + return aiResourceLifecycleService.submitAction( + ApprovalResourceType.BOT.getCode(), + id, + ApprovalActionType.OFFLINE.getCode(), + SaTokenUtil.getLoginAccount().getId() + ); } /** * 提交聊天助手删除审批。 * * @param id 助手 ID - * @return 助手 ID + * @return 动作执行结果 */ - @ApprovalAction( - resourceType = "BOT", - actionType = "DELETE", - idExpr = "#id" - ) - public BigInteger submitDeleteApproval(BigInteger id) { + public ApprovalActionResult submitDeleteApproval(BigInteger id) { assertId(id); - return id; + return aiResourceLifecycleService.submitAction( + ApprovalResourceType.BOT.getCode(), + id, + ApprovalActionType.DELETE.getCode(), + SaTokenUtil.getLoginAccount().getId() + ); } private void assertId(BigInteger id) { diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/KnowledgeApprovalSubjectHandler.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/KnowledgeApprovalSubjectHandler.java index da604ca..9dd39b9 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/KnowledgeApprovalSubjectHandler.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/KnowledgeApprovalSubjectHandler.java @@ -1,6 +1,6 @@ package tech.easyflow.ai.publish; -import com.alibaba.fastjson2.JSON; +import com.fasterxml.jackson.databind.ObjectMapper; import com.mybatisflex.core.query.QueryWrapper; import org.springframework.stereotype.Component; import tech.easyflow.ai.entity.BotDocumentCollection; @@ -12,43 +12,36 @@ import tech.easyflow.ai.service.BotDocumentCollectionService; import tech.easyflow.ai.service.DocumentCollectionCategoryService; import tech.easyflow.ai.service.DocumentCollectionService; import tech.easyflow.ai.service.ModelService; -import tech.easyflow.approval.entity.ApprovalInstance; -import tech.easyflow.approval.entity.vo.ApprovalCallbackContext; -import tech.easyflow.approval.entity.vo.ApprovalSubmitCallbackContext; -import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest; -import tech.easyflow.approval.enums.ApprovalActionType; -import tech.easyflow.approval.enums.ApprovalResourceType; +import tech.easyflow.ai.service.ResourceOfflineImpactService; +import tech.easyflow.ai.vo.OfflineImpactCheckVo; import tech.easyflow.approval.service.ApprovalInstanceService; -import tech.easyflow.approval.service.ApprovalSubjectHandler; +import tech.easyflow.approval.enums.ApprovalResourceType; import tech.easyflow.common.web.exceptions.BusinessException; import tech.easyflow.system.entity.SysDept; import tech.easyflow.system.enums.CategoryResourceType; import tech.easyflow.system.enums.ResourceAction; import tech.easyflow.system.enums.VisibilityScope; -import tech.easyflow.system.service.SysDeptService; import tech.easyflow.system.service.ResourceAccessService; +import tech.easyflow.system.service.SysDeptService; import java.math.BigInteger; import java.util.Collections; -import java.util.Date; import java.util.LinkedHashMap; import java.util.Map; /** - * 知识库审批处理器。 + * 知识库生命周期处理器。 */ @Component -public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler { - - private static final String SNAPSHOT_KEY = "resourceSnapshot"; +public class KnowledgeApprovalSubjectHandler extends AbstractAiResourceLifecycleHandler { private final DocumentCollectionService documentCollectionService; private final ResourceAccessService resourceAccessService; - private final ApprovalInstanceService approvalInstanceService; private final BotDocumentCollectionService botDocumentCollectionService; private final ModelService modelService; private final DocumentCollectionCategoryService documentCollectionCategoryService; private final SysDeptService sysDeptService; + private final ResourceOfflineImpactService resourceOfflineImpactService; public KnowledgeApprovalSubjectHandler(DocumentCollectionService documentCollectionService, ResourceAccessService resourceAccessService, @@ -56,14 +49,17 @@ public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler { BotDocumentCollectionService botDocumentCollectionService, ModelService modelService, DocumentCollectionCategoryService documentCollectionCategoryService, - SysDeptService sysDeptService) { + SysDeptService sysDeptService, + ResourceOfflineImpactService resourceOfflineImpactService, + ObjectMapper objectMapper) { + super(approvalInstanceService, objectMapper); this.documentCollectionService = documentCollectionService; this.resourceAccessService = resourceAccessService; - this.approvalInstanceService = approvalInstanceService; this.botDocumentCollectionService = botDocumentCollectionService; this.modelService = modelService; this.documentCollectionCategoryService = documentCollectionCategoryService; this.sysDeptService = sysDeptService; + this.resourceOfflineImpactService = resourceOfflineImpactService; } @Override @@ -71,72 +67,6 @@ public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler { return ApprovalResourceType.KNOWLEDGE.getCode(); } - @Override - public ApprovalSubmitRequest buildSubmitRequest(BigInteger resourceId, String actionType, BigInteger operatorId) { - DocumentCollection knowledge = requireKnowledge(resourceId); - resourceAccessService.assertAccess(CategoryResourceType.KNOWLEDGE, knowledge, ResourceAction.MANAGE, "无权限管理知识库"); - if (approvalInstanceService.existsActiveInstance(resourceType(), resourceId)) { - throw new BusinessException("当前知识库存在未结束审批,请先处理完成"); - } - - ApprovalActionType approvalActionType = ApprovalActionType.from(actionType); - Map resourceSnapshot = buildResourceSnapshot(knowledge); - if (approvalActionType == ApprovalActionType.PUBLISH - && JSON.toJSONString(resourceSnapshot).equals(JSON.toJSONString(knowledge.getPublishedSnapshotJson()))) { - throw new BusinessException("当前知识库没有变更,无需重复发布"); - } - if (approvalActionType == ApprovalActionType.DELETE && hasBotBinding(resourceId)) { - throw new BusinessException("此知识库还关联着bot,请先取消关联!"); - } - - ApprovalSubmitRequest request = new ApprovalSubmitRequest(); - request.setResourceType(resourceType()); - request.setResourceId(resourceId); - request.setActionType(approvalActionType.getCode()); - request.setApplicantId(operatorId); - request.setCategoryId(knowledge.getCategoryId()); - request.setDeptId(knowledge.getDeptId()); - request.setSummary((approvalActionType == ApprovalActionType.PUBLISH ? "发布" : "删除") + "知识库:" + knowledge.getTitle()); - request.setSnapshotJson(Map.of(SNAPSHOT_KEY, resourceSnapshot)); - return request; - } - - @Override - public void onSubmitted(ApprovalSubmitCallbackContext context) { - DocumentCollection update = new DocumentCollection(); - update.setId(context.getResourceId()); - update.setCurrentApprovalInstanceId(context.getInstanceId()); - update.setPublishStatus(resolvePendingStatus(context.getActionType()).getCode()); - documentCollectionService.updateById(update); - } - - @Override - public void onApproved(ApprovalCallbackContext context) { - ApprovalInstance instance = context.getInstance(); - if (ApprovalActionType.PUBLISH.getCode().equals(instance.getActionType())) { - DocumentCollection update = new DocumentCollection(); - update.setId(instance.getResourceId()); - update.setPublishStatus(PublishStatus.PUBLISHED.getCode()); - update.setCurrentApprovalInstanceId(null); - update.setPublishedSnapshotJson(readResourceSnapshot(instance)); - update.setPublishedAt(new Date()); - update.setPublishedBy(context.getOperatorId()); - documentCollectionService.updateById(update); - return; - } - documentCollectionService.removeById(instance.getResourceId()); - } - - @Override - public void onRejected(ApprovalCallbackContext context) { - clearPendingStatus(context.getInstance().getResourceId()); - } - - @Override - public void onRevoked(ApprovalCallbackContext context) { - clearPendingStatus(context.getInstance().getResourceId()); - } - @Override public void assertPublishedAccess(Object identifier, String denyMessage) { DocumentCollection collection = documentCollectionService.getDetail(String.valueOf(identifier)); @@ -146,20 +76,47 @@ public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler { } } - private DocumentCollection requireKnowledge(BigInteger id) { - DocumentCollection knowledge = documentCollectionService.getById(id); + @Override + protected DocumentCollection requireResource(BigInteger resourceId) { + DocumentCollection knowledge = documentCollectionService.getById(resourceId); if (knowledge == null) { throw new BusinessException("知识库不存在"); } return knowledge; } - private boolean hasBotBinding(BigInteger knowledgeId) { - QueryWrapper queryWrapper = QueryWrapper.create().eq(BotDocumentCollection::getDocumentCollectionId, knowledgeId); - return botDocumentCollectionService.exists(queryWrapper); + @Override + protected void assertManagePermission(DocumentCollection resource) { + resourceAccessService.assertAccess(CategoryResourceType.KNOWLEDGE, resource, ResourceAction.MANAGE, "无权限管理知识库"); } - private Map buildResourceSnapshot(DocumentCollection collection) { + @Override + protected BigInteger getCategoryId(DocumentCollection resource) { + return resource.getCategoryId(); + } + + @Override + protected BigInteger getDeptId(DocumentCollection resource) { + return resource.getDeptId(); + } + + @Override + protected String getTitle(DocumentCollection resource) { + return resource.getTitle(); + } + + @Override + protected PublishStatus getCurrentStatus(DocumentCollection resource) { + return PublishStatus.from(resource.getPublishStatus()); + } + + @Override + protected Map getPublishedSnapshot(DocumentCollection resource) { + return resource.getPublishedSnapshotJson(); + } + + @Override + protected Map buildResourceSnapshot(DocumentCollection collection) { Model vectorModel = resolveModel(collection.getVectorEmbedModelId(), "知识库向量模型不存在,无法提交审批"); Model rerankModel = resolveOptionalModel(collection.getRerankModelId(), "知识库重排模型不存在,无法提交审批"); DocumentCollectionCategory category = resolveCategory(collection.getCategoryId()); @@ -197,12 +154,82 @@ public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler { return snapshot; } - /** - * 解析知识库分类信息。 - * - * @param categoryId 分类 ID - * @return 分类实体,不存在时返回 {@code null} - */ + @Override + protected void persistResourceState(BigInteger resourceId, PublishStatus publishStatus, BigInteger currentApprovalInstanceId) { + DocumentCollection update = new DocumentCollection(); + update.setId(resourceId); + update.setPublishStatus(publishStatus.getCode()); + update.setCurrentApprovalInstanceId(currentApprovalInstanceId); + documentCollectionService.updateById(update); + } + + @Override + protected void publishResource(BigInteger resourceId, Map resourceSnapshot, BigInteger operatorId) { + DocumentCollection update = new DocumentCollection(); + update.setId(resourceId); + update.setPublishStatus(PublishStatus.PUBLISHED.getCode()); + update.setCurrentApprovalInstanceId(null); + update.setPublishedSnapshotJson(resourceSnapshot); + update.setPublishedAt(new java.util.Date()); + update.setPublishedBy(operatorId); + documentCollectionService.updateById(update); + } + + @Override + protected void markResourceOffline(BigInteger resourceId) { + DocumentCollection update = new DocumentCollection(); + update.setId(resourceId); + update.setPublishStatus(PublishStatus.OFFLINE.getCode()); + update.setCurrentApprovalInstanceId(null); + documentCollectionService.updateById(update); + } + + @Override + protected void removeResource(BigInteger resourceId) { + documentCollectionService.removeById(resourceId); + } + + @Override + protected String resourceLabel() { + return "知识库"; + } + + @Override + protected void enrichOfflineSnapshot(DocumentCollection resource, Map snapshot) { + OfflineImpactCheckVo impact = resourceOfflineImpactService.checkKnowledgeImpact(resource.getId()); + if (!impact.isCanProceed()) { + throw new BusinessException(buildWorkflowUsageBlockMessage(impact)); + } + if (impact.isHasBotBindings()) { + snapshot.put("botBindings", impact.getBotBindings()); + } + } + + @Override + protected void validateDelete(DocumentCollection resource, PublishStatus currentStatus) { + if (hasBotBinding(resource.getId())) { + throw new BusinessException("此知识库还关联着bot,请先取消关联!"); + } + } + + @Override + protected void afterOffline(BigInteger resourceId) { + resourceOfflineImpactService.unbindKnowledgeFromBots(resourceId); + } + + private boolean hasBotBinding(BigInteger knowledgeId) { + QueryWrapper queryWrapper = QueryWrapper.create().eq(BotDocumentCollection::getDocumentCollectionId, knowledgeId); + return botDocumentCollectionService.exists(queryWrapper); + } + + private String buildWorkflowUsageBlockMessage(OfflineImpactCheckVo impact) { + String names = impact.getWorkflowUsages().stream() + .map(item -> item.getTitle() == null ? String.valueOf(item.getId()) : item.getTitle()) + .reduce((left, right) -> left + "、" + right) + .orElse("未知工作流"); + return "当前知识库被以下工作流使用:" + names + ",请先在工作流中调整解绑后再下线"; + } + private DocumentCollectionCategory resolveCategory(BigInteger categoryId) { if (categoryId == null) { return null; @@ -210,12 +237,6 @@ public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler { return documentCollectionCategoryService.getById(categoryId); } - /** - * 解析部门信息。 - * - * @param deptId 部门 ID - * @return 部门实体,不存在时返回 {@code null} - */ private SysDept resolveDept(BigInteger deptId) { if (deptId == null) { return null; @@ -223,13 +244,6 @@ public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler { return sysDeptService.getById(deptId); } - /** - * 解析必填模型。 - * - * @param modelId 模型 ID - * @param errorMessage 模型不存在时抛出的提示 - * @return 模型实体 - */ private Model resolveModel(BigInteger modelId, String errorMessage) { if (modelId == null) { throw new BusinessException(errorMessage); @@ -241,13 +255,6 @@ public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler { return model; } - /** - * 解析可选模型。 - * - * @param modelId 模型 ID - * @param errorMessage 模型不存在时抛出的提示 - * @return 模型实体,不存在时返回 {@code null} - */ private Model resolveOptionalModel(BigInteger modelId, String errorMessage) { if (modelId == null) { return null; @@ -259,75 +266,30 @@ public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler { return model; } - /** - * 生成模型展示名称。 - * - * @param model 模型实体 - * @return 模型名称 - */ private String resolveModelName(Model model) { if (model == null) { return null; } - if (model.getTitle() != null && !model.getTitle().isBlank()) { - return model.getTitle(); + String providerName = model.getModelProvider() == null ? null : model.getModelProvider().getProviderName(); + if (providerName == null || providerName.isBlank()) { + return model.getModelName(); } - return model.getModelName(); + return providerName + " / " + model.getModelName(); } - /** - * 解析知识库可见范围文案。 - * - * @param visibilityScope 可见范围编码 - * @return 展示文案 - */ - private String resolveVisibilityScopeLabel(String visibilityScope) { - return switch (VisibilityScope.fromOrDefault(visibilityScope, VisibilityScope.PRIVATE)) { - case DEPT -> "部门"; - case PUBLIC -> "公开"; - default -> "个人"; - }; - } - - /** - * 解析知识库类型文案。 - * - * @param collectionType 知识库类型 - * @return 展示文案 - */ private String resolveCollectionTypeLabel(String collectionType) { - if (DocumentCollection.TYPE_FAQ.equalsIgnoreCase(collectionType)) { + if ("FAQ".equalsIgnoreCase(collectionType)) { return "FAQ"; } return "文档"; } - @SuppressWarnings("unchecked") - private Map readResourceSnapshot(ApprovalInstance instance) { - Object snapshot = instance.getSnapshotJson() == null ? null : instance.getSnapshotJson().get(SNAPSHOT_KEY); - if (!(snapshot instanceof Map map)) { - throw new BusinessException("审批快照缺少知识库发布内容"); - } - return (Map) map; - } - - private PublishStatus resolvePendingStatus(String actionType) { - return ApprovalActionType.DELETE.getCode().equals(actionType) - ? PublishStatus.DELETE_PENDING - : PublishStatus.PUBLISH_PENDING; - } - - private void clearPendingStatus(BigInteger knowledgeId) { - DocumentCollection collection = documentCollectionService.getById(knowledgeId); - if (collection == null) { - return; - } - DocumentCollection update = new DocumentCollection(); - update.setId(knowledgeId); - update.setCurrentApprovalInstanceId(null); - update.setPublishStatus(collection.getPublishedSnapshotJson() == null || collection.getPublishedSnapshotJson().isEmpty() - ? PublishStatus.DRAFT.getCode() - : PublishStatus.PUBLISHED.getCode()); - documentCollectionService.updateById(update); + private String resolveVisibilityScopeLabel(String visibilityScope) { + VisibilityScope scope = VisibilityScope.from(visibilityScope); + return switch (scope) { + case PRIVATE -> "仅自己"; + case DEPT -> "本部门"; + case PUBLIC -> "公开"; + }; } } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/KnowledgePublishAppService.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/KnowledgePublishAppService.java index f1a05ea..c25847a 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/KnowledgePublishAppService.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/KnowledgePublishAppService.java @@ -1,8 +1,16 @@ package tech.easyflow.ai.publish; import org.springframework.stereotype.Service; -import tech.easyflow.approval.annotation.ApprovalAction; +import tech.easyflow.ai.entity.DocumentCollection; +import tech.easyflow.ai.enums.PublishStatus; +import tech.easyflow.ai.service.DocumentCollectionService; +import tech.easyflow.ai.service.ResourceOfflineImpactService; +import tech.easyflow.ai.vo.OfflineImpactCheckVo; +import tech.easyflow.approval.entity.vo.ApprovalActionResult; +import tech.easyflow.approval.enums.ApprovalActionType; +import tech.easyflow.approval.enums.ApprovalResourceType; import tech.easyflow.common.web.exceptions.BusinessException; +import tech.easyflow.common.satoken.util.SaTokenUtil; import java.math.BigInteger; @@ -12,36 +20,82 @@ import java.math.BigInteger; @Service public class KnowledgePublishAppService { + private final DocumentCollectionService documentCollectionService; + private final ResourceOfflineImpactService resourceOfflineImpactService; + private final AiResourceLifecycleService aiResourceLifecycleService; + + public KnowledgePublishAppService(DocumentCollectionService documentCollectionService, + ResourceOfflineImpactService resourceOfflineImpactService, + AiResourceLifecycleService aiResourceLifecycleService) { + this.documentCollectionService = documentCollectionService; + this.resourceOfflineImpactService = resourceOfflineImpactService; + this.aiResourceLifecycleService = aiResourceLifecycleService; + } + /** * 提交知识库发布审批。 * * @param id 知识库 ID - * @return 知识库 ID + * @return 动作执行结果 */ - @ApprovalAction( - resourceType = "KNOWLEDGE", - actionType = "PUBLISH", - idExpr = "#id" - ) - public BigInteger submitPublishApproval(BigInteger id) { + public ApprovalActionResult submitPublishApproval(BigInteger id) { assertId(id); - return id; + return aiResourceLifecycleService.submitAction( + ApprovalResourceType.KNOWLEDGE.getCode(), + id, + ApprovalActionType.PUBLISH.getCode(), + SaTokenUtil.getLoginAccount().getId() + ); + } + + /** + * 提交知识库下线审批。 + * + * @param id 知识库 ID + * @return 动作执行结果 + */ + public ApprovalActionResult submitOfflineApproval(BigInteger id) { + assertId(id); + return aiResourceLifecycleService.submitAction( + ApprovalResourceType.KNOWLEDGE.getCode(), + id, + ApprovalActionType.OFFLINE.getCode(), + SaTokenUtil.getLoginAccount().getId() + ); + } + + /** + * 检查知识库下线影响。 + * + * @param id 知识库 ID + * @return 下线影响结果 + */ + public OfflineImpactCheckVo checkOfflineImpact(BigInteger id) { + assertId(id); + DocumentCollection knowledge = documentCollectionService.getById(id); + if (knowledge == null) { + throw new BusinessException("知识库不存在"); + } + if (PublishStatus.from(knowledge.getPublishStatus()) != PublishStatus.PUBLISHED) { + throw new BusinessException("当前知识库尚未发布,无法下线"); + } + return resourceOfflineImpactService.checkKnowledgeImpact(id); } /** * 提交知识库删除审批。 * * @param id 知识库 ID - * @return 知识库 ID + * @return 动作执行结果 */ - @ApprovalAction( - resourceType = "KNOWLEDGE", - actionType = "DELETE", - idExpr = "#id" - ) - public BigInteger submitDeleteApproval(BigInteger id) { + public ApprovalActionResult submitDeleteApproval(BigInteger id) { assertId(id); - return id; + return aiResourceLifecycleService.submitAction( + ApprovalResourceType.KNOWLEDGE.getCode(), + id, + ApprovalActionType.DELETE.getCode(), + SaTokenUtil.getLoginAccount().getId() + ); } private void assertId(BigInteger id) { diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/WorkflowApprovalSubjectHandler.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/WorkflowApprovalSubjectHandler.java index 709b3ce..45388b5 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/WorkflowApprovalSubjectHandler.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/WorkflowApprovalSubjectHandler.java @@ -1,52 +1,48 @@ package tech.easyflow.ai.publish; -import com.alibaba.fastjson2.JSON; +import com.fasterxml.jackson.databind.ObjectMapper; import com.mybatisflex.core.query.QueryWrapper; import org.springframework.stereotype.Component; import tech.easyflow.ai.entity.BotWorkflow; import tech.easyflow.ai.entity.Workflow; import tech.easyflow.ai.enums.PublishStatus; import tech.easyflow.ai.service.BotWorkflowService; +import tech.easyflow.ai.service.ResourceOfflineImpactService; import tech.easyflow.ai.service.WorkflowService; -import tech.easyflow.approval.entity.ApprovalInstance; -import tech.easyflow.approval.entity.vo.ApprovalCallbackContext; -import tech.easyflow.approval.entity.vo.ApprovalSubmitCallbackContext; -import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest; -import tech.easyflow.approval.enums.ApprovalActionType; -import tech.easyflow.approval.enums.ApprovalResourceType; +import tech.easyflow.ai.vo.OfflineImpactCheckVo; import tech.easyflow.approval.service.ApprovalInstanceService; -import tech.easyflow.approval.service.ApprovalSubjectHandler; +import tech.easyflow.approval.enums.ApprovalResourceType; import tech.easyflow.common.web.exceptions.BusinessException; 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.Date; import java.util.LinkedHashMap; import java.util.Map; /** - * 工作流审批处理器。 + * 工作流生命周期处理器。 */ @Component -public class WorkflowApprovalSubjectHandler implements ApprovalSubjectHandler { - - private static final String SNAPSHOT_KEY = "resourceSnapshot"; +public class WorkflowApprovalSubjectHandler extends AbstractAiResourceLifecycleHandler { private final WorkflowService workflowService; private final ResourceAccessService resourceAccessService; - private final ApprovalInstanceService approvalInstanceService; private final BotWorkflowService botWorkflowService; + private final ResourceOfflineImpactService resourceOfflineImpactService; public WorkflowApprovalSubjectHandler(WorkflowService workflowService, ResourceAccessService resourceAccessService, ApprovalInstanceService approvalInstanceService, - BotWorkflowService botWorkflowService) { + BotWorkflowService botWorkflowService, + ResourceOfflineImpactService resourceOfflineImpactService, + ObjectMapper objectMapper) { + super(approvalInstanceService, objectMapper); this.workflowService = workflowService; this.resourceAccessService = resourceAccessService; - this.approvalInstanceService = approvalInstanceService; this.botWorkflowService = botWorkflowService; + this.resourceOfflineImpactService = resourceOfflineImpactService; } @Override @@ -54,72 +50,6 @@ public class WorkflowApprovalSubjectHandler implements ApprovalSubjectHandler { return ApprovalResourceType.WORKFLOW.getCode(); } - @Override - public ApprovalSubmitRequest buildSubmitRequest(BigInteger resourceId, String actionType, BigInteger operatorId) { - Workflow workflow = requireWorkflow(resourceId); - resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, workflow, ResourceAction.MANAGE, "无权限管理工作流"); - if (approvalInstanceService.existsActiveInstance(resourceType(), resourceId)) { - throw new BusinessException("当前工作流存在未结束审批,请先处理完成"); - } - - ApprovalActionType approvalActionType = ApprovalActionType.from(actionType); - Map resourceSnapshot = buildResourceSnapshot(workflow); - if (approvalActionType == ApprovalActionType.PUBLISH - && JSON.toJSONString(resourceSnapshot).equals(JSON.toJSONString(workflow.getPublishedSnapshotJson()))) { - throw new BusinessException("当前工作流没有变更,无需重复发布"); - } - if (approvalActionType == ApprovalActionType.DELETE && hasBotBinding(resourceId)) { - throw new BusinessException("此工作流还关联有bot,请先取消关联后再删除!"); - } - - ApprovalSubmitRequest request = new ApprovalSubmitRequest(); - request.setResourceType(resourceType()); - request.setResourceId(resourceId); - request.setActionType(approvalActionType.getCode()); - request.setApplicantId(operatorId); - request.setCategoryId(workflow.getCategoryId()); - request.setDeptId(workflow.getDeptId()); - request.setSummary((approvalActionType == ApprovalActionType.PUBLISH ? "发布" : "删除") + "工作流:" + workflow.getTitle()); - request.setSnapshotJson(Map.of(SNAPSHOT_KEY, resourceSnapshot)); - return request; - } - - @Override - public void onSubmitted(ApprovalSubmitCallbackContext context) { - Workflow update = new Workflow(); - update.setId(context.getResourceId()); - update.setCurrentApprovalInstanceId(context.getInstanceId()); - update.setPublishStatus(resolvePendingStatus(context.getActionType()).getCode()); - workflowService.updateById(update); - } - - @Override - public void onApproved(ApprovalCallbackContext context) { - ApprovalInstance instance = context.getInstance(); - if (ApprovalActionType.PUBLISH.getCode().equals(instance.getActionType())) { - Workflow update = new Workflow(); - update.setId(instance.getResourceId()); - update.setPublishStatus(PublishStatus.PUBLISHED.getCode()); - update.setCurrentApprovalInstanceId(null); - update.setPublishedSnapshotJson(readResourceSnapshot(instance)); - update.setPublishedAt(new Date()); - update.setPublishedBy(context.getOperatorId()); - workflowService.updateById(update); - return; - } - workflowService.removeById(instance.getResourceId()); - } - - @Override - public void onRejected(ApprovalCallbackContext context) { - clearPendingStatus(context.getInstance().getResourceId()); - } - - @Override - public void onRevoked(ApprovalCallbackContext context) { - clearPendingStatus(context.getInstance().getResourceId()); - } - @Override public void assertPublishedAccess(Object identifier, String denyMessage) { Workflow workflow = workflowService.getDetail(String.valueOf(identifier)); @@ -129,62 +59,125 @@ public class WorkflowApprovalSubjectHandler implements ApprovalSubjectHandler { } } - private Workflow requireWorkflow(BigInteger id) { - Workflow workflow = workflowService.getById(id); + @Override + protected Workflow requireResource(BigInteger resourceId) { + Workflow workflow = workflowService.getById(resourceId); if (workflow == null) { throw new BusinessException("工作流不存在"); } return workflow; } + @Override + protected void assertManagePermission(Workflow resource) { + resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, resource, ResourceAction.MANAGE, "无权限管理工作流"); + } + + @Override + protected BigInteger getCategoryId(Workflow resource) { + return resource.getCategoryId(); + } + + @Override + protected BigInteger getDeptId(Workflow resource) { + return resource.getDeptId(); + } + + @Override + protected String getTitle(Workflow resource) { + return resource.getTitle(); + } + + @Override + protected PublishStatus getCurrentStatus(Workflow resource) { + return PublishStatus.from(resource.getPublishStatus()); + } + + @Override + protected Map getPublishedSnapshot(Workflow resource) { + return resource.getPublishedSnapshotJson(); + } + + @Override + protected Map buildResourceSnapshot(Workflow resource) { + Map snapshot = new LinkedHashMap<>(); + snapshot.put("id", resource.getId()); + snapshot.put("alias", resource.getAlias()); + snapshot.put("deptId", resource.getDeptId()); + snapshot.put("tenantId", resource.getTenantId()); + snapshot.put("title", resource.getTitle()); + snapshot.put("description", resource.getDescription()); + snapshot.put("icon", resource.getIcon()); + snapshot.put("content", resource.getContent()); + snapshot.put("englishName", resource.getEnglishName()); + snapshot.put("status", resource.getStatus()); + snapshot.put("categoryId", resource.getCategoryId()); + snapshot.put("visibilityScope", resource.getVisibilityScope()); + return snapshot; + } + + @Override + protected void persistResourceState(BigInteger resourceId, PublishStatus publishStatus, BigInteger currentApprovalInstanceId) { + Workflow update = new Workflow(); + update.setId(resourceId); + update.setPublishStatus(publishStatus.getCode()); + update.setCurrentApprovalInstanceId(currentApprovalInstanceId); + workflowService.updateById(update); + } + + @Override + protected void publishResource(BigInteger resourceId, Map resourceSnapshot, BigInteger operatorId) { + Workflow update = new Workflow(); + update.setId(resourceId); + update.setPublishStatus(PublishStatus.PUBLISHED.getCode()); + update.setCurrentApprovalInstanceId(null); + update.setPublishedSnapshotJson(resourceSnapshot); + update.setPublishedAt(new java.util.Date()); + update.setPublishedBy(operatorId); + workflowService.updateById(update); + } + + @Override + protected void markResourceOffline(BigInteger resourceId) { + Workflow update = new Workflow(); + update.setId(resourceId); + update.setPublishStatus(PublishStatus.OFFLINE.getCode()); + update.setCurrentApprovalInstanceId(null); + workflowService.updateById(update); + } + + @Override + protected void removeResource(BigInteger resourceId) { + workflowService.removeById(resourceId); + } + + @Override + protected String resourceLabel() { + return "工作流"; + } + + @Override + protected void enrichOfflineSnapshot(Workflow resource, Map snapshot) { + OfflineImpactCheckVo impact = resourceOfflineImpactService.checkWorkflowImpact(resource.getId()); + if (impact.isHasBotBindings()) { + snapshot.put("botBindings", impact.getBotBindings()); + } + } + + @Override + protected void validateDelete(Workflow resource, PublishStatus currentStatus) { + if (hasBotBinding(resource.getId())) { + throw new BusinessException("此工作流还关联有bot,请先取消关联后再删除!"); + } + } + + @Override + protected void afterOffline(BigInteger resourceId) { + resourceOfflineImpactService.unbindWorkflowFromBots(resourceId); + } + private boolean hasBotBinding(BigInteger workflowId) { QueryWrapper queryWrapper = QueryWrapper.create().eq(BotWorkflow::getWorkflowId, workflowId); return botWorkflowService.exists(queryWrapper); } - - private Map buildResourceSnapshot(Workflow workflow) { - Map snapshot = new LinkedHashMap<>(); - snapshot.put("id", workflow.getId()); - snapshot.put("alias", workflow.getAlias()); - snapshot.put("deptId", workflow.getDeptId()); - snapshot.put("tenantId", workflow.getTenantId()); - snapshot.put("title", workflow.getTitle()); - snapshot.put("description", workflow.getDescription()); - snapshot.put("icon", workflow.getIcon()); - snapshot.put("content", workflow.getContent()); - snapshot.put("englishName", workflow.getEnglishName()); - snapshot.put("status", workflow.getStatus()); - snapshot.put("categoryId", workflow.getCategoryId()); - snapshot.put("visibilityScope", workflow.getVisibilityScope()); - return snapshot; - } - - @SuppressWarnings("unchecked") - private Map readResourceSnapshot(ApprovalInstance instance) { - Object snapshot = instance.getSnapshotJson() == null ? null : instance.getSnapshotJson().get(SNAPSHOT_KEY); - if (!(snapshot instanceof Map map)) { - throw new BusinessException("审批快照缺少工作流发布内容"); - } - return (Map) map; - } - - private PublishStatus resolvePendingStatus(String actionType) { - return ApprovalActionType.DELETE.getCode().equals(actionType) - ? PublishStatus.DELETE_PENDING - : PublishStatus.PUBLISH_PENDING; - } - - private void clearPendingStatus(BigInteger workflowId) { - Workflow workflow = workflowService.getById(workflowId); - if (workflow == null) { - return; - } - Workflow update = new Workflow(); - update.setId(workflowId); - update.setCurrentApprovalInstanceId(null); - update.setPublishStatus(workflow.getPublishedSnapshotJson() == null || workflow.getPublishedSnapshotJson().isEmpty() - ? PublishStatus.DRAFT.getCode() - : PublishStatus.PUBLISHED.getCode()); - workflowService.updateById(update); - } } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/WorkflowPublishAppService.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/WorkflowPublishAppService.java index 5b31002..7e13fff 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/WorkflowPublishAppService.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/WorkflowPublishAppService.java @@ -1,10 +1,16 @@ package tech.easyflow.ai.publish; import org.springframework.stereotype.Service; -import tech.easyflow.approval.annotation.ApprovalAction; +import tech.easyflow.ai.entity.Workflow; +import tech.easyflow.ai.enums.PublishStatus; +import tech.easyflow.ai.service.ResourceOfflineImpactService; +import tech.easyflow.ai.service.WorkflowService; +import tech.easyflow.ai.vo.OfflineImpactCheckVo; +import tech.easyflow.approval.entity.vo.ApprovalActionResult; import tech.easyflow.approval.enums.ApprovalActionType; import tech.easyflow.approval.enums.ApprovalResourceType; import tech.easyflow.common.web.exceptions.BusinessException; +import tech.easyflow.common.satoken.util.SaTokenUtil; import java.math.BigInteger; @@ -14,36 +20,82 @@ import java.math.BigInteger; @Service public class WorkflowPublishAppService { + private final WorkflowService workflowService; + private final ResourceOfflineImpactService resourceOfflineImpactService; + private final AiResourceLifecycleService aiResourceLifecycleService; + + public WorkflowPublishAppService(WorkflowService workflowService, + ResourceOfflineImpactService resourceOfflineImpactService, + AiResourceLifecycleService aiResourceLifecycleService) { + this.workflowService = workflowService; + this.resourceOfflineImpactService = resourceOfflineImpactService; + this.aiResourceLifecycleService = aiResourceLifecycleService; + } + /** * 提交工作流发布审批。 * * @param id 工作流 ID - * @return 工作流 ID + * @return 动作执行结果 */ - @ApprovalAction( - resourceType = "WORKFLOW", - actionType = "PUBLISH", - idExpr = "#id" - ) - public BigInteger submitPublishApproval(BigInteger id) { + public ApprovalActionResult submitPublishApproval(BigInteger id) { assertId(id, ApprovalResourceType.WORKFLOW.getCode(), ApprovalActionType.PUBLISH.getCode()); - return id; + return aiResourceLifecycleService.submitAction( + ApprovalResourceType.WORKFLOW.getCode(), + id, + ApprovalActionType.PUBLISH.getCode(), + SaTokenUtil.getLoginAccount().getId() + ); + } + + /** + * 提交工作流下线审批。 + * + * @param id 工作流 ID + * @return 动作执行结果 + */ + public ApprovalActionResult submitOfflineApproval(BigInteger id) { + assertId(id, ApprovalResourceType.WORKFLOW.getCode(), ApprovalActionType.OFFLINE.getCode()); + return aiResourceLifecycleService.submitAction( + ApprovalResourceType.WORKFLOW.getCode(), + id, + ApprovalActionType.OFFLINE.getCode(), + SaTokenUtil.getLoginAccount().getId() + ); + } + + /** + * 检查工作流下线影响。 + * + * @param id 工作流 ID + * @return 下线影响结果 + */ + public OfflineImpactCheckVo checkOfflineImpact(BigInteger id) { + assertId(id, ApprovalResourceType.WORKFLOW.getCode(), ApprovalActionType.OFFLINE.getCode()); + Workflow workflow = workflowService.getById(id); + if (workflow == null) { + throw new BusinessException("工作流不存在"); + } + if (PublishStatus.from(workflow.getPublishStatus()) != PublishStatus.PUBLISHED) { + throw new BusinessException("当前工作流尚未发布,无法下线"); + } + return resourceOfflineImpactService.checkWorkflowImpact(id); } /** * 提交工作流删除审批。 * * @param id 工作流 ID - * @return 工作流 ID + * @return 动作执行结果 */ - @ApprovalAction( - resourceType = "WORKFLOW", - actionType = "DELETE", - idExpr = "#id" - ) - public BigInteger submitDeleteApproval(BigInteger id) { + public ApprovalActionResult submitDeleteApproval(BigInteger id) { assertId(id, ApprovalResourceType.WORKFLOW.getCode(), ApprovalActionType.DELETE.getCode()); - return id; + return aiResourceLifecycleService.submitAction( + ApprovalResourceType.WORKFLOW.getCode(), + id, + ApprovalActionType.DELETE.getCode(), + SaTokenUtil.getLoginAccount().getId() + ); } private void assertId(BigInteger id, String resourceType, String actionType) { diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/AiResourceApprovalStateService.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/AiResourceApprovalStateService.java new file mode 100644 index 0000000..38a6d69 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/AiResourceApprovalStateService.java @@ -0,0 +1,58 @@ +package tech.easyflow.ai.service; + +import tech.easyflow.ai.entity.Bot; +import tech.easyflow.ai.entity.DocumentCollection; +import tech.easyflow.ai.entity.Workflow; + +import java.util.Collection; + +/** + * AI 资源审批状态派生服务。 + *

+ * 该服务仅负责根据当前审批实例与资源真实状态派生只读展示字段, + * 不参与资源状态回写,也不在读链路中执行任何数据库修复操作。 + */ +public interface AiResourceApprovalStateService { + + /** + * 填充工作流审批展示状态。 + * + * @param workflow 工作流 + */ + void fillWorkflowApprovalState(Workflow workflow); + + /** + * 批量填充工作流审批展示状态。 + * + * @param workflows 工作流集合 + */ + void fillWorkflowApprovalState(Collection workflows); + + /** + * 填充知识库审批展示状态。 + * + * @param collection 知识库 + */ + void fillKnowledgeApprovalState(DocumentCollection collection); + + /** + * 批量填充知识库审批展示状态。 + * + * @param collections 知识库集合 + */ + void fillKnowledgeApprovalState(Collection collections); + + /** + * 填充聊天助手审批展示状态。 + * + * @param bot 聊天助手 + */ + void fillBotApprovalState(Bot bot); + + /** + * 批量填充聊天助手审批展示状态。 + * + * @param bots 聊天助手集合 + */ + void fillBotApprovalState(Collection bots); +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/ResourceOfflineImpactService.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/ResourceOfflineImpactService.java new file mode 100644 index 0000000..587d488 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/ResourceOfflineImpactService.java @@ -0,0 +1,41 @@ +package tech.easyflow.ai.service; + +import tech.easyflow.ai.vo.OfflineImpactCheckVo; + +import java.math.BigInteger; + +/** + * 资源下线影响检查与解绑服务。 + */ +public interface ResourceOfflineImpactService { + + /** + * 检查工作流下线影响。 + * + * @param workflowId 工作流 ID + * @return 下线影响结果 + */ + OfflineImpactCheckVo checkWorkflowImpact(BigInteger workflowId); + + /** + * 检查知识库下线影响。 + * + * @param knowledgeId 知识库 ID + * @return 下线影响结果 + */ + OfflineImpactCheckVo checkKnowledgeImpact(BigInteger knowledgeId); + + /** + * 工作流下线后,静默解绑所有关联 Bot。 + * + * @param workflowId 工作流 ID + */ + void unbindWorkflowFromBots(BigInteger workflowId); + + /** + * 知识库下线后,静默解绑所有关联 Bot。 + * + * @param knowledgeId 知识库 ID + */ + void unbindKnowledgeFromBots(BigInteger knowledgeId); +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/AiResourceApprovalStateServiceImpl.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/AiResourceApprovalStateServiceImpl.java new file mode 100644 index 0000000..bcf84f0 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/AiResourceApprovalStateServiceImpl.java @@ -0,0 +1,242 @@ +package tech.easyflow.ai.service.impl; + +import com.mybatisflex.core.query.QueryWrapper; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; +import tech.easyflow.ai.entity.Bot; +import tech.easyflow.ai.entity.DocumentCollection; +import tech.easyflow.ai.entity.Workflow; +import tech.easyflow.ai.enums.PublishStatus; +import tech.easyflow.ai.service.AiResourceApprovalStateService; +import tech.easyflow.approval.entity.ApprovalInstance; +import tech.easyflow.approval.enums.ApprovalActionType; +import tech.easyflow.approval.enums.ApprovalInstanceStatus; +import tech.easyflow.approval.enums.ApprovalResourceType; +import tech.easyflow.approval.mapper.ApprovalInstanceMapper; + +import java.math.BigInteger; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * AI 资源审批状态派生服务实现。 + */ +@Service +public class AiResourceApprovalStateServiceImpl implements AiResourceApprovalStateService { + + private final ApprovalInstanceMapper approvalInstanceMapper; + + public AiResourceApprovalStateServiceImpl(ApprovalInstanceMapper approvalInstanceMapper) { + this.approvalInstanceMapper = approvalInstanceMapper; + } + + /** + * {@inheritDoc} + */ + @Override + public void fillWorkflowApprovalState(Workflow workflow) { + fillWorkflowApprovalState(workflow == null ? List.of() : List.of(workflow)); + } + + /** + * {@inheritDoc} + */ + @Override + public void fillWorkflowApprovalState(Collection workflows) { + fillApprovalState( + workflows, + ApprovalResourceType.WORKFLOW.getCode(), + Workflow::getCurrentApprovalInstanceId, + workflow -> PublishStatus.from(workflow.getPublishStatus()), + Workflow::getPublishedSnapshotJson, + Workflow::setApprovalPending, + Workflow::setCurrentApprovalActionType, + Workflow::setDisplayPublishStatus + ); + } + + /** + * {@inheritDoc} + */ + @Override + public void fillKnowledgeApprovalState(DocumentCollection collection) { + fillKnowledgeApprovalState(collection == null ? List.of() : List.of(collection)); + } + + /** + * {@inheritDoc} + */ + @Override + public void fillKnowledgeApprovalState(Collection collections) { + fillApprovalState( + collections, + ApprovalResourceType.KNOWLEDGE.getCode(), + DocumentCollection::getCurrentApprovalInstanceId, + collection -> PublishStatus.from(collection.getPublishStatus()), + DocumentCollection::getPublishedSnapshotJson, + DocumentCollection::setApprovalPending, + DocumentCollection::setCurrentApprovalActionType, + DocumentCollection::setDisplayPublishStatus + ); + } + + /** + * {@inheritDoc} + */ + @Override + public void fillBotApprovalState(Bot bot) { + fillBotApprovalState(bot == null ? List.of() : List.of(bot)); + } + + /** + * {@inheritDoc} + */ + @Override + public void fillBotApprovalState(Collection bots) { + fillApprovalState( + bots, + ApprovalResourceType.BOT.getCode(), + Bot::getCurrentApprovalInstanceId, + bot -> PublishStatus.from(bot.getPublishStatus()), + Bot::getPublishedSnapshotJson, + Bot::setApprovalPending, + Bot::setCurrentApprovalActionType, + Bot::setDisplayPublishStatus + ); + } + + /** + * 统一派生审批展示状态。 + * + * @param resources 资源集合 + * @param resourceType 资源类型 + * @param instanceIdGetter 当前审批实例 ID 获取器 + * @param statusGetter 发布状态获取器 + * @param snapshotGetter 已发布快照获取器 + * @param pendingSetter 审批中标记写入器 + * @param actionSetter 当前审批动作写入器 + * @param displaySetter 展示状态写入器 + * @param 资源类型 + */ + private void fillApprovalState(Collection resources, + String resourceType, + Function instanceIdGetter, + Function statusGetter, + Function> snapshotGetter, + BiConsumer pendingSetter, + BiConsumer actionSetter, + BiConsumer displaySetter) { + if (CollectionUtils.isEmpty(resources)) { + return; + } + List validResources = resources.stream().filter(Objects::nonNull).toList(); + if (validResources.isEmpty()) { + return; + } + Map instanceMap = loadInstanceMap(validResources, instanceIdGetter); + for (T resource : validResources) { + PublishStatus currentStatus = statusGetter.apply(resource); + BigInteger instanceId = instanceIdGetter.apply(resource); + ApprovalInstance instance = instanceId == null ? null : instanceMap.get(instanceId); + if (!isValidCurrentInstance(resourceType, instance)) { + pendingSetter.accept(resource, false); + actionSetter.accept(resource, null); + displaySetter.accept(resource, resolveDisplayStatusWithoutActiveInstance(currentStatus, snapshotGetter.apply(resource)).getCode()); + continue; + } + + ApprovalInstanceStatus instanceStatus = ApprovalInstanceStatus.from(instance.getStatus()); + if (instanceStatus.isFinished()) { + pendingSetter.accept(resource, false); + actionSetter.accept(resource, null); + displaySetter.accept(resource, resolveDisplayStatusWithoutActiveInstance(currentStatus, snapshotGetter.apply(resource)).getCode()); + continue; + } + + ApprovalActionType actionType = ApprovalActionType.from(instance.getActionType()); + pendingSetter.accept(resource, true); + actionSetter.accept(resource, actionType.getCode()); + displaySetter.accept(resource, resolveDisplayStatusWithActiveInstance(currentStatus, actionType).getCode()); + } + } + + /** + * 批量加载当前审批实例。 + * + * @param resources 资源集合 + * @param instanceIdGetter 当前审批实例 ID 获取器 + * @param 资源类型 + * @return 审批实例映射 + */ + private Map loadInstanceMap(Collection resources, + Function instanceIdGetter) { + Set instanceIds = resources.stream() + .map(instanceIdGetter) + .filter(Objects::nonNull) + .collect(Collectors.toCollection(LinkedHashSet::new)); + if (instanceIds.isEmpty()) { + return Collections.emptyMap(); + } + List instances = approvalInstanceMapper.selectListByQuery( + QueryWrapper.create().in(ApprovalInstance::getId, instanceIds) + ); + return instances.stream().collect(Collectors.toMap(ApprovalInstance::getId, Function.identity())); + } + + /** + * 判断审批实例是否仍是当前资源的有效实例。 + * + * @param resourceType 资源类型 + * @param instance 审批实例 + * @return 有效返回 true + */ + private boolean isValidCurrentInstance(String resourceType, ApprovalInstance instance) { + return instance != null && resourceType.equals(instance.getResourceType()); + } + + /** + * 在没有活动审批实例时派生展示状态。 + * + * @param currentStatus 当前真实状态 + * @param publishedSnapshot 已发布快照 + * @return 展示状态 + */ + private PublishStatus resolveDisplayStatusWithoutActiveInstance(PublishStatus currentStatus, + Map publishedSnapshot) { + // 读接口不再猜测或掩盖真实状态。若资源表已经落成 pending 但当前找不到有效审批实例, + // 页面应直接看到真实状态,后续再由独立修复流程处理脏数据,而不是在展示层伪装成已发布。 + return currentStatus; + } + + /** + * 在存在活动审批实例时派生展示状态。 + * + * @param currentStatus 当前真实状态 + * @param actionType 当前审批动作 + * @return 展示状态 + */ + private PublishStatus resolveDisplayStatusWithActiveInstance(PublishStatus currentStatus, + ApprovalActionType actionType) { + if (currentStatus == PublishStatus.PUBLISHED && actionType == ApprovalActionType.PUBLISH) { + return PublishStatus.PUBLISH_PENDING; + } + if (currentStatus == PublishStatus.PUBLISH_PENDING + || currentStatus == PublishStatus.OFFLINE_PENDING + || currentStatus == PublishStatus.DELETE_PENDING) { + return currentStatus; + } + return switch (actionType) { + case PUBLISH -> PublishStatus.PUBLISH_PENDING; + case OFFLINE -> PublishStatus.OFFLINE_PENDING; + case DELETE -> PublishStatus.DELETE_PENDING; + }; + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/ResourceOfflineImpactServiceImpl.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/ResourceOfflineImpactServiceImpl.java new file mode 100644 index 0000000..fb7169e --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/ResourceOfflineImpactServiceImpl.java @@ -0,0 +1,299 @@ +package tech.easyflow.ai.service.impl; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.mybatisflex.core.query.QueryWrapper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; +import tech.easyflow.ai.entity.Bot; +import tech.easyflow.ai.entity.BotDocumentCollection; +import tech.easyflow.ai.entity.BotWorkflow; +import tech.easyflow.ai.entity.Workflow; +import tech.easyflow.ai.service.BotDocumentCollectionService; +import tech.easyflow.ai.service.BotService; +import tech.easyflow.ai.service.BotWorkflowService; +import tech.easyflow.ai.service.ResourceOfflineImpactService; +import tech.easyflow.ai.service.WorkflowService; +import tech.easyflow.ai.vo.OfflineImpactBindingVo; +import tech.easyflow.ai.vo.OfflineImpactCheckVo; +import tech.easyflow.common.cache.RedisLockExecutor; + +import java.math.BigInteger; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * 资源下线影响检查与 Bot 静默解绑实现。 + */ +@Service +public class ResourceOfflineImpactServiceImpl implements ResourceOfflineImpactService { + + private static final String KNOWLEDGE_NODE_TYPE = "knowledgeNode"; + private static final String BOT_BINDING_LOCK_KEY_PREFIX = "easyflow:lock:bot:binding:"; + private static final Duration LOCK_WAIT_TIMEOUT = Duration.ofSeconds(2); + private static final Duration LOCK_LEASE_TIMEOUT = Duration.ofSeconds(10); + + private final BotWorkflowService botWorkflowService; + private final BotDocumentCollectionService botDocumentCollectionService; + private final BotService botService; + private final WorkflowService workflowService; + private final RedisLockExecutor redisLockExecutor; + + public ResourceOfflineImpactServiceImpl(BotWorkflowService botWorkflowService, + BotDocumentCollectionService botDocumentCollectionService, + BotService botService, + WorkflowService workflowService, + RedisLockExecutor redisLockExecutor) { + this.botWorkflowService = botWorkflowService; + this.botDocumentCollectionService = botDocumentCollectionService; + this.botService = botService; + this.workflowService = workflowService; + this.redisLockExecutor = redisLockExecutor; + } + + /** + * {@inheritDoc} + */ + @Override + public OfflineImpactCheckVo checkWorkflowImpact(BigInteger workflowId) { + List botBindings = listBotsByWorkflowId(workflowId); + OfflineImpactCheckVo result = new OfflineImpactCheckVo(); + result.setCanProceed(true); + result.setBotBindings(botBindings); + result.setHasBotBindings(!botBindings.isEmpty()); + result.setWorkflowUsages(Collections.emptyList()); + result.setHasWorkflowUsages(false); + result.setMessage(botBindings.isEmpty() + ? "当前工作流下线后不会影响已有绑定" + : "当前工作流下线成功后,将自动从相关聊天助手中解绑"); + return result; + } + + /** + * {@inheritDoc} + */ + @Override + public OfflineImpactCheckVo checkKnowledgeImpact(BigInteger knowledgeId) { + List botBindings = listBotsByKnowledgeId(knowledgeId); + List workflowUsages = listWorkflowsUsingKnowledge(knowledgeId); + OfflineImpactCheckVo result = new OfflineImpactCheckVo(); + result.setBotBindings(botBindings); + result.setHasBotBindings(!botBindings.isEmpty()); + result.setWorkflowUsages(workflowUsages); + result.setHasWorkflowUsages(!workflowUsages.isEmpty()); + result.setCanProceed(workflowUsages.isEmpty()); + result.setMessage(workflowUsages.isEmpty() + ? (botBindings.isEmpty() + ? "当前知识库下线后不会影响已有绑定" + : "当前知识库下线成功后,将自动从相关聊天助手中解绑") + : "当前知识库仍被工作流使用,请先调整工作流后再下线"); + return result; + } + + /** + * {@inheritDoc} + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void unbindWorkflowFromBots(BigInteger workflowId) { + List relations = botWorkflowService.list(QueryWrapper.create() + .eq(BotWorkflow::getWorkflowId, workflowId)); + Set botIds = collectBotIds(relations, BotWorkflow::getBotId); + for (BigInteger botId : botIds) { + redisLockExecutor.executeWithLock( + BOT_BINDING_LOCK_KEY_PREFIX + botId, + LOCK_WAIT_TIMEOUT, + LOCK_LEASE_TIMEOUT, + () -> { + botWorkflowService.remove(QueryWrapper.create() + .eq(BotWorkflow::getBotId, botId) + .eq(BotWorkflow::getWorkflowId, workflowId)); + trimPublishedSnapshotBindings(botId, "workflowBindings", "workflowId", workflowId); + } + ); + } + } + + /** + * {@inheritDoc} + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void unbindKnowledgeFromBots(BigInteger knowledgeId) { + List relations = botDocumentCollectionService.list(QueryWrapper.create() + .eq(BotDocumentCollection::getDocumentCollectionId, knowledgeId)); + Set botIds = collectBotIds(relations, BotDocumentCollection::getBotId); + for (BigInteger botId : botIds) { + redisLockExecutor.executeWithLock( + BOT_BINDING_LOCK_KEY_PREFIX + botId, + LOCK_WAIT_TIMEOUT, + LOCK_LEASE_TIMEOUT, + () -> { + botDocumentCollectionService.remove(QueryWrapper.create() + .eq(BotDocumentCollection::getBotId, botId) + .eq(BotDocumentCollection::getDocumentCollectionId, knowledgeId)); + trimPublishedSnapshotBindings(botId, "knowledgeBindings", "knowledgeId", knowledgeId); + } + ); + } + } + + private List listBotsByWorkflowId(BigInteger workflowId) { + List relations = botWorkflowService.list(QueryWrapper.create() + .eq(BotWorkflow::getWorkflowId, workflowId)); + return listBotsByIds(collectBotIds(relations, BotWorkflow::getBotId)); + } + + private List listBotsByKnowledgeId(BigInteger knowledgeId) { + List relations = botDocumentCollectionService.list(QueryWrapper.create() + .eq(BotDocumentCollection::getDocumentCollectionId, knowledgeId)); + return listBotsByIds(collectBotIds(relations, BotDocumentCollection::getBotId)); + } + + private List listBotsByIds(Set botIds) { + if (botIds.isEmpty()) { + return Collections.emptyList(); + } + List bots = botService.listByIds(botIds); + Map botMap = new HashMap<>(); + for (Bot bot : bots) { + botMap.put(bot.getId(), bot); + } + List result = new ArrayList<>(botIds.size()); + for (BigInteger botId : botIds) { + Bot bot = botMap.get(botId); + if (bot == null) { + continue; + } + result.add(toBindingVo(bot.getId(), bot.getTitle())); + } + return result; + } + + private List listWorkflowsUsingKnowledge(BigInteger knowledgeId) { + List workflows = workflowService.list(); + if (workflows == null || workflows.isEmpty()) { + return Collections.emptyList(); + } + List result = new ArrayList<>(); + for (Workflow workflow : workflows) { + if (workflow == null || workflow.getId() == null) { + continue; + } + if (containsKnowledgeReference(workflow.getContent(), knowledgeId)) { + result.add(toBindingVo(workflow.getId(), workflow.getTitle())); + } + } + return result; + } + + private boolean containsKnowledgeReference(String content, BigInteger knowledgeId) { + if (!StringUtils.hasText(content) || knowledgeId == null) { + return false; + } + try { + Object parsed = JSON.parse(content); + if (!(parsed instanceof JSONObject root)) { + return false; + } + JSONArray nodes = root.getJSONArray("nodes"); + if (nodes == null || nodes.isEmpty()) { + return false; + } + String expected = knowledgeId.toString(); + for (int i = 0; i < nodes.size(); i++) { + JSONObject node = nodes.getJSONObject(i); + if (node == null || !KNOWLEDGE_NODE_TYPE.equals(node.getString("type"))) { + continue; + } + JSONObject data = node.getJSONObject("data"); + if (data == null) { + continue; + } + Object rawKnowledgeId = data.get("knowledgeId"); + if (rawKnowledgeId != null && expected.equals(String.valueOf(rawKnowledgeId))) { + return true; + } + } + return false; + } catch (Exception ignored) { + return false; + } + } + + private void trimPublishedSnapshotBindings(BigInteger botId, + String bindingsKey, + String idKey, + BigInteger resourceId) { + Bot bot = botService.getById(botId); + if (bot == null || bot.getPublishedSnapshotJson() == null || bot.getPublishedSnapshotJson().isEmpty()) { + return; + } + Map snapshot = new LinkedHashMap<>(bot.getPublishedSnapshotJson()); + Object rawBindings = snapshot.get(bindingsKey); + if (!(rawBindings instanceof List bindings)) { + return; + } + + List> filtered = new ArrayList<>(); + boolean changed = false; + String expectedId = resourceId == null ? null : resourceId.toString(); + for (Object item : bindings) { + if (!(item instanceof Map bindingMap)) { + continue; + } + Object currentId = bindingMap.get(idKey); + if (expectedId != null && currentId != null && expectedId.equals(String.valueOf(currentId))) { + changed = true; + continue; + } + filtered.add(new LinkedHashMap<>((Map) bindingMap)); + } + if (!changed) { + return; + } + snapshot.put(bindingsKey, filtered); + Bot update = new Bot(); + update.setId(botId); + update.setPublishedSnapshotJson(snapshot); + botService.updateById(update); + } + + private Set collectBotIds(Collection relations, BotIdGetter getter) { + if (relations == null || relations.isEmpty()) { + return Collections.emptySet(); + } + Set result = new LinkedHashSet<>(); + for (T relation : relations) { + BigInteger botId = getter.getBotId(relation); + if (botId != null) { + result.add(botId); + } + } + return result; + } + + private OfflineImpactBindingVo toBindingVo(BigInteger id, String title) { + OfflineImpactBindingVo vo = new OfflineImpactBindingVo(); + vo.setId(id); + vo.setTitle(title); + return vo; + } + + @FunctionalInterface + private interface BotIdGetter { + + BigInteger getBotId(T relation); + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/utils/DocUtil.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/utils/DocUtil.java index 570b5f7..3122de3 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/utils/DocUtil.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/utils/DocUtil.java @@ -5,6 +5,8 @@ import okhttp3.Call; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; +import org.apache.poi.extractor.ExtractorFactory; +import org.apache.poi.extractor.POITextExtractor; import org.apache.pdfbox.multipdf.Splitter; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPageTree; @@ -22,8 +24,10 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; public class DocUtil { @@ -85,6 +89,27 @@ public class DocUtil { } } + /** + * 读取素材预览文本。 + * + * @param suffix 文件后缀 + * @param is 文件输入流 + * @return 预览文本 + */ + public static String readPreviewContent(String suffix, InputStream is) { + String normalizedSuffix = normalizeSuffix(suffix); + if (isPlainTextSuffix(normalizedSuffix)) { + return readPlainTextFile(is); + } + if ("pdf".equals(normalizedSuffix)) { + return readPdfFile(is); + } + if (isOfficeSuffix(normalizedSuffix)) { + return readOfficeFile(is); + } + throw new IllegalArgumentException("不支持的文件类型: " + suffix); + } + public static Map splitPdf(byte[] bytes, int splitSize) { Map map = new HashMap<>(); @@ -174,6 +199,16 @@ public class DocUtil { return name.substring(name.lastIndexOf(".") + 1); } + /** + * 规范化文件后缀,统一使用小写。 + * + * @param suffix 原始文件后缀 + * @return 规范化后的后缀 + */ + public static String normalizeSuffix(String suffix) { + return suffix == null ? "" : suffix.trim().toLowerCase(Locale.ROOT); + } + public static byte[] readBytes(InputStream inputStream) { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); try { @@ -194,4 +229,59 @@ public class DocUtil { public static String getFileNameByUrl(String url) { return url.substring(url.lastIndexOf("/") + 1); } + + /** + * 读取纯文本类型文件。 + * + * @param is 文件输入流 + * @return 文本内容 + */ + private static String readPlainTextFile(InputStream is) { + try { + return new String(readBytes(is), StandardCharsets.UTF_8); + } catch (Exception e) { + log.error("读取文本文件失败:", e); + throw new RuntimeException(e); + } + } + + /** + * 使用 Apache POI 提取 Office 文档中的纯文本。 + * + * @param is 文件输入流 + * @return 文本内容 + */ + private static String readOfficeFile(InputStream is) { + try (POITextExtractor extractor = ExtractorFactory.createExtractor(is)) { + return extractor.getText(); + } catch (Exception e) { + log.error("读取 Office 文件失败:", e); + throw new RuntimeException(e); + } + } + + /** + * 判断是否为纯文本类文件。 + * + * @param suffix 文件后缀 + * @return 是否纯文本类文件 + */ + private static boolean isPlainTextSuffix(String suffix) { + return "txt".equals(suffix) || "md".equals(suffix) || "csv".equals(suffix); + } + + /** + * 判断是否为 Office 文件。 + * + * @param suffix 文件后缀 + * @return 是否 Office 文件 + */ + private static boolean isOfficeSuffix(String suffix) { + return "doc".equals(suffix) + || "docx".equals(suffix) + || "xls".equals(suffix) + || "xlsx".equals(suffix) + || "ppt".equals(suffix) + || "pptx".equals(suffix); + } } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/vo/OfflineImpactBindingVo.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/vo/OfflineImpactBindingVo.java new file mode 100644 index 0000000..3aab87f --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/vo/OfflineImpactBindingVo.java @@ -0,0 +1,49 @@ +package tech.easyflow.ai.vo; + +import java.math.BigInteger; + +/** + * 下线影响项视图对象。 + */ +public class OfflineImpactBindingVo { + + private BigInteger id; + + private String title; + + /** + * 获取资源 ID。 + * + * @return 资源 ID + */ + public BigInteger getId() { + return id; + } + + /** + * 设置资源 ID。 + * + * @param id 资源 ID + */ + public void setId(BigInteger id) { + this.id = id; + } + + /** + * 获取资源名称。 + * + * @return 资源名称 + */ + public String getTitle() { + return title; + } + + /** + * 设置资源名称。 + * + * @param title 资源名称 + */ + public void setTitle(String title) { + this.title = title; + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/vo/OfflineImpactCheckVo.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/vo/OfflineImpactCheckVo.java new file mode 100644 index 0000000..ea58be8 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/vo/OfflineImpactCheckVo.java @@ -0,0 +1,130 @@ +package tech.easyflow.ai.vo; + +import java.util.ArrayList; +import java.util.List; + +/** + * 下线影响检查结果。 + */ +public class OfflineImpactCheckVo { + + private boolean canProceed; + + private boolean hasBotBindings; + + private boolean hasWorkflowUsages; + + private List botBindings = new ArrayList<>(); + + private List workflowUsages = new ArrayList<>(); + + private String message; + + /** + * 是否允许继续下线。 + * + * @return 是否允许继续 + */ + public boolean isCanProceed() { + return canProceed; + } + + /** + * 设置是否允许继续下线。 + * + * @param canProceed 是否允许继续 + */ + public void setCanProceed(boolean canProceed) { + this.canProceed = canProceed; + } + + /** + * 是否存在 Bot 绑定。 + * + * @return 是否存在 Bot 绑定 + */ + public boolean isHasBotBindings() { + return hasBotBindings; + } + + /** + * 设置是否存在 Bot 绑定。 + * + * @param hasBotBindings 是否存在 Bot 绑定 + */ + public void setHasBotBindings(boolean hasBotBindings) { + this.hasBotBindings = hasBotBindings; + } + + /** + * 是否存在工作流引用。 + * + * @return 是否存在工作流引用 + */ + public boolean isHasWorkflowUsages() { + return hasWorkflowUsages; + } + + /** + * 设置是否存在工作流引用。 + * + * @param hasWorkflowUsages 是否存在工作流引用 + */ + public void setHasWorkflowUsages(boolean hasWorkflowUsages) { + this.hasWorkflowUsages = hasWorkflowUsages; + } + + /** + * 获取 Bot 绑定列表。 + * + * @return Bot 绑定列表 + */ + public List getBotBindings() { + return botBindings; + } + + /** + * 设置 Bot 绑定列表。 + * + * @param botBindings Bot 绑定列表 + */ + public void setBotBindings(List botBindings) { + this.botBindings = botBindings; + } + + /** + * 获取工作流引用列表。 + * + * @return 工作流引用列表 + */ + public List getWorkflowUsages() { + return workflowUsages; + } + + /** + * 设置工作流引用列表。 + * + * @param workflowUsages 工作流引用列表 + */ + public void setWorkflowUsages(List workflowUsages) { + this.workflowUsages = workflowUsages; + } + + /** + * 获取提示信息。 + * + * @return 提示信息 + */ + public String getMessage() { + return message; + } + + /** + * 设置提示信息。 + * + * @param message 提示信息 + */ + public void setMessage(String message) { + this.message = message; + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/aspect/ApprovalActionAspect.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/aspect/ApprovalActionAspect.java index 8ab3e4c..1256976 100644 --- a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/aspect/ApprovalActionAspect.java +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/aspect/ApprovalActionAspect.java @@ -11,11 +11,15 @@ import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.stereotype.Component; import tech.easyflow.approval.annotation.ApprovalAction; +import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest; +import tech.easyflow.approval.entity.vo.ApprovalActionResult; +import tech.easyflow.approval.service.ApprovalSubjectHandler; import tech.easyflow.approval.service.ApprovalActionFacade; import tech.easyflow.common.satoken.util.SaTokenUtil; import java.lang.reflect.Method; import java.math.BigInteger; +import java.util.List; /** * 提审动作切面。 @@ -25,11 +29,14 @@ import java.math.BigInteger; public class ApprovalActionAspect { private final ApprovalActionFacade approvalActionFacade; + private final List handlers; private final ExpressionParser expressionParser = new SpelExpressionParser(); private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); - public ApprovalActionAspect(ApprovalActionFacade approvalActionFacade) { + public ApprovalActionAspect(ApprovalActionFacade approvalActionFacade, + List handlers) { this.approvalActionFacade = approvalActionFacade; + this.handlers = handlers; } /** @@ -37,7 +44,7 @@ public class ApprovalActionAspect { * * @param joinPoint 切点 * @param approvalAction 注解 - * @return 审批实例 ID + * @return 动作执行结果 * @throws Throwable 执行异常 */ @Around("@annotation(approvalAction)") @@ -45,12 +52,14 @@ public class ApprovalActionAspect { Object identifier = resolveIdentifier(joinPoint, approvalAction.idExpr()); BigInteger resourceId = identifier == null ? null : new BigInteger(String.valueOf(identifier)); joinPoint.proceed(); - return approvalActionFacade.submit( - approvalAction.resourceType(), + ApprovalSubjectHandler handler = getHandler(approvalAction.resourceType()); + ApprovalSubmitRequest request = handler.buildSubmitRequest( resourceId, approvalAction.actionType(), SaTokenUtil.getLoginAccount().getId() ); + ApprovalActionResult result = approvalActionFacade.submit(request); + return result; } private Object resolveIdentifier(ProceedingJoinPoint joinPoint, String idExpr) { @@ -63,4 +72,11 @@ public class ApprovalActionAspect { ); return expressionParser.parseExpression(idExpr).getValue(context); } + + private ApprovalSubjectHandler getHandler(String resourceType) { + return handlers.stream() + .filter(item -> item.resourceType().equals(resourceType)) + .findFirst() + .orElseThrow(() -> new IllegalStateException("未找到审批处理器: " + resourceType)); + } } diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalActionResult.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalActionResult.java new file mode 100644 index 0000000..5240175 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalActionResult.java @@ -0,0 +1,53 @@ +package tech.easyflow.approval.entity.vo; + +import java.math.BigInteger; + +/** + * 审批动作执行结果。 + */ +public class ApprovalActionResult { + + private boolean approvalRequired; + + private BigInteger instanceId; + + /** + * 构造需要审批的结果。 + * + * @param instanceId 审批实例 ID + * @return 审批结果 + */ + public static ApprovalActionResult required(BigInteger instanceId) { + ApprovalActionResult result = new ApprovalActionResult(); + result.setApprovalRequired(true); + result.setInstanceId(instanceId); + return result; + } + + /** + * 构造直接执行完成的结果。 + * + * @return 审批结果 + */ + public static ApprovalActionResult direct() { + ApprovalActionResult result = new ApprovalActionResult(); + result.setApprovalRequired(false); + return result; + } + + public boolean isApprovalRequired() { + return approvalRequired; + } + + public void setApprovalRequired(boolean approvalRequired) { + this.approvalRequired = approvalRequired; + } + + public BigInteger getInstanceId() { + return instanceId; + } + + public void setInstanceId(BigInteger instanceId) { + this.instanceId = instanceId; + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalInstanceDetailVo.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalInstanceDetailVo.java index afba506..de102b5 100644 --- a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalInstanceDetailVo.java +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalInstanceDetailVo.java @@ -33,6 +33,8 @@ public class ApprovalInstanceDetailVo { private String applicantName; + private String applicantAccount; + private Date submittedAt; private Date finishedAt; @@ -137,6 +139,14 @@ public class ApprovalInstanceDetailVo { this.applicantName = applicantName; } + public String getApplicantAccount() { + return applicantAccount; + } + + public void setApplicantAccount(String applicantAccount) { + this.applicantAccount = applicantAccount; + } + public Date getSubmittedAt() { return submittedAt; } diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalLogVo.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalLogVo.java index 2031ac7..10eab8b 100644 --- a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalLogVo.java +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalLogVo.java @@ -15,6 +15,8 @@ public class ApprovalLogVo { private BigInteger operatorId; + private String operatorAccount; + private String operatorName; private Date created; @@ -45,6 +47,14 @@ public class ApprovalLogVo { this.operatorId = operatorId; } + public String getOperatorAccount() { + return operatorAccount; + } + + public void setOperatorAccount(String operatorAccount) { + this.operatorAccount = operatorAccount; + } + public String getOperatorName() { return operatorName; } diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/enums/ApprovalActionType.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/enums/ApprovalActionType.java index 1a97b0d..f5612d0 100644 --- a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/enums/ApprovalActionType.java +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/enums/ApprovalActionType.java @@ -9,6 +9,7 @@ import java.util.Locale; public enum ApprovalActionType { PUBLISH("PUBLISH"), + OFFLINE("OFFLINE"), DELETE("DELETE"); private final String code; diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/ApprovalActionFacade.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/ApprovalActionFacade.java index 6c7ec0b..1cff02a 100644 --- a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/ApprovalActionFacade.java +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/ApprovalActionFacade.java @@ -1,6 +1,8 @@ package tech.easyflow.approval.service; import tech.easyflow.approval.entity.ApprovalInstance; +import tech.easyflow.approval.entity.vo.ApprovalActionResult; +import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest; import java.math.BigInteger; @@ -12,13 +14,10 @@ public interface ApprovalActionFacade { /** * 提交审批。 * - * @param resourceType 资源类型 - * @param resourceId 资源 ID - * @param actionType 动作类型 - * @param operatorId 操作人 ID - * @return 审批实例 ID + * @param request 审批提交请求 + * @return 动作执行结果 */ - BigInteger submit(String resourceType, BigInteger resourceId, String actionType, BigInteger operatorId); + ApprovalActionResult submit(ApprovalSubmitRequest request); /** * 处理审批通过后的业务回调。 diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/ApprovalMatchService.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/ApprovalMatchService.java index 5659f0e..3de1f7d 100644 --- a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/ApprovalMatchService.java +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/ApprovalMatchService.java @@ -15,4 +15,12 @@ public interface ApprovalMatchService { * @return 命中的流程详情 */ ApprovalFlowDetailVo matchFlow(ApprovalSubmitRequest request); + + /** + * 根据资源上下文匹配审批流程,未命中时返回 {@code null}。 + * + * @param request 审批提交请求 + * @return 命中的流程详情,未命中时返回 {@code null} + */ + ApprovalFlowDetailVo matchFlowOrNull(ApprovalSubmitRequest request); } diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/ApprovalResultHandler.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/ApprovalResultHandler.java new file mode 100644 index 0000000..b1a5b32 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/ApprovalResultHandler.java @@ -0,0 +1,38 @@ +package tech.easyflow.approval.service; + +import tech.easyflow.approval.entity.ApprovalInstance; + +import java.math.BigInteger; + +/** + * 审批结果业务处理器。 + */ +public interface ApprovalResultHandler { + + /** + * 处理审批通过后的业务回调。 + * + * @param instance 审批实例 + * @param operatorId 操作人 ID + * @param comment 审批意见 + */ + void handleApproved(ApprovalInstance instance, BigInteger operatorId, String comment); + + /** + * 处理审批驳回后的业务回调。 + * + * @param instance 审批实例 + * @param operatorId 操作人 ID + * @param comment 审批意见 + */ + void handleRejected(ApprovalInstance instance, BigInteger operatorId, String comment); + + /** + * 处理审批撤回后的业务回调。 + * + * @param instance 审批实例 + * @param operatorId 操作人 ID + * @param comment 审批意见 + */ + void handleRevoked(ApprovalInstance instance, BigInteger operatorId, String comment); +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/ApprovalSubjectHandler.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/ApprovalSubjectHandler.java index 3bab5ca..ef82325 100644 --- a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/ApprovalSubjectHandler.java +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/ApprovalSubjectHandler.java @@ -1,7 +1,5 @@ package tech.easyflow.approval.service; -import tech.easyflow.approval.entity.vo.ApprovalCallbackContext; -import tech.easyflow.approval.entity.vo.ApprovalSubmitCallbackContext; import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest; import java.math.BigInteger; @@ -28,34 +26,6 @@ public interface ApprovalSubjectHandler { */ ApprovalSubmitRequest buildSubmitRequest(BigInteger resourceId, String actionType, BigInteger operatorId); - /** - * 审批提交完成后的回调。 - * - * @param context 回调上下文 - */ - void onSubmitted(ApprovalSubmitCallbackContext context); - - /** - * 审批通过后的回调。 - * - * @param context 回调上下文 - */ - void onApproved(ApprovalCallbackContext context); - - /** - * 审批驳回后的回调。 - * - * @param context 回调上下文 - */ - void onRejected(ApprovalCallbackContext context); - - /** - * 审批撤回后的回调。 - * - * @param context 回调上下文 - */ - void onRevoked(ApprovalCallbackContext context); - /** * 校验资源是否已发布。 * diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalActionFacadeImpl.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalActionFacadeImpl.java index 46655d4..f9887c4 100644 --- a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalActionFacadeImpl.java +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalActionFacadeImpl.java @@ -1,14 +1,15 @@ package tech.easyflow.approval.service.impl; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; import tech.easyflow.approval.entity.ApprovalInstance; -import tech.easyflow.approval.entity.vo.ApprovalCallbackContext; -import tech.easyflow.approval.entity.vo.ApprovalSubmitCallbackContext; +import tech.easyflow.approval.entity.vo.ApprovalActionResult; +import tech.easyflow.approval.entity.vo.ApprovalFlowDetailVo; import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest; import tech.easyflow.approval.enums.ApprovalResourceType; import tech.easyflow.approval.service.ApprovalActionFacade; import tech.easyflow.approval.service.ApprovalInstanceService; +import tech.easyflow.approval.service.ApprovalMatchService; +import tech.easyflow.approval.service.ApprovalResultHandler; import tech.easyflow.approval.service.ApprovalSubjectHandler; import java.math.BigInteger; @@ -22,31 +23,30 @@ public class ApprovalActionFacadeImpl implements ApprovalActionFacade { private final List handlers; private final ApprovalInstanceService approvalInstanceService; + private final ApprovalMatchService approvalMatchService; + private final ApprovalResultHandler approvalResultHandler; public ApprovalActionFacadeImpl(List handlers, - ApprovalInstanceService approvalInstanceService) { + ApprovalInstanceService approvalInstanceService, + ApprovalMatchService approvalMatchService, + ApprovalResultHandler approvalResultHandler) { this.handlers = handlers; this.approvalInstanceService = approvalInstanceService; + this.approvalMatchService = approvalMatchService; + this.approvalResultHandler = approvalResultHandler; } /** * {@inheritDoc} */ @Override - @Transactional(rollbackFor = Exception.class) - public BigInteger submit(String resourceType, BigInteger resourceId, String actionType, BigInteger operatorId) { - ApprovalSubjectHandler handler = getHandler(resourceType); - ApprovalSubmitRequest request = handler.buildSubmitRequest(resourceId, actionType, operatorId); + public ApprovalActionResult submit(ApprovalSubmitRequest request) { + ApprovalFlowDetailVo flow = approvalMatchService.matchFlowOrNull(request); + if (flow == null) { + return ApprovalActionResult.direct(); + } BigInteger instanceId = approvalInstanceService.submitApproval(request); - - ApprovalSubmitCallbackContext context = new ApprovalSubmitCallbackContext(); - context.setInstanceId(instanceId); - context.setResourceType(request.getResourceType()); - context.setResourceId(request.getResourceId()); - context.setActionType(request.getActionType()); - context.setOperatorId(operatorId); - handler.onSubmitted(context); - return instanceId; + return ApprovalActionResult.required(instanceId); } /** @@ -54,8 +54,7 @@ public class ApprovalActionFacadeImpl implements ApprovalActionFacade { */ @Override public void handleApproved(ApprovalInstance instance, BigInteger operatorId, String comment) { - ApprovalSubjectHandler handler = getHandler(instance.getResourceType()); - handler.onApproved(buildCallbackContext(instance, operatorId, comment)); + approvalResultHandler.handleApproved(instance, operatorId, comment); } /** @@ -63,8 +62,7 @@ public class ApprovalActionFacadeImpl implements ApprovalActionFacade { */ @Override public void handleRejected(ApprovalInstance instance, BigInteger operatorId, String comment) { - ApprovalSubjectHandler handler = getHandler(instance.getResourceType()); - handler.onRejected(buildCallbackContext(instance, operatorId, comment)); + approvalResultHandler.handleRejected(instance, operatorId, comment); } /** @@ -72,8 +70,7 @@ public class ApprovalActionFacadeImpl implements ApprovalActionFacade { */ @Override public void handleRevoked(ApprovalInstance instance, BigInteger operatorId, String comment) { - ApprovalSubjectHandler handler = getHandler(instance.getResourceType()); - handler.onRevoked(buildCallbackContext(instance, operatorId, comment)); + approvalResultHandler.handleRevoked(instance, operatorId, comment); } /** @@ -85,14 +82,6 @@ public class ApprovalActionFacadeImpl implements ApprovalActionFacade { handler.assertPublishedAccess(identifier, denyMessage); } - private ApprovalCallbackContext buildCallbackContext(ApprovalInstance instance, BigInteger operatorId, String comment) { - ApprovalCallbackContext context = new ApprovalCallbackContext(); - context.setInstance(instance); - context.setOperatorId(operatorId); - context.setComment(comment); - return context; - } - private ApprovalSubjectHandler getHandler(String resourceType) { String normalized = ApprovalResourceType.from(resourceType).getCode(); return handlers.stream() diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalMatchServiceImpl.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalMatchServiceImpl.java index b1a3431..f80a199 100644 --- a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalMatchServiceImpl.java +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalMatchServiceImpl.java @@ -53,6 +53,18 @@ public class ApprovalMatchServiceImpl implements ApprovalMatchService { */ @Override public ApprovalFlowDetailVo matchFlow(ApprovalSubmitRequest request) { + ApprovalFlowDetailVo matchedFlow = matchFlowOrNull(request); + if (matchedFlow == null) { + throw new BusinessException("当前资源上下文未命中审批流程"); + } + return matchedFlow; + } + + /** + * {@inheritDoc} + */ + @Override + public ApprovalFlowDetailVo matchFlowOrNull(ApprovalSubmitRequest request) { ApprovalSubmitRequest normalized = normalizeRequest(request); QueryWrapper flowWrapper = QueryWrapper.create() .eq(ApprovalFlow::getResourceType, normalized.getResourceType()) @@ -60,7 +72,7 @@ public class ApprovalMatchServiceImpl implements ApprovalMatchService { .eq(ApprovalFlow::getStatus, ApprovalFlowStatus.ENABLED.getCode()); List flows = approvalFlowMapper.selectListByQuery(flowWrapper); if (CollectionUtil.isEmpty(flows)) { - throw new BusinessException("未找到可用的审批流程"); + return null; } List flowIds = flows.stream().map(ApprovalFlow::getId).collect(Collectors.toList()); Map> scopeMap = approvalFlowScopeMapper.selectListByQuery( @@ -76,7 +88,7 @@ public class ApprovalMatchServiceImpl implements ApprovalMatchService { } } if (matchedFlows.isEmpty()) { - throw new BusinessException("当前资源上下文未命中审批流程"); + return null; } matchedFlows.sort(Comparator diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalQueryServiceImpl.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalQueryServiceImpl.java index 3913e82..24c0abf 100644 --- a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalQueryServiceImpl.java +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalQueryServiceImpl.java @@ -154,8 +154,9 @@ public class ApprovalQueryServiceImpl implements ApprovalQueryService { List logs = approvalLogMapper.selectListByQuery( QueryWrapper.create().eq(ApprovalLog::getInstanceId, instanceId)); Map frozenStepMap = resolveFrozenStepMap(instance); - Map accountNameMap = loadAccountNameMap(instance, tasks, logs); - detail.setApplicantName(accountNameMap.get(instance.getApplicantId())); + Map accountMap = loadAccountMap(instance, tasks, logs); + detail.setApplicantName(resolveAccountName(accountMap.get(instance.getApplicantId()))); + detail.setApplicantAccount(resolveAccountLoginName(accountMap.get(instance.getApplicantId()))); detail.setTasks(tasks.stream() .sorted(Comparator.comparing(ApprovalTask::getStepNo)) @@ -171,7 +172,7 @@ public class ApprovalQueryServiceImpl implements ApprovalQueryService { taskVo.setAssigneeTargetCode(item.getAssigneeTargetCode()); taskVo.setAssigneeTargetName(item.getAssigneeTargetName()); taskVo.setActedBy(item.getActedBy()); - taskVo.setActedByName(accountNameMap.get(item.getActedBy())); + taskVo.setActedByName(resolveAccountName(accountMap.get(item.getActedBy()))); taskVo.setActedAt(item.getActedAt()); taskVo.setComment(item.getComment()); return taskVo; @@ -185,7 +186,8 @@ public class ApprovalQueryServiceImpl implements ApprovalQueryService { logVo.setId(item.getId()); logVo.setEventType(item.getEventType()); logVo.setOperatorId(item.getOperatorId()); - logVo.setOperatorName(accountNameMap.get(item.getOperatorId())); + logVo.setOperatorAccount(resolveAccountLoginName(accountMap.get(item.getOperatorId()))); + logVo.setOperatorName(resolveAccountName(accountMap.get(item.getOperatorId()))); logVo.setCreated(item.getCreated()); logVo.setPayloadJson(item.getPayloadJson()); return logVo; @@ -205,14 +207,14 @@ public class ApprovalQueryServiceImpl implements ApprovalQueryService { } /** - * 批量加载审批详情里涉及到的账号显示名称。 + * 批量加载审批详情里涉及到的账号信息。 * * @param instance 审批实例 * @param tasks 审批任务列表 * @param logs 审批日志列表 - * @return 账号 ID 到展示名称的映射 + * @return 账号 ID 到账号实体的映射 */ - private Map loadAccountNameMap(ApprovalInstance instance, List tasks, + private Map loadAccountMap(ApprovalInstance instance, List tasks, List logs) { Set accountIds = new HashSet<>(); if (instance.getApplicantId() != null) { @@ -232,7 +234,7 @@ public class ApprovalQueryServiceImpl implements ApprovalQueryService { return sysAccountService.listByIds(accountIds).stream() .collect(Collectors.toMap( SysAccount::getId, - this::resolveAccountName, + account -> account, (left, right) -> left, LinkedHashMap::new)); } @@ -256,6 +258,19 @@ public class ApprovalQueryServiceImpl implements ApprovalQueryService { return null; } + /** + * 解析账号登录名。 + * + * @param account 账号实体 + * @return 登录账号 + */ + private String resolveAccountLoginName(SysAccount account) { + if (account == null || !StringUtils.hasText(account.getLoginName())) { + return null; + } + return account.getLoginName().trim(); + } + private QueryWrapper buildBaseQuery(String resourceType, String actionType, String keyword) { QueryWrapper queryWrapper = QueryWrapper.create(); if (StringUtils.hasText(resourceType)) { diff --git a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysMenuServiceImpl.java b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysMenuServiceImpl.java index 4d12d2a..4893e55 100644 --- a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysMenuServiceImpl.java +++ b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysMenuServiceImpl.java @@ -15,7 +15,11 @@ import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.math.BigInteger; import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; /** @@ -32,6 +36,16 @@ public class SysMenuServiceImpl extends ServiceImpl impl @Resource private SysAccountRoleService sysAccountRoleService; + /** + * 根据账号查询菜单,并自动补齐已授权节点的父级菜单链。 + *

+ * 这样当角色只勾选了某个页面下的按钮权限或子能力时, + * 其所属的页面菜单仍能正常出现在侧边栏中,避免出现“有子权限但无入口”的问题。 + * + * @param entity 菜单过滤条件 + * @param accountId 账号 ID + * @return 当前账号可访问的菜单集合 + */ @Override public List getMenusByAccountId(SysMenu entity, BigInteger accountId) { // 查询用户对应角色id集合 @@ -48,11 +62,53 @@ public class SysMenuServiceImpl extends ServiceImpl impl if (CollectionUtil.isEmpty(menuIds)) { return new ArrayList<>(); } + List fullMenuIds = collectMenuIdsWithParents(menuIds); // 查询当前用户拥有的菜单 SqlOperators ops = SqlOperatorsUtil.build(SysMenu.class); QueryWrapper queryWrapper = QueryWrapper.create(entity, ops); - queryWrapper.in(SysMenu::getId, menuIds); + queryWrapper.in(SysMenu::getId, fullMenuIds); queryWrapper.orderBy("sort_no asc"); return list(queryWrapper); } + + /** + * 收集菜单自身及其所有父级菜单 ID。 + * + * @param menuIds 已授权的菜单 ID 列表 + * @return 包含父级链路的完整菜单 ID 列表 + */ + private List collectMenuIdsWithParents(List menuIds) { + List allMenus = list(); + if (CollectionUtil.isEmpty(allMenus)) { + return menuIds; + } + Map menuMap = new HashMap<>(); + for (SysMenu menu : allMenus) { + menuMap.put(menu.getId(), menu); + } + Set result = new HashSet<>(menuIds); + for (BigInteger menuId : menuIds) { + appendParentMenuIds(menuId, menuMap, result); + } + return new ArrayList<>(result); + } + + /** + * 递归追加父级菜单 ID,直到根节点或无父节点为止。 + * + * @param menuId 当前菜单 ID + * @param menuMap 全量菜单映射 + * @param result 结果集合 + */ + private void appendParentMenuIds(BigInteger menuId, Map menuMap, Set result) { + SysMenu current = menuMap.get(menuId); + if (current == null) { + return; + } + BigInteger parentId = current.getParentId(); + if (parentId == null || BigInteger.ZERO.equals(parentId) || !result.add(parentId)) { + return; + } + appendParentMenuIds(parentId, menuMap, result); + } } diff --git a/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/mysql/V8__mysql_approval_tab_permission_patch.sql b/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/mysql/V8__mysql_approval_tab_permission_patch.sql new file mode 100644 index 0000000..da07545 --- /dev/null +++ b/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/mysql/V8__mysql_approval_tab_permission_patch.sql @@ -0,0 +1,64 @@ +SET NAMES utf8mb4; + +UPDATE `tb_sys_menu` +SET + `menu_url` = '/sys/approval', + `component` = '/system/approval/ApprovalManage', + `menu_icon` = 'svg:approval', + `is_show` = 1, + `modified` = NOW(), + `modified_by` = 1, + `remark` = '审批管理菜单' +WHERE `id` = 367100000000000001; + +UPDATE `tb_sys_menu` +SET + `menu_type` = 1, + `menu_url` = '', + `component` = '', + `menu_icon` = '', + `is_show` = 0, + `permission_tag` = '/page/approval/flow', + `modified` = NOW(), + `modified_by` = 1, + `remark` = '审批管理-流程配置页签权限' +WHERE `id` = 367100000000000002; + +UPDATE `tb_sys_menu` +SET + `menu_type` = 1, + `menu_url` = '', + `component` = '', + `menu_icon` = '', + `is_show` = 0, + `permission_tag` = '/page/approval/pending', + `modified` = NOW(), + `modified_by` = 1, + `remark` = '审批管理-待审批页签权限' +WHERE `id` = 367100000000000003; + +UPDATE `tb_sys_menu` +SET + `menu_type` = 1, + `menu_url` = '', + `component` = '', + `menu_icon` = '', + `is_show` = 0, + `permission_tag` = '/page/approval/processed', + `modified` = NOW(), + `modified_by` = 1, + `remark` = '审批管理-已审批页签权限' +WHERE `id` = 367100000000000004; + +UPDATE `tb_sys_menu` +SET + `menu_type` = 1, + `menu_url` = '', + `component` = '', + `menu_icon` = '', + `is_show` = 0, + `permission_tag` = '/page/approval/initiated', + `modified` = NOW(), + `modified_by` = 1, + `remark` = '审批管理-我发起页签权限' +WHERE `id` = 367100000000000005; diff --git a/easyflow-ui-admin/app/src/api/ai/bot.ts b/easyflow-ui-admin/app/src/api/ai/bot.ts index 7e82308..261d5c4 100644 --- a/easyflow-ui-admin/app/src/api/ai/bot.ts +++ b/easyflow-ui-admin/app/src/api/ai/bot.ts @@ -61,6 +61,14 @@ export const submitBotPublishApproval = (id: string) => { ); }; +/** 提交 Bot 下线审批 */ +export const submitBotOfflineApproval = (id: string) => { + return api.post>( + '/api/v1/bot/submitOfflineApproval', + { id }, + ); +}; + /** 提交 Bot 删除审批 */ export const submitBotDeleteApproval = (id: string) => { return api.post>( diff --git a/easyflow-ui-admin/app/src/components/page/CardList.vue b/easyflow-ui-admin/app/src/components/page/CardList.vue index e51cd99..e4f34be 100644 --- a/easyflow-ui-admin/app/src/components/page/CardList.vue +++ b/easyflow-ui-admin/app/src/components/page/CardList.vue @@ -28,6 +28,7 @@ export interface ActionButton { permission?: string; placement?: ActionPlacement; tone?: ActionTone; + visible?: ((row: any) => boolean) | boolean; onClick: (row: any) => void; } @@ -77,6 +78,13 @@ function hasPermission(permission?: string) { return !permission || hasAccessByCodes([permission]); } +function isActionVisible(action: ActionButton, row: any) { + if (typeof action.visible === 'function') { + return action.visible(row); + } + return action.visible !== false; +} + const resolvedPrimaryAction = computed(() => { if (!props.primaryAction || !hasPermission(props.primaryAction.permission)) { return undefined; @@ -109,24 +117,6 @@ const resolvedActions = computed(() => { })); }); -const inlineActions = computed(() => { - return resolvedActions.value.filter( - (action) => action.placement === 'inline', - ); -}); - -const menuActions = computed(() => { - return resolvedActions.value.filter((action) => action.placement === 'menu'); -}); - -const showFooter = computed(() => { - return Boolean( - resolvedPrimaryAction.value || - inlineActions.value.length > 0 || - menuActions.value.length > 0, - ); -}); - function handlePrimaryAction(item: any) { resolvedPrimaryAction.value?.onClick(item); } @@ -139,6 +129,24 @@ function handleActionClick(event: Event, action: ActionButton, item: any) { function resolveActionText(action: ActionButton, item: any) { return typeof action.text === 'function' ? action.text(item) : action.text; } + +function resolveInlineActions(item: any) { + return resolvedActions.value.filter( + (action) => action.placement === 'inline' && isActionVisible(action, item), + ); +} + +function resolveMenuActions(item: any) { + return resolvedActions.value.filter( + (action) => action.placement === 'menu' && isActionVisible(action, item), + ); +} + +function hasVisibleActions(item: any) { + return ( + resolveInlineActions(item).length > 0 || resolveMenuActions(item).length > 0 + ); +}