Skip to content

Commit d204ee2

Browse files
claudejaquanli
authored andcommitted
docs(116393): 补充并行/人工/包容分支节点排序错误的 Bug 文档与修复计划
🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent 8df9022 commit d204ee2

2 files changed

Lines changed: 182 additions & 0 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# processNodes 接口分支节点排序异常
2+
3+
> 编码: 116393 | 日期: 2026-05-14 | 严重度: S2 | 状态: 待确认
4+
5+
## 现象描述
6+
7+
`processNodes` 接口返回的节点列表中,并行分支节点(`ParallelBranchNode`)、人工分支节点(`ManualBranchNode`)、包容分支节点(`InclusiveBranchNode` / `InclusiveElseBranchNode`)等分支类节点出现在了结束节点(`EndNode`)之后,未按流程图中的节点定义顺序排序,导致前端展示的流程路径不直观。
8+
9+
## 关键信息
10+
11+
- **可能关联的模块**
12+
- flow-engine-starter-api(REST API 层)- 提供 `/api/cmd/record/processNodes` 接口
13+
- flow-engine-framework(节点引擎)- `FlowProcessNodeService``NextNodeLoader`
14+
15+
- **可能关联的节点类型**
16+
- `ParallelNode` / `ParallelBranchNode`(并行分支)
17+
- `ManualNode` / `ManualBranchNode`(人工分支)
18+
- `InclusiveNode` / `InclusiveBranchNode` / `InclusiveElseBranchNode`(包容分支)
19+
20+
- **受影响的功能**
21+
- `processNodes` 接口的节点返回顺序
22+
- 流程预览中分支节点位置展示
23+
24+
- **初步判断**
25+
问题可能在 `FlowProcessNodeService.NextNodeLoader.fetchNextNode()``flow-engine-framework/src/main/java/com/codingapi/flow/service/impl/FlowProcessNodeService.java:170-192`)的递归遍历逻辑中。当前实现是深度优先递归,将每个节点 add 到一个共享 `nodeList`,分支控制节点的子分支可能在递归回溯阶段才被加入,导致顺序晚于 End 节点。需要确认递归顺序与节点 `order` 字段的关系,或在遍历完成后按节点定义顺序重新排序输出。
26+
27+
## 期望行为
28+
29+
`processNodes` 接口返回的节点列表应按照流程图中的节点定义顺序排序(即按 Start → 各业务节点 → 各分支控制节点 → End 的顺序),分支节点应出现在它实际所在的流程位置,而不是 End 节点之后。
30+
31+
## 备注
32+
33+
{待人工补充}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# 修复计划: processNodes 接口分支节点排序异常
2+
3+
> 编码: 116393 | 日期: 2026-05-14 | 类型: Bug 修复 | 来源: bugfix | 基于: docs/bugs/116393-并行人工包容分支节点排序错误.md
4+
5+
## 根因分析
6+
7+
问题位于 `FlowProcessNodeService.NextNodeLoader.fetchNextNode()``flow-engine-framework/src/main/java/com/codingapi/flow/service/impl/FlowProcessNodeService.java:174-197`),递归逻辑是**深度优先(DFS)**
8+
9+
```java
10+
this.nodeList.add(processNode); // 先 add 当前
11+
List<IFlowNode> nextNodes = workflow.nextNodes(flowNode);
12+
List<IFlowNode> selected = selectBranchNodes(nextNodes, ...);
13+
this.fetchNextNode(flowSession.updateSession(flowNode), selected); // 再递归
14+
```
15+
16+
对并行(`ParallelNode`)/ 人工(`ManualNode`)/ 包容(`InclusiveNode`,当多分支同时满足)等"必须走多条分支"的控制节点,遍历会**先把第一条分支一路走到 End**,再回溯处理其它分支:
17+
18+
```
19+
顺序: Start → ParallelNode → Branch_1 → Approval_A → ParallelEnd → End
20+
↑ DFS 一路走到底
21+
→ (回溯) Branch_2 → Approval_B ← 此时才 add,已经在 End 之后
22+
→ (回溯) Branch_3 → Approval_C
23+
```
24+
25+
`NextNodeLoader.loadNextNode()``FlowProcessNodeService.java:229-240`)最后只做了 `isDisplay` 过滤和"重复 nodeId 去重",**没有按节点定义顺序重排**,因此最终对外返回的列表顺序就是 DFS 的访问顺序:`[Start, Approval_A, ParallelEnd, End, Approval_B, Approval_C]`,分支节点错位在 End 之后。
26+
27+
而条件分支(`ConditionNode`)不受影响,因为 `selectBranchNodes``FlowProcessNodeService.java:199-227`)会把条件分支收敛到单条路径,DFS 不会回溯到其它分支。
28+
29+
证据:
30+
- `FlowProcessNodeService.java:174-197` DFS 递归未做后置排序
31+
- `FlowProcessNodeService.java:199-227` 当前 `selectBranchNodes` 只覆盖 `ConditionBranchNode` / `InclusiveBranchNode` 等 4 个类型,且对 if 命中的情况会单选一条
32+
- `ParallelBranchNode.filterBranches``ParallelBranchNode.java:50-65`)返回全部分支节点(不收敛),由此走 DFS 全遍历
33+
- `ManualNode.filterBranches``ManualNode.java:45-54`)在无 advice 时返回全部分支
34+
- 节点的"定义顺序"权威来源是 `Workflow.nodes``Workflow.java:108、162-164`),它是按 `WorkflowBuilder.addNode` 顺序追加的有序 `List`
35+
36+
## 影响面评估
37+
38+
- 直接修改的 Maven 模块: `flow-engine-framework`
39+
- 可能回归的场景:
40+
- 含并行节点(`ParallelNode`)的流程预览顺序
41+
- 含人工节点(`ManualNode`)的流程预览顺序
42+
- 含包容节点(`InclusiveNode`)多分支同时命中场景
43+
- 普通直线流程 / 条件分支流程(应当保持原顺序,因为 DFS 顺序本就与定义顺序一致)
44+
- 已运行流程(含历史节点 + 未来节点混合返回的场景)
45+
- 是否需要数据迁移/配置变更: 否
46+
47+
## 新增文件
48+
49+
| 文件路径 | 用途说明 |
50+
|----------|----------|
51+
| (无) | 排序逻辑直接复用 `Workflow.nodes` 索引,无需新增类 |
52+
53+
## 修改文件
54+
55+
| 文件路径 | 修改内容 |
56+
|----------|----------|
57+
| `flow-engine-framework/src/main/java/com/codingapi/flow/service/impl/FlowProcessNodeService.java` |`NextNodeLoader.loadNextNode()` 完成 DFS 与去重后,按 `workflow.getNodes()` 索引对结果重排,让"未来节点"按节点定义顺序输出。历史节点(buildHistoryNodes 返回)的顺序在 `processNodes()` 中独立保留,不受此处影响 |
58+
59+
## 关键代码修改
60+
61+
`NextNodeLoader.loadNextNode()` 当前实现:
62+
63+
```java
64+
public List<ProcessNode> loadNextNode(FlowSession flowSession) {
65+
this.fetchNextNode(flowSession, List.of(this.currentNode));
66+
List<ProcessNode> displayNodes = nodeList.stream().filter(ProcessNode::isDisplay).toList();
67+
List<ProcessNode> processNodeList = new ArrayList<>();
68+
for (ProcessNode node : displayNodes) {
69+
if (!processNodeList.contains(node)) {
70+
processNodeList.add(node);
71+
}
72+
}
73+
return processNodeList;
74+
}
75+
```
76+
77+
修改为:
78+
79+
```java
80+
public List<ProcessNode> loadNextNode(FlowSession flowSession) {
81+
this.fetchNextNode(flowSession, List.of(this.currentNode));
82+
List<ProcessNode> displayNodes = nodeList.stream().filter(ProcessNode::isDisplay).toList();
83+
List<ProcessNode> processNodeList = new ArrayList<>();
84+
for (ProcessNode node : displayNodes) {
85+
if (!processNodeList.contains(node)) {
86+
processNodeList.add(node);
87+
}
88+
}
89+
return sortByWorkflowNodeOrder(processNodeList);
90+
}
91+
92+
private List<ProcessNode> sortByWorkflowNodeOrder(List<ProcessNode> processNodes) {
93+
List<IFlowNode> workflowNodes = workflow.getNodes();
94+
Map<String, Integer> indexMap = new HashMap<>();
95+
for (int i = 0; i < workflowNodes.size(); i++) {
96+
indexMap.put(workflowNodes.get(i).getId(), i);
97+
}
98+
// 未在 workflow.nodes 顶层注册的节点(如嵌套子节点)放到末尾,相对顺序保持稳定
99+
return processNodes.stream()
100+
.sorted(Comparator.comparingInt(
101+
n -> indexMap.getOrDefault(n.getNodeId(), Integer.MAX_VALUE)))
102+
.toList();
103+
}
104+
```
105+
106+
要点说明:
107+
108+
1. **`Workflow.nodes` 索引为权威排序键**:该列表在 `WorkflowBuilder.addNode()` 中按调用顺序追加,对应前端流程图保存时的节点顺序,是"定义顺序"的事实依据
109+
2. **稳定排序**`Comparator.comparingInt` 配合 `Stream.sorted` 是稳定排序,索引相同(如未注册节点)的元素保持 DFS 顺序,不会引入新的不确定性
110+
3. **不动 `fetchNextNode` 内部逻辑**:递归 add 顺序保持原状,仅在最终输出前做一次重排。避免影响 `processNode.setCurrentState()` 等中间状态判定
111+
4. **不影响历史节点**`processNodes()` 在调用 `loadNextNode` 之前已经把历史节点 add 到 `this.nodeList` —— 这里返回的 `processNodeList` 只是"未来节点"部分,外层 `nodeList.addAll(nextNodes)` 仍把它们追加在历史节点之后
112+
113+
## 回归验证
114+
115+
| 验证项 | 说明 |
116+
|--------|------|
117+
| 并行分支顺序 | 构造 Start → ParallelNode(含 3 个分支,每分支 1 个 Approval)→ ParallelEnd → End 的流程,调用 `processNodes`,验证返回顺序为 Start → Approval_A → Approval_B → Approval_C → ParallelEnd → End |
118+
| 人工分支顺序 | 构造含 `ManualNode` 的流程(无 advice,返回全部分支),验证分支节点按定义顺序排列 |
119+
| 包容分支顺序 | 构造含 `InclusiveNode` 多分支同时命中的场景,验证分支节点按定义顺序排列 |
120+
| 条件分支不回归 | 验证 107562 修复的条件分支单路径选择行为依旧正确(包括 if 命中和 fallback else 两种情况) |
121+
| 直线流程不回归 | 普通 Start → Approval → End 流程顺序不变 |
122+
| 历史节点混合 | 已运行流程预览:历史节点(按时间顺序)排在前,未来节点(按定义顺序)排在后 |
123+
| 编译验证 | `mvn -pl flow-engine-framework -am compile` |
124+
| 既有测试不回归 | `mvn -pl flow-engine-framework test` |
125+
126+
具体验证命令:
127+
128+
```bash
129+
mvn -pl flow-engine-framework -am compile
130+
mvn -pl flow-engine-framework test
131+
```
132+
133+
## 执行顺序
134+
135+
1. 创建 git worktree(按项目规范,所有开发必须在 worktree 中进行)
136+
2. 修改 `flow-engine-framework/src/main/java/com/codingapi/flow/service/impl/FlowProcessNodeService.java`
137+
- 增加 `java.util.Comparator` / `java.util.HashMap` / `java.util.Map` 的 import(如未引入)
138+
-`NextNodeLoader` 类中新增私有方法 `sortByWorkflowNodeOrder(List<ProcessNode>)`
139+
- 修改 `loadNextNode(FlowSession)``return` 语句,最后调用 `sortByWorkflowNodeOrder`
140+
3. 执行 `mvn -pl flow-engine-framework -am compile` 编译验证
141+
4. 执行 `mvn -pl flow-engine-framework test` 完整回归
142+
5. 人工审核后提交 PR(目标分支:dev)
143+
144+
## 注意事项
145+
146+
- 严格遵循 CLAUDE.md 中的开发规范:本变更必须在 git worktree 中进行,PR 目标为 `dev` 分支
147+
- **依赖 107562 的修复**:本计划基于"selectBranchNodes 已存在"的代码版本撰写。若所在分支尚未合并 107562,需先合并或同源开发,避免 `loadNextNode` 区域出现冲突
148+
- 不修改运行时分支选择路径(`FlowSession.matchNextNodes` / 各 `filterBranches` 实现),仅作用于 `processNodes` 预览路径
149+
- `Workflow.nodes` 列表中通常仅包含顶层节点;嵌套在 `IBlockNode.blocks` 中的子节点(如 `ConditionBranchNode`)会被 `indexMap` 视为未注册节点排到末尾。鉴于这些子节点本身不是 `IDisplayNode`,已在 `loadNextNode` 上游被过滤,不会出现在最终结果中

0 commit comments

Comments
 (0)