update 优化 WorkflowService 增加一些常用的事件节点api方法 对外暴漏方便调用

This commit is contained in:
疯狂的狮子Li 2026-10-26 18:50:17 +08:00
parent 42e7196b13
commit b9c5975ae1
6 changed files with 317 additions and 0 deletions

View File

@ -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);
}

View File

@ -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<String, Object> payload;
/** 操作人,留空时使用系统处理人。 */
private String handler;
}

View File

@ -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<String, Object> payload;
/** 操作人,留空时使用系统处理人。 */
private String handler;
}

View File

@ -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<String, Object> payload;
/** 操作人,留空时使用系统处理人。 */
private String handler;
}

View File

@ -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;
}
}

View File

@ -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<FlowEngine> engine = mockStatic(FlowEngine.class)) {
engine.when(FlowEngine::insService).thenReturn(insService);
assertEquals(100L, service.startByMessageEvent(request));
ArgumentCaptor<WorkflowContext> 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<FlowEngine> engine = mockStatic(FlowEngine.class)) {
engine.when(FlowEngine::insService).thenReturn(insService);
engine.when(FlowEngine::getFlowConfig).thenReturn(config);
assertEquals(200L, service.startByTimerEvent(request));
ArgumentCaptor<String> businessId = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<WorkflowContext> 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<FlowEngine> engine = mockStatic(FlowEngine.class)) {
engine.when(FlowEngine::insService).thenReturn(insService);
assertEquals(201L, service.startByTimerEvent(request));
ArgumentCaptor<WorkflowContext> 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<FlowEngine> engine = mockStatic(FlowEngine.class)) {
engine.when(FlowEngine::insService).thenReturn(insService);
engine.when(FlowEngine::taskService).thenReturn(taskService);
assertEquals(300L, service.triggerMessageEvent(request));
ArgumentCaptor<WorkflowContext> 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"));
}
}
}