feat(pigx-flow-api): 新增延时器节点支持

- 新增 TimerConfig 和 TimerDuration DTO 类,用于配置延时器参数
- 在 Node 类中增加 timerConfig 字段,支持延时器配置
- 新增 TIMER 节点类型枚举值

feat(pigx-flow-biz): 实现延时器节点功能

- 在 ModelUtil 中新增 buildTimerNode 方法,支持构建延时器节点
- 新增 TimerHandler 组件,处理表单字段日期格式转换
- 支持三种延时模式:固定时长、指定日期时间、表单字段日期
- 优化连线名称生成逻辑,避免重复设置
This commit is contained in:
PIGCLOUD committed 2026-03-08 15:06:17 +08:00
1 parent ecdbfaca68
commit b28d86cb59
6 files changed
+208 -6

No files matched your search

@@ -46,7 +46,12 @@ public enum NodeTypeEnum {
* 并行分支 - 同时执行多条分支,所有分支都完成后才能继续后续流程
*/
PARALLEL_GATEWAY("并行分支", 5, true),
/**
* 延时器节点 - 流程暂停等待指定时间后自动继续
*/
TIMER("延时器", 10, false),
/**
* 空节点 - 占位节点,用于流程设计时的过渡
*/
@@ -150,4 +150,14 @@ public class Node {
/** 规则名称 */
private String ruleName;
/**
* 延时器配置
* <p>
* 包含延时类型(timerType)和对应参数:
* - DURATION: 固定时长延时,包含value和unit
* - DATETIME: 指定日期时间延时
* - FORM_FIELD: 表单字段日期延时
*/
private TimerConfig timerConfig;
}
@@ -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;
}
@@ -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;
}
@@ -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;
/**
* 延时器日期处理器
* <p>
* 将表单字段中的日期值转换为Flowable可解析的ISO 8601格式。
* 表单字段通常存储为 "yyyy-MM-dd HH:mm:ss" 格式,
* 而Flowable的timeDate要求ISO 8601格式 "yyyy-MM-ddTHH:mm:ss"。
* </p>
* <p>
* 使用方式:
* 在TimerEventDefinition的timeDate中配置:${timerHandler.resolveDate(execution, 'formFieldId')}
* </p>
*
* @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;
}
}
}
@@ -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;
}
/**
* 构建延时器节点
* <p>
* 创建Flowable中间捕获事件(Timer Intermediate Catch Event),
* 用于在流程中暂停执行,等待指定时间后自动继续。
* <p>
* 支持三种延时模式:
* <ul>
* <li>DURATION - 固定时长(ISO 8601格式,如PT30M、PT2H、P1D、P1W)</li>
* <li>DATETIME - 指定日期时间(ISO 8601格式)</li>
* <li>FORM_FIELD - 表单字段日期(使用流程变量表达式)</li>
* </ul>
*
* @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;
}
/**
* 创建节点内部连接线
* <p>
@@ -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;
}