REQ-3626 增加回退至指定节点通用能力

This commit is contained in:
yangqicheng 2025-02-23 23:02:28 +08:00
parent a5000681b2
commit 0c77f34dfb
7 changed files with 248 additions and 72 deletions

View File

@ -89,6 +89,15 @@ public interface ProcessTaskApi {
@PostMapping("/api/process/task/back")
CommonResponse<Boolean> backTask(@Validated @RequestBody BpmnTaskBackAuditDTO dto);
/**
* 用于系统内部操作跳转到指定节点
* @param dto 请求参数
* @return 是否成功
*/
@Operation(summary = "系统操作回退任务到指定节点")
@PostMapping("/system/back")
CommonResponse<Boolean> systemBackTask(@Validated @RequestBody BpmnNodeBackSystemOperateDTO dto);
/**
* 驳回
*

View File

@ -0,0 +1,61 @@
package cn.axzo.workflow.common.model.request.bpmn.task;
import cn.axzo.workflow.common.constraint.AttachmentTypeValidator;
import cn.axzo.workflow.common.constraint.AttachmentValidator;
import cn.axzo.workflow.common.enums.AttachmentTypeEnum;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import java.io.Serializable;
import java.util.List;
/**
* 审批节点退回模型
*
*/
@ApiModel("审批节点退回模型")
@Data
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
public class BpmnNodeBackSystemOperateDTO implements Serializable {
private static final long serialVersionUID = -4160538355403179298L;
@ApiModelProperty(value = "流程实例id", required = true)
@NotBlank(message = "流程实例id不能为空")
private String processInstanceId;
@ApiModelProperty(value = "当前流程节点id", required = true)
@NotBlank(message = "当前节点id不能为空")
private String currentActivityId;
@ApiModelProperty(value = "目标流程节点id", required = true)
@NotBlank(message = "目标流程节点id不能为空")
private String toActivityId;
@ApiModelProperty(value = "意见")
private String advice;
@ApiModelProperty(value = "操作描述")
private String operationDesc;
/**
* 附件列表
*/
@ApiModelProperty(value = "附件列表")
@Size(max = 11, message = "附件数量超过限制")
@AttachmentValidator(types = {
@AttachmentTypeValidator(type = AttachmentTypeEnum.image, max = 5),
@AttachmentTypeValidator(type = AttachmentTypeEnum.file, max = 5),
@AttachmentTypeValidator(type = AttachmentTypeEnum.signature, max = 1)}
)
private List<AttachmentDTO> attachmentList;
}

View File

@ -1,7 +1,8 @@
package cn.axzo.workflow.core.engine.cmd;
import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskBackAuditDTO;
import cn.axzo.workflow.common.exception.WorkflowEngineException;
import cn.axzo.workflow.common.model.request.bpmn.task.AttachmentDTO;
import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskDelegateAssigner;
import cn.axzo.workflow.core.common.utils.BpmnModelUtils;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
@ -15,26 +16,18 @@ import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl;
import org.flowable.engine.impl.util.CommandContextUtil;
import org.flowable.engine.impl.util.ProcessDefinitionUtil;
import org.flowable.task.api.history.HistoricTaskInstance;
import org.flowable.task.api.history.HistoricTaskInstanceQuery;
import org.flowable.task.service.TaskService;
import org.flowable.task.service.impl.persistence.entity.TaskEntity;
import org.springframework.util.StringUtils;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.*;
import static cn.axzo.workflow.common.constant.BpmnConstants.COMMENT_TYPE_ADVICE;
import static cn.axzo.workflow.common.constant.BpmnConstants.COMMENT_TYPE_OPERATION_DESC;
import static cn.axzo.workflow.common.constant.BpmnConstants.INTERNAL_TASK_RELATION_ASSIGNEE_LIST_INFO;
import static cn.axzo.workflow.common.constant.BpmnConstants.TASK_COMPLETE_OPERATION_TYPE;
import static cn.axzo.workflow.common.enums.BpmnProcessInstanceResultEnum.BACKED;
import static cn.axzo.workflow.common.code.BpmnTaskRespCode.BACK_NODE_CANNOT_REACHABLE;
import static cn.axzo.workflow.common.code.BpmnTaskRespCode.BACK_TARGET_ACTIVITY_NOT_EXISTS;
import static cn.axzo.workflow.core.engine.cmd.helper.CustomTaskHelper.addComment;
import static cn.axzo.workflow.core.engine.cmd.helper.CustomTaskHelper.batchAddAttachment;
import static cn.axzo.workflow.core.engine.cmd.helper.CustomTaskHelper.validTask;
import static cn.axzo.workflow.common.constant.BpmnConstants.*;
import static cn.axzo.workflow.common.enums.BpmnProcessInstanceResultEnum.BACKED;
import static cn.axzo.workflow.core.engine.cmd.helper.CustomTaskHelper.*;
/**
* 回退命令
@ -43,65 +36,94 @@ import static cn.axzo.workflow.core.engine.cmd.helper.CustomTaskHelper.validTask
public class CustomBackTaskCmd extends AbstractCommand<Void> implements Serializable {
private static final long serialVersionUID = -1241290344311892346L;
private final BpmnTaskBackAuditDTO dto;
private final List<String> targetTaskIds;
private final String advice;
private final String operationDesc;
private final List<AttachmentDTO> attachmentList;
private final BpmnTaskDelegateAssigner approver;
private final String processInstanceId;
private final String currentActivityId;
private final String toActivityId;
private final boolean validateApprover;
private static final String OPERATION_DESC = "回退至";
public CustomBackTaskCmd(List<String> targetTaskIds,
String advice,
String operationDesc,
List<AttachmentDTO> attachmentList,
BpmnTaskDelegateAssigner approver,
String processInstanceId,
String currentActivityId,
String toActivityId,
boolean validateApprover) {
this.targetTaskIds = targetTaskIds;
this.advice = advice;
this.operationDesc = operationDesc;
this.attachmentList = attachmentList;
this.approver = approver;
this.processInstanceId = processInstanceId;
this.currentActivityId = currentActivityId;
this.toActivityId = toActivityId;
this.validateApprover = validateApprover;
}
@Override
public String paramToJsonString() {
Map<String, Object> params = new HashMap<>();
params.put("taskId", dto.getTaskId());
params.put("advice", dto.getAdvice());
params.put("targetTaskIds", JSON.toJSONString(this.targetTaskIds));
params.put("advice", advice);
params.put("operationDesc", OPERATION_DESC);
params.put("attachmentList", JSON.toJSONString(dto.getAttachmentList()));
params.put("approver", JSON.toJSONString(dto.getApprover()));
params.put("nodeTypes", JSON.toJSONString(dto.getNodeTypes()));
params.put("attachmentList", JSON.toJSONString(attachmentList));
params.put("approver", JSON.toJSONString(approver));
params.put("toActivityId", JSON.toJSONString(this.toActivityId));
params.put("processInstanceId", JSON.toJSONString(this.processInstanceId));
params.put("validateApprover", validateApprover);
return JSON.toJSONString(params);
}
public CustomBackTaskCmd(BpmnTaskBackAuditDTO dto) {
this.dto = dto;
}
@Override
public Void execute(CommandContext commandContext) {
ProcessEngineConfigurationImpl processEngineConfiguration =
CommandContextUtil.getProcessEngineConfiguration(commandContext);
HistoricTaskInstanceQuery taskQuery =
processEngineConfiguration.getHistoryService().createHistoricTaskInstanceQuery();
CommandContextUtil.getProcessEngineConfiguration(commandContext);
List<HistoricTaskInstance> taskList = processEngineConfiguration.getHistoryService()
.createHistoricTaskInstanceQuery()
.taskDefinitionId(currentActivityId)
.processInstanceId(processInstanceId)
.list();
TaskService taskService = processEngineConfiguration.getTaskServiceConfiguration().getTaskService();
HistoricTaskInstance historicTaskInstance = taskQuery.taskId(dto.getTaskId()).singleResult();
TaskEntity task = taskService.getTask(dto.getTaskId());
validTask(historicTaskInstance, task, dto.getApprover(), dto.getNodeTypes());
Process process = ProcessDefinitionUtil.getProcess(task.getProcessDefinitionId());
FlowElement targetFlowElement = process.getFlowElement(dto.getToActivityId(), true);
if (Objects.isNull(targetFlowElement)) {
throw new WorkflowEngineException(BACK_TARGET_ACTIVITY_NOT_EXISTS, dto.getToActivityId());
if (validateApprover) {
taskList.forEach(hi -> validTask(hi, taskService.getTask(hi.getId()), approver, null));
}
FlowElement sourceFlowElement = process.getFlowElement(task.getTaskDefinitionKey(), true);
String processDefinitionId = taskList.get(0).getProcessDefinitionId();
Process process = ProcessDefinitionUtil.getProcess(processDefinitionId);
FlowElement targetFlowElement = process.getFlowElement(toActivityId, true);
if (Objects.isNull(targetFlowElement)) {
throw new WorkflowEngineException(BACK_TARGET_ACTIVITY_NOT_EXISTS, toActivityId);
}
FlowElement sourceFlowElement = process.getFlowElement(currentActivityId, true);
// 退回节点到当前节点不可达到不允许退回
if (!BpmnModelUtils.isReachable(process, (FlowNode) targetFlowElement, (FlowNode) sourceFlowElement)) {
throw new WorkflowEngineException(BACK_NODE_CANNOT_REACHABLE, dto.getToActivityId());
throw new WorkflowEngineException(BACK_NODE_CANNOT_REACHABLE, toActivityId);
}
batchAddAttachment(commandContext, task.getProcessInstanceId(), task, dto.getAttachmentList(), dto.getApprover());
Authentication.setAuthenticatedUserId(dto.getApprover().buildAssigneeId());
addComment(commandContext, task, COMMENT_TYPE_ADVICE, dto.getAdvice());
addComment(commandContext, task, COMMENT_TYPE_OPERATION_DESC, OPERATION_DESC + targetFlowElement.getName());
Authentication.setAuthenticatedUserId(null);
RuntimeService runtimeService = processEngineConfiguration.getRuntimeService();
task.setTransientVariable(TASK_COMPLETE_OPERATION_TYPE + dto.getTaskId(), BACKED.getStatus());
taskList.forEach(th -> {
TaskEntity task = taskService.getTask(th.getId());
batchAddAttachment(commandContext, processInstanceId, task, attachmentList, approver);
Authentication.setAuthenticatedUserId(approver.buildAssigneeId());
addComment(commandContext, task, COMMENT_TYPE_ADVICE, advice);
addComment(commandContext, task, COMMENT_TYPE_OPERATION_DESC, StringUtils.hasText(operationDesc) ? operationDesc : OPERATION_DESC + targetFlowElement.getName());
Authentication.setAuthenticatedUserId(null);
task.setTransientVariable(TASK_COMPLETE_OPERATION_TYPE + task.getId(), BACKED.getStatus());
});
// 移除回退到的指定节点的变量 EngineExecutionStartListener 重新计算该节点的人
runtimeService.removeVariable(task.getProcessInstanceId(), INTERNAL_TASK_RELATION_ASSIGNEE_LIST_INFO + dto.getToActivityId());
runtimeService.removeVariable(processInstanceId, INTERNAL_TASK_RELATION_ASSIGNEE_LIST_INFO + toActivityId);
runtimeService.createChangeActivityStateBuilder()
.processInstanceId(task.getProcessInstanceId())
.moveActivityIdsToSingleActivityId(Collections.singletonList(task.getTaskDefinitionKey()), dto.getToActivityId())
.changeState();
.processInstanceId(processInstanceId)
.moveActivityIdsToSingleActivityId(Collections.singletonList(currentActivityId), toActivityId)
.changeState();
return null;
}

View File

@ -12,6 +12,7 @@ import org.flowable.job.service.impl.persistence.entity.JobEntity;
import org.flowable.task.api.Task;
import org.flowable.variable.api.delegate.VariableScope;
import java.util.Collections;
import java.util.Objects;
@Slf4j
@ -35,7 +36,16 @@ public class AsyncBackTaskJobHandler extends AbstractExecuteWithLockJobHandler i
if (Objects.isNull(task)) {
return;
}
processEngineConfiguration.getCommandExecutor().execute(new CustomBackTaskCmd(dto));
processEngineConfiguration.getCommandExecutor().execute(new CustomBackTaskCmd(
Collections.singletonList(dto.getTaskId()),
dto.getAdvice(),
dto.getOperationDesc(),
dto.getAttachmentList(),
dto.getApprover(),
task.getProcessInstanceId(),
task.getTaskDefinitionKey(),
dto.getToActivityId(),
true));
}
}

View File

@ -29,11 +29,18 @@ public interface BpmnProcessTaskService {
void approveTask(BpmnTaskAuditDTO taskAuditDTO);
void approveTaskWithForm(BpmnTaskAuditWithFormDTO taskAuditDto);
/**
* 回退
*/
void backTask(BpmnTaskBackAuditDTO taskAuditDTO);
/**
* 用于系统内部回退操作
* @param taskAuditDTO 请求参数
*/
void systemBackTask(BpmnNodeBackSystemOperateDTO taskAuditDTO);
/**
* 批量同意
*
@ -43,9 +50,10 @@ public interface BpmnProcessTaskService {
/**
* 回退到指定节点可以回退节点选项
* @param taskId 任务id
* @param processInstanceId 流程实例id
* @param currentActivityId 当前节点定义id
*/
List<BpmnOptionalNodeDTO> getBackOptionalNodes(String taskId);
List<BpmnOptionalNodeDTO> getBackOptionalNodes(String processInstanceId, String currentActivityId);
/**
* 驳回

View File

@ -5,14 +5,11 @@ import cn.axzo.workflow.common.enums.BpmnCountersignTypeEnum;
import cn.axzo.workflow.common.enums.BpmnFlowNodeType;
import cn.axzo.workflow.common.enums.BpmnProcessInstanceResultEnum;
import cn.axzo.workflow.common.exception.WorkflowEngineException;
import cn.axzo.workflow.common.model.request.BpmnApproveConf;
import cn.axzo.workflow.common.model.request.bpmn.BpmnButtonConf;
import cn.axzo.workflow.common.model.request.bpmn.BpmnNoticeConf;
import cn.axzo.workflow.common.model.request.bpmn.task.*;
import cn.axzo.workflow.common.model.response.BpmPageResult;
import cn.axzo.workflow.common.model.response.bpmn.BatchOperationItemResultVO;
import cn.axzo.workflow.common.model.response.bpmn.BatchOperationResultVO;
import cn.axzo.workflow.common.model.response.bpmn.process.BpmnProcessInstanceLogVO;
import cn.axzo.workflow.common.model.response.bpmn.task.*;
import cn.axzo.workflow.core.common.utils.BpmnMetaParserHelper;
import cn.axzo.workflow.core.engine.cmd.*;
@ -20,14 +17,12 @@ import cn.axzo.workflow.core.engine.event.MessagePushEventBuilder;
import cn.axzo.workflow.core.engine.event.MessagePushEventImpl;
import cn.axzo.workflow.core.engine.event.MessagePushEventType;
import cn.axzo.workflow.core.repository.entity.ExtAxHiTaskInst;
import cn.axzo.workflow.core.repository.entity.ExtAxProcessLog;
import cn.axzo.workflow.core.service.BpmnProcessDefinitionService;
import cn.axzo.workflow.core.service.BpmnProcessTaskService;
import cn.axzo.workflow.core.service.ExtAxHiTaskInstService;
import cn.axzo.workflow.core.service.ExtAxProcessLogService;
import cn.axzo.workflow.core.service.converter.*;
import cn.hutool.core.collection.CollUtil;
import com.alibaba.fastjson.JSON;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
@ -79,8 +74,6 @@ import static cn.axzo.workflow.common.enums.BpmnProcessInstanceResultEnum.*;
import static cn.axzo.workflow.common.util.BpmnNativeQueryUtil.countSql;
import static cn.axzo.workflow.common.util.BpmnNativeQueryUtil.sqlConnectors;
import static cn.axzo.workflow.core.common.utils.BpmnCollectionUtils.convertSet;
import static cn.axzo.workflow.core.common.utils.BpmnMetaParserHelper.getButtonConfig;
import static cn.axzo.workflow.core.common.utils.BpmnMetaParserHelper.getProcessApproveConf;
@Service
@Slf4j
@ -295,7 +288,11 @@ public class BpmnProcessTaskServiceImpl implements BpmnProcessTaskService {
@Override
@Transactional(rollbackFor = Exception.class)
public void backTask(BpmnTaskBackAuditDTO dto) {
List<BpmnOptionalNodeDTO> backOptionalNodes = getBackOptionalNodes(dto.getTaskId());
Task task = processEngineConfiguration.getTaskService().createTaskQuery().taskId(dto.getTaskId()).singleResult();
if (task == null) {
throw new WorkflowEngineException(PROCESS_TASK_NOT_EXISTS);
}
List<BpmnOptionalNodeDTO> backOptionalNodes = getBackOptionalNodes(task.getProcessDefinitionId(), task.getTaskDefinitionKey());
if (CollectionUtils.isEmpty(backOptionalNodes)) {
throw new WorkflowEngineException(BACK_NODE_CANNOT_REACHABLE, dto.getToActivityId());
}
@ -324,17 +321,73 @@ public class BpmnProcessTaskServiceImpl implements BpmnProcessTaskService {
if (Boolean.TRUE.equals(dto.getAsync())) {
commandExecutor.execute(new CustomBackTaskAsyncCmd(dto));
} else {
commandExecutor.execute(new CustomBackTaskCmd(dto));
commandExecutor.execute(new CustomBackTaskCmd(Collections.singletonList(dto.getTaskId()),
dto.getAdvice(),
dto.getOperationDesc(),
dto.getAttachmentList(),
dto.getApprover(),
task.getProcessInstanceId(),
task.getTaskDefinitionKey(),
dto.getToActivityId(),
true));
}
}
@Override
public List<BpmnOptionalNodeDTO> getBackOptionalNodes(String taskId) {
Task task = processEngineConfiguration.getTaskService().createTaskQuery().taskId(taskId).singleResult();
if (task == null) {
throw new WorkflowEngineException(PROCESS_TASK_NOT_EXISTS);
public void systemBackTask(BpmnNodeBackSystemOperateDTO dto) {
//需要查询当前流程是否停留在当前节点
//需要查询退回的节点是否可达
//然后进行退回操作
ProcessEngineConfigurationImpl pcConfiguration = CommandContextUtil.getProcessEngineConfiguration();
HistoricTaskInstanceQuery taskQuery = pcConfiguration.getHistoryService().createHistoricTaskInstanceQuery();
List<HistoricTaskInstance> taskList = taskQuery.processInstanceId(dto.getProcessInstanceId())
.taskDefinitionId(dto.getCurrentActivityId())
.list();
List<HistoricTaskInstance> valuableTasks;
if (CollectionUtils.isEmpty(taskList) || CollectionUtils.isEmpty((valuableTasks = taskList.stream()
.filter(t -> Objects.nonNull(t.getEndTime()))
.collect(Collectors.toList())))) {
throw new WorkflowEngineException(TASK_HAS_BEEN_COMPLETE);
}
String processInstanceId = task.getProcessInstanceId();
List<BpmnOptionalNodeDTO> backOptionalNodes = getBackOptionalNodes(valuableTasks.get(0).getProcessInstanceId(), valuableTasks.get(0).getTaskDefinitionKey());
if (CollectionUtils.isEmpty(backOptionalNodes)) {
throw new WorkflowEngineException(BACK_NODE_CANNOT_REACHABLE, dto.getToActivityId());
}
List<String> activityList = backOptionalNodes.stream().map(BpmnOptionalNodeDTO::getProcessActivityId).collect(Collectors.toList());
if (!activityList.contains(dto.getToActivityId())) {
throw new WorkflowEngineException(BACK_NODE_CANNOT_REACHABLE, dto.getToActivityId());
}
Optional<String> instOpt = backOptionalNodes.stream()
.map(BpmnOptionalNodeDTO::getProcessInstanceId)
.filter(StringUtils::hasText)
.findAny();
if (instOpt.isPresent()) {
ExtHiTaskSearchDTO searchDTO = new ExtHiTaskSearchDTO();
searchDTO.setProcessInstanceId(instOpt.get());
List<ExtAxHiTaskInst> extAxHiTaskInsts = extAxHiTaskInstService.queryList(searchDTO);
if (CollectionUtils.isNotEmpty(extAxHiTaskInsts)) {
long backOpeCount = extAxHiTaskInsts.stream()
.filter(ext -> BACKED.getStatus().equals(ext.getStatus()))
.count();
if (backOpeCount > MAX_BACKED_OPERATE_COUNT) {
throw new WorkflowEngineException(REACHED_BACKED_MAXIMUM_NUM, String.valueOf(MAX_BACKED_OPERATE_COUNT));
}
}
}
CommandExecutor commandExecutor = processEngineConfiguration.getCommandExecutor();
commandExecutor.execute(new CustomBackTaskCmd(valuableTasks.stream().map(TaskInfo::getId).collect(Collectors.toList()),
dto.getAdvice(),
dto.getOperationDesc(),
dto.getAttachmentList(),
null,
valuableTasks.get(0).getProcessInstanceId(),
dto.getToActivityId(),
dto.getCurrentActivityId(),
false));
}
@Override
public List<BpmnOptionalNodeDTO> getBackOptionalNodes(String processInstanceId, String currentActivityId) {
//1.获取当前的流程实例
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();
if (processInstance == null) {
@ -393,7 +446,7 @@ public class BpmnProcessTaskServiceImpl implements BpmnProcessTaskService {
AtomicInteger index = new AtomicInteger(0);
List<BpmnOptionalNodeDTO> resultList = valuableList
.stream()
.filter(pair -> !task.getTaskDefinitionKey().equals(pair.getLeft())) //排除当前节点
.filter(pair -> !currentActivityId.equals(pair.getLeft())) //排除当前节点
.map(pair -> flowElementMap.get(pair.getLeft()))
.filter(flowElement -> {
BpmnFlowNodeType currentNodeType = BpmnMetaParserHelper.getNodeType(flowElement).orElse(NODE_EMPTY);
@ -402,8 +455,8 @@ public class BpmnProcessTaskServiceImpl implements BpmnProcessTaskService {
.filter(flowElement -> !NODE_STARTER.getType().equals(flowElement.getId()))
.map(flowElement -> BpmnOptionalNodeDTO
.builder()
.processInstanceId(task.getProcessInstanceId())
.processDefinitionId(task.getProcessDefinitionId())
.processInstanceId(processInstanceId)
.processDefinitionId(processInstance.getProcessDefinitionId())
.processActivityId(flowElement.getId())
.processActivityName(flowElement.getName())
.processNodeDesc(flowElement.getName())

View File

@ -152,6 +152,19 @@ public class BpmnProcessTaskController extends BasicPopulateAvatarController imp
return success(true);
}
/**
* 回退到指定节点
*/
@Operation(summary = "系统操作回退任务到指定节点")
@PostMapping("/system/back")
@RepeatSubmit
@Override
public CommonResponse<Boolean> systemBackTask(@Validated @RequestBody BpmnNodeBackSystemOperateDTO dto) {
log.info("系统回退 systemBackTask===>>>参数:{}", JSON.toJSONString(dto));
bpmnProcessTaskService.systemBackTask(dto);
return success(true);
}
/**
* 驳回
*/