diff --git a/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/constant/NodeTypeEnum.java b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/constant/NodeTypeEnum.java index c42a70477..3620a3d95 100644 --- a/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/constant/NodeTypeEnum.java +++ b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/constant/NodeTypeEnum.java @@ -36,7 +36,12 @@ public enum NodeTypeEnum { * 抄送节点 - 仅用于通知,不阻塞流程执行,抄送人只需查看无需审批 */ CC("抄送节点", 2, false), - + + /** + * 触发器节点 - 调用外部HTTP接口,支持传递表单数据并将返回值映射回流程变量 + */ + TRIGGER("触发器", 11, false), + /** * 条件分支 - 根据表单数据或表达式判断流程走向,只会选择一条分支执行 */ diff --git a/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/Node.java b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/Node.java index 37aa8d8ad..9c116da2b 100644 --- a/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/Node.java +++ b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/Node.java @@ -160,4 +160,9 @@ public class Node { */ private TimerConfig timerConfig; + /** + * 触发器配置 + */ + private TriggerConfig triggerConfig; + } diff --git a/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/TriggerConfig.java b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/TriggerConfig.java new file mode 100644 index 000000000..5fbbdd746 --- /dev/null +++ b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/TriggerConfig.java @@ -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 headers; + + private List body; + + private List returnMapping; + + /** + * 异常处理方式: THROW(抛出异常) | TERMINATE(终止流程) + */ + private String errorHandler; + +} diff --git a/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/TriggerParam.java b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/TriggerParam.java new file mode 100644 index 000000000..d594ea7c6 --- /dev/null +++ b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/TriggerParam.java @@ -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; + +} diff --git a/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/TriggerReturnMapping.java b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/TriggerReturnMapping.java new file mode 100644 index 000000000..add32441b --- /dev/null +++ b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/TriggerReturnMapping.java @@ -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; + +} diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/tasks/TriggerServiceTask.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/tasks/TriggerServiceTask.java new file mode 100644 index 000000000..d31d04be8 --- /dev/null +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/tasks/TriggerServiceTask.java @@ -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; + +/** + * 触发器服务任务 + *

+ * 在流程执行过程中调用外部 HTTP POST 接口, + * 支持传递表单数据作为请求参数,并将返回值映射回流程变量。 + *

+ * 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 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 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 mappings, Object data, DelegateExecution execution) { + if (CollUtil.isEmpty(mappings) || data == null) { + return; + } + + Map dataMap; + if (data instanceof Map) { + dataMap = (Map) data; + } else { + ObjectMapper objectMapper = SpringUtil.getBean(ObjectMapper.class); + dataMap = objectMapper.convertValue(data, Map.class); + } + + // 收集需要更新的表单字段 + Map 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 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 existingFormData = new HashMap<>(); + if (StrUtil.isNotBlank(record.getFormData())) { + existingFormData = objectMapper.readValue(record.getFormData(), new TypeReference>() { + }); + } + + 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); + } + +} diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/utils/ModelUtil.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/utils/ModelUtil.java index 089247525..b31e6ddc9 100644 --- a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/utils/ModelUtil.java +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/utils/ModelUtil.java @@ -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 { } /** - * 创建抄送节点 + * 创建简单服务任务节点(通用方法) *

- * 创建抄送服务任务节点,用于将流程信息通知给相关人员。 - * 配置了失败重试机制,确保抄送任务的可靠性。 + * 创建一个通用的服务任务节点,配置了失败重试机制(重试5次,每次间隔1分钟)。 * - * @param node 抄送节点数据 + * @param node 节点数据 + * @param delegateClass JavaDelegate 实现类 * @return 服务任务元素 */ - private static FlowElement buildCCNode(Node node) { - + private static FlowElement buildSimpleServiceTaskNode(Node node, Class 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); + } + /** * 创建节点内部连接线 *

diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/utils/NodeFormatUtil.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/utils/NodeFormatUtil.java index 35e142123..8edfa0500 100644 --- a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/utils/NodeFormatUtil.java +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/utils/NodeFormatUtil.java @@ -203,6 +203,8 @@ public class NodeFormatUtil { List tempList = buildUser(nodeUserList); userVoList.addAll(tempList); + } else if (type == NodeTypeEnum.TRIGGER.getValue().intValue()) { + // 触发器节点 } // 根据userVo 的ID 去重userVoList