feat(pigx-flow-biz): 实现触发器节点功能

- 在 NodeFormatUtil 中新增 TRIGGER 节点类型处理逻辑
- 重构 ModelUtil 中的 buildSimpleServiceTaskNode 通用方法,支持构建服务任务节点
- 新增 buildTriggerNode 方法,使用 TriggerServiceTask 处理触发器逻辑
- 新增 TriggerServiceTask 组件,支持调用外部 HTTP 接口并处理返回值映射
- 支持配置请求头、请求体和返回值映射到表单字段
- 提供异常处理机制,支持抛出异常或终止流程两种策略
This commit is contained in:
PIGCLOUD committed 2026-03-08 15:06:36 +08:00
1 parent b28d86cb59
commit 325f58996d
8 files changed
+307 -15

No files matched your search

@@ -36,7 +36,12 @@ public enum NodeTypeEnum {
* 抄送节点 - 仅用于通知,不阻塞流程执行,抄送人只需查看无需审批
*/
CC("抄送节点", 2, false),
/**
* 触发器节点 - 调用外部HTTP接口,支持传递表单数据并将返回值映射回流程变量
*/
TRIGGER("触发器", 11, false),
/**
* 条件分支 - 根据表单数据或表达式判断流程走向,只会选择一条分支执行
*/
@@ -160,4 +160,9 @@ public class Node {
*/
private TimerConfig timerConfig;
/**
* 触发器配置
*/
private TriggerConfig triggerConfig;
}
@@ -0,0 +1,28 @@
package com.pig4cloud.pigx.flow.dto;
import lombok.Data;
import java.util.List;
/**
* 触发器节点配置
*
* @author pigx
*/
@Data
public class TriggerConfig {
private String url;
private List<TriggerParam> headers;
private List<TriggerParam> body;
private List<TriggerReturnMapping> returnMapping;
/**
* 异常处理方式: THROW(抛出异常) | TERMINATE(终止流程)
*/
private String errorHandler;
}
@@ -0,0 +1,23 @@
package com.pig4cloud.pigx.flow.dto;
import lombok.Data;
/**
* 触发器请求参数
*
* @author pigx
*/
@Data
public class TriggerParam {
private String key;
private Boolean useForm;
private String formFieldId;
private String formFieldName;
private String value;
}
@@ -0,0 +1,19 @@
package com.pig4cloud.pigx.flow.dto;
import lombok.Data;
/**
* 触发器返回值映射
*
* @author pigx
*/
@Data
public class TriggerReturnMapping {
private String formFieldId;
private String formFieldName;
private String responseKey;
}
@@ -0,0 +1,202 @@
package com.pig4cloud.pigx.flow.support.tasks;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.pig4cloud.pigx.flow.dto.Node;
import com.pig4cloud.pigx.flow.dto.TriggerConfig;
import com.pig4cloud.pigx.flow.dto.TriggerParam;
import com.pig4cloud.pigx.flow.dto.TriggerReturnMapping;
import com.pig4cloud.pigx.flow.entity.ProcessInstanceRecord;
import com.pig4cloud.pigx.flow.service.IProcessInstanceRecordService;
import com.pig4cloud.pigx.flow.service.IProcessNodeDataService;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.flowable.common.engine.api.FlowableException;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.delegate.JavaDelegate;
import org.flowable.engine.impl.persistence.entity.ExecutionEntityImpl;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 触发器服务任务
* <p>
* 在流程执行过程中调用外部 HTTP POST 接口,
* 支持传递表单数据作为请求参数,并将返回值映射回流程变量。
* <p>
* HTTP 状态码 200 即视为调用成功,响应体按 JSON 对象解析后进行返回值映射。
* 异常处理支持两种策略:THROW(抛出异常)和 TERMINATE(终止流程)。
*
* @author pigx
* @date 2026-03-03
*/
@Slf4j
public class TriggerServiceTask implements JavaDelegate {
private static final int READ_TIMEOUT = 30000;
@SneakyThrows
@Override
public void execute(DelegateExecution execution) {
ExecutionEntityImpl entity = (ExecutionEntityImpl) execution;
String nodeId = entity.getActivityId();
String flowId = entity.getProcessDefinitionKey();
IProcessNodeDataService processNodeDataService = SpringUtil.getBean(IProcessNodeDataService.class);
Node node = processNodeDataService.getNodeData(flowId, nodeId).getData();
TriggerConfig config = node.getTriggerConfig();
if (config == null || StrUtil.isBlank(config.getUrl())) {
throw new FlowableException("触发器配置无效: URL为空");
}
// 构建请求头
Map<String, String> headerMap = new HashMap<>();
headerMap.put("Content-Type", "application/json");
if (CollUtil.isNotEmpty(config.getHeaders())) {
for (TriggerParam param : config.getHeaders()) {
String value = resolveParamValue(param, execution);
if (StrUtil.isNotBlank(param.getKey()) && value != null) {
headerMap.put(param.getKey(), value);
}
}
}
// 构建请求体
Map<String, Object> body = new HashMap<>();
if (CollUtil.isNotEmpty(config.getBody())) {
for (TriggerParam param : config.getBody()) {
Object value = resolveParamObjectValue(param, execution);
if (StrUtil.isNotBlank(param.getKey())) {
body.put(param.getKey(), value);
}
}
}
// 发起 POST 请求
try {
HttpResponse response = HttpRequest.post(config.getUrl())
.headerMap(headerMap, true)
.body(JSONUtil.toJsonStr(body))
.timeout(READ_TIMEOUT)
.execute();
// HTTP 状态码非 2xx 视为调用失败
if (!response.isOk()) {
String msg = StrUtil.format("HTTP请求失败: status={}, body={}", response.getStatus(), response.body());
log.warn("触发器返回异常: url={}, {}", config.getUrl(), msg);
handleError(config.getErrorHandler(), execution, msg);
return;
}
// 响应体按 JSON 对象解析,进行返回值映射
String responseBody = response.body();
if (StrUtil.isNotBlank(responseBody)) {
ObjectMapper objectMapper = SpringUtil.getBean(ObjectMapper.class);
Object data = objectMapper.readValue(responseBody, Object.class);
mapReturnValues(config.getReturnMapping(), data, execution);
}
} catch (Exception ex) {
log.error("触发器调用异常: url={}, error={}", config.getUrl(), ex.getMessage());
handleError(config.getErrorHandler(), execution, ex.getMessage());
}
}
private String resolveParamValue(TriggerParam param, DelegateExecution execution) {
Object value = resolveParamObjectValue(param, execution);
return value != null ? value.toString() : null;
}
private Object resolveParamObjectValue(TriggerParam param, DelegateExecution execution) {
if (Boolean.TRUE.equals(param.getUseForm()) && StrUtil.isNotBlank(param.getFormFieldId())) {
return execution.getVariable(param.getFormFieldId());
}
return param.getValue();
}
@SneakyThrows
@SuppressWarnings("unchecked")
private void mapReturnValues(List<TriggerReturnMapping> mappings, Object data, DelegateExecution execution) {
if (CollUtil.isEmpty(mappings) || data == null) {
return;
}
Map<String, Object> dataMap;
if (data instanceof Map) {
dataMap = (Map<String, Object>) data;
} else {
ObjectMapper objectMapper = SpringUtil.getBean(ObjectMapper.class);
dataMap = objectMapper.convertValue(data, Map.class);
}
// 收集需要更新的表单字段
Map<String, Object> formUpdates = new HashMap<>();
for (TriggerReturnMapping mapping : mappings) {
Object value = dataMap.get(mapping.getResponseKey());
if (value != null && StrUtil.isNotBlank(mapping.getFormFieldId())) {
execution.setVariable(mapping.getFormFieldId(), value);
formUpdates.put(mapping.getFormFieldId(), value);
}
}
// 同步更新 ProcessInstanceRecord.formData
if (CollUtil.isNotEmpty(formUpdates)) {
syncFormData(execution.getProcessInstanceId(), formUpdates);
}
}
/**
* 同步更新流程实例记录的表单数据
*/
@SneakyThrows
private void syncFormData(String processInstanceId, Map<String, Object> formUpdates) {
try {
ObjectMapper objectMapper = SpringUtil.getBean(ObjectMapper.class);
IProcessInstanceRecordService recordService = SpringUtil.getBean(IProcessInstanceRecordService.class);
ProcessInstanceRecord record = recordService.lambdaQuery()
.eq(ProcessInstanceRecord::getProcessInstanceId, processInstanceId)
.one();
if (record == null) {
return;
}
Map<String, Object> existingFormData = new HashMap<>();
if (StrUtil.isNotBlank(record.getFormData())) {
existingFormData = objectMapper.readValue(record.getFormData(), new TypeReference<Map<String, Object>>() {
});
}
existingFormData.putAll(formUpdates);
String updatedFormData = objectMapper.writeValueAsString(existingFormData);
recordService.lambdaUpdate()
.set(ProcessInstanceRecord::getFormData, updatedFormData)
.eq(ProcessInstanceRecord::getProcessInstanceId, processInstanceId)
.update();
log.debug("触发器同步更新流程实例 {} 的表单数据", processInstanceId);
} catch (Exception e) {
log.error("触发器同步更新表单数据失败: {}", e.getMessage(), e);
}
}
private void handleError(String handler, DelegateExecution execution, String msg) {
if ("TERMINATE".equals(handler)) {
RuntimeService runtimeService = SpringUtil.getBean(RuntimeService.class);
runtimeService.deleteProcessInstance(execution.getProcessInstanceId(), "触发器异常终止: " + msg);
}
throw new FlowableException("触发器调用失败: " + msg);
}
}
@@ -18,9 +18,11 @@ import com.pig4cloud.pigx.flow.support.listeners.FlowProcessEventListener;
import com.pig4cloud.pigx.flow.support.listeners.RootTaskCreateListener;
import com.pig4cloud.pigx.flow.support.tasks.ApproveServiceTask;
import com.pig4cloud.pigx.flow.support.tasks.CopyServiceTask;
import com.pig4cloud.pigx.flow.support.tasks.TriggerServiceTask;
import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.model.*;
import org.flowable.bpmn.model.Process;
import org.flowable.engine.delegate.JavaDelegate;
import java.util.ArrayList;
import java.util.List;
@@ -509,6 +511,10 @@ public class ModelUtil {
}
userTask.setTaskListeners(taskListeners);
}
// 触发器
if (node.getType() == NodeTypeEnum.TRIGGER.getValue().intValue()) {
flowElementList.add(buildTriggerNode(node));
}
return userTask;
}
@@ -660,31 +666,25 @@ public class ModelUtil {
}
/**
* 创建抄送节点
* 创建简单服务任务节点(通用方法)
* <p>
* 创建抄送服务任务节点,用于将流程信息通知给相关人员。
* 配置了失败重试机制,确保抄送任务的可靠性。
* 创建一个通用的服务任务节点,配置了失败重试机制(重试5次,每次间隔1分钟)。
*
* @param node 抄送节点数据
* @param node 节点数据
* @param delegateClass JavaDelegate 实现类
* @return 服务任务元素
*/
private static FlowElement buildCCNode(Node node) {
private static FlowElement buildSimpleServiceTaskNode(Node node, Class<? extends JavaDelegate> delegateClass) {
ServiceTask serviceTask = new ServiceTask();
serviceTask.setId(node.getId());
serviceTask.setName(node.getName());
serviceTask.setAsynchronous(false);
serviceTask.setImplementationType("class");
serviceTask.setImplementation(CopyServiceTask.class.getCanonicalName());
serviceTask.setImplementation(delegateClass.getCanonicalName());
ExtensionElement e = new ExtensionElement();
{
e.setName("flowable:failedJobRetryTimeCycle");
// 上面的例子会让作业执行器重试5次,并在每次重试前等待1分钟。
e.setElementText("R5/PT1M");
}
e.setName("flowable:failedJobRetryTimeCycle");
e.setElementText("R5/PT1M");
serviceTask.addExtensionElement(e);
return serviceTask;
@@ -753,6 +753,14 @@ public class ModelUtil {
return catchEvent;
}
private static FlowElement buildCCNode(Node node) {
return buildSimpleServiceTaskNode(node, CopyServiceTask.class);
}
private static FlowElement buildTriggerNode(Node node) {
return buildSimpleServiceTaskNode(node, TriggerServiceTask.class);
}
/**
* 创建节点内部连接线
* <p>
@@ -203,6 +203,8 @@ public class NodeFormatUtil {
List<UserVo> tempList = buildUser(nodeUserList);
userVoList.addAll(tempList);
} else if (type == NodeTypeEnum.TRIGGER.getValue().intValue()) {
// 触发器节点
}
// 根据userVo 的ID 去重userVoList