diff --git a/AGENTS.md b/AGENTS.md index ef0a810f..6e2788de 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -95,11 +95,6 @@ cd flow-engine-example && mvn spring-boot:run ## 开发规范 -### 禁止自动提交 - -- **未经用户明确要求,任何情况下不得执行 `git commit` / `git push` / `git merge` 操作** -- 所有代码变更需经用户审核后再提交 - ### GitHub Issue 标准处理流程 处理任何来自 GitHub 的 issue 时,必须遵循 [docs/conventions/workflow/github-issue.md](./docs/conventions/workflow/github-issue.md): diff --git a/flow-engine-framework/src/main/java/com/codingapi/flow/action/actions/PassAction.java b/flow-engine-framework/src/main/java/com/codingapi/flow/action/actions/PassAction.java index 77453520..fa51f533 100644 --- a/flow-engine-framework/src/main/java/com/codingapi/flow/action/actions/PassAction.java +++ b/flow-engine-framework/src/main/java/com/codingapi/flow/action/actions/PassAction.java @@ -115,19 +115,13 @@ public void run(FlowSession flowSession) { if (currentNode.getType().equalsIgnoreCase(StartNode.NODE_TYPE)) { // 发起人设定模式:开始节点提交时,扫描整个工作流中所有 INITIATOR_SELECT 节点 operatorSelectNodes.addAll(this.loadInitiatorSelectNodes(flowSession)); - // 开始节点直接下游若为审批人设定(APPROVER_SELECT)节点,由于没有前序审批人可为其指定操作人, - // 需由发起人在提交开始节点时指定,否则节点取不到审批人,流程会停滞在开始节点 + // 开始节点下游若为审批人设定(APPROVER_SELECT)节点,由于没有前序审批人可为其指定操作人, + // 需由发起人在提交开始节点时指定,否则节点取不到审批人,流程会停滞在开始节点。 + // 必须与审批节点走同一套递归收集:人工节点选中分支后,下游实际是分支节点(MANUAL_BRANCH), + // 只判断直接下游节点会漏掉分支内的 APPROVER_SELECT 节点(issue #259) if (nextNodes != null) { for (IFlowNode nextNode : nextNodes) { - if (nextNode.strategyManager().getOperatorSelectType() != OperatorSelectType.APPROVER_SELECT) { - continue; - } - if (operatorSelectMap == null || !operatorSelectMap.containsKey(nextNode.getId()) - || operatorSelectMap.get(nextNode.getId()).isEmpty()) { - List range = nextNode.strategyManager() - .loadOperatorRange(flowSession.updateSession(nextNode)); - operatorSelectNodes.add(new NodeOption(nextNode, range)); - } + collectApproverSelectNodes(flowSession, nextNode, operatorSelectMap, operatorSelectNodes); } } } else { diff --git a/flow-engine-framework/src/main/java/com/codingapi/flow/service/FlowRecordSaveService.java b/flow-engine-framework/src/main/java/com/codingapi/flow/service/FlowRecordSaveService.java index 03aae621..fb9aa8dd 100644 --- a/flow-engine-framework/src/main/java/com/codingapi/flow/service/FlowRecordSaveService.java +++ b/flow-engine-framework/src/main/java/com/codingapi/flow/service/FlowRecordSaveService.java @@ -137,42 +137,56 @@ private void saveRecords() { private void removeTodoMergeRecords() { for (FlowRecord flowRecord : flowRecords) { - if (flowRecord.isDone()) { - if (flowRecord.isMergeable()) { - FlowTodoRecord todoMargeRecord = flowTodoRecordRepository.getByTodoKey(flowRecord.getTodoKey()); - if (todoMargeRecord != null) { - List margeRelations = flowTodoMergeRepository.findByTodoId(todoMargeRecord.getId()); - if (margeRelations != null && !margeRelations.isEmpty()) { - for (FlowTodoMerge margeRelation : margeRelations) { - if (margeRelation.isRecord(flowRecord.getId())) { - flowTodoMergeRepository.delete(margeRelation); - todoMargeRecord.divMergeCount(); - if (todoMargeRecord.hasMergeCount()) { - // 代表记录可能正是本次办结的记录,需要重新指向组内剩余的待办记录(issue #240) - refreshTodoMergeRecord(todoMargeRecord, margeRelations, flowRecord.getId()); - flowTodoRecordRepository.save(todoMargeRecord); - } else { - flowTodoRecordRepository.delete(todoMargeRecord); - } - } - } - } - } - } else { - FlowTodoRecord flowTodoRecord = flowTodoRecordRepository.getByTodoKey(flowRecord.getTodoKey()); - if (flowTodoRecord != null) { - flowTodoRecordRepository.delete(flowTodoRecord); - } - } - }else { - if(flowRecord.isRevoked()){ - FlowTodoRecord flowTodoRecord = flowTodoRecordRepository.getByTodoKey(flowRecord.getTodoKey()); - if (flowTodoRecord != null) { - flowTodoRecordRepository.delete(flowTodoRecord); - } - } + // 已办结与已撤销都要把该记录从待办中摘除。 + // 合并待办按「合并依据 + 工作流运行时 + 节点」聚合,组内一条流程记录办结或撤销时, + // 只能摘除它自己的合并关系,不能连带删除同组其他流程记录的待办(issue #260) + if (flowRecord.isDone() || flowRecord.isRevoked()) { + removeTodoRecord(flowRecord); + } + } + } + + private void removeTodoRecord(FlowRecord flowRecord) { + FlowTodoRecord flowTodoRecord = flowTodoRecordRepository.getByTodoKey(flowRecord.getTodoKey()); + if (flowTodoRecord == null) { + return; + } + if (!flowRecord.isMergeable()) { + flowTodoRecordRepository.delete(flowTodoRecord); + return; + } + List margeRelations = flowTodoMergeRepository.findByTodoId(flowTodoRecord.getId()); + if (margeRelations == null || margeRelations.isEmpty()) { + return; + } + for (FlowTodoMerge margeRelation : margeRelations) { + if (!margeRelation.isRecord(flowRecord.getId())) { + continue; + } + flowTodoMergeRepository.delete(margeRelation); + flowTodoRecord.divMergeCount(); + if (flowTodoRecord.hasMergeCount()) { + // 代表记录可能正是本次办结/撤销的记录,需要重新指向组内剩余的待办记录(issue #240 / #260) + refreshTodoMergeRecord(flowTodoRecord, margeRelations, flowRecord.getId()); + flowTodoRecordRepository.save(flowTodoRecord); + } else { + // 组内已无剩余记录:删除待办行,并清理残余合并关系,避免留下指向已删除待办的孤儿数据 + removeTodoRecordWithRelations(flowTodoRecord); + } + } + } + + /** + * 删除合并待办行及其全部残余合并关系。 + */ + private void removeTodoRecordWithRelations(FlowTodoRecord flowTodoRecord) { + List margeRelations = flowTodoMergeRepository.findByTodoId(flowTodoRecord.getId()); + if (margeRelations != null) { + for (FlowTodoMerge margeRelation : margeRelations) { + flowTodoMergeRepository.delete(margeRelation); } } + flowTodoRecordRepository.delete(flowTodoRecord); } /** diff --git a/flow-engine-framework/src/test/java/com/codingapi/flow/service/FlowIssue259ManualBranchApproverSelectTest.java b/flow-engine-framework/src/test/java/com/codingapi/flow/service/FlowIssue259ManualBranchApproverSelectTest.java new file mode 100644 index 00000000..3f9af74c --- /dev/null +++ b/flow-engine-framework/src/test/java/com/codingapi/flow/service/FlowIssue259ManualBranchApproverSelectTest.java @@ -0,0 +1,215 @@ +package com.codingapi.flow.service; + +import com.codingapi.flow.action.IFlowAction; +import com.codingapi.flow.builder.FormFieldPermissionsBuilder; +import com.codingapi.flow.builder.NodeStrategyBuilder; +import com.codingapi.flow.context.GatewayContext; +import com.codingapi.flow.factory.MyFlowServiceFactory; +import com.codingapi.flow.form.DataType; +import com.codingapi.flow.form.FlowForm; +import com.codingapi.flow.form.FlowFormBuilder; +import com.codingapi.flow.form.permission.PermissionType; +import com.codingapi.flow.node.nodes.ApprovalNode; +import com.codingapi.flow.node.nodes.EndNode; +import com.codingapi.flow.node.nodes.ManualBranchNode; +import com.codingapi.flow.node.nodes.ManualNode; +import com.codingapi.flow.node.nodes.StartNode; +import com.codingapi.flow.pojo.body.FlowAdviceBody; +import com.codingapi.flow.pojo.request.FlowActionRequest; +import com.codingapi.flow.pojo.request.FlowCreateRequest; +import com.codingapi.flow.pojo.response.ActionResponse; +import com.codingapi.flow.record.FlowRecord; +import com.codingapi.flow.strategy.node.FormFieldPermissionStrategy; +import com.codingapi.flow.strategy.node.OperatorLoadStrategy; +import com.codingapi.flow.user.User; +import com.codingapi.flow.workflow.Workflow; +import com.codingapi.flow.workflow.WorkflowBuilder; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * issue #259 复现测试:人工节点分支下存在审批人设定节点 + * + *

流程设计:A(开始) -> B(人工节点) -> C1(审批人设定) / C2(审批人设定) -> D(结束)。 + * + *

问题:发起人提交开始节点后首先弹出人工节点的分支选择框;选择 C1 所在分支后, + * 引擎未继续弹出 C1 节点的审批人设定框,流程直接流转到 C1,导致 C1 无审批人而异常。 + * + *

期望:选择分支后紧跟着提示为 C1 设定审批人;只有选中的分支才会被提示, + * 未选中分支(C2)不应出现在提示中。 + */ +class FlowIssue259ManualBranchApproverSelectTest { + + private final MyFlowServiceFactory factory = new MyFlowServiceFactory(); + + private ApprovalNode c1Node; + private ApprovalNode c2Node; + private ManualBranchNode c1BranchNode; + + /** + * 选择 C1 分支后应提示为 C1 设定审批人,指定后流程流转到 C1。 + * + *

前置条件:流程为 A(开始) -> B(人工) -> C1/C2(审批人设定) -> D(结束)。 + *

执行步骤:发起流程 -> 提交开始节点 -> 选择 C1 分支 -> 为 C1 指定审批人。 + *

期望:选择分支的响应为 OPERATOR_SELECT 且只包含 C1;指定审批人后 C1 收到待办。 + */ + @Test + void shouldPromptApproverSelectAfterSelectingManualBranch() { + // given + User user = new User(1, "user"); + User c1User = new User(2, "c1User"); + User c2User = new User(3, "c2User"); + factory.userGateway.save(user); + factory.userGateway.save(c1User); + factory.userGateway.save(c2User); + GatewayContext.getInstance().setFlowOperatorGateway(factory.userGateway); + + Workflow workflow = buildWorkflow(user); + factory.workflowService.saveWorkflow(workflow); + + Map data = Map.of("name", "lorne", "reason", "leave"); + + List startActions = workflow.getStartNode().actionManager().getActions(); + createFlow(workflow, user, data, startActions); + FlowRecord startRecord = todoOf(user, "发起人应收到开始节点待办"); + + // when: 第一次提交开始节点,应提示选择人工节点分支 + ActionResponse manualResponse = submitStart(startActions, startRecord, user, data, null, null); + + // then + assertNotNull(manualResponse, "开始节点提交后应提示选择人工节点分支"); + assertEquals(ActionResponse.ResponseType.MANUAL_NODE_SELECT, manualResponse.getResponseType()); + assertEquals(2, manualResponse.getOptions().size()); + + // when: 选择 C1 所在分支后再次提交 + ActionResponse operatorResponse = submitStart( + startActions, startRecord, user, data, c1BranchNode.getId(), null); + + // then: 应紧跟着提示为 C1 设定审批人,且只提示选中分支内的 C1 + assertNotNull(operatorResponse, "选择 C1 分支后应提示为 C1 节点设定审批人"); + assertEquals(ActionResponse.ResponseType.OPERATOR_SELECT, operatorResponse.getResponseType()); + assertEquals(1, operatorResponse.getOptions().size()); + assertEquals(c1Node.getId(), operatorResponse.getOptions().get(0).getId()); + + // when: 为 C1 指定审批人后再次提交 + ActionResponse finishResponse = submitStart( + startActions, startRecord, user, data, c1BranchNode.getId(), + Map.of(c1Node.getId(), List.of(c1User.getUserId()))); + + // then: 流程流转到 C1,未选中分支的 C2 不产生待办 + assertNull(finishResponse); + assertEquals(1, factory.flowRecordRepository.findTodoByOperator(c1User.getUserId()).size(), + "为 C1 指定审批人后,流程应流转到 C1 节点"); + assertEquals(0, factory.flowRecordRepository.findTodoByOperator(c2User.getUserId()).size(), + "未选中分支的 C2 不应产生待办"); + } + + // ---------- 辅助方法 ---------- + + private Workflow buildWorkflow(User user) { + FlowForm form = FlowFormBuilder.builder() + .name("请假流程") + .code("leave") + .addField("请假人", "name", DataType.STRING) + .addField("请假事由", "reason", DataType.STRING) + .build(); + + StartNode startNode = StartNode.builder() + .strategies(NodeStrategyBuilder.builder().addStrategy(writePermission()).build()) + .build(); + + // C1、C2 均为审批人设定(APPROVER_SELECT)节点 + c1Node = approverSelectNode("C1审批人设定"); + c2Node = approverSelectNode("C2审批人设定"); + + c1BranchNode = ManualBranchNode.builder() + .name("C1分支") + .blocks(c1Node) + .order(1) + .build(); + + ManualBranchNode c2BranchNode = ManualBranchNode.builder() + .name("C2分支") + .blocks(c2Node) + .order(2) + .build(); + + ManualNode manualNode = ManualNode.builder() + .name("B人工节点") + .blocks(c1BranchNode, c2BranchNode) + .build(); + + EndNode endNode = EndNode.builder().build(); + + return WorkflowBuilder.builder() + .title("请假流程") + .code("leave") + .createdOperator(user) + .form(form) + .addNode(startNode) + .addNode(manualNode) + .addNode(endNode) + .build(); + } + + private void createFlow(Workflow workflow, User user, Map data, List startActions) { + FlowCreateRequest createRequest = new FlowCreateRequest(); + createRequest.setWorkCode(workflow.getCode()); + createRequest.setFormData(data); + createRequest.setActionId(startActions.get(0).id()); + createRequest.setOperatorId(user.getUserId()); + factory.flowService.create(createRequest); + } + + private ActionResponse submitStart(List startActions, + FlowRecord record, + User user, + Map data, + String manualNodeId, + Map> operatorSelectMap) { + FlowActionRequest request = new FlowActionRequest(); + request.setFormData(data); + request.setRecordId(record.getId()); + FlowAdviceBody advice = new FlowAdviceBody(startActions.get(0).id(), "提交", user.getUserId()); + advice.setManualNodeId(manualNodeId); + advice.setOperatorSelectMap(operatorSelectMap); + request.setAdvice(advice); + return factory.flowService.action(request); + } + + private FlowRecord todoOf(User target, String message) { + List records = factory.flowRecordRepository.findTodoByOperator(target.getUserId()); + assertEquals(1, records.size(), message); + return records.get(0); + } + + private ApprovalNode approverSelectNode(String name) { + return ApprovalNode.builder() + .name(name) + .strategies(NodeStrategyBuilder.builder() + .addStrategy(readPermission()) + .addStrategy(OperatorLoadStrategy.approverSelectStrategy()) + .build()) + .build(); + } + + private FormFieldPermissionStrategy writePermission() { + return new FormFieldPermissionStrategy(FormFieldPermissionsBuilder.builder() + .addPermission("leave", "name", PermissionType.WRITE) + .addPermission("leave", "reason", PermissionType.WRITE) + .build()); + } + + private FormFieldPermissionStrategy readPermission() { + return new FormFieldPermissionStrategy(FormFieldPermissionsBuilder.builder() + .addPermission("leave", "name", PermissionType.READ) + .addPermission("leave", "reason", PermissionType.READ) + .build()); + } +} diff --git a/flow-engine-framework/src/test/java/com/codingapi/flow/service/FlowIssue260MergeRevokeTest.java b/flow-engine-framework/src/test/java/com/codingapi/flow/service/FlowIssue260MergeRevokeTest.java new file mode 100644 index 00000000..d2b91f63 --- /dev/null +++ b/flow-engine-framework/src/test/java/com/codingapi/flow/service/FlowIssue260MergeRevokeTest.java @@ -0,0 +1,219 @@ +package com.codingapi.flow.service; + +import com.codingapi.flow.action.IFlowAction; +import com.codingapi.flow.action.actions.CustomAction; +import com.codingapi.flow.builder.ActionBuilder; +import com.codingapi.flow.builder.FormFieldPermissionsBuilder; +import com.codingapi.flow.builder.NodeStrategyBuilder; +import com.codingapi.flow.context.GatewayContext; +import com.codingapi.flow.factory.MyFlowServiceFactory; +import com.codingapi.flow.form.DataType; +import com.codingapi.flow.form.FlowForm; +import com.codingapi.flow.form.FlowFormBuilder; +import com.codingapi.flow.form.permission.PermissionType; +import com.codingapi.flow.node.nodes.ApprovalNode; +import com.codingapi.flow.node.nodes.EndNode; +import com.codingapi.flow.node.nodes.StartNode; +import com.codingapi.flow.pojo.body.FlowAdviceBody; +import com.codingapi.flow.pojo.request.FlowActionRequest; +import com.codingapi.flow.pojo.request.FlowCreateRequest; +import com.codingapi.flow.pojo.request.FlowRevokeRequest; +import com.codingapi.flow.record.FlowRecord; +import com.codingapi.flow.record.FlowTodoRecord; +import com.codingapi.flow.script.factory.FlowGroovyScriptFactory; +import com.codingapi.flow.strategy.node.FormFieldPermissionStrategy; +import com.codingapi.flow.strategy.node.MultiOperatorAuditStrategy; +import com.codingapi.flow.strategy.node.OperatorLoadStrategy; +import com.codingapi.flow.strategy.node.RecordMergeStrategy; +import com.codingapi.flow.strategy.node.RevokeStrategy; +import com.codingapi.flow.user.User; +import com.codingapi.flow.workflow.Workflow; +import com.codingapi.flow.workflow.WorkflowBuilder; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * issue #260:合并审批流程由开始节点人员撤回流程时,同组其他流程的合并待办被一并删除。 + * + *

流程 A-B-D,B 为多人审批(或签,审批人 b1、b2)且开启记录合并。 + * a1、a2 两条流程在 B 节点合并为 b1、b2 各自的合并待办(组内各 2 条记录)。

+ * + *

期望:a1 撤回后,b1、b2 的合并待办仅移除 a1 的记录、保留 a2; + * a1 重新提交后,合并待办恢复为 2 条。

+ */ +public class FlowIssue260MergeRevokeTest { + + private final MyFlowServiceFactory factory = new MyFlowServiceFactory(); + + @Test + void revokeOneProcessOfMergedTodo_shouldKeepRemainingProcessAndRestoreAfterResubmit() { + // given + User a1 = new User(11, "a1"); + User a2 = new User(12, "a2"); + User b1 = new User(2, "b1"); + User b2 = new User(3, "b2"); + List.of(a1, a2, b1, b2).forEach(factory.userGateway::save); + GatewayContext.getInstance().setFlowOperatorGateway(factory.userGateway); + + Workflow workflow = buildWorkflow(a1, "issue260"); + factory.workflowService.saveWorkflow(workflow); + String bNodeId = bNodeId(workflow); + + Map data = Map.of("name", "lorne"); + + // a2 先提交、a1 后提交:合并待办的代表记录为 a1 的 B 记录,与问题现场一致 + createAndSubmit(a2, workflow, data); + long a1StartRecordId = createAndSubmit(a1, workflow, data); + + for (User approver : List.of(b1, b2)) { + FlowTodoRecord group = singleGroupOf(approver, bNodeId); + assertEquals(2, group.getMargeCount(), approver.getName() + " 的合并组应有 a1、a2 两条记录"); + assertEquals(a1.getUserId(), factory.flowRecordRepository.get(group.getRecordId()).getCreateOperatorId(), + "合并待办代表记录应为最后提交的 a1"); + } + + // when a1 撤回自己的流程 + factory.flowService.revoke(new FlowRevokeRequest(a1StartRecordId, a1.getUserId())); + + // then b1、b2 的合并待办应保留 a2,且代表记录仍指向未办理的待办 + for (User approver : List.of(b1, b2)) { + FlowTodoRecord group = singleGroupOf(approver, bNodeId); + assertEquals(1, group.getMargeCount(), approver.getName() + " 的合并组应仅剩 a2 一条记录"); + FlowRecord representative = factory.flowRecordRepository.get(group.getRecordId()); + assertNotNull(representative, approver.getName() + " 的合并代表记录应存在"); + assertEquals(a2.getUserId(), representative.getCreateOperatorId(), + approver.getName() + " 的合并代表记录应指向仍待办的 a2"); + assertTrue(representative.isTodo(), + approver.getName() + " 的合并代表记录应仍为待办,实际:" + representative.getId()); + assertEquals(1, countTodoRecords(approver, bNodeId), + approver.getName() + " 在 B 节点应仅剩 1 条待办记录"); + assertEquals(1, factory.flowTodoMergeRepository.findByTodoId(group.getId()).size(), + "合并关系应仅剩 a2 一条"); + } + // 已撤销记录的合并关系必须被清理,不能残留指向已删除待办的孤儿关系 + assertEquals(2, factory.flowTodoMergeRepository.findAll().size(), + "合并关系总数应为 b1、b2 各 1 条"); + + // when a1 重新提交 + resubmit(a1, workflow, data, a1StartRecordId); + + // then 合并待办恢复为两条 + for (User approver : List.of(b1, b2)) { + FlowTodoRecord group = singleGroupOf(approver, bNodeId); + assertEquals(2, group.getMargeCount(), approver.getName() + " 的合并组应恢复为 2 条记录"); + assertEquals(2, countTodoRecords(approver, bNodeId), + approver.getName() + " 在 B 节点应恢复为 2 条待办记录"); + } + assertEquals(2, factory.flowTodoRecordRepository.findAll().size(), + "b1、b2 应各保留 1 条合并待办组"); + } + + // ─── 辅助方法 ─── + + private FlowTodoRecord singleGroupOf(User approver, String nodeId) { + List groups = factory.flowTodoRecordRepository.findByOperatorId(approver.getUserId()).stream() + .filter(group -> nodeId.equals(group.getNodeId())) + .toList(); + assertEquals(1, groups.size(), approver.getName() + " 在合并节点应有且仅有 1 条合并待办组"); + return groups.get(0); + } + + private long countTodoRecords(User approver, String nodeId) { + return factory.flowRecordRepository.findTodoByOperator(approver.getUserId()).stream() + .filter(record -> nodeId.equals(record.getNodeId())) + .count(); + } + + private String bNodeId(Workflow workflow) { + return workflow.getNodes().stream() + .filter(node -> node.getName().equals("B审批")) + .findFirst().orElseThrow().getId(); + } + + private Workflow buildWorkflow(User createdBy, String code) { + FlowForm form = FlowFormBuilder.builder() + .name("测试流程") + .code(code) + .addField("name", "name", DataType.STRING) + .build(); + + StartNode startNode = StartNode.builder() + .strategies(NodeStrategyBuilder.builder() + .addStrategy(new FormFieldPermissionStrategy(FormFieldPermissionsBuilder.builder() + .addPermission(code, "name", PermissionType.WRITE) + .build())) + .addStrategy(new RevokeStrategy(true, RevokeStrategy.Type.REVOKE_CURRENT)) + .build()) + .actions(ActionBuilder.builder() + .addAction(CustomAction.defaultAction()) + .build()) + .build(); + + ApprovalNode bNode = ApprovalNode.builder() + .name("B审批") + .strategies(NodeStrategyBuilder.builder() + .addStrategy(new FormFieldPermissionStrategy(FormFieldPermissionsBuilder.builder() + .addPermission(code, "name", PermissionType.WRITE) + .build())) + .addStrategy(new OperatorLoadStrategy(FlowGroovyScriptFactory + .createOperatorLoadScript("def run(request){return [2,3]}").getKey())) + // 或签:b1、b2 各自持有待办记录,两条流程在各自审批人下分别合并 + .addStrategy(new MultiOperatorAuditStrategy(MultiOperatorAuditStrategy.Type.ANY, 0f)) + .addStrategy(new RecordMergeStrategy(true)) + .build()) + .build(); + + EndNode endNode = EndNode.builder().build(); + + return WorkflowBuilder.builder() + .title("测试流程") + .code(code) + .createdOperator(createdBy) + .form(form) + .addNode(startNode) + .addNode(bNode) + .addNode(endNode) + .build(); + } + + /** + * 发起并提交到 B 节点,返回发起节点记录id + */ + private long createAndSubmit(User user, Workflow workflow, Map data) { + StartNode startNode = (StartNode) workflow.getStartNode(); + IFlowAction startAction = startNode.actionManager().getActions().get(0); + + FlowCreateRequest createRequest = new FlowCreateRequest(); + createRequest.setWorkCode(workflow.getCode()); + createRequest.setFormData(data); + createRequest.setActionId(startAction.id()); + createRequest.setOperatorId(user.getUserId()); + long recordId = factory.flowService.create(createRequest); + + submit(startAction, user, data, recordId); + return recordId; + } + + /** + * 撤回后发起记录回到待办,重新提交 + */ + private void resubmit(User user, Workflow workflow, Map data, long startRecordId) { + StartNode startNode = (StartNode) workflow.getStartNode(); + IFlowAction startAction = startNode.actionManager().getActions().get(0); + submit(startAction, user, data, startRecordId); + } + + private void submit(IFlowAction action, User user, Map data, long recordId) { + FlowActionRequest actionRequest = new FlowActionRequest(); + actionRequest.setFormData(data); + actionRequest.setRecordId(recordId); + actionRequest.setAdvice(new FlowAdviceBody(action.id(), "同意", user.getUserId())); + factory.flowService.action(actionRequest); + } +}