|
|
@@ -8,6 +8,8 @@ import com.qqflow.engine.domain.flow.enums.ApprovalResult;
|
|
|
import com.qqflow.engine.domain.flow.enums.NodeType;
|
|
|
import com.qqflow.engine.domain.flow.enums.ProcessStatus;
|
|
|
import com.qqflow.engine.domain.flow.enums.TaskStatus;
|
|
|
+import com.qqflow.engine.domain.flow.event.ProcessCompletedEvent;
|
|
|
+import com.qqflow.engine.domain.flow.event.TaskAssignedEvent;
|
|
|
import com.qqflow.engine.domain.flow.mapper.ApprovalTaskMapper;
|
|
|
import com.qqflow.engine.domain.flow.mapper.ProcessDefinitionMapper;
|
|
|
import com.qqflow.engine.domain.flow.mapper.ProcessInstanceMapper;
|
|
|
@@ -17,8 +19,6 @@ import com.qqflow.engine.domain.flow.model.FlowNode;
|
|
|
import com.qqflow.engine.domain.flow.po.ApprovalTask;
|
|
|
import com.qqflow.engine.domain.flow.po.ProcessDefinition;
|
|
|
import com.qqflow.engine.domain.flow.po.ProcessInstance;
|
|
|
-import com.qqflow.engine.domain.flow.event.ProcessCompletedEvent;
|
|
|
-import com.qqflow.engine.domain.flow.event.TaskAssignedEvent;
|
|
|
import com.qqflow.engine.domain.flow.service.FlowEngineService;
|
|
|
import com.qqflow.engine.domain.system.entity.SysDept;
|
|
|
import com.qqflow.engine.domain.system.entity.SysRole;
|
|
|
@@ -28,29 +28,48 @@ import com.qqflow.engine.domain.system.mapper.SysDeptMapper;
|
|
|
import com.qqflow.engine.domain.system.mapper.SysRoleMapper;
|
|
|
import com.qqflow.engine.domain.system.mapper.SysUserMapper;
|
|
|
import com.qqflow.engine.domain.system.mapper.SysUserRoleMapper;
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
|
|
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
import lombok.SneakyThrows;
|
|
|
import org.springframework.context.ApplicationEventPublisher;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.transaction.annotation.Transactional;
|
|
|
+import org.springframework.util.CollectionUtils;
|
|
|
|
|
|
import java.time.LocalDateTime;
|
|
|
+import java.util.ArrayDeque;
|
|
|
import java.util.ArrayList;
|
|
|
import java.util.Collections;
|
|
|
+import java.util.Deque;
|
|
|
+import java.util.HashSet;
|
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
|
import java.util.Objects;
|
|
|
+import java.util.Set;
|
|
|
+import java.util.regex.Matcher;
|
|
|
+import java.util.regex.Pattern;
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
+import static com.qqflow.engine.common.constant.SecurityConstants.ASSIGNEE_TYPE_ROLE;
|
|
|
+import static com.qqflow.engine.common.constant.SecurityConstants.ASSIGNEE_TYPE_USER;
|
|
|
+import static com.qqflow.engine.common.constant.SecurityConstants.USER_TYPE_SYSTEM;
|
|
|
+
|
|
|
@Service
|
|
|
@RequiredArgsConstructor
|
|
|
public class FlowEngineServiceImpl implements FlowEngineService {
|
|
|
|
|
|
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
|
|
+ private static final Pattern CONDITION_PATTERN = Pattern.compile(
|
|
|
+ "^\\s*([A-Za-z_][A-Za-z0-9_]*)\\s*(>=|<=|!=|==|>|<)\\s*(.+?)\\s*$");
|
|
|
+ private static final int MAX_FLOW_DEPTH = 100;
|
|
|
|
|
|
private final ProcessDefinitionMapper processDefinitionMapper;
|
|
|
private final ProcessInstanceMapper processInstanceMapper;
|
|
|
private final ApprovalTaskMapper approvalTaskMapper;
|
|
|
private final ApprovalTaskAssembler approvalTaskAssembler;
|
|
|
+ private final ApprovalTaskHelper approvalTaskHelper;
|
|
|
private final SysRoleMapper sysRoleMapper;
|
|
|
private final SysUserRoleMapper sysUserRoleMapper;
|
|
|
private final SysUserMapper sysUserMapper;
|
|
|
@@ -69,11 +88,12 @@ public class FlowEngineServiceImpl implements FlowEngineService {
|
|
|
}
|
|
|
|
|
|
public List<FlowNode> getNextNodes(FlowModel model, String currentNodeId, ProcessInstance instance) {
|
|
|
- List<FlowEdge> edges = model.getEdges().stream()
|
|
|
- .filter(e -> Objects.equals(e.getSourceNodeId(), currentNodeId))
|
|
|
- .collect(Collectors.toList());
|
|
|
+ List<FlowEdge> edges = safeEdges(model);
|
|
|
List<String> targetIds = new ArrayList<>();
|
|
|
for (FlowEdge edge : edges) {
|
|
|
+ if (!Objects.equals(edge.getSourceNodeId(), currentNodeId)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
Object conditionObj = edge.getCondition() != null ? edge.getCondition().get("condition") : null;
|
|
|
String condition = conditionObj != null ? conditionObj.toString() : null;
|
|
|
if (condition == null || condition.trim().isEmpty()) {
|
|
|
@@ -82,31 +102,27 @@ public class FlowEngineServiceImpl implements FlowEngineService {
|
|
|
targetIds.add(edge.getTargetNodeId());
|
|
|
}
|
|
|
}
|
|
|
- return model.getNodes().stream()
|
|
|
+ return safeNodes(model).stream()
|
|
|
.filter(n -> targetIds.contains(n.getId()))
|
|
|
.collect(Collectors.toList());
|
|
|
}
|
|
|
|
|
|
private boolean evaluateCondition(String condition, String formDataJson) {
|
|
|
try {
|
|
|
- Map<String, Object> formData = formDataJson != null ? OBJECT_MAPPER.readValue(formDataJson, Map.class) : Collections.emptyMap();
|
|
|
- condition = condition.trim();
|
|
|
- // 支持格式:field == value, field > value, field >= value, field < value, field <= value, field != value
|
|
|
- String[] operators = {">=", "<=", "!=", "==", ">", "<"};
|
|
|
- String foundOp = null;
|
|
|
- for (String op : operators) {
|
|
|
- if (condition.contains(op)) {
|
|
|
- foundOp = op;
|
|
|
- break;
|
|
|
- }
|
|
|
+ Map<String, Object> formData = formDataJson != null
|
|
|
+ ? OBJECT_MAPPER.readValue(formDataJson, Map.class)
|
|
|
+ : Collections.emptyMap();
|
|
|
+ Matcher matcher = CONDITION_PATTERN.matcher(condition.trim());
|
|
|
+ if (!matcher.find()) {
|
|
|
+ return false;
|
|
|
}
|
|
|
- if (foundOp == null) return false;
|
|
|
- String[] parts = condition.split(java.util.regex.Pattern.quote(foundOp), 2);
|
|
|
- if (parts.length != 2) return false;
|
|
|
- String field = parts[0].trim();
|
|
|
- String value = parts[1].trim();
|
|
|
+ String field = matcher.group(1);
|
|
|
+ String foundOp = matcher.group(2);
|
|
|
+ String value = matcher.group(3).trim();
|
|
|
Object actual = formData.get(field);
|
|
|
- if (actual == null) return false;
|
|
|
+ if (actual == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
String actualStr = actual.toString();
|
|
|
// 尝试数字比较
|
|
|
try {
|
|
|
@@ -122,10 +138,11 @@ public class FlowEngineServiceImpl implements FlowEngineService {
|
|
|
default -> false;
|
|
|
};
|
|
|
} catch (NumberFormatException e) {
|
|
|
- // 字符串比较
|
|
|
+ // 字符串比较:去除单/双引号
|
|
|
+ String normalizedValue = value.replaceAll("^['\"]|['\"]$", "");
|
|
|
return switch (foundOp) {
|
|
|
- case "==" -> actualStr.equals(value);
|
|
|
- case "!=" -> !actualStr.equals(value);
|
|
|
+ case "==" -> actualStr.equals(normalizedValue);
|
|
|
+ case "!=" -> !actualStr.equals(normalizedValue);
|
|
|
default -> false;
|
|
|
};
|
|
|
}
|
|
|
@@ -152,7 +169,7 @@ public class FlowEngineServiceImpl implements FlowEngineService {
|
|
|
Object approverObj = props.get("approver");
|
|
|
if (approverObj != null) {
|
|
|
String approver = approverObj.toString();
|
|
|
- return this.doCalculateAssignees("ROLE", approver, instance);
|
|
|
+ return this.doCalculateAssignees(ASSIGNEE_TYPE_ROLE, approver, instance);
|
|
|
}
|
|
|
return Collections.emptyList();
|
|
|
}
|
|
|
@@ -176,16 +193,19 @@ public class FlowEngineServiceImpl implements FlowEngineService {
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
public void executeTransition(ProcessInstance instance, ApprovalTask currentTask, ApprovalAction action) {
|
|
|
this.executeTransition(instance, currentTask, action, null);
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
public void executeTransition(ProcessInstance instance, ApprovalTask currentTask, ApprovalAction action, String comment) {
|
|
|
this.executeTransition(instance, currentTask, action, comment, null);
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
public void executeTransition(ProcessInstance instance, ApprovalTask currentTask, ApprovalAction action, String comment, String targetNodeId) {
|
|
|
switch (action) {
|
|
|
case APPROVE -> this.handleApprove(instance, currentTask, comment);
|
|
|
@@ -196,7 +216,18 @@ public class FlowEngineServiceImpl implements FlowEngineService {
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
public void startInstance(ProcessInstance instance, ProcessDefinition definition) {
|
|
|
+ if (instance == null || instance.getId() == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // 防御重复启动:已存在任务则直接返回
|
|
|
+ long existingTaskCount = this.approvalTaskMapper.selectCount(
|
|
|
+ new LambdaQueryWrapper<ApprovalTask>()
|
|
|
+ .eq(ApprovalTask::getInstanceId, instance.getId()));
|
|
|
+ if (existingTaskCount > 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
FlowModel model = this.parseModel(definition.getModelJson());
|
|
|
FlowNode startNode = this.findStartNode(model);
|
|
|
List<FlowNode> nextNodes = this.getNextNodes(model, startNode.getId(), instance);
|
|
|
@@ -215,16 +246,16 @@ public class FlowEngineServiceImpl implements FlowEngineService {
|
|
|
if ("SELF".equals(assigneeType)) {
|
|
|
return Collections.singletonList(instance.getApplicantId());
|
|
|
}
|
|
|
- if ("ROLE".equals(assigneeType) && assigneeValue != null) {
|
|
|
+ if (ASSIGNEE_TYPE_ROLE.equals(assigneeType) && assigneeValue != null) {
|
|
|
SysRole role = this.sysRoleMapper.selectByRoleCode(assigneeValue);
|
|
|
if (role == null) {
|
|
|
return Collections.emptyList();
|
|
|
}
|
|
|
// 查询该角色下的所有用户
|
|
|
List<SysUserRole> userRoles = this.sysUserRoleMapper.selectList(
|
|
|
- new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SysUserRole>()
|
|
|
+ new LambdaQueryWrapper<SysUserRole>()
|
|
|
.eq(SysUserRole::getRoleId, role.getId()));
|
|
|
- if (userRoles == null || userRoles.isEmpty()) {
|
|
|
+ if (CollectionUtils.isEmpty(userRoles)) {
|
|
|
return Collections.emptyList();
|
|
|
}
|
|
|
return userRoles.stream().map(SysUserRole::getUserId).distinct().collect(Collectors.toList());
|
|
|
@@ -244,21 +275,38 @@ public class FlowEngineServiceImpl implements FlowEngineService {
|
|
|
}
|
|
|
|
|
|
private void handleApprove(ProcessInstance instance, ApprovalTask currentTask, String comment) {
|
|
|
- this.completeCurrentTask(currentTask, ApprovalResult.PASS.getCode(), comment);
|
|
|
+ if (instance == null || instance.getId() == null || currentTask == null || currentTask.getId() == null) {
|
|
|
+ throw new BusinessException("参数不能为空");
|
|
|
+ }
|
|
|
+ // 对实例加行锁,防止会签/或签并发竞态
|
|
|
+ this.lockInstance(instance.getId());
|
|
|
+
|
|
|
+ // 在锁内重新查询任务状态,防止重复处理
|
|
|
+ ApprovalTask lockedTask = this.approvalTaskMapper.selectByIdForUpdate(currentTask.getId());
|
|
|
+ if (lockedTask == null || !TaskStatus.PENDING.getCode().equals(lockedTask.getTaskStatus())) {
|
|
|
+ throw new BusinessException("任务已处理");
|
|
|
+ }
|
|
|
+
|
|
|
+ this.completeCurrentTask(lockedTask, ApprovalResult.PASS.getCode(), comment);
|
|
|
|
|
|
FlowModel model = this.getModelByInstance(instance);
|
|
|
- FlowNode currentNode = model.getNodes().stream()
|
|
|
- .filter(n -> n.getId().equals(currentTask.getNodeId()))
|
|
|
+ FlowNode currentNode = safeNodes(model).stream()
|
|
|
+ .filter(n -> n.getId().equals(lockedTask.getNodeId()))
|
|
|
.findFirst()
|
|
|
.orElse(null);
|
|
|
- String approveMode = currentNode != null ? this.getApproveMode(currentNode) : "or";
|
|
|
+ String approveMode;
|
|
|
+ if (currentNode == null) {
|
|
|
+ approveMode = "or";
|
|
|
+ } else {
|
|
|
+ approveMode = this.getApproveMode(currentNode);
|
|
|
+ }
|
|
|
|
|
|
// 会签模式:检查同节点是否还有未处理的任务
|
|
|
if ("and".equals(approveMode)) {
|
|
|
long pendingCount = this.approvalTaskMapper.selectCount(
|
|
|
- new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ApprovalTask>()
|
|
|
+ new LambdaQueryWrapper<ApprovalTask>()
|
|
|
.eq(ApprovalTask::getInstanceId, instance.getId())
|
|
|
- .eq(ApprovalTask::getNodeId, currentTask.getNodeId())
|
|
|
+ .eq(ApprovalTask::getNodeId, lockedTask.getNodeId())
|
|
|
.eq(ApprovalTask::getTaskStatus, TaskStatus.PENDING.getCode()));
|
|
|
if (pendingCount > 0) {
|
|
|
// 还有未处理的任务,不推进流程
|
|
|
@@ -267,15 +315,15 @@ public class FlowEngineServiceImpl implements FlowEngineService {
|
|
|
} else {
|
|
|
// 或签模式:将同节点其他 PENDING 任务标记为 SKIPPED
|
|
|
this.approvalTaskMapper.update(null,
|
|
|
- new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<ApprovalTask>()
|
|
|
+ new LambdaUpdateWrapper<ApprovalTask>()
|
|
|
.eq(ApprovalTask::getInstanceId, instance.getId())
|
|
|
- .eq(ApprovalTask::getNodeId, currentTask.getNodeId())
|
|
|
+ .eq(ApprovalTask::getNodeId, lockedTask.getNodeId())
|
|
|
.eq(ApprovalTask::getTaskStatus, TaskStatus.PENDING.getCode())
|
|
|
- .ne(ApprovalTask::getId, currentTask.getId())
|
|
|
+ .ne(ApprovalTask::getId, lockedTask.getId())
|
|
|
.set(ApprovalTask::getTaskStatus, TaskStatus.SKIPPED.getCode()));
|
|
|
}
|
|
|
|
|
|
- List<FlowNode> nextNodes = this.getNextNodes(model, currentTask.getNodeId(), instance);
|
|
|
+ List<FlowNode> nextNodes = this.getNextNodes(model, lockedTask.getNodeId(), instance);
|
|
|
if (this.isEndNode(nextNodes)) {
|
|
|
this.completeInstance(instance);
|
|
|
return;
|
|
|
@@ -285,31 +333,82 @@ public class FlowEngineServiceImpl implements FlowEngineService {
|
|
|
}
|
|
|
|
|
|
private void handleReject(ProcessInstance instance, ApprovalTask currentTask, String comment) {
|
|
|
+ this.lockInstance(instance.getId());
|
|
|
this.completeCurrentTask(currentTask, ApprovalResult.REJECT.getCode(), comment);
|
|
|
+ this.approvalTaskHelper.cancelPendingTasks(instance.getId(), LocalDateTime.now());
|
|
|
this.rejectInstance(instance);
|
|
|
}
|
|
|
|
|
|
private void handleReturn(ProcessInstance instance, ApprovalTask currentTask, String comment, String targetNodeId) {
|
|
|
- this.updateTaskStatus(currentTask, TaskStatus.RETURNED.getCode(), comment);
|
|
|
+ this.lockInstance(instance.getId());
|
|
|
FlowModel model = this.getModelByInstance(instance);
|
|
|
- FlowNode targetNode = null;
|
|
|
- if (targetNodeId != null && !targetNodeId.isEmpty()) {
|
|
|
- targetNode = model.getNodes().stream()
|
|
|
- .filter(n -> n.getId().equals(targetNodeId))
|
|
|
- .findFirst()
|
|
|
- .orElse(null);
|
|
|
- }
|
|
|
- if (targetNode == null) {
|
|
|
- targetNode = this.findPreviousNode(model, currentTask.getNodeId());
|
|
|
- }
|
|
|
+ FlowNode currentNode = safeNodes(model).stream()
|
|
|
+ .filter(n -> n.getId().equals(currentTask.getNodeId()))
|
|
|
+ .findFirst()
|
|
|
+ .orElse(null);
|
|
|
+ FlowNode targetNode = this.resolveReturnTarget(model, currentNode, targetNodeId);
|
|
|
if (targetNode == null) {
|
|
|
throw new BusinessException("无法找到回退目标节点");
|
|
|
}
|
|
|
+ this.updateTaskStatus(currentTask, TaskStatus.RETURNED.getCode(), comment);
|
|
|
+ this.approvalTaskHelper.cancelPendingTasks(instance.getId(), LocalDateTime.now());
|
|
|
List<FlowNode> returnTargets = Collections.singletonList(targetNode);
|
|
|
this.createTasksForNodes(instance, returnTargets, model);
|
|
|
this.updateInstanceNode(instance, returnTargets);
|
|
|
}
|
|
|
|
|
|
+ private void lockInstance(Long instanceId) {
|
|
|
+ this.processInstanceMapper.selectOne(
|
|
|
+ Wrappers.<ProcessInstance>lambdaQuery()
|
|
|
+ .eq(ProcessInstance::getId, instanceId)
|
|
|
+ .last("FOR UPDATE"));
|
|
|
+ }
|
|
|
+
|
|
|
+ private FlowNode resolveReturnTarget(FlowModel model, FlowNode currentNode, String targetNodeId) {
|
|
|
+ if (currentNode == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ List<String> upstreamIds = this.collectUpstreamNodeIds(model, currentNode.getId());
|
|
|
+ if (targetNodeId != null && !targetNodeId.isEmpty()) {
|
|
|
+ if (!upstreamIds.contains(targetNodeId)) {
|
|
|
+ throw new BusinessException("回退目标节点不在当前节点上游");
|
|
|
+ }
|
|
|
+ FlowNode target = safeNodes(model).stream()
|
|
|
+ .filter(n -> n.getId().equals(targetNodeId))
|
|
|
+ .findFirst()
|
|
|
+ .orElse(null);
|
|
|
+ if (target != null && NodeType.APPROVAL.getCode().equals(target.getType())) {
|
|
|
+ return target;
|
|
|
+ }
|
|
|
+ throw new BusinessException("回退目标节点必须是已审批节点");
|
|
|
+ }
|
|
|
+ return this.findPreviousNode(model, currentNode.getId());
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<String> collectUpstreamNodeIds(FlowModel model, String currentNodeId) {
|
|
|
+ List<String> upstream = new ArrayList<>();
|
|
|
+ Set<String> visited = new HashSet<>();
|
|
|
+ Deque<String> stack = new ArrayDeque<>();
|
|
|
+ stack.push(currentNodeId);
|
|
|
+ while (!stack.isEmpty()) {
|
|
|
+ String nodeId = stack.pop();
|
|
|
+ if (!visited.add(nodeId)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ List<String> sourceIds = safeEdges(model).stream()
|
|
|
+ .filter(e -> nodeId.equals(e.getTargetNodeId()))
|
|
|
+ .map(FlowEdge::getSourceNodeId)
|
|
|
+ .toList();
|
|
|
+ for (String sourceId : sourceIds) {
|
|
|
+ if (!visited.contains(sourceId)) {
|
|
|
+ upstream.add(sourceId);
|
|
|
+ stack.push(sourceId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return upstream;
|
|
|
+ }
|
|
|
+
|
|
|
private FlowModel getModelByInstance(ProcessInstance instance) {
|
|
|
ProcessDefinition definition = this.processDefinitionMapper.selectById(instance.getProcessDefinitionId());
|
|
|
if (definition == null) {
|
|
|
@@ -319,18 +418,18 @@ public class FlowEngineServiceImpl implements FlowEngineService {
|
|
|
}
|
|
|
|
|
|
private FlowNode findStartNode(FlowModel model) {
|
|
|
- return model.getNodes().stream()
|
|
|
+ return safeNodes(model).stream()
|
|
|
.filter(n -> NodeType.START.getCode().equals(n.getType()))
|
|
|
.findFirst()
|
|
|
.orElseThrow(() -> new BusinessException("未找到开始节点"));
|
|
|
}
|
|
|
|
|
|
private FlowNode findPreviousNode(FlowModel model, String currentNodeId) {
|
|
|
- List<String> sourceIds = model.getEdges().stream()
|
|
|
+ List<String> sourceIds = safeEdges(model).stream()
|
|
|
.filter(e -> Objects.equals(e.getTargetNodeId(), currentNodeId))
|
|
|
.map(FlowEdge::getSourceNodeId)
|
|
|
.collect(Collectors.toList());
|
|
|
- return model.getNodes().stream()
|
|
|
+ return safeNodes(model).stream()
|
|
|
.filter(n -> sourceIds.contains(n.getId()) && !NodeType.START.getCode().equals(n.getType()))
|
|
|
.findFirst()
|
|
|
.orElse(null);
|
|
|
@@ -340,71 +439,136 @@ public class FlowEngineServiceImpl implements FlowEngineService {
|
|
|
return nodes.stream().anyMatch(n -> NodeType.END.getCode().equals(n.getType()));
|
|
|
}
|
|
|
|
|
|
+ private List<FlowNode> safeNodes(FlowModel model) {
|
|
|
+ return model != null && model.getNodes() != null ? model.getNodes() : Collections.emptyList();
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<FlowEdge> safeEdges(FlowModel model) {
|
|
|
+ return model != null && model.getEdges() != null ? model.getEdges() : Collections.emptyList();
|
|
|
+ }
|
|
|
+
|
|
|
private void createTasksForNodes(ProcessInstance instance, List<FlowNode> nodes, FlowModel model) {
|
|
|
+ this.createTasksForNodes(instance, nodes, model, new HashSet<>(), 0);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void createTasksForNodes(ProcessInstance instance, List<FlowNode> nodes, FlowModel model,
|
|
|
+ Set<String> visited, int depth) {
|
|
|
+ if (depth > MAX_FLOW_DEPTH) {
|
|
|
+ throw new BusinessException("流程模型存在循环或层级过深,已超过最大处理深度");
|
|
|
+ }
|
|
|
ProcessDefinition definition = this.processDefinitionMapper.selectById(instance.getProcessDefinitionId());
|
|
|
String processName = definition != null ? definition.getProcessName() : "";
|
|
|
for (FlowNode node : nodes) {
|
|
|
if (NodeType.START.getCode().equals(node.getType()) || NodeType.END.getCode().equals(node.getType())) {
|
|
|
continue;
|
|
|
}
|
|
|
+ // 幂等:已存在处理/跳过/待处理任务则不再重复创建
|
|
|
+ if (this.hasExistingTaskForNode(instance.getId(), node.getId())) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (!visited.add(node.getId())) {
|
|
|
+ // 已在当前执行路径中处理过,防止循环
|
|
|
+ continue;
|
|
|
+ }
|
|
|
// CC节点:创建抄送任务并自动标记为已阅,同时继续推进下游
|
|
|
if (NodeType.CC.getCode().equals(node.getType())) {
|
|
|
- List<Long> ccAssignees = this.calculateAssignees(node, instance);
|
|
|
+ Map<String, Object> ccProps = node.getProperties();
|
|
|
+ String ccAssigneeType = ASSIGNEE_TYPE_ROLE;
|
|
|
+ String ccAssigneeValue = null;
|
|
|
+ if (ccProps != null) {
|
|
|
+ Object typeObj = ccProps.get("assigneeType");
|
|
|
+ Object valueObj = ccProps.get("assigneeValue");
|
|
|
+ if (typeObj != null) {
|
|
|
+ ccAssigneeType = typeObj.toString();
|
|
|
+ ccAssigneeValue = valueObj != null ? valueObj.toString() : null;
|
|
|
+ }
|
|
|
+ // 旧格式:ccUsers 直接保存了 roleCode
|
|
|
+ Object ccUsersObj = ccProps.get("ccUsers");
|
|
|
+ if (ccAssigneeValue == null && ccUsersObj != null) {
|
|
|
+ ccAssigneeValue = ccUsersObj.toString();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ List<Long> ccAssignees;
|
|
|
+ String ccTaskAssigneeType;
|
|
|
+ if (ASSIGNEE_TYPE_ROLE.equals(ccAssigneeType) && ccAssigneeValue != null) {
|
|
|
+ SysRole role = this.sysRoleMapper.selectByRoleCode(ccAssigneeValue);
|
|
|
+ ccAssignees = role != null ? Collections.singletonList(role.getId()) : Collections.emptyList();
|
|
|
+ ccTaskAssigneeType = ASSIGNEE_TYPE_ROLE;
|
|
|
+ } else {
|
|
|
+ ccAssignees = this.calculateAssignees(node, instance);
|
|
|
+ ccTaskAssigneeType = ASSIGNEE_TYPE_USER;
|
|
|
+ }
|
|
|
if (ccAssignees.isEmpty()) {
|
|
|
ccAssignees = Collections.singletonList(instance.getApplicantId());
|
|
|
+ ccTaskAssigneeType = ASSIGNEE_TYPE_USER;
|
|
|
}
|
|
|
for (Long assigneeId : ccAssignees) {
|
|
|
ApprovalTask ccTask = this.approvalTaskAssembler.buildNew(
|
|
|
instance.getId(), node.getId(), node.getName(),
|
|
|
- node.getType(), assigneeId, "USER", TaskStatus.PENDING.getCode()
|
|
|
+ node.getType(), assigneeId, ccTaskAssigneeType, TaskStatus.PENDING.getCode()
|
|
|
);
|
|
|
this.approvalTaskMapper.insert(ccTask);
|
|
|
}
|
|
|
// 自动推进到CC节点的下游
|
|
|
List<FlowNode> ccNextNodes = this.getNextNodes(model, node.getId(), instance);
|
|
|
if (!ccNextNodes.isEmpty()) {
|
|
|
- this.createTasksForNodes(instance, ccNextNodes, model);
|
|
|
+ this.createTasksForNodes(instance, ccNextNodes, model, visited, depth + 1);
|
|
|
}
|
|
|
continue;
|
|
|
}
|
|
|
- List<Long> assignees = this.calculateAssignees(node, instance);
|
|
|
- if (assignees.isEmpty()) {
|
|
|
- assignees = Collections.singletonList(instance.getApplicantId());
|
|
|
- }
|
|
|
- String approveMode = this.getApproveMode(node);
|
|
|
- List<Long> finalAssignees = assignees;
|
|
|
- if ("or".equals(approveMode) && assignees.size() > 1) {
|
|
|
- finalAssignees = assignees;
|
|
|
- }
|
|
|
// 确定处理人类型
|
|
|
Map<String, Object> nodeProps = node.getProperties();
|
|
|
- String taskAssigneeType = "USER";
|
|
|
+ String assigneeType = ASSIGNEE_TYPE_ROLE;
|
|
|
+ String assigneeValue = null;
|
|
|
if (nodeProps != null) {
|
|
|
Object typeObj = nodeProps.get("assigneeType");
|
|
|
+ Object valueObj = nodeProps.get("assigneeValue");
|
|
|
if (typeObj != null) {
|
|
|
- taskAssigneeType = typeObj.toString();
|
|
|
+ assigneeType = typeObj.toString();
|
|
|
+ assigneeValue = valueObj != null ? valueObj.toString() : null;
|
|
|
} else {
|
|
|
Object approverObj = nodeProps.get("approver");
|
|
|
if (approverObj != null) {
|
|
|
- taskAssigneeType = "ROLE";
|
|
|
+ assigneeValue = approverObj.toString();
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
- // SELF/LEADER 已解析为具体用户ID,assigneeType 设为 USER
|
|
|
- if ("SELF".equals(taskAssigneeType) || "LEADER".equals(taskAssigneeType)) {
|
|
|
- taskAssigneeType = "USER";
|
|
|
+
|
|
|
+ List<Long> assignees;
|
|
|
+ String taskAssigneeType;
|
|
|
+ if (ASSIGNEE_TYPE_ROLE.equals(assigneeType)) {
|
|
|
+ // 角色审批:任务分配给角色账号,保持 assigneeType=ROLE
|
|
|
+ SysRole role = assigneeValue != null ? this.sysRoleMapper.selectByRoleCode(assigneeValue) : null;
|
|
|
+ assignees = role != null ? Collections.singletonList(role.getId()) : Collections.emptyList();
|
|
|
+ taskAssigneeType = ASSIGNEE_TYPE_ROLE;
|
|
|
+ } else {
|
|
|
+ // USER / SELF / LEADER 解析为具体用户
|
|
|
+ assignees = this.calculateAssignees(node, instance);
|
|
|
+ taskAssigneeType = ASSIGNEE_TYPE_USER;
|
|
|
}
|
|
|
+ if (assignees.isEmpty()) {
|
|
|
+ assignees = Collections.singletonList(instance.getApplicantId());
|
|
|
+ taskAssigneeType = ASSIGNEE_TYPE_USER;
|
|
|
+ }
|
|
|
+
|
|
|
+ String approveMode = this.getApproveMode(node);
|
|
|
+ LocalDateTime timeoutTime = this.calculateTimeoutTime(node);
|
|
|
+ String timeoutAction = this.calculateTimeoutAction(node);
|
|
|
+ List<Long> finalAssignees = assignees;
|
|
|
for (Long assigneeId : finalAssignees) {
|
|
|
ApprovalTask task = this.approvalTaskAssembler.buildNew(
|
|
|
instance.getId(), node.getId(), node.getName(),
|
|
|
node.getType(), assigneeId, taskAssigneeType, TaskStatus.PENDING.getCode()
|
|
|
);
|
|
|
+ task.setTimeoutTime(timeoutTime);
|
|
|
+ task.setTimeoutAction(timeoutAction);
|
|
|
this.approvalTaskMapper.insert(task);
|
|
|
}
|
|
|
// 发布任务分配通知事件
|
|
|
this.eventPublisher.publishEvent(new TaskAssignedEvent(
|
|
|
this, instance.getId(), instance.getTitle(),
|
|
|
- processName, node.getName(), finalAssignees
|
|
|
+ processName, node.getName(), finalAssignees, taskAssigneeType
|
|
|
));
|
|
|
}
|
|
|
}
|
|
|
@@ -419,7 +583,7 @@ public class FlowEngineServiceImpl implements FlowEngineService {
|
|
|
}
|
|
|
Integer status = ProcessStatus.PENDING.getCode();
|
|
|
this.processInstanceMapper.update(null,
|
|
|
- new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<ProcessInstance>()
|
|
|
+ new LambdaUpdateWrapper<ProcessInstance>()
|
|
|
.eq(ProcessInstance::getId, instance.getId())
|
|
|
.set(ProcessInstance::getCurrentNodeId, firstNode.getId())
|
|
|
.set(ProcessInstance::getStatus, status));
|
|
|
@@ -427,8 +591,12 @@ public class FlowEngineServiceImpl implements FlowEngineService {
|
|
|
|
|
|
private void completeInstance(ProcessInstance instance) {
|
|
|
this.processInstanceMapper.update(null,
|
|
|
- new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<ProcessInstance>()
|
|
|
+ new LambdaUpdateWrapper<ProcessInstance>()
|
|
|
.eq(ProcessInstance::getId, instance.getId())
|
|
|
+ .ne(ProcessInstance::getStatus, ProcessStatus.COMPLETED.getCode())
|
|
|
+ .ne(ProcessInstance::getStatus, ProcessStatus.REJECTED.getCode())
|
|
|
+ .ne(ProcessInstance::getStatus, ProcessStatus.TERMINATED.getCode())
|
|
|
+ .ne(ProcessInstance::getStatus, ProcessStatus.REVOKED.getCode())
|
|
|
.set(ProcessInstance::getStatus, ProcessStatus.COMPLETED.getCode())
|
|
|
.set(ProcessInstance::getEndTime, LocalDateTime.now())
|
|
|
.set(ProcessInstance::getResult, ApprovalResult.PASS.getCode()));
|
|
|
@@ -442,8 +610,12 @@ public class FlowEngineServiceImpl implements FlowEngineService {
|
|
|
|
|
|
private void rejectInstance(ProcessInstance instance) {
|
|
|
this.processInstanceMapper.update(null,
|
|
|
- new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<ProcessInstance>()
|
|
|
+ new LambdaUpdateWrapper<ProcessInstance>()
|
|
|
.eq(ProcessInstance::getId, instance.getId())
|
|
|
+ .ne(ProcessInstance::getStatus, ProcessStatus.COMPLETED.getCode())
|
|
|
+ .ne(ProcessInstance::getStatus, ProcessStatus.REJECTED.getCode())
|
|
|
+ .ne(ProcessInstance::getStatus, ProcessStatus.TERMINATED.getCode())
|
|
|
+ .ne(ProcessInstance::getStatus, ProcessStatus.REVOKED.getCode())
|
|
|
.set(ProcessInstance::getStatus, ProcessStatus.REJECTED.getCode())
|
|
|
.set(ProcessInstance::getResult, ApprovalResult.REJECT.getCode())
|
|
|
.set(ProcessInstance::getEndTime, LocalDateTime.now()));
|
|
|
@@ -455,22 +627,82 @@ public class FlowEngineServiceImpl implements FlowEngineService {
|
|
|
));
|
|
|
}
|
|
|
|
|
|
+ private boolean hasExistingTaskForNode(Long instanceId, String nodeId) {
|
|
|
+ if (instanceId == null || nodeId == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ long count = this.approvalTaskMapper.selectCount(
|
|
|
+ new LambdaQueryWrapper<ApprovalTask>()
|
|
|
+ .eq(ApprovalTask::getInstanceId, instanceId)
|
|
|
+ .eq(ApprovalTask::getNodeId, nodeId)
|
|
|
+ .in(ApprovalTask::getTaskStatus,
|
|
|
+ TaskStatus.PENDING.getCode(),
|
|
|
+ TaskStatus.HANDLED.getCode(),
|
|
|
+ TaskStatus.SKIPPED.getCode()));
|
|
|
+ return count > 0;
|
|
|
+ }
|
|
|
+
|
|
|
private void completeCurrentTask(ApprovalTask task, String result, String comment) {
|
|
|
- this.approvalTaskMapper.update(null,
|
|
|
- new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<ApprovalTask>()
|
|
|
- .eq(ApprovalTask::getId, task.getId())
|
|
|
- .set(ApprovalTask::getTaskStatus, TaskStatus.HANDLED.getCode())
|
|
|
- .set(ApprovalTask::getApprovalResult, result)
|
|
|
- .set(ApprovalTask::getApprovalComment, comment)
|
|
|
- .set(ApprovalTask::getHandleTime, LocalDateTime.now()));
|
|
|
+ task.setTaskStatus(TaskStatus.HANDLED.getCode());
|
|
|
+ task.setApprovalResult(result);
|
|
|
+ task.setApprovalComment(comment);
|
|
|
+ task.setHandleTime(LocalDateTime.now());
|
|
|
+ this.approvalTaskMapper.updateById(task);
|
|
|
}
|
|
|
|
|
|
private void updateTaskStatus(ApprovalTask task, Integer status, String comment) {
|
|
|
- this.approvalTaskMapper.update(null,
|
|
|
- new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<ApprovalTask>()
|
|
|
- .eq(ApprovalTask::getId, task.getId())
|
|
|
- .set(ApprovalTask::getTaskStatus, status)
|
|
|
- .set(ApprovalTask::getApprovalComment, comment)
|
|
|
- .set(ApprovalTask::getHandleTime, LocalDateTime.now()));
|
|
|
+ task.setTaskStatus(status);
|
|
|
+ task.setApprovalComment(comment);
|
|
|
+ task.setHandleTime(LocalDateTime.now());
|
|
|
+ this.approvalTaskMapper.updateById(task);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据节点属性计算超时时间
|
|
|
+ *
|
|
|
+ * @param node 流程节点
|
|
|
+ * @return 超时时间,未配置或配置无效时返回 null
|
|
|
+ */
|
|
|
+ private LocalDateTime calculateTimeoutTime(FlowNode node) {
|
|
|
+ Map<String, Object> props = node.getProperties();
|
|
|
+ if (props == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ Object hoursObj = props.get("timeoutHours");
|
|
|
+ if (hoursObj == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ int hours;
|
|
|
+ try {
|
|
|
+ hours = Integer.parseInt(hoursObj.toString());
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (hours <= 0) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return LocalDateTime.now().plusHours(hours);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据节点属性计算超时动作
|
|
|
+ *
|
|
|
+ * @param node 流程节点
|
|
|
+ * @return 超时动作:pass-自动通过,reject-自动驳回,remind-提醒(默认)
|
|
|
+ */
|
|
|
+ private String calculateTimeoutAction(FlowNode node) {
|
|
|
+ Map<String, Object> props = node.getProperties();
|
|
|
+ if (props == null) {
|
|
|
+ return "remind";
|
|
|
+ }
|
|
|
+ Object actionObj = props.get("timeoutAction");
|
|
|
+ if (actionObj == null) {
|
|
|
+ return "remind";
|
|
|
+ }
|
|
|
+ String action = actionObj.toString();
|
|
|
+ if ("pass".equals(action) || "reject".equals(action)) {
|
|
|
+ return action;
|
|
|
+ }
|
|
|
+ return "remind";
|
|
|
}
|
|
|
}
|