ye-zhaojia 3 dní pred
rodič
commit
001b5fad91

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

@@ -56,7 +56,6 @@ import java.util.stream.Collectors;
 
 import static com.qqflow.engine.common.constant.SecurityConstants.ASSIGNEE_TYPE_ROLE;
 import static com.qqflow.engine.common.constant.SecurityConstants.ASSIGNEE_TYPE_USER;
-import static com.qqflow.engine.common.constant.SecurityConstants.USER_TYPE_SYSTEM;
 
 @Service
 @RequiredArgsConstructor
@@ -692,8 +691,9 @@ public class FlowEngineServiceImpl implements FlowEngineService {
                 }
                 continue;
             }
-            // 审批节点:若开启子节点,则按子节点创建任务,不创建父节点任务
-            if (NodeType.APPROVAL.getCode().equals(node.getType()) && this.isSubNodesEnabled(node)) {
+            // 审批节点:若开启子节点且存在子节点配置,则按子节点创建任务,不创建父节点任务
+            if (NodeType.APPROVAL.getCode().equals(node.getType()) && this.isSubNodesEnabled(node)
+                    && !this.extractSubNodes(node).isEmpty()) {
                 this.createSubNodeTasks(instance, node, model, visited, depth, allowRecreate);
                 continue;
             }

+ 4 - 0
src/main/resources/application-test.yml

@@ -9,6 +9,10 @@ spring:
       host: 192.168.1.139
       port: 6379
       password: redis123456
+  sql:
+    init:
+      mode: always
+      schema-locations: classpath:schema-mysql.sql
 
 mybatis-plus:
   mapper-locations: classpath*:/mapper/**/*.xml

+ 40 - 3
src/main/resources/schema-mysql.sql

@@ -192,11 +192,48 @@ CREATE TABLE IF NOT EXISTS bpm_attachment (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='流程附件表';
 
 -- 兼容已存在的 MySQL 数据库:补充审批任务子节点字段
-ALTER TABLE bpm_approval_task ADD COLUMN IF NOT EXISTS sub_node_id VARCHAR(50) COMMENT '子节点ID(父审批节点下的子节点标识)';
+SET @sql = (
+    SELECT IF(COUNT(*) = 0,
+        'ALTER TABLE bpm_approval_task ADD COLUMN sub_node_id VARCHAR(50) COMMENT ''子节点ID(父审批节点下的子节点标识)''',
+        'SELECT 1'
+    )
+    FROM INFORMATION_SCHEMA.COLUMNS
+    WHERE TABLE_NAME = 'bpm_approval_task'
+      AND COLUMN_NAME = 'sub_node_id'
+      AND TABLE_SCHEMA = DATABASE()
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
 
 -- 兼容已存在的 MySQL 数据库:补充角色企业微信字段
-ALTER TABLE sys_role ADD COLUMN IF NOT EXISTS wecom_user_id VARCHAR(100) COMMENT '企业微信用户ID';
-ALTER TABLE sys_role ADD COLUMN IF NOT EXISTS wecom_remind_enabled TINYINT DEFAULT 1 COMMENT '是否开启企微提醒:0-否 1-是';
+SET @sql = (
+    SELECT IF(COUNT(*) = 0,
+        'ALTER TABLE sys_role ADD COLUMN wecom_user_id VARCHAR(100) COMMENT ''企业微信用户ID''',
+        'SELECT 1'
+    )
+    FROM INFORMATION_SCHEMA.COLUMNS
+    WHERE TABLE_NAME = 'sys_role'
+      AND COLUMN_NAME = 'wecom_user_id'
+      AND TABLE_SCHEMA = DATABASE()
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+SET @sql = (
+    SELECT IF(COUNT(*) = 0,
+        'ALTER TABLE sys_role ADD COLUMN wecom_remind_enabled TINYINT DEFAULT 1 COMMENT ''是否开启企微提醒:0-否 1-是''',
+        'SELECT 1'
+    )
+    FROM INFORMATION_SCHEMA.COLUMNS
+    WHERE TABLE_NAME = 'sys_role'
+      AND COLUMN_NAME = 'wecom_remind_enabled'
+      AND TABLE_SCHEMA = DATABASE()
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
 
 -- ----------------------------
 -- 9. 通知配置表

+ 121 - 0
src/test/java/com/qqflow/engine/domain/flow/service/FlowSubNodeServiceTest.java

@@ -2,6 +2,7 @@ package com.qqflow.engine.domain.flow.service;
 
 import com.qqflow.engine.config.security.LoginUser;
 import com.qqflow.engine.domain.flow.dto.ApproveTaskDTO;
+import com.qqflow.engine.domain.flow.dto.NextNodeDTO;
 import com.qqflow.engine.domain.flow.dto.ProcessProgressDTO;
 import com.qqflow.engine.domain.flow.dto.StartProcessDTO;
 import com.qqflow.engine.domain.flow.enums.DefinitionStatus;
@@ -166,6 +167,70 @@ class FlowSubNodeServiceTest {
     }
 
     /**
+     * 普通审批节点(未开启子节点):应能正常创建任务、审批通过并结束流程。
+     */
+    @Test
+    void testNormalApprovalNodeWithoutSubNodes() {
+        Long definitionId = createDefinitionWithoutSubNodes();
+        Long instanceId = startProcess(definitionId);
+
+        List<ApprovalTask> tasks = approvalTaskMapper.selectByInstanceId(instanceId);
+        assertEquals(1, tasks.size(), "普通节点启动后应产生一个任务");
+        ApprovalTask task = tasks.get(0);
+        assertNull(task.getSubNodeId(), "普通节点任务不应有子节点ID");
+        assertEquals(TaskStatus.PENDING.getCode(), task.getTaskStatus());
+
+        // 详情页应能正常加载
+        ProcessProgressDTO progress = processInstanceService.getProgress(instanceId);
+        assertEquals(1, progress.getRemainingNodeCount(), "应剩余一个待审批节点");
+        assertTrue(progress.getNodes().isEmpty() || progress.getNodes().get(0).getSubNodes() == null,
+                "普通节点不应包含子节点进度");
+
+        approve(task.getId());
+        progress = processInstanceService.getProgress(instanceId);
+        assertEquals(5, progress.getInstance().getStatus(), "流程应已通过完成");
+        assertEquals(0, progress.getRemainingNodeCount(), "剩余节点数应为 0");
+    }
+
+    /**
+     * 开启子节点但未配置任何子节点:应回退为普通审批节点处理,避免无任务可审导致流程卡死。
+     */
+    @Test
+    void testApprovalNodeWithEmptySubNodesFallback() {
+        Long definitionId = createDefinitionWithEmptySubNodes();
+        Long instanceId = startProcess(definitionId);
+
+        List<ApprovalTask> tasks = approvalTaskMapper.selectByInstanceId(instanceId);
+        assertEquals(1, tasks.size(), "空子节点配置时应回退为普通节点并产生任务");
+        ApprovalTask task = tasks.get(0);
+        assertNull(task.getSubNodeId(), "回退为普通节点后任务不应有子节点ID");
+        assertEquals(TaskStatus.PENDING.getCode(), task.getTaskStatus());
+
+        approve(task.getId());
+        ProcessProgressDTO progress = processInstanceService.getProgress(instanceId);
+        assertEquals(5, progress.getInstance().getStatus(), "流程应已通过完成");
+    }
+
+    /**
+     * 子节点任务应能正确返回父节点的下游节点列表,用于前端选择下一节点。
+     */
+    @Test
+    void testGetNextNodesForSubNodeTask() {
+        Long definitionId = createDefinitionWithSubNodesAndNextApproval();
+        Long instanceId = startProcess(definitionId);
+
+        List<ApprovalTask> tasks = approvalTaskMapper.selectByInstanceId(instanceId);
+        ApprovalTask sub1Task = tasks.stream()
+                .filter(t -> "sub1".equals(t.getSubNodeId()) && TaskStatus.PENDING.getCode().equals(t.getTaskStatus()))
+                .findFirst()
+                .orElseThrow();
+
+        List<NextNodeDTO> nextNodes = approvalTaskService.getNextNodes(sub1Task.getId());
+        assertEquals(1, nextNodes.size(), "子节点任务应返回父节点的下游节点");
+        assertEquals("second_approval", nextNodes.get(0).getNodeId(), "应返回正确的下游审批节点");
+    }
+
+    /**
      * 并行子节点驳回:驳回一个子节点,只影响该子节点,其他子节点继续,重新审批后流程结束。
      */
     @Test
@@ -222,6 +287,62 @@ class FlowSubNodeServiceTest {
         return processDefinitionService.saveDefinition(po);
     }
 
+    private Long createDefinitionWithoutSubNodes() {
+        String modelJson = "{\"nodes\":["
+                + "{\"id\":\"start\",\"type\":\"start\"}"
+                + ", {\"id\":\"approval\",\"type\":\"approval\",\"name\":\"审批\","
+                + "\"properties\":{\"assigneeType\":\"ROLE\",\"assigneeValue\":\"super_admin\",\"approveMode\":\"or\"}}"
+                + ", {\"id\":\"end\",\"type\":\"end\"}],"
+                + "\"edges\":["
+                + "{\"sourceNodeId\":\"start\",\"targetNodeId\":\"approval\"}"
+                + ", {\"sourceNodeId\":\"approval\",\"targetNodeId\":\"end\"}]}";
+        ProcessDefinition po = new ProcessDefinition();
+        po.setProcessCode("NORMAL_FLOW_" + System.currentTimeMillis());
+        po.setProcessName("普通流程");
+        po.setModelJson(modelJson);
+        po.setStatus(DefinitionStatus.ENABLED.getCode());
+        return processDefinitionService.saveDefinition(po);
+    }
+
+    private Long createDefinitionWithEmptySubNodes() {
+        String modelJson = "{\"nodes\":["
+                + "{\"id\":\"start\",\"type\":\"start\"}"
+                + ", {\"id\":\"approval\",\"type\":\"approval\",\"name\":\"审批\","
+                + "\"properties\":{\"assigneeType\":\"ROLE\",\"assigneeValue\":\"super_admin\",\"approveMode\":\"or\",\"subNodesEnabled\":true,\"subNodeMode\":\"sequential\",\"subNodes\":[]}}"
+                + ", {\"id\":\"end\",\"type\":\"end\"}],"
+                + "\"edges\":["
+                + "{\"sourceNodeId\":\"start\",\"targetNodeId\":\"approval\"}"
+                + ", {\"sourceNodeId\":\"approval\",\"targetNodeId\":\"end\"}]}";
+        ProcessDefinition po = new ProcessDefinition();
+        po.setProcessCode("EMPTY_SUB_NODE_" + System.currentTimeMillis());
+        po.setProcessName("空子节点流程");
+        po.setModelJson(modelJson);
+        po.setStatus(DefinitionStatus.ENABLED.getCode());
+        return processDefinitionService.saveDefinition(po);
+    }
+
+    private Long createDefinitionWithSubNodesAndNextApproval() {
+        String subNodes = "[{\"id\":\"sub1\",\"name\":\"初审\",\"assigneeValue\":\"super_admin\",\"approveMode\":\"or\"}" +
+                ",{\"id\":\"sub2\",\"name\":\"复审\",\"assigneeValue\":\"super_admin\",\"approveMode\":\"or\"}]";
+        String modelJson = "{\"nodes\":["
+                + "{\"id\":\"start\",\"type\":\"start\"}"
+                + ", {\"id\":\"approval\",\"type\":\"approval\",\"name\":\"一级审批\","
+                + "\"properties\":{\"subNodesEnabled\":true,\"subNodeMode\":\"parallel\",\"subNodes\":" + subNodes + "}}"
+                + ", {\"id\":\"second_approval\",\"type\":\"approval\",\"name\":\"二级审批\","
+                + "\"properties\":{\"assigneeType\":\"ROLE\",\"assigneeValue\":\"super_admin\",\"approveMode\":\"or\"}}"
+                + ", {\"id\":\"end\",\"type\":\"end\"}],"
+                + "\"edges\":["
+                + "{\"sourceNodeId\":\"start\",\"targetNodeId\":\"approval\"}"
+                + ", {\"sourceNodeId\":\"approval\",\"targetNodeId\":\"second_approval\"}"
+                + ", {\"sourceNodeId\":\"second_approval\",\"targetNodeId\":\"end\"}]}";
+        ProcessDefinition po = new ProcessDefinition();
+        po.setProcessCode("SUB_NODE_WITH_NEXT_" + System.currentTimeMillis());
+        po.setProcessName("带子节点及下游节点流程");
+        po.setModelJson(modelJson);
+        po.setStatus(DefinitionStatus.ENABLED.getCode());
+        return processDefinitionService.saveDefinition(po);
+    }
+
     private Long startProcess(Long definitionId) {
         StartProcessDTO dto = new StartProcessDTO();
         dto.setProcessDefinitionId(definitionId);