Procházet zdrojové kódy

feat: 去掉转办入口并完善回退到上一节点

- 流程引擎回退时允许在目标节点重建待处理任务,解决回退后无新任务的问题
- findPreviousNode 跳过条件/CC 节点,直接定位上一个审批节点
- 新增补充/修正表单数据接口,仅当前节点处理人可操作
- 后端编译通过
ye-zhaojia před 1 týdnem
rodič
revize
322418a58e

+ 9 - 0
src/main/java/com/qqflow/engine/domain/flow/controller/ProcessInstanceController.java

@@ -9,6 +9,7 @@ import com.qqflow.engine.domain.flow.dto.AttachmentDTO;
 import com.qqflow.engine.domain.flow.dto.ProcessInstanceDTO;
 import com.qqflow.engine.domain.flow.dto.ProcessProgressDTO;
 import com.qqflow.engine.domain.flow.dto.StartProcessDTO;
+import com.qqflow.engine.domain.flow.dto.UpdateFormDataDTO;
 import com.qqflow.engine.domain.flow.service.ProcessInstanceService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
@@ -109,6 +110,14 @@ public class ProcessInstanceController {
         return Result.ok(this.processInstanceService.listAttachments(id));
     }
 
+    @PostMapping("/{id}/form-data")
+    @Operation(summary = "补充/修正表单数据")
+    public Result<Void> updateFormData(@PathVariable Long id,
+                                       @RequestBody @Valid UpdateFormDataDTO dto) {
+        this.processInstanceService.supplementFormData(id, dto.getFormData());
+        return Result.ok();
+    }
+
     @GetMapping("/participated")
     @Operation(summary = "我参与的流程实例列表")
     public Result<PageResult<ProcessInstanceDTO>> participated(

+ 14 - 0
src/main/java/com/qqflow/engine/domain/flow/dto/UpdateFormDataDTO.java

@@ -0,0 +1,14 @@
+package com.qqflow.engine.domain.flow.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+@Data
+@Schema(description = "补充/更新表单数据")
+public class UpdateFormDataDTO {
+
+    @NotBlank(message = "表单数据不能为空")
+    @Schema(description = "表单数据 JSON")
+    private String formData;
+}

+ 3 - 0
src/main/java/com/qqflow/engine/domain/flow/service/ProcessInstanceService.java

@@ -35,4 +35,7 @@ public interface ProcessInstanceService {
 
     /** 更新已有流程实例的表单数据,并可推进到指定节点(导入更新用) */
     void updateFormData(Long instanceId, String formData, String nodeName);
+
+    /** 当前节点负责人补充/修正表单数据 */
+    void supplementFormData(Long instanceId, String formData);
 }

+ 34 - 14
src/main/java/com/qqflow/engine/domain/flow/service/impl/FlowEngineServiceImpl.java

@@ -391,7 +391,7 @@ public class FlowEngineServiceImpl implements FlowEngineService {
         this.updateTaskStatus(currentTask, TaskStatus.RETURNED.getCode(), comment);
         this.approvalTaskHelper.cancelPendingTasks(instance.getId(), LocalDateTime.now());
         List<FlowNode> returnTargets = Collections.singletonList(targetNode);
-        this.createTasksForNodes(instance, returnTargets, model);
+        this.createTasksForNodes(instance, returnTargets, model, new HashSet<>(), 0, true);
         this.updateInstanceNode(instance, returnTargets);
     }
 
@@ -463,14 +463,34 @@ public class FlowEngineServiceImpl implements FlowEngineService {
     }
 
     private FlowNode findPreviousNode(FlowModel model, String currentNodeId) {
-        List<String> sourceIds = safeEdges(model).stream()
-                .filter(e -> Objects.equals(e.getTargetNodeId(), currentNodeId))
-                .map(FlowEdge::getSourceNodeId)
-                .collect(Collectors.toList());
-        return safeNodes(model).stream()
-                .filter(n -> sourceIds.contains(n.getId()) && !NodeType.START.getCode().equals(n.getType()))
-                .findFirst()
-                .orElse(null);
+        // 按直接入边找上一个审批节点;若上游为条件/CC 节点,继续向上追溯
+        Set<String> visited = new HashSet<>();
+        Deque<String> stack = new ArrayDeque<>();
+        stack.push(currentNodeId);
+        while (!stack.isEmpty()) {
+            String nodeId = stack.pop();
+            if (!visited.add(nodeId)) {
+                continue;
+            }
+            List<String> sourceIds = safeEdges(model).stream()
+                    .filter(e -> Objects.equals(e.getTargetNodeId(), nodeId))
+                    .map(FlowEdge::getSourceNodeId)
+                    .collect(Collectors.toList());
+            for (String sourceId : sourceIds) {
+                FlowNode sourceNode = safeNodes(model).stream()
+                        .filter(n -> n.getId().equals(sourceId))
+                        .findFirst()
+                        .orElse(null);
+                if (sourceNode == null || NodeType.START.getCode().equals(sourceNode.getType())) {
+                    continue;
+                }
+                if (NodeType.APPROVAL.getCode().equals(sourceNode.getType())) {
+                    return sourceNode;
+                }
+                stack.push(sourceId);
+            }
+        }
+        return null;
     }
 
     private boolean isEndNode(List<FlowNode> nodes) {
@@ -486,11 +506,11 @@ public class FlowEngineServiceImpl implements FlowEngineService {
     }
 
     private void createTasksForNodes(ProcessInstance instance, List<FlowNode> nodes, FlowModel model) {
-        this.createTasksForNodes(instance, nodes, model, new HashSet<>(), 0);
+        this.createTasksForNodes(instance, nodes, model, new HashSet<>(), 0, false);
     }
 
     private void createTasksForNodes(ProcessInstance instance, List<FlowNode> nodes, FlowModel model,
-                                     Set<String> visited, int depth) {
+                                     Set<String> visited, int depth, boolean allowRecreate) {
         if (depth > MAX_FLOW_DEPTH) {
             throw new BusinessException("流程模型存在循环或层级过深,已超过最大处理深度");
         }
@@ -500,8 +520,8 @@ public class FlowEngineServiceImpl implements FlowEngineService {
             if (NodeType.START.getCode().equals(node.getType()) || NodeType.END.getCode().equals(node.getType())) {
                 continue;
             }
-            // 幂等:已存在处理/跳过/待处理任务则不再重复创建
-            if (this.hasExistingTaskForNode(instance.getId(), node.getId())) {
+            // 幂等:已存在处理/跳过/待处理任务则不再重复创建(回退场景允许重建)
+            if (!allowRecreate && this.hasExistingTaskForNode(instance.getId(), node.getId())) {
                 continue;
             }
             if (!visited.add(node.getId())) {
@@ -551,7 +571,7 @@ public class FlowEngineServiceImpl implements FlowEngineService {
                 // 自动推进到CC节点的下游
                 List<FlowNode> ccNextNodes = this.getNextNodes(model, node.getId(), instance);
                 if (!ccNextNodes.isEmpty()) {
-                    this.createTasksForNodes(instance, ccNextNodes, model, visited, depth + 1);
+                    this.createTasksForNodes(instance, ccNextNodes, model, visited, depth + 1, allowRecreate);
                 }
                 continue;
             }

+ 48 - 0
src/main/java/com/qqflow/engine/domain/flow/service/impl/ProcessInstanceServiceImpl.java

@@ -1,5 +1,7 @@
 package com.qqflow.engine.domain.flow.service.impl;
 
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.qqflow.engine.common.PageResult;
@@ -530,6 +532,52 @@ public class ProcessInstanceServiceImpl implements ProcessInstanceService {
         }
     }
 
+    @Override
+    @Transactional
+    public void supplementFormData(Long instanceId, String formData) {
+        Long operatorId = SecurityUtils.getUserId();
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+        String userType = loginUser != null ? loginUser.getUserType() : null;
+        ProcessInstance instance = this.processInstanceMapper.selectById(instanceId);
+        if (instance == null) throw new BusinessException("流程实例不存在");
+        String currentNodeId = instance.getCurrentNodeId();
+        if (currentNodeId == null) throw new BusinessException("流程已结束,无需补充材料");
+        // 查询当前节点待处理任务
+        List<ApprovalTask> pendingTasks = this.approvalTaskMapper.selectList(
+                new LambdaQueryWrapper<ApprovalTask>()
+                        .eq(ApprovalTask::getInstanceId, instanceId)
+                        .eq(ApprovalTask::getNodeId, currentNodeId)
+                        .eq(ApprovalTask::getTaskStatus, TaskStatus.PENDING.getCode()));
+        boolean hasPermission = false;
+        for (ApprovalTask task : pendingTasks) {
+            if (this.canOperateTask(task, operatorId, userType)) {
+                hasPermission = true;
+                break;
+            }
+        }
+        if (!hasPermission) throw new BusinessException("只有当前节点处理人可补充材料");
+        this.processInstanceMapper.update(null, Wrappers.<ProcessInstance>lambdaUpdate()
+                .eq(ProcessInstance::getId, instanceId)
+                .set(ProcessInstance::getFormData, formData));
+    }
+
+    private boolean canOperateTask(ApprovalTask task, Long operatorId, String userType) {
+        if (ASSIGNEE_TYPE_ROLE.equals(task.getAssigneeType())) {
+            if (USER_TYPE_ROLE.equals(userType) && task.getAssigneeId().equals(operatorId)) {
+                return true;
+            }
+            if (!USER_TYPE_ROLE.equals(userType)) {
+                List<SysUserRole> userRoles = this.sysUserRoleMapper.selectList(
+                        new LambdaQueryWrapper<SysUserRole>()
+                                .eq(SysUserRole::getUserId, operatorId)
+                                .eq(SysUserRole::getRoleId, task.getAssigneeId()));
+                return !CollectionUtils.isEmpty(userRoles);
+            }
+            return false;
+        }
+        return USER_TYPE_SYSTEM.equals(userType) && task.getAssigneeId().equals(operatorId);
+    }
+
     private void fillBizValue(ProcessInstanceDTO dto, Long definitionId) {
         if (definitionId == null || dto.getFormData() == null) return;
         try {