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 a32a6dde7..c42a70477 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 @@ -46,7 +46,12 @@ public enum NodeTypeEnum { * 并行分支 - 同时执行多条分支,所有分支都完成后才能继续后续流程 */ PARALLEL_GATEWAY("并行分支", 5, true), - + + /** + * 延时器节点 - 流程暂停等待指定时间后自动继续 + */ + TIMER("延时器", 10, 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 2199d837a..37aa8d8ad 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 @@ -150,4 +150,14 @@ public class Node { /** 规则名称 */ private String ruleName; + /** + * 延时器配置 + *

+ * 包含延时类型(timerType)和对应参数: + * - DURATION: 固定时长延时,包含value和unit + * - DATETIME: 指定日期时间延时 + * - FORM_FIELD: 表单字段日期延时 + */ + private TimerConfig timerConfig; + } diff --git a/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/TimerConfig.java b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/TimerConfig.java new file mode 100644 index 000000000..f3015d42f --- /dev/null +++ b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/TimerConfig.java @@ -0,0 +1,38 @@ +package com.pig4cloud.pigx.flow.dto; + +import lombok.Data; + +/** + * 延时器配置 + * + * @author pigx + */ +@Data +public class TimerConfig { + + /** + * 延时类型: DURATION-固定时长, DATETIME-指定日期时间, FORM_FIELD-表单字段日期 + */ + private String timerType; + + /** + * 固定时长配置 当timerType=DURATION时使用 + */ + private TimerDuration duration; + + /** + * 指定日期时间 当timerType=DATETIME时使用,ISO 8601格式 + */ + private String dateTime; + + /** + * 表单字段ID 当timerType=FORM_FIELD时使用 + */ + private String formFieldId; + + /** + * 表单字段名称 用于显示 + */ + private String formFieldName; + +} diff --git a/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/TimerDuration.java b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/TimerDuration.java new file mode 100644 index 000000000..dbc8ba806 --- /dev/null +++ b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/TimerDuration.java @@ -0,0 +1,23 @@ +package com.pig4cloud.pigx.flow.dto; + +import lombok.Data; + +/** + * 延时器固定时长配置 + * + * @author pigx + */ +@Data +public class TimerDuration { + + /** + * 时长数值 + */ + private Integer value; + + /** + * 时长单位: MINUTE-分钟, HOUR-小时, DAY-天, WEEK-周 + */ + private String unit; + +} diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/node/TimerHandler.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/node/TimerHandler.java new file mode 100644 index 000000000..53be8076c --- /dev/null +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/node/TimerHandler.java @@ -0,0 +1,61 @@ +package com.pig4cloud.pigx.flow.support.node; + +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.util.StrUtil; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.delegate.DelegateExecution; +import org.springframework.stereotype.Component; + +/** + * 延时器日期处理器 + *

+ * 将表单字段中的日期值转换为Flowable可解析的ISO 8601格式。 + * 表单字段通常存储为 "yyyy-MM-dd HH:mm:ss" 格式, + * 而Flowable的timeDate要求ISO 8601格式 "yyyy-MM-ddTHH:mm:ss"。 + *

+ *

+ * 使用方式: + * 在TimerEventDefinition的timeDate中配置:${timerHandler.resolveDate(execution, 'formFieldId')} + *

+ * + * @author pigx + */ +@Component("timerHandler") +@Slf4j +public class TimerHandler { + + /** + * 从流程变量中获取日期字段值并转换为ISO 8601格式 + * + * @param execution 流程执行上下文 + * @param fieldId 表单字段ID + * @return ISO 8601格式日期字符串 + */ + public String resolveDate(DelegateExecution execution, String fieldId) { + Object value = execution.getVariable(fieldId); + if (value == null) { + log.warn("延时器表单字段值为空,fieldId: {},流程实例ID: {}", fieldId, execution.getProcessInstanceId()); + return null; + } + + String dateStr = value.toString().trim(); + if (StrUtil.isBlank(dateStr)) { + return null; + } + + // 已经是ISO 8601格式(包含T),直接返回 + if (dateStr.contains("T")) { + return dateStr; + } + + // "yyyy-MM-dd HH:mm:ss" → "yyyy-MM-ddTHH:mm:ss" + try { + return LocalDateTimeUtil.parse(dateStr, DatePattern.NORM_DATETIME_FORMATTER).toString(); + } catch (Exception e) { + log.error("延时器日期格式转换失败,原始值: {},fieldId: {}", dateStr, fieldId, e); + return dateStr; + } + } + +} 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 d9fdd3d89..089247525 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 @@ -9,6 +9,8 @@ import com.pig4cloud.pigx.flow.constant.NodeTypeEnum; import com.pig4cloud.pigx.flow.constant.ProcessInstanceConstant; import com.pig4cloud.pigx.flow.dto.Node; import com.pig4cloud.pigx.flow.dto.ProcessNodeDataDto; +import com.pig4cloud.pigx.flow.dto.TimerConfig; +import com.pig4cloud.pigx.flow.dto.TimerDuration; import com.pig4cloud.pigx.flow.service.IProcessNodeDataService; import com.pig4cloud.pigx.flow.support.expression.condition.NodeExpressionStrategyFactory; import com.pig4cloud.pigx.flow.support.listeners.ApprovalCreateListener; @@ -346,6 +348,10 @@ public class ModelUtil { if (node.getType() == NodeTypeEnum.PARALLEL_GATEWAY.getValue().intValue()) { flowElementList.addAll(buildParallelGatewayNode(node)); } + // 延时器 + if (node.getType() == NodeTypeEnum.TIMER.getValue().intValue()) { + flowElementList.add(buildTimerNode(node)); + } return flowElementList; } @@ -684,6 +690,69 @@ public class ModelUtil { return serviceTask; } + /** + * 构建延时器节点 + *

+ * 创建Flowable中间捕获事件(Timer Intermediate Catch Event), + * 用于在流程中暂停执行,等待指定时间后自动继续。 + *

+ * 支持三种延时模式: + *

+ * + * @param node 延时器节点数据 + * @return 中间捕获事件元素 + */ + private static FlowElement buildTimerNode(Node node) { + IntermediateCatchEvent catchEvent = new IntermediateCatchEvent(); + catchEvent.setId(node.getId()); + catchEvent.setName(node.getName()); + + TimerEventDefinition timerDef = new TimerEventDefinition(); + + TimerConfig config = node.getTimerConfig(); + if (config != null && StrUtil.isNotBlank(config.getTimerType())) { + String timerType = config.getTimerType(); + + if ("DURATION".equals(timerType)) { + TimerDuration duration = config.getDuration(); + if (duration != null && duration.getValue() != null) { + int value = duration.getValue(); + String unit = duration.getUnit() != null ? duration.getUnit() : "MINUTE"; + String iso = switch (unit) { + case "HOUR" -> StrUtil.format("PT{}H", value); + case "DAY" -> StrUtil.format("P{}D", value); + case "WEEK" -> StrUtil.format("P{}W", value); + default -> StrUtil.format("PT{}M", value); + }; + timerDef.setTimeDuration(iso); + } + } else if ("DATETIME".equals(timerType)) { + String dateTime = config.getDateTime(); + if (StrUtil.isNotBlank(dateTime)) { + timerDef.setTimeDate(dateTime); + } + } else if ("FORM_FIELD".equals(timerType)) { + String formFieldId = config.getFormFieldId(); + if (StrUtil.isNotBlank(formFieldId)) { + timerDef.setTimeDate("${timerHandler.resolveDate(execution, '" + formFieldId + "')}"); + } + } else { + log.warn("未知的延时器类型: {},使用默认30分钟", timerType); + timerDef.setTimeDuration("PT30M"); + } + } else { + timerDef.setTimeDuration("PT30M"); + } + + catchEvent.setEventDefinitions(List.of(timerDef)); + + return catchEvent; + } + /** * 创建节点内部连接线 *

@@ -748,11 +817,7 @@ public class ModelUtil { } SequenceFlow sequenceFlow = new SequenceFlow(pId, childId); sequenceFlow.setConditionExpression(expression); - sequenceFlow.setName(StrUtil.format("{}|{}", pId, childId)); - sequenceFlow.setName(StrUtil.format("连线[{}]", RandomUtil.randomString(5))); - if (StrUtil.isNotBlank(name)) { - sequenceFlow.setName(name); - } + sequenceFlow.setName(StrUtil.isNotBlank(name) ? name : StrUtil.format("连线[{}]", RandomUtil.randomString(5))); sequenceFlow.setId(StrUtil.format("sq-id-{}-{}", IdUtil.fastSimpleUUID(), RandomUtil.randomInt(1, 10000000))); return sequenceFlow; }