修正流程配置校验:存档节点可以按组确认(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);
|
||||
} else if (ApprovalConstants.NODE_COUNTERSIGN.equals(nodeType)) {
|
||||
checkGroups(where, node);
|
||||
} else if (hasGroups(node) && ApprovalConstants.NODE_CONFIRM.equals(nodeType)) {
|
||||
checkConfirmGroups(where, node);
|
||||
} else {
|
||||
if (node.getGroups() != null && !node.getGroups().isEmpty()) {
|
||||
throw new BusinessException(where + "只有会签节点可以配置会签组");
|
||||
if (hasGroups(node)) {
|
||||
throw new BusinessException(where + "只有会签节点和存档节点可以配置会签组");
|
||||
}
|
||||
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) {
|
||||
List<ApprovalNode.ApprovalGroup> groups = node.getGroups();
|
||||
if (groups == null || groups.isEmpty()) {
|
||||
|
||||
@@ -144,6 +144,36 @@ class ApprovalFlowConfigIT {
|
||||
.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
|
||||
void listPermissionAloneCannotPublish() throws Exception {
|
||||
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