test: 会签与回填引擎级用例(#47/#49);缺配按组名报错、通过时校验必填可写字段

This commit is contained in:
weicw
2026-10-31 06:48:52 +08:00
parent a99b97ac8a
commit 80e1837f75
5 changed files with 609 additions and 13 deletions
@@ -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;
}
}