From aed3f25f81412a406d5ea9e4294acccdb413c83b Mon Sep 17 00:00:00 2001 From: wangli <274027703@qq.com> Date: Tue, 10 Sep 2024 14:46:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(REQ-2924)=20-=20Starter=20=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E6=8E=A7=E5=88=B6=E6=97=A7=E7=89=88=E6=9C=AC=20Feign?= =?UTF-8?q?=20=E6=8E=A5=E5=8F=A3=20Bean=20=E6=B3=A8=E5=86=8C=EF=BC=8C?= =?UTF-8?q?=E9=81=BF=E5=85=8D=E7=89=88=E6=9C=AC=E5=8D=87=E7=BA=A7=E6=97=B6?= =?UTF-8?q?=E9=9C=80=E5=BC=BA=E5=88=B6=E4=BF=AE=E6=94=B9=E5=8E=9F=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StarterFeignClientConfiguration.java | 39 + ...orkflowEngineStarterAutoConfiguration.java | 4 +- .../WorkflowEngineStarterProperties.java | 12 + .../starter/api/WorkflowCoreService.java | 337 +++++++- .../starter/api/WorkflowManageService.java | 719 +++++++++++++++++- .../MetaFeignClientEnableSelector.java | 320 ++++++++ 6 files changed, 1422 insertions(+), 9 deletions(-) create mode 100644 workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/selector/MetaFeignClientEnableSelector.java diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/StarterFeignClientConfiguration.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/StarterFeignClientConfiguration.java index 0c6b6e763..91d071282 100644 --- a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/StarterFeignClientConfiguration.java +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/StarterFeignClientConfiguration.java @@ -1,10 +1,48 @@ package cn.axzo.workflow.starter; +import cn.axzo.workflow.client.annotation.WorkflowEngineFeignClient; import cn.axzo.workflow.starter.api.WorkflowCoreService; import cn.axzo.workflow.starter.api.WorkflowManageService; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.config.BeanExpressionContext; +import org.springframework.beans.factory.config.BeanExpressionResolver; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.cloud.openfeign.FeignClientFactoryBean; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.env.Environment; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; /** * 根据参数动态实例化可用 Bean @@ -25,4 +63,5 @@ public class StarterFeignClientConfiguration { @EnableFeignClients(clients = WorkflowManageService.class) public static class WorkflowManageServiceClient { } + } diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/WorkflowEngineStarterAutoConfiguration.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/WorkflowEngineStarterAutoConfiguration.java index 232c50771..466f464d9 100644 --- a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/WorkflowEngineStarterAutoConfiguration.java +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/WorkflowEngineStarterAutoConfiguration.java @@ -25,6 +25,7 @@ import cn.axzo.workflow.starter.mq.broadcast.consumer.InnerTaskEventListener; import cn.axzo.workflow.starter.mq.broadcast.consumer.InnerWorkflowListener; import cn.axzo.workflow.starter.mq.monitor.WorkflowEngineStarterDefaultMQMonitor; import cn.axzo.workflow.starter.mq.monitor.console.WorkflowEngineStarterMQMonitorController; +import cn.axzo.workflow.starter.selector.MetaFeignClientEnableSelector; import org.apache.commons.lang3.StringUtils; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.common.MixAll; @@ -51,7 +52,7 @@ import java.util.List; */ @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(WorkflowEngineStarterProperties.class) -@Import({StarterFeignClientConfiguration.class, StarterBroadcastMQConfiguration.class, StarterRPCInvokeMQConfiguration.class}) +@Import({StarterFeignClientConfiguration.class, StarterBroadcastMQConfiguration.class, StarterRPCInvokeMQConfiguration.class, MetaFeignClientEnableSelector.class}) public class WorkflowEngineStarterAutoConfiguration { private final Logger log = LoggerFactory.getLogger(WorkflowEngineStarterAutoConfiguration.class); @@ -147,4 +148,5 @@ public class WorkflowEngineStarterAutoConfiguration { Environment environment) { return new WorkflowEngineStarterDefaultMQMonitor(mqAdminExtObjectProvider, broadcastDLQProcessorObjectProvider, rpcDLQProcessorObjectProvider, workflowEngineStarterProperties, environment); } + } diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/WorkflowEngineStarterProperties.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/WorkflowEngineStarterProperties.java index 584d75484..9d103a013 100644 --- a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/WorkflowEngineStarterProperties.java +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/WorkflowEngineStarterProperties.java @@ -18,6 +18,10 @@ import static cn.axzo.workflow.common.enums.RpcInvokeModeEnum.ASYNC; */ @ConfigurationProperties(prefix = "workflow.engine.starter") public class WorkflowEngineStarterProperties { + /** + * + */ + private Boolean metaFeign = false; /** * 特殊用途,不建议接入方使用 */ @@ -84,6 +88,14 @@ public class WorkflowEngineStarterProperties { */ private Boolean alert = false; + public Boolean getMetaFeign() { + return metaFeign; + } + + public void setMetaFeign(Boolean metaFeign) { + this.metaFeign = metaFeign; + } + public Boolean getManageable() { return manageable; } diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/api/WorkflowCoreService.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/api/WorkflowCoreService.java index 92b09c7e4..9eb987904 100644 --- a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/api/WorkflowCoreService.java +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/api/WorkflowCoreService.java @@ -1,10 +1,339 @@ package cn.axzo.workflow.starter.api; +import cn.axzo.workflow.starter.feign.ext.WorkflowEngineStarterFeignConfiguration; +import cn.axzo.workflow.common.util.ThreadUtil; +import static cn.axzo.workflow.common.enums.RpcInvokeModeEnum.ASYNC; +import static cn.axzo.workflow.common.enums.RpcInvokeModeEnum.SYNC; +import cn.axzo.workflow.client.annotation.WorkflowEngineFeignClient; +import cn.axzo.workflow.common.model.request.bpmn.task.BpmnActivitySetAssigneeDTO; +import cn.axzo.workflow.common.model.request.bpmn.task.BpmnActivityTriggerDTO; +import cn.azxo.framework.common.model.CommonResponse; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import javax.validation.constraints.NotBlank; +import cn.axzo.workflow.common.annotation.InvokeMode; +import cn.axzo.workflow.common.annotation.Manageable; +import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceAbortDTO; +import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceAdminPageReqVO; +import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceCancelDTO; +import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceCarbonCopyDTO; +import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceCheckApproverDTO; +import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceCreateDTO; +import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceCreateWithFormDTO; +import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceLogQueryDTO; +import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceMyPageReqVO; +import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceQueryDTO; +import cn.axzo.workflow.common.model.response.BpmPageResult; +import cn.axzo.workflow.common.model.response.bpmn.BatchOperationResultVO; +import cn.axzo.workflow.common.model.response.bpmn.process.BpmnProcessInstanceAdminPageItemVO; +import cn.axzo.workflow.common.model.response.bpmn.process.BpmnProcessInstanceLogVO; +import cn.axzo.workflow.common.model.response.bpmn.process.BpmnProcessInstancePageItemVO; +import cn.axzo.workflow.common.model.response.bpmn.process.BpmnProcessInstanceVO; +import cn.axzo.workflow.common.model.response.bpmn.process.ProcessNodeDetailVO; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PutMapping; +import javax.annotation.Nullable; +import javax.validation.constraints.NotNull; +import java.util.List; +import java.util.Map; +import cn.axzo.workflow.common.model.request.bpmn.task.BpmnOptionalNodeDTO; +import cn.axzo.workflow.common.model.request.bpmn.task.BpmnRobotTaskCompleteDTO; +import cn.axzo.workflow.common.model.request.bpmn.task.BpmnRobotTaskCreateDTO; +import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskAttachmentDTO; +import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskAuditDTO; +import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskBackAuditDTO; +import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskCommentDTO; +import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskCountersignDTO; +import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskPageSearchDTO; +import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskRemindDTO; +import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskTransferDTO; +import cn.axzo.workflow.common.model.response.bpmn.task.BpmnHistoricTaskInstanceGroupVO; +import cn.axzo.workflow.common.model.response.bpmn.task.BpmnHistoricTaskInstanceVO; +import cn.axzo.workflow.common.model.response.bpmn.task.BpmnTaskDonePageItemVO; +import cn.axzo.workflow.common.model.response.bpmn.task.BpmnTaskInstanceVO; +import cn.axzo.workflow.common.model.response.bpmn.task.BpmnTaskTodoPageItemVO; +import javax.validation.constraints.NotEmpty; + /** - * just for compiler - * - * @author wangli - * @since 2024-09-09 18:25 + * Workflow Engine Starter Core Service + *

+ * 该类是根据 API 动态生成,不同版本可能会开放新的接口,或回收一些旧接口 */ +@org.springframework.cloud.openfeign.FeignClient(name = "workflow-engine-starter-core", url = "${axzo.service.workflow-engine.starter:http://workflow-engine:8080}", configuration = WorkflowEngineStarterFeignConfiguration.class) public interface WorkflowCoreService { + + /** + * 业务节点唤醒, 该节点废弃,请换成 {@link ProcessActivityApi#trigger(cn.axzo.workflow.common.model.request.bpmn.task.BpmnActivityTriggerDTO)} 接口 + *

+ * 当模型中使用了“业务节点”,且设置了“不设置审批人”模式,则当业务监听到 PROCESS_ACTIVITY_START 事件时,可通过该接口推动流程继续运行 + */ + @Deprecated + @GetMapping("/api/process/activity/trigger") + Boolean trigger(@NotBlank(message = "触发 ID 不能为空") @RequestParam String triggerId); + + /** + * 业务节点唤醒 + * + * @param dto + * @return + */ + @PostMapping("/api/process/activity/trigger") + Boolean trigger(@Validated @RequestBody BpmnActivityTriggerDTO dto); + + /** + * 业务节点设置审批人, 不支持重复设置 + *

+ * 当模型中使用了“业务节点”,且设置了“业务指定审批人”模式,则当业务监听到 PROCESS_ACTIVITY_WAIT_ASSIGNEE 事件时,可通过该接口设置动态设置审批人 + *

+ * 注意:如果调用接口时,传入的审批人集合为空,流程引擎将对该审批流程实例自动中止。 + * + * @param dto + * @return + */ + @PostMapping("/api/process/activity/assignee/set") + @Operation(summary = "业务节点设置审批人,不支持重复调用设置审批人,需一次性传入所有审批人") + Boolean setAssignee(@Validated @RequestBody BpmnActivitySetAssigneeDTO dto); + + /** + * 创建审批流程 + * + *

+     *   MQ 触发规则:
+     *     1. 当前流程实例会依次触发 process-instance-created 和 process-instance-started 事件
+     *     2. 第一个审批任务会依次触发 process-task-assigned 和 process-task-created 事件
+     * 
+ * + * @param dto {@link BpmnProcessInstanceCreateDTO} + */ + @Operation(summary = "创建审批流程, MQ 触发规则:1. 当前流程实例会依次触发 process-instance-created 和 process-instance-started 事件,2. 第一个审批任务会依次触发 process-task-assigned 和 process-task-created 事件") + @PostMapping("/api/process/instance/create") + @InvokeMode(SYNC) + String createProcessInstance(@Validated @RequestBody BpmnProcessInstanceCreateDTO dto); + + /** + * 发起人主动撤回审核 + * + *
+     *   MQ 触发规则:
+     *     1. 当前流程实例中现存的任务会依次触发 process-task-deleted 事件
+     *     2. 当前流程实例会触发 process-instance-cancelled 事件
+     * 
+ * + * @param dto {@link BpmnProcessInstanceCancelDTO} + * @return + */ + @Operation(summary = "发起人主动撤回审核,MQ 触发规则:1. 当前流程实例中现存的任务会依次触发 process-task-deleted 事件,2. 当前流程实例会触发 process-instance-cancelled 事件") + @DeleteMapping("/api/process/instance/cancel") + Boolean cancelProcessInstance(@Validated @RequestBody BpmnProcessInstanceCancelDTO dto); + + /** + * 中止流程实例 + * + * @param dto + * @return + */ + @Operation(summary = "中止流程实例") + @DeleteMapping("/api/process/instance/abort") + Boolean abortProcessInstance(@Validated @RequestBody BpmnProcessInstanceAbortDTO dto); + + /** + * 批量中止流程实例 + * + * @param dtos + * @return + */ + @Operation(summary = "批量中止流程实例") + @DeleteMapping("/api/process/instance/batch/abort") + BatchOperationResultVO batchAbortProcessInstance(@Validated @RequestBody List dtos); + + /** + * 抄送流程实例(未实现) + * + * @param dto + * @return + */ + @Operation(summary = "抄送流程实例") + @PostMapping("/api/process/instance/carbon-copy") + @Deprecated + Boolean carbonCopyProcessInstance(@Validated @RequestBody BpmnProcessInstanceCarbonCopyDTO dto); + + /** + * 获得流程实例 + * + * @param dto {@link BpmnProcessInstanceQueryDTO} 可根据 Id,BusinessKey进行查询 + * @return 流程实例, 租户Id不必传 + */ + @Operation(summary = "获得流程实例") + @GetMapping("/api/process/instance/get") + @InvokeMode(SYNC) + BpmnProcessInstanceVO getProcessInstanceVO(@Validated @RequestBody BpmnProcessInstanceQueryDTO dto); + + /** + * 获取指定流程实例的流程变量 + * + * @param processInstanceId + * @param tenantId + * @return + */ + @Operation(summary = "获取指定流程实例的流程变量") + @GetMapping("/api/process/instance/cooperation-org") + @InvokeMode(SYNC) + Map getProcessVariables(@NotBlank(message = "流程实例 ID 不能为空") @RequestParam String processInstanceId, @Nullable @RequestParam(required = false) String tenantId); + + /** + * 获取指定流程的日志 + * + * @param dto + * @return + */ + @Operation(summary = "获取指定流程的日志") + @PostMapping("/api/process/instance/logs") + @InvokeMode(SYNC) + BpmnProcessInstanceLogVO getProcessInstanceLogs(@Validated @RequestBody BpmnProcessInstanceLogQueryDTO dto); + + /** + * 同意 + * + *
+     * MQ 触发规则:
+     *  1. 当前审批任务会依次触发 process-task-completed 和 process-task-deleted 事件(如果有下一级审批,则会触发第 2.1 点中的事件,
+     *  如果当前审核任务最后一级审批,则会触发第 2.2 点中的事件)
+     *  2.1. 下一级审批任务会依次触发 process-task-assigned 和 process-task-created 事件
+     *  2.2. 流程实例正常结束会触发 process-instance-completed 事件
+     * 
+ */ + @Operation(summary = "同意,MQ 触发规则:1. 当前审批任务会依次触发 process-task-completed 和 process-task-deleted 事件(如果有下一级审批,则会触发第 2.1 点中的事件,如果当前审核任务最后一级审批,则会触发第 2.2 点中的事件),2.1. 下一级审批任务会依次触发 process-task-assigned 和 process-task-created 事件,2.2. 流程实例正常结束会触发 process-instance-completed 事件") + @PostMapping("/api/process/task/approve") + Boolean approveTask(@Validated @RequestBody BpmnTaskAuditDTO dto); + + /** + * 批量同意 + * + * @param dtos + * @return + */ + @Operation(summary = "批量同意") + @PostMapping("/api/process/task/batch/approve") + BatchOperationResultVO batchApproveTask(@Validated @RequestBody List dtos); + + /** + * 获取当前节点可退回节点选项列表 + * @param taskId 当前任务id + * @return 可以退回节点列表 + */ + @Operation(summary = "获取当前节点可退回节点选项列表") + @GetMapping("/api/process/task/back/optional/nodes") + List getBackOptionalNodes(@RequestParam @NotBlank(message = "任务id不能为空") String taskId); + + /** + * 回退到指定节点 + * @param dto + * @return + */ + @Operation(summary = "回退") + @PostMapping("/api/process/task/back") + Boolean backTask(@Validated @RequestBody BpmnTaskBackAuditDTO dto); + + /** + * 驳回 + * + *
+     * MQ 触发规则:
+     *   1. 当前审批任务会触发 process-task-deleted 事件
+     *   2. 当前流程实例会触发 process-instance-rejected 事件
+     * 
+ */ + @Operation(summary = "驳回,MQ 触发规则:1. 当前审批任务会触发 process-task-deleted 事件, 2. 当前流程实例会触发 process-instance-rejected 事件") + @PostMapping("/api/process/task/reject") + Boolean rejectTask(@Validated @RequestBody BpmnTaskAuditDTO dto); + + /** + * 批量驳回 + * + * @param dtos 批量请求参数 + * @return + */ + @PostMapping("/api/process/task/batch/reject") + BatchOperationResultVO batchRejectTask(@Validated @RequestBody List dtos); + + /** + * 转交 + * + * @param dto + * @return + */ + @Operation(summary = "直接修改审批任务的审批人") + @PostMapping("/api/process/task/transfer") + Boolean transferTask(@Validated @RequestBody BpmnTaskTransferDTO dto); + + /** + * 批量转交 + * + * @param dtos + * @return + */ + @Operation(summary = "批量修改审批任务的审批人") + @PostMapping("/api/process/task/batch/transfer") + BatchOperationResultVO batchTransferTask(@Validated @RequestBody List dtos); + + /** + * 评论 + * + * @param dto 评论请求参数 + * @return + */ + @Operation(summary = "审批流程评论") + @PostMapping("/api/process/task/comment") + Boolean commentTask(@Validated @RequestBody BpmnTaskCommentDTO dto); + + /** + * 加签 + * + * @param dto 加签请求参数 + * @return + */ + @Operation(summary = "审批流程加签") + @PostMapping("/api/process/task/countersign") + Boolean countersignTask(@Validated @RequestBody BpmnTaskCountersignDTO dto); + + /** + * 暂停流程任务,并创建机器人节点,等待业务推动 + * + * @param dto + * @return 返回机器人节点任务 ID + */ + @Operation(summary = "创建机器人节点, 暂停流程任务") + @PostMapping("/api/process/task/robot/create") + String createRobotTask(@Validated @RequestBody BpmnRobotTaskCreateDTO dto); + + /** + * 完成机器人节点 + * + * @param dto + * @return + */ + @Operation(summary = "完成机器人节点, 继续流程任务") + @PostMapping("/api/process/task/robot/complete") + Boolean completeRobotTask(@Validated @RequestBody BpmnRobotTaskCompleteDTO dto); + + /** + * 强制使用‘异步’模式调用该方法,请在调用真实方法前调用该方法 + *
+     *   workflowCoreService.async().createProcessInstance();
+     * 
+ */ + default WorkflowCoreService sync() { + ThreadUtil.set(SYNC); + return this; + } + + default WorkflowCoreService async() { + ThreadUtil.set(ASYNC); + return this; + } } diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/api/WorkflowManageService.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/api/WorkflowManageService.java index 2d5ba5e8d..b6c262f2a 100644 --- a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/api/WorkflowManageService.java +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/api/WorkflowManageService.java @@ -1,10 +1,721 @@ package cn.axzo.workflow.starter.api; +import cn.axzo.workflow.starter.feign.ext.WorkflowEngineStarterFeignConfiguration; +import cn.axzo.workflow.common.util.ThreadUtil; +import static cn.axzo.workflow.common.enums.RpcInvokeModeEnum.ASYNC; +import static cn.axzo.workflow.common.enums.RpcInvokeModeEnum.SYNC; +import cn.axzo.workflow.client.annotation.WorkflowEngineFeignClient; +import cn.axzo.workflow.client.config.CommonFeignConfiguration; +import cn.axzo.workflow.common.annotation.InvokeMode; +import cn.axzo.workflow.common.annotation.Manageable; +import cn.axzo.workflow.common.model.request.bpmn.BpmnButtonMetaInfo; +import cn.azxo.framework.common.model.CommonResponse; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import java.util.List; +import cn.axzo.workflow.common.model.request.bpmn.model.BpmnModelCreateDTO; +import cn.axzo.workflow.common.model.request.bpmn.model.BpmnModelSearchDTO; +import cn.axzo.workflow.common.model.request.bpmn.model.BpmnModelUpdateDTO; +import cn.axzo.workflow.common.model.response.BpmPageResult; +import cn.axzo.workflow.common.model.response.bpmn.model.BpmnModelDetailVO; +import cn.axzo.workflow.common.model.response.bpmn.model.BpmnModelExtVO; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import cn.axzo.workflow.common.model.request.bpmn.task.BpmnActivitySetAssigneeDTO; +import cn.axzo.workflow.common.model.request.bpmn.task.BpmnActivityTriggerDTO; +import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceAbortDTO; +import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceAdminPageReqVO; +import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceCancelDTO; +import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceCarbonCopyDTO; +import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceCheckApproverDTO; +import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceCreateDTO; +import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceCreateWithFormDTO; +import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceLogQueryDTO; +import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceMyPageReqVO; +import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceQueryDTO; +import cn.axzo.workflow.common.model.response.bpmn.BatchOperationResultVO; +import cn.axzo.workflow.common.model.response.bpmn.process.BpmnProcessInstanceAdminPageItemVO; +import cn.axzo.workflow.common.model.response.bpmn.process.BpmnProcessInstanceLogVO; +import cn.axzo.workflow.common.model.response.bpmn.process.BpmnProcessInstancePageItemVO; +import cn.axzo.workflow.common.model.response.bpmn.process.BpmnProcessInstanceVO; +import cn.axzo.workflow.common.model.response.bpmn.process.ProcessNodeDetailVO; +import com.fasterxml.jackson.databind.node.ObjectNode; +import javax.annotation.Nullable; +import java.util.Map; +import cn.axzo.workflow.common.model.request.category.CategoryConfigCreateDTO; +import cn.axzo.workflow.common.model.request.category.CategoryConfigSearchDTO; +import cn.axzo.workflow.common.model.request.category.CategoryCreateDTO; +import cn.axzo.workflow.common.model.request.category.CategorySearchDTO; +import cn.axzo.workflow.common.model.request.category.CategoryUpdateDTO; +import cn.axzo.workflow.common.model.response.category.CategoryConfigItemVO; +import cn.axzo.workflow.common.model.response.category.CategoryItemVO; +import org.springframework.web.bind.annotation.PathVariable; +import cn.axzo.workflow.common.model.request.bpmn.RestBpmnProcessVariable; +import cn.axzo.workflow.common.model.request.bpmn.task.BpmnOptionalNodeDTO; +import cn.axzo.workflow.common.model.request.bpmn.task.BpmnRobotTaskCompleteDTO; +import cn.axzo.workflow.common.model.request.bpmn.task.BpmnRobotTaskCreateDTO; +import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskAttachmentDTO; +import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskAuditDTO; +import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskBackAuditDTO; +import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskCommentDTO; +import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskCountersignDTO; +import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskPageSearchDTO; +import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskRemindDTO; +import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskTransferDTO; +import cn.axzo.workflow.common.model.response.bpmn.task.BpmnHistoricTaskInstanceGroupVO; +import cn.axzo.workflow.common.model.response.bpmn.task.BpmnHistoricTaskInstanceVO; +import cn.axzo.workflow.common.model.response.bpmn.task.BpmnTaskDonePageItemVO; +import cn.axzo.workflow.common.model.response.bpmn.task.BpmnTaskInstanceVO; +import cn.axzo.workflow.common.model.response.bpmn.task.BpmnTaskTodoPageItemVO; +import javax.validation.constraints.NotEmpty; +import cn.axzo.workflow.common.model.request.bpmn.definition.BpmnProcessDefinitionUpdateDTO; +import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessDefinitionPageDTO; +import cn.axzo.workflow.common.model.response.bpmn.process.BpmnProcessDefinitionVO; + /** - * just for compiler - * - * @author wangli - * @since 2024-09-09 18:26 + * Workflow Engine Starter Management Service + *

+ * 该类是根据 API 动态生成,不同版本可能会开放新的接口,或回收一些旧接口 */ +@org.springframework.cloud.openfeign.FeignClient(name = "workflow-engine-starter-manage", url = "${axzo.service.workflow-engine.starter:http://workflow-engine:8080}", configuration = WorkflowEngineStarterFeignConfiguration.class) public interface WorkflowManageService { + + /** + * 获取流程操作按钮列表 + * + * @return 流程操作按钮列表 + */ + @GetMapping("/api/process/config/button/list") + @InvokeMode(SYNC) + List getDefaultButtons(); + + /** + * 流程模型列表 + * + * @param dto + * @return + */ + @Operation(summary = "流程模型列表") + @GetMapping("/api/process/model/page") + @InvokeMode(SYNC) + BpmPageResult page(@Validated @RequestBody BpmnModelSearchDTO dto); + + /** + * 创建流程, + * return modelId的主键 + */ + @Operation(summary = "创建流程模型") + @PostMapping("/api/process/model/create") + @InvokeMode(SYNC) + String create(@Validated @RequestBody BpmnModelCreateDTO dto); + + /** + * 通过模型 ID 获取模型 + */ + @Operation(summary = "通过模型ID查询指定流程模型") + @GetMapping("/api/process/model/get") + @InvokeMode(SYNC) + BpmnModelDetailVO getById(@NotBlank(message = "流程模型 ID 不能为空") @RequestParam(required = false) String processModelId, @RequestParam(required = false) String tenantId); + + /** + * 通过模型 KEY 获取模型 + */ + @Operation(summary = "通过模型KEY查询指定流程模型") + @GetMapping("/api/process/model/getByKey") + @InvokeMode(SYNC) + BpmnModelDetailVO getByKey(@NotBlank(message = "流程模型 KEY 不能为空") @RequestParam(required = false) String processModelKey, @NotBlank(message = "租户不能为空") @RequestParam(required = false) String tenantId); + + /** + * 校验是否能更新模型即其关联的流程定义内容 + *

+ * 偏向业务的接口,flowable 引擎是站在流程定义维度进行激活和挂起 + * + * @return true 能更新,表明该模型关联的所有定义版本都是挂起状态 + */ + @Operation(summary = "获取指定模型的扩展属性") + @GetMapping("/api/process/model/ext") + @InvokeMode(SYNC) + BpmnModelExtVO getModelExt(@NotBlank(message = "模型 ID 不能为空") @RequestParam(required = false) String modelId); + + /** + * 修改流程信息 + */ + @Operation(summary = "更新流程模型") + @PutMapping("/api/process/model/update") + @InvokeMode(SYNC) + String update(@RequestBody BpmnModelUpdateDTO dto); + + /** + * 通过模型 ID 部署模型 + * + * @return 部署完成的流程定义Id + */ + @Operation(summary = "通过模型 ID 部署流程模型") + @PostMapping("/api/process/model/deploy") + @InvokeMode(SYNC) + String deployById(@NotBlank(message = "流程模型 ID 不能为空") @RequestParam(required = false) String processModelId, @RequestParam(required = false, defaultValue = "") String modelTenantId, @RequestParam(required = false) String operator); + + /** + * 通过模型 KEY 部署模型 + * + * @return 部署完成的流程定义Id + */ + @Operation(summary = "通过模型 KEY 部署流程模型") + @PostMapping("/api/process/model/deployByKey") + @InvokeMode(SYNC) + String deployByKey(@NotBlank(message = "流程模型 KEY 不能为空") @RequestParam(required = false) String processModelKey, @NotBlank(message = "租户不能为空") @RequestParam(required = false) String modelTenantId, @RequestParam(required = false) String operator); + + /** + * 通过模型 ID 取消部署流程模型 + * + * @param processModelId + * @param tenantId + * @param operator + * @return + */ + @Operation(summary = "通过模型 ID 取消部署流程模型") + @PostMapping("/api/process/model/undeploy") + @InvokeMode(SYNC) + Void unDeployById(@NotBlank(message = "流程模型 ID 不能为空") @RequestParam(required = false) String processModelId, @RequestParam(required = false, defaultValue = "") String tenantId, @RequestParam(required = false) String operator); + + /** + * 通过模型 ID 删除模型 + */ + @Operation(summary = "删除指定模型 ID 的流程模型") + @DeleteMapping("/api/process/model/delete") + @InvokeMode(SYNC) + Void deleteById(@NotBlank(message = "流程模型 ID 不能为空") @RequestParam String processModelId, @RequestParam(required = false, defaultValue = "") String tenantId); + + /** + * 通过模型 KEY 删除模型 + * + * @param processModelKey + * @param tenantId + * @return + */ + @Operation(summary = "删除指定模型 KEY 的流程模型") + @DeleteMapping("/api/process/model/deleteByKey") + @InvokeMode(SYNC) + Void deleteByKey(@NotBlank(message = "流程模型 KEY 不能为空") @RequestParam String processModelKey, @RequestParam(required = false, defaultValue = "") String tenantId); + + /** + * 通过模型 ID 修改模型状态 + * + * @param modelId + * @param status + * @param operator + * @return + */ + @Operation(summary = "修改模型状态") + @PostMapping("/api/process/model/changeStatus") + @InvokeMode(SYNC) + Void changeStatus(@NotBlank(message = "模型 ID 不能为空") @RequestParam String modelId, @NotNull(message = "状态不能为空") @RequestParam Integer status, @RequestParam(required = false) String operator); + + /** + * 查询流程模型使用的分类列表 + * + * @return + */ + @Operation(summary = "查询流程模型使用的分类列表") + @GetMapping("/api/process/model/category/ids") + @InvokeMode(SYNC) + List getModelCategoryList(); + + /** + * 查询模型的租户集合 + * + * @return + */ + @Operation(summary = "查询模型的租户集合") + @GetMapping("/api/process/model/tenant/ids") + @InvokeMode(SYNC) + List getModelTenantIds(); + + /** + * 创建审批流程并带上表单 + * + * @param dto + * @return + */ + @Operation(summary = "创建审批流程并带上表单") + @PostMapping("/api/process/instance/form/create") + @Manageable + @InvokeMode(SYNC) + String createProcessInstanceWith(@Validated @RequestBody BpmnProcessInstanceCreateWithFormDTO dto); + + /** + * 查询所有的审批流 + * + * @return + */ + @Operation(summary = "查询所有的审批流") + @PostMapping("/api/process/instance/page/all") + @Manageable + @InvokeMode(SYNC) + BpmPageResult getAllProcessInstancePage(@Validated @RequestBody BpmnProcessInstanceAdminPageReqVO dto); + + /** + * 我发起的审批列表 + */ + @Operation(summary = "我发起的审批列表") + @PostMapping("/api/process/instance/page/my") + @Manageable + @InvokeMode(SYNC) + BpmPageResult getMyProcessInstancePage(@Validated @RequestBody BpmnProcessInstanceMyPageReqVO dto); + + /** + * 更新流程定义的状态 + * + * @param processDefinitionId 流程定义的编号 + * @param status 1, "active"; 2, "suspended" + */ + @Operation(summary = "更新指定流程定义的版本的状态, 处于 suspended 状态的流程模型将不能再发起实例") + @PutMapping("/api/process/instance/status/update") + @Manageable + @InvokeMode(SYNC) + Boolean updateProcessStatus(@NotBlank(message = "流程定义 ID 不能为空") @RequestParam String processDefinitionId, @NotNull(message = "状态不能为空") @RequestParam Integer status); + + /** + * 获取审批流程实例的运行图 + * + * @param processInstanceId + * @param tenantId + * @return + */ + @Operation(summary = "获取审批流程实例的运行图") + @GetMapping("/api/process/instance/graphical") + @Manageable + @InvokeMode(SYNC) + ObjectNode processInstanceGraphical(@NotBlank(message = "流程实例 ID 不能为空") @RequestParam String processInstanceId, @Nullable @RequestParam(required = false) String tenantId); + + /** + * 推断指定流程实例的所有节点执行顺序 + * + * @return + */ + @Operation(summary = "推断指定流程实例的所有节点执行顺序") + @GetMapping("/api/process/instance/node/forecasting") + @Manageable + @InvokeMode(SYNC) + List processInstanceNodeForecast(@NotBlank(message = "流程实例 ID 不能为空") @RequestParam(required = false) String processInstanceId, @Nullable @RequestParam(required = false) String tenantId); + + /** + * 推断指定流程实例的过滤掉部分节点执行顺序 + * + * @param allNode 如果为真时,相当于调用 {@link ProcessInstanceApi#processInstanceNodeForecast(String, String)} 方法,切会直接丢弃 nodeDefinitionKeys 参数 + * 如果为假时,才结合 nodeDefinitionKeys 过滤掉传入的节点 + * @return + */ + @Operation(summary = "推断指定流程实例的过滤掉部分节点执行顺序") + @GetMapping("/api/process/instance/node/filter/forecasting") + @Manageable + @InvokeMode(SYNC) + List processInstanceFilterNodeForecast(@NotBlank(message = "流程实例 ID 不能为空") @RequestParam(required = false) String processInstanceId, @Nullable @RequestParam(required = false) String tenantId, @RequestParam(required = false, defaultValue = "false") Boolean allNode, @Nullable @RequestParam(required = false) List nodeDefinitionKeys); + + /** + * 查询实例的租户集合 + * + * @return + */ + @Operation(summary = "查询实例的租户集合") + @GetMapping("/api/process/instance/tenant/ids") + @Manageable + @InvokeMode(SYNC) + List getTenantIds(); + + /** + * 校验指定流程实例下,是否存在指定的审批人 + * + * @return true 是在当前流程实例中,存在指定的审批人 + */ + @Operation(summary = "校验指定流程实例下,是否存在指定的审批人") + @PostMapping("/api/process/instance/check/approver") + @Manageable + @InvokeMode(SYNC) + Boolean checkInstanceApprover(@Validated @RequestBody BpmnProcessInstanceCheckApproverDTO dto); + + /** + * 将死信队列中的任务转移到正常 JOB 队列中 + *

+ * 入参是二选一:当只有 jobId 时,仅将指定的 JOB 转移到正常的队列中; + * 而传入的是具体的实例 ID,那么会将这个流程下的所有在死信队列中的任务都转移到正常的队列中 + * + * @param jobId 具体的 JOB ID + * @param procInstId 具体的实例 ID + * @return + */ + @GetMapping("/api/process/job/dead-letter/resume") + @Manageable + Void executeDeadLetterJobAction(@RequestParam(required = false) String jobId, @RequestParam(required = false) String procInstId); + + /** + * 获取指定业务分类 + * + * @return + */ + @GetMapping("/api/process/category/get") + @InvokeMode(SYNC) + CategoryItemVO get(@RequestParam Long id); + + /** + * 获取指定业务分类集合 + * + * @param ids + * @return + */ + @GetMapping("/api/process/category/getByIds") + @InvokeMode(SYNC) + List getByIds(@RequestParam List ids); + + /** + * 获取指定业务分类集合 + * + * @param values + * @return + */ + @GetMapping("/api/process/category/getByValues") + @InvokeMode(SYNC) + List getByValues(@RequestParam List values); + + /** + * 新增业务分类 + * + * @return + */ + @PostMapping("/api/process/category/create") + @InvokeMode(SYNC) + CategoryItemVO create(@Validated @RequestBody CategoryCreateDTO req); + + /** + * 更新业务分类 + * + * @return + */ + @PutMapping("/api/process/category/update") + @InvokeMode(SYNC) + CategoryItemVO update(@Validated @RequestBody CategoryUpdateDTO dto); + + /** + * 删除指定业务分类 + * + * @param id + * @return + */ + @DeleteMapping("/api/process/category/delete") + @InvokeMode(SYNC) + Boolean delete(@RequestParam Long id); + + /** + * 更新指定分类状态 + * + * @param id + * @param state + * @return + */ + @PutMapping("/api/process/category/update/state") + @InvokeMode(SYNC) + Boolean updateState(@RequestParam Long id, @RequestParam Boolean state); + + /** + * 获取指定业务分类集合 + * + * @return + */ + @PostMapping("/api/process/category/list") + @InvokeMode(SYNC) + List list(@RequestBody CategorySearchDTO dto); + + /** + * 搜索业务分类 + * + * @return + */ + @PostMapping("/api/process/category/page/search") + @InvokeMode(SYNC) + BpmPageResult search(@RequestBody CategorySearchDTO dto); + + /** + * 业务分类创建黑白名单配置项 + * + * @param dto + * @return + */ + @PostMapping("/api/process/category/config/create") + @InvokeMode(SYNC) + Boolean createConfig(@RequestBody CategoryConfigCreateDTO dto); + + /** + * 删除指定分类的配置项数据 + * + * @param id + * @return + */ + @DeleteMapping("/api/process/category/config/delete/{id}") + @InvokeMode(SYNC) + Boolean deleteConfig(@PathVariable Long id); + + /** + * 业务分类黑白名单查询 + * + * @return + */ + @PostMapping("/api/process/category/config/page/search") + @InvokeMode(SYNC) + BpmPageResult configSearch(@RequestBody CategoryConfigSearchDTO dto); + + /** + * 更新业务分类的配置类型 + * + * @param id + * @param configType + * @return + */ + @PostMapping("/api/process/category/config/type/update") + @InvokeMode(SYNC) + Boolean updateCategoryConfigType(@RequestParam Long id, @RequestParam String configType); + + /** + * 用于业务方判断指定的业务分类的黑白名单配置 + * + * @param tenantId 租户ID + * @param categoryCode 业务分类编码 + * @return true: 可以发起创建流程实例, false: 不可用 + */ + @GetMapping("/api/process/category/check/status") + @InvokeMode(SYNC) + Boolean checkCategoryStatus(@RequestParam Long tenantId, @RequestParam String categoryCode); + + /** + * 为指定流程新增变量 + */ + @PostMapping("/api/process/variable/create/{executionId}") + @InvokeMode(SYNC) + Void createVariable(@PathVariable @NotBlank(message = "流程实例 ID 不能为空") String executionId, @RequestBody @Validated RestBpmnProcessVariable restVariable); + + /** + * 仅更新流程已存在的变量 + * + * @param executionId + * @param restVariable + * @return + */ + @PostMapping("/api/process/variable/update/{executionId}") + @InvokeMode(SYNC) + Void updateVariable(@PathVariable @NotBlank(message = "流程实例 ID 不能为空") String executionId, @RequestBody @Validated RestBpmnProcessVariable restVariable); + + /** + * 批量删除流程变量 + */ + @DeleteMapping("/api/process/variable/delete/{executionId}") + @InvokeMode(SYNC) + Void deleteVariables(@PathVariable("executionId") String executionId, @RequestParam String variableNames, @RequestParam(value = "scope", required = false) String scope); + + /** + * 催办 + * + * @param dto + * @return + */ + @Operation(summary = "审批流程催办") + @PostMapping("/api/process/task/remind") + @Manageable + @InvokeMode(SYNC) + Boolean remindTask(@Validated @RequestBody BpmnTaskRemindDTO dto); + + /** + * 添加附件 + * + * @param dto + * @return + */ + @Operation(summary = "添加附件") + @PostMapping("/api/process/task/attachment") + @Manageable + Void addAttachment(@Validated @RequestBody BpmnTaskAttachmentDTO dto); + + /** + * 待审核列表 + */ + @Operation(summary = "待审核列表") + @GetMapping("/api/process/task/page/todo") + @Manageable + @InvokeMode(SYNC) + BpmPageResult getTodoTaskPage(@Validated @RequestBody BpmnTaskPageSearchDTO dto); + + /** + * 已完成的审批列表 + */ + @Operation(summary = "已完成的审批列表") + @GetMapping("/api/process/task/page/done") + @Manageable + @InvokeMode(SYNC) + BpmPageResult getDoneTaskPage(@Validated @RequestBody BpmnTaskPageSearchDTO dto); + + /** + * 获取指定流程实例的审批过程信息 + *

+ * 同一层级结构 + */ + @Operation(summary = "获取指定流程实例的审批过程信息") + @GetMapping("/api/process/task/list/flat") + @Manageable + @InvokeMode(SYNC) + List getTaskListFlatByProcessInstanceId(@NotBlank(message = "流程实例 ID " + "不能为空") @RequestParam String processInstanceId, @Nullable @RequestParam(required = false) String tenantId); + + /** + * 获取指定流程实例的审批过程信息 + *

+ * 分组结构 + */ + @Operation(summary = "获取指定流程实例的审批过程信息") + @GetMapping("/api/process/task/list/group") + @Manageable + @InvokeMode(SYNC) + List getTaskListGroupByProcessInstanceId(@NotBlank(message = "流程实例 ID 不能为空") @RequestParam String processInstanceId, @Nullable @RequestParam(required = false) String tenantId); + + /** + * 获取实例正在审核的人列表 + */ + @Operation(summary = "获取实例正在审核的人列表") + @GetMapping("/api/process/task/active/list") + @Manageable + @InvokeMode(SYNC) + List getActiveTasksByProcessInstanceId(@NotBlank(message = "流程实例 ID 不能为空") @RequestParam String processInstanceId, @NotBlank(message = "租户不能为空") @RequestParam String tenantId); + + /** + * 根据实例 ID 和自然人 ID 查询对应待处理的任务 ID + * + * @return + */ + @Operation(summary = "根据实例 ID 和自然人 ID 查询对应待处理的任务 ID") + @GetMapping("/api/process/task/find") + @Manageable + @InvokeMode(SYNC) + String findTaskIdByInstanceIdAndPersonId(@RequestParam(required = false) @NotBlank(message = "流程实例 ID 不能为空") String processInstanceId, @RequestParam(required = false) @NotBlank(message = "自然人 ID 不能为空") String personId); + + /** + * 根据实例 ID列表 和自然人 ID 查询对应待处理的任务 ID + * + * @return + */ + @Operation(summary = "根据实例 ID列表 和自然人 ID 查询对应待处理的任务 ID") + @GetMapping("/api/process/task/batch/find") + @Manageable + @InvokeMode(SYNC) + Map findTaskIdByInstanceIdsAndPersonId(@RequestParam(required = false) @NotEmpty(message = "流程实例 ID列表 不能为空") List processInstanceIds, @RequestParam(required = false) @NotBlank(message = "自然人 ID 不能为空") String personId); + + /** + * 获取活跃的流程定义分页 + */ + @GetMapping("/api/process/definition/page") + @InvokeMode(SYNC) + BpmPageResult getProcessDefinitionPage(@Validated @RequestBody BpmnProcessDefinitionPageDTO dto); + + /** + * 通过模型 ID 更新流程定义 + * + * @param dto + * @return + */ + @PutMapping("/api/process/definition/update") + @InvokeMode(SYNC) + Boolean updateProcessDefinition(@Validated @RequestBody BpmnProcessDefinitionUpdateDTO dto); + + /** + * 获得指定定义编号对应的流程定义内容 + * + * @param processDefinitionId 编号 + * @return 流程定义 + */ + @GetMapping("/api/process/definition/get") + @InvokeMode(SYNC) + BpmnProcessDefinitionVO getProcessDefinition(@NotBlank(message = "流程定义 ID 不能为空") @RequestParam String processDefinitionId); + + /** + * 获得 deploymentId 对应的 ProcessDefinition + * + * @param deploymentId 部署编号 + * @return 流程定义 + */ + @GetMapping("/api/process/definition/getByDeploymentId") + @InvokeMode(SYNC) + BpmnProcessDefinitionVO getProcessDefinitionByDeploymentId(@NotBlank(message = "流程部署 ID 不能为空") @RequestParam String deploymentId); + + /** + * 获得流程定义标识对应的激活的流程定义 + * + * @param key 流程定义的标识 + * @return 流程定义 + */ + @GetMapping("/api/process/definition/active/getByKey") + @InvokeMode(SYNC) + BpmnProcessDefinitionVO getActiveProcessDefinitionByKey(@NotBlank(message = "模型定义KEY不能为空") @RequestParam String key); + + /** + * 挂起/激活流程, + * 激活:SuspensionState.ACTIVE.getStateCode() + * 挂起:SuspensionState.SUSPENDED.getStateCode() + * {@see SuspensionState} + */ + @PutMapping("/api/process/definition/state/update") + @InvokeMode(SYNC) + Boolean getActiveProcessDefinitionByKey(@NotBlank(message = "流程定义ID不能为空") @RequestParam String processDefinitionId, @NotNull(message = "状态不能为空") @RequestParam Integer state); + + /** + * 获取指定模型的定义 ID + * + * @return 流程定义ID + */ + @GetMapping("/api/process/definition/active/getByCategory") + @InvokeMode(SYNC) + String getActiveProcessDefinitionId(@RequestParam(required = false) String tenantId, @NotBlank(message = "分类不能为空") @RequestParam(required = false) String category); + + /** + * 获取指定模型激活的流程定义 JSON 模型 + * + * @return 流程定义ID + */ + @GetMapping("/api/process/definition/active/json/model") + @InvokeMode(SYNC) + BpmnModelUpdateDTO getActiveProcessDefinitionJsonModel(@NotBlank(message = "模型 ID 不能为空") @RequestParam(required = false) String modelId, @NotBlank(message = "分类不能为空") @RequestParam(required = false) String category, @RequestParam(required = false) String tenantId); + + /** + * 删除流程部署及定义 + * + * @param deploymentId 流程定义部署 ID + * @param cascade 是否级联参数定义对应的流程实例及 job 等管理内容 + * @return + */ + @GetMapping("/api/process/definition/delete") + @InvokeMode(SYNC) + Void delete(@NotBlank(message = "流程定义部署 ID 不能为空") String deploymentId, @RequestParam(required = false) Boolean cascade); + + /** + * 强制使用‘同步’模式调用该方法,请在调用真实方法前调用该方法 + *

+     *   workflowManageService.sync().getTenantIds();
+     * 
+ */ + default WorkflowManageService sync() { + ThreadUtil.set(SYNC); + return this; + } + + /** + * 强制使用‘异步’模式调用该方法,请在调用真实方法前调用该方法 + *
+     *   workflowManageService.async().getTenantIds();
+     * 
+ */ + default WorkflowManageService async() { + ThreadUtil.set(ASYNC); + return this; + } } diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/selector/MetaFeignClientEnableSelector.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/selector/MetaFeignClientEnableSelector.java new file mode 100644 index 000000000..13599b0fd --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/selector/MetaFeignClientEnableSelector.java @@ -0,0 +1,320 @@ +package cn.axzo.workflow.starter.selector; + +import cn.axzo.workflow.client.annotation.WorkflowEngineFeignClient; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.config.BeanExpressionContext; +import org.springframework.beans.factory.config.BeanExpressionResolver; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.client.ConditionalOnReactiveDiscoveryEnabled; +import org.springframework.cloud.openfeign.FeignClientFactoryBean; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.env.Environment; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; + +/** + * 原生 FeignClient 的注册 + * + * @author wangli + * @since 2024-09-10 09:56 + */ +@Slf4j +public class MetaFeignClientEnableSelector implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware { + + private ResourceLoader resourceLoader; + + private Environment environment; + + static void validateFallback(final Class clazz) { + Assert.isTrue(!clazz.isInterface(), "Fallback class must implement the interface annotated by @FeignClient"); + } + + static void validateFallbackFactory(final Class clazz) { + Assert.isTrue(!clazz.isInterface(), "Fallback factory must produce instances " + + "of fallback classes that implement the interface annotated by @FeignClient"); + } + + static String getName(String name) { + if (!StringUtils.hasText(name)) { + return ""; + } + + String host = null; + try { + String url; + if (!name.startsWith("http://") && !name.startsWith("https://")) { + url = "http://" + name; + } + else { + url = name; + } + host = new URI(url).getHost(); + + } + catch (URISyntaxException e) { + } + Assert.state(host != null, "Service id not legal hostname (" + name + ")"); + return name; + } + + static String getUrl(String url) { + if (StringUtils.hasText(url) && !(url.startsWith("#{") && url.contains("}"))) { + if (!url.contains("://")) { + url = "http://" + url; + } + try { + new URL(url); + } + catch (MalformedURLException e) { + throw new IllegalArgumentException(url + " is malformed", e); + } + } + return url; + } + + static String getPath(String path) { + if (StringUtils.hasText(path)) { + path = path.trim(); + if (!path.startsWith("/")) { + path = "/" + path; + } + if (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + } + return path; + } + + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } + + @Override + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { + Boolean metaFeignEnabled = environment.getProperty("workflow.engine.starter.meta-feign", Boolean.class); + log.error("current meta-feign result: {}", metaFeignEnabled); + if(!Boolean.TRUE.equals(metaFeignEnabled)){ + return; + } + LinkedHashSet candidateComponents = new LinkedHashSet<>(); + ClassPathScanningCandidateComponentProvider scanner = getScanner(); + scanner.setResourceLoader(this.resourceLoader); + scanner.addIncludeFilter(new AnnotationTypeFilter(WorkflowEngineFeignClient.class)); + candidateComponents.addAll(scanner.findCandidateComponents("cn.axzo.workflow.client.feign")); + + for (BeanDefinition candidateComponent : candidateComponents) { + if (candidateComponent instanceof AnnotatedBeanDefinition) { + // verify annotated class is an interface + AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent; + AnnotationMetadata annotationMetadata = beanDefinition.getMetadata(); + Assert.isTrue(annotationMetadata.isInterface(), "@FeignClient can only be specified on an interface"); + + Map attributes = new HashMap<>(); + attributes.put("configuration", new Class[]{}); + attributes.put("contextId", ""); + attributes.put("decode404", false); + attributes.put("fallback", void.class); + attributes.put("fallbackFactory", void.class); + attributes.put("name", "workflow-engine"); + attributes.put("path", ""); + attributes.put("primary", true); + attributes.put("qualifier", ""); + attributes.put("qualifiers", new String[]{}); + String workflowEngineUrl = environment.getProperty("axzo.service.workflow-engine", String.class); + if(!StringUtils.hasText(workflowEngineUrl)) { + workflowEngineUrl = "http://workflow-engine:8080"; + } + attributes.put("url", workflowEngineUrl); + attributes.put("value", "workflow-engine"); + + registerFeignClient(registry, annotationMetadata, attributes); + } + } + } + + private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, + Map attributes) { + String className = annotationMetadata.getClassName(); + Class clazz = ClassUtils.resolveClassName(className, null); + ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory + ? (ConfigurableBeanFactory) registry : null; + String contextId = getContextId(beanFactory, attributes); + String name = getName(attributes); + FeignClientFactoryBean factoryBean = new FeignClientFactoryBean(); + factoryBean.setBeanFactory(beanFactory); + factoryBean.setName(name); + factoryBean.setContextId(contextId); + factoryBean.setType(clazz); + BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> { + factoryBean.setUrl(getUrl(beanFactory, attributes)); + factoryBean.setPath(getPath(beanFactory, attributes)); + factoryBean.setDecode404(Boolean.parseBoolean(String.valueOf(attributes.get("decode404")))); + Object fallback = attributes.get("fallback"); + if (fallback != null) { + factoryBean.setFallback(fallback instanceof Class ? (Class) fallback + : ClassUtils.resolveClassName(fallback.toString(), null)); + } + Object fallbackFactory = attributes.get("fallbackFactory"); + if (fallbackFactory != null) { + factoryBean.setFallbackFactory(fallbackFactory instanceof Class ? (Class) fallbackFactory + : ClassUtils.resolveClassName(fallbackFactory.toString(), null)); + } + return factoryBean.getObject(); + }); + definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); + definition.setLazyInit(true); + validate(attributes); + + AbstractBeanDefinition beanDefinition = definition.getBeanDefinition(); + beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className); + beanDefinition.setAttribute("feignClientsRegistrarFactoryBean", factoryBean); + + // has a default, won't be null + boolean primary = (Boolean) attributes.get("primary"); + + beanDefinition.setPrimary(primary); + + String[] qualifiers = getQualifiers(attributes); + if (ObjectUtils.isEmpty(qualifiers)) { + qualifiers = new String[] { contextId + "FeignClient" }; + } + + BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, qualifiers); + BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry); + } + + protected ClassPathScanningCandidateComponentProvider getScanner() { + return new ClassPathScanningCandidateComponentProvider(false, this.environment) { + @Override + protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { + boolean isCandidate = false; + if (beanDefinition.getMetadata().isIndependent()) { + if (!beanDefinition.getMetadata().isAnnotation()) { + isCandidate = true; + } + } + return isCandidate; + } + }; + } + + private void validate(Map attributes) { + AnnotationAttributes annotation = AnnotationAttributes.fromMap(attributes); + // This blows up if an aliased property is overspecified + // FIXME annotation.getAliasedString("name", FeignClient.class, null); + validateFallback(annotation.getClass("fallback")); + validateFallbackFactory(annotation.getClass("fallbackFactory")); + } + + private String getContextId(ConfigurableBeanFactory beanFactory, Map attributes) { + String contextId = (String) attributes.get("contextId"); + if (!StringUtils.hasText(contextId)) { + return getName(attributes); + } + + contextId = resolve(beanFactory, contextId); + return getName(contextId); + } + + private String getUrl(ConfigurableBeanFactory beanFactory, Map attributes) { + String url = resolve(beanFactory, (String) attributes.get("url")); + return getUrl(url); + } + + private String getPath(ConfigurableBeanFactory beanFactory, Map attributes) { + String path = resolve(beanFactory, (String) attributes.get("path")); + return getPath(path); + } + + /* for testing */ String getName(Map attributes) { + return getName(null, attributes); + } + + private String getQualifier(Map client) { + if (client == null) { + return null; + } + String qualifier = (String) client.get("qualifier"); + if (StringUtils.hasText(qualifier)) { + return qualifier; + } + return null; + } + + private String[] getQualifiers(Map client) { + if (client == null) { + return null; + } + List qualifierList = new ArrayList<>(Arrays.asList((String[]) client.get("qualifiers"))); + qualifierList.removeIf(qualifier -> !StringUtils.hasText(qualifier)); + if (qualifierList.isEmpty() && getQualifier(client) != null) { + qualifierList = Collections.singletonList(getQualifier(client)); + } + return !qualifierList.isEmpty() ? qualifierList.toArray(new String[0]) : null; + } + + String getName(ConfigurableBeanFactory beanFactory, Map attributes) { + String name = (String) attributes.get("serviceId"); + if (!StringUtils.hasText(name)) { + name = (String) attributes.get("name"); + } + if (!StringUtils.hasText(name)) { + name = (String) attributes.get("value"); + } + name = resolve(beanFactory, name); + return getName(name); + } + + private String resolve(ConfigurableBeanFactory beanFactory, String value) { + if (StringUtils.hasText(value)) { + if (beanFactory == null) { + return this.environment.resolvePlaceholders(value); + } + BeanExpressionResolver resolver = beanFactory.getBeanExpressionResolver(); + String resolved = beanFactory.resolveEmbeddedValue(value); + if (resolver == null) { + return resolved; + } + return String.valueOf(resolver.evaluate(resolved, new BeanExpressionContext(beanFactory, null))); + } + return value; + } + +}