Skip to content

Commit a31f94c

Browse files
xlorneclaude
andcommitted
fix: 任意审批节点拒绝后自动办结同节点其他待办 issue #235
- RejectAction 拒绝时,将同节点其他未办理的待办自动置为已办(autoDone) - 修复或签节点一人拒绝后,其他审批人遗留待办仍可继续办理的问题 - 修复流程记录/流程图因遗留待办停留在当前节点、错误预览结束节点的问题 - 新增 FlowIssue235AnyAuditRejectTest 覆盖 A-B-C(c1/c2 或签)-D 拒绝退回开始节点场景 Co-Authored-By: Claude Code <noreply@anthropic.com>
1 parent 8791bcf commit a31f94c

2 files changed

Lines changed: 288 additions & 0 deletions

File tree

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,16 @@ public void run(FlowSession flowSession) {
105105
// 当前审批为已办状态
106106
flowEvents.add(new FlowRecordDoneEvent(flowRecord, flowSession.isMock()));
107107

108+
// 拒绝即结束当前节点:将同节点其他未办理的待办(或签/并签的其他审批人)自动置为已办。
109+
// 否则节点被退回后仍会遗留待办,审批人可继续办理,流程视图也会停留在当前节点(issue #235)。
110+
for (FlowRecord record : flowSession.getCurrentNodeRecords()) {
111+
if (record.getId() != flowRecord.getId() && record.isTodo()) {
112+
record.autoDone();
113+
recordList.add(record);
114+
flowEvents.add(new FlowRecordDoneEvent(record, flowSession.isMock()));
115+
}
116+
}
117+
108118
List<FlowRecord> records = this.generateRecords(flowSession);
109119
if (!records.isEmpty()) {
110120
recordList.addAll(records);
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
package com.codingapi.flow.service;
2+
3+
import com.codingapi.flow.action.ActionType;
4+
import com.codingapi.flow.action.IFlowAction;
5+
import com.codingapi.flow.action.actions.CustomAction;
6+
import com.codingapi.flow.action.actions.PassAction;
7+
import com.codingapi.flow.action.actions.RejectAction;
8+
import com.codingapi.flow.builder.ActionBuilder;
9+
import com.codingapi.flow.builder.FormFieldPermissionsBuilder;
10+
import com.codingapi.flow.builder.NodeStrategyBuilder;
11+
import com.codingapi.flow.context.GatewayContext;
12+
import com.codingapi.flow.exception.FlowStateException;
13+
import com.codingapi.flow.factory.MyFlowServiceFactory;
14+
import com.codingapi.flow.form.DataType;
15+
import com.codingapi.flow.form.FlowForm;
16+
import com.codingapi.flow.form.FlowFormBuilder;
17+
import com.codingapi.flow.form.permission.PermissionType;
18+
import com.codingapi.flow.node.IFlowNode;
19+
import com.codingapi.flow.node.nodes.ApprovalNode;
20+
import com.codingapi.flow.node.nodes.EndNode;
21+
import com.codingapi.flow.node.nodes.StartNode;
22+
import com.codingapi.flow.pojo.body.FlowAdviceBody;
23+
import com.codingapi.flow.pojo.request.FlowActionRequest;
24+
import com.codingapi.flow.pojo.request.FlowCreateRequest;
25+
import com.codingapi.flow.pojo.request.FlowProcessNodeRequest;
26+
import com.codingapi.flow.pojo.response.ProcessNode;
27+
import com.codingapi.flow.record.FlowRecord;
28+
import com.codingapi.flow.script.factory.FlowGroovyScriptFactory;
29+
import com.codingapi.flow.strategy.node.FormFieldPermissionStrategy;
30+
import com.codingapi.flow.strategy.node.MultiOperatorAuditStrategy;
31+
import com.codingapi.flow.strategy.node.OperatorLoadStrategy;
32+
import com.codingapi.flow.user.User;
33+
import com.codingapi.flow.workflow.Workflow;
34+
import com.codingapi.flow.workflow.WorkflowBuilder;
35+
import org.junit.jupiter.api.Test;
36+
37+
import java.util.List;
38+
import java.util.Map;
39+
40+
import static org.junit.jupiter.api.Assertions.assertEquals;
41+
import static org.junit.jupiter.api.Assertions.assertThrows;
42+
import static org.junit.jupiter.api.Assertions.assertTrue;
43+
44+
/**
45+
* issue #235 任意审批(或签)节点拒绝场景。
46+
*
47+
* <p>流程设计:A(开始) -> B(审批,审批人 b1) -> C(审批,审批人 c1/c2,任意审核) -> D(结束)。
48+
* C 节点拒绝动作退回开始节点。
49+
*
50+
* <p>当 c1 拒绝时,C 节点即告结束:
51+
* <ul>
52+
* <li>c2 在同节点的待办应自动办结(视为已审核),不能继续审批;</li>
53+
* <li>流程退回开始节点,发起人收到新的待办;</li>
54+
* <li>流程节点视图中 C 节点不再处于审批中,也不会从遗留的 c2 待办错误预览后续的 D 节点。</li>
55+
* </ul>
56+
*/
57+
class FlowIssue235AnyAuditRejectTest {
58+
59+
private final MyFlowServiceFactory factory = new MyFlowServiceFactory();
60+
61+
@Test
62+
void anyAuditRejectShouldAutoDoneSiblingTodoAndReturnToStart() {
63+
// given:A-B-C-D,B 的审批人为 b1,C 的审批人为 c1/c2,C 为任意审核,拒绝退回开始节点
64+
User a = new User(1, "a");
65+
User b1 = new User(2, "b1");
66+
User c1 = new User(3, "c1");
67+
User c2 = new User(4, "c2");
68+
registerUsers(a, b1, c1, c2);
69+
70+
StartNode startNode = startNode();
71+
ApprovalNode bNode = approvalNode("B审批", "[2]", null);
72+
RejectAction rejectAction = RejectAction.defaultAction();
73+
rejectAction.setScript(FlowGroovyScriptFactory
74+
.createActionRejectScript("def run(request){return request.getStartNode().getId()}")
75+
.getKey());
76+
ApprovalNode cNode = approvalNode("C审批", "[3,4]",
77+
new MultiOperatorAuditStrategy(MultiOperatorAuditStrategy.Type.ANY, 0), rejectAction);
78+
EndNode endNode = EndNode.builder().build();
79+
Workflow workflow = saveWorkflow(a, startNode, bNode, cNode, endNode);
80+
Map<String, Object> data = data();
81+
82+
// 1. 发起人提交,b1 审批通过,流程到达 C 节点
83+
startAndSubmit(workflow, startNode, a, data);
84+
pass(todoOf(b1), bNode, b1, data);
85+
FlowRecord c1Todo = todoOf(c1);
86+
FlowRecord c2Todo = todoOf(c2);
87+
assertEquals(cNode.getId(), c1Todo.getNodeId());
88+
assertEquals(cNode.getId(), c2Todo.getNodeId());
89+
90+
// when:c1 拒绝
91+
reject(c1Todo, cNode, c1, data);
92+
93+
// then 1:c2 的同节点待办自动办结,c1/c2 均无待办
94+
assertNoTodo(c1);
95+
assertNoTodo(c2);
96+
assertTrue(factory.flowRecordRepository.get(c2Todo.getId()).isDone(),
97+
"c1 拒绝后,c2 的同节点待办应自动置为已办");
98+
99+
// then 2:流程退回开始节点,仅发起人存在一条新的待办
100+
List<FlowRecord> todoRecords = factory.flowRecordRepository.findTodoByOperator(a.getUserId());
101+
assertEquals(1, todoRecords.size(), "流程退回后发起人应收到一条待办");
102+
assertEquals(startNode.getId(), todoRecords.get(0).getNodeId());
103+
104+
// then 3:C 节点两条记录均为已办,流程中不存在遗留的 C 节点待办
105+
List<FlowRecord> records = processRecords(c1Todo);
106+
List<FlowRecord> cRecords = recordsOfNode(records, cNode);
107+
assertEquals(2, cRecords.size(), "C 节点应保留 c1、c2 两条记录");
108+
assertEquals(2, cRecords.stream().filter(FlowRecord::isDone).count(),
109+
"C 节点两条记录都应为已办(c1 拒绝,c2 自动办结)");
110+
assertEquals(1, records.stream().filter(FlowRecord::isTodo).count(),
111+
"流程中仅应存在发起人的一条待办");
112+
113+
// then 4:c2 不能再对该节点执行审批
114+
assertThrows(FlowStateException.class, () -> pass(c2Todo, cNode, c2, data),
115+
"c2 的记录已自动办结,不应再允许审批");
116+
117+
// then 5:流程节点视图不再停留在 C 节点审批中
118+
List<ProcessNode> nodeList = processNodes(todoRecords.get(0), a, data);
119+
120+
ProcessNode cNodeView = nodeList.stream()
121+
.filter(node -> cNode.getId().equals(node.getNodeId()))
122+
.filter(node -> !node.isHistory())
123+
.findFirst()
124+
.orElse(null);
125+
assertTrue(cNodeView == null || cNodeView.getApproveState() != ProcessNode.ApproveState.PROCESSING,
126+
"C 节点不应仍展示为审批中");
127+
128+
ProcessNode cHistoryView = nodeList.stream()
129+
.filter(node -> cNode.getId().equals(node.getNodeId()))
130+
.filter(ProcessNode::isHistory)
131+
.findFirst()
132+
.orElseThrow();
133+
assertTrue(cHistoryView.getOperators().stream()
134+
.noneMatch(operator -> ActionType.PASS.name().equals(operator.getActionType())),
135+
"C 节点历史中不应把拒绝误记为通过");
136+
}
137+
138+
// ==================== 流程装配 ====================
139+
140+
private void registerUsers(User... users) {
141+
for (User user : users) {
142+
factory.userGateway.save(user);
143+
}
144+
GatewayContext.getInstance().setFlowOperatorGateway(factory.userGateway);
145+
}
146+
147+
private FlowForm form() {
148+
return FlowFormBuilder.builder()
149+
.name("issue235")
150+
.code("issue235")
151+
.addField("name", "name", DataType.STRING)
152+
.addField("days", "days", DataType.INTEGER)
153+
.build();
154+
}
155+
156+
private Map<String, Object> data() {
157+
return Map.of("name", "lorne", "days", 1);
158+
}
159+
160+
private FormFieldPermissionStrategy perm() {
161+
return new FormFieldPermissionStrategy(FormFieldPermissionsBuilder.builder()
162+
.addPermission("issue235", "name", PermissionType.WRITE)
163+
.addPermission("issue235", "days", PermissionType.WRITE)
164+
.build());
165+
}
166+
167+
private StartNode startNode() {
168+
return StartNode.builder()
169+
.strategies(NodeStrategyBuilder.builder()
170+
.addStrategy(perm())
171+
.build())
172+
.actions(ActionBuilder.builder()
173+
.addAction(CustomAction.defaultAction())
174+
.build())
175+
.build();
176+
}
177+
178+
private ApprovalNode approvalNode(String name, String operatorIds, MultiOperatorAuditStrategy auditStrategy) {
179+
return approvalNode(name, operatorIds, auditStrategy, null);
180+
}
181+
182+
private ApprovalNode approvalNode(String name, String operatorIds,
183+
MultiOperatorAuditStrategy auditStrategy,
184+
RejectAction rejectAction) {
185+
NodeStrategyBuilder strategyBuilder = NodeStrategyBuilder.builder()
186+
.addStrategy(perm())
187+
.addStrategy(new OperatorLoadStrategy(
188+
FlowGroovyScriptFactory.createOperatorLoadScript("def run(request){return " + operatorIds + "}")
189+
.getKey()));
190+
if (auditStrategy != null) {
191+
strategyBuilder.addStrategy(auditStrategy);
192+
}
193+
ActionBuilder actionBuilder = ActionBuilder.builder().addAction(PassAction.defaultAction());
194+
if (rejectAction != null) {
195+
actionBuilder.addAction(rejectAction);
196+
}
197+
return ApprovalNode.builder()
198+
.name(name)
199+
.strategies(strategyBuilder.build())
200+
.actions(actionBuilder.build())
201+
.build();
202+
}
203+
204+
private Workflow saveWorkflow(User creator, IFlowNode... nodes) {
205+
WorkflowBuilder builder = WorkflowBuilder.builder()
206+
.title("issue235")
207+
.code("issue235")
208+
.createdOperator(creator)
209+
.form(form());
210+
for (IFlowNode node : nodes) {
211+
builder.addNode(node);
212+
}
213+
Workflow workflow = builder.build();
214+
factory.workflowService.saveWorkflow(workflow);
215+
return workflow;
216+
}
217+
218+
// ==================== 操作与断言 ====================
219+
220+
private void startAndSubmit(Workflow workflow, StartNode startNode, User user, Map<String, Object> data) {
221+
List<IFlowAction> startActions = startNode.actionManager().getActions();
222+
FlowCreateRequest createRequest = new FlowCreateRequest();
223+
createRequest.setWorkCode(workflow.getCode());
224+
createRequest.setFormData(data);
225+
createRequest.setActionId(startActions.get(0).id());
226+
createRequest.setOperatorId(user.getUserId());
227+
factory.flowService.create(createRequest);
228+
229+
pass(todoOf(user), startNode, user, data);
230+
}
231+
232+
private void pass(FlowRecord record, IFlowNode node, User operator, Map<String, Object> data) {
233+
IFlowAction action = node.actionManager().getActions().stream()
234+
.filter(item -> ActionType.PASS.name().equals(item.type()))
235+
.findFirst().orElseThrow();
236+
flowAction(record, operator, action, data, "同意");
237+
}
238+
239+
private void reject(FlowRecord record, IFlowNode node, User operator, Map<String, Object> data) {
240+
IFlowAction action = node.actionManager().getActions().stream()
241+
.filter(item -> ActionType.REJECT.name().equals(item.type()))
242+
.findFirst().orElseThrow();
243+
flowAction(record, operator, action, data, "拒绝");
244+
}
245+
246+
private void flowAction(FlowRecord record, User operator, IFlowAction action,
247+
Map<String, Object> data, String advice) {
248+
FlowActionRequest request = new FlowActionRequest();
249+
request.setFormData(data);
250+
request.setRecordId(record.getId());
251+
request.setAdvice(new FlowAdviceBody(action.id(), advice, operator.getUserId()));
252+
factory.flowService.action(request);
253+
}
254+
255+
private FlowRecord todoOf(User user) {
256+
List<FlowRecord> list = factory.flowRecordRepository.findTodoByOperator(user.getUserId());
257+
assertEquals(1, list.size(), user.getName() + " 应有且仅有一条待办");
258+
return list.get(0);
259+
}
260+
261+
private void assertNoTodo(User user) {
262+
assertEquals(0, factory.flowRecordRepository.findTodoByOperator(user.getUserId()).size(),
263+
user.getName() + " 不应有待办");
264+
}
265+
266+
private List<FlowRecord> processRecords(FlowRecord anyRecord) {
267+
return factory.flowRecordRepository.findProcessRecords(anyRecord.getProcessId());
268+
}
269+
270+
private List<FlowRecord> recordsOfNode(List<FlowRecord> records, IFlowNode node) {
271+
return records.stream().filter(record -> record.getNodeId().equals(node.getId())).toList();
272+
}
273+
274+
private List<ProcessNode> processNodes(FlowRecord record, User viewer, Map<String, Object> data) {
275+
return factory.flowService.processNodes(
276+
new FlowProcessNodeRequest(record.getId(), viewer.getUserId(), data));
277+
}
278+
}

0 commit comments

Comments
 (0)