Merge branch 'feature/REQ-1309' into dev

This commit is contained in:
wangli 2023-10-11 14:30:07 +08:00
commit 150737b67c
23 changed files with 1227 additions and 447 deletions

View File

@ -102,7 +102,7 @@ public interface ProcessInstanceApi {
*
* @return
*/
@GetMapping("/api/process/instance/node/calc")
CommonResponse<List<ProcessNodeDetailVO>> processInstanceNodeCalc(@NotBlank(message = "流程实例 ID 不能为空") @RequestParam(required = false) String processInstanceId,
@Nullable @RequestParam(required = false) String tenantId);
@GetMapping("/api/process/instance/node/forecasting")
CommonResponse<List<ProcessNodeDetailVO>> processInstanceNodeForecast(@NotBlank(message = "流程实例 ID 不能为空") @RequestParam(required = false) String processInstanceId,
@Nullable @RequestParam(required = false) String tenantId);
}

View File

@ -55,6 +55,7 @@ public enum BpmErrorCode implements IProjectRespCode {
FORM_DEFINITION_PARSER_ERROR("08002", "表单定义内容解析出错"),
// ========== form Instance 09-001 ==========
// ========== flowable Engine 10-001 ==========
ENGINE_EXECUTION_LOST_ID_ERROR("10001", "Execution 丢失"),
// // ========== 流程模型 01-001 ==========

View File

@ -0,0 +1,29 @@
package cn.axzo.workflow.core.common.utils;
/**
* @author wangli
* @since 2023/10/9 15:10
*/
public class BpmnNativeQueryUtil {
public static String sqlConnectors(StringBuilder stringBuilder) {
if (stringBuilder.indexOf("WHERE") < 0) {
return " WHERE";
} else if (stringBuilder.indexOf("LEFT") >= 0 && stringBuilder.indexOf("ON") < 0) {
return " ON";
}
return " AND";
}
public static String countSql(StringBuilder stringBuilder) {
int start;
if ((start = stringBuilder.indexOf("SELECT")) < 0) {
return stringBuilder.toString();
}
if (stringBuilder.indexOf("LEFT JOIN") < 0) {
return stringBuilder.replace(start + 7, 8, "count(1)").toString();
} else {
return stringBuilder.replace(start + 7, 10, "count(1)").toString();
}
}
}

View File

@ -98,5 +98,5 @@ public interface BpmnProcessInstanceService {
BpmPageResult<HistoricProcessInstanceVO> historicProcessInstancePage(HistoricProcessInstanceSearchDTO dto);
List<ProcessNodeDetailVO> getProcessNodes(String processInstanceId, @Nullable String tenantId);
List<ProcessNodeDetailVO> getProcessInstanceNodeForecast(String processInstanceId, String tenantId);
}

View File

@ -18,40 +18,21 @@ import cn.axzo.workflow.common.model.response.bpmn.process.ProcessNodeDetailVO;
import cn.axzo.workflow.core.common.enums.BpmnProcessTaskResultEnum;
import cn.axzo.workflow.core.common.exception.WorkflowEngineException;
import cn.axzo.workflow.core.common.utils.BpmCollectionUtils;
import cn.axzo.workflow.core.repository.mapper.InfoMapper;
import cn.axzo.workflow.core.service.BpmnProcessDefinitionService;
import cn.axzo.workflow.core.service.BpmnProcessInstanceService;
import cn.axzo.workflow.core.service.converter.BpmnHistoricProcessInstanceConverter;
import cn.axzo.workflow.core.service.converter.BpmnProcessInstanceConverter;
import cn.axzo.workflow.core.service.converter.BpmnProcessInstancePageItemConverter;
import cn.axzo.workflow.core.service.support.FlowNodeForecastService;
import cn.axzo.workflow.core.service.support.ProcessGraphicService;
import cn.azxo.framework.common.utils.StringUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.model.Artifact;
import org.flowable.bpmn.model.Association;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.bpmn.model.ConditionalEventDefinition;
import org.flowable.bpmn.model.ErrorEventDefinition;
import org.flowable.bpmn.model.EscalationEventDefinition;
import org.flowable.bpmn.model.Event;
import org.flowable.bpmn.model.EventDefinition;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.bpmn.model.FlowNode;
import org.flowable.bpmn.model.GraphicInfo;
import org.flowable.bpmn.model.Lane;
import org.flowable.bpmn.model.MessageEventDefinition;
import org.flowable.bpmn.model.Pool;
import org.flowable.bpmn.model.SequenceFlow;
import org.flowable.bpmn.model.ServiceTask;
import org.flowable.bpmn.model.SignalEventDefinition;
import org.flowable.bpmn.model.StartEvent;
import org.flowable.bpmn.model.SubProcess;
import org.flowable.bpmn.model.TextAnnotation;
import org.flowable.bpmn.model.TimerEventDefinition;
import org.flowable.bpmn.model.UserTask;
import org.flowable.bpmn.model.VariableListenerEventDefinition;
import org.flowable.common.engine.impl.db.SuspensionState;
import org.flowable.common.engine.impl.identity.Authentication;
import org.flowable.engine.HistoryService;
@ -75,7 +56,6 @@ import javax.annotation.Nullable;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@ -103,6 +83,8 @@ import static cn.axzo.workflow.core.common.enums.BpmErrorCode.PROCESS_INSTANCE_C
import static cn.axzo.workflow.core.common.enums.BpmErrorCode.PROCESS_INSTANCE_ID_NOT_EXISTS;
import static cn.axzo.workflow.core.common.enums.BpmErrorCode.PROCESS_INSTANCE_NOT_EXISTS;
import static cn.axzo.workflow.core.common.enums.BpmErrorCode.PROCESS_OPERATION_PARAM_VALID_ERROR;
import static cn.axzo.workflow.core.common.utils.BpmnNativeQueryUtil.countSql;
import static cn.axzo.workflow.core.common.utils.BpmnNativeQueryUtil.sqlConnectors;
@Service
@Slf4j
@ -117,8 +99,6 @@ public class BpmnProcessInstanceServiceImpl implements BpmnProcessInstanceServic
@Resource
private HistoryService historyService;
@Resource
private ObjectMapper objectMapper;
@Resource
private BpmnProcessInstancePageItemConverter instancePageItemConverter;
@Resource
private BpmnProcessInstanceConverter instanceConverter;
@ -126,10 +106,10 @@ public class BpmnProcessInstanceServiceImpl implements BpmnProcessInstanceServic
private BpmnHistoricProcessInstanceConverter historicProcessInstanceConverter;
@Resource
private ManagementService managementService;
protected List<String> eventElementTypes = new ArrayList<>();
protected Map<String, InfoMapper> propertyMappers = new HashMap<>();
@Resource
private ProcessGraphicService graphicService;
@Resource
private FlowNodeForecastService forecastService;
@Override
public HistoricProcessInstance getProcessInstanceByBusinessKey(String businessKey, @Nullable String tenantId,
@ -228,7 +208,7 @@ public class BpmnProcessInstanceServiceImpl implements BpmnProcessInstanceServic
return true;
}
log.error("[updateProcessDefinitionState][流程定义({}) 修改未知状态({})]", processDefinitionId, status);
return null;
return false;
}
@ -444,12 +424,13 @@ public class BpmnProcessInstanceServiceImpl implements BpmnProcessInstanceServic
}
}
// Gather completed flows
List<String> completedFlows = gatherCompletedFlows(completedActivityInstances, currentElements, bpmnModel);
List<String> completedFlows = graphicService.gatherCompletedFlows(completedActivityInstances, currentElements
, bpmnModel);
Set<String> completedElements = new HashSet<>(completedActivityInstances);
completedElements.addAll(completedFlows);
ObjectNode displayNode = processProcessElements(bpmnModel, completedElements, currentElements,
ObjectNode displayNode = graphicService.processProcessElements(bpmnModel, completedElements, currentElements,
new ArrayList<>(), processInstanceId);
if (!CollectionUtils.isEmpty(completedActivityInstances)) {
ArrayNode completedActivities = displayNode.putArray("completedActivities");
@ -553,7 +534,6 @@ public class BpmnProcessInstanceServiceImpl implements BpmnProcessInstanceServic
}
}
@Override
public List<ProcessNodeDetailVO> getProcessNodes(String processInstanceId, @Nullable String tenantId) {
HistoricProcessInstance instance =
historyService.createHistoricProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();
@ -594,392 +574,43 @@ public class BpmnProcessInstanceServiceImpl implements BpmnProcessInstanceServic
return resultList;
}
private List<String> gatherCompletedFlows(Set<String> completedActivityInstances,
Set<String> currentActivityInstances, BpmnModel pojoModel) {
@Override
public List<ProcessNodeDetailVO> getProcessInstanceNodeForecast(String processInstanceId,
@Nullable String tenantId) {
ProcessInstance instance = runtimeService.createProcessInstanceQuery()
.processInstanceId(processInstanceId).processInstanceTenantId(tenantId).singleResult();
if (Objects.isNull(instance)) {
throw new WorkflowEngineException(PROCESS_INSTANCE_ID_NOT_EXISTS, processInstanceId);
}
List<FlowElement> flowElements = forecastService.performProcessForecasting(processInstanceId, instance);
List<String> completedFlows = new ArrayList<>();
List<String> activities = new ArrayList<>(completedActivityInstances);
activities.addAll(currentActivityInstances);
// TODO: not a robust way of checking when parallel paths are active, should be revisited
// Go over all activities and check if it's possible to match any outgoing paths against the activities
for (FlowElement activity : pojoModel.getMainProcess().getFlowElements()) {
if (activity instanceof FlowNode) {
int index = activities.indexOf(activity.getId());
if (index >= 0 && index + 1 < activities.size()) {
List<SequenceFlow> outgoingFlows = ((FlowNode) activity).getOutgoingFlows();
for (SequenceFlow flow : outgoingFlows) {
String destinationFlowId = flow.getTargetRef();
if (destinationFlowId.equals(activities.get(index + 1))) {
completedFlows.add(flow.getId());
}
}
}
List<ProcessNodeDetailVO> resultList = new ArrayList<>(flowElements.size() + 1);
flowElements.stream().filter(UserTask.class::isInstance).forEach(i -> {
UserTask userTask = (UserTask) i;
ProcessNodeDetailVO node = new ProcessNodeDetailVO().setId(userTask.getId())
.setName(userTask.getName())
.setFormKey(userTask.getFormKey());
if (userTask.getBehavior() instanceof MultiInstanceActivityBehavior) {
MultiInstanceActivityBehavior behavior = (MultiInstanceActivityBehavior) userTask.getBehavior();
node.setNodeMode(Objects.equals(AND_SIGN_EXPRESSION, behavior.getCompletionCondition()) ? AND : OR);
} else if (userTask.getBehavior() instanceof UserTaskActivityBehavior) {
node.setNodeMode(BpmnFlowNodeMode.GENERAL);
}
}
return completedFlows;
resultList.add(node);
});
// 处理发起节点
List<FlowElement> startNodes =
flowElements.stream().filter(StartEvent.class::isInstance).collect(Collectors.toList());
startNodes.forEach(i -> {
StartEvent startEvent = (StartEvent) i;
ProcessNodeDetailVO node = new ProcessNodeDetailVO()
.setId(startEvent.getId())
.setName(startEvent.getName())
.setFormKey(startEvent.getFormKey())
.setNodeMode(BpmnFlowNodeMode.STARTNODE);
resultList.add(0, node);
});
return resultList;
}
private ObjectNode processProcessElements(BpmnModel pojoModel, Set<String> completedElements,
Set<String> currentElements, Collection<String> breakpoints,
String processInstanceId) {
ObjectNode displayNode = objectMapper.createObjectNode();
GraphicInfo diagramInfo = new GraphicInfo();
ArrayNode elementArray = objectMapper.createArrayNode();
ArrayNode flowArray = objectMapper.createArrayNode();
ArrayNode collapsedArray = objectMapper.createArrayNode();
if (!CollectionUtils.isEmpty(pojoModel.getPools())) {
ArrayNode poolArray = objectMapper.createArrayNode();
boolean firstElement = true;
for (Pool pool : pojoModel.getPools()) {
ObjectNode poolNode = objectMapper.createObjectNode();
poolNode.put("id", pool.getId());
poolNode.put("name", pool.getName());
GraphicInfo poolInfo = pojoModel.getGraphicInfo(pool.getId());
fillGraphicInfo(poolNode, poolInfo, true);
org.flowable.bpmn.model.Process process = pojoModel.getProcess(pool.getId());
if (process != null && !CollectionUtils.isEmpty(process.getLanes())) {
ArrayNode laneArray = objectMapper.createArrayNode();
for (Lane lane : process.getLanes()) {
ObjectNode laneNode = objectMapper.createObjectNode();
laneNode.put("id", lane.getId());
laneNode.put("name", lane.getName());
fillGraphicInfo(laneNode, pojoModel.getGraphicInfo(lane.getId()), true);
laneArray.add(laneNode);
}
poolNode.set("lanes", laneArray);
}
poolArray.add(poolNode);
double rightX = poolInfo.getX() + poolInfo.getWidth();
double bottomY = poolInfo.getY() + poolInfo.getHeight();
double middleX = poolInfo.getX() + (poolInfo.getWidth() / 2);
if (firstElement || middleX < diagramInfo.getX()) {
diagramInfo.setX(middleX);
}
if (firstElement || poolInfo.getY() < diagramInfo.getY()) {
diagramInfo.setY(poolInfo.getY());
}
if (rightX > diagramInfo.getWidth()) {
diagramInfo.setWidth(rightX);
}
if (bottomY > diagramInfo.getHeight()) {
diagramInfo.setHeight(bottomY);
}
firstElement = false;
}
displayNode.set("pools", poolArray);
} else {
// in initialize with fake x and y to make sure the minimal
// values are set
diagramInfo.setX(9999);
diagramInfo.setY(1000);
}
for (org.flowable.bpmn.model.Process process : pojoModel.getProcesses()) {
processElements(process.getFlowElements(), pojoModel, elementArray, flowArray, collapsedArray,
diagramInfo, completedElements, currentElements, breakpoints, null, processInstanceId);
processArtifacts(process.getArtifacts(), pojoModel, elementArray, flowArray, diagramInfo);
}
displayNode.set("elements", elementArray);
displayNode.set("flows", flowArray);
displayNode.set("collapsed", collapsedArray);
displayNode.put("diagramBeginX", diagramInfo.getX());
displayNode.put("diagramBeginY", diagramInfo.getY());
displayNode.put("diagramWidth", diagramInfo.getWidth());
displayNode.put("diagramHeight", diagramInfo.getHeight());
return displayNode;
}
private void fillGraphicInfo(ObjectNode elementNode, GraphicInfo graphicInfo, boolean includeWidthAndHeight) {
commonFillGraphicInfo(elementNode, graphicInfo.getX(), graphicInfo.getY(), graphicInfo.getWidth(),
graphicInfo.getHeight(), includeWidthAndHeight);
}
private void commonFillGraphicInfo(ObjectNode elementNode, double x, double y, double width, double height,
boolean includeWidthAndHeight) {
elementNode.put("x", x);
elementNode.put("y", y);
if (includeWidthAndHeight) {
elementNode.put("width", width);
elementNode.put("height", height);
}
}
private void processElements(Collection<FlowElement> elementList, BpmnModel model, ArrayNode elementArray,
ArrayNode flowArray,
ArrayNode collapsedArray, GraphicInfo diagramInfo, Set<String> completedElements,
Set<String> currentElements, Collection<String> breakpoints,
ObjectNode collapsedNode, String processInstanceId) {
for (FlowElement element : elementList) {
ObjectNode elementNode = objectMapper.createObjectNode();
if (completedElements != null) {
elementNode.put("completed", completedElements.contains(element.getId()));
}
if (!breakpoints.isEmpty()) {
elementNode.put("breakpoint", breakpoints.contains(element.getId()));
}
if (currentElements != null) {
elementNode.put("current", currentElements.contains(element.getId()));
}
if (element instanceof SequenceFlow) {
SequenceFlow flow = (SequenceFlow) element;
elementNode.put("id", flow.getId());
elementNode.put("type", "sequenceFlow");
elementNode.put("sourceRef", flow.getSourceRef());
elementNode.put("targetRef", flow.getTargetRef());
elementNode.put("name", flow.getName());
List<GraphicInfo> flowInfo = model.getFlowLocationGraphicInfo(flow.getId());
if (!CollectionUtils.isEmpty(flowInfo)) {
ArrayNode waypointArray = objectMapper.createArrayNode();
for (GraphicInfo graphicInfo : flowInfo) {
ObjectNode pointNode = objectMapper.createObjectNode();
fillGraphicInfo(pointNode, graphicInfo, false);
waypointArray.add(pointNode);
fillDiagramInfo(graphicInfo, diagramInfo);
}
elementNode.set("waypoints", waypointArray);
String className = element.getClass().getSimpleName();
if (propertyMappers.containsKey(className)) {
elementNode.set("properties", propertyMappers.get(className).map(element));
}
if (collapsedNode != null) {
((ArrayNode) collapsedNode.get("flows")).add(elementNode);
} else {
flowArray.add(elementNode);
}
}
} else {
elementNode.put("id", element.getId());
elementNode.put("name", element.getName());
GraphicInfo graphicInfo = model.getGraphicInfo(element.getId());
if (graphicInfo != null) {
fillGraphicInfo(elementNode, graphicInfo, true);
fillDiagramInfo(graphicInfo, diagramInfo);
}
String className = element.getClass().getSimpleName();
elementNode.put("type", className);
fillEventTypes(className, element, elementNode);
if (element instanceof ServiceTask) {
ServiceTask serviceTask = (ServiceTask) element;
if (ServiceTask.MAIL_TASK.equals(serviceTask.getType())) {
elementNode.put("taskType", "mail");
} else if ("camel".equals(serviceTask.getType())) {
elementNode.put("taskType", "camel");
} else if ("mule".equals(serviceTask.getType())) {
elementNode.put("taskType", "mule");
} else if (ServiceTask.HTTP_TASK.equals(serviceTask.getType())) {
elementNode.put("taskType", "http");
} else if (ServiceTask.SHELL_TASK.equals(serviceTask.getType())) {
elementNode.put("taskType", "shell");
}
}
if (propertyMappers.containsKey(className)) {
elementNode.set("properties", propertyMappers.get(className).map(element));
}
if (collapsedNode != null) {
((ArrayNode) collapsedNode.get("elements")).add(elementNode);
} else {
elementArray.add(elementNode);
}
if (element instanceof SubProcess) {
SubProcess subProcess = (SubProcess) element;
ObjectNode newCollapsedNode = collapsedNode;
// skip collapsed sub processes
if (graphicInfo != null && graphicInfo.getExpanded() != null && !graphicInfo.getExpanded()) {
elementNode.put("collapsed", "true");
newCollapsedNode = objectMapper.createObjectNode();
newCollapsedNode.put("id", subProcess.getId());
newCollapsedNode.putArray("elements");
newCollapsedNode.putArray("flows");
collapsedArray.add(newCollapsedNode);
}
processElements(subProcess.getFlowElements(), model, elementArray, flowArray, collapsedArray,
diagramInfo, completedElements, currentElements, breakpoints, newCollapsedNode,
processInstanceId);
processArtifacts(subProcess.getArtifacts(), model, elementArray, flowArray, diagramInfo);
}
}
}
}
private void processArtifacts(Collection<Artifact> artifactList, BpmnModel model, ArrayNode elementArray,
ArrayNode flowArray, GraphicInfo diagramInfo) {
for (Artifact artifact : artifactList) {
if (artifact instanceof Association) {
ObjectNode elementNode = objectMapper.createObjectNode();
Association flow = (Association) artifact;
elementNode.put("id", flow.getId());
elementNode.put("type", "association");
elementNode.put("sourceRef", flow.getSourceRef());
elementNode.put("targetRef", flow.getTargetRef());
fillWaypoints(flow.getId(), model, elementNode, diagramInfo);
flowArray.add(elementNode);
} else {
ObjectNode elementNode = objectMapper.createObjectNode();
elementNode.put("id", artifact.getId());
if (artifact instanceof TextAnnotation) {
TextAnnotation annotation = (TextAnnotation) artifact;
elementNode.put("text", annotation.getText());
}
GraphicInfo graphicInfo = model.getGraphicInfo(artifact.getId());
if (graphicInfo != null) {
fillGraphicInfo(elementNode, graphicInfo, true);
fillDiagramInfo(graphicInfo, diagramInfo);
}
String className = artifact.getClass().getSimpleName();
elementNode.put("type", className);
elementArray.add(elementNode);
}
}
}
private void fillDiagramInfo(GraphicInfo graphicInfo, GraphicInfo diagramInfo) {
double rightX = graphicInfo.getX() + graphicInfo.getWidth();
double bottomY = graphicInfo.getY() + graphicInfo.getHeight();
double middleX = graphicInfo.getX() + (graphicInfo.getWidth() / 2);
if (middleX < diagramInfo.getX()) {
diagramInfo.setX(middleX);
}
if (graphicInfo.getY() < diagramInfo.getY()) {
diagramInfo.setY(graphicInfo.getY());
}
if (rightX > diagramInfo.getWidth()) {
diagramInfo.setWidth(rightX);
}
if (bottomY > diagramInfo.getHeight()) {
diagramInfo.setHeight(bottomY);
}
}
private void fillWaypoints(String id, BpmnModel model, ObjectNode elementNode, GraphicInfo diagramInfo) {
List<GraphicInfo> flowInfo = model.getFlowLocationGraphicInfo(id);
ArrayNode waypointArray = objectMapper.createArrayNode();
for (GraphicInfo graphicInfo : flowInfo) {
ObjectNode pointNode = objectMapper.createObjectNode();
fillGraphicInfo(pointNode, graphicInfo, false);
waypointArray.add(pointNode);
fillDiagramInfo(graphicInfo, diagramInfo);
}
elementNode.set("waypoints", waypointArray);
}
private void fillEventTypes(String className, FlowElement element, ObjectNode elementNode) {
if (eventElementTypes.contains(className)) {
Event event = (Event) element;
if (!CollectionUtils.isEmpty(event.getEventDefinitions())) {
EventDefinition eventDef = event.getEventDefinitions().get(0);
ObjectNode eventNode = objectMapper.createObjectNode();
if (eventDef instanceof TimerEventDefinition) {
TimerEventDefinition timerDef = (TimerEventDefinition) eventDef;
eventNode.put("type", "timer");
if (org.apache.commons.lang3.StringUtils.isNotEmpty(timerDef.getTimeCycle())) {
eventNode.put("timeCycle", timerDef.getTimeCycle());
}
if (org.apache.commons.lang3.StringUtils.isNotEmpty(timerDef.getTimeDate())) {
eventNode.put("timeDate", timerDef.getTimeDate());
}
if (org.apache.commons.lang3.StringUtils.isNotEmpty(timerDef.getTimeDuration())) {
eventNode.put("timeDuration", timerDef.getTimeDuration());
}
} else if (eventDef instanceof ConditionalEventDefinition) {
ConditionalEventDefinition conditionalDef = (ConditionalEventDefinition) eventDef;
eventNode.put("type", "conditional");
if (org.apache.commons.lang3.StringUtils.isNotEmpty(conditionalDef.getConditionExpression())) {
eventNode.put("condition", conditionalDef.getConditionExpression());
}
} else if (eventDef instanceof ErrorEventDefinition) {
ErrorEventDefinition errorDef = (ErrorEventDefinition) eventDef;
eventNode.put("type", "error");
if (org.apache.commons.lang3.StringUtils.isNotEmpty(errorDef.getErrorCode())) {
eventNode.put("errorCode", errorDef.getErrorCode());
}
} else if (eventDef instanceof EscalationEventDefinition) {
EscalationEventDefinition escalationDef = (EscalationEventDefinition) eventDef;
eventNode.put("type", "escalation");
if (org.apache.commons.lang3.StringUtils.isNotEmpty(escalationDef.getEscalationCode())) {
eventNode.put("escalationCode", escalationDef.getEscalationCode());
}
} else if (eventDef instanceof SignalEventDefinition) {
SignalEventDefinition signalDef = (SignalEventDefinition) eventDef;
eventNode.put("type", "signal");
if (org.apache.commons.lang3.StringUtils.isNotEmpty(signalDef.getSignalRef())) {
eventNode.put("signalRef", signalDef.getSignalRef());
}
} else if (eventDef instanceof MessageEventDefinition) {
MessageEventDefinition messageDef = (MessageEventDefinition) eventDef;
eventNode.put("type", "message");
if (org.apache.commons.lang3.StringUtils.isNotEmpty(messageDef.getMessageRef())) {
eventNode.put("messageRef", messageDef.getMessageRef());
}
} else if (eventDef instanceof VariableListenerEventDefinition) {
VariableListenerEventDefinition variableDef = (VariableListenerEventDefinition) eventDef;
eventNode.put("type", "variable");
if (org.apache.commons.lang3.StringUtils.isNotEmpty(variableDef.getVariableName())) {
eventNode.put("variableName", variableDef.getVariableName());
}
if (org.apache.commons.lang3.StringUtils.isNotEmpty(variableDef.getVariableChangeType())) {
eventNode.put("variableChangeType", variableDef.getVariableChangeType());
}
}
elementNode.set("eventDefinition", eventNode);
}
}
}
public static String sqlConnectors(StringBuilder stringBuilder) {
if (stringBuilder.indexOf("WHERE") < 0) {
return " WHERE";
} else if (stringBuilder.indexOf("LEFT") >= 0 && stringBuilder.indexOf("ON") < 0) {
return " ON";
}
return " AND";
}
public static String countSql(StringBuilder stringBuilder) {
int start = 0;
if ((start = stringBuilder.indexOf("SELECT")) < 0) {
return stringBuilder.toString();
}
return stringBuilder.replace(start + 7, 10, "count(1)").toString();
}
}

View File

@ -16,7 +16,12 @@ import cn.axzo.workflow.core.service.converter.FormModelConverter;
import org.flowable.common.engine.impl.db.SuspensionState;
import org.flowable.engine.ManagementService;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.repository.*;
import org.flowable.engine.repository.Deployment;
import org.flowable.engine.repository.Model;
import org.flowable.engine.repository.ModelQuery;
import org.flowable.engine.repository.NativeModelQuery;
import org.flowable.engine.repository.ProcessDefinition;
import org.flowable.engine.repository.ProcessDefinitionQuery;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -32,10 +37,16 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import static cn.axzo.workflow.common.constant.MetaInfoConstants.*;
import static cn.axzo.workflow.core.common.enums.BpmErrorCode.*;
import static cn.axzo.workflow.core.service.impl.FormModelServiceImpl.countSql;
import static cn.axzo.workflow.core.service.impl.FormModelServiceImpl.sqlConnectors;
import static cn.axzo.workflow.common.constant.MetaInfoConstants.MODEL_DESCRIPTION;
import static cn.axzo.workflow.common.constant.MetaInfoConstants.MODEL_TYPE;
import static cn.axzo.workflow.common.constant.MetaInfoConstants.MODEL_TYPE_PROCESS;
import static cn.axzo.workflow.core.common.enums.BpmErrorCode.MODEL_ID_NOT_EXISTS;
import static cn.axzo.workflow.core.common.enums.BpmErrorCode.MODEL_KEY_EXISTS;
import static cn.axzo.workflow.core.common.enums.BpmErrorCode.MODEL_KEY_NOT_EXISTS;
import static cn.axzo.workflow.core.common.enums.BpmErrorCode.MODEL_NOT_EXISTS;
import static cn.axzo.workflow.core.common.enums.BpmErrorCode.PROCESS_DEFINITION_BPMN_NOT_EXISTS;
import static cn.axzo.workflow.core.common.utils.BpmnNativeQueryUtil.countSql;
import static cn.axzo.workflow.core.common.utils.BpmnNativeQueryUtil.sqlConnectors;
@Service
public class BpmnProcessModelServiceImpl implements BpmnProcessModelService {

View File

@ -85,8 +85,8 @@ import static cn.axzo.workflow.core.common.enums.BpmErrorCode.PROCESS_INSTANCE_N
import static cn.axzo.workflow.core.common.enums.BpmErrorCode.TASK_COMPLETE_FAIL_ASSIGN_NOT_SELF;
import static cn.axzo.workflow.core.common.enums.BpmErrorCode.TASK_COMPLETE_FAIL_NOT_EXISTS;
import static cn.axzo.workflow.core.common.utils.BpmCollectionUtils.convertSet;
import static cn.axzo.workflow.core.service.impl.BpmnProcessInstanceServiceImpl.countSql;
import static cn.axzo.workflow.core.service.impl.BpmnProcessInstanceServiceImpl.sqlConnectors;
import static cn.axzo.workflow.core.common.utils.BpmnNativeQueryUtil.countSql;
import static cn.axzo.workflow.core.common.utils.BpmnNativeQueryUtil.sqlConnectors;
import static org.flowable.engine.impl.persistence.entity.CommentEntity.TYPE_COMMENT;
@Service

View File

@ -32,9 +32,11 @@ import static cn.axzo.workflow.common.constant.BpmConstants.FORM_FILE_SUFFIX;
import static cn.axzo.workflow.common.constant.MetaInfoConstants.MODEL_TYPE;
import static cn.axzo.workflow.common.constant.MetaInfoConstants.MODEL_TYPE_FORM;
import static cn.axzo.workflow.core.common.enums.BpmErrorCode.FORM_MODEL_NOT_EXISTS;
import static cn.axzo.workflow.core.common.utils.BpmnNativeQueryUtil.countSql;
import static cn.axzo.workflow.core.common.utils.BpmnNativeQueryUtil.sqlConnectors;
/**
* TODO
* 表单模型 Service
*
* @author wangli
* @since 2023/7/25 10:13
@ -217,18 +219,4 @@ public class FormModelServiceImpl implements FormModelService {
repositoryService.deleteModel(model.getId());
}
public static String sqlConnectors(StringBuilder stringBuilder) {
if (stringBuilder.indexOf("where") < 0) {
return " where";
}
return " and";
}
public static String countSql(StringBuilder stringBuilder) {
int start = 0;
if ((start = stringBuilder.indexOf("SELECT")) < 0) {
return stringBuilder.toString();
}
return stringBuilder.replace(start + 7, 8, "count(1)").toString();
}
}

View File

@ -0,0 +1,53 @@
package cn.axzo.workflow.core.service.support;
import cn.axzo.workflow.core.common.exception.WorkflowEngineException;
import cn.azxo.framework.common.utils.StringUtils;
import org.flowable.common.engine.api.delegate.Expression;
import org.flowable.common.engine.impl.interceptor.Command;
import org.flowable.common.engine.impl.interceptor.CommandContext;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl;
import org.flowable.engine.impl.persistence.entity.ExecutionEntity;
import java.io.Serializable;
import static cn.axzo.workflow.core.common.enums.BpmErrorCode.ENGINE_EXECUTION_LOST_ID_ERROR;
/**
* 流程定义内部的表达式评估命令器
*
* @author wangli
* @since 2023/10/9 19:30
*/
public class ExpressionConditionCmd implements Command<Boolean>, Serializable {
protected final RuntimeService runtimeService;
protected final ProcessEngineConfigurationImpl processEngineConfiguration;
protected final String processInstanceId;
protected final String exp;
public ExpressionConditionCmd(RuntimeService runtimeService,
ProcessEngineConfigurationImpl processEngineConfiguration,
String processInstanceId, String exp) {
this.runtimeService = runtimeService;
this.processEngineConfiguration = processEngineConfiguration;
this.processInstanceId = processInstanceId;
this.exp = exp;
}
@Override
public Boolean execute(CommandContext commandContext) {
Expression expression = processEngineConfiguration.getExpressionManager().createExpression(this.exp);
ExecutionEntity executionEntity;
if (StringUtils.isNotBlank(this.processInstanceId)) {
executionEntity = (ExecutionEntity) runtimeService.createProcessInstanceQuery()
.processInstanceId(this.processInstanceId).includeProcessVariables().singleResult();
} else {
// 不能单纯的 new ExecutionEntityImpl, 后续在调用 setVariables ,
// 引擎用了很多 ExecutionEntityImpl 的其他属性,这些属性都是在 new 不会自动生成的
throw new WorkflowEngineException(ENGINE_EXECUTION_LOST_ID_ERROR);
}
Object value = expression.getValue(executionEntity);
return value != null && "true".equals(value.toString());
}
}

View File

@ -0,0 +1,184 @@
package cn.axzo.workflow.core.service.support;
import cn.axzo.workflow.core.common.exception.WorkflowEngineException;
import cn.axzo.workflow.core.service.support.forecast.AbstractForecast;
import cn.axzo.workflow.core.service.support.forecast.Forecast;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.bpmn.model.StartEvent;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.runtime.ProcessInstance;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.ResolvableType;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import javax.annotation.Nullable;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import static cn.axzo.workflow.core.common.enums.BpmErrorCode.PROCESS_INSTANCE_NOT_EXISTS;
/**
* 审批实例的运行节点预测/推断
* <p>
* 一个流程定义中所有的节点都是 FlowElement 类型, FlowElement 是引擎中的一个基类, 如果要完整推测审批流程执行顺序不亚于重写一套流程执行引擎
* 所以这里仅按照业务所需的节点类型进行处理.并且每个节点都未处理出口是多个的情况,目前仅支持单出口!
* <p>
* 全量的接入类型如下:
* <pre>
* FlowElement (org.flowable.bpmn.model)
* |-- SequenceFlow (org.flowable.bpmn.model)
* |-- DataStoreReference (org.flowable.bpmn.model)
* |-- DataObject (org.flowable.bpmn.model)
* |-- ValuedDataObject (org.flowable.bpmn.model)
* |-- StringDataObject (org.flowable.bpmn.model)
* |-- LongDataObject (org.flowable.bpmn.model)
* |-- BooleanDataObject (org.flowable.bpmn.model)
* |-- DoubleDataObject (org.flowable.bpmn.model)
* |-- JsonDataObject (org.flowable.bpmn.model)
* |-- DateDataObject (org.flowable.bpmn.model)
* |-- IntegerDataObject (org.flowable.bpmn.model)
* |-- FlowNode (org.flowable.bpmn.model)
* |-- Activity (org.flowable.bpmn.model)
* |-- CallActivity (org.flowable.bpmn.model)
* |-- SubProcess (org.flowable.bpmn.model)
* |-- Transaction (org.flowable.bpmn.model)
* |-- EventSubProcess (org.flowable.bpmn.model)
* |-- AdhocSubProcess (org.flowable.bpmn.model)
* |-- Task (org.flowable.bpmn.model)
* |-- ScriptTask (org.flowable.bpmn.model)
* |-- ManualTask (org.flowable.bpmn.model)
* |-- ReceiveTask (org.flowable.bpmn.model)
* |-- BusinessRuleTask (org.flowable.bpmn.model)
* |-- TaskWithFieldExtensions (org.flowable.bpmn.model)
* |-- SendTask (org.flowable.bpmn.model)
* |-- ServiceTask (org.flowable.bpmn.model)
* |-- AlfrescoScriptTask (org.flowable.bpmn.model.alfresco)
* |-- HttpServiceTask (org.flowable.bpmn.model)
* |-- CaseServiceTask (org.flowable.bpmn.model)
* |-- ExternalWorkerServiceTask (org.flowable.bpmn.model)
* |-- SendEventServiceTask (org.flowable.bpmn.model)
* |-- AlfrescoMailTask (org.flowable.bpmn.model.alfresco)
* |-- UserTask (org.flowable.bpmn.model)
* |-- AlfrescoUserTask (org.flowable.bpmn.model.alfresco)
* |-- Gateway (org.flowable.bpmn.model)
* |-- ExclusiveGateway (org.flowable.bpmn.model)
* |-- ComplexGateway (org.flowable.bpmn.model)
* |-- ParallelGateway (org.flowable.bpmn.model)
* |-- InclusiveGateway (org.flowable.bpmn.model)
* |-- EventGateway (org.flowable.bpmn.model)
* |-- Event (org.flowable.bpmn.model)
* |-- EndEvent (org.flowable.bpmn.model)
* |-- BoundaryEvent (org.flowable.bpmn.model)
* |-- ThrowEvent (org.flowable.bpmn.model)
* |-- StartEvent (org.flowable.bpmn.model)
* |-- AlfrescoStartEvent (org.flowable.bpmn.model.alfresco)
* |-- IntermediateCatchEvent (org.flowable.bpmn.model)
* </pre>
*
* @author wangli
* @since 2023/10/9 17:38
*/
@Service
public class FlowNodeForecastService implements InitializingBean {
public final static Map<Class<?>, AbstractForecast<? extends FlowElement>> FORECAST_MAP = new HashMap<>();
@Resource
private RepositoryService repositoryService;
@Resource
private RuntimeService runtimeService;
@Resource
private List<Forecast<? extends FlowElement>> forecasts;
/**
* 执行运行中的流程预测
* <p>
* 已完成的流程可以直接查询流程审批记录就行
*
* @param processInstanceId 指定运行时的流程实例 ID
* @param instance 外部传入流程实例 (与另外一个参数必须二选一)
*/
public List<FlowElement> performProcessForecasting(@Nullable String processInstanceId,
@Nullable ProcessInstance instance) {
if (Objects.nonNull(instance)) {
// nothing to do
} else if (!StringUtils.hasLength(processInstanceId)) {
throw new WorkflowEngineException(PROCESS_INSTANCE_NOT_EXISTS);
} else {
instance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId)
.includeProcessVariables().singleResult();
}
BpmnModel bpmnModel = repositoryService.getBpmnModel(instance.getProcessDefinitionId());
// 保持推测出来的节点执行顺序的容器
List<FlowElement> orderNodes = new ArrayList<>();
// 流程定义中所有的FlowElement
Collection<FlowElement> flowElements = bpmnModel.getMainProcess().getFlowElements();
// 开始节点
findStartNode(flowElements).ifPresent(startNode -> {
addOrderFlowNodes(orderNodes, startNode);
});
startForecasting(orderNodes, instance);
return orderNodes;
}
private void startForecasting(List<FlowElement> orderNodes, ProcessInstance instance) {
getLastNode(orderNodes).ifPresent(flowElement -> forecasts.forEach(i -> {
if (i.support(flowElement)) {
List<FlowElement> list = i.nextFlowElement(flowElement, instance);
if (!CollectionUtils.isEmpty(list)) {
addOrderFlowNodes(orderNodes, list.get(0));
startForecasting(orderNodes, instance);
}
}
}));
}
/**
* 查找开始节点
* <p>
* 由于一个定义中, 是允许存在多个 StartEvent, 但至少有一个 StartEvent 节点. 但我们目前的业务还未用到多个开始事件, 所以未做过多的筛选动作
*
* @param flowElements 全量的 FlowElement
* @return
*/
private Optional<StartEvent> findStartNode(Collection<FlowElement> flowElements) {
return Optional.ofNullable(flowElements.stream().filter(StartEvent.class::isInstance).findFirst()
.map(StartEvent.class::cast).orElseThrow(() -> new IllegalArgumentException("非法的参数, 正确的流程定义一定有一个 " +
"StartEvent 节点")));
}
private void addOrderFlowNodes(List<FlowElement> orderFlowNodes, FlowElement flowElement) {
if (Objects.nonNull(flowElement)) {
orderFlowNodes.add(flowElement);
}
}
private Optional<FlowElement> getLastNode(List<FlowElement> orderFlowNodes) {
if (CollectionUtils.isEmpty(orderFlowNodes)) {
return Optional.empty();
}
return Optional.of(orderFlowNodes.get(orderFlowNodes.size() - 1));
}
@Override
public void afterPropertiesSet() {
forecasts.forEach(i -> {
Class<?> rawClass = ResolvableType.forClass(i.getClass(), Forecast.class)
.getSuperType().getGenerics()[0].getRawClass();
FORECAST_MAP.put(rawClass, (AbstractForecast<? extends FlowElement>) i);
});
}
}

View File

@ -0,0 +1,424 @@
package cn.axzo.workflow.core.service.support;
import cn.axzo.workflow.core.repository.mapper.InfoMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.AllArgsConstructor;
import org.flowable.bpmn.model.Artifact;
import org.flowable.bpmn.model.Association;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.bpmn.model.ConditionalEventDefinition;
import org.flowable.bpmn.model.ErrorEventDefinition;
import org.flowable.bpmn.model.EscalationEventDefinition;
import org.flowable.bpmn.model.Event;
import org.flowable.bpmn.model.EventDefinition;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.bpmn.model.FlowNode;
import org.flowable.bpmn.model.GraphicInfo;
import org.flowable.bpmn.model.Lane;
import org.flowable.bpmn.model.MessageEventDefinition;
import org.flowable.bpmn.model.Pool;
import org.flowable.bpmn.model.SequenceFlow;
import org.flowable.bpmn.model.ServiceTask;
import org.flowable.bpmn.model.SignalEventDefinition;
import org.flowable.bpmn.model.SubProcess;
import org.flowable.bpmn.model.TextAnnotation;
import org.flowable.bpmn.model.TimerEventDefinition;
import org.flowable.bpmn.model.VariableListenerEventDefinition;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 流程节点图形化工具服务类
*
* @author wangli
* @since 2023/10/9 17:23
*/
@Service
@AllArgsConstructor
public class ProcessGraphicService {
private final ObjectMapper objectMapper;
private List<String> eventElementTypes = new ArrayList<>();
private Map<String, InfoMapper> propertyMappers = new HashMap<>();
public List<String> gatherCompletedFlows(Set<String> completedActivityInstances,
Set<String> currentActivityInstances, BpmnModel pojoModel) {
List<String> completedFlows = new ArrayList<>();
List<String> activities = new ArrayList<>(completedActivityInstances);
activities.addAll(currentActivityInstances);
// TODO: not a robust way of checking when parallel paths are active, should be revisited
// Go over all activities and check if it's possible to match any outgoing paths against the activities
for (FlowElement activity : pojoModel.getMainProcess().getFlowElements()) {
if (activity instanceof FlowNode) {
int index = activities.indexOf(activity.getId());
if (index >= 0 && index + 1 < activities.size()) {
List<SequenceFlow> outgoingFlows = ((FlowNode) activity).getOutgoingFlows();
for (SequenceFlow flow : outgoingFlows) {
String destinationFlowId = flow.getTargetRef();
if (destinationFlowId.equals(activities.get(index + 1))) {
completedFlows.add(flow.getId());
}
}
}
}
}
return completedFlows;
}
public ObjectNode processProcessElements(BpmnModel pojoModel, Set<String> completedElements,
Set<String> currentElements, Collection<String> breakpoints,
String processInstanceId) {
ObjectNode displayNode = objectMapper.createObjectNode();
GraphicInfo diagramInfo = new GraphicInfo();
ArrayNode elementArray = objectMapper.createArrayNode();
ArrayNode flowArray = objectMapper.createArrayNode();
ArrayNode collapsedArray = objectMapper.createArrayNode();
if (!CollectionUtils.isEmpty(pojoModel.getPools())) {
ArrayNode poolArray = objectMapper.createArrayNode();
boolean firstElement = true;
for (Pool pool : pojoModel.getPools()) {
ObjectNode poolNode = objectMapper.createObjectNode();
poolNode.put("id", pool.getId());
poolNode.put("name", pool.getName());
GraphicInfo poolInfo = pojoModel.getGraphicInfo(pool.getId());
fillGraphicInfo(poolNode, poolInfo, true);
org.flowable.bpmn.model.Process process = pojoModel.getProcess(pool.getId());
if (process != null && !CollectionUtils.isEmpty(process.getLanes())) {
ArrayNode laneArray = objectMapper.createArrayNode();
for (Lane lane : process.getLanes()) {
ObjectNode laneNode = objectMapper.createObjectNode();
laneNode.put("id", lane.getId());
laneNode.put("name", lane.getName());
fillGraphicInfo(laneNode, pojoModel.getGraphicInfo(lane.getId()), true);
laneArray.add(laneNode);
}
poolNode.set("lanes", laneArray);
}
poolArray.add(poolNode);
double rightX = poolInfo.getX() + poolInfo.getWidth();
double bottomY = poolInfo.getY() + poolInfo.getHeight();
double middleX = poolInfo.getX() + (poolInfo.getWidth() / 2);
if (firstElement || middleX < diagramInfo.getX()) {
diagramInfo.setX(middleX);
}
if (firstElement || poolInfo.getY() < diagramInfo.getY()) {
diagramInfo.setY(poolInfo.getY());
}
if (rightX > diagramInfo.getWidth()) {
diagramInfo.setWidth(rightX);
}
if (bottomY > diagramInfo.getHeight()) {
diagramInfo.setHeight(bottomY);
}
firstElement = false;
}
displayNode.set("pools", poolArray);
} else {
// in initialize with fake x and y to make sure the minimal
// values are set
diagramInfo.setX(9999);
diagramInfo.setY(1000);
}
for (org.flowable.bpmn.model.Process process : pojoModel.getProcesses()) {
processElements(process.getFlowElements(), pojoModel, elementArray, flowArray, collapsedArray,
diagramInfo, completedElements, currentElements, breakpoints, null, processInstanceId);
processArtifacts(process.getArtifacts(), pojoModel, elementArray, flowArray, diagramInfo);
}
displayNode.set("elements", elementArray);
displayNode.set("flows", flowArray);
displayNode.set("collapsed", collapsedArray);
displayNode.put("diagramBeginX", diagramInfo.getX());
displayNode.put("diagramBeginY", diagramInfo.getY());
displayNode.put("diagramWidth", diagramInfo.getWidth());
displayNode.put("diagramHeight", diagramInfo.getHeight());
return displayNode;
}
private void fillGraphicInfo(ObjectNode elementNode, GraphicInfo graphicInfo, boolean includeWidthAndHeight) {
commonFillGraphicInfo(elementNode, graphicInfo.getX(), graphicInfo.getY(), graphicInfo.getWidth(),
graphicInfo.getHeight(), includeWidthAndHeight);
}
private void commonFillGraphicInfo(ObjectNode elementNode, double x, double y, double width, double height,
boolean includeWidthAndHeight) {
elementNode.put("x", x);
elementNode.put("y", y);
if (includeWidthAndHeight) {
elementNode.put("width", width);
elementNode.put("height", height);
}
}
private void processElements(Collection<FlowElement> elementList, BpmnModel model, ArrayNode elementArray,
ArrayNode flowArray,
ArrayNode collapsedArray, GraphicInfo diagramInfo, Set<String> completedElements,
Set<String> currentElements, Collection<String> breakpoints,
ObjectNode collapsedNode, String processInstanceId) {
for (FlowElement element : elementList) {
ObjectNode elementNode = objectMapper.createObjectNode();
if (completedElements != null) {
elementNode.put("completed", completedElements.contains(element.getId()));
}
if (!breakpoints.isEmpty()) {
elementNode.put("breakpoint", breakpoints.contains(element.getId()));
}
if (currentElements != null) {
elementNode.put("current", currentElements.contains(element.getId()));
}
if (element instanceof SequenceFlow) {
SequenceFlow flow = (SequenceFlow) element;
elementNode.put("id", flow.getId());
elementNode.put("type", "sequenceFlow");
elementNode.put("sourceRef", flow.getSourceRef());
elementNode.put("targetRef", flow.getTargetRef());
elementNode.put("name", flow.getName());
List<GraphicInfo> flowInfo = model.getFlowLocationGraphicInfo(flow.getId());
if (!CollectionUtils.isEmpty(flowInfo)) {
ArrayNode waypointArray = objectMapper.createArrayNode();
for (GraphicInfo graphicInfo : flowInfo) {
ObjectNode pointNode = objectMapper.createObjectNode();
fillGraphicInfo(pointNode, graphicInfo, false);
waypointArray.add(pointNode);
fillDiagramInfo(graphicInfo, diagramInfo);
}
elementNode.set("waypoints", waypointArray);
String className = element.getClass().getSimpleName();
if (propertyMappers.containsKey(className)) {
elementNode.set("properties", propertyMappers.get(className).map(element));
}
if (collapsedNode != null) {
((ArrayNode) collapsedNode.get("flows")).add(elementNode);
} else {
flowArray.add(elementNode);
}
}
} else {
elementNode.put("id", element.getId());
elementNode.put("name", element.getName());
GraphicInfo graphicInfo = model.getGraphicInfo(element.getId());
if (graphicInfo != null) {
fillGraphicInfo(elementNode, graphicInfo, true);
fillDiagramInfo(graphicInfo, diagramInfo);
}
String className = element.getClass().getSimpleName();
elementNode.put("type", className);
fillEventTypes(className, element, elementNode);
if (element instanceof ServiceTask) {
ServiceTask serviceTask = (ServiceTask) element;
if (ServiceTask.MAIL_TASK.equals(serviceTask.getType())) {
elementNode.put("taskType", "mail");
} else if ("camel".equals(serviceTask.getType())) {
elementNode.put("taskType", "camel");
} else if ("mule".equals(serviceTask.getType())) {
elementNode.put("taskType", "mule");
} else if (ServiceTask.HTTP_TASK.equals(serviceTask.getType())) {
elementNode.put("taskType", "http");
} else if (ServiceTask.SHELL_TASK.equals(serviceTask.getType())) {
elementNode.put("taskType", "shell");
}
}
if (propertyMappers.containsKey(className)) {
elementNode.set("properties", propertyMappers.get(className).map(element));
}
if (collapsedNode != null) {
((ArrayNode) collapsedNode.get("elements")).add(elementNode);
} else {
elementArray.add(elementNode);
}
if (element instanceof SubProcess) {
SubProcess subProcess = (SubProcess) element;
ObjectNode newCollapsedNode = collapsedNode;
// skip collapsed sub processes
if (graphicInfo != null && graphicInfo.getExpanded() != null && !graphicInfo.getExpanded()) {
elementNode.put("collapsed", "true");
newCollapsedNode = objectMapper.createObjectNode();
newCollapsedNode.put("id", subProcess.getId());
newCollapsedNode.putArray("elements");
newCollapsedNode.putArray("flows");
collapsedArray.add(newCollapsedNode);
}
processElements(subProcess.getFlowElements(), model, elementArray, flowArray, collapsedArray,
diagramInfo, completedElements, currentElements, breakpoints, newCollapsedNode,
processInstanceId);
processArtifacts(subProcess.getArtifacts(), model, elementArray, flowArray, diagramInfo);
}
}
}
}
private void processArtifacts(Collection<Artifact> artifactList, BpmnModel model, ArrayNode elementArray,
ArrayNode flowArray, GraphicInfo diagramInfo) {
for (Artifact artifact : artifactList) {
if (artifact instanceof Association) {
ObjectNode elementNode = objectMapper.createObjectNode();
Association flow = (Association) artifact;
elementNode.put("id", flow.getId());
elementNode.put("type", "association");
elementNode.put("sourceRef", flow.getSourceRef());
elementNode.put("targetRef", flow.getTargetRef());
fillWaypoints(flow.getId(), model, elementNode, diagramInfo);
flowArray.add(elementNode);
} else {
ObjectNode elementNode = objectMapper.createObjectNode();
elementNode.put("id", artifact.getId());
if (artifact instanceof TextAnnotation) {
TextAnnotation annotation = (TextAnnotation) artifact;
elementNode.put("text", annotation.getText());
}
GraphicInfo graphicInfo = model.getGraphicInfo(artifact.getId());
if (graphicInfo != null) {
fillGraphicInfo(elementNode, graphicInfo, true);
fillDiagramInfo(graphicInfo, diagramInfo);
}
String className = artifact.getClass().getSimpleName();
elementNode.put("type", className);
elementArray.add(elementNode);
}
}
}
private void fillDiagramInfo(GraphicInfo graphicInfo, GraphicInfo diagramInfo) {
double rightX = graphicInfo.getX() + graphicInfo.getWidth();
double bottomY = graphicInfo.getY() + graphicInfo.getHeight();
double middleX = graphicInfo.getX() + (graphicInfo.getWidth() / 2);
if (middleX < diagramInfo.getX()) {
diagramInfo.setX(middleX);
}
if (graphicInfo.getY() < diagramInfo.getY()) {
diagramInfo.setY(graphicInfo.getY());
}
if (rightX > diagramInfo.getWidth()) {
diagramInfo.setWidth(rightX);
}
if (bottomY > diagramInfo.getHeight()) {
diagramInfo.setHeight(bottomY);
}
}
private void fillWaypoints(String id, BpmnModel model, ObjectNode elementNode, GraphicInfo diagramInfo) {
List<GraphicInfo> flowInfo = model.getFlowLocationGraphicInfo(id);
ArrayNode waypointArray = objectMapper.createArrayNode();
for (GraphicInfo graphicInfo : flowInfo) {
ObjectNode pointNode = objectMapper.createObjectNode();
fillGraphicInfo(pointNode, graphicInfo, false);
waypointArray.add(pointNode);
fillDiagramInfo(graphicInfo, diagramInfo);
}
elementNode.set("waypoints", waypointArray);
}
private void fillEventTypes(String className, FlowElement element, ObjectNode elementNode) {
if (eventElementTypes.contains(className)) {
Event event = (Event) element;
if (!CollectionUtils.isEmpty(event.getEventDefinitions())) {
EventDefinition eventDef = event.getEventDefinitions().get(0);
ObjectNode eventNode = objectMapper.createObjectNode();
if (eventDef instanceof TimerEventDefinition) {
TimerEventDefinition timerDef = (TimerEventDefinition) eventDef;
eventNode.put("type", "timer");
if (org.apache.commons.lang3.StringUtils.isNotEmpty(timerDef.getTimeCycle())) {
eventNode.put("timeCycle", timerDef.getTimeCycle());
}
if (org.apache.commons.lang3.StringUtils.isNotEmpty(timerDef.getTimeDate())) {
eventNode.put("timeDate", timerDef.getTimeDate());
}
if (org.apache.commons.lang3.StringUtils.isNotEmpty(timerDef.getTimeDuration())) {
eventNode.put("timeDuration", timerDef.getTimeDuration());
}
} else if (eventDef instanceof ConditionalEventDefinition) {
ConditionalEventDefinition conditionalDef = (ConditionalEventDefinition) eventDef;
eventNode.put("type", "conditional");
if (org.apache.commons.lang3.StringUtils.isNotEmpty(conditionalDef.getConditionExpression())) {
eventNode.put("condition", conditionalDef.getConditionExpression());
}
} else if (eventDef instanceof ErrorEventDefinition) {
ErrorEventDefinition errorDef = (ErrorEventDefinition) eventDef;
eventNode.put("type", "error");
if (org.apache.commons.lang3.StringUtils.isNotEmpty(errorDef.getErrorCode())) {
eventNode.put("errorCode", errorDef.getErrorCode());
}
} else if (eventDef instanceof EscalationEventDefinition) {
EscalationEventDefinition escalationDef = (EscalationEventDefinition) eventDef;
eventNode.put("type", "escalation");
if (org.apache.commons.lang3.StringUtils.isNotEmpty(escalationDef.getEscalationCode())) {
eventNode.put("escalationCode", escalationDef.getEscalationCode());
}
} else if (eventDef instanceof SignalEventDefinition) {
SignalEventDefinition signalDef = (SignalEventDefinition) eventDef;
eventNode.put("type", "signal");
if (org.apache.commons.lang3.StringUtils.isNotEmpty(signalDef.getSignalRef())) {
eventNode.put("signalRef", signalDef.getSignalRef());
}
} else if (eventDef instanceof MessageEventDefinition) {
MessageEventDefinition messageDef = (MessageEventDefinition) eventDef;
eventNode.put("type", "message");
if (org.apache.commons.lang3.StringUtils.isNotEmpty(messageDef.getMessageRef())) {
eventNode.put("messageRef", messageDef.getMessageRef());
}
} else if (eventDef instanceof VariableListenerEventDefinition) {
VariableListenerEventDefinition variableDef = (VariableListenerEventDefinition) eventDef;
eventNode.put("type", "variable");
if (org.apache.commons.lang3.StringUtils.isNotEmpty(variableDef.getVariableName())) {
eventNode.put("variableName", variableDef.getVariableName());
}
if (org.apache.commons.lang3.StringUtils.isNotEmpty(variableDef.getVariableChangeType())) {
eventNode.put("variableChangeType", variableDef.getVariableChangeType());
}
}
elementNode.set("eventDefinition", eventNode);
}
}
}
}

View File

@ -0,0 +1,87 @@
package cn.axzo.workflow.core.service.support.forecast;
import cn.axzo.workflow.core.service.support.forecast.impl.SequenceFlowForecasting;
import cn.axzo.workflow.core.service.support.spring.FlowableConditionEvaluatorAware;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.engine.runtime.ProcessInstance;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import static cn.axzo.workflow.core.service.support.FlowNodeForecastService.FORECAST_MAP;
/**
* 抽象的流程预测
*
* @author wangli
* @since 2023/10/10 10:55
*/
public abstract class AbstractForecast<T extends FlowElement> implements Forecast<T>, FlowableConditionEvaluatorAware {
private FlowableConditionEvaluator conditionEvaluator;
@Override
public final List<FlowElement> nextFlowElement(FlowElement sourceFlowElement, ProcessInstance instance) {
return forecastNextNodes(getOutgoing((T) sourceFlowElement), instance);
}
protected abstract List<? extends FlowElement> getOutgoing(T flowElement);
/**
* 计算出口节点,此时的入参 sourceFlowElement 已经不等于泛型类
*
* @param sourceFlowElements 当前节点的出口节点集合
* @param instance 流程实例
* @return
*/
private List<FlowElement> forecastNextNodes(List<? extends FlowElement> sourceFlowElements,
ProcessInstance instance) {
if (CollectionUtils.isEmpty(sourceFlowElements)) {
return Collections.emptyList();
}
Map<Class<? extends FlowElement>, List<FlowElement>> elementGroup =
sourceFlowElements.stream().collect(Collectors.groupingBy(FlowElement::getClass, Collectors.toList()));
List<FlowElement> result = new ArrayList<>();
elementGroup.forEach((k, v) -> {
AbstractForecast forecast = FORECAST_MAP.get(k);
if (Objects.nonNull(forecast)) {
result.addAll(forecast.calcRealOutgoingNodes(v, instance));
}
});
return result;
}
/**
* 由于每个节点的下级节点是多个,且有可能是不同类型, 在预测下级节点时,需要将同类型的节点传递给对应类型的预测器来计算
*
* @param flowElements
* @param instance
* @return
*/
public List<? extends FlowElement> calcRealOutgoingNodes(List<T> flowElements, ProcessInstance instance) {
return Collections.emptyList();
}
@Override
public void setConditionEvaluator(FlowableConditionEvaluator conditionEvaluator) {
this.conditionEvaluator = conditionEvaluator;
}
/**
* 提供给所有实现类的通用调用引擎评估节点可能存在的条件表达式
*
* @param exp 表达式
* @param processInstanceId 实例 ID
* @return
* @see SequenceFlowForecasting#calcRealOutgoingNodes(List, ProcessInstance) 参考这里的用法
*/
protected final Boolean conditionOn(String exp, String processInstanceId) {
return conditionEvaluator.conditionOn(exp, processInstanceId);
}
}

View File

@ -0,0 +1,37 @@
package cn.axzo.workflow.core.service.support.forecast;
import cn.axzo.workflow.core.service.support.ExpressionConditionCmd;
import lombok.AllArgsConstructor;
import org.flowable.engine.ManagementService;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl;
import org.springframework.stereotype.Component;
/**
* 基于 Flowable 引擎能力的表达式评估器
*
* @author wangli
* @since 2023/10/10 10:41
*/
@Component
@AllArgsConstructor
public class FlowableConditionEvaluator {
private final ManagementService managementService;
private final RuntimeService runtimeService;
private final ProcessEngineConfigurationImpl processEngineConfiguration;
/**
* 计算表达式真假, 需结合引擎能力
*
* @param exp 条件表达式
* @param variables 流程变量
* @param processInstanceId 流程实例 ID (可与 variables 参入二选一, 如果都传,则直接用实例 ID 去引擎中查真实的变量表)
* @return true Or false
*/
public Boolean conditionOn(String exp, String processInstanceId) {
return managementService.executeCommand(new ExpressionConditionCmd(runtimeService,
processEngineConfiguration, processInstanceId, exp));
}
}

View File

@ -0,0 +1,35 @@
package cn.axzo.workflow.core.service.support.forecast;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.engine.runtime.ProcessInstance;
import java.util.List;
/**
* 流程节点顺序预测接口
*
* @author wangli
* @since 2023/10/10 10:29
*/
public interface Forecast<T extends FlowElement> {
/**
* 用于判断是否支持处理指定节点类型
*
* @param t Flowable 的子类
* @return
*/
Boolean support(FlowElement flowElement);
/**
* 通过给定的源 FlowElement 结合 Flowable 的条件评估器去计算符合条件的下一级节点
* <p>
* 由于标准的 BPMN 协议中很多节点都是允许多条路并行走的, 所以接口返回模型定义为 List 集合,
* 但实际上, 按照现在的业务定义, 只允许存在 0 1 个下一级节点.
*
* @param sourceFlowElement 当前节点, 实现类中该方法则是通过 getOutgoing 获取出口节点集合, 并调用对应类型中的 calcRealOutgoingNodes 查找真正的下级节点
* @param instance 流程运行实例
* @return
*/
List<FlowElement> nextFlowElement(FlowElement sourceFlowElement, ProcessInstance instance);
}

View File

@ -0,0 +1,38 @@
package cn.axzo.workflow.core.service.support.forecast.impl;
import cn.axzo.workflow.core.service.support.forecast.AbstractForecast;
import org.flowable.bpmn.model.EndEvent;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.engine.runtime.ProcessInstance;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.Collections;
import java.util.List;
/**
* 结束节点
*
* @author wangli
* @since 2023/10/10 18:58
*/
@Component
public class EndEventForecasting extends AbstractForecast<EndEvent> {
@Override
public Boolean support(FlowElement flowElement) {
return flowElement instanceof EndEvent;
}
@Override
protected List<? extends FlowElement> getOutgoing(EndEvent flowElement) {
return flowElement.getOutgoingFlows();
}
@Override
public List<? extends FlowElement> calcRealOutgoingNodes(List<EndEvent> flowElements, ProcessInstance instance) {
if (CollectionUtils.isEmpty(flowElements)) {
return Collections.emptyList();
}
return super.calcRealOutgoingNodes(flowElements, instance);
}
}

View File

@ -0,0 +1,42 @@
package cn.axzo.workflow.core.service.support.forecast.impl;
import cn.axzo.workflow.core.service.support.forecast.AbstractForecast;
import org.flowable.bpmn.model.ExclusiveGateway;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.engine.runtime.ProcessInstance;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.Collections;
import java.util.List;
/**
* 排他网关
*
* @author wangli
* @since 2023/10/11 09:57
*/
@Component
public class ExclusiveGatewayForecasting extends AbstractForecast<ExclusiveGateway> {
@Override
public Boolean support(FlowElement flowElement) {
return flowElement instanceof ExclusiveGateway;
}
@Override
protected List<? extends FlowElement> getOutgoing(ExclusiveGateway flowElement) {
return flowElement.getOutgoingFlows();
}
@Override
public List<? extends FlowElement> calcRealOutgoingNodes(List<ExclusiveGateway> flowElements,
ProcessInstance instance) {
if (CollectionUtils.isEmpty(flowElements)) {
return Collections.emptyList();
}
if (flowElements.size() == 1) {
return flowElements;
}
return super.calcRealOutgoingNodes(flowElements, instance);
}
}

View File

@ -0,0 +1,58 @@
package cn.axzo.workflow.core.service.support.forecast.impl;
import cn.axzo.workflow.core.service.support.forecast.AbstractForecast;
import com.google.common.collect.Lists;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.bpmn.model.SequenceFlow;
import org.flowable.engine.runtime.ProcessInstance;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
/**
* 顺序流节点预测唯一一条通路
*
* @author wangli
* @since 2023/10/10 10:28
*/
@Component
public class SequenceFlowForecasting extends AbstractForecast<SequenceFlow> {
@Override
public Boolean support(FlowElement flowElement) {
return flowElement instanceof SequenceFlow;
}
@Override
protected List<? extends FlowElement> getOutgoing(SequenceFlow flowElement) {
return Lists.newArrayList(flowElement.getTargetFlowElement());
}
@Override
public List<? extends FlowElement> calcRealOutgoingNodes(List<SequenceFlow> flowElements,
ProcessInstance instance) {
if (CollectionUtils.isEmpty(flowElements)) {
return Collections.emptyList();
}
// 评估顺序流集合中条件为 true 的顺序流
List<SequenceFlow> executableFlows =
flowElements.stream().filter(i -> StringUtils.hasLength(i.getConditionExpression()))
.filter(i -> conditionOn(i.getConditionExpression(), instance.getId())).collect(Collectors.toList());
if (!CollectionUtils.isEmpty(executableFlows) && executableFlows.size() == 1) {
return Lists.newArrayList(executableFlows.get(0));
}
// 如果 conditionExpression 为空, 则认为是默认流
List<SequenceFlow> defaultFlows =
flowElements.stream().filter(i -> !StringUtils.hasLength(i.getConditionExpression())).collect(Collectors.toList());
if (!CollectionUtils.isEmpty(defaultFlows) && defaultFlows.size() == 1) {
return Lists.newArrayList(defaultFlows.get(0));
}
return super.calcRealOutgoingNodes(flowElements, instance);
}
}

View File

@ -0,0 +1,29 @@
package cn.axzo.workflow.core.service.support.forecast.impl;
import cn.axzo.workflow.core.service.support.forecast.AbstractForecast;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.bpmn.model.StartEvent;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 开始节点
*
* @author wangli
* @since 2023/10/10 13:55
*/
@Component
public class StartEventForecasting extends AbstractForecast<StartEvent> {
@Override
public Boolean support(FlowElement flowElement) {
return flowElement instanceof StartEvent;
}
@Override
protected List<? extends FlowElement> getOutgoing(StartEvent flowElement) {
return flowElement.getOutgoingFlows();
}
}

View File

@ -0,0 +1,41 @@
package cn.axzo.workflow.core.service.support.forecast.impl;
import cn.axzo.workflow.core.service.support.forecast.AbstractForecast;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.bpmn.model.UserTask;
import org.flowable.engine.runtime.ProcessInstance;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.Collections;
import java.util.List;
/**
* 用户任务节点预测
*
* @author wangli
* @since 2023/10/10 11:29
*/
@Component
public class UserTaskForecasting extends AbstractForecast<UserTask> {
@Override
public Boolean support(FlowElement flowElement) {
return flowElement instanceof UserTask;
}
@Override
protected List<? extends FlowElement> getOutgoing(UserTask flowElement) {
return flowElement.getOutgoingFlows();
}
@Override
public List<? extends FlowElement> calcRealOutgoingNodes(List<UserTask> flowElements, ProcessInstance instance) {
if (CollectionUtils.isEmpty(flowElements)) {
return Collections.emptyList();
}
if (flowElements.size() == 1) {
return flowElements;
}
return super.calcRealOutgoingNodes(flowElements, instance);
}
}

View File

@ -0,0 +1,19 @@
package cn.axzo.workflow.core.service.support.spring;
import cn.axzo.workflow.core.service.support.forecast.FlowableConditionEvaluator;
/**
* 按照 Spring 规范提供一个 Flowable Condition Evaluator Aware
*
* @author wangli
* @since 2023/10/10 10:44
*/
public interface FlowableConditionEvaluatorAware {
/**
* 注入条件评估器
*
* @param conditionEvaluator {@link FlowableConditionEvaluator}
*/
void setConditionEvaluator(FlowableConditionEvaluator conditionEvaluator);
}

View File

@ -0,0 +1,28 @@
package cn.axzo.workflow.core.service.support.spring;
import cn.axzo.workflow.core.service.support.forecast.FlowableConditionEvaluator;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
/**
* 自定义的 Aware 模式的接口处理类
*
* @author wangli
* @since 2023/10/11 10:23
*/
@Component
public class FlowableConditionEvaluatorAwareProcessor implements BeanPostProcessor {
private final FlowableConditionEvaluator conditionEvaluator;
public FlowableConditionEvaluatorAwareProcessor(FlowableConditionEvaluator conditionEvaluator) {this.conditionEvaluator = conditionEvaluator;}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof FlowableConditionEvaluatorAware) {
((FlowableConditionEvaluatorAware) bean).setConditionEvaluator(conditionEvaluator);
}
return bean;
}
}

