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 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 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 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; } }