feat(pigx-flow-biz): 新增流程撤回功能

- 在 ITaskService 接口中添加 withdrawProcessInstance 方法用于撤回流程实例
- 实现 TaskServiceImpl 中的撤回逻辑,包括权限校验和流程状态更新
- 新增 terminateProcessInstance 私有方法统一处理流程终止和撤回操作
- 在 ProcessInstanceRecordVo 中添加撤回相关字段 canWithdraw 和 withdrawDisabledReason
- 优化 ProcessInstanceServiceImpl 查询逻辑,批量处理驳回状态和撤回状态判断
- 在 ProcessTaskController 中添加撤回流程实例的接口端点
- 定义流程撤回相关的常量,包括结束原因和执行人状态
This commit is contained in:
PIGCLOUD committed 2026-03-19 08:41:55 +08:00
1 parent b553787cfd
commit f759865202
6 files changed
+354 -62

No files matched your search

@@ -60,6 +60,36 @@ public class ProcessInstanceConstant {
*/
public static final String REJECT_TO_STARTER_VAR = "rejectToStarter";
/**
* 流程结束原因:拒绝
*/
public static final String FINISH_REASON_REFUSE = "0";
/**
* 流程结束原因:通过
*/
public static final String FINISH_REASON_PASS = "1";
/**
* 流程结束原因:撤回
*/
public static final String FINISH_REASON_WITHDRAW = "3";
/**
* 流程结束原因:终止
*/
public static final String FINISH_REASON_TERMINATE = "9";
/**
* 流程节点执行人状态:撤回
*/
public static final int ASSIGN_USER_STATUS_WITHDRAW = 3;
/**
* 流程节点执行人状态:终止
*/
public static final int ASSIGN_USER_STATUS_TERMINATE = 5;
/**
* 多人审批模式:会签 所有审批人都必须同意才能通过,任何一人拒绝则不通过
*/
@@ -44,4 +44,14 @@ public class ProcessInstanceRecordVo extends ProcessInstanceRecord {
*/
private Boolean hasPrintTemplate;
/**
* 是否允许撤回
*/
private Boolean canWithdraw;
/**
* 不可撤回原因
*/
private String withdrawDisabledReason;
}
@@ -144,6 +144,17 @@ public class ProcessTaskController {
}
/**
* 撤回流程实例
*
* @param taskParamDto 撤回参数,包含流程实例ID
* @return R 统一响应对象,操作成功返回撤回信息
*/
@PostMapping("withdrawProcessInstance")
public R withdrawProcessInstance(@RequestBody TaskParamDto taskParamDto) {
return taskService.withdrawProcessInstance(taskParamDto);
}
/**
* 退回任务
* <p>
@@ -87,6 +87,14 @@ public interface ITaskService {
*/
R stopProcessInstance(TaskParamDto taskParamDto);
/**
* 撤回流程实例
*
* @param taskParamDto 任务参数,包含流程实例ID
* @return 操作结果
*/
R withdrawProcessInstance(TaskParamDto taskParamDto);
/**
* 退回任务
* <p>
@@ -18,11 +18,10 @@ import com.pig4cloud.pigx.common.security.service.PigxUser;
import com.pig4cloud.pigx.common.security.util.SecurityUtils;
import com.pig4cloud.pigx.flow.constant.NodeStatusEnum;
import com.pig4cloud.pigx.flow.constant.NodeUserTypeEnum;
import com.pig4cloud.pigx.flow.constant.ProcessInstanceConstant;
import com.pig4cloud.pigx.flow.dto.*;
import com.pig4cloud.pigx.flow.entity.*;
import com.pig4cloud.pigx.flow.entity.Process;
import com.pig4cloud.pigx.flow.entity.ProcessCopy;
import com.pig4cloud.pigx.flow.entity.ProcessInstanceRecord;
import com.pig4cloud.pigx.flow.entity.ProcessNodeRecord;
import com.pig4cloud.pigx.flow.service.*;
import com.pig4cloud.pigx.flow.support.utils.NodeFormatUtil;
import com.pig4cloud.pigx.flow.support.utils.NodeUtil;
@@ -42,6 +41,7 @@ import org.flowable.engine.history.HistoricActivityInstanceQuery;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.Task;
import org.flowable.task.api.TaskQuery;
import org.flowable.variable.api.history.HistoricVariableInstance;
import org.springframework.stereotype.Service;
import java.time.ZoneId;
@@ -82,6 +82,8 @@ public class ProcessInstanceServiceImpl implements IProcessInstanceService {
private final IProcessNodeRecordService processNodeRecordService;
private final IProcessNodeRecordAssignUserService processNodeRecordAssignUserService;
private final ObjectMapper objectMapper;
/**
@@ -374,36 +376,59 @@ public class ProcessInstanceServiceImpl implements IProcessInstanceService {
.collect(Collectors.toSet());
List<Process> processList = CollUtil.isEmpty(flowIds) ? Collections.emptyList()
: processService.lambdaQuery().in(Process::getFlowId, flowIds).list();
Set<String> processInstanceIds = voList.stream()
.map(ProcessInstanceRecordVo::getProcessInstanceId)
.filter(StrUtil::isNotBlank)
.collect(Collectors.toSet());
Map<String, List<ProcessNodeRecordAssignUser>> assignUserMap = queryAssignUserMap(processInstanceIds);
// 批量查询进行中流程的 rejectToStarter 变量,避免 N+1
Set<String> runningInstanceIds = voList.stream()
.filter(vo -> Objects.equals(vo.getStatus(), NodeStatusEnum.JXZ.getCode()))
.map(ProcessInstanceRecordVo::getProcessInstanceId)
.filter(StrUtil::isNotBlank)
.collect(Collectors.toSet());
Set<String> rejectToStarterIds = batchQueryRejectToStarter(runningInstanceIds);
// 批量查询驳回待提交的 root 任务,避免 N+1
Map<String, String> resubmitTaskMap = Collections.emptyMap();
if (!rejectToStarterIds.isEmpty()) {
resubmitTaskMap = taskService.createTaskQuery()
.processInstanceIdIn(rejectToStarterIds)
.taskDefinitionKey("root")
.list()
.stream()
.collect(Collectors.toMap(Task::getProcessInstanceId, Task::getId, (a, b) -> a));
}
// 构建打印模板 Map,避免循环内 stream 遍历
Map<String, Process> processMap = processList.stream()
.collect(Collectors.toMap(p -> p.getFlowId().toLowerCase(), p -> p, (a, b) -> a));
for (ProcessInstanceRecordVo vo : voList) {
try {
// 从 Flowable 变量中获取 rejectToStarter 状态
Object rejectVar = runtimeService.getVariable(
vo.getProcessInstanceId(),
"rejectToStarter"
);
vo.setRejectToStarter(Boolean.TRUE.equals(rejectVar));
boolean rejectToStarter = false;
if (Objects.equals(vo.getStatus(), NodeStatusEnum.JXZ.getCode())) {
rejectToStarter = rejectToStarterIds.contains(vo.getProcessInstanceId());
vo.setRejectToStarter(rejectToStarter);
// 如果是驳回状态,获取 root 任务 ID 用于重新提交
if (Boolean.TRUE.equals(vo.getRejectToStarter())) {
Task task = taskService.createTaskQuery()
.processInstanceId(vo.getProcessInstanceId())
.taskDefinitionKey("root")
.singleResult();
if (task != null) {
vo.setResubmitTaskId(task.getId());
}
if (rejectToStarter) {
vo.setResubmitTaskId(resubmitTaskMap.get(vo.getProcessInstanceId()));
}
} catch (Exception e) {
// 流程可能已结束,设置为 false
} else {
vo.setRejectToStarter(false);
}
// 检查是否配置了打印模板(不依赖运行时变量,无需放在 try-catch 内)
processList.stream()
.filter(process -> process.getFlowId().equalsIgnoreCase(vo.getFlowId()))
.findFirst()
.ifPresent(process -> vo.setHasPrintTemplate(StrUtil.isNotBlank(process.getPrintTemplate())));
WithdrawState withdrawState = buildWithdrawState(vo, vo.getRejectToStarter(),
assignUserMap.getOrDefault(vo.getProcessInstanceId(), Collections.emptyList()));
vo.setCanWithdraw(withdrawState.canWithdraw);
vo.setWithdrawDisabledReason(withdrawState.disabledReason);
// 检查是否配置了打印模板
Process matchedProcess = processMap.get(vo.getFlowId().toLowerCase());
if (matchedProcess != null) {
vo.setHasPrintTemplate(StrUtil.isNotBlank(matchedProcess.getPrintTemplate()));
}
}
// 构建返回的分页对象
@@ -549,10 +574,128 @@ public class ProcessInstanceServiceImpl implements IProcessInstanceService {
Node nodeDto = objectMapper.readValue(process, Node.class);
Map<String, String> formPerms1 = nodeDto.getFormPerms();
boolean rejectToStarter = Objects.equals(processInstanceRecord.getStatus(), NodeStatusEnum.JXZ.getCode())
&& isRejectToStarter(processInstanceId);
WithdrawState withdrawState = buildWithdrawState(processInstanceRecord, rejectToStarter,
queryAssignUserMap(Collections.singleton(processInstanceId))
.getOrDefault(processInstanceId, Collections.emptyList()));
Dict set = Dict.create().set(ProcessInstanceRecord.Fields.processInstanceId, processInstanceId).set(ProcessInstanceRecord.Fields.status, processInstanceRecord.getStatus()).set("process", oaForms.getProcess()).set("formItems", oaForms.getFormItems()).set("formData", formData).set("formConfig", oaForms.getFormConfig()).set("formPerms", formPerms1);
Dict set = Dict.create()
.set(ProcessInstanceRecord.Fields.processInstanceId, processInstanceId)
.set(ProcessInstanceRecord.Fields.status, processInstanceRecord.getStatus())
.set("finishReason", processInstanceRecord.getFinishReason())
.set("rejectToStarter", rejectToStarter)
.set("canWithdraw", withdrawState.canWithdraw)
.set("withdrawDisabledReason", withdrawState.disabledReason)
.set("process", oaForms.getProcess())
.set("formItems", oaForms.getFormItems())
.set("formData", formData)
.set("formConfig", oaForms.getFormConfig())
.set("formPerms", formPerms1);
return R.ok(set);
}
private Map<String, List<ProcessNodeRecordAssignUser>> queryAssignUserMap(Set<String> processInstanceIds) {
if (CollUtil.isEmpty(processInstanceIds)) {
return Collections.emptyMap();
}
return processNodeRecordAssignUserService.lambdaQuery()
.in(ProcessNodeRecordAssignUser::getProcessInstanceId, processInstanceIds)
.ne(ProcessNodeRecordAssignUser::getNodeId, "root")
.list()
.stream()
.collect(Collectors.groupingBy(ProcessNodeRecordAssignUser::getProcessInstanceId));
}
private boolean isRejectToStarter(String processInstanceId) {
try {
Object rejectVar = runtimeService.getVariable(processInstanceId,
ProcessInstanceConstant.REJECT_TO_STARTER_VAR);
return Boolean.TRUE.equals(rejectVar);
} catch (Exception e) {
log.debug("查询驳回待提交状态失败: processInstanceId={}, error={}", processInstanceId, e.getMessage());
return false;
}
}
/**
* 批量查询流程实例的驳回到发起人状态
*
* @param processInstanceIds 流程实例ID集合
* @return 处于驳回待提交状态的流程实例ID集合
*/
private Set<String> batchQueryRejectToStarter(Set<String> processInstanceIds) {
if (CollUtil.isEmpty(processInstanceIds)) {
return Collections.emptySet();
}
try {
return historyService.createHistoricVariableInstanceQuery()
.processInstanceIds(processInstanceIds)
.variableName(ProcessInstanceConstant.REJECT_TO_STARTER_VAR)
.list()
.stream()
.filter(v -> Boolean.TRUE.equals(v.getValue()))
.map(HistoricVariableInstance::getProcessInstanceId)
.collect(Collectors.toSet());
} catch (Exception e) {
log.warn("批量查询驳回状态失败,降级为逐条查询: {}", e.getMessage());
return processInstanceIds.stream()
.filter(this::isRejectToStarter)
.collect(Collectors.toSet());
}
}
private WithdrawState buildWithdrawState(ProcessInstanceRecord processInstanceRecord, Boolean rejectToStarter,
List<ProcessNodeRecordAssignUser> assignUsers) {
if (!Objects.equals(processInstanceRecord.getStatus(), NodeStatusEnum.JXZ.getCode())) {
return new WithdrawState(false, getFinishedProcessReason(processInstanceRecord.getFinishReason()));
}
if (Boolean.TRUE.equals(rejectToStarter)) {
return new WithdrawState(false, "驳回待提交状态不可撤回");
}
boolean hasProcessedApproval = assignUsers.stream().anyMatch(this::isProcessedAssignUser);
if (hasProcessedApproval) {
return new WithdrawState(false, "首个审批节点已处理,无法撤回");
}
return new WithdrawState(true, null);
}
private boolean isProcessedAssignUser(ProcessNodeRecordAssignUser assignUser) {
return assignUser.getEndTime() != null
|| !Objects.equals(assignUser.getStatus(), NodeStatusEnum.JXZ.getCode());
}
private String getFinishedProcessReason(String finishReason) {
if (StrUtil.equals(finishReason, ProcessInstanceConstant.FINISH_REASON_WITHDRAW)) {
return "流程已撤回";
}
if (StrUtil.equals(finishReason, ProcessInstanceConstant.FINISH_REASON_TERMINATE)) {
return "流程已终止";
}
if (StrUtil.equals(finishReason, ProcessInstanceConstant.FINISH_REASON_PASS)) {
return "流程已通过";
}
if (StrUtil.equals(finishReason, ProcessInstanceConstant.FINISH_REASON_REFUSE)) {
return "流程已拒绝";
}
return "流程已结束";
}
private static final class WithdrawState {
private final boolean canWithdraw;
private final String disabledReason;
private WithdrawState(boolean canWithdraw, String disabledReason) {
this.canWithdraw = canWithdraw;
this.disabledReason = disabledReason;
}
}
}
@@ -10,14 +10,13 @@ import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.data.tenant.TenantContextHolder;
import com.pig4cloud.pigx.common.security.util.SecurityUtils;
import com.pig4cloud.pigx.flow.constant.NodeStatusEnum;
import com.pig4cloud.pigx.flow.constant.ProcessInstanceConstant;
import com.pig4cloud.pigx.flow.dto.IndexPageStatistics;
import com.pig4cloud.pigx.flow.dto.Node;
import com.pig4cloud.pigx.flow.dto.TaskParamDto;
import com.pig4cloud.pigx.flow.dto.TaskResultDto;
import com.pig4cloud.pigx.flow.entity.*;
import com.pig4cloud.pigx.flow.entity.Process;
import com.pig4cloud.pigx.flow.entity.ProcessCopy;
import com.pig4cloud.pigx.flow.entity.ProcessInstanceRecord;
import com.pig4cloud.pigx.flow.entity.ProcessNodeRecordAssignUser;
import com.pig4cloud.pigx.flow.service.*;
import com.pig4cloud.pigx.flow.support.utils.NodeUtil;
import lombok.RequiredArgsConstructor;
@@ -67,6 +66,8 @@ public class TaskServiceImpl implements ITaskService {
private final IProcessInstanceRecordService processInstanceRecordService;
private final IProcessNodeRecordService processNodeRecordService;
private final IFlowableEngineService flowableEngineService;
private final TaskService taskService;
@@ -476,39 +477,60 @@ public class TaskServiceImpl implements ITaskService {
*/
@Override
public R stopProcessInstance(TaskParamDto taskParamDto) {
String reqProcessInstanceId = taskParamDto.getProcessInstanceId();
List<String> allStopProcessInstanceIdList = getAllStopProcessInstanceIdList(reqProcessInstanceId);
CollUtil.reverse(allStopProcessInstanceIdList);
allStopProcessInstanceIdList.add(reqProcessInstanceId);
allStopProcessInstanceIdList.forEach(processInstanceId -> {
// 查询流程实例
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery()
.processInstanceId(processInstanceId)
.processInstanceTenantId(TenantContextHolder.getTenantId().toString())
.singleResult();
if (Optional.ofNullable(processInstance).isPresent()) {
// 查询执行实例
List<String> executionIds = runtimeService.createExecutionQuery()
.parentId(processInstanceId)
.list()
.stream()
.map(Execution::getId)
.toList();
// 更改活动状态为结束
runtimeService.createChangeActivityStateBuilder()
.moveExecutionsToSingleActivityId(executionIds, "end")
.changeState();
}
});
return terminateProcessInstance(taskParamDto.getProcessInstanceId(),
ProcessInstanceConstant.FINISH_REASON_TERMINATE);
}
// 停止流程实例后,更新流程实例记录状态
processInstanceRecordService.lambdaUpdate()
.set(ProcessInstanceRecord::getStatus, NodeStatusEnum.YJS.getCode())
.set(ProcessInstanceRecord::getFinishReason, String.valueOf(NodeStatusEnum.YZZ.getCode()))
.set(ProcessInstanceRecord::getEndTime, new Date())
.eq(ProcessInstanceRecord::getProcessInstanceId, reqProcessInstanceId)
.update(new ProcessInstanceRecord());
return R.ok();
/**
* 撤回流程
*
* @param taskParamDto 任务参数,包含流程实例ID
* @return R 操作结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
public R withdrawProcessInstance(TaskParamDto taskParamDto) {
String processInstanceId = taskParamDto.getProcessInstanceId();
if (StrUtil.isBlank(processInstanceId)) {
return R.failed("流程实例ID不能为空");
}
long userId = SecurityUtils.getUser().getId();
ProcessInstanceRecord processInstanceRecord = processInstanceRecordService.lambdaQuery()
.eq(ProcessInstanceRecord::getProcessInstanceId, processInstanceId)
.one();
if (processInstanceRecord == null
|| !Objects.equals(processInstanceRecord.getStatus(), NodeStatusEnum.JXZ.getCode())) {
return R.failed("流程不存在或已结束");
}
if (!Objects.equals(processInstanceRecord.getUserId(), userId)) {
return R.failed("仅发起人可撤回流程");
}
try {
Object rejectToStarter = runtimeService.getVariable(processInstanceId,
ProcessInstanceConstant.REJECT_TO_STARTER_VAR);
if (Boolean.TRUE.equals(rejectToStarter)) {
return R.failed("驳回待提交状态不可撤回");
}
} catch (Exception e) {
log.warn("查询撤回状态变量失败: processInstanceId={}, error={}", processInstanceId, e.getMessage());
}
boolean hasProcessedApproval = processNodeRecordAssignUserService.lambdaQuery()
.eq(ProcessNodeRecordAssignUser::getProcessInstanceId, processInstanceId)
.ne(ProcessNodeRecordAssignUser::getNodeId, "root")
.and(wrapper -> wrapper
.isNotNull(ProcessNodeRecordAssignUser::getEndTime)
.or()
.ne(ProcessNodeRecordAssignUser::getStatus, NodeStatusEnum.JXZ.getCode()))
.exists();
if (hasProcessedApproval) {
return R.failed("首个审批节点已处理,无法撤回");
}
return terminateProcessInstance(processInstanceId, ProcessInstanceConstant.FINISH_REASON_WITHDRAW);
}
/**
@@ -626,4 +648,72 @@ public class TaskServiceImpl implements ITaskService {
return collect;
}
private R terminateProcessInstance(String reqProcessInstanceId, String finishReason) {
if (StrUtil.isBlank(reqProcessInstanceId)) {
return R.failed("流程实例ID不能为空");
}
List<String> allStopProcessInstanceIdList = getAllStopProcessInstanceIdList(reqProcessInstanceId);
CollUtil.reverse(allStopProcessInstanceIdList);
allStopProcessInstanceIdList.add(reqProcessInstanceId);
Date now = new Date();
int assignUserStatus = Objects.equals(finishReason, ProcessInstanceConstant.FINISH_REASON_WITHDRAW)
? ProcessInstanceConstant.ASSIGN_USER_STATUS_WITHDRAW
: ProcessInstanceConstant.ASSIGN_USER_STATUS_TERMINATE;
// 先更新本地记录,确保流程结束通知能拿到准确的 finishReason
processInstanceRecordService.lambdaUpdate()
.set(ProcessInstanceRecord::getStatus, NodeStatusEnum.YJS.getCode())
.set(ProcessInstanceRecord::getFinishReason, finishReason)
.set(ProcessInstanceRecord::getEndTime, now)
.in(ProcessInstanceRecord::getProcessInstanceId, allStopProcessInstanceIdList)
.update(new ProcessInstanceRecord());
processNodeRecordService.lambdaUpdate()
.set(ProcessNodeRecord::getStatus, NodeStatusEnum.YJS.getCode())
.set(ProcessNodeRecord::getEndTime, now)
.in(ProcessNodeRecord::getProcessInstanceId, allStopProcessInstanceIdList)
.eq(ProcessNodeRecord::getStatus, NodeStatusEnum.JXZ.getCode())
.update(new ProcessNodeRecord());
processNodeRecordAssignUserService.lambdaUpdate()
.set(ProcessNodeRecordAssignUser::getStatus, assignUserStatus)
.set(ProcessNodeRecordAssignUser::getEndTime, Convert.toLocalDateTime(now))
.set(ProcessNodeRecordAssignUser::getTaskType,
Objects.equals(finishReason, ProcessInstanceConstant.FINISH_REASON_WITHDRAW)
? "WITHDRAW"
: "TERMINATE")
.in(ProcessNodeRecordAssignUser::getProcessInstanceId, allStopProcessInstanceIdList)
.eq(ProcessNodeRecordAssignUser::getStatus, NodeStatusEnum.JXZ.getCode())
.update(new ProcessNodeRecordAssignUser());
allStopProcessInstanceIdList.forEach(processInstanceId -> {
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery()
.processInstanceId(processInstanceId)
.processInstanceTenantId(TenantContextHolder.getTenantId().toString())
.singleResult();
if (processInstance == null) {
return;
}
List<String> executionIds = runtimeService.createExecutionQuery()
.parentId(processInstanceId)
.list()
.stream()
.map(Execution::getId)
.toList();
if (CollUtil.isEmpty(executionIds)) {
log.warn("流程实例缺少可迁移执行流,跳过状态迁移: processInstanceId={}", processInstanceId);
return;
}
runtimeService.createChangeActivityStateBuilder()
.moveExecutionsToSingleActivityId(executionIds, "end")
.changeState();
});
return R.ok();
}
}