mirror of
http://git.pig4cloud.com/pig/pigx.git
synced 2026-11-08 12:39:05 +08:00
feat(pigx-flow-biz): 实现触发器节点功能
- 在 NodeFormatUtil 中新增 TRIGGER 节点类型处理逻辑 - 重构 ModelUtil 中的 buildSimpleServiceTaskNode 通用方法,支持构建服务任务节点 - 新增 buildTriggerNode 方法,使用 TriggerServiceTask 处理触发器逻辑 - 新增 TriggerServiceTask 组件,支持调用外部 HTTP 接口并处理返回值映射 - 支持配置请求头、请求体和返回值映射到表单字段 - 提供异常处理机制,支持抛出异常或终止流程两种策略
This commit is contained in:
1 parent
b28d86cb59
commit
325f58996d
8 files changed
+307
-15
No files matched your search
+6
-1
@@ -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;
|
||||
|
||||
}
|
||||
+19
@@ -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;
|
||||
|
||||
}
|
||||
+202
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+22
-14
@@ -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>
|
||||
|
||||
+2
@@ -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
|
||||
|
||||
Reference in new issue
Block a user