فهرست منبع

feat: 流程间的实例迁移

wuwenyi 1 هفته پیش
والد
کامیت
c674d3d968

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

@@ -26,6 +26,7 @@ import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
 import java.util.List;
+import java.util.Map;
 
 @RestController
 @RequestMapping("/flow/instance")
@@ -131,6 +132,24 @@ public class ProcessInstanceController {
         return Result.ok(this.processInstanceService.participatedList(userId, userType, pageNum, pageSize));
     }
 
+    @PostMapping("/{id}/migrate")
+    @Operation(summary = "迁移单个流程实例到新版本定义")
+    @PreAuthorize("hasAnyRole('super_admin','flow_manager')")
+    public Result<Void> migrate(@PathVariable Long id, @RequestParam Long targetDefinitionId) {
+        this.processInstanceService.migrateToDefinition(id, targetDefinitionId);
+        return Result.ok();
+    }
+
+    @PostMapping("/migrate-all")
+    @Operation(summary = "批量迁移指定定义下所有运行中的实例到新版本定义")
+    @PreAuthorize("hasAnyRole('super_admin','flow_manager')")
+    public Result<Map<String, Object>> migrateAll(@RequestParam Long fromDefinitionId, @RequestParam Long toDefinitionId) {
+        int count = this.processInstanceService.migrateAllToDefinition(fromDefinitionId, toDefinitionId);
+        Map<String, Object> result = new java.util.LinkedHashMap<>();
+        result.put("migratedCount", count);
+        return Result.ok(result);
+    }
+
     private LoginUser requireLoginUser() {
         LoginUser loginUser = SecurityUtils.getLoginUser();
         if (loginUser == null) {

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

@@ -41,4 +41,10 @@ public interface ProcessInstanceService {
 
     /** 校验当前登录用户是否有权查看指定流程实例 */
     void checkInstanceVisibility(Long instanceId);
+
+    /** 将实例迁移到新版本的流程定义(人工确认兼容后执行) */
+    void migrateToDefinition(Long instanceId, Long targetDefinitionId);
+
+    /** 批量迁移指定定义下所有运行中的实例到新版本定义 */
+    int migrateAllToDefinition(Long fromDefinitionId, Long toDefinitionId);
 }

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

@@ -588,6 +588,94 @@ public class ProcessInstanceServiceImpl implements ProcessInstanceService {
         return USER_TYPE_SYSTEM.equals(userType) && task.getAssigneeId().equals(operatorId);
     }
 
+    @Override
+    @Transactional
+    public void migrateToDefinition(Long instanceId, Long targetDefinitionId) {
+        ProcessInstance instance = this.processInstanceMapper.selectById(instanceId);
+        if (instance == null) throw new BusinessException("流程实例不存在");
+        Long oldDefinitionId = instance.getProcessDefinitionId();
+        ProcessDefinition oldDef = this.processDefinitionMapper.selectById(oldDefinitionId);
+        ProcessDefinition newDef = this.processDefinitionMapper.selectById(targetDefinitionId);
+        if (oldDef == null || newDef == null) throw new BusinessException("流程定义不存在");
+        if (!Objects.equals(oldDef.getProcessCode(), newDef.getProcessCode())) {
+            throw new BusinessException("只能迁移到同一流程编码的不同版本");
+        }
+        // 匹配当前节点在新定义中的位置(按节点名称)
+        FlowModel oldModel = this.flowEngineService.parseModel(oldDef.getModelJson());
+        FlowModel newModel = this.flowEngineService.parseModel(newDef.getModelJson());
+        String currentNodeId = instance.getCurrentNodeId();
+        if (currentNodeId != null) {
+            String currentNodeName = oldModel.getNodes().stream()
+                    .filter(n -> n.getId().equals(currentNodeId))
+                    .findFirst().map(FlowNode::getName).orElse(null);
+            if (currentNodeName != null) {
+                String newNodeId = newModel.getNodes().stream()
+                        .filter(n -> currentNodeName.equals(n.getName())
+                                && !com.qqflow.engine.domain.flow.enums.NodeType.START.getCode().equals(n.getType())
+                                && !com.qqflow.engine.domain.flow.enums.NodeType.END.getCode().equals(n.getType()))
+                        .findFirst().map(FlowNode::getId).orElse(null);
+                if (newNodeId == null) {
+                    throw new BusinessException("当前节点 '" + currentNodeName + "' 在新流程定义中不存在,无法迁移");
+                }
+                // 更新实例指向新定义
+                this.processInstanceMapper.update(null, Wrappers.<ProcessInstance>lambdaUpdate()
+                        .eq(ProcessInstance::getId, instanceId)
+                        .set(ProcessInstance::getProcessDefinitionId, targetDefinitionId)
+                        .set(ProcessInstance::getVersion, newDef.getVersion())
+                        .set(ProcessInstance::getCurrentNodeId, newNodeId));
+                return;
+            }
+        }
+        // 无当前节点(已结束),直接更新定义ID
+        this.processInstanceMapper.update(null, Wrappers.<ProcessInstance>lambdaUpdate()
+                .eq(ProcessInstance::getId, instanceId)
+                .set(ProcessInstance::getProcessDefinitionId, targetDefinitionId)
+                .set(ProcessInstance::getVersion, newDef.getVersion()));
+    }
+
+    @Override
+    @Transactional
+    public int migrateAllToDefinition(Long fromDefinitionId, Long toDefinitionId) {
+        ProcessDefinition oldDef = this.processDefinitionMapper.selectById(fromDefinitionId);
+        ProcessDefinition newDef = this.processDefinitionMapper.selectById(toDefinitionId);
+        if (oldDef == null || newDef == null) throw new BusinessException("流程定义不存在");
+        if (!Objects.equals(oldDef.getProcessCode(), newDef.getProcessCode())) {
+            throw new BusinessException("只能迁移到同一流程编码的不同版本");
+        }
+        FlowModel oldModel = this.flowEngineService.parseModel(oldDef.getModelJson());
+        FlowModel newModel = this.flowEngineService.parseModel(newDef.getModelJson());
+        // 构建旧节点名 → 新节点ID映射
+        Map<String, String> nodeNameToNewId = newModel.getNodes().stream()
+                .filter(n -> n.getName() != null)
+                .collect(Collectors.toMap(FlowNode::getName, FlowNode::getId, (a, b) -> a));
+
+        List<ProcessInstance> instances = this.processInstanceMapper.selectList(
+                new LambdaQueryWrapper<ProcessInstance>()
+                        .eq(ProcessInstance::getProcessDefinitionId, fromDefinitionId)
+                        .eq(ProcessInstance::getDeleted, 0)
+                        .in(ProcessInstance::getStatus, 0, 1, 4)); // 待接收、运行中、已回退
+
+        int count = 0;
+        for (ProcessInstance pi : instances) {
+            String currentNodeId = pi.getCurrentNodeId();
+            String newNodeId = null;
+            if (currentNodeId != null) {
+                String nodeName = oldModel.getNodes().stream()
+                        .filter(n -> n.getId().equals(currentNodeId))
+                        .findFirst().map(FlowNode::getName).orElse(null);
+                if (nodeName != null) newNodeId = nodeNameToNewId.get(nodeName);
+                if (newNodeId == null) continue; // 节点不兼容,跳过
+            }
+            this.processInstanceMapper.update(null, Wrappers.<ProcessInstance>lambdaUpdate()
+                    .eq(ProcessInstance::getId, pi.getId())
+                    .set(ProcessInstance::getProcessDefinitionId, toDefinitionId)
+                    .set(ProcessInstance::getVersion, newDef.getVersion())
+                    .set(currentNodeId != null, ProcessInstance::getCurrentNodeId, newNodeId));
+            count++;
+        }
+        return count;
+    }
+
     private void fillBizValue(ProcessInstanceDTO dto, Long definitionId) {
         if (definitionId == null || dto.getFormData() == null) return;
         try {