View File

@ -0,0 +1,45 @@
package cn.axzo.workflow.server.controller.web;
import cn.axzo.workflow.common.model.response.bpmn.process.ProcessNodeDetailVO;
import cn.axzo.workflow.core.service.BpmnProcessInstanceService;
import cn.axzo.workflow.core.service.support.FlowNodeForecastService;
import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.model.FlowElement;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* TODO
*
* @author wangli
* @since 2023/10/10 13:59
*/
@Slf4j
@RequestMapping("/web/v1/api")
@RestController
@Validated
public class TestController {
@Autowired
private FlowNodeForecastService forecastService;
@Autowired
private BpmnProcessInstanceService instanceService;
@GetMapping("/test")
public void test(@RequestParam String processInstanceId) {
List<FlowElement> flowElements = forecastService.performProcessForecasting(processInstanceId, null);
System.out.println("flowElements = " + flowElements);
}
@GetMapping("/test2")
public void test2(@RequestParam String processInstanceId) {
List<ProcessNodeDetailVO> detailVOS = instanceService.getProcessInstanceNodeForecast(processInstanceId, "296");
System.out.println("detailVOS = " + detailVOS);
}
}

View File

@ -48,7 +48,6 @@ public class BpmnProcessInstanceController implements ProcessInstanceApi {
@Resource
private BpmnProcessInstanceService bpmnProcessInstanceService;
/**
* 我发起的审批列表
*/
@ -141,9 +140,10 @@ public class BpmnProcessInstanceController implements ProcessInstanceApi {
*/
@GetMapping("/node/calc")
@Override
public CommonResponse<List<ProcessNodeDetailVO>> processInstanceNodeCalc(@NotBlank(message = "流程实例 ID 不能为空") @RequestParam String processInstanceId,
@Nullable String tenantId) {
return success(bpmnProcessInstanceService.getProcessNodes(processInstanceId, tenantId));
public CommonResponse<List<ProcessNodeDetailVO>> processInstanceNodeForecast(@NotBlank(message = "流程实例 ID 不能为空") @RequestParam String processInstanceId,
@Nullable String tenantId) {
// return success(bpmnProcessInstanceService.getProcessNodes(processInstanceId, tenantId));
return success(bpmnProcessInstanceService.getProcessInstanceNodeForecast(processInstanceId, tenantId));
}
/**