From 5f3bff90e1b08abc9fe4c3c062e2e4bb75990808 Mon Sep 17 00:00:00 2001 From: PIGCLOUD Date: Mon, 11 May 2026 17:33:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(pigx-flow-biz):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=88=86=E7=BB=84ID=E4=B8=BA=E7=A9=BA=E6=97=B6=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E6=89=80=E6=9C=89=E6=9C=AA=E9=9A=90=E8=97=8F=E6=B5=81?= =?UTF-8?q?=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/feign/RemoteFlowApiFlowService.java | 78 +++---- .../constant/ProcessInstanceConstant.java | 30 +++ .../pigx/flow/dto/BizFlowBaseDto.java | 2 +- .../pigx/flow/vo/ProcessInstanceRecordVo.java | 10 + .../ProcessCombinationController.java | 3 +- .../controller/ProcessTaskController.java | 11 + .../pigx/flow/service/ITaskService.java | 8 + .../impl/CombinationGroupServiceImpl.java | 4 +- .../impl/ProcessInstanceServiceImpl.java | 197 +++++++++++++++--- .../flow/service/impl/TaskServiceImpl.java | 162 ++++++++++---- .../pigx/flow/support/utils/ModelUtil.java | 24 +-- 11 files changed, 411 insertions(+), 118 deletions(-) diff --git a/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/api/feign/RemoteFlowApiFlowService.java b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/api/feign/RemoteFlowApiFlowService.java index 5c5c33650..c88a7f05d 100644 --- a/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/api/feign/RemoteFlowApiFlowService.java +++ b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/api/feign/RemoteFlowApiFlowService.java @@ -27,46 +27,46 @@ import java.util.List; @FeignClient(contextId = "remoteFlowApiFlowService", value = ServiceNameConstants.FLOW_SERVER) public interface RemoteFlowApiFlowService { - /** - * 查询当前用户可发起的流程列表 - *

- * 根据当前登录用户的权限,查询该用户有权发起的所有流程。 - * 返回结果按分组组织,方便前端展示。 - *

- * - * @return 分组后的流程列表,包含分组信息和各分组下的流程 - */ - @GetMapping("/combination/group/listCurrentUserStartGroup") - R> listCurrentUserStartGroup(); + /** + * 查询当前用户可发起的流程列表 + *

+ * 根据当前登录用户的权限,查询该用户有权发起的所有流程。 + * 返回结果按分组组织,方便前端展示。 + *

+ * + * @return 分组后的流程列表,包含分组信息和各分组下的流程 + */ + @GetMapping("/combination/group/listCurrentUserStartGroup") + R> listCurrentUserStartGroup(); - /** - * 获取流程详细信息 - *

- * 根据流程ID获取流程的完整配置信息,包括: - * - 流程基本信息(名称、图标、描述等) - * - 表单配置(表单项定义、验证规则等) - * - 流程配置(节点定义、流转规则等) - *

- * - * @param flowId 流程定义ID - * @return 流程详细信息视图对象 - */ - @GetMapping("/process/getDetail") - R getDetail(@RequestParam("flowId") String flowId); + /** + * 获取流程详细信息 + *

+ * 根据流程ID获取流程的完整配置信息,包括: + * - 流程基本信息(名称、图标、描述等) + * - 表单配置(表单项定义、验证规则等) + * - 流程配置(节点定义、流转规则等) + *

+ * + * @param flowId 流程定义ID + * @return 流程详细信息视图对象 + */ + @GetMapping("/process/getDetail") + R getDetail(@RequestParam("flowId") String flowId); - /** - * 启动流程实例 - *

- * 根据流程定义创建新的流程实例。启动流程时需要提供: - * - 流程ID - * - 表单数据 - * - 需要自选的节点执行人信息 - *

- * - * @param paramDto 流程实例启动参数,包含流程ID、表单数据等 - * @return 启动成功后的流程实例ID - */ - @PostMapping("/process-instance/startProcessInstance") - R startProcessInstance(@RequestBody ProcessInstanceParamDto paramDto); + /** + * 启动流程实例 + *

+ * 根据流程定义创建新的流程实例。启动流程时需要提供: + * - 流程ID + * - 表单数据 + * - 需要自选的节点执行人信息 + *

+ * + * @param paramDto 流程实例启动参数,包含流程ID、表单数据等 + * @return 启动成功后的流程实例ID + */ + @PostMapping("/process-instance/startProcessInstance") + R startProcessInstance(@RequestBody ProcessInstanceParamDto paramDto); } 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/dto/BizFlowBaseDto.java b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/BizFlowBaseDto.java index bd19ea5eb..2962651b0 100644 --- a/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/BizFlowBaseDto.java +++ b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/BizFlowBaseDto.java @@ -1,6 +1,6 @@ package com.pig4cloud.pigx.flow.dto; -import cn.idev.excel.annotation.ExcelIgnore; +import org.apache.fesod.sheet.annotation.ExcelIgnore; import com.baomidou.mybatisplus.annotation.TableField; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; 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/ProcessCombinationController.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/controller/ProcessCombinationController.java index 547d77970..bb32182ca 100644 --- a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/controller/ProcessCombinationController.java +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/controller/ProcessCombinationController.java @@ -6,6 +6,7 @@ import com.pig4cloud.pigx.flow.service.ICombinationGroupService; import lombok.RequiredArgsConstructor; 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; /** @@ -37,7 +38,7 @@ public class ProcessCombinationController { * @return R 统一响应对象,包含分页后的流程组及流程列表数据 */ @GetMapping("listGroupWithProcess") - public R listGroupWithProcess(Page page, Long groupId) { + public R listGroupWithProcess(Page page, @RequestParam(required = false) Long groupId) { return combinationGroupService.listGroupWithProcess(page, groupId); } 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/CombinationGroupServiceImpl.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/CombinationGroupServiceImpl.java index cb85c5c46..e6b21e98d 100644 --- a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/CombinationGroupServiceImpl.java +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/CombinationGroupServiceImpl.java @@ -48,13 +48,13 @@ public class CombinationGroupServiceImpl implements ICombinationGroupService { * 根据流程分组ID查询该分组下所有未隐藏的流程,按照排序值升序和创建时间降序排列 *

* @param page 分页参数,用于控制查询结果的分页 - * @param groupId 流程分组ID + * @param groupId 流程分组ID,可为空;为空时返回所有分组下的未隐藏流程 * @return R 包含分页后的流程列表 */ @Override public R listGroupWithProcess(Page page, Long groupId) { return R.ok(processService.lambdaQuery() - .eq(Process::getGroupId, groupId) + .eq(Objects.nonNull(groupId), Process::getGroupId, groupId) .eq(Process::getHidden, YesNoEnum.NO.getCode()) .orderByAsc(Process::getSort) .orderByDesc(Process::getCreateTime) 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..1ee68fc2b 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; @@ -279,6 +280,8 @@ public class TaskServiceImpl implements ITaskService { } Dict set = Dict.create() + .set("taskId", taskId) + .set("nodeId", taskResultDto.getNodeId()) .set("processInstanceId", taskResultDto.getProcessInstanceId()) .set("node", node) .set("process", oaForms.getProcess()) @@ -476,39 +479,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 +650,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(); + } + } diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/utils/ModelUtil.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/utils/ModelUtil.java index 4471acc76..f5637f167 100644 --- a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/utils/ModelUtil.java +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/utils/ModelUtil.java @@ -225,8 +225,6 @@ public class ModelUtil { if (nodeDto.getType() == NodeTypeEnum.EXCLUSIVE_GATEWAY.getValue().intValue()) { if (ord == size) { expression = NodeExpressionStrategyFactory.handleDefaultBranch(branchs, ord - 1); - } else if (ord > 1) { - expression = NodeExpressionStrategyFactory.handleDefaultBranch(branchs, ord - 1); } else { expression = NodeExpressionStrategyFactory.handle(branch); } @@ -337,7 +335,7 @@ public class ModelUtil { } // 条件分支 if (node.getType() == NodeTypeEnum.EXCLUSIVE_GATEWAY.getValue().intValue()) { - flowElementList.addAll(buildInclusiveGatewayNode(node)); + flowElementList.addAll(buildExclusiveGatewayNode(node)); } // 并行分支 if (node.getType() == NodeTypeEnum.PARALLEL_GATEWAY.getValue().intValue()) { @@ -560,27 +558,27 @@ public class ModelUtil { } /** - * 构建包容网关 + * 构建排他网关 *

- * 创建包容网关(条件分支)节点和对应的合并网关。 - * 包容网关用于根据条件创建不同的执行路径。 + * 创建排他网关(条件分支)节点和对应的合并网关。 + * 排他网关用于根据条件只选择一条执行路径,避免多个条件同时为真时并行进入多个分支。 * - * @param node 包容网关节点数据 + * @param node 排他网关节点数据 * @return 包含分叉网关和合并网关的元素列表 */ - private static List buildInclusiveGatewayNode(Node node) { + private static List buildExclusiveGatewayNode(Node node) { node.setTailId(StrUtil.format("{}_merge_gateway", node.getId())); List flowElementList = new ArrayList<>(); - InclusiveGateway inclusiveGateway = new InclusiveGateway(); - inclusiveGateway.setId(node.getId()); - inclusiveGateway.setName(node.getName()); - flowElementList.add(inclusiveGateway); + ExclusiveGateway exclusiveGateway = new ExclusiveGateway(); + exclusiveGateway.setId(node.getId()); + exclusiveGateway.setName(node.getName()); + flowElementList.add(exclusiveGateway); // 合并网关 - InclusiveGateway gateway = new InclusiveGateway(); + ExclusiveGateway gateway = new ExclusiveGateway(); gateway.setId(StrUtil.format("{}_merge_gateway", node.getId())); gateway.setName(StrUtil.format("{}_合并网关", node.getName())); flowElementList.add(gateway);