diff --git a/easyflow-api/easyflow-api-admin/pom.xml b/easyflow-api/easyflow-api-admin/pom.xml index 0d648ab..6bec8d3 100644 --- a/easyflow-api/easyflow-api-admin/pom.xml +++ b/easyflow-api/easyflow-api-admin/pom.xml @@ -12,6 +12,10 @@ easyflow-api-admin + + tech.easyflow + easyflow-module-approval + tech.easyflow easyflow-module-ai 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 0241a9e..4d8124e 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 @@ -18,6 +18,7 @@ import org.springframework.web.multipart.MultipartFile; 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.ai.service.*; import tech.easyflow.ai.service.impl.BotServiceImpl; import tech.easyflow.common.audio.core.AudioServiceManager; @@ -67,6 +68,8 @@ public class BotController extends BaseCurdController { private AudioServiceManager audioServiceManager; @Resource private CategoryPermissionService categoryPermissionService; + @Resource + private BotPublishAppService botPublishAppService; public BotController(BotService service, ModelService modelService, BotWorkflowService botWorkflowService, BotDocumentCollectionService botDocumentCollectionService, BotMessageService botMessageService) { @@ -184,17 +187,23 @@ public class BotController extends BaseCurdController { @GetMapping("getDetail") @SaIgnore public Result getDetail(String id) { - Bot bot = botService.getDetail(id); + Bot bot = StpUtil.isLogin() ? botService.getDetail(id) : botService.getPublishedDetail(id); if (bot != null && StpUtil.isLogin()) { categoryPermissionService.assertCategoryResourceVisible("BOT", bot.getCreatedBy(), bot.getCategoryId(), "无权限访问聊天助手"); } + if (bot == null) { + return Result.ok(null); + } + if (!StpUtil.isLogin() && !tech.easyflow.ai.enums.PublishStatus.from(bot.getPublishStatus()).isExternallyVisible()) { + throw new BusinessException("聊天助手尚未发布"); + } return Result.ok(bot); } @Override @SaIgnore public Result detail(String id) { - Bot data = botService.getDetail(id); + Bot data = StpUtil.isLogin() ? botService.getDetail(id) : botService.getPublishedDetail(id); if (data == null) { return Result.ok(data); } @@ -202,6 +211,10 @@ public class BotController extends BaseCurdController { categoryPermissionService.assertCategoryResourceVisible("BOT", data.getCreatedBy(), data.getCategoryId(), "无权限访问聊天助手"); } + if (!StpUtil.isLogin() && !tech.easyflow.ai.enums.PublishStatus.from(data.getPublishStatus()).isExternallyVisible()) { + throw new BusinessException("聊天助手尚未发布"); + } + Map llmOptions = data.getModelOptions(); if (llmOptions == null) { llmOptions = new HashMap<>(); @@ -232,6 +245,18 @@ public class BotController extends BaseCurdController { return Result.ok(data); } + @PostMapping("/submitPublishApproval") + @SaCheckPermission("/api/v1/bot/save") + public Result submitPublishApproval(@JsonBody("id") BigInteger id) { + return Result.ok(botPublishAppService.submitPublishApproval(id)); + } + + @PostMapping("/submitDeleteApproval") + @SaCheckPermission("/api/v1/bot/remove") + public Result submitDeleteApproval(@JsonBody("id") BigInteger id) { + return Result.ok(botPublishAppService.submitDeleteApproval(id)); + } + @Override public Result> list(Bot entity, Boolean asTree, String sortKey, String sortType) { QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity)); @@ -360,6 +385,12 @@ public class BotController extends BaseCurdController { return super.onRemoveBefore(ids); } + @Override + @PostMapping("remove") + public Result remove(@JsonBody(value = "id", required = true) Serializable id) { + return Result.fail(1, "请提交删除审批"); + } + /** * 系统提示词优化 * diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/BotDocumentCollectionController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/BotDocumentCollectionController.java index 224041e..c4b6ac1 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/BotDocumentCollectionController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/BotDocumentCollectionController.java @@ -4,6 +4,7 @@ import org.springframework.web.bind.annotation.PostMapping; import tech.easyflow.ai.dto.BotKnowledgeBindingRequest; import tech.easyflow.ai.entity.BotDocumentCollection; import tech.easyflow.ai.entity.DocumentCollection; +import tech.easyflow.ai.enums.PublishStatus; import tech.easyflow.ai.permission.KnowledgeReadAccessSnapshot; import tech.easyflow.ai.permission.KnowledgeVisibilityQueryHelper; import tech.easyflow.ai.service.BotDocumentCollectionService; @@ -76,6 +77,9 @@ public class BotDocumentCollectionController extends BaseCurdController submitPublishApproval(@JsonBody("id") BigInteger id) { + return Result.ok(knowledgePublishAppService.submitPublishApproval(id)); + } + + /** + * 提交删除审批。 + * + * @param id 知识库 ID + * @return 审批实例 ID + */ + @PostMapping("/submitDeleteApproval") + @SaCheckPermission("/api/v1/documentCollection/remove") + public Result submitDeleteApproval(@JsonBody("id") BigInteger id) { + return Result.ok(knowledgePublishAppService.submitDeleteApproval(id)); + } + @PostMapping("splitterProfile/save") @SaCheckPermission("/api/v1/documentCollection/save") @RequireResourceAccess( @@ -218,6 +248,7 @@ public class DocumentCollectionController extends BaseCurdController> list(DocumentCollection entity, Boolean asTree, String sortKey, String sortType) { QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity)); knowledgeVisibilityQueryHelper.applyReadableAccess(queryWrapper); + applyPublishedOnlyFilter(queryWrapper); queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy())); return Result.ok(service.list(queryWrapper)); } @@ -225,9 +256,16 @@ public class DocumentCollectionController extends BaseCurdController queryPage(Page page, QueryWrapper queryWrapper) { knowledgeVisibilityQueryHelper.applyReadableAccess(queryWrapper); + applyPublishedOnlyFilter(queryWrapper); return super.queryPage(page, queryWrapper); } + @Override + @PostMapping("remove") + public Result remove(@JsonBody(value = "id", required = true) Serializable id) { + return Result.fail(1, "请提交删除审批"); + } + private void normalizeVisibilityScope(DocumentCollection entity, boolean isSave) { if (entity == null) { return; @@ -275,6 +313,17 @@ public class DocumentCollectionController extends BaseCurdController toKnowledgeSearchResult(List documents) { List results = new ArrayList<>(); if (documents == null) { 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 7550d1e..5227779 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 @@ -11,6 +11,8 @@ import com.easyagents.flow.core.chain.runtime.ChainExecutor; import com.easyagents.flow.core.parser.ChainParser; import com.mybatisflex.core.query.QueryWrapper; import org.springframework.util.StringUtils; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import tech.easyflow.ai.permission.WorkflowVisibilityQueryHelper; @@ -23,6 +25,8 @@ import tech.easyflow.ai.easyagentsflow.service.TinyFlowService; import tech.easyflow.ai.easyagentsflow.service.WorkflowCheckService; 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.BotWorkflowService; import tech.easyflow.ai.service.ModelService; import tech.easyflow.ai.service.WorkflowService; @@ -82,6 +86,8 @@ public class WorkflowController extends BaseCurdController submitPublishApproval(@JsonBody("id") BigInteger id) { + return Result.ok(workflowPublishAppService.submitPublishApproval(id)); + } + + /** + * 提交删除审批。 + * + * @param id 工作流 ID + * @return 审批实例 ID + */ + @PostMapping("/submitDeleteApproval") + @SaCheckPermission("/api/v1/workflow/remove") + public Result submitDeleteApproval(@JsonBody("id") BigInteger id) { + return Result.ok(workflowPublishAppService.submitDeleteApproval(id)); + } + @GetMapping("/supportedCodeEngines") @SaCheckPermission("/api/v1/workflow/query") public Result supportedCodeEngines() { @@ -336,6 +366,7 @@ public class WorkflowController extends BaseCurdController> list(Workflow entity, Boolean asTree, String sortKey, String sortType) { QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity)); workflowVisibilityQueryHelper.applyReadableAccess(queryWrapper); + applyPublishedOnlyFilter(queryWrapper); queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy())); return Result.ok(service.list(queryWrapper)); } @@ -343,9 +374,16 @@ public class WorkflowController extends BaseCurdController queryPage(Page page, QueryWrapper queryWrapper) { workflowVisibilityQueryHelper.applyReadableAccess(queryWrapper); + applyPublishedOnlyFilter(queryWrapper); return super.queryPage(page, queryWrapper); } + @Override + @PostMapping("remove") + public Result remove(@JsonBody(value = "id", required = true) Serializable id) { + return Result.fail(1, "请提交删除审批"); + } + @Override protected Result onRemoveBefore(Collection ids) { for (Serializable id : ids) { @@ -381,4 +419,15 @@ public class WorkflowController extends BaseCurdController> page(String name, String resourceType, String actionType, String status, + Long pageNumber, Long pageSize) { + return Result.ok(approvalFlowService.pageFlows(name, resourceType, actionType, status, pageNumber, pageSize)); + } + + /** + * 查询审批流程详情。 + * + * @param id 流程ID + * @return 流程详情 + */ + @GetMapping("/detail") + @SaCheckPermission("/api/v1/approvalFlow/query") + public Result detail(BigInteger id) { + return Result.ok(approvalFlowService.getFlowDetail(id)); + } + + /** + * 查询审批角色选项。 + * + * @return 角色选项 + */ + @GetMapping("/assigneeRoleOptions") + @SaCheckPermission("/api/v1/approvalFlow/query") + public Result> assigneeRoleOptions() { + return Result.ok(approvalAssigneeService.listRoleOptions()); + } + + /** + * 分页查询审批用户选项。 + * + * @param keyword 关键词 + * @param pageNumber 页码 + * @param pageSize 每页数量 + * @return 用户选项分页 + */ + @GetMapping("/assigneeAccountPage") + @SaCheckPermission("/api/v1/approvalFlow/query") + public Result> assigneeAccountPage(String keyword, Long pageNumber, Long pageSize) { + return Result.ok(approvalAssigneeService.pageAccountOptions(keyword, pageNumber, pageSize)); + } + + /** + * 新增审批流程。 + * + * @param request 流程请求体 + * @return 新增流程ID + */ + @PostMapping("/save") + @SaCheckPermission("/api/v1/approvalFlow/save") + public Result save(@JsonBody ApprovalFlowDetailVo request) { + assertSuperAdmin(); + BigInteger operatorId = SaTokenUtil.getLoginAccount().getId(); + return Result.ok(approvalFlowService.saveFlow(request, operatorId)); + } + + /** + * 更新审批流程。 + * + * @param request 流程请求体 + * @return 处理结果 + */ + @PostMapping("/update") + @SaCheckPermission("/api/v1/approvalFlow/save") + public Result update(@JsonBody ApprovalFlowDetailVo request) { + assertSuperAdmin(); + BigInteger operatorId = SaTokenUtil.getLoginAccount().getId(); + approvalFlowService.updateFlow(request, operatorId); + return Result.ok(); + } + + /** + * 启用审批流程。 + * + * @param id 流程ID + * @return 处理结果 + */ + @PostMapping("/enable") + @SaCheckPermission("/api/v1/approvalFlow/enable") + public Result enable(@JsonBody(value = "id", required = true) BigInteger id) { + assertSuperAdmin(); + approvalFlowService.enableFlow(id, SaTokenUtil.getLoginAccount().getId()); + return Result.ok(); + } + + /** + * 停用审批流程。 + * + * @param id 流程ID + * @return 处理结果 + */ + @PostMapping("/disable") + @SaCheckPermission("/api/v1/approvalFlow/disable") + public Result disable(@JsonBody(value = "id", required = true) BigInteger id) { + assertSuperAdmin(); + approvalFlowService.disableFlow(id, SaTokenUtil.getLoginAccount().getId()); + return Result.ok(); + } + + /** + * 删除审批流程。 + * + * @param id 流程ID + * @return 处理结果 + */ + @PostMapping("/remove") + @SaCheckPermission("/api/v1/approvalFlow/remove") + public Result remove(@JsonBody(value = "id", required = true) BigInteger id) { + assertSuperAdmin(); + approvalFlowService.removeFlow(id); + return Result.ok(); + } + + private void assertSuperAdmin() { + if (!categoryPermissionService.isCurrentSuperAdmin()) { + throw new BusinessException("仅超级管理员可管理审批流程"); + } + } +} diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/ApprovalInstanceController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/ApprovalInstanceController.java new file mode 100644 index 0000000..e635e21 --- /dev/null +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/ApprovalInstanceController.java @@ -0,0 +1,139 @@ +package tech.easyflow.admin.controller.system; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.mybatisflex.core.paginate.Page; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import tech.easyflow.common.domain.Result; +import tech.easyflow.common.satoken.util.SaTokenUtil; +import tech.easyflow.common.web.jsonbody.JsonBody; +import tech.easyflow.approval.entity.vo.ApprovalActionRequest; +import tech.easyflow.approval.entity.vo.ApprovalInstanceDetailVo; +import tech.easyflow.approval.entity.vo.ApprovalInstancePageVo; +import tech.easyflow.approval.service.ApprovalInstanceService; +import tech.easyflow.approval.service.ApprovalQueryService; + +import javax.annotation.Resource; +import java.math.BigInteger; + +/** + * 审批实例控制器。 + */ +@RestController +@RequestMapping("/api/v1/approvalInstance") +public class ApprovalInstanceController { + + @Resource + private ApprovalQueryService approvalQueryService; + + @Resource + private ApprovalInstanceService approvalInstanceService; + + /** + * 分页查询待审批列表。 + * + * @param resourceType 资源类型 + * @param actionType 动作类型 + * @param keyword 关键词 + * @param pageNumber 页码 + * @param pageSize 每页数量 + * @return 分页结果 + */ + @GetMapping("/pendingPage") + @SaCheckPermission("/api/v1/approvalInstance/query") + public Result> pendingPage(String resourceType, String actionType, String keyword, + Long pageNumber, Long pageSize) { + return Result.ok(approvalQueryService.pendingPage(resourceType, actionType, keyword, pageNumber, pageSize)); + } + + /** + * 分页查询已审批列表。 + * + * @param resourceType 资源类型 + * @param actionType 动作类型 + * @param keyword 关键词 + * @param pageNumber 页码 + * @param pageSize 每页数量 + * @return 分页结果 + */ + @GetMapping("/processedPage") + @SaCheckPermission("/api/v1/approvalInstance/query") + public Result> processedPage(String resourceType, String actionType, String keyword, + Long pageNumber, Long pageSize) { + return Result.ok(approvalQueryService.processedPage(resourceType, actionType, keyword, pageNumber, pageSize)); + } + + /** + * 分页查询我发起的审批列表。 + * + * @param resourceType 资源类型 + * @param actionType 动作类型 + * @param keyword 关键词 + * @param pageNumber 页码 + * @param pageSize 每页数量 + * @return 分页结果 + */ + @GetMapping("/initiatedPage") + @SaCheckPermission("/api/v1/approvalInstance/query") + public Result> initiatedPage(String resourceType, String actionType, String keyword, + Long pageNumber, Long pageSize) { + return Result.ok(approvalQueryService.initiatedPage(resourceType, actionType, keyword, pageNumber, pageSize)); + } + + /** + * 查询审批实例详情。 + * + * @param id 审批实例ID + * @return 详情结果 + */ + @GetMapping("/detail") + @SaCheckPermission("/api/v1/approvalInstance/query") + public Result detail(BigInteger id) { + return Result.ok(approvalQueryService.detail(id)); + } + + /** + * 通过审批。 + * + * @param request 审批动作请求 + * @return 处理结果 + */ + @PostMapping("/approve") + @SaCheckPermission("/api/v1/approvalInstance/approve") + public Result approve(@JsonBody ApprovalActionRequest request) { + approvalInstanceService.approve(request.getInstanceId(), request.getComment(), currentOperatorId()); + return Result.ok(); + } + + /** + * 驳回审批。 + * + * @param request 审批动作请求 + * @return 处理结果 + */ + @PostMapping("/reject") + @SaCheckPermission("/api/v1/approvalInstance/reject") + public Result reject(@JsonBody ApprovalActionRequest request) { + approvalInstanceService.reject(request.getInstanceId(), request.getComment(), currentOperatorId()); + return Result.ok(); + } + + /** + * 撤回审批。 + * + * @param request 审批动作请求 + * @return 处理结果 + */ + @PostMapping("/revoke") + @SaCheckPermission("/api/v1/approvalInstance/revoke") + public Result revoke(@JsonBody ApprovalActionRequest request) { + approvalInstanceService.revoke(request.getInstanceId(), request.getComment(), currentOperatorId()); + return Result.ok(); + } + + private BigInteger currentOperatorId() { + return SaTokenUtil.getLoginAccount().getId(); + } +} diff --git a/easyflow-api/easyflow-api-public/src/main/java/tech/easyflow/publicapi/controller/PublicBotController.java b/easyflow-api/easyflow-api-public/src/main/java/tech/easyflow/publicapi/controller/PublicBotController.java index 27fd01b..e7364be 100644 --- a/easyflow-api/easyflow-api-public/src/main/java/tech/easyflow/publicapi/controller/PublicBotController.java +++ b/easyflow-api/easyflow-api-public/src/main/java/tech/easyflow/publicapi/controller/PublicBotController.java @@ -7,6 +7,7 @@ import jakarta.validation.constraints.NotBlank; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import tech.easyflow.approval.annotation.RequirePublishedAccess; import tech.easyflow.ai.entity.Bot; import tech.easyflow.ai.entity.ChatRequestParams; import tech.easyflow.ai.service.BotService; @@ -37,8 +38,9 @@ public class PublicBotController { * 根据id或别名获取bot详情 */ @GetMapping("/getByIdOrAlias") + @RequirePublishedAccess(resourceType = "BOT", idExpr = "#key", denyMessage = "聊天助手尚未发布") public Result getByIdOrAlias(@NotBlank(message = "key不能为空") String key) { - return Result.ok(botService.getDetail(key)); + return Result.ok(botService.getPublishedDetail(key)); } /** @@ -47,6 +49,7 @@ public class PublicBotController { * @return 返回SseEmitter对象,用于服务器向客户端推送聊天响应数据 */ @PostMapping("chat") + @RequirePublishedAccess(resourceType = "BOT", idExpr = "#chatRequestParams.botId", denyMessage = "聊天助手尚未发布") public SseEmitter chat(@RequestBody ChatRequestParams chatRequestParams, HttpServletRequest request) { String apikey = request.getHeader(SysApiKey.KEY_Apikey); String requestURI = request.getRequestURI(); diff --git a/easyflow-api/easyflow-api-public/src/main/java/tech/easyflow/publicapi/controller/PublicWorkflowController.java b/easyflow-api/easyflow-api-public/src/main/java/tech/easyflow/publicapi/controller/PublicWorkflowController.java index de0e6e6..7405302 100644 --- a/easyflow-api/easyflow-api-public/src/main/java/tech/easyflow/publicapi/controller/PublicWorkflowController.java +++ b/easyflow-api/easyflow-api-public/src/main/java/tech/easyflow/publicapi/controller/PublicWorkflowController.java @@ -8,6 +8,8 @@ import com.easyagents.flow.core.parser.ChainParser; import jakarta.annotation.Resource; import jakarta.validation.constraints.NotBlank; import org.springframework.web.bind.annotation.*; +import tech.easyflow.approval.annotation.RequirePublishedAccess; +import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds; import tech.easyflow.ai.easyagentsflow.entity.ChainInfo; import tech.easyflow.ai.easyagentsflow.entity.NodeInfo; import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage; @@ -51,10 +53,11 @@ public class PublicWorkflowController { * @return 工作流详情 */ @GetMapping(value = "/getByIdOrAlias") + @RequirePublishedAccess(resourceType = "WORKFLOW", idExpr = "#key", denyMessage = "工作流尚未发布") public Result getByIdOrAlias( @RequestParam @NotBlank(message = "key不能为空") String key) { - Workflow workflow = workflowService.getDetail(key); + Workflow workflow = workflowService.getPublishedDetail(key); return Result.ok(workflow); } @@ -81,17 +84,18 @@ public class PublicWorkflowController { */ @PostMapping("/runAsync") @SaCheckPermission("/api/v1/workflow/save") + @RequirePublishedAccess(resourceType = "WORKFLOW", idExpr = "#id", denyMessage = "工作流尚未发布") public Result runAsync(@JsonBody(value = "id", required = true) BigInteger id, @JsonBody("variables") Map variables) { if (variables == null) { variables = new HashMap<>(); } - Workflow workflow = workflowService.getById(id); + Workflow workflow = workflowService.getPublishedById(id); if (workflow == null) { throw new RuntimeException("工作流不存在"); } workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId()); - String executeId = chainExecutor.executeAsync(id.toString(), variables); + String executeId = chainExecutor.executeAsync(PublishedWorkflowDefinitionIds.published(id.toString()), variables); return Result.ok(executeId); } @@ -118,8 +122,9 @@ public class PublicWorkflowController { @GetMapping("getRunningParameters") @SaCheckPermission("/api/v1/workflow/query") + @RequirePublishedAccess(resourceType = "WORKFLOW", idExpr = "#id", denyMessage = "工作流尚未发布") public Result getRunningParameters(@RequestParam BigInteger id) { - Workflow workflow = workflowService.getById(id); + Workflow workflow = workflowService.getPublishedById(id); if (workflow == null) { return Result.fail(1, "can not find the workflow by id: " + id); diff --git a/easyflow-modules/easyflow-module-ai/pom.xml b/easyflow-modules/easyflow-module-ai/pom.xml index e0a6d7f..c177e1d 100644 --- a/easyflow-modules/easyflow-module-ai/pom.xml +++ b/easyflow-modules/easyflow-module-ai/pom.xml @@ -80,6 +80,10 @@ tech.easyflow easyflow-module-system + + tech.easyflow + easyflow-module-approval + org.java-websocket diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagents/tool/WorkflowTool.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagents/tool/WorkflowTool.java index 8076aa6..9d02e6e 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagents/tool/WorkflowTool.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagents/tool/WorkflowTool.java @@ -14,25 +14,31 @@ import java.util.*; public class WorkflowTool extends BaseTool { private BigInteger workflowId; + private String definitionId; public WorkflowTool() { } public WorkflowTool(Workflow workflow, boolean needEnglishName) { + this(workflow, needEnglishName, workflow.getId() == null ? null : workflow.getId().toString()); + } + + public WorkflowTool(Workflow workflow, boolean needEnglishName, String definitionId) { this.workflowId = workflow.getId(); + this.definitionId = definitionId; if (needEnglishName) { this.name = workflow.getEnglishName(); } else { this.name = workflow.getTitle(); } this.description = workflow.getDescription(); - this.parameters = toParameters(workflow); + this.parameters = toParameters(workflow, definitionId); } - static Parameter[] toParameters(Workflow workflow) { + static Parameter[] toParameters(Workflow workflow, String definitionId) { ChainExecutor executor = SpringContextUtil.getBean(ChainExecutor.class); - ChainDefinition definition = executor.getDefinitionRepository().getChainDefinitionById(workflow.getId().toString()); + ChainDefinition definition = executor.getDefinitionRepository().getChainDefinitionById(definitionId); List parameterDefs = definition.getStartParameters(); if (parameterDefs == null || parameterDefs.isEmpty()) { return new Parameter[0]; @@ -131,16 +137,25 @@ public class WorkflowTool extends BaseTool { this.workflowId = workflowId; } + public String getDefinitionId() { + return definitionId; + } + + public void setDefinitionId(String definitionId) { + this.definitionId = definitionId; + } + @Override public Object invoke(Map argsMap) { ChainExecutor executor = SpringContextUtil.getBean(ChainExecutor.class); - return executor.execute(workflowId.toString(), argsMap); + return executor.execute(definitionId, argsMap); } @Override public String toString() { return "AiWorkflowFunction{" + "workflowId=" + workflowId + + ", definitionId='" + definitionId + '\'' + ", name='" + name + '\'' + ", description='" + description + '\'' + ", parameters=" + Arrays.toString(parameters) + diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/repository/ChainDefinitionRepositoryImpl.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/repository/ChainDefinitionRepositoryImpl.java index e6a40c9..5781d91 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/repository/ChainDefinitionRepositoryImpl.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/repository/ChainDefinitionRepositoryImpl.java @@ -4,6 +4,7 @@ import com.easyagents.flow.core.chain.ChainDefinition; import com.easyagents.flow.core.chain.repository.ChainDefinitionRepository; import com.easyagents.flow.core.parser.ChainParser; import org.springframework.stereotype.Component; +import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds; import tech.easyflow.ai.easyagentsflow.service.WorkflowDatacenterContentService; import tech.easyflow.ai.entity.Workflow; import tech.easyflow.ai.service.WorkflowService; @@ -22,10 +23,14 @@ public class ChainDefinitionRepositoryImpl implements ChainDefinitionRepository @Override public ChainDefinition getChainDefinitionById(String id) { - Workflow workflow = workflowService.getById(id); + boolean publishedDefinition = PublishedWorkflowDefinitionIds.isPublished(id); + String workflowId = PublishedWorkflowDefinitionIds.unwrap(id); + Workflow workflow = publishedDefinition + ? workflowService.getPublishedById(new java.math.BigInteger(workflowId)) + : workflowService.getById(workflowId); String json = workflowDatacenterContentService.prepareContent(workflow.getContent()); ChainDefinition chainDefinition = chainParser.parse(json); - chainDefinition.setId(workflow.getId().toString()); + chainDefinition.setId(id); chainDefinition.setName(workflow.getEnglishName()); chainDefinition.setDescription(workflow.getDescription()); return chainDefinition; diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/support/PublishedWorkflowDefinitionIds.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/support/PublishedWorkflowDefinitionIds.java new file mode 100644 index 0000000..6f5f68a --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/support/PublishedWorkflowDefinitionIds.java @@ -0,0 +1,45 @@ +package tech.easyflow.ai.easyagentsflow.support; + +/** + * 已发布工作流定义 ID 工具。 + */ +public final class PublishedWorkflowDefinitionIds { + + private static final String PREFIX = "published:"; + + private PublishedWorkflowDefinitionIds() { + } + + /** + * 构建已发布定义 ID。 + * + * @param workflowId 工作流 ID + * @return 已发布定义 ID + */ + public static String published(String workflowId) { + return PREFIX + workflowId; + } + + /** + * 是否为已发布定义 ID。 + * + * @param definitionId 定义 ID + * @return 命中已发布定义前缀时返回 true + */ + public static boolean isPublished(String definitionId) { + return definitionId != null && definitionId.startsWith(PREFIX); + } + + /** + * 还原真实工作流 ID。 + * + * @param definitionId 定义 ID + * @return 原始工作流 ID + */ + public static String unwrap(String definitionId) { + if (!isPublished(definitionId)) { + return definitionId; + } + return definitionId.substring(PREFIX.length()); + } +} 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 2f27eb8..dd10c75 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 @@ -19,4 +19,8 @@ public class Workflow extends WorkflowBase implements VisibilityResource { public Tool toFunction(boolean needEnglishName) { return new WorkflowTool(this, needEnglishName); } + + public Tool toFunction(boolean needEnglishName, String definitionId) { + return new WorkflowTool(this, needEnglishName, definitionId); + } } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/base/BotBase.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/base/BotBase.java index d717e47..e96394d 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/base/BotBase.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/base/BotBase.java @@ -111,6 +111,36 @@ public class BotBase extends DateEntity implements Serializable { @Column(comment = "修改者ID") private BigInteger modifiedBy; + /** + * 发布状态 + */ + @Column(comment = "发布状态") + private String publishStatus; + + /** + * 当前审批实例ID + */ + @Column(comment = "当前审批实例ID") + private BigInteger currentApprovalInstanceId; + + /** + * 已发布快照 + */ + @Column(typeHandler = FastjsonTypeHandler.class, comment = "已发布快照") + private Map publishedSnapshotJson; + + /** + * 发布时间 + */ + @Column(comment = "发布时间") + private Date publishedAt; + + /** + * 发布人 + */ + @Column(comment = "发布人") + private BigInteger publishedBy; + public BigInteger getId() { return id; } @@ -239,4 +269,44 @@ public class BotBase extends DateEntity implements Serializable { this.modifiedBy = modifiedBy; } + public String getPublishStatus() { + return publishStatus; + } + + public void setPublishStatus(String publishStatus) { + this.publishStatus = publishStatus; + } + + public BigInteger getCurrentApprovalInstanceId() { + return currentApprovalInstanceId; + } + + public void setCurrentApprovalInstanceId(BigInteger currentApprovalInstanceId) { + this.currentApprovalInstanceId = currentApprovalInstanceId; + } + + public Map getPublishedSnapshotJson() { + return publishedSnapshotJson; + } + + public void setPublishedSnapshotJson(Map publishedSnapshotJson) { + this.publishedSnapshotJson = publishedSnapshotJson; + } + + public Date getPublishedAt() { + return publishedAt; + } + + public void setPublishedAt(Date publishedAt) { + this.publishedAt = publishedAt; + } + + public BigInteger getPublishedBy() { + return publishedBy; + } + + public void setPublishedBy(BigInteger publishedBy) { + this.publishedBy = publishedBy; + } + } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/base/DocumentCollectionBase.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/base/DocumentCollectionBase.java index 6072589..7752445 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/base/DocumentCollectionBase.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/base/DocumentCollectionBase.java @@ -166,6 +166,36 @@ public class DocumentCollectionBase extends DateEntity implements Serializable { @Column(comment = "可见范围") private String visibilityScope; + /** + * 发布状态 + */ + @Column(comment = "发布状态") + private String publishStatus; + + /** + * 当前审批实例ID + */ + @Column(comment = "当前审批实例ID") + private BigInteger currentApprovalInstanceId; + + /** + * 已发布快照 + */ + @Column(typeHandler = FastjsonTypeHandler.class, comment = "已发布快照") + private Map publishedSnapshotJson; + + /** + * 发布时间 + */ + @Column(comment = "发布时间") + private Date publishedAt; + + /** + * 发布人 + */ + @Column(comment = "发布人") + private BigInteger publishedBy; + public BigInteger getId() { return id; } @@ -366,4 +396,44 @@ public class DocumentCollectionBase extends DateEntity implements Serializable { this.visibilityScope = visibilityScope; } + public String getPublishStatus() { + return publishStatus; + } + + public void setPublishStatus(String publishStatus) { + this.publishStatus = publishStatus; + } + + public BigInteger getCurrentApprovalInstanceId() { + return currentApprovalInstanceId; + } + + public void setCurrentApprovalInstanceId(BigInteger currentApprovalInstanceId) { + this.currentApprovalInstanceId = currentApprovalInstanceId; + } + + public Map getPublishedSnapshotJson() { + return publishedSnapshotJson; + } + + public void setPublishedSnapshotJson(Map publishedSnapshotJson) { + this.publishedSnapshotJson = publishedSnapshotJson; + } + + public Date getPublishedAt() { + return publishedAt; + } + + public void setPublishedAt(Date publishedAt) { + this.publishedAt = publishedAt; + } + + public BigInteger getPublishedBy() { + return publishedBy; + } + + public void setPublishedBy(BigInteger publishedBy) { + this.publishedBy = publishedBy; + } + } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/base/WorkflowBase.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/base/WorkflowBase.java index 56134a9..97430f5 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/base/WorkflowBase.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/base/WorkflowBase.java @@ -3,9 +3,11 @@ package tech.easyflow.ai.entity.base; import com.mybatisflex.annotation.Column; import com.mybatisflex.annotation.Id; import com.mybatisflex.annotation.KeyType; +import com.mybatisflex.core.handler.FastjsonTypeHandler; import java.io.Serializable; import java.math.BigInteger; import java.util.Date; +import java.util.Map; import tech.easyflow.common.entity.DateEntity; @@ -109,6 +111,36 @@ public class WorkflowBase extends DateEntity implements Serializable { @Column(comment = "可见范围") private String visibilityScope; + /** + * 发布状态 + */ + @Column(comment = "发布状态") + private String publishStatus; + + /** + * 当前审批实例ID + */ + @Column(comment = "当前审批实例ID") + private BigInteger currentApprovalInstanceId; + + /** + * 已发布快照 + */ + @Column(typeHandler = FastjsonTypeHandler.class, comment = "已发布快照") + private Map publishedSnapshotJson; + + /** + * 发布时间 + */ + @Column(comment = "发布时间") + private Date publishedAt; + + /** + * 发布人 + */ + @Column(comment = "发布人") + private BigInteger publishedBy; + public BigInteger getId() { return id; } @@ -237,4 +269,44 @@ public class WorkflowBase extends DateEntity implements Serializable { this.visibilityScope = visibilityScope; } + public String getPublishStatus() { + return publishStatus; + } + + public void setPublishStatus(String publishStatus) { + this.publishStatus = publishStatus; + } + + public BigInteger getCurrentApprovalInstanceId() { + return currentApprovalInstanceId; + } + + public void setCurrentApprovalInstanceId(BigInteger currentApprovalInstanceId) { + this.currentApprovalInstanceId = currentApprovalInstanceId; + } + + public Map getPublishedSnapshotJson() { + return publishedSnapshotJson; + } + + public void setPublishedSnapshotJson(Map publishedSnapshotJson) { + this.publishedSnapshotJson = publishedSnapshotJson; + } + + public Date getPublishedAt() { + return publishedAt; + } + + public void setPublishedAt(Date publishedAt) { + this.publishedAt = publishedAt; + } + + public BigInteger getPublishedBy() { + return publishedBy; + } + + public void setPublishedBy(BigInteger publishedBy) { + this.publishedBy = publishedBy; + } + } 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 new file mode 100644 index 0000000..24c5d96 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/enums/PublishStatus.java @@ -0,0 +1,56 @@ +package tech.easyflow.ai.enums; + +import java.util.Arrays; +import java.util.Locale; + +/** + * AI 资源发布状态。 + */ +public enum PublishStatus { + + DRAFT("DRAFT"), + PUBLISH_PENDING("PUBLISH_PENDING"), + PUBLISHED("PUBLISHED"), + DELETE_PENDING("DELETE_PENDING"); + + private final String code; + + PublishStatus(String code) { + this.code = code; + } + + /** + * 获取状态编码。 + * + * @return 状态编码 + */ + public String getCode() { + return code; + } + + /** + * 是否允许作为线上版本使用。 + * + * @return 允许外部访问或线上运行时返回 true + */ + public boolean isExternallyVisible() { + return this == PUBLISHED || this == DELETE_PENDING; + } + + /** + * 解析发布状态。 + * + * @param code 状态编码 + * @return 发布状态 + */ + public static PublishStatus from(String code) { + if (code == null || code.isBlank()) { + return DRAFT; + } + String normalized = code.trim().toUpperCase(Locale.ROOT); + return Arrays.stream(values()) + .filter(item -> item.code.equals(normalized)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("不支持的发布状态: " + code)); + } +} 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 new file mode 100644 index 0000000..fa24d84 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/BotApprovalSubjectHandler.java @@ -0,0 +1,438 @@ +package tech.easyflow.ai.publish; + +import com.alibaba.fastjson2.JSON; +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.BotMcp; +import tech.easyflow.ai.entity.BotPlugin; +import tech.easyflow.ai.entity.BotWorkflow; +import tech.easyflow.ai.entity.DocumentCollection; +import tech.easyflow.ai.entity.Mcp; +import tech.easyflow.ai.entity.Model; +import tech.easyflow.ai.entity.PluginItem; +import tech.easyflow.ai.entity.Workflow; +import tech.easyflow.ai.enums.PublishStatus; +import tech.easyflow.ai.service.BotCategoryService; +import tech.easyflow.ai.service.BotDocumentCollectionService; +import tech.easyflow.ai.service.BotMcpService; +import tech.easyflow.ai.service.BotPluginService; +import tech.easyflow.ai.service.BotService; +import tech.easyflow.ai.service.BotWorkflowService; +import tech.easyflow.ai.service.DocumentCollectionService; +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.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.SysDeptService; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Date; +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"; + + private final BotService botService; + private final BotWorkflowService botWorkflowService; + private final BotDocumentCollectionService botDocumentCollectionService; + private final BotPluginService botPluginService; + 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; + private final SysDeptService sysDeptService; + private final PluginItemService pluginItemService; + private final McpService mcpService; + + public BotApprovalSubjectHandler(BotService botService, + BotWorkflowService botWorkflowService, + BotDocumentCollectionService botDocumentCollectionService, + BotPluginService botPluginService, + BotMcpService botMcpService, + WorkflowService workflowService, + DocumentCollectionService documentCollectionService, + ApprovalInstanceService approvalInstanceService, + CategoryPermissionService categoryPermissionService, + ModelService modelService, + BotCategoryService botCategoryService, + SysDeptService sysDeptService, + PluginItemService pluginItemService, + McpService mcpService) { + this.botService = botService; + this.botWorkflowService = botWorkflowService; + this.botDocumentCollectionService = botDocumentCollectionService; + this.botPluginService = botPluginService; + this.botMcpService = botMcpService; + this.workflowService = workflowService; + this.documentCollectionService = documentCollectionService; + this.approvalInstanceService = approvalInstanceService; + this.categoryPermissionService = categoryPermissionService; + this.modelService = modelService; + this.botCategoryService = botCategoryService; + this.sysDeptService = sysDeptService; + this.pluginItemService = pluginItemService; + this.mcpService = mcpService; + } + + @Override + public String resourceType() { + 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)); + if (bot == null || !PublishStatus.from(bot.getPublishStatus()).isExternallyVisible() + || bot.getPublishedSnapshotJson() == null || bot.getPublishedSnapshotJson().isEmpty()) { + throw new BusinessException(denyMessage); + } + } + + private Bot requireBot(BigInteger id) { + Bot bot = botService.getById(id); + if (bot == null) { + throw new BusinessException("聊天助手不存在"); + } + return bot; + } + + private void assertManagePermission(Bot bot) { + LoginAccount account = SaTokenUtil.getLoginAccount(); + boolean superAdmin = categoryPermissionService.isCurrentSuperAdmin(); + boolean creator = account != null && account.getId() != null && account.getId().equals(bot.getCreatedBy()); + if (!superAdmin && !creator) { + throw new BusinessException("仅创建者或超级管理员可管理聊天助手"); + } + } + + private Map buildResourceSnapshot(Bot bot) { + Model model = resolveModel(bot.getModelId()); + BotCategory category = resolveCategory(bot.getCategoryId()); + SysDept dept = resolveDept(bot.getDeptId()); + Map snapshot = new LinkedHashMap<>(); + snapshot.put("id", bot.getId()); + snapshot.put("alias", bot.getAlias()); + snapshot.put("deptId", bot.getDeptId()); + snapshot.put("deptName", dept == null ? null : dept.getDeptName()); + snapshot.put("categoryId", bot.getCategoryId()); + snapshot.put("categoryName", category == null ? null : category.getCategoryName()); + snapshot.put("title", bot.getTitle()); + snapshot.put("description", bot.getDescription()); + snapshot.put("icon", bot.getIcon()); + snapshot.put("modelId", bot.getModelId()); + snapshot.put("modelName", resolveModelName(model)); + snapshot.put("modelOptions", bot.getModelOptions()); + snapshot.put("systemPrompt", resolveSystemPrompt(bot)); + snapshot.put("temperature", readNumberOption(bot.getModelOptions(), "temperature")); + snapshot.put("topP", readNumberOption(bot.getModelOptions(), "topP")); + snapshot.put("topK", readNumberOption(bot.getModelOptions(), "topK")); + snapshot.put("maxReplyLength", readNumberOption(bot.getModelOptions(), "maxReplyLength")); + snapshot.put("maxMessageCount", readNumberOption(bot.getModelOptions(), Bot.KEY_MAX_MESSAGE_COUNT)); + snapshot.put("status", bot.getStatus()); + snapshot.put("options", bot.getOptions()); + snapshot.put("anonymousEnabled", bot.isAnonymousEnabled()); + snapshot.put("workflowBindings", buildWorkflowBindings(bot.getId())); + snapshot.put("knowledgeBindings", buildKnowledgeBindings(bot.getId())); + snapshot.put("pluginBindings", buildPluginBindings(bot.getId())); + snapshot.put("mcpBindings", buildMcpBindings(bot.getId())); + return snapshot; + } + + private List> buildWorkflowBindings(BigInteger botId) { + QueryWrapper queryWrapper = QueryWrapper.create().eq(BotWorkflow::getBotId, botId); + List relations = botWorkflowService.getMapper().selectListWithRelationsByQuery(queryWrapper); + List> result = new ArrayList<>(); + for (BotWorkflow relation : relations) { + Workflow workflow = relation.getWorkflow(); + if (workflow == null || !PublishStatus.from(workflow.getPublishStatus()).isExternallyVisible()) { + throw new BusinessException("聊天助手绑定的工作流未发布,无法发布聊天助手"); + } + Map item = new LinkedHashMap<>(); + item.put("workflowId", relation.getWorkflowId()); + item.put("workflowName", workflow.getTitle()); + result.add(item); + } + return result; + } + + private List> buildKnowledgeBindings(BigInteger botId) { + QueryWrapper queryWrapper = QueryWrapper.create().eq(BotDocumentCollection::getBotId, botId); + List relations = botDocumentCollectionService.getMapper().selectListWithRelationsByQuery(queryWrapper); + List> result = new ArrayList<>(); + for (BotDocumentCollection relation : relations) { + DocumentCollection knowledge = relation.getKnowledge(); + if (knowledge == null || !PublishStatus.from(knowledge.getPublishStatus()).isExternallyVisible()) { + throw new BusinessException("聊天助手绑定的知识库未发布,无法发布聊天助手"); + } + Map item = new LinkedHashMap<>(); + item.put("knowledgeId", relation.getDocumentCollectionId()); + item.put("knowledgeName", knowledge.getTitle()); + item.put("retrievalMode", relation.getRetrievalMode() == null ? null : relation.getRetrievalMode().name()); + result.add(item); + } + return result; + } + + private List> buildPluginBindings(BigInteger botId) { + QueryWrapper queryWrapper = QueryWrapper.create().eq(BotPlugin::getBotId, botId); + List relations = botPluginService.list(queryWrapper); + List> result = new ArrayList<>(); + for (BotPlugin relation : relations) { + PluginItem pluginItem = pluginItemService.getById(relation.getPluginItemId()); + if (pluginItem == null) { + throw new BusinessException("聊天助手绑定的插件工具不存在,无法发布聊天助手"); + } + Map item = new LinkedHashMap<>(); + item.put("pluginItemId", relation.getPluginItemId()); + item.put("pluginItemName", resolvePluginName(pluginItem)); + result.add(item); + } + return result; + } + + private List> buildMcpBindings(BigInteger botId) { + QueryWrapper queryWrapper = QueryWrapper.create().eq(BotMcp::getBotId, botId); + List relations = botMcpService.list(queryWrapper); + List> result = new ArrayList<>(); + for (BotMcp relation : relations) { + Mcp mcp = mcpService.getById(relation.getMcpId()); + if (mcp == null) { + 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)); + } +} 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 new file mode 100644 index 0000000..5e8bccd --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/BotPublishAppService.java @@ -0,0 +1,52 @@ +package tech.easyflow.ai.publish; + +import org.springframework.stereotype.Service; +import tech.easyflow.approval.annotation.ApprovalAction; +import tech.easyflow.common.web.exceptions.BusinessException; + +import java.math.BigInteger; + +/** + * 聊天助手发布生命周期应用服务。 + */ +@Service +public class BotPublishAppService { + + /** + * 提交聊天助手发布审批。 + * + * @param id 助手 ID + * @return 助手 ID + */ + @ApprovalAction( + resourceType = "BOT", + actionType = "PUBLISH", + idExpr = "#id" + ) + public BigInteger submitPublishApproval(BigInteger id) { + assertId(id); + return id; + } + + /** + * 提交聊天助手删除审批。 + * + * @param id 助手 ID + * @return 助手 ID + */ + @ApprovalAction( + resourceType = "BOT", + actionType = "DELETE", + idExpr = "#id" + ) + public BigInteger submitDeleteApproval(BigInteger id) { + assertId(id); + return id; + } + + private void assertId(BigInteger id) { + if (id == null) { + throw new BusinessException("聊天助手审批时资源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 new file mode 100644 index 0000000..da604ca --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/KnowledgeApprovalSubjectHandler.java @@ -0,0 +1,333 @@ +package tech.easyflow.ai.publish; + +import com.alibaba.fastjson2.JSON; +import com.mybatisflex.core.query.QueryWrapper; +import org.springframework.stereotype.Component; +import tech.easyflow.ai.entity.BotDocumentCollection; +import tech.easyflow.ai.entity.DocumentCollection; +import tech.easyflow.ai.entity.DocumentCollectionCategory; +import tech.easyflow.ai.entity.Model; +import tech.easyflow.ai.enums.PublishStatus; +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.approval.service.ApprovalInstanceService; +import tech.easyflow.approval.service.ApprovalSubjectHandler; +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 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"; + + 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; + + public KnowledgeApprovalSubjectHandler(DocumentCollectionService documentCollectionService, + ResourceAccessService resourceAccessService, + ApprovalInstanceService approvalInstanceService, + BotDocumentCollectionService botDocumentCollectionService, + ModelService modelService, + DocumentCollectionCategoryService documentCollectionCategoryService, + SysDeptService sysDeptService) { + this.documentCollectionService = documentCollectionService; + this.resourceAccessService = resourceAccessService; + this.approvalInstanceService = approvalInstanceService; + this.botDocumentCollectionService = botDocumentCollectionService; + this.modelService = modelService; + this.documentCollectionCategoryService = documentCollectionCategoryService; + this.sysDeptService = sysDeptService; + } + + @Override + public String resourceType() { + 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)); + if (collection == null || !PublishStatus.from(collection.getPublishStatus()).isExternallyVisible() + || collection.getPublishedSnapshotJson() == null || collection.getPublishedSnapshotJson().isEmpty()) { + throw new BusinessException(denyMessage); + } + } + + private DocumentCollection requireKnowledge(BigInteger id) { + DocumentCollection knowledge = documentCollectionService.getById(id); + 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); + } + + private Map buildResourceSnapshot(DocumentCollection collection) { + Model vectorModel = resolveModel(collection.getVectorEmbedModelId(), "知识库向量模型不存在,无法提交审批"); + Model rerankModel = resolveOptionalModel(collection.getRerankModelId(), "知识库重排模型不存在,无法提交审批"); + DocumentCollectionCategory category = resolveCategory(collection.getCategoryId()); + SysDept dept = resolveDept(collection.getDeptId()); + Map snapshot = new LinkedHashMap<>(); + snapshot.put("id", collection.getId()); + snapshot.put("collectionType", collection.getCollectionType()); + snapshot.put("collectionTypeLabel", resolveCollectionTypeLabel(collection.getCollectionType())); + snapshot.put("alias", collection.getAlias()); + snapshot.put("deptId", collection.getDeptId()); + snapshot.put("deptName", dept == null ? null : dept.getDeptName()); + snapshot.put("icon", collection.getIcon()); + snapshot.put("title", collection.getTitle()); + snapshot.put("description", collection.getDescription()); + snapshot.put("slug", collection.getSlug()); + snapshot.put("vectorStoreEnable", collection.getVectorStoreEnable()); + snapshot.put("vectorStoreType", collection.getVectorStoreType()); + snapshot.put("vectorEmbedModelId", collection.getVectorEmbedModelId()); + snapshot.put("vectorEmbedModelName", resolveModelName(vectorModel)); + snapshot.put("dimensionOfVectorModel", collection.getDimensionOfVectorModel()); + Map options = collection.getOptions() == null + ? Collections.emptyMap() + : new LinkedHashMap<>(collection.getOptions()); + snapshot.put("options", options); + snapshot.put("canUpdateEmbeddingModel", collection.getOptionsByKey(DocumentCollection.KEY_CAN_UPDATE_EMBEDDING_MODEL)); + snapshot.put("rerankEnable", collection.getOptionsByKey(DocumentCollection.KEY_RERANK_ENABLE)); + snapshot.put("rerankModelId", collection.getRerankModelId()); + snapshot.put("rerankModelName", rerankModel == null ? null : resolveModelName(rerankModel)); + snapshot.put("searchEngineEnable", collection.getSearchEngineEnable()); + snapshot.put("englishName", collection.getEnglishName()); + snapshot.put("categoryId", collection.getCategoryId()); + snapshot.put("categoryName", category == null ? null : category.getCategoryName()); + snapshot.put("visibilityScope", collection.getVisibilityScope()); + snapshot.put("visibilityScopeLabel", resolveVisibilityScopeLabel(collection.getVisibilityScope())); + return snapshot; + } + + /** + * 解析知识库分类信息。 + * + * @param categoryId 分类 ID + * @return 分类实体,不存在时返回 {@code null} + */ + private DocumentCollectionCategory resolveCategory(BigInteger categoryId) { + if (categoryId == null) { + return null; + } + return documentCollectionCategoryService.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 + * @param errorMessage 模型不存在时抛出的提示 + * @return 模型实体 + */ + private Model resolveModel(BigInteger modelId, String errorMessage) { + if (modelId == null) { + throw new BusinessException(errorMessage); + } + Model model = modelService.getById(modelId); + if (model == null) { + throw new BusinessException(errorMessage); + } + return model; + } + + /** + * 解析可选模型。 + * + * @param modelId 模型 ID + * @param errorMessage 模型不存在时抛出的提示 + * @return 模型实体,不存在时返回 {@code null} + */ + private Model resolveOptionalModel(BigInteger modelId, String errorMessage) { + if (modelId == null) { + return null; + } + Model model = modelService.getById(modelId); + if (model == null) { + throw new BusinessException(errorMessage); + } + 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(); + } + return 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)) { + 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); + } +} 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 new file mode 100644 index 0000000..f1a05ea --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/KnowledgePublishAppService.java @@ -0,0 +1,52 @@ +package tech.easyflow.ai.publish; + +import org.springframework.stereotype.Service; +import tech.easyflow.approval.annotation.ApprovalAction; +import tech.easyflow.common.web.exceptions.BusinessException; + +import java.math.BigInteger; + +/** + * 知识库发布生命周期应用服务。 + */ +@Service +public class KnowledgePublishAppService { + + /** + * 提交知识库发布审批。 + * + * @param id 知识库 ID + * @return 知识库 ID + */ + @ApprovalAction( + resourceType = "KNOWLEDGE", + actionType = "PUBLISH", + idExpr = "#id" + ) + public BigInteger submitPublishApproval(BigInteger id) { + assertId(id); + return id; + } + + /** + * 提交知识库删除审批。 + * + * @param id 知识库 ID + * @return 知识库 ID + */ + @ApprovalAction( + resourceType = "KNOWLEDGE", + actionType = "DELETE", + idExpr = "#id" + ) + public BigInteger submitDeleteApproval(BigInteger id) { + assertId(id); + return id; + } + + private void assertId(BigInteger id) { + if (id == null) { + throw new BusinessException("知识库审批时资源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 new file mode 100644 index 0000000..709b3ce --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/WorkflowApprovalSubjectHandler.java @@ -0,0 +1,190 @@ +package tech.easyflow.ai.publish; + +import com.alibaba.fastjson2.JSON; +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.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.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"; + + private final WorkflowService workflowService; + private final ResourceAccessService resourceAccessService; + private final ApprovalInstanceService approvalInstanceService; + private final BotWorkflowService botWorkflowService; + + public WorkflowApprovalSubjectHandler(WorkflowService workflowService, + ResourceAccessService resourceAccessService, + ApprovalInstanceService approvalInstanceService, + BotWorkflowService botWorkflowService) { + this.workflowService = workflowService; + this.resourceAccessService = resourceAccessService; + this.approvalInstanceService = approvalInstanceService; + this.botWorkflowService = botWorkflowService; + } + + @Override + public String resourceType() { + 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)); + if (workflow == null || !PublishStatus.from(workflow.getPublishStatus()).isExternallyVisible() + || workflow.getPublishedSnapshotJson() == null || workflow.getPublishedSnapshotJson().isEmpty()) { + throw new BusinessException(denyMessage); + } + } + + private Workflow requireWorkflow(BigInteger id) { + Workflow workflow = workflowService.getById(id); + if (workflow == null) { + throw new BusinessException("工作流不存在"); + } + return workflow; + } + + 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 new file mode 100644 index 0000000..5b31002 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/publish/WorkflowPublishAppService.java @@ -0,0 +1,54 @@ +package tech.easyflow.ai.publish; + +import org.springframework.stereotype.Service; +import tech.easyflow.approval.annotation.ApprovalAction; +import tech.easyflow.approval.enums.ApprovalActionType; +import tech.easyflow.approval.enums.ApprovalResourceType; +import tech.easyflow.common.web.exceptions.BusinessException; + +import java.math.BigInteger; + +/** + * 工作流发布生命周期应用服务。 + */ +@Service +public class WorkflowPublishAppService { + + /** + * 提交工作流发布审批。 + * + * @param id 工作流 ID + * @return 工作流 ID + */ + @ApprovalAction( + resourceType = "WORKFLOW", + actionType = "PUBLISH", + idExpr = "#id" + ) + public BigInteger submitPublishApproval(BigInteger id) { + assertId(id, ApprovalResourceType.WORKFLOW.getCode(), ApprovalActionType.PUBLISH.getCode()); + return id; + } + + /** + * 提交工作流删除审批。 + * + * @param id 工作流 ID + * @return 工作流 ID + */ + @ApprovalAction( + resourceType = "WORKFLOW", + actionType = "DELETE", + idExpr = "#id" + ) + public BigInteger submitDeleteApproval(BigInteger id) { + assertId(id, ApprovalResourceType.WORKFLOW.getCode(), ApprovalActionType.DELETE.getCode()); + return id; + } + + private void assertId(BigInteger id, String resourceType, String actionType) { + if (id == null) { + throw new BusinessException(resourceType + " " + actionType + " 审批时资源ID不能为空"); + } + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/BotService.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/BotService.java index 058b660..b0ad996 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/BotService.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/BotService.java @@ -30,6 +30,30 @@ public interface BotService extends IService { Bot getByAlias(String alias); + /** + * 获取已发布视图。 + * + * @param idOrAlias ID 或别名 + * @return 已发布视图 + */ + Bot getPublishedDetail(String idOrAlias); + + /** + * 根据 ID 获取已发布视图。 + * + * @param id 机器人 ID + * @return 已发布视图 + */ + Bot getPublishedById(BigInteger id); + + /** + * 把当前资源映射为已发布视图。 + * + * @param bot 当前资源 + * @return 已发布视图 + */ + Bot toPublishedView(Bot bot); + SseEmitter checkChatBeforeStart(BigInteger botId, String prompt, String conversationId, BotServiceImpl.ChatCheckResult chatCheckResult); SseEmitter startChat(BigInteger botId, String prompt, BigInteger conversationId, List> messages, diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/DocumentCollectionService.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/DocumentCollectionService.java index c8748a2..b29bc8c 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/DocumentCollectionService.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/DocumentCollectionService.java @@ -23,4 +23,28 @@ public interface DocumentCollectionService extends IService DocumentCollection getDetail(String idOrAlias); DocumentCollection getByAlias(String idOrAlias); + + /** + * 获取已发布视图。 + * + * @param idOrAlias ID 或别名 + * @return 已发布视图 + */ + DocumentCollection getPublishedDetail(String idOrAlias); + + /** + * 根据 ID 获取已发布视图。 + * + * @param id 知识库 ID + * @return 已发布视图 + */ + DocumentCollection getPublishedById(BigInteger id); + + /** + * 把当前资源映射为已发布视图。 + * + * @param collection 当前资源 + * @return 已发布视图 + */ + DocumentCollection toPublishedView(DocumentCollection collection); } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/WorkflowService.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/WorkflowService.java index 4fd7171..d5a2a1c 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/WorkflowService.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/WorkflowService.java @@ -3,6 +3,8 @@ package tech.easyflow.ai.service; import tech.easyflow.ai.entity.Workflow; import com.mybatisflex.core.service.IService; +import java.math.BigInteger; + /** * 服务层。 * @@ -18,4 +20,28 @@ public interface WorkflowService extends IService { Workflow getByAlias(String alias); + + /** + * 获取已发布视图。 + * + * @param idOrAlias ID 或别名 + * @return 已发布视图 + */ + Workflow getPublishedDetail(String idOrAlias); + + /** + * 根据 ID 获取已发布视图。 + * + * @param id 工作流 ID + * @return 已发布视图 + */ + Workflow getPublishedById(BigInteger id); + + /** + * 把当前资源映射为已发布视图。 + * + * @param workflow 当前资源 + * @return 已发布视图 + */ + Workflow toPublishedView(Workflow workflow); } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/BotServiceImpl.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/BotServiceImpl.java index cc3bde5..12e8ea5 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/BotServiceImpl.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/BotServiceImpl.java @@ -1,5 +1,6 @@ package tech.easyflow.ai.service.impl; +import com.alibaba.fastjson2.JSON; import cn.dev33.satoken.stp.StpUtil; import com.easyagents.core.file2text.File2TextService; import com.easyagents.core.file2text.source.HttpDocumentSource; @@ -27,7 +28,10 @@ import tech.easyflow.ai.easyagents.listener.ChatStreamListener; import tech.easyflow.ai.easyagents.memory.DefaultBotMessageMemory; import tech.easyflow.ai.easyagents.memory.PublicBotMessageMemory; import tech.easyflow.ai.easyagents.memory.RuntimeChatMemory; +import tech.easyflow.ai.easyagents.tool.WorkflowTool; +import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds; import tech.easyflow.ai.entity.*; +import tech.easyflow.ai.enums.PublishStatus; import tech.easyflow.ai.mapper.BotMapper; import tech.easyflow.ai.service.*; import tech.easyflow.ai.utils.CustomBeanUtils; @@ -75,6 +79,7 @@ public class BotServiceImpl extends ServiceImpl implements BotSe private Map modelOptions; private ChatModel chatModel; private String conversationIdStr; + private boolean publishedAccess; public Bot getAiBot() {return aiBot;} @@ -91,6 +96,10 @@ public class BotServiceImpl extends ServiceImpl implements BotSe public String getConversationIdStr() {return conversationIdStr;} public void setConversationIdStr(String conversationIdStr) {this.conversationIdStr = conversationIdStr;} + + public boolean isPublishedAccess() {return publishedAccess;} + + public void setPublishedAccess(boolean publishedAccess) {this.publishedAccess = publishedAccess;} } @Resource(name = "sseThreadPool") @@ -100,8 +109,12 @@ public class BotServiceImpl extends ServiceImpl implements BotSe @Resource private BotWorkflowService botWorkflowService; @Resource + private WorkflowService workflowService; + @Resource private BotDocumentCollectionService botDocumentCollectionService; @Resource + private DocumentCollectionService documentCollectionService; + @Resource private BotPluginService botPluginService; @Resource private PluginItemService pluginItemService; @@ -141,6 +154,57 @@ public class BotServiceImpl extends ServiceImpl implements BotSe return getOne(queryWrapper); } + /** + * {@inheritDoc} + */ + @Override + public Bot getPublishedDetail(String id) { + return toPublishedView(getDetail(id)); + } + + /** + * {@inheritDoc} + */ + @Override + public Bot getPublishedById(BigInteger id) { + return toPublishedView(getById(id)); + } + + /** + * {@inheritDoc} + */ + @Override + public Bot toPublishedView(Bot bot) { + if (bot == null) { + return null; + } + Map snapshot = bot.getPublishedSnapshotJson(); + if (snapshot == null || snapshot.isEmpty()) { + return bot; + } + Bot published = JSON.parseObject(JSON.toJSONString(snapshot), Bot.class); + if (published == null) { + return bot; + } + published.setId(bot.getId()); + published.setTenantId(bot.getTenantId()); + published.setCategoryId(bot.getCategoryId()); + published.setDeptId(bot.getDeptId()); + published.setCreated(bot.getCreated()); + published.setCreatedBy(bot.getCreatedBy()); + published.setModified(bot.getModified()); + published.setModifiedBy(bot.getModifiedBy()); + published.setPublishStatus(bot.getPublishStatus()); + published.setCurrentApprovalInstanceId(bot.getCurrentApprovalInstanceId()); + published.setPublishedSnapshotJson(bot.getPublishedSnapshotJson()); + published.setPublishedAt(bot.getPublishedAt()); + published.setPublishedBy(bot.getPublishedBy()); + if (published.getPublishStatus() == null) { + published.setPublishStatus(PublishStatus.DRAFT.getCode()); + } + return published; + } + public SseEmitter checkChatBeforeStart(BigInteger botId, String prompt, String conversationId, ChatCheckResult chatCheckResult) { if (!StringUtils.hasLength(prompt)) { return ChatSseUtil.sendSystemError(conversationId, "提示词不能为空"); @@ -175,6 +239,16 @@ public class BotServiceImpl extends ServiceImpl implements BotSe if ((!login || anonymousAccount) && !aiBot.isAnonymousEnabled()) { return ChatSseUtil.sendSystemError(conversationId, "此聊天助手不支持匿名访问"); } + if (!login || anonymousAccount) { + Bot publishedBot = toPublishedView(aiBot); + if (!PublishStatus.from(aiBot.getPublishStatus()).isExternallyVisible()) { + return ChatSseUtil.sendSystemError(conversationId, "聊天助手尚未发布"); + } + aiBot = publishedBot; + chatCheckResult.setPublishedAccess(true); + } else { + chatCheckResult.setPublishedAccess(false); + } Map modelOptions = aiBot.getModelOptions(); Model model = modelService.getModelInstance(aiBot.getModelId()); if (model == null) { @@ -213,7 +287,10 @@ public class BotServiceImpl extends ServiceImpl implements BotSe prompt = "【用户问题】:\n" + prompt + "\n\n请基于用户上传的附件内容回答用户问题: \n" + "【用户上传的附件内容】:\n" + attachmentsToString ; } UserMessage userMessage = new UserMessage(prompt); - userMessage.addTools(buildFunctionList(Maps.of("botId", botId).set("needEnglishName", false))); + userMessage.addTools(buildFunctionList(Maps.of("botId", botId) + .set("needEnglishName", false) + .set("bot", chatCheckResult.getAiBot()) + .set("publishedOnly", chatCheckResult.isPublishedAccess()))); ChatOptions chatOptions = getChatOptions(modelOptions); Boolean enableDeepThinking = MapUtil.getBoolean(modelOptions, Bot.KEY_ENABLE_DEEP_THINKING, false); chatOptions.setThinkingEnabled(enableDeepThinking); @@ -270,6 +347,8 @@ public class BotServiceImpl extends ServiceImpl implements BotSe userMessage.addTools(buildFunctionList(Maps.of("botId", botId) .set("needEnglishName", false) .set("needAccountId", false) + .set("bot", chatCheckResult.getAiBot()) + .set("publishedOnly", chatCheckResult.isPublishedAccess()) )); ChatSseEmitter chatSseEmitter = new ChatSseEmitter(); SseEmitter emitter = chatSseEmitter.getEmitter(); @@ -380,30 +459,39 @@ public class BotServiceImpl extends ServiceImpl implements BotSe if (needEnglishName == null) { needEnglishName = false; } + Bot runtimeBot = (Bot) buildParams.get("bot"); + boolean usePublishedSnapshot = Boolean.TRUE.equals(buildParams.get("publishedOnly")) + && runtimeBot != null + && runtimeBot.getPublishedSnapshotJson() != null + && PublishStatus.from(runtimeBot.getPublishStatus()).isExternallyVisible(); QueryWrapper queryWrapper = QueryWrapper.create(); - - // 工作流 function 集合 - queryWrapper.eq(BotWorkflow::getBotId, botId); - List botWorkflows = botWorkflowService.getMapper() - .selectListWithRelationsByQuery(queryWrapper); - if (botWorkflows != null && !botWorkflows.isEmpty()) { - for (BotWorkflow botWorkflow : botWorkflows) { - Tool function = botWorkflow.getWorkflow().toFunction(needEnglishName); - functionList.add(function); + if (usePublishedSnapshot) { + appendPublishedWorkflowTools(functionList, runtimeBot, needEnglishName); + appendPublishedKnowledgeTools(functionList, runtimeBot, needEnglishName); + } else { + // 工作流 function 集合 + queryWrapper.eq(BotWorkflow::getBotId, botId); + List botWorkflows = botWorkflowService.getMapper() + .selectListWithRelationsByQuery(queryWrapper); + if (botWorkflows != null && !botWorkflows.isEmpty()) { + for (BotWorkflow botWorkflow : botWorkflows) { + Tool function = botWorkflow.getWorkflow().toFunction(needEnglishName); + functionList.add(function); + } } - } - // 知识库 function 集合 - queryWrapper = QueryWrapper.create(); - queryWrapper.eq(BotDocumentCollection::getBotId, botId); - List botDocumentCollections = botDocumentCollectionService.getMapper() - .selectListWithRelationsByQuery(queryWrapper); - if (botDocumentCollections != null && !botDocumentCollections.isEmpty()) { - for (BotDocumentCollection botDocumentCollection : botDocumentCollections) { - Tool function = botDocumentCollection.getKnowledge() - .toFunction(needEnglishName, botDocumentCollection.getRetrievalMode().name()); - functionList.add(function); + // 知识库 function 集合 + queryWrapper = QueryWrapper.create(); + queryWrapper.eq(BotDocumentCollection::getBotId, botId); + List botDocumentCollections = botDocumentCollectionService.getMapper() + .selectListWithRelationsByQuery(queryWrapper); + if (botDocumentCollections != null && !botDocumentCollections.isEmpty()) { + for (BotDocumentCollection botDocumentCollection : botDocumentCollections) { + Tool function = botDocumentCollection.getKnowledge() + .toFunction(needEnglishName, botDocumentCollection.getRetrievalMode().name()); + functionList.add(function); + } } } @@ -437,6 +525,59 @@ public class BotServiceImpl extends ServiceImpl implements BotSe return functionList; } + @SuppressWarnings("unchecked") + private void appendPublishedWorkflowTools(List functionList, Bot runtimeBot, boolean needEnglishName) { + Object workflows = runtimeBot.getPublishedSnapshotJson().get("workflowBindings"); + if (!(workflows instanceof List workflowBindings)) { + return; + } + for (Object item : workflowBindings) { + if (!(item instanceof Map workflowMap)) { + continue; + } + Object workflowId = workflowMap.get("workflowId"); + if (workflowId == null) { + continue; + } + Workflow workflow = workflowService.getPublishedById(new BigInteger(String.valueOf(workflowId))); + if (workflow == null) { + continue; + } + WorkflowTool tool = new WorkflowTool( + workflow, + needEnglishName, + PublishedWorkflowDefinitionIds.published(String.valueOf(workflow.getId())) + ); + functionList.add(tool); + } + } + + @SuppressWarnings("unchecked") + private void appendPublishedKnowledgeTools(List functionList, Bot runtimeBot, boolean needEnglishName) { + Object knowledges = runtimeBot.getPublishedSnapshotJson().get("knowledgeBindings"); + if (!(knowledges instanceof List knowledgeBindings)) { + return; + } + for (Object item : knowledgeBindings) { + if (!(item instanceof Map bindingMap)) { + continue; + } + Object knowledgeId = bindingMap.get("knowledgeId"); + if (knowledgeId == null) { + continue; + } + DocumentCollection knowledge = documentCollectionService.getPublishedById(new BigInteger(String.valueOf(knowledgeId))); + if (knowledge == null) { + continue; + } + Object retrievalMode = bindingMap.get("retrievalMode"); + functionList.add(knowledge.toFunction( + needEnglishName, + retrievalMode == null ? null : String.valueOf(retrievalMode) + )); + } + } + public String attachmentsToString(List fileList) { StringBuilder messageBuilder = new StringBuilder(); if (fileList != null && !fileList.isEmpty()) { diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/DocumentCollectionServiceImpl.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/DocumentCollectionServiceImpl.java index 4a118b1..ae16454 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/DocumentCollectionServiceImpl.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/DocumentCollectionServiceImpl.java @@ -1,5 +1,6 @@ package tech.easyflow.ai.service.impl; +import com.alibaba.fastjson2.JSON; import com.easyagents.core.document.Document; import com.easyagents.core.model.rerank.RerankException; import com.easyagents.core.model.rerank.RerankModel; @@ -29,6 +30,7 @@ import tech.easyflow.ai.entity.DocumentChunk; import tech.easyflow.ai.entity.DocumentCollection; import tech.easyflow.ai.entity.FaqItem; import tech.easyflow.ai.entity.Model; +import tech.easyflow.ai.enums.PublishStatus; import tech.easyflow.ai.mapper.DocumentChunkMapper; import tech.easyflow.ai.mapper.DocumentCollectionMapper; import tech.easyflow.ai.mapper.FaqItemMapper; @@ -148,6 +150,57 @@ public class DocumentCollectionServiceImpl extends ServiceImpl snapshot = collection.getPublishedSnapshotJson(); + if (snapshot == null || snapshot.isEmpty()) { + return collection; + } + DocumentCollection published = JSON.parseObject(JSON.toJSONString(snapshot), DocumentCollection.class); + if (published == null) { + return collection; + } + published.setId(collection.getId()); + published.setTenantId(collection.getTenantId()); + published.setCategoryId(collection.getCategoryId()); + published.setDeptId(collection.getDeptId()); + published.setCreated(collection.getCreated()); + published.setCreatedBy(collection.getCreatedBy()); + published.setModified(collection.getModified()); + published.setModifiedBy(collection.getModifiedBy()); + published.setPublishStatus(collection.getPublishStatus()); + published.setCurrentApprovalInstanceId(collection.getCurrentApprovalInstanceId()); + published.setPublishedSnapshotJson(collection.getPublishedSnapshotJson()); + published.setPublishedAt(collection.getPublishedAt()); + published.setPublishedBy(collection.getPublishedBy()); + if (published.getPublishStatus() == null) { + published.setPublishStatus(PublishStatus.DRAFT.getCode()); + } + return published; + } + private VectorRetriever buildVectorRetriever(DocumentCollection documentCollection, int docRecallMaxNum, Float minSimilarity) { diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/WorkflowServiceImpl.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/WorkflowServiceImpl.java index 284c3c1..751e050 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/WorkflowServiceImpl.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/WorkflowServiceImpl.java @@ -1,7 +1,8 @@ package tech.easyflow.ai.service.impl; - +import com.alibaba.fastjson2.JSON; import tech.easyflow.ai.entity.Workflow; +import tech.easyflow.ai.enums.PublishStatus; import tech.easyflow.ai.mapper.WorkflowMapper; import tech.easyflow.ai.service.WorkflowService; import com.mybatisflex.spring.service.impl.ServiceImpl; @@ -11,6 +12,9 @@ import com.mybatisflex.core.query.QueryWrapper; import tech.easyflow.common.web.exceptions.BusinessException; import tech.easyflow.ai.utils.CustomBeanUtils; +import java.math.BigInteger; +import java.util.Map; + /** * 服务层实现。 * @@ -52,6 +56,57 @@ public class WorkflowServiceImpl extends ServiceImpl i } + /** + * {@inheritDoc} + */ + @Override + public Workflow getPublishedDetail(String idOrAlias) { + return toPublishedView(getDetail(idOrAlias)); + } + + /** + * {@inheritDoc} + */ + @Override + public Workflow getPublishedById(BigInteger id) { + return toPublishedView(getById(id)); + } + + /** + * {@inheritDoc} + */ + @Override + public Workflow toPublishedView(Workflow workflow) { + if (workflow == null) { + return null; + } + Map snapshot = workflow.getPublishedSnapshotJson(); + if (snapshot == null || snapshot.isEmpty()) { + return workflow; + } + Workflow published = JSON.parseObject(JSON.toJSONString(snapshot), Workflow.class); + if (published == null) { + return workflow; + } + published.setId(workflow.getId()); + published.setTenantId(workflow.getTenantId()); + published.setCategoryId(workflow.getCategoryId()); + published.setDeptId(workflow.getDeptId()); + published.setCreated(workflow.getCreated()); + published.setCreatedBy(workflow.getCreatedBy()); + published.setModified(workflow.getModified()); + published.setModifiedBy(workflow.getModifiedBy()); + published.setPublishStatus(workflow.getPublishStatus()); + published.setCurrentApprovalInstanceId(workflow.getCurrentApprovalInstanceId()); + published.setPublishedSnapshotJson(workflow.getPublishedSnapshotJson()); + published.setPublishedAt(workflow.getPublishedAt()); + published.setPublishedBy(workflow.getPublishedBy()); + if (published.getPublishStatus() == null) { + published.setPublishStatus(PublishStatus.DRAFT.getCode()); + } + return published; + } + @Override public boolean updateById(Workflow entity, boolean ignoreNulls) { diff --git a/easyflow-modules/easyflow-module-approval/pom.xml b/easyflow-modules/easyflow-module-approval/pom.xml new file mode 100644 index 0000000..6a66b3f --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + tech.easyflow + easyflow-modules + ${revision} + + + easyflow-module-approval + easyflow-module-approval + + + + com.mybatis-flex + mybatis-flex-spring-boot3-starter + + + tech.easyflow + easyflow-common-base + + + tech.easyflow + easyflow-common-satoken + + + tech.easyflow + easyflow-common-web + + + tech.easyflow + easyflow-module-system + + + diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/annotation/ApprovalAction.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/annotation/ApprovalAction.java new file mode 100644 index 0000000..b9f41f1 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/annotation/ApprovalAction.java @@ -0,0 +1,35 @@ +package tech.easyflow.approval.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 提交审批动作注解。 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ApprovalAction { + + /** + * 资源类型。 + * + * @return 资源类型编码 + */ + String resourceType(); + + /** + * 动作类型。 + * + * @return 动作类型编码 + */ + String actionType(); + + /** + * 资源 ID 的 SpEL 表达式。 + * + * @return SpEL 表达式 + */ + String idExpr(); +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/annotation/RequirePublishedAccess.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/annotation/RequirePublishedAccess.java new file mode 100644 index 0000000..ced8a41 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/annotation/RequirePublishedAccess.java @@ -0,0 +1,35 @@ +package tech.easyflow.approval.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 已发布访问门禁注解。 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface RequirePublishedAccess { + + /** + * 资源类型。 + * + * @return 资源类型编码 + */ + String resourceType(); + + /** + * 资源标识的 SpEL 表达式。 + * + * @return 标识表达式 + */ + String idExpr(); + + /** + * 拒绝访问时的提示文案。 + * + * @return 错误提示 + */ + String denyMessage() default "资源尚未发布"; +} 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 new file mode 100644 index 0000000..8ab3e4c --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/aspect/ApprovalActionAspect.java @@ -0,0 +1,66 @@ +package tech.easyflow.approval.aspect; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.context.expression.MethodBasedEvaluationContext; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.stereotype.Component; +import tech.easyflow.approval.annotation.ApprovalAction; +import tech.easyflow.approval.service.ApprovalActionFacade; +import tech.easyflow.common.satoken.util.SaTokenUtil; + +import java.lang.reflect.Method; +import java.math.BigInteger; + +/** + * 提审动作切面。 + */ +@Aspect +@Component +public class ApprovalActionAspect { + + private final ApprovalActionFacade approvalActionFacade; + private final ExpressionParser expressionParser = new SpelExpressionParser(); + private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); + + public ApprovalActionAspect(ApprovalActionFacade approvalActionFacade) { + this.approvalActionFacade = approvalActionFacade; + } + + /** + * 拦截提审方法,先执行资源校验,再统一发起审批。 + * + * @param joinPoint 切点 + * @param approvalAction 注解 + * @return 审批实例 ID + * @throws Throwable 执行异常 + */ + @Around("@annotation(approvalAction)") + public Object doAround(ProceedingJoinPoint joinPoint, ApprovalAction approvalAction) throws Throwable { + Object identifier = resolveIdentifier(joinPoint, approvalAction.idExpr()); + BigInteger resourceId = identifier == null ? null : new BigInteger(String.valueOf(identifier)); + joinPoint.proceed(); + return approvalActionFacade.submit( + approvalAction.resourceType(), + resourceId, + approvalAction.actionType(), + SaTokenUtil.getLoginAccount().getId() + ); + } + + private Object resolveIdentifier(ProceedingJoinPoint joinPoint, String idExpr) { + Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); + MethodBasedEvaluationContext context = new MethodBasedEvaluationContext( + joinPoint.getTarget(), + method, + joinPoint.getArgs(), + parameterNameDiscoverer + ); + return expressionParser.parseExpression(idExpr).getValue(context); + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/aspect/RequirePublishedAccessAspect.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/aspect/RequirePublishedAccessAspect.java new file mode 100644 index 0000000..2d5ddcb --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/aspect/RequirePublishedAccessAspect.java @@ -0,0 +1,63 @@ +package tech.easyflow.approval.aspect; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.context.expression.MethodBasedEvaluationContext; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.stereotype.Component; +import tech.easyflow.approval.annotation.RequirePublishedAccess; +import tech.easyflow.approval.service.ApprovalActionFacade; + +import java.lang.reflect.Method; + +/** + * 已发布访问门禁切面。 + */ +@Aspect +@Component +public class RequirePublishedAccessAspect { + + private final ApprovalActionFacade approvalActionFacade; + private final ExpressionParser expressionParser = new SpelExpressionParser(); + private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); + + public RequirePublishedAccessAspect(ApprovalActionFacade approvalActionFacade) { + this.approvalActionFacade = approvalActionFacade; + } + + /** + * 执行发布态门禁校验。 + * + * @param joinPoint 切点 + * @param requirePublishedAccess 注解 + * @return 原方法返回值 + * @throws Throwable 执行异常 + */ + @Around("@annotation(requirePublishedAccess)") + public Object doAround(ProceedingJoinPoint joinPoint, + RequirePublishedAccess requirePublishedAccess) throws Throwable { + Object identifier = resolveIdentifier(joinPoint, requirePublishedAccess.idExpr()); + approvalActionFacade.assertPublishedAccess( + requirePublishedAccess.resourceType(), + identifier, + requirePublishedAccess.denyMessage() + ); + return joinPoint.proceed(); + } + + private Object resolveIdentifier(ProceedingJoinPoint joinPoint, String idExpr) { + Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); + MethodBasedEvaluationContext context = new MethodBasedEvaluationContext( + joinPoint.getTarget(), + method, + joinPoint.getArgs(), + parameterNameDiscoverer + ); + return expressionParser.parseExpression(idExpr).getValue(context); + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/config/ApprovalModuleConfig.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/config/ApprovalModuleConfig.java new file mode 100644 index 0000000..c32eb84 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/config/ApprovalModuleConfig.java @@ -0,0 +1,12 @@ +package tech.easyflow.approval.config; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.autoconfigure.AutoConfiguration; + +/** + * 审批模块配置。 + */ +@MapperScan("tech.easyflow.approval.mapper") +@AutoConfiguration +public class ApprovalModuleConfig { +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/ApprovalFlow.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/ApprovalFlow.java new file mode 100644 index 0000000..d244ca6 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/ApprovalFlow.java @@ -0,0 +1,11 @@ +package tech.easyflow.approval.entity; + +import com.mybatisflex.annotation.Table; +import tech.easyflow.approval.entity.base.ApprovalFlowBase; + +/** + * 审批流程实体。 + */ +@Table("tb_approval_flow") +public class ApprovalFlow extends ApprovalFlowBase { +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/ApprovalFlowScope.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/ApprovalFlowScope.java new file mode 100644 index 0000000..95263c1 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/ApprovalFlowScope.java @@ -0,0 +1,11 @@ +package tech.easyflow.approval.entity; + +import com.mybatisflex.annotation.Table; +import tech.easyflow.approval.entity.base.ApprovalFlowScopeBase; + +/** + * 审批流程范围实体。 + */ +@Table("tb_approval_flow_scope") +public class ApprovalFlowScope extends ApprovalFlowScopeBase { +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/ApprovalFlowStep.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/ApprovalFlowStep.java new file mode 100644 index 0000000..6a67fb7 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/ApprovalFlowStep.java @@ -0,0 +1,11 @@ +package tech.easyflow.approval.entity; + +import com.mybatisflex.annotation.Table; +import tech.easyflow.approval.entity.base.ApprovalFlowStepBase; + +/** + * 审批流程步骤实体。 + */ +@Table("tb_approval_flow_step") +public class ApprovalFlowStep extends ApprovalFlowStepBase { +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/ApprovalInstance.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/ApprovalInstance.java new file mode 100644 index 0000000..440216e --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/ApprovalInstance.java @@ -0,0 +1,11 @@ +package tech.easyflow.approval.entity; + +import com.mybatisflex.annotation.Table; +import tech.easyflow.approval.entity.base.ApprovalInstanceBase; + +/** + * 审批实例实体。 + */ +@Table("tb_approval_instance") +public class ApprovalInstance extends ApprovalInstanceBase { +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/ApprovalLog.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/ApprovalLog.java new file mode 100644 index 0000000..e558ee8 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/ApprovalLog.java @@ -0,0 +1,11 @@ +package tech.easyflow.approval.entity; + +import com.mybatisflex.annotation.Table; +import tech.easyflow.approval.entity.base.ApprovalLogBase; + +/** + * 审批日志实体。 + */ +@Table("tb_approval_log") +public class ApprovalLog extends ApprovalLogBase { +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/ApprovalTask.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/ApprovalTask.java new file mode 100644 index 0000000..b388e92 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/ApprovalTask.java @@ -0,0 +1,11 @@ +package tech.easyflow.approval.entity; + +import com.mybatisflex.annotation.Table; +import tech.easyflow.approval.entity.base.ApprovalTaskBase; + +/** + * 审批任务实体。 + */ +@Table("tb_approval_task") +public class ApprovalTask extends ApprovalTaskBase { +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/base/ApprovalFlowBase.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/base/ApprovalFlowBase.java new file mode 100644 index 0000000..9579b51 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/base/ApprovalFlowBase.java @@ -0,0 +1,149 @@ +package tech.easyflow.approval.entity.base; + +import com.mybatisflex.annotation.Column; +import com.mybatisflex.annotation.Id; +import com.mybatisflex.annotation.KeyType; + +import java.io.Serializable; +import java.math.BigInteger; +import java.util.Date; + +/** + * 审批流程主表基础字段。 + */ +public class ApprovalFlowBase implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id(keyType = KeyType.Generator, value = "snowFlakeId", comment = "主键") + private BigInteger id; + + @Column(comment = "流程名称") + private String name; + + @Column(comment = "资源类型") + private String resourceType; + + @Column(comment = "动作类型") + private String actionType; + + @Column(comment = "优先级") + private Integer priority; + + @Column(comment = "流程状态") + private String status; + + @Column(comment = "流程版本") + private Integer version; + + @Column(comment = "备注") + private String remark; + + @Column(comment = "创建时间") + private Date created; + + @Column(comment = "创建者") + private BigInteger createdBy; + + @Column(comment = "修改时间") + private Date modified; + + @Column(comment = "修改者") + private BigInteger modifiedBy; + + public BigInteger getId() { + return id; + } + + public void setId(BigInteger id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getResourceType() { + return resourceType; + } + + public void setResourceType(String resourceType) { + this.resourceType = resourceType; + } + + public String getActionType() { + return actionType; + } + + public void setActionType(String actionType) { + this.actionType = actionType; + } + + public Integer getPriority() { + return priority; + } + + public void setPriority(Integer priority) { + this.priority = priority; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public Integer getVersion() { + return version; + } + + public void setVersion(Integer version) { + this.version = version; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public BigInteger getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(BigInteger createdBy) { + this.createdBy = createdBy; + } + + public Date getModified() { + return modified; + } + + public void setModified(Date modified) { + this.modified = modified; + } + + public BigInteger getModifiedBy() { + return modifiedBy; + } + + public void setModifiedBy(BigInteger modifiedBy) { + this.modifiedBy = modifiedBy; + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/base/ApprovalFlowScopeBase.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/base/ApprovalFlowScopeBase.java new file mode 100644 index 0000000..b84d1b9 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/base/ApprovalFlowScopeBase.java @@ -0,0 +1,116 @@ +package tech.easyflow.approval.entity.base; + +import com.mybatisflex.annotation.Column; +import com.mybatisflex.annotation.Id; +import com.mybatisflex.annotation.KeyType; + +import java.io.Serializable; +import java.math.BigInteger; +import java.util.Date; + +/** + * 审批流程范围基础字段。 + */ +public class ApprovalFlowScopeBase implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id(keyType = KeyType.Generator, value = "snowFlakeId", comment = "主键") + private BigInteger id; + + @Column(comment = "流程ID") + private BigInteger flowId; + + @Column(comment = "范围类型") + private String scopeType; + + @Column(comment = "范围值") + private BigInteger scopeValue; + + @Column(comment = "是否包含子节点") + private Integer includeChildren; + + @Column(comment = "创建时间") + private Date created; + + @Column(comment = "创建者") + private BigInteger createdBy; + + @Column(comment = "修改时间") + private Date modified; + + @Column(comment = "修改者") + private BigInteger modifiedBy; + + public BigInteger getId() { + return id; + } + + public void setId(BigInteger id) { + this.id = id; + } + + public BigInteger getFlowId() { + return flowId; + } + + public void setFlowId(BigInteger flowId) { + this.flowId = flowId; + } + + public String getScopeType() { + return scopeType; + } + + public void setScopeType(String scopeType) { + this.scopeType = scopeType; + } + + public BigInteger getScopeValue() { + return scopeValue; + } + + public void setScopeValue(BigInteger scopeValue) { + this.scopeValue = scopeValue; + } + + public Integer getIncludeChildren() { + return includeChildren; + } + + public void setIncludeChildren(Integer includeChildren) { + this.includeChildren = includeChildren; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public BigInteger getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(BigInteger createdBy) { + this.createdBy = createdBy; + } + + public Date getModified() { + return modified; + } + + public void setModified(Date modified) { + this.modified = modified; + } + + public BigInteger getModifiedBy() { + return modifiedBy; + } + + public void setModifiedBy(BigInteger modifiedBy) { + this.modifiedBy = modifiedBy; + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/base/ApprovalFlowStepBase.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/base/ApprovalFlowStepBase.java new file mode 100644 index 0000000..21dc6df --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/base/ApprovalFlowStepBase.java @@ -0,0 +1,149 @@ +package tech.easyflow.approval.entity.base; + +import com.mybatisflex.annotation.Column; +import com.mybatisflex.annotation.Id; +import com.mybatisflex.annotation.KeyType; + +import java.io.Serializable; +import java.math.BigInteger; +import java.util.Date; + +/** + * 审批流程步骤基础字段。 + */ +public class ApprovalFlowStepBase implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id(keyType = KeyType.Generator, value = "snowFlakeId", comment = "主键") + private BigInteger id; + + @Column(comment = "流程ID") + private BigInteger flowId; + + @Column(comment = "步骤顺序") + private Integer stepNo; + + @Column(comment = "步骤名称") + private String stepName; + + @Column(comment = "审批对象类型") + private String assigneeType; + + @Column(comment = "审批对象ID") + private BigInteger assigneeTargetId; + + @Column(comment = "审批对象编码") + private String assigneeTargetCode; + + @Column(comment = "审批对象名称") + private String assigneeTargetName; + + @Column(comment = "创建时间") + private Date created; + + @Column(comment = "创建者") + private BigInteger createdBy; + + @Column(comment = "修改时间") + private Date modified; + + @Column(comment = "修改者") + private BigInteger modifiedBy; + + public BigInteger getId() { + return id; + } + + public void setId(BigInteger id) { + this.id = id; + } + + public BigInteger getFlowId() { + return flowId; + } + + public void setFlowId(BigInteger flowId) { + this.flowId = flowId; + } + + public Integer getStepNo() { + return stepNo; + } + + public void setStepNo(Integer stepNo) { + this.stepNo = stepNo; + } + + public String getStepName() { + return stepName; + } + + public void setStepName(String stepName) { + this.stepName = stepName; + } + + public String getAssigneeType() { + return assigneeType; + } + + public void setAssigneeType(String assigneeType) { + this.assigneeType = assigneeType; + } + + public BigInteger getAssigneeTargetId() { + return assigneeTargetId; + } + + public void setAssigneeTargetId(BigInteger assigneeTargetId) { + this.assigneeTargetId = assigneeTargetId; + } + + public String getAssigneeTargetCode() { + return assigneeTargetCode; + } + + public void setAssigneeTargetCode(String assigneeTargetCode) { + this.assigneeTargetCode = assigneeTargetCode; + } + + public String getAssigneeTargetName() { + return assigneeTargetName; + } + + public void setAssigneeTargetName(String assigneeTargetName) { + this.assigneeTargetName = assigneeTargetName; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public BigInteger getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(BigInteger createdBy) { + this.createdBy = createdBy; + } + + public Date getModified() { + return modified; + } + + public void setModified(Date modified) { + this.modified = modified; + } + + public BigInteger getModifiedBy() { + return modifiedBy; + } + + public void setModifiedBy(BigInteger modifiedBy) { + this.modifiedBy = modifiedBy; + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/base/ApprovalInstanceBase.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/base/ApprovalInstanceBase.java new file mode 100644 index 0000000..f6e859c --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/base/ApprovalInstanceBase.java @@ -0,0 +1,206 @@ +package tech.easyflow.approval.entity.base; + +import com.mybatisflex.annotation.Column; +import com.mybatisflex.annotation.Id; +import com.mybatisflex.annotation.KeyType; +import com.mybatisflex.core.handler.FastjsonTypeHandler; + +import java.io.Serializable; +import java.math.BigInteger; +import java.util.Date; +import java.util.Map; + +/** + * 审批实例基础字段。 + */ +public class ApprovalInstanceBase implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id(keyType = KeyType.Generator, value = "snowFlakeId", comment = "主键") + private BigInteger id; + + @Column(comment = "流程ID") + private BigInteger flowId; + + @Column(comment = "流程版本") + private Integer flowVersion; + + @Column(comment = "资源类型") + private String resourceType; + + @Column(comment = "资源ID") + private BigInteger resourceId; + + @Column(comment = "动作类型") + private String actionType; + + @Column(comment = "实例状态") + private String status; + + @Column(comment = "当前步骤序号") + private Integer currentStepNo; + + @Column(typeHandler = FastjsonTypeHandler.class, comment = "审批快照") + private Map snapshotJson; + + @Column(comment = "审批摘要") + private String summary; + + @Column(comment = "申请人ID") + private BigInteger applicantId; + + @Column(comment = "提交时间") + private Date submittedAt; + + @Column(comment = "完成时间") + private Date finishedAt; + + @Column(comment = "创建时间") + private Date created; + + @Column(comment = "创建者") + private BigInteger createdBy; + + @Column(comment = "修改时间") + private Date modified; + + @Column(comment = "修改者") + private BigInteger modifiedBy; + + public BigInteger getId() { + return id; + } + + public void setId(BigInteger id) { + this.id = id; + } + + public BigInteger getFlowId() { + return flowId; + } + + public void setFlowId(BigInteger flowId) { + this.flowId = flowId; + } + + public Integer getFlowVersion() { + return flowVersion; + } + + public void setFlowVersion(Integer flowVersion) { + this.flowVersion = flowVersion; + } + + public String getResourceType() { + return resourceType; + } + + public void setResourceType(String resourceType) { + this.resourceType = resourceType; + } + + public BigInteger getResourceId() { + return resourceId; + } + + public void setResourceId(BigInteger resourceId) { + this.resourceId = resourceId; + } + + public String getActionType() { + return actionType; + } + + public void setActionType(String actionType) { + this.actionType = actionType; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public Integer getCurrentStepNo() { + return currentStepNo; + } + + public void setCurrentStepNo(Integer currentStepNo) { + this.currentStepNo = currentStepNo; + } + + public Map getSnapshotJson() { + return snapshotJson; + } + + public void setSnapshotJson(Map snapshotJson) { + this.snapshotJson = snapshotJson; + } + + public String getSummary() { + return summary; + } + + public void setSummary(String summary) { + this.summary = summary; + } + + public BigInteger getApplicantId() { + return applicantId; + } + + public void setApplicantId(BigInteger applicantId) { + this.applicantId = applicantId; + } + + public Date getSubmittedAt() { + return submittedAt; + } + + public void setSubmittedAt(Date submittedAt) { + this.submittedAt = submittedAt; + } + + public Date getFinishedAt() { + return finishedAt; + } + + public void setFinishedAt(Date finishedAt) { + this.finishedAt = finishedAt; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public BigInteger getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(BigInteger createdBy) { + this.createdBy = createdBy; + } + + public Date getModified() { + return modified; + } + + public void setModified(Date modified) { + this.modified = modified; + } + + public BigInteger getModifiedBy() { + return modifiedBy; + } + + public void setModifiedBy(BigInteger modifiedBy) { + this.modifiedBy = modifiedBy; + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/base/ApprovalLogBase.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/base/ApprovalLogBase.java new file mode 100644 index 0000000..5614ad4 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/base/ApprovalLogBase.java @@ -0,0 +1,118 @@ +package tech.easyflow.approval.entity.base; + +import com.mybatisflex.annotation.Column; +import com.mybatisflex.annotation.Id; +import com.mybatisflex.annotation.KeyType; +import com.mybatisflex.core.handler.FastjsonTypeHandler; + +import java.io.Serializable; +import java.math.BigInteger; +import java.util.Date; +import java.util.Map; + +/** + * 审批日志基础字段。 + */ +public class ApprovalLogBase implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id(keyType = KeyType.Generator, value = "snowFlakeId", comment = "主键") + private BigInteger id; + + @Column(comment = "审批实例ID") + private BigInteger instanceId; + + @Column(comment = "事件类型") + private String eventType; + + @Column(comment = "操作人ID") + private BigInteger operatorId; + + @Column(typeHandler = FastjsonTypeHandler.class, comment = "事件载荷") + private Map payloadJson; + + @Column(comment = "创建时间") + private Date created; + + @Column(comment = "创建者") + private BigInteger createdBy; + + @Column(comment = "修改时间") + private Date modified; + + @Column(comment = "修改者") + private BigInteger modifiedBy; + + public BigInteger getId() { + return id; + } + + public void setId(BigInteger id) { + this.id = id; + } + + public BigInteger getInstanceId() { + return instanceId; + } + + public void setInstanceId(BigInteger instanceId) { + this.instanceId = instanceId; + } + + public String getEventType() { + return eventType; + } + + public void setEventType(String eventType) { + this.eventType = eventType; + } + + public BigInteger getOperatorId() { + return operatorId; + } + + public void setOperatorId(BigInteger operatorId) { + this.operatorId = operatorId; + } + + public Map getPayloadJson() { + return payloadJson; + } + + public void setPayloadJson(Map payloadJson) { + this.payloadJson = payloadJson; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public BigInteger getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(BigInteger createdBy) { + this.createdBy = createdBy; + } + + public Date getModified() { + return modified; + } + + public void setModified(Date modified) { + this.modified = modified; + } + + public BigInteger getModifiedBy() { + return modifiedBy; + } + + public void setModifiedBy(BigInteger modifiedBy) { + this.modifiedBy = modifiedBy; + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/base/ApprovalTaskBase.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/base/ApprovalTaskBase.java new file mode 100644 index 0000000..39f80ac --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/base/ApprovalTaskBase.java @@ -0,0 +1,193 @@ +package tech.easyflow.approval.entity.base; + +import com.mybatisflex.annotation.Column; +import com.mybatisflex.annotation.Id; +import com.mybatisflex.annotation.KeyType; + +import java.io.Serializable; +import java.math.BigInteger; +import java.util.Date; + +/** + * 审批任务基础字段。 + */ +public class ApprovalTaskBase implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id(keyType = KeyType.Generator, value = "snowFlakeId", comment = "主键") + private BigInteger id; + + @Column(comment = "审批实例ID") + private BigInteger instanceId; + + @Column(comment = "步骤序号") + private Integer stepNo; + + @Column(comment = "任务状态") + private String status; + + @Column(comment = "指派角色编码") + private String assigneeRoleCode; + + @Column(comment = "审批对象类型") + private String assigneeType; + + @Column(comment = "审批对象ID") + private BigInteger assigneeTargetId; + + @Column(comment = "审批对象编码") + private String assigneeTargetCode; + + @Column(comment = "审批对象名称") + private String assigneeTargetName; + + @Column(comment = "处理人ID") + private BigInteger actedBy; + + @Column(comment = "处理时间") + private Date actedAt; + + @Column(comment = "处理意见") + private String comment; + + @Column(comment = "创建时间") + private Date created; + + @Column(comment = "创建者") + private BigInteger createdBy; + + @Column(comment = "修改时间") + private Date modified; + + @Column(comment = "修改者") + private BigInteger modifiedBy; + + public BigInteger getId() { + return id; + } + + public void setId(BigInteger id) { + this.id = id; + } + + public BigInteger getInstanceId() { + return instanceId; + } + + public void setInstanceId(BigInteger instanceId) { + this.instanceId = instanceId; + } + + public Integer getStepNo() { + return stepNo; + } + + public void setStepNo(Integer stepNo) { + this.stepNo = stepNo; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getAssigneeRoleCode() { + return assigneeRoleCode; + } + + public void setAssigneeRoleCode(String assigneeRoleCode) { + this.assigneeRoleCode = assigneeRoleCode; + } + + public String getAssigneeType() { + return assigneeType; + } + + public void setAssigneeType(String assigneeType) { + this.assigneeType = assigneeType; + } + + public BigInteger getAssigneeTargetId() { + return assigneeTargetId; + } + + public void setAssigneeTargetId(BigInteger assigneeTargetId) { + this.assigneeTargetId = assigneeTargetId; + } + + public String getAssigneeTargetCode() { + return assigneeTargetCode; + } + + public void setAssigneeTargetCode(String assigneeTargetCode) { + this.assigneeTargetCode = assigneeTargetCode; + } + + public String getAssigneeTargetName() { + return assigneeTargetName; + } + + public void setAssigneeTargetName(String assigneeTargetName) { + this.assigneeTargetName = assigneeTargetName; + } + + public BigInteger getActedBy() { + return actedBy; + } + + public void setActedBy(BigInteger actedBy) { + this.actedBy = actedBy; + } + + public Date getActedAt() { + return actedAt; + } + + public void setActedAt(Date actedAt) { + this.actedAt = actedAt; + } + + public String getComment() { + return comment; + } + + public void setComment(String comment) { + this.comment = comment; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public BigInteger getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(BigInteger createdBy) { + this.createdBy = createdBy; + } + + public Date getModified() { + return modified; + } + + public void setModified(Date modified) { + this.modified = modified; + } + + public BigInteger getModifiedBy() { + return modifiedBy; + } + + public void setModifiedBy(BigInteger modifiedBy) { + this.modifiedBy = modifiedBy; + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalActionRequest.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalActionRequest.java new file mode 100644 index 0000000..66896b9 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalActionRequest.java @@ -0,0 +1,29 @@ +package tech.easyflow.approval.entity.vo; + +import java.math.BigInteger; + +/** + * 审批动作请求。 + */ +public class ApprovalActionRequest { + + private BigInteger instanceId; + + private String comment; + + public BigInteger getInstanceId() { + return instanceId; + } + + public void setInstanceId(BigInteger instanceId) { + this.instanceId = instanceId; + } + + public String getComment() { + return comment; + } + + public void setComment(String comment) { + this.comment = comment; + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalAssigneeOptionVo.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalAssigneeOptionVo.java new file mode 100644 index 0000000..ad02c50 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalAssigneeOptionVo.java @@ -0,0 +1,39 @@ +package tech.easyflow.approval.entity.vo; + +import java.math.BigInteger; + +/** + * 审批对象选项。 + */ +public class ApprovalAssigneeOptionVo { + + private BigInteger id; + + private String code; + + private String name; + + public BigInteger getId() { + return id; + } + + public void setId(BigInteger id) { + this.id = id; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalCallbackContext.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalCallbackContext.java new file mode 100644 index 0000000..1073a23 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalCallbackContext.java @@ -0,0 +1,41 @@ +package tech.easyflow.approval.entity.vo; + +import tech.easyflow.approval.entity.ApprovalInstance; + +import java.math.BigInteger; + +/** + * 审批回调上下文。 + */ +public class ApprovalCallbackContext { + + private ApprovalInstance instance; + + private BigInteger operatorId; + + private String comment; + + public ApprovalInstance getInstance() { + return instance; + } + + public void setInstance(ApprovalInstance instance) { + this.instance = instance; + } + + public BigInteger getOperatorId() { + return operatorId; + } + + public void setOperatorId(BigInteger operatorId) { + this.operatorId = operatorId; + } + + public String getComment() { + return comment; + } + + public void setComment(String comment) { + this.comment = comment; + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalFlowDetailVo.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalFlowDetailVo.java new file mode 100644 index 0000000..9234c88 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalFlowDetailVo.java @@ -0,0 +1,152 @@ +package tech.easyflow.approval.entity.vo; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * 审批流程详情。 + */ +public class ApprovalFlowDetailVo { + + private BigInteger id; + + private String name; + + private String resourceType; + + private String actionType; + + private Integer priority; + + private String status; + + private Integer version; + + private String remark; + + private Date created; + + private Date modified; + + private boolean deletable; + + private long pendingInstanceCount; + + private List scopes = new ArrayList<>(); + + private List steps = new ArrayList<>(); + + public BigInteger getId() { + return id; + } + + public void setId(BigInteger id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getResourceType() { + return resourceType; + } + + public void setResourceType(String resourceType) { + this.resourceType = resourceType; + } + + public String getActionType() { + return actionType; + } + + public void setActionType(String actionType) { + this.actionType = actionType; + } + + public Integer getPriority() { + return priority; + } + + public void setPriority(Integer priority) { + this.priority = priority; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public Integer getVersion() { + return version; + } + + public void setVersion(Integer version) { + this.version = version; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public Date getModified() { + return modified; + } + + public void setModified(Date modified) { + this.modified = modified; + } + + public boolean isDeletable() { + return deletable; + } + + public void setDeletable(boolean deletable) { + this.deletable = deletable; + } + + public long getPendingInstanceCount() { + return pendingInstanceCount; + } + + public void setPendingInstanceCount(long pendingInstanceCount) { + this.pendingInstanceCount = pendingInstanceCount; + } + + public List getScopes() { + return scopes; + } + + public void setScopes(List scopes) { + this.scopes = scopes; + } + + public List getSteps() { + return steps; + } + + public void setSteps(List steps) { + this.steps = steps; + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalFlowPageVo.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalFlowPageVo.java new file mode 100644 index 0000000..51f11b0 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalFlowPageVo.java @@ -0,0 +1,120 @@ +package tech.easyflow.approval.entity.vo; + +import java.math.BigInteger; +import java.util.Date; + +/** + * 审批流程分页项。 + */ +public class ApprovalFlowPageVo { + + private BigInteger id; + + private String name; + + private String resourceType; + + private String actionType; + + private Integer priority; + + private String status; + + private Integer version; + + private String scopeSummary; + + private Integer stepCount; + + private long pendingInstanceCount; + + private Date modified; + + public BigInteger getId() { + return id; + } + + public void setId(BigInteger id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getResourceType() { + return resourceType; + } + + public void setResourceType(String resourceType) { + this.resourceType = resourceType; + } + + public String getActionType() { + return actionType; + } + + public void setActionType(String actionType) { + this.actionType = actionType; + } + + public Integer getPriority() { + return priority; + } + + public void setPriority(Integer priority) { + this.priority = priority; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public Integer getVersion() { + return version; + } + + public void setVersion(Integer version) { + this.version = version; + } + + public String getScopeSummary() { + return scopeSummary; + } + + public void setScopeSummary(String scopeSummary) { + this.scopeSummary = scopeSummary; + } + + public Integer getStepCount() { + return stepCount; + } + + public void setStepCount(Integer stepCount) { + this.stepCount = stepCount; + } + + public long getPendingInstanceCount() { + return pendingInstanceCount; + } + + public void setPendingInstanceCount(long pendingInstanceCount) { + this.pendingInstanceCount = pendingInstanceCount; + } + + public Date getModified() { + return modified; + } + + public void setModified(Date modified) { + this.modified = modified; + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalFlowScopeVo.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalFlowScopeVo.java new file mode 100644 index 0000000..6b78f0b --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalFlowScopeVo.java @@ -0,0 +1,49 @@ +package tech.easyflow.approval.entity.vo; + +import java.math.BigInteger; + +/** + * 审批流程范围项。 + */ +public class ApprovalFlowScopeVo { + + private BigInteger id; + + private String scopeType; + + private BigInteger scopeValue; + + private Integer includeChildren; + + public BigInteger getId() { + return id; + } + + public void setId(BigInteger id) { + this.id = id; + } + + public String getScopeType() { + return scopeType; + } + + public void setScopeType(String scopeType) { + this.scopeType = scopeType; + } + + public BigInteger getScopeValue() { + return scopeValue; + } + + public void setScopeValue(BigInteger scopeValue) { + this.scopeValue = scopeValue; + } + + public Integer getIncludeChildren() { + return includeChildren; + } + + public void setIncludeChildren(Integer includeChildren) { + this.includeChildren = includeChildren; + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalFlowStepVo.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalFlowStepVo.java new file mode 100644 index 0000000..1364e53 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalFlowStepVo.java @@ -0,0 +1,79 @@ +package tech.easyflow.approval.entity.vo; + +import java.math.BigInteger; + +/** + * 审批流程步骤项。 + */ +public class ApprovalFlowStepVo { + + private BigInteger id; + + private Integer stepNo; + + private String stepName; + + private String assigneeType; + + private BigInteger assigneeTargetId; + + private String assigneeTargetCode; + + private String assigneeTargetName; + + public BigInteger getId() { + return id; + } + + public void setId(BigInteger id) { + this.id = id; + } + + public Integer getStepNo() { + return stepNo; + } + + public void setStepNo(Integer stepNo) { + this.stepNo = stepNo; + } + + public String getStepName() { + return stepName; + } + + public void setStepName(String stepName) { + this.stepName = stepName; + } + + public String getAssigneeType() { + return assigneeType; + } + + public void setAssigneeType(String assigneeType) { + this.assigneeType = assigneeType; + } + + public BigInteger getAssigneeTargetId() { + return assigneeTargetId; + } + + public void setAssigneeTargetId(BigInteger assigneeTargetId) { + this.assigneeTargetId = assigneeTargetId; + } + + public String getAssigneeTargetCode() { + return assigneeTargetCode; + } + + public void setAssigneeTargetCode(String assigneeTargetCode) { + this.assigneeTargetCode = assigneeTargetCode; + } + + public String getAssigneeTargetName() { + return assigneeTargetName; + } + + public void setAssigneeTargetName(String assigneeTargetName) { + this.assigneeTargetName = assigneeTargetName; + } +} 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 new file mode 100644 index 0000000..afba506 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalInstanceDetailVo.java @@ -0,0 +1,203 @@ +package tech.easyflow.approval.entity.vo; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * 审批实例详情。 + */ +public class ApprovalInstanceDetailVo { + + private BigInteger id; + + private BigInteger flowId; + + private Integer flowVersion; + + private String resourceType; + + private BigInteger resourceId; + + private String actionType; + + private String status; + + private Integer currentStepNo; + + private String summary; + + private BigInteger applicantId; + + private String applicantName; + + private Date submittedAt; + + private Date finishedAt; + + private Map snapshotJson; + + private boolean canApprove; + + private boolean canReject; + + private boolean canRevoke; + + private List tasks = new ArrayList<>(); + + private List logs = new ArrayList<>(); + + public BigInteger getId() { + return id; + } + + public void setId(BigInteger id) { + this.id = id; + } + + public BigInteger getFlowId() { + return flowId; + } + + public void setFlowId(BigInteger flowId) { + this.flowId = flowId; + } + + public Integer getFlowVersion() { + return flowVersion; + } + + public void setFlowVersion(Integer flowVersion) { + this.flowVersion = flowVersion; + } + + public String getResourceType() { + return resourceType; + } + + public void setResourceType(String resourceType) { + this.resourceType = resourceType; + } + + public BigInteger getResourceId() { + return resourceId; + } + + public void setResourceId(BigInteger resourceId) { + this.resourceId = resourceId; + } + + public String getActionType() { + return actionType; + } + + public void setActionType(String actionType) { + this.actionType = actionType; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public Integer getCurrentStepNo() { + return currentStepNo; + } + + public void setCurrentStepNo(Integer currentStepNo) { + this.currentStepNo = currentStepNo; + } + + public String getSummary() { + return summary; + } + + public void setSummary(String summary) { + this.summary = summary; + } + + public BigInteger getApplicantId() { + return applicantId; + } + + public void setApplicantId(BigInteger applicantId) { + this.applicantId = applicantId; + } + + public String getApplicantName() { + return applicantName; + } + + public void setApplicantName(String applicantName) { + this.applicantName = applicantName; + } + + public Date getSubmittedAt() { + return submittedAt; + } + + public void setSubmittedAt(Date submittedAt) { + this.submittedAt = submittedAt; + } + + public Date getFinishedAt() { + return finishedAt; + } + + public void setFinishedAt(Date finishedAt) { + this.finishedAt = finishedAt; + } + + public Map getSnapshotJson() { + return snapshotJson; + } + + public void setSnapshotJson(Map snapshotJson) { + this.snapshotJson = snapshotJson; + } + + public boolean isCanApprove() { + return canApprove; + } + + public void setCanApprove(boolean canApprove) { + this.canApprove = canApprove; + } + + public boolean isCanReject() { + return canReject; + } + + public void setCanReject(boolean canReject) { + this.canReject = canReject; + } + + public boolean isCanRevoke() { + return canRevoke; + } + + public void setCanRevoke(boolean canRevoke) { + this.canRevoke = canRevoke; + } + + public List getTasks() { + return tasks; + } + + public void setTasks(List tasks) { + this.tasks = tasks; + } + + public List getLogs() { + return logs; + } + + public void setLogs(List logs) { + this.logs = logs; + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalInstancePageVo.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalInstancePageVo.java new file mode 100644 index 0000000..67e56b4 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalInstancePageVo.java @@ -0,0 +1,150 @@ +package tech.easyflow.approval.entity.vo; + +import java.math.BigInteger; +import java.util.Date; + +/** + * 审批实例分页项。 + */ +public class ApprovalInstancePageVo { + + private BigInteger id; + + private String resourceType; + + private BigInteger resourceId; + + private String actionType; + + private String status; + + private Integer currentStepNo; + + private String currentStepName; + + private String summary; + + private BigInteger applicantId; + + private Date submittedAt; + + private Date finishedAt; + + private boolean canApprove; + + private boolean canReject; + + private boolean canRevoke; + + public BigInteger getId() { + return id; + } + + public void setId(BigInteger id) { + this.id = id; + } + + public String getResourceType() { + return resourceType; + } + + public void setResourceType(String resourceType) { + this.resourceType = resourceType; + } + + public BigInteger getResourceId() { + return resourceId; + } + + public void setResourceId(BigInteger resourceId) { + this.resourceId = resourceId; + } + + public String getActionType() { + return actionType; + } + + public void setActionType(String actionType) { + this.actionType = actionType; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public Integer getCurrentStepNo() { + return currentStepNo; + } + + public void setCurrentStepNo(Integer currentStepNo) { + this.currentStepNo = currentStepNo; + } + + public String getCurrentStepName() { + return currentStepName; + } + + public void setCurrentStepName(String currentStepName) { + this.currentStepName = currentStepName; + } + + public String getSummary() { + return summary; + } + + public void setSummary(String summary) { + this.summary = summary; + } + + public BigInteger getApplicantId() { + return applicantId; + } + + public void setApplicantId(BigInteger applicantId) { + this.applicantId = applicantId; + } + + public Date getSubmittedAt() { + return submittedAt; + } + + public void setSubmittedAt(Date submittedAt) { + this.submittedAt = submittedAt; + } + + public Date getFinishedAt() { + return finishedAt; + } + + public void setFinishedAt(Date finishedAt) { + this.finishedAt = finishedAt; + } + + public boolean isCanApprove() { + return canApprove; + } + + public void setCanApprove(boolean canApprove) { + this.canApprove = canApprove; + } + + public boolean isCanReject() { + return canReject; + } + + public void setCanReject(boolean canReject) { + this.canReject = canReject; + } + + public boolean isCanRevoke() { + return canRevoke; + } + + public void setCanRevoke(boolean canRevoke) { + this.canRevoke = canRevoke; + } +} 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 new file mode 100644 index 0000000..2031ac7 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalLogVo.java @@ -0,0 +1,71 @@ +package tech.easyflow.approval.entity.vo; + +import java.math.BigInteger; +import java.util.Date; +import java.util.Map; + +/** + * 审批日志视图。 + */ +public class ApprovalLogVo { + + private BigInteger id; + + private String eventType; + + private BigInteger operatorId; + + private String operatorName; + + private Date created; + + private Map payloadJson; + + public BigInteger getId() { + return id; + } + + public void setId(BigInteger id) { + this.id = id; + } + + public String getEventType() { + return eventType; + } + + public void setEventType(String eventType) { + this.eventType = eventType; + } + + public BigInteger getOperatorId() { + return operatorId; + } + + public void setOperatorId(BigInteger operatorId) { + this.operatorId = operatorId; + } + + public String getOperatorName() { + return operatorName; + } + + public void setOperatorName(String operatorName) { + this.operatorName = operatorName; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public Map getPayloadJson() { + return payloadJson; + } + + public void setPayloadJson(Map payloadJson) { + this.payloadJson = payloadJson; + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalSubmitCallbackContext.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalSubmitCallbackContext.java new file mode 100644 index 0000000..2f5a297 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalSubmitCallbackContext.java @@ -0,0 +1,59 @@ +package tech.easyflow.approval.entity.vo; + +import java.math.BigInteger; + +/** + * 审批提交后回调上下文。 + */ +public class ApprovalSubmitCallbackContext { + + private BigInteger instanceId; + + private String resourceType; + + private BigInteger resourceId; + + private String actionType; + + private BigInteger operatorId; + + public BigInteger getInstanceId() { + return instanceId; + } + + public void setInstanceId(BigInteger instanceId) { + this.instanceId = instanceId; + } + + public String getResourceType() { + return resourceType; + } + + public void setResourceType(String resourceType) { + this.resourceType = resourceType; + } + + public BigInteger getResourceId() { + return resourceId; + } + + public void setResourceId(BigInteger resourceId) { + this.resourceId = resourceId; + } + + public String getActionType() { + return actionType; + } + + public void setActionType(String actionType) { + this.actionType = actionType; + } + + public BigInteger getOperatorId() { + return operatorId; + } + + public void setOperatorId(BigInteger operatorId) { + this.operatorId = operatorId; + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalSubmitRequest.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalSubmitRequest.java new file mode 100644 index 0000000..f0531b4 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalSubmitRequest.java @@ -0,0 +1,90 @@ +package tech.easyflow.approval.entity.vo; + +import java.math.BigInteger; +import java.util.Map; + +/** + * 发起审批请求。 + */ +public class ApprovalSubmitRequest { + + private String resourceType; + + private BigInteger resourceId; + + private String actionType; + + private BigInteger categoryId; + + private BigInteger deptId; + + private BigInteger applicantId; + + private String summary; + + private Map snapshotJson; + + public String getResourceType() { + return resourceType; + } + + public void setResourceType(String resourceType) { + this.resourceType = resourceType; + } + + public BigInteger getResourceId() { + return resourceId; + } + + public void setResourceId(BigInteger resourceId) { + this.resourceId = resourceId; + } + + public String getActionType() { + return actionType; + } + + public void setActionType(String actionType) { + this.actionType = actionType; + } + + public BigInteger getCategoryId() { + return categoryId; + } + + public void setCategoryId(BigInteger categoryId) { + this.categoryId = categoryId; + } + + public BigInteger getDeptId() { + return deptId; + } + + public void setDeptId(BigInteger deptId) { + this.deptId = deptId; + } + + public BigInteger getApplicantId() { + return applicantId; + } + + public void setApplicantId(BigInteger applicantId) { + this.applicantId = applicantId; + } + + public String getSummary() { + return summary; + } + + public void setSummary(String summary) { + this.summary = summary; + } + + public Map getSnapshotJson() { + return snapshotJson; + } + + public void setSnapshotJson(Map snapshotJson) { + this.snapshotJson = snapshotJson; + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalTaskVo.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalTaskVo.java new file mode 100644 index 0000000..c0e8433 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalTaskVo.java @@ -0,0 +1,140 @@ +package tech.easyflow.approval.entity.vo; + +import java.math.BigInteger; +import java.util.Date; + +/** + * 审批任务视图。 + */ +public class ApprovalTaskVo { + + private BigInteger id; + + private Integer stepNo; + + private String stepName; + + private String status; + + private String assigneeRoleCode; + + private String assigneeType; + + private BigInteger assigneeTargetId; + + private String assigneeTargetCode; + + private String assigneeTargetName; + + private BigInteger actedBy; + + private String actedByName; + + private Date actedAt; + + private String comment; + + public BigInteger getId() { + return id; + } + + public void setId(BigInteger id) { + this.id = id; + } + + public Integer getStepNo() { + return stepNo; + } + + public void setStepNo(Integer stepNo) { + this.stepNo = stepNo; + } + + public String getStepName() { + return stepName; + } + + public void setStepName(String stepName) { + this.stepName = stepName; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getAssigneeRoleCode() { + return assigneeRoleCode; + } + + public void setAssigneeRoleCode(String assigneeRoleCode) { + this.assigneeRoleCode = assigneeRoleCode; + } + + public String getAssigneeType() { + return assigneeType; + } + + public void setAssigneeType(String assigneeType) { + this.assigneeType = assigneeType; + } + + public BigInteger getAssigneeTargetId() { + return assigneeTargetId; + } + + public void setAssigneeTargetId(BigInteger assigneeTargetId) { + this.assigneeTargetId = assigneeTargetId; + } + + public String getAssigneeTargetCode() { + return assigneeTargetCode; + } + + public void setAssigneeTargetCode(String assigneeTargetCode) { + this.assigneeTargetCode = assigneeTargetCode; + } + + public String getAssigneeTargetName() { + return assigneeTargetName; + } + + public void setAssigneeTargetName(String assigneeTargetName) { + this.assigneeTargetName = assigneeTargetName; + } + + public BigInteger getActedBy() { + return actedBy; + } + + public void setActedBy(BigInteger actedBy) { + this.actedBy = actedBy; + } + + public String getActedByName() { + return actedByName; + } + + public void setActedByName(String actedByName) { + this.actedByName = actedByName; + } + + public Date getActedAt() { + return actedAt; + } + + public void setActedAt(Date actedAt) { + this.actedAt = actedAt; + } + + public String getComment() { + return comment; + } + + public void setComment(String comment) { + this.comment = comment; + } +} 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 new file mode 100644 index 0000000..1a97b0d --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/enums/ApprovalActionType.java @@ -0,0 +1,45 @@ +package tech.easyflow.approval.enums; + +import java.util.Arrays; +import java.util.Locale; + +/** + * 审批动作类型枚举。 + */ +public enum ApprovalActionType { + + PUBLISH("PUBLISH"), + DELETE("DELETE"); + + private final String code; + + ApprovalActionType(String code) { + this.code = code; + } + + /** + * 获取动作类型编码。 + * + * @return 动作类型编码 + */ + public String getCode() { + return code; + } + + /** + * 解析动作类型。 + * + * @param code 动作类型编码 + * @return 动作类型 + */ + public static ApprovalActionType from(String code) { + if (code == null) { + throw new IllegalArgumentException("actionType不能为空"); + } + String normalized = code.trim().toUpperCase(Locale.ROOT); + return Arrays.stream(values()) + .filter(item -> item.code.equals(normalized)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("不支持的actionType: " + code)); + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/enums/ApprovalAssigneeType.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/enums/ApprovalAssigneeType.java new file mode 100644 index 0000000..1950344 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/enums/ApprovalAssigneeType.java @@ -0,0 +1,41 @@ +package tech.easyflow.approval.enums; + +import org.springframework.util.StringUtils; +import tech.easyflow.common.web.exceptions.BusinessException; + +/** + * 审批对象类型。 + */ +public enum ApprovalAssigneeType { + + ROLE("ROLE"), + USER("USER"); + + private final String code; + + ApprovalAssigneeType(String code) { + this.code = code; + } + + /** + * 解析审批对象类型。 + * + * @param code 类型编码 + * @return 审批对象类型 + */ + public static ApprovalAssigneeType from(String code) { + if (!StringUtils.hasText(code)) { + throw new BusinessException("审批对象类型不能为空"); + } + for (ApprovalAssigneeType value : values()) { + if (value.code.equalsIgnoreCase(code.trim())) { + return value; + } + } + throw new BusinessException("审批对象类型非法"); + } + + public String getCode() { + return code; + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/enums/ApprovalEventType.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/enums/ApprovalEventType.java new file mode 100644 index 0000000..3bd7d36 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/enums/ApprovalEventType.java @@ -0,0 +1,48 @@ +package tech.easyflow.approval.enums; + +import java.util.Arrays; +import java.util.Locale; + +/** + * 审批实例日志事件枚举。 + */ +public enum ApprovalEventType { + + SUBMITTED("SUBMITTED"), + STEP_CREATED("STEP_CREATED"), + APPROVED("APPROVED"), + REJECTED("REJECTED"), + REVOKED("REVOKED"); + + private final String code; + + ApprovalEventType(String code) { + this.code = code; + } + + /** + * 获取事件编码。 + * + * @return 事件编码 + */ + public String getCode() { + return code; + } + + /** + * 解析事件类型。 + * + * @param code 事件编码 + * @return 事件类型 + */ + public static ApprovalEventType from(String code) { + if (code == null) { + throw new IllegalArgumentException("eventType不能为空"); + } + String normalized = code.trim().toUpperCase(Locale.ROOT); + return Arrays.stream(values()) + .filter(item -> item.code.equals(normalized)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("不支持的eventType: " + code)); + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/enums/ApprovalFlowStatus.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/enums/ApprovalFlowStatus.java new file mode 100644 index 0000000..52897e0 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/enums/ApprovalFlowStatus.java @@ -0,0 +1,45 @@ +package tech.easyflow.approval.enums; + +import java.util.Arrays; +import java.util.Locale; + +/** + * 审批流程状态枚举。 + */ +public enum ApprovalFlowStatus { + + ENABLED("ENABLED"), + DISABLED("DISABLED"); + + private final String code; + + ApprovalFlowStatus(String code) { + this.code = code; + } + + /** + * 获取状态编码。 + * + * @return 状态编码 + */ + public String getCode() { + return code; + } + + /** + * 解析流程状态。 + * + * @param code 状态编码 + * @return 流程状态 + */ + public static ApprovalFlowStatus from(String code) { + if (code == null) { + throw new IllegalArgumentException("status不能为空"); + } + String normalized = code.trim().toUpperCase(Locale.ROOT); + return Arrays.stream(values()) + .filter(item -> item.code.equals(normalized)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("不支持的status: " + code)); + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/enums/ApprovalInstanceStatus.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/enums/ApprovalInstanceStatus.java new file mode 100644 index 0000000..0427369 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/enums/ApprovalInstanceStatus.java @@ -0,0 +1,57 @@ +package tech.easyflow.approval.enums; + +import java.util.Arrays; +import java.util.Locale; + +/** + * 审批实例状态枚举。 + */ +public enum ApprovalInstanceStatus { + + PENDING("PENDING"), + PROCESSING("PROCESSING"), + APPROVED("APPROVED"), + REJECTED("REJECTED"), + REVOKED("REVOKED"); + + private final String code; + + ApprovalInstanceStatus(String code) { + this.code = code; + } + + /** + * 获取状态编码。 + * + * @return 状态编码 + */ + public String getCode() { + return code; + } + + /** + * 判断是否已结束。 + * + * @return 是否结束 + */ + public boolean isFinished() { + return this == APPROVED || this == REJECTED || this == REVOKED; + } + + /** + * 解析实例状态。 + * + * @param code 状态编码 + * @return 实例状态 + */ + public static ApprovalInstanceStatus from(String code) { + if (code == null) { + throw new IllegalArgumentException("status不能为空"); + } + String normalized = code.trim().toUpperCase(Locale.ROOT); + return Arrays.stream(values()) + .filter(item -> item.code.equals(normalized)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("不支持的status: " + code)); + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/enums/ApprovalResourceType.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/enums/ApprovalResourceType.java new file mode 100644 index 0000000..1a1e117 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/enums/ApprovalResourceType.java @@ -0,0 +1,57 @@ +package tech.easyflow.approval.enums; + +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; + +/** + * 审批资源类型枚举。 + */ +public enum ApprovalResourceType { + + BOT("BOT"), + WORKFLOW("WORKFLOW"), + KNOWLEDGE("KNOWLEDGE"); + + private final String code; + + ApprovalResourceType(String code) { + this.code = code; + } + + /** + * 获取资源类型编码。 + * + * @return 资源类型编码 + */ + public String getCode() { + return code; + } + + /** + * 解析资源类型。 + * + * @param code 资源类型编码 + * @return 资源类型 + */ + public static ApprovalResourceType from(String code) { + if (code == null) { + throw new IllegalArgumentException("resourceType不能为空"); + } + String normalized = code.trim().toUpperCase(Locale.ROOT); + return Arrays.stream(values()) + .filter(item -> item.code.equals(normalized)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("不支持的resourceType: " + code)); + } + + /** + * 返回全部资源类型编码。 + * + * @return 资源类型编码列表 + */ + public static List allCodes() { + return Arrays.stream(values()).map(ApprovalResourceType::getCode).collect(Collectors.toList()); + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/enums/ApprovalScopeType.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/enums/ApprovalScopeType.java new file mode 100644 index 0000000..31b0191 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/enums/ApprovalScopeType.java @@ -0,0 +1,45 @@ +package tech.easyflow.approval.enums; + +import java.util.Arrays; +import java.util.Locale; + +/** + * 审批流程范围类型枚举。 + */ +public enum ApprovalScopeType { + + CATEGORY("CATEGORY"), + DEPT("DEPT"); + + private final String code; + + ApprovalScopeType(String code) { + this.code = code; + } + + /** + * 获取范围类型编码。 + * + * @return 范围类型编码 + */ + public String getCode() { + return code; + } + + /** + * 解析范围类型。 + * + * @param code 范围类型编码 + * @return 范围类型 + */ + public static ApprovalScopeType from(String code) { + if (code == null) { + throw new IllegalArgumentException("scopeType不能为空"); + } + String normalized = code.trim().toUpperCase(Locale.ROOT); + return Arrays.stream(values()) + .filter(item -> item.code.equals(normalized)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("不支持的scopeType: " + code)); + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/enums/ApprovalTaskStatus.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/enums/ApprovalTaskStatus.java new file mode 100644 index 0000000..dbc86ba --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/enums/ApprovalTaskStatus.java @@ -0,0 +1,47 @@ +package tech.easyflow.approval.enums; + +import java.util.Arrays; +import java.util.Locale; + +/** + * 审批任务状态枚举。 + */ +public enum ApprovalTaskStatus { + + PENDING("PENDING"), + APPROVED("APPROVED"), + REJECTED("REJECTED"), + REVOKED("REVOKED"); + + private final String code; + + ApprovalTaskStatus(String code) { + this.code = code; + } + + /** + * 获取任务状态编码。 + * + * @return 任务状态编码 + */ + public String getCode() { + return code; + } + + /** + * 解析任务状态。 + * + * @param code 任务状态编码 + * @return 任务状态 + */ + public static ApprovalTaskStatus from(String code) { + if (code == null) { + throw new IllegalArgumentException("taskStatus不能为空"); + } + String normalized = code.trim().toUpperCase(Locale.ROOT); + return Arrays.stream(values()) + .filter(item -> item.code.equals(normalized)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("不支持的taskStatus: " + code)); + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/mapper/ApprovalFlowMapper.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/mapper/ApprovalFlowMapper.java new file mode 100644 index 0000000..6a2cbd5 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/mapper/ApprovalFlowMapper.java @@ -0,0 +1,10 @@ +package tech.easyflow.approval.mapper; + +import com.mybatisflex.core.BaseMapper; +import tech.easyflow.approval.entity.ApprovalFlow; + +/** + * 审批流程 Mapper。 + */ +public interface ApprovalFlowMapper extends BaseMapper { +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/mapper/ApprovalFlowScopeMapper.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/mapper/ApprovalFlowScopeMapper.java new file mode 100644 index 0000000..2600c97 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/mapper/ApprovalFlowScopeMapper.java @@ -0,0 +1,10 @@ +package tech.easyflow.approval.mapper; + +import com.mybatisflex.core.BaseMapper; +import tech.easyflow.approval.entity.ApprovalFlowScope; + +/** + * 审批流程范围 Mapper。 + */ +public interface ApprovalFlowScopeMapper extends BaseMapper { +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/mapper/ApprovalFlowStepMapper.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/mapper/ApprovalFlowStepMapper.java new file mode 100644 index 0000000..d93e9ff --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/mapper/ApprovalFlowStepMapper.java @@ -0,0 +1,10 @@ +package tech.easyflow.approval.mapper; + +import com.mybatisflex.core.BaseMapper; +import tech.easyflow.approval.entity.ApprovalFlowStep; + +/** + * 审批流程步骤 Mapper。 + */ +public interface ApprovalFlowStepMapper extends BaseMapper { +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/mapper/ApprovalInstanceMapper.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/mapper/ApprovalInstanceMapper.java new file mode 100644 index 0000000..4b5a9c6 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/mapper/ApprovalInstanceMapper.java @@ -0,0 +1,10 @@ +package tech.easyflow.approval.mapper; + +import com.mybatisflex.core.BaseMapper; +import tech.easyflow.approval.entity.ApprovalInstance; + +/** + * 审批实例 Mapper。 + */ +public interface ApprovalInstanceMapper extends BaseMapper { +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/mapper/ApprovalLogMapper.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/mapper/ApprovalLogMapper.java new file mode 100644 index 0000000..76ddd2a --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/mapper/ApprovalLogMapper.java @@ -0,0 +1,10 @@ +package tech.easyflow.approval.mapper; + +import com.mybatisflex.core.BaseMapper; +import tech.easyflow.approval.entity.ApprovalLog; + +/** + * 审批日志 Mapper。 + */ +public interface ApprovalLogMapper extends BaseMapper { +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/mapper/ApprovalTaskMapper.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/mapper/ApprovalTaskMapper.java new file mode 100644 index 0000000..5867687 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/mapper/ApprovalTaskMapper.java @@ -0,0 +1,10 @@ +package tech.easyflow.approval.mapper; + +import com.mybatisflex.core.BaseMapper; +import tech.easyflow.approval.entity.ApprovalTask; + +/** + * 审批任务 Mapper。 + */ +public interface ApprovalTaskMapper extends BaseMapper { +} 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 new file mode 100644 index 0000000..6c7ec0b --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/ApprovalActionFacade.java @@ -0,0 +1,58 @@ +package tech.easyflow.approval.service; + +import tech.easyflow.approval.entity.ApprovalInstance; + +import java.math.BigInteger; + +/** + * 审批动作门面。 + */ +public interface ApprovalActionFacade { + + /** + * 提交审批。 + * + * @param resourceType 资源类型 + * @param resourceId 资源 ID + * @param actionType 动作类型 + * @param operatorId 操作人 ID + * @return 审批实例 ID + */ + BigInteger submit(String resourceType, BigInteger resourceId, String actionType, BigInteger operatorId); + + /** + * 处理审批通过后的业务回调。 + * + * @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); + + /** + * 校验已发布访问权限。 + * + * @param resourceType 资源类型 + * @param identifier 资源标识 + * @param denyMessage 拒绝提示 + */ + void assertPublishedAccess(String resourceType, Object identifier, String denyMessage); +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/ApprovalAssigneeService.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/ApprovalAssigneeService.java new file mode 100644 index 0000000..a5dda16 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/ApprovalAssigneeService.java @@ -0,0 +1,69 @@ +package tech.easyflow.approval.service; + +import com.mybatisflex.core.paginate.Page; +import tech.easyflow.approval.entity.ApprovalTask; +import tech.easyflow.approval.entity.vo.ApprovalAssigneeOptionVo; +import tech.easyflow.approval.entity.vo.ApprovalFlowStepVo; + +import java.math.BigInteger; +import java.util.List; +import java.util.Set; + +/** + * 审批对象服务。 + */ +public interface ApprovalAssigneeService { + + /** + * 规范化并校验审批步骤中的审批对象配置。 + * + * @param step 步骤配置 + * @return 规范化后的步骤配置 + */ + ApprovalFlowStepVo normalizeStepAssignee(ApprovalFlowStepVo step); + + /** + * 查询可用角色选项。 + * + * @return 角色选项 + */ + List listRoleOptions(); + + /** + * 分页查询可用用户选项。 + * + * @param keyword 关键词 + * @param pageNumber 页码 + * @param pageSize 每页数量 + * @return 用户选项分页 + */ + Page pageAccountOptions(String keyword, Long pageNumber, Long pageSize); + + /** + * 查询指定账号当前仍有效的角色 ID 集合。 + * + * @param accountId 账号 ID + * @return 角色 ID 集合 + */ + Set getAvailableRoleIds(BigInteger accountId); + + /** + * 判断当前账号是否命中审批任务。 + * + * @param task 审批任务 + * @param operatorId 当前操作人 + * @param roleIds 当前操作人的有效角色集合 + * @return 是否命中 + */ + boolean canHandleTask(ApprovalTask task, BigInteger operatorId, Set roleIds); + + /** + * 查询当前账号命中的待审批实例 ID 集合。 + * + * @param operatorId 当前操作人 + * @param roleIds 当前操作人的有效角色集合 + * @param instanceIds 可选的实例 ID 过滤条件 + * @return 命中的实例 ID 集合 + */ + Set listPendingInstanceIds(BigInteger operatorId, Set roleIds, List instanceIds); +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/ApprovalFlowService.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/ApprovalFlowService.java new file mode 100644 index 0000000..20b8add --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/ApprovalFlowService.java @@ -0,0 +1,77 @@ +package tech.easyflow.approval.service; + +import com.mybatisflex.core.paginate.Page; +import com.mybatisflex.core.service.IService; +import tech.easyflow.approval.entity.ApprovalFlow; +import tech.easyflow.approval.entity.vo.ApprovalFlowDetailVo; +import tech.easyflow.approval.entity.vo.ApprovalFlowPageVo; + +import java.math.BigInteger; + +/** + * 审批流程服务。 + */ +public interface ApprovalFlowService extends IService { + + /** + * 分页查询审批流程。 + * + * @param name 流程名称 + * @param resourceType 资源类型 + * @param actionType 动作类型 + * @param status 流程状态 + * @param pageNumber 页码 + * @param pageSize 每页数量 + * @return 流程分页结果 + */ + Page pageFlows(String name, String resourceType, String actionType, String status, + Long pageNumber, Long pageSize); + + /** + * 查询审批流程详情。 + * + * @param id 流程ID + * @return 流程详情 + */ + ApprovalFlowDetailVo getFlowDetail(BigInteger id); + + /** + * 新增审批流程。 + * + * @param request 流程详情请求 + * @param operatorId 操作人ID + * @return 新增后的流程ID + */ + BigInteger saveFlow(ApprovalFlowDetailVo request, BigInteger operatorId); + + /** + * 更新审批流程。 + * + * @param request 流程详情请求 + * @param operatorId 操作人ID + */ + void updateFlow(ApprovalFlowDetailVo request, BigInteger operatorId); + + /** + * 启用审批流程。 + * + * @param id 流程ID + * @param operatorId 操作人ID + */ + void enableFlow(BigInteger id, BigInteger operatorId); + + /** + * 停用审批流程。 + * + * @param id 流程ID + * @param operatorId 操作人ID + */ + void disableFlow(BigInteger id, BigInteger operatorId); + + /** + * 删除审批流程。 + * + * @param id 流程ID + */ + void removeFlow(BigInteger id); +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/ApprovalInstanceService.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/ApprovalInstanceService.java new file mode 100644 index 0000000..01e3248 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/ApprovalInstanceService.java @@ -0,0 +1,64 @@ +package tech.easyflow.approval.service; + +import tech.easyflow.approval.entity.ApprovalInstance; +import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest; + +import java.math.BigInteger; + +/** + * 审批实例服务。 + */ +public interface ApprovalInstanceService { + + /** + * 发起审批。 + * + * @param request 发起审批请求 + * @return 审批实例ID + */ + BigInteger submitApproval(ApprovalSubmitRequest request); + + /** + * 通过审批。 + * + * @param instanceId 审批实例ID + * @param comment 审批意见 + * @param operatorId 操作人ID + */ + void approve(BigInteger instanceId, String comment, BigInteger operatorId); + + /** + * 驳回审批。 + * + * @param instanceId 审批实例ID + * @param comment 审批意见 + * @param operatorId 操作人ID + */ + void reject(BigInteger instanceId, String comment, BigInteger operatorId); + + /** + * 撤回审批。 + * + * @param instanceId 审批实例ID + * @param comment 撤回说明 + * @param operatorId 操作人ID + */ + void revoke(BigInteger instanceId, String comment, BigInteger operatorId); + + /** + * 判断资源是否存在未结束审批实例。 + * + * @param resourceType 资源类型 + * @param resourceId 资源 ID + * @return 存在未结束实例时返回 true + */ + boolean existsActiveInstance(String resourceType, BigInteger resourceId); + + /** + * 根据实例 ID 查询实例。 + * + * @param instanceId 审批实例 ID + * @return 审批实例 + */ + ApprovalInstance getById(BigInteger instanceId); +} 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 new file mode 100644 index 0000000..5659f0e --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/ApprovalMatchService.java @@ -0,0 +1,18 @@ +package tech.easyflow.approval.service; + +import tech.easyflow.approval.entity.vo.ApprovalFlowDetailVo; +import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest; + +/** + * 审批流程匹配服务。 + */ +public interface ApprovalMatchService { + + /** + * 根据资源上下文匹配审批流程。 + * + * @param request 审批提交请求 + * @return 命中的流程详情 + */ + ApprovalFlowDetailVo matchFlow(ApprovalSubmitRequest request); +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/ApprovalQueryService.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/ApprovalQueryService.java new file mode 100644 index 0000000..9870b3f --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/ApprovalQueryService.java @@ -0,0 +1,60 @@ +package tech.easyflow.approval.service; + +import com.mybatisflex.core.paginate.Page; +import tech.easyflow.approval.entity.vo.ApprovalInstanceDetailVo; +import tech.easyflow.approval.entity.vo.ApprovalInstancePageVo; + +import java.math.BigInteger; + +/** + * 审批查询服务。 + */ +public interface ApprovalQueryService { + + /** + * 分页查询待审批列表。 + * + * @param resourceType 资源类型 + * @param actionType 动作类型 + * @param keyword 关键词 + * @param pageNumber 页码 + * @param pageSize 每页数量 + * @return 待审批分页 + */ + Page pendingPage(String resourceType, String actionType, String keyword, + Long pageNumber, Long pageSize); + + /** + * 分页查询已审批列表。 + * + * @param resourceType 资源类型 + * @param actionType 动作类型 + * @param keyword 关键词 + * @param pageNumber 页码 + * @param pageSize 每页数量 + * @return 已审批分页 + */ + Page processedPage(String resourceType, String actionType, String keyword, + Long pageNumber, Long pageSize); + + /** + * 分页查询我发起的审批列表。 + * + * @param resourceType 资源类型 + * @param actionType 动作类型 + * @param keyword 关键词 + * @param pageNumber 页码 + * @param pageSize 每页数量 + * @return 我发起的审批分页 + */ + Page initiatedPage(String resourceType, String actionType, String keyword, + Long pageNumber, Long pageSize); + + /** + * 查询审批实例详情。 + * + * @param instanceId 审批实例ID + * @return 审批实例详情 + */ + ApprovalInstanceDetailVo detail(BigInteger instanceId); +} 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 new file mode 100644 index 0000000..3bab5ca --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/ApprovalSubjectHandler.java @@ -0,0 +1,66 @@ +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; + +/** + * 审批资源处理器。 + */ +public interface ApprovalSubjectHandler { + + /** + * 当前处理器支持的资源类型。 + * + * @return 资源类型编码 + */ + String resourceType(); + + /** + * 构建审批提交请求。 + * + * @param resourceId 资源 ID + * @param actionType 动作类型 + * @param operatorId 操作人 ID + * @return 审批请求 + */ + 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); + + /** + * 校验资源是否已发布。 + * + * @param identifier 资源标识(ID 或别名) + * @param denyMessage 拒绝提示 + */ + void assertPublishedAccess(Object identifier, String denyMessage); +} 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 new file mode 100644 index 0000000..46655d4 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalActionFacadeImpl.java @@ -0,0 +1,103 @@ +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.ApprovalSubmitRequest; +import tech.easyflow.approval.enums.ApprovalResourceType; +import tech.easyflow.approval.service.ApprovalActionFacade; +import tech.easyflow.approval.service.ApprovalInstanceService; +import tech.easyflow.approval.service.ApprovalSubjectHandler; + +import java.math.BigInteger; +import java.util.List; + +/** + * 审批动作门面实现。 + */ +@Service +public class ApprovalActionFacadeImpl implements ApprovalActionFacade { + + private final List handlers; + private final ApprovalInstanceService approvalInstanceService; + + public ApprovalActionFacadeImpl(List handlers, + ApprovalInstanceService approvalInstanceService) { + this.handlers = handlers; + this.approvalInstanceService = approvalInstanceService; + } + + /** + * {@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); + 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; + } + + /** + * {@inheritDoc} + */ + @Override + public void handleApproved(ApprovalInstance instance, BigInteger operatorId, String comment) { + ApprovalSubjectHandler handler = getHandler(instance.getResourceType()); + handler.onApproved(buildCallbackContext(instance, operatorId, comment)); + } + + /** + * {@inheritDoc} + */ + @Override + public void handleRejected(ApprovalInstance instance, BigInteger operatorId, String comment) { + ApprovalSubjectHandler handler = getHandler(instance.getResourceType()); + handler.onRejected(buildCallbackContext(instance, operatorId, comment)); + } + + /** + * {@inheritDoc} + */ + @Override + public void handleRevoked(ApprovalInstance instance, BigInteger operatorId, String comment) { + ApprovalSubjectHandler handler = getHandler(instance.getResourceType()); + handler.onRevoked(buildCallbackContext(instance, operatorId, comment)); + } + + /** + * {@inheritDoc} + */ + @Override + public void assertPublishedAccess(String resourceType, Object identifier, String denyMessage) { + ApprovalSubjectHandler handler = getHandler(resourceType); + 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() + .filter(item -> normalized.equals(item.resourceType())) + .findFirst() + .orElseThrow(() -> new IllegalStateException("未找到审批处理器: " + resourceType)); + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalAssigneeServiceImpl.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalAssigneeServiceImpl.java new file mode 100644 index 0000000..f7ed886 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalAssigneeServiceImpl.java @@ -0,0 +1,201 @@ +package tech.easyflow.approval.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import com.mybatisflex.core.paginate.Page; +import com.mybatisflex.core.query.QueryWrapper; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import tech.easyflow.approval.entity.ApprovalTask; +import tech.easyflow.approval.entity.vo.ApprovalAssigneeOptionVo; +import tech.easyflow.approval.entity.vo.ApprovalFlowStepVo; +import tech.easyflow.approval.enums.ApprovalAssigneeType; +import tech.easyflow.approval.enums.ApprovalTaskStatus; +import tech.easyflow.approval.mapper.ApprovalTaskMapper; +import tech.easyflow.approval.service.ApprovalAssigneeService; +import tech.easyflow.common.constant.enums.EnumDataStatus; +import tech.easyflow.common.web.exceptions.BusinessException; +import tech.easyflow.system.entity.SysAccount; +import tech.easyflow.system.entity.SysRole; +import tech.easyflow.system.service.SysAccountService; +import tech.easyflow.system.service.SysRoleService; + +import javax.annotation.Resource; +import java.math.BigInteger; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 审批对象服务实现。 + */ +@Service +public class ApprovalAssigneeServiceImpl implements ApprovalAssigneeService { + + @Resource + private SysRoleService sysRoleService; + + @Resource + private SysAccountService sysAccountService; + + @Resource + private ApprovalTaskMapper approvalTaskMapper; + + /** + * {@inheritDoc} + */ + @Override + public ApprovalFlowStepVo normalizeStepAssignee(ApprovalFlowStepVo step) { + if (step == null) { + throw new BusinessException("审批步骤不能为空"); + } + ApprovalAssigneeType assigneeType = ApprovalAssigneeType.from(step.getAssigneeType()); + BigInteger targetId = step.getAssigneeTargetId(); + if (targetId == null) { + throw new BusinessException("审批对象不能为空"); + } + ApprovalAssigneeOptionVo option = resolveAssigneeOption(assigneeType, targetId); + step.setAssigneeType(assigneeType.getCode()); + step.setAssigneeTargetId(option.getId()); + step.setAssigneeTargetCode(option.getCode()); + step.setAssigneeTargetName(option.getName()); + return step; + } + + /** + * {@inheritDoc} + */ + @Override + public List listRoleOptions() { + return sysRoleService.list(QueryWrapper.create() + .eq(SysRole::getStatus, EnumDataStatus.AVAILABLE.getCode()) + .orderBy("role_name asc, id asc")) + .stream() + .map(this::toRoleOption) + .collect(Collectors.toList()); + } + + /** + * {@inheritDoc} + */ + @Override + public Page pageAccountOptions(String keyword, Long pageNumber, Long pageSize) { + long actualPageNumber = pageNumber == null || pageNumber < 1 ? 1L : pageNumber; + long actualPageSize = pageSize == null || pageSize < 1 ? 20L : pageSize; + QueryWrapper queryWrapper = QueryWrapper.create() + .eq(SysAccount::getStatus, EnumDataStatus.AVAILABLE.getCode()) + .orderBy("nickname asc, login_name asc, id asc"); + if (StringUtils.hasText(keyword)) { + String likeKeyword = "%" + keyword.trim() + "%"; + queryWrapper.and("(`login_name` like ? or `nickname` like ?)", likeKeyword, likeKeyword); + } + Page page = sysAccountService.page(new Page<>(actualPageNumber, actualPageSize), queryWrapper); + Page result = new Page<>(actualPageNumber, actualPageSize, page.getTotalRow()); + result.setRecords(page.getRecords().stream().map(this::toAccountOption).collect(Collectors.toList())); + return result; + } + + /** + * {@inheritDoc} + */ + @Override + public Set getAvailableRoleIds(BigInteger accountId) { + if (accountId == null) { + return Set.of(); + } + return sysRoleService.getRolesByAccountId(accountId).stream() + .filter(role -> role != null && EnumDataStatus.AVAILABLE.getCode().equals(role.getStatus())) + .map(SysRole::getId) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean canHandleTask(ApprovalTask task, BigInteger operatorId, Set roleIds) { + if (task == null || operatorId == null) { + return false; + } + ApprovalAssigneeType assigneeType = ApprovalAssigneeType.from(task.getAssigneeType()); + if (ApprovalAssigneeType.USER == assigneeType) { + return operatorId.equals(task.getAssigneeTargetId()); + } + return roleIds != null && roleIds.contains(task.getAssigneeTargetId()); + } + + /** + * {@inheritDoc} + */ + @Override + public Set listPendingInstanceIds(BigInteger operatorId, Set roleIds, List instanceIds) { + if (operatorId == null) { + return Set.of(); + } + Set result = new LinkedHashSet<>(); + QueryWrapper userQuery = QueryWrapper.create() + .eq(ApprovalTask::getStatus, ApprovalTaskStatus.PENDING.getCode()); + if (CollectionUtil.isNotEmpty(instanceIds)) { + userQuery.in(ApprovalTask::getInstanceId, instanceIds); + } + userQuery.eq(ApprovalTask::getAssigneeType, ApprovalAssigneeType.USER.getCode()) + .eq(ApprovalTask::getAssigneeTargetId, operatorId); + result.addAll(approvalTaskMapper.selectListByQuery(userQuery).stream() + .map(ApprovalTask::getInstanceId) + .collect(Collectors.toCollection(LinkedHashSet::new))); + if (CollectionUtil.isNotEmpty(roleIds)) { + QueryWrapper roleQuery = QueryWrapper.create() + .eq(ApprovalTask::getStatus, ApprovalTaskStatus.PENDING.getCode()) + .eq(ApprovalTask::getAssigneeType, ApprovalAssigneeType.ROLE.getCode()) + .in(ApprovalTask::getAssigneeTargetId, roleIds); + if (CollectionUtil.isNotEmpty(instanceIds)) { + roleQuery.in(ApprovalTask::getInstanceId, instanceIds); + } + result.addAll(approvalTaskMapper.selectListByQuery(roleQuery).stream() + .map(ApprovalTask::getInstanceId) + .collect(Collectors.toCollection(LinkedHashSet::new))); + } + return result; + } + + private ApprovalAssigneeOptionVo resolveAssigneeOption(ApprovalAssigneeType assigneeType, BigInteger targetId) { + if (ApprovalAssigneeType.ROLE == assigneeType) { + SysRole role = sysRoleService.getById(targetId); + if (role == null || !EnumDataStatus.AVAILABLE.getCode().equals(role.getStatus())) { + throw new BusinessException("审批角色不存在或未启用"); + } + return toRoleOption(role); + } + SysAccount account = sysAccountService.getById(targetId); + if (account == null || !EnumDataStatus.AVAILABLE.getCode().equals(account.getStatus())) { + throw new BusinessException("审批用户不存在或未启用"); + } + return toAccountOption(account); + } + + private ApprovalAssigneeOptionVo toRoleOption(SysRole role) { + ApprovalAssigneeOptionVo option = new ApprovalAssigneeOptionVo(); + option.setId(role.getId()); + option.setCode(role.getRoleKey()); + option.setName(role.getRoleName()); + return option; + } + + private ApprovalAssigneeOptionVo toAccountOption(SysAccount account) { + ApprovalAssigneeOptionVo option = new ApprovalAssigneeOptionVo(); + option.setId(account.getId()); + option.setCode(account.getLoginName()); + option.setName(resolveAccountDisplayName(account)); + return option; + } + + private String resolveAccountDisplayName(SysAccount account) { + if (StringUtils.hasText(account.getNickname())) { + return account.getNickname().trim(); + } + if (StringUtils.hasText(account.getLoginName())) { + return account.getLoginName().trim(); + } + return String.valueOf(account.getId()); + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalFlowServiceImpl.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalFlowServiceImpl.java new file mode 100644 index 0000000..28b7a90 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalFlowServiceImpl.java @@ -0,0 +1,464 @@ +package tech.easyflow.approval.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import com.mybatisflex.core.paginate.Page; +import com.mybatisflex.core.query.QueryWrapper; +import com.mybatisflex.spring.service.impl.ServiceImpl; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; +import tech.easyflow.common.web.exceptions.BusinessException; +import tech.easyflow.approval.entity.ApprovalFlow; +import tech.easyflow.approval.entity.ApprovalFlowScope; +import tech.easyflow.approval.entity.ApprovalFlowStep; +import tech.easyflow.approval.entity.ApprovalInstance; +import tech.easyflow.approval.entity.vo.ApprovalFlowDetailVo; +import tech.easyflow.approval.entity.vo.ApprovalFlowPageVo; +import tech.easyflow.approval.entity.vo.ApprovalFlowScopeVo; +import tech.easyflow.approval.entity.vo.ApprovalFlowStepVo; +import tech.easyflow.approval.enums.ApprovalActionType; +import tech.easyflow.approval.enums.ApprovalFlowStatus; +import tech.easyflow.approval.enums.ApprovalInstanceStatus; +import tech.easyflow.approval.enums.ApprovalResourceType; +import tech.easyflow.approval.enums.ApprovalScopeType; +import tech.easyflow.approval.mapper.ApprovalFlowMapper; +import tech.easyflow.approval.mapper.ApprovalFlowScopeMapper; +import tech.easyflow.approval.mapper.ApprovalFlowStepMapper; +import tech.easyflow.approval.mapper.ApprovalInstanceMapper; +import tech.easyflow.approval.service.ApprovalAssigneeService; +import tech.easyflow.approval.service.ApprovalFlowService; + +import javax.annotation.Resource; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 审批流程服务实现。 + */ +@Service +public class ApprovalFlowServiceImpl extends ServiceImpl implements ApprovalFlowService { + + @Resource + private ApprovalFlowMapper approvalFlowMapper; + + @Resource + private ApprovalFlowScopeMapper approvalFlowScopeMapper; + + @Resource + private ApprovalFlowStepMapper approvalFlowStepMapper; + + @Resource + private ApprovalInstanceMapper approvalInstanceMapper; + + @Resource + private ApprovalAssigneeService approvalAssigneeService; + + /** + * {@inheritDoc} + */ + @Override + public Page pageFlows(String name, String resourceType, String actionType, String status, + Long pageNumber, Long pageSize) { + long actualPageNumber = pageNumber == null || pageNumber < 1 ? 1L : pageNumber; + long actualPageSize = pageSize == null || pageSize < 1 ? 10L : pageSize; + QueryWrapper queryWrapper = QueryWrapper.create(); + if (StringUtils.hasText(name)) { + queryWrapper.like(ApprovalFlow::getName, name.trim()); + } + if (StringUtils.hasText(resourceType)) { + queryWrapper.eq(ApprovalFlow::getResourceType, ApprovalResourceType.from(resourceType).getCode()); + } + if (StringUtils.hasText(actionType)) { + queryWrapper.eq(ApprovalFlow::getActionType, ApprovalActionType.from(actionType).getCode()); + } + if (StringUtils.hasText(status)) { + queryWrapper.eq(ApprovalFlow::getStatus, ApprovalFlowStatus.from(status).getCode()); + } + queryWrapper.orderBy("priority desc, modified desc, created asc"); + + Page entityPage = page(new Page<>(actualPageNumber, actualPageSize), queryWrapper); + List records = entityPage.getRecords(); + List flowIds = records.stream().map(ApprovalFlow::getId).collect(Collectors.toList()); + + Map> scopeMap = loadScopeMap(flowIds); + Map> stepMap = loadStepMap(flowIds); + Map pendingCountMap = loadPendingInstanceCountMap(flowIds); + + List result = new ArrayList<>(); + for (ApprovalFlow record : records) { + ApprovalFlowPageVo item = new ApprovalFlowPageVo(); + item.setId(record.getId()); + item.setName(record.getName()); + item.setResourceType(record.getResourceType()); + item.setActionType(record.getActionType()); + item.setPriority(record.getPriority()); + item.setStatus(record.getStatus()); + item.setVersion(record.getVersion()); + item.setModified(record.getModified()); + item.setPendingInstanceCount(pendingCountMap.getOrDefault(record.getId(), 0L)); + item.setStepCount(stepMap.getOrDefault(record.getId(), List.of()).size()); + item.setScopeSummary(buildScopeSummary(scopeMap.get(record.getId()))); + result.add(item); + } + + Page voPage = new Page<>(actualPageNumber, actualPageSize, entityPage.getTotalRow()); + voPage.setRecords(result); + return voPage; + } + + /** + * {@inheritDoc} + */ + @Override + public ApprovalFlowDetailVo getFlowDetail(BigInteger id) { + ApprovalFlow flow = requireFlow(id); + ApprovalFlowDetailVo detail = toDetailVo(flow); + detail.setPendingInstanceCount(countPendingInstances(id)); + detail.setDeletable(detail.getPendingInstanceCount() == 0); + detail.setScopes(loadScopeVos(id)); + detail.setSteps(loadStepVos(id)); + return detail; + } + + /** + * {@inheritDoc} + */ + @Override + @Transactional(rollbackFor = Exception.class) + public BigInteger saveFlow(ApprovalFlowDetailVo request, BigInteger operatorId) { + ApprovalFlowDetailVo normalized = normalizeRequest(request, true); + Date now = new Date(); + ApprovalFlow flow = new ApprovalFlow(); + flow.setName(normalized.getName()); + flow.setResourceType(normalized.getResourceType()); + flow.setActionType(normalized.getActionType()); + flow.setPriority(normalized.getPriority()); + flow.setStatus(normalized.getStatus()); + flow.setVersion(1); + flow.setRemark(normalized.getRemark()); + flow.setCreated(now); + flow.setCreatedBy(operatorId); + flow.setModified(now); + flow.setModifiedBy(operatorId); + approvalFlowMapper.insert(flow); + replaceChildren(flow.getId(), normalized, operatorId, now); + return flow.getId(); + } + + /** + * {@inheritDoc} + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void updateFlow(ApprovalFlowDetailVo request, BigInteger operatorId) { + ApprovalFlow existing = requireFlow(request == null ? null : request.getId()); + ApprovalFlowDetailVo normalized = normalizeRequest(request, false); + Date now = new Date(); + existing.setName(normalized.getName()); + existing.setResourceType(normalized.getResourceType()); + existing.setActionType(normalized.getActionType()); + existing.setPriority(normalized.getPriority()); + existing.setStatus(normalized.getStatus()); + existing.setVersion((existing.getVersion() == null ? 1 : existing.getVersion()) + 1); + existing.setRemark(normalized.getRemark()); + existing.setModified(now); + existing.setModifiedBy(operatorId); + approvalFlowMapper.update(existing); + clearChildren(existing.getId()); + replaceChildren(existing.getId(), normalized, operatorId, now); + } + + /** + * {@inheritDoc} + */ + @Override + public void enableFlow(BigInteger id, BigInteger operatorId) { + updateStatus(id, ApprovalFlowStatus.ENABLED.getCode(), operatorId); + } + + /** + * {@inheritDoc} + */ + @Override + public void disableFlow(BigInteger id, BigInteger operatorId) { + updateStatus(id, ApprovalFlowStatus.DISABLED.getCode(), operatorId); + } + + /** + * {@inheritDoc} + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void removeFlow(BigInteger id) { + requireFlow(id); + if (countPendingInstances(id) > 0) { + throw new BusinessException("存在未完成审批实例,当前流程不允许删除"); + } + clearChildren(id); + approvalFlowMapper.deleteById(id); + } + + private void updateStatus(BigInteger id, String status, BigInteger operatorId) { + ApprovalFlow existing = requireFlow(id); + if (ApprovalFlowStatus.ENABLED.getCode().equals(status)) { + validateStoredStepAssignees(id); + } + existing.setStatus(status); + existing.setModified(new Date()); + existing.setModifiedBy(operatorId); + approvalFlowMapper.update(existing); + } + + private ApprovalFlow requireFlow(BigInteger id) { + if (id == null) { + throw new BusinessException("流程ID不能为空"); + } + ApprovalFlow flow = approvalFlowMapper.selectOneById(id); + if (flow == null) { + throw new BusinessException("审批流程不存在"); + } + return flow; + } + + private ApprovalFlowDetailVo normalizeRequest(ApprovalFlowDetailVo request, boolean create) { + if (request == null) { + throw new BusinessException("审批流程不能为空"); + } + if (!create && request.getId() == null) { + throw new BusinessException("流程ID不能为空"); + } + if (!StringUtils.hasText(request.getName())) { + throw new BusinessException("流程名称不能为空"); + } + if (request.getPriority() == null) { + throw new BusinessException("优先级不能为空"); + } + ApprovalFlowDetailVo normalized = new ApprovalFlowDetailVo(); + normalized.setId(request.getId()); + normalized.setName(request.getName().trim()); + normalized.setResourceType(ApprovalResourceType.from(request.getResourceType()).getCode()); + normalized.setActionType(ApprovalActionType.from(request.getActionType()).getCode()); + normalized.setPriority(request.getPriority()); + normalized.setStatus(StringUtils.hasText(request.getStatus()) + ? ApprovalFlowStatus.from(request.getStatus()).getCode() + : ApprovalFlowStatus.ENABLED.getCode()); + normalized.setRemark(request.getRemark()); + normalized.setScopes(normalizeScopes(request.getScopes())); + normalized.setSteps(normalizeSteps(request.getSteps())); + return normalized; + } + + private List normalizeScopes(List scopes) { + if (CollectionUtil.isEmpty(scopes)) { + return new ArrayList<>(); + } + Set keys = new LinkedHashSet<>(); + List result = new ArrayList<>(); + for (ApprovalFlowScopeVo item : scopes) { + if (item == null || item.getScopeValue() == null || !StringUtils.hasText(item.getScopeType())) { + continue; + } + ApprovalFlowScopeVo normalized = new ApprovalFlowScopeVo(); + normalized.setScopeType(ApprovalScopeType.from(item.getScopeType()).getCode()); + normalized.setScopeValue(item.getScopeValue()); + normalized.setIncludeChildren(item.getIncludeChildren() != null && item.getIncludeChildren() == 1 ? 1 : 0); + String uniqueKey = normalized.getScopeType() + ":" + normalized.getScopeValue(); + if (keys.add(uniqueKey)) { + result.add(normalized); + } + } + return result; + } + + private List normalizeSteps(List steps) { + if (CollectionUtil.isEmpty(steps)) { + throw new BusinessException("审批步骤不能为空"); + } + List result = new ArrayList<>(); + int index = 1; + for (ApprovalFlowStepVo item : steps) { + if (item == null || !StringUtils.hasText(item.getStepName())) { + continue; + } + ApprovalFlowStepVo normalized = new ApprovalFlowStepVo(); + normalized.setStepNo(index++); + normalized.setStepName(item.getStepName().trim()); + normalized.setAssigneeType(item.getAssigneeType()); + normalized.setAssigneeTargetId(item.getAssigneeTargetId()); + approvalAssigneeService.normalizeStepAssignee(normalized); + result.add(normalized); + } + if (result.isEmpty()) { + throw new BusinessException("审批步骤不能为空"); + } + return result; + } + + private void clearChildren(BigInteger flowId) { + approvalFlowScopeMapper.deleteByQuery(QueryWrapper.create().eq(ApprovalFlowScope::getFlowId, flowId)); + approvalFlowStepMapper.deleteByQuery(QueryWrapper.create().eq(ApprovalFlowStep::getFlowId, flowId)); + } + + private void replaceChildren(BigInteger flowId, ApprovalFlowDetailVo request, BigInteger operatorId, Date now) { + for (ApprovalFlowScopeVo scopeVo : request.getScopes()) { + ApprovalFlowScope scope = new ApprovalFlowScope(); + scope.setFlowId(flowId); + scope.setScopeType(scopeVo.getScopeType()); + scope.setScopeValue(scopeVo.getScopeValue()); + scope.setIncludeChildren(scopeVo.getIncludeChildren()); + scope.setCreated(now); + scope.setCreatedBy(operatorId); + scope.setModified(now); + scope.setModifiedBy(operatorId); + approvalFlowScopeMapper.insert(scope); + } + for (ApprovalFlowStepVo stepVo : request.getSteps()) { + ApprovalFlowStep step = new ApprovalFlowStep(); + step.setFlowId(flowId); + step.setStepNo(stepVo.getStepNo()); + step.setStepName(stepVo.getStepName()); + step.setAssigneeType(stepVo.getAssigneeType()); + step.setAssigneeTargetId(stepVo.getAssigneeTargetId()); + step.setAssigneeTargetCode(stepVo.getAssigneeTargetCode()); + step.setAssigneeTargetName(stepVo.getAssigneeTargetName()); + step.setCreated(now); + step.setCreatedBy(operatorId); + step.setModified(now); + step.setModifiedBy(operatorId); + approvalFlowStepMapper.insert(step); + } + } + + private ApprovalFlowDetailVo toDetailVo(ApprovalFlow flow) { + ApprovalFlowDetailVo detail = new ApprovalFlowDetailVo(); + detail.setId(flow.getId()); + detail.setName(flow.getName()); + detail.setResourceType(flow.getResourceType()); + detail.setActionType(flow.getActionType()); + detail.setPriority(flow.getPriority()); + detail.setStatus(flow.getStatus()); + detail.setVersion(flow.getVersion()); + detail.setRemark(flow.getRemark()); + detail.setCreated(flow.getCreated()); + detail.setModified(flow.getModified()); + return detail; + } + + private List loadScopeVos(BigInteger flowId) { + return approvalFlowScopeMapper.selectListByQuery(QueryWrapper.create().eq(ApprovalFlowScope::getFlowId, flowId)) + .stream() + .sorted(Comparator.comparing(ApprovalFlowScope::getScopeType).thenComparing(ApprovalFlowScope::getScopeValue)) + .map(item -> { + ApprovalFlowScopeVo scopeVo = new ApprovalFlowScopeVo(); + scopeVo.setId(item.getId()); + scopeVo.setScopeType(item.getScopeType()); + scopeVo.setScopeValue(item.getScopeValue()); + scopeVo.setIncludeChildren(item.getIncludeChildren()); + return scopeVo; + }) + .collect(Collectors.toList()); + } + + private List loadStepVos(BigInteger flowId) { + return approvalFlowStepMapper.selectListByQuery(QueryWrapper.create().eq(ApprovalFlowStep::getFlowId, flowId)) + .stream() + .sorted(Comparator.comparing(ApprovalFlowStep::getStepNo)) + .map(item -> { + ApprovalFlowStepVo stepVo = new ApprovalFlowStepVo(); + stepVo.setId(item.getId()); + stepVo.setStepNo(item.getStepNo()); + stepVo.setStepName(item.getStepName()); + stepVo.setAssigneeType(item.getAssigneeType()); + stepVo.setAssigneeTargetId(item.getAssigneeTargetId()); + stepVo.setAssigneeTargetCode(item.getAssigneeTargetCode()); + stepVo.setAssigneeTargetName(item.getAssigneeTargetName()); + return stepVo; + }) + .collect(Collectors.toList()); + } + + /** + * 校验数据库中已保存步骤的审批对象仍然有效。 + * + * @param flowId 流程ID + */ + private void validateStoredStepAssignees(BigInteger flowId) { + List steps = loadStepVos(flowId); + if (CollectionUtil.isEmpty(steps)) { + throw new BusinessException("审批步骤不能为空"); + } + for (ApprovalFlowStepVo step : steps) { + approvalAssigneeService.normalizeStepAssignee(step); + } + } + + private long countPendingInstances(BigInteger flowId) { + QueryWrapper queryWrapper = QueryWrapper.create() + .eq(ApprovalInstance::getFlowId, flowId) + .in(ApprovalInstance::getStatus, List.of( + ApprovalInstanceStatus.PENDING.getCode(), + ApprovalInstanceStatus.PROCESSING.getCode())); + return approvalInstanceMapper.selectCountByQuery(queryWrapper); + } + + private Map loadPendingInstanceCountMap(List flowIds) { + if (CollectionUtil.isEmpty(flowIds)) { + return Map.of(); + } + QueryWrapper queryWrapper = QueryWrapper.create() + .in(ApprovalInstance::getFlowId, flowIds) + .in(ApprovalInstance::getStatus, List.of( + ApprovalInstanceStatus.PENDING.getCode(), + ApprovalInstanceStatus.PROCESSING.getCode())); + List instances = approvalInstanceMapper.selectListByQuery(queryWrapper); + return instances.stream().collect(Collectors.groupingBy(ApprovalInstance::getFlowId, Collectors.counting())); + } + + private Map> loadScopeMap(List flowIds) { + if (CollectionUtil.isEmpty(flowIds)) { + return Map.of(); + } + List scopes = approvalFlowScopeMapper.selectListByQuery( + QueryWrapper.create().in(ApprovalFlowScope::getFlowId, flowIds)); + return scopes.stream().collect(Collectors.groupingBy( + ApprovalFlowScope::getFlowId, LinkedHashMap::new, Collectors.toList())); + } + + private Map> loadStepMap(List flowIds) { + if (CollectionUtil.isEmpty(flowIds)) { + return Map.of(); + } + List steps = approvalFlowStepMapper.selectListByQuery( + QueryWrapper.create().in(ApprovalFlowStep::getFlowId, flowIds)); + return steps.stream().collect(Collectors.groupingBy( + ApprovalFlowStep::getFlowId, LinkedHashMap::new, Collectors.toList())); + } + + private String buildScopeSummary(List scopes) { + if (CollectionUtil.isEmpty(scopes)) { + return "全部"; + } + long categoryCount = scopes.stream() + .filter(item -> ApprovalScopeType.CATEGORY.getCode().equals(item.getScopeType())) + .count(); + long deptCount = scopes.stream() + .filter(item -> ApprovalScopeType.DEPT.getCode().equals(item.getScopeType())) + .count(); + List parts = new ArrayList<>(); + if (categoryCount > 0) { + parts.add("分类 " + categoryCount); + } + if (deptCount > 0) { + parts.add("部门 " + deptCount); + } + return String.join(" / ", parts); + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalInstanceServiceImpl.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalInstanceServiceImpl.java new file mode 100644 index 0000000..30b5a8a --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalInstanceServiceImpl.java @@ -0,0 +1,431 @@ +package tech.easyflow.approval.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import com.mybatisflex.core.query.QueryWrapper; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import tech.easyflow.common.web.exceptions.BusinessException; +import tech.easyflow.approval.entity.ApprovalInstance; +import tech.easyflow.approval.entity.ApprovalFlowStep; +import tech.easyflow.approval.entity.ApprovalLog; +import tech.easyflow.approval.entity.ApprovalTask; +import tech.easyflow.approval.entity.vo.ApprovalFlowDetailVo; +import tech.easyflow.approval.entity.vo.ApprovalFlowStepVo; +import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest; +import tech.easyflow.approval.service.ApprovalActionFacade; +import tech.easyflow.approval.service.ApprovalAssigneeService; +import tech.easyflow.approval.enums.ApprovalAssigneeType; +import tech.easyflow.approval.enums.ApprovalEventType; +import tech.easyflow.approval.enums.ApprovalInstanceStatus; +import tech.easyflow.approval.enums.ApprovalTaskStatus; +import tech.easyflow.approval.mapper.ApprovalFlowStepMapper; +import tech.easyflow.approval.mapper.ApprovalInstanceMapper; +import tech.easyflow.approval.mapper.ApprovalLogMapper; +import tech.easyflow.approval.mapper.ApprovalTaskMapper; +import tech.easyflow.approval.service.ApprovalInstanceService; +import tech.easyflow.approval.service.ApprovalMatchService; + +import javax.annotation.Resource; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 审批实例服务实现。 + */ +@Service +public class ApprovalInstanceServiceImpl implements ApprovalInstanceService { + + @Resource + private ApprovalMatchService approvalMatchService; + + @Resource + private ApprovalInstanceMapper approvalInstanceMapper; + + @Resource + private ApprovalTaskMapper approvalTaskMapper; + + @Resource + private ApprovalLogMapper approvalLogMapper; + + @Resource + private ApprovalFlowStepMapper approvalFlowStepMapper; + + @Resource + private ApprovalAssigneeService approvalAssigneeService; + + @Lazy + @Resource + private ApprovalActionFacade approvalActionFacade; + + /** + * {@inheritDoc} + */ + @Override + @Transactional(rollbackFor = Exception.class) + public BigInteger submitApproval(ApprovalSubmitRequest request) { + ApprovalFlowDetailVo flow = approvalMatchService.matchFlow(request); + if (CollectionUtil.isEmpty(flow.getSteps())) { + throw new BusinessException("审批流程未配置步骤"); + } + if (request.getApplicantId() == null) { + throw new BusinessException("申请人不能为空"); + } + + List steps = new ArrayList<>(flow.getSteps()); + steps.sort(Comparator.comparing(ApprovalFlowStepVo::getStepNo)); + ApprovalFlowStepVo firstStep = steps.get(0); + Date now = new Date(); + + ApprovalInstance instance = new ApprovalInstance(); + instance.setFlowId(flow.getId()); + instance.setFlowVersion(flow.getVersion()); + instance.setResourceType(flow.getResourceType()); + instance.setResourceId(request.getResourceId()); + instance.setActionType(flow.getActionType()); + instance.setStatus(ApprovalInstanceStatus.PENDING.getCode()); + instance.setCurrentStepNo(firstStep.getStepNo()); + instance.setSnapshotJson(buildInstanceSnapshot(request, flow, steps)); + instance.setSummary(request.getSummary()); + instance.setApplicantId(request.getApplicantId()); + instance.setSubmittedAt(now); + instance.setCreated(now); + instance.setCreatedBy(request.getApplicantId()); + instance.setModified(now); + instance.setModifiedBy(request.getApplicantId()); + approvalInstanceMapper.insert(instance); + + createTask(instance.getId(), firstStep, request.getApplicantId(), now); + appendLog(instance.getId(), ApprovalEventType.SUBMITTED.getCode(), request.getApplicantId(), Map.of( + "flowId", flow.getId(), + "flowVersion", flow.getVersion(), + "summary", request.getSummary() + ), now); + appendLog(instance.getId(), ApprovalEventType.STEP_CREATED.getCode(), request.getApplicantId(), + buildStepCreatedPayload(firstStep), now); + return instance.getId(); + } + + /** + * {@inheritDoc} + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void approve(BigInteger instanceId, String comment, BigInteger operatorId) { + ApprovalInstance instance = requireActiveInstance(instanceId); + ApprovalTask currentTask = requireCurrentTask(instanceId, instance.getCurrentStepNo()); + assertTaskOperable(currentTask, operatorId); + List steps = resolveFrozenSteps(instance); + + Date now = new Date(); + finishTask(currentTask, ApprovalTaskStatus.APPROVED.getCode(), comment, operatorId, now); + int currentIndex = findCurrentStepIndex(steps, instance.getCurrentStepNo()); + if (currentIndex == steps.size() - 1) { + instance.setStatus(ApprovalInstanceStatus.APPROVED.getCode()); + instance.setFinishedAt(now); + } else { + ApprovalFlowStepVo nextStep = steps.get(currentIndex + 1); + instance.setStatus(ApprovalInstanceStatus.PROCESSING.getCode()); + instance.setCurrentStepNo(nextStep.getStepNo()); + createTask(instance.getId(), nextStep, operatorId, now); + appendLog(instance.getId(), ApprovalEventType.STEP_CREATED.getCode(), operatorId, + buildStepCreatedPayload(nextStep), now); + } + instance.setModified(now); + instance.setModifiedBy(operatorId); + approvalInstanceMapper.update(instance); + appendLog(instance.getId(), ApprovalEventType.APPROVED.getCode(), operatorId, Map.of( + "stepNo", currentTask.getStepNo(), + "comment", comment == null ? "" : comment + ), now); + if (ApprovalInstanceStatus.from(instance.getStatus()).isFinished()) { + approvalActionFacade.handleApproved(instance, operatorId, comment); + } + } + + /** + * {@inheritDoc} + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void reject(BigInteger instanceId, String comment, BigInteger operatorId) { + ApprovalInstance instance = requireActiveInstance(instanceId); + ApprovalTask currentTask = requireCurrentTask(instanceId, instance.getCurrentStepNo()); + assertTaskOperable(currentTask, operatorId); + Date now = new Date(); + finishTask(currentTask, ApprovalTaskStatus.REJECTED.getCode(), comment, operatorId, now); + instance.setStatus(ApprovalInstanceStatus.REJECTED.getCode()); + instance.setFinishedAt(now); + instance.setModified(now); + instance.setModifiedBy(operatorId); + approvalInstanceMapper.update(instance); + appendLog(instance.getId(), ApprovalEventType.REJECTED.getCode(), operatorId, Map.of( + "stepNo", currentTask.getStepNo(), + "comment", comment == null ? "" : comment + ), now); + approvalActionFacade.handleRejected(instance, operatorId, comment); + } + + /** + * {@inheritDoc} + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void revoke(BigInteger instanceId, String comment, BigInteger operatorId) { + ApprovalInstance instance = requireActiveInstance(instanceId); + ApprovalTask currentTask = requireCurrentTask(instanceId, instance.getCurrentStepNo()); + assertTaskOperable(currentTask, operatorId); + Date now = new Date(); + finishTask(currentTask, ApprovalTaskStatus.REVOKED.getCode(), comment, operatorId, now); + instance.setStatus(ApprovalInstanceStatus.REVOKED.getCode()); + instance.setFinishedAt(now); + instance.setModified(now); + instance.setModifiedBy(operatorId); + approvalInstanceMapper.update(instance); + appendLog(instance.getId(), ApprovalEventType.REVOKED.getCode(), operatorId, Map.of( + "stepNo", currentTask.getStepNo(), + "comment", comment == null ? "" : comment + ), now); + approvalActionFacade.handleRevoked(instance, operatorId, comment); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean existsActiveInstance(String resourceType, BigInteger resourceId) { + QueryWrapper queryWrapper = QueryWrapper.create() + .eq(ApprovalInstance::getResourceType, resourceType) + .eq(ApprovalInstance::getResourceId, resourceId) + .notIn(ApprovalInstance::getStatus, + ApprovalInstanceStatus.APPROVED.getCode(), + ApprovalInstanceStatus.REJECTED.getCode(), + ApprovalInstanceStatus.REVOKED.getCode()); + return approvalInstanceMapper.selectCountByQuery(queryWrapper) > 0; + } + + /** + * {@inheritDoc} + */ + @Override + public ApprovalInstance getById(BigInteger instanceId) { + return approvalInstanceMapper.selectOneById(instanceId); + } + + private Map buildInstanceSnapshot(ApprovalSubmitRequest request, ApprovalFlowDetailVo flow, + List steps) { + Map snapshot = new LinkedHashMap<>(); + if (request.getSnapshotJson() != null) { + snapshot.putAll(request.getSnapshotJson()); + } + snapshot.put("categoryId", request.getCategoryId()); + snapshot.put("deptId", request.getDeptId()); + snapshot.put("flowId", flow.getId()); + snapshot.put("flowVersion", flow.getVersion()); + snapshot.put("steps", steps.stream().map(item -> { + Map map = new LinkedHashMap<>(); + map.put("stepNo", item.getStepNo()); + map.put("stepName", item.getStepName()); + map.put("assigneeType", item.getAssigneeType()); + map.put("assigneeTargetId", item.getAssigneeTargetId()); + map.put("assigneeTargetCode", item.getAssigneeTargetCode()); + map.put("assigneeTargetName", item.getAssigneeTargetName()); + return map; + }).collect(Collectors.toList())); + return snapshot; + } + + private List resolveFrozenSteps(ApprovalInstance instance) { + if (instance.getSnapshotJson() == null) { + throw new BusinessException("审批实例缺少流程快照"); + } + Object value = instance.getSnapshotJson().get("steps"); + if (!(value instanceof List steps)) { + throw new BusinessException("审批实例缺少冻结步骤数据"); + } + List result = new ArrayList<>(); + for (Object step : steps) { + if (!(step instanceof Map stepMap)) { + continue; + } + Object stepNo = stepMap.get("stepNo"); + Object stepName = stepMap.get("stepName"); + if (!(stepNo instanceof Number) || stepName == null) { + continue; + } + ApprovalFlowStepVo stepVo = new ApprovalFlowStepVo(); + stepVo.setStepNo(((Number) stepNo).intValue()); + stepVo.setStepName(String.valueOf(stepName)); + Object assigneeType = stepMap.get("assigneeType"); + Object assigneeTargetId = stepMap.get("assigneeTargetId"); + Object assigneeTargetCode = stepMap.get("assigneeTargetCode"); + Object assigneeTargetName = stepMap.get("assigneeTargetName"); + if (assigneeType instanceof String type) { + stepVo.setAssigneeType(type); + } + if (assigneeTargetId instanceof Number number) { + stepVo.setAssigneeTargetId(BigInteger.valueOf(number.longValue())); + } else if (assigneeTargetId instanceof String string && !string.isBlank()) { + stepVo.setAssigneeTargetId(new BigInteger(string)); + } + if (assigneeTargetCode != null) { + stepVo.setAssigneeTargetCode(String.valueOf(assigneeTargetCode)); + } + if (assigneeTargetName != null) { + stepVo.setAssigneeTargetName(String.valueOf(assigneeTargetName)); + } + result.add(stepVo); + } + mergeStepAssigneeFromFlow(instance.getFlowId(), result); + result.sort(Comparator.comparing(ApprovalFlowStepVo::getStepNo)); + if (result.isEmpty()) { + throw new BusinessException("审批实例缺少冻结步骤数据"); + } + return result; + } + + private void createTask(BigInteger instanceId, ApprovalFlowStepVo step, BigInteger operatorId, Date now) { + ApprovalTask task = new ApprovalTask(); + task.setInstanceId(instanceId); + task.setStepNo(step.getStepNo()); + task.setStatus(ApprovalTaskStatus.PENDING.getCode()); + task.setAssigneeRoleCode(ApprovalAssigneeType.ROLE.getCode().equals(step.getAssigneeType()) + ? step.getAssigneeTargetCode() + : null); + task.setAssigneeType(step.getAssigneeType()); + task.setAssigneeTargetId(step.getAssigneeTargetId()); + task.setAssigneeTargetCode(step.getAssigneeTargetCode()); + task.setAssigneeTargetName(step.getAssigneeTargetName()); + task.setCreated(now); + task.setCreatedBy(operatorId); + task.setModified(now); + task.setModifiedBy(operatorId); + approvalTaskMapper.insert(task); + } + + private void finishTask(ApprovalTask task, String status, String comment, BigInteger operatorId, Date now) { + task.setStatus(status); + task.setComment(comment); + task.setActedBy(operatorId); + task.setActedAt(now); + task.setModified(now); + task.setModifiedBy(operatorId); + approvalTaskMapper.update(task); + } + + private void appendLog(BigInteger instanceId, String eventType, BigInteger operatorId, Map payload, Date now) { + ApprovalLog log = new ApprovalLog(); + log.setInstanceId(instanceId); + log.setEventType(eventType); + log.setOperatorId(operatorId); + log.setPayloadJson(new LinkedHashMap<>(payload)); + log.setCreated(now); + log.setCreatedBy(operatorId); + log.setModified(now); + log.setModifiedBy(operatorId); + approvalLogMapper.insert(log); + } + + private ApprovalInstance requireActiveInstance(BigInteger instanceId) { + if (instanceId == null) { + throw new BusinessException("审批实例ID不能为空"); + } + ApprovalInstance instance = approvalInstanceMapper.selectOneById(instanceId); + if (instance == null) { + throw new BusinessException("审批实例不存在"); + } + if (ApprovalInstanceStatus.from(instance.getStatus()).isFinished()) { + throw new BusinessException("审批实例已结束,无法继续处理"); + } + return instance; + } + + private ApprovalTask requireCurrentTask(BigInteger instanceId, Integer stepNo) { + QueryWrapper queryWrapper = QueryWrapper.create() + .eq(ApprovalTask::getInstanceId, instanceId) + .eq(ApprovalTask::getStepNo, stepNo) + .eq(ApprovalTask::getStatus, ApprovalTaskStatus.PENDING.getCode()); + ApprovalTask task = approvalTaskMapper.selectOneByQuery(queryWrapper); + if (task == null) { + throw new BusinessException("当前审批任务不存在"); + } + return task; + } + + private int findCurrentStepIndex(List steps, Integer currentStepNo) { + for (int i = 0; i < steps.size(); i++) { + if (steps.get(i).getStepNo().equals(currentStepNo)) { + return i; + } + } + throw new BusinessException("审批流程步骤不存在"); + } + + /** + * 校验当前操作人是否命中审批任务。 + * + * @param task 审批任务 + * @param operatorId 当前操作人 + */ + private void assertTaskOperable(ApprovalTask task, BigInteger operatorId) { + if (!approvalAssigneeService.canHandleTask(task, operatorId, approvalAssigneeService.getAvailableRoleIds(operatorId))) { + throw new BusinessException("当前用户无权处理该审批任务"); + } + } + + /** + * 为历史审批实例补齐流程步骤里的审批对象信息。 + * + * @param flowId 流程ID + * @param steps 冻结步骤 + */ + private void mergeStepAssigneeFromFlow(BigInteger flowId, List steps) { + List storedSteps = approvalFlowStepMapper.selectListByQuery( + QueryWrapper.create().eq(ApprovalFlowStep::getFlowId, flowId)); + if (CollectionUtil.isEmpty(storedSteps)) { + return; + } + Map storedMap = storedSteps.stream().collect(Collectors.toMap( + ApprovalFlowStep::getStepNo, + item -> item, + (left, right) -> left, + LinkedHashMap::new + )); + for (ApprovalFlowStepVo step : steps) { + if (step.getAssigneeTargetId() != null && step.getAssigneeType() != null) { + continue; + } + ApprovalFlowStep storedStep = storedMap.get(step.getStepNo()); + if (storedStep == null) { + continue; + } + step.setAssigneeType(storedStep.getAssigneeType()); + step.setAssigneeTargetId(storedStep.getAssigneeTargetId()); + step.setAssigneeTargetCode(storedStep.getAssigneeTargetCode()); + step.setAssigneeTargetName(storedStep.getAssigneeTargetName()); + } + } + + /** + * 组装步骤创建日志载荷。 + * + * @param step 步骤信息 + * @return 日志载荷 + */ + private Map buildStepCreatedPayload(ApprovalFlowStepVo step) { + Map payload = new LinkedHashMap<>(); + payload.put("stepNo", step.getStepNo()); + payload.put("stepName", step.getStepName()); + payload.put("assigneeType", step.getAssigneeType()); + payload.put("assigneeTargetId", step.getAssigneeTargetId()); + payload.put("assigneeTargetCode", step.getAssigneeTargetCode()); + payload.put("assigneeTargetName", step.getAssigneeTargetName()); + return payload; + } +} 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 new file mode 100644 index 0000000..b1a3431 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalMatchServiceImpl.java @@ -0,0 +1,193 @@ +package tech.easyflow.approval.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import com.mybatisflex.core.query.QueryWrapper; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import tech.easyflow.common.web.exceptions.BusinessException; +import tech.easyflow.approval.entity.ApprovalFlow; +import tech.easyflow.approval.entity.ApprovalFlowScope; +import tech.easyflow.approval.entity.vo.ApprovalFlowDetailVo; +import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest; +import tech.easyflow.approval.enums.ApprovalActionType; +import tech.easyflow.approval.enums.ApprovalFlowStatus; +import tech.easyflow.approval.enums.ApprovalResourceType; +import tech.easyflow.approval.enums.ApprovalScopeType; +import tech.easyflow.approval.mapper.ApprovalFlowMapper; +import tech.easyflow.approval.mapper.ApprovalFlowScopeMapper; +import tech.easyflow.approval.service.ApprovalFlowService; +import tech.easyflow.approval.service.ApprovalMatchService; +import tech.easyflow.system.entity.SysDept; +import tech.easyflow.system.service.SysDeptService; + +import javax.annotation.Resource; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 审批流程匹配服务实现。 + */ +@Service +public class ApprovalMatchServiceImpl implements ApprovalMatchService { + + @Resource + private ApprovalFlowMapper approvalFlowMapper; + + @Resource + private ApprovalFlowScopeMapper approvalFlowScopeMapper; + + @Resource + private ApprovalFlowService approvalFlowService; + + @Resource + private SysDeptService sysDeptService; + + /** + * {@inheritDoc} + */ + @Override + public ApprovalFlowDetailVo matchFlow(ApprovalSubmitRequest request) { + ApprovalSubmitRequest normalized = normalizeRequest(request); + QueryWrapper flowWrapper = QueryWrapper.create() + .eq(ApprovalFlow::getResourceType, normalized.getResourceType()) + .eq(ApprovalFlow::getActionType, normalized.getActionType()) + .eq(ApprovalFlow::getStatus, ApprovalFlowStatus.ENABLED.getCode()); + List flows = approvalFlowMapper.selectListByQuery(flowWrapper); + if (CollectionUtil.isEmpty(flows)) { + throw new BusinessException("未找到可用的审批流程"); + } + List flowIds = flows.stream().map(ApprovalFlow::getId).collect(Collectors.toList()); + Map> scopeMap = approvalFlowScopeMapper.selectListByQuery( + QueryWrapper.create().in(ApprovalFlowScope::getFlowId, flowIds)) + .stream() + .collect(Collectors.groupingBy(ApprovalFlowScope::getFlowId, LinkedHashMap::new, Collectors.toList())); + + List matchedFlows = new ArrayList<>(); + for (ApprovalFlow flow : flows) { + List scopes = scopeMap.getOrDefault(flow.getId(), List.of()); + if (matches(scopes, normalized)) { + matchedFlows.add(new MatchedFlow(flow, computeSpecificity(scopes))); + } + } + if (matchedFlows.isEmpty()) { + throw new BusinessException("当前资源上下文未命中审批流程"); + } + + matchedFlows.sort(Comparator + .comparing((MatchedFlow item) -> item.flow.getPriority(), Comparator.nullsLast(Integer::compareTo)).reversed() + .thenComparing(MatchedFlow::getSpecificity, Comparator.reverseOrder()) + .thenComparing(item -> item.flow.getCreated(), Comparator.nullsLast(Comparator.naturalOrder()))); + + MatchedFlow first = matchedFlows.get(0); + if (matchedFlows.size() > 1) { + MatchedFlow second = matchedFlows.get(1); + boolean samePriority = Objects.equals(first.flow.getPriority(), second.flow.getPriority()); + boolean sameSpecificity = Objects.equals(first.specificity, second.specificity); + boolean sameCreated = Objects.equals(first.flow.getCreated(), second.flow.getCreated()); + if (samePriority && sameSpecificity && sameCreated) { + throw new BusinessException("审批流程匹配冲突,请调整优先级或范围配置"); + } + } + return approvalFlowService.getFlowDetail(first.flow.getId()); + } + + private ApprovalSubmitRequest normalizeRequest(ApprovalSubmitRequest request) { + if (request == null) { + throw new BusinessException("审批请求不能为空"); + } + if (request.getResourceId() == null) { + throw new BusinessException("资源ID不能为空"); + } + ApprovalSubmitRequest normalized = new ApprovalSubmitRequest(); + normalized.setResourceType(ApprovalResourceType.from(request.getResourceType()).getCode()); + normalized.setActionType(ApprovalActionType.from(request.getActionType()).getCode()); + normalized.setResourceId(request.getResourceId()); + normalized.setApplicantId(request.getApplicantId()); + normalized.setCategoryId(request.getCategoryId()); + normalized.setDeptId(request.getDeptId()); + normalized.setSummary(request.getSummary()); + normalized.setSnapshotJson(request.getSnapshotJson()); + return normalized; + } + + private boolean matches(List scopes, ApprovalSubmitRequest request) { + List categoryScopes = scopes.stream() + .filter(item -> ApprovalScopeType.CATEGORY.getCode().equals(item.getScopeType())) + .collect(Collectors.toList()); + List deptScopes = scopes.stream() + .filter(item -> ApprovalScopeType.DEPT.getCode().equals(item.getScopeType())) + .collect(Collectors.toList()); + return matchesCategory(categoryScopes, request.getCategoryId()) && matchesDept(deptScopes, request.getDeptId()); + } + + private boolean matchesCategory(List scopes, BigInteger categoryId) { + if (CollectionUtil.isEmpty(scopes)) { + return true; + } + if (categoryId == null) { + return false; + } + return scopes.stream().anyMatch(item -> categoryId.equals(item.getScopeValue())); + } + + private boolean matchesDept(List scopes, BigInteger deptId) { + if (CollectionUtil.isEmpty(scopes)) { + return true; + } + if (deptId == null) { + return false; + } + SysDept dept = sysDeptService.getById(deptId); + String ancestors = dept == null ? "" : dept.getAncestors(); + List ancestorIds = new ArrayList<>(); + if (StringUtils.hasText(ancestors)) { + for (String ancestor : ancestors.split(",")) { + if (!StringUtils.hasText(ancestor) || "0".equals(ancestor.trim())) { + continue; + } + ancestorIds.add(new BigInteger(ancestor.trim())); + } + } + return scopes.stream().anyMatch(item -> { + if (deptId.equals(item.getScopeValue())) { + return true; + } + return item.getIncludeChildren() != null + && item.getIncludeChildren() == 1 + && ancestorIds.contains(item.getScopeValue()); + }); + } + + private int computeSpecificity(List scopes) { + int score = scopes.size(); + boolean hasCategory = scopes.stream().anyMatch(item -> ApprovalScopeType.CATEGORY.getCode().equals(item.getScopeType())); + boolean hasDept = scopes.stream().anyMatch(item -> ApprovalScopeType.DEPT.getCode().equals(item.getScopeType())); + if (hasCategory) { + score += 10; + } + if (hasDept) { + score += 10; + } + return score; + } + + private static class MatchedFlow { + private final ApprovalFlow flow; + private final Integer specificity; + + private MatchedFlow(ApprovalFlow flow, Integer specificity) { + this.flow = flow; + this.specificity = specificity; + } + + public Integer getSpecificity() { + return specificity; + } + } +} 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 new file mode 100644 index 0000000..3913e82 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalQueryServiceImpl.java @@ -0,0 +1,402 @@ +package tech.easyflow.approval.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import com.mybatisflex.core.paginate.Page; +import com.mybatisflex.core.query.QueryWrapper; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import tech.easyflow.common.entity.LoginAccount; +import tech.easyflow.common.satoken.util.SaTokenUtil; +import tech.easyflow.common.web.exceptions.BusinessException; +import tech.easyflow.approval.entity.ApprovalFlowStep; +import tech.easyflow.approval.entity.ApprovalInstance; +import tech.easyflow.approval.entity.ApprovalLog; +import tech.easyflow.approval.entity.ApprovalTask; +import tech.easyflow.approval.entity.vo.ApprovalInstanceDetailVo; +import tech.easyflow.approval.entity.vo.ApprovalInstancePageVo; +import tech.easyflow.approval.entity.vo.ApprovalFlowStepVo; +import tech.easyflow.approval.entity.vo.ApprovalLogVo; +import tech.easyflow.approval.entity.vo.ApprovalTaskVo; +import tech.easyflow.approval.enums.ApprovalActionType; +import tech.easyflow.approval.enums.ApprovalInstanceStatus; +import tech.easyflow.approval.enums.ApprovalResourceType; +import tech.easyflow.approval.enums.ApprovalTaskStatus; +import tech.easyflow.approval.mapper.ApprovalFlowStepMapper; +import tech.easyflow.approval.mapper.ApprovalInstanceMapper; +import tech.easyflow.approval.mapper.ApprovalLogMapper; +import tech.easyflow.approval.mapper.ApprovalTaskMapper; +import tech.easyflow.approval.service.ApprovalAssigneeService; +import tech.easyflow.approval.service.ApprovalQueryService; +import tech.easyflow.system.entity.SysAccount; +import tech.easyflow.system.service.SysAccountService; + +import javax.annotation.Resource; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 审批查询服务实现。 + */ +@Service +public class ApprovalQueryServiceImpl implements ApprovalQueryService { + + @Resource + private ApprovalInstanceMapper approvalInstanceMapper; + + @Resource + private ApprovalTaskMapper approvalTaskMapper; + + @Resource + private ApprovalLogMapper approvalLogMapper; + + @Resource + private ApprovalFlowStepMapper approvalFlowStepMapper; + + @Resource + private ApprovalAssigneeService approvalAssigneeService; + + @Resource + private SysAccountService sysAccountService; + + /** + * {@inheritDoc} + */ + @Override + public Page pendingPage(String resourceType, String actionType, String keyword, + Long pageNumber, Long pageSize) { + LoginAccount account = requireLoginAccount(); + Set roleIds = approvalAssigneeService.getAvailableRoleIds(account.getId()); + Set instanceIds = approvalAssigneeService.listPendingInstanceIds(account.getId(), roleIds, null); + if (CollectionUtil.isEmpty(instanceIds)) { + return new Page<>(List.of(), safePageNumber(pageNumber), safePageSize(pageSize), 0L); + } + QueryWrapper queryWrapper = buildBaseQuery(resourceType, actionType, keyword); + queryWrapper.in(ApprovalInstance::getId, instanceIds); + queryWrapper.in(ApprovalInstance::getStatus, List.of( + ApprovalInstanceStatus.PENDING.getCode(), + ApprovalInstanceStatus.PROCESSING.getCode())); + queryWrapper.orderBy("submitted_at desc, id desc"); + return mapPage(queryWrapper, safePageNumber(pageNumber), safePageSize(pageSize), true, account, roleIds); + } + + /** + * {@inheritDoc} + */ + @Override + public Page processedPage(String resourceType, String actionType, String keyword, + Long pageNumber, Long pageSize) { + LoginAccount account = requireLoginAccount(); + QueryWrapper actedTaskWrapper = QueryWrapper.create() + .eq(ApprovalTask::getActedBy, account.getId()) + .in(ApprovalTask::getStatus, List.of( + ApprovalTaskStatus.APPROVED.getCode(), + ApprovalTaskStatus.REJECTED.getCode(), + ApprovalTaskStatus.REVOKED.getCode())); + List instanceIds = approvalTaskMapper.selectListByQuery(actedTaskWrapper).stream() + .map(ApprovalTask::getInstanceId) + .distinct() + .collect(Collectors.toList()); + if (CollectionUtil.isEmpty(instanceIds)) { + return new Page<>(List.of(), safePageNumber(pageNumber), safePageSize(pageSize), 0L); + } + QueryWrapper queryWrapper = buildBaseQuery(resourceType, actionType, keyword); + queryWrapper.in(ApprovalInstance::getId, instanceIds); + queryWrapper.orderBy("finished_at desc, id desc"); + return mapPage(queryWrapper, safePageNumber(pageNumber), safePageSize(pageSize), false, account, Set.of()); + } + + /** + * {@inheritDoc} + */ + @Override + public Page initiatedPage(String resourceType, String actionType, String keyword, + Long pageNumber, Long pageSize) { + LoginAccount account = requireLoginAccount(); + QueryWrapper queryWrapper = buildBaseQuery(resourceType, actionType, keyword); + queryWrapper.eq(ApprovalInstance::getApplicantId, account.getId()); + queryWrapper.orderBy("submitted_at desc, id desc"); + return mapPage(queryWrapper, safePageNumber(pageNumber), safePageSize(pageSize), false, account, Set.of()); + } + + /** + * {@inheritDoc} + */ + @Override + public ApprovalInstanceDetailVo detail(BigInteger instanceId) { + ApprovalInstance instance = approvalInstanceMapper.selectOneById(instanceId); + if (instance == null) { + throw new BusinessException("审批实例不存在"); + } + ApprovalInstanceDetailVo detail = new ApprovalInstanceDetailVo(); + detail.setId(instance.getId()); + detail.setFlowId(instance.getFlowId()); + detail.setFlowVersion(instance.getFlowVersion()); + detail.setResourceType(instance.getResourceType()); + detail.setResourceId(instance.getResourceId()); + detail.setActionType(instance.getActionType()); + detail.setStatus(instance.getStatus()); + detail.setCurrentStepNo(instance.getCurrentStepNo()); + detail.setSummary(instance.getSummary()); + detail.setApplicantId(instance.getApplicantId()); + detail.setSubmittedAt(instance.getSubmittedAt()); + detail.setFinishedAt(instance.getFinishedAt()); + detail.setSnapshotJson(instance.getSnapshotJson()); + + List tasks = approvalTaskMapper.selectListByQuery( + QueryWrapper.create().eq(ApprovalTask::getInstanceId, instanceId)); + 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())); + + detail.setTasks(tasks.stream() + .sorted(Comparator.comparing(ApprovalTask::getStepNo)) + .map(item -> { + ApprovalTaskVo taskVo = new ApprovalTaskVo(); + taskVo.setId(item.getId()); + taskVo.setStepNo(item.getStepNo()); + taskVo.setStepName(resolveStepName(frozenStepMap, item.getStepNo())); + taskVo.setStatus(item.getStatus()); + taskVo.setAssigneeRoleCode(item.getAssigneeRoleCode()); + taskVo.setAssigneeType(item.getAssigneeType()); + taskVo.setAssigneeTargetId(item.getAssigneeTargetId()); + taskVo.setAssigneeTargetCode(item.getAssigneeTargetCode()); + taskVo.setAssigneeTargetName(item.getAssigneeTargetName()); + taskVo.setActedBy(item.getActedBy()); + taskVo.setActedByName(accountNameMap.get(item.getActedBy())); + taskVo.setActedAt(item.getActedAt()); + taskVo.setComment(item.getComment()); + return taskVo; + }) + .collect(Collectors.toList())); + + detail.setLogs(logs.stream() + .sorted(Comparator.comparing(ApprovalLog::getCreated)) + .map(item -> { + ApprovalLogVo logVo = new ApprovalLogVo(); + logVo.setId(item.getId()); + logVo.setEventType(item.getEventType()); + logVo.setOperatorId(item.getOperatorId()); + logVo.setOperatorName(accountNameMap.get(item.getOperatorId())); + logVo.setCreated(item.getCreated()); + logVo.setPayloadJson(item.getPayloadJson()); + return logVo; + }) + .collect(Collectors.toList())); + + LoginAccount account = requireLoginAccount(); + Set roleIds = approvalAssigneeService.getAvailableRoleIds(account.getId()); + boolean canOperate = !ApprovalInstanceStatus.from(instance.getStatus()).isFinished() + && tasks.stream().anyMatch(item -> item.getStepNo().equals(instance.getCurrentStepNo()) + && ApprovalTaskStatus.PENDING.getCode().equals(item.getStatus()) + && approvalAssigneeService.canHandleTask(item, account.getId(), roleIds)); + detail.setCanApprove(canOperate); + detail.setCanReject(canOperate); + detail.setCanRevoke(canOperate); + return detail; + } + + /** + * 批量加载审批详情里涉及到的账号显示名称。 + * + * @param instance 审批实例 + * @param tasks 审批任务列表 + * @param logs 审批日志列表 + * @return 账号 ID 到展示名称的映射 + */ + private Map loadAccountNameMap(ApprovalInstance instance, List tasks, + List logs) { + Set accountIds = new HashSet<>(); + if (instance.getApplicantId() != null) { + accountIds.add(instance.getApplicantId()); + } + tasks.stream() + .map(ApprovalTask::getActedBy) + .filter(id -> id != null) + .forEach(accountIds::add); + logs.stream() + .map(ApprovalLog::getOperatorId) + .filter(id -> id != null) + .forEach(accountIds::add); + if (CollectionUtil.isEmpty(accountIds)) { + return Map.of(); + } + return sysAccountService.listByIds(accountIds).stream() + .collect(Collectors.toMap( + SysAccount::getId, + this::resolveAccountName, + (left, right) -> left, + LinkedHashMap::new)); + } + + /** + * 解析账号展示名称,优先昵称,其次登录名。 + * + * @param account 账号实体 + * @return 展示名称 + */ + private String resolveAccountName(SysAccount account) { + if (account == null) { + return null; + } + if (StringUtils.hasText(account.getNickname())) { + return account.getNickname().trim(); + } + if (StringUtils.hasText(account.getLoginName())) { + return account.getLoginName().trim(); + } + return null; + } + + private QueryWrapper buildBaseQuery(String resourceType, String actionType, String keyword) { + QueryWrapper queryWrapper = QueryWrapper.create(); + if (StringUtils.hasText(resourceType)) { + queryWrapper.eq(ApprovalInstance::getResourceType, ApprovalResourceType.from(resourceType).getCode()); + } + if (StringUtils.hasText(actionType)) { + queryWrapper.eq(ApprovalInstance::getActionType, ApprovalActionType.from(actionType).getCode()); + } + if (StringUtils.hasText(keyword)) { + queryWrapper.like(ApprovalInstance::getSummary, keyword.trim()); + } + return queryWrapper; + } + + private Page mapPage(QueryWrapper queryWrapper, long pageNumber, long pageSize, + boolean pendingMode, LoginAccount account, Set roleIds) { + Page page = approvalInstanceMapper.paginate(pageNumber, pageSize, queryWrapper); + List records = page.getRecords(); + Set pendingTaskInstanceIds = pendingMode + ? approvalAssigneeService.listPendingInstanceIds(account.getId(), roleIds, + records.stream().map(ApprovalInstance::getId).collect(Collectors.toList())) + : Set.of(); + + List result = new ArrayList<>(); + for (ApprovalInstance record : records) { + ApprovalInstancePageVo item = new ApprovalInstancePageVo(); + item.setId(record.getId()); + item.setResourceType(record.getResourceType()); + item.setResourceId(record.getResourceId()); + item.setActionType(record.getActionType()); + item.setStatus(record.getStatus()); + item.setCurrentStepNo(record.getCurrentStepNo()); + item.setCurrentStepName(resolveCurrentStepName(record)); + item.setSummary(record.getSummary()); + item.setApplicantId(record.getApplicantId()); + item.setSubmittedAt(record.getSubmittedAt()); + item.setFinishedAt(record.getFinishedAt()); + boolean canOperate = pendingMode + && pendingTaskInstanceIds.contains(record.getId()) + && !ApprovalInstanceStatus.from(record.getStatus()).isFinished(); + item.setCanApprove(canOperate); + item.setCanReject(canOperate); + item.setCanRevoke(canOperate); + result.add(item); + } + + Page voPage = new Page<>(pageNumber, pageSize, page.getTotalRow()); + voPage.setRecords(result); + return voPage; + } + + private long safePageNumber(Long pageNumber) { + return pageNumber == null || pageNumber < 1 ? 1L : pageNumber; + } + + private long safePageSize(Long pageSize) { + return pageSize == null || pageSize < 1 ? 10L : pageSize; + } + + private LoginAccount requireLoginAccount() { + LoginAccount account = SaTokenUtil.getLoginAccount(); + if (account == null) { + throw new BusinessException("当前未登录"); + } + return account; + } + + private String resolveCurrentStepName(ApprovalInstance instance) { + Map stepMap = resolveFrozenStepMap(instance); + return resolveStepName(stepMap, instance.getCurrentStepNo()); + } + + private String resolveStepName(Map stepMap, Integer stepNo) { + ApprovalFlowStepVo step = stepMap.get(stepNo); + return step == null ? null : step.getStepName(); + } + + private Map resolveFrozenStepMap(ApprovalInstance instance) { + Map result = new LinkedHashMap<>(); + Object steps = instance.getSnapshotJson() == null ? null : instance.getSnapshotJson().get("steps"); + if (steps instanceof List frozenSteps) { + for (Object item : frozenSteps) { + if (!(item instanceof Map map)) { + continue; + } + Object stepNo = map.get("stepNo"); + Object stepName = map.get("stepName"); + if (!(stepNo instanceof Number) || stepName == null) { + continue; + } + ApprovalFlowStepVo stepVo = new ApprovalFlowStepVo(); + stepVo.setStepNo(((Number) stepNo).intValue()); + stepVo.setStepName(String.valueOf(stepName)); + Object assigneeType = map.get("assigneeType"); + Object assigneeTargetId = map.get("assigneeTargetId"); + Object assigneeTargetCode = map.get("assigneeTargetCode"); + Object assigneeTargetName = map.get("assigneeTargetName"); + if (assigneeType != null) { + stepVo.setAssigneeType(String.valueOf(assigneeType)); + } + if (assigneeTargetId instanceof Number number) { + stepVo.setAssigneeTargetId(BigInteger.valueOf(number.longValue())); + } else if (assigneeTargetId instanceof String string && StringUtils.hasText(string)) { + stepVo.setAssigneeTargetId(new BigInteger(string)); + } + if (assigneeTargetCode != null) { + stepVo.setAssigneeTargetCode(String.valueOf(assigneeTargetCode)); + } + if (assigneeTargetName != null) { + stepVo.setAssigneeTargetName(String.valueOf(assigneeTargetName)); + } + result.put(stepVo.getStepNo(), stepVo); + } + } + if (!result.isEmpty() && result.values().stream().allMatch(item -> item.getAssigneeType() != null && item.getAssigneeTargetId() != null)) { + return result; + } + List storedSteps = approvalFlowStepMapper.selectListByQuery( + QueryWrapper.create().eq(ApprovalFlowStep::getFlowId, instance.getFlowId())); + for (ApprovalFlowStep step : storedSteps) { + ApprovalFlowStepVo stepVo = result.computeIfAbsent(step.getStepNo(), key -> { + ApprovalFlowStepVo value = new ApprovalFlowStepVo(); + value.setStepNo(step.getStepNo()); + return value; + }); + if (!StringUtils.hasText(stepVo.getStepName())) { + stepVo.setStepName(step.getStepName()); + } + if (!StringUtils.hasText(stepVo.getAssigneeType())) { + stepVo.setAssigneeType(step.getAssigneeType()); + } + if (stepVo.getAssigneeTargetId() == null) { + stepVo.setAssigneeTargetId(step.getAssigneeTargetId()); + } + if (!StringUtils.hasText(stepVo.getAssigneeTargetCode())) { + stepVo.setAssigneeTargetCode(step.getAssigneeTargetCode()); + } + if (!StringUtils.hasText(stepVo.getAssigneeTargetName())) { + stepVo.setAssigneeTargetName(step.getAssigneeTargetName()); + } + } + return result; + } +} diff --git a/easyflow-modules/pom.xml b/easyflow-modules/pom.xml index 08764a3..44a1c2b 100644 --- a/easyflow-modules/pom.xml +++ b/easyflow-modules/pom.xml @@ -12,6 +12,7 @@ easyflow-module-system + easyflow-module-approval easyflow-module-log easyflow-module-auth easyflow-module-autoconfig diff --git a/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/mysql/V4__mysql_approval_patch.sql b/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/mysql/V4__mysql_approval_patch.sql new file mode 100644 index 0000000..f9f1ba1 --- /dev/null +++ b/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/mysql/V4__mysql_approval_patch.sql @@ -0,0 +1,293 @@ +SET NAMES utf8mb4; + +CREATE TABLE IF NOT EXISTS `tb_approval_flow` ( + `id` bigint NOT NULL COMMENT '主键', + `name` varchar(128) NOT NULL COMMENT '流程名称', + `resource_type` varchar(32) NOT NULL COMMENT '资源类型', + `action_type` varchar(32) NOT NULL COMMENT '动作类型', + `priority` int NOT NULL DEFAULT 0 COMMENT '优先级', + `status` varchar(32) NOT NULL COMMENT '流程状态', + `version` int NOT NULL DEFAULT 1 COMMENT '流程版本', + `remark` varchar(500) DEFAULT NULL COMMENT '备注', + `created` datetime DEFAULT NULL COMMENT '创建时间', + `created_by` bigint DEFAULT NULL COMMENT '创建者', + `modified` datetime DEFAULT NULL COMMENT '修改时间', + `modified_by` bigint DEFAULT NULL COMMENT '修改者', + PRIMARY KEY (`id`), + KEY `idx_approval_flow_match` (`resource_type`, `action_type`, `status`, `priority`), + KEY `idx_approval_flow_modified` (`modified`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批流程'; + +CREATE TABLE IF NOT EXISTS `tb_approval_flow_scope` ( + `id` bigint NOT NULL COMMENT '主键', + `flow_id` bigint NOT NULL COMMENT '流程ID', + `scope_type` varchar(32) NOT NULL COMMENT '范围类型', + `scope_value` bigint NOT NULL COMMENT '范围值', + `include_children` tinyint NOT NULL DEFAULT 0 COMMENT '是否包含子节点', + `created` datetime DEFAULT NULL COMMENT '创建时间', + `created_by` bigint DEFAULT NULL COMMENT '创建者', + `modified` datetime DEFAULT NULL COMMENT '修改时间', + `modified_by` bigint DEFAULT NULL COMMENT '修改者', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_approval_flow_scope` (`flow_id`, `scope_type`, `scope_value`), + KEY `idx_approval_flow_scope_type` (`scope_type`, `scope_value`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批流程范围'; + +CREATE TABLE IF NOT EXISTS `tb_approval_flow_step` ( + `id` bigint NOT NULL COMMENT '主键', + `flow_id` bigint NOT NULL COMMENT '流程ID', + `step_no` int NOT NULL COMMENT '步骤序号', + `step_name` varchar(128) NOT NULL COMMENT '步骤名称', + `created` datetime DEFAULT NULL COMMENT '创建时间', + `created_by` bigint DEFAULT NULL COMMENT '创建者', + `modified` datetime DEFAULT NULL COMMENT '修改时间', + `modified_by` bigint DEFAULT NULL COMMENT '修改者', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_approval_flow_step` (`flow_id`, `step_no`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批流程步骤'; + +CREATE TABLE IF NOT EXISTS `tb_approval_instance` ( + `id` bigint NOT NULL COMMENT '主键', + `flow_id` bigint NOT NULL COMMENT '流程ID', + `flow_version` int NOT NULL COMMENT '流程版本', + `resource_type` varchar(32) NOT NULL COMMENT '资源类型', + `resource_id` bigint NOT NULL COMMENT '资源ID', + `action_type` varchar(32) NOT NULL COMMENT '动作类型', + `status` varchar(32) NOT NULL COMMENT '实例状态', + `current_step_no` int DEFAULT NULL COMMENT '当前步骤序号', + `snapshot_json` json DEFAULT NULL COMMENT '审批快照', + `summary` varchar(255) DEFAULT NULL COMMENT '审批摘要', + `applicant_id` bigint NOT NULL COMMENT '申请人ID', + `submitted_at` datetime DEFAULT NULL COMMENT '提交时间', + `finished_at` datetime DEFAULT NULL COMMENT '完成时间', + `created` datetime DEFAULT NULL COMMENT '创建时间', + `created_by` bigint DEFAULT NULL COMMENT '创建者', + `modified` datetime DEFAULT NULL COMMENT '修改时间', + `modified_by` bigint DEFAULT NULL COMMENT '修改者', + PRIMARY KEY (`id`), + KEY `idx_approval_instance_flow_status` (`flow_id`, `status`), + KEY `idx_approval_instance_resource` (`resource_type`, `resource_id`, `action_type`), + KEY `idx_approval_instance_applicant` (`applicant_id`, `submitted_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批实例'; + +CREATE TABLE IF NOT EXISTS `tb_approval_task` ( + `id` bigint NOT NULL COMMENT '主键', + `instance_id` bigint NOT NULL COMMENT '实例ID', + `step_no` int NOT NULL COMMENT '步骤序号', + `status` varchar(32) NOT NULL COMMENT '任务状态', + `assignee_role_code` varchar(64) NOT NULL COMMENT '指派角色编码', + `acted_by` bigint DEFAULT NULL COMMENT '处理人ID', + `acted_at` datetime DEFAULT NULL COMMENT '处理时间', + `comment` varchar(500) DEFAULT NULL COMMENT '处理意见', + `created` datetime DEFAULT NULL COMMENT '创建时间', + `created_by` bigint DEFAULT NULL COMMENT '创建者', + `modified` datetime DEFAULT NULL COMMENT '修改时间', + `modified_by` bigint DEFAULT NULL COMMENT '修改者', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_approval_task_step` (`instance_id`, `step_no`), + KEY `idx_approval_task_status` (`status`, `assignee_role_code`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批任务'; + +CREATE TABLE IF NOT EXISTS `tb_approval_log` ( + `id` bigint NOT NULL COMMENT '主键', + `instance_id` bigint NOT NULL COMMENT '实例ID', + `event_type` varchar(32) NOT NULL COMMENT '事件类型', + `operator_id` bigint DEFAULT NULL COMMENT '操作人ID', + `payload_json` json DEFAULT NULL COMMENT '事件载荷', + `created` datetime DEFAULT NULL COMMENT '创建时间', + `created_by` bigint DEFAULT NULL COMMENT '创建者', + `modified` datetime DEFAULT NULL COMMENT '修改时间', + `modified_by` bigint DEFAULT NULL COMMENT '修改者', + PRIMARY KEY (`id`), + KEY `idx_approval_log_instance` (`instance_id`, `created`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批日志'; + +INSERT INTO `tb_sys_menu` ( + `id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, + `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark` +) +SELECT + 367100000000000001, 258052082618335232, 0, 'menus.system.approval', '/sys/approval', '/system/approval/ApprovalManage', 'svg:approval', + 1, '', 750, 0, '2026-04-06 10:00:00', 1, '2026-04-06 10:00:00', 1, '审批管理菜单' +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `tb_sys_menu` WHERE `id` = 367100000000000001 +); + +INSERT INTO `tb_sys_menu` ( + `id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, + `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark` +) +SELECT + 367100000000000011, 367100000000000001, 1, '查询', '', '', '', + 0, '/api/v1/approvalFlow/query', 1, 0, '2026-04-06 10:00:00', 1, '2026-04-06 10:00:00', 1, '审批流程-查询' +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `tb_sys_menu` WHERE `id` = 367100000000000011 +); + +INSERT INTO `tb_sys_menu` ( + `id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, + `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark` +) +SELECT + 367100000000000012, 367100000000000001, 1, '保存', '', '', '', + 0, '/api/v1/approvalFlow/save', 2, 0, '2026-04-06 10:00:00', 1, '2026-04-06 10:00:00', 1, '审批流程-保存' +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `tb_sys_menu` WHERE `id` = 367100000000000012 +); + +INSERT INTO `tb_sys_menu` ( + `id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, + `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark` +) +SELECT + 367100000000000013, 367100000000000001, 1, '删除', '', '', '', + 0, '/api/v1/approvalFlow/remove', 3, 0, '2026-04-06 10:00:00', 1, '2026-04-06 10:00:00', 1, '审批流程-删除' +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `tb_sys_menu` WHERE `id` = 367100000000000013 +); + +INSERT INTO `tb_sys_menu` ( + `id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, + `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark` +) +SELECT + 367100000000000014, 367100000000000001, 1, '启用', '', '', '', + 0, '/api/v1/approvalFlow/enable', 4, 0, '2026-04-06 10:00:00', 1, '2026-04-06 10:00:00', 1, '审批流程-启用' +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `tb_sys_menu` WHERE `id` = 367100000000000014 +); + +INSERT INTO `tb_sys_menu` ( + `id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, + `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark` +) +SELECT + 367100000000000015, 367100000000000001, 1, '停用', '', '', '', + 0, '/api/v1/approvalFlow/disable', 5, 0, '2026-04-06 10:00:00', 1, '2026-04-06 10:00:00', 1, '审批流程-停用' +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `tb_sys_menu` WHERE `id` = 367100000000000015 +); + +INSERT INTO `tb_sys_menu` ( + `id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, + `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark` +) +SELECT + 367100000000000021, 367100000000000001, 1, '实例查询', '', '', '', + 0, '/api/v1/approvalInstance/query', 6, 0, '2026-04-06 10:00:00', 1, '2026-04-06 10:00:00', 1, '审批实例-查询' +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `tb_sys_menu` WHERE `id` = 367100000000000021 +); + +INSERT INTO `tb_sys_menu` ( + `id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, + `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark` +) +SELECT + 367100000000000022, 367100000000000001, 1, '通过审批', '', '', '', + 0, '/api/v1/approvalInstance/approve', 7, 0, '2026-04-06 10:00:00', 1, '2026-04-06 10:00:00', 1, '审批实例-通过' +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `tb_sys_menu` WHERE `id` = 367100000000000022 +); + +INSERT INTO `tb_sys_menu` ( + `id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, + `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark` +) +SELECT + 367100000000000023, 367100000000000001, 1, '驳回审批', '', '', '', + 0, '/api/v1/approvalInstance/reject', 8, 0, '2026-04-06 10:00:00', 1, '2026-04-06 10:00:00', 1, '审批实例-驳回' +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `tb_sys_menu` WHERE `id` = 367100000000000023 +); + +INSERT INTO `tb_sys_menu` ( + `id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, + `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark` +) +SELECT + 367100000000000024, 367100000000000001, 1, '撤回审批', '', '', '', + 0, '/api/v1/approvalInstance/revoke', 9, 0, '2026-04-06 10:00:00', 1, '2026-04-06 10:00:00', 1, '审批实例-撤回' +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `tb_sys_menu` WHERE `id` = 367100000000000024 +); + +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) +SELECT 367100000000000101, 1, 367100000000000001 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `tb_sys_role_menu` WHERE `id` = 367100000000000101 +); + +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) +SELECT 367100000000000111, 1, 367100000000000011 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `tb_sys_role_menu` WHERE `id` = 367100000000000111 +); + +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) +SELECT 367100000000000112, 1, 367100000000000012 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `tb_sys_role_menu` WHERE `id` = 367100000000000112 +); + +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) +SELECT 367100000000000113, 1, 367100000000000013 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `tb_sys_role_menu` WHERE `id` = 367100000000000113 +); + +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) +SELECT 367100000000000114, 1, 367100000000000014 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `tb_sys_role_menu` WHERE `id` = 367100000000000114 +); + +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) +SELECT 367100000000000115, 1, 367100000000000015 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `tb_sys_role_menu` WHERE `id` = 367100000000000115 +); + +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) +SELECT 367100000000000121, 1, 367100000000000021 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `tb_sys_role_menu` WHERE `id` = 367100000000000121 +); + +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) +SELECT 367100000000000122, 1, 367100000000000022 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `tb_sys_role_menu` WHERE `id` = 367100000000000122 +); + +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) +SELECT 367100000000000123, 1, 367100000000000023 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `tb_sys_role_menu` WHERE `id` = 367100000000000123 +); + +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) +SELECT 367100000000000124, 1, 367100000000000024 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `tb_sys_role_menu` WHERE `id` = 367100000000000124 +); diff --git a/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/mysql/V5__mysql_ai_publish_patch.sql b/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/mysql/V5__mysql_ai_publish_patch.sql new file mode 100644 index 0000000..e905b77 --- /dev/null +++ b/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/mysql/V5__mysql_ai_publish_patch.sql @@ -0,0 +1,60 @@ +SET NAMES utf8mb4; + +DROP PROCEDURE IF EXISTS `sp_add_column_if_missing`; + +DELIMITER $$ +CREATE PROCEDURE `sp_add_column_if_missing`( + IN in_table_name VARCHAR(128), + IN in_column_name VARCHAR(128), + IN in_definition TEXT +) +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = in_table_name + AND COLUMN_NAME = in_column_name + ) THEN + SET @ddl = CONCAT('ALTER TABLE `', in_table_name, '` ADD COLUMN ', in_definition); + PREPARE stmt FROM @ddl; + EXECUTE stmt; + DEALLOCATE PREPARE stmt; + END IF; +END $$ +DELIMITER ; + +CALL `sp_add_column_if_missing`('tb_workflow', 'publish_status', + '`publish_status` varchar(32) NOT NULL DEFAULT ''DRAFT'' COMMENT ''发布状态'''); +CALL `sp_add_column_if_missing`('tb_workflow', 'current_approval_instance_id', + '`current_approval_instance_id` bigint DEFAULT NULL COMMENT ''当前审批实例ID'''); +CALL `sp_add_column_if_missing`('tb_workflow', 'published_snapshot_json', + '`published_snapshot_json` json DEFAULT NULL COMMENT ''已发布快照'''); +CALL `sp_add_column_if_missing`('tb_workflow', 'published_at', + '`published_at` datetime DEFAULT NULL COMMENT ''发布时间'''); +CALL `sp_add_column_if_missing`('tb_workflow', 'published_by', + '`published_by` bigint DEFAULT NULL COMMENT ''发布人'''); + +CALL `sp_add_column_if_missing`('tb_document_collection', 'publish_status', + '`publish_status` varchar(32) NOT NULL DEFAULT ''DRAFT'' COMMENT ''发布状态'''); +CALL `sp_add_column_if_missing`('tb_document_collection', 'current_approval_instance_id', + '`current_approval_instance_id` bigint DEFAULT NULL COMMENT ''当前审批实例ID'''); +CALL `sp_add_column_if_missing`('tb_document_collection', 'published_snapshot_json', + '`published_snapshot_json` json DEFAULT NULL COMMENT ''已发布快照'''); +CALL `sp_add_column_if_missing`('tb_document_collection', 'published_at', + '`published_at` datetime DEFAULT NULL COMMENT ''发布时间'''); +CALL `sp_add_column_if_missing`('tb_document_collection', 'published_by', + '`published_by` bigint DEFAULT NULL COMMENT ''发布人'''); + +CALL `sp_add_column_if_missing`('tb_bot', 'publish_status', + '`publish_status` varchar(32) NOT NULL DEFAULT ''DRAFT'' COMMENT ''发布状态'''); +CALL `sp_add_column_if_missing`('tb_bot', 'current_approval_instance_id', + '`current_approval_instance_id` bigint DEFAULT NULL COMMENT ''当前审批实例ID'''); +CALL `sp_add_column_if_missing`('tb_bot', 'published_snapshot_json', + '`published_snapshot_json` json DEFAULT NULL COMMENT ''已发布快照'''); +CALL `sp_add_column_if_missing`('tb_bot', 'published_at', + '`published_at` datetime DEFAULT NULL COMMENT ''发布时间'''); +CALL `sp_add_column_if_missing`('tb_bot', 'published_by', + '`published_by` bigint DEFAULT NULL COMMENT ''发布人'''); + +DROP PROCEDURE IF EXISTS `sp_add_column_if_missing`; diff --git a/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/mysql/V6__mysql_approval_assignee_patch.sql b/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/mysql/V6__mysql_approval_assignee_patch.sql new file mode 100644 index 0000000..9c0dcf5 --- /dev/null +++ b/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/mysql/V6__mysql_approval_assignee_patch.sql @@ -0,0 +1,124 @@ +SET NAMES utf8mb4; + +DROP PROCEDURE IF EXISTS `sp_add_column_if_missing`; + +DELIMITER $$ +CREATE PROCEDURE `sp_add_column_if_missing`( + IN in_table_name VARCHAR(128), + IN in_column_name VARCHAR(128), + IN in_definition TEXT +) +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = in_table_name + AND COLUMN_NAME = in_column_name + ) THEN + SET @ddl = CONCAT('ALTER TABLE `', in_table_name, '` ADD COLUMN ', in_definition); + PREPARE stmt FROM @ddl; + EXECUTE stmt; + DEALLOCATE PREPARE stmt; + END IF; +END $$ +DELIMITER ; + +CALL `sp_add_column_if_missing`( + 'tb_approval_flow_step', + 'assignee_type', + '`assignee_type` varchar(16) DEFAULT NULL COMMENT ''审批对象类型'' AFTER `step_name`' +); +CALL `sp_add_column_if_missing`( + 'tb_approval_flow_step', + 'assignee_target_id', + '`assignee_target_id` bigint DEFAULT NULL COMMENT ''审批对象ID'' AFTER `assignee_type`' +); +CALL `sp_add_column_if_missing`( + 'tb_approval_flow_step', + 'assignee_target_code', + '`assignee_target_code` varchar(128) DEFAULT NULL COMMENT ''审批对象编码'' AFTER `assignee_target_id`' +); +CALL `sp_add_column_if_missing`( + 'tb_approval_flow_step', + 'assignee_target_name', + '`assignee_target_name` varchar(128) DEFAULT NULL COMMENT ''审批对象名称'' AFTER `assignee_target_code`' +); + +ALTER TABLE `tb_approval_task` + MODIFY COLUMN `assignee_role_code` varchar(64) DEFAULT NULL COMMENT '指派角色编码'; + +CALL `sp_add_column_if_missing`( + 'tb_approval_task', + 'assignee_type', + '`assignee_type` varchar(16) DEFAULT NULL COMMENT ''审批对象类型'' AFTER `assignee_role_code`' +); +CALL `sp_add_column_if_missing`( + 'tb_approval_task', + 'assignee_target_id', + '`assignee_target_id` bigint DEFAULT NULL COMMENT ''审批对象ID'' AFTER `assignee_type`' +); +CALL `sp_add_column_if_missing`( + 'tb_approval_task', + 'assignee_target_code', + '`assignee_target_code` varchar(128) DEFAULT NULL COMMENT ''审批对象编码'' AFTER `assignee_target_id`' +); +CALL `sp_add_column_if_missing`( + 'tb_approval_task', + 'assignee_target_name', + '`assignee_target_name` varchar(128) DEFAULT NULL COMMENT ''审批对象名称'' AFTER `assignee_target_code`' +); + +DROP PROCEDURE IF EXISTS `sp_patch_approval_assignee`; +DELIMITER $$ +CREATE PROCEDURE `sp_patch_approval_assignee`() +BEGIN + DECLARE v_super_role_id BIGINT; + DECLARE v_super_role_code VARCHAR(128); + DECLARE v_super_role_name VARCHAR(128); + DECLARE v_index_count INT DEFAULT 0; + + SELECT `id`, `role_key`, `role_name` + INTO v_super_role_id, v_super_role_code, v_super_role_name + FROM `tb_sys_role` + WHERE `role_key` = 'super_admin' + LIMIT 1; + + IF v_super_role_id IS NULL THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'super_admin role is required for approval assignee backfill'; + END IF; + + UPDATE `tb_approval_flow_step` + SET `assignee_type` = 'ROLE', + `assignee_target_id` = v_super_role_id, + `assignee_target_code` = v_super_role_code, + `assignee_target_name` = v_super_role_name + WHERE `assignee_type` IS NULL + OR `assignee_target_id` IS NULL; + + UPDATE `tb_approval_task` + SET `assignee_role_code` = IFNULL(`assignee_role_code`, v_super_role_code), + `assignee_type` = 'ROLE', + `assignee_target_id` = v_super_role_id, + `assignee_target_code` = v_super_role_code, + `assignee_target_name` = v_super_role_name + WHERE `assignee_type` IS NULL + OR `assignee_target_id` IS NULL; + + SELECT COUNT(1) + INTO v_index_count + FROM `information_schema`.`statistics` + WHERE `table_schema` = DATABASE() + AND `table_name` = 'tb_approval_task' + AND `index_name` = 'idx_approval_task_assignee'; + + IF v_index_count = 0 THEN + ALTER TABLE `tb_approval_task` + ADD INDEX `idx_approval_task_assignee` (`status`, `assignee_type`, `assignee_target_id`); + END IF; +END$$ +DELIMITER ; + +CALL `sp_patch_approval_assignee`(); +DROP PROCEDURE IF EXISTS `sp_patch_approval_assignee`; +DROP PROCEDURE IF EXISTS `sp_add_column_if_missing`; diff --git a/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/mysql/V7__mysql_approval_page_menu_patch.sql b/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/mysql/V7__mysql_approval_page_menu_patch.sql new file mode 100644 index 0000000..fef5841 --- /dev/null +++ b/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/mysql/V7__mysql_approval_page_menu_patch.sql @@ -0,0 +1,85 @@ +SET NAMES utf8mb4; + +UPDATE `tb_sys_menu` +SET + `component` = '', + `modified` = NOW(), + `modified_by` = 1, + `remark` = '审批管理分组菜单' +WHERE `id` = 367100000000000001; + +INSERT INTO `tb_sys_menu` ( + `id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, + `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark` +) +SELECT + 367100000000000002, 367100000000000001, 0, 'menus.system.approvalFlow', '/sys/approval/flow', '/system/approval/ApprovalManage', '', + 1, '', 1, 0, '2026-04-07 18:00:00', 1, '2026-04-07 18:00:00', 1, '审批管理-流程配置' +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `tb_sys_menu` WHERE `id` = 367100000000000002 +); + +INSERT INTO `tb_sys_menu` ( + `id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, + `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark` +) +SELECT + 367100000000000003, 367100000000000001, 0, 'menus.system.approvalPending', '/sys/approval/pending', '/system/approval/ApprovalManage', '', + 1, '', 2, 0, '2026-04-07 18:00:00', 1, '2026-04-07 18:00:00', 1, '审批管理-待审批' +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `tb_sys_menu` WHERE `id` = 367100000000000003 +); + +INSERT INTO `tb_sys_menu` ( + `id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, + `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark` +) +SELECT + 367100000000000004, 367100000000000001, 0, 'menus.system.approvalProcessed', '/sys/approval/processed', '/system/approval/ApprovalManage', '', + 1, '', 3, 0, '2026-04-07 18:00:00', 1, '2026-04-07 18:00:00', 1, '审批管理-已审批' +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `tb_sys_menu` WHERE `id` = 367100000000000004 +); + +INSERT INTO `tb_sys_menu` ( + `id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, + `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark` +) +SELECT + 367100000000000005, 367100000000000001, 0, 'menus.system.approvalInitiated', '/sys/approval/initiated', '/system/approval/ApprovalManage', '', + 1, '', 4, 0, '2026-04-07 18:00:00', 1, '2026-04-07 18:00:00', 1, '审批管理-我发起' +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `tb_sys_menu` WHERE `id` = 367100000000000005 +); + +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) +SELECT 367100000000000102, 1, 367100000000000002 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `tb_sys_role_menu` WHERE `id` = 367100000000000102 +); + +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) +SELECT 367100000000000103, 1, 367100000000000003 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `tb_sys_role_menu` WHERE `id` = 367100000000000103 +); + +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) +SELECT 367100000000000104, 1, 367100000000000004 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `tb_sys_role_menu` WHERE `id` = 367100000000000104 +); + +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) +SELECT 367100000000000105, 1, 367100000000000005 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `tb_sys_role_menu` WHERE `id` = 367100000000000105 +); diff --git a/easyflow-ui-admin/app/src/api/ai/bot.ts b/easyflow-ui-admin/app/src/api/ai/bot.ts index 4b674bf..7e82308 100644 --- a/easyflow-ui-admin/app/src/api/ai/bot.ts +++ b/easyflow-ui-admin/app/src/api/ai/bot.ts @@ -53,6 +53,22 @@ export const removeBotFromId = (id: string) => { return api.post('/api/v1/bot/remove', { id }); }; +/** 提交 Bot 发布审批 */ +export const submitBotPublishApproval = (id: string) => { + return api.post>( + '/api/v1/bot/submitPublishApproval', + { id }, + ); +}; + +/** 提交 Bot 删除审批 */ +export const submitBotDeleteApproval = (id: string) => { + return api.post>( + '/api/v1/bot/submitDeleteApproval', + { id }, + ); +}; + export interface GetMessageListParams { conversationId: string; botId: string; diff --git a/easyflow-ui-admin/app/src/components/page/CardList.vue b/easyflow-ui-admin/app/src/components/page/CardList.vue index 9c5f5b9..e51cd99 100644 --- a/easyflow-ui-admin/app/src/components/page/CardList.vue +++ b/easyflow-ui-admin/app/src/components/page/CardList.vue @@ -23,7 +23,7 @@ export type ActionTone = 'danger' | 'default'; export interface ActionButton { icon?: any; - text: string; + text: ((row: any) => string) | string; className?: string; permission?: string; placement?: ActionPlacement; @@ -135,6 +135,10 @@ function handleActionClick(event: Event, action: ActionButton, item: any) { event.stopPropagation(); action.onClick(item); } + +function resolveActionText(action: ActionButton, item: any) { + return typeof action.text === 'function' ? action.text(item) : action.text; +} - {{ action.text }} + {{ resolveActionText(action, item) }} - {{ action.text }} + {{ resolveActionText(action, item) }} diff --git a/easyflow-ui-admin/app/src/locales/langs/en-US/aiWorkflow.json b/easyflow-ui-admin/app/src/locales/langs/en-US/aiWorkflow.json index 198cb4f..2849867 100644 --- a/easyflow-ui-admin/app/src/locales/langs/en-US/aiWorkflow.json +++ b/easyflow-ui-admin/app/src/locales/langs/en-US/aiWorkflow.json @@ -62,6 +62,15 @@ "subProcess": "SubProcess", "workflowSelect": "WorkflowSelect", "bochaSearch": "BochaSearch", + "publishStatusDraft": "Draft", + "publishStatusPublishPending": "Publish Pending", + "publishStatusPublished": "Published", + "publishStatusDeletePending": "Delete Pending", + "publishStatusLabel": "Release", + "submitPublishApprovalConfirm": "The current draft will enter the publish approval flow. It becomes externally available only after approval.", + "submitDeleteApprovalConfirm": "The workflow will enter the delete approval flow. It will be physically deleted only after approval.", + "publishPendingHint": "There is already an approval in progress for this workflow.", + "deletePendingHint": "There is already an approval in progress for this workflow.", "check": "Check", "checkPassed": "Workflow check passed", "checkFailed": "Workflow check failed. Please fix the issues first", diff --git a/easyflow-ui-admin/app/src/locales/langs/en-US/approval.json b/easyflow-ui-admin/app/src/locales/langs/en-US/approval.json new file mode 100644 index 0000000..82b7edb --- /dev/null +++ b/easyflow-ui-admin/app/src/locales/langs/en-US/approval.json @@ -0,0 +1,161 @@ +{ + "title": "Approval Detail", + "tab": { + "flow": "Flow Config", + "pending": "Pending", + "processed": "Processed", + "initiated": "Initiated" + }, + "resource": { + "bot": "Chat Assistant", + "workflow": "Workflow", + "knowledge": "Knowledge Base" + }, + "action": { + "publish": "Publish", + "delete": "Delete", + "addFlow": "New Flow", + "editFlow": "Edit Flow", + "enableFlow": "Enable Flow", + "disableFlow": "Disable Flow", + "approve": "Approve", + "reject": "Reject", + "revoke": "Revoke", + "addScope": "Add Scope", + "addStep": "Add Step" + }, + "scope": { + "category": "Category", + "dept": "Department" + }, + "assignee": { + "role": "Role", + "user": "User" + }, + "status": { + "enabled": "Enabled", + "disabled": "Disabled", + "pending": "Pending", + "processing": "Processing", + "approved": "Approved", + "rejected": "Rejected", + "revoked": "Revoked" + }, + "section": { + "basic": "Basic Info", + "scope": "Scope Config", + "steps": "Approval Steps", + "tasks": "Approval Tasks", + "logs": "Approval Logs", + "snapshot": "Approval Snapshot" + }, + "helper": { + "scope": "Limit the flow by resource category or applicant department. Leave empty to apply globally.", + "scopeEmpty": "No scope configured. This flow applies globally." + }, + "fields": { + "flowName": "Flow Name", + "resourceType": "Resource Type", + "actionType": "Action Type", + "priority": "Priority", + "version": "Version", + "status": "Status", + "remark": "Remark", + "scopeSummary": "Scope Summary", + "stepCount": "Step Count", + "includeChildren": "Include Children", + "stepName": "Step Name", + "stepNoLabel": "Step No.", + "currentStep": "Current Step", + "summary": "Summary", + "resourceId": "Resource ID", + "taskId": "Approval Task ID", + "applicant": "Applicant", + "applicantId": "Applicant ID", + "submittedAt": "Submitted At", + "finishedAt": "Finished At", + "assigneeTarget": "Assignee", + "actedBy": "Acted By", + "actedAt": "Acted At", + "comment": "Comment", + "eventType": "Event Type", + "operatorId": "Operator ID", + "operatorName": "Operator Name", + "createdAt": "Created At", + "eventInfo": "Event Info", + "stepNo": "Step {value}" + }, + "event": { + "approved": "Approved", + "rejected": "Rejected", + "revoked": "Revoked", + "stepCreated": "Step Created", + "submitted": "Submitted" + }, + "placeholder": { + "flowName": "Search flow name", + "keyword": "Search summary", + "resourceType": "Filter resource type", + "actionType": "Filter action type", + "flowStatus": "Filter flow status", + "instanceStatus": "Status", + "scopeValue": "Select scope value", + "assigneeType": "Select assignee type", + "assigneeTarget": "Select assignee", + "stepName": "Enter step name", + "actionComment": "Enter a comment" + }, + "message": { + "needStep": "At least one step is required", + "needStepAssignee": "Each approval step requires an assignee", + "saveSuccess": "Flow saved", + "statusUpdated": "Flow status updated", + "deleteSuccess": "Flow deleted", + "actionSuccess": "Approval action completed", + "confirmDeleteFlow": "This action cannot be undone. Continue?", + "confirmFlowStatus": "Confirm to {title}?", + "eventApproved": "Approval completed", + "eventApprovedStep": "Step {value} approved", + "eventRejected": "Approval rejected", + "eventRejectedStep": "Step {value} rejected", + "eventRevoked": "Approval revoked", + "eventRevokedStep": "Step {value} revoked", + "workflowSnapshotUntitled": "Untitled workflow snapshot", + "workflowSnapshotMissing": "Workflow snapshot not found", + "workflowSnapshotParseFailed": "Failed to parse workflow snapshot" + }, + "snapshot": { + "knowledgeBasic": "Basic Info", + "knowledgeConfig": "Retrieval Config", + "botOverview": "Assistant Overview", + "botModelConfig": "Model Config", + "botBindings": "Capability Bindings", + "systemPrompt": "System Prompt", + "department": "Department", + "category": "Category", + "modelName": "Model Name", + "vectorStoreType": "Vector Store Type", + "vectorEmbedModel": "Embedding Model", + "rerankModel": "Rerank Model", + "maxMessageCount": "Max Context Messages", + "canUpdateEmbeddingModel": "Allow Embedding Update", + "anonymousEnabled": "Anonymous Access", + "anonymousDisabled": "Login Only", + "knowledgeBindings": "Knowledge Bases", + "workflowBindings": "Workflows", + "pluginBindings": "Plugins", + "mcpBindings": "MCP", + "expandPrompt": "Expand", + "collapsePrompt": "Collapse", + "enabled": "Enabled", + "disabled": "Disabled", + "notConfigured": "Not configured", + "noBindings": "No bindings", + "untitledKnowledge": "Untitled knowledge base", + "untitledBot": "Untitled assistant", + "unnamedKnowledge": "Unnamed knowledge base", + "unnamedWorkflow": "Unnamed workflow", + "unnamedPlugin": "Unnamed plugin", + "unnamedMcp": "Unnamed MCP" + } +} diff --git a/easyflow-ui-admin/app/src/locales/langs/en-US/bot.json b/easyflow-ui-admin/app/src/locales/langs/en-US/bot.json index 46548f4..ea64675 100644 --- a/easyflow-ui-admin/app/src/locales/langs/en-US/bot.json +++ b/easyflow-ui-admin/app/src/locales/langs/en-US/bot.json @@ -11,6 +11,20 @@ "deepThinking": "DeepThinking", "enableDeepThinking": "EnableDeepThinking", "publish": "Publish", + "publishStatusLabel": "Current Release", + "publishStatusDraft": "Draft", + "publishStatusDraftDesc": "Only the draft is saved. External chat and Public API are still unavailable.", + "publishStatusPublishPending": "Publish Pending", + "publishStatusPublishPendingDesc": "The assistant will switch to the new release after approval.", + "publishStatusPublished": "Published", + "publishStatusPublishedDesc": "The current release is externally available. Ongoing edits still stay in draft.", + "publishStatusDeletePending": "Delete Pending", + "publishStatusDeletePendingDesc": "The current release remains available, but it is no longer offered as a new binding candidate.", + "submitPublishApprovalConfirm": "Submit the current draft to publish approval. It becomes externally available only after approval.", + "submitDeleteApprovalConfirm": "Submit the bot to delete approval. It will be physically deleted only after approval.", + "publishPendingHint": "There is already an approval in progress for this bot.", + "deletePendingHint": "There is already an approval in progress for this bot.", + "publishRequiredHint": "There is no released version yet. Submit publish approval first.", "postToWeChatOfficialAccount": "PostToWeChatOfficialAccount", "publishExternalLink": "Publish External Chat Link", "configured": "Configured", diff --git a/easyflow-ui-admin/app/src/locales/langs/en-US/button.json b/easyflow-ui-admin/app/src/locales/langs/en-US/button.json index c39ad7f..ad7e89f 100644 --- a/easyflow-ui-admin/app/src/locales/langs/en-US/button.json +++ b/easyflow-ui-admin/app/src/locales/langs/en-US/button.json @@ -41,7 +41,10 @@ "markAsResolved": "MarkAsResolved", "optimizing": "Optimizing", "regenerate": "Regenerate", + "republish": "Republish", "hide": "Hide", "more": "Mode", + "submitDeleteApproval": "Submit Delete Approval", + "submitPublishApproval": "Submit Publish Approval", "viewSegmentation": "ViewSegmentation" } diff --git a/easyflow-ui-admin/app/src/locales/langs/en-US/documentCollection.json b/easyflow-ui-admin/app/src/locales/langs/en-US/documentCollection.json index 11081d4..3482c6c 100644 --- a/easyflow-ui-admin/app/src/locales/langs/en-US/documentCollection.json +++ b/easyflow-ui-admin/app/src/locales/langs/en-US/documentCollection.json @@ -35,6 +35,15 @@ "documentType": "DocumentType", "fileName": "fileName", "knowledgeCount": "Number of knowledge items", + "publishStatusDraft": "Draft", + "publishStatusPublishPending": "Publish Pending", + "publishStatusPublished": "Published", + "publishStatusDeletePending": "Delete Pending", + "publishStatusLabel": "Release", + "submitPublishApprovalConfirm": "The knowledge base will enter the publish approval flow. It can be referenced by bots only after approval.", + "submitDeleteApprovalConfirm": "The knowledge base will enter the delete approval flow. It will be physically deleted only after approval.", + "publishPendingHint": "There is already an approval in progress for this knowledge base.", + "deletePendingHint": "There is already an approval in progress for this knowledge base.", "createdModifyTime": "Creation/update time", "documentList": "documentList", "knowledgeRetrieval": "knowledgeRetrieval", diff --git a/easyflow-ui-admin/app/src/locales/langs/en-US/menus.json b/easyflow-ui-admin/app/src/locales/langs/en-US/menus.json index 32dded5..779dcb9 100644 --- a/easyflow-ui-admin/app/src/locales/langs/en-US/menus.json +++ b/easyflow-ui-admin/app/src/locales/langs/en-US/menus.json @@ -13,6 +13,11 @@ "sysJob": "Job", "sysLog": "Log", "sysFeedback": "UserFeedback", + "approval": "Approval", + "approvalFlow": "Flow Setup", + "approvalPending": "Pending", + "approvalProcessed": "Processed", + "approvalInitiated": "Initiated", "sysAppearance": "Appearance", "oauth": "OAuth" }, diff --git a/easyflow-ui-admin/app/src/locales/langs/zh-CN/aiWorkflow.json b/easyflow-ui-admin/app/src/locales/langs/zh-CN/aiWorkflow.json index cc787df..eef363f 100644 --- a/easyflow-ui-admin/app/src/locales/langs/zh-CN/aiWorkflow.json +++ b/easyflow-ui-admin/app/src/locales/langs/zh-CN/aiWorkflow.json @@ -62,6 +62,15 @@ "subProcess": "子流程", "workflowSelect": "工作流选择", "bochaSearch": "博查搜索", + "publishStatusDraft": "草稿", + "publishStatusPublishPending": "发布审批中", + "publishStatusPublished": "已发布", + "publishStatusDeletePending": "删除审批中", + "publishStatusLabel": "发布状态", + "submitPublishApprovalConfirm": "提交后会进入发布审批,审批通过后新版本才会正式对外可用。", + "submitDeleteApprovalConfirm": "提交后会进入删除审批,审批通过后将执行真实删除。", + "publishPendingHint": "当前工作流已有进行中的审批,请等待处理完成。", + "deletePendingHint": "当前工作流已有进行中的审批,请等待处理完成。", "check": "检查", "checkPassed": "工作流检查通过", "checkFailed": "工作流检查未通过,请先修复问题", diff --git a/easyflow-ui-admin/app/src/locales/langs/zh-CN/approval.json b/easyflow-ui-admin/app/src/locales/langs/zh-CN/approval.json new file mode 100644 index 0000000..2c0fa37 --- /dev/null +++ b/easyflow-ui-admin/app/src/locales/langs/zh-CN/approval.json @@ -0,0 +1,161 @@ +{ + "title": "审批详情", + "tab": { + "flow": "流程配置", + "pending": "待审批", + "processed": "已审批", + "initiated": "我发起" + }, + "resource": { + "bot": "聊天助手", + "workflow": "工作流", + "knowledge": "知识库" + }, + "action": { + "publish": "发布", + "delete": "删除", + "addFlow": "新建流程", + "editFlow": "编辑流程", + "enableFlow": "启用流程", + "disableFlow": "停用流程", + "approve": "通过", + "reject": "驳回", + "revoke": "撤回", + "addScope": "新增范围", + "addStep": "新增步骤" + }, + "scope": { + "category": "分类", + "dept": "部门" + }, + "assignee": { + "role": "角色", + "user": "用户" + }, + "status": { + "enabled": "启用", + "disabled": "停用", + "pending": "待审批", + "processing": "审批中", + "approved": "已通过", + "rejected": "已驳回", + "revoked": "已撤回" + }, + "section": { + "basic": "基础信息", + "scope": "范围配置", + "steps": "审批步骤", + "tasks": "审批任务", + "logs": "审批日志", + "snapshot": "审批快照" + }, + "helper": { + "scope": "按资源分类或申请人部门限定流程命中范围;留空时默认对全部资源生效。", + "scopeEmpty": "未配置范围,当前流程默认全局生效。" + }, + "fields": { + "flowName": "流程名称", + "resourceType": "资源类型", + "actionType": "动作类型", + "priority": "优先级", + "version": "版本", + "status": "状态", + "remark": "备注", + "scopeSummary": "命中范围", + "stepCount": "步骤数", + "includeChildren": "包含子级", + "stepName": "步骤名称", + "stepNoLabel": "步骤序号", + "currentStep": "当前步骤", + "summary": "审批摘要", + "resourceId": "资源ID", + "taskId": "审批任务ID", + "applicant": "申请人", + "applicantId": "申请人ID", + "submittedAt": "提交时间", + "finishedAt": "完成时间", + "assigneeTarget": "审批对象", + "actedBy": "处理人", + "actedAt": "处理时间", + "comment": "处理意见", + "eventType": "事件类型", + "operatorId": "操作人ID", + "operatorName": "操作人名称", + "createdAt": "创建时间", + "eventInfo": "事件信息", + "stepNo": "第 {value} 步" + }, + "event": { + "approved": "审批通过", + "rejected": "审批驳回", + "revoked": "审批撤回", + "stepCreated": "步骤创建", + "submitted": "提交审批" + }, + "placeholder": { + "flowName": "搜索流程名称", + "keyword": "搜索审批摘要", + "resourceType": "筛选资源类型", + "actionType": "筛选动作类型", + "flowStatus": "筛选流程状态", + "instanceStatus": "审批状态", + "scopeValue": "请选择范围值", + "assigneeType": "请选择审批方式", + "assigneeTarget": "请选择审批对象", + "stepName": "请输入步骤名称", + "actionComment": "请输入处理说明" + }, + "message": { + "needStep": "至少需要一个审批步骤", + "needStepAssignee": "每个审批步骤都需要配置审批对象", + "saveSuccess": "审批流程已保存", + "statusUpdated": "流程状态已更新", + "deleteSuccess": "流程已删除", + "actionSuccess": "审批操作已完成", + "confirmDeleteFlow": "删除后将无法恢复,确认继续吗?", + "confirmFlowStatus": "确认执行{title}吗?", + "eventApproved": "审批已通过", + "eventApprovedStep": "第 {value} 步已通过", + "eventRejected": "审批已驳回", + "eventRejectedStep": "第 {value} 步已驳回", + "eventRevoked": "审批已撤回", + "eventRevokedStep": "第 {value} 步已撤回", + "workflowSnapshotUntitled": "未命名工作流快照", + "workflowSnapshotMissing": "未找到工作流快照", + "workflowSnapshotParseFailed": "工作流快照解析失败" + }, + "snapshot": { + "knowledgeBasic": "基础信息", + "knowledgeConfig": "检索配置", + "botOverview": "助手概览", + "botModelConfig": "模型配置", + "botBindings": "能力绑定", + "systemPrompt": "系统提示词", + "department": "所属部门", + "category": "所属分类", + "modelName": "模型名称", + "vectorStoreType": "向量数据库类型", + "vectorEmbedModel": "向量模型", + "rerankModel": "重排模型", + "maxMessageCount": "最大上下文消息数", + "canUpdateEmbeddingModel": "允许更新向量模型", + "anonymousEnabled": "允许匿名访问", + "anonymousDisabled": "仅登录访问", + "knowledgeBindings": "知识库", + "workflowBindings": "工作流", + "pluginBindings": "插件", + "mcpBindings": "MCP", + "expandPrompt": "展开全文", + "collapsePrompt": "收起全文", + "enabled": "已开启", + "disabled": "已关闭", + "notConfigured": "未配置", + "noBindings": "未绑定任何能力", + "untitledKnowledge": "未命名知识库", + "untitledBot": "未命名聊天助手", + "unnamedKnowledge": "未命名知识库", + "unnamedWorkflow": "未命名工作流", + "unnamedPlugin": "未命名插件", + "unnamedMcp": "未命名MCP" + } +} diff --git a/easyflow-ui-admin/app/src/locales/langs/zh-CN/bot.json b/easyflow-ui-admin/app/src/locales/langs/zh-CN/bot.json index b170b12..8711279 100644 --- a/easyflow-ui-admin/app/src/locales/langs/zh-CN/bot.json +++ b/easyflow-ui-admin/app/src/locales/langs/zh-CN/bot.json @@ -11,6 +11,20 @@ "deepThinking": "深度思考", "enableDeepThinking": "是否启用深度思考", "publish": "发布", + "publishStatusLabel": "当前正式版本", + "publishStatusDraft": "草稿", + "publishStatusDraftDesc": "当前仅保存草稿,外链聊天和 Public API 仍不可用。", + "publishStatusPublishPending": "发布审批中", + "publishStatusPublishPendingDesc": "审批通过后,聊天助手会切换为新的正式版本。", + "publishStatusPublished": "已发布", + "publishStatusPublishedDesc": "当前正式版本已可对外使用,编辑中的草稿不会立即影响线上。", + "publishStatusDeletePending": "删除审批中", + "publishStatusDeletePendingDesc": "当前正式版本仍可访问,但不会继续作为新的绑定候选。", + "submitPublishApprovalConfirm": "提交后会进入发布审批,审批通过后聊天助手才会正式对外可用。", + "submitDeleteApprovalConfirm": "提交后会进入删除审批,审批通过后将执行真实删除。", + "publishPendingHint": "当前聊天助手已有进行中的审批,请等待处理完成。", + "deletePendingHint": "当前聊天助手已有进行中的审批,请等待处理完成。", + "publishRequiredHint": "当前还没有正式发布版本,请先提交发布审批。", "postToWeChatOfficialAccount": "发布到微信公众号", "publishExternalLink": "发布外链聊天页", "configured": "已配置", diff --git a/easyflow-ui-admin/app/src/locales/langs/zh-CN/button.json b/easyflow-ui-admin/app/src/locales/langs/zh-CN/button.json index cc640fb..26f4491 100644 --- a/easyflow-ui-admin/app/src/locales/langs/zh-CN/button.json +++ b/easyflow-ui-admin/app/src/locales/langs/zh-CN/button.json @@ -41,7 +41,10 @@ "markAsResolved": "标记已处理", "optimizing": "正在优化中...", "regenerate": "重新生成", + "republish": "重新发布", "hide": "隐藏", "more": "更多", + "submitDeleteApproval": "提交删除审批", + "submitPublishApproval": "提交发布审批", "viewSegmentation": "查看分段" } diff --git a/easyflow-ui-admin/app/src/locales/langs/zh-CN/documentCollection.json b/easyflow-ui-admin/app/src/locales/langs/zh-CN/documentCollection.json index 0ddd855..4f1665e 100644 --- a/easyflow-ui-admin/app/src/locales/langs/zh-CN/documentCollection.json +++ b/easyflow-ui-admin/app/src/locales/langs/zh-CN/documentCollection.json @@ -35,6 +35,15 @@ "documentType": "文件类型", "fileName": "文件名", "knowledgeCount": "知识条数", + "publishStatusDraft": "草稿", + "publishStatusPublishPending": "发布审批中", + "publishStatusPublished": "已发布", + "publishStatusDeletePending": "删除审批中", + "publishStatusLabel": "发布状态", + "submitPublishApprovalConfirm": "提交后会进入发布审批,审批通过后该知识库才可作为正式版本被聊天助手引用。", + "submitDeleteApprovalConfirm": "提交后会进入删除审批,审批通过后将执行真实删除。", + "publishPendingHint": "当前知识库已有进行中的审批,请等待处理完成。", + "deletePendingHint": "当前知识库已有进行中的审批,请等待处理完成。", "createdModifyTime": "创建/更新时间", "documentList": "文档列表", "knowledgeRetrieval": "知识检索", diff --git a/easyflow-ui-admin/app/src/locales/langs/zh-CN/menus.json b/easyflow-ui-admin/app/src/locales/langs/zh-CN/menus.json index 42dbcd2..414899b 100644 --- a/easyflow-ui-admin/app/src/locales/langs/zh-CN/menus.json +++ b/easyflow-ui-admin/app/src/locales/langs/zh-CN/menus.json @@ -13,6 +13,11 @@ "sysJob": "定时任务", "sysLog": "日志管理", "sysFeedback": "用户反馈", + "approval": "审批管理", + "approvalFlow": "流程配置", + "approvalPending": "待审批", + "approvalProcessed": "已审批", + "approvalInitiated": "我发起", "sysAppearance": "外观设置", "oauth": "认证设置" }, diff --git a/easyflow-ui-admin/app/src/router/routes/modules/approval.ts b/easyflow-ui-admin/app/src/router/routes/modules/approval.ts new file mode 100644 index 0000000..034bb18 --- /dev/null +++ b/easyflow-ui-admin/app/src/router/routes/modules/approval.ts @@ -0,0 +1,20 @@ +import type { RouteRecordRaw } from 'vue-router'; + +import { $t } from '#/locales'; + +const routes: RouteRecordRaw[] = [ + { + name: 'ApprovalDetail', + path: '/sys/approval/detail/:id', + component: () => import('#/views/system/approval/ApprovalDetail.vue'), + meta: { + title: $t('menus.system.approval'), + hideInMenu: true, + hideInBreadcrumb: true, + hideInTab: true, + activePath: '/sys/approval', + }, + }, +]; + +export default routes; diff --git a/easyflow-ui-admin/app/src/types/tinyflow-ai-vue.d.ts b/easyflow-ui-admin/app/src/types/tinyflow-ai-vue.d.ts index e4f7f21..7d7a7b5 100644 --- a/easyflow-ui-admin/app/src/types/tinyflow-ai-vue.d.ts +++ b/easyflow-ui-admin/app/src/types/tinyflow-ai-vue.d.ts @@ -1,9 +1,14 @@ declare module '@tinyflow-ai/vue' { import type { DefineComponent } from 'vue'; + import type { TinyflowData } from '@tinyflow-ai/ui'; export const Tinyflow: DefineComponent< Record, Record, any >; + + export function sanitizeTinyflowDataForReadonlyPreview( + input: TinyflowData | string, + ): TinyflowData | null | undefined; } diff --git a/easyflow-ui-admin/app/src/views/ai/bots/index.vue b/easyflow-ui-admin/app/src/views/ai/bots/index.vue index d39e861..85926fc 100644 --- a/easyflow-ui-admin/app/src/views/ai/bots/index.vue +++ b/easyflow-ui-admin/app/src/views/ai/bots/index.vue @@ -14,7 +14,7 @@ import { useRouter } from 'vue-router'; import { EasyFlowFormModal } from '@easyflow/common-ui'; import { $t } from '@easyflow/locales'; -import { Delete, Edit, Plus, Setting } from '@element-plus/icons-vue'; +import { Delete, Edit, Plus, Promotion, Setting } from '@element-plus/icons-vue'; import { ElForm, ElFormItem, @@ -22,16 +22,22 @@ import { ElInputNumber, ElMessage, ElMessageBox, + ElTag, } from 'element-plus'; import { tryit } from 'radash'; -import { removeBotFromId } from '#/api'; +import { submitBotDeleteApproval, submitBotPublishApproval } from '#/api'; import { api } from '#/api/request'; import defaultAvatar from '#/assets/ai/bot/defaultBotAvatar.png'; import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue'; import CardList from '#/components/page/CardList.vue'; import PageData from '#/components/page/PageData.vue'; import PageSide from '#/components/page/PageSide.vue'; +import { + isAiResourceApprovalPending, + isAiResourcePublished, + normalizeAiPublishStatus, +} from '#/views/ai/shared/publish-status'; import { useDictStore } from '#/store'; import Modal from './modal.vue'; @@ -97,37 +103,101 @@ const actions: ActionButton[] = [ }, }, { - icon: Delete, - text: $t('button.delete'), - tone: 'danger', - permission: '/api/v1/bot/remove', + icon: Promotion, + text: (row: BotInfo) => + isAiResourcePublished(row.publishStatus) + ? $t('button.republish') + : $t('button.submitPublishApproval'), + permission: '/api/v1/bot/save', placement: 'inline', onClick(row: BotInfo) { - removeBot(row); + handleSubmitPublishApproval(row); + }, + }, + { + icon: Delete, + text: $t('button.submitDeleteApproval'), + tone: 'danger', + permission: '/api/v1/bot/remove', + placement: 'menu', + onClick(row: BotInfo) { + handleSubmitDeleteApproval(row); }, }, ]; -const removeBot = async (bot: BotInfo) => { - const [action] = await tryit(ElMessageBox.confirm)( - $t('message.deleteAlert'), - $t('message.noticeTitle'), - { - confirmButtonText: $t('message.ok'), - cancelButtonText: $t('message.cancel'), - type: 'warning', - }, - ); - - if (!action) { - const [err, res] = await tryit(removeBotFromId)(bot.id); - - if (!err && res.errorCode === 0) { - ElMessage.success($t('message.deleteOkMessage')); - pageDataRef.value.setQuery({}); - } +const handleSubmitPublishApproval = async (bot: BotInfo) => { + if (isAiResourceApprovalPending(bot.publishStatus)) { + ElMessage.warning($t('bot.publishPendingHint')); + return; + } + try { + await ElMessageBox.confirm( + $t('bot.submitPublishApprovalConfirm'), + $t('message.noticeTitle'), + { + confirmButtonText: $t('button.confirm'), + cancelButtonText: $t('button.cancel'), + type: 'info', + }, + ); + } catch { + return; + } + const res = await submitBotPublishApproval(String(bot.id)); + if (res.errorCode === 0) { + ElMessage.success(res.message || $t('message.saveOkMessage')); + pageDataRef.value?.reload?.(); } }; +const handleSubmitDeleteApproval = async (bot: BotInfo) => { + if (isAiResourceApprovalPending(bot.publishStatus)) { + ElMessage.warning($t('bot.deletePendingHint')); + return; + } + try { + await ElMessageBox.confirm( + $t('bot.submitDeleteApprovalConfirm'), + $t('message.noticeTitle'), + { + confirmButtonText: $t('button.confirm'), + cancelButtonText: $t('button.cancel'), + type: 'warning', + }, + ); + } catch { + return; + } + const res = await submitBotDeleteApproval(String(bot.id)); + if (res.errorCode === 0) { + ElMessage.success(res.message || $t('message.saveOkMessage')); + pageDataRef.value?.reload?.(); + } +}; +function resolvePublishStatusMeta(status?: string) { + switch (normalizeAiPublishStatus(status)) { + case 'PUBLISHED': + return { + label: $t('bot.publishStatusPublished'), + type: 'success' as const, + }; + case 'PUBLISH_PENDING': + return { + label: $t('bot.publishStatusPublishPending'), + type: 'warning' as const, + }; + case 'DELETE_PENDING': + return { + label: $t('bot.publishStatusDeletePending'), + type: 'danger' as const, + }; + default: + return { + label: $t('bot.publishStatusDraft'), + type: 'info' as const, + }; + } +} const handleSearch = (params: string) => { pageDataRef.value.setQuery({ title: params, isQueryOr: true }); @@ -302,7 +372,18 @@ const getSideList = async () => { :data="pageList" :primary-action="primaryAction" :actions="actions" - /> + > + + diff --git a/easyflow-ui-admin/app/src/views/ai/bots/pages/setting/config.vue b/easyflow-ui-admin/app/src/views/ai/bots/pages/setting/config.vue index 8c56958..31436a5 100644 --- a/easyflow-ui-admin/app/src/views/ai/bots/pages/setting/config.vue +++ b/easyflow-ui-admin/app/src/views/ai/bots/pages/setting/config.vue @@ -34,12 +34,15 @@ import { ElSkeleton, ElSlider, ElSwitch, + ElTag, ElTooltip, } from 'element-plus'; import { tryit } from 'radash'; import { getPerQuestions, + submitBotDeleteApproval, + submitBotPublishApproval, updateBotApi, updateBotOptions, updateLlmId, @@ -52,6 +55,12 @@ import CollapseViewItem from '#/components/collapseViewItem/CollapseViewItem.vue import CommonSelectDataModal from '#/components/commonSelectModal/CommonSelectDataModal.vue'; import DictSelect from '#/components/dict/DictSelect.vue'; import UploadAvatar from '#/components/upload/UploadAvatar.vue'; +import { + isAiResourceApprovalPending, + isAiResourceExternallyVisible, + isAiResourcePublished, + normalizeAiPublishStatus, +} from '#/views/ai/shared/publish-status'; interface SelectedMcpTool { name: string; @@ -154,6 +163,46 @@ const publicChatUrl = computed(() => { const publicChatEmbedUrl = computed(() => { return buildPublicChatUrl(true); }); +const publishStatusMeta = computed<{ + description: string; + label: string; + type: 'danger' | 'info' | 'success' | 'warning'; +}>(() => { + switch (normalizeAiPublishStatus(botInfo.value?.publishStatus)) { + case 'PUBLISHED': + return { + label: $t('bot.publishStatusPublished'), + type: 'success', + description: $t('bot.publishStatusPublishedDesc'), + }; + case 'PUBLISH_PENDING': + return { + label: $t('bot.publishStatusPublishPending'), + type: 'warning', + description: $t('bot.publishStatusPublishPendingDesc'), + }; + case 'DELETE_PENDING': + return { + label: $t('bot.publishStatusDeletePending'), + type: 'danger', + description: $t('bot.publishStatusDeletePendingDesc'), + }; + default: + return { + label: $t('bot.publishStatusDraft'), + type: 'info', + description: $t('bot.publishStatusDraftDesc'), + }; + } +}); +const canUsePublicAccess = computed(() => + isAiResourceExternallyVisible(botInfo.value?.publishStatus), +); +const publishPrimaryActionLabel = computed(() => + isAiResourcePublished(botInfo.value?.publishStatus) + ? $t('button.republish') + : $t('button.submitPublishApproval'), +); const iframeCode = computed(() => { if (!publicChatEmbedUrl.value) { return ''; @@ -494,6 +543,10 @@ const handleCopyValue = async (value: string, successMessage?: string) => { }; const openPublicPage = () => { + if (!canUsePublicAccess.value) { + ElMessage.warning($t('bot.publishRequiredHint')); + return; + } if (!publicChatUrl.value) { ElMessage.warning($t('bot.chatPublishBaseUrlMissing')); return; @@ -767,6 +820,64 @@ const handleDeletePresetQuestion = (item: any) => { const handlePublishWx = () => { publishWxRef.value.openDialog(botId.value, botInfo.value?.options || {}); }; +const handleSubmitPublishApproval = async () => { + if (!botInfo.value) { + return; + } + if (isAiResourceApprovalPending(botInfo.value.publishStatus)) { + ElMessage.warning($t('bot.publishPendingHint')); + return; + } + try { + await ElMessageBox.confirm( + $t('bot.submitPublishApprovalConfirm'), + $t('message.noticeTitle'), + { + confirmButtonText: $t('button.confirm'), + cancelButtonText: $t('button.cancel'), + type: 'info', + }, + ); + } catch { + return; + } + const res = await submitBotPublishApproval(String(botInfo.value.id)); + if (res.errorCode === 0) { + ElMessage.success(res.message || $t('message.saveOkMessage')); + getBotDetail(); + } else { + ElMessage.error(res.message || $t('message.saveFailMessage')); + } +}; +const handleSubmitDeleteApproval = async () => { + if (!botInfo.value) { + return; + } + if (isAiResourceApprovalPending(botInfo.value.publishStatus)) { + ElMessage.warning($t('bot.deletePendingHint')); + return; + } + try { + await ElMessageBox.confirm( + $t('bot.submitDeleteApprovalConfirm'), + $t('message.noticeTitle'), + { + confirmButtonText: $t('button.confirm'), + cancelButtonText: $t('button.cancel'), + type: 'warning', + }, + ); + } catch { + return; + } + const res = await submitBotDeleteApproval(String(botInfo.value.id)); + if (res.errorCode === 0) { + ElMessage.success(res.message || $t('message.saveOkMessage')); + getBotDetail(); + } else { + ElMessage.error(res.message || $t('message.saveFailMessage')); + } +}; const handleUpdatePublishWx = () => { api .post('/api/v1/bot/updateOptions', { @@ -1352,6 +1463,44 @@ const handleBasicInfoChange = async (

{{ $t('bot.publish') }}

+
+
+
+ {{ $t('bot.publishStatusLabel') }} +
+
+ + {{ publishStatusMeta.label }} + + + #{{ botInfo.currentApprovalInstanceId }} + +
+

+ {{ publishStatusMeta.description }} +

+
+
+ + {{ publishPrimaryActionLabel }} + + + {{ $t('button.submitDeleteApproval') }} + +
+
@@ -1428,12 +1577,23 @@ const handleBasicInfoChange = async ( +
+ +
@@ -1441,7 +1601,12 @@ const handleBasicInfoChange = async ( {{ $t('bot.copyLink') }} - + @@ -1477,6 +1642,7 @@ const handleBasicInfoChange = async ( @@ -1511,6 +1677,7 @@ const handleBasicInfoChange = async ( width="730" ref="knowledgeDataRef" page-url="/api/v1/documentCollection/page" + :extra-query-params="{ publishedOnly: true }" @get-data="confirmUpdateAiBotKnowledge" /> @@ -1520,6 +1687,7 @@ const handleBasicInfoChange = async ( width="730" ref="workflowDataRef" page-url="/api/v1/workflow/page" + :extra-query-params="{ publishedOnly: true }" @get-data="confirmUpdateAiBotWorkflow" /> @@ -1643,6 +1811,58 @@ const handleBasicInfoChange = async ( background-color: var(--bot-collapse-itme-back); } +.publish-summary-card { + display: flex; + flex-wrap: wrap; + gap: 16px; + align-items: center; + justify-content: space-between; + padding: 16px; + background: hsl(var(--surface-subtle) / 78%); + border: 1px solid hsl(var(--line-subtle)); + border-radius: 16px; +} + +.publish-summary-main { + display: flex; + flex: 1; + flex-direction: column; + gap: 8px; + min-width: 220px; +} + +.publish-summary-label { + font-size: 13px; + font-weight: 600; + color: hsl(var(--muted-foreground)); +} + +.publish-summary-row { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; +} + +.publish-summary-instance { + font-size: 12px; + color: hsl(var(--muted-foreground)); +} + +.publish-summary-desc { + margin: 0; + font-size: 13px; + line-height: 1.6; + color: hsl(var(--text-secondary)); +} + +.publish-summary-actions { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; +} + .publish-wx { display: flex; align-items: center; diff --git a/easyflow-ui-admin/app/src/views/ai/documentCollection/DocumentCollection.vue b/easyflow-ui-admin/app/src/views/ai/documentCollection/DocumentCollection.vue index d9ef487..c58e1b0 100644 --- a/easyflow-ui-admin/app/src/views/ai/documentCollection/DocumentCollection.vue +++ b/easyflow-ui-admin/app/src/views/ai/documentCollection/DocumentCollection.vue @@ -44,6 +44,12 @@ import CardPage from '#/components/page/CardList.vue'; import PageData from '#/components/page/PageData.vue'; import PageSide from '#/components/page/PageSide.vue'; import DocumentCollectionModal from '#/views/ai/documentCollection/DocumentCollectionModal.vue'; +import AiResourceCornerMeta from '#/views/ai/shared/AiResourceCornerMeta.vue'; +import { + isAiResourceApprovalPending, + isAiResourcePublished, + normalizeAiPublishStatus, +} from '#/views/ai/shared/publish-status'; const router = useRouter(); const userStore = useUserStore(); @@ -178,16 +184,31 @@ const actions: ActionButton[] = [ }, }, { - text: $t('button.delete'), - icon: Delete, - tone: 'danger', - permission: '/api/v1/documentCollection/remove', + icon: Promotion, + text: (row) => + isAiResourcePublished(row.publishStatus) + ? $t('button.republish') + : $t('button.submitPublishApproval'), + permission: '/api/v1/documentCollection/save', placement: 'inline', onClick(row) { if (!ensureManageKnowledgeItem(row)) { return; } - handleDelete(row); + submitPublishApproval(row); + }, + }, + { + text: $t('button.submitDeleteApproval'), + icon: Delete, + tone: 'danger', + permission: '/api/v1/documentCollection/remove', + placement: 'menu', + onClick(row) { + if (!ensureManageKnowledgeItem(row)) { + return; + } + submitDeleteApproval(row); }, }, ]; @@ -195,24 +216,92 @@ const actions: ActionButton[] = [ onMounted(() => { getCategoryList(); }); -const handleDelete = (item: any) => { - ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), { - confirmButtonText: $t('message.ok'), - cancelButtonText: $t('message.cancel'), - type: 'warning', - }) - .then(() => { - api - .post('/api/v1/documentCollection/remove', { id: item.id }) - .then((res) => { - if (res.errorCode === 0) { - ElMessage.success($t('message.deleteOkMessage')); - pageDataRef.value.setQuery({}); - } - }); - }) - .catch(() => {}); +const submitPublishApproval = async (item: any) => { + if (isAiResourceApprovalPending(item.publishStatus)) { + ElMessage.warning($t('documentCollection.publishPendingHint')); + return; + } + try { + await ElMessageBox.confirm( + $t('documentCollection.submitPublishApprovalConfirm'), + $t('message.noticeTitle'), + { + confirmButtonText: $t('button.confirm'), + cancelButtonText: $t('button.cancel'), + type: 'info', + }, + ); + } catch { + return; + } + const res = await api.post( + '/api/v1/documentCollection/submitPublishApproval', + { + id: item.id, + }, + ); + if (res.errorCode === 0) { + ElMessage.success(res.message || $t('message.saveOkMessage')); + reloadKnowledgeList(); + } }; +const submitDeleteApproval = async (item: any) => { + if (isAiResourceApprovalPending(item.publishStatus)) { + ElMessage.warning($t('documentCollection.deletePendingHint')); + return; + } + try { + await ElMessageBox.confirm( + $t('documentCollection.submitDeleteApprovalConfirm'), + $t('message.noticeTitle'), + { + confirmButtonText: $t('button.confirm'), + cancelButtonText: $t('button.cancel'), + type: 'warning', + }, + ); + } catch { + return; + } + const res = await api.post( + '/api/v1/documentCollection/submitDeleteApproval', + { + id: item.id, + }, + ); + if (res.errorCode === 0) { + ElMessage.success(res.message || $t('message.saveOkMessage')); + reloadKnowledgeList(); + } +}; +function resolvePublishStatusMeta(status?: string) { + switch (normalizeAiPublishStatus(status)) { + case 'DELETE_PENDING': { + return { + label: $t('documentCollection.publishStatusDeletePending'), + tone: 'danger', + }; + } + case 'PUBLISH_PENDING': { + return { + label: $t('documentCollection.publishStatusPublishPending'), + tone: 'pending', + }; + } + case 'PUBLISHED': { + return { + label: $t('documentCollection.publishStatusPublished'), + tone: 'published', + }; + } + default: { + return { + label: $t('documentCollection.publishStatusDraft'), + tone: 'draft', + }; + } + } +} const pageDataRef = ref(); const aiKnowledgeModalRef = ref(); @@ -460,21 +549,97 @@ function changeCategory(category: any) { :tag-map="collectionTypeLabelMap" > @@ -600,6 +710,54 @@ function changeCategory(category: any) { diff --git a/easyflow-ui-admin/app/src/views/ai/shared/publish-status.ts b/easyflow-ui-admin/app/src/views/ai/shared/publish-status.ts new file mode 100644 index 0000000..515d357 --- /dev/null +++ b/easyflow-ui-admin/app/src/views/ai/shared/publish-status.ts @@ -0,0 +1,51 @@ +export type AiPublishStatus = + | 'DELETE_PENDING' + | 'DRAFT' + | 'PUBLISHED' + | 'PUBLISH_PENDING'; + +/** + * 规范化发布状态,避免页面散落默认值判断。 + */ +export function normalizeAiPublishStatus( + value?: null | string, +): AiPublishStatus { + switch (value) { + case 'PUBLISHED': + case 'PUBLISH_PENDING': + case 'DELETE_PENDING': + return value; + default: + return 'DRAFT'; + } +} + +/** + * 当前资源是否已有正式线上版本。 + */ +export function isAiResourcePublished(value?: null | string) { + return normalizeAiPublishStatus(value) === 'PUBLISHED'; +} + +/** + * 当前资源是否允许对外可见。 + */ +export function isAiResourceExternallyVisible(value?: null | string) { + const normalized = normalizeAiPublishStatus(value); + return normalized === 'PUBLISHED' || normalized === 'DELETE_PENDING'; +} + +/** + * 当前资源是否允许作为新的 Bot 引用候选。 + */ +export function isAiResourceSelectableForBot(value?: null | string) { + return normalizeAiPublishStatus(value) === 'PUBLISHED'; +} + +/** + * 当前资源是否处于审批处理中。 + */ +export function isAiResourceApprovalPending(value?: null | string) { + const normalized = normalizeAiPublishStatus(value); + return normalized === 'PUBLISH_PENDING' || normalized === 'DELETE_PENDING'; +} diff --git a/easyflow-ui-admin/app/src/views/ai/workflow/WorkflowDesign.vue b/easyflow-ui-admin/app/src/views/ai/workflow/WorkflowDesign.vue index fe3932c..84de27a 100644 --- a/easyflow-ui-admin/app/src/views/ai/workflow/WorkflowDesign.vue +++ b/easyflow-ui-admin/app/src/views/ai/workflow/WorkflowDesign.vue @@ -5,15 +5,30 @@ import { useRoute } from 'vue-router'; import { usePreferences } from '@easyflow/preferences'; import { getOptions, sortNodes } from '@easyflow/utils'; -import { ArrowLeft, CircleCheck, Close } from '@element-plus/icons-vue'; +import { + ArrowLeft, + CircleCheck, + Close, + Promotion, +} from '@element-plus/icons-vue'; import { Tinyflow } from '@tinyflow-ai/vue'; -import { ElButton, ElDrawer, ElMessage, ElSkeleton } from 'element-plus'; +import { + ElButton, + ElDrawer, + ElMessage, + ElMessageBox, + ElSkeleton, +} from 'element-plus'; import { api } from '#/api/request'; import CommonSelectDataModal from '#/components/commonSelectModal/CommonSelectDataModal.vue'; import { $t } from '#/locales'; import { router } from '#/router'; import { getIconByValue } from '#/views/ai/model/modelUtils/defaultIcon'; +import { + isAiResourceApprovalPending, + normalizeAiPublishStatus, +} from '#/views/ai/shared/publish-status'; import ExecResult from '#/views/ai/workflow/components/ExecResult.vue'; import SingleRun from '#/views/ai/workflow/components/SingleRun.vue'; import WorkflowForm from '#/views/ai/workflow/components/WorkflowForm.vue'; @@ -135,6 +150,7 @@ const customNode = ref(); const showTinyFlow = ref(false); const saveLoading = ref(false); const checkLoading = ref(false); +const publishLoading = ref(false); const checkIssuesVisible = ref(false); const checkResult = ref(null); const checkContentSnapshot = ref(null); @@ -234,6 +250,30 @@ const pluginSelectRef = ref(); const updatePluginNode = ref(null); const pageLoading = ref(false); const chainInfo = ref(null); +const publishActionText = computed(() => { + switch (normalizeAiPublishStatus(workflowInfo.value?.publishStatus)) { + case 'DELETE_PENDING': { + return $t('aiWorkflow.publishStatusDeletePending'); + } + case 'PUBLISH_PENDING': { + return $t('aiWorkflow.publishStatusPublishPending'); + } + case 'PUBLISHED': { + return `${$t('aiWorkflow.publishStatusPublished')} · ${$t('button.republish')}`; + } + default: { + return `${$t('aiWorkflow.publishStatusDraft')} · ${$t('button.submitPublishApproval')}`; + } + } +}); +const publishActionDisabled = computed( + () => + !workflowId.value || + saveLoading.value || + checkLoading.value || + publishLoading.value || + isAiResourceApprovalPending(workflowInfo.value?.publishStatus), +); function syncNavTitle(title: string) { if (!title) { @@ -458,6 +498,44 @@ function closeCheckIssues() { async function handleCheck() { await runCheck('PRE_EXECUTE'); } +async function handlePublish() { + if (publishLoading.value) { + return; + } + if (isAiResourceApprovalPending(workflowInfo.value?.publishStatus)) { + ElMessage.warning($t('aiWorkflow.publishPendingHint')); + return; + } + try { + await ElMessageBox.confirm( + $t('aiWorkflow.submitPublishApprovalConfirm'), + $t('message.noticeTitle'), + { + confirmButtonText: $t('button.confirm'), + cancelButtonText: $t('button.cancel'), + type: 'info', + }, + ); + } catch { + return; + } + const saved = await handleSave(); + if (!saved) { + return; + } + publishLoading.value = true; + try { + const res = await api.post('/api/v1/workflow/submitPublishApproval', { + id: workflowId.value, + }); + if (res.errorCode === 0) { + ElMessage.success(res.message || $t('message.saveOkMessage')); + await getWorkflowInfo(workflowId.value); + } + } finally { + publishLoading.value = false; + } +} function onSubmit() { initState.value = !initState.value; } @@ -584,7 +662,7 @@ function onAsyncExecute(info: any) {
@@ -592,11 +670,21 @@ function onAsyncExecute(info: any) { {{ $t('button.save') }}(ctrl+s) + + {{ publishActionText }} +
+ isAiResourcePublished(row.publishStatus) + ? $t('button.republish') + : $t('button.submitPublishApproval'), + permission: '/api/v1/workflow/save', placement: 'inline', onClick: (row: any) => { - remove(row); + submitPublishApproval(row); + }, + }, + { + icon: Delete, + text: $t('button.submitDeleteApproval'), + tone: 'danger', + permission: '/api/v1/workflow/remove', + placement: 'menu', + onClick: (row: any) => { + submitDeleteApproval(row); }, }, ]; @@ -263,32 +282,85 @@ function showDialog(row: any, importMode = false) { function resolveNavTitle(row: any) { return row?.title || row?.name || ''; } -function remove(row: any) { - ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), { - confirmButtonText: $t('message.ok'), - cancelButtonText: $t('message.cancel'), - type: 'warning', - beforeClose: (action, instance, done) => { - if (action === 'confirm') { - instance.confirmButtonLoading = true; - api - .post('/api/v1/workflow/remove', { id: row.id }) - .then((res) => { - instance.confirmButtonLoading = false; - if (res.errorCode === 0) { - ElMessage.success(res.message); - reset(); - done(); - } - }) - .catch(() => { - instance.confirmButtonLoading = false; - }); - } else { - done(); - } - }, - }).catch(() => {}); +async function submitPublishApproval(row: any) { + if (isAiResourceApprovalPending(row.publishStatus)) { + ElMessage.warning($t('aiWorkflow.publishPendingHint')); + return; + } + try { + await ElMessageBox.confirm( + $t('aiWorkflow.submitPublishApprovalConfirm'), + $t('message.noticeTitle'), + { + confirmButtonText: $t('button.confirm'), + cancelButtonText: $t('button.cancel'), + type: 'info', + }, + ); + } catch { + return; + } + const res = await api.post('/api/v1/workflow/submitPublishApproval', { + id: row.id, + }); + if (res.errorCode === 0) { + ElMessage.success(res.message || $t('message.saveOkMessage')); + pageDataRef.value?.reload?.(); + } +} +async function submitDeleteApproval(row: any) { + if (isAiResourceApprovalPending(row.publishStatus)) { + ElMessage.warning($t('aiWorkflow.deletePendingHint')); + return; + } + try { + await ElMessageBox.confirm( + $t('aiWorkflow.submitDeleteApprovalConfirm'), + $t('message.noticeTitle'), + { + confirmButtonText: $t('button.confirm'), + cancelButtonText: $t('button.cancel'), + type: 'warning', + }, + ); + } catch { + return; + } + const res = await api.post('/api/v1/workflow/submitDeleteApproval', { + id: row.id, + }); + if (res.errorCode === 0) { + ElMessage.success(res.message || $t('message.saveOkMessage')); + pageDataRef.value?.reload?.(); + } +} +function resolvePublishStatusMeta(status?: string) { + switch (normalizeAiPublishStatus(status)) { + case 'DELETE_PENDING': { + return { + label: $t('aiWorkflow.publishStatusDeletePending'), + tone: 'danger', + }; + } + case 'PUBLISH_PENDING': { + return { + label: $t('aiWorkflow.publishStatusPublishPending'), + tone: 'pending', + }; + } + case 'PUBLISHED': { + return { + label: $t('aiWorkflow.publishStatusPublished'), + tone: 'published', + }; + } + default: { + return { + label: $t('aiWorkflow.publishStatusDraft'), + tone: 'draft', + }; + } + } } function toDesignPage(row: any) { router.push({ @@ -496,21 +568,97 @@ function handleHeaderButtonClick(data: any) { :actions="actions" > @@ -631,18 +724,68 @@ function handleHeaderButtonClick(data: any) { diff --git a/easyflow-ui-admin/app/src/views/system/approval/ApprovalFlowModal.vue b/easyflow-ui-admin/app/src/views/system/approval/ApprovalFlowModal.vue new file mode 100644 index 0000000..a0bd5b9 --- /dev/null +++ b/easyflow-ui-admin/app/src/views/system/approval/ApprovalFlowModal.vue @@ -0,0 +1,792 @@ + + + + + diff --git a/easyflow-ui-admin/app/src/views/system/approval/ApprovalManage.vue b/easyflow-ui-admin/app/src/views/system/approval/ApprovalManage.vue new file mode 100644 index 0000000..4335eb3 --- /dev/null +++ b/easyflow-ui-admin/app/src/views/system/approval/ApprovalManage.vue @@ -0,0 +1,1134 @@ + + + + + diff --git a/easyflow-ui-admin/app/src/views/system/approval/components/BotApprovalSnapshotPreview.vue b/easyflow-ui-admin/app/src/views/system/approval/components/BotApprovalSnapshotPreview.vue new file mode 100644 index 0000000..3e2c4a7 --- /dev/null +++ b/easyflow-ui-admin/app/src/views/system/approval/components/BotApprovalSnapshotPreview.vue @@ -0,0 +1,468 @@ + + + + + diff --git a/easyflow-ui-admin/app/src/views/system/approval/components/KnowledgeApprovalSnapshotPreview.vue b/easyflow-ui-admin/app/src/views/system/approval/components/KnowledgeApprovalSnapshotPreview.vue new file mode 100644 index 0000000..cace2e8 --- /dev/null +++ b/easyflow-ui-admin/app/src/views/system/approval/components/KnowledgeApprovalSnapshotPreview.vue @@ -0,0 +1,372 @@ + + + + + diff --git a/easyflow-ui-admin/app/src/views/system/approval/components/WorkflowApprovalSnapshotPreview.vue b/easyflow-ui-admin/app/src/views/system/approval/components/WorkflowApprovalSnapshotPreview.vue new file mode 100644 index 0000000..90df86e --- /dev/null +++ b/easyflow-ui-admin/app/src/views/system/approval/components/WorkflowApprovalSnapshotPreview.vue @@ -0,0 +1,157 @@ + + + + + diff --git a/easyflow-ui-admin/packages/effects/common-ui/src/components/icon-picker/icon-picker.vue b/easyflow-ui-admin/packages/effects/common-ui/src/components/icon-picker/icon-picker.vue index 86c34fb..a4eebe1 100644 --- a/easyflow-ui-admin/packages/effects/common-ui/src/components/icon-picker/icon-picker.vue +++ b/easyflow-ui-admin/packages/effects/common-ui/src/components/icon-picker/icon-picker.vue @@ -90,6 +90,7 @@ watchDebounced( 'svg:talk', 'svg:plugin', 'svg:workflow', + 'svg:approval', 'svg:knowledge', 'svg:resource', 'svg:data-center', diff --git a/easyflow-ui-admin/packages/icons/src/svg/icons/approval.svg b/easyflow-ui-admin/packages/icons/src/svg/icons/approval.svg new file mode 100644 index 0000000..b31799a --- /dev/null +++ b/easyflow-ui-admin/packages/icons/src/svg/icons/approval.svg @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/easyflow-ui-admin/packages/icons/src/svg/index.ts b/easyflow-ui-admin/packages/icons/src/svg/index.ts index fbd3086..52edc90 100644 --- a/easyflow-ui-admin/packages/icons/src/svg/index.ts +++ b/easyflow-ui-admin/packages/icons/src/svg/index.ts @@ -21,6 +21,7 @@ const SvgTDesignIcon = createIconifyIcon('svg:tdesign-logo'); const SvgTalkIcon = createIconifyIcon('svg:talk'); const SvgPluginIcon = createIconifyIcon('svg:plugin'); const SvgWorkflowIcon = createIconifyIcon('svg:workflow'); +const SvgApprovalIcon = createIconifyIcon('svg:approval'); const SvgKnowledgeIcon = createIconifyIcon('svg:knowledge'); const SvgResourceIcon = createIconifyIcon('svg:resource'); const SvgDataCenterIcon = createIconifyIcon('svg:data-center'); @@ -36,6 +37,7 @@ const SvgApiIcon = createIconifyIcon('svg:api'); export { SvgAccountIcon, + SvgApprovalIcon, SvgAntdvLogoIcon, SvgApiIcon, SvgAvatar1Icon, diff --git a/easyflow-ui-admin/packages/tinyflow-ui/src/Tinyflow.ts b/easyflow-ui-admin/packages/tinyflow-ui/src/Tinyflow.ts index 9c4a0b8..89eff08 100644 --- a/easyflow-ui-admin/packages/tinyflow-ui/src/Tinyflow.ts +++ b/easyflow-ui-admin/packages/tinyflow-ui/src/Tinyflow.ts @@ -139,6 +139,18 @@ export class Tinyflow { return true; } + async fitView(options?: { duration?: number; padding?: number }) { + const flow = this._getFlowInstance(); + if (!flow) { + return false; + } + await flow.fitView({ + duration: options?.duration ?? 220, + padding: options?.padding ?? 0.2, + }); + return true; + } + setTheme(theme: TinyflowTheme) { this.options.theme = theme; if (this.tinyflowEl) { diff --git a/easyflow-ui-admin/packages/tinyflow-ui/src/components/TinyflowComponent.svelte b/easyflow-ui-admin/packages/tinyflow-ui/src/components/TinyflowComponent.svelte index 633baec..784adc1 100644 --- a/easyflow-ui-admin/packages/tinyflow-ui/src/components/TinyflowComponent.svelte +++ b/easyflow-ui-admin/packages/tinyflow-ui/src/components/TinyflowComponent.svelte @@ -20,6 +20,7 @@ } = $props(); let { data } = options; + let initialViewport = null; if (typeof data === 'string') { try { @@ -28,7 +29,12 @@ console.error('Invalid JSON data:', data); } } - store.init((data as TinyflowData)?.nodes || [], (data as TinyflowData)?.edges || []); + initialViewport = (data as TinyflowData)?.viewport || null; + store.init( + (data as TinyflowData)?.nodes || [], + (data as TinyflowData)?.edges || [], + initialViewport, + ); setContext('tinyflow_options', options); diff --git a/easyflow-ui-admin/packages/tinyflow-ui/src/components/TinyflowCore.svelte b/easyflow-ui-admin/packages/tinyflow-ui/src/components/TinyflowCore.svelte index d3e37a4..64cb37e 100644 --- a/easyflow-ui-admin/packages/tinyflow-ui/src/components/TinyflowCore.svelte +++ b/easyflow-ui-admin/packages/tinyflow-ui/src/components/TinyflowCore.svelte @@ -57,9 +57,11 @@ let flowRootEl = $state(null); let inlineNodePickerEl = $state(null); let connectStartPoint = $state<{ x: number; y: number } | null>(null); - let canvasLocked = $state(false); const asString = (value: unknown) => (value == null ? '' : String(value)); const options = getOptions(); + const readonly = options.readonly === true; + let canvasLocked = $state(readonly); + const hideBottomDock = options.hideBottomDock === true; const availableNodes = getAvailableNodes(options); const onRunTest = options.onRunTest; @@ -567,6 +569,9 @@ }; function handleGlobalPointerDown(event: PointerEvent) { + if (readonly) { + return; + } if (!nodePickerVisible || !inlineNodePickerEl) { return; } @@ -579,15 +584,19 @@ onMount(() => { store.updateEdges((edges) => edges.map((edge) => ensureEdgeVisualDefaults(edge))); - window.addEventListener('keydown', handleKeyDown); - window.addEventListener('paste', handleGlobalPaste); - window.addEventListener('pointerdown', handleGlobalPointerDown); + if (!readonly) { + window.addEventListener('keydown', handleKeyDown); + window.addEventListener('paste', handleGlobalPaste); + window.addEventListener('pointerdown', handleGlobalPointerDown); + } }); onDestroy(() => { - window.removeEventListener('keydown', handleKeyDown); - window.removeEventListener('paste', handleGlobalPaste); - window.removeEventListener('pointerdown', handleGlobalPointerDown); + if (!readonly) { + window.removeEventListener('keydown', handleKeyDown); + window.removeEventListener('paste', handleGlobalPaste); + window.removeEventListener('pointerdown', handleGlobalPointerDown); + } }); const customNodeTypes = { @@ -630,24 +639,30 @@ nodesDraggable={!canvasLocked} nodesConnectable={!canvasLocked} elementsSelectable={!canvasLocked} - panOnDrag={!canvasLocked} - zoomOnScroll={!canvasLocked} - zoomOnDoubleClick={!canvasLocked} - ondrop={onDrop} - ondragover={onDragOver} + panOnDrag={readonly ? true : !canvasLocked} + zoomOnScroll={readonly ? true : !canvasLocked} + zoomOnDoubleClick={readonly ? true : !canvasLocked} + ondrop={readonly ? undefined : onDrop} + ondragover={readonly ? undefined : onDragOver} isValidConnection={isValidConnection} - onconnectend={onconnectend} - onconnectstart={onconnectstart} - onconnect={onconnect} + onconnectend={readonly ? undefined : onconnectend} + onconnectstart={readonly ? undefined : onconnectstart} + onconnect={readonly ? undefined : onconnect} connectionRadius={50} connectionLineComponent={FlowConnectionLine} onedgeclick={(e) => { + if (readonly) { + return; + } showEdgePanel = true; currentEdge = e.edge; }} onbeforeconnect={(edge: any) => normalizeEdgeBeforeConnect(edge)} - ondelete={onDelete} + ondelete={readonly ? undefined : onDelete} onclick={(e) => { + if (readonly) { + return; + } const el = e.target as HTMLElement; if (el.classList.contains("svelte-flow__edge-interaction") || el.classList.contains('panel-content') @@ -750,6 +765,7 @@
{/if} + {#if !hideBottomDock}
@@ -818,6 +834,7 @@ {/if}
+ {/if}