package jnpf.dmsService.impl;
|
|
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
import jnpf.base.Pagination;
|
import jnpf.base.UserInfo;
|
import jnpf.dmsEntity.DmsWenjianBianhaoEntity;
|
import jnpf.dmsEntity.DmsWenjianBianhaoGuizeEntity;
|
import jnpf.dmsEntity.DmsWenjianBianhaoGuizeZixiangEntity;
|
import jnpf.dmsEntity.DmsWenjianBianhaoShenqingEntity;
|
import jnpf.dmsEntity.DmsWenjianMingchengBiangengShenqingEntity;
|
import jnpf.dmsEntity.DmsWenjianZiyuanEntity;
|
import jnpf.dmsEntity.wenjianbianhao.DmsFlowEventType;
|
import jnpf.dmsEntity.wenjianbianhao.form.DmsWenjianBianhaoSubmitCheckForm;
|
import jnpf.dmsMapper.DmsWenjianBianhaoGuizeMapper;
|
import jnpf.dmsMapper.DmsWenjianBianhaoGuizeZixiangMapper;
|
import jnpf.dmsMapper.DmsWenjianBianhaoLiushuiJishuMapper;
|
import jnpf.dmsMapper.DmsWenjianBianhaoMapper;
|
import jnpf.dmsMapper.DmsWenjianBianhaoShenqingMapper;
|
import jnpf.dmsMapper.DmsWenjianMingchengBiangengShenqingMapper;
|
import jnpf.dmsMapper.DmsWenjianZiyuanMapper;
|
import jnpf.dmsPermission.DmsCurrentIdentity;
|
import jnpf.dmsService.DmsWenjianBianhaoPermissionService;
|
import jnpf.exception.DataException;
|
import jnpf.permission.UserApi;
|
import jnpf.permission.entity.UserEntity;
|
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.params.ParameterizedTest;
|
import org.junit.jupiter.params.provider.NullAndEmptySource;
|
import org.junit.jupiter.params.provider.NullSource;
|
import org.junit.jupiter.params.provider.CsvSource;
|
import org.junit.jupiter.params.provider.ValueSource;
|
import org.mockito.InjectMocks;
|
import org.mockito.Mock;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
|
import java.util.Arrays;
|
import java.util.Collections;
|
import java.util.Date;
|
import java.util.List;
|
import java.math.BigDecimal;
|
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.mockito.Mockito.verifyNoInteractions;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.anyLong;
|
import static org.mockito.Mockito.lenient;
|
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.when;
|
|
@ExtendWith(MockitoExtension.class)
|
class DmsWenjianBianhaoApplicationServiceImplTest {
|
|
private static final String TENANT_ID = "tenant-1";
|
private static final String FLOW_ID = "flow-1";
|
private static final String TASK_ID = "task-1";
|
|
@Mock private DmsCurrentIdentity identity;
|
@Mock private DmsWenjianBianhaoPermissionService permissionService;
|
@Mock private DmsWenjianBianhaoGuizeMapper ruleMapper;
|
@Mock private DmsWenjianBianhaoGuizeZixiangMapper partMapper;
|
@Mock private DmsWenjianBianhaoShenqingMapper applicationMapper;
|
@Mock private DmsWenjianBianhaoMapper numberMapper;
|
@Mock private DmsWenjianBianhaoLiushuiJishuMapper counterMapper;
|
@Mock private DmsWenjianMingchengBiangengShenqingMapper nameChangeMapper;
|
@Mock private DmsWenjianZiyuanMapper resourceMapper;
|
@Mock private UserApi userApi;
|
|
@InjectMocks
|
private DmsWenjianBianhaoApplicationServiceImpl service;
|
|
@BeforeEach
|
void setUpIdentity() {
|
UserInfo user = new UserInfo();
|
user.setUserId("operator-1");
|
user.setTenantId(TENANT_ID);
|
when(identity.requireUser()).thenReturn(user);
|
lenient().when(identity.tenantId(user)).thenReturn(TENANT_ID);
|
}
|
|
@Test
|
void enabledRulesArePermissionFilteredBeforePaging() throws DataException {
|
Pagination pagination = new Pagination();
|
pagination.setCurrentPage(1);
|
pagination.setPageSize(20);
|
DmsWenjianBianhaoGuizeEntity allowed = rule("rule-1", "QC-SOP规则");
|
allowed.setGuizeLeixing("SOP");
|
allowed.setChangqu("kaiyuandadao");
|
allowed.setWenjianMulu("/质量控制部QC/SOP");
|
DmsWenjianBianhaoGuizeEntity denied = rule("rule-2", "QA-SOP规则");
|
when(ruleMapper.selectEnabled(TENANT_ID)).thenReturn(Arrays.asList(allowed, denied));
|
when(permissionService.canApplyRule("operator-1", "rule-1")).thenReturn(true);
|
when(permissionService.canApplyRule("operator-1", "rule-2")).thenReturn(false);
|
when(ruleMapper.selectActiveById(TENANT_ID, "rule-1")).thenReturn(allowed);
|
when(partMapper.selectActiveByRule(TENANT_ID, "rule-1")).thenReturn(Arrays.asList(
|
part("1", "gudingzhi", "QC", "-"),
|
part("2", "liushuihao", "000", "")));
|
|
jnpf.dmsEntity.wenjianbianhao.vo.DmsWenjianBianhaoRuleVO result =
|
service.enabledRules(pagination, null, "SOP").get(0);
|
|
assertEquals("rule-1", result.getId());
|
assertEquals("kaiyuandadao", result.getChangqu());
|
assertEquals("/", result.getZidingyiTishi());
|
assertEquals("QC-000", result.getYulanBianhao());
|
assertEquals("QC-SOP规则 / QC-000", result.getGuizeXianshi());
|
assertEquals(1, pagination.getTotal());
|
verify(permissionService).canApplyRule("operator-1", "rule-1");
|
verify(permissionService).canApplyRule("operator-1", "rule-2");
|
}
|
|
@Test
|
void enabledRulesSkipInvalidTemplatesBeforeCountingAndPaging() throws DataException {
|
DmsWenjianBianhaoGuizeEntity invalid = rule("invalid", "无效规则");
|
DmsWenjianBianhaoGuizeEntity first = rule("first", "可用规则一");
|
DmsWenjianBianhaoGuizeEntity second = rule("second", "可用规则二");
|
when(ruleMapper.selectEnabled(TENANT_ID)).thenReturn(Arrays.asList(invalid, first, second));
|
for (DmsWenjianBianhaoGuizeEntity candidate : Arrays.asList(invalid, first, second)) {
|
when(permissionService.canApplyRule("operator-1", candidate.getId())).thenReturn(true);
|
when(ruleMapper.selectActiveById(TENANT_ID, candidate.getId())).thenReturn(candidate);
|
when(partMapper.selectActiveByRule(TENANT_ID, candidate.getId())).thenReturn(
|
Collections.singletonList(part("1", "liushuihao",
|
candidate == invalid ? "00A" : "000", "")));
|
}
|
Pagination pagination = new Pagination();
|
pagination.setPageSize(1);
|
pagination.setCurrentPage(1);
|
|
assertEquals("first", service.enabledRules(pagination, null, null).get(0).getId());
|
assertEquals(2, pagination.getTotal());
|
pagination.setCurrentPage(2);
|
assertEquals("second", service.enabledRules(pagination, null, null).get(0).getId());
|
assertEquals(2, pagination.getTotal());
|
pagination.setCurrentPage(3);
|
assertTrue(service.enabledRules(pagination, null, null).isEmpty());
|
assertEquals(2, pagination.getTotal());
|
verifyNoInteractions(counterMapper, numberMapper);
|
}
|
|
@ParameterizedTest
|
@ValueSource(strings = {"missingParts", "invalidSerial", "duplicateSerial", "invalidSnapshot", "disabled", "unapproved"})
|
void enabledRulesReturnEmptyForAnInvalidSelectedRule(String invalidity) throws DataException {
|
DmsWenjianBianhaoGuizeEntity candidate = rule("rule-1", "无效规则");
|
List<DmsWenjianBianhaoGuizeZixiangEntity> parts = Collections.singletonList(
|
part("1", "liushuihao", "000", ""));
|
switch (invalidity) {
|
case "missingParts": parts = Collections.emptyList(); break;
|
case "invalidSerial": parts.get(0).setZhi("00A"); break;
|
case "duplicateSerial": parts = Arrays.asList(parts.get(0), part("2", "liushuihao", "00", "")); break;
|
case "invalidSnapshot": candidate.setSnapshotData("invalid-json"); break;
|
case "disabled": candidate.setBizEnabled("disabled"); break;
|
case "unapproved": candidate.setFlowState(1); break;
|
default: throw new IllegalArgumentException(invalidity);
|
}
|
when(ruleMapper.selectEnabled(TENANT_ID)).thenReturn(Collections.singletonList(candidate));
|
when(permissionService.canApplyRule("operator-1", "rule-1")).thenReturn(true);
|
when(ruleMapper.selectActiveById(TENANT_ID, "rule-1")).thenReturn(candidate);
|
if (!"disabled".equals(invalidity) && !"unapproved".equals(invalidity)) {
|
when(partMapper.selectActiveByRule(TENANT_ID, "rule-1")).thenReturn(parts);
|
}
|
Pagination pagination = new Pagination();
|
|
assertTrue(service.enabledRules(pagination, "rule-1", null).isEmpty());
|
assertEquals(0, pagination.getTotal());
|
assertThrows(DataException.class, () -> service.preview("rule-1"));
|
assertThrows(DataException.class, () -> service.validateSubmission(submission(null, "rule-1")));
|
verifyNoInteractions(counterMapper, numberMapper);
|
}
|
|
@Test
|
void enabledRulesPropagateDatabaseFailures() {
|
DmsWenjianBianhaoGuizeEntity candidate = rule("rule-1", "可用规则");
|
when(ruleMapper.selectEnabled(TENANT_ID)).thenReturn(Collections.singletonList(candidate));
|
when(permissionService.canApplyRule("operator-1", "rule-1")).thenReturn(true);
|
org.springframework.dao.DataAccessResourceFailureException failure =
|
new org.springframework.dao.DataAccessResourceFailureException("database unavailable");
|
when(ruleMapper.selectActiveById(TENANT_ID, "rule-1")).thenThrow(failure);
|
|
assertEquals(failure, assertThrows(org.springframework.dao.DataAccessResourceFailureException.class,
|
() -> service.enabledRules(new Pagination(), null, null)));
|
}
|
|
@Test
|
void rejectsEventUntilFormAndWorkflowStatesMatch() {
|
DmsWenjianBianhaoShenqingEntity application = application(1);
|
when(applicationMapper.selectByFlowForUpdate(TENANT_ID, FLOW_ID, TASK_ID))
|
.thenReturn(Collections.singletonList(application));
|
|
assertThrows(DataException.class, () -> service.syncApplicationFlow(
|
FLOW_ID, TASK_ID, 2, DmsFlowEventType.END));
|
|
verify(numberMapper, never()).selectByApplicationForUpdate(any(), any());
|
}
|
|
@ParameterizedTest
|
@NullAndEmptySource
|
@ValueSource(strings = {"disabled", "draft", "enabled "})
|
void previewRejectsRulesThatAreNotExplicitlyEnabled(String status) {
|
DmsWenjianBianhaoGuizeEntity rule = rule("rule-1", "QC-SOP规则");
|
rule.setBizEnabled(status);
|
when(permissionService.canApplyRule("operator-1", "rule-1")).thenReturn(true);
|
when(ruleMapper.selectActiveById(TENANT_ID, "rule-1")).thenReturn(rule);
|
|
DataException exception = assertThrows(DataException.class, () -> service.preview("rule-1"));
|
|
assertEquals("文件编号规则不存在或不可用,请重新选择已启用且审批通过的规则", exception.getMessage());
|
verify(partMapper, never()).selectActiveByRule(any(), any());
|
}
|
|
@ParameterizedTest
|
@NullAndEmptySource
|
@ValueSource(strings = {"disabled", "draft"})
|
void firstStartRechecksRuleAvailabilityBeforeFreezing(String status) {
|
DmsWenjianBianhaoShenqingEntity application = application(1);
|
application.setShenqingRenId("applicant-1");
|
DmsWenjianBianhaoEntity number = number("number-1", "rule-1", "设备清洁规程");
|
when(applicationMapper.selectByFlowForUpdate(TENANT_ID, FLOW_ID, TASK_ID))
|
.thenReturn(Collections.singletonList(application));
|
when(numberMapper.selectByApplicationForUpdate(TENANT_ID, application.getId()))
|
.thenReturn(Collections.singletonList(number));
|
when(permissionService.canApplyRule("applicant-1", "rule-1")).thenReturn(true);
|
DmsWenjianBianhaoGuizeEntity rule = rule("rule-1", "QC-SOP规则");
|
rule.setBizEnabled(status);
|
when(ruleMapper.selectActiveById(TENANT_ID, "rule-1")).thenReturn(rule);
|
|
assertThrows(DataException.class, () -> service.syncApplicationFlow(
|
FLOW_ID, TASK_ID, 1, DmsFlowEventType.START));
|
|
verify(partMapper, never()).selectActiveByRule(any(), any());
|
verify(numberMapper, never()).updateById(any(DmsWenjianBianhaoEntity.class));
|
verify(counterMapper, never()).next(any(), any(), any(), anyLong());
|
}
|
|
@ParameterizedTest
|
@NullSource
|
@ValueSource(ints = {0, 1, 3, 4, 5, 6, 7, 8, 9, 10, 99})
|
void previewRejectsEnabledRulesWithoutApproval(Integer flowState) {
|
DmsWenjianBianhaoGuizeEntity rule = rule("rule-1", "QC-SOP规则");
|
rule.setFlowState(flowState);
|
when(permissionService.canApplyRule("operator-1", "rule-1")).thenReturn(true);
|
when(ruleMapper.selectActiveById(TENANT_ID, "rule-1")).thenReturn(rule);
|
|
DataException exception = assertThrows(DataException.class, () -> service.preview("rule-1"));
|
|
assertEquals("文件编号规则不存在或不可用,请重新选择已启用且审批通过的规则", exception.getMessage());
|
verify(partMapper, never()).selectActiveByRule(any(), any());
|
}
|
|
@ParameterizedTest
|
@NullSource
|
@ValueSource(ints = {0, 1, 3, 4, 5, 6, 7, 8, 9, 10, 99})
|
void firstStartRejectsEnabledRulesWithoutApprovalBeforeWriting(Integer flowState) {
|
DmsWenjianBianhaoShenqingEntity application = application(1);
|
application.setShenqingRenId("applicant-1");
|
DmsWenjianBianhaoEntity number = number("number-1", "rule-1", "设备清洁规程");
|
when(applicationMapper.selectByFlowForUpdate(TENANT_ID, FLOW_ID, TASK_ID))
|
.thenReturn(Collections.singletonList(application));
|
when(numberMapper.selectByApplicationForUpdate(TENANT_ID, application.getId()))
|
.thenReturn(Collections.singletonList(number));
|
when(permissionService.canApplyRule("applicant-1", "rule-1")).thenReturn(true);
|
DmsWenjianBianhaoGuizeEntity rule = rule("rule-1", "QC-SOP规则");
|
rule.setFlowState(flowState);
|
when(ruleMapper.selectActiveById(TENANT_ID, "rule-1")).thenReturn(rule);
|
|
DataException exception = assertThrows(DataException.class, () -> service.syncApplicationFlow(
|
FLOW_ID, TASK_ID, 1, DmsFlowEventType.START));
|
|
assertEquals("文件编号规则不存在或不可用,请重新选择已启用且审批通过的规则", exception.getMessage());
|
verify(partMapper, never()).selectActiveByRule(any(), any());
|
verify(numberMapper, never()).updateById(any(DmsWenjianBianhaoEntity.class));
|
verify(counterMapper, never()).next(any(), any(), any(), anyLong());
|
}
|
|
@ParameterizedTest
|
@NullAndEmptySource
|
@ValueSource(strings = {"1.0", "01.00", "版本\"A\\B"})
|
void approvalPreservesSubmittedVersionAndSnapshotDespiteRuleChanges(String version) throws Exception {
|
DmsWenjianBianhaoShenqingEntity application = application(1);
|
application.setShenqingRenId("applicant-1");
|
DmsWenjianBianhaoEntity number = number("number-1", "rule-1", "设备清洁规程");
|
when(applicationMapper.selectByFlowForUpdate(TENANT_ID, FLOW_ID, TASK_ID))
|
.thenReturn(Collections.singletonList(application));
|
when(numberMapper.selectByApplicationForUpdate(TENANT_ID, application.getId()))
|
.thenReturn(Collections.singletonList(number));
|
when(permissionService.canApplyRule("applicant-1", "rule-1")).thenReturn(true);
|
DmsWenjianBianhaoGuizeEntity rule = rule("rule-1", "QC-SOP规则");
|
rule.setBanbenhao(version);
|
when(ruleMapper.selectActiveById(TENANT_ID, "rule-1")).thenReturn(rule);
|
when(partMapper.selectActiveByRule(TENANT_ID, "rule-1"))
|
.thenReturn(Collections.singletonList(part("1", "gudingzhi", "QC", "")));
|
when(numberMapper.updateById(number)).thenReturn(1);
|
|
service.syncApplicationFlow(FLOW_ID, TASK_ID, 1, DmsFlowEventType.START);
|
String frozen = number.getGuizeKuaizhao();
|
JsonNode snapshot = new ObjectMapper().readTree(frozen);
|
assertEquals(version, snapshot.get("banbenhao").isNull() ? null : snapshot.get("banbenhao").textValue());
|
|
rule.setBanbenhao("2.0");
|
rule.setBizEnabled("disabled");
|
rule.setFlowState(1);
|
application.setFlowState(2);
|
service.syncApplicationFlow(FLOW_ID, TASK_ID, 2, DmsFlowEventType.END);
|
|
assertEquals(frozen, number.getGuizeKuaizhao());
|
verify(ruleMapper, times(1)).selectActiveById(TENANT_ID, "rule-1");
|
verify(numberMapper, times(2)).updateById(number);
|
}
|
|
@ParameterizedTest
|
@NullAndEmptySource
|
@ValueSource(strings = {"[]", " [\n{\"fileId\":\"file-1\",\"name\":\"签批页\\\"A\\\".pdf\",\"url\":\"dir\\\\file.pdf\"}\n] "})
|
void startEventFreezesOrderedRuleSnapshotAndUsesApplicantPermission(String files) throws Exception {
|
DmsWenjianBianhaoShenqingEntity application = application(1);
|
application.setShenqingRenId("applicant-1");
|
DmsWenjianBianhaoEntity number = number("number-1", "rule-1", "设备清洁规程");
|
when(applicationMapper.selectByFlowForUpdate(TENANT_ID, FLOW_ID, TASK_ID))
|
.thenReturn(Collections.singletonList(application));
|
when(numberMapper.selectByApplicationForUpdate(TENANT_ID, application.getId()))
|
.thenReturn(Collections.singletonList(number));
|
when(permissionService.canApplyRule("applicant-1", "rule-1")).thenReturn(true);
|
DmsWenjianBianhaoGuizeEntity rule = rule("rule-1", "QC-SOP规则");
|
String qianpiyeId = files == null ? null : "qianpiye-1";
|
rule.setChangqu(files == null ? null : "kaiyuandadao");
|
rule.setQianpiyeId(qianpiyeId);
|
rule.setQianpiyeFiles(files);
|
when(ruleMapper.selectActiveById(TENANT_ID, "rule-1")).thenReturn(rule);
|
when(partMapper.selectActiveByRule(TENANT_ID, "rule-1")).thenReturn(Arrays.asList(
|
part("1", "gudingzhi", "QC", "-"),
|
part("2", "liushuihao", "000", "")));
|
when(numberMapper.updateById(number)).thenReturn(1);
|
|
service.syncApplicationFlow(FLOW_ID, TASK_ID, 1, DmsFlowEventType.START);
|
String frozenSnapshot = number.getGuizeKuaizhao();
|
JsonNode snapshot = new ObjectMapper().readTree(frozenSnapshot);
|
assertEquals(files == null ? null : "kaiyuandadao", snapshot.get("changqu").isNull()
|
? null : snapshot.get("changqu").textValue());
|
assertEquals(qianpiyeId, snapshot.get("qianpiye_id").isNull()
|
? null : snapshot.get("qianpiye_id").textValue());
|
assertEquals(files, snapshot.get("qianpiye_files").isNull()
|
? null : snapshot.get("qianpiye_files").textValue());
|
assertEquals("QC", snapshot.get("parts").get(0).get("value").textValue());
|
assertEquals(number.getWenjianMuluId(), snapshot.get("wenjian_mulu_id").textValue());
|
rule.setChangqu("ruixianglu");
|
rule.setQianpiyeId("qianpiye-2");
|
rule.setQianpiyeFiles("[{\"fileId\":\"file-2\"}]");
|
service.syncApplicationFlow(FLOW_ID, TASK_ID, 1, DmsFlowEventType.START);
|
|
JsonNode refreshed = new ObjectMapper().readTree(number.getGuizeKuaizhao());
|
assertEquals("ruixianglu", refreshed.get("changqu").asText());
|
assertEquals("qianpiye-2", refreshed.get("qianpiye_id").asText());
|
assertEquals(rule.getQianpiyeFiles(), refreshed.get("qianpiye_files").asText());
|
assertEquals("pending_review", number.getBizStatus());
|
assertEquals("rule-1", number.getGuizeKuaizhaoId());
|
assertEquals("QC-{SERIAL:3}", number.getBianhaoMuban());
|
assertEquals("QC-000", number.getYulanBianhao());
|
assertEquals("QC-000", number.getShenqingBianhao());
|
assertEquals("设备清洁规程", number.getDangqianWenjianMingcheng());
|
assertEquals(1, number.getMingchengBanben());
|
verify(permissionService, times(2)).canApplyRule("applicant-1", "rule-1");
|
verify(ruleMapper, times(2)).selectActiveById(TENANT_ID, "rule-1");
|
verify(partMapper, times(2)).selectActiveByRule(TENANT_ID, "rule-1");
|
verify(numberMapper, times(2)).updateById(number);
|
}
|
|
@Test
|
void freezesAllMainAndDetailFieldsIncludingFutureColumnsWithoutChangingLegacyAliases() throws Exception {
|
DmsWenjianBianhaoShenqingEntity application = application(1);
|
application.setShenqingRenId("applicant-1");
|
DmsWenjianBianhaoEntity number = number("number-1", "rule-1", "设备清洁规程");
|
DmsWenjianBianhaoGuizeEntity rule = rule("rule-1", " QC-SOP规则 ");
|
rule.setGuizeLeixing(" SOP ");
|
rule.setBanbenhao("01.00");
|
ObjectMapper mapper = new ObjectMapper().enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS);
|
ObjectNode source = (ObjectNode) mapper.readTree(rule.getSnapshotData());
|
source.put("guize_leixing", rule.getGuizeLeixing());
|
source.put("banbenhao", rule.getBanbenhao());
|
source.put("guize_liushuihao", "000023");
|
source.put("wenjian_mulu_id", "original-folder");
|
source.put("shenqing_riqi", "2026-09-16T09:10:11.123456");
|
source.put("f_version", 7);
|
source.put("future_decimal", new BigDecimal("1234567890.1234567890123456789"));
|
source.put("future_text", " 新字段\n\"原文\"\\保留 ");
|
rule.setSnapshotData(source.toString());
|
DmsWenjianBianhaoGuizeZixiangEntity part = part("1", "gudingzhi", " QC ", " - ");
|
ObjectNode detail = (ObjectNode) mapper.readTree(part.getSnapshotData());
|
detail.put("future_column", "新增明细字段");
|
part.setSnapshotData(detail.toString());
|
when(applicationMapper.selectByFlowForUpdate(TENANT_ID, FLOW_ID, TASK_ID))
|
.thenReturn(Collections.singletonList(application));
|
when(numberMapper.selectByApplicationForUpdate(TENANT_ID, application.getId()))
|
.thenReturn(Collections.singletonList(number));
|
when(permissionService.canApplyRule("applicant-1", "rule-1")).thenReturn(true);
|
when(ruleMapper.selectActiveById(TENANT_ID, "rule-1")).thenReturn(rule);
|
when(partMapper.selectActiveByRule(TENANT_ID, "rule-1"))
|
.thenReturn(Collections.singletonList(part));
|
when(numberMapper.updateById(number)).thenReturn(1);
|
|
service.syncApplicationFlow(FLOW_ID, TASK_ID, 1, DmsFlowEventType.START);
|
|
String frozen = number.getGuizeKuaizhao();
|
JsonNode snapshot = mapper.readTree(frozen);
|
assertEquals(source, snapshot.get("rule"));
|
assertEquals(detail, snapshot.get("parts").get(0).get("fields"));
|
assertEquals("01.00", snapshot.get("banbenhao").textValue());
|
assertEquals("000023", snapshot.get("guize_liushuihao").textValue());
|
assertEquals("QC-SOP规则", snapshot.get("ruleName").textValue());
|
assertEquals("SOP", snapshot.get("ruleType").textValue());
|
assertEquals("QC", snapshot.get("parts").get(0).get("value").textValue());
|
assertEquals("QC-", number.getBianhaoMuban());
|
assertEquals(number.getWenjianMuluId(), snapshot.get("wenjian_mulu_id").textValue());
|
assertEquals("original-folder", snapshot.get("rule").get("wenjian_mulu_id").textValue());
|
|
source.put("guize_liushuihao", "000024");
|
source.put("banbenhao", "2.0");
|
rule.setSnapshotData(source.toString());
|
detail.put("zhi", "QA");
|
part.setSnapshotData(detail.toString());
|
service.syncApplicationFlow(FLOW_ID, TASK_ID, 1, DmsFlowEventType.START);
|
|
JsonNode refreshed = mapper.readTree(number.getGuizeKuaizhao());
|
assertEquals(source, refreshed.get("rule"));
|
assertEquals(detail, refreshed.get("parts").get(0).get("fields"));
|
verify(ruleMapper, times(2)).selectActiveById(TENANT_ID, "rule-1");
|
verify(partMapper, times(2)).selectActiveByRule(TENANT_ID, "rule-1");
|
verify(numberMapper, times(2)).updateById(number);
|
}
|
|
@ParameterizedTest
|
@NullAndEmptySource
|
@ValueSource(strings = {"null", "[]", "broken-json"})
|
void incompleteSourceBlocksFreezingWithoutWritingNumber(String invalidSource) {
|
for (boolean invalidMain : new boolean[]{true, false}) {
|
DmsWenjianBianhaoShenqingEntity application = application(1);
|
application.setShenqingRenId("applicant-1");
|
DmsWenjianBianhaoEntity number = number("number-1", "rule-1", "设备清洁规程");
|
DmsWenjianBianhaoGuizeEntity rule = rule("rule-1", "QC-SOP规则");
|
DmsWenjianBianhaoGuizeZixiangEntity part = part("1", "gudingzhi", "QC", "");
|
if (invalidMain) {
|
rule.setSnapshotData(invalidSource);
|
} else {
|
part.setSnapshotData(invalidSource);
|
}
|
when(applicationMapper.selectByFlowForUpdate(TENANT_ID, FLOW_ID, TASK_ID))
|
.thenReturn(Collections.singletonList(application));
|
when(numberMapper.selectByApplicationForUpdate(TENANT_ID, application.getId()))
|
.thenReturn(Collections.singletonList(number));
|
when(permissionService.canApplyRule("applicant-1", "rule-1")).thenReturn(true);
|
when(ruleMapper.selectActiveById(TENANT_ID, "rule-1")).thenReturn(rule);
|
when(partMapper.selectActiveByRule(TENANT_ID, "rule-1"))
|
.thenReturn(Collections.singletonList(part));
|
|
DataException exception = assertThrows(DataException.class, () -> service.syncApplicationFlow(
|
FLOW_ID, TASK_ID, 1, DmsFlowEventType.START));
|
|
assertEquals("文件编号规则完整快照读取失败,请重新选择规则后提交", exception.getMessage());
|
verify(numberMapper, never()).updateById(any(DmsWenjianBianhaoEntity.class));
|
verify(counterMapper, never()).next(any(), any(), any(), anyLong());
|
}
|
}
|
|
@Test
|
void returnedSubmissionCompletelyReplacesOldSnapshotAndCurrentFields() throws Exception {
|
var application = application(8);
|
application.setShenqingRenId("applicant-1");
|
var number = number("number-1", "rule-2", "修改后的文件名");
|
number.setGuizeKuaizhao("{\"obsolete\":true,\"wenjian_mulu_id\":\"old-folder\"}");
|
number.setGuizeKuaizhaoId("rule-1");
|
number.setBianhaoMuban("OLD");
|
number.setBizStatus("pending_review");
|
number.setWenjianMuluId("new-folder");
|
number.setShiyongRenId("selected-user");
|
when(applicationMapper.selectByFlowForUpdate(TENANT_ID, FLOW_ID, TASK_ID))
|
.thenReturn(List.of(application));
|
when(numberMapper.selectByApplicationForUpdate(TENANT_ID, application.getId()))
|
.thenReturn(List.of(number));
|
when(permissionService.canApplyRule("applicant-1", "rule-2")).thenReturn(true);
|
when(ruleMapper.selectActiveById(TENANT_ID, "rule-2")).thenReturn(rule("rule-2", "新规则"));
|
when(partMapper.selectActiveByRule(TENANT_ID, "rule-2"))
|
.thenReturn(List.of(part("1", "gudingzhi", "NEW", "")));
|
when(numberMapper.updateById(number)).thenReturn(1);
|
|
service.syncApplicationFlow(FLOW_ID, TASK_ID, 8, DmsFlowEventType.END);
|
application.setFlowState(1);
|
service.syncApplicationFlow(FLOW_ID, TASK_ID, 1, DmsFlowEventType.START);
|
|
JsonNode snapshot = new ObjectMapper().readTree(number.getGuizeKuaizhao());
|
assertTrue(!snapshot.has("obsolete"));
|
assertEquals("new-folder", snapshot.get("wenjian_mulu_id").asText());
|
assertEquals("rule-2", snapshot.get("ruleId").asText());
|
assertEquals("rule-2", number.getGuizeKuaizhaoId());
|
assertEquals("新规则", number.getGuizeMingchengKuaizhao());
|
assertEquals("NEW", number.getBianhaoMuban());
|
assertEquals("NEW", number.getYulanBianhao());
|
assertEquals("NEW", number.getShenqingBianhao());
|
assertEquals("修改后的文件名", number.getDangqianWenjianMingcheng());
|
assertEquals("selected-user", number.getShiyongRenId());
|
verify(numberMapper).updateById(number);
|
verifyNoInteractions(counterMapper);
|
}
|
|
@Test
|
void repeatedPassedEventDoesNotConsumeAnotherSerial() {
|
DmsWenjianBianhaoShenqingEntity application = application(2);
|
DmsWenjianBianhaoEntity number = number("number-1", "rule-1", "设备清洁规程");
|
number.setGuizeKuaizhao("{\"guize_liushuihao\":\"family-1\",\"hasCustom\":false}");
|
number.setGuizeKuaizhaoId("rule-1");
|
number.setBianhaoMuban("QC-{SERIAL:3}");
|
number.setBizStatus("pending_review");
|
when(applicationMapper.selectByFlowForUpdate(TENANT_ID, FLOW_ID, TASK_ID))
|
.thenReturn(Collections.singletonList(application));
|
when(numberMapper.selectByApplicationForUpdate(TENANT_ID, application.getId()))
|
.thenReturn(Collections.singletonList(number));
|
when(counterMapper.next(TENANT_ID, "family-1", "template:QC-{SERIAL:3}", 1L)).thenReturn(7L);
|
when(numberMapper.updateById(number)).thenReturn(1);
|
|
service.syncApplicationFlow(FLOW_ID, TASK_ID, 2, DmsFlowEventType.END);
|
service.syncApplicationFlow(FLOW_ID, TASK_ID, 2, DmsFlowEventType.END);
|
|
assertEquals("QC-007", number.getWenjianBianhao());
|
assertEquals("QC-007", number.getShenqingBianhao());
|
assertEquals("007", number.getLiushuiHao());
|
assertEquals("using", number.getBizStatus());
|
org.junit.jupiter.api.Assertions.assertNotNull(number.getBianhaoFafangShijian());
|
verify(counterMapper, times(1)).next(TENANT_ID, "family-1", "template:QC-{SERIAL:3}", 1L);
|
verify(numberMapper, times(1)).updateById(number);
|
}
|
|
@Test
|
void failureMarkerUsesValidatedApplicationAndLocksBeforeUpdating() {
|
var application = application(2);
|
when(applicationMapper.selectByFlowForUpdate(TENANT_ID, FLOW_ID, TASK_ID))
|
.thenReturn(List.of(application));
|
service.markIssuanceFailed(FLOW_ID, TASK_ID);
|
var order = org.mockito.Mockito.inOrder(applicationMapper, numberMapper);
|
order.verify(applicationMapper).selectByFlowForUpdate(TENANT_ID, FLOW_ID, TASK_ID);
|
order.verify(numberMapper).selectByApplicationForUpdate(TENANT_ID, application.getId());
|
order.verify(numberMapper).markUnissuedFailure(TENANT_ID, application.getId());
|
verifyNoInteractions(counterMapper);
|
}
|
|
@Test
|
void failureMarkerRejectsUnapprovedApplication() {
|
when(applicationMapper.selectByFlowForUpdate(TENANT_ID, FLOW_ID, TASK_ID))
|
.thenReturn(List.of(application(1)));
|
assertThrows(DataException.class, () -> service.markIssuanceFailed(FLOW_ID, TASK_ID));
|
verifyNoInteractions(numberMapper, counterMapper);
|
}
|
|
@Test
|
void failureMarkerRejectsMissingApplication() {
|
when(applicationMapper.selectByFlowForUpdate(TENANT_ID, FLOW_ID, TASK_ID)).thenReturn(List.of());
|
assertThrows(DataException.class, () -> service.markIssuanceFailed(FLOW_ID, TASK_ID));
|
verifyNoInteractions(numberMapper, counterMapper);
|
}
|
|
@Test
|
void successfulRetryReplacesFailureMarkerWithFinalNumber() {
|
var application = application(2);
|
var number = number("number-1", "rule-1", "设备清洁规程");
|
number.setShenqingBianhao("发号失败");
|
number.setGuizeKuaizhao("{\"guize_liushuihao\":\"family-1\",\"hasCustom\":false}");
|
number.setGuizeKuaizhaoId("rule-1");
|
number.setBianhaoMuban("QC-{SERIAL:3}");
|
number.setBizStatus("pending_review");
|
when(applicationMapper.selectByFlowForUpdate(TENANT_ID, FLOW_ID, TASK_ID)).thenReturn(List.of(application));
|
when(numberMapper.selectByApplicationForUpdate(TENANT_ID, application.getId())).thenReturn(List.of(number));
|
when(counterMapper.next(TENANT_ID, "family-1", "template:QC-{SERIAL:3}", 1L)).thenReturn(17L);
|
when(numberMapper.updateById(number)).thenReturn(1);
|
service.syncApplicationFlow(FLOW_ID, TASK_ID, 2, DmsFlowEventType.END);
|
assertEquals("QC-017", number.getShenqingBianhao());
|
assertEquals("QC-017", number.getWenjianBianhao());
|
assertEquals("using", number.getBizStatus());
|
}
|
|
@ParameterizedTest
|
@CsvSource({"0100,100,0100,0101", "001,1,001,002", "000,1,001,002", "0000,1,0001,0002", "10,10,10,11"})
|
void numericSerialTemplateFreezesStartAndIssuesPaddedSequence(
|
String template, long initial, String firstSerial, String secondSerial) throws Exception {
|
DmsWenjianBianhaoGuizeEntity candidate = rule("rule-1", "起始值规则");
|
DmsWenjianBianhaoGuizeZixiangEntity serialPart = part("2", "liushuihao", template, "");
|
when(ruleMapper.selectEnabled(TENANT_ID)).thenReturn(List.of(candidate));
|
when(ruleMapper.selectActiveById(TENANT_ID, "rule-1")).thenReturn(candidate);
|
when(partMapper.selectActiveByRule(TENANT_ID, "rule-1"))
|
.thenReturn(List.of(part("1", "gudingzhi", "QC", "-"), serialPart));
|
when(permissionService.canApplyRule("operator-1", "rule-1")).thenReturn(true);
|
var options = service.enabledRules(new Pagination(), null, null);
|
assertEquals(1, options.size());
|
assertEquals("QC-" + template, options.get(0).getYulanBianhao());
|
assertEquals(options.get(0).getYulanBianhao(), service.preview("rule-1").getYulanBianhao());
|
service.validateSubmission(submission(null, "rule-1"));
|
verifyNoInteractions(counterMapper);
|
|
DmsWenjianBianhaoShenqingEntity application = application(1);
|
application.setShenqingRenId("operator-1");
|
DmsWenjianBianhaoEntity first = number("first", "rule-1", "文件一");
|
DmsWenjianBianhaoEntity second = number("second", "rule-1", "文件二");
|
when(applicationMapper.selectByFlowForUpdate(TENANT_ID, FLOW_ID, TASK_ID))
|
.thenReturn(List.of(application));
|
when(numberMapper.selectByApplicationForUpdate(TENANT_ID, application.getId()))
|
.thenReturn(List.of(first, second));
|
when(numberMapper.updateById(any(DmsWenjianBianhaoEntity.class))).thenReturn(1);
|
service.syncApplicationFlow(FLOW_ID, TASK_ID, 1, DmsFlowEventType.START);
|
assertEquals(initial, new ObjectMapper().readTree(first.getGuizeKuaizhao()).get("serialStart").longValue());
|
String key = "QC-{SERIAL:" + template.length() + "}";
|
assertEquals(key, first.getBianhaoMuban());
|
assertEquals("QC-" + template, first.getYulanBianhao());
|
assertEquals("QC-" + template, first.getShenqingBianhao());
|
verifyNoInteractions(counterMapper);
|
|
// 发号使用已冻结的起始值,不能因审批期间修改源规则而变化。
|
serialPart.setZhi("9999");
|
application.setFlowState(2);
|
when(counterMapper.next(TENANT_ID, "family-1", "template:" + key, initial)).thenReturn(initial, initial + 1);
|
service.syncApplicationFlow(FLOW_ID, TASK_ID, 2, DmsFlowEventType.END);
|
service.syncApplicationFlow(FLOW_ID, TASK_ID, 2, DmsFlowEventType.END);
|
assertEquals("QC-" + firstSerial, first.getWenjianBianhao());
|
assertEquals("QC-" + secondSerial, second.getWenjianBianhao());
|
assertEquals(firstSerial, first.getLiushuiHao());
|
assertEquals(secondSerial, second.getLiushuiHao());
|
verify(counterMapper, times(2)).next(TENANT_ID, "family-1", "template:" + key, initial);
|
}
|
|
@ParameterizedTest
|
@NullAndEmptySource
|
@ValueSource(strings = {"00A", "-100", "1.00", "+100", "9223372036854775808"})
|
void invalidNumericTemplatesAreHiddenAndCannotBeSubmitted(String template) throws DataException {
|
DmsWenjianBianhaoGuizeEntity candidate = rule("rule-1", "无效起始值");
|
when(ruleMapper.selectEnabled(TENANT_ID)).thenReturn(List.of(candidate));
|
when(permissionService.canApplyRule("operator-1", "rule-1")).thenReturn(true);
|
when(ruleMapper.selectActiveById(TENANT_ID, "rule-1")).thenReturn(candidate);
|
when(partMapper.selectActiveByRule(TENANT_ID, "rule-1"))
|
.thenReturn(List.of(part("1", "liushuihao", template, "")));
|
Pagination pagination = new Pagination();
|
assertTrue(service.enabledRules(pagination, null, null).isEmpty());
|
assertEquals(0, pagination.getTotal());
|
assertThrows(DataException.class, () -> service.validateSubmission(submission(null, "rule-1")));
|
verifyNoInteractions(counterMapper, numberMapper);
|
}
|
|
@Test
|
void usingNumbersReturnsPopupFieldsFromTenantScopedQuery() throws DataException {
|
Pagination pagination = new Pagination();
|
pagination.setCurrentPage(1);
|
pagination.setPageSize(20);
|
DmsWenjianBianhaoEntity number = number("number-1", "rule-1", "原文件名称");
|
number.setWenjianBianhao("QC-001");
|
number.setDangqianWenjianMingcheng("当前文件名称");
|
number.setMingchengBanben(2);
|
number.setGuizeMingchengKuaizhao("QC-SOP规则");
|
number.setShiyongRenId("user-1");
|
number.setBianhaoFafangShijian(new Date());
|
when(numberMapper.selectUsing(TENANT_ID, "number-1", pagination))
|
.thenReturn(Collections.singletonList(number));
|
when(permissionService.canViewNumber("operator-1", "number-1")).thenReturn(true);
|
UserEntity user = new UserEntity();
|
user.setId("user-1");
|
user.setRealName("张三");
|
when(userApi.getUserName(Collections.singletonList("user-1")))
|
.thenReturn(Collections.singletonList(user));
|
|
jnpf.dmsEntity.wenjianbianhao.vo.DmsWenjianBianhaoOptionVO result =
|
service.usingNumbers(pagination, " number-1 ", "当前").get(0);
|
|
assertEquals("number-1", result.getId());
|
assertEquals("QC-001", result.getWenjianBianhao());
|
assertEquals("当前文件名称", result.getWenjianMingcheng());
|
assertEquals(2, result.getMingchengBanben());
|
assertEquals("张三", result.getShiyongRen());
|
assertEquals("QC-001 / 当前文件名称", result.getBianhaoXianshi());
|
assertEquals("当前", pagination.getKeyword());
|
verify(permissionService).canViewNumber("operator-1", "number-1");
|
verify(numberMapper).selectUsing(TENANT_ID, "number-1", pagination);
|
}
|
|
@Test
|
void usingNumbersExcludeRowsDeniedByPermissionService() throws DataException {
|
Pagination pagination = new Pagination();
|
pagination.setCurrentPage(1);
|
pagination.setPageSize(20);
|
DmsWenjianBianhaoEntity denied = number("number-1", "rule-1", "受限文件");
|
denied.setWenjianBianhao("QA-001");
|
when(numberMapper.selectUsing(TENANT_ID, "", pagination))
|
.thenReturn(Collections.singletonList(denied));
|
when(permissionService.canViewNumber("operator-1", "number-1")).thenReturn(false);
|
|
assertEquals(0, service.usingNumbers(pagination, null, null).size());
|
|
verify(permissionService).canViewNumber("operator-1", "number-1");
|
verify(userApi, never()).getUserName(any());
|
}
|
|
@Test
|
void nameChangeOptionsUseThePreReviewNumberQuery() throws DataException {
|
Pagination pagination = new Pagination();
|
pagination.setCurrentPage(1);
|
pagination.setPageSize(20);
|
DmsWenjianBianhaoEntity number = number("number-1", "rule-1", "待变更名称");
|
number.setWenjianBianhao("QC-001");
|
when(numberMapper.selectNameChangeAvailable(TENANT_ID, "", pagination))
|
.thenReturn(Collections.singletonList(number));
|
when(permissionService.canViewNumber("operator-1", "number-1")).thenReturn(true);
|
|
assertEquals("number-1", service.availableNameChangeNumbers(pagination, null, null).get(0).getId());
|
|
verify(numberMapper).selectNameChangeAvailable(TENANT_ID, "", pagination);
|
}
|
|
@ParameterizedTest
|
@CsvSource(value = {
|
"{\"changqu\":\"kaiyuandadao\",\"rule\":{\"changqu\":\"ruixianglu\"}}|kaiyuandadao",
|
"{\"rule\":{\"changqu\":\"ruixianglu\"}}|ruixianglu",
|
"{\"changqu\":null,\"rule\":{\"changqu\":\"ruixianglu\"}}|",
|
"{}|",
|
"invalid|"
|
}, delimiter = '|')
|
void nameChangeOptionsUseHistoricalSite(String snapshot, String expected) throws Exception {
|
Pagination pagination = new Pagination();
|
DmsWenjianBianhaoEntity number = number("number-1", "rule-1", "文件一");
|
number.setGuizeKuaizhao(snapshot);
|
when(numberMapper.selectNameChangeAvailable(TENANT_ID, "", pagination))
|
.thenReturn(Collections.singletonList(number));
|
when(permissionService.canViewNumber("operator-1", "number-1")).thenReturn(true);
|
|
var option = service.availableNameChangeNumbers(pagination, null, null).get(0);
|
assertEquals(expected, option.getChangqu());
|
assertEquals(expected, new ObjectMapper().valueToTree(option).path("changqu").asText(null));
|
verifyNoInteractions(ruleMapper, partMapper);
|
}
|
|
@Test
|
void recallOnlyRestoresUnissuedNumberToDraft() {
|
DmsWenjianBianhaoShenqingEntity application = application(9);
|
DmsWenjianBianhaoEntity pending = number("number-1", "rule-1", "文件一");
|
pending.setBizStatus("pending_review");
|
DmsWenjianBianhaoEntity issued = number("number-2", "rule-1", "文件二");
|
issued.setBizStatus("using");
|
issued.setWenjianBianhao("QC-001");
|
when(applicationMapper.selectByFlowForUpdate(TENANT_ID, FLOW_ID, TASK_ID))
|
.thenReturn(Collections.singletonList(application));
|
when(numberMapper.selectByApplicationForUpdate(TENANT_ID, application.getId()))
|
.thenReturn(Arrays.asList(pending, issued));
|
when(numberMapper.updateById(pending)).thenReturn(1);
|
|
service.syncApplicationFlow(FLOW_ID, TASK_ID, 9, DmsFlowEventType.RECALL);
|
|
assertEquals("draft", pending.getBizStatus());
|
assertEquals("using", issued.getBizStatus());
|
verify(numberMapper).updateById(pending);
|
verify(numberMapper, never()).updateById(issued);
|
}
|
|
@Test
|
void nameChangePassIsVersionCheckedAndIdempotent() {
|
DmsWenjianMingchengBiangengShenqingEntity change = new DmsWenjianMingchengBiangengShenqingEntity();
|
change.setId("change-1");
|
change.setTenantId(TENANT_ID);
|
change.setFlowId(FLOW_ID);
|
change.setFlowTaskId(TASK_ID);
|
change.setFlowState(1);
|
change.setBianhaoId("number-1");
|
change.setBiangengHouWenjianMingcheng("新文件名称");
|
DmsWenjianBianhaoEntity number = number("number-1", "rule-1", "原文件名称");
|
number.setBizStatus("using");
|
number.setWenjianBianhao("QC-001");
|
number.setDangqianWenjianMingcheng("原文件名称");
|
number.setMingchengBanben(3);
|
when(nameChangeMapper.selectByFlowForUpdate(TENANT_ID, FLOW_ID, TASK_ID))
|
.thenReturn(Collections.singletonList(change));
|
when(numberMapper.selectActiveByIdForUpdate(TENANT_ID, "number-1")).thenReturn(number);
|
when(nameChangeMapper.updateById(change)).thenReturn(1);
|
when(numberMapper.updateById(number)).thenReturn(1);
|
|
service.syncNameChangeFlow(FLOW_ID, TASK_ID, 1, DmsFlowEventType.START);
|
change.setFlowState(2);
|
service.syncNameChangeFlow(FLOW_ID, TASK_ID, 2, DmsFlowEventType.END);
|
service.syncNameChangeFlow(FLOW_ID, TASK_ID, 2, DmsFlowEventType.END);
|
|
assertEquals("原文件名称", change.getYuanWenjianMingcheng());
|
assertEquals(3, change.getYuanMingchengBanben());
|
assertEquals("新文件名称", number.getDangqianWenjianMingcheng());
|
assertEquals(4, number.getMingchengBanben());
|
verify(numberMapper, times(1)).updateById(number);
|
}
|
|
@Test
|
void nameChangeStartRejectsAnotherActiveApplicationForTheSameNumber() {
|
DmsWenjianMingchengBiangengShenqingEntity change =
|
new DmsWenjianMingchengBiangengShenqingEntity();
|
change.setId("change-2");
|
change.setTenantId(TENANT_ID);
|
change.setFlowId(FLOW_ID);
|
change.setFlowTaskId(TASK_ID);
|
change.setFlowState(1);
|
change.setBianhaoId("number-1");
|
change.setBiangengHouWenjianMingcheng("另一个新名称");
|
DmsWenjianBianhaoEntity number = number("number-1", "rule-1", "原文件名称");
|
number.setBizStatus("using");
|
number.setWenjianBianhao("QC-001");
|
when(nameChangeMapper.selectByFlowForUpdate(TENANT_ID, FLOW_ID, TASK_ID))
|
.thenReturn(Collections.singletonList(change));
|
when(numberMapper.selectActiveByIdForUpdate(TENANT_ID, "number-1")).thenReturn(number);
|
when(nameChangeMapper.countActiveByNumber(TENANT_ID, "number-1", "change-2"))
|
.thenReturn(1);
|
|
DataException exception = assertThrows(DataException.class,
|
() -> service.syncNameChangeFlow(FLOW_ID, TASK_ID, 1, DmsFlowEventType.START));
|
|
assertEquals("该文件编号已有进行中的名称变更申请,请勿重复提交", exception.getMessage());
|
verify(nameChangeMapper, never()).updateById(change);
|
}
|
|
@Test
|
void nameChangeRejectsAnyUsedNumberIncludingReviewDraft() {
|
DmsWenjianMingchengBiangengShenqingEntity change = new DmsWenjianMingchengBiangengShenqingEntity();
|
change.setId("change-3");
|
change.setTenantId(TENANT_ID);
|
change.setFlowId(FLOW_ID);
|
change.setFlowTaskId(TASK_ID);
|
change.setFlowState(1);
|
change.setBianhaoId("number-1");
|
change.setBiangengHouWenjianMingcheng("不应更新的名称");
|
DmsWenjianZiyuanEntity reviewedFile = new DmsWenjianZiyuanEntity();
|
reviewedFile.setId("file-1");
|
when(nameChangeMapper.selectByFlowForUpdate(TENANT_ID, FLOW_ID, TASK_ID))
|
.thenReturn(Collections.singletonList(change));
|
when(numberMapper.numberAlreadyUsed(TENANT_ID, "number-1")).thenReturn(true);
|
|
DataException exception = assertThrows(DataException.class,
|
() -> service.syncNameChangeFlow(FLOW_ID, TASK_ID, 1, DmsFlowEventType.START));
|
|
assertEquals("文件编号已被使用(包括审核草稿),不能变更文件名称", exception.getMessage());
|
org.mockito.InOrder order = org.mockito.Mockito.inOrder(numberMapper);
|
order.verify(numberMapper).selectActiveByIdForUpdate(TENANT_ID, "number-1");
|
order.verify(numberMapper).numberAlreadyUsed(TENANT_ID, "number-1");
|
verify(nameChangeMapper, never()).updateById(change);
|
}
|
|
@Test
|
void nameChangeCompletionRejectsAFileThatPassedReviewAfterSubmission() {
|
DmsWenjianMingchengBiangengShenqingEntity change = new DmsWenjianMingchengBiangengShenqingEntity();
|
change.setId("change-4");
|
change.setTenantId(TENANT_ID);
|
change.setFlowId(FLOW_ID);
|
change.setFlowTaskId(TASK_ID);
|
change.setFlowState(2);
|
change.setBianhaoId("number-1");
|
change.setYuanMingchengBanben(1);
|
change.setBiangengHouWenjianMingcheng("不应更新的名称");
|
DmsWenjianZiyuanEntity reviewedFile = new DmsWenjianZiyuanEntity();
|
reviewedFile.setId("file-1");
|
when(nameChangeMapper.selectByFlowForUpdate(TENANT_ID, FLOW_ID, TASK_ID))
|
.thenReturn(Collections.singletonList(change));
|
when(numberMapper.numberAlreadyUsed(TENANT_ID, "number-1")).thenReturn(true);
|
|
DataException exception = assertThrows(DataException.class,
|
() -> service.syncNameChangeFlow(FLOW_ID, TASK_ID, 2, DmsFlowEventType.END));
|
|
assertEquals("文件编号已被使用(包括审核草稿),不能变更文件名称", exception.getMessage());
|
verify(numberMapper, never()).updateById(any(DmsWenjianBianhaoEntity.class));
|
}
|
|
@Test
|
void submitRejectsRepeatedFixedNumberWithinTheApplication() {
|
submitRule("rule-1", false);
|
DataException error = assertThrows(DataException.class,
|
() -> service.validateSubmission(submission(null, "rule-1", "rule-1")));
|
assertTrue(error.getMessage().contains("第1行与第2行"));
|
verifyNoInteractions(counterMapper, numberMapper);
|
}
|
|
@Test
|
void submitRejectsDifferentRulesRenderingTheSameFixedNumber() {
|
submitRule("rule-1", false);
|
submitRule("rule-2", false);
|
DataException error = assertThrows(DataException.class,
|
() -> service.validateSubmission(submission(null, "rule-1", "rule-2")));
|
assertTrue(error.getMessage().contains("无流水号的编号只能申请一次"));
|
verifyNoInteractions(counterMapper, numberMapper);
|
}
|
|
@Test
|
void submitAllowsRepeatedSerialRulesWithoutAllocatingOrWriting() throws Exception {
|
submitRule("rule-1", true);
|
service.validateSubmission(submission(null, "rule-1", "rule-1"));
|
verifyNoInteractions(counterMapper, numberMapper, applicationMapper);
|
verify(ruleMapper, times(1)).selectActiveById(TENANT_ID, "rule-1");
|
}
|
|
@Test
|
void submitChecksOnlyFixedNumbersInMixedApplication() throws Exception {
|
submitRule("fixed", false);
|
submitRule("serial", true);
|
service.validateSubmission(submission(null, "serial", "fixed", "serial"));
|
verify(numberMapper).selectFixedNumberConflicts(TENANT_ID, List.of("FIXED"), null);
|
verifyNoInteractions(counterMapper);
|
verify(numberMapper, never()).updateById(any(DmsWenjianBianhaoEntity.class));
|
}
|
|
@ParameterizedTest
|
@ValueSource(booleans = {true, false})
|
void submitReportsIssuedAndPendingConflicts(boolean issued) {
|
submitRule("rule-1", false);
|
DmsWenjianBianhaoEntity conflict = number("other", "rule-1", "占用");
|
conflict.setBianhaoMuban("FIXED");
|
if (issued) conflict.setWenjianBianhao("FIXED");
|
when(numberMapper.selectFixedNumberConflicts(TENANT_ID, List.of("FIXED"), null))
|
.thenReturn(List.of(conflict));
|
DataException error = assertThrows(DataException.class,
|
() -> service.validateSubmission(submission(null, "rule-1")));
|
assertTrue(error.getMessage().contains(issued ? "已发放" : "其他审批中的申请占用"));
|
verifyNoInteractions(counterMapper);
|
}
|
|
@ParameterizedTest
|
@ValueSource(ints = {1, 5, 6, 8, 10})
|
void approvalUsesPersistedSnapshotAndExcludesOwnApplication(int state) throws Exception {
|
persistedSubmission(state, "another-applicant");
|
service.validateSubmission(submission("application-1", "rule-1"));
|
verify(numberMapper).selectFixedNumberConflicts(TENANT_ID, List.of("FROZEN"), "application-1");
|
verifyNoInteractions(ruleMapper, partMapper, permissionService, counterMapper);
|
verify(numberMapper, never()).updateById(any(DmsWenjianBianhaoEntity.class));
|
}
|
|
@Test
|
void approvalCannotSwapRulesOrOmitRowsToBypassDuplicateCheck() {
|
persistedSubmission(1, "another-applicant");
|
assertThrows(DataException.class,
|
() -> service.validateSubmission(submission("application-1", "rule-2")));
|
DmsWenjianBianhaoSubmitCheckForm form = submission("application-1", "rule-1", "rule-1");
|
assertThrows(DataException.class, () -> service.validateSubmission(form));
|
verifyNoInteractions(ruleMapper, counterMapper);
|
}
|
|
@ParameterizedTest
|
@ValueSource(ints = {0, 8, 9})
|
void editableSubmissionRechecksCurrentRuleEvenWithExistingSnapshot(int state) throws Exception {
|
DmsWenjianBianhaoEntity row = persistedSubmission(state, "operator-1");
|
submitRule("rule-1", false);
|
service.validateSubmission(submission("application-1", "rule-1"));
|
verify(numberMapper).selectFixedNumberConflicts(TENANT_ID, List.of("FIXED"), "application-1");
|
verifyNoInteractions(counterMapper);
|
}
|
|
@Test
|
void returnedApplicantCanChangeRuleWithoutReusingItsOldSnapshot() throws Exception {
|
persistedSubmission(8, "operator-1");
|
submitRule("rule-2", false);
|
service.validateSubmission(submission("application-1", "rule-2"));
|
verify(numberMapper).selectFixedNumberConflicts(TENANT_ID, List.of("FIXED"), "application-1");
|
}
|
|
@Test
|
void submitRejectsUnknownOrOtherTenantApplication() {
|
assertThrows(DataException.class,
|
() -> service.validateSubmission(submission("unknown", "rule-1")));
|
verify(applicationMapper).selectActiveById(TENANT_ID, "unknown");
|
verifyNoInteractions(numberMapper, ruleMapper, counterMapper);
|
}
|
|
@ParameterizedTest
|
@ValueSource(ints = {2, 3, 4, 7})
|
void submitRejectsFinishedApplications(int state) {
|
DmsWenjianBianhaoShenqingEntity app = application(state);
|
when(applicationMapper.selectActiveById(TENANT_ID, "application-1")).thenReturn(app);
|
assertThrows(DataException.class,
|
() -> service.validateSubmission(submission("application-1", "rule-1")));
|
verifyNoInteractions(numberMapper, counterMapper);
|
}
|
|
@ParameterizedTest
|
@ValueSource(ints = {0, 9})
|
void submitCannotImpersonateAnotherApplicant(int state) {
|
DmsWenjianBianhaoShenqingEntity app = application(state);
|
app.setShenqingRenId("someone-else");
|
when(applicationMapper.selectActiveById(TENANT_ID, "application-1")).thenReturn(app);
|
assertThrows(DataException.class,
|
() -> service.validateSubmission(submission("application-1", "rule-1")));
|
verifyNoInteractions(numberMapper, counterMapper);
|
}
|
|
@Test
|
void submitRejectsEmptyItems() {
|
assertThrows(DataException.class, () -> service.validateSubmission(submission(null)));
|
verifyNoInteractions(applicationMapper, numberMapper, counterMapper);
|
}
|
|
@Test
|
void submitRejectsAlreadyIssuedOwnRow() {
|
DmsWenjianBianhaoEntity row = persistedSubmission(1, "operator-1");
|
row.setWenjianBianhao("FROZEN");
|
assertThrows(DataException.class,
|
() -> service.validateSubmission(submission("application-1", "rule-1")));
|
verifyNoInteractions(ruleMapper, counterMapper);
|
}
|
|
@Test
|
void customPreviewUsesInputAndConfiguredSerialDefault() throws Exception {
|
customRule(true);
|
assertEquals("QC-待填写-001", service.preview("rule-1").getYulanBianhao());
|
var preview = service.preview("rule-1", " A$\\B ");
|
assertEquals("QC-A$\\B-001", preview.getYulanBianhao());
|
assertTrue(preview.isHasCustom());
|
assertEquals("不能用规则里的值", preview.getCustomHint());
|
verifyNoInteractions(counterMapper, numberMapper);
|
}
|
|
@Test
|
void ruleSelectionReturnsCustomHintWithoutUsingItAsNumberContent() throws Exception {
|
customRule(true);
|
var rule = rule("rule-1", "自定义规则");
|
rule.setChangqu("kaiyuandadao");
|
when(ruleMapper.selectEnabled(TENANT_ID)).thenReturn(List.of(rule));
|
var result = service.enabledRules(new Pagination(), "rule-1", null).get(0);
|
var json = new ObjectMapper().valueToTree(result);
|
assertEquals("kaiyuandadao", json.path("changqu").asText());
|
assertEquals("不能用规则里的值", json.path("zidingyi_tishi").asText());
|
assertEquals("QC-待填写-001", json.path("yulan_bianhao").asText());
|
when(partMapper.selectActiveByRule(TENANT_ID, "rule-1"))
|
.thenReturn(List.of(part("1", "zidingyi", " ", "")));
|
assertEquals("/", service.enabledRules(new Pagination(), "rule-1", null).get(0).getZidingyiTishi());
|
assertEquals("", service.preview("rule-1").getCustomHint());
|
assertEquals("待填写", service.preview("rule-1").getYulanBianhao());
|
verifyNoInteractions(numberMapper, counterMapper);
|
}
|
|
@Test
|
void customHintIsEmptyForBlankHintAndFixedRule() throws Exception {
|
customRule(false);
|
when(partMapper.selectActiveByRule(TENANT_ID, "rule-1"))
|
.thenReturn(List.of(part("1", "zidingyi", " ", "")));
|
var preview = service.preview("rule-1");
|
assertEquals("", preview.getCustomHint());
|
assertEquals("待填写", preview.getYulanBianhao());
|
assertTrue(preview.isHasCustom());
|
when(partMapper.selectActiveByRule(TENANT_ID, "rule-1"))
|
.thenReturn(List.of(part("1", "gudingzhi", "FIXED", "")));
|
preview = service.preview("rule-1");
|
assertEquals("", preview.getCustomHint());
|
assertEquals(false, preview.isHasCustom());
|
assertEquals("FIXED", preview.getYulanBianhao());
|
}
|
|
@ParameterizedTest
|
@NullAndEmptySource
|
@ValueSource(strings = {" ", "{SERIAL:3}", "x{SERIAL:4}y"})
|
void customSubmissionRejectsMissingTextAndTemplateInjection(String text) {
|
customRule(true);
|
var form = submission(null, "rule-1");
|
form.getItems().get(0).setZidingyiNeirong(text);
|
assertThrows(DataException.class, () -> service.validateSubmission(form));
|
verifyNoInteractions(counterMapper, numberMapper);
|
}
|
|
@Test
|
void customFixedNumbersAreCheckedPerRowRatherThanPerRule() throws Exception {
|
customRule(false);
|
var form = submission(null, "rule-1", "rule-1");
|
form.getItems().get(0).setZidingyiNeirong("A");
|
form.getItems().get(1).setZidingyiNeirong("B");
|
service.validateSubmission(form);
|
verify(numberMapper).selectFixedNumberConflicts(TENANT_ID, List.of("QC-A-", "QC-B-"), null);
|
form.getItems().get(1).setZidingyiNeirong(" A ");
|
assertThrows(DataException.class, () -> service.validateSubmission(form));
|
verifyNoInteractions(counterMapper);
|
}
|
|
@Test
|
void approvalCannotChangeCustomText() {
|
var row = persistedSubmission(1, "applicant-1");
|
row.setZidingyiNeirong("A");
|
var form = submission("application-1", "rule-1");
|
form.getItems().get(0).setZidingyiNeirong("B");
|
assertThrows(DataException.class, () -> service.validateSubmission(form));
|
verifyNoInteractions(ruleMapper, counterMapper);
|
}
|
|
@Test
|
void customStartFreezesInputAndEndIssuesFromFrozenTemplate() throws Exception {
|
customRule(true);
|
var app = application(1);
|
app.setShenqingRenId("operator-1");
|
var row = number("row-0", "rule-1", "文档");
|
row.setZidingyiNeirong("A");
|
when(applicationMapper.selectByFlowForUpdate(TENANT_ID, FLOW_ID, TASK_ID)).thenReturn(List.of(app));
|
when(numberMapper.selectByApplicationForUpdate(TENANT_ID, app.getId())).thenReturn(List.of(row));
|
when(numberMapper.updateById(row)).thenReturn(1);
|
service.syncApplicationFlow(FLOW_ID, TASK_ID, 1, DmsFlowEventType.START);
|
assertEquals("QC-A-{SERIAL:3}", row.getBianhaoMuban());
|
assertEquals("A", new ObjectMapper().readTree(row.getGuizeKuaizhao()).get("customValue").asText());
|
row.setZidingyiNeirong("B");
|
service.syncApplicationFlow(FLOW_ID, TASK_ID, 1, DmsFlowEventType.START);
|
assertEquals("B", new ObjectMapper().readTree(row.getGuizeKuaizhao()).get("customValue").asText());
|
verify(ruleMapper, times(2)).selectActiveById(TENANT_ID, "rule-1");
|
when(counterMapper.next(TENANT_ID, "family-1", "template:QC-B-{SERIAL:3}", 1L)).thenReturn(7L);
|
app.setFlowState(2);
|
service.syncApplicationFlow(FLOW_ID, TASK_ID, 2, DmsFlowEventType.END);
|
assertEquals("QC-B-007", row.getWenjianBianhao());
|
}
|
|
private void customRule(boolean serial) {
|
when(permissionService.canApplyRule("operator-1", "rule-1")).thenReturn(true);
|
when(ruleMapper.selectActiveById(TENANT_ID, "rule-1")).thenReturn(rule("rule-1", "自定义规则"));
|
var parts = new java.util.ArrayList<>(List.of(part("1", "gudingzhi", "QC", "-"),
|
part("2", "zidingyi", "不能用规则里的值", "-")));
|
if (serial) parts.add(part("3", "liushuihao", "001", ""));
|
when(partMapper.selectActiveByRule(TENANT_ID, "rule-1")).thenReturn(parts);
|
}
|
|
private void submitRule(String id, boolean serial) {
|
when(permissionService.canApplyRule("operator-1", id)).thenReturn(true);
|
when(ruleMapper.selectActiveById(TENANT_ID, id)).thenReturn(rule(id, id));
|
when(partMapper.selectActiveByRule(TENANT_ID, id)).thenReturn(serial
|
? List.of(part("part-1", "gudingzhi", "FIXED", "-"), part("part-2", "liushuihao", "000", ""))
|
: List.of(part("part-1", "gudingzhi", "FIXED", "")));
|
}
|
|
private DmsWenjianBianhaoEntity persistedSubmission(int state, String applicant) {
|
DmsWenjianBianhaoShenqingEntity app = application(state);
|
app.setShenqingRenId(applicant);
|
when(applicationMapper.selectActiveById(TENANT_ID, "application-1")).thenReturn(app);
|
DmsWenjianBianhaoEntity row = number("row-0", "rule-1", "文档");
|
row.setBizStatus("pending_review");
|
row.setGuizeKuaizhaoId("rule-1");
|
row.setGuizeKuaizhao("{\"wenjian_mulu_id\":\"folder\"}");
|
row.setBianhaoMuban("FROZEN");
|
when(numberMapper.selectByApplication(TENANT_ID, "application-1")).thenReturn(List.of(row));
|
return row;
|
}
|
|
private DmsWenjianBianhaoSubmitCheckForm submission(String applicationId, String... rules) {
|
DmsWenjianBianhaoSubmitCheckForm form = new DmsWenjianBianhaoSubmitCheckForm();
|
form.setApplicationId(applicationId);
|
List<DmsWenjianBianhaoSubmitCheckForm.Item> items = new java.util.ArrayList<>();
|
for (int i = 0; i < rules.length; i++) {
|
DmsWenjianBianhaoSubmitCheckForm.Item item = new DmsWenjianBianhaoSubmitCheckForm.Item();
|
item.setId("row-" + i);
|
item.setGuizeId(rules[i]);
|
items.add(item);
|
}
|
form.setItems(items);
|
return form;
|
}
|
|
private DmsWenjianBianhaoShenqingEntity application(int flowState) {
|
DmsWenjianBianhaoShenqingEntity application = new DmsWenjianBianhaoShenqingEntity();
|
application.setId("application-1");
|
application.setTenantId(TENANT_ID);
|
application.setFlowId(FLOW_ID);
|
application.setFlowTaskId(TASK_ID);
|
application.setFlowState(flowState);
|
return application;
|
}
|
|
private DmsWenjianBianhaoEntity number(String id, String ruleId, String name) {
|
DmsWenjianBianhaoEntity number = new DmsWenjianBianhaoEntity();
|
number.setId(id);
|
number.setForeignId("application-1");
|
number.setTenantId(TENANT_ID);
|
number.setGuizeId(ruleId);
|
number.setWenjianMuluId("00000000-0000-0000-0000-000000000001");
|
number.setYuanWenjianMingcheng(name);
|
return number;
|
}
|
|
@Test
|
void differentTemplatesWithinSameFamilyUseIndependentSequences() {
|
var oldVersion = frozenNumber("old", "version-1", "family-1", false, "QC-{SERIAL:3}");
|
var newVersion = frozenNumber("new", "version-2", "family-1", false, "NEW-{SERIAL:4}");
|
prepareIssuance(List.of(oldVersion, newVersion));
|
when(counterMapper.next(TENANT_ID, "family-1", "template:QC-{SERIAL:3}", 1L)).thenReturn(17L);
|
when(counterMapper.next(TENANT_ID, "family-1", "template:NEW-{SERIAL:4}", 1L)).thenReturn(1L);
|
service.syncApplicationFlow(FLOW_ID, TASK_ID, 2, DmsFlowEventType.END);
|
assertEquals("QC-017", oldVersion.getWenjianBianhao());
|
assertEquals("NEW-0001", newVersion.getWenjianBianhao());
|
assertEquals("version-1", oldVersion.getGuizeKuaizhaoId());
|
verify(ruleMapper, never()).selectHistoricalById(any(), any());
|
}
|
|
@Test
|
void identicalTemplatesAcrossVersionsAndCompositionMethodsShareSequence() {
|
var fixed = frozenNumber("fixed", "v1", "family-1", false, "QC-A-{SERIAL:3}");
|
var custom = frozenNumber("custom", "v2", "family-1", true, "QC-A-{SERIAL:3}");
|
var legacy = frozenNumber("legacy", "v0", "family-1", false, "QC-A-{SERIAL:3}");
|
legacy.setGuizeKuaizhao("{\"guize_liushuihao\":\"family-1\"}");
|
prepareIssuance(List.of(fixed, custom, legacy));
|
when(counterMapper.next(TENANT_ID, "family-1", "template:QC-A-{SERIAL:3}", 1L))
|
.thenReturn(17L, 18L, 19L);
|
service.syncApplicationFlow(FLOW_ID, TASK_ID, 2, DmsFlowEventType.END);
|
assertEquals("QC-A-017", fixed.getWenjianBianhao());
|
assertEquals("QC-A-018", custom.getWenjianBianhao());
|
assertEquals("QC-A-019", legacy.getWenjianBianhao());
|
verify(counterMapper, times(3)).next(TENANT_ID, "family-1", "template:QC-A-{SERIAL:3}", 1L);
|
}
|
|
@Test
|
void customTemplatesShareOnlyWithinSameRuleFamilyAndAcquireInStableOrder() {
|
var b = frozenNumber("b", "v1", "family-1", true, "QC-B-{SERIAL:3}");
|
var a = frozenNumber("a", "v2", "family-1", true, "QC-A-{SERIAL:3}");
|
var aOld = frozenNumber("a-old", "v1", "family-1", true, "QC-A-{SERIAL:3}");
|
var other = frozenNumber("other", "v3", "family-2", true, "QC-A-{SERIAL:3}");
|
prepareIssuance(List.of(other, b, a, aOld));
|
when(counterMapper.next(TENANT_ID, "family-1", "template:QC-A-{SERIAL:3}", 1L)).thenReturn(17L, 18L);
|
when(counterMapper.next(TENANT_ID, "family-1", "template:QC-B-{SERIAL:3}", 1L)).thenReturn(1L);
|
when(counterMapper.next(TENANT_ID, "family-2", "template:QC-A-{SERIAL:3}", 1L)).thenReturn(1L);
|
service.syncApplicationFlow(FLOW_ID, TASK_ID, 2, DmsFlowEventType.END);
|
assertEquals("QC-A-017", a.getWenjianBianhao());
|
assertEquals("QC-A-018", aOld.getWenjianBianhao());
|
assertEquals("QC-B-001", b.getWenjianBianhao());
|
assertEquals("QC-A-001", other.getWenjianBianhao());
|
var order = org.mockito.Mockito.inOrder(counterMapper);
|
order.verify(counterMapper, times(2)).next(TENANT_ID, "family-1", "template:QC-A-{SERIAL:3}", 1L);
|
order.verify(counterMapper).next(TENANT_ID, "family-1", "template:QC-B-{SERIAL:3}", 1L);
|
order.verify(counterMapper).next(TENANT_ID, "family-2", "template:QC-A-{SERIAL:3}", 1L);
|
}
|
|
@Test
|
void legacySnapshotResolvesOnlyItsHistoricalVersionIncludingDisabledRecord() {
|
var n = frozenNumber("n", "v1", "family-1", false, "QC-{SERIAL:3}");
|
n.setGuizeKuaizhao("{\"parts\":[{\"type\":\"liushuihao\"}]}");
|
var historical = rule("v1", "旧规则");
|
historical.setBizEnabled("disabled");
|
historical.setDeleteMark(1);
|
when(ruleMapper.selectHistoricalById(TENANT_ID, "v1")).thenReturn(historical);
|
prepareIssuance(List.of(n));
|
when(counterMapper.next(TENANT_ID, "family-1", "template:QC-{SERIAL:3}", 1L)).thenReturn(17L);
|
service.syncApplicationFlow(FLOW_ID, TASK_ID, 2, DmsFlowEventType.END);
|
assertEquals("QC-017", n.getWenjianBianhao());
|
verify(ruleMapper, never()).selectActiveById(any(), any());
|
}
|
|
@Test
|
void unresolvedOrConflictingSnapshotFailsBeforeAllocating() {
|
var n = frozenNumber("n", "v1", "family-1", false, "QC-{SERIAL:3}");
|
var app = application(2);
|
when(applicationMapper.selectByFlowForUpdate(TENANT_ID, FLOW_ID, TASK_ID)).thenReturn(List.of(app));
|
when(numberMapper.selectByApplicationForUpdate(TENANT_ID, app.getId())).thenReturn(List.of(n));
|
for (String snapshot : List.of(
|
"{\"hasCustom\":false}",
|
"{\"guize_liushuihao\":\"a\",\"rule\":{\"guize_liushuihao\":\"b\"},\"hasCustom\":false}",
|
"[]")) {
|
n.setGuizeKuaizhao(snapshot);
|
assertThrows(DataException.class, () -> service.syncApplicationFlow(FLOW_ID, TASK_ID, 2, DmsFlowEventType.END));
|
}
|
verifyNoInteractions(counterMapper);
|
verify(numberMapper, never()).updateById(any(DmsWenjianBianhaoEntity.class));
|
}
|
|
@Test
|
void duplicateFinalNumberDoesNotSkipOrBorrowAnotherFamilySequence() {
|
var n = frozenNumber("n", "v3", "family-3", true, "QC-A-{SERIAL:3}");
|
var app = application(2);
|
when(applicationMapper.selectByFlowForUpdate(TENANT_ID, FLOW_ID, TASK_ID)).thenReturn(List.of(app));
|
when(numberMapper.selectByApplicationForUpdate(TENANT_ID, app.getId())).thenReturn(List.of(n));
|
when(counterMapper.next(TENANT_ID, "family-3", "template:QC-A-{SERIAL:3}", 1L)).thenReturn(1L);
|
when(numberMapper.updateById(n)).thenThrow(new org.springframework.dao.DuplicateKeyException("duplicate"));
|
assertThrows(org.springframework.dao.DuplicateKeyException.class,
|
() -> service.syncApplicationFlow(FLOW_ID, TASK_ID, 2, DmsFlowEventType.END));
|
verify(counterMapper, times(1)).next(TENANT_ID, "family-3", "template:QC-A-{SERIAL:3}", 1L);
|
org.mockito.Mockito.verifyNoMoreInteractions(counterMapper);
|
}
|
|
private DmsWenjianBianhaoEntity frozenNumber(String id, String version, String family,
|
boolean custom, String template) {
|
var n = number(id, version, "测试文件");
|
n.setGuizeKuaizhaoId(version);
|
n.setGuizeKuaizhao("{\"guize_liushuihao\":\"" + family + "\",\"hasCustom\":" + custom + "}");
|
n.setBianhaoMuban(template);
|
n.setBizStatus("pending_review");
|
return n;
|
}
|
|
private void prepareIssuance(List<DmsWenjianBianhaoEntity> numbers) {
|
var app = application(2);
|
when(applicationMapper.selectByFlowForUpdate(TENANT_ID, FLOW_ID, TASK_ID)).thenReturn(List.of(app));
|
when(numberMapper.selectByApplicationForUpdate(TENANT_ID, app.getId())).thenReturn(numbers);
|
when(numberMapper.updateById(any(DmsWenjianBianhaoEntity.class))).thenReturn(1);
|
}
|
|
private DmsWenjianBianhaoGuizeEntity rule(String id, String name) {
|
DmsWenjianBianhaoGuizeEntity rule = new DmsWenjianBianhaoGuizeEntity();
|
rule.setId(id);
|
rule.setGuizeLiushuihao("family-1");
|
rule.setTenantId(TENANT_ID);
|
rule.setGuizeMingcheng(name);
|
rule.setBizEnabled("enabled");
|
rule.setFlowState(2);
|
ObjectNode fields = new ObjectMapper().createObjectNode();
|
for (String key : Arrays.asList("guize_mingcheng", "guize_leixing", "shengcheng_bianhao",
|
"dengji", "shengxiao_nianxian", "wenjian_mulu", "shenqing_ren", "shenqing_riqi",
|
"biz_enabled", "biz_sign", "f_id", "f_tenant_id", "f_flow_id", "f_version",
|
"f_flow_task_id", "f_flow_state", "f_delete_mark", "f_delete_time", "f_delete_user_id",
|
"wenjian_mulu_id", "qianpiye_id", "qianpiye_files", "banbenhao", "guize_liushuihao")) {
|
fields.putNull(key);
|
}
|
fields.put("guize_liushuihao", "family-1");
|
fields.put("f_id", id).put("f_tenant_id", TENANT_ID)
|
.put("guize_mingcheng", name).put("biz_enabled", "enabled").put("f_flow_state", 2);
|
rule.setSnapshotData(fields.toString());
|
return rule;
|
}
|
|
private DmsWenjianBianhaoGuizeZixiangEntity part(
|
String id, String type, String value, String separator) {
|
DmsWenjianBianhaoGuizeZixiangEntity part = new DmsWenjianBianhaoGuizeZixiangEntity();
|
part.setId(id);
|
part.setTenantId(TENANT_ID);
|
part.setForeignId("rule-1");
|
part.setBianhaoZuheLeixing(type);
|
part.setZhi(value);
|
part.setFengefu(separator);
|
ObjectNode fields = new ObjectMapper().createObjectNode();
|
fields.put("guize_liushuihao", "family-1");
|
fields.put("f_id", id).put("f_tenant_id", TENANT_ID).put("f_foreign_id", "rule-1")
|
.put("bianhao_zuhe_leixing", type).put("zhi", value).put("fengefu", separator)
|
.putNull("f_delete_mark").putNull("f_delete_time").putNull("f_delete_user_id");
|
part.setSnapshotData(fields.toString());
|
return part;
|
}
|
}
|