diff --git a/Changelog.md b/Changelog.md index 1ab9ece08..5e61a3ff6 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,13 @@ # Change logs +### 1.4.0-SNAPSHOT + +> - 新增 workflow-engine-spring-boot-starter 组件,大幅简化业务方接入使用的难度。 +> - 在 Starter 中新增 MQ 监控功能,避免死信依赖运维。 +> - 优化发送待办时,提前判断是否有按钮,以及能否支持批量处理。 +> - 修复同一个实例下,多个人同时操作任务,可能导致数据库的 FK 异常等问题。 +> - 修复其他的一些小问题 + ### 1.3.3-SNAPSHOT > - 支持“政务”类型的模型管理。 diff --git a/pom.xml b/pom.xml index 319b2cfec..03df7489b 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 @@ -15,7 +16,7 @@ workflow-engine - 1.3.3-SNAPSHOT + 1.4.0-SNAPSHOT 2.0.0-SNAPSHOT 2.0.0-SNAPSHOT 11.8 @@ -25,6 +26,7 @@ 2.0.0 3.7.1 3.2.5 + 3.26.0 @@ -64,11 +66,21 @@ workflow-engine-core ${project.version} + ${project.groupId} workflow-engine-server ${project.version} + + cn.axzo.workflow + workflow-engine-spring-boot-starter + ${project.version} + cn.axzo.maokai maokai-api @@ -99,6 +111,11 @@ maven-artifact ${apache-maven.version} + + com.github.javaparser + javaparser-core + ${javaparse.version} + @@ -120,6 +137,11 @@ mapstruct-processor ${mapstruct.version} + + org.springframework.boot + spring-boot-configuration-processor + ${spring.boot.version} + @@ -138,6 +160,7 @@ workflow-engine-common workflow-engine-core workflow-engine-server - + workflow-engine-support + workflow-engine-spring-boot-starter diff --git a/workflow-engine-api/pom.xml b/workflow-engine-api/pom.xml index ae99c41a8..11ba8fffc 100644 --- a/workflow-engine-api/pom.xml +++ b/workflow-engine-api/pom.xml @@ -11,7 +11,7 @@ workflow-engine-api jar - workflow-engine-api + Workflow Engine Api diff --git a/workflow-engine-api/src/main/java/cn/axzo/workflow/client/config/WorkflowEngineClientAutoConfiguration.java b/workflow-engine-api/src/main/java/cn/axzo/workflow/client/config/WorkflowEngineClientAutoConfiguration.java index abfde80e5..84439b9dc 100644 --- a/workflow-engine-api/src/main/java/cn/axzo/workflow/client/config/WorkflowEngineClientAutoConfiguration.java +++ b/workflow-engine-api/src/main/java/cn/axzo/workflow/client/config/WorkflowEngineClientAutoConfiguration.java @@ -32,7 +32,7 @@ import java.util.regex.Pattern; * @Date: 2022/9/17 * @Description: */ -@EnableFeignClients(basePackages = {"cn.axzo.workflow.client.feign"}) +//@EnableFeignClients(basePackages = {"cn.axzo.workflow.client.feign"}) @Configuration(proxyBeanMethods = false) @Slf4j public class WorkflowEngineClientAutoConfiguration { diff --git a/workflow-engine-api/src/main/java/cn/axzo/workflow/client/config/WorkflowRequestInterceptor.java b/workflow-engine-api/src/main/java/cn/axzo/workflow/client/config/WorkflowRequestInterceptor.java index 25cd26a3e..a5c831343 100644 --- a/workflow-engine-api/src/main/java/cn/axzo/workflow/client/config/WorkflowRequestInterceptor.java +++ b/workflow-engine-api/src/main/java/cn/axzo/workflow/client/config/WorkflowRequestInterceptor.java @@ -20,9 +20,9 @@ public class WorkflowRequestInterceptor implements RequestInterceptor { this.environment = environment; } - public static final String HEADER_HTTP_CLIENT = "http-client"; - public static final String HEADER_HTTP_CLIENT_VALUE = "workflowEngine-feign"; - public static final String HEADER_API_VERSION = "service-version"; + public static final String HEADER_HTTP_CLIENT = "Http-Client"; + public static final String HEADER_HTTP_CLIENT_VALUE = "WorkflowEngine-Feign"; + public static final String HEADER_API_VERSION = "Service-Version"; public static final String HEADER_SERVER_NAME = "X-SERVER-NAME"; diff --git a/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/bpmn/ProcessActivityApi.java b/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/bpmn/ProcessActivityApi.java index 832c13806..1f319c9c4 100644 --- a/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/bpmn/ProcessActivityApi.java +++ b/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/bpmn/ProcessActivityApi.java @@ -3,6 +3,7 @@ package cn.axzo.workflow.client.feign.bpmn; import cn.axzo.workflow.client.config.CommonFeignConfiguration; import cn.axzo.workflow.common.model.request.bpmn.task.BpmnActivitySetAssigneeDTO; import cn.azxo.framework.common.model.CommonResponse; +import io.swagger.v3.oas.annotations.Operation; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; @@ -18,26 +19,29 @@ import javax.validation.constraints.NotBlank; * @author wangli * @since 2023/11/17 16:28 */ -@FeignClient(name = "workflow-engine", url = "${axzo.service.workflow-engine:http://workflow-engine:8080}", configuration = CommonFeignConfiguration.class) +//@FeignClient(name = "workflow-engine", url = "${axzo.service.workflow-engine:http://workflow-engine:8080}", configuration = CommonFeignConfiguration.class) public interface ProcessActivityApi { /** * 业务节点唤醒 + *

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

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

+ * 注意:如果调用接口时,传入的审批人集合为空,流程引擎将对该审批流程实例自动中止。 * * @param dto * @return */ @PostMapping("/api/process/activity/assignee/set") + @Operation(summary = "业务节点设置审批人,不支持重复调用设置审批人,需一次性传入所有审批人") CommonResponse setAssignee(@Validated @RequestBody BpmnActivitySetAssigneeDTO dto); } diff --git a/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/bpmn/ProcessDefinitionApi.java b/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/bpmn/ProcessDefinitionApi.java index 56fc309d0..7a04b923a 100644 --- a/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/bpmn/ProcessDefinitionApi.java +++ b/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/bpmn/ProcessDefinitionApi.java @@ -1,6 +1,8 @@ package cn.axzo.workflow.client.feign.bpmn; 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.definition.BpmnProcessDefinitionUpdateDTO; import cn.axzo.workflow.common.model.request.bpmn.model.BpmnModelUpdateDTO; import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessDefinitionPageDTO; @@ -17,13 +19,16 @@ import org.springframework.web.bind.annotation.RequestParam; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; +import static cn.axzo.workflow.common.enums.RpcInvokeModeEnum.SYNC; + /** * 流程定义 API * * @author wangli * @since 2023/9/21 16:25 */ -@FeignClient(name = "workflow-engine", url = "${axzo.service.workflow-engine:http://workflow-engine:8080}", configuration = CommonFeignConfiguration.class) +//@FeignClient(name = "workflow-engine", url = "${axzo.service.workflow-engine:http://workflow-engine:8080}", configuration = CommonFeignConfiguration.class) +@Manageable public interface ProcessDefinitionApi { @@ -31,6 +36,7 @@ public interface ProcessDefinitionApi { * 获取活跃的流程定义分页 */ @GetMapping("/api/process/definition/page") + @InvokeMode(SYNC) CommonResponse> getProcessDefinitionPage(@Validated @RequestBody BpmnProcessDefinitionPageDTO dto); /** @@ -40,6 +46,7 @@ public interface ProcessDefinitionApi { * @return */ @PutMapping("/api/process/definition/update") + @InvokeMode(SYNC) CommonResponse updateProcessDefinition(@Validated @RequestBody BpmnProcessDefinitionUpdateDTO dto); /** @@ -49,6 +56,7 @@ public interface ProcessDefinitionApi { * @return 流程定义 */ @GetMapping("/api/process/definition/get") + @InvokeMode(SYNC) CommonResponse getProcessDefinition(@NotBlank(message = "流程定义 ID 不能为空") @RequestParam String processDefinitionId); /** @@ -58,6 +66,7 @@ public interface ProcessDefinitionApi { * @return 流程定义 */ @GetMapping("/api/process/definition/getByDeploymentId") + @InvokeMode(SYNC) CommonResponse getProcessDefinitionByDeploymentId( @NotBlank(message = "流程部署 ID 不能为空") @RequestParam String deploymentId); @@ -69,6 +78,7 @@ public interface ProcessDefinitionApi { * @return 流程定义 */ @GetMapping("/api/process/definition/active/getByKey") + @InvokeMode(SYNC) CommonResponse getActiveProcessDefinitionByKey(@NotBlank(message = "模型定义KEY不能为空") @RequestParam String key); @@ -79,6 +89,7 @@ public interface ProcessDefinitionApi { * {@see SuspensionState} */ @PutMapping("/api/process/definition/state/update") + @InvokeMode(SYNC) CommonResponse getActiveProcessDefinitionByKey(@NotBlank(message = "流程定义ID不能为空") @RequestParam String processDefinitionId, @NotNull(message = "状态不能为空") @RequestParam Integer state); @@ -88,6 +99,7 @@ public interface ProcessDefinitionApi { * @return 流程定义ID */ @GetMapping("/api/process/definition/active/getByCategory") + @InvokeMode(SYNC) CommonResponse getActiveProcessDefinitionId(@RequestParam(required = false) String tenantId, @NotBlank(message = "分类不能为空") @RequestParam(required = false) String category); @@ -97,6 +109,7 @@ public interface ProcessDefinitionApi { * @return 流程定义ID */ @GetMapping("/api/process/definition/active/json/model") + @InvokeMode(SYNC) CommonResponse getActiveProcessDefinitionJsonModel(@NotBlank(message = "模型 ID 不能为空") @RequestParam(required = false) String modelId, @NotBlank(message = "分类不能为空") @RequestParam(required = false) String category, @RequestParam(required = false) String tenantId); @@ -109,6 +122,7 @@ public interface ProcessDefinitionApi { * @return */ @GetMapping("/api/process/definition/delete") + @InvokeMode(SYNC) CommonResponse delete(@NotBlank(message = "流程定义部署 ID 不能为空") String deploymentId, @RequestParam(required = false) Boolean cascade); } diff --git a/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/bpmn/ProcessInstanceApi.java b/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/bpmn/ProcessInstanceApi.java index 78cd9ccde..e2368600f 100644 --- a/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/bpmn/ProcessInstanceApi.java +++ b/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/bpmn/ProcessInstanceApi.java @@ -1,6 +1,8 @@ package cn.axzo.workflow.client.feign.bpmn; 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.process.BpmnProcessInstanceAbortDTO; import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceAdminPageReqVO; import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceCancelDTO; @@ -34,28 +36,16 @@ import javax.validation.constraints.NotNull; import java.util.List; import java.util.Map; +import static cn.axzo.workflow.common.enums.RpcInvokeModeEnum.SYNC; + /** * 流程实例 API * * @author wangli * @since 2023/9/21 16:26 */ -@FeignClient(name = "workflow-engine", url = "${axzo.service.workflow-engine:http://workflow-engine:8080}", configuration = CommonFeignConfiguration.class) +//@FeignClient(name = "workflow-engine", url = "${axzo.service.workflow-engine:http://workflow-engine:8080}", configuration = CommonFeignConfiguration.class) public interface ProcessInstanceApi { - /** - * 查询所有的审批流 - * - * @return - */ - @PostMapping("/api/process/instance/page/all") - CommonResponse> getAllProcessInstancePage(@Validated @RequestBody BpmnProcessInstanceAdminPageReqVO dto); - - /** - * 我发起的审批列表 - */ - @PostMapping("/api/process/instance/page/my") - CommonResponse> getMyProcessInstancePage(@Validated @RequestBody BpmnProcessInstanceMyPageReqVO dto); - /** * 创建审批流程 * @@ -67,18 +57,11 @@ public interface ProcessInstanceApi { * * @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) CommonResponse createProcessInstance(@Validated @RequestBody BpmnProcessInstanceCreateDTO dto); - /** - * 创建审批流程并带上表单 - * - * @param dto - * @return - */ - @PostMapping("/api/process/instance/form/create") - CommonResponse createProcessInstanceWith(@Validated @RequestBody BpmnProcessInstanceCreateWithFormDTO dto); - /** * 发起人主动撤回审核 * @@ -91,6 +74,7 @@ public interface ProcessInstanceApi { * @param dto {@link BpmnProcessInstanceCancelDTO} * @return */ + @Operation(summary = "发起人主动撤回审核,MQ 触发规则:1. 当前流程实例中现存的任务会依次触发 process-task-deleted 事件,2. 当前流程实例会触发 process-instance-cancelled 事件") @DeleteMapping("/api/process/instance/cancel") CommonResponse cancelProcessInstance(@Validated @RequestBody BpmnProcessInstanceCancelDTO dto); @@ -100,6 +84,7 @@ public interface ProcessInstanceApi { * @param dto * @return */ + @Operation(summary = "中止流程实例") @DeleteMapping("/api/process/instance/abort") CommonResponse abortProcessInstance(@Validated @RequestBody BpmnProcessInstanceAbortDTO dto); @@ -109,16 +94,19 @@ public interface ProcessInstanceApi { * @param dtos * @return */ + @Operation(summary = "批量中止流程实例") @DeleteMapping("/api/process/instance/batch/abort") CommonResponse batchAbortProcessInstance(@Validated @RequestBody List dtos); /** - * 抄送流程实例 + * 抄送流程实例(未实现) * * @param dto * @return */ + @Operation(summary = "抄送流程实例") @PostMapping("/api/process/instance/carbon-copy") + @Deprecated CommonResponse carbonCopyProcessInstance(@Validated @RequestBody BpmnProcessInstanceCarbonCopyDTO dto); /** @@ -127,9 +115,56 @@ public interface ProcessInstanceApi { * @param dto {@link BpmnProcessInstanceQueryDTO} 可根据 Id,BusinessKey进行查询 * @return 流程实例, 租户Id不必传 */ + @Operation(summary = "获得流程实例") @GetMapping("/api/process/instance/get") + @InvokeMode(SYNC) CommonResponse getProcessInstanceVO(@Validated @RequestBody BpmnProcessInstanceQueryDTO dto); + /** + * 获取指定流程实例的流程变量 + * + * @param processInstanceId + * @param tenantId + * @return + */ + @Operation(summary = "获取指定流程实例的流程变量") + @GetMapping("/api/process/instance/cooperation-org") + @InvokeMode(SYNC) + CommonResponse> getProcessVariables(@NotBlank(message = "流程实例 ID 不能为空") @RequestParam String processInstanceId, + @Nullable @RequestParam(required = false) String tenantId); + + /** + * 创建审批流程并带上表单 + * + * @param dto + * @return + */ + @Operation(summary = "创建审批流程并带上表单") + @PostMapping("/api/process/instance/form/create") + @Manageable + @InvokeMode(SYNC) + CommonResponse createProcessInstanceWith(@Validated @RequestBody BpmnProcessInstanceCreateWithFormDTO dto); + + /** + * 查询所有的审批流 + * + * @return + */ + @Operation(summary = "查询所有的审批流") + @PostMapping("/api/process/instance/page/all") + @Manageable + @InvokeMode(SYNC) + CommonResponse> getAllProcessInstancePage(@Validated @RequestBody BpmnProcessInstanceAdminPageReqVO dto); + + /** + * 我发起的审批列表 + */ + @Operation(summary = "我发起的审批列表") + @PostMapping("/api/process/instance/page/my") + @Manageable + @InvokeMode(SYNC) + CommonResponse> getMyProcessInstancePage(@Validated @RequestBody BpmnProcessInstanceMyPageReqVO dto); + /** * 更新流程定义的状态 * @@ -138,6 +173,8 @@ public interface ProcessInstanceApi { */ @Operation(summary = "更新指定流程定义的版本的状态, 处于 suspended 状态的流程模型将不能再发起实例") @PutMapping("/api/process/instance/status/update") + @Manageable + @InvokeMode(SYNC) CommonResponse updateProcessStatus(@NotBlank(message = "流程定义 ID 不能为空") @RequestParam String processDefinitionId, @NotNull(message = "状态不能为空") @RequestParam Integer status); @@ -148,7 +185,10 @@ public interface ProcessInstanceApi { * @param tenantId * @return */ + @Operation(summary = "获取审批流程实例的运行图") @GetMapping("/api/process/instance/graphical") + @Manageable + @InvokeMode(SYNC) CommonResponse processInstanceGraphical(@NotBlank(message = "流程实例 ID 不能为空") @RequestParam String processInstanceId, @Nullable @RequestParam(required = false) String tenantId); @@ -157,7 +197,10 @@ public interface ProcessInstanceApi { * * @return */ + @Operation(summary = "推断指定流程实例的所有节点执行顺序") @GetMapping("/api/process/instance/node/forecasting") + @Manageable + @InvokeMode(SYNC) CommonResponse> processInstanceNodeForecast(@NotBlank(message = "流程实例 ID 不能为空") @RequestParam(required = false) String processInstanceId, @Nullable @RequestParam(required = false) String tenantId); @@ -168,23 +211,15 @@ public interface ProcessInstanceApi { * 如果为假时,才结合 nodeDefinitionKeys 过滤掉传入的节点 * @return */ + @Operation(summary = "推断指定流程实例的过滤掉部分节点执行顺序") @GetMapping("/api/process/instance/node/filter/forecasting") + @Manageable + @InvokeMode(SYNC) CommonResponse> 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); - /** - * 获取指定流程实例的流程变量 - * - * @param processInstanceId - * @param tenantId - * @return - */ - @GetMapping("/api/process/instance/cooperation-org") - CommonResponse> getProcessVariables(@NotBlank(message = "流程实例 ID 不能为空") @RequestParam String processInstanceId, - @Nullable @RequestParam(required = false) String tenantId); - /** * 查询实例的租户集合 * @@ -192,6 +227,8 @@ public interface ProcessInstanceApi { */ @Operation(summary = "查询实例的租户集合") @GetMapping("/api/process/instance/tenant/ids") + @Manageable + @InvokeMode(SYNC) CommonResponse> getTenantIds(); /** @@ -201,5 +238,7 @@ public interface ProcessInstanceApi { */ @Operation(summary = "校验指定流程实例下,是否存在指定的审批人") @PostMapping("/api/process/instance/check/approver") + @Manageable + @InvokeMode(SYNC) CommonResponse checkInstanceApprover(@Validated @RequestBody BpmnProcessInstanceCheckApproverDTO dto); } diff --git a/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/bpmn/ProcessJobApi.java b/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/bpmn/ProcessJobApi.java index 8a4169893..906bbc1f3 100644 --- a/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/bpmn/ProcessJobApi.java +++ b/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/bpmn/ProcessJobApi.java @@ -1,15 +1,30 @@ package cn.axzo.workflow.client.feign.bpmn; import cn.axzo.workflow.client.config.CommonFeignConfiguration; +import cn.axzo.workflow.common.annotation.InvokeMode; +import cn.axzo.workflow.common.annotation.Manageable; import cn.azxo.framework.common.model.CommonResponse; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; -@FeignClient(name = "workflow-engine", url = "${axzo.service.workflow-engine:http://workflow-engine:8080}", configuration = CommonFeignConfiguration.class) +import static cn.axzo.workflow.common.enums.RpcInvokeModeEnum.SYNC; + +//@FeignClient(name = "workflow-engine", url = "${axzo.service.workflow-engine:http://workflow-engine:8080}", configuration = CommonFeignConfiguration.class) public interface ProcessJobApi { + /** + * 将死信队列中的任务转移到正常 JOB 队列中 + *

+ * 入参是二选一:当只有 jobId 时,仅将指定的 JOB 转移到正常的队列中; + * 而传入的是具体的实例 ID,那么会将这个流程下的所有在死信队列中的任务都转移到正常的队列中 + * + * @param jobId 具体的 JOB ID + * @param procInstId 具体的实例 ID + * @return + */ @GetMapping("/api/process/job/dead-letter/resume") + @Manageable CommonResponse executeDeadLetterJobAction(@RequestParam(required = false) String jobId, @RequestParam(required = false) String procInstId); } diff --git a/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/bpmn/ProcessModelApi.java b/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/bpmn/ProcessModelApi.java index 4f203e3f7..8fc7f7f00 100644 --- a/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/bpmn/ProcessModelApi.java +++ b/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/bpmn/ProcessModelApi.java @@ -1,6 +1,8 @@ package cn.axzo.workflow.client.feign.bpmn; 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.model.BpmnModelCreateDTO; import cn.axzo.workflow.common.model.request.bpmn.model.BpmnModelSearchDTO; import cn.axzo.workflow.common.model.request.bpmn.model.BpmnModelUpdateDTO; @@ -22,13 +24,16 @@ import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import java.util.List; +import static cn.axzo.workflow.common.enums.RpcInvokeModeEnum.SYNC; + /** * 流程模型 API * * @author wangli * @since 2023/9/21 15:47 */ -@FeignClient(name = "workflow-engine", url = "${axzo.service.workflow-engine:http://workflow-engine:8080}", configuration = CommonFeignConfiguration.class) +//@FeignClient(name = "workflow-engine", url = "${axzo.service.workflow-engine:http://workflow-engine:8080}", configuration = CommonFeignConfiguration.class) +@Manageable public interface ProcessModelApi { /** @@ -39,6 +44,7 @@ public interface ProcessModelApi { */ @Operation(summary = "流程模型列表") @GetMapping("/api/process/model/page") + @InvokeMode(SYNC) CommonResponse> page(@Validated @RequestBody BpmnModelSearchDTO dto); /** @@ -47,6 +53,7 @@ public interface ProcessModelApi { */ @Operation(summary = "创建流程模型") @PostMapping("/api/process/model/create") + @InvokeMode(SYNC) CommonResponse create(@Validated @RequestBody BpmnModelCreateDTO dto); /** @@ -54,6 +61,7 @@ public interface ProcessModelApi { */ @Operation(summary = "通过模型ID查询指定流程模型") @GetMapping("/api/process/model/get") + @InvokeMode(SYNC) CommonResponse getById(@NotBlank(message = "流程模型 ID 不能为空") @RequestParam(required = false) String processModelId, @RequestParam(required = false) String tenantId); @@ -62,6 +70,7 @@ public interface ProcessModelApi { */ @Operation(summary = "通过模型KEY查询指定流程模型") @GetMapping("/api/process/model/getByKey") + @InvokeMode(SYNC) CommonResponse getByKey(@NotBlank(message = "流程模型 KEY 不能为空") @RequestParam(required = false) String processModelKey, @NotBlank(message = "租户不能为空") @RequestParam(required = false) String tenantId); @@ -74,6 +83,7 @@ public interface ProcessModelApi { */ @Operation(summary = "获取指定模型的扩展属性") @GetMapping("/api/process/model/ext") + @InvokeMode(SYNC) CommonResponse getModelExt(@NotBlank(message = "模型 ID 不能为空") @RequestParam(required = false) String modelId); /** @@ -81,6 +91,7 @@ public interface ProcessModelApi { */ @Operation(summary = "更新流程模型") @PutMapping("/api/process/model/update") + @InvokeMode(SYNC) CommonResponse update(@RequestBody BpmnModelUpdateDTO dto); @@ -91,6 +102,7 @@ public interface ProcessModelApi { */ @Operation(summary = "通过模型 ID 部署流程模型") @PostMapping("/api/process/model/deploy") + @InvokeMode(SYNC) CommonResponse deployById(@NotBlank(message = "流程模型 ID 不能为空") @RequestParam(required = false) String processModelId, @RequestParam(required = false, defaultValue = "") String modelTenantId, @RequestParam(required = false) String operator); @@ -102,6 +114,7 @@ public interface ProcessModelApi { */ @Operation(summary = "通过模型 KEY 部署流程模型") @PostMapping("/api/process/model/deployByKey") + @InvokeMode(SYNC) CommonResponse deployByKey(@NotBlank(message = "流程模型 KEY 不能为空") @RequestParam(required = false) String processModelKey, @NotBlank(message = "租户不能为空") @RequestParam(required = false) String modelTenantId, @RequestParam(required = false) String operator); @@ -116,6 +129,7 @@ public interface ProcessModelApi { */ @Operation(summary = "通过模型 ID 取消部署流程模型") @PostMapping("/api/process/model/undeploy") + @InvokeMode(SYNC) CommonResponse unDeployById(@NotBlank(message = "流程模型 ID 不能为空") @RequestParam(required = false) String processModelId, @RequestParam(required = false, defaultValue = "") String tenantId, @RequestParam(required = false) String operator); @@ -125,6 +139,7 @@ public interface ProcessModelApi { */ @Operation(summary = "删除指定模型 ID 的流程模型") @DeleteMapping("/api/process/model/delete") + @InvokeMode(SYNC) CommonResponse deleteById(@NotBlank(message = "流程模型 ID 不能为空") @RequestParam String processModelId, @RequestParam(required = false, defaultValue = "") String tenantId); @@ -137,6 +152,7 @@ public interface ProcessModelApi { */ @Operation(summary = "删除指定模型 KEY 的流程模型") @DeleteMapping("/api/process/model/deleteByKey") + @InvokeMode(SYNC) CommonResponse deleteByKey(@NotBlank(message = "流程模型 KEY 不能为空") @RequestParam String processModelKey, @RequestParam(required = false, defaultValue = "") String tenantId); @@ -150,6 +166,7 @@ public interface ProcessModelApi { */ @Operation(summary = "修改模型状态") @PostMapping("/api/process/model/changeStatus") + @InvokeMode(SYNC) CommonResponse changeStatus(@NotBlank(message = "模型 ID 不能为空") @RequestParam String modelId, @NotNull(message = "状态不能为空") @RequestParam Integer status, @RequestParam(required = false) String operator); @@ -161,6 +178,7 @@ public interface ProcessModelApi { */ @Operation(summary = "查询流程模型使用的分类列表") @GetMapping("/api/process/model/category/ids") + @InvokeMode(SYNC) CommonResponse> getModelCategoryList(); /** @@ -170,5 +188,6 @@ public interface ProcessModelApi { */ @Operation(summary = "查询模型的租户集合") @GetMapping("/api/process/model/tenant/ids") - CommonResponse> getTenantIds(); + @InvokeMode(SYNC) + CommonResponse> getModelTenantIds(); } diff --git a/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/bpmn/ProcessTaskApi.java b/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/bpmn/ProcessTaskApi.java index 7a679ba47..80d57d980 100644 --- a/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/bpmn/ProcessTaskApi.java +++ b/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/bpmn/ProcessTaskApi.java @@ -1,6 +1,8 @@ package cn.axzo.workflow.client.feign.bpmn; 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.task.BpmnRobotTaskCompleteDTO; import cn.axzo.workflow.common.model.request.bpmn.task.BpmnRobotTaskCreateDTO; import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskAttachmentDTO; @@ -28,10 +30,12 @@ import org.springframework.web.bind.annotation.RequestParam; import javax.annotation.Nullable; import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; +import javax.validation.constraints.NotEmpty; import java.util.List; import java.util.Map; +import static cn.axzo.workflow.common.enums.RpcInvokeModeEnum.SYNC; + /** * 流程任务 API @@ -39,46 +43,9 @@ import java.util.Map; * @author wangli * @since 2023/9/21 16:26 */ -@FeignClient(name = "workflow-engine", url = "${axzo.service.workflow-engine:http://workflow-engine:8080}", configuration = CommonFeignConfiguration.class) +//@FeignClient(name = "workflow-engine", url = "${axzo.service.workflow-engine:http://workflow-engine:8080}", configuration = CommonFeignConfiguration.class) public interface ProcessTaskApi { - /** - * 待审核列表 - */ - @GetMapping("/api/process/task/page/todo") - CommonResponse> getTodoTaskPage(@Validated @RequestBody BpmnTaskPageSearchDTO dto); - - /** - * 已完成的审批列表 - */ - @GetMapping("/api/process/task/page/done") - CommonResponse> getDoneTaskPage(@Validated @RequestBody BpmnTaskPageSearchDTO dto); - - /** - * 获取指定流程实例的审批过程信息 - *

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

- * 分组结构 - */ - @GetMapping("/api/process/task/list/group") - CommonResponse> getTaskListGroupByProcessInstanceId(@NotBlank(message = - "流程实例 ID 不能为空") @RequestParam String processInstanceId, @Nullable @RequestParam(required = false) String tenantId); - - /** - * 获取实例正在审核的人列表 - */ - @GetMapping("/api/process/task/active/list") - CommonResponse> getActiveTasksByProcessInstanceId(@NotBlank(message = "流程实例 ID 不能为空") @RequestParam String processInstanceId, - @NotBlank(message = "租户不能为空") @RequestParam String tenantId); - /** * 同意 * @@ -90,6 +57,7 @@ public interface ProcessTaskApi { * 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") CommonResponse approveTask(@Validated @RequestBody BpmnTaskAuditDTO dto); @@ -99,6 +67,7 @@ public interface ProcessTaskApi { * @param dtos * @return */ + @Operation(summary = "批量同意") @PostMapping("/api/process/task/batch/approve") CommonResponse batchApproveTask(@Validated @RequestBody List dtos); @@ -111,6 +80,7 @@ public interface ProcessTaskApi { * 2. 当前流程实例会触发 process-instance-rejected 事件 * */ + @Operation(summary = "驳回,MQ 触发规则:1. 当前审批任务会触发 process-task-deleted 事件, 2. 当前流程实例会触发 process-instance-rejected 事件") @PostMapping("/api/process/task/reject") CommonResponse rejectTask(@Validated @RequestBody BpmnTaskAuditDTO dto); @@ -153,16 +123,6 @@ public interface ProcessTaskApi { @PostMapping("/api/process/task/comment") CommonResponse commentTask(@Validated @RequestBody BpmnTaskCommentDTO dto); - /** - * 添加附件 - * - * @param dto - * @return - */ - @Operation(summary = "添加附件") - @PostMapping("/api/process/task/attachment") - CommonResponse addAttachment(@Validated @RequestBody BpmnTaskAttachmentDTO dto); - /** * 加签 * @@ -181,6 +141,8 @@ public interface ProcessTaskApi { */ @Operation(summary = "审批流程催办") @PostMapping("/api/process/task/remind") + @Manageable + @InvokeMode(SYNC) CommonResponse remindTask(@Validated @RequestBody BpmnTaskRemindDTO dto); /** @@ -203,6 +165,69 @@ public interface ProcessTaskApi { @PostMapping("/api/process/task/robot/complete") CommonResponse completeRobotTask(@Validated @RequestBody BpmnRobotTaskCompleteDTO dto); + /** + * 添加附件 + * + * @param dto + * @return + */ + @Operation(summary = "添加附件") + @PostMapping("/api/process/task/attachment") + @Manageable + CommonResponse addAttachment(@Validated @RequestBody BpmnTaskAttachmentDTO dto); + + /** + * 待审核列表 + */ + @Operation(summary = "待审核列表") + @GetMapping("/api/process/task/page/todo") + @Manageable + @InvokeMode(SYNC) + CommonResponse> getTodoTaskPage(@Validated @RequestBody BpmnTaskPageSearchDTO dto); + + /** + * 已完成的审批列表 + */ + @Operation(summary = "已完成的审批列表") + @GetMapping("/api/process/task/page/done") + @Manageable + @InvokeMode(SYNC) + CommonResponse> getDoneTaskPage(@Validated @RequestBody BpmnTaskPageSearchDTO dto); + + /** + * 获取指定流程实例的审批过程信息 + *

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

+ * 分组结构 + */ + @Operation(summary = "获取指定流程实例的审批过程信息") + @GetMapping("/api/process/task/list/group") + @Manageable + @InvokeMode(SYNC) + CommonResponse> getTaskListGroupByProcessInstanceId(@NotBlank(message = + "流程实例 ID 不能为空") @RequestParam String processInstanceId, @Nullable @RequestParam(required = false) String tenantId); + + /** + * 获取实例正在审核的人列表 + */ + @Operation(summary = "获取实例正在审核的人列表") + @GetMapping("/api/process/task/active/list") + @Manageable + @InvokeMode(SYNC) + CommonResponse> getActiveTasksByProcessInstanceId(@NotBlank(message = "流程实例 ID 不能为空") @RequestParam String processInstanceId, + @NotBlank(message = "租户不能为空") @RequestParam String tenantId); + /** * 根据实例 ID 和自然人 ID 查询对应待处理的任务 ID * @@ -210,6 +235,8 @@ public interface ProcessTaskApi { */ @Operation(summary = "根据实例 ID 和自然人 ID 查询对应待处理的任务 ID") @GetMapping("/api/process/task/find") + @Manageable + @InvokeMode(SYNC) CommonResponse findTaskIdByInstanceIdAndPersonId(@RequestParam(required = false) @NotBlank(message = "流程实例 ID 不能为空") String processInstanceId, @RequestParam(required = false) @NotBlank(message = "自然人 ID 不能为空") String personId); @@ -220,6 +247,8 @@ public interface ProcessTaskApi { */ @Operation(summary = "根据实例 ID列表 和自然人 ID 查询对应待处理的任务 ID") @GetMapping("/api/process/task/batch/find") - CommonResponse> findTaskIdByInstanceIdsAndPersonId(@RequestParam(required = false) @NotNull(message = "流程实例 ID列表 不能为空") List processInstanceIds, + @Manageable + @InvokeMode(SYNC) + CommonResponse> findTaskIdByInstanceIdsAndPersonId(@RequestParam(required = false) @NotEmpty(message = "流程实例 ID列表 不能为空") List processInstanceIds, @RequestParam(required = false) @NotBlank(message = "自然人 ID 不能为空") String personId); } diff --git a/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/bpmn/ProcessVariableApi.java b/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/bpmn/ProcessVariableApi.java index a6fe0f77d..2c219f88b 100644 --- a/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/bpmn/ProcessVariableApi.java +++ b/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/bpmn/ProcessVariableApi.java @@ -1,6 +1,8 @@ package cn.axzo.workflow.client.feign.bpmn; 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.RestBpmnProcessVariable; import cn.azxo.framework.common.model.CommonResponse; import org.springframework.cloud.openfeign.FeignClient; @@ -13,16 +15,20 @@ import org.springframework.web.bind.annotation.RequestParam; import javax.validation.constraints.NotBlank; +import static cn.axzo.workflow.common.enums.RpcInvokeModeEnum.SYNC; + /** * 流程变量api */ -@FeignClient(name = "workflow-engine", url = "${axzo.service.workflow-engine:http://workflow-engine:8080}", configuration = CommonFeignConfiguration.class) +//@FeignClient(name = "workflow-engine", url = "${axzo.service.workflow-engine:http://workflow-engine:8080}", configuration = CommonFeignConfiguration.class) +@Manageable public interface ProcessVariableApi { /** * 为指定流程新增变量 */ @PostMapping("/api/process/variable/create/{executionId}") + @InvokeMode(SYNC) CommonResponse createVariable(@PathVariable @NotBlank(message = "流程实例 ID 不能为空") String executionId, @RequestBody @Validated RestBpmnProcessVariable restVariable); @@ -34,6 +40,7 @@ public interface ProcessVariableApi { * @return */ @PostMapping("/api/process/variable/update/{executionId}") + @InvokeMode(SYNC) CommonResponse updateVariable(@PathVariable @NotBlank(message = "流程实例 ID 不能为空") String executionId, @RequestBody @Validated RestBpmnProcessVariable restVariable); @@ -41,6 +48,7 @@ public interface ProcessVariableApi { * 批量删除流程变量 */ @DeleteMapping("/api/process/variable/delete/{executionId}") + @InvokeMode(SYNC) CommonResponse deleteVariables(@PathVariable("executionId") String executionId, @RequestParam String variableNames, @RequestParam(value = "scope", required = false) String scope); diff --git a/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/manage/ProcessCategoryApi.java b/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/manage/ProcessCategoryApi.java index f3a9b9011..aa1d51447 100644 --- a/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/manage/ProcessCategoryApi.java +++ b/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/manage/ProcessCategoryApi.java @@ -1,6 +1,8 @@ package cn.axzo.workflow.client.feign.manage; 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.category.CategoryConfigCreateDTO; import cn.axzo.workflow.common.model.request.category.CategoryConfigSearchDTO; import cn.axzo.workflow.common.model.request.category.CategoryCreateDTO; @@ -22,6 +24,8 @@ import org.springframework.web.bind.annotation.RequestParam; import java.util.List; +import static cn.axzo.workflow.common.enums.RpcInvokeModeEnum.SYNC; + /** * OMS流程业务管理API @@ -31,7 +35,8 @@ import java.util.List; * @date 2023/11/6 16:01 */ -@FeignClient(name = "workflow-engine", url = "${axzo.service.workflow-engine:http://workflow-engine:8080}", configuration = CommonFeignConfiguration.class) +//@FeignClient(name = "workflow-engine", url = "${axzo.service.workflow-engine:http://workflow-engine:8080}", configuration = CommonFeignConfiguration.class) +@Manageable public interface ProcessCategoryApi { /** @@ -40,6 +45,7 @@ public interface ProcessCategoryApi { * @return */ @GetMapping("/api/process/category/get") + @InvokeMode(SYNC) CommonResponse get(@RequestParam Long id); /** @@ -49,6 +55,7 @@ public interface ProcessCategoryApi { * @return */ @GetMapping("/api/process/category/getByIds") + @InvokeMode(SYNC) CommonResponse> getByIds(@RequestParam List ids); /** @@ -58,6 +65,7 @@ public interface ProcessCategoryApi { * @return */ @GetMapping("/api/process/category/getByValues") + @InvokeMode(SYNC) CommonResponse> getByValues(@RequestParam List values); /** @@ -66,6 +74,7 @@ public interface ProcessCategoryApi { * @return */ @PostMapping("/api/process/category/create") + @InvokeMode(SYNC) CommonResponse create(@Validated @RequestBody CategoryCreateDTO req); /** @@ -74,6 +83,7 @@ public interface ProcessCategoryApi { * @return */ @PutMapping("/api/process/category/update") + @InvokeMode(SYNC) CommonResponse update(@Validated @RequestBody CategoryUpdateDTO dto); /** @@ -83,6 +93,7 @@ public interface ProcessCategoryApi { * @return */ @DeleteMapping("/api/process/category/delete") + @InvokeMode(SYNC) CommonResponse delete(@RequestParam Long id); /** @@ -93,6 +104,7 @@ public interface ProcessCategoryApi { * @return */ @PutMapping("/api/process/category/update/state") + @InvokeMode(SYNC) CommonResponse updateState(@RequestParam Long id, @RequestParam Boolean state); /** @@ -101,6 +113,7 @@ public interface ProcessCategoryApi { * @return */ @PostMapping("/api/process/category/list") + @InvokeMode(SYNC) CommonResponse> list(@RequestBody CategorySearchDTO dto); /** @@ -109,6 +122,7 @@ public interface ProcessCategoryApi { * @return */ @PostMapping("/api/process/category/page/search") + @InvokeMode(SYNC) CommonResponse> search(@RequestBody CategorySearchDTO dto); /** @@ -118,6 +132,7 @@ public interface ProcessCategoryApi { * @return */ @PostMapping("/api/process/category/config/create") + @InvokeMode(SYNC) CommonResponse createConfig(@RequestBody CategoryConfigCreateDTO dto); /** @@ -127,6 +142,7 @@ public interface ProcessCategoryApi { * @return */ @DeleteMapping("/api/process/category/config/delete/{id}") + @InvokeMode(SYNC) CommonResponse deleteConfig(@PathVariable Long id); /** @@ -135,6 +151,7 @@ public interface ProcessCategoryApi { * @return */ @PostMapping("/api/process/category/config/page/search") + @InvokeMode(SYNC) CommonResponse> configSearch(@RequestBody CategoryConfigSearchDTO dto); /** @@ -145,6 +162,7 @@ public interface ProcessCategoryApi { * @return */ @PostMapping("/api/process/category/config/type/update") + @InvokeMode(SYNC) CommonResponse updateCategoryConfigType(@RequestParam Long id, @RequestParam String configType); /** @@ -155,5 +173,6 @@ public interface ProcessCategoryApi { * @return true: 可以发起创建流程实例, false: 不可用 */ @GetMapping("/api/process/category/check/status") + @InvokeMode(SYNC) CommonResponse checkCategoryStatus(@RequestParam Long tenantId, @RequestParam String categoryCode); } diff --git a/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/manage/ProcessConfigApi.java b/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/manage/ProcessConfigApi.java index d949c3267..5f56d5d64 100644 --- a/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/manage/ProcessConfigApi.java +++ b/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/manage/ProcessConfigApi.java @@ -1,6 +1,8 @@ package cn.axzo.workflow.client.feign.manage; 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; @@ -8,6 +10,8 @@ import org.springframework.web.bind.annotation.GetMapping; import java.util.List; +import static cn.axzo.workflow.common.enums.RpcInvokeModeEnum.SYNC; + /** * OMS流程业务管理API @@ -17,7 +21,8 @@ import java.util.List; * @date 2023/11/6 16:01 */ -@FeignClient(name = "workflow-engine", url = "${axzo.service.workflow-engine:http://workflow-engine:8080}", configuration = CommonFeignConfiguration.class) +//@FeignClient(name = "workflow-engine", url = "${axzo.service.workflow-engine:http://workflow-engine:8080}", configuration = CommonFeignConfiguration.class) +@Manageable public interface ProcessConfigApi { /** @@ -26,6 +31,7 @@ public interface ProcessConfigApi { * @return 流程操作按钮列表 */ @GetMapping("/api/process/config/button/list") + @InvokeMode(SYNC) CommonResponse> getDefaultButtons(); diff --git a/workflow-engine-common/pom.xml b/workflow-engine-common/pom.xml index 943cb9df0..fcb362248 100644 --- a/workflow-engine-common/pom.xml +++ b/workflow-engine-common/pom.xml @@ -11,7 +11,7 @@ workflow-engine-common jar - workflow-engine-common + workflow Engine Common diff --git a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/annotation/InvokeMode.java b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/annotation/InvokeMode.java new file mode 100644 index 000000000..26a45dcf6 --- /dev/null +++ b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/annotation/InvokeMode.java @@ -0,0 +1,28 @@ +package cn.axzo.workflow.common.annotation; + +import cn.axzo.workflow.common.enums.RpcInvokeModeEnum; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static cn.axzo.workflow.common.enums.RpcInvokeModeEnum.ASYNC; + +/** + * 用于标记接口的方法调用模式 + * + * @author wangli + * @since 2024/6/1 19:01 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface InvokeMode { + + /** + * 调用模式, 默认异步 + */ + RpcInvokeModeEnum value() default ASYNC; +} diff --git a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/annotation/Manageable.java b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/annotation/Manageable.java new file mode 100644 index 000000000..f8d9622a1 --- /dev/null +++ b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/annotation/Manageable.java @@ -0,0 +1,10 @@ +package cn.axzo.workflow.common.annotation; + +/** + * 控制接口是否调用受限,标记了注解的方法,表示受控,不允许暴露给客户端使用 + * + * @author wangli + * @since 2024/6/7 18:21 + */ +public @interface Manageable { +} diff --git a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/constant/BpmnConstants.java b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/constant/BpmnConstants.java index 69865ff76..9a3a41d7b 100644 --- a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/constant/BpmnConstants.java +++ b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/constant/BpmnConstants.java @@ -11,6 +11,7 @@ public interface BpmnConstants { */ String FLOWABLE_SKIP_EXPRESSION_ENABLE = "[_FLOWABLE_SKIP_EXPRESSION_ENABLED_]"; String MQ_UNIQUE_ID = "[_MQ_UNIQUE_ID_]"; + String PROCESS_OWNERSHIP_APPLICATION = "[_PROCESS_OWNERSHIP_APPLICATION_]"; String WORKFLOW_ENGINE_VERSION = "[_WORKFLOW_ENGINE_VERSION_]"; String INTERNAL_INITIATOR = "[_INTERNAL_INITIATOR_]"; @Deprecated @@ -154,5 +155,6 @@ public interface BpmnConstants { /** * 用于 MQ 的 Header, 记录当前事件的归属应用 */ - String MQ_OWNERSHIP_APP = "MQ_OWNERSHIP_APPLICATION"; + String MQ_OWNERSHIP_APPLICATION = "MQ_OWNERSHIP_APPLICATION"; + String MQ_OWNERSHIP_PROCESS_DEFINITION_KEY = "MQ_OWNERSHIP_PROCESS_DEFINITION_KEY"; } diff --git a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/constant/StarterConstants.java b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/constant/StarterConstants.java new file mode 100644 index 000000000..66747128f --- /dev/null +++ b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/constant/StarterConstants.java @@ -0,0 +1,16 @@ +package cn.axzo.workflow.common.constant; + +/** + * Starter 常量类 + * + * @author wangli + * @since 2024/5/29 11:13 + */ +public interface StarterConstants { + String STARTER_INVOKE_MODE = "WORKFLOW-ENGINE-STARTER-INVOKE-MODE"; + String DEBUGGING_MQ_SUFFIX = "_debugging"; + String K8S_POD_NAME_SPACE = "MY_POD_NAMESPACE"; + String NACOS_PROFILES_ACTIVE = "NACOS_PROFILES_ACTIVE"; + String MQ_GID_NAME_SEGMENT = "GID_SEGMENT"; + String ENABLE_MANAGEABLE = "ENABLE_MANAGEABLE"; +} diff --git a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/enums/RpcInvokeModeEnum.java b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/enums/RpcInvokeModeEnum.java new file mode 100644 index 000000000..4a8f4c439 --- /dev/null +++ b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/enums/RpcInvokeModeEnum.java @@ -0,0 +1,12 @@ +package cn.axzo.workflow.common.enums; + +/** + * PRC 调用模式枚举 + * + * @author wangli + * @since 2024/5/29 10:27 + */ +public enum RpcInvokeModeEnum { + SYNC, + ASYNC, +} diff --git a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/enums/WorkflowEngineEventEnum.java b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/enums/WorkflowEngineEventEnum.java index 3a0980518..b03fb9847 100644 --- a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/enums/WorkflowEngineEventEnum.java +++ b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/enums/WorkflowEngineEventEnum.java @@ -11,7 +11,7 @@ import cn.axzo.framework.rocketmq.Event; public enum WorkflowEngineEventEnum { WORKFLOW_ENGINE_SERVER("workflow-engine", "workflow-engine-server", "引擎服务端事件"), - WORKFLOW_ENGINE_CLIENT("workflow-engine", "workflow-engine-client-%s", "引擎客户端事件"), + WORKFLOW_ENGINE_STARTER("workflow-engine", "workflow-engine-starter-%s", "引擎客户端事件"), ; private final String module; @@ -34,4 +34,16 @@ public enum WorkflowEngineEventEnum { return eventCode; } + public String getModule() { + return module; + } + + public String getTag() { + return eventCode.getName(); + } + + public String getDesc() { + return desc; + } + } diff --git a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/dto/CooperationOrgDTO.java b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/dto/CooperationOrgDTO.java index 507c9f8c1..858e68eb3 100644 --- a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/dto/CooperationOrgDTO.java +++ b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/dto/CooperationOrgDTO.java @@ -20,6 +20,7 @@ import java.util.List; @Accessors(chain = true) @NoArgsConstructor @AllArgsConstructor +@Builder public class CooperationOrgDTO implements Serializable { private static final long serialVersionUID = 4739924705980062997L; diff --git a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/BpmnProcessVariable.java b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/BpmnProcessVariable.java index 125df6b66..149602ce8 100644 --- a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/BpmnProcessVariable.java +++ b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/BpmnProcessVariable.java @@ -22,9 +22,9 @@ public class BpmnProcessVariable { @NotBlank(message = "流程变量名称不能为空") private String name; - @ApiModelProperty(value = "流程变量类型") - @NotBlank(message = "流程变量类型不能为空") - private String type; +// @ApiModelProperty(value = "流程变量类型") +// @NotBlank(message = "流程变量类型不能为空") +// private String type; @ApiModelProperty(value = "流程变量值") @NotNull(message = "流程变量值不能为空") diff --git a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/RestBpmnProcessVariable.java b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/RestBpmnProcessVariable.java index 1a5a8ae5b..615330700 100644 --- a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/RestBpmnProcessVariable.java +++ b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/RestBpmnProcessVariable.java @@ -20,7 +20,7 @@ public class RestBpmnProcessVariable extends BpmnProcessVariable { /** * 变量作用域, 目前支持 LOCAL(局部变量) 和 GLOBAL(全局变量) */ - private RestVariableScope scope; + private RestVariableScope scope = RestVariableScope.GLOBAL; public String getVariableScope() { String scope = null; diff --git a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/process/BpmnBasicProcessInstanceQueryDTO.java b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/process/BpmnBasicProcessInstanceQueryDTO.java index 4abf1f7f8..8b770bb76 100644 --- a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/process/BpmnBasicProcessInstanceQueryDTO.java +++ b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/process/BpmnBasicProcessInstanceQueryDTO.java @@ -2,7 +2,10 @@ package cn.axzo.workflow.common.model.request.bpmn.process; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import lombok.experimental.Accessors; import javax.annotation.Nullable; @@ -17,6 +20,9 @@ import javax.validation.constraints.NotBlank; @ApiModel("最基础的流程实例查询入参模型") @Data @Accessors(chain = true) +@AllArgsConstructor +@NoArgsConstructor +@Builder public class BpmnBasicProcessInstanceQueryDTO { /** * 流程实例 ID diff --git a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/process/BpmnProcessInstanceAbortDTO.java b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/process/BpmnProcessInstanceAbortDTO.java index 9f201a93a..734b7651b 100644 --- a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/process/BpmnProcessInstanceAbortDTO.java +++ b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/process/BpmnProcessInstanceAbortDTO.java @@ -2,7 +2,10 @@ package cn.axzo.workflow.common.model.request.bpmn.process; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import org.hibernate.validator.constraints.Length; import javax.validation.constraints.NotBlank; @@ -15,6 +18,9 @@ import javax.validation.constraints.NotBlank; */ @ApiModel("中止流程实例的入参模型") @Data +@AllArgsConstructor +@NoArgsConstructor +@Builder public class BpmnProcessInstanceAbortDTO { /** diff --git a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/process/BpmnProcessInstanceCancelDTO.java b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/process/BpmnProcessInstanceCancelDTO.java index efae1c772..5b44b327c 100644 --- a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/process/BpmnProcessInstanceCancelDTO.java +++ b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/process/BpmnProcessInstanceCancelDTO.java @@ -3,7 +3,10 @@ package cn.axzo.workflow.common.model.request.bpmn.process; import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskDelegateAssigner; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import javax.validation.Valid; import javax.validation.constraints.NotBlank; @@ -17,6 +20,9 @@ import javax.validation.constraints.NotNull; */ @ApiModel("取消流程实例的入参模型") @Data +@AllArgsConstructor +@NoArgsConstructor +@Builder public class BpmnProcessInstanceCancelDTO { /** diff --git a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/process/BpmnProcessInstanceCarbonCopyDTO.java b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/process/BpmnProcessInstanceCarbonCopyDTO.java index 963fb2a3f..bf558b9a0 100644 --- a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/process/BpmnProcessInstanceCarbonCopyDTO.java +++ b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/process/BpmnProcessInstanceCarbonCopyDTO.java @@ -4,7 +4,10 @@ import cn.axzo.workflow.common.model.request.bpmn.task.AttachmentDTO; import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskDelegateAssigner; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotEmpty; @@ -18,6 +21,9 @@ import java.util.List; */ @ApiModel("抄送流程实例的入参模型") @Data +@AllArgsConstructor +@NoArgsConstructor +@Builder public class BpmnProcessInstanceCarbonCopyDTO { /** diff --git a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/process/BpmnProcessInstanceCreateDTO.java b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/process/BpmnProcessInstanceCreateDTO.java index 47e979c87..80c33308d 100644 --- a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/process/BpmnProcessInstanceCreateDTO.java +++ b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/process/BpmnProcessInstanceCreateDTO.java @@ -4,7 +4,10 @@ import cn.axzo.workflow.common.model.dto.CooperationOrgDTO; import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskDelegateAssigner; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import javax.validation.Valid; import javax.validation.constraints.NotEmpty; @@ -17,6 +20,9 @@ import java.util.Map; */ @ApiModel("创建流程实例的入参模型") @Data +@AllArgsConstructor +@NoArgsConstructor +@Builder public class BpmnProcessInstanceCreateDTO { /** @@ -28,7 +34,7 @@ public class BpmnProcessInstanceCreateDTO { /** * 流程定义的标识 *

- * [对应业务分类的 businessId] + * [对应 OMS 系统中审批业务的业务 ID] */ @NotEmpty(message = "流程定义的标识不能为空") private String processDefinitionKey; @@ -56,6 +62,7 @@ public class BpmnProcessInstanceCreateDTO { *

* 用于流程引擎计算对应节点的审批人, 例如第一个审批节点配置的是劳务分包的岗位,第二个审批节点配置的专业分包的角色, * 那么, 组织关系就需要传入劳务分包的信息以及专业分包的信息,如果还有更多的审批节点配置,以此类推. + * 同时,该属性还会用于计算抄送人,以及消息通知的目标接收人 */ @ApiModelProperty(value = "组织关系") @NotNull(message = "组织关系不能为空") @@ -81,17 +88,9 @@ public class BpmnProcessInstanceCreateDTO { private String customProcessInstanceName; /** - * 是否异步执行 + * 是否异步执行,该异步是引擎的一种运行模式 */ @ApiModelProperty(value = "是否异步", notes = "异步时,只接收请求便返回数据") private Boolean async = true; - /** - * 废弃 - * 下级审批人 - */ - // @ApiModelProperty(value = "下级审批人", notes = "可为空,定义选择审批人,如果不为空,则覆盖下一级任务的审核人") - // @Deprecated - // private BpmnTaskDelegateAssigner nextApprover; - } diff --git a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/process/BpmnProcessInstanceQueryDTO.java b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/process/BpmnProcessInstanceQueryDTO.java index 8a445e11b..013ec1689 100644 --- a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/process/BpmnProcessInstanceQueryDTO.java +++ b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/process/BpmnProcessInstanceQueryDTO.java @@ -2,8 +2,11 @@ package cn.axzo.workflow.common.model.request.bpmn.process; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; import lombok.experimental.Accessors; /** diff --git a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/task/BpmnActivitySetAssigneeDTO.java b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/task/BpmnActivitySetAssigneeDTO.java index 32dd5fec4..7ddf139f7 100644 --- a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/task/BpmnActivitySetAssigneeDTO.java +++ b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/task/BpmnActivitySetAssigneeDTO.java @@ -2,7 +2,10 @@ package cn.axzo.workflow.common.model.request.bpmn.task; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import javax.validation.Valid; import javax.validation.constraints.NotBlank; @@ -16,6 +19,9 @@ import java.util.List; */ @ApiModel("业务节点设置审批人") @Data +@AllArgsConstructor +@NoArgsConstructor +@Builder public class BpmnActivitySetAssigneeDTO { /** * PROCESS_ACTIVITY_WAIT_ASSIGNEE 事件中的触发 ID diff --git a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/task/BpmnRobotTaskCompleteDTO.java b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/task/BpmnRobotTaskCompleteDTO.java index 13eab3eec..046387245 100644 --- a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/task/BpmnRobotTaskCompleteDTO.java +++ b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/task/BpmnRobotTaskCompleteDTO.java @@ -2,7 +2,10 @@ package cn.axzo.workflow.common.model.request.bpmn.task; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import javax.validation.Valid; import javax.validation.constraints.NotBlank; @@ -15,6 +18,9 @@ import javax.validation.constraints.NotBlank; */ @ApiModel("完成机器人节点的入参模型") @Data +@AllArgsConstructor +@NoArgsConstructor +@Builder public class BpmnRobotTaskCompleteDTO { /** diff --git a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/task/BpmnRobotTaskCreateDTO.java b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/task/BpmnRobotTaskCreateDTO.java index 7f3b9cd6b..df45977c1 100644 --- a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/task/BpmnRobotTaskCreateDTO.java +++ b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/task/BpmnRobotTaskCreateDTO.java @@ -2,7 +2,10 @@ package cn.axzo.workflow.common.model.request.bpmn.task; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import javax.validation.Valid; import javax.validation.constraints.NotBlank; @@ -16,6 +19,9 @@ import javax.validation.constraints.NotNull; */ @ApiModel("创建机器人节点的入参模型") @Data +@AllArgsConstructor +@NoArgsConstructor +@Builder public class BpmnRobotTaskCreateDTO { /** * 流程实例 ID diff --git a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/task/BpmnTaskAuditDTO.java b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/task/BpmnTaskAuditDTO.java index b841e5b88..d1c7a3638 100644 --- a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/task/BpmnTaskAuditDTO.java +++ b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/task/BpmnTaskAuditDTO.java @@ -3,7 +3,10 @@ package cn.axzo.workflow.common.model.request.bpmn.task; import cn.axzo.workflow.common.enums.BpmnFlowNodeType; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import org.springframework.validation.annotation.Validated; import javax.validation.Valid; @@ -19,6 +22,9 @@ import java.util.List; @ApiModel("审批任务节点的入参模型") @Data @Validated +@AllArgsConstructor +@NoArgsConstructor +@Builder public class BpmnTaskAuditDTO { @ApiModelProperty(value = "任务编号", required = true, example = "1024") diff --git a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/task/BpmnTaskCommentDTO.java b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/task/BpmnTaskCommentDTO.java index 2f02ecbcb..78aaf766c 100644 --- a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/task/BpmnTaskCommentDTO.java +++ b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/task/BpmnTaskCommentDTO.java @@ -2,7 +2,10 @@ package cn.axzo.workflow.common.model.request.bpmn.task; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import org.hibernate.validator.constraints.Length; import javax.validation.Valid; @@ -19,6 +22,9 @@ import java.util.List; */ @ApiModel("评论的入参模型") @Data +@AllArgsConstructor +@NoArgsConstructor +@Builder public class BpmnTaskCommentDTO implements Serializable { /** diff --git a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/task/BpmnTaskCountersignDTO.java b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/task/BpmnTaskCountersignDTO.java index 537d25e15..2dc711af6 100644 --- a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/task/BpmnTaskCountersignDTO.java +++ b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/task/BpmnTaskCountersignDTO.java @@ -2,7 +2,10 @@ package cn.axzo.workflow.common.model.request.bpmn.task; import cn.axzo.workflow.common.enums.BpmnCountersignTypeEnum; import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import lombok.experimental.Accessors; import javax.validation.Valid; @@ -21,6 +24,9 @@ import java.util.List; */ @Data @Accessors(chain = true) +@AllArgsConstructor +@NoArgsConstructor +@Builder public class BpmnTaskCountersignDTO implements Serializable { private static final long serialVersionUID = -8106887960942113552L; diff --git a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/task/BpmnTaskDelegateAssigner.java b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/task/BpmnTaskDelegateAssigner.java index 899584415..cf11bb52c 100644 --- a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/task/BpmnTaskDelegateAssigner.java +++ b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/task/BpmnTaskDelegateAssigner.java @@ -44,7 +44,7 @@ public class BpmnTaskDelegateAssigner implements Serializable { private static final long serialVersionUID = -8106887960942113552L; /** - * 审核人标识, 应该必传 + * 【废弃】审核人标识, 应该必传 *

* 枢智:用户 ID * 安心筑:身份 ID @@ -53,7 +53,7 @@ public class BpmnTaskDelegateAssigner implements Serializable { private String assignee; /** - * 审核人标识扩展信息 + * 【废弃】审核人标识扩展信息 *

* 枢智:可不传 * 安心筑:身份 Type 应该必传 diff --git a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/task/BpmnTaskTransferDTO.java b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/task/BpmnTaskTransferDTO.java index f91c54403..54fb509fc 100644 --- a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/task/BpmnTaskTransferDTO.java +++ b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/request/bpmn/task/BpmnTaskTransferDTO.java @@ -2,7 +2,10 @@ package cn.axzo.workflow.common.model.request.bpmn.task; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import javax.validation.Valid; import javax.validation.constraints.NotBlank; @@ -16,6 +19,9 @@ import java.util.List; */ @ApiModel("转交审批任务的入参模型") @Data +@AllArgsConstructor +@NoArgsConstructor +@Builder public class BpmnTaskTransferDTO { /** diff --git a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/response/mq/MessagePushDTO.java b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/response/mq/MessagePushDTO.java index 95f97ab77..04a5e0d79 100644 --- a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/response/mq/MessagePushDTO.java +++ b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/response/mq/MessagePushDTO.java @@ -31,6 +31,11 @@ public class MessagePushDTO implements Serializable { */ private String processInstanceId; + /** + * 业务 ID + */ + private String processDefinitionKey; + /** * 流程任务 ID */ diff --git a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/response/mq/ProcessTaskDTO.java b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/response/mq/ProcessTaskDTO.java index a3db4b04c..0dfef6a2f 100644 --- a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/response/mq/ProcessTaskDTO.java +++ b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/response/mq/ProcessTaskDTO.java @@ -28,8 +28,11 @@ public class ProcessTaskDTO implements Serializable { private ProcessTaskEventEnum type; /** + * 请使用 {@link ProcessTaskDTO#processDefinitionKey} 代替访问此属性,未来将逐步废弃 + *

* 流程实例所属业务分类,同时也等于流程模型对应的业务分类 ID */ + @Deprecated private String category; /** @@ -41,6 +44,10 @@ public class ProcessTaskDTO implements Serializable { * 流程实例 ID */ private String processInstanceId; + /** + * 流程实例所属业务分类,同时也等于流程模型对应的业务分类 ID + */ + private String processDefinitionKey; /** * 流程定义中当前任务节点 Key diff --git a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/response/mq/WorkflowEngineStarterRpcInvokeDTO.java b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/response/mq/WorkflowEngineStarterRpcInvokeDTO.java new file mode 100644 index 000000000..d01e38598 --- /dev/null +++ b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/model/response/mq/WorkflowEngineStarterRpcInvokeDTO.java @@ -0,0 +1,44 @@ +package cn.axzo.workflow.common.model.response.mq; + +import java.io.Serializable; +import java.util.List; + +/** + * Starter RPC 调用事件模型 + * + * @author wangli + * @since 2024/5/30 11:21 + */ +public class WorkflowEngineStarterRpcInvokeDTO implements Serializable { + private static final long serialVersionUID = -1L; + + private String className; + + private String methodName; + + private List parameters; + + public String getClassName() { + return className; + } + + public void setClassName(String className) { + this.className = className; + } + + public String getMethodName() { + return methodName; + } + + public void setMethodName(String methodName) { + this.methodName = methodName; + } + + public List getParameters() { + return parameters; + } + + public void setParameters(List parameters) { + this.parameters = parameters; + } +} diff --git a/workflow-engine-common/src/main/java/cn/axzo/workflow/common/util/ThreadUtil.java b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/util/ThreadUtil.java new file mode 100644 index 000000000..6a4f1458c --- /dev/null +++ b/workflow-engine-common/src/main/java/cn/axzo/workflow/common/util/ThreadUtil.java @@ -0,0 +1,27 @@ +package cn.axzo.workflow.common.util; + + +import cn.axzo.workflow.common.enums.RpcInvokeModeEnum; + +/** + * TODO + * + * @author wangli + * @since 2024/5/29 23:57 + */ +public class ThreadUtil { + + final static InheritableThreadLocal threadLocal = new InheritableThreadLocal<>(); + + public static void set(RpcInvokeModeEnum rpcInvokeMode) { + threadLocal.set(rpcInvokeMode); + } + + public static RpcInvokeModeEnum get() { + return threadLocal.get(); + } + + public static void clear() { + threadLocal.remove(); + } +} diff --git a/workflow-engine-core/pom.xml b/workflow-engine-core/pom.xml index e3d9c7f72..00e1896d7 100644 --- a/workflow-engine-core/pom.xml +++ b/workflow-engine-core/pom.xml @@ -9,8 +9,7 @@ ${revision} workflow-engine-core jar - - workflow-engine-core + Workflow Engine Core 6.7.2 2.0.0-SNAPSHOT @@ -98,5 +97,9 @@ org.apache.maven maven-artifact + + jakarta.servlet + jakarta.servlet-api + diff --git a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/common/code/BpmnTaskRespCode.java b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/common/code/BpmnTaskRespCode.java index cd2775b32..e3b3b5616 100644 --- a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/common/code/BpmnTaskRespCode.java +++ b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/common/code/BpmnTaskRespCode.java @@ -33,6 +33,7 @@ public enum BpmnTaskRespCode implements IModuleRespCode { FIND_TASK_BY_PERSON_ID_ERROR("018", "流程实例中:【{}】未查找指定自然人:【{}】的待处理的流程任务!"), PROCESS_INSTANCE_IS_NOT_EXIST("019", "Execution:{} 对应流程实例不存在,流程状态异常!"), TASK_TYPE_MISMATCH("020", "节点类型不匹配,当前节点类型:【{}】,指定节点类型:【{}】!"), + PROCESS_CANT_SET_ASSIGNEE("021", "当前审批状态不允许设置审批人"), ; private String code; diff --git a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/common/code/FlowableEngineRespCode.java b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/common/code/FlowableEngineRespCode.java index 91443883f..e82a9f833 100644 --- a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/common/code/FlowableEngineRespCode.java +++ b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/common/code/FlowableEngineRespCode.java @@ -18,6 +18,8 @@ public enum FlowableEngineRespCode implements IModuleRespCode { ENGINE_USER_TASK_TYPE_NOT_SUPPORT("003", "审批指定方式暂不支持"), ENGINE_USER_TASK_PARAM_ERROR("004", "构建后的查询审批人入参为空. 任务节点【nodeId:{}】, 该节点选择的\"审批人所在范围\"是:【{}】,请检查 cooperationOrg 参数"), ENGINE_NOTICE_CUSTOM_FLOW_ELEMENT_ERROR("005", "查询通知目标用户前参数发生异常,未获取到 WorkspaceType"), + ENGINE_ASYNC_COMMAND_EXECUTION_ERROR("006", "引擎出现 SQL 相关异常, 异常信息:【{}】"), + ENGINE_ASYNC_COMMAND_EXECUTION_RETRY_GIVE_UP("007", "命令重试尝试【{}】次仍然失败,并出现异常, 将放弃"), ; private String code; diff --git a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/common/utils/TraceUtil.java b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/common/utils/TraceUtil.java new file mode 100644 index 000000000..86fd83f41 --- /dev/null +++ b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/common/utils/TraceUtil.java @@ -0,0 +1,24 @@ +package cn.axzo.workflow.core.common.utils; + +import org.slf4j.MDC; +import org.springframework.util.StringUtils; + +import static cn.axzo.workflow.common.constant.LogFieldConstants.X_REQUEST_ID; +import static cn.azxo.framework.common.constatns.Constants.CTX_LOG_ID_MDC; + +/** + * 获取 TraceId + * + * @author wangli + * @since 2024/7/3 10:14 + */ +public final class TraceUtil { + + private TraceUtil() { + } + + public static String traceId() { + String xRequestId = MDC.get(X_REQUEST_ID); + return StringUtils.hasText(xRequestId) ? xRequestId : MDC.get(CTX_LOG_ID_MDC); + } +} diff --git a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/conf/FlowableConfiguration.java b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/conf/FlowableConfiguration.java index 5fa140ab4..2b47c563b 100644 --- a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/conf/FlowableConfiguration.java +++ b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/conf/FlowableConfiguration.java @@ -3,6 +3,7 @@ package cn.axzo.workflow.core.conf; import cn.axzo.workflow.core.engine.behavior.CustomActivityBehaviorFactory; import cn.axzo.workflow.core.engine.cmd.CustomCommandContextFactory; import cn.axzo.workflow.core.engine.id.BasedNacosSnowflakeIdGenerator; +import cn.axzo.workflow.core.engine.interceptor.CustomRetryInterceptor; import cn.axzo.workflow.core.engine.job.AsyncAbortProcessInstanceHandler; import cn.axzo.workflow.core.engine.job.AsyncApproveTaskJobHandler; import cn.axzo.workflow.core.engine.job.AsyncBpmnProcessActivityJobHandler; @@ -30,6 +31,7 @@ import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.StringRedisTemplate; import java.time.Duration; import java.util.List; @@ -56,7 +58,8 @@ public class FlowableConfiguration { BpmnProcessActivityService bpmnProcessActivityService, List jobProcessors, NacosServiceManager nacosServiceManager, - NacosDiscoveryProperties nacosDiscoveryProperties) { + NacosDiscoveryProperties nacosDiscoveryProperties, + StringRedisTemplate redisTemplate) { return configuration -> { configuration.setEnableHistoricTaskLogging(true); configuration.setHistoryLevel(HistoryLevel.AUDIT); @@ -92,6 +95,9 @@ public class FlowableConfiguration { new CustomWorkflowEngineExceptionHandler(), new CustomAsyncRunnableExceptionExceptionHandler())); configuration.setCommandContextFactory(new CustomCommandContextFactory()); + configuration.setCustomPreCommandInterceptors(Lists.newArrayList( + new CustomRetryInterceptor() + )); }; } diff --git a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/AbstractCommand.java b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/AbstractCommand.java new file mode 100644 index 000000000..491c72cb8 --- /dev/null +++ b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/AbstractCommand.java @@ -0,0 +1,15 @@ +package cn.axzo.workflow.core.engine.cmd; + + +import org.flowable.common.engine.impl.interceptor.Command; + +/** + * TODO + * + * @author wangli + * @since 2024/7/1 13:59 + */ +public abstract class AbstractCommand implements Command { + + public abstract String paramToJsonString(); +} diff --git a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomAbortProcessInstanceAsyncCmd.java b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomAbortProcessInstanceAsyncCmd.java index 4dbc65b81..ec87d66fa 100644 --- a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomAbortProcessInstanceAsyncCmd.java +++ b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomAbortProcessInstanceAsyncCmd.java @@ -4,7 +4,7 @@ import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceAbo import cn.axzo.workflow.core.common.exception.WorkflowEngineException; import cn.axzo.workflow.core.engine.job.AsyncAbortProcessInstanceHandler; import cn.hutool.json.JSONUtil; -import org.flowable.common.engine.impl.interceptor.Command; +import com.alibaba.fastjson.JSON; import org.flowable.common.engine.impl.interceptor.CommandContext; import org.flowable.engine.HistoryService; import org.flowable.engine.history.HistoricProcessInstance; @@ -23,7 +23,7 @@ import static cn.axzo.workflow.core.common.code.BpmnInstanceRespCode.PROCESS_INS import static cn.axzo.workflow.core.common.code.BpmnInstanceRespCode.PROCESS_INSTANCE_CANT_ABORT; import static cn.axzo.workflow.core.common.code.BpmnInstanceRespCode.PROCESS_INSTANCE_NOT_EXISTS; -public class CustomAbortProcessInstanceAsyncCmd implements Command, Serializable { +public class CustomAbortProcessInstanceAsyncCmd extends AbstractCommand implements Serializable { private final BpmnProcessInstanceAbortDTO dto; @@ -31,6 +31,11 @@ public class CustomAbortProcessInstanceAsyncCmd implements Command, Serial this.dto = dto; } + @Override + public String paramToJsonString() { + return JSON.toJSONString(dto); + } + @Override public Void execute(CommandContext commandContext) { String processInstanceId = dto.getProcessInstanceId(); @@ -80,4 +85,5 @@ public class CustomAbortProcessInstanceAsyncCmd implements Command, Serial jobService.createAsyncJob(job, false); jobService.scheduleAsyncJob(job); } + } diff --git a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomAbortProcessInstanceCmd.java b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomAbortProcessInstanceCmd.java index 15a121697..91020386c 100644 --- a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomAbortProcessInstanceCmd.java +++ b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomAbortProcessInstanceCmd.java @@ -4,6 +4,8 @@ import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskDelegateAssigner; import cn.axzo.workflow.core.common.exception.WorkflowEngineException; import cn.axzo.workflow.core.engine.operation.DeleteProcessInstanceOperation; import cn.axzo.workflow.core.service.ExtAxHiTaskInstService; +import com.alibaba.fastjson.JSON; +import liquibase.pro.packaged.V; import org.flowable.common.engine.impl.interceptor.Command; import org.flowable.common.engine.impl.interceptor.CommandContext; import org.flowable.engine.HistoryService; @@ -44,7 +46,7 @@ import static cn.axzo.workflow.core.engine.cmd.helper.CustomTaskHelper.createVir * @author wangli * @since 2024/1/2 18:19 */ -public class CustomAbortProcessInstanceCmd implements Command, Serializable { +public class CustomAbortProcessInstanceCmd extends AbstractCommand implements Serializable { private final String processInstanceId; private final String tenantId; private final String reason; @@ -58,6 +60,15 @@ public class CustomAbortProcessInstanceCmd implements Command, Serializabl this.extAxHiTaskInstService = extAxHiTaskInstService; } + @Override + public String paramToJsonString() { + Map params = new HashMap<>(); + params.put("processInstanceId", processInstanceId); + params.put("tenantId", tenantId); + params.put("reason", reason); + return JSON.toJSONString(params); + } + @Override public Void execute(CommandContext commandContext) { ProcessEngineConfigurationImpl processEngineConfiguration = @@ -102,4 +113,5 @@ public class CustomAbortProcessInstanceCmd implements Command, Serializabl runtimeService.setVariable(task.getProcessInstanceId(), TASK_COMPLETE_OPERATION_TYPE + task.getId(), ABORTED); return null; } + } diff --git a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomApproveTaskAsyncCmd.java b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomApproveTaskAsyncCmd.java index 0f5d57be6..8c23dc946 100644 --- a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomApproveTaskAsyncCmd.java +++ b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomApproveTaskAsyncCmd.java @@ -3,6 +3,7 @@ package cn.axzo.workflow.core.engine.cmd; import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskAuditDTO; import cn.axzo.workflow.core.engine.job.AsyncApproveTaskJobHandler; import cn.hutool.json.JSONUtil; +import com.alibaba.fastjson.JSON; import org.flowable.common.engine.impl.interceptor.Command; import org.flowable.common.engine.impl.interceptor.CommandContext; import org.flowable.engine.TaskService; @@ -18,6 +19,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; import static cn.axzo.workflow.core.engine.cmd.helper.CustomTaskHelper.validTask; @@ -28,7 +31,7 @@ import static cn.axzo.workflow.core.engine.cmd.helper.CustomTaskHelper.validTask * @author wangli * @since 2024/1/4 15:50 */ -public class CustomApproveTaskAsyncCmd implements Command, Serializable { +public class CustomApproveTaskAsyncCmd extends AbstractCommand implements Serializable { private static final Logger log = LoggerFactory.getLogger(CustomApproveTaskAsyncCmd.class); private final BpmnTaskAuditDTO dto; @@ -37,6 +40,11 @@ public class CustomApproveTaskAsyncCmd implements Command, Serializable { this.dto = dto; } + @Override + public String paramToJsonString() { + return JSON.toJSONString(dto); + } + @Override public Void execute(CommandContext commandContext) { ProcessEngineConfigurationImpl processEngineConfiguration = diff --git a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomApproveTaskCmd.java b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomApproveTaskCmd.java index 9bdaabe5f..427be70d2 100644 --- a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomApproveTaskCmd.java +++ b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomApproveTaskCmd.java @@ -4,12 +4,14 @@ import cn.axzo.workflow.common.enums.BpmnFlowNodeType; import cn.axzo.workflow.common.model.request.bpmn.task.AttachmentDTO; import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskAuditDTO; import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskDelegateAssigner; +import com.alibaba.fastjson.JSON; import org.flowable.common.engine.impl.identity.Authentication; import org.flowable.common.engine.impl.interceptor.Command; import org.flowable.common.engine.impl.interceptor.CommandContext; import org.flowable.engine.RuntimeService; import org.flowable.engine.TaskService; import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl; +import org.flowable.engine.impl.context.Context; import org.flowable.engine.impl.util.CommandContextUtil; import org.flowable.task.api.Task; import org.flowable.task.api.history.HistoricTaskInstance; @@ -20,7 +22,9 @@ import org.slf4j.LoggerFactory; import org.springframework.util.StringUtils; import java.io.Serializable; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import static cn.axzo.workflow.common.constant.BpmnConstants.COMMENT_TYPE_ADVICE; @@ -38,7 +42,7 @@ import static cn.axzo.workflow.core.engine.cmd.helper.CustomTaskHelper.validTask * @author wangli * @since 2024/1/4 15:50 */ -public class CustomApproveTaskCmd implements Command, Serializable { +public class CustomApproveTaskCmd extends AbstractCommand implements Serializable { private static final Logger log = LoggerFactory.getLogger(CustomApproveTaskCmd.class); private final String taskId; @@ -62,6 +66,18 @@ public class CustomApproveTaskCmd implements Command, Serializable { */ private List nodeTypes; + @Override + public String paramToJsonString() { + Map params = new HashMap<>(); + params.put("taskId", taskId); + params.put("advice", advice); + params.put("operationDesc", operationDesc); + params.put("attachmentList", JSON.toJSONString(attachmentList)); + params.put("approver", JSON.toJSONString(approver)); + params.put("nextApprover", JSON.toJSONString(nextApprover)); + params.put("nodeTypes", JSON.toJSONString(nodeTypes)); + return JSON.toJSONString(params); + } public CustomApproveTaskCmd(BpmnTaskAuditDTO dto) { this(dto, null); diff --git a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomBizSpecifyAssigneeToTaskCmd.java b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomBizSpecifyAssigneeToTaskCmd.java index 76cba22d2..2867af38e 100644 --- a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomBizSpecifyAssigneeToTaskCmd.java +++ b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomBizSpecifyAssigneeToTaskCmd.java @@ -3,10 +3,13 @@ package cn.axzo.workflow.core.engine.cmd; import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskDelegateAssigner; import cn.axzo.workflow.core.common.exception.WorkflowEngineException; import cn.axzo.workflow.core.engine.cmd.helper.CustomTaskHelper; +import com.alibaba.fastjson.JSON; import org.flowable.common.engine.impl.interceptor.Command; import org.flowable.common.engine.impl.interceptor.CommandContext; +import org.flowable.engine.HistoryService; import org.flowable.engine.RuntimeService; import org.flowable.engine.TaskService; +import org.flowable.engine.history.HistoricProcessInstance; import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl; import org.flowable.engine.impl.util.CommandContextUtil; import org.flowable.task.api.Task; @@ -14,7 +17,9 @@ import org.flowable.task.service.impl.persistence.entity.TaskEntity; import org.springframework.util.CollectionUtils; import java.io.Serializable; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import static cn.axzo.workflow.common.constant.BpmnConstants.HIDDEN_ASSIGNEE_ID; @@ -22,8 +27,11 @@ import static cn.axzo.workflow.common.constant.BpmnConstants.INTERNAL_ACTIVITY_R import static cn.axzo.workflow.common.constant.BpmnConstants.NO_ASSIGNEE; import static cn.axzo.workflow.common.constant.BpmnConstants.TASK_COMPLETE_OPERATION_TYPE; import static cn.axzo.workflow.common.enums.BpmnProcessInstanceResultEnum.DELETED; +import static cn.axzo.workflow.common.enums.BpmnProcessInstanceResultEnum.PROCESSING; +import static cn.axzo.workflow.core.common.code.BpmnInstanceRespCode.PROCESS_INSTANCE_ID_NOT_EXISTS; import static cn.axzo.workflow.core.common.code.BpmnTaskRespCode.ACTIVITY_BIZ_SET_ASSIGNEE_ERROR; import static cn.axzo.workflow.core.common.code.BpmnTaskRespCode.ACTIVITY_CANT_SET_ASSIGNEE; +import static cn.axzo.workflow.core.common.code.BpmnTaskRespCode.PROCESS_CANT_SET_ASSIGNEE; /** * 自定的业务指定审批人命令实现 @@ -31,7 +39,7 @@ import static cn.axzo.workflow.core.common.code.BpmnTaskRespCode.ACTIVITY_CANT_S * @author wangli * @since 2023/12/22 13:51 */ -public class CustomBizSpecifyAssigneeToTaskCmd implements Command, Serializable { +public class CustomBizSpecifyAssigneeToTaskCmd extends AbstractCommand implements Serializable { private final String executionId; private final List addedAssigners; @@ -41,6 +49,14 @@ public class CustomBizSpecifyAssigneeToTaskCmd implements Command, Seri this.addedAssigners = addedAssigners; } + @Override + public String paramToJsonString() { + Map params = new HashMap<>(); + params.put("executionId", executionId); + params.put("addedAssigners", addedAssigners); + return JSON.toJSONString(params); + } + @Override public Boolean execute(CommandContext commandContext) { ProcessEngineConfigurationImpl processEngineConfiguration = @@ -53,6 +69,8 @@ public class CustomBizSpecifyAssigneeToTaskCmd implements Command, Seri validTask(task); + validProcessInstance(commandContext, task); + changeAssigneeSnapshot(commandContext, task); addAssignee(commandContext, taskService, task); @@ -60,6 +78,20 @@ public class CustomBizSpecifyAssigneeToTaskCmd implements Command, Seri return true; } + private void validProcessInstance(CommandContext commandContext, Task task) { + ProcessEngineConfigurationImpl processEngineConfiguration = + CommandContextUtil.getProcessEngineConfiguration(commandContext); + HistoryService historyService = processEngineConfiguration.getHistoryService(); + HistoricProcessInstance processInstance = historyService.createHistoricProcessInstanceQuery().processInstanceId(task.getProcessInstanceId()).singleResult(); + if (Objects.isNull(processInstance)) { + throw new WorkflowEngineException(PROCESS_INSTANCE_ID_NOT_EXISTS, task.getProcessInstanceId()); + } + if (!Objects.equals(PROCESSING.getStatus(), processInstance.getBusinessStatus())) { + throw new WorkflowEngineException(PROCESS_CANT_SET_ASSIGNEE); + } + } + + private void changeAssigneeSnapshot(CommandContext commandContext, Task task) { ProcessEngineConfigurationImpl processEngineConfiguration = CommandContextUtil.getProcessEngineConfiguration(commandContext); @@ -85,9 +117,7 @@ public class CustomBizSpecifyAssigneeToTaskCmd implements Command, Seri return; } - addedAssigners.forEach(i -> { - CustomTaskHelper.addMultiTask(commandContext, (TaskEntity) task, i); - }); + addedAssigners.forEach(i -> CustomTaskHelper.addMultiTask(commandContext, (TaskEntity) task, i)); taskService.setAssignee(task.getId(), HIDDEN_ASSIGNEE_ID); ((TaskEntity) task).setTransientVariable(TASK_COMPLETE_OPERATION_TYPE + task.getId(), DELETED.getStatus()); diff --git a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomCancelProcessInstanceAsyncCmd.java b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomCancelProcessInstanceAsyncCmd.java index b99ec73f3..38e8174f8 100644 --- a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomCancelProcessInstanceAsyncCmd.java +++ b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomCancelProcessInstanceAsyncCmd.java @@ -5,7 +5,7 @@ import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskDelegateAssigner; import cn.axzo.workflow.core.common.exception.WorkflowEngineException; import cn.axzo.workflow.core.engine.job.AsyncCancelProcessInstanceHandler; import cn.hutool.json.JSONUtil; -import org.flowable.common.engine.impl.interceptor.Command; +import com.alibaba.fastjson.JSON; import org.flowable.common.engine.impl.interceptor.CommandContext; import org.flowable.engine.HistoryService; import org.flowable.engine.history.HistoricProcessInstance; @@ -23,7 +23,7 @@ import static cn.axzo.workflow.core.common.code.BpmnInstanceRespCode.PROCESS_INS import static cn.axzo.workflow.core.common.code.BpmnInstanceRespCode.PROCESS_INSTANCE_CANT_CANCEL; import static cn.axzo.workflow.core.common.code.BpmnInstanceRespCode.PROCESS_INSTANCE_NOT_EXISTS; -public class CustomCancelProcessInstanceAsyncCmd implements Command, Serializable { +public class CustomCancelProcessInstanceAsyncCmd extends AbstractCommand implements Serializable { private final BpmnProcessInstanceCancelDTO dto; @@ -31,6 +31,11 @@ public class CustomCancelProcessInstanceAsyncCmd implements Command, Seria this.dto = dto; } + @Override + public String paramToJsonString() { + return JSON.toJSONString(dto); + } + @Override public Void execute(CommandContext commandContext) { String processInstanceId = dto.getProcessInstanceId(), tenantId = dto.getTenantId(); @@ -81,4 +86,5 @@ public class CustomCancelProcessInstanceAsyncCmd implements Command, Seria jobService.createAsyncJob(job, false); jobService.scheduleAsyncJob(job); } + } diff --git a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomCancelProcessInstanceCmd.java b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomCancelProcessInstanceCmd.java index 93c447265..500a3a83b 100644 --- a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomCancelProcessInstanceCmd.java +++ b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomCancelProcessInstanceCmd.java @@ -1,9 +1,11 @@ package cn.axzo.workflow.core.engine.cmd; +import cn.axzo.framework.context.validation.SpringValidator; import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskDelegateAssigner; import cn.axzo.workflow.core.common.exception.WorkflowEngineException; import cn.axzo.workflow.core.engine.operation.DeleteProcessInstanceOperation; import cn.axzo.workflow.core.service.ExtAxHiTaskInstService; +import com.alibaba.fastjson.JSON; import org.flowable.common.engine.impl.interceptor.Command; import org.flowable.common.engine.impl.interceptor.CommandContext; import org.flowable.engine.HistoryService; @@ -12,6 +14,7 @@ import org.flowable.engine.history.HistoricProcessInstance; import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl; import org.flowable.engine.impl.util.CommandContextUtil; import org.flowable.task.api.Task; +import org.springframework.util.StringUtils; import java.io.Serializable; import java.util.HashMap; @@ -41,7 +44,7 @@ import static cn.axzo.workflow.core.engine.cmd.helper.CustomTaskHelper.createVir * @author wangli * @since 2024/1/2 18:19 */ -public class CustomCancelProcessInstanceCmd implements Command, Serializable { +public class CustomCancelProcessInstanceCmd extends AbstractCommand implements Serializable { private final String processInstanceId; private final String tenantId; private final String reason; @@ -58,6 +61,16 @@ public class CustomCancelProcessInstanceCmd implements Command, Serializab this.extAxHiTaskInstService = extAxHiTaskInstService; } + @Override + public String paramToJsonString() { + Map params = new HashMap<>(); + params.put("processInstanceId", processInstanceId); + params.put("tenantId", tenantId); + params.put("reason", reason); + params.put("initiator", initiator); + return JSON.toJSONString(params); + } + @Override public Void execute(CommandContext commandContext) { ProcessEngineConfigurationImpl processEngineConfiguration = @@ -102,4 +115,5 @@ public class CustomCancelProcessInstanceCmd implements Command, Serializab addComment(commandContext, task, COMMENT_TYPE_OPERATION_DESC, "已撤回"); return null; } + } diff --git a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomCarbonCopyUserSelectorCmd.java b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomCarbonCopyUserSelectorCmd.java index f43a8bb8b..885e332e9 100644 --- a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomCarbonCopyUserSelectorCmd.java +++ b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomCarbonCopyUserSelectorCmd.java @@ -10,6 +10,7 @@ import cn.hutool.json.JSONUtil; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.google.common.collect.Lists; +import lombok.extern.slf4j.Slf4j; import org.flowable.bpmn.model.CustomProperty; import org.flowable.bpmn.model.ServiceTask; import org.flowable.common.engine.impl.interceptor.Command; @@ -27,12 +28,14 @@ import org.springframework.util.CollectionUtils; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.function.Function; import java.util.stream.Collectors; +import static cn.axzo.workflow.common.constant.BpmnConstants.INTERNAL_ACTIVITY_RELATION_ASSIGNEE_LIST_INFO_SNAPSHOT; import static cn.axzo.workflow.common.constant.BpmnConstants.INTERNAL_INITIATOR; import static cn.axzo.workflow.common.constant.BpmnConstants.INTERNAL_TASK_RELATION_ASSIGNEE_INFO; import static cn.axzo.workflow.common.constant.BpmnConstants.NO_ASSIGNEE; @@ -45,7 +48,8 @@ import static cn.axzo.workflow.core.engine.cmd.helper.CustomTaskHelper.removeDup * @author wangli * @since 18/03/2024 11:33 */ -public class CustomCarbonCopyUserSelectorCmd implements Command>, Serializable { +@Slf4j +public class CustomCarbonCopyUserSelectorCmd extends AbstractCommand> implements Serializable { private static final long serialVersionUID = 1L; private final String processInstanceId; @@ -83,6 +87,15 @@ public class CustomCarbonCopyUserSelectorCmd implements Command params = new HashMap<>(); + params.put("processInstanceId", processInstanceId); + params.put("carbons", carbons); + params.put("serviceVersion", serviceVersion); + return JSON.toJSONString(params); + } + @Override public List execute(CommandContext commandContext) { if (CollectionUtils.isEmpty(carbons)) { @@ -97,6 +110,17 @@ public class CustomCarbonCopyUserSelectorCmd implements Command hisVarInst = processEngineConfiguration.getHistoryService().createHistoricVariableInstanceQuery() + .processInstanceId(processInstanceId) + .variableName(INTERNAL_ACTIVITY_RELATION_ASSIGNEE_LIST_INFO_SNAPSHOT + serviceTask.getId()).list(); + if (!CollectionUtils.isEmpty(hisVarInst)) { + return (List) hisVarInst.get(0).getValue(); + } + log.warn("executions is empty,get historic task assigner list empty,processInstanceId: {},serviceTaskId: {}", processInstanceId, serviceTask.getId()); + return Collections.emptyList(); + } List assigners = new ArrayList<>(); carbons.forEach(carbon -> { CarbonCopyObjectType carbonCopyObjectType = carbon.getCarbonCopyObjectType(); @@ -108,6 +132,9 @@ public class CustomCarbonCopyUserSelectorCmd implements Command, Serializable { +public class CustomCommentTaskCmd extends AbstractCommand implements Serializable { private final String processInstanceId; private final BpmnTaskDelegateAssigner operator; private final String comment; @@ -63,6 +66,17 @@ public class CustomCommentTaskCmd implements Command, Serializable { this.extAxHiTaskInstService = extAxHiTaskInstService; } + @Override + public String paramToJsonString() { + Map params = new HashMap<>(); + params.put("processInstanceId", processInstanceId); + params.put("operator", operator); + params.put("comment", comment); + params.put("commentExt", commentExt); + params.put("attachmentList", attachmentList); + return JSON.toJSONString(params); + } + @Override public Void execute(CommandContext commandContext) { ProcessEngineConfigurationImpl processEngineConfiguration = diff --git a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomCompleteDummyTaskCmd.java b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomCompleteDummyTaskCmd.java index 6a9f920af..a00361cbf 100644 --- a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomCompleteDummyTaskCmd.java +++ b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomCompleteDummyTaskCmd.java @@ -5,6 +5,7 @@ import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskDelegateAssigner; import cn.axzo.workflow.core.common.exception.WorkflowEngineException; import cn.axzo.workflow.core.engine.cmd.helper.CustomTaskHelper; import cn.axzo.workflow.core.service.ExtAxHiTaskInstService; +import com.alibaba.fastjson.JSON; import org.flowable.common.engine.impl.identity.Authentication; import org.flowable.common.engine.impl.interceptor.Command; import org.flowable.common.engine.impl.interceptor.CommandContext; @@ -16,7 +17,9 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import java.io.Serializable; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import static cn.axzo.workflow.common.constant.BpmnConstants.COMMENT_TYPE_OPERATION_DESC; @@ -30,7 +33,7 @@ import static cn.axzo.workflow.core.common.code.BpmnTaskRespCode.DUMMY_TASK_NOT_ * @author wangli * @since 2023/12/27 11:24 */ -public class CustomCompleteDummyTaskCmd implements Command, Serializable { +public class CustomCompleteDummyTaskCmd extends AbstractCommand implements Serializable { private final String processInstanceId; private final String taskId; private final String flowNodeName; @@ -46,6 +49,16 @@ public class CustomCompleteDummyTaskCmd implements Command, Serializable { this.extAxHiTaskInstService = extAxHiTaskInstService; } + @Override + public String paramToJsonString() { + Map params = new HashMap<>(); + params.put("processInstanceId", processInstanceId); + params.put("taskId", taskId); + params.put("flowNodeName", flowNodeName); + params.put("operationDesc", operationDesc); + return JSON.toJSONString(params); + } + @Override public Void execute(CommandContext commandContext) { ProcessEngineConfigurationImpl processEngineConfiguration = @@ -93,4 +106,5 @@ public class CustomCompleteDummyTaskCmd implements Command, Serializable { taskService.setOwner(i.getId(), null); }); } + } diff --git a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomCountersignUserTaskAsyncCmd.java b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomCountersignUserTaskAsyncCmd.java index 81956e9e7..f347c124d 100644 --- a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomCountersignUserTaskAsyncCmd.java +++ b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomCountersignUserTaskAsyncCmd.java @@ -3,6 +3,7 @@ package cn.axzo.workflow.core.engine.cmd; import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskCountersignDTO; import cn.axzo.workflow.core.engine.job.AsyncCountersignUserTaskJobHandler; import cn.hutool.json.JSONUtil; +import com.alibaba.fastjson.JSON; import org.flowable.common.engine.impl.interceptor.Command; import org.flowable.common.engine.impl.interceptor.CommandContext; import org.flowable.engine.TaskService; @@ -16,17 +17,24 @@ import org.flowable.task.api.history.HistoricTaskInstanceQuery; import org.flowable.task.service.impl.persistence.entity.TaskEntity; import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; import static cn.axzo.workflow.core.engine.cmd.helper.CustomTaskHelper.validTask; import static cn.axzo.workflow.core.engine.cmd.helper.CustomTaskHelper.validTaskAssignerDuplicated; -public class CustomCountersignUserTaskAsyncCmd implements Command, Serializable { +public class CustomCountersignUserTaskAsyncCmd extends AbstractCommand implements Serializable { private final BpmnTaskCountersignDTO dto; public CustomCountersignUserTaskAsyncCmd(BpmnTaskCountersignDTO dto) { this.dto = dto; } + @Override + public String paramToJsonString() { + return JSON.toJSONString(dto); + } + @Override public Void execute(CommandContext commandContext) { ProcessEngineConfigurationImpl processEngineConfiguration = @@ -66,4 +74,5 @@ public class CustomCountersignUserTaskAsyncCmd implements Command, Seriali jobService.createAsyncJob(job, false); jobService.scheduleAsyncJob(job); } + } diff --git a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomCountersignUserTaskCmd.java b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomCountersignUserTaskCmd.java index 329b022cf..877cad88d 100644 --- a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomCountersignUserTaskCmd.java +++ b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomCountersignUserTaskCmd.java @@ -6,6 +6,7 @@ import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskDelegateAssigner; import cn.axzo.workflow.core.engine.cmd.helper.CustomTaskHelper; import cn.axzo.workflow.core.service.ExtAxHiTaskInstService; import cn.hutool.json.JSONUtil; +import com.alibaba.fastjson.JSON; import lombok.extern.slf4j.Slf4j; import org.flowable.common.engine.impl.interceptor.Command; import org.flowable.common.engine.impl.interceptor.CommandContext; @@ -19,7 +20,9 @@ import org.flowable.task.api.history.HistoricTaskInstanceQuery; import org.flowable.task.service.impl.persistence.entity.TaskEntity; import java.io.Serializable; +import java.util.HashMap; import java.util.List; +import java.util.Map; import static cn.axzo.workflow.common.constant.BpmnConstants.COMMENT_TYPE_OPERATION_DESC; import static cn.axzo.workflow.common.constant.BpmnConstants.INTERNAL_ACTIVITY_RELATION_ASSIGNEE_LIST_INFO_SNAPSHOT; @@ -39,7 +42,7 @@ import static cn.axzo.workflow.core.engine.cmd.helper.CustomTaskHelper.validTask * @since 2023/12/25 14:05 */ @Slf4j -public class CustomCountersignUserTaskCmd implements Command, Serializable { +public class CustomCountersignUserTaskCmd extends AbstractCommand implements Serializable { private final BpmnCountersignTypeEnum countersignType; private final String originTaskId; @@ -62,6 +65,18 @@ public class CustomCountersignUserTaskCmd implements Command, Serializable this.extAxHiTaskInstService = extAxHiTaskInstService; } + @Override + public String paramToJsonString() { + Map params = new HashMap<>(); + params.put("countersignType", countersignType.getType()); + params.put("originTaskId", originTaskId); + params.put("originTaskAssignee", originTaskAssignee); + params.put("advice", advice); + params.put("attachmentList", JSON.toJSONString(attachmentList)); + params.put("targetTaskAssigneeList", JSON.toJSONString(targetTaskAssigneeList)); + return JSON.toJSONString(params); + } + @Override public Void execute(CommandContext commandContext) { ProcessEngineConfigurationImpl processEngineConfiguration = @@ -139,4 +154,5 @@ public class CustomCountersignUserTaskCmd implements Command, Serializable addComment(commandContext, virtualTask, COMMENT_TYPE_OPERATION_DESC, message.toString()); batchAddAttachment(commandContext, task.getProcessInstanceId(), task.getId(), attachmentList, originTaskAssignee); } + } diff --git a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomCreateDummyTaskCmd.java b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomCreateDummyTaskCmd.java index ba6cda820..f1973b35b 100644 --- a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomCreateDummyTaskCmd.java +++ b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomCreateDummyTaskCmd.java @@ -5,6 +5,7 @@ import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskDelegateAssigner; import cn.axzo.workflow.core.common.exception.WorkflowEngineException; import cn.axzo.workflow.core.engine.cmd.helper.CustomTaskHelper; import cn.axzo.workflow.core.service.ExtAxHiTaskInstService; +import com.alibaba.fastjson.JSON; import org.flowable.common.engine.impl.cfg.IdGenerator; import org.flowable.common.engine.impl.identity.Authentication; import org.flowable.common.engine.impl.interceptor.Command; @@ -21,7 +22,9 @@ import org.springframework.util.CollectionUtils; import java.io.Serializable; import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import static cn.axzo.workflow.common.constant.BpmnConstants.COMMENT_TYPE_OPERATION_DESC; @@ -41,7 +44,7 @@ import static org.flowable.task.api.Task.DEFAULT_PRIORITY; * @author wangli * @since 2023/12/27 10:21 */ -public class CustomCreateDummyTaskCmd implements Command, Serializable { +public class CustomCreateDummyTaskCmd extends AbstractCommand implements Serializable { private final String processInstanceId; private final String flowNodeName; private final String operationDesc; @@ -57,6 +60,16 @@ public class CustomCreateDummyTaskCmd implements Command, Serializable { this.extAxHiTaskInstService = extAxHiTaskInstService; } + @Override + public String paramToJsonString() { + Map params = new HashMap<>(); + params.put("processInstanceId", processInstanceId); + params.put("flowNodeName", flowNodeName); + params.put("operationDesc", operationDesc); + params.put("operator", operator); + return JSON.toJSONString(params); + } + /** * 在同一个实例下,不允许创建多个执行中的虚拟任务节点 * @@ -139,4 +152,5 @@ public class CustomCreateDummyTaskCmd implements Command, Serializable { }); } } + } diff --git a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomForecastUserTaskAssigneeCmd.java b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomForecastUserTaskAssigneeCmd.java index bb6070b5c..a17379858 100644 --- a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomForecastUserTaskAssigneeCmd.java +++ b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomForecastUserTaskAssigneeCmd.java @@ -19,7 +19,9 @@ import org.springframework.util.CollectionUtils; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import static cn.axzo.workflow.core.common.utils.BpmnMetaParserHelper.getApproverSpecify; @@ -30,7 +32,7 @@ import static cn.axzo.workflow.core.common.utils.BpmnMetaParserHelper.getApprove * @since 2023/12/11 17:56 */ @Slf4j -public class CustomForecastUserTaskAssigneeCmd implements Command>, Serializable { +public class CustomForecastUserTaskAssigneeCmd extends AbstractCommand> implements Serializable { private final String processInstanceId; private final UserTask userTask; private final EngineExecutionStartListener engineExecutionStartListener; @@ -42,6 +44,13 @@ public class CustomForecastUserTaskAssigneeCmd implements Command params = new HashMap<>(); + params.put("processInstanceId", processInstanceId); + return JSON.toJSONString(params); + } + @Override public List execute(CommandContext commandContext) { ProcessEngineConfigurationImpl processEngineConfiguration = @@ -81,4 +90,5 @@ public class CustomForecastUserTaskAssigneeCmd implements Command>, Serializable { +public class CustomNoticeDestinationUserSelectorCmd extends AbstractCommand> implements Serializable { private static final long serialVersionUID = 1L; private final EngineExecutionStartListener engineExecutionStartListener; private final BpmnHistoricTaskInstanceConverter historicTaskInstanceConverter; @@ -63,6 +64,17 @@ public class CustomNoticeDestinationUserSelectorCmd implements Command params = new HashMap<>(); + params.put("serviceVersion", serviceVersion); + params.put("workspaceType", workspaceType); + params.put("noticeProperty", noticeProperty); + params.put("processInstanceId", processInstanceId); + params.put("initiator", initiator); + return JSON.toJSONString(params); + } + @Override public List execute(CommandContext commandContext) { if (!noticeProperty.getSendMessage()) { @@ -170,5 +182,4 @@ public class CustomNoticeDestinationUserSelectorCmd implements Command, Serializable { +public class CustomRejectionTaskAsyncCmd extends AbstractCommand implements Serializable { private final BpmnTaskAuditDTO dto; public CustomRejectionTaskAsyncCmd(BpmnTaskAuditDTO dto) { this.dto = dto; } + @Override + public String paramToJsonString() { + return JSON.toJSONString(dto); + } + @Override public Void execute(CommandContext commandContext) { ProcessEngineConfigurationImpl processEngineConfiguration = @@ -69,4 +75,5 @@ public class CustomRejectionTaskAsyncCmd implements Command, Serializable jobService.createAsyncJob(job, false); jobService.scheduleAsyncJob(job); } + } diff --git a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomRejectionTaskCmd.java b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomRejectionTaskCmd.java index b1f559d6c..610d2df65 100644 --- a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomRejectionTaskCmd.java +++ b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomRejectionTaskCmd.java @@ -6,11 +6,12 @@ import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskAuditDTO; import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskDelegateAssigner; import cn.axzo.workflow.core.engine.operation.DeleteProcessInstanceOperation; import cn.axzo.workflow.core.service.ExtAxHiTaskInstService; -import org.flowable.common.engine.impl.interceptor.Command; +import com.alibaba.fastjson.JSON; import org.flowable.common.engine.impl.interceptor.CommandContext; import org.flowable.engine.RuntimeService; import org.flowable.engine.TaskService; import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl; +import org.flowable.engine.impl.context.Context; import org.flowable.engine.impl.util.CommandContextUtil; import org.flowable.task.api.Task; import org.flowable.task.api.history.HistoricTaskInstance; @@ -21,6 +22,7 @@ import java.io.Serializable; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import static cn.axzo.workflow.common.constant.BpmnConstants.COMMENT_TYPE_OPERATION_DESC; import static cn.axzo.workflow.common.constant.BpmnConstants.INTERNAL_DELETE_PROCESS_FLAG; @@ -42,7 +44,7 @@ import static cn.axzo.workflow.core.engine.cmd.helper.CustomTaskHelper.validTask * @author wangli * @since 2024/1/4 13:36 */ -public class CustomRejectionTaskCmd implements Command, Serializable { +public class CustomRejectionTaskCmd extends AbstractCommand implements Serializable { private final String taskId; private final String advice; @@ -60,6 +62,17 @@ public class CustomRejectionTaskCmd implements Command, Serializable { this.nodeTypes = dto.getNodeTypes(); } + @Override + public String paramToJsonString() { + Map params = new HashMap<>(); + params.put("taskId", taskId); + params.put("advice", advice); + params.put("attachmentList", attachmentList); + params.put("approver", approver); + params.put("nodeTypes", nodeTypes); + return JSON.toJSONString(params); + } + @Override public Void execute(CommandContext commandContext) { ProcessEngineConfigurationImpl processEngineConfiguration = @@ -99,4 +112,5 @@ public class CustomRejectionTaskCmd implements Command, Serializable { .planOperation(new DeleteProcessInstanceOperation(commandContext, task.getProcessInstanceId(), extAxHiTaskInstService)); } + } diff --git a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomTransferUserTaskAsyncCmd.java b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomTransferUserTaskAsyncCmd.java index df620da96..79176e80a 100644 --- a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomTransferUserTaskAsyncCmd.java +++ b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomTransferUserTaskAsyncCmd.java @@ -3,8 +3,8 @@ package cn.axzo.workflow.core.engine.cmd; import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskTransferDTO; import cn.axzo.workflow.core.engine.job.AsyncTransferUserTaskJobHandler; import cn.hutool.json.JSONUtil; +import com.alibaba.fastjson.JSON; import com.google.common.collect.Lists; -import org.flowable.common.engine.impl.interceptor.Command; import org.flowable.common.engine.impl.interceptor.CommandContext; import org.flowable.engine.TaskService; import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl; @@ -17,6 +17,8 @@ import org.flowable.task.api.history.HistoricTaskInstanceQuery; import org.flowable.task.service.impl.persistence.entity.TaskEntity; import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; import static cn.axzo.workflow.core.engine.cmd.helper.CustomTaskHelper.validTask; import static cn.axzo.workflow.core.engine.cmd.helper.CustomTaskHelper.validTaskAssignerDuplicated; @@ -24,7 +26,7 @@ import static cn.axzo.workflow.core.engine.cmd.helper.CustomTaskHelper.validTask /** * 异步转交任务的命令执行器 */ -public class CustomTransferUserTaskAsyncCmd implements Command, Serializable { +public class CustomTransferUserTaskAsyncCmd extends AbstractCommand implements Serializable { private final BpmnTaskTransferDTO dto; @@ -32,6 +34,11 @@ public class CustomTransferUserTaskAsyncCmd implements Command, Serializab this.dto = dto; } + @Override + public String paramToJsonString() { + return JSON.toJSONString(dto); + } + @Override public Void execute(CommandContext commandContext) { ProcessEngineConfigurationImpl processEngineConfiguration = CommandContextUtil.getProcessEngineConfiguration(commandContext); @@ -65,4 +72,5 @@ public class CustomTransferUserTaskAsyncCmd implements Command, Serializab jobService.createAsyncJob(job, false); jobService.scheduleAsyncJob(job); } + } diff --git a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomTransferUserTaskCmd.java b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomTransferUserTaskCmd.java index 18c558946..5cf121307 100644 --- a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomTransferUserTaskCmd.java +++ b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/cmd/CustomTransferUserTaskCmd.java @@ -3,6 +3,7 @@ package cn.axzo.workflow.core.engine.cmd; import cn.axzo.workflow.common.model.request.bpmn.task.AttachmentDTO; import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskDelegateAssigner; import cn.axzo.workflow.core.common.exception.WorkflowEngineException; +import com.alibaba.fastjson.JSON; import com.google.common.collect.Lists; import org.flowable.common.engine.impl.identity.Authentication; import org.flowable.common.engine.impl.interceptor.Command; @@ -18,7 +19,9 @@ import org.flowable.task.service.impl.persistence.entity.TaskEntity; import org.springframework.util.StringUtils; import java.io.Serializable; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -43,7 +46,7 @@ import static cn.axzo.workflow.core.engine.cmd.helper.CustomTaskHelper.validTask * @author wangli * @since 2023/12/22 10:22 */ -public class CustomTransferUserTaskCmd implements Command, Serializable { +public class CustomTransferUserTaskCmd extends AbstractCommand implements Serializable { private final String originTaskId; private final BpmnTaskDelegateAssigner originTaskAssignee; @@ -60,6 +63,16 @@ public class CustomTransferUserTaskCmd implements Command, Serializable { this.targetTaskAssignee = targetTaskAssignee; } + @Override + public String paramToJsonString() { + Map params = new HashMap<>(); + params.put("originTaskId", originTaskId); + params.put("originTaskAssignee", originTaskAssignee); + params.put("advice", advice); + params.put("attachmentList", attachmentList); + params.put("targetTaskAssignee", targetTaskAssignee); + return JSON.toJSONString(params); + } @Override public Void execute(CommandContext commandContext) { @@ -125,4 +138,5 @@ public class CustomTransferUserTaskCmd implements Command, Serializable { INTERNAL_ACTIVITY_RELATION_ASSIGNEE_LIST_INFO_SNAPSHOT + task.getTaskDefinitionKey(), originAssingeeList); } + } diff --git a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/event/MessagePushEvent.java b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/event/MessagePushEvent.java index 1af8582a1..6f57f3383 100644 --- a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/event/MessagePushEvent.java +++ b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/event/MessagePushEvent.java @@ -25,6 +25,8 @@ public interface MessagePushEvent extends FlowableEvent { String getProcessDefinitionId(); + String getProcessDefinitionKey(); + String getCurrentTaskDefinitionKey(); String getTenantId(); diff --git a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/event/MessagePushEventBuilder.java b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/event/MessagePushEventBuilder.java index f3c11e84a..a6ffc9439 100644 --- a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/event/MessagePushEventBuilder.java +++ b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/event/MessagePushEventBuilder.java @@ -26,21 +26,21 @@ import static cn.axzo.workflow.core.engine.event.MessagePushEventType.SMS; public class MessagePushEventBuilder { public static MessagePushEventImpl createEvent(MessagePushEventType type, List assigners, - BpmnNoticeConf noticeConf, String processInstanceId, + BpmnNoticeConf noticeConf, String processInstanceId, String processDefinitionKey, String tenantId, String taskId) { switch (type) { case NOTICE: - return createNoticeEvent(assigners, noticeConf, processInstanceId, tenantId, taskId); + return createNoticeEvent(assigners, noticeConf, processInstanceId, processDefinitionKey, tenantId, taskId); case PENDING_PUSH: throw new WorkflowEngineException(MESSAGE_PUSH_EVENT_BUILD_ERROR); case PENDING_COMPLETE: - return createPendingCompleteEvent(assigners, noticeConf, processInstanceId, tenantId, taskId); + return createPendingCompleteEvent(assigners, noticeConf, processInstanceId, processDefinitionKey, tenantId, taskId); case CARBON_COPY: - return createCarbonCopyEvent(assigners, noticeConf, processInstanceId, tenantId); + return createCarbonCopyEvent(assigners, noticeConf, processInstanceId, processDefinitionKey, tenantId); case CARBON_COPY_COMPLETE: - return createCarbonCopyCompleteEvent(assigners, noticeConf, processInstanceId, tenantId); + return createCarbonCopyCompleteEvent(assigners, noticeConf, processInstanceId, processDefinitionKey, tenantId); case SMS: - return createSmsEvent(assigners, noticeConf, processInstanceId, tenantId, taskId); + return createSmsEvent(assigners, noticeConf, processInstanceId, processDefinitionKey, tenantId, taskId); default: throw new WorkflowEngineException(MES_PUSH_OBJECT_BUILD_ERROR); } @@ -48,9 +48,9 @@ public class MessagePushEventBuilder { public static MessagePushEventImpl createNoticeEvent(List assigners, BpmnNoticeConf noticeConf - , String processInstanceId, String tenantId, String taskId) { + , String processInstanceId, String processDefinitionKey, String tenantId, String taskId) { MessagePushEventImpl newEvent = new MessagePushEventImpl(NOTICE, assigners, noticeConf, processInstanceId, - tenantId, taskId); + processDefinitionKey, tenantId, taskId); return newEvent; } @@ -58,47 +58,54 @@ public class MessagePushEventBuilder { BpmnNoticeConf noticeConf, BpmnApproveConf processApproveConf, String processInstanceId, - String processDefinitionId, String currentTaskDefinitionKey, + String processDefinitionId, + String processDefinitionKey, + String currentTaskDefinitionKey, String tenantId, String taskId) { - MessagePushEventImpl newEvent = new MessagePushEventImpl(PENDING_PUSH, assigners, noticeConf, processApproveConf, processInstanceId, - processDefinitionId, currentTaskDefinitionKey, tenantId, taskId); + MessagePushEventImpl newEvent = new MessagePushEventImpl(PENDING_PUSH, assigners, noticeConf, processApproveConf, + processInstanceId, processDefinitionId, processDefinitionKey, currentTaskDefinitionKey, tenantId, taskId); return newEvent; } public static MessagePushEventImpl createPendingCompleteEvent(List assigners, BpmnNoticeConf noticeConf, String processInstanceId, + String processDefinitionKey, String tenantId, String taskId) { MessagePushEventImpl newEvent = new MessagePushEventImpl(PENDING_COMPLETE, assigners, noticeConf, - processInstanceId, - tenantId, taskId); + processInstanceId, processDefinitionKey, tenantId, taskId); return newEvent; } - public static MessagePushEvent createPendingRollbackEvent(String processInstanceId, String tenantId, String taskId, BpmnNoticeConf noticeConf) { - return new MessagePushEventImpl(PENDING_ROLLBACK, null, noticeConf, processInstanceId, tenantId, taskId); + public static MessagePushEvent createPendingRollbackEvent(String processInstanceId, String processDefinitionKey, + String tenantId, String taskId, BpmnNoticeConf noticeConf) { + return new MessagePushEventImpl(PENDING_ROLLBACK, null, noticeConf, processInstanceId, processDefinitionKey, tenantId, taskId); } public static MessagePushEventImpl createCarbonCopyEvent(List assigners, BpmnNoticeConf noticeConf, String processInstanceId, + String processDefinitionKey, String tenantId) { MessagePushEventImpl newEvent = new MessagePushEventImpl(CARBON_COPY, assigners, noticeConf, - processInstanceId, tenantId, null); + processInstanceId, processDefinitionKey, tenantId, null); return newEvent; } public static MessagePushEventImpl createCarbonCopyCompleteEvent(List assigners, BpmnNoticeConf noticeConf, String processInstanceId, + String processDefinitionKey, String tenantId) { MessagePushEventImpl newEvent = new MessagePushEventImpl(CARBON_COPY_COMPLETE, assigners, noticeConf, - processInstanceId, tenantId, null); + processInstanceId, processDefinitionKey, tenantId, null); return newEvent; } public static MessagePushEventImpl createSmsEvent(List assigners, BpmnNoticeConf noticeConf, - String processInstanceId, String tenantId, String taskId) { + String processInstanceId, + String processDefinitionKey, + String tenantId, String taskId) { MessagePushEventImpl newEvent = new MessagePushEventImpl(SMS, assigners, noticeConf, processInstanceId, - tenantId, taskId); + processDefinitionKey, tenantId, taskId); return newEvent; } } diff --git a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/event/MessagePushEventImpl.java b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/event/MessagePushEventImpl.java index 98bf4dc09..d6eabf517 100644 --- a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/event/MessagePushEventImpl.java +++ b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/event/MessagePushEventImpl.java @@ -25,6 +25,7 @@ public class MessagePushEventImpl implements MessagePushEvent { private BpmnApproveConf processApproveConfig; private String processInstanceId; private String processDefinitionId; + private String processDefinitionKey; private String currentTaskDefinitionKey; private String tenantId; private String taskId; @@ -48,11 +49,13 @@ public class MessagePushEventImpl implements MessagePushEvent { * @param tenantId * @param taskId */ - public MessagePushEventImpl(FlowableEventType type, List assigners, BpmnNoticeConf noticeConfig, String processInstanceId, String tenantId, String taskId) { + public MessagePushEventImpl(FlowableEventType type, List assigners, BpmnNoticeConf noticeConfig, + String processInstanceId, String processDefinitionKey, String tenantId, String taskId) { this.type = type; this.assigners = assigners; this.noticeConfig = noticeConfig; this.processInstanceId = processInstanceId; + this.processDefinitionKey = processDefinitionKey; this.tenantId = tenantId; this.taskId = taskId; } @@ -75,7 +78,7 @@ public class MessagePushEventImpl implements MessagePushEvent { public MessagePushEventImpl(FlowableEventType type, List assigners, BpmnNoticeConf noticeConfig, BpmnApproveConf processApproveConfig, - String processInstanceId, String processDefinitionId, + String processInstanceId, String processDefinitionId, String processDefinitionKey, String currentTaskDefinitionKey, String tenantId, String taskId) { this.type = type; this.assigners = assigners; @@ -141,6 +144,15 @@ public class MessagePushEventImpl implements MessagePushEvent { this.processDefinitionId = processDefinitionId; } + @Override + public String getProcessDefinitionKey() { + return processDefinitionKey; + } + + public void setProcessDefinitionKey(String processDefinitionKey) { + this.processDefinitionKey = processDefinitionKey; + } + @Override public String getCurrentTaskDefinitionKey() { return currentTaskDefinitionKey; diff --git a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/interceptor/CustomRetryInterceptor.java b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/interceptor/CustomRetryInterceptor.java new file mode 100644 index 000000000..093f1d108 --- /dev/null +++ b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/interceptor/CustomRetryInterceptor.java @@ -0,0 +1,89 @@ +package cn.axzo.workflow.core.engine.interceptor; + +import cn.axzo.workflow.core.common.exception.WorkflowEngineException; +import cn.axzo.workflow.core.common.utils.TraceUtil; +import cn.axzo.workflow.core.engine.cmd.AbstractCommand; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.exceptions.PersistenceException; +import org.flowable.common.engine.impl.interceptor.AbstractCommandInterceptor; +import org.flowable.common.engine.impl.interceptor.Command; +import org.flowable.common.engine.impl.interceptor.CommandConfig; +import org.flowable.common.engine.impl.interceptor.CommandExecutor; + +import static cn.axzo.workflow.core.common.code.FlowableEngineRespCode.ENGINE_ASYNC_COMMAND_EXECUTION_RETRY_GIVE_UP; + +/** + * 基于 RetryInterceptor 的命令重试拦截器, 本意想用来处理多人同时操作同一个任务, 但会存在超过重试次数后,会丢弃原有的命令. + * + * @author wangli + * @since 2024/7/1 13:51 + */ +@Slf4j +public class CustomRetryInterceptor extends AbstractCommandInterceptor { + + protected int numOfRetries = 3; + protected int waitTimeInMs = 60; + protected int waitIncreaseFactor = 5; + + @Override + public T execute(CommandConfig config, Command command, CommandExecutor commandExecutor) { + long waitTime = waitTimeInMs; + int failedAttempts = 0; + do { + if (failedAttempts > 0) { + log.warn("Waiting for {}ms before retrying the command.", waitTime); + waitBeforeRetry(waitTime); + waitTime *= waitIncreaseFactor; + } + + try { + // try to execute the command + if (AbstractCommand.class.isAssignableFrom(command.getClass())) { + // 如果在以后,重试三次也不能解决的话, 可以利用这里的拿到的参数,重新自动构造CMD,并执行. + log.info("traceId:{} Executing command params: {}", TraceUtil.traceId(), + ((AbstractCommand) command).paramToJsonString()); + } + return next.execute(config, command, commandExecutor); + + } catch (PersistenceException e) { + log.warn("Caught persistence exception: {}", e.getMessage(), e); + } + + failedAttempts++; + } while (failedAttempts <= numOfRetries); + + throw new WorkflowEngineException(ENGINE_ASYNC_COMMAND_EXECUTION_RETRY_GIVE_UP, String.valueOf(numOfRetries)); + } + + protected void waitBeforeRetry(long waitTime) { + try { + Thread.sleep(waitTime); + } catch (InterruptedException e) { + log.warn("I am interrupted while waiting for a retry."); + } + } + + public void setNumOfRetries(int numOfRetries) { + this.numOfRetries = numOfRetries; + } + + public void setWaitIncreaseFactor(int waitIncreaseFactor) { + this.waitIncreaseFactor = waitIncreaseFactor; + } + + public void setWaitTimeInMs(int waitTimeInMs) { + this.waitTimeInMs = waitTimeInMs; + } + + public int getNumOfRetries() { + return numOfRetries; + } + + public int getWaitIncreaseFactor() { + return waitIncreaseFactor; + } + + public int getWaitTimeInMs() { + return waitTimeInMs; + } +} diff --git a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/job/exception/handle/CustomWorkflowEngineExceptionHandler.java b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/job/exception/handle/CustomWorkflowEngineExceptionHandler.java index 352b104ec..80ef948ed 100644 --- a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/job/exception/handle/CustomWorkflowEngineExceptionHandler.java +++ b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/job/exception/handle/CustomWorkflowEngineExceptionHandler.java @@ -19,15 +19,16 @@ public class CustomWorkflowEngineExceptionHandler implements AsyncRunnableExecut @Override public boolean handleException(JobServiceConfiguration jobServiceConfiguration, JobInfo job, Throwable e) { Throwable rootCause = getRootCause(e); - if (rootCause.getClass().isAssignableFrom(WorkflowEngineException.class)) { - WorkflowEngineException workflowEngineException = (WorkflowEngineException) rootCause; - if (Objects.equals(workflowEngineException.getCode(), "99806020")) { - log.info("AsyncApproveTaskJobHandler execute exception code: {} info: {}", - workflowEngineException.getCode(), rootCause.getMessage(), rootCause); - return true; - } - } - return false; +// if (rootCause.getClass().isAssignableFrom(WorkflowEngineException.class)) { +// WorkflowEngineException workflowEngineException = (WorkflowEngineException) rootCause; +// if (Objects.equals(workflowEngineException.getCode(), "99806020")) { +// log.info("AsyncApproveTaskJobHandler execute exception code: {} info: {}", +// workflowEngineException.getCode(), rootCause.getMessage(), rootCause); +// return true; +// } +// } + log.warn("Async Runnable Execution Exception: {}", rootCause.getMessage(), e); + return WorkflowEngineException.class.isAssignableFrom(rootCause.getClass()); } private Throwable getRootCause(Throwable throwable) { diff --git a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/listener/EngineCarbonCopyEventListener.java b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/listener/EngineCarbonCopyEventListener.java index c9f26d948..47d9a3cae 100644 --- a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/listener/EngineCarbonCopyEventListener.java +++ b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/listener/EngineCarbonCopyEventListener.java @@ -38,6 +38,7 @@ import java.util.concurrent.atomic.AtomicReference; import static cn.axzo.workflow.common.constant.BpmnConstants.INTERNAL_ACTIVITY_RELATION_ASSIGNEE_LIST_INFO_SNAPSHOT; import static cn.axzo.workflow.core.common.utils.BpmnMetaParserHelper.getCarbonCopyConfigs; +import static cn.axzo.workflow.core.listener.AbstractBpmnEventListener.parseProcessDefinitionKey; /** * 抄送功能的具体实现 @@ -110,6 +111,7 @@ public class EngineCarbonCopyEventListener implements JavaDelegate { MessagePushEventImpl event = MessagePushEventBuilder.createEvent(MessagePushEventType.CARBON_COPY, carbonUsers, bpmnNoticeConf, execution.getProcessInstanceId(), + parseProcessDefinitionKey(execution.getProcessDefinitionId()), execution.getTenantId(), getCarbonTaskId(execution)); eventDispatcher.dispatchEvent(event, processEngineConfiguration.getEngineCfgKey()); } diff --git a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/listener/EngineProcessInstanceEventListener.java b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/listener/EngineProcessInstanceEventListener.java index 8a0073440..25aa3d6ba 100644 --- a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/listener/EngineProcessInstanceEventListener.java +++ b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/engine/listener/EngineProcessInstanceEventListener.java @@ -8,7 +8,6 @@ import cn.axzo.workflow.core.common.utils.BpmnMetaParserHelper; import cn.axzo.workflow.core.engine.cmd.CustomNoticeDestinationUserSelectorCmd; import cn.axzo.workflow.core.engine.event.MessagePushEventBuilder; import cn.axzo.workflow.core.engine.event.MessagePushEventImpl; -import cn.axzo.workflow.core.engine.event.MessagePushEventType; import cn.axzo.workflow.core.listener.BpmnProcessEventListener; import cn.axzo.workflow.core.service.converter.BpmnHistoricTaskInstanceConverter; import cn.hutool.json.JSONUtil; @@ -48,6 +47,7 @@ import static cn.axzo.workflow.common.enums.BpmnProcessInstanceResultEnum.APPROV import static cn.axzo.workflow.common.enums.BpmnProcessInstanceResultEnum.CANCELLED; import static cn.axzo.workflow.common.enums.BpmnProcessInstanceResultEnum.DELETED; import static cn.axzo.workflow.common.enums.BpmnProcessInstanceResultEnum.REJECTED; +import static cn.axzo.workflow.core.engine.event.MessagePushEventType.NOTICE; /** * 引擎全局的流程实例事件监听 @@ -157,8 +157,9 @@ public class EngineProcessInstanceEventListener extends AbstractFlowableEngineEv List assigners = processEngineConfiguration.getCommandExecutor() .execute(new CustomNoticeDestinationUserSelectorCmd(engineExecutionStartListener, historicTaskInstanceConverter, serviceVersion, workspaceType, config.getNotice(), event.getProcessInstanceId(), assigner)); - MessagePushEventImpl messagePushEvent = MessagePushEventBuilder.createEvent(MessagePushEventType.NOTICE, - assigners, config, processInstance.getProcessInstanceId(), processInstance.getTenantId(), null); + MessagePushEventImpl messagePushEvent = MessagePushEventBuilder.createEvent(NOTICE, + assigners, config, processInstance.getProcessInstanceId(), processInstance.getProcessDefinitionKey(), + processInstance.getTenantId(), null); log.info("发送通知消息: {}", JSONUtil.toJsonStr(messagePushEvent)); eventDispatcher.dispatchEvent(messagePushEvent, processEngineConfiguration.getEngineCfgKey()); } diff --git a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/listener/AbstractBpmnEventListener.java b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/listener/AbstractBpmnEventListener.java index 63399b2ec..8a9c3aae3 100644 --- a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/listener/AbstractBpmnEventListener.java +++ b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/listener/AbstractBpmnEventListener.java @@ -3,6 +3,7 @@ package cn.axzo.workflow.core.listener; import cn.axzo.workflow.core.common.context.OperationContext; import cn.hutool.json.JSONUtil; import org.slf4j.MDC; +import org.springframework.util.StringUtils; import static cn.azxo.framework.common.constatns.Constants.CTX_LOG_ID_MDC; @@ -33,4 +34,11 @@ public abstract class AbstractBpmnEventListener impl protected String buildCacheKey(String apiUrl, Object request) { return apiUrl + JSONUtil.toJsonStr(request); } + + public static String parseProcessDefinitionKey(String processDefinitionId) { + if (!StringUtils.hasText(processDefinitionId)) { + return ""; + } + return processDefinitionId.split(":")[0]; + } } diff --git a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/mq/CustomRocketMQEventProducer.java b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/mq/CustomRocketMQEventProducer.java index 72c7e36cf..b0235be43 100644 --- a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/mq/CustomRocketMQEventProducer.java +++ b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/mq/CustomRocketMQEventProducer.java @@ -17,7 +17,7 @@ import java.util.List; import java.util.Optional; import java.util.function.BiConsumer; -import static cn.axzo.workflow.common.constant.BpmnConstants.MQ_OWNERSHIP_APP; +import static cn.axzo.workflow.common.constant.BpmnConstants.MQ_OWNERSHIP_APPLICATION; /** * 默认的 RocketMQ 事件生产者的装饰器 @@ -68,7 +68,9 @@ public class CustomRocketMQEventProducer extends RocketMQEventProducer { newHeaders.put(TraceUtils.TRACE_ID, TraceUtils.getOrCreateTraceId()); newHeaders.put(TraceUtils.CTX_LOG_ID, TraceUtils.getOrCreateTraceId()); newHeaders.put(TraceUtils.TRACE_ID_IN_MDC, TraceUtils.getOrCreateTraceId()); - newHeaders.put(MQ_OWNERSHIP_APP, applicationName); + // FIXME +// newHeaders.put(MQ_OWNERSHIP_APPLICATION, applicationName); + newHeaders.put(MQ_OWNERSHIP_APPLICATION, "senna"); final Context copiedContext = context.toBuilder().headers(newHeaders).build(); Runnable runnable = () -> { @@ -94,7 +96,7 @@ public class CustomRocketMQEventProducer extends RocketMQEventProducer { } else { // 并发会导致事件时序出现问题. 所以串行执行 log.info("runnable not transaction event={}", copiedEvent.toJsonString()); - getAfterCommitExecutor().executeAndRollback(() -> runnable.run(), () -> rollbackRunnable.run()); + getAfterCommitExecutor().executeAndRollback(runnable, rollbackRunnable); } List runnables = getAfterCommitExecutor().getRunnables(); diff --git a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/repository/entity/ExtAxProperty.java b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/repository/entity/ExtAxProperty.java index 79272e41d..9fd14518d 100644 --- a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/repository/entity/ExtAxProperty.java +++ b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/repository/entity/ExtAxProperty.java @@ -34,5 +34,9 @@ public class ExtAxProperty extends BaseEntity { @TableField("value") private String value; - + /** + * 接入方是否使用了 manageable + */ + @TableField("manageable") + private Boolean manageable; } diff --git a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/service/impl/BpmnProcessInstanceServiceImpl.java b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/service/impl/BpmnProcessInstanceServiceImpl.java index 5f8752afd..92e63cffa 100644 --- a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/service/impl/BpmnProcessInstanceServiceImpl.java +++ b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/service/impl/BpmnProcessInstanceServiceImpl.java @@ -86,9 +86,12 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; import javax.annotation.Nullable; import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; import javax.validation.Valid; import java.util.ArrayList; import java.util.Collections; @@ -102,6 +105,7 @@ import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; +import static cn.axzo.workflow.client.config.WorkflowRequestInterceptor.HEADER_SERVER_NAME; import static cn.axzo.workflow.common.constant.BpmnConstants.AND_SIGN_EXPRESSION; import static cn.axzo.workflow.common.constant.BpmnConstants.BIZ_ORG_RELATION; import static cn.axzo.workflow.common.constant.BpmnConstants.BPM_MODEL_CATEGORY; @@ -112,6 +116,7 @@ import static cn.axzo.workflow.common.constant.BpmnConstants.INTERNAL_PROCESS_AG import static cn.axzo.workflow.common.constant.BpmnConstants.INTERNAL_PROCESS_WORKSPACE_TYPE; import static cn.axzo.workflow.common.constant.BpmnConstants.OLD_INTERNAL_INITIATOR; import static cn.axzo.workflow.common.constant.BpmnConstants.PENDING_TEMPLATE_VARIABLE; +import static cn.axzo.workflow.common.constant.BpmnConstants.PROCESS_OWNERSHIP_APPLICATION; import static cn.axzo.workflow.common.constant.BpmnConstants.WORKFLOW_ENGINE_VERSION; import static cn.axzo.workflow.common.enums.ApprovalMethodEnum.autoPassed; import static cn.axzo.workflow.common.enums.ApprovalMethodEnum.autoRejection; @@ -320,6 +325,9 @@ public class BpmnProcessInstanceServiceImpl implements BpmnProcessInstanceServic dto.getVariables().put(INTERNAL_PROCESS_AGENT, StringUtils.isNotBlank(definition.getTenantId())); dto.getVariables().put(CREATE_INSTANCE_PARAMS, JSONUtil.toJsonStr(dto)); + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + dto.getVariables().put(PROCESS_OWNERSHIP_APPLICATION, request.getHeader(HEADER_SERVER_NAME)); +// dto.getVariables().put(MQ_OWNERSHIP_APPLICATION, ); // if (Objects.nonNull(dto.getNextApprover())) { // BpmnTaskDelegateAssigner nextApprover = dto.getNextApprover(); // nextApprover.setTenantId(Objects.nonNull(nextApprover.getTenantId()) ? nextApprover.getTenantId diff --git a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/service/impl/BpmnProcessModelServiceImpl.java b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/service/impl/BpmnProcessModelServiceImpl.java index de8ef5c0e..1916f9e82 100644 --- a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/service/impl/BpmnProcessModelServiceImpl.java +++ b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/service/impl/BpmnProcessModelServiceImpl.java @@ -458,7 +458,7 @@ public class BpmnProcessModelServiceImpl implements BpmnProcessModelService { if (CollectionUtils.isEmpty(list)) { return Collections.emptyList(); } - return list.stream().map(Model::getTenantId).collect(Collectors.toList()); + return list.stream().map(Model::getTenantId).distinct().collect(Collectors.toList()); } private void updateProcessDefinitionSuspended(String deploymentId) { diff --git a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/service/impl/BpmnProcessTaskServiceImpl.java b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/service/impl/BpmnProcessTaskServiceImpl.java index 5eb112cf3..86f7d06f8 100644 --- a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/service/impl/BpmnProcessTaskServiceImpl.java +++ b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/service/impl/BpmnProcessTaskServiceImpl.java @@ -628,6 +628,7 @@ public class BpmnProcessTaskServiceImpl implements BpmnProcessTaskService { MessagePushEventImpl event = MessagePushEventBuilder.createEvent(MessagePushEventType.valueOf(type), Lists.newArrayList(assigner), noticeConfig.orElse(null), processInstance.getProcessInstanceId(), + processInstance.getProcessDefinitionKey(), processInstance.getTenantId(), task.getId()); event.setProcessInstanceId(processInstance.getProcessInstanceId()); event.setTenantId(processInstance.getTenantId()); diff --git a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/service/impl/ExtAxPropertyServiceImpl.java b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/service/impl/ExtAxPropertyServiceImpl.java index f496a7208..7ebf4702d 100644 --- a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/service/impl/ExtAxPropertyServiceImpl.java +++ b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/service/impl/ExtAxPropertyServiceImpl.java @@ -5,6 +5,9 @@ import cn.axzo.workflow.core.repository.mapper.ExtAxPropertyMapper; import cn.axzo.workflow.core.service.ExtAxPropertyService; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; @@ -23,18 +26,21 @@ public class ExtAxPropertyServiceImpl implements ExtAxPropertyService { @Resource private ExtAxPropertyMapper mapper; + @CacheEvict(value = "property", key = "#property.name") @Override public ExtAxProperty add(ExtAxProperty property) { mapper.insert(property); return property; } + @CachePut(value = "property", key = "#property.name") @Override public ExtAxProperty update(ExtAxProperty property) { mapper.updateById(property); return property; } + @Cacheable(value = "property", key = "#name") @Override public Optional getByName(String name) { if (!StringUtils.hasText(name)) { diff --git a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/service/support/ExpressionConditionCmd.java b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/service/support/ExpressionConditionCmd.java index 672db4e0a..54c7080b3 100644 --- a/workflow-engine-core/src/main/java/cn/axzo/workflow/core/service/support/ExpressionConditionCmd.java +++ b/workflow-engine-core/src/main/java/cn/axzo/workflow/core/service/support/ExpressionConditionCmd.java @@ -1,7 +1,9 @@ package cn.axzo.workflow.core.service.support; import cn.axzo.workflow.core.common.exception.WorkflowEngineException; +import cn.axzo.workflow.core.engine.cmd.AbstractCommand; import cn.azxo.framework.common.utils.StringUtils; +import com.alibaba.fastjson.JSON; import org.flowable.common.engine.api.delegate.Expression; import org.flowable.common.engine.impl.interceptor.Command; import org.flowable.common.engine.impl.interceptor.CommandContext; @@ -10,6 +12,8 @@ import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl; import org.flowable.engine.impl.persistence.entity.ExecutionEntity; import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; import static cn.axzo.workflow.core.common.code.FlowableEngineRespCode.ENGINE_EXECUTION_LOST_ID_ERROR; @@ -20,7 +24,7 @@ import static cn.axzo.workflow.core.common.code.FlowableEngineRespCode.ENGINE_EX * @author wangli * @since 2023/10/9 19:30 */ -public class ExpressionConditionCmd implements Command, Serializable { +public class ExpressionConditionCmd extends AbstractCommand implements Serializable { protected final RuntimeService runtimeService; protected final ProcessEngineConfigurationImpl processEngineConfiguration; protected final String processInstanceId; @@ -35,6 +39,14 @@ public class ExpressionConditionCmd implements Command, Serializable { this.exp = exp; } + @Override + public String paramToJsonString() { + Map params = new HashMap<>(); + params.put("processInstanceId", processInstanceId); + params.put("exp", exp); + return JSON.toJSONString(params); + } + @Override public Boolean execute(CommandContext commandContext) { Expression expression = processEngineConfiguration.getExpressionManager().createExpression(this.exp); diff --git a/workflow-engine-core/src/main/java/org/flowable/engine/impl/cmd/CreateAttachmentCmd.java b/workflow-engine-core/src/main/java/org/flowable/engine/impl/cmd/CreateAttachmentCmd.java index aa30f0396..9f0f63f53 100644 --- a/workflow-engine-core/src/main/java/org/flowable/engine/impl/cmd/CreateAttachmentCmd.java +++ b/workflow-engine-core/src/main/java/org/flowable/engine/impl/cmd/CreateAttachmentCmd.java @@ -1,5 +1,8 @@ package org.flowable.engine.impl.cmd; +import cn.axzo.workflow.core.engine.cmd.AbstractCommand; +import com.alibaba.fastjson.JSON; +import liquibase.pro.packaged.M; import org.flowable.common.engine.api.FlowableException; import org.flowable.common.engine.api.FlowableObjectNotFoundException; import org.flowable.common.engine.api.delegate.event.FlowableEngineEventType; @@ -22,6 +25,9 @@ import org.flowable.task.api.Task; import org.flowable.task.service.impl.persistence.entity.TaskEntity; import java.io.InputStream; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; /** * 覆盖 Flowable 自带的 CreateAttachmentCmd, 由于 Flowable 5 的实现导致异常,需要覆盖 @@ -29,7 +35,7 @@ import java.io.InputStream; * @author Tom Baeyens * @author Joram Barrez */ -public class CreateAttachmentCmd implements Command { +public class CreateAttachmentCmd extends AbstractCommand implements Serializable { protected String attachmentType; protected String taskId; @@ -50,6 +56,19 @@ public class CreateAttachmentCmd implements Command { this.url = url; } + @Override + public String paramToJsonString() { + Map params = new HashMap<>(); + params.put("attachmentType", attachmentType); + params.put("taskId", taskId); + params.put("processInstanceId", processInstanceId); + params.put("attachmentName", attachmentName); + params.put("attachmentDescription", attachmentDescription); +// params.put("content", content); + params.put("url", url); + return JSON.toJSONString(params); + } + @Override public Attachment execute(CommandContext commandContext) { diff --git a/workflow-engine-core/src/main/resources/sql/upgrade_to_1.4.0.sql b/workflow-engine-core/src/main/resources/sql/upgrade_to_1.4.0.sql new file mode 100644 index 000000000..4905c4e7d --- /dev/null +++ b/workflow-engine-core/src/main/resources/sql/upgrade_to_1.4.0.sql @@ -0,0 +1,3 @@ +alter table ext_ax_property + add manageable boolean default false null comment '接入方是否使用了 manageable'; + diff --git a/workflow-engine-server/pom.xml b/workflow-engine-server/pom.xml index 279ccf123..afb7ae83a 100644 --- a/workflow-engine-server/pom.xml +++ b/workflow-engine-server/pom.xml @@ -10,6 +10,7 @@ 4.0.0 workflow-engine-server + Workflow Engine Server 8 @@ -20,6 +21,10 @@ 3.25.0 + cn.axzo.framework axzo-web-spring-boot-starter @@ -95,10 +100,10 @@ cn.axzo.maokai maokai-api - - cn.axzo.tyr - tyr-api - + + + + cn.axzo.karma karma-api diff --git a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/WorkflowEnginApplication.java b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/WorkflowEnginApplication.java index 131f005b9..697702527 100644 --- a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/WorkflowEnginApplication.java +++ b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/WorkflowEnginApplication.java @@ -1,9 +1,11 @@ package cn.axzo.workflow.server; +import liquibase.pro.packaged.E; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration; +import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.ComponentScan; import org.springframework.transaction.annotation.EnableTransactionManagement; @@ -12,6 +14,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; @ComponentScan({"cn.axzo.workflow", "cn.axzo.maokai"}) @SpringBootApplication(exclude = RabbitAutoConfiguration.class) @EnableTransactionManagement +@EnableCaching public class WorkflowEnginApplication { public static void main(String[] args) { diff --git a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/advice/FlowableExceptionResultHandlerAdvice.java b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/advice/FlowableExceptionResultHandlerAdvice.java index 00b8820cb..374e90fa0 100644 --- a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/advice/FlowableExceptionResultHandlerAdvice.java +++ b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/advice/FlowableExceptionResultHandlerAdvice.java @@ -2,6 +2,7 @@ package cn.axzo.workflow.server.advice; import cn.axzo.framework.autoconfigure.web.exception.RespErrorCodeMappingProperties; import cn.axzo.framework.autoconfigure.web.exception.handler.AbstractExceptionApiResultHandler; +import cn.axzo.framework.domain.web.code.BaseCode; import cn.axzo.framework.domain.web.code.IRespCode; import cn.axzo.framework.domain.web.code.RespCode; import cn.axzo.workflow.core.common.exception.WorkflowEngineException; @@ -30,8 +31,8 @@ public class FlowableExceptionResultHandlerAdvice extends AbstractExceptionApiRe protected IRespCode decode(FlowableException ex, IRespCode fallbackCode) { log.warn("发现 FlowableException Error: ", ex); if (Objects.nonNull(ex.getCause()) && ex.getCause() instanceof WorkflowEngineException) { - return new RespCode(UNAVAILABLE_FOR_LEGAL_REASONS.getCode(), ex.getCause().getMessage()); + return new RespCode(BaseCode.SUCCESS.getCode(), ex.getCause().getMessage()); } - return new RespCode(UNAVAILABLE_FOR_LEGAL_REASONS.getCode(), ex.getMessage()); + return new RespCode(BaseCode.SUCCESS.getCode(), ex.getMessage()); } } diff --git a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/advice/FlowableOptimisticLockingExceptionHandlerAdvice.java b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/advice/FlowableOptimisticLockingExceptionHandlerAdvice.java new file mode 100644 index 000000000..841a2d04e --- /dev/null +++ b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/advice/FlowableOptimisticLockingExceptionHandlerAdvice.java @@ -0,0 +1,31 @@ +package cn.axzo.workflow.server.advice; + +import cn.axzo.framework.autoconfigure.web.exception.RespErrorCodeMappingProperties; +import cn.axzo.framework.autoconfigure.web.exception.handler.AbstractExceptionApiResultHandler; +import cn.axzo.framework.domain.web.code.BaseCode; +import cn.axzo.framework.domain.web.code.IRespCode; +import cn.axzo.framework.domain.web.code.RespCode; +import org.flowable.common.engine.api.FlowableOptimisticLockingException; +import org.flowable.common.engine.impl.interceptor.CommandContext; +import org.springframework.stereotype.Component; + +import static cn.axzo.framework.domain.web.code.BaseCode.UNAVAILABLE_FOR_LEGAL_REASONS; + +/** + * 降级 FlowableOptimisticLockingException 异常,该异常在 Flowable 框架中是可以忽略的 + * + * @author wangli + * @see CommandContext#logException() + * @since 2024/6/19 17:32 + */ +@Component +public class FlowableOptimisticLockingExceptionHandlerAdvice extends AbstractExceptionApiResultHandler { + public FlowableOptimisticLockingExceptionHandlerAdvice(RespErrorCodeMappingProperties properties) { + super(properties); + } + + @Override + protected IRespCode decode(FlowableOptimisticLockingException ex, IRespCode fallbackCode) { + return new RespCode(BaseCode.SUCCESS.getCode(), ex.getMessage()); + } +} diff --git a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/advice/SQLIntegrityConstraintViolationExceptionHandlerAdvice.java b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/advice/SQLIntegrityConstraintViolationExceptionHandlerAdvice.java new file mode 100644 index 000000000..0a618d5fd --- /dev/null +++ b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/advice/SQLIntegrityConstraintViolationExceptionHandlerAdvice.java @@ -0,0 +1,36 @@ +package cn.axzo.workflow.server.advice; + +import cn.axzo.framework.autoconfigure.web.exception.RespErrorCodeMappingProperties; +import cn.axzo.framework.autoconfigure.web.exception.handler.AbstractExceptionApiResultHandler; +import cn.axzo.framework.domain.web.code.BaseCode; +import cn.axzo.framework.domain.web.code.IRespCode; +import cn.axzo.framework.domain.web.code.RespCode; +import cn.axzo.workflow.client.feign.bpmn.ProcessActivityApi; +import cn.axzo.workflow.client.feign.bpmn.ProcessInstanceApi; +import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceAbortDTO; +import cn.axzo.workflow.common.model.request.bpmn.task.BpmnActivitySetAssigneeDTO; +import org.springframework.stereotype.Component; + +import java.sql.SQLIntegrityConstraintViolationException; + + +/** + * 处理多个接口同时操作一个流程实例时,可能会抛出数据库的异常,这种异常是可以被忽略的 + *

+ * 例如: {@link ProcessActivityApi#setAssignee(BpmnActivitySetAssigneeDTO)} 于 {@link ProcessInstanceApi#abortProcessInstance(BpmnProcessInstanceAbortDTO)} + * 两个接口并发访问时,由于 abort 先执行完,并提交事务后,setAssignee 方法内虽然有判断实例状态,但最后事务提交时突然发现实例状态被中止了就会抛出异常。 + * + * @author wangli + * @since 2024/6/20 09:42 + */ +@Component +public class SQLIntegrityConstraintViolationExceptionHandlerAdvice extends AbstractExceptionApiResultHandler { + public SQLIntegrityConstraintViolationExceptionHandlerAdvice(RespErrorCodeMappingProperties properties) { + super(properties); + } + + @Override + protected IRespCode decode(SQLIntegrityConstraintViolationException ex, IRespCode fallbackCode) { + return new RespCode(BaseCode.SUCCESS.getCode(), ex.getMessage()); + } +} diff --git a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/common/annotation/ReporterType.java b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/common/annotation/ReporterType.java index 0ad2a3894..e42b9df9d 100644 --- a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/common/annotation/ReporterType.java +++ b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/common/annotation/ReporterType.java @@ -1,8 +1,6 @@ package cn.axzo.workflow.server.common.annotation; -import cn.axzo.workflow.core.common.exception.WorkflowEngineException; import cn.axzo.workflow.server.common.util.DingTalkUtils; -import cn.azxo.framework.common.utils.LogUtil; import lombok.extern.slf4j.Slf4j; import static cn.axzo.workflow.server.common.aspectj.RepeatSubmitAspect.argsArrayToString; @@ -71,10 +69,11 @@ public enum ReporterType { public abstract void executeAction(String profile, String title, Boolean sendDingTalk, Object[] args, String shortString, Throwable e, Boolean downgrade); private static void logWarn(Throwable throwable, String shortString) { - if (throwable instanceof WorkflowEngineException) { - log.warn("引擎内部正常业务异常类型,日志降级: " + throwable.getMessage(), throwable); - } else { - LogUtil.error(LogUtil.ErrorType.ERROR_BUSINESS, shortString, throwable.getMessage(), throwable); - } + // 由于框架底层 AbstractExceptionApiResultHandler 默认会打印,所以此处都只是降级打印 +// if (throwable instanceof WorkflowEngineException) { + log.warn("引擎内部正常业务异常类型,日志降级: " + throwable.getMessage(), throwable); +// } else { +// LogUtil.error(LogUtil.ErrorType.ERROR_BUSINESS, shortString, throwable.getMessage(), throwable); +// } } } diff --git a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/common/aspectj/ErrorReportAspect.java b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/common/aspectj/ErrorReportAspect.java index be686966f..d21404b0c 100644 --- a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/common/aspectj/ErrorReportAspect.java +++ b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/common/aspectj/ErrorReportAspect.java @@ -4,6 +4,7 @@ import cn.axzo.workflow.core.common.event.ApiLogEvent; import cn.axzo.workflow.server.common.annotation.EnvConfig; import cn.axzo.workflow.server.common.annotation.ErrorReporter; import cn.axzo.workflow.server.common.config.property.WorkflowProperties; +import com.google.common.collect.Lists; import io.swagger.v3.oas.annotations.Operation; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; @@ -26,6 +27,7 @@ import org.springframework.web.context.request.ServletRequestAttributes; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import java.util.Arrays; +import java.util.List; import java.util.Objects; import static cn.axzo.workflow.client.config.WorkflowRequestInterceptor.HEADER_API_VERSION; @@ -50,6 +52,7 @@ public class ErrorReportAspect implements Ordered { private ApplicationEventPublisher applicationEventPublisher; @Resource private WorkflowProperties workflowProperties; + private static List methodNames = Lists.newArrayList("HealthCheckController.checkDeath()"); @Override public int getOrder() { @@ -73,6 +76,10 @@ public class ErrorReportAspect implements Ordered { watch.stop(); log.info("StopWatch '{}': running time = {} 's", watch.getLastTaskName(), watch.getTotalTimeSeconds()); + if (!methodNames.contains(watch.getLastTaskName())) { + log.info("StopWatch '{}': running time = {} 's", watch.getLastTaskName(), watch.getTotalTimeSeconds()); + } + if (!signature.toShortString().contains("ExtAxApiLogServiceImpl")) { String type = getType(joinPoint); ApiLogEvent event = new ApiLogEvent(MDC.get(CTX_LOG_ID_MDC), diff --git a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/common/filter/HttpTraceLogFilter.java b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/common/filter/HttpTraceLogFilter.java index 1af797b06..d8452d23b 100644 --- a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/common/filter/HttpTraceLogFilter.java +++ b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/common/filter/HttpTraceLogFilter.java @@ -1,6 +1,5 @@ package cn.axzo.workflow.server.common.filter; -import cn.azxo.framework.common.constatns.Constants; import cn.hutool.core.util.IdUtil; import com.google.common.base.Strings; import lombok.extern.slf4j.Slf4j; @@ -9,6 +8,7 @@ import org.apache.skywalking.apm.toolkit.trace.TraceContext; import org.slf4j.MDC; import org.springframework.core.Ordered; import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; @@ -18,6 +18,7 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; import static cn.axzo.workflow.common.constant.LogFieldConstants.X_REQUEST_ID; +import static cn.azxo.framework.common.constatns.Constants.CTX_LOG_ID_MDC; /** * Http接口日志记录 @@ -44,18 +45,18 @@ public class HttpTraceLogFilter extends OncePerRequestFilter implements Ordered } else { MDC.put(X_REQUEST_ID, requestId); } - String ctxLogId = request.getHeader(Constants.CTX_LOG_ID_MDC); + String ctxLogId = request.getHeader(CTX_LOG_ID_MDC); if (Strings.isNullOrEmpty(ctxLogId)) { - MDC.put(Constants.CTX_LOG_ID_MDC, getTraceId()); + MDC.put(CTX_LOG_ID_MDC, getTraceId()); } else { - MDC.put(Constants.CTX_LOG_ID_MDC, ctxLogId); + MDC.put(CTX_LOG_ID_MDC, ctxLogId); } try { filterChain.doFilter(request, response); } finally { - response.setHeader(Constants.CTX_LOG_ID_MDC, MDC.get(Constants.CTX_LOG_ID_MDC)); - response.setHeader(X_REQUEST_ID, MDC.get(Constants.CTX_LOG_ID_MDC)); + response.setHeader(CTX_LOG_ID_MDC, MDC.get(CTX_LOG_ID_MDC)); + response.setHeader(X_REQUEST_ID, MDC.get(CTX_LOG_ID_MDC)); MDC.clear(); } } @@ -64,4 +65,10 @@ public class HttpTraceLogFilter extends OncePerRequestFilter implements Ordered String contextTraceId = TraceContext.traceId(); return Strings.isNullOrEmpty(contextTraceId) ? IdUtil.fastSimpleUUID() : contextTraceId; } + + public static String traceId() { + String xRequestId = MDC.get(X_REQUEST_ID); + return StringUtils.hasText(xRequestId) ? xRequestId : MDC.get(CTX_LOG_ID_MDC); + + } } diff --git a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/common/hook/NacosShutdownHook.java b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/common/hook/NacosShutdownHook.java deleted file mode 100644 index b2eaa31a8..000000000 --- a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/common/hook/NacosShutdownHook.java +++ /dev/null @@ -1,26 +0,0 @@ -package cn.axzo.workflow.server.common.hook; - -import org.springframework.context.SmartLifecycle; - -/** - * TODO - * - * @author wangli - * @since 2024/6/6 14:35 - */ -public class NacosShutdownHook implements SmartLifecycle { - @Override - public void start() { - - } - - @Override - public void stop() { - - } - - @Override - public boolean isRunning() { - return false; - } -} diff --git a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/common/interceptor/RequestHeaderContextInterceptor.java b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/common/interceptor/RequestHeaderContextInterceptor.java index d8c65b447..cfc316e8e 100644 --- a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/common/interceptor/RequestHeaderContextInterceptor.java +++ b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/common/interceptor/RequestHeaderContextInterceptor.java @@ -3,6 +3,7 @@ package cn.axzo.workflow.server.common.interceptor; import cn.axzo.workflow.core.common.exception.WorkflowEngineException; import cn.axzo.workflow.core.repository.entity.ExtAxProperty; import cn.axzo.workflow.core.service.ExtAxPropertyService; +import cn.axzo.workflow.server.common.util.RedisUtils; import lombok.extern.slf4j.Slf4j; import org.apache.maven.artifact.versioning.DefaultArtifactVersion; import org.springframework.beans.factory.annotation.Autowired; @@ -12,14 +13,17 @@ import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.time.Duration; import java.util.Enumeration; import java.util.Objects; +import java.util.Optional; import static cn.axzo.workflow.client.config.WorkflowRequestInterceptor.HEADER_API_VERSION; import static cn.axzo.workflow.client.config.WorkflowRequestInterceptor.HEADER_HTTP_CLIENT; import static cn.axzo.workflow.client.config.WorkflowRequestInterceptor.HEADER_HTTP_CLIENT_VALUE; import static cn.axzo.workflow.client.config.WorkflowRequestInterceptor.HEADER_SERVER_NAME; import static cn.axzo.workflow.common.constant.BpmnConstants.FLOW_SERVER_VERSION_130; +import static cn.axzo.workflow.common.constant.StarterConstants.ENABLE_MANAGEABLE; import static cn.axzo.workflow.core.common.code.OtherRespCode.CLIENT_VERSION_SUPPORT; import static cn.axzo.workflow.core.common.code.OtherRespCode.MICRO_SERVER_NEED_REBUILD; @@ -36,6 +40,8 @@ public class RequestHeaderContextInterceptor implements HandlerInterceptor { private String serviceVersion; @Autowired private ExtAxPropertyService extAxPropertyService; + private static final ThreadLocal KEY_CACHE = new ThreadLocal<>(); + private static final String REPEAT_KEY = "global:api_application:"; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { @@ -74,28 +80,52 @@ public class RequestHeaderContextInterceptor implements HandlerInterceptor { private void recordClientInfo(HttpServletRequest request, String headerClientVersion, DefaultArtifactVersion clientVersion) { - log.info("HEADER_SERVER_NAME : {}", request.getHeader(HEADER_SERVER_NAME)); - if (!StringUtils.hasText(request.getHeader(HEADER_SERVER_NAME))) { + String applicationName = request.getHeader(HEADER_SERVER_NAME); + log.info("HEADER_SERVER_NAME : {}", applicationName); + if (!StringUtils.hasText(applicationName)) { return; } - ExtAxProperty property = extAxPropertyService.getByName(request.getHeader(HEADER_SERVER_NAME)).map(entity -> { - entity.setCreated(!Objects.equals(entity.getValue(), serviceVersion)); - entity.setValue(headerClientVersion); - return entity; - }).orElseGet(() -> { - ExtAxProperty extAxProperty = new ExtAxProperty(); - extAxProperty.setCreated(true); - extAxProperty.setName(request.getHeader(HEADER_SERVER_NAME)); - extAxProperty.setValue(clientVersion.toString()); - return extAxProperty; - }); + String manageableStatus = request.getHeader(ENABLE_MANAGEABLE); - if (property.getCreated()) { - if (Objects.isNull(property.getId())) { - extAxPropertyService.add(property); - } else { - extAxPropertyService.update(property); - } + Optional extAxProperty = extAxPropertyService.getByName(applicationName); + String cacheRepeatKey = REPEAT_KEY + applicationName; + log.info("repeatApi key: {}", cacheRepeatKey); + + //success为true表示key不存在,执行成功,false表示key存在,执行失败 + Boolean success = RedisUtils.trySetObject(cacheRepeatKey, "", Duration.ofSeconds(5)); + if (success) { + KEY_CACHE.set(cacheRepeatKey); + insert(extAxProperty, applicationName, clientVersion, manageableStatus); + } + } + + private void update(Optional extAxProperty, String requestApplicationName, DefaultArtifactVersion clientVersion, String manageableStatus) { + if (!extAxProperty.isPresent()) { + return; + } + ExtAxProperty property = extAxProperty.get(); + if (Objects.equals(property.getValue(), clientVersion.toString()) + && Objects.equals(property.getManageable().toString(), manageableStatus)) { + return; + } + property.setName(requestApplicationName); + property.setValue(clientVersion.toString()); + property.setManageable(Boolean.valueOf(manageableStatus)); + extAxPropertyService.update(property); + } + + private void insert(Optional extAxProperty, String requestApplicationName, DefaultArtifactVersion clientVersion, String manageableStatus) { + if (extAxProperty.isPresent()) { + update(extAxProperty, requestApplicationName, clientVersion, manageableStatus); + } else { + extAxPropertyService.add(extAxProperty.orElseGet(() -> { + ExtAxProperty property = new ExtAxProperty(); + property.setCreated(true); + property.setName(requestApplicationName); + property.setValue(clientVersion.toString()); + property.setManageable(Boolean.valueOf(manageableStatus)); + return property; + })); } } diff --git a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/common/util/RedisUtils.java b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/common/util/RedisUtils.java index 7ecef815f..fcb207f7a 100644 --- a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/common/util/RedisUtils.java +++ b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/common/util/RedisUtils.java @@ -7,6 +7,7 @@ import org.redisson.api.RAtomicLong; import org.redisson.api.RBatch; import org.redisson.api.RBucket; import org.redisson.api.RBucketAsync; +import org.redisson.api.RFuture; import org.redisson.api.RKeys; import org.redisson.api.RList; import org.redisson.api.RMap; @@ -23,6 +24,7 @@ import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -140,6 +142,16 @@ public class RedisUtils { batch.execute(); } + + public static Boolean trySetObject(final String key, final T value, final Duration duration) { + RBatch batch = CLIENT.createBatch(); + RBucketAsync bucket = batch.getBucket(key); + RFuture future = bucket.trySetAsync(value, duration.toMillis(), TimeUnit.MILLISECONDS); + batch.execute(); + return future.join(); + } + + /** * 注册对象监听器 *

diff --git a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/listener/activity/RocketMqBpmActivityEventListener.java b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/listener/activity/RocketMqBpmActivityEventListener.java index 1da62614e..f6a54b77a 100644 --- a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/listener/activity/RocketMqBpmActivityEventListener.java +++ b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/listener/activity/RocketMqBpmActivityEventListener.java @@ -25,15 +25,19 @@ import org.springframework.context.annotation.Scope; import org.springframework.core.Ordered; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; import javax.annotation.Resource; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; import static cn.axzo.workflow.common.constant.BpmnConstants.FLOW_SERVER_VERSION_121; import static cn.axzo.workflow.common.constant.BpmnConstants.INTERNAL_ACTIVITY_RELATION_ASSIGNEE_LIST_INFO_SNAPSHOT; +import static cn.axzo.workflow.common.constant.BpmnConstants.MQ_OWNERSHIP_PROCESS_DEFINITION_KEY; import static cn.axzo.workflow.common.constant.BpmnConstants.WORKFLOW_ENGINE_VERSION; import static cn.axzo.workflow.common.enums.BpmnProcessInstanceResultEnum.APPROVED; import static cn.axzo.workflow.common.enums.ProcessActivityEventEnum.PROCESS_ACTIVITY_END; @@ -179,13 +183,18 @@ public class RocketMqBpmActivityEventListener extends AbstractBpmnEventListener< if (!sendMQ) { return; } + Map header = new HashMap<>(); + if (StringUtils.hasText(dto.getProcessDefinitionKey())) { + log.warn("record process definition key: {}", dto.getProcessDefinitionKey()); + header.put(MQ_OWNERSHIP_PROCESS_DEFINITION_KEY, dto.getProcessDefinitionKey()); + } eventProducer.send(Event.builder() .shardingKey(dto.getProcessInstanceId()) .eventCode(eventEnum.getEventCode()) .targetId(dto.getProcessInstanceId()) - .targetType(eventEnum.getTag()) + .targetType(dto.getProcessDefinitionKey()) .data(dto) - .build()); + .build(), header); } @Override diff --git a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/listener/error/ApproveErrorReporterEventListener.java b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/listener/error/ApproveErrorReporterEventListener.java index 501ae8c5e..0f684ac04 100644 --- a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/listener/error/ApproveErrorReporterEventListener.java +++ b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/listener/error/ApproveErrorReporterEventListener.java @@ -31,6 +31,7 @@ import java.util.Optional; import static cn.axzo.workflow.core.common.code.BpmnTaskRespCode.TASK_COMPLETE_FAIL_ASSIGN_NOT_SELF; import static cn.axzo.workflow.core.common.code.BpmnTaskRespCode.TASK_COMPLETE_FAIL_NOT_EXISTS; import static cn.axzo.workflow.core.common.code.BpmnTaskRespCode.TASK_HAS_BEEN_COMPLETE; +import static cn.axzo.workflow.core.listener.AbstractBpmnEventListener.parseProcessDefinitionKey; import static org.flowable.common.engine.api.delegate.event.FlowableEngineEventType.JOB_MOVED_TO_DEADLETTER; /** @@ -80,7 +81,9 @@ public class ApproveErrorReporterEventListener implements BpmnAsyncJobEventListe Optional noticeConfig = BpmnMetaParserHelper.getNoticeConfig(process); ProcessEngineConfigurationImpl processEngineConfiguration = CommandContextUtil.getProcessEngineConfiguration(); FlowableEventDispatcher eventDispatcher = processEngineConfiguration.getEventDispatcher(); - MessagePushEvent event = MessagePushEventBuilder.createPendingRollbackEvent(jobEvent.getProcessInstanceId(), jobInfo.getTenantId(), dto.getTaskId(), noticeConfig.orElse(new BpmnNoticeConf())); + MessagePushEvent event = MessagePushEventBuilder.createPendingRollbackEvent(jobEvent.getProcessInstanceId(), + parseProcessDefinitionKey(jobEvent.getProcessDefinitionId()), + jobInfo.getTenantId(), dto.getTaskId(), noticeConfig.orElse(new BpmnNoticeConf())); log.info("发送恢复待办的流程事件消息: taskDto:{},event:{}", JSONUtil.toJsonStr(dto), JSONUtil.toJsonStr(event)); eventDispatcher.dispatchEvent(event, processEngineConfiguration.getEngineCfgKey()); } diff --git a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/listener/notice/RocketMqMessagePushEventListener.java b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/listener/notice/RocketMqMessagePushEventListener.java index 42d5e652a..820d255a7 100644 --- a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/listener/notice/RocketMqMessagePushEventListener.java +++ b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/listener/notice/RocketMqMessagePushEventListener.java @@ -54,6 +54,7 @@ import static cn.axzo.workflow.common.constant.BpmnConstants.INTERNAL_PROCESS_TY import static cn.axzo.workflow.common.constant.BpmnConstants.INTERNAL_SPECIFY_NEXT_APPROVER; import static cn.axzo.workflow.common.constant.BpmnConstants.INTERNAL_TASK_RELATION_ASSIGNEE_INFO; import static cn.axzo.workflow.common.constant.BpmnConstants.INTERNAL_TASK_RELATION_ASSIGNEE_LIST_INFO; +import static cn.axzo.workflow.common.constant.BpmnConstants.MQ_OWNERSHIP_PROCESS_DEFINITION_KEY; import static cn.axzo.workflow.common.constant.BpmnConstants.MULTI_INSTANCE_LOOP_COUNTER; import static cn.axzo.workflow.common.constant.BpmnConstants.NUMBER_OF_INSTANCES; import static cn.axzo.workflow.common.constant.BpmnConstants.TASK_COMPLETE_OPERATION_TYPE; @@ -244,6 +245,7 @@ public class RocketMqMessagePushEventListener extends AbstractBpmnEventListener< || !StringUtils.hasText(event.getNoticeConfig().getCarbonCopy().getCarbonCopyMessageId())) { return; } + // TODO 这里的抄送人,不建议使用一个 MQ 事件来承载,而是一个人一个事件 log.info("RocketMqMessagePushEventListener#onCarbonCopy... cc' templateId: {}, receivePerson: {}, processInstanceId: {}", event.getNoticeConfig().getCarbonCopy().getCarbonCopyMessageId(), JSONUtil.toJsonStr(event.getAssigners()), event.getProcessInstanceId()); @@ -376,6 +378,7 @@ public class RocketMqMessagePushEventListener extends AbstractBpmnEventListener< Object> variables) { return new MessagePushDTO() .setProcessInstanceId(event.getProcessInstanceId()) + .setProcessDefinitionKey(event.getProcessDefinitionKey()) .setType(type) .setTemplateId(templateId) .setTaskId(event.getTaskId()) @@ -389,13 +392,18 @@ public class RocketMqMessagePushEventListener extends AbstractBpmnEventListener< if (!sendMQ) { return; } + Map header = new HashMap<>(); + if (StringUtils.hasText(dto.getProcessDefinitionKey())) { + log.warn("record process definition key: {}", dto.getProcessDefinitionKey()); + header.put(MQ_OWNERSHIP_PROCESS_DEFINITION_KEY, dto.getProcessDefinitionKey()); + } eventProducer.send(Event.builder() .shardingKey(dto.getProcessInstanceId()) .eventCode(eventEnum.getEventCode()) .targetId(dto.getProcessInstanceId()) - .targetType(eventEnum.getTag()) + .targetType(dto.getProcessDefinitionKey()) .data(dto) - .build()); + .build(), header); } @Override diff --git a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/listener/process/MessagePushProcessEventListener.java b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/listener/process/MessagePushProcessEventListener.java index 81cc92bb9..75660f280 100644 --- a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/listener/process/MessagePushProcessEventListener.java +++ b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/listener/process/MessagePushProcessEventListener.java @@ -76,7 +76,7 @@ public class MessagePushProcessEventListener extends AbstractBpmnEventListener

{ MessagePushEventImpl messagePushEvent = MessagePushEventBuilder.createEvent(MessagePushEventType.PENDING_COMPLETE, null, noticeConfig, - event.getProcessInstanceId(), null, null); + event.getProcessInstanceId(), parseProcessDefinitionKey(event.getProcessDefinitionId()), null, null); log.info("发送完成实例下所有待办的消息: {}", JSONUtil.toJsonStr(messagePushEvent)); @@ -87,7 +87,7 @@ public class MessagePushProcessEventListener extends AbstractBpmnEventListener

header = new HashMap<>(); + if (StringUtils.hasText(dto.getProcessDefinitionKey())) { + log.warn("record process definition key: {}", dto.getProcessDefinitionKey()); + header.put(MQ_OWNERSHIP_PROCESS_DEFINITION_KEY, dto.getProcessDefinitionKey()); + } eventProducer.send(Event.builder() .shardingKey(dto.getProcessInstanceId()) .eventCode(eventEnum.getEventCode()) .targetId(dto.getProcessInstanceId()) - .targetType(eventEnum.getTag()) + .targetType(dto.getProcessDefinitionKey()) .data(dto) - .build()); + .build(), header); } @Override diff --git a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/listener/task/MessagePushTaskEvent_103_Listener.java b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/listener/task/MessagePushTaskEvent_103_Listener.java index 8af0e6153..eb07a8387 100644 --- a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/listener/task/MessagePushTaskEvent_103_Listener.java +++ b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/listener/task/MessagePushTaskEvent_103_Listener.java @@ -114,7 +114,8 @@ public class MessagePushTaskEvent_103_Listener extends AbstractBpmnEventListener BpmnMetaParserHelper.getNodePendingConfig(userTask).ifPresent(noticeConfig::setPending); MessagePushEventImpl event = MessagePushEventBuilder.createEvent(MessagePushEventType.PENDING_COMPLETE, - null, noticeConfig, delegateTask.getProcessInstanceId(), null, delegateTask.getId()); + null, noticeConfig, delegateTask.getProcessInstanceId(), + parseProcessDefinitionKey(delegateTask.getProcessDefinitionId()), null, delegateTask.getId()); log.info("发送完成待办的消息: {}, processInstanceId:{}", JSONUtil.toJsonStr(event), delegateTask.getProcessInstanceId()); eventDispatcher.dispatchEvent(event, processEngineConfiguration.getEngineCfgKey()); }); @@ -150,7 +151,8 @@ public class MessagePushTaskEvent_103_Listener extends AbstractBpmnEventListener noticeConf, processApproveConf.orElse(new BpmnApproveConf()), processInstance.getProcessInstanceId(), - processInstance.getProcessDefinitionId(), userTask.getId(), + processInstance.getProcessDefinitionId(), + processInstance.getProcessDefinitionKey(), userTask.getId(), processInstance.getTenantId(), delegateTask.getId()); log.info("发送推送待办的消息: {}, processInstanceId:{}", JSONUtil.toJsonStr(event), event.getProcessInstanceId()); eventDispatcher.dispatchEvent(event, processEngineConfiguration.getEngineCfgKey()); diff --git a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/listener/task/RocketMqBpmnTaskEvent_102_Listener.java b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/listener/task/RocketMqBpmnTaskEvent_102_Listener.java index 355dbd01d..d56610415 100644 --- a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/listener/task/RocketMqBpmnTaskEvent_102_Listener.java +++ b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/listener/task/RocketMqBpmnTaskEvent_102_Listener.java @@ -23,11 +23,14 @@ import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import javax.annotation.Resource; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; import static cn.axzo.workflow.common.constant.BpmnConstants.FLOW_SERVER_VERSION_121; import static cn.axzo.workflow.common.constant.BpmnConstants.INTERNAL_INITIATOR; import static cn.axzo.workflow.common.constant.BpmnConstants.INTERNAL_TASK_RELATION_ASSIGNEE_INFO; +import static cn.axzo.workflow.common.constant.BpmnConstants.MQ_OWNERSHIP_PROCESS_DEFINITION_KEY; import static cn.axzo.workflow.common.constant.BpmnConstants.TASK_ASSIGNEE_SKIP_FLAT; import static cn.axzo.workflow.common.constant.BpmnConstants.WORKFLOW_ENGINE_VERSION; import static cn.axzo.workflow.common.enums.ProcessTaskEventEnum.PROCESS_TASK_ASSIGNED; @@ -103,9 +106,11 @@ public class RocketMqBpmnTaskEvent_102_Listener extends AbstractBpmnEventListene public ProcessTaskDTO build(DelegateTask delegateTask, ProcessTaskEventEnum type) { Process mainProcess = getContext().getProcess(() -> repositoryService.getBpmnModel(delegateTask.getProcessDefinitionId()).getMainProcess()); + String category = getDeployment(delegateTask.getProcessInstanceId()).getCategory(); ProcessTaskDTO dto = new ProcessTaskDTO() .setType(type) - .setCategory(getDeployment(delegateTask.getProcessInstanceId()).getCategory()) + .setCategory(category) + .setProcessDefinitionKey(category) .setProcessTaskId(delegateTask.getId()) .setProcessInstanceId(delegateTask.getProcessInstanceId()) .setCurrentElementKey(delegateTask.getTaskDefinitionKey()) @@ -131,13 +136,20 @@ public class RocketMqBpmnTaskEvent_102_Listener extends AbstractBpmnEventListene if (!sendMQ) { return; } + Map header = new HashMap<>(); + if (StringUtils.hasText(dto.getProcessDefinitionKey())) { + if (log.isDebugEnabled()) { + log.debug("record process definition key: {}", dto.getProcessDefinitionKey()); + } + header.put(MQ_OWNERSHIP_PROCESS_DEFINITION_KEY, dto.getProcessDefinitionKey()); + } eventProducer.send(Event.builder() .shardingKey(dto.getProcessInstanceId()) .eventCode(eventEnum.getEventCode()) .targetId(dto.getProcessInstanceId()) - .targetType(eventEnum.getTag()) + .targetType(dto.getProcessDefinitionKey()) .data(dto) - .build()); + .build(), header); } diff --git a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/web/TestController.java b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/web/TestController.java index 7628389ee..a3fe66874 100644 --- a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/web/TestController.java +++ b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/web/TestController.java @@ -1,8 +1,13 @@ package cn.axzo.workflow.server.controller.web; import cn.axzo.workflow.client.feign.bpmn.ProcessInstanceApi; +import cn.axzo.workflow.common.model.dto.CooperationOrgDTO; +import cn.axzo.workflow.common.model.request.bpmn.RestBpmnProcessVariable; import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceAbortDTO; +import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceCreateDTO; import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceQueryDTO; +import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskAuditDTO; +import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskDelegateAssigner; import cn.axzo.workflow.common.model.response.bpmn.process.BpmnProcessInstanceVO; import cn.axzo.workflow.common.model.response.bpmn.process.ProcessNodeDetailVO; import cn.axzo.workflow.core.service.BpmnProcessInstanceService; @@ -25,8 +30,10 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.io.InputStream; +import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -35,6 +42,7 @@ import static cn.axzo.workflow.common.enums.BpmnProcessInstanceResultEnum.PROCES /** * 测试接口 + * * @author wangli * @since 2023/10/10 13:59 */ @@ -58,6 +66,10 @@ public class TestController { private ProcessInstanceApi processInstanceApi; @Autowired private BpmnProcessInstanceService bpmnProcessInstanceService; +// @Autowired +// private WorkflowCoreService workflowCoreService; +// @Autowired +// private WorkflowManageService workflowManageService; @RepeatSubmit @GetMapping("/test") @@ -124,4 +136,117 @@ public class TestController { return CommonResponse.success(true); } + /*@GetMapping("/create") + public CommonResponse test5(@RequestParam(required = false) Boolean sync) { + BpmnProcessInstanceCreateDTO dto = new BpmnProcessInstanceCreateDTO(); + dto.setProcessDefinitionKey("1"); + dto.setCooperationOrg(new CooperationOrgDTO()); + dto.setBusinessKey("businessKey"); + dto.setInitiator(new BpmnTaskDelegateAssigner()); + if (Objects.nonNull(sync)) { + if (sync) { + workflowCoreService.sync(); + } else { + workflowCoreService.async(); + } + } + return CommonResponse.success(workflowCoreService.createProcessInstance(dto)); + } + + @GetMapping("/approve") + public CommonResponse test6(@RequestParam(required = false) Boolean sync, @RequestParam String taskId) { + if (Objects.nonNull(sync)) { + if (sync) { + workflowCoreService.sync(); + } else { + workflowCoreService.async(); + } + } + BpmnTaskAuditDTO dto = new BpmnTaskAuditDTO(); + dto.setTaskId(taskId); + BpmnTaskDelegateAssigner approver = new BpmnTaskDelegateAssigner("2002456", "3", "彭建伦", "9000586", "374", "6064", "https://axzo-app.oss-cn-chengdu.aliyuncs.com/face/face_dev/f5f5634ff6e84bfd9291b8e1a8ad215d.jpg"); + dto.setApprover(approver); + return CommonResponse.success(workflowCoreService.approveTask(dto)); + } + + @GetMapping("/get") + public CommonResponse test7(@RequestParam(required = false) Boolean sync, @RequestParam String processInstanceId) { + if (Objects.isNull(sync)) { + throw new RuntimeException("sync 参数不能为空"); + } + if (Objects.nonNull(sync)) { + if (sync) { + workflowCoreService.sync(); + } else { + workflowCoreService.async(); + } + } + BpmnProcessInstanceQueryDTO dto = new BpmnProcessInstanceQueryDTO(); + dto.setProcessInstanceId(processInstanceId); + dto.setHasVariable(true); +// return CommonResponse.success(workflowCoreService.getProcessInstanceVO(dto)); + return CommonResponse.success(workflowCoreService.getProcessInstanceVO(dto)); + } + + @GetMapping("/get/vars") + public CommonResponse> test8(@RequestParam(required = false) Boolean sync, @RequestParam String processInstanceId) { + if (Objects.isNull(sync)) { + throw new RuntimeException("sync 参数不能为空"); + } + if (Objects.nonNull(sync)) { + if (sync) { + workflowCoreService.sync(); + } else { + workflowCoreService.async(); + } + } + return CommonResponse.success(workflowCoreService.getProcessVariables(processInstanceId, null)); + } + + @GetMapping("/create/var") + public CommonResponse test9(@RequestParam(required = false) Boolean sync, @RequestParam String processInstanceId) { + if (Objects.isNull(sync)) { + throw new RuntimeException("sync 参数不能为 null"); + } + if (Objects.nonNull(sync)) { + if (sync) { + workflowCoreService.sync(); + } else { + workflowCoreService.async(); + } + } + RestBpmnProcessVariable variable = new RestBpmnProcessVariable(); + variable.setName("testVar"); + variable.setValue("testValue"); +// workflowCoreService.createVariable(processInstanceId, variable); + return CommonResponse.success(null); + } + + @GetMapping("/batch/abort") + public CommonResponse test10(@RequestParam(required = false) Boolean sync) { + if (Objects.isNull(sync)) { + throw new RuntimeException("sync 参数不能为 null"); + } + if (Objects.nonNull(sync)) { + if (sync) { + workflowCoreService.sync(); + } else { + workflowCoreService.async(); + } + } + List dtos = new ArrayList<>(); + BpmnProcessInstanceAbortDTO abort = new BpmnProcessInstanceAbortDTO(); + abort.setProcessInstanceId("1"); + abort.setTenantId("2"); + abort.setReason("reason"); + dtos.add(abort); + workflowCoreService.batchAbortProcessInstance(dtos); + return CommonResponse.success(dtos); + } + + @GetMapping("/tenant/get/ids") + public CommonResponse> test11() { + List tenantIds = workflowManageService.sync().getTenantIds(); + return CommonResponse.success(tenantIds); + }*/ } diff --git a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/web/bpmn/BpmnProcessActivityController.java b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/web/bpmn/BpmnProcessActivityController.java index b9601c36b..6d83c6c38 100644 --- a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/web/bpmn/BpmnProcessActivityController.java +++ b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/web/bpmn/BpmnProcessActivityController.java @@ -49,8 +49,7 @@ public class BpmnProcessActivityController implements ProcessActivityApi { * @return */ @Operation(summary = "业务节点唤醒") - @GetMapping("/trigger") - @Override + @GetMapping("/v2/trigger") @RepeatSubmit public CommonResponse trigger(@RequestParam @NotBlank(message = "触发 ID 不能为空") String triggerId, @RequestParam(required = false, defaultValue = "false") Boolean async) { @@ -63,8 +62,14 @@ public class BpmnProcessActivityController implements ProcessActivityApi { return CommonResponse.success(true); } + /** + * old/trigger 地址是 1.3.3 版本以前的接口,在接入方未完全升级前,都需要保留 + * + * @param triggerId + * @return + */ @Operation(summary = "业务节点唤醒") - @GetMapping("/old/trigger") + @GetMapping({"/trigger", "/old/trigger"}) @Override @RepeatSubmit public CommonResponse trigger(@NotBlank(message = "触发 ID 不能为空") @RequestParam String triggerId) { diff --git a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/web/bpmn/BpmnProcessInstanceController.java b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/web/bpmn/BpmnProcessInstanceController.java index af74fb7ef..173adfd8b 100644 --- a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/web/bpmn/BpmnProcessInstanceController.java +++ b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/web/bpmn/BpmnProcessInstanceController.java @@ -27,6 +27,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import io.swagger.v3.oas.annotations.Operation; import lombok.extern.slf4j.Slf4j; import org.flowable.engine.history.HistoricProcessInstance; +import org.springframework.cache.annotation.CachePut; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -268,7 +269,6 @@ public class BpmnProcessInstanceController implements ProcessInstanceApi { @Operation(summary = "获取指定流程实例的流程变量") @GetMapping("/cooperation-org") @Override - @RepeatSubmit public CommonResponse> getProcessVariables(@NotBlank(message = "流程实例 ID 不能为空") @RequestParam String processInstanceId, @Nullable String tenantId) { HistoricProcessInstance processInstance = bpmnProcessInstanceService.getProcessInstance(processInstanceId, diff --git a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/web/bpmn/BpmnProcessModelController.java b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/web/bpmn/BpmnProcessModelController.java index 9d92c3dc7..2924e846b 100644 --- a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/web/bpmn/BpmnProcessModelController.java +++ b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/web/bpmn/BpmnProcessModelController.java @@ -274,7 +274,7 @@ public class BpmnProcessModelController implements ProcessModelApi { @Operation(summary = "查询模型的租户集合") @GetMapping("/tenant/ids") @Override - public CommonResponse> getTenantIds() { + public CommonResponse> getModelTenantIds() { log.info("查询模型的租户集合getTenantIds"); return success(bpmnProcessModelService.getTenantIds()); } diff --git a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/web/bpmn/BpmnProcessTaskController.java b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/web/bpmn/BpmnProcessTaskController.java index 37c1eba57..0a87ef7e1 100644 --- a/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/web/bpmn/BpmnProcessTaskController.java +++ b/workflow-engine-server/src/main/java/cn/axzo/workflow/server/controller/web/bpmn/BpmnProcessTaskController.java @@ -39,7 +39,7 @@ import org.springframework.web.bind.annotation.RestController; import javax.annotation.Nullable; import javax.annotation.Resource; import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; +import javax.validation.constraints.NotEmpty; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -333,7 +333,7 @@ public class BpmnProcessTaskController implements ProcessTaskApi { @Operation(summary = "根据实例 ID 和自然人 ID 查询对应待处理的任务 ID") @GetMapping("/batch/find") @Override - public CommonResponse> findTaskIdByInstanceIdsAndPersonId(@RequestParam @NotNull(message = "流程实例 ID列表 不能为空") List processInstanceIds, + public CommonResponse> findTaskIdByInstanceIdsAndPersonId(@RequestParam @NotEmpty(message = "流程实例 ID列表 不能为空") List processInstanceIds, @RequestParam @NotBlank(message = "自然人 ID 不能为空") String personId) { return success(bpmnProcessTaskService.findTaskIdByInstanceIdsAndPersonId(processInstanceIds, personId)); } diff --git a/workflow-engine-server/src/main/resources/application.yaml b/workflow-engine-server/src/main/resources/application.yml similarity index 100% rename from workflow-engine-server/src/main/resources/application.yaml rename to workflow-engine-server/src/main/resources/application.yml diff --git a/workflow-engine-server/src/main/resources/logback-spring.xml b/workflow-engine-server/src/main/resources/logback-spring.xml index e56446467..eaaa97bd8 100644 --- a/workflow-engine-server/src/main/resources/logback-spring.xml +++ b/workflow-engine-server/src/main/resources/logback-spring.xml @@ -3,10 +3,59 @@ + + + ${LOG_PATH}/ERROR/error.log + + + ${LOG_PATH}/ERROR/%d{yyyy-MM}/error.%d{yyyy-MM-dd}.%i.log.gz + + 50MB + + 30 + + + + %date [%thread] %-5level [%logger{50}] %file:%line - %msg%n + + + ERROR + + + + + + ${LOG_PATH}/WARN/warn.log + + + ${LOG_PATH}/WARN/%d{yyyy-MM}/warn.%d{yyyy-MM-dd}.%i.log.gz + + 50MB + + 30 + + + + %date [%thread] %-5level [%logger{50}] %file:%line - %msg%n + + + WARN + + + + + + + + + + + + diff --git a/workflow-engine-spring-boot-starter/README.md b/workflow-engine-spring-boot-starter/README.md new file mode 100644 index 000000000..341f4682c --- /dev/null +++ b/workflow-engine-spring-boot-starter/README.md @@ -0,0 +1,29 @@ +## Workflow Engine Spring Boot Starter + +### 说明 + +该 module 主要是为业务方提供快速接入工作流引擎的能力,starter +的架构设计点击[查看](https://alidocs.dingtalk.com/i/nodes/Obva6QBXJwDeXvymIv1mQmR48n4qY5Pr) + +### 功能点 + +- 包装监听 Workflow Engine 服务端所有的广播事件,业务方仅简单实现或继承指定类完成自己的业务逻辑即可,不再关注其他细节。 + - 业务可实现监听器: + 1. ProcessInstanceListener + 2. ProcessTaskListener + 3. ProcessActivityListener + 4. MessageNotificationListener +- 内部与工作流的数据交互,也将异步解耦,确保在调用对端服务时,不会因为对端服务中止而失败。 + - 接口需要有分类: + 1. 部分接口仅支持同步调 + 2. 大部分接口支持同步和异步调用, + - 异步调用则是利用 MQ 进行触发 + - 同步调用则是扫API包后,根据注解为某些接口生成 Feign 的代理类,而非让自动生成 Feign 代理类 + 3. 接口需要有调用限制的控制,并非搜有的使用方均可直接调用 + 4. 针对同步调用设计支持回调接口,供使用方自行处理 +- 内部将主动监听 MQ 队列,避免因为顺序消费从而导致的队列阻塞。 + - 降级实现,只要能确保顺序消费把所有的异常情况都考虑到,不阻塞消费便无需该功能。 +- 本地启动将不再主动消费容器环境中的 MQ 广播,避免本地异常消费。 +- 包装引擎抛出的所有异常,不再由业务方判断各类异常 code,而由 starter 统一确定是否抛出异常。 + +### 使用方法 diff --git a/workflow-engine-spring-boot-starter/pom.xml b/workflow-engine-spring-boot-starter/pom.xml new file mode 100644 index 000000000..3af6e195f --- /dev/null +++ b/workflow-engine-spring-boot-starter/pom.xml @@ -0,0 +1,49 @@ + + 4.0.0 + + cn.axzo.workflow + workflow-engine + ${revision} + + workflow-engine-spring-boot-starter + jar + Workflow Engine Spring Boot Starter + + + org.springframework.boot + spring-boot-starter + true + + + org.springframework.boot + spring-boot-configuration-processor + true + + + cn.axzo.framework.rocketmq + axzo-common-rocketmq + provided + + + com.aliyun + alibaba-dingtalk-service-sdk + provided + + + org.apache.rocketmq + rocketmq-tools + 4.9.1 + + + ${project.groupId} + workflow-engine-api + ${project.version} + + + jakarta.servlet + jakarta.servlet-api + provided + + + diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/BroadcastListenerProperties.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/BroadcastListenerProperties.java new file mode 100644 index 000000000..679d752bd --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/BroadcastListenerProperties.java @@ -0,0 +1,157 @@ +package cn.axzo.workflow.starter; + +import cn.axzo.workflow.starter.common.enums.FailHandleTypeEnum; + +import java.util.HashSet; +import java.util.Set; + +import static cn.axzo.workflow.starter.common.enums.FailHandleTypeEnum.FAIL_OVER; + +/** + * 监听流程引擎广播的事件时一些行为控制 + *

比如:
+ * 1. 在遇到异常时的处理策略
+ * 2. 过滤事件
+ * 
+ * + * @author wangli + * @since 2024/6/4 14:14 + */ +public class BroadcastListenerProperties { + /** + * 是否开启根据应用名过滤 MQ 事件 + *

+ * 如果为 true 时,在不对 {@link BroadcastListenerProperties#filterApplicationNames} 设任何值时,默认会将当前应用名加入进去 + */ + private Boolean enableFilterApplicationName = false; + + /** + * 仅过滤这些应用名称创建的流程实例 + */ + private Set filterApplicationNames = new HashSet<>(); + + /** + * 是否开启根据业务 ID 集合过滤 MQ 事件 + * + * 业务 ID:从 OMS 平台中的“审批业务”查看, + *

+     * 
+     * 
+ * 或者从“审批模板”中“业务 ID”进行查看 + *
+     *     
+     * 
+ */ + private Boolean enableFilterDefinitionKey = false; + + /** + * 过滤出 MQ 事件中包含这些业务 ID 的事件 + *

+ * 只有当 {@link BroadcastListenerProperties#enableFilterDefinitionKey} 才生效 + *

+ * 注意: 如果 enableFilterDefinitionKey = true,但该属性集合为空, 将不会过滤任何消息 + */ + private Set filterProcessDefinitionKeys = new HashSet<>(); + + /** + * 失败处理策略: + *

+     * 1、FAIL_OVER, 当前listener执行出错,在经历重试后,抛出异常,并将消息加入死信队列,然后继续往下执行(默认策略)
+     * 2、FAIL_FAST, 快速失败,不管内部是什么异常类型,都将吞掉异常,正确结束,不会增加死信队列计数
+     * 3、FAIL_BACK, 失败自动恢复,在后台记录失败的消息,并按照一定的策略后期再进行重试,目前暂不支持
+     * 
+ */ + private FailHandleTypeEnum failHandleType = FAIL_OVER; + + /** + * 广播的 DLQ 监控 + */ + private Boolean monitorStatus = false; + + /** + * 自动重试次数 + */ + private int numOfRetries = 3; + + /** + * 初始等待时间,单位:毫秒 + */ + private int waitTimeInMs = 1500; + + /** + * 重试累乘因子, 意味多次重试时,每次重试间隔为 waitTimeInMs * waitIncreaseFactor 毫秒 + */ + private int waitIncreaseFactor = 3; + + public Boolean getEnableFilterApplicationName() { + return enableFilterApplicationName; + } + + public void setEnableFilterApplicationName(Boolean enableFilterApplicationName) { + this.enableFilterApplicationName = enableFilterApplicationName; + } + + public Set getFilterApplicationNames() { + return filterApplicationNames; + } + + public void setFilterApplicationNames(Set filterApplicationNames) { + this.filterApplicationNames = filterApplicationNames; + } + + public Boolean getEnableFilterDefinitionKey() { + return enableFilterDefinitionKey; + } + + public void setEnableFilterDefinitionKey(Boolean enableFilterDefinitionKey) { + this.enableFilterDefinitionKey = enableFilterDefinitionKey; + } + + public Set getFilterProcessDefinitionKeys() { + return filterProcessDefinitionKeys; + } + + public void setFilterProcessDefinitionKeys(Set filterProcessDefinitionKeys) { + this.filterProcessDefinitionKeys = filterProcessDefinitionKeys; + } + + public FailHandleTypeEnum getFailHandleType() { + return failHandleType; + } + + public void setFailHandleType(FailHandleTypeEnum failHandleType) { + this.failHandleType = failHandleType; + } + + public Boolean getMonitorStatus() { + return monitorStatus; + } + + public void setMonitorStatus(Boolean monitorStatus) { + this.monitorStatus = monitorStatus; + } + + public int getNumOfRetries() { + return numOfRetries; + } + + public void setNumOfRetries(int numOfRetries) { + this.numOfRetries = numOfRetries; + } + + public int getWaitTimeInMs() { + return waitTimeInMs; + } + + public void setWaitTimeInMs(int waitTimeInMs) { + this.waitTimeInMs = waitTimeInMs; + } + + public int getWaitIncreaseFactor() { + return waitIncreaseFactor; + } + + public void setWaitIncreaseFactor(int waitIncreaseFactor) { + this.waitIncreaseFactor = waitIncreaseFactor; + } +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/StarterBroadcastMQConfiguration.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/StarterBroadcastMQConfiguration.java new file mode 100644 index 000000000..9ac804265 --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/StarterBroadcastMQConfiguration.java @@ -0,0 +1,131 @@ +package cn.axzo.workflow.starter; + +import cn.axzo.framework.rocketmq.BaseListener; +import cn.axzo.framework.rocketmq.DefaultEventConsumer; +import cn.axzo.framework.rocketmq.Event; +import cn.axzo.framework.rocketmq.EventConsumer; +import cn.axzo.framework.rocketmq.EventHandlerRepository; +import cn.axzo.workflow.starter.common.condition.NonContainerEnvironmentCondition; +import cn.axzo.workflow.starter.common.exception.WorkflowEngineStarterException; +import cn.axzo.workflow.starter.handler.filter.global.BroadcastMessageQueueFilter; +import cn.axzo.workflow.starter.mq.broadcast.consumer.InnerWorkflowListener; +import cn.axzo.workflow.starter.mq.broadcast.consumer.WorkflowEngineBroadcastEventListener; +import cn.axzo.workflow.starter.mq.broadcast.filter.InnerFilterDefinitionKey; +import cn.axzo.workflow.starter.mq.broadcast.filter.InnerFilterExtension; +import cn.axzo.workflow.starter.mq.broadcast.filter.InnerFilterMQOwnerShip; +import cn.axzo.workflow.starter.mq.broadcast.filter.InnerMessageQueueHandleBeforeFilter; +import com.google.common.collect.ImmutableList; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.spring.annotation.ConsumeMode; +import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; +import org.apache.rocketmq.spring.core.RocketMQListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.List; +import java.util.function.Consumer; + +import static cn.axzo.workflow.starter.StarterRPCInvokeMQConfiguration.DEFAULT_EVENT; + +/** + * 配置监听流程引擎服务广播消息的 RocketMQ 相关配置 + * + * @author wangli + * @since 2024/6/5 17:39 + */ +@Configuration(proxyBeanMethods = false) +public class StarterBroadcastMQConfiguration { + private final Logger log = LoggerFactory.getLogger(StarterBroadcastMQConfiguration.class); + public static final String BROADCAST_EVENT_HANDLER_REPOSITORY_BEAN_NAME = "broadcastEventHandlerRepository"; + public static final String BROADCAST_EVENT_CONSUMER_BEAN_NAME = "broadcastEventConsumer"; + @Value("${spring.application.name}") + private String applicationName; + @Value("${spring.profiles.active:dev}") + private String activeProfile; + + //================================= Workflow Engine Broadcast MQ =================================// + @Bean(BROADCAST_EVENT_HANDLER_REPOSITORY_BEAN_NAME) + @ConditionalOnMissingBean(name = BROADCAST_EVENT_HANDLER_REPOSITORY_BEAN_NAME) + public EventHandlerRepository broadcastEventHandlerRepository() { + return new EventHandlerRepository((ex, logText) -> { + // 不管 cn.axzo.workflow.starter.BroadcastListenerProperties.failHandleType 设置是哪种策略,这里最终都会打印 WARN 级别日志,框架底层会对所有异常的消息打印 ERROR 日志 + // 如果不想打印需要扩展内部逻辑,请主动注册自己扩展的 Bean,且名称为 StarterBroadcastMQConfiguration.BROADCAST_EVENT_HANDLER_REPOSITORY_BEAN_NAME + log.warn("Workflow Engine Starter MQ, an exception occurred during processing {}", logText, ex); + // 让其进入死信队列 + throw new WorkflowEngineStarterException(ex); + }); + } + + @Bean(BROADCAST_EVENT_CONSUMER_BEAN_NAME) + @ConditionalOnMissingBean(name = BROADCAST_EVENT_CONSUMER_BEAN_NAME) + public EventConsumer broadcastEventConsumer(@Qualifier(BROADCAST_EVENT_HANDLER_REPOSITORY_BEAN_NAME) EventHandlerRepository eventHandlerRepository) { + Consumer callback = eventWrapper -> { + if (eventWrapper.isHandled()) { + // 只收集被App真正消费的消息. + if (log.isDebugEnabled()) { + Event event = eventWrapper.getEvent(); + log.debug("WorkflowEngine Broadcast MQ, handled event: {}", event.toPrettyJsonString()); + } + } + }; + return new DefaultEventConsumer(applicationName, eventHandlerRepository, callback); + } + + @Component + @Conditional(NonContainerEnvironmentCondition.class) + @RocketMQMessageListener(topic = DEFAULT_EVENT + "${spring.profiles.active}", + consumerGroup = "GID_${spring.application.name}_workflow_engine_${GID_SEGMENT}_consumer", + consumeMode = ConsumeMode.ORDERLY, + maxReconsumeTimes = 0, + nameServer = "${rocketmq.name-server}" + ) + public static class WorkflowEngineBroadcastConsumer extends BaseListener implements RocketMQListener, InitializingBean { + private final Logger log = LoggerFactory.getLogger(WorkflowEngineBroadcastConsumer.class); + + @Resource(name = BROADCAST_EVENT_CONSUMER_BEAN_NAME) + private EventConsumer broadcastEventConsumer; + @Value("${spring.application.name}") + private String applicationName; + @Resource + private WorkflowEngineStarterProperties starterProperties; + @Resource + private ObjectProvider> businessFilterProvider; + private List filters; + + @Override + public void onMessage(MessageExt message) { + for (InnerMessageQueueHandleBeforeFilter filter : filters) { + if (filter.doFilter(message)) { + log.info("【{}】message has been filtered, messageId: {}", this.getClass().getSimpleName(), message.getMsgId()); + return; + } + } + super.onEvent(message, broadcastEventConsumer); + } + + @Override + public void afterPropertiesSet() { + this.filters = ImmutableList.of( + new InnerFilterMQOwnerShip(starterProperties, applicationName), + new InnerFilterDefinitionKey(starterProperties), + new InnerFilterExtension(businessFilterProvider)); + } + } + + @Bean("workflowEngineBroadcastEventListener") + public WorkflowEngineBroadcastEventListener workflowEngineBroadcastEventListener(@Qualifier(BROADCAST_EVENT_CONSUMER_BEAN_NAME) EventConsumer broadcastEventConsumer, + WorkflowEngineStarterProperties workflowEngineStarterProperties, + List listenerProvider) { + return new WorkflowEngineBroadcastEventListener(broadcastEventConsumer, workflowEngineStarterProperties, listenerProvider); + } +} 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 new file mode 100644 index 000000000..0c6b6e763 --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/StarterFeignClientConfiguration.java @@ -0,0 +1,28 @@ +package cn.axzo.workflow.starter; + +import cn.axzo.workflow.starter.api.WorkflowCoreService; +import cn.axzo.workflow.starter.api.WorkflowManageService; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.Configuration; + +/** + * 根据参数动态实例化可用 Bean + * + * @author wangli + * @since 2024/6/11 21:26 + */ +@Configuration(proxyBeanMethods = false) +public class StarterFeignClientConfiguration { + + @Configuration(proxyBeanMethods = false) + @EnableFeignClients(clients = WorkflowCoreService.class) + public static class WorkflowCoreServiceClient { + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnProperty(prefix = "workflow.engine.starter", value = "manageable", havingValue = "true") + @EnableFeignClients(clients = WorkflowManageService.class) + public static class WorkflowManageServiceClient { + } +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/StarterRPCInvokeMQConfiguration.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/StarterRPCInvokeMQConfiguration.java new file mode 100644 index 000000000..aa47f3f3d --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/StarterRPCInvokeMQConfiguration.java @@ -0,0 +1,207 @@ +package cn.axzo.workflow.starter; + +import cn.axzo.framework.domain.data.IdHelper; +import cn.axzo.framework.rocketmq.BaseListener; +import cn.axzo.framework.rocketmq.DefaultEventConsumer; +import cn.axzo.framework.rocketmq.Event; +import cn.axzo.framework.rocketmq.EventConsumer; +import cn.axzo.framework.rocketmq.EventHandlerRepository; +import cn.axzo.framework.rocketmq.EventProducer; +import cn.axzo.framework.rocketmq.RocketMQEventProducer; +import cn.axzo.workflow.starter.api.WorkflowCoreService; +import cn.axzo.workflow.starter.api.WorkflowManageService; +import cn.axzo.workflow.starter.common.condition.NonContainerEnvironmentCondition; +import cn.axzo.workflow.starter.common.exception.WorkflowEngineStarterException; +import cn.axzo.workflow.starter.mq.broadcast.filter.InnerFilterMQOwnerShip; +import cn.axzo.workflow.starter.mq.broadcast.filter.InnerMessageQueueHandleBeforeFilter; +import cn.axzo.workflow.starter.mq.retry.consumer.WorkflowEngineStarterRetryEventListener; +import cn.axzo.workflow.starter.mq.retry.producer.RpcInvokeEventProducer; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.spring.annotation.ConsumeMode; +import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; +import org.apache.rocketmq.spring.core.RocketMQListener; +import org.apache.rocketmq.spring.core.RocketMQTemplate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.HashMap; +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import static cn.axzo.framework.rocketmq.RocketMQEventProducer.MQ_MESSAGE_ID; + +/** + * 配置 RPC 动作的 RocketMQ 消息的发送方和消息方等配置信息 + * + * @author wangli + * @since 2024/5/30 14:05 + */ +@Configuration(proxyBeanMethods = false) +public class StarterRPCInvokeMQConfiguration { + private final Logger log = LoggerFactory.getLogger(StarterRPCInvokeMQConfiguration.class); + public static final String WORKFLOW_ENGINE_STARTER_EVENT_PRODUCER_BEAN_NAME = "workflowEngineStarterEventProducer"; + public static final String WORKFLOW_ENGINE_STARTER_EVENT_CONSUMER_BEAN_NAME = "workflowEngineStarterEventConsumer"; + public static final String WORKFLOW_ENGINE_STARTER_EVENT_HANDLER_REPOSITORY_BEAN_NAME = "workflowEngineStarterEventHandlerRepository"; + public static final String DEFAULT_MODULE = "workflowEngine"; + public static final String DEFAULT_EVENT = "topic_workflow_engine_"; + private static final String MODULE_NAME_SUFFIX = "_rpc_retry_starter"; + + @Value("${spring.application.name}") + private String applicationName; + @Value("${spring.profiles.active:dev}") + private String activeProfile; + + //======================================= RPC Invoke MQ ========================================// + + /** + * 客户端 RPC Retry 事件生产者 + * + * @param rocketMQTemplate + * @return + */ + @Bean(WORKFLOW_ENGINE_STARTER_EVENT_PRODUCER_BEAN_NAME) + public EventProducer workflowEngineStarterEventProducer(RocketMQTemplate rocketMQTemplate) { + return new RpcInvokeEventProducer(rocketMQTemplate, + DEFAULT_MODULE, + applicationName + MODULE_NAME_SUFFIX, + EventProducer.Context.builder() + .meta(RocketMQEventProducer.RocketMQMessageMeta.builder() + .topic(DEFAULT_EVENT + activeProfile) + .build()) + .headers(new HashMap<>()) + .syncSending(Boolean.TRUE) + .exceptionHandler(context -> { + log.error("MQ, send event error: {}, event: {}", + context.getThrowable().getCause().getMessage(), + context.getEvent().toPrettyJsonString(), + context.getThrowable()); + }) + .build(), + getSendBeforeCallback(), + getSendAfterCallback(), + getTransactionRollbackHandler() + ); + } + + /** + * 真实执行 MQ 发送前的回调, + *

+ * 将整个待发送的事件内容通过 spring 的事件分发器发送出去, 现目前主要是记录 MQ 的发送记录 + * + * @return + */ + private BiConsumer> getSendBeforeCallback() { + return (event, context) -> { + event.setEventId(IdHelper.get32UUID()); + log.info("mq_send_Before: {}, uniqueId: {}", event.getShardingKey(), event.getEventId()); + }; + } + + /** + * 真实执行 MQ 发送后的回调 + *

+ * 将发送前的 MQ 发送记录更新 MQ 组件自己的 MessageId 字段。 + * + * @return + */ + private BiConsumer> getSendAfterCallback() { + return (event, context) -> { + String messageId = context.getHeaders().get(MQ_MESSAGE_ID); + log.info("mq_send_after: {}, uniqueId: {}, messageId: {}", event.getShardingKey(), event.getEventId(), messageId); + }; + } + + /** + * 如果 MQ 注册的事务回滚后的回调 + *

+ * 将 MQ 发送记录更新为删除状态,意为这类数据可以不关注,可以物理删除,但该功能还是用逻辑删除。 + * + * @return + */ + private BiConsumer> getTransactionRollbackHandler() { + return (event, context) -> { + log.info("mq_transaction_rollback: {}, uniqueId: {}", event.getShardingKey(), event.getEventId()); + }; + } + + @Bean(WORKFLOW_ENGINE_STARTER_EVENT_HANDLER_REPOSITORY_BEAN_NAME) + @ConditionalOnMissingBean(name = WORKFLOW_ENGINE_STARTER_EVENT_HANDLER_REPOSITORY_BEAN_NAME) + public EventHandlerRepository workflowEngineStarterEventHandlerRepository() { + return new EventHandlerRepository((ex, logText) -> { + log.warn("MQ event handle repository has exception record: {}", logText, ex); + if (Objects.nonNull(ex)) { + throw new WorkflowEngineStarterException(ex); + } + }); + } + + @Bean(WORKFLOW_ENGINE_STARTER_EVENT_CONSUMER_BEAN_NAME) + @ConditionalOnMissingBean(name = WORKFLOW_ENGINE_STARTER_EVENT_CONSUMER_BEAN_NAME) + public EventConsumer workflowEngineStarterEventConsumer(@Qualifier(WORKFLOW_ENGINE_STARTER_EVENT_HANDLER_REPOSITORY_BEAN_NAME) EventHandlerRepository workflowEngineStarterEventHandlerRepository) { + Consumer callback = eventWrapper -> { + if (eventWrapper.isHandled()) { + // 只收集被App真正消费的消息. + Event event = eventWrapper.getEvent(); + log.info("WorkflowEngineStarter RPC MQ, handled event: {}", event.toPrettyJsonString()); + } + }; + return new DefaultEventConsumer(applicationName + MODULE_NAME_SUFFIX, workflowEngineStarterEventHandlerRepository, callback); + } + + @Component + @Conditional(NonContainerEnvironmentCondition.class) + @RocketMQMessageListener(topic = DEFAULT_EVENT + "${spring.profiles.active}", + consumerGroup = "GID_${spring.application.name}_workflow_engine_starter_${GID_SEGMENT}_consumer", + consumeMode = ConsumeMode.CONCURRENTLY, + maxReconsumeTimes = 7, // 发布时需调整为 7, 总共耗时在 15min 内, 目前容器为滚动发布,一版不会出现停机这么久 + nameServer = "${rocketmq.name-server}" + ) + public static class WorkflowEngineStarterRetryConsumer extends BaseListener implements RocketMQListener, InitializingBean { + private final Logger log = LoggerFactory.getLogger(WorkflowEngineStarterRetryConsumer.class); + + @Resource(name = WORKFLOW_ENGINE_STARTER_EVENT_CONSUMER_BEAN_NAME) + private EventConsumer workflowEngineStarterEventConsumer; + @Value("${spring.application.name}") + private String applicationName; + @Resource + private WorkflowEngineStarterProperties starterProperties; + private InnerMessageQueueHandleBeforeFilter filter; + + @Override + public void onMessage(MessageExt message) { + if (filter.doFilter(message)) { + log.info("【{}】message has been filtered, messageId: {}", this.getClass().getSimpleName(), message.getMsgId()); + return; + } + super.onEvent(message, workflowEngineStarterEventConsumer); + } + + @Override + public void afterPropertiesSet() { + this.filter = new InnerFilterMQOwnerShip(starterProperties, applicationName); + } + } + + @Bean("workflowEngineClientRetryEventListener") + public WorkflowEngineStarterRetryEventListener workflowEngineClientRetryEventListener(@Qualifier(WORKFLOW_ENGINE_STARTER_EVENT_CONSUMER_BEAN_NAME) EventConsumer workflowEngineStarterEventConsumer, + Environment environment, + WorkflowEngineStarterProperties starterProperties, + ObjectProvider workflowCoreServiceObjectProvider, + ObjectProvider workflowManageServiceObjectProvider) { + return new WorkflowEngineStarterRetryEventListener(workflowEngineStarterEventConsumer, environment, starterProperties, + workflowCoreServiceObjectProvider, workflowManageServiceObjectProvider); + } + +} 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 new file mode 100644 index 000000000..232c50771 --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/WorkflowEngineStarterAutoConfiguration.java @@ -0,0 +1,150 @@ +package cn.axzo.workflow.starter; + +import cn.axzo.workflow.starter.common.enums.FailHandleTypeEnum; +import cn.axzo.workflow.starter.common.exception.WorkflowUnsupportedException; +import cn.axzo.workflow.starter.handler.MessageNotificationEventHandler; +import cn.axzo.workflow.starter.handler.ProcessActivityEventHandler; +import cn.axzo.workflow.starter.handler.ProcessInstanceEventHandler; +import cn.axzo.workflow.starter.handler.ProcessTaskEventHandler; +import cn.axzo.workflow.starter.handler.execute.ListenerExecutor; +import cn.axzo.workflow.starter.handler.execute.interceptor.ExecuteInterceptor; +import cn.axzo.workflow.starter.handler.execute.interceptor.ExecutorInvoker; +import cn.axzo.workflow.starter.handler.execute.interceptor.FailFastInterceptor; +import cn.axzo.workflow.starter.handler.execute.interceptor.FailOverInterceptor; +import cn.axzo.workflow.starter.handler.execute.interceptor.LogInterceptor; +import cn.axzo.workflow.starter.handler.filter.MessageNotificationEventFilter; +import cn.axzo.workflow.starter.handler.filter.ProcessActivityEventFilter; +import cn.axzo.workflow.starter.handler.filter.ProcessInstanceEventFilter; +import cn.axzo.workflow.starter.handler.filter.ProcessTaskEventFilter; +import cn.axzo.workflow.starter.handler.monitor.BroadcastDLQReporter; +import cn.axzo.workflow.starter.handler.monitor.RpcDLQReporter; +import cn.axzo.workflow.starter.mq.broadcast.consumer.InnerActivityEventListener; +import cn.axzo.workflow.starter.mq.broadcast.consumer.InnerInstanceEventListener; +import cn.axzo.workflow.starter.mq.broadcast.consumer.InnerNotificationEventListener; +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 org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.env.Environment; + +import java.util.ArrayList; +import java.util.List; + +/** + * Workflow Engine Auto Configuration + * + * @author wangli + * @since 2024/5/21 11:47 + */ +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(WorkflowEngineStarterProperties.class) +@Import({StarterFeignClientConfiguration.class, StarterBroadcastMQConfiguration.class, StarterRPCInvokeMQConfiguration.class}) +public class WorkflowEngineStarterAutoConfiguration { + + private final Logger log = LoggerFactory.getLogger(WorkflowEngineStarterAutoConfiguration.class); + + @Bean + public ListenerExecutor listenerExecutor(WorkflowEngineStarterProperties starterProperties, + List additionalInterceptors) { + List interceptors = new ArrayList<>(); + interceptors.add(new LogInterceptor()); + interceptors.add(getFailInterceptor(starterProperties)); + // TODO +// if (!CollectionUtils.isEmpty(additionalInterceptors)) { +// additionalInterceptors.forEach(interceptor -> interceptor.setNext(interceptor)); +// } + interceptors.add(new ExecutorInvoker()); + return new ListenerExecutor(interceptors); + } + + @Bean + public InnerWorkflowListener innerProcessEventListener(ListenerExecutor executor, + ObjectProvider> handlerProvider, + ObjectProvider> filterProvider) { + return new InnerInstanceEventListener(executor, handlerProvider, filterProvider); + } + + @Bean + public InnerWorkflowListener innerActivityEventListener(ListenerExecutor executor, + ObjectProvider> handlerProvider, + ObjectProvider> filterProvider) { + return new InnerActivityEventListener(executor, handlerProvider, filterProvider); + } + + @Bean + public InnerWorkflowListener innerTaskEventListener(ListenerExecutor executor, + ObjectProvider> handlerProvider, + ObjectProvider> filterProvider) { + return new InnerTaskEventListener(executor, handlerProvider, filterProvider); + } + + @Bean + public InnerWorkflowListener innerNotificationEventListener(ListenerExecutor executor, + ObjectProvider> handlerProvider, + ObjectProvider> filterProvider) { + return new InnerNotificationEventListener(executor, handlerProvider, filterProvider); + } + + private ExecuteInterceptor getFailInterceptor(WorkflowEngineStarterProperties starterProperties) { + BroadcastListenerProperties listenerRetry = starterProperties.getBroadcast(); + FailHandleTypeEnum failHandleType = listenerRetry.getFailHandleType(); + log.info("workflow engine starter fail handle type : {}", failHandleType); + switch (failHandleType) { + case FAIL_BACK: +// return new FailBackInterceptor(); + throw new WorkflowUnsupportedException("暂不支持该模式, 请调整 MQ 处理失败策略, workflow.engine.starter.broadcast.fail-handle-type"); + case FAIL_FAST: + return new FailFastInterceptor(); + case FAIL_OVER: + default: + return new FailOverInterceptor(starterProperties.getBroadcast()); + } + } + + @Bean(destroyMethod = "shutdown", name = "defaultMQAdminExt") + @ConditionalOnProperty(prefix = "workflow.engine.starter", value = "enable-dlq-monitor", havingValue = "true", matchIfMissing = true) + public DefaultMQAdminExt defaultMQAdminExt(Environment environment) { + String namesrvAddress = environment.getProperty("rocketmq.name-server"); + if (StringUtils.isBlank(namesrvAddress)) { + log.error("Build DefaultMQAdminExt error, namesrv is null"); + throw new RuntimeException("Build DefaultMQAdminExt error, namesrv is null", null); + } + DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt((RPCHook) null, 5000L); + defaultMQAdminExt.setInstanceName("workflow-engine-starter-" + System.currentTimeMillis()); + defaultMQAdminExt.setNamesrvAddr(namesrvAddress); + try { + defaultMQAdminExt.start(); + } catch (MQClientException ex) { + log.error(String.format("init default admin error, namesrv=%s", System.getProperty(MixAll.NAMESRV_ADDR_PROPERTY)), ex); + } + return defaultMQAdminExt; + } + + @Bean + public WorkflowEngineStarterMQMonitorController workflowEngineStarterMQMonitorController() { + return new WorkflowEngineStarterMQMonitorController(); + } + + @Bean + @ConditionalOnProperty(prefix = "workflow.engine.starter", value = "enable-dlq-monitor", havingValue = "true", matchIfMissing = true) + public WorkflowEngineStarterDefaultMQMonitor workflowEngineStarterDefaultMQMonitor(ObjectProvider mqAdminExtObjectProvider, + ObjectProvider broadcastDLQProcessorObjectProvider, + ObjectProvider rpcDLQProcessorObjectProvider, + WorkflowEngineStarterProperties workflowEngineStarterProperties, + 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 new file mode 100644 index 000000000..584d75484 --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/WorkflowEngineStarterProperties.java @@ -0,0 +1,158 @@ +package cn.axzo.workflow.starter; + +import cn.axzo.workflow.common.annotation.InvokeMode; +import cn.axzo.workflow.common.enums.RpcInvokeModeEnum; +import cn.axzo.workflow.starter.api.WorkflowCoreService; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; + +import static cn.axzo.workflow.common.enums.RpcInvokeModeEnum.ASYNC; + +/** + * Workflow Engine Starter Properties + *

+ * 全量参数参考:META-INF/application.yml.demo + * + * @author wangli + * @since 2024/5/21 15:24 + */ +@ConfigurationProperties(prefix = "workflow.engine.starter") +public class WorkflowEngineStarterProperties { + /** + * 特殊用途,不建议接入方使用 + */ + private Boolean manageable = false; + + /** + *

该参数只针对容器环境生效

+ * 本地启动时,是否将本地的 MQ 消费者加入集群消费组 + *
+     * 默认 false, 本地启动应用时, 将创建消息组名称中含有"debugging"的消费组.
+     * 否则, 本地启动应用时, 消费者将加入容器环境, 进行集群消费.
+     * 
+ */ + private Boolean joinContainerGroup = false; + + /** + *

该参数只针对容器环境生效

+ * 配合 joinContainerGroup 使用,且只在 joinContainerGroup = false 时生效 + *
+     * 在本地有多台开发机同时启动时,又会组成新的集群消费,也会导致消息异常消费,
+     * 所以该参数就是为了创建完全唯一的消费者,避免本地开发机组成集群。
+     * 
+ */ + private String specialId; + + /** + * WorkflowCoreService 类中所有方法未标记{@link InvokeMode}注解的方法调用时, 默认采用的模式 + * + *
+     * 如果是同步调用,则直接通过普通 FeignClient 进行调用,
+     * 否则将通过 MQ 将 RPC 调用进行解耦
+     * 
+ *

+ * 如果方法上有{@link InvokeMode}注解, 则以注解上的模式优先, 如果还想覆盖注解中的模式, + * 则可以通过 {@link WorkflowCoreService#sync()}或{@link WorkflowCoreService#async()}方法进行覆盖 + */ + private RpcInvokeModeEnum invokeMode = ASYNC; + + /** + * 监听流程引擎广播的处理器,异常后的重试相关策略及配置 + */ + @NestedConfigurationProperty + private BroadcastListenerProperties broadcast = new BroadcastListenerProperties(); + + /** + * 是否开启死信队列的监控的特性,不代表真实开始监控,如果需要默认开始,请设置 monitorStatus = true + */ + private Boolean enableDlqMonitor = true; + + /** + * 监控状态,默认 true,真实开始监控 RPC 的私信队列监控 + */ + private Boolean rpcMonitorStatus = true; + + /** + * 监控死信队列,周期间隔,单位:毫秒 + */ + private long dlqMonitorIntervalInMs = 4 * 60 * 60 * 1000L; + + /** + * 开启后,会在主动给“工作流小分队”群中发送信息 + *

+ * 只针对容器环境中的应用生效 + */ + private Boolean alert = false; + + public Boolean getManageable() { + return manageable; + } + + public void setManageable(Boolean manageable) { + this.manageable = manageable; + } + + public Boolean getJoinContainerGroup() { + return joinContainerGroup; + } + + public void setJoinContainerGroup(Boolean joinContainerGroup) { + this.joinContainerGroup = joinContainerGroup; + } + + public String getSpecialId() { + return specialId; + } + + public void setSpecialId(String specialId) { + this.specialId = specialId; + } + + public RpcInvokeModeEnum getInvokeMode() { + return invokeMode; + } + + public void setInvokeMode(RpcInvokeModeEnum invokeMode) { + this.invokeMode = invokeMode; + } + + public BroadcastListenerProperties getBroadcast() { + return broadcast; + } + + public void setBroadcast(BroadcastListenerProperties broadcast) { + this.broadcast = broadcast; + } + + public Boolean getEnableDlqMonitor() { + return enableDlqMonitor; + } + + public void setEnableDlqMonitor(Boolean enableDlqMonitor) { + this.enableDlqMonitor = enableDlqMonitor; + } + + public Boolean getRpcMonitorStatus() { + return rpcMonitorStatus; + } + + public void setRpcMonitorStatus(Boolean rpcMonitorStatus) { + this.rpcMonitorStatus = rpcMonitorStatus; + } + + public long getDlqMonitorIntervalInMs() { + return dlqMonitorIntervalInMs; + } + + public void setDlqMonitorIntervalInMs(long dlqMonitorIntervalInMs) { + this.dlqMonitorIntervalInMs = dlqMonitorIntervalInMs; + } + + public Boolean getAlert() { + return alert; + } + + public void setAlert(Boolean alert) { + this.alert = alert; + } +} 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 new file mode 100644 index 000000000..f7d7aefdf --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/api/WorkflowCoreService.java @@ -0,0 +1,294 @@ +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.config.CommonFeignConfiguration; +import cn.axzo.workflow.common.model.request.bpmn.task.BpmnActivitySetAssigneeDTO; +import cn.azxo.framework.common.model.CommonResponse; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.cloud.openfeign.FeignClient; +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.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.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.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.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; + +/** + * Workflow Engine Starter Core Service
该类是根据 API 动态生成,不同版本可能会开放新的接口,或回收一些旧接口 + */ +@FeignClient(name = "workflow-engine-starter-core", url = "${axzo.service.workflow-engine:workflow-engine:8080}", configuration = WorkflowEngineStarterFeignConfiguration.class) +public interface WorkflowCoreService { + + /** + * 业务节点唤醒 + *

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

+ * 当模型中使用了“业务节点”,且设置了“业务指定审批人”模式,则当业务监听到 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); + + /** + * 同意 + * + *
+     * 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); + + /** + * 驳回 + * + *
+     * 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 new file mode 100644 index 000000000..78809f2da --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/api/WorkflowManageService.java @@ -0,0 +1,713 @@ +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.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.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.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.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.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.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; + +/** + * Workflow Engine Starter Management Service
该类是根据 API 动态生成,不同版本可能会开放新的接口,或回收一些旧接口 + */ +@FeignClient(name = "workflow-engine-starter-manage", url = "${axzo.service.workflow-engine: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/common/condition/NonContainerEnvironmentCondition.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/common/condition/NonContainerEnvironmentCondition.java new file mode 100644 index 000000000..153aafe0c --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/common/condition/NonContainerEnvironmentCondition.java @@ -0,0 +1,65 @@ +package cn.axzo.workflow.starter.common.condition; + +import cn.axzo.workflow.starter.WorkflowEngineStarterProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.util.StringUtils; + +import java.util.Objects; + +import static cn.axzo.workflow.common.constant.StarterConstants.DEBUGGING_MQ_SUFFIX; +import static cn.axzo.workflow.common.constant.StarterConstants.K8S_POD_NAME_SPACE; +import static cn.axzo.workflow.common.constant.StarterConstants.MQ_GID_NAME_SEGMENT; +import static cn.axzo.workflow.common.constant.StarterConstants.NACOS_PROFILES_ACTIVE; + +/** + * 用于处理 MQ 的消费者, 在本地启动或在容器中启动时, 能自主控制是否并入统一的消费组 + *

+ * 可查看 {@link WorkflowEngineStarterProperties#joinContainerGroup} 属性, 来了解本类的用途, + * 特别需要注意的是: Starter 是结合 K8S 的命名空间(namespace) 来处理的. + * + * @author wangli + * @since 2024/5/30 22:19 + */ +public class NonContainerEnvironmentCondition implements Condition { + + private static final Logger log = LoggerFactory.getLogger(NonContainerEnvironmentCondition.class); + + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + ConfigurableEnvironment environment = (ConfigurableEnvironment) context.getEnvironment(); + // 依赖 K8S 设置的环境变量信息,如果变量的 key 发生变化,会导致此处的功能可能出现异常 + String myPodNamespace = environment.getProperty(K8S_POD_NAME_SPACE); + String activeProfile = environment.getProperty(NACOS_PROFILES_ACTIVE); + if (!StringUtils.hasText(activeProfile)) { + activeProfile = environment.getProperty("spring.profiles.active", String.class); + } + // 在容器环境时, 强制加入集群消费组 + if (StringUtils.hasText(myPodNamespace)) { + environment.getSystemProperties().put(MQ_GID_NAME_SEGMENT, activeProfile); + return true; + } + + // 优先外部化配置 + Boolean joinContainerGroup = environment.getProperty("workflow.engine.starter.join-container-group", Boolean.class); + if (Objects.isNull(joinContainerGroup)) { + // 获取默认值 + joinContainerGroup = new WorkflowEngineStarterProperties().getJoinContainerGroup(); + } + if (log.isDebugEnabled()) { + log.debug("workflow engine starter join-container-group status: {} ", joinContainerGroup); + } + + String specialId = environment.getProperty("workflow.engine.starter.special-id", String.class); + environment.getSystemProperties().put(MQ_GID_NAME_SEGMENT, + joinContainerGroup ? activeProfile : + activeProfile + (StringUtils.hasText(specialId) ? "_" + specialId : "") + DEBUGGING_MQ_SUFFIX); + + return true; + } + +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/common/enums/FailHandleTypeEnum.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/common/enums/FailHandleTypeEnum.java new file mode 100644 index 000000000..0f6d64e32 --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/common/enums/FailHandleTypeEnum.java @@ -0,0 +1,23 @@ +package cn.axzo.workflow.starter.common.enums; + + +import lombok.Getter; + +/** + * 异常处理策略,执行客户端自定义Listener异常处理策略,默认未FAIL_OVER + */ +@Getter +public enum FailHandleTypeEnum { + FAIL_OVER("fail_over", "当前listener执行出错,忽略继续往下执行,可配置重试相关参数,不抛出异常"), + FAIL_FAST("fail_fast", "快速失败,出错直接抛出异常,listener不再往下执行"), + FAIL_BACK("fail_back", "失败自动恢复,在后台记录失败的消息,并按照一定的策略后期再进行重试,目前暂不支持"), + ; + private final String code; + private final String description; + + FailHandleTypeEnum(String code, String description) { + this.code = code; + this.description = description; + } + +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/common/exception/WorkflowEngineStarterException.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/common/exception/WorkflowEngineStarterException.java new file mode 100644 index 000000000..cf4244523 --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/common/exception/WorkflowEngineStarterException.java @@ -0,0 +1,22 @@ +package cn.axzo.workflow.starter.common.exception; + +/** + * 流程引擎 starter 的异常类 + * + * @author wangli + * @since 2024/5/21 17:57 + */ +public class WorkflowEngineStarterException extends RuntimeException { + + public WorkflowEngineStarterException(Throwable cause) { + super(cause); + } + + public WorkflowEngineStarterException(String message) { + super(message); + } + + public WorkflowEngineStarterException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/common/exception/WorkflowListenerExecutionException.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/common/exception/WorkflowListenerExecutionException.java new file mode 100644 index 000000000..163dccd2e --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/common/exception/WorkflowListenerExecutionException.java @@ -0,0 +1,13 @@ +package cn.axzo.workflow.starter.common.exception; + +public class WorkflowListenerExecutionException extends WorkflowEngineStarterException { + + public WorkflowListenerExecutionException(String message) { + super(message); + } + + public WorkflowListenerExecutionException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/common/exception/WorkflowNoMethodException.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/common/exception/WorkflowNoMethodException.java new file mode 100644 index 000000000..b4a7ffba8 --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/common/exception/WorkflowNoMethodException.java @@ -0,0 +1,17 @@ +package cn.axzo.workflow.starter.common.exception; + +/** + * 特殊的 invoke 逻辑中,没有特定的方法异常 + * + * @author wangli + * @since 2024/6/14 13:35 + */ +public class WorkflowNoMethodException extends WorkflowEngineStarterException { + public WorkflowNoMethodException(String message) { + super(message); + } + + public WorkflowNoMethodException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/common/exception/WorkflowRpcInvokeException.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/common/exception/WorkflowRpcInvokeException.java new file mode 100644 index 000000000..4ffde34dc --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/common/exception/WorkflowRpcInvokeException.java @@ -0,0 +1,21 @@ +package cn.axzo.workflow.starter.common.exception; + +/** + * Starter 的 RPC 动作调用异常 + * + * @author wangli + * @since 2024/6/18 14:20 + */ +public class WorkflowRpcInvokeException extends WorkflowEngineStarterException { + public WorkflowRpcInvokeException(Throwable cause) { + super(cause); + } + + public WorkflowRpcInvokeException(String message) { + super(message); + } + + public WorkflowRpcInvokeException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/common/exception/WorkflowUnsupportedException.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/common/exception/WorkflowUnsupportedException.java new file mode 100644 index 000000000..09bda2345 --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/common/exception/WorkflowUnsupportedException.java @@ -0,0 +1,18 @@ +package cn.axzo.workflow.starter.common.exception; + +/** + * 不支持的操作的异常类 + * + * @author wangli + * @since 2024/6/12 15:25 + */ +public class WorkflowUnsupportedException extends WorkflowEngineStarterException { + + public WorkflowUnsupportedException(String message) { + super(message); + } + + public WorkflowUnsupportedException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/feign/ext/ComplexInvokeClient.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/feign/ext/ComplexInvokeClient.java new file mode 100644 index 000000000..017237e0e --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/feign/ext/ComplexInvokeClient.java @@ -0,0 +1,245 @@ +package cn.axzo.workflow.starter.feign.ext; + +import cn.axzo.framework.rocketmq.EventProducer; +import cn.axzo.workflow.common.enums.RpcInvokeModeEnum; +import cn.axzo.workflow.common.model.response.mq.WorkflowEngineStarterRpcInvokeDTO; +import cn.axzo.workflow.starter.WorkflowEngineStarterProperties; +import cn.axzo.workflow.starter.common.exception.WorkflowEngineStarterException; +import cn.axzo.workflow.starter.mq.retry.producer.RpcInvokeEventProducer; +import cn.azxo.framework.common.model.CommonResponse; +import com.alibaba.fastjson.JSON; +import feign.Client; +import feign.MethodMetadata; +import feign.Request; +import feign.Response; +import lombok.SneakyThrows; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.util.CollectionUtils; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +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 java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Queue; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static cn.axzo.workflow.common.enums.RpcInvokeModeEnum.SYNC; +import static cn.axzo.workflow.common.enums.WorkflowEngineEventEnum.WORKFLOW_ENGINE_STARTER; +import static cn.axzo.workflow.common.constant.StarterConstants.STARTER_INVOKE_MODE; +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * 适用于 Starter 中复合型的 FeignClient 实现 + *

+ * 如果使用方调用的服务方法是同步,则使用原生的 FeignClient 实现, 否则通过 MQ 事件解耦请求 + * + * @author wangli + * @since 2024/5/28 15:23 + */ +public class ComplexInvokeClient implements Client { + + private final Logger log = LoggerFactory.getLogger(ComplexInvokeClient.class); + private final WorkflowEngineStarterProperties starterProperties; + private final RpcInvokeEventProducer eventProducer; + private final Client feignClient; + + public ComplexInvokeClient(WorkflowEngineStarterProperties starterProperties, + EventProducer eventProducer, + Client feignClient) { + this.starterProperties = starterProperties; + this.eventProducer = (RpcInvokeEventProducer) eventProducer; + this.feignClient = feignClient; + } + + @Override + public Response execute(Request request, Request.Options options) throws IOException { + log.debug("ComplexInvokeClient execute... Url: {}", request.url()); + RpcInvokeModeEnum currentInvokeModeEnum = getInvokeMode(request); + Map> headers = request.headers(); + headers.forEach((k, v) -> log.debug("ComplexInvokeClient Header: {} = {}", k, v)); + + log.debug("[{}] invoke url: {}", currentInvokeModeEnum, request.url()); + if (Objects.equals(SYNC, currentInvokeModeEnum)) { + return feignClient.execute(request, options); + } + asyncInvoke(request); + + return Response.builder() + .status(HttpStatus.OK.value()) + .reason(HttpStatus.OK.getReasonPhrase()) + .headers(headers) + .request(request) + .body(body) + .build(); + } + + /** + * 发送 RPC 调用动作的 MQ 事件 + * + * @param request + */ + private void asyncInvoke(Request request) { + WorkflowEngineStarterRpcInvokeDTO event = new WorkflowEngineStarterRpcInvokeDTO(); + MethodMetadata metadata = request.requestTemplate().methodMetadata(); + event.setClassName(metadata.targetType().getName()); + event.setMethodName(metadata.method().getName()); + + List args = new ArrayList<>(); + event.setParameters(args); + buildArgs(request, metadata, args); + log.debug("[async-invoke] sourceEvent: {}", JSON.toJSONString(event)); + eventProducer.send(WORKFLOW_ENGINE_STARTER, event); + } + + @SneakyThrows + private void buildArgs(Request request, MethodMetadata metadata, List args) { + Parameter[] parameters = metadata.method().getParameters(); + Annotation[][] parameterAnnotations = metadata.method().getParameterAnnotations(); + Map> queries = request.requestTemplate().queries(); + byte[] body = request.requestTemplate().body(); + + // 提前解析出 PathVariable 变量,避免后续循环时再解析 + Map pathVariableMap = parsePathVariables(getRequestMappingsValue(metadata.method()), getUrlRemoveDomain(request)); + Queue pathVariableQueue = convertMapValueToQueue(pathVariableMap); + for (int i = 0; i < parameters.length; i++) { + // 目前工作流的所有 API,只用了 RequestParam / RequestBody / PathVariable 三个注解,所以这里只处理这三种 + Parameter parameter = parameters[i]; + Annotation[] annotations = parameterAnnotations[i]; + for (Annotation annotation : annotations) { + if (annotation instanceof RequestParam) { + // RequestParam 支持 Nullable + List values = (List) queries.getOrDefault(parameter.getName(), Collections.emptyList()); + args.add(JSON.toJSONString(values)); + } else if (annotation instanceof PathVariable) { + args.add(pathVariableQueue.poll()); + } else if (annotation instanceof RequestBody) { + args.add(new String(body, UTF_8)); + } + } + } + } + + private String getRequestMappingsValue(Method method) { + Annotation[] annotations = method.getAnnotations(); + for (Annotation annotation : annotations) { + if (annotation instanceof GetMapping) { + GetMapping getMapping = (GetMapping) annotation; + return getMapping.value()[0]; + } else if (annotation instanceof PostMapping) { + PostMapping postMapping = (PostMapping) annotation; + return postMapping.value()[0]; + } else if (annotation instanceof PutMapping) { + PutMapping putMapping = (PutMapping) annotation; + return putMapping.value()[0]; + } else if (annotation instanceof DeleteMapping) { + DeleteMapping deleteMapping = (DeleteMapping) annotation; + return deleteMapping.value()[0]; + } + } + if (log.isDebugEnabled()) { + log.debug("{} not found request mappings", method.getName()); + } + return ""; + } + + private String getUrlRemoveDomain(Request request) { + return request.url().replace(request.requestTemplate().feignTarget().url(), ""); + } + + private Queue convertMapValueToQueue(Map map) { + Queue queue = new LinkedList<>(); + if (CollectionUtils.isEmpty(map)) { + return queue; + } + map.forEach((k, v) -> queue.add(v)); + return queue; + } + + private Map parsePathVariables(String template, String actualPath) { + Map variables = new HashMap<>(); + + // 创建一个正则表达式,用于匹配模板中的占位符 + String regex = template.replaceAll("\\{([^}]+)\\}", "([^/]+)"); + regex = regex.replaceAll("/+", "/"); // 去除可能由于连续占位符产生的多余斜杠 + regex = "^" + regex + "$"; // 锚定整个字符串的匹配 + + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(actualPath); + + if (matcher.find()) { + // 提取占位符名称和对应的值 + Pattern variablePattern = Pattern.compile("\\{([^}]+)\\}"); + Matcher variableMatcher = variablePattern.matcher(template); + int index = 1; // 匹配值的索引从1开始(0是整个匹配的字符串) + while (variableMatcher.find()) { + variables.put(variableMatcher.group(1), matcher.group(index++)); + } + } + + return variables; + } + + private RpcInvokeModeEnum getInvokeMode(Request request) { + Collection invokeModel = request.headers().getOrDefault(STARTER_INVOKE_MODE, + Collections.singletonList(starterProperties.getInvokeMode().name())); + if (CollectionUtils.isEmpty(invokeModel)) { + return starterProperties.getInvokeMode(); + } else if (invokeModel.size() > 1) { + throw new WorkflowEngineStarterException("Multiple invoke mode is not supported"); + } + return RpcInvokeModeEnum.valueOf(invokeModel.iterator().next()); + } + + static Response.Body body = new Response.Body() { + final ByteArrayInputStream inputStream = new ByteArrayInputStream(JSON.toJSONString(CommonResponse.success(HttpStatus.OK.value(), "Send MQ Success", null)) + .getBytes(UTF_8)); + + @Override + public Integer length() { + return null; + } + + @Override + public boolean isRepeatable() { + return false; + } + + @Override + public InputStream asInputStream() throws IOException { + return inputStream; + } + + @Override + public Reader asReader(Charset charset) throws IOException { + return new InputStreamReader(asInputStream(), charset); + } + + @Override + public void close() throws IOException { + inputStream.close(); + } + }; +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/feign/ext/WorkflowEngineStarterDecoder.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/feign/ext/WorkflowEngineStarterDecoder.java new file mode 100644 index 000000000..a6aa43f3c --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/feign/ext/WorkflowEngineStarterDecoder.java @@ -0,0 +1,87 @@ +package cn.axzo.workflow.starter.feign.ext; + +import cn.axzo.workflow.common.model.response.BpmPageResult; +import cn.axzo.workflow.starter.common.exception.WorkflowEngineStarterException; +import cn.axzo.workflow.starter.common.exception.WorkflowRpcInvokeException; +import cn.azxo.framework.common.model.CommonResponse; +import com.google.common.collect.Lists; +import feign.Response; +import feign.Util; +import feign.codec.Decoder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl; + +import java.io.IOException; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +/** + * Workflow Engine Starter Complex Invoke Client Decoder + * + * @author wangli + * @since 2024/5/31 14:55 + */ +final class WorkflowEngineStarterDecoder implements Decoder { + private final Logger log = LoggerFactory.getLogger(WorkflowEngineStarterDecoder.class); + final Decoder delegate; + + public WorkflowEngineStarterDecoder(Decoder delegate) { + Objects.requireNonNull(delegate, "Decoder must not be null. "); + this.delegate = delegate; + } + + @Override + public Object decode(Response response, Type type) throws IOException { + if (!isOptional(type)) { + return convert(response, type); + } + if (response.status() == 404 || response.status() == 204) { + return Optional.empty(); + } + Type enclosedType = Util.resolveLastTypeParameter(type, Optional.class); + return Optional.ofNullable(convert(response, enclosedType)); + } + + static boolean isOptional(Type type) { + if (!(type instanceof ParameterizedType)) { + return false; + } + ParameterizedType parameterizedType = (ParameterizedType) type; + return parameterizedType.getRawType().equals(Optional.class); + } + + /** + * 这里做返回数据的解析,并处理引擎返回的一些正常的业务异常 + * + * @param response + * @param type + * @return + */ + Object convert(Response response, Type type) throws IOException { + ParameterizedTypeImpl wrappedType; + List> cls = Lists.newArrayList(BpmPageResult.class, Collection.class, List.class, Map.class); + if (type instanceof ParameterizedType && !cls.contains(((ParameterizedType) type).getRawType())) { + wrappedType = (ParameterizedTypeImpl) type; + } else { + wrappedType = ParameterizedTypeImpl.make(CommonResponse.class, new Type[]{type}, null); + } + Object decode = delegate.decode(response, wrappedType); + if (decode instanceof CommonResponse) { + CommonResponse commonResponse = (CommonResponse) decode; + if (response.status() == 202) { + log.warn("workflow engine starter rpc invoke return msg: {}", commonResponse.getMsg()); + throw new WorkflowRpcInvokeException(commonResponse.getMsg()); + } else if (response.status() != 200) { + log.error("WorkflowEngineStarterDecoder has error, status:{} msg: {}", response.status(), commonResponse.getMsg()); + } + return commonResponse.getData(); + } + return decode; + } +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/feign/ext/WorkflowEngineStarterFeignConfiguration.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/feign/ext/WorkflowEngineStarterFeignConfiguration.java new file mode 100644 index 000000000..19a406cb3 --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/feign/ext/WorkflowEngineStarterFeignConfiguration.java @@ -0,0 +1,132 @@ +package cn.axzo.workflow.starter.feign.ext; + +import cn.axzo.framework.rocketmq.EventProducer; +import cn.axzo.workflow.common.enums.RpcInvokeModeEnum; +import cn.axzo.workflow.common.util.ThreadUtil; +import cn.axzo.workflow.starter.WorkflowEngineStarterProperties; +import cn.azxo.framework.common.constatns.Constants; +import feign.Client; +import feign.RequestInterceptor; +import feign.RequestTemplate; +import feign.Retryer; +import feign.Target; +import feign.codec.Decoder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.http.HttpMessageConverters; +import org.springframework.cloud.openfeign.FeignBuilderCustomizer; +import org.springframework.cloud.openfeign.support.ResponseEntityDecoder; +import org.springframework.cloud.openfeign.support.SpringDecoder; +import org.springframework.context.annotation.Bean; +import org.springframework.core.env.Environment; +import org.springframework.util.CollectionUtils; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; +import java.util.Collection; +import java.util.Collections; +import java.util.Objects; + +//import static cn.axzo.framework.web.filter.BasicRecordExceptionFilter.MICRO_SERVER_RECORD_ERROR_GET_PARAM_NAME; +import static cn.axzo.workflow.client.config.WorkflowRequestInterceptor.HEADER_API_VERSION; +import static cn.axzo.workflow.client.config.WorkflowRequestInterceptor.HEADER_HTTP_CLIENT; +import static cn.axzo.workflow.client.config.WorkflowRequestInterceptor.HEADER_HTTP_CLIENT_VALUE; +import static cn.axzo.workflow.client.config.WorkflowRequestInterceptor.HEADER_SERVER_NAME; +import static cn.axzo.workflow.common.constant.StarterConstants.ENABLE_MANAGEABLE; +import static cn.axzo.workflow.common.constant.StarterConstants.STARTER_INVOKE_MODE; +import static java.util.concurrent.TimeUnit.SECONDS; + +/** + * Starter Feign Client 的自定义的配置 + * + * @author wangli + * @since 2024/5/28 23:17 + */ +public class WorkflowEngineStarterFeignConfiguration { + private final Logger log = LoggerFactory.getLogger(WorkflowEngineStarterFeignConfiguration.class); + + @Bean + public Retryer defaultRetryer() { + return new Retryer.Default(100, SECONDS.toMillis(1), 3); + } + + @Bean + public Client complexInvokeClient(WorkflowEngineStarterProperties starterProperties, + @Qualifier("workflowEngineStarterEventProducer") EventProducer eventProducer, + Client feignClient) { + return new ComplexInvokeClient(starterProperties, eventProducer, feignClient); + } + + @Bean + public Decoder workflowEngineStarterDecoder(ObjectFactory messageConverters) { + return new WorkflowEngineStarterDecoder(new ResponseEntityDecoder(new SpringDecoder(messageConverters))); + } + + @Bean + public RequestInterceptor workflowEngineStarterRequestInterceptor(WorkflowEngineStarterProperties starterProperties, + Environment environment, + String serviceVersion) { + return template -> { + // 接入应用上报 + template.header(HEADER_SERVER_NAME, environment.getProperty("spring.application.name")); + resetInvokeMode(starterProperties, template); + // 通过服务端的校验 + Target.HardCodedTarget target = (Target.HardCodedTarget) template.feignTarget(); + String apiClassPath = target.type().getName(); + if (apiClassPath.contains("cn.axzo.workflow.starter.api")) { + template.header(HEADER_HTTP_CLIENT, HEADER_HTTP_CLIENT_VALUE); + template.header(HEADER_API_VERSION, serviceVersion); + } + //链路追踪 + template.header(Constants.CTX_LOG_ID_MDC, MDC.get(Constants.CTX_LOG_ID_MDC)); + template.header(ENABLE_MANAGEABLE, starterProperties.getManageable().toString()); + HttpServletRequest originalRequest = getOriginalRequest(); + if (Objects.nonNull(originalRequest)) { + setRequestParams(template, originalRequest); + } + }; + } + + private void setRequestParams(RequestTemplate template, HttpServletRequest originalRequest) { + String MICRO_SERVER_RECORD_ERROR_GET_PARAM_NAME = "pkg"; + String[] packageNames = originalRequest.getParameterValues(MICRO_SERVER_RECORD_ERROR_GET_PARAM_NAME); + template.query(MICRO_SERVER_RECORD_ERROR_GET_PARAM_NAME, packageNames); + } + + public HttpServletRequest getOriginalRequest() { + try { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + return attributes.getRequest(); + } catch (Exception e) { + log.debug("not HttpServletRequest instance bean found"); + return null; + } + } + + private void resetInvokeMode(WorkflowEngineStarterProperties starterProperties, RequestTemplate template) { + Collection invokeModeInHeader = template.headers().getOrDefault(STARTER_INVOKE_MODE, Collections.emptyList()); + template.removeHeader(STARTER_INVOKE_MODE); + // 接口的同步异步模式 + RpcInvokeModeEnum rpcInvokeModeEnum = ThreadUtil.get(); + ThreadUtil.clear(); + if (Objects.isNull(rpcInvokeModeEnum)) { + if (CollectionUtils.isEmpty(invokeModeInHeader)) { + log.debug("Without calling the async or sync methods of the WorkflowCoreService, the default configuration in WorkflowEngineStarterProperties will be loaded."); + rpcInvokeModeEnum = starterProperties.getInvokeMode(); + } else { + rpcInvokeModeEnum = RpcInvokeModeEnum.valueOf(invokeModeInHeader.iterator().next()); + } + } + template.header(STARTER_INVOKE_MODE, rpcInvokeModeEnum.name()); + } + + + @Bean + public FeignBuilderCustomizer workflowEngineFeignBuilderCustomizer() { + return builder -> builder.invocationHandlerFactory(new WorkflowEngineStarterInvocationHandlerFactory()); + } +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/feign/ext/WorkflowEngineStarterInvocationHandler.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/feign/ext/WorkflowEngineStarterInvocationHandler.java new file mode 100644 index 000000000..a3c4ba593 --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/feign/ext/WorkflowEngineStarterInvocationHandler.java @@ -0,0 +1,100 @@ +package cn.axzo.workflow.starter.feign.ext; + +import cn.axzo.workflow.common.annotation.InvokeMode; +import cn.axzo.workflow.common.util.ThreadUtil; +import com.alibaba.fastjson.JSON; +import com.google.common.collect.Lists; +import feign.InvocationHandlerFactory; +import feign.Target; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.StopWatch; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Workflow Engine Starter Core Service Invocation Handler + * + * @author wangli + * @since 2024/5/29 22:58 + */ +class WorkflowEngineStarterInvocationHandler implements InvocationHandler { + + private static final Logger log = LoggerFactory.getLogger(WorkflowEngineStarterInvocationHandler.class); + private final Target target; + private final Map dispatch; + + public WorkflowEngineStarterInvocationHandler(Target target, Map dispatch) { + this.target = target; + this.dispatch = dispatch; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if ("equals".equals(method.getName())) { + try { + Object otherHandler = + args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null; + return equals(otherHandler); + } catch (IllegalArgumentException e) { + return false; + } + } else if ("hashCode".equals(method.getName())) { + return hashCode(); + } else if ("toString".equals(method.getName())) { + return toString(); + } + + parseInvokeModelAnnotation(method); + StopWatch stopWatch = new StopWatch(); + stopWatch.start(method.getName()); + Object invoke = null; + try { + invoke = dispatch.get(method).invoke(args); + } finally { + stopWatch.stop(); + List ignoreMethods = Lists.newArrayList("sync", "async"); + if (!ignoreMethods.contains(method.getName())) { + log.info("Workflow starter Method invoke record: {}.{}, args: {}, result: {} const: {} ms", + target.type().getSimpleName(), method.getName(), JSON.toJSONString(args), JSON.toJSONString(invoke), stopWatch.getTotalTimeMillis()); + } + } + return invoke; + } + + private void parseInvokeModelAnnotation(Method method) { + InvokeMode annotation = AnnotationUtils.getAnnotation(method, InvokeMode.class); + if (Objects.nonNull(annotation)) { + ThreadUtil.set(annotation.value()); + } + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof WorkflowEngineStarterInvocationHandler) { + WorkflowEngineStarterInvocationHandler other = (WorkflowEngineStarterInvocationHandler) obj; + return target.equals(other.target); + } + return false; + } + + @Override + public int hashCode() { + return target.hashCode(); + } + + @Override + public String toString() { + return target.toString(); + } + + public Class getTargetClass() { + return target.getClass(); + } +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/feign/ext/WorkflowEngineStarterInvocationHandlerFactory.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/feign/ext/WorkflowEngineStarterInvocationHandlerFactory.java new file mode 100644 index 000000000..26a133e30 --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/feign/ext/WorkflowEngineStarterInvocationHandlerFactory.java @@ -0,0 +1,21 @@ +package cn.axzo.workflow.starter.feign.ext; + +import feign.InvocationHandlerFactory; +import feign.Target; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.util.Map; + +/** + * 用于生成 InvocationHandler + * + * @author wangli + * @since 2024/5/29 22:56 + */ +public class WorkflowEngineStarterInvocationHandlerFactory implements InvocationHandlerFactory { + @Override + public InvocationHandler create(Target target, Map dispatch) { + return new WorkflowEngineStarterInvocationHandler(target, dispatch); + } +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/MessageNotificationEventHandler.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/MessageNotificationEventHandler.java new file mode 100644 index 000000000..d07200a38 --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/MessageNotificationEventHandler.java @@ -0,0 +1,72 @@ +package cn.axzo.workflow.starter.handler; + +import cn.axzo.workflow.common.model.response.mq.MessagePushDTO; +import org.springframework.core.Ordered; + +/** + * 引擎广播发送消息的事件, 业务一般无需实现,该事件由引擎的中继服务去处理待办相关动作的。 + *

+ * 注意:Order 的顺序,遵循值越小越优先。(取值范围:Integer. MIN_VALUE - Integer. MAX_VALUE) + * + * @author wangli + * @since 2024/5/27 16:25 + */ +public interface MessageNotificationEventHandler extends Ordered { + + /** + * 站内信推送 + * + * @param dto + */ + default void pushNotice(MessagePushDTO dto) { + } + + /** + * 待办推送 + * + * @param dto + */ + default void pushPending(MessagePushDTO dto) { + } + + /** + * 完成待办 + * + * @param dto + */ + default void completePending(MessagePushDTO dto) { + } + + /** + * 审批失败,恢复待办 + * + * @param dto + */ + default void rollbackPending(MessagePushDTO dto) { + } + + /** + * 抄送流程 + * + * @param dto + */ + default void carbonCopy(MessagePushDTO dto) { + } + + /** + * 完成抄送 + * + * @param dto + */ + default void carbonCopyComplete(MessagePushDTO dto) { + } + + /** + * 短信推送 + * + * @param dto + */ + default void pushSms(MessagePushDTO dto) { + } + +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/ProcessActivityEventHandler.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/ProcessActivityEventHandler.java new file mode 100644 index 000000000..eeb3c2724 --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/ProcessActivityEventHandler.java @@ -0,0 +1,52 @@ +package cn.axzo.workflow.starter.handler; + +import cn.axzo.workflow.common.model.response.mq.ProcessActivityDTO; +import org.springframework.core.Ordered; + +/** + * 流程节点相关事件 + *

+ * 节点代表“流程配置”中的一个“审批节点”或“业务节点”,流程配置请按照以下路径去查看 + *

+ *  OMS -> 审批流程 -> 审批配置台 -> 流程配置
+ * 
+ *

+ * 注意:Order 的顺序,遵循值越小越优先。(取值范围:Integer. MIN_VALUE - Integer. MAX_VALUE) + * + * @author wangli + * @since 2024/5/27 16:25 + */ +public interface ProcessActivityEventHandler extends Ordered { + + /** + * 节点已启动 + * + * @param dto 入参 + */ + default void onStart(ProcessActivityDTO dto) { + } + + /** + * 节点等待业务指定审批人 + * + * @param dto 入参 + */ + default void onWaitAssignee(ProcessActivityDTO dto) { + } + + /** + * 节点已完成 + * + * @param dto 入参 + */ + default void onTake(ProcessActivityDTO dto) { + } + + /** + * 节点已取消 + * + * @param dto 入参 + */ + default void onEnd(ProcessActivityDTO dto) { + } +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/ProcessInstanceEventHandler.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/ProcessInstanceEventHandler.java new file mode 100644 index 000000000..f427ac927 --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/ProcessInstanceEventHandler.java @@ -0,0 +1,73 @@ +package cn.axzo.workflow.starter.handler; + +import cn.axzo.workflow.client.feign.bpmn.ProcessInstanceApi; +import cn.axzo.workflow.common.model.request.bpmn.process.BpmnProcessInstanceAbortDTO; +import cn.axzo.workflow.common.model.response.mq.ProcessInstanceDTO; +import org.springframework.core.Ordered; + +/** + * 流程实例相关事件 + *

+ * 注意:Order 的顺序,遵循值越小越优先。(取值范围:Integer. MIN_VALUE - Integer. MAX_VALUE) + * + * @author wangli + * @since 2024/5/27 16:20 + */ +public interface ProcessInstanceEventHandler extends Ordered { + + /** + * 流程实例创建成功后回调 + * + * @param dto + */ + default void onCreated(ProcessInstanceDTO dto) { + } + + /** + * 流程实例开始运行后回调 + * + * @param dto + */ + default void onStarted(ProcessInstanceDTO dto) { + } + + /** + * 流程实例运行完成(通过)后回调 + *

+ * 注意: 该接口表明流程已经走完正向逻辑,正向逻辑比如:通过、同意等 + * + * @param dto + */ + default void onCompleted(ProcessInstanceDTO dto) { + } + + /** + * 流程实例被“撤回”后回调 + *

+ * 撤回只有发起人能触发 + * + * @param dto + */ + default void onCancelled(ProcessInstanceDTO dto) { + } + + /** + * 流程实例被“驳回”后回调 + *

+ * 审批过程中,有一个审批人或者有节点配置的是“自动驳回”,都能触发该事件。 + * + * @param dto + */ + default void onRejected(ProcessInstanceDTO dto) { + } + + /** + * 流程实例被中止后回调 + *

+ * 一般由接入方主动触发,比如调用了 {@link ProcessInstanceApi#abortProcessInstance(BpmnProcessInstanceAbortDTO)} 方法等 + * + * @param dto + */ + default void onAborted(ProcessInstanceDTO dto) { + } +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/ProcessTaskEventHandler.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/ProcessTaskEventHandler.java new file mode 100644 index 000000000..abb3d896c --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/ProcessTaskEventHandler.java @@ -0,0 +1,45 @@ +package cn.axzo.workflow.starter.handler; + +import cn.axzo.workflow.common.model.response.mq.ProcessTaskDTO; +import org.springframework.core.Ordered; + +/** + * 审批任务相关事件 + *

+ * 一个节点(Activity)可以包含 0个或多个任务(Task),它们两者是包含关系 + *

+ * 注意:Order 的顺序,遵循值越小越优先。(取值范围:Integer. MIN_VALUE - Integer. MAX_VALUE) + * + * @author wangli + * @since 2024/5/27 16:21 + */ +public interface ProcessTaskEventHandler extends Ordered { + + /** + * 用户任务已指派审核人 + */ + default void onAssigned(ProcessTaskDTO dto) { + } + + /** + * 用户任务已创建,未指派审核人 + */ + default void onCreated(ProcessTaskDTO dto) { + } + + /** + * 用户任务已通过 + *

+ * 仅审核通过一个用户任务时触发, 如果任务是驳回了, 则直接走实例撤回事件 + */ + default void onCompleted(ProcessTaskDTO dto) { + } + + /** + * 用户任务已删除 + *

+ * 删除不代表驳回或拒绝,因为通过也会走该事件 + */ + default void onDeleted(ProcessTaskDTO dto) { + } +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/execute/ListenerExecutor.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/execute/ListenerExecutor.java new file mode 100644 index 000000000..300f9c05f --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/execute/ListenerExecutor.java @@ -0,0 +1,31 @@ +package cn.axzo.workflow.starter.handler.execute; + +import cn.axzo.framework.domain.ServiceException; +import cn.axzo.framework.rocketmq.EventConsumer; +import cn.axzo.workflow.starter.handler.execute.interceptor.ExecuteInterceptor; + +import java.util.List; +import java.util.function.Consumer; + +public final class ListenerExecutor { + + private final ExecuteInterceptor firstExecuteInterceptor; + + public ListenerExecutor(List chain) { + this.firstExecuteInterceptor = initInterceptorChain(chain); + } + + public void execute(Consumer command, EventConsumer.Context context, T t) { + firstExecuteInterceptor.execute(this, command, context, t); + } + + private ExecuteInterceptor initInterceptorChain(List chain) { + if (chain == null || chain.isEmpty()) { + throw new ServiceException("invalid command interceptor chain configuration: " + chain); + } + for (int i = 0; i < chain.size() - 1; i++) { + chain.get(i).setNext(chain.get(i + 1)); + } + return chain.get(0); + } +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/execute/interceptor/AbstractListenerInterceptor.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/execute/interceptor/AbstractListenerInterceptor.java new file mode 100644 index 000000000..8fbec537c --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/execute/interceptor/AbstractListenerInterceptor.java @@ -0,0 +1,16 @@ +package cn.axzo.workflow.starter.handler.execute.interceptor; + +public abstract class AbstractListenerInterceptor implements ExecuteInterceptor { + + protected ExecuteInterceptor next; + + @Override + public ExecuteInterceptor getNext() { + return next; + } + + @Override + public void setNext(ExecuteInterceptor next) { + this.next = next; + } +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/execute/interceptor/ExecuteInterceptor.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/execute/interceptor/ExecuteInterceptor.java new file mode 100644 index 000000000..c7767f514 --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/execute/interceptor/ExecuteInterceptor.java @@ -0,0 +1,15 @@ +package cn.axzo.workflow.starter.handler.execute.interceptor; + +import cn.axzo.framework.rocketmq.EventConsumer; +import cn.axzo.workflow.starter.handler.execute.ListenerExecutor; + +import java.util.function.Consumer; + +public interface ExecuteInterceptor { + + void execute(ListenerExecutor executor, Consumer consumer, EventConsumer.Context context, T t); + + ExecuteInterceptor getNext(); + + void setNext(ExecuteInterceptor next); +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/execute/interceptor/ExecutorInvoker.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/execute/interceptor/ExecutorInvoker.java new file mode 100644 index 000000000..0cbb6c611 --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/execute/interceptor/ExecutorInvoker.java @@ -0,0 +1,34 @@ +package cn.axzo.workflow.starter.handler.execute.interceptor; + +import cn.axzo.framework.rocketmq.EventConsumer; +import cn.axzo.workflow.starter.handler.execute.ListenerExecutor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Objects; +import java.util.function.Consumer; + +public final class ExecutorInvoker extends AbstractListenerInterceptor { + + private static final Logger log = LoggerFactory.getLogger(ExecutorInvoker.class); + + @Override + public void execute(ListenerExecutor executor, Consumer consumer, EventConsumer.Context context, T t) { + if (Objects.isNull(consumer)) { + log.warn("Unsupported message eventCode type, Please check message producer"); + return; + } + consumer.accept(t); + } + + @Override + public ExecuteInterceptor getNext() { + return null; + } + + @Override + public void setNext(ExecuteInterceptor next) { + throw new UnsupportedOperationException("ExecutorInvoker must be the last interceptor in the chain"); + } + +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/execute/interceptor/FailBackInterceptor.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/execute/interceptor/FailBackInterceptor.java new file mode 100644 index 000000000..cc3cebea7 --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/execute/interceptor/FailBackInterceptor.java @@ -0,0 +1,21 @@ +package cn.axzo.workflow.starter.handler.execute.interceptor; + +import cn.axzo.framework.rocketmq.EventConsumer; +import cn.axzo.workflow.starter.handler.execute.ListenerExecutor; + +import java.util.function.Consumer; + +/** + * 失败自动恢复,在后台记录失败的消息,并按照一定的策略后期再进行重试,目前暂不支持 + */ +public final class FailBackInterceptor extends AbstractListenerInterceptor { + @Override + public void execute(ListenerExecutor executor, Consumer consumer, EventConsumer.Context context, T t) { + throw new UnsupportedOperationException("FailBackInterceptor"); + } + + @Override + public void setNext(ExecuteInterceptor next) { + throw new UnsupportedOperationException("FailBackInterceptor"); + } +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/execute/interceptor/FailFastInterceptor.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/execute/interceptor/FailFastInterceptor.java new file mode 100644 index 000000000..77b46c5a0 --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/execute/interceptor/FailFastInterceptor.java @@ -0,0 +1,24 @@ +package cn.axzo.workflow.starter.handler.execute.interceptor; + +import cn.axzo.framework.rocketmq.EventConsumer; +import cn.axzo.workflow.starter.common.exception.WorkflowListenerExecutionException; +import cn.axzo.workflow.starter.handler.execute.ListenerExecutor; + +import java.util.function.Consumer; + +/** + * 快速失败,出错直接抛出异常,listener不再往下执行 + */ +public final class FailFastInterceptor extends AbstractListenerInterceptor { + + @Override + public void execute(ListenerExecutor executor, Consumer consumer, EventConsumer.Context context, T t) { + try { + getNext().execute(executor, consumer, context, t); + } catch (Throwable e) { + throw new WorkflowListenerExecutionException( + "Failed to invoke the method. Last error is: " + e.getMessage(), e.getCause() != null ? e.getCause() : e); + } + } + +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/execute/interceptor/FailOverInterceptor.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/execute/interceptor/FailOverInterceptor.java new file mode 100644 index 000000000..0eb2659fc --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/execute/interceptor/FailOverInterceptor.java @@ -0,0 +1,72 @@ +package cn.axzo.workflow.starter.handler.execute.interceptor; + +import cn.axzo.framework.rocketmq.EventConsumer; +import cn.axzo.workflow.starter.BroadcastListenerProperties; +import cn.axzo.workflow.starter.handler.execute.ListenerExecutor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.function.Consumer; + +/** + * 当前listener执行出错,忽略继续往下执行,可配置重试相关参数,不抛出异常 + */ +public final class FailOverInterceptor extends AbstractListenerInterceptor { + private static final Logger log = LoggerFactory.getLogger(FailOverInterceptor.class); + + private int numOfRetries = 3; + private int waitTimeInMs = 1500; + private int waitIncreaseFactor = 3; + + public FailOverInterceptor() { + } + + public FailOverInterceptor(BroadcastListenerProperties broadcast) { + this.numOfRetries = 0 == Math.abs(broadcast.getNumOfRetries()) ? this.numOfRetries : Math.abs(broadcast.getNumOfRetries()); + this.waitTimeInMs = Math.abs(broadcast.getWaitTimeInMs()); + this.waitIncreaseFactor = Math.abs(broadcast.getWaitIncreaseFactor()); + } + + @Override + public void execute(ListenerExecutor executor, Consumer consumer, EventConsumer.Context context, T t) { + // 避免组件抛出“程序处理超时”的异常信息, 为业务冗余 10 处理时间 + context.setMaxAllowElapsedMillis(calc() + 10_000L); + long waitTime = waitTimeInMs; + int failedAttempts = 0; + do { + if (failedAttempts > 0) { + log.info("Waiting for {} ms before retrying the command. retryTimes: {}", waitTime, failedAttempts); + waitBeforeRetry(waitTime); + waitTime *= waitIncreaseFactor; + } + try { + getNext().execute(executor, consumer, context, t); + return; + } catch (Throwable e) { + if (failedAttempts == numOfRetries) { + log.error("Workflow Engine Starter caught exception: {}", e.getMessage(), e); + throw e; + } + } + failedAttempts++; + } while (failedAttempts <= numOfRetries); + } + + private Long calc() { + long waitTime = waitTimeInMs; + for (int failedAttempts = 0; failedAttempts < numOfRetries; failedAttempts++) { + // 更新等待时间 + waitTime *= waitIncreaseFactor; + } + // 返回最后一次等待时间(注意:这可能不是最终你想要的逻辑,取决于你的具体需求) + return waitTime; + } + + private void waitBeforeRetry(long waitTime) { + try { + Thread.sleep(waitTime); + } catch (InterruptedException e) { + log.debug(" interrupted while waiting for a retry."); + } + } +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/execute/interceptor/LogInterceptor.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/execute/interceptor/LogInterceptor.java new file mode 100644 index 000000000..1ff75e8ad --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/execute/interceptor/LogInterceptor.java @@ -0,0 +1,44 @@ +package cn.axzo.workflow.starter.handler.execute.interceptor; + +import cn.axzo.framework.rocketmq.EventConsumer; +import cn.axzo.workflow.starter.common.exception.WorkflowEngineStarterException; +import cn.axzo.workflow.starter.common.exception.WorkflowListenerExecutionException; +import cn.axzo.workflow.starter.handler.execute.ListenerExecutor; +import com.alibaba.fastjson.JSON; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.StopWatch; + +import java.util.function.Consumer; + +public final class LogInterceptor extends AbstractListenerInterceptor { + private static final Logger log = LoggerFactory.getLogger(LogInterceptor.class); + + @Override + public void execute(ListenerExecutor executor, Consumer consumer, EventConsumer.Context context, T t) { + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + if (log.isDebugEnabled()) { + log.debug("--- starting handle mq ---- "); + } + if (log.isInfoEnabled()) { + log.info("messageId: {}, eventCode: {}, messageBody: {}", context.getMsgId(), context.getEventCode().toString(), JSON.toJSONString(t)); + } + try { + getNext().execute(executor, consumer, context, t); + } catch (WorkflowListenerExecutionException e) { + // fail_fast mode , will be ignored + } catch (Throwable e) { + if (e instanceof WorkflowEngineStarterException) { + // 如果业务想使用异常来正确结束事件处理,可以抛出该移除类型 + } else { + throw e; + } + } finally { + stopWatch.stop(); + if (log.isDebugEnabled()) { + log.debug("--- messageId: {} finished ,timeCost:{} ms ---", executor.getClass().getSimpleName(), stopWatch.getTotalTimeMillis()); + } + } + } +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/filter/MessageNotificationEventFilter.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/filter/MessageNotificationEventFilter.java new file mode 100644 index 000000000..aa184f8ed --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/filter/MessageNotificationEventFilter.java @@ -0,0 +1,15 @@ +package cn.axzo.workflow.starter.handler.filter; + +import cn.axzo.workflow.common.model.response.mq.MessagePushDTO; +import cn.axzo.workflow.starter.mq.broadcast.filter.BasicMessageQueueFilter; + +/** + * MessageNotificationEvent 的自定义过滤接口 + *

+ * 注意:Order 的顺序,遵循值越小越优先。(取值范围:Integer. MIN_VALUE - Integer. MAX_VALUE) + * + * @author wangli + * @since 2024/6/5 15:49 + */ +public interface MessageNotificationEventFilter extends BasicMessageQueueFilter { +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/filter/ProcessActivityEventFilter.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/filter/ProcessActivityEventFilter.java new file mode 100644 index 000000000..a769d28b3 --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/filter/ProcessActivityEventFilter.java @@ -0,0 +1,15 @@ +package cn.axzo.workflow.starter.handler.filter; + +import cn.axzo.workflow.common.model.response.mq.ProcessActivityDTO; +import cn.axzo.workflow.starter.mq.broadcast.filter.BasicMessageQueueFilter; + +/** + * ProcessActivityEvent 自定义的过滤接口 + *

+ * 注意:Order 的顺序,遵循值越小越优先。(取值范围:Integer. MIN_VALUE - Integer. MAX_VALUE) + * + * @author wangli + * @since 2024/6/5 15:49 + */ +public interface ProcessActivityEventFilter extends BasicMessageQueueFilter { +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/filter/ProcessInstanceEventFilter.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/filter/ProcessInstanceEventFilter.java new file mode 100644 index 000000000..fb04e3360 --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/filter/ProcessInstanceEventFilter.java @@ -0,0 +1,15 @@ +package cn.axzo.workflow.starter.handler.filter; + +import cn.axzo.workflow.common.model.response.mq.ProcessInstanceDTO; +import cn.axzo.workflow.starter.mq.broadcast.filter.BasicMessageQueueFilter; + +/** + * ProcessInstanceEvent 自定义的过滤接口 + *

+ * 注意:Order 的顺序,遵循值越小越优先。(取值范围:Integer. MIN_VALUE - Integer. MAX_VALUE) + * + * @author wangli + * @since 2024/6/5 15:44 + */ +public interface ProcessInstanceEventFilter extends BasicMessageQueueFilter { +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/filter/ProcessTaskEventFilter.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/filter/ProcessTaskEventFilter.java new file mode 100644 index 000000000..22ac91008 --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/filter/ProcessTaskEventFilter.java @@ -0,0 +1,15 @@ +package cn.axzo.workflow.starter.handler.filter; + +import cn.axzo.workflow.common.model.response.mq.ProcessTaskDTO; +import cn.axzo.workflow.starter.mq.broadcast.filter.BasicMessageQueueFilter; + +/** + * ProcessTaskEvent 自定义的过滤接口 + *

+ * 注意:Order 的顺序,遵循值越小越优先。(取值范围:Integer. MIN_VALUE - Integer. MAX_VALUE) + * + * @author wangli + * @since 2024/6/5 15:50 + */ +public interface ProcessTaskEventFilter extends BasicMessageQueueFilter { +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/filter/global/BroadcastMessageQueueFilter.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/filter/global/BroadcastMessageQueueFilter.java new file mode 100644 index 000000000..d17283353 --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/filter/global/BroadcastMessageQueueFilter.java @@ -0,0 +1,23 @@ +package cn.axzo.workflow.starter.handler.filter.global; + +import org.apache.rocketmq.common.message.MessageExt; +import org.springframework.core.Ordered; + +/** + * 对流程引擎广播的事件进行全局过滤,业务可扩展的 MQ 消息过滤接口 + *

+ * 注意:Order 的顺序,遵循值越小越优先。(取值范围:Integer.MIN_VALUE - Integer.MAX_VALUE) + * + * @author wangli + * @since 2024/6/4 17:04 + */ +public interface BroadcastMessageQueueFilter extends Ordered { + + /** + * @param message MQ 消息元数据 + * @param jsonData MQ 消息中的 payload,是 JSON 格式的字符串 + * @return 返回 true 时,将跳过该 {@link MessageExt} 消息,不进行消费 + */ + boolean doFilter(MessageExt message, String jsonData); + +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/monitor/BroadcastDLQReporter.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/monitor/BroadcastDLQReporter.java new file mode 100644 index 000000000..9305f449c --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/monitor/BroadcastDLQReporter.java @@ -0,0 +1,24 @@ +package cn.axzo.workflow.starter.handler.monitor; + +import org.apache.rocketmq.common.admin.TopicOffset; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 广播死信队列监控的报告器, 默认仅仅是打印异常。 + *

+ * 可以用它来发生钉钉之类的。 + *

+ * 注意:该接口目前不是为了消费死信队列中的消息的。后续必要时,再开放死信队列的消费扩展点 + * + * @author wangli + * @since 2024/6/14 17:04 + */ +public interface BroadcastDLQReporter { + Logger log = LoggerFactory.getLogger(BroadcastDLQReporter.class); + + default void process(TopicOffset v, String dlqName) { + long dlqCount = v.getMaxOffset() - v.getMinOffset(); + log.error("found Broadcast MQ DLQ, count: {}", dlqCount); + } +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/monitor/RpcDLQReporter.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/monitor/RpcDLQReporter.java new file mode 100644 index 000000000..ceeffc63e --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/handler/monitor/RpcDLQReporter.java @@ -0,0 +1,25 @@ +package cn.axzo.workflow.starter.handler.monitor; + +import org.apache.rocketmq.common.admin.TopicOffset; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * RPC 重试的死信队列监控报告器, 默认仅仅是打印异常 + *

+ * 可以用它来发生钉钉之类的. + *

+ * 注意:该接口目前不是为了消费死信队列中的消息的。后续必要时,再开放死信队列的消费扩展点 + * + * @author wangli + * @since 2024/6/14 17:01 + */ + +public interface RpcDLQReporter { + Logger log = LoggerFactory.getLogger(RpcDLQReporter.class); + + default void process(TopicOffset v, String dlqName) { + long dlqCount = v.getMaxOffset() - v.getMinOffset(); + log.error("found rpc MQ DLQ, count: {}", dlqCount); + } +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/broadcast/consumer/AbstractInnerWorkflowListener.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/broadcast/consumer/AbstractInnerWorkflowListener.java new file mode 100644 index 000000000..0956606c0 --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/broadcast/consumer/AbstractInnerWorkflowListener.java @@ -0,0 +1,83 @@ +package cn.axzo.workflow.starter.mq.broadcast.consumer; + +import cn.axzo.framework.rocketmq.Event; +import cn.axzo.framework.rocketmq.EventConsumer; +import cn.axzo.workflow.starter.handler.execute.ListenerExecutor; +import cn.axzo.workflow.starter.mq.broadcast.filter.BasicMessageQueueFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.core.Ordered; +import org.springframework.util.CollectionUtils; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +public abstract class AbstractInnerWorkflowListener, Target> implements InnerWorkflowListener { + + private static final Logger log = LoggerFactory.getLogger(AbstractInnerWorkflowListener.class); + + /** + * 接入方对 cn/axzo/workflow/starter/handler/ 包下的 EventHandler 实现 + */ + protected final List businessListeners; + /** + * 接入方对 cn/axzo/workflow/starter/handler/filter/ 包下的EventFilter 实现 + */ + protected final List businessFilters; + protected final ListenerExecutor listenerExecutor; + + + public AbstractInnerWorkflowListener(ListenerExecutor listenerExecutor, + ObjectProvider> handlerProvider, + ObjectProvider> filterProvider) { + this.listenerExecutor = listenerExecutor; + this.businessListeners = getOrderedBusinessListeners(handlerProvider); + this.businessFilters = getOrderedBusinessFilters(filterProvider); + } + + private List getOrderedBusinessFilters(ObjectProvider> filterProvider) { + return filterProvider.getIfAvailable(Collections::emptyList) + .stream() + .sorted(Comparator.comparingInt(Ordered::getOrder)) + .collect(Collectors.toList()); + } + + protected List getOrderedBusinessListeners(ObjectProvider> handlerProvider) { + return handlerProvider.getIfAvailable(Collections::emptyList) + .stream() + .sorted(Comparator.comparingInt(Ordered::getOrder)) + .collect(Collectors.toList()); + } + + @Override + public void handleEvent(Event event, EventConsumer.Context context) { + if (CollectionUtils.isEmpty(businessListeners)) { + return; + } + + Target convert = convert(event); + + for (F filter : businessFilters) { + if (filter.doFilter(event, context, convert)) { + log.info("【{}】filtered message, messageId: {}", filter.getClass().getSimpleName(), context.getMsgId()); + return; + } + } + + onEvent(convert, context); + } + + protected abstract Target convert(Event event); + + protected abstract void onEvent(Target data, EventConsumer.Context context); + + @Override + public boolean dispatch(Event event) { + return getSupportEventCodes().contains(event.getEventCode()); + } + + protected abstract List getSupportEventCodes(); +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/broadcast/consumer/InnerActivityEventListener.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/broadcast/consumer/InnerActivityEventListener.java new file mode 100644 index 000000000..51556fa42 --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/broadcast/consumer/InnerActivityEventListener.java @@ -0,0 +1,74 @@ +package cn.axzo.workflow.starter.mq.broadcast.consumer; + +import cn.axzo.framework.rocketmq.Event; +import cn.axzo.framework.rocketmq.EventConsumer; +import cn.axzo.workflow.common.enums.ProcessActivityEventEnum; +import cn.axzo.workflow.common.model.response.mq.ProcessActivityDTO; +import cn.axzo.workflow.starter.handler.ProcessActivityEventHandler; +import cn.axzo.workflow.starter.handler.execute.ListenerExecutor; +import cn.axzo.workflow.starter.handler.filter.ProcessActivityEventFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.ObjectProvider; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** + * @author wangli + * @since 2024/5/21 15:51 + */ +public class InnerActivityEventListener extends AbstractInnerWorkflowListener { + private final Logger log = LoggerFactory.getLogger(InnerActivityEventListener.class); + + public final static Supplier> SUPPORTED_EVENT_CODES_SUPPLIER = () -> + Arrays.stream(ProcessActivityEventEnum.values()).map(ProcessActivityEventEnum::getEventCode) + .collect(Collectors.toList()); + + public InnerActivityEventListener(ListenerExecutor executor, + ObjectProvider> provider, + ObjectProvider> filterProvider) { + super(executor, provider, filterProvider); + } + + @Override + protected ProcessActivityDTO convert(Event event) { + return event.normalizedData(ProcessActivityDTO.class); + } + + @Override + public void onEvent(ProcessActivityDTO dto, EventConsumer.Context context) { + log.debug("【{}】new message begin processing, messageId: {}", + this.getClass().getSimpleName(), context.getMsgId()); + ProcessActivityEventEnum type = dto.getType(); + for (ProcessActivityEventHandler activityListener : businessListeners) { + Consumer consumer = null; + switch (type) { + case PROCESS_ACTIVITY_START: + consumer = activityListener::onStart; + break; + case PROCESS_ACTIVITY_WAIT_ASSIGNEE: + consumer = activityListener::onWaitAssignee; + break; + case PROCESS_ACTIVITY_TAKE: + consumer = activityListener::onTake; + break; + case PROCESS_ACTIVITY_END: + consumer = activityListener::onEnd; + break; + default: + log.warn("unknown process activity event type: {}", type); + } + listenerExecutor.execute(consumer, context, dto); + } + } + + @Override + protected List getSupportEventCodes() { + return SUPPORTED_EVENT_CODES_SUPPLIER.get(); + } + +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/broadcast/consumer/InnerInstanceEventListener.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/broadcast/consumer/InnerInstanceEventListener.java new file mode 100644 index 000000000..31411756e --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/broadcast/consumer/InnerInstanceEventListener.java @@ -0,0 +1,80 @@ +package cn.axzo.workflow.starter.mq.broadcast.consumer; + +import cn.axzo.framework.rocketmq.Event; +import cn.axzo.framework.rocketmq.EventConsumer; +import cn.axzo.workflow.common.enums.ProcessInstanceEventEnum; +import cn.axzo.workflow.common.model.response.mq.ProcessInstanceDTO; +import cn.axzo.workflow.starter.handler.ProcessInstanceEventHandler; +import cn.axzo.workflow.starter.handler.execute.ListenerExecutor; +import cn.axzo.workflow.starter.handler.filter.ProcessInstanceEventFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.ObjectProvider; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** + * @author wangli + * @since 2024/5/21 15:51 + */ +public class InnerInstanceEventListener extends AbstractInnerWorkflowListener { + private final Logger log = LoggerFactory.getLogger(InnerInstanceEventListener.class); + + public final static Supplier> SUPPORTED_EVENT_CODES_SUPPLIER = () -> + Arrays.stream(ProcessInstanceEventEnum.values()).map(ProcessInstanceEventEnum::getEventCode) + .collect(Collectors.toList()); + + public InnerInstanceEventListener(ListenerExecutor listenerExecutor, + ObjectProvider> handlerProvider, + ObjectProvider> filterProvider) { + super(listenerExecutor, handlerProvider, filterProvider); + } + + @Override + protected ProcessInstanceDTO convert(Event event) { + return event.normalizedData(ProcessInstanceDTO.class); + } + + @Override + public void onEvent(ProcessInstanceDTO dto, EventConsumer.Context context) { + log.debug("【{}】new message begin processing, messageId: {}", + this.getClass().getSimpleName(), context.getMsgId()); + ProcessInstanceEventEnum type = dto.getType(); + for (ProcessInstanceEventHandler instanceListener : businessListeners) { + Consumer consumer = null; + switch (type) { + case PROCESS_INSTANCE_CREATED: + consumer = instanceListener::onCreated; + break; + case PROCESS_INSTANCE_STARTED: + consumer = instanceListener::onStarted; + break; + case PROCESS_INSTANCE_CANCELLED: + consumer = instanceListener::onCancelled; + break; + case PROCESS_INSTANCE_ABORTED: + consumer = instanceListener::onAborted; + break; + case PROCESS_INSTANCE_COMPLETED: + consumer = instanceListener::onCompleted; + break; + case PROCESS_INSTANCE_REJECTED: + consumer = instanceListener::onRejected; + break; + default: + log.warn("unknown process activity event type: {}", type); + } + listenerExecutor.execute(consumer, context, dto); + } + } + + @Override + protected List getSupportEventCodes() { + return SUPPORTED_EVENT_CODES_SUPPLIER.get(); + } + +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/broadcast/consumer/InnerNotificationEventListener.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/broadcast/consumer/InnerNotificationEventListener.java new file mode 100644 index 000000000..136b67fef --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/broadcast/consumer/InnerNotificationEventListener.java @@ -0,0 +1,81 @@ +package cn.axzo.workflow.starter.mq.broadcast.consumer; + +import cn.axzo.framework.rocketmq.Event; +import cn.axzo.framework.rocketmq.EventConsumer; +import cn.axzo.workflow.common.enums.ProcessMessagePushEventEnum; +import cn.axzo.workflow.common.model.response.mq.MessagePushDTO; +import cn.axzo.workflow.starter.handler.MessageNotificationEventHandler; +import cn.axzo.workflow.starter.handler.execute.ListenerExecutor; +import cn.axzo.workflow.starter.handler.filter.MessageNotificationEventFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.ObjectProvider; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** + * @author wangli + * @since 2024/5/21 15:53 + */ +public class InnerNotificationEventListener extends AbstractInnerWorkflowListener { + private final static Logger log = LoggerFactory.getLogger(InnerNotificationEventListener.class); + + public final static Supplier> SUPPORTED_EVENT_CODES_SUPPLIER = () -> + Arrays.stream(ProcessMessagePushEventEnum.values()).map(ProcessMessagePushEventEnum::getEventCode) + .collect(Collectors.toList()); + + public InnerNotificationEventListener(ListenerExecutor executor, ObjectProvider> provider, ObjectProvider> filterProvider) { + super(executor, provider, filterProvider); + } + + @Override + protected MessagePushDTO convert(Event event) { + return event.normalizedData(MessagePushDTO.class); + } + + @Override + public void onEvent(MessagePushDTO dto, EventConsumer.Context context) { + log.debug("【{}】new message begin processing, messageId: {}", + this.getClass().getSimpleName(), context.getMsgId()); + ProcessMessagePushEventEnum type = dto.getType(); + for (MessageNotificationEventHandler noticeListener : businessListeners) { + Consumer consumer = null; + switch (type) { + case PROCESS_PUSH_NOTICE: + consumer = noticeListener::pushNotice; + break; + case PROCESS_PUSH_PENDING: + consumer = noticeListener::pushPending; + break; + case PROCESS_PUSH_PENDING_COMPLETE: + consumer = noticeListener::completePending; + break; + case PROCESS_PUSH_PENDING_ROLLBACK: + consumer = noticeListener::rollbackPending; + break; + case PROCESS_CARBON_COPY: + consumer = noticeListener::carbonCopy; + break; + case PROCESS_CARBON_COPY_COMPLETE: + consumer = noticeListener::carbonCopyComplete; + break; + case PROCESS_PUSH_SMS: + consumer = noticeListener::pushSms; + break; + default: + log.warn("unknown message event type: {}", type); + } + listenerExecutor.execute(consumer, context, dto); + } + } + + @Override + protected List getSupportEventCodes() { + return SUPPORTED_EVENT_CODES_SUPPLIER.get(); + } + +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/broadcast/consumer/InnerTaskEventListener.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/broadcast/consumer/InnerTaskEventListener.java new file mode 100644 index 000000000..b6039034f --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/broadcast/consumer/InnerTaskEventListener.java @@ -0,0 +1,72 @@ +package cn.axzo.workflow.starter.mq.broadcast.consumer; + +import cn.axzo.framework.rocketmq.Event; +import cn.axzo.framework.rocketmq.EventConsumer; +import cn.axzo.workflow.common.enums.ProcessTaskEventEnum; +import cn.axzo.workflow.common.model.response.mq.ProcessTaskDTO; +import cn.axzo.workflow.starter.handler.ProcessTaskEventHandler; +import cn.axzo.workflow.starter.handler.execute.ListenerExecutor; +import cn.axzo.workflow.starter.handler.filter.ProcessTaskEventFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.ObjectProvider; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** + * @author wangli + * @since 2024/5/21 15:51 + */ +public class InnerTaskEventListener extends AbstractInnerWorkflowListener { + private final Logger log = LoggerFactory.getLogger(InnerTaskEventListener.class); + + public final static Supplier> SUPPORTED_EVENT_CODES_SUPPLIER = () -> + Arrays.stream(ProcessTaskEventEnum.values()).map(ProcessTaskEventEnum::getEventCode) + .collect(Collectors.toList()); + + public InnerTaskEventListener(ListenerExecutor executor, ObjectProvider> provider, ObjectProvider> filterProvider) { + super(executor, provider, filterProvider); + } + + @Override + protected ProcessTaskDTO convert(Event event) { + return event.normalizedData(ProcessTaskDTO.class); + } + + @Override + public void onEvent(ProcessTaskDTO dto, EventConsumer.Context context) { + log.debug("【{}】new message begin processing, messageId: {}", + this.getClass().getSimpleName(), context.getMsgId()); + ProcessTaskEventEnum type = dto.getType(); + for (ProcessTaskEventHandler taskListener : businessListeners) { + Consumer consumer = null; + switch (type) { + case PROCESS_TASK_CREATED: + consumer = taskListener::onCreated; + break; + case PROCESS_TASK_COMPLETED: + consumer = taskListener::onCompleted; + break; + case PROCESS_TASK_ASSIGNED: + consumer = taskListener::onAssigned; + break; + case PROCESS_TASK_DELETED: + consumer = taskListener::onDeleted; + break; + default: + log.warn("unknown task event type: {}", type); + } + listenerExecutor.execute(consumer, context, dto); + } + } + + @Override + protected List getSupportEventCodes() { + return SUPPORTED_EVENT_CODES_SUPPLIER.get(); + } + +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/broadcast/consumer/InnerWorkflowListener.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/broadcast/consumer/InnerWorkflowListener.java new file mode 100644 index 000000000..03940f45d --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/broadcast/consumer/InnerWorkflowListener.java @@ -0,0 +1,12 @@ +package cn.axzo.workflow.starter.mq.broadcast.consumer; + +import cn.axzo.framework.rocketmq.Event; +import cn.axzo.framework.rocketmq.EventConsumer; + +public interface InnerWorkflowListener { + + void handleEvent(Event event, EventConsumer.Context context); + + boolean dispatch(Event event); +} + diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/broadcast/consumer/WorkflowEngineBroadcastEventListener.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/broadcast/consumer/WorkflowEngineBroadcastEventListener.java new file mode 100644 index 000000000..d3c09b78b --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/broadcast/consumer/WorkflowEngineBroadcastEventListener.java @@ -0,0 +1,69 @@ +package cn.axzo.workflow.starter.mq.broadcast.consumer; + +import cn.axzo.framework.rocketmq.Event; +import cn.axzo.framework.rocketmq.EventConsumer; +import cn.axzo.framework.rocketmq.EventHandler; +import cn.axzo.workflow.starter.WorkflowEngineStarterProperties; +import com.alibaba.fastjson.JSON; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.CollectionUtils; + +import java.util.List; +import java.util.Objects; + +/** + * 流程引擎服务广播的事件监听器 + * + * @author wangli + * @since 2024/5/21 15:50 + */ +public class WorkflowEngineBroadcastEventListener implements EventHandler, InitializingBean { + private final Logger log = LoggerFactory.getLogger(WorkflowEngineBroadcastEventListener.class); + private final EventConsumer eventConsumer; + private final WorkflowEngineStarterProperties starterProperties; + + private final List workflowListeners; + + public WorkflowEngineBroadcastEventListener(EventConsumer eventConsumer, WorkflowEngineStarterProperties workflowEngineStarterProperties, List workflowListeners) { + this.eventConsumer = eventConsumer; + this.starterProperties = workflowEngineStarterProperties; + this.workflowListeners = workflowListeners; + } + + @Override + public void onEvent(Event event, EventConsumer.Context context) { + if (Objects.isNull(event) || Objects.isNull(event.getEventCode())) { + log.warn("illegal event code: {}", JSON.toJSONString(event)); + return; + } + + try { + JSON.parse(JSON.toJSONString(event.getData())); + } catch (Exception e) { + log.warn("event data json format error, will be ignored, messageId: {}", context.getMsgId()); + return; + } + + if (CollectionUtils.isEmpty(workflowListeners)) { + log.warn("no business listeners implementation found, please check @Component annotation"); + return; + } + + for (InnerWorkflowListener workflowListener : workflowListeners) { + if (workflowListener.dispatch(event)) { + workflowListener.handleEvent(event, context); + } + } + } + + @Override + public void afterPropertiesSet() { + eventConsumer.registerHandlers(InnerActivityEventListener.SUPPORTED_EVENT_CODES_SUPPLIER.get(), this); + eventConsumer.registerHandlers(InnerInstanceEventListener.SUPPORTED_EVENT_CODES_SUPPLIER.get(), this); + eventConsumer.registerHandlers(InnerNotificationEventListener.SUPPORTED_EVENT_CODES_SUPPLIER.get(), this); + eventConsumer.registerHandlers(InnerTaskEventListener.SUPPORTED_EVENT_CODES_SUPPLIER.get(), this); + } + +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/broadcast/filter/BasicMessageQueueFilter.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/broadcast/filter/BasicMessageQueueFilter.java new file mode 100644 index 000000000..aa3a31af6 --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/broadcast/filter/BasicMessageQueueFilter.java @@ -0,0 +1,31 @@ +package cn.axzo.workflow.starter.mq.broadcast.filter; + +import cn.axzo.framework.rocketmq.Event; +import cn.axzo.framework.rocketmq.EventConsumer; +import cn.axzo.workflow.common.constant.BpmnConstants; +import org.springframework.core.Ordered; + +/** + * 一个基于某个具体的 MQ 消息的过滤接口 + * + * @author wangli + * @since 2024/6/5 16:08 + */ +public interface BasicMessageQueueFilter extends Ordered { + + /** + * 基于具体的 {@link Event.EventCode#module} 下的自定义的 MQ 过滤器 + * + * @param event 事件原对象 + * @param context event 上下文对象,包含 msgId,headers 等信息,其中
+ * 有 MQ 消息归属哪个应用,使用 {@link BpmnConstants#MQ_OWNERSHIP_APPLICATION} 获取值;
+ * 还有 MQ 消息归属哪个业务 ID,使用 {@link BpmnConstants#MQ_OWNERSHIP_PROCESS_DEFINITION_KEY} 获取值. + *

获取归属应用示例: + *

String owner_application = new String(context.getHeaders().getOrDefault(MQ_OWNERSHIP_APPLICATION, new byte[0]), StandardCharsets.UTF_8);
+ * @param data payload 被转换后的目标对象 + * @return 返回 true 时,将跳过该 {@link Event} 消息,不进行消费 + */ + default boolean doFilter(Event event, EventConsumer.Context context, T data) { + return false; + } +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/broadcast/filter/InnerFilterDefinitionKey.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/broadcast/filter/InnerFilterDefinitionKey.java new file mode 100644 index 000000000..a08e15f6b --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/broadcast/filter/InnerFilterDefinitionKey.java @@ -0,0 +1,45 @@ +package cn.axzo.workflow.starter.mq.broadcast.filter; + +import cn.axzo.workflow.starter.BroadcastListenerProperties; +import cn.axzo.workflow.starter.WorkflowEngineStarterProperties; +import org.apache.rocketmq.common.message.MessageExt; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.CollectionUtils; + +import java.util.Map; + +import static cn.axzo.workflow.common.constant.BpmnConstants.MQ_OWNERSHIP_PROCESS_DEFINITION_KEY; + +/** + * 对 {@link BroadcastListenerProperties#filterProcessDefinitionKeys} 的实现 + * + * @author wangli + * @since 2024/6/4 17:06 + */ +public class InnerFilterDefinitionKey implements InnerMessageQueueHandleBeforeFilter { + private final Logger log = LoggerFactory.getLogger(InnerFilterDefinitionKey.class); + private final WorkflowEngineStarterProperties properties; + + public InnerFilterDefinitionKey(WorkflowEngineStarterProperties properties) { + this.properties = properties; + } + + @Override + public boolean doFilter(MessageExt message) { + if (!properties.getBroadcast().getEnableFilterDefinitionKey()) { + return false; + } + if (CollectionUtils.isEmpty(properties.getBroadcast().getFilterProcessDefinitionKeys())) { + log.warn("【{}】Your system has the filter process-definition-key enabled, but `BroadcastListenerConfigurationProperties.filterProcessDefinitionKeys` is empty", this.getClass().getSimpleName()); + return false; + } + Map headers = message.getProperties(); + String processDefinitionKey = headers.getOrDefault(MQ_OWNERSHIP_PROCESS_DEFINITION_KEY, ""); + if (properties.getBroadcast().getFilterProcessDefinitionKeys().contains(processDefinitionKey)) { + return false; + } + log.debug("【{}】The broadcast message does not belong to the key of interest, will be ignored. messageId: {}, message process-definition-key: {}", this.getClass().getSimpleName(), message.getMsgId(), processDefinitionKey); + return true; + } +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/broadcast/filter/InnerFilterExtension.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/broadcast/filter/InnerFilterExtension.java new file mode 100644 index 000000000..dfb5b3b3a --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/broadcast/filter/InnerFilterExtension.java @@ -0,0 +1,59 @@ +package cn.axzo.workflow.starter.mq.broadcast.filter; + +import cn.axzo.workflow.starter.handler.filter.global.BroadcastMessageQueueFilter; +import org.apache.rocketmq.common.message.MessageExt; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.core.Ordered; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * 用于扩展业务自己的 MQ 事件过滤器 + * + * @author wangli + * @since 2024/6/4 17:10 + */ +public class InnerFilterExtension implements InnerMessageQueueHandleBeforeFilter { + private final Logger log = LoggerFactory.getLogger(InnerFilterExtension.class); + + private final List businessFilters; + + public InnerFilterExtension(ObjectProvider> businessFilterProvider) { + this.businessFilters = businessFilterProvider.getIfAvailable(Collections::emptyList) + .stream().sorted(Comparator.comparing(Ordered::getOrder)) + .collect(Collectors.toList()); + } + + @Override + public boolean doFilter(MessageExt message) { + if (businessFilters.isEmpty()) { + log.debug("【{}】business extension filter is empty", this.getClass().getSimpleName()); + return false; + } + for (BroadcastMessageQueueFilter businessFilter : businessFilters) { + if (businessFilter.doFilter(message, new String(message.getBody(), UTF_8))) { + return true; + } + } + return false; + } + + + private Class getParameterizedTypeClass(BroadcastMessageQueueFilter filter) { + Type type = filter.getClass().getGenericInterfaces()[0]; + if (type instanceof ParameterizedType) { + return (Class) ((ParameterizedType) type).getActualTypeArguments()[0]; + } + return null; + } + +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/broadcast/filter/InnerFilterMQOwnerShip.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/broadcast/filter/InnerFilterMQOwnerShip.java new file mode 100644 index 000000000..1ccfd2f85 --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/broadcast/filter/InnerFilterMQOwnerShip.java @@ -0,0 +1,50 @@ +package cn.axzo.workflow.starter.mq.broadcast.filter; + +import cn.axzo.workflow.starter.BroadcastListenerProperties; +import cn.axzo.workflow.starter.WorkflowEngineStarterProperties; +import org.apache.rocketmq.common.message.MessageExt; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.StringUtils; + +import java.util.Map; + +import static cn.axzo.workflow.common.constant.BpmnConstants.MQ_OWNERSHIP_APPLICATION; + +/** + * 对 {@link BroadcastListenerProperties#filterApplicationNames} 的实现 + * + * @author wangli + * @since 2024/6/4 17:06 + */ +public class InnerFilterMQOwnerShip implements InnerMessageQueueHandleBeforeFilter { + private final Logger log = LoggerFactory.getLogger(InnerFilterMQOwnerShip.class); + + private final WorkflowEngineStarterProperties properties; + + public InnerFilterMQOwnerShip(WorkflowEngineStarterProperties properties, String applicationName) { + this.properties = properties; + this.properties.getBroadcast().getFilterApplicationNames().add(applicationName); + } + + @Override + public boolean doFilter(MessageExt message) { + if (!properties.getBroadcast().getEnableFilterApplicationName()) { + return false; + } + Map headers = message.getProperties(); + String mqOwnerShip = headers.getOrDefault(MQ_OWNERSHIP_APPLICATION, null); + + if (!StringUtils.hasText(mqOwnerShip)) { + log.debug("【{}】The broadcast message does not attribute the message, will not be ignored. messageId: {}", this.getClass().getSimpleName(), message.getMsgId()); + return false; + } else { + if (properties.getBroadcast().getFilterApplicationNames().contains(mqOwnerShip)) { + return false; + } else { + log.debug("【{}】The broadcast message does not attribute the message, will be ignored. messageId: {}, mq-ownership-application: {}", this.getClass().getSimpleName(), message.getMsgId(), mqOwnerShip); + return true; + } + } + } +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/broadcast/filter/InnerMessageQueueHandleBeforeFilter.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/broadcast/filter/InnerMessageQueueHandleBeforeFilter.java new file mode 100644 index 000000000..454e1617f --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/broadcast/filter/InnerMessageQueueHandleBeforeFilter.java @@ -0,0 +1,21 @@ +package cn.axzo.workflow.starter.mq.broadcast.filter; + +import org.apache.rocketmq.common.message.MessageExt; + +/** + * 过滤 MQ 消息, 不再对其进行消费 + * + * @author wangli + * @since 2024/6/4 17:12 + */ +public interface InnerMessageQueueHandleBeforeFilter { + + /** + * 传入 RocketMQ 底层的消息对象,对消费过程中的消息过滤 + * + * @param message + * @return 为 true 时,代表需要被过滤 + */ + boolean doFilter(MessageExt message); + +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/monitor/AlertBroadcastDLQReporter.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/monitor/AlertBroadcastDLQReporter.java new file mode 100644 index 000000000..97a7a24ae --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/monitor/AlertBroadcastDLQReporter.java @@ -0,0 +1,75 @@ +package cn.axzo.workflow.starter.mq.monitor; + +import cn.axzo.workflow.starter.handler.monitor.BroadcastDLQReporter; +import com.dingtalk.api.DefaultDingTalkClient; +import com.dingtalk.api.DingTalkClient; +import com.dingtalk.api.request.OapiRobotSendRequest; +import com.dingtalk.api.response.OapiRobotSendResponse; +import lombok.SneakyThrows; +import org.apache.rocketmq.common.admin.TopicOffset; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.env.Environment; + +/** + * Starter 内置的 Broadcast DLQ 钉钉通知 + * + * @author wangli + * @since 2024/6/19 11:04 + */ +public class AlertBroadcastDLQReporter implements BroadcastDLQReporter { + + private static final Logger log = LoggerFactory.getLogger(AlertBroadcastDLQReporter.class); + private final String dingtalk_robot_webhook = "https://oapi.dingtalk.com/robot/send?access_token=341ee2907f3ebc15dc495fb7771a646230058710999fec7838066c109849878e"; + private final String profile; + private final String applicationName; + + public AlertBroadcastDLQReporter(Environment environment) { + this.profile = environment.getProperty("spring.profiles.active"); + this.applicationName = environment.getProperty("spring.application.name"); + } + + @Override + public void process(TopicOffset v, String dlqName) { + log.warn("found DLQ"); + long count = v.getMaxOffset() - v.getMinOffset(); + if (count > 0) { + sendDingTalk(count, dlqName); + } + } + + @SneakyThrows + public void sendDingTalk(Long count, String dlqName) { + DingTalkClient client = new DefaultDingTalkClient(dingtalk_robot_webhook); + OapiRobotSendRequest request = new OapiRobotSendRequest(); + + request.setMsgtype("markdown"); + OapiRobotSendRequest.Markdown markdown = new OapiRobotSendRequest.Markdown(); + markdown.setTitle("Notice Env: " + profile); + markdown.setText("#### [" + profile + "]环境[" + applicationName + "]发现“DLQ”数据(Rpc)\n" + + "> ###### 1. 如需查看详情,[请点击这里](" + findDomain() + "m) \n" + + "> ###### 2. 如需调整 MQ DLQ 的监控状态: \n" + + "> ###### 3. DLQ名称:" + dlqName + " \n" + + "[关闭监控](" + findDomain() + "m/set?status=false) \n | " + + "[开启监控](" + findDomain() + "m/set?status=true) \n" + + "> 积累条数:" + count + " \n"); + request.setMarkdown(markdown); + log.warn(markdown.getText()); + OapiRobotSendResponse response = client.execute(request); + } + + private String findDomain() { + switch (profile) { + case "dev": + return "https://dev-app.axzo.cn/" + applicationName + "/web/we/s/"; + case "test": + return "https://test-api.axzo.cn/" + applicationName + "/web/we/s/"; + case "pre": + return "https://pre-api.axzo.cn/" + applicationName + "/web/we/s/"; + case "live": + return "https://live-api.axzo.cn/" + applicationName + "/web/we/s/"; + default: + return "https://api.axzo.cn/" + applicationName + "/web/we/s/"; + } + } +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/monitor/AlertRcpDLQReporter.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/monitor/AlertRcpDLQReporter.java new file mode 100644 index 000000000..3619f7f83 --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/monitor/AlertRcpDLQReporter.java @@ -0,0 +1,75 @@ +package cn.axzo.workflow.starter.mq.monitor; + +import cn.axzo.workflow.starter.handler.monitor.RpcDLQReporter; +import com.dingtalk.api.DefaultDingTalkClient; +import com.dingtalk.api.DingTalkClient; +import com.dingtalk.api.request.OapiRobotSendRequest; +import com.dingtalk.api.response.OapiRobotSendResponse; +import lombok.SneakyThrows; +import org.apache.rocketmq.common.admin.TopicOffset; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.env.Environment; + +/** + * Starter 内置的 Rpc DLQ 钉钉通知 + * + * @author wangli + * @since 2024/6/19 11:04 + */ +public class AlertRcpDLQReporter implements RpcDLQReporter { + + private static final Logger log = LoggerFactory.getLogger(AlertRcpDLQReporter.class); + private final String dingtalk_robot_webhook = "https://oapi.dingtalk.com/robot/send?access_token=341ee2907f3ebc15dc495fb7771a646230058710999fec7838066c109849878e"; + private final String profile; + private final String applicationName; + + public AlertRcpDLQReporter(Environment environment) { + this.profile = environment.getProperty("spring.profiles.active"); + this.applicationName = environment.getProperty("spring.application.name"); + } + + @Override + public void process(TopicOffset v, String dlqName) { + log.warn("found DLQ"); + long count = v.getMaxOffset() - v.getMinOffset(); + if (count > 0) { + sendDingTalk(count, dlqName); + } + } + + @SneakyThrows + public void sendDingTalk(Long count, String dlqName) { + DingTalkClient client = new DefaultDingTalkClient(dingtalk_robot_webhook); + OapiRobotSendRequest request = new OapiRobotSendRequest(); + + request.setMsgtype("markdown"); + OapiRobotSendRequest.Markdown markdown = new OapiRobotSendRequest.Markdown(); + markdown.setTitle("Notice Env: " + profile); + markdown.setText("#### [" + profile + "]环境[" + applicationName + "]发现“DLQ”数据(Rpc)\n" + + "> ###### 1. 如需查看详情,[请点击这里](" + findDomain() + "m) \n" + + "> ###### 2. 如需调整 MQ DLQ 的监控状态: \n" + + "> ###### 3. DLQ名称:" + dlqName + " \n" + + "[关闭监控](" + findDomain() + "m/set?status=false) \n | " + + "[开启监控](" + findDomain() + "m/set?status=true) \n" + + "> 积累条数:" + count + " \n"); + request.setMarkdown(markdown); + log.warn(markdown.getText()); + OapiRobotSendResponse response = client.execute(request); + } + + private String findDomain() { + switch (profile) { + case "dev": + return "https://dev-app.axzo.cn/" + applicationName + "/web/we/s/"; + case "test": + return "https://test-api.axzo.cn/" + applicationName + "/web/we/s/"; + case "pre": + return "https://pre-api.axzo.cn/" + applicationName + "/web/we/s/"; + case "live": + return "https://live-api.axzo.cn/" + applicationName + "/web/we/s/"; + default: + return "https://api.axzo.cn/" + applicationName + "/web/we/s/"; + } + } +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/monitor/WorkflowEngineStarterDefaultMQMonitor.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/monitor/WorkflowEngineStarterDefaultMQMonitor.java new file mode 100644 index 000000000..578f05d3a --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/monitor/WorkflowEngineStarterDefaultMQMonitor.java @@ -0,0 +1,158 @@ +package cn.axzo.workflow.starter.mq.monitor; + +import cn.axzo.workflow.starter.WorkflowEngineStarterProperties; +import cn.axzo.workflow.starter.handler.monitor.BroadcastDLQReporter; +import cn.axzo.workflow.starter.handler.monitor.RpcDLQReporter; +import lombok.SneakyThrows; +import org.apache.rocketmq.common.admin.TopicStatsTable; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.context.SmartLifecycle; +import org.springframework.core.env.Environment; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; + +import static cn.axzo.workflow.common.constant.StarterConstants.K8S_POD_NAME_SPACE; +import static cn.axzo.workflow.common.constant.StarterConstants.MQ_GID_NAME_SEGMENT; +import static cn.axzo.workflow.starter.mq.monitor.console.WorkflowEngineStarterMQMonitorController.BROADCAST_CONSUMER_GROUP; +import static cn.axzo.workflow.starter.mq.monitor.console.WorkflowEngineStarterMQMonitorController.RPC_RETRY_CONSUMER_GROUP; + +/** + * 用于监控 Starter 关注的 Topic 相关的 Producer 与 Consumer 信息 + * + * @author wangli + * @since 2024/6/6 10:04 + */ +public class WorkflowEngineStarterDefaultMQMonitor implements SmartLifecycle { + + private static final Logger log = LoggerFactory.getLogger(WorkflowEngineStarterDefaultMQMonitor.class); + public static final String DLQ_PREFIX = "%DLQ%"; + private static final String BROADCAST = "broadcast"; + private static final String RPC = "rpc"; + /** + * 可能为 null + */ + private final DefaultMQAdminExt defaultMQAdminExt; + private final Environment environment; + private final ThreadPoolTaskScheduler taskScheduler; + private BroadcastDLQReporter broadcastDLQProcessor; + private RpcDLQReporter rpcDLQProcessor; + private final WorkflowEngineStarterProperties starterProperties; + private final AtomicBoolean running = new AtomicBoolean(false); + + public WorkflowEngineStarterDefaultMQMonitor(ObjectProvider mqAdminExtObjectProvider, + ObjectProvider broadcastDLQProcessorObjectProvider, + ObjectProvider rpcDLQProcessorObjectProvider, + WorkflowEngineStarterProperties workflowEngineStarterProperties, + Environment environment) { + this.defaultMQAdminExt = mqAdminExtObjectProvider.getIfAvailable(); + this.environment = environment; + if (workflowEngineStarterProperties.getAlert()) { + this.broadcastDLQProcessor = new AlertBroadcastDLQReporter(environment); + this.rpcDLQProcessor = new AlertRcpDLQReporter(environment); + } else { + this.broadcastDLQProcessor = broadcastDLQProcessorObjectProvider.getIfAvailable(() -> new BroadcastDLQReporter() { + }); + this.rpcDLQProcessor = rpcDLQProcessorObjectProvider.getIfAvailable(() -> new RpcDLQReporter() { + }); + } + this.starterProperties = workflowEngineStarterProperties; + this.taskScheduler = init(); + } + + private ThreadPoolTaskScheduler init() { + ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); + taskScheduler.setBeanName("Workflow-Engine-Watch-MQ-Scheduler"); + taskScheduler.initialize(); + return taskScheduler; + } + + @SneakyThrows + public void mqWatch() { + mqWatch(BROADCAST); + mqWatch(RPC); + } + + @SneakyThrows + public void mqWatch(String type) { + String segment = environment.getProperty(MQ_GID_NAME_SEGMENT); + String applicationName = environment.getProperty("spring.application.name"); + // 周期性比对 + if (Objects.equals(type, BROADCAST)) { + compareDLQ(String.format(BROADCAST_CONSUMER_GROUP, applicationName, segment), type); + } else if (Objects.equals(type, RPC)) { + compareDLQ(String.format(RPC_RETRY_CONSUMER_GROUP, applicationName, segment), type); + } + } + + private void compareDLQ(String consumerGroupName, String dqlType) { + log.info("current broadcast consumer group is :{}", consumerGroupName); + String myPodNamespace = environment.getProperty(K8S_POD_NAME_SPACE); + if (!StringUtils.hasText(myPodNamespace)) { + return; + } + try { + TopicStatsTable table = defaultMQAdminExt.examineTopicStats(DLQ_PREFIX + consumerGroupName); + if (Objects.isNull(table) || CollectionUtils.isEmpty(table.getOffsetTable())) { + return; + } + table.getOffsetTable().forEach((k, v) -> { + if (Objects.equals(DLQ_PREFIX + consumerGroupName, k.getTopic())) { + if (BROADCAST.equals(dqlType)) { + broadcastDLQProcessor.process(v, DLQ_PREFIX + consumerGroupName); + } else if (RPC.equals(dqlType)) { + rpcDLQProcessor.process(v, DLQ_PREFIX + consumerGroupName); + } + } + }); + } catch (Exception e) { + log.warn("monitor Broadcast DLQ error: {}", e.getMessage(), e); + } + } + + public void setBroadcastDLQProcessor(BroadcastDLQReporter broadcastDLQProcessor) { + this.broadcastDLQProcessor = broadcastDLQProcessor; + } + + public void setRpcDLQProcessor(RpcDLQReporter rpcDLQProcessor) { + this.rpcDLQProcessor = rpcDLQProcessor; + } + + @Override + public boolean isAutoStartup() { + if (starterProperties.getEnableDlqMonitor() && starterProperties.getRpcMonitorStatus()) { + taskScheduler.scheduleWithFixedDelay(() -> mqWatch(RPC), starterProperties.getDlqMonitorIntervalInMs()); + } + if (starterProperties.getEnableDlqMonitor() && starterProperties.getBroadcast().getMonitorStatus()) { + taskScheduler.scheduleWithFixedDelay(() -> mqWatch(BROADCAST), starterProperties.getDlqMonitorIntervalInMs()); + } + return false; + } + + @Override + public void start() { + if (running.compareAndSet(false, true)) { + log.info("starting workflow engine mq monitor"); + taskScheduler.scheduleWithFixedDelay(this::mqWatch, 30000); + } + } + + @Override + public void stop() { + if (running.compareAndSet(true, false)) { + log.info("destroying workflow engine mq monitor"); + taskScheduler.shutdown(); + } + } + + @Override + public boolean isRunning() { + return this.running.get(); + } +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/monitor/console/WorkflowEngineStarterMQMonitorController.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/monitor/console/WorkflowEngineStarterMQMonitorController.java new file mode 100644 index 000000000..2d4bf6dff --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/monitor/console/WorkflowEngineStarterMQMonitorController.java @@ -0,0 +1,147 @@ +package cn.axzo.workflow.starter.mq.monitor.console; + +import cn.axzo.workflow.starter.WorkflowEngineStarterProperties; +import cn.axzo.workflow.starter.handler.monitor.BroadcastDLQReporter; +import cn.axzo.workflow.starter.handler.monitor.RpcDLQReporter; +import cn.axzo.workflow.starter.mq.monitor.AlertBroadcastDLQReporter; +import cn.axzo.workflow.starter.mq.monitor.AlertRcpDLQReporter; +import cn.axzo.workflow.starter.mq.monitor.WorkflowEngineStarterDefaultMQMonitor; +import cn.azxo.framework.common.model.CommonResponse; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.env.Environment; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import static cn.axzo.workflow.common.constant.StarterConstants.MQ_GID_NAME_SEGMENT; +import static cn.axzo.workflow.starter.StarterRPCInvokeMQConfiguration.DEFAULT_EVENT; +import static cn.axzo.workflow.starter.mq.monitor.WorkflowEngineStarterDefaultMQMonitor.DLQ_PREFIX; + +/** + * Workflow Engine Starter MessageQueue Monitor Controller + * + * @author wangli + * @since 2024/6/6 10:04 + */ +@RestController +@RequestMapping("/web/we/s") +public class WorkflowEngineStarterMQMonitorController { + private static final Logger log = LoggerFactory.getLogger(WorkflowEngineStarterMQMonitorController.class); + @Resource + private WorkflowEngineStarterProperties starterProperties; + @Resource + private ObjectProvider mqAdminExtObjectProvider; + @Resource + private ObjectProvider monitorObjectProvider; + @Resource + private ObjectProvider broadcastDLQProcessorObjectProvider; + @Resource + private ObjectProvider rpcDLQProcessorObjectProvider; + @Resource + private Environment environment; + @Value("${spring.application.name}") + private String applicationName; + @Value("${spring.profiles.active}") + private String activeProfile; + public static String BROADCAST_CONSUMER_GROUP = "GID_%s_workflow_engine_%s_consumer"; + public static String RPC_RETRY_CONSUMER_GROUP = "GID_%s_workflow_engine_starter_%s_consumer"; + + @GetMapping("/m") + public CommonResponse> monitor() { + String topic = DEFAULT_EVENT + activeProfile; + Map result = new HashMap<>(); + if (!starterProperties.getEnableDlqMonitor()) { + result.put("Info", "未开启·死信队列·的监控,如需,请设置 workflow.engine.starter.enableDlqMonitor = true 后再重试!"); + } else { + mqAdminExtObjectProvider.ifAvailable(defaultMQAdminExt -> { + String segment = environment.getProperty(MQ_GID_NAME_SEGMENT); + String broadcastConsumer = String.format(BROADCAST_CONSUMER_GROUP, applicationName, segment); + String rpcConsumer = String.format(RPC_RETRY_CONSUMER_GROUP, applicationName, segment); + try { + result.put("Engine Broadcast MQ", defaultMQAdminExt.examineConsumeStats(broadcastConsumer)); + } catch (Exception e) { + log.warn("monitor controller examineConsumeStats broadcast error: {}", e.getMessage(), e); + } + try { + result.put("Starter RPC MQ", defaultMQAdminExt.examineConsumeStats(rpcConsumer)); + } catch (Exception e) { + log.warn("monitor controller examineConsumeStats rpc error: {}", e.getMessage(), e); + } +// result.put("BrokerClusterInfo", defaultMQAdminExt.examineBrokerClusterInfo()); +// result.put("TopicClusterList", defaultMQAdminExt.getTopicClusterList(topic)); + try { + result.put("TopicRouteInfo", defaultMQAdminExt.examineTopicRouteInfo(topic)); + } catch (Exception e) { + log.warn("monitor controller examineTopicRouteInfo error: {}", e.getMessage(), e); + } +// result.put("TopicStats", defaultMQAdminExt.examineTopicStats(topic)); + try { + result.put("Broadcast-DLQ", defaultMQAdminExt.examineTopicStats(DLQ_PREFIX + broadcastConsumer)); + } catch (Exception e) { + log.warn("monitor controller examineTopicStats broadcast error: {}", e.getMessage(), e); + } + try { + result.put("RPC-DLQ", defaultMQAdminExt.examineTopicStats(DLQ_PREFIX + rpcConsumer)); + } catch (Exception e) { + log.warn("monitor controller examineTopicStats rpc error: {}", e.getMessage(), e); + } + + }); + } + return CommonResponse.success(result); + } + + /** + * @param status + * @return + */ + @GetMapping("/m/set") + public CommonResponse changeMonitorState(@RequestParam("status") Boolean status) { + WorkflowEngineStarterDefaultMQMonitor monitor = monitorObjectProvider.getIfAvailable(); + if (Objects.isNull(monitor)) { + return CommonResponse.success("未开启·死信队列·的监控,如需,请设置 workflow.engine.starter.enableDlqMonitor = true 后再重试!"); + } + if (status) { + if (!monitor.isRunning()) { + monitor.start(); + } + return CommonResponse.success("已临时开始 MQ 死信队列监控!" + + "在应用重启前,将持续监控。重启后,将根据环境变量来确定是否监控。"); + } else { + if (monitor.isRunning()) { + monitor.stop(); + } + return CommonResponse.success("已临时关闭 MQ 死信队列监控!" + + "在应用重启前,将不再监控。重启后,将根据环境变量配置来确定是否监控。"); + } + } + + @GetMapping("/m/a/set") + public CommonResponse changeDLQAlert(@RequestParam("status") Boolean status) { + WorkflowEngineStarterDefaultMQMonitor monitor = monitorObjectProvider.getIfAvailable(); + if (Objects.isNull(monitor)) { + return CommonResponse.success("未开启·死信队列·的监控,如需,请设置 workflow.engine.starter.enableDlqMonitor = true 后再重试!"); + } + if (status) { + monitor.setBroadcastDLQProcessor(new AlertBroadcastDLQReporter(environment)); + monitor.setRpcDLQProcessor(new AlertRcpDLQReporter(environment)); + return CommonResponse.success("开启 DLQ 钉钉通知"); + } else { + monitor.setBroadcastDLQProcessor(broadcastDLQProcessorObjectProvider.getIfAvailable(() -> new BroadcastDLQReporter() { + })); + monitor.setRpcDLQProcessor(rpcDLQProcessorObjectProvider.getIfAvailable(() -> new RpcDLQReporter() { + })); + return CommonResponse.success("以关闭 DLQ 钉钉通知"); + } + } +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/retry/consumer/WorkflowEngineStarterRetryEventListener.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/retry/consumer/WorkflowEngineStarterRetryEventListener.java new file mode 100644 index 000000000..0b847c0d0 --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/retry/consumer/WorkflowEngineStarterRetryEventListener.java @@ -0,0 +1,183 @@ +package cn.axzo.workflow.starter.mq.retry.consumer; + +import cn.axzo.framework.rocketmq.Event; +import cn.axzo.framework.rocketmq.EventConsumer; +import cn.axzo.framework.rocketmq.EventHandler; +import cn.axzo.workflow.common.enums.WorkflowEngineEventEnum; +import cn.axzo.workflow.common.model.response.mq.WorkflowEngineStarterRpcInvokeDTO; +import cn.axzo.workflow.common.util.ThreadUtil; +import cn.axzo.workflow.starter.WorkflowEngineStarterProperties; +import cn.axzo.workflow.starter.api.WorkflowCoreService; +import cn.axzo.workflow.starter.api.WorkflowManageService; +import cn.axzo.workflow.starter.common.exception.WorkflowNoMethodException; +import com.alibaba.fastjson.JSON; +import lombok.SneakyThrows; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.env.Environment; +import org.springframework.util.CollectionUtils; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.net.ConnectException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static cn.axzo.workflow.common.enums.RpcInvokeModeEnum.SYNC; + +/** + * RPC 动作事件的 MQ 消费者(集成业务系统中的自产自销) + * + * @author wangli + * @since 2024/5/21 16:28 + */ +public class WorkflowEngineStarterRetryEventListener implements EventHandler, InitializingBean { + private final Logger log = LoggerFactory.getLogger(WorkflowEngineStarterRetryEventListener.class); + private final EventConsumer workflowEngineStarterEventConsumer; + private final Environment environment; + private final WorkflowEngineStarterProperties starterProperties; + private final String currentApplicationName; + private WorkflowCoreService workflowCoreService; + private WorkflowManageService workflowManageService; + private final Map methodCache = new HashMap<>(); + + class InterfaceMapping { + private final Object interfaceObject; + private final Method method; + + public InterfaceMapping(Object interfaceObject, Method method) { + this.interfaceObject = interfaceObject; + this.method = method; + } + + public Object getInterfaceObject() { + return interfaceObject; + } + + public Method getMethod() { + return method; + } + } + + public WorkflowEngineStarterRetryEventListener(EventConsumer workflowEngineStarterEventConsumer, + Environment environment, + WorkflowEngineStarterProperties starterProperties, + ObjectProvider workflowCoreServiceObjectProvider, + ObjectProvider workflowManageServiceObjectProvider) { + this.workflowEngineStarterEventConsumer = workflowEngineStarterEventConsumer; + this.environment = environment; + this.starterProperties = starterProperties; + this.currentApplicationName = environment.getProperty("spring.application.name"); + workflowCoreServiceObjectProvider.ifAvailable(core -> { + this.workflowCoreService = core; + parseWorkflowCoreService(); + }); + workflowManageServiceObjectProvider.ifAvailable(manage -> { + this.workflowManageService = manage; + parseWorkflowManageService(); + }); + } + + + private void parseWorkflowCoreService() { + Class coreService = (Class) workflowCoreService.getClass().getGenericInterfaces()[0]; + FeignClient feignClient = AnnotationUtils.findAnnotation(coreService, FeignClient.class); + if (Objects.isNull(feignClient)) { + throw new IllegalStateException("WorkflowCoreService 配置错误,没有找到 FeignClient 注解"); + } + + Method[] methods = coreService.getDeclaredMethods(); + for (Method method : methods) { + methodCache.put(method.getName(), new InterfaceMapping(workflowCoreService, method)); + } + } + + private void parseWorkflowManageService() { + Class manageService = (Class) workflowManageService.getClass().getGenericInterfaces()[0]; + FeignClient feignClient = AnnotationUtils.findAnnotation(manageService, FeignClient.class); + if (Objects.isNull(feignClient)) { + throw new IllegalStateException("WorkflowCoreService 配置错误,没有找到 FeignClient 注解"); + } + + Method[] methods = manageService.getDeclaredMethods(); + for (Method method : methods) { + methodCache.put(method.getName(), new InterfaceMapping(workflowManageService, method)); + } + } + + @SneakyThrows + @Override + public void onEvent(Event event, EventConsumer.Context context) { + log.info("WorkflowEngineClientRetryEventListener onEvent: {}", event.toPrettyJsonString()); + WorkflowEngineStarterRpcInvokeDTO dto = event.normalizedData(WorkflowEngineStarterRpcInvokeDTO.class); + + InterfaceMapping mapping = methodCache.getOrDefault(dto.getMethodName(), null); + if (Objects.isNull(mapping)) { + throw new WorkflowNoMethodException("Not method found: " + dto.getMethodName()); + } + try { + // 事件处理 RPC 请求, 强制使用同步模式 + ThreadUtil.set(SYNC); + Object[] args = convertToActualArgs(mapping.getMethod(), dto.getParameters()); + if (log.isDebugEnabled()) { + log.debug("event rpc request args: {}", JSON.toJSONString(args)); + } + Object invoke = mapping.getMethod().invoke(mapping.getInterfaceObject(), args); + if (log.isDebugEnabled()) { + log.debug("Event Invoke Result: {}", JSON.toJSONString(invoke)); + } + } catch (Throwable e) { + // 能抛出异常目前只有两种情况, 一个是网络异常, 另一个是对端服务内部异常 + Throwable cause = getRealCause(e); + log.warn("onEvent Invoke root exception cause by: {}", cause.getMessage(), cause); + if (cause instanceof ConnectException) { + // 而只有当网络异常时, 利用 RocketMQ 的重试机制 + throw cause; + } + } + } + + private Object[] convertToActualArgs(Method method, List parameters) { + Object[] args = new Object[parameters.size()]; + Class[] parameterTypes = method.getParameterTypes(); + Annotation[][] parameterAnnotations = method.getParameterAnnotations(); + for (int i = 0; i < parameters.size(); i++) { + Class parameterType = parameterTypes[i]; + Annotation[] annotations = parameterAnnotations[i]; + for (Annotation annotation : annotations) { + if (annotation instanceof RequestParam) { + List arg = JSON.parseObject(parameters.get(i), List.class); + args[i] = Objects.equals(parameterType, List.class) ? arg : CollectionUtils.isEmpty(arg) ? null : arg.get(0); + } else if (annotation instanceof PathVariable) { + args[i] = parameters.get(i); + } else if (annotation instanceof RequestBody) { + args[i] = JSON.parseObject(parameters.get(i), parameterType); + } + } + } + return args; + } + + private Throwable getRealCause(Throwable error) { + while (Objects.nonNull(error.getCause())) { + // TODO 如果有必要, 可以在这里对异常进行判断/过滤之类的动作, 目前暂未发现需要处理的场景, + // 后续如果发现, 则需要在此处进行扩展, 注意不是 HardCode, 而是用接口扩展 + error = error.getCause(); + } + return error; + } + + @Override + public void afterPropertiesSet() { + workflowEngineStarterEventConsumer.registerHandler(WorkflowEngineEventEnum.WORKFLOW_ENGINE_STARTER.getEventCode(currentApplicationName), this); + } +} diff --git a/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/retry/producer/RpcInvokeEventProducer.java b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/retry/producer/RpcInvokeEventProducer.java new file mode 100644 index 000000000..de7f2fc80 --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/mq/retry/producer/RpcInvokeEventProducer.java @@ -0,0 +1,132 @@ +package cn.axzo.workflow.starter.mq.retry.producer; + +import cn.axzo.framework.rocketmq.Event; +import cn.axzo.framework.rocketmq.RocketMQEventProducer; +import cn.axzo.framework.rocketmq.utils.TraceUtils; +import cn.axzo.workflow.common.enums.WorkflowEngineEventEnum; +import cn.axzo.workflow.common.model.response.mq.WorkflowEngineStarterRpcInvokeDTO; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import lombok.NonNull; +import org.apache.commons.collections4.ListUtils; +import org.apache.rocketmq.spring.core.RocketMQTemplate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeanUtils; + +import java.util.HashMap; +import java.util.List; +import java.util.Optional; +import java.util.function.BiConsumer; + +import static cn.axzo.workflow.common.constant.BpmnConstants.MQ_OWNERSHIP_APPLICATION; + +/** + * RPC 的调用动作的 MQ 事件生产者 + * + * @author wangli + * @since 2024/5/22 10:02 + */ +public class RpcInvokeEventProducer extends RocketMQEventProducer { + private final Logger log = LoggerFactory.getLogger(RpcInvokeEventProducer.class); + + private BiConsumer> sendBeforeCallback; + private BiConsumer> rollbackHandler; + private final String applicationName; + + public RpcInvokeEventProducer(RocketMQTemplate rocketMQTemplate, String defaultModule, + String appName, Context defaultContext, + BiConsumer> sendBeforeCallback, + BiConsumer> sendAfterCallback, + BiConsumer> rollbackHandler) { + super(rocketMQTemplate, defaultModule, appName, defaultContext, sendAfterCallback); + this.applicationName = appName; + this.sendBeforeCallback = sendBeforeCallback; + this.rollbackHandler = rollbackHandler; + } + + @Override + public void send(@NonNull Event event, @NonNull Context context) { + if (log.isDebugEnabled()) { + log.debug("发送事件内容: {}", event.toPrettyJsonString()); + } + if (sendBeforeCallback != null) { + sendBeforeCallback.accept(event, context); + } + // XXX:不要在send的时候修改event的值,有副作用。 + // 例如:当将同一个event发送到不同的topic的时候,buildSchemaHash会用不同的topic赋值两次,导致一些异常case + Event copiedEvent = Event.builder().build(); + BeanUtils.copyProperties(event, copiedEvent); + if (Strings.isNullOrEmpty(copiedEvent.getTargetId())) { + log.warn("targetId of event is black, best practice of targetId is present, event = {}", event.toJsonString()); + } + + if (copiedEvent.getData() == null) { + log.warn("data of event is empty, best practice of data must present, event = {}", event.toJsonString()); + } + Preconditions.checkArgument(!Strings.isNullOrEmpty(copiedEvent.getEventModule()), "eventModule不能为空"); + Preconditions.checkArgument(!Strings.isNullOrEmpty(copiedEvent.getEventName()), "eventName不能为空"); + + // 复制一份 context,并加入链路跟踪信息 traceId + HashMap newHeaders = Maps.newHashMap(Optional.ofNullable(context.getHeaders()).orElse(ImmutableMap.of())); + newHeaders.put(TraceUtils.TRACE_ID, TraceUtils.getOrCreateTraceId()); + newHeaders.put(TraceUtils.CTX_LOG_ID, TraceUtils.getOrCreateTraceId()); + newHeaders.put(TraceUtils.TRACE_ID_IN_MDC, TraceUtils.getOrCreateTraceId()); + newHeaders.put(MQ_OWNERSHIP_APPLICATION, applicationName); + final Context copiedContext = context.toBuilder().headers(newHeaders).build(); + + Runnable runnable = () -> { + try { + getSender().accept(copiedEvent, copiedContext); + } catch (Exception e) { + log.error("====MQ PRODUCER ====, context={}, message = {}", copiedContext, copiedEvent.toPrettyJsonString(), e); + throw e; + } + }; + + Runnable rollbackRunnable = () -> { + try { + getRollbackHandler().accept(copiedEvent, copiedContext); + } catch (Exception e) { + // ignore + } + }; + if (copiedContext.isTransactional()) { + // https://www.jianshu.com/p/59891ede5f90 + log.info("runnable is transaction event={}", copiedEvent.toJsonString()); + runnable.run(); + } else { + // 并发会导致事件时序出现问题. 所以串行执行 + log.info("runnable not transaction event={}", copiedEvent.toJsonString()); + getAfterCommitExecutor().executeAndRollback(runnable, rollbackRunnable); + } + + if (log.isDebugEnabled()) { + List runnables = ListUtils.emptyIfNull(getAfterCommitExecutor().getRunnables()); + log.debug("runnables.size(): {}", runnables.size()); + } + } + + @Override + public BiConsumer> getRollbackHandler() { + return rollbackHandler; + } + + /** + * 发送 RPC 调用的事件 + * + * @param eventEnum {@link WorkflowEngineEventEnum} + * @param data {@link WorkflowEngineStarterRpcInvokeDTO} + */ + public void send(WorkflowEngineEventEnum eventEnum, WorkflowEngineStarterRpcInvokeDTO data) { + send(Event.builder() + .shardingKey(applicationName) + .eventCode(eventEnum.getEventCode(applicationName)) + .targetId(data.getMethodName()) + .targetType(eventEnum.getTag()) + .data(data) + .build()); + } +} diff --git a/workflow-engine-spring-boot-starter/src/main/resources/META-INF/application.yml.demo b/workflow-engine-spring-boot-starter/src/main/resources/META-INF/application.yml.demo new file mode 100644 index 000000000..9e64ea2a5 --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/resources/META-INF/application.yml.demo @@ -0,0 +1,21 @@ +workflow: + engine: + starter: + invoke-mode: async # 调用 workflowCoreService 中方法的方式,可选值:sync、async + join-container-group: false # 本地开发机启动时,是否将 MQ 消费者加入到集群中,默认不加入,并默认生成 GID_${spring.application.name}_workflow_engine_${spring.profiles.active}_debugging_consumer 的消费者组,该参数只对非容器环境生效 + manageable: false # 是否可管理,默认 false, 开启后 Spring 容器中将多一个 WorkflowManageService 的 Bean,可调用受限访问接口 + enable-dlq-monitor: true # 是否启用 starter 中使用到的死信队列监控功能,仅是启动功能,未真实监控,需要配合 monitor-status = true 使用 + monitor-status: false # 应用启动后,如果该属性为 true,则真实启动监控,否则不启动 + dql-monitor-interval-in-ms: 4 * 60 * 60 * 1000 # 周期性获取死信队列监控信息的间隔时间 + broadcast: # 关于 Starter 中监听引擎广播的事件相关配置 + enable-filter-application-name: false # 过滤广播事件,只接收来自指定应用名的创建的流程的广播事件,默认 false,不进行过滤。如果为 true,则将配合 filter-application-names 参数进行广播事件的过滤。该过滤功能的前提是使用了 1.4.0 及以上版本的 workflow-engine-spring-boot-starter.jar + filter-application-names: # 配合 enable-filter-application-name 属性,当它为 true 时,该参数生效。设置的值为需要消费的 MQ 事件 + - 'workflow-engine' + enable-filter-definition-key: false # 过滤广播事件,只接收指定流程定义的创建的广播事件,默认 false,不进行过滤。如果为 true,则将配合 filter-process-definition-keys 参数进行广播事件的过滤。 + filter-process-definition-keys: # 配合 enable-filter-definition-key 属性,当它为 true 时,该参数生效。设置的值为需要消费的 MQ 事件 + - '1' + fail-handle-type: fail_over # 广播事件失败时,如何处理。可选值:fail_over(会按下面三个属性进行重试)、fail_fast(直接打印异常,跳过该消息的消费) + monitor-status: false # 广播 MQ 的私信队列监控 + num-of-retries: # 当 fail-handle-type 为 fail_over 时,广播事件失败时,重试次数 + wait-increase-factor: # 当 fail-handle-type 为 fail_over 时,广播事件失败时,重试间隔时间增长因子 + wait-time-in-ms: # 当 fail-handle-type 为 fail_over 时,广播事件失败时,重试间隔时间 diff --git a/workflow-engine-spring-boot-starter/src/main/resources/META-INF/spring.factories b/workflow-engine-spring-boot-starter/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..272293ebe --- /dev/null +++ b/workflow-engine-spring-boot-starter/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + cn.axzo.workflow.starter.WorkflowEngineStarterAutoConfiguration diff --git a/workflow-engine-spring-boot-starter/src/main/resources/imgs/business.png b/workflow-engine-spring-boot-starter/src/main/resources/imgs/business.png new file mode 100644 index 000000000..09051ab51 Binary files /dev/null and b/workflow-engine-spring-boot-starter/src/main/resources/imgs/business.png differ diff --git a/workflow-engine-spring-boot-starter/src/main/resources/imgs/process-model.png b/workflow-engine-spring-boot-starter/src/main/resources/imgs/process-model.png new file mode 100644 index 000000000..722b88b58 Binary files /dev/null and b/workflow-engine-spring-boot-starter/src/main/resources/imgs/process-model.png differ diff --git a/workflow-engine-support/pom.xml b/workflow-engine-support/pom.xml new file mode 100644 index 000000000..1c04d54f7 --- /dev/null +++ b/workflow-engine-support/pom.xml @@ -0,0 +1,44 @@ + + 4.0.0 + + cn.axzo.workflow + workflow-engine + ${revision} + + ${revision} + workflow-engine-support + jar + Workflow Engine Support + + + com.github.javaparser + javaparser-core + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + + run-custom-code + process-classes + + java + + + cn.axzo.workflow.support.api.CodeGenerator + + + + compile + + + + + + + diff --git a/workflow-engine-support/src/main/java/cn/axzo/workflow/support/api/CodeGenerator.java b/workflow-engine-support/src/main/java/cn/axzo/workflow/support/api/CodeGenerator.java new file mode 100644 index 000000000..3f3861f92 --- /dev/null +++ b/workflow-engine-support/src/main/java/cn/axzo/workflow/support/api/CodeGenerator.java @@ -0,0 +1,17 @@ +package cn.axzo.workflow.support.api; + +/** + * Maven plugin execution entrance + * + * @author wangli + * @since 2024/6/12 10:56 + */ +public class CodeGenerator { + + public static void main(String[] args) throws Exception { + + CoreServiceCodeGeneration.generate(); + ManageServiceCodeGeneration.generate(); + + } +} diff --git a/workflow-engine-support/src/main/java/cn/axzo/workflow/support/api/CoreServiceCodeGeneration.java b/workflow-engine-support/src/main/java/cn/axzo/workflow/support/api/CoreServiceCodeGeneration.java new file mode 100644 index 000000000..bb486ff9e --- /dev/null +++ b/workflow-engine-support/src/main/java/cn/axzo/workflow/support/api/CoreServiceCodeGeneration.java @@ -0,0 +1,183 @@ +package cn.axzo.workflow.support.api; + +import com.github.javaparser.ParserConfiguration; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.ImportDeclaration; +import com.github.javaparser.ast.Modifier; +import com.github.javaparser.ast.NodeList; +import com.github.javaparser.ast.body.BodyDeclaration; +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.body.MethodDeclaration; +import com.github.javaparser.ast.comments.BlockComment; +import com.github.javaparser.ast.expr.ClassExpr; +import com.github.javaparser.ast.expr.MethodCallExpr; +import com.github.javaparser.ast.expr.NameExpr; +import com.github.javaparser.ast.expr.StringLiteralExpr; +import com.github.javaparser.ast.expr.ThisExpr; +import com.github.javaparser.ast.stmt.BlockStmt; +import com.github.javaparser.ast.stmt.ExpressionStmt; +import com.github.javaparser.ast.stmt.ReturnStmt; +import com.github.javaparser.ast.type.ClassOrInterfaceType; +import com.github.javaparser.ast.type.Type; +import com.github.javaparser.printer.DefaultPrettyPrinter; +import com.github.javaparser.printer.Printer; +import com.github.javaparser.utils.SourceRoot; + +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Optional; + +/** + * 生成 WorkflowCoreService 接口 + * + * @author wangli + * @since 2024/6/7 17:14 + */ +public class CoreServiceCodeGeneration { + + public static void generate() throws Exception { + String newClassName = "WorkflowCoreService"; +// CompilationUnit originFile = parseOriginFile(newClassName); + CompilationUnit testGeneric = genericCode("cn.axzo.workflow.starter.api", newClassName); + writeToStarter(testGeneric, newClassName); + + } + + private static CompilationUnit parseOriginFile(String newClassName) throws Exception { + String projectRootDir = System.getProperty("user.dir"); + Path sourceCodeRoot = Paths.get(projectRootDir, "/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/api/"); + SourceRoot sourceRoot = new SourceRoot(sourceCodeRoot); + sourceRoot.getParserConfiguration().setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_8); + Printer printer = new DefaultPrettyPrinter(); + sourceRoot.setPrinter(printer::print); + sourceRoot.tryToParse(); + + List compilationUnits = sourceRoot.getCompilationUnits(); + for (CompilationUnit cu : compilationUnits) { + Optional primaryTypeName = cu.getPrimaryTypeName(); + if (primaryTypeName.isPresent() && primaryTypeName.get().equals(newClassName)) { + System.out.println("compilationUnit = " + cu); + return cu; + } + } + return null; + } + + private static void writeToStarter(CompilationUnit cu, String newClassName) { + String projectRootDir = System.getProperty("user.dir"); + String genericFilePath = Paths.get(projectRootDir, "/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/api/", newClassName + ".java").toString(); + try (FileWriter fileWriter = new FileWriter(genericFilePath)) { + fileWriter.append(cu.toString()); + } catch (IOException e) { + // nothing to do + } + } + + private static CompilationUnit genericCode(String pkg, String newClassName) throws Exception { + CompilationUnit cu = new CompilationUnit(pkg); + ClassOrInterfaceDeclaration interfaceDeclaration = setCommon(cu, newClassName); + + addMethods(interfaceDeclaration, cu); + addDefaultMethods(interfaceDeclaration); + return cu; + } + + private static void addDefaultMethods(ClassOrInterfaceDeclaration interfaceDeclaration) { + MethodDeclaration sync = createDefaultMethod("sync", interfaceDeclaration); + sync.setJavadocComment("强制使用‘同步’模式调用该方法,请在调用真实方法前调用该方法\r\n" + + "
\r\n" +
+                "  workflowCoreService.sync().createProcessInstance();\r\n" +
+                "
"); + + MethodDeclaration async = createDefaultMethod("async", interfaceDeclaration); + sync.setJavadocComment("强制使用‘异步’模式调用该方法,请在调用真实方法前调用该方法\r\n" + + "
\r\n" +
+                "  workflowCoreService.async().createProcessInstance();\r\n" +
+                "
"); + } + + private static MethodDeclaration createDefaultMethod(String methodName, ClassOrInterfaceDeclaration interfaceDeclaration) { + // 将sync方法添加到WorkflowCoreService类中 + MethodDeclaration methodDeclaration = interfaceDeclaration.addMethod(methodName, Modifier.Keyword.DEFAULT); + methodDeclaration.setType(new ClassOrInterfaceType("WorkflowCoreService")); + + // 创建方法体 + BlockStmt methodBody = new BlockStmt(); + + MethodCallExpr setMethodCall = new MethodCallExpr(new NameExpr("ThreadUtil"), "set"); + setMethodCall.addArgument(new NameExpr(methodName.toUpperCase())); // 使用NameExpr来表示枚举常量 + ExpressionStmt setStatement = new ExpressionStmt(setMethodCall); + + methodBody.addStatement(setStatement); + // 创建return this;的语句 + ReturnStmt returnStmt = new ReturnStmt(new ThisExpr()); + methodBody.addStatement(returnStmt); + + // 将方法体设置到sync方法中 + methodDeclaration.setBody(methodBody); + return methodDeclaration; + } + + private static void addMethods(ClassOrInterfaceDeclaration interfaceDeclaration, CompilationUnit cu) throws Exception { + String projectRootDir = System.getProperty("user.dir"); + Path sourceCodeRoot = Paths.get(projectRootDir, "/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/"); + SourceRoot sourceRoot = new SourceRoot(sourceCodeRoot); + sourceRoot.getParserConfiguration().setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_8); + Printer printer = new DefaultPrettyPrinter(); + sourceRoot.setPrinter(printer::print); + sourceRoot.tryToParse(); + + List compilationUnits = sourceRoot.getCompilationUnits(); + NodeList> targetMethods = interfaceDeclaration.getMembers(); + for (CompilationUnit apiCU : compilationUnits) { + if (!apiCU.getPrimaryType().filter(e -> e.getAnnotationByName("Manageable").isPresent()).isPresent()) { + addImports(apiCU, cu); + // 类上含有 @Manageable,不进行解析 + String interfaceName = apiCU.getPrimaryTypeName().get(); + List methods = apiCU.getInterfaceByName(interfaceName).get().findAll(MethodDeclaration.class); + for (MethodDeclaration method : methods) { + if (!method.getAnnotationByName("Manageable").isPresent()) { + MethodDeclaration methodDeclaration = method.clone(); + methodDeclaration.setType(changeReturnType(methodDeclaration.getType())); + targetMethods.add(methodDeclaration); + } + } + } + } + } + + private static Type changeReturnType(Type type) { + if (type instanceof ClassOrInterfaceType && type.asClassOrInterfaceType().getTypeArguments().isPresent()) { + NodeList types = type.asClassOrInterfaceType().getTypeArguments().orElse(new NodeList<>()); + if (types.size() != 1) { + throw new RuntimeException("workflow-engine-api 中的接口返回类型有误"); + } + return types.getFirst().get(); + } + return type; + } + + private static void addImports(CompilationUnit apiCU, CompilationUnit cu) { + for (ImportDeclaration sourceImport : apiCU.getImports()) { + cu.addImport(sourceImport.clone()); + } + } + + private static ClassOrInterfaceDeclaration setCommon(CompilationUnit cu, String newClassName) { + ClassOrInterfaceDeclaration classOrInterfaceDeclaration = cu.addInterface(newClassName).setPublic(true); + classOrInterfaceDeclaration.setJavadocComment("Workflow Engine Starter Core Service
" + + "该类是根据 API 动态生成,不同版本可能会开放新的接口,或回收一些旧接口"); + classOrInterfaceDeclaration.addAndGetAnnotation("FeignClient") + .addPair("name", new StringLiteralExpr("workflow-engine-starter-core")) + .addPair("url", new StringLiteralExpr("${axzo.service.workflow-engine:workflow-engine:8080}")) + .addPair("configuration", new ClassExpr(new ClassOrInterfaceType("WorkflowEngineStarterFeignConfiguration"))); + cu.addImport("cn.axzo.workflow.starter.feign.ext.WorkflowEngineStarterFeignConfiguration", false, false); + cu.addImport("cn.axzo.workflow.common.util.ThreadUtil", false, false); + cu.addImport("cn.axzo.workflow.common.enums.RpcInvokeModeEnum.ASYNC", true, false); + cu.addImport("cn.axzo.workflow.common.enums.RpcInvokeModeEnum.SYNC", true, false); + return classOrInterfaceDeclaration; + } +} diff --git a/workflow-engine-support/src/main/java/cn/axzo/workflow/support/api/ManageServiceCodeGeneration.java b/workflow-engine-support/src/main/java/cn/axzo/workflow/support/api/ManageServiceCodeGeneration.java new file mode 100644 index 000000000..c5ceff26b --- /dev/null +++ b/workflow-engine-support/src/main/java/cn/axzo/workflow/support/api/ManageServiceCodeGeneration.java @@ -0,0 +1,173 @@ +package cn.axzo.workflow.support.api; + +import com.github.javaparser.ParserConfiguration; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.ImportDeclaration; +import com.github.javaparser.ast.Modifier; +import com.github.javaparser.ast.NodeList; +import com.github.javaparser.ast.body.BodyDeclaration; +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.body.MethodDeclaration; +import com.github.javaparser.ast.expr.ClassExpr; +import com.github.javaparser.ast.expr.MethodCallExpr; +import com.github.javaparser.ast.expr.NameExpr; +import com.github.javaparser.ast.expr.StringLiteralExpr; +import com.github.javaparser.ast.expr.ThisExpr; +import com.github.javaparser.ast.stmt.BlockStmt; +import com.github.javaparser.ast.stmt.ExpressionStmt; +import com.github.javaparser.ast.stmt.ReturnStmt; +import com.github.javaparser.ast.type.ClassOrInterfaceType; +import com.github.javaparser.ast.type.Type; +import com.github.javaparser.printer.DefaultPrettyPrinter; +import com.github.javaparser.printer.Printer; +import com.github.javaparser.utils.SourceRoot; + +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +/** + * 生成 WorkflowManageService 接口 + * + * @author wangli + * @since 2024/6/11 17:38 + */ +public class ManageServiceCodeGeneration { + + public static void generate() throws Exception { + String newClassName = "WorkflowManageService"; + + CompilationUnit testGeneric = genericCode("cn.axzo.workflow.starter.api", newClassName); + writeToStarter(testGeneric, newClassName); + + } + + private static void writeToStarter(CompilationUnit cu, String newClassName) { + String projectRootDir = System.getProperty("user.dir"); + String genericFilePath = Paths.get(projectRootDir, "/workflow-engine-spring-boot-starter/src/main/java/cn/axzo/workflow/starter/api/", newClassName + ".java").toString(); + try (FileWriter fileWriter = new FileWriter(genericFilePath)) { + fileWriter.append(cu.toString()); + } catch (IOException e) { + // nothing to do + } + } + + private static CompilationUnit genericCode(String pkg, String newClassName) throws Exception { + CompilationUnit cu = new CompilationUnit(pkg); + ClassOrInterfaceDeclaration interfaceDeclaration = setCommon(cu, newClassName); + + addMethods(interfaceDeclaration, cu); + addDefaultMethods(interfaceDeclaration); + return cu; + } + + private static void addDefaultMethods(ClassOrInterfaceDeclaration interfaceDeclaration) { + MethodDeclaration sync = createDefaultMethod("sync", interfaceDeclaration); + sync.setJavadocComment("强制使用‘同步’模式调用该方法,请在调用真实方法前调用该方法\r\n" + + "
\r\n" +
+                "  workflowManageService.sync().getTenantIds();\r\n" +
+                "
"); + + MethodDeclaration async = createDefaultMethod("async", interfaceDeclaration); + async.setJavadocComment("强制使用‘异步’模式调用该方法,请在调用真实方法前调用该方法\r\n" + + "
\r\n" +
+                "  workflowManageService.async().getTenantIds();\r\n" +
+                "
"); + } + + private static MethodDeclaration createDefaultMethod(String methodName, ClassOrInterfaceDeclaration interfaceDeclaration) { + // 将sync方法添加到WorkflowCoreService类中 + MethodDeclaration methodDeclaration = interfaceDeclaration.addMethod(methodName, Modifier.Keyword.DEFAULT); + methodDeclaration.setType(new ClassOrInterfaceType("WorkflowManageService")); + + // 创建方法体 + BlockStmt methodBody = new BlockStmt(); + + MethodCallExpr setMethodCall = new MethodCallExpr(new NameExpr("ThreadUtil"), "set"); + setMethodCall.addArgument(new NameExpr(methodName.toUpperCase())); // 使用NameExpr来表示枚举常量 + ExpressionStmt setStatement = new ExpressionStmt(setMethodCall); + + methodBody.addStatement(setStatement); + // 创建return this;的语句 + ReturnStmt returnStmt = new ReturnStmt(new ThisExpr()); + methodBody.addStatement(returnStmt); + + // 将方法体设置到sync方法中 + methodDeclaration.setBody(methodBody); + return methodDeclaration; + } + + private static void addMethods(ClassOrInterfaceDeclaration interfaceDeclaration, CompilationUnit cu) throws Exception { + String projectRootDir = System.getProperty("user.dir"); + Path sourceCodeRoot = Paths.get(projectRootDir, "/workflow-engine-api/src/main/java/cn/axzo/workflow/client/feign/"); + SourceRoot sourceRoot = new SourceRoot(sourceCodeRoot); + sourceRoot.getParserConfiguration().setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_8); + Printer printer = new DefaultPrettyPrinter(); + sourceRoot.setPrinter(printer::print); + sourceRoot.tryToParse(); + + List compilationUnits = sourceRoot.getCompilationUnits(); + NodeList> targetMethods = interfaceDeclaration.getMembers(); + for (CompilationUnit apiCU : compilationUnits) { + // 如果类上包含 @Manageable 注解,则直接将所有方法解析进新目标接口中 + if (apiCU.getPrimaryType().filter(e -> e.getAnnotationByName("Manageable").isPresent()).isPresent()) { + addImports(apiCU, cu); + // 类上含有 @Manageable,不进行解析 + addMethod0(apiCU, targetMethods, false); + } else { + addImports(apiCU, cu); + addMethod0(apiCU, targetMethods, true); + } + } + } + + private static void addMethod0(CompilationUnit apiCU, NodeList> targetMethods, boolean containAnnotation) { + String interfaceName = apiCU.getPrimaryTypeName().get(); + List methods = apiCU.getInterfaceByName(interfaceName).get().findAll(MethodDeclaration.class); + for (MethodDeclaration method : methods) { + if (containAnnotation && method.getAnnotationByName("Manageable").isPresent()) { + MethodDeclaration methodDeclaration = method.clone(); + methodDeclaration.setType(changeReturnType(methodDeclaration.getType())); + targetMethods.add(methodDeclaration); + } else if (!containAnnotation && !method.getAnnotationByName("Manageable").isPresent()) { + MethodDeclaration methodDeclaration = method.clone(); + methodDeclaration.setType(changeReturnType(methodDeclaration.getType())); + targetMethods.add(methodDeclaration); + } + } + } + + private static Type changeReturnType(Type type) { + if (type instanceof ClassOrInterfaceType && type.asClassOrInterfaceType().getTypeArguments().isPresent()) { + NodeList types = type.asClassOrInterfaceType().getTypeArguments().orElse(new NodeList<>()); + if (types.size() != 1) { + throw new RuntimeException("workflow-engine-api 中的接口返回类型有误"); + } + return types.getFirst().get(); + } + return type; + } + + private static void addImports(CompilationUnit apiCU, CompilationUnit cu) { + for (ImportDeclaration sourceImport : apiCU.getImports()) { + cu.addImport(sourceImport.clone()); + } + } + + private static ClassOrInterfaceDeclaration setCommon(CompilationUnit cu, String newClassName) { + ClassOrInterfaceDeclaration classOrInterfaceDeclaration = cu.addInterface(newClassName).setPublic(true); + classOrInterfaceDeclaration.setJavadocComment("Workflow Engine Starter Management Service
" + + "该类是根据 API 动态生成,不同版本可能会开放新的接口,或回收一些旧接口"); + classOrInterfaceDeclaration.addAndGetAnnotation("FeignClient") + .addPair("name", new StringLiteralExpr("workflow-engine-starter-manage")) + .addPair("url", new StringLiteralExpr("${axzo.service.workflow-engine:workflow-engine:8080}")) + .addPair("configuration", new ClassExpr(new ClassOrInterfaceType("WorkflowEngineStarterFeignConfiguration"))); + cu.addImport("cn.axzo.workflow.starter.feign.ext.WorkflowEngineStarterFeignConfiguration", false, false); + cu.addImport("cn.axzo.workflow.common.util.ThreadUtil", false, false); + cu.addImport("cn.axzo.workflow.common.enums.RpcInvokeModeEnum.ASYNC", true, false); + cu.addImport("cn.axzo.workflow.common.enums.RpcInvokeModeEnum.SYNC", true, false); + return classOrInterfaceDeclaration; + } +}