From f759865202ff5f0d885f9a308a01b7f0300a9165 Mon Sep 17 00:00:00 2001 From: PIGCLOUD Date: Thu, 19 Mar 2026 08:41:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(pigx-flow-biz):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=E6=92=A4=E5=9B=9E=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 ITaskService 接口中添加 withdrawProcessInstance 方法用于撤回流程实例 - 实现 TaskServiceImpl 中的撤回逻辑,包括权限校验和流程状态更新 - 新增 terminateProcessInstance 私有方法统一处理流程终止和撤回操作 - 在 ProcessInstanceRecordVo 中添加撤回相关字段 canWithdraw 和 withdrawDisabledReason - 优化 ProcessInstanceServiceImpl 查询逻辑,批量处理驳回状态和撤回状态判断 - 在 ProcessTaskController 中添加撤回流程实例的接口端点 - 定义流程撤回相关的常量,包括结束原因和执行人状态 --- .../constant/ProcessInstanceConstant.java | 30 +++ .../pigx/flow/vo/ProcessInstanceRecordVo.java | 10 + .../controller/ProcessTaskController.java | 11 + .../pigx/flow/service/ITaskService.java | 8 + .../impl/ProcessInstanceServiceImpl.java | 197 +++++++++++++++--- .../flow/service/impl/TaskServiceImpl.java | 160 ++++++++++---- 6 files changed, 354 insertions(+), 62 deletions(-) diff --git a/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/constant/ProcessInstanceConstant.java b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/constant/ProcessInstanceConstant.java index 2ab6f372c..73bad9241 100644 --- a/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/constant/ProcessInstanceConstant.java +++ b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/constant/ProcessInstanceConstant.java @@ -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; + /** * 多人审批模式:会签 所有审批人都必须同意才能通过,任何一人拒绝则不通过 */ diff --git a/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/vo/ProcessInstanceRecordVo.java b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/vo/ProcessInstanceRecordVo.java index 2d7057c62..361bc7a9c 100644 --- a/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/vo/ProcessInstanceRecordVo.java +++ b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/vo/ProcessInstanceRecordVo.java @@ -44,4 +44,14 @@ public class ProcessInstanceRecordVo extends ProcessInstanceRecord { */ private Boolean hasPrintTemplate; + /** + * 是否允许撤回 + */ + private Boolean canWithdraw; + + /** + * 不可撤回原因 + */ + private String withdrawDisabledReason; + } diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/controller/ProcessTaskController.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/controller/ProcessTaskController.java index 94f0116d7..efc88cf45 100644 --- a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/controller/ProcessTaskController.java +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/controller/ProcessTaskController.java @@ -144,6 +144,17 @@ public class ProcessTaskController { } + /** + * 撤回流程实例 + * + * @param taskParamDto 撤回参数,包含流程实例ID + * @return R 统一响应对象,操作成功返回撤回信息 + */ + @PostMapping("withdrawProcessInstance") + public R withdrawProcessInstance(@RequestBody TaskParamDto taskParamDto) { + return taskService.withdrawProcessInstance(taskParamDto); + } + /** * 退回任务 *

diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/ITaskService.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/ITaskService.java index b15de35a5..982b7f308 100644 --- a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/ITaskService.java +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/ITaskService.java @@ -87,6 +87,14 @@ public interface ITaskService { */ R stopProcessInstance(TaskParamDto taskParamDto); + /** + * 撤回流程实例 + * + * @param taskParamDto 任务参数,包含流程实例ID + * @return 操作结果 + */ + R withdrawProcessInstance(TaskParamDto taskParamDto); + /** * 退回任务 *

diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/ProcessInstanceServiceImpl.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/ProcessInstanceServiceImpl.java index 1b8331bf0..d9a3c917d 100644 --- a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/ProcessInstanceServiceImpl.java +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/ProcessInstanceServiceImpl.java @@ -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 processList = CollUtil.isEmpty(flowIds) ? Collections.emptyList() : processService.lambdaQuery().in(Process::getFlowId, flowIds).list(); + Set processInstanceIds = voList.stream() + .map(ProcessInstanceRecordVo::getProcessInstanceId) + .filter(StrUtil::isNotBlank) + .collect(Collectors.toSet()); + Map> assignUserMap = queryAssignUserMap(processInstanceIds); + + // 批量查询进行中流程的 rejectToStarter 变量,避免 N+1 + Set runningInstanceIds = voList.stream() + .filter(vo -> Objects.equals(vo.getStatus(), NodeStatusEnum.JXZ.getCode())) + .map(ProcessInstanceRecordVo::getProcessInstanceId) + .filter(StrUtil::isNotBlank) + .collect(Collectors.toSet()); + + Set rejectToStarterIds = batchQueryRejectToStarter(runningInstanceIds); + + // 批量查询驳回待提交的 root 任务,避免 N+1 + Map 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 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 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> queryAssignUserMap(Set 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 batchQueryRejectToStarter(Set 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 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; + } + } + } diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/TaskServiceImpl.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/TaskServiceImpl.java index 6886c467f..ee7a251d9 100644 --- a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/TaskServiceImpl.java +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/TaskServiceImpl.java @@ -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 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 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 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 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(); + } + }