Skip to content

Commit a197541

Browse files
xlorneclaude
andcommitted
fix: 并行分支内审批人设定节点提前提示(issue #231)
PassAction 收集审批人设定节点时,对分支节点递归遍历了整条线性执行链, 导致并行节点下靠后的抄送节点在进入并行前就被提示设定审批人,后续审批 时又重复提示。 - 控制节点(条件/并行/包容/人工)继续进入各分支,条件节点仍只沿命中分支 - 分支节点仅检查首个节点,分支内后续节点由前序节点提交时再提示 - 新增 issue #231 复现测试 Closes #231 Co-Authored-By: Claude Code <noreply@anthropic.com>
1 parent 6999a5d commit a197541

2 files changed

Lines changed: 279 additions & 9 deletions

File tree

‎flow-engine-framework/src/main/java/com/codingapi/flow/action/actions/PassAction.java‎

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import com.codingapi.flow.event.FlowRecordTodoEvent;
1010
import com.codingapi.flow.event.IFlowEvent;
1111
import com.codingapi.flow.generator.FlowIDGeneratorGatewayContext;
12+
import com.codingapi.flow.manager.FlowNodeState;
1213
import com.codingapi.flow.manager.NodeStrategyManager;
1314
import com.codingapi.flow.node.BaseAuditNode;
1415
import com.codingapi.flow.node.IFlowNode;
@@ -242,8 +243,14 @@ private boolean isEndNodeTodoEvent(IFlowEvent event) {
242243
}
243244

244245
/**
245-
* 收集需要审批人设定的下游节点
246-
* 若下游节点本身是控制节点(如条件节点),则递归检查其子节点
246+
* 收集需要审批人设定的下游节点。
247+
*
248+
* <p>仅沿本次提交后实际会执行到的节点递归,避免把分支内更靠后的节点提前纳入提示(issue #231):
249+
* <ul>
250+
* <li>控制节点(条件、并行、包容、人工):进入其下各分支,条件节点仅沿命中的分支(issue #200)</li>
251+
* <li>分支节点:分支内是线性执行链,只检查排在最前的首个节点;
252+
* 分支内的后续节点由该分支上前一个节点提交时再提示</li>
253+
* </ul>
247254
*/
248255
private void collectApproverSelectNodes(FlowSession flowSession, IFlowNode node, Map<String, List<Long>> operatorSelectMap, List<NodeOption> result) {
249256
OperatorSelectType selectType = node.strategyManager().getOperatorSelectType();
@@ -259,13 +266,21 @@ private void collectApproverSelectNodes(FlowSession flowSession, IFlowNode node,
259266
if (blocks == null || blocks.isEmpty()) {
260267
return;
261268
}
262-
// 条件控制节点:仅沿实际命中的分支递归,避免把不会执行的分支中
263-
// 的审批人设定节点误加入提示(issue #200)
264-
if (node instanceof ConditionNode) {
265-
blocks = blocks.get(0).filterBranches(blocks, flowSession);
266-
}
267-
for (IFlowNode block : blocks) {
268-
collectApproverSelectNodes(flowSession, block, operatorSelectMap, result);
269+
FlowNodeState nodeState = new FlowNodeState(node);
270+
if (nodeState.isBlockNode()) {
271+
// 控制节点:递归进入各分支;条件节点仅沿实际命中的分支递归,
272+
// 避免把不会执行的分支中的审批人设定节点误加入提示(issue #200)
273+
if (node instanceof ConditionNode) {
274+
blocks = blocks.get(0).filterBranches(blocks, flowSession);
275+
}
276+
for (IFlowNode block : blocks) {
277+
collectApproverSelectNodes(flowSession, block, operatorSelectMap, result);
278+
}
279+
} else if (nodeState.isBranchNode()) {
280+
// 分支节点:仅检查分支的首个节点,分支内后续节点由前序节点提交时再提示(issue #231)
281+
for (IFlowNode firstNode : nodeState.getFirstBlocks()) {
282+
collectApproverSelectNodes(flowSession, firstNode, operatorSelectMap, result);
283+
}
269284
}
270285
}
271286
}
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
package com.codingapi.flow.service;
2+
3+
import com.codingapi.flow.action.IFlowAction;
4+
import com.codingapi.flow.builder.FormFieldPermissionsBuilder;
5+
import com.codingapi.flow.builder.NodeStrategyBuilder;
6+
import com.codingapi.flow.context.GatewayContext;
7+
import com.codingapi.flow.factory.MyFlowServiceFactory;
8+
import com.codingapi.flow.form.DataType;
9+
import com.codingapi.flow.form.FlowForm;
10+
import com.codingapi.flow.form.FlowFormBuilder;
11+
import com.codingapi.flow.form.permission.PermissionType;
12+
import com.codingapi.flow.node.IFlowNode;
13+
import com.codingapi.flow.node.nodes.*;
14+
import com.codingapi.flow.pojo.body.FlowAdviceBody;
15+
import com.codingapi.flow.pojo.request.FlowActionRequest;
16+
import com.codingapi.flow.pojo.request.FlowCreateRequest;
17+
import com.codingapi.flow.pojo.response.ActionResponse;
18+
import com.codingapi.flow.record.FlowRecord;
19+
import com.codingapi.flow.script.factory.FlowGroovyScriptFactory;
20+
import com.codingapi.flow.strategy.node.FormFieldPermissionStrategy;
21+
import com.codingapi.flow.strategy.node.OperatorLoadStrategy;
22+
import com.codingapi.flow.user.User;
23+
import com.codingapi.flow.workflow.Workflow;
24+
import com.codingapi.flow.workflow.WorkflowBuilder;
25+
import org.junit.jupiter.api.Test;
26+
27+
import java.util.List;
28+
import java.util.Map;
29+
30+
import static org.junit.jupiter.api.Assertions.*;
31+
32+
/**
33+
* issue #231 复现测试:并行节点下审批人设定节点被提前提示
34+
*
35+
* <p>流程设计:A(开始) -> B(审批) -> C(并行) -> D(结束)
36+
* <p>C 下两条并行分支:
37+
* <ul>
38+
* <li>分支1:A1(审批) -> A2(抄送,审批人设定)</li>
39+
* <li>分支2:B1(审批) -> B2(抄送,审批人设定)</li>
40+
* </ul>
41+
*
42+
* <p>问题:B 节点提交审批时就弹出 A2、B2 的审批人设定提示;A1、B1 审批时又再次提示。
43+
* <p>期望:仅在 A1 提交时提示设定 A2,在 B1 提交时提示设定 B2;B 节点提交时不提示。
44+
*/
45+
class FlowIssue231ParallelApproverSelectTest {
46+
47+
private final MyFlowServiceFactory factory = new MyFlowServiceFactory();
48+
49+
private NotifyNode a2Node;
50+
private NotifyNode b2Node;
51+
52+
@Test
53+
void shouldNotPromptApproverSelectAtParallelControlNode() {
54+
// given
55+
User user = new User(1, "user");
56+
User bUser = new User(2, "bUser");
57+
User a1User = new User(3, "a1User");
58+
User b1User = new User(4, "b1User");
59+
User a2User = new User(5, "a2User");
60+
User b2User = new User(6, "b2User");
61+
factory.userGateway.save(user);
62+
factory.userGateway.save(bUser);
63+
factory.userGateway.save(a1User);
64+
factory.userGateway.save(b1User);
65+
factory.userGateway.save(a2User);
66+
factory.userGateway.save(b2User);
67+
GatewayContext.getInstance().setFlowOperatorGateway(factory.userGateway);
68+
69+
Workflow workflow = buildWorkflow(user);
70+
factory.workflowService.saveWorkflow(workflow);
71+
72+
Map<String, Object> data = Map.of("name", "lorne", "days", 1, "reason", "leave");
73+
74+
// when: 发起流程并提交开始节点,B 收到待办
75+
createAndSubmitStart(workflow, user, data);
76+
FlowRecord bTodo = todoOf(bUser, "B 节点应收到待办");
77+
78+
// when: B 节点提交审批(不带 operatorSelectMap)
79+
ActionResponse response = submitRecord(workflow, bTodo, bUser, data, null);
80+
81+
// then: B 的下一节点是并行控制节点 C,其分支首节点 A1/B1 都不需要设定审批人,
82+
// 不应弹出任何审批人设定提示,流程应直接进入 A1、B1
83+
assertNull(response, "B 节点提交时不应弹出下游抄送节点的审批人设定提示");
84+
todoOf(a1User, "A1 节点应收到待办");
85+
todoOf(b1User, "B1 节点应收到待办");
86+
87+
// when: A1 提交审批(不带 operatorSelectMap)
88+
FlowRecord a1Todo = todoOf(a1User, "A1 节点应收到待办");
89+
ActionResponse a1Response = submitRecord(workflow, a1Todo, a1User, data, null);
90+
91+
// then: 此时才提示为 A2 设定审批人,且仅包含 A2
92+
assertNotNull(a1Response, "A1 节点提交时应提示为 A2 设定审批人");
93+
assertEquals(ActionResponse.ResponseType.OPERATOR_SELECT, a1Response.getResponseType());
94+
assertEquals(1, a1Response.getOptions().size());
95+
assertEquals(a2Node.getId(), a1Response.getOptions().get(0).getId());
96+
97+
// when: A1 再次提交并为 A2 指定审批人
98+
assertNull(submitRecord(workflow, a1Todo, a1User, data,
99+
Map.of(a2Node.getId(), List.of(a2User.getUserId()))));
100+
101+
// then: A2 抄送记录生成
102+
assertEquals(1, doneOf(a2User).size(), "A2 应收到抄送记录");
103+
104+
// when: B1 提交审批(不带 operatorSelectMap)
105+
FlowRecord b1Todo = todoOf(b1User, "B1 节点应收到待办");
106+
ActionResponse b1Response = submitRecord(workflow, b1Todo, b1User, data, null);
107+
108+
// then: 此时才提示为 B2 设定审批人,且仅包含 B2
109+
assertNotNull(b1Response, "B1 节点提交时应提示为 B2 设定审批人");
110+
assertEquals(ActionResponse.ResponseType.OPERATOR_SELECT, b1Response.getResponseType());
111+
assertEquals(1, b1Response.getOptions().size());
112+
assertEquals(b2Node.getId(), b1Response.getOptions().get(0).getId());
113+
114+
// when: B1 再次提交并为 B2 指定审批人
115+
assertNull(submitRecord(workflow, b1Todo, b1User, data,
116+
Map.of(b2Node.getId(), List.of(b2User.getUserId()))));
117+
118+
// then: B2 抄送记录生成
119+
assertEquals(1, doneOf(b2User).size(), "B2 应收到抄送记录");
120+
}
121+
122+
// ---------- 辅助方法 ----------
123+
124+
private Workflow buildWorkflow(User user) {
125+
FlowForm form = FlowFormBuilder.builder()
126+
.name("请假流程")
127+
.code("leave")
128+
.addField("请假人", "name", DataType.STRING)
129+
.addField("请假天数", "days", DataType.INTEGER)
130+
.addField("请假事由", "reason", DataType.STRING)
131+
.build();
132+
133+
StartNode startNode = StartNode.builder()
134+
.strategies(NodeStrategyBuilder.builder().addStrategy(writePermission()).build())
135+
.build();
136+
137+
// B 审批节点:固定审批人 bUser(2)
138+
ApprovalNode bNode = approvalNode("B审批", "def run(request){return [2]}");
139+
140+
// 分支1:A1(审批) -> A2(抄送,审批人设定)
141+
ApprovalNode a1Node = approvalNode("A1审批", "def run(request){return [3]}");
142+
a2Node = notifyApproverSelectNode("A2抄送");
143+
ParallelBranchNode branch1 = ParallelBranchNode.builder()
144+
.name("并行分支1")
145+
.order(1)
146+
.blocks(a1Node, a2Node)
147+
.build();
148+
149+
// 分支2:B1(审批) -> B2(抄送,审批人设定)
150+
ApprovalNode b1Node = approvalNode("B1审批", "def run(request){return [4]}");
151+
b2Node = notifyApproverSelectNode("B2抄送");
152+
ParallelBranchNode branch2 = ParallelBranchNode.builder()
153+
.name("并行分支2")
154+
.order(2)
155+
.blocks(b1Node, b2Node)
156+
.build();
157+
158+
ParallelNode parallelNode = ParallelNode.builder()
159+
.name("并行控制")
160+
.blocks(branch1, branch2)
161+
.build();
162+
163+
EndNode endNode = EndNode.builder().build();
164+
165+
return WorkflowBuilder.builder()
166+
.title("请假流程")
167+
.code("leave")
168+
.createdOperator(user)
169+
.form(form)
170+
.addNode(startNode)
171+
.addNode(bNode)
172+
.addNode(parallelNode)
173+
.addNode(endNode)
174+
.build();
175+
}
176+
177+
private void createAndSubmitStart(Workflow workflow, User user, Map<String, Object> data) {
178+
List<IFlowAction> startActions = workflow.getStartNode().actionManager().getActions();
179+
FlowCreateRequest createRequest = new FlowCreateRequest();
180+
createRequest.setWorkCode(workflow.getCode());
181+
createRequest.setFormData(data);
182+
createRequest.setActionId(startActions.get(0).id());
183+
createRequest.setOperatorId(user.getUserId());
184+
factory.flowService.create(createRequest);
185+
186+
List<FlowRecord> userRecords = factory.flowRecordRepository.findTodoByOperator(user.getUserId());
187+
assertEquals(1, userRecords.size(), "发起人应收到开始节点待办");
188+
189+
FlowActionRequest userRequest = new FlowActionRequest();
190+
userRequest.setFormData(data);
191+
userRequest.setRecordId(userRecords.get(0).getId());
192+
userRequest.setAdvice(new FlowAdviceBody(startActions.get(0).id(), "提交", user.getUserId()));
193+
factory.flowService.action(userRequest);
194+
}
195+
196+
private FlowRecord todoOf(User target, String message) {
197+
List<FlowRecord> records = factory.flowRecordRepository.findTodoByOperator(target.getUserId());
198+
assertEquals(1, records.size(), message);
199+
return records.get(0);
200+
}
201+
202+
private List<FlowRecord> doneOf(User target) {
203+
return factory.flowRecordRepository.findDoneByOperator(target.getUserId());
204+
}
205+
206+
private ActionResponse submitRecord(Workflow workflow, FlowRecord record, User operator, Map<String, Object> data,
207+
Map<String, List<Long>> operatorSelectMap) {
208+
IFlowNode node = workflow.getFlowNode(record.getNodeId());
209+
List<IFlowAction> actions = node.actionManager().getActions();
210+
FlowActionRequest request = new FlowActionRequest();
211+
request.setFormData(data);
212+
request.setRecordId(record.getId());
213+
FlowAdviceBody advice = new FlowAdviceBody(actions.get(0).id(), "同意", operator.getUserId());
214+
advice.setOperatorSelectMap(operatorSelectMap);
215+
request.setAdvice(advice);
216+
return factory.flowService.action(request);
217+
}
218+
219+
private ApprovalNode approvalNode(String name, String operatorScript) {
220+
return ApprovalNode.builder()
221+
.name(name)
222+
.strategies(NodeStrategyBuilder.builder()
223+
.addStrategy(readPermission())
224+
.addStrategy(new OperatorLoadStrategy(
225+
FlowGroovyScriptFactory.createOperatorLoadScript(operatorScript).getKey()))
226+
.build())
227+
.build();
228+
}
229+
230+
private NotifyNode notifyApproverSelectNode(String name) {
231+
return NotifyNode.builder()
232+
.name(name)
233+
.strategies(NodeStrategyBuilder.builder()
234+
.addStrategy(readPermission())
235+
.addStrategy(OperatorLoadStrategy.approverSelectStrategy())
236+
.build())
237+
.build();
238+
}
239+
240+
private FormFieldPermissionStrategy writePermission() {
241+
return new FormFieldPermissionStrategy(FormFieldPermissionsBuilder.builder()
242+
.addPermission("leave", "name", PermissionType.WRITE)
243+
.addPermission("leave", "days", PermissionType.WRITE)
244+
.addPermission("leave", "reason", PermissionType.WRITE)
245+
.build());
246+
}
247+
248+
private FormFieldPermissionStrategy readPermission() {
249+
return new FormFieldPermissionStrategy(FormFieldPermissionsBuilder.builder()
250+
.addPermission("leave", "name", PermissionType.READ)
251+
.addPermission("leave", "days", PermissionType.READ)
252+
.addPermission("leave", "reason", PermissionType.READ)
253+
.build());
254+
}
255+
}

0 commit comments

Comments
 (0)