From b9c5975ae1377bfe5a5ba9043f192184b2ad6b06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=96=AF=E7=8B=82=E7=9A=84=E7=8B=AE=E5=AD=90Li?= <15040126243@163.com> Date: Mon, 26 Oct 2026 18:50:17 +0800 Subject: [PATCH] =?UTF-8?q?update=20=E4=BC=98=E5=8C=96=20WorkflowService?= =?UTF-8?q?=20=E5=A2=9E=E5=8A=A0=E4=B8=80=E4=BA=9B=E5=B8=B8=E7=94=A8?= =?UTF-8?q?=E7=9A=84=E4=BA=8B=E4=BB=B6=E8=8A=82=E7=82=B9api=E6=96=B9?= =?UTF-8?q?=E6=B3=95=20=E5=AF=B9=E5=A4=96=E6=9A=B4=E6=BC=8F=E6=96=B9?= =?UTF-8?q?=E4=BE=BF=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dromara/workflow/api/WorkflowService.java | 47 +++++++ .../api/domain/StartMessageEventDTO.java | 28 ++++ .../api/domain/StartTimerEventDTO.java | 28 ++++ .../api/domain/TriggerMessageEventDTO.java | 26 ++++ .../service/impl/WorkflowServiceImpl.java | 57 ++++++++ .../impl/WorkflowServiceImplEventTest.java | 131 ++++++++++++++++++ 6 files changed, 317 insertions(+) create mode 100644 ruoyi-api/src/main/java/org/dromara/workflow/api/domain/StartMessageEventDTO.java create mode 100644 ruoyi-api/src/main/java/org/dromara/workflow/api/domain/StartTimerEventDTO.java create mode 100644 ruoyi-api/src/main/java/org/dromara/workflow/api/domain/TriggerMessageEventDTO.java create mode 100644 ruoyi-modules/ruoyi-workflow/src/test/java/org/dromara/workflow/service/impl/WorkflowServiceImplEventTest.java diff --git a/ruoyi-api/src/main/java/org/dromara/workflow/api/WorkflowService.java b/ruoyi-api/src/main/java/org/dromara/workflow/api/WorkflowService.java index 16ea0551e..ef09b936c 100644 --- a/ruoyi-api/src/main/java/org/dromara/workflow/api/WorkflowService.java +++ b/ruoyi-api/src/main/java/org/dromara/workflow/api/WorkflowService.java @@ -3,6 +3,9 @@ package org.dromara.workflow.api; import org.dromara.workflow.api.domain.CompleteTaskDTO; import org.dromara.workflow.api.domain.StartProcessDTO; import org.dromara.workflow.api.domain.StartProcessReturnDTO; +import org.dromara.workflow.api.domain.StartMessageEventDTO; +import org.dromara.workflow.api.domain.StartTimerEventDTO; +import org.dromara.workflow.api.domain.TriggerMessageEventDTO; import java.util.List; import java.util.Map; @@ -96,4 +99,48 @@ public interface WorkflowService { * @return 首节点办理成功返回 {@code true} */ boolean startCompleteTask(StartProcessDTO startProcess); + + /** + * 外部消息按流程编码和事件名称启动新实例。 + * + * @param request 消息启动参数,业务 ID 由调用方提供 + * @return 新建流程实例 ID + */ + Long startByMessageEvent(StartMessageEventDTO request); + + /** + * 触发实例中等待的消息捕获或消息边界事件。重复投递时引擎保持实例不变。 + * + * @param request 消息事件参数 + * @return 流程实例 ID + */ + Long triggerMessageEvent(TriggerMessageEventDTO request); + + /** + * 手动发起定时启动事件,不改变等待记录及下一次自动触发时间。 + * + * @param request 定时启动参数,业务 ID 留空时生成数字雪花 ID + * @return 新建流程实例 ID + */ + Long startByTimerEvent(StartTimerEventDTO request); + + /** + * 从实例级错误捕获节点处理外部错误。 + * + * @param instanceId 流程实例 ID + * @param errorCode 错误码,精确匹配优先于通配 + * @param errorMessage 错误描述 + * @return 错误事件已被处理时返回 true + */ + boolean throwInstanceErrorEvent(Long instanceId, String errorCode, String errorMessage); + + /** + * 从当前待办的错误边界节点处理外部错误。 + * + * @param taskId 当前待办任务 ID + * @param errorCode 错误码,精确匹配优先于通配 + * @param errorMessage 错误描述 + * @return 错误事件已被处理时返回 true + */ + boolean throwTaskErrorEvent(Long taskId, String errorCode, String errorMessage); } diff --git a/ruoyi-api/src/main/java/org/dromara/workflow/api/domain/StartMessageEventDTO.java b/ruoyi-api/src/main/java/org/dromara/workflow/api/domain/StartMessageEventDTO.java new file mode 100644 index 000000000..a3b71298b --- /dev/null +++ b/ruoyi-api/src/main/java/org/dromara/workflow/api/domain/StartMessageEventDTO.java @@ -0,0 +1,28 @@ +package org.dromara.workflow.api.domain; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Map; + +/** + * 消息启动事件请求。 + */ +@Data +public class StartMessageEventDTO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 业务主键,调用方必须提供。 */ + private String businessId; + /** 流程定义编码。 */ + private String flowCode; + /** 消息启动事件名称。 */ + private String eventName; + /** 合并到流程变量的消息数据,可为空。 */ + private Map payload; + /** 操作人,留空时使用系统处理人。 */ + private String handler; +} diff --git a/ruoyi-api/src/main/java/org/dromara/workflow/api/domain/StartTimerEventDTO.java b/ruoyi-api/src/main/java/org/dromara/workflow/api/domain/StartTimerEventDTO.java new file mode 100644 index 000000000..4ee0910e7 --- /dev/null +++ b/ruoyi-api/src/main/java/org/dromara/workflow/api/domain/StartTimerEventDTO.java @@ -0,0 +1,28 @@ +package org.dromara.workflow.api.domain; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Map; + +/** + * 手动发起定时启动事件请求。 + */ +@Data +public class StartTimerEventDTO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 业务主键,留空时生成数字雪花 ID。 */ + private String businessId; + /** 已发布的流程定义 ID。 */ + private Long definitionId; + /** 定时启动事件节点编码。 */ + private String nodeCode; + /** 合并到流程变量的业务数据,可为空。 */ + private Map payload; + /** 操作人,留空时使用系统处理人。 */ + private String handler; +} diff --git a/ruoyi-api/src/main/java/org/dromara/workflow/api/domain/TriggerMessageEventDTO.java b/ruoyi-api/src/main/java/org/dromara/workflow/api/domain/TriggerMessageEventDTO.java new file mode 100644 index 000000000..e38f930ed --- /dev/null +++ b/ruoyi-api/src/main/java/org/dromara/workflow/api/domain/TriggerMessageEventDTO.java @@ -0,0 +1,26 @@ +package org.dromara.workflow.api.domain; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Map; + +/** + * 触发实例中等待的消息事件请求。 + */ +@Data +public class TriggerMessageEventDTO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 已存在的流程实例 ID。 */ + private Long instanceId; + /** 等待中的消息捕获或消息边界事件名称。 */ + private String eventName; + /** 合并到流程变量的消息数据,可为空。 */ + private Map payload; + /** 操作人,留空时使用系统处理人。 */ + private String handler; +} diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/WorkflowServiceImpl.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/WorkflowServiceImpl.java index 3955d163a..71bca9879 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/WorkflowServiceImpl.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/WorkflowServiceImpl.java @@ -2,13 +2,20 @@ package org.dromara.workflow.service.impl; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.toolkit.IdWorker; import lombok.RequiredArgsConstructor; +import org.dromara.common.core.exception.ServiceException; import org.dromara.common.core.utils.StringUtils; +import org.dromara.warm.flow.core.FlowEngine; +import org.dromara.warm.flow.core.workflow.context.WorkflowContext; import org.dromara.warm.flow.orm.entity.FlowInstance; import org.dromara.workflow.api.WorkflowService; import org.dromara.workflow.api.domain.CompleteTaskDTO; +import org.dromara.workflow.api.domain.StartMessageEventDTO; import org.dromara.workflow.api.domain.StartProcessDTO; import org.dromara.workflow.api.domain.StartProcessReturnDTO; +import org.dromara.workflow.api.domain.StartTimerEventDTO; +import org.dromara.workflow.api.domain.TriggerMessageEventDTO; import org.dromara.workflow.common.ConditionalOnEnable; import org.dromara.workflow.common.enums.MessageTypeEnum; import org.dromara.workflow.domain.FlowInstanceBizExt; @@ -170,4 +177,54 @@ public class WorkflowServiceImpl implements WorkflowService { taskBo.setHandler(startProcess.getHandler()); return flwTaskService.completeTask(taskBo); } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long startByMessageEvent(StartMessageEventDTO request) { + if (request == null) { + throw new ServiceException("消息启动参数不能为空"); + } + return FlowEngine.insService().startByMessage(request.getBusinessId(), request.getFlowCode(), + request.getEventName(), request.getPayload(), eventContext(request.getHandler())).getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long triggerMessageEvent(TriggerMessageEventDTO request) { + if (request == null) { + throw new ServiceException("消息事件参数不能为空"); + } + return FlowEngine.insService().triggerMessage(request.getInstanceId(), request.getEventName(), + request.getPayload(), eventContext(request.getHandler())).getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long startByTimerEvent(StartTimerEventDTO request) { + if (request == null) { + throw new ServiceException("定时启动参数不能为空"); + } + String businessId = StringUtils.isNotBlank(request.getBusinessId()) + ? request.getBusinessId().trim() : String.valueOf(IdWorker.getId()); + return FlowEngine.insService().startByTimer(businessId, request.getDefinitionId(), + request.getNodeCode(), request.getPayload(), eventContext(request.getHandler())).getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean throwInstanceErrorEvent(Long instanceId, String errorCode, String errorMessage) { + return FlowEngine.insService().throwError(instanceId, errorCode, errorMessage); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean throwTaskErrorEvent(Long taskId, String errorCode, String errorMessage) { + return FlowEngine.taskService().throwError(taskId, errorCode, errorMessage); + } + + private WorkflowContext eventContext(String handler) { + WorkflowContext context = new WorkflowContext(); + context.setHandler(StringUtils.isNotBlank(handler) ? handler : FlowEngine.getFlowConfig().getSystemHandler()); + return context; + } } diff --git a/ruoyi-modules/ruoyi-workflow/src/test/java/org/dromara/workflow/service/impl/WorkflowServiceImplEventTest.java b/ruoyi-modules/ruoyi-workflow/src/test/java/org/dromara/workflow/service/impl/WorkflowServiceImplEventTest.java new file mode 100644 index 000000000..ab1485462 --- /dev/null +++ b/ruoyi-modules/ruoyi-workflow/src/test/java/org/dromara/workflow/service/impl/WorkflowServiceImplEventTest.java @@ -0,0 +1,131 @@ +package org.dromara.workflow.service.impl; + +import org.dromara.warm.flow.core.FlowEngine; +import org.dromara.warm.flow.core.config.WarmFlow; +import org.dromara.warm.flow.core.entity.Instance; +import org.dromara.warm.flow.core.service.InsService; +import org.dromara.warm.flow.core.service.TaskService; +import org.dromara.warm.flow.core.workflow.context.WorkflowContext; +import org.dromara.workflow.api.domain.StartMessageEventDTO; +import org.dromara.workflow.api.domain.StartTimerEventDTO; +import org.dromara.workflow.api.domain.TriggerMessageEventDTO; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.MockedStatic; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class WorkflowServiceImplEventTest { + + private final WorkflowServiceImpl service = new WorkflowServiceImpl(null, null); + + @Test + void messageStartPassesPayloadAndHandler() { + StartMessageEventDTO request = new StartMessageEventDTO(); + request.setBusinessId("order-1"); + request.setFlowCode("order_flow"); + request.setEventName("paid"); + request.setPayload(Map.of("amount", 20)); + request.setHandler("42"); + InsService insService = mock(InsService.class); + Instance instance = mock(Instance.class); + when(instance.getId()).thenReturn(100L); + when(insService.startByMessage(eq("order-1"), eq("order_flow"), eq("paid"), eq(request.getPayload()), + any(WorkflowContext.class))).thenReturn(instance); + + try (MockedStatic engine = mockStatic(FlowEngine.class)) { + engine.when(FlowEngine::insService).thenReturn(insService); + assertEquals(100L, service.startByMessageEvent(request)); + ArgumentCaptor context = ArgumentCaptor.forClass(WorkflowContext.class); + verify(insService).startByMessage(eq("order-1"), eq("order_flow"), eq("paid"), + eq(request.getPayload()), context.capture()); + assertEquals("42", context.getValue().getHandler()); + } + } + + @Test + void timerStartGeneratesNumericBusinessIdAndUsesSystemHandler() { + StartTimerEventDTO request = new StartTimerEventDTO(); + request.setDefinitionId(10L); + request.setNodeCode("timerStart"); + InsService insService = mock(InsService.class); + Instance instance = mock(Instance.class); + when(instance.getId()).thenReturn(200L); + when(insService.startByTimer(any(String.class), eq(10L), eq("timerStart"), + any(), any(WorkflowContext.class))).thenReturn(instance); + WarmFlow config = mock(WarmFlow.class); + when(config.getSystemHandler()).thenReturn("system"); + + try (MockedStatic engine = mockStatic(FlowEngine.class)) { + engine.when(FlowEngine::insService).thenReturn(insService); + engine.when(FlowEngine::getFlowConfig).thenReturn(config); + assertEquals(200L, service.startByTimerEvent(request)); + ArgumentCaptor businessId = ArgumentCaptor.forClass(String.class); + ArgumentCaptor context = ArgumentCaptor.forClass(WorkflowContext.class); + verify(insService).startByTimer(businessId.capture(), eq(10L), eq("timerStart"), + any(), context.capture()); + assertTrue(businessId.getValue().matches("[0-9]+")); + assertEquals("system", context.getValue().getHandler()); + } + } + + @Test + void timerStartKeepsProvidedBusinessIdAndHandler() { + StartTimerEventDTO request = new StartTimerEventDTO(); + request.setBusinessId("manual-42"); + request.setDefinitionId(10L); + request.setNodeCode("timerStart"); + request.setHandler("operator"); + request.setPayload(Map.of("reason", "retry")); + InsService insService = mock(InsService.class); + Instance instance = mock(Instance.class); + when(instance.getId()).thenReturn(201L); + when(insService.startByTimer(eq("manual-42"), eq(10L), eq("timerStart"), + eq(request.getPayload()), any(WorkflowContext.class))).thenReturn(instance); + + try (MockedStatic engine = mockStatic(FlowEngine.class)) { + engine.when(FlowEngine::insService).thenReturn(insService); + assertEquals(201L, service.startByTimerEvent(request)); + ArgumentCaptor context = ArgumentCaptor.forClass(WorkflowContext.class); + verify(insService).startByTimer(eq("manual-42"), eq(10L), eq("timerStart"), + eq(request.getPayload()), context.capture()); + assertEquals("operator", context.getValue().getHandler()); + } + } + + @Test + void messageAndErrorEventsDelegateToEngine() { + TriggerMessageEventDTO request = new TriggerMessageEventDTO(); + request.setInstanceId(300L); + request.setEventName("confirmed"); + request.setHandler("42"); + InsService insService = mock(InsService.class); + TaskService taskService = mock(TaskService.class); + Instance instance = mock(Instance.class); + when(instance.getId()).thenReturn(300L); + when(insService.triggerMessage(eq(300L), eq("confirmed"), any(), any(WorkflowContext.class))) + .thenReturn(instance); + when(insService.throwError(300L, "external_error", "failed")).thenReturn(true); + when(taskService.throwError(400L, "external_error", "failed")).thenReturn(true); + + try (MockedStatic engine = mockStatic(FlowEngine.class)) { + engine.when(FlowEngine::insService).thenReturn(insService); + engine.when(FlowEngine::taskService).thenReturn(taskService); + assertEquals(300L, service.triggerMessageEvent(request)); + ArgumentCaptor context = ArgumentCaptor.forClass(WorkflowContext.class); + verify(insService).triggerMessage(eq(300L), eq("confirmed"), any(), context.capture()); + assertEquals("42", context.getValue().getHandler()); + assertTrue(service.throwInstanceErrorEvent(300L, "external_error", "failed")); + assertTrue(service.throwTaskErrorEvent(400L, "external_error", "failed")); + } + } +}