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