test: 会签与回填引擎级用例(#47/#49);缺配按组名报错、通过时校验必填可写字段
This commit is contained in:
@@ -0,0 +1,387 @@
|
||||
package com.gxwebsoft.approval;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.gxwebsoft.approval.constant.ApprovalConstants;
|
||||
import com.gxwebsoft.approval.support.CountersignProbeDocumentType;
|
||||
import com.gxwebsoft.support.ApprovalTestData;
|
||||
import com.gxwebsoft.support.TestAuthentication;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.ResultActions;
|
||||
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* 引擎级用例:会签分组(#47)与节点回填(#49)。
|
||||
*
|
||||
* <p>用测试专用探针单据 {@link CountersignProbeDocumentType},会签组按本次用例新建的
|
||||
* 组织定位负责人角色,避免共享测试库里常驻账号干扰「某组缺配」的构造。</p>
|
||||
*/
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles({"gxtyzx-test", "test"})
|
||||
class ApprovalCountersignEngineIT {
|
||||
|
||||
private static final int TENANT = TestAuthentication.TENANT_ID;
|
||||
private static final String LIST = "approval:instance:list";
|
||||
private static final String SUBMIT = "approval:instance:submit";
|
||||
private static final String APPROVE = "approval:instance:approve";
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
@Autowired
|
||||
private DataSource dataSource;
|
||||
|
||||
private ApprovalTestData data;
|
||||
private ApprovalTestData.TestUser reporter;
|
||||
private Integer organizationId;
|
||||
private ApprovalTestData.TestUser leader;
|
||||
|
||||
/** 四个会签组:组织 + 负责人,按顺序。 */
|
||||
private final List<Integer> groupOrgs = new ArrayList<>();
|
||||
private final List<ApprovalTestData.TestUser> groupLeaders = new ArrayList<>();
|
||||
private final List<String> groupNames = List.of("场馆部", "运营管理部", "安全运保部", "财务部");
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
data = new ApprovalTestData(dataSource);
|
||||
String suffix = data.uniqueSuffix();
|
||||
organizationId = data.createOrganization("会签报送部门" + suffix, 136);
|
||||
reporter = data.createUser("会签报送人" + suffix, organizationId);
|
||||
|
||||
ApprovalTestData.TestRole leaderRole =
|
||||
data.createRole(data.leaderRoleCode(organizationId), "部门负责人");
|
||||
leader = data.createUser("会签负责人" + suffix, organizationId);
|
||||
data.bind(leader.userId, leaderRole.roleId);
|
||||
|
||||
ApprovalTestData.TestRole archivistRole =
|
||||
data.createRole(ApprovalConstants.ROLE_ARCHIVIST, "收文员");
|
||||
ApprovalTestData.TestUser archivist = data.createUser("会签收文员" + suffix, organizationId);
|
||||
data.bind(archivist.userId, archivistRole.roleId);
|
||||
|
||||
List<String> codes = new ArrayList<>();
|
||||
for (int i = 0; i < groupNames.size(); i++) {
|
||||
Integer org = data.createOrganization("会签组" + (i + 1) + suffix, 136);
|
||||
ApprovalTestData.TestRole role = data.createRole(data.leaderRoleCode(org), "负责人" + (i + 1));
|
||||
ApprovalTestData.TestUser member = data.createUser("会签人" + (i + 1) + suffix, org);
|
||||
data.bind(member.userId, role.roleId);
|
||||
groupOrgs.add(org);
|
||||
groupLeaders.add(member);
|
||||
codes.add(data.organizationCode(org));
|
||||
}
|
||||
// 探针流程定义要按用例重建:旧定义里的组织编码指向上一个用例已删除的组织
|
||||
data.master().update("DELETE FROM approval_flow_definition WHERE tenant_id = ? AND flow_code = ?",
|
||||
TENANT, CountersignProbeDocumentType.FLOW_CODE);
|
||||
CountersignProbeDocumentType.configureGroups(codes, groupNames);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
data.cleanup();
|
||||
}
|
||||
|
||||
@Test
|
||||
void activatesAllFourGroupsAndAdvancesOnlyAfterEveryGroupSigns() throws Exception {
|
||||
long instanceId = createAndSubmit();
|
||||
|
||||
assertThat(countersignTaskCount(instanceId)).isEqualTo(4);
|
||||
assertThat(data.queryMaster("SELECT COUNT(DISTINCT group_key) FROM approval_task "
|
||||
+ "WHERE instance_id = ? AND node_key = 'countersign'", Integer.class, instanceId)).isEqualTo(4);
|
||||
|
||||
mockMvc.perform(authed(get("/api/approval/instance/" + instanceId), reporter, LIST))
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.currentNodeName").value("四部门会签"))
|
||||
.andExpect(jsonPath("$.data.document.groupProgress.length()").value(4))
|
||||
.andExpect(jsonPath("$.data.document.groupProgress[0].groupSeq").value(1))
|
||||
.andExpect(jsonPath("$.data.document.groupProgress[0].signed").value(0))
|
||||
.andExpect(jsonPath("$.data.document.groupProgress[0].total").value(1));
|
||||
|
||||
approve(taskFor(instanceId, groupLeaders.get(0))).andExpect(jsonPath("$.code").value(0));
|
||||
assertThat(instanceStatus(instanceId)).isEqualTo("PENDING");
|
||||
assertThat(currentNodeKey(instanceId)).isEqualTo("countersign");
|
||||
assertThat(countersignPendingCount(instanceId)).isEqualTo(3);
|
||||
|
||||
approve(taskFor(instanceId, groupLeaders.get(1))).andExpect(jsonPath("$.code").value(0));
|
||||
approve(taskFor(instanceId, groupLeaders.get(2))).andExpect(jsonPath("$.code").value(0));
|
||||
assertThat(countersignPendingCount(instanceId)).isEqualTo(1);
|
||||
|
||||
approve(taskFor(instanceId, groupLeaders.get(3)))
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.currentNodeName").value("存档"));
|
||||
assertThat(currentNodeKey(instanceId)).isEqualTo("confirm");
|
||||
}
|
||||
|
||||
@Test
|
||||
void onePersonInTwoGroupsCompletesBothWithSingleClick() throws Exception {
|
||||
// 让第一组负责人同时挂上第二组的部门负责人角色:他会拿到两条会签任务
|
||||
Integer secondRoleId = data.server().queryForObject(
|
||||
"SELECT role_id FROM sys_role WHERE tenant_id = ? AND deleted = 0 AND role_code = ?",
|
||||
Integer.class, TENANT, data.leaderRoleCode(groupOrgs.get(1)));
|
||||
data.bind(groupLeaders.get(0).userId, secondRoleId);
|
||||
// 第二组只留这一个成员,使总任务数仍是 4,其中该用户占 2 条
|
||||
data.server().update("DELETE FROM sys_user_role WHERE user_id = ? AND role_id = ?",
|
||||
groupLeaders.get(1).userId, secondRoleId);
|
||||
|
||||
long instanceId = createAndSubmit();
|
||||
assertThat(countersignTaskCount(instanceId)).isEqualTo(4);
|
||||
|
||||
approve(taskFor(instanceId, groupLeaders.get(0))).andExpect(jsonPath("$.code").value(0));
|
||||
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_task WHERE instance_id = ? "
|
||||
+ "AND assignee_id = ? AND status = 'DONE'", instanceId, groupLeaders.get(0).userId)).isEqualTo(2);
|
||||
assertThat(currentNodeKey(instanceId)).isEqualTo("countersign");
|
||||
assertThat(countersignPendingCount(instanceId)).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void anyGroupMissingBlocksWholeNodeAndSubmitPreflightNamesTheGroup() throws Exception {
|
||||
// 第 3 组(安全运保部)清空负责人:整节点不激活,提交预检指名缺的是哪一组
|
||||
data.server().update("DELETE FROM sys_user_role WHERE user_id = ?", groupLeaders.get(2).userId);
|
||||
long draft = createDraft();
|
||||
uploadAttachment(draft);
|
||||
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + draft + "/submit"), reporter, SUBMIT))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("安全运保部")));
|
||||
|
||||
assertThat(instanceStatus(draft)).isEqualTo("DRAFT");
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_task WHERE instance_id = ?", draft)).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void anyGroupRejectEndsWholeNodeAndRaisesRound() throws Exception {
|
||||
long instanceId = createAndSubmit();
|
||||
String bizNo = data.queryMaster("SELECT biz_no FROM approval_instance WHERE id = ?",
|
||||
String.class, instanceId);
|
||||
int roundBefore = round(instanceId);
|
||||
|
||||
approve(taskFor(instanceId, groupLeaders.get(0))).andExpect(jsonPath("$.code").value(0));
|
||||
mockMvc.perform(authed(post("/api/approval/task/" + taskFor(instanceId, groupLeaders.get(1)) + "/reject"),
|
||||
groupLeaders.get(1), APPROVE)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"opinion\":\"金额对不上\"}"))
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.status").value("REJECTED"));
|
||||
|
||||
assertThat(round(instanceId)).isEqualTo(roundBefore + 1);
|
||||
assertThat(data.queryMaster("SELECT biz_no FROM approval_instance WHERE id = ?", String.class, instanceId))
|
||||
.isEqualTo(bizNo);
|
||||
// 本轮已通过的组任务保持 DONE 留痕
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_task WHERE instance_id = ? "
|
||||
+ "AND round = ? AND node_key = 'countersign' AND status = 'DONE'", instanceId, roundBefore))
|
||||
.isEqualTo(2);
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_task WHERE instance_id = ? "
|
||||
+ "AND round = ? AND node_key = 'countersign' AND status = 'PENDING'", instanceId, roundBefore))
|
||||
.isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void backfillWritesFieldWithoutAdvancingNode() throws Exception {
|
||||
long instanceId = createDraft();
|
||||
uploadAttachment(instanceId);
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + instanceId + "/submit"), reporter, SUBMIT))
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
|
||||
mockMvc.perform(authed(post("/api/approval/task/" + taskFor(instanceId, leader) + "/backfill"),
|
||||
leader, APPROVE)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"fields\":{\"probeNo\":\"P-001\"}}"))
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
|
||||
assertThat(currentNodeKey(instanceId)).isEqualTo("deptLeader");
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_field_change WHERE instance_id = ? "
|
||||
+ "AND field_key = 'probeNo' AND new_value = 'P-001'", instanceId)).isEqualTo(1);
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_record WHERE instance_id = ? "
|
||||
+ "AND action = 'BACKFILL'", instanceId)).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void approveRequiresWritableFieldBeforeAdvancing() throws Exception {
|
||||
long instanceId = createDraft();
|
||||
uploadAttachment(instanceId);
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + instanceId + "/submit"), reporter, SUBMIT))
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
|
||||
mockMvc.perform(authed(post("/api/approval/task/" + taskFor(instanceId, leader) + "/approve"),
|
||||
leader, APPROVE)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"opinion\":\"同意\"}"))
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("测试编号")));
|
||||
|
||||
assertThat(currentNodeKey(instanceId)).isEqualTo("deptLeader");
|
||||
assertThat(taskStatus(instanceId, leader.userId)).isEqualTo("PENDING");
|
||||
}
|
||||
|
||||
@Test
|
||||
void approveWithFieldAdvancesAndLocksValue() throws Exception {
|
||||
long instanceId = createDraft();
|
||||
uploadAttachment(instanceId);
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + instanceId + "/submit"), reporter, SUBMIT))
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
|
||||
mockMvc.perform(authed(post("/api/approval/task/" + taskFor(instanceId, leader) + "/approve"),
|
||||
leader, APPROVE)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"opinion\":\"同意\",\"fields\":{\"probeNo\":\"P-002\"}}"))
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.currentNodeKey").value("countersign"));
|
||||
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_field_change WHERE instance_id = ? "
|
||||
+ "AND new_value = 'P-002'", instanceId)).isEqualTo(1);
|
||||
// 节点已推进,会签组处理人没有该字段的可写权
|
||||
mockMvc.perform(authed(get("/api/approval/instance/" + instanceId), groupLeaders.get(0), LIST))
|
||||
.andExpect(jsonPath("$.data.permissions.writableFields.length()").value(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void withdrawClearsRoundScopedFields() throws Exception {
|
||||
long instanceId = createDraft();
|
||||
uploadAttachment(instanceId);
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + instanceId + "/submit"), reporter, SUBMIT))
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
mockMvc.perform(authed(post("/api/approval/task/" + taskFor(instanceId, leader) + "/backfill"),
|
||||
leader, APPROVE)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"fields\":{\"probeNo\":\"P-003\"}}"))
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
|
||||
// 本轮尚无人处理,报送人可撤回
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + instanceId + "/withdraw"), reporter, SUBMIT))
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.status").value("WITHDRAWN"));
|
||||
|
||||
assertThat(instanceStatus(instanceId)).isEqualTo("WITHDRAWN");
|
||||
assertThat(round(instanceId)).isEqualTo(2);
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
|
||||
private long createDraft() throws Exception {
|
||||
String response = mockMvc.perform(authed(post("/api/approval/instance/probe"), reporter, SUBMIT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8);
|
||||
long instanceId = objectMapper.readTree(response).path("data").asLong();
|
||||
data.trackInstance(instanceId);
|
||||
return instanceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 建草稿 → 提交 → 报送负责人带着必填字段通过 → 停在会签节点。
|
||||
*/
|
||||
private long createAndSubmit() throws Exception {
|
||||
long instanceId = createDraft();
|
||||
uploadAttachment(instanceId);
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + instanceId + "/submit"), reporter, SUBMIT))
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
mockMvc.perform(authed(post("/api/approval/task/" + taskFor(instanceId, leader) + "/approve"),
|
||||
leader, APPROVE)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"opinion\":\"同意\",\"fields\":{\"probeNo\":\"P-INIT\"}}"))
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.currentNodeKey").value("countersign"));
|
||||
return instanceId;
|
||||
}
|
||||
|
||||
private ResultActions approve(long taskId) throws Exception {
|
||||
return mockMvc.perform(authed(post("/api/approval/task/" + taskId + "/approve"),
|
||||
operatorOf(taskId), APPROVE)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"opinion\":\"同意\"}"));
|
||||
}
|
||||
|
||||
private ApprovalTestData.TestUser operatorOf(long taskId) {
|
||||
int assigneeId = data.queryMaster("SELECT assignee_id FROM approval_task WHERE id = ?",
|
||||
Integer.class, taskId);
|
||||
for (ApprovalTestData.TestUser candidate : groupLeaders) {
|
||||
if (candidate.userId == assigneeId) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("未知处理人: " + assigneeId);
|
||||
}
|
||||
|
||||
private long taskFor(long instanceId, ApprovalTestData.TestUser user) {
|
||||
Long taskId = data.queryMaster("SELECT id FROM approval_task WHERE instance_id = ? "
|
||||
+ "AND assignee_id = ? AND status = 'PENDING' ORDER BY id LIMIT 1", Long.class, instanceId,
|
||||
user.userId);
|
||||
assertThat(taskId).as("待办任务应存在: " + user.userId).isNotNull();
|
||||
return taskId;
|
||||
}
|
||||
|
||||
private String taskStatus(long instanceId, int userId) {
|
||||
return data.queryMaster("SELECT status FROM approval_task WHERE instance_id = ? "
|
||||
+ "AND assignee_id = ? ORDER BY id DESC LIMIT 1", String.class, instanceId, userId);
|
||||
}
|
||||
|
||||
private int countersignTaskCount(long instanceId) {
|
||||
return data.queryMaster("SELECT COUNT(*) FROM approval_task WHERE instance_id = ? "
|
||||
+ "AND node_key = 'countersign'", Integer.class, instanceId);
|
||||
}
|
||||
|
||||
private int countersignPendingCount(long instanceId) {
|
||||
return data.queryMaster("SELECT COUNT(*) FROM approval_task WHERE instance_id = ? "
|
||||
+ "AND node_key = 'countersign' AND status = 'PENDING'", Integer.class, instanceId);
|
||||
}
|
||||
|
||||
private String instanceStatus(long instanceId) {
|
||||
return data.queryMaster("SELECT status FROM approval_instance WHERE id = ?", String.class, instanceId);
|
||||
}
|
||||
|
||||
private String currentNodeKey(long instanceId) {
|
||||
return data.queryMaster("SELECT current_node_key FROM approval_instance WHERE id = ?",
|
||||
String.class, instanceId);
|
||||
}
|
||||
|
||||
private int round(long instanceId) {
|
||||
return data.queryMaster("SELECT round FROM approval_instance WHERE id = ?", Integer.class, instanceId);
|
||||
}
|
||||
|
||||
private void uploadAttachment(long instanceId) throws Exception {
|
||||
MockMultipartFile file = new MockMultipartFile("file", "sign.png", "image/png",
|
||||
new byte[]{(byte) 0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A});
|
||||
mockMvc.perform(multipart("/api/approval/attachment/upload")
|
||||
.file(file)
|
||||
.param("instanceId", String.valueOf(instanceId))
|
||||
.param("category", ApprovalConstants.CATEGORY_SIGNED)
|
||||
.header("tenantId", TENANT)
|
||||
.with(authentication(TestAuthentication.user(reporter.userId, reporter.username,
|
||||
organizationId, TENANT, SUBMIT))))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
}
|
||||
|
||||
private MockHttpServletRequestBuilder authed(MockHttpServletRequestBuilder builder,
|
||||
ApprovalTestData.TestUser user,
|
||||
String... authorities) {
|
||||
return builder.header("tenantId", TENANT)
|
||||
.with(authentication(TestAuthentication.user(user.userId, user.username, organizationId,
|
||||
TENANT, authorities)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package com.gxwebsoft.approval.support;
|
||||
|
||||
import com.gxwebsoft.approval.document.AbstractApprovalDocumentType;
|
||||
import com.gxwebsoft.approval.document.ApprovalDocumentParam;
|
||||
import com.gxwebsoft.approval.document.ApprovalFieldUpdate;
|
||||
import com.gxwebsoft.approval.entity.ApprovalInstance;
|
||||
import com.gxwebsoft.approval.vo.ApprovalDetail;
|
||||
import com.gxwebsoft.common.core.exception.BusinessException;
|
||||
import com.gxwebsoft.common.system.entity.User;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* 测试专用单据实现:只用于验证引擎能力(会签分组、回填),不承担业务。
|
||||
*
|
||||
* <p>会签组按**固定组织编码**声明,由用例在 {@link #configureGroups} 里指定,每个用例
|
||||
* 用自己的组织;不用租户岗位角色是因为共享测试库里有常驻的 test02 账号持有全部审批角色,
|
||||
* 会让「某组缺配」无法构造。</p>
|
||||
*
|
||||
* <p>节点 JSON 随用例变化,因此每次配置都递增发布版本(流程定义版本不可变)。</p>
|
||||
*/
|
||||
@Component
|
||||
public class CountersignProbeDocumentType extends AbstractApprovalDocumentType {
|
||||
|
||||
/** 测试流程编码。 */
|
||||
public static final String FLOW_CODE = "probe";
|
||||
|
||||
/** 可回填字段键。 */
|
||||
public static final String FIELD_PROBE_NO = "probeNo";
|
||||
|
||||
public static final String NODE_DEPT_LEADER = "deptLeader";
|
||||
public static final String NODE_COUNTERSIGN = "countersign";
|
||||
|
||||
/** 本次用例的会签组组织编码(有序)。 */
|
||||
private static volatile List<String> groupCodes = new ArrayList<>();
|
||||
/** 本次用例的会签组显示名(与 groupCodes 对应)。 */
|
||||
private static volatile List<String> groupNames = new ArrayList<>();
|
||||
/**
|
||||
* 探针流程版本固定 1;用 {@link #reset()} 在用例间清理定义行,
|
||||
* 否则跨 JVM 运行时旧版本行会被 {@code getOrCreate} 当成「已是最新」复用,
|
||||
* 而其组织编码已被上一个用例清理,导致所有组都缺配。
|
||||
*/
|
||||
private static final AtomicInteger VERSION = new AtomicInteger(1);
|
||||
|
||||
private static final Map<Long, Map<String, String>> VALUES = new ConcurrentHashMap<>();
|
||||
|
||||
/** 用例开始前配置会签组(组织编码 + 显示名,一一对应)。 */
|
||||
public static synchronized void configureGroups(List<String> orgCodes, List<String> names) {
|
||||
groupCodes = new ArrayList<>(orgCodes);
|
||||
groupNames = new ArrayList<>(names);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String flowCode() {
|
||||
return FLOW_CODE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String docType() {
|
||||
return FLOW_CODE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String flowName() {
|
||||
return "引擎测试单据";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String nodesJson() {
|
||||
StringBuilder groups = new StringBuilder();
|
||||
List<String> codes = groupCodes;
|
||||
List<String> names = groupNames;
|
||||
for (int i = 0; i < codes.size(); i++) {
|
||||
if (i > 0) {
|
||||
groups.append(',');
|
||||
}
|
||||
groups.append("{\"key\":\"g").append(i + 1).append("\",\"name\":\"")
|
||||
.append(names.size() > i ? names.get(i) : "会签组" + (i + 1))
|
||||
.append("\",\"resolver\":\"orgLeaderByCode\",\"orgCode\":\"")
|
||||
.append(codes.get(i)).append("\"}");
|
||||
}
|
||||
return "["
|
||||
+ "{\"key\":\"deptLeader\",\"type\":\"APPROVE\",\"resolver\":\"reporterOrgLeader\","
|
||||
+ "\"name\":\"报送部门负责人\",\"writableFields\":[{\"field\":\"probeNo\",\"required\":true}]},"
|
||||
+ "{\"key\":\"countersign\",\"type\":\"COUNTERSIGN\",\"name\":\"四部门会签\",\"groups\":["
|
||||
+ groups + "]},"
|
||||
+ "{\"key\":\"confirm\",\"type\":\"CONFIRM\",\"resolver\":\"reporter\",\"name\":\"存档\"},"
|
||||
+ "{\"key\":\"archive\",\"type\":\"ARCHIVE\",\"resolver\":\"archivist\",\"name\":\"归档\"}"
|
||||
+ "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int latestVersion() {
|
||||
return VERSION.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String noPrefix() {
|
||||
return "PB";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends ApprovalDocumentParam> paramClass() {
|
||||
return ProbeParam.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(ApprovalDocumentParam param, User user) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long persist(Long instanceId, Long bizId, ApprovalDocumentParam param, User user) {
|
||||
return bizId == null ? instanceId : bizId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(ApprovalInstance instance, ApprovalDetail detail) {
|
||||
detail.addField("probeNo", "测试编号", readField(instance, FIELD_PROBE_NO));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String title(ApprovalInstance instance) {
|
||||
return "引擎测试单据";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<com.gxwebsoft.approval.support.ApprovalFieldSpec> fieldSpecs() {
|
||||
List<com.gxwebsoft.approval.support.ApprovalFieldSpec> specs = new ArrayList<>();
|
||||
specs.add(com.gxwebsoft.approval.support.ApprovalFieldSpec.uniqueText(FIELD_PROBE_NO, "测试编号", 40));
|
||||
return specs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ApprovalFieldUpdate> applyWritableFields(ApprovalInstance instance, Map<String, String> fields,
|
||||
User user, boolean requireComplete) {
|
||||
String submitted = fields.get(FIELD_PROBE_NO);
|
||||
if (submitted != null) {
|
||||
submitted = submitted.trim();
|
||||
}
|
||||
if (requireComplete && (submitted == null || submitted.isEmpty())) {
|
||||
throw new BusinessException("测试编号不能为空");
|
||||
}
|
||||
if (submitted != null && submitted.length() > 40) {
|
||||
throw new BusinessException("测试编号不能超过40字");
|
||||
}
|
||||
if (submitted == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
Map<String, String> values = VALUES.computeIfAbsent(instance.getId(), ignored -> new ConcurrentHashMap<>());
|
||||
String oldValue = values.get(FIELD_PROBE_NO);
|
||||
values.put(FIELD_PROBE_NO, submitted);
|
||||
List<ApprovalFieldUpdate> changes = new ArrayList<>();
|
||||
changes.add(new ApprovalFieldUpdate(FIELD_PROBE_NO, "测试编号", oldValue, submitted));
|
||||
return changes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readField(ApprovalInstance instance, String field) {
|
||||
Map<String, String> values = VALUES.get(instance.getId());
|
||||
return values == null ? null : values.get(field);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRoundReset(ApprovalInstance instance, User user) {
|
||||
Map<String, String> values = VALUES.get(instance.getId());
|
||||
if (values != null) {
|
||||
values.remove(FIELD_PROBE_NO);
|
||||
}
|
||||
}
|
||||
|
||||
/** 清空测试缓存(用例结束时调用)。 */
|
||||
public static void clear(Long instanceId) {
|
||||
VALUES.remove(instanceId);
|
||||
}
|
||||
|
||||
/** 探针单据参数。 */
|
||||
public static class ProbeParam extends ApprovalDocumentParam {
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user