package jnpf.dmsService; import jnpf.dmsEntity.DmsWenjianBanbenEntity; import jnpf.dmsEntity.DmsWenjianZiyuanEntity; import jnpf.dmsEntity.banben.DmsWenjianBanbenJiesuanModel; import jnpf.dmsMapper.DmsWenjianBanbenMapper; import jnpf.dmsMapper.DmsWenjianGuanxiMapper; import jnpf.dmsMapper.DmsWenjianZiyuanMapper; import jnpf.exception.DataException; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.dao.DuplicateKeyException; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class DmsWenjianYewuBanbenServiceTest { @Mock private DmsWenjianZiyuanMapper resourceMapper; @Mock private DmsWenjianBanbenMapper versionMapper; @Mock private DmsWenjianGuanxiMapper relationMapper; @InjectMocks private DmsWenjianYewuBanbenService service; @Test void initialVersionCreatesResourceAndD00() throws Exception { when(resourceMapper.insertResource(any())).thenReturn(1); when(versionMapper.insertVersion(any())).thenReturn(1); when(resourceMapper.setInitialCurrentVersion(any())).thenReturn(1); DmsWenjianBanbenEntity result = service.createInitialVersion(initialModel()); assertEquals(0, result.getBanbenXuhao()); assertEquals("D(00)", result.getBanbenhao()); ArgumentCaptor resourceCaptor = ArgumentCaptor.forClass(DmsWenjianZiyuanEntity.class); verify(resourceMapper).insertResource(resourceCaptor.capture()); assertEquals("zhuwenjian", resourceCaptor.getValue().getWenjianLeixing()); assertEquals(result.getId(), resourceCaptor.getValue().getDangqianBanbenId()); } @Test void approvedVersionWritesCenterIdentityAndActualDates() { when(resourceMapper.insertResource(any())).thenReturn(1); when(versionMapper.insertVersion(any())).thenReturn(1); when(resourceMapper.setInitialCurrentVersion(any())).thenReturn(1); DmsWenjianBanbenJiesuanModel model = initialModel(); model.setCenterDocumentId("center-document"); model.setCenterVersionId("center-version"); model.setCenterVersionLabel("D(00)"); model.setBianhaoShiyongRenId("number-owner"); model.setZhidingRenId("author"); model.setZhidingrenBumenId("author-dept"); model.setZhidingRiqi(java.sql.Date.valueOf("2026-09-14")); model.setShenheLiuchengId("flow"); model.setShenheLiuchengRenwuId("task"); model.setWenjianDuixiangId("object"); java.util.Date effective = java.util.Date.from(java.time.Instant.parse("2026-09-15T02:00:00Z")); model.setEffectiveAt(effective); model.setEffectiveTo(java.util.Date.from(java.time.Instant.parse("2026-09-20T16:00:00Z"))); DmsWenjianBanbenEntity version = service.createInitialVersion(model); assertEquals("center-version", version.getWenjianZhongxinBanbenId()); assertEquals(effective, version.getShengxiaoShijian()); ArgumentCaptor resource = ArgumentCaptor.forClass(DmsWenjianZiyuanEntity.class); verify(resourceMapper).insertResource(resource.capture()); assertEquals("center-document", resource.getValue().getWenjianZhongxinWenjianId()); assertEquals(model.getEffectiveTo(), resource.getValue().getYouxiaoQiZhi()); assertEquals(effective, resource.getValue().getShengxiaoShijian()); assertEquals("guding_riqi", resource.getValue().getYouxiaoqiFangshi()); assertNull(resource.getValue().getShengxiaoNianxian()); assertEquals("center-version", resource.getValue().getWenjianZhongxinBanbenId()); assertEquals(version.getId(), resource.getValue().getDangqianBanbenId()); assertEquals("D(00)", resource.getValue().getDangqianBanbenhao()); assertEquals(0, resource.getValue().getDangqianBanbenXuhao()); assertEquals(effective, resource.getValue().getShouciShengxiaoShijian()); assertEquals("author", version.getZhidingRenId()); assertEquals("author-dept", resource.getValue().getZhidingrenBumenId()); assertEquals(model.getZhidingRiqi(), version.getZhidingRiqi()); assertEquals("number-owner", version.getBianhaoShiyongRenId()); assertEquals("object", version.getWenjianDuixiangId()); assertEquals("object", resource.getValue().getWenjianDuixiangId()); assertEquals("flow", resource.getValue().getShenheLiuchengId()); assertEquals("task", version.getShenheLiuchengRenwuId()); assertEquals(model.getLaiyuanDanjuId(), resource.getValue().getLaiyuanDanjuId()); assertEquals(model.getWenjianBianhao(), version.getWenjianBianhao()); assertEquals(model.getWenjianMingcheng(), version.getWenjianMingcheng()); assertEquals(model.getEffectiveTo(), version.getYouxiaoQiZhi()); assertEquals("guding_riqi", version.getYouxiaoqiFangshi()); } @Test void approvedOpenEndedVersionPersistsExplicitLongTermMode() { when(resourceMapper.insertResource(any())).thenReturn(1); when(versionMapper.insertVersion(any())).thenReturn(1); when(resourceMapper.setInitialCurrentVersion(any())).thenReturn(1); DmsWenjianBanbenJiesuanModel model = initialModel(); model.setEffectiveAt(new java.util.Date()); service.createInitialVersion(model); ArgumentCaptor resource = ArgumentCaptor.forClass(DmsWenjianZiyuanEntity.class); verify(resourceMapper).insertResource(resource.capture()); assertEquals("changqi", resource.getValue().getYouxiaoqiFangshi()); assertNull(resource.getValue().getYouxiaoQiZhi()); assertNull(resource.getValue().getShengxiaoNianxian()); } @Test void externalInitialVersionUsesApprovedStandardLabelWithoutIssuedNumberDetail() { when(resourceMapper.insertResource(any())).thenReturn(1); when(versionMapper.insertVersion(any())).thenReturn(1); when(resourceMapper.setInitialCurrentVersion(any())).thenReturn(1); DmsWenjianBanbenJiesuanModel model = initialModel(); model.setLaiyuanDanjuLeixing("wailai_wenjian_xinzeng_shenhe"); model.setWenjianLeixing("wailaiWenjian"); model.setWenjianBianhaoId(null); model.setCenterVersionLabel("D(01)"); model.setWenjianFenlei(null); DmsWenjianBanbenEntity version = service.createInitialVersion(model); assertEquals(0, version.getBanbenXuhao()); assertEquals("D(01)", version.getBanbenhao()); assertEquals("wailaiWenjian", version.getWenjianLeixing()); assertNull(version.getWenjianFenlei()); ArgumentCaptor resource = ArgumentCaptor.forClass(DmsWenjianZiyuanEntity.class); verify(resourceMapper).setInitialCurrentVersion(resource.capture()); assertEquals("wailaiWenjian", resource.getValue().getWenjianLeixing()); assertNull(resource.getValue().getWenjianFenlei()); assertEquals("D(01)", resource.getValue().getDangqianBanbenhao()); } @Test void externalFileTypeRequiresExternalApprovalSource() { DmsWenjianBanbenJiesuanModel model = initialModel(); model.setWenjianLeixing("wailaiWenjian"); assertThrows(DataException.class, () -> service.createInitialVersion(model)); model.setLaiyuanDanjuLeixing("wailai_wenjian_xinzeng_shenhe"); model.setWenjianLeixing("zhuwenjian"); assertThrows(DataException.class, () -> service.createInitialVersion(model)); } @Test void softDeletedResourceStillReservesItsNumber() { DmsWenjianZiyuanEntity historical = currentResource(); historical.setDeleteMark(1); when(resourceMapper.selectByNumberForUpdate("tenant-1", "DOC-001")) .thenReturn(historical); DataException exception = assertThrows(DataException.class, () -> service.createInitialVersion(initialModel())); assertEquals("文件编号已被占用,历史编号不可复用", exception.getMessage()); verify(resourceMapper, never()).insertResource(any()); verify(versionMapper, never()).insertVersion(any()); } @Test void historicalLedgerReferenceCannotCreateAnotherResourceWithDifferentNumber() { when(resourceMapper.selectByNumberDetailForUpdate("tenant-1", "number-1")) .thenReturn(currentResource()); assertThrows(DataException.class, () -> service.createInitialVersion(initialModel())); verify(resourceMapper, never()).insertResource(any()); } @Test void concurrentReservationConflictDoesNotCreateAVersion() { when(resourceMapper.insertResource(any())).thenThrow(new DuplicateKeyException("occupied")); DataException exception = assertThrows(DataException.class, () -> service.createInitialVersion(initialModel())); assertEquals("文件资源或编号已被占用,历史编号不可复用", exception.getMessage()); verify(versionMapper, never()).insertVersion(any()); } @Test void repeatedInitialSettlementReturnsExistingVersionWithoutReservingAgain() throws Exception { DmsWenjianBanbenEntity existing = currentVersion(); existing.setWenjianNeirong("content"); existing.setWenjianMuluId("folder-1"); existing.setWenjianLeixing("zhuwenjian"); when(versionMapper.selectBySourceBusinessKey("tenant-1", "new:audit-1")) .thenReturn(existing); assertEquals(existing, service.createInitialVersion(initialModel())); verify(resourceMapper, never()).selectByNumberForUpdate(any(), any()); verify(resourceMapper, never()).insertResource(any()); } @Test void revisionCreatesNextImmutableVersionAndAdvancesPointer() throws Exception { DmsWenjianBanbenJiesuanModel model = revisionModel(); DmsWenjianZiyuanEntity resource = currentResource(); java.util.Date first = java.sql.Timestamp.valueOf("2026-01-01 09:00:00"); resource.setShouciShengxiaoShijian(first); resource.setWenjianBianhao("DOC-001"); resource.setWenjianBianhaoId("number-1"); resource.setBianhaoShiyongRenId("original-number-owner"); resource.setWenjianFenlei("existing-class"); model.setEffectiveAt(java.sql.Timestamp.valueOf("2026-09-15 18:00:00")); model.setCenterVersionId("new-center-version"); model.setWenjianDuixiangId("new-object"); model.setLaiyuanZixiangId("record-row"); model.setZhidingRenId("new-author"); DmsWenjianBanbenEntity current = currentVersion(); when(resourceMapper.selectByIdForUpdate("tenant-1", "file-1")).thenReturn(resource); when(versionMapper.selectById("tenant-1", "version-1")).thenReturn(current); when(versionMapper.markSuperseded("tenant-1", "version-1", "user-1", "修订升版,由 D(02) 替代")) .thenReturn(1); when(versionMapper.insertVersion(any())).thenReturn(1); when(resourceMapper.advanceCurrentVersion(any(), org.mockito.ArgumentMatchers.eq("version-1"))) .thenReturn(1); DmsWenjianBanbenEntity result = service.createRevisionVersion(model); assertEquals(first, resource.getShouciShengxiaoShijian()); assertEquals("new-center-version", resource.getWenjianZhongxinBanbenId()); assertEquals("new-object", resource.getWenjianDuixiangId()); assertEquals("new-author", result.getZhidingRenId()); assertEquals("D(02)", resource.getDangqianBanbenhao()); assertEquals("record-row", resource.getLaiyuanZixiangId()); assertEquals("DOC-001", result.getWenjianBianhao()); assertEquals("original-number-owner", result.getBianhaoShiyongRenId()); assertEquals("existing-class", result.getWenjianFenlei()); assertEquals(2, result.getBanbenXuhao()); assertEquals("D(02)", result.getBanbenhao()); assertEquals("version-1", result.getYuanWenjianBanbenId()); verify(versionMapper).markSuperseded("tenant-1", "version-1", "user-1", "修订升版,由 D(02) 替代"); verify(resourceMapper).advanceCurrentVersion(resource, "version-1"); } @Test void repeatedBusinessKeyReturnsOriginalVersionEvenAfterItWasSuperseded() throws Exception { DmsWenjianBanbenJiesuanModel model = revisionModel(); DmsWenjianBanbenEntity repeated = currentVersion(); repeated.setYuanWenjianBanbenId("version-1"); repeated.setWenjianNeirong("new-content"); repeated.setBizStatus("superseded"); when(versionMapper.selectBySourceBusinessKey("tenant-1", "revision:audit-1")) .thenReturn(repeated); DmsWenjianBanbenEntity result = service.createRevisionVersion(model); assertEquals(repeated, result); verify(resourceMapper, never()).selectByIdForUpdate(any(), any()); verify(versionMapper, never()).insertVersion(any()); } @Test void staleSourceVersionIsRejectedBeforeVersionMutation() { DmsWenjianZiyuanEntity resource = currentResource(); resource.setDangqianBanbenId("version-2"); when(resourceMapper.selectByIdForUpdate("tenant-1", "file-1")).thenReturn(resource); DataException exception = assertThrows(DataException.class, () -> service.createRevisionVersion(revisionModel())); assertEquals("来源文件版本已不是当前有效版本,请重新发起申请", exception.getMessage()); verify(versionMapper, never()).markSuperseded(any(), any(), any(), any()); } @Test void unchangedVersionRequiresExpectedCurrentPointer() { DmsWenjianZiyuanEntity resource = currentResource(); resource.setDangqianBanbenId("version-2"); when(resourceMapper.selectByIdForUpdate("tenant-1", "file-1")).thenReturn(resource); DataException exception = assertThrows(DataException.class, () -> service.reuseCurrentVersion("tenant-1", "file-1", "version-1")); assertEquals("来源文件版本已不是当前有效版本,请重新发起申请", exception.getMessage()); } @Test void recordAttributesAreCopiedToVersionAndResourceAndCheckedOnRetry() { DmsWenjianBanbenJiesuanModel model = initialModel(); model.setWenjianLeixing("jilu"); model.setWenjianBianhaoId(null); model.setJiluLeixing("benJilu"); model.setDengji("A"); model.setShifouShoukong("yes"); model.setJiluDengjiGuanliId("config-1"); model.setJiluDengjiGuanliBanben(3); when(resourceMapper.insertResource(any())).thenReturn(1); when(versionMapper.insertVersion(any())).thenReturn(1); when(resourceMapper.setInitialCurrentVersion(any())).thenReturn(1); DmsWenjianBanbenEntity result = service.createInitialVersion(model); assertEquals("folder-1", result.getWenjianMuluId()); assertEquals("benJilu", result.getJiluLeixing()); assertEquals("A", result.getDengji()); assertEquals("yes", result.getShifouShoukong()); assertEquals(3, result.getJiluDengjiGuanliBanben()); ArgumentCaptor saved = ArgumentCaptor.forClass(DmsWenjianZiyuanEntity.class); verify(resourceMapper).insertResource(saved.capture()); assertEquals("yes", saved.getValue().getShifouShoukong()); assertEquals("folder-1", saved.getValue().getWenjianMuluId()); when(versionMapper.selectBySourceBusinessKey(model.getTenantId(), model.getLaiyuanYewuJian())).thenReturn(result); assertEquals(result, service.createInitialVersion(model)); model.setShifouShoukong("no"); assertThrows(DataException.class, () -> service.createInitialVersion(model)); } @Test void defaultUncontrolledSnapshotSurvivesSettlementAndRetryWithoutConfiguration() { DmsWenjianBanbenJiesuanModel model = initialModel(); model.setWenjianLeixing("jilu"); model.setJiluLeixing("pishengchanJilu"); model.setDengji("B"); model.setShifouShoukong("no"); when(resourceMapper.insertResource(any())).thenReturn(1); when(versionMapper.insertVersion(any())).thenReturn(1); when(resourceMapper.setInitialCurrentVersion(any())).thenReturn(1); DmsWenjianBanbenEntity result = service.createInitialVersion(model); assertEquals("no", result.getShifouShoukong()); assertEquals(null, result.getJiluDengjiGuanliId()); assertEquals(null, result.getJiluDengjiGuanliBanben()); ArgumentCaptor saved = ArgumentCaptor.forClass(DmsWenjianZiyuanEntity.class); verify(resourceMapper).insertResource(saved.capture()); assertEquals("no", saved.getValue().getShifouShoukong()); assertEquals(null, saved.getValue().getJiluDengjiGuanliId()); when(versionMapper.selectBySourceBusinessKey(model.getTenantId(), model.getLaiyuanYewuJian())) .thenReturn(result); assertEquals(result, service.createInitialVersion(model)); } @org.junit.jupiter.params.ParameterizedTest @org.junit.jupiter.params.provider.ValueSource(strings = {"yes", "no"}) void auditRecordModePersistsWithoutTypeGradeOrConfiguration(String controlled) { DmsWenjianBanbenJiesuanModel model = initialModel(); model.setWenjianLeixing("jilu"); model.setWenjianBianhaoId(null); model.setShifouShoukong(controlled); when(resourceMapper.insertResource(any())).thenReturn(1); when(versionMapper.insertVersion(any())).thenReturn(1); when(resourceMapper.setInitialCurrentVersion(any())).thenReturn(1); DmsWenjianBanbenEntity result = service.createInitialVersion(model); assertEquals(controlled, result.getShifouShoukong()); assertNull(result.getJiluLeixing()); assertNull(result.getDengji()); assertNull(result.getJiluDengjiGuanliId()); assertNull(result.getJiluDengjiGuanliBanben()); ArgumentCaptor saved = ArgumentCaptor.forClass(DmsWenjianZiyuanEntity.class); verify(resourceMapper).insertResource(saved.capture()); assertEquals(controlled, saved.getValue().getShifouShoukong()); when(versionMapper.selectBySourceBusinessKey(model.getTenantId(), model.getLaiyuanYewuJian())).thenReturn(result); assertEquals(result, service.createInitialVersion(model)); model.setShifouShoukong("yes".equals(controlled) ? "no" : "yes"); assertThrows(DataException.class, () -> service.createInitialVersion(model)); } @org.junit.jupiter.params.ParameterizedTest @org.junit.jupiter.params.provider.ValueSource(strings = {"yes", "no"}) void revisionRecordModePersistsWithoutGradeConfiguration(String controlled) { DmsWenjianBanbenJiesuanModel model = revisionModel(); model.setWenjianLeixing("jilu"); model.setShifouShoukong(controlled); DmsWenjianZiyuanEntity resource = currentResource(); resource.setWenjianLeixing("jilu"); DmsWenjianBanbenEntity current = currentVersion(); current.setWenjianLeixing("jilu"); when(resourceMapper.selectByIdForUpdate("tenant-1", "file-1")).thenReturn(resource); when(versionMapper.selectById("tenant-1", "version-1")).thenReturn(current); when(versionMapper.markSuperseded(any(), any(), any(), any())).thenReturn(1); when(versionMapper.insertVersion(any())).thenReturn(1); when(resourceMapper.advanceCurrentVersion(any(), any())).thenReturn(1); DmsWenjianBanbenEntity result = service.createRevisionVersion(model); assertEquals(controlled, result.getShifouShoukong()); assertEquals(controlled, resource.getShifouShoukong()); assertNull(result.getJiluLeixing()); assertNull(result.getDengji()); assertEquals("version-1", result.getYuanWenjianBanbenId()); } @org.junit.jupiter.params.ParameterizedTest @org.junit.jupiter.params.provider.NullAndEmptySource @org.junit.jupiter.params.provider.ValueSource(strings = {"online", "offline", "YES"}) void invalidAuditControlFlagIsRejectedBeforePersistence(String controlled) { DmsWenjianBanbenJiesuanModel model = initialModel(); model.setWenjianLeixing("jilu"); model.setShifouShoukong(controlled); assertThrows(DataException.class, () -> service.createInitialVersion(model)); verify(versionMapper, never()).insertVersion(any()); verify(resourceMapper, never()).insertResource(any()); } @org.junit.jupiter.params.ParameterizedTest @org.junit.jupiter.params.provider.ValueSource(strings = {"yes", "no"}) void reusedAttachmentUpdatesMetadataWithoutCreatingVersion(String mode) { var model=revisionModel();model.setWenjianLeixing("jilu");model.setShifouShoukong(mode); model.setWenjianMingcheng("updated name");model.setGuishuBumenId("department"); model.setCenterVersionId("center-version"); var current=currentVersion();current.setWenjianLeixing("jilu"); current.setWenjianZhongxinBanbenId("center-version"); when(resourceMapper.selectByIdForUpdate("tenant-1","file-1")).thenReturn(currentResource()); when(versionMapper.selectById("tenant-1","version-1")).thenReturn(current); when(versionMapper.updateReusedRecordMetadata(model)).thenReturn(1); when(resourceMapper.updateReusedRecordMetadata(model)).thenReturn(1); var result=service.updateReusedRecordMetadata(model); assertEquals(mode,result.getShifouShoukong()); assertEquals("updated name",result.getWenjianMingcheng()); assertEquals("department",result.getGuishuBumenId()); assertEquals("version-1",result.getId());assertEquals("D(01)",result.getBanbenhao()); assertEquals("content",result.getWenjianNeirong()); assertEquals("center-version",result.getWenjianZhongxinBanbenId()); verify(versionMapper,never()).insertVersion(any()); verify(resourceMapper,never()).advanceCurrentVersion(any(),any()); } @Test void reusedAttachmentRejectsDifferentCenterVersion() { var model=revisionModel();model.setWenjianLeixing("jilu");model.setShifouShoukong("yes"); model.setWenjianMingcheng("name");model.setGuishuBumenId("department"); model.setCenterVersionId("replacement"); var current=currentVersion();current.setWenjianLeixing("jilu"); current.setWenjianZhongxinBanbenId("original"); when(resourceMapper.selectByIdForUpdate("tenant-1","file-1")).thenReturn(currentResource()); when(versionMapper.selectById("tenant-1","version-1")).thenReturn(current); assertThrows(DataException.class,()->service.updateReusedRecordMetadata(model)); verify(versionMapper,never()).updateReusedRecordMetadata(any()); verify(resourceMapper,never()).updateReusedRecordMetadata(any()); } private DmsWenjianBanbenJiesuanModel initialModel() { DmsWenjianBanbenJiesuanModel model = commonModel(); model.setWenjianLeixing("zhuwenjian"); model.setWenjianBianhaoId("number-1"); model.setWenjianBianhao("DOC-001"); model.setWenjianMingcheng("SOP文件"); model.setLaiyuanDanjuLeixing("xinzeng_shenhe"); model.setLaiyuanYewuJian("new:audit-1"); return model; } private DmsWenjianBanbenJiesuanModel revisionModel() { DmsWenjianBanbenJiesuanModel model = commonModel(); model.setWenjianId("file-1"); model.setWenjianLeixing("zhuwenjian"); model.setYuanWenjianBanbenId("version-1"); model.setWenjianNeirong("new-content"); model.setLaiyuanDanjuLeixing("xiuding_shenhe"); model.setLaiyuanYewuJian("revision:audit-1"); return model; } private DmsWenjianBanbenJiesuanModel commonModel() { DmsWenjianBanbenJiesuanModel model = new DmsWenjianBanbenJiesuanModel(); model.setTenantId("tenant-1"); model.setWenjianMuluId("folder-1"); model.setWenjianNeirong("content"); model.setLaiyuanDanjuId("audit-1"); model.setUserId("user-1"); return model; } private DmsWenjianZiyuanEntity currentResource() { DmsWenjianZiyuanEntity resource = new DmsWenjianZiyuanEntity(); resource.setId("file-1"); resource.setTenantId("tenant-1"); resource.setWenjianMuluId("folder-1"); resource.setWenjianLeixing("zhuwenjian"); resource.setWenjianMingcheng("SOP文件"); resource.setDangqianBanbenId("version-1"); resource.setBizStatus("effective"); return resource; } private DmsWenjianBanbenEntity currentVersion() { DmsWenjianBanbenEntity version = new DmsWenjianBanbenEntity(); version.setId("version-1"); version.setTenantId("tenant-1"); version.setWenjianMuluId("folder-1"); version.setWenjianLeixing("zhuwenjian"); version.setWenjianId("file-1"); version.setBanbenXuhao(1); version.setBanbenhao("D(01)"); version.setWenjianNeirong("content"); version.setLaiyuanYewuJian("revision:audit-1"); version.setBizStatus("effective"); return version; } }