From b25c7eaa1ea5963c1e45a1879f20b5bd97c33284 Mon Sep 17 00:00:00 2001 From: weicw <594098497@qq.com> Date: Sun, 1 Nov 2026 15:59:14 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=92=A4=E6=8E=89=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E7=8E=AF=E8=8A=82=E5=BC=80=E5=85=B3=EF=BC=8C=E9=99=84?= =?UTF-8?q?=E4=BB=B6=E8=A6=81=E6=B1=82=E5=8F=AA=E6=8C=89=E8=8A=82=E7=82=B9?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=EF=BC=88ADR=200016=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../approval/constant/ApprovalConstants.java | 3 -- .../service/ApprovalDocumentService.java | 29 +++----------- .../service/ApprovalEngineService.java | 6 +-- .../service/ApprovalInstanceService.java | 5 +-- .../service/ApprovalNodeValidator.java | 23 +---------- .../approval/support/ApprovalNode.java | 15 +------ .../approval/ApprovalAttachmentConfigIT.java | 39 +++++++------------ 7 files changed, 27 insertions(+), 93 deletions(-) diff --git a/src/main/java/com/gxwebsoft/approval/constant/ApprovalConstants.java b/src/main/java/com/gxwebsoft/approval/constant/ApprovalConstants.java index 71a0330..335f79f 100644 --- a/src/main/java/com/gxwebsoft/approval/constant/ApprovalConstants.java +++ b/src/main/java/com/gxwebsoft/approval/constant/ApprovalConstants.java @@ -40,9 +40,6 @@ public final class ApprovalConstants { public static final String NODE_CONFIRM = "CONFIRM"; public static final String NODE_ARCHIVE = "ARCHIVE"; - /** 提交环节:链首只读节点,不产生待办(ADR 0016)。 */ - public static final String NODE_SUBMIT = "SUBMIT"; - // 动作 public static final String ACTION_SUBMIT = "SUBMIT"; public static final String ACTION_RESUBMIT = "RESUBMIT"; diff --git a/src/main/java/com/gxwebsoft/approval/service/ApprovalDocumentService.java b/src/main/java/com/gxwebsoft/approval/service/ApprovalDocumentService.java index 387e2f0..d2d0cc8 100644 --- a/src/main/java/com/gxwebsoft/approval/service/ApprovalDocumentService.java +++ b/src/main/java/com/gxwebsoft/approval/service/ApprovalDocumentService.java @@ -77,16 +77,6 @@ public class ApprovalDocumentService { createDraft(type, param, user, definition, organizationId, organizationName, prepared)); } - /** 链首提交环节是否要求附件;没有提交环节时沿用代码声明(ADR 0016)。 */ - private boolean submitAttachmentRequired(List nodes) { - for (ApprovalNode node : nodes) { - if (ApprovalConstants.NODE_SUBMIT.equals(node.getType())) { - return node.isAttachmentRequired(); - } - } - return true; - } - /** * 提交预检:按单据声明校验必传附件分类,再逐节点逐组解析审批人(ADR 0003/0005)。 * @@ -95,21 +85,14 @@ public class ApprovalDocumentService { public void preflight(ApprovalInstance instance, List nodes) { ApprovalDocumentType type = documentRegistry.find(instance.getFlowCode()); String requiredCategory = type == null ? null : type.requiredAttachmentCategory(); - // 链首提交环节可关闭整单必传(ADR 0016);没有提交环节的存量链沿用代码声明 - if (submitAttachmentRequired(nodes)) { - if (requiredCategory != null) { - if (attachmentService.countCategory(instance.getId(), requiredCategory) == 0) { - throw new BusinessException("请至少上传一张" + ApprovalConstants.categoryName(requiredCategory)); - } - } else if (attachmentService.count(instance.getId()) == 0) { - throw new BusinessException("请至少上传一张附件"); + if (requiredCategory != null) { + if (attachmentService.countCategory(instance.getId(), requiredCategory) == 0) { + throw new BusinessException("请至少上传一张" + ApprovalConstants.categoryName(requiredCategory)); } + } else if (attachmentService.count(instance.getId()) == 0) { + throw new BusinessException("请至少上传一张附件"); } - // 提交环节不产生待办,解析审批人时跳过(ADR 0016) - List processable = nodes.stream() - .filter(node -> !ApprovalConstants.NODE_SUBMIT.equals(node.getType())) - .collect(java.util.stream.Collectors.toList()); - String missing = approverResolver.preflight(instance, processable); + String missing = approverResolver.preflight(instance, nodes); if (missing != null) { throw new BusinessException(missing); } diff --git a/src/main/java/com/gxwebsoft/approval/service/ApprovalEngineService.java b/src/main/java/com/gxwebsoft/approval/service/ApprovalEngineService.java index 933bef5..4d372c7 100644 --- a/src/main/java/com/gxwebsoft/approval/service/ApprovalEngineService.java +++ b/src/main/java/com/gxwebsoft/approval/service/ApprovalEngineService.java @@ -49,12 +49,10 @@ public class ApprovalEngineService { public int startFlow(ApprovalInstance instance, List approvers, User operator, String action) { List nodes = flowService.parseNodes(instance.getDefinitionId()); - // 链首的提交环节不产生待办(ADR 0016) - ApprovalNode first = ApprovalNode.firstProcessable(nodes); - if (first == null) { + if (nodes.isEmpty()) { throw new BusinessException("审批流程未配置节点"); } - return activateNode(instance, first, approvers, operator, action, null); + return activateNode(instance, nodes.get(0), approvers, operator, action, null); } /** diff --git a/src/main/java/com/gxwebsoft/approval/service/ApprovalInstanceService.java b/src/main/java/com/gxwebsoft/approval/service/ApprovalInstanceService.java index a78f0d2..6286a65 100644 --- a/src/main/java/com/gxwebsoft/approval/service/ApprovalInstanceService.java +++ b/src/main/java/com/gxwebsoft/approval/service/ApprovalInstanceService.java @@ -150,13 +150,12 @@ public class ApprovalInstanceService { List nodes = flowService.parseNodes(instance.getDefinitionId()); // 附件分类与逐节点逐组审批人解析(ADR 0003 / 0005),必须在事务外完成 documentService.preflight(instance, nodes); - ApprovalNode firstProcessable = ApprovalNode.firstProcessable(nodes); - if (firstProcessable == null) { + if (nodes.isEmpty()) { throw new BusinessException("审批流程未配置节点"); } // 首节点审批人同样在事务外解析:TransactionTemplate 内 @DS("server") 不生效 ApprovalApproverResolver.Resolution firstNode = - approverResolver.resolve(instance, firstProcessable); + approverResolver.resolve(instance, nodes.get(0)); return inTransaction(() -> doSubmit(instanceId, user, organization, resolvedAcceptOrg, type, firstNode)); diff --git a/src/main/java/com/gxwebsoft/approval/service/ApprovalNodeValidator.java b/src/main/java/com/gxwebsoft/approval/service/ApprovalNodeValidator.java index cf20185..3461711 100644 --- a/src/main/java/com/gxwebsoft/approval/service/ApprovalNodeValidator.java +++ b/src/main/java/com/gxwebsoft/approval/service/ApprovalNodeValidator.java @@ -40,8 +40,7 @@ public class ApprovalNodeValidator { ApprovalConstants.NODE_COUNTERSIGN, ApprovalConstants.NODE_DISPATCH, ApprovalConstants.NODE_CONFIRM, - ApprovalConstants.NODE_ARCHIVE, - ApprovalConstants.NODE_SUBMIT); + ApprovalConstants.NODE_ARCHIVE); /** 按固定组织编码取负责人的解析器,必须配 orgCode。 */ private static final List ORG_RESOLVERS = List.of( @@ -193,9 +192,7 @@ public class ApprovalNodeValidator { if (ApprovalConstants.NODE_DISPATCH.equals(nodeType)) { dispatchCount++; } - if (ApprovalConstants.NODE_SUBMIT.equals(nodeType)) { - checkSubmitNode(where, i, node); - } else if (ApprovalConstants.NODE_COUNTERSIGN.equals(nodeType)) { + if (ApprovalConstants.NODE_COUNTERSIGN.equals(nodeType)) { checkGroups(where, node); } else { if (node.getGroups() != null && !node.getGroups().isEmpty()) { @@ -225,22 +222,6 @@ public class ApprovalNodeValidator { } } - /** 提交环节只能位于链首,且不配审批人、会签组与回填字段(ADR 0016)。 */ - private void checkSubmitNode(String where, int index, ApprovalNode node) { - if (index != 0) { - throw new BusinessException("提交环节只能位于流程首位"); - } - if (!isBlank(node.getResolver()) || !isBlank(node.getOrgCode())) { - throw new BusinessException("提交环节不需要配置审批人解析器"); - } - if (node.getGroups() != null && !node.getGroups().isEmpty()) { - throw new BusinessException("提交环节不能配置会签组"); - } - if (node.getWritableFields() != null && !node.getWritableFields().isEmpty()) { - throw new BusinessException("提交环节不能配置回填字段"); - } - } - private void checkGroups(String where, ApprovalNode node) { List groups = node.getGroups(); if (groups == null || groups.isEmpty()) { diff --git a/src/main/java/com/gxwebsoft/approval/support/ApprovalNode.java b/src/main/java/com/gxwebsoft/approval/support/ApprovalNode.java index 1307c48..d6a0eb7 100644 --- a/src/main/java/com/gxwebsoft/approval/support/ApprovalNode.java +++ b/src/main/java/com/gxwebsoft/approval/support/ApprovalNode.java @@ -44,23 +44,10 @@ public class ApprovalNode { /** * 处理人通过前是否必须本次新传附件(ADR 0016)。 * - *

SUBMIT 节点上表示整单提交时是否必须上传附件;缺省 false,存量链不受影响。

+ *

缺省 false;提交时的必传仍由单据实现的 requiredAttachmentCategory 决定。

*/ private boolean attachmentRequired; - /** 链首的提交环节不产生待办,取首个可处理节点时跳过它。 */ - public static ApprovalNode firstProcessable(List nodes) { - if (nodes == null) { - return null; - } - for (ApprovalNode node : nodes) { - if (!com.gxwebsoft.approval.constant.ApprovalConstants.NODE_SUBMIT.equals(node.getType())) { - return node; - } - } - return null; - } - /** 会签组定义:组标识、显示名、解析器与可选固定组织编码。 */ @Data @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/src/test/java/com/gxwebsoft/approval/ApprovalAttachmentConfigIT.java b/src/test/java/com/gxwebsoft/approval/ApprovalAttachmentConfigIT.java index 982490c..07b42af 100644 --- a/src/test/java/com/gxwebsoft/approval/ApprovalAttachmentConfigIT.java +++ b/src/test/java/com/gxwebsoft/approval/ApprovalAttachmentConfigIT.java @@ -30,7 +30,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** - * 附件要求可配置(ADR 0016):提交环节关闭必传、提交环节要求附件、审批节点要求本次新传。 + * 节点级附件要求(ADR 0016):审批节点要求本次新传、补传后放行;提交必传仍是代码声明。 */ @SpringBootTest @AutoConfigureMockMvc @@ -105,28 +105,25 @@ class ApprovalAttachmentConfigIT { } @Test - void submitWithoutAttachmentPassesWhenSubmitStageAllows() throws Exception { - publish(chain(false, false)); + void submitStillRequiresAttachmentFromCode() throws Exception { + publish(chain(false)); long instanceId = createDraft(); - submit(instanceId).andExpect(jsonPath("$.code").value(0)); - } - - @Test - void submitWithoutAttachmentBlockedWhenSubmitStageRequires() throws Exception { - publish(chain(true, false)); - long instanceId = createDraft(); - submit(instanceId) + mockMvc.perform(authed(post("/api/approval/instance/" + instanceId + "/submit"), reporter, SUBMIT)) + .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(1)) .andExpect(jsonPath("$.message").value(containsString("请至少上传一张"))); } @Test void approverMustUploadOnNodeThatRequiresIt() throws Exception { - publish(chain(false, true)); + publish(chain(true)); long instanceId = createDraft(); - submit(instanceId).andExpect(jsonPath("$.code").value(0)); + uploadAs(reporter, instanceId); + mockMvc.perform(authed(post("/api/approval/instance/" + instanceId + "/submit"), reporter, SUBMIT)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(0)); - // 未上传:节点拦住 + // 未新传:节点拦住 mockMvc.perform(authed(post("/api/approval/task/" + pendingTaskId(instanceId, leader.userId) + "/approve"), leader, APPROVE) .contentType(MediaType.APPLICATION_JSON) @@ -135,7 +132,7 @@ class ApprovalAttachmentConfigIT { .andExpect(jsonPath("$.code").value(1)) .andExpect(jsonPath("$.message").value("本节点处理前请先上传附件")); - // 处理人补传后可继续 + // 处理人补传后放行 uploadAs(leader, instanceId); mockMvc.perform(authed(post("/api/approval/task/" + pendingTaskId(instanceId, leader.userId) + "/approve"), leader, APPROVE) @@ -145,10 +142,8 @@ class ApprovalAttachmentConfigIT { .andExpect(jsonPath("$.code").value(0)); } - private String chain(boolean submitRequired, boolean leaderRequired) { - return "[{\"key\":\"submit\",\"type\":\"SUBMIT\",\"name\":\"提交\"," - + "\"attachmentRequired\":" + submitRequired + "}," - + "{\"key\":\"deptLeader\",\"type\":\"APPROVE\",\"name\":\"部门负责人\"," + private String chain(boolean leaderRequired) { + return "[{\"key\":\"deptLeader\",\"type\":\"APPROVE\",\"name\":\"部门负责人\"," + "\"resolver\":\"reporterOrgLeader\",\"attachmentRequired\":" + leaderRequired + "}," + "{\"key\":\"confirm\",\"type\":\"CONFIRM\",\"name\":\"存档\"," + "\"resolver\":\"reporter\"}," @@ -180,12 +175,6 @@ class ApprovalAttachmentConfigIT { return instanceId; } - private org.springframework.test.web.servlet.ResultActions submit(long instanceId) throws Exception { - return mockMvc.perform(authed(post("/api/approval/instance/" + instanceId + "/submit"), - reporter, SUBMIT)) - .andExpect(status().isOk()); - } - private void uploadAs(ApprovalTestData.TestUser user, long instanceId) throws Exception { MockMultipartFile file = new MockMultipartFile("file", "voucher.png", "image/png", new byte[]{(byte) 0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A});