修正流程配置校验:存档节点可以按组确认(ADR 0015)
收据申请单与设施价格表的「存档」节点用 groups 表达「报送人 + 部门负责人 全部确认」,而校验器只允许会签节点带组,于是后台打开流程配置、什么都不改 点保存就报「第 x 个节点只有会签节点可以配置会签组」。 - 放行 CONFIRM 带组,组声明复用会签组那套校验;带组时不许再配单解析器 (ApprovalApproverResolver 里 groups 优先,否则配置被静默忽略) - 其余节点类型带组仍拒绝,提示改为「只有会签节点和存档节点可以配置会签组」 - 新增 ApprovalFlowTemplateIT:逐个流程校验代码里的出厂模板,模板与校验器 漂移会在集成测试里暴露 - ApprovalFlowConfigIT 补两条:存档节点带组可发布、审批节点带组被拒
This commit is contained in:
@@ -202,9 +202,11 @@ public class ApprovalNodeValidator {
|
|||||||
checkSubmitNode(where, i, node);
|
checkSubmitNode(where, i, node);
|
||||||
} else if (ApprovalConstants.NODE_COUNTERSIGN.equals(nodeType)) {
|
} else if (ApprovalConstants.NODE_COUNTERSIGN.equals(nodeType)) {
|
||||||
checkGroups(where, node);
|
checkGroups(where, node);
|
||||||
|
} else if (hasGroups(node) && ApprovalConstants.NODE_CONFIRM.equals(nodeType)) {
|
||||||
|
checkConfirmGroups(where, node);
|
||||||
} else {
|
} else {
|
||||||
if (node.getGroups() != null && !node.getGroups().isEmpty()) {
|
if (hasGroups(node)) {
|
||||||
throw new BusinessException(where + "只有会签节点可以配置会签组");
|
throw new BusinessException(where + "只有会签节点和存档节点可以配置会签组");
|
||||||
}
|
}
|
||||||
checkResolver(where, node.getResolver(), node.getOrgCode());
|
checkResolver(where, node.getResolver(), node.getOrgCode());
|
||||||
}
|
}
|
||||||
@@ -246,6 +248,21 @@ public class ApprovalNodeValidator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 存档节点也可以按组确认:多组全部确认后节点才前进({@link ApprovalApproverResolver}
|
||||||
|
* 里 groups 优先于单解析器,两处同口径)。组声明本身与会签组同构,复用 {@link #checkGroups}。
|
||||||
|
*/
|
||||||
|
private void checkConfirmGroups(String where, ApprovalNode node) {
|
||||||
|
if (!isBlank(node.getResolver()) || !isBlank(node.getOrgCode())) {
|
||||||
|
throw new BusinessException(where + "按组确认时不要再配置审批人解析器");
|
||||||
|
}
|
||||||
|
checkGroups(where, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasGroups(ApprovalNode node) {
|
||||||
|
return node.getGroups() != null && !node.getGroups().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
private void checkGroups(String where, ApprovalNode node) {
|
private void checkGroups(String where, ApprovalNode node) {
|
||||||
List<ApprovalNode.ApprovalGroup> groups = node.getGroups();
|
List<ApprovalNode.ApprovalGroup> groups = node.getGroups();
|
||||||
if (groups == null || groups.isEmpty()) {
|
if (groups == null || groups.isEmpty()) {
|
||||||
|
|||||||
@@ -144,6 +144,36 @@ class ApprovalFlowConfigIT {
|
|||||||
.andExpect(jsonPath("$.message").value("流程最后一个节点必须是归档节点"));
|
.andExpect(jsonPath("$.message").value("流程最后一个节点必须是归档节点"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void confirmNodeWithGroupsIsAccepted() throws Exception {
|
||||||
|
// 收据单、价格表的「存档」节点就是按组确认(报送人 + 部门负责人),保存不能被拦下
|
||||||
|
String nodes = "[{\"key\":\"deptLeader\",\"type\":\"APPROVE\",\"name\":\"部门负责人\","
|
||||||
|
+ "\"resolver\":\"reporterOrgLeader\"},"
|
||||||
|
+ "{\"key\":\"confirm\",\"type\":\"CONFIRM\",\"name\":\"存档\",\"groups\":["
|
||||||
|
+ "{\"key\":\"reporter\",\"name\":\"报送人\",\"resolver\":\"reporter\"},"
|
||||||
|
+ "{\"key\":\"finance\",\"name\":\"财务部负责人\",\"resolver\":\"orgLeaderByCode\","
|
||||||
|
+ "\"orgCode\":\"APPROVAL_FINANCE\"}]},"
|
||||||
|
+ "{\"key\":\"archive\",\"type\":\"ARCHIVE\",\"name\":\"归档\","
|
||||||
|
+ "\"resolver\":\"archivist\"}]";
|
||||||
|
publish(nodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void groupsAreRejectedOnOtherNodeTypes() throws Exception {
|
||||||
|
String nodes = "[{\"key\":\"deptLeader\",\"type\":\"APPROVE\",\"name\":\"部门负责人\","
|
||||||
|
+ "\"groups\":[{\"key\":\"g1\",\"name\":\"会签组1\",\"resolver\":\"reporter\"}]},"
|
||||||
|
+ "{\"key\":\"archive\",\"type\":\"ARCHIVE\",\"name\":\"归档\","
|
||||||
|
+ "\"resolver\":\"archivist\"}]";
|
||||||
|
mockMvc.perform(post("/api/approval/flow/publish")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(body(nodes))
|
||||||
|
.header("tenantId", TENANT)
|
||||||
|
.with(authentication(TestAuthentication.admin(LIST, PUBLISH))))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(1))
|
||||||
|
.andExpect(jsonPath("$.message").value("第 1 个节点只有会签节点和存档节点可以配置会签组"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void listPermissionAloneCannotPublish() throws Exception {
|
void listPermissionAloneCannotPublish() throws Exception {
|
||||||
mockMvc.perform(post("/api/approval/flow/publish")
|
mockMvc.perform(post("/api/approval/flow/publish")
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package com.gxwebsoft.approval;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.gxwebsoft.approval.document.ApprovalDocumentRegistry;
|
||||||
|
import com.gxwebsoft.approval.document.ApprovalDocumentType;
|
||||||
|
import com.gxwebsoft.approval.service.ApprovalNodeValidator;
|
||||||
|
import com.gxwebsoft.approval.support.CountersignProbeDocumentType;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 出厂模板自检(ADR 0015 / 0016):每个已注册流程代码里声明的节点链都必须过节点校验。
|
||||||
|
*
|
||||||
|
* <p>模板与校验器漂移的代价很直接:后台打开流程配置、什么都不改点保存就报错
|
||||||
|
* (存档节点按组确认曾经被「只有会签节点可以配置会签组」拦下)。这里按流程逐个校验,
|
||||||
|
* 新增单据时模板写错也会在集成测试里暴露。</p>
|
||||||
|
*/
|
||||||
|
@SpringBootTest
|
||||||
|
@ActiveProfiles({"gxtyzx-test", "test"})
|
||||||
|
class ApprovalFlowTemplateIT {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ApprovalDocumentRegistry documentRegistry;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ApprovalNodeValidator nodeValidator;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void factoryTemplatesPassValidation() throws Exception {
|
||||||
|
List<ApprovalDocumentType> types = documentRegistry.all();
|
||||||
|
assertFalse(types.isEmpty());
|
||||||
|
for (ApprovalDocumentType type : types) {
|
||||||
|
// 引擎探针是测试专用单据,节点 JSON 随用例动态拼装,不适用静态模板校验
|
||||||
|
if (CountersignProbeDocumentType.FLOW_CODE.equals(type.flowCode())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
JsonNode nodes = objectMapper.readTree(type.nodesJson());
|
||||||
|
assertDoesNotThrow(() -> nodeValidator.validate(type.flowCode(), nodes),
|
||||||
|
type.flowName() + " 的出厂模板未通过节点校验");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user