package jnpf.dmsService; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; import jnpf.base.UserInfo; import jnpf.base.DictionaryDataApi; import jnpf.base.entity.DictionaryDataEntity; import jnpf.dmsEntity.watermark.DmsWatermarkParameters; import jnpf.dmsEntity.permission.entity.DmsDocumentEntity; import jnpf.dmsEntity.permission.entity.DmsFileVersionEntity; import jnpf.dmsEntity.preview.model.DmsWatermarkSnapshot; import jnpf.dmsEntity.watermark.DmsWatermarkConfigEntity; import jnpf.dmsEntity.watermark.DmsWatermarkPositionEntity; import jnpf.dmsIntegration.audit.DmsVersionStateAuditRow; import jnpf.dmsMapper.DmsDocumentMapper; import jnpf.dmsMapper.DmsVersionStateMapper; import jnpf.dmsMapper.DmsWatermarkConfigMapper; import jnpf.dmsMapper.DmsWatermarkPositionMapper; import jnpf.permission.OrganizeApi; import org.apache.ibatis.builder.MapperBuilderAssistant; import org.apache.ibatis.session.Configuration; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.ArgumentCaptor; import org.mockito.junit.jupiter.MockitoExtension; import java.math.BigDecimal; import java.util.Collections; import java.util.List; import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.Mockito.lenient; @ExtendWith(MockitoExtension.class) class DmsWatermarkPreviewResolverTest { private static final UUID VERSION_ID = UUID.fromString("0199d0e5-8e70-7000-8000-000000000010"); private static final UUID DOCUMENT_ID = UUID.fromString("0199d0e5-8e70-7000-8000-000000000011"); private static final UUID CONFIG_ID = UUID.fromString("0199d0e5-8e70-7000-8000-000000000012"); @Mock private DmsWatermarkConfigMapper configMapper; @Mock private DmsWatermarkPositionMapper positionMapper; @Mock private DmsDocumentMapper documentMapper; @Mock private DmsVersionStateMapper versionStateMapper; @Mock private OrganizeApi organizeApi; @Mock private DictionaryDataApi dictionaryDataApi; private DmsWatermarkPreviewResolver resolver; @BeforeEach void setUp() { initTable(DmsWatermarkConfigEntity.class); initTable(DmsWatermarkPositionEntity.class); resolver = new DmsWatermarkPreviewResolver(configMapper, positionMapper, documentMapper, versionStateMapper, organizeApi, dictionaryDataApi); DictionaryDataEntity entry = new DictionaryDataEntity(); entry.setDictionaryTypeId("879384732964547653"); entry.setEnCode("RECORD"); entry.setEnabledMark(1); lenient().when(dictionaryDataApi.getList("879384732964547653", "true")).thenReturn(List.of(entry)); lenient().when(configMapper.selectMatchingConfigIds(any(), any(), any())).thenReturn(List.of(CONFIG_ID)); } @Test void resolvesPreviewWatermarkForCurrentDocxAndReplacesTrustedVariables() { UserInfo user = user(); user.setOrganizeIds(Collections.emptyList()); DmsVersionStateAuditRow state = new DmsVersionStateAuditRow(); state.setLifecycleStatus("EFFECTIVE"); when(versionStateMapper.selectAuditState("tenant-1", VERSION_ID)).thenReturn(state); DmsWatermarkConfigEntity aligned = config(); aligned.setHorizontalAlign("RIGHT"); aligned.setVerticalAlign("MIDDLE"); when(configMapper.selectList(any())).thenReturn(Collections.singletonList(aligned)); when(positionMapper.selectList(any())).thenReturn(Collections.singletonList(position())); DmsDocumentEntity document = new DmsDocumentEntity(); document.setDocumentNo("SOP-001"); when(documentMapper.selectByTenantAndId("tenant-1", DOCUMENT_ID)).thenReturn(document); List result = resolver.resolve(user, "tenant-1", version("manual.docx"), types()); assertEquals(1, result.size()); assertEquals("张三 / 质量部 / SOP-001 / V1.0 / 现行版本", result.get(0).getContent()); assertEquals(1, result.get(0).getPositions().size()); for (DmsWatermarkSnapshot snapshot : List.of(result.get(0), resolver.resolveDownload(user, "tenant-1", version("manual.docx"), types()).get(0), resolver.resolvePrint(user, "tenant-1", version("manual.docx"), types()).get(0))) { assertEquals("RIGHT", snapshot.getHorizontalAlign()); assertEquals("MIDDLE", snapshot.getVerticalAlign()); } } @Test void copiesBarcodeTextDisplayIntoPreviewSnapshot() { DmsVersionStateAuditRow state = new DmsVersionStateAuditRow(); state.setLifecycleStatus("EFFECTIVE"); when(versionStateMapper.selectAuditState("tenant-1", VERSION_ID)).thenReturn(state); DmsWatermarkConfigEntity config = config(); config.setWatermarkType("BARCODE"); config.setBarcodeFormat("QR_CODE"); config.setContentTemplate("{/FILENO}"); config.setShowBarcodeText(true); when(configMapper.selectList(any())).thenReturn(Collections.singletonList(config)); when(positionMapper.selectList(any())).thenReturn(Collections.singletonList(position())); DmsDocumentEntity document = new DmsDocumentEntity(); document.setDocumentNo("SOP-001"); when(documentMapper.selectByTenantAndId("tenant-1", DOCUMENT_ID)).thenReturn(document); List result = resolver.resolve(user(), "tenant-1", version("manual.docx"), types()); assertEquals(Boolean.TRUE, result.get(0).getShowBarcodeText()); assertEquals("SOP-001", result.get(0).getContent()); } @Test void downloadUsesDownloadSceneConfiguration() { DmsVersionStateAuditRow state = new DmsVersionStateAuditRow(); state.setLifecycleStatus("EFFECTIVE"); when(versionStateMapper.selectAuditState("tenant-1", VERSION_ID)).thenReturn(state); when(configMapper.selectList(any())).thenReturn(Collections.singletonList(config())); when(positionMapper.selectList(any())).thenReturn(Collections.singletonList(position())); resolver.resolveDownload(user(), "tenant-1", version("manual.docx"), types()); @SuppressWarnings("unchecked") ArgumentCaptor> captor = ArgumentCaptor.forClass(LambdaQueryWrapper.class); verify(configMapper).selectList(captor.capture()); String sql = captor.getValue().getSqlSegment(); assertTrue(sql.contains("load_download")); assertTrue(!sql.contains("load_preview")); } @Test void printUsesPrintSceneConfiguration() { DmsVersionStateAuditRow state = new DmsVersionStateAuditRow(); state.setLifecycleStatus("EFFECTIVE"); when(versionStateMapper.selectAuditState("tenant-1", VERSION_ID)).thenReturn(state); when(configMapper.selectList(any())).thenReturn(Collections.singletonList(config())); when(positionMapper.selectList(any())).thenReturn(Collections.singletonList(position())); resolver.resolvePrint(user(), "tenant-1", version("manual.docx"), types()); @SuppressWarnings("unchecked") ArgumentCaptor> captor = ArgumentCaptor.forClass(LambdaQueryWrapper.class); verify(configMapper).selectList(captor.capture()); String sql = captor.getValue().getSqlSegment(); assertTrue(sql.contains("load_print")); assertTrue(!sql.contains("load_preview")); assertTrue(!sql.contains("load_download")); } @Test void rejectsChineseFileNameInCode128BeforeIssuingPreview() { DmsVersionStateAuditRow state = new DmsVersionStateAuditRow(); state.setLifecycleStatus("EFFECTIVE"); when(versionStateMapper.selectAuditState("tenant-1", VERSION_ID)).thenReturn(state); DmsWatermarkConfigEntity config = config(); config.setName("测试2"); config.setWatermarkType("BARCODE"); config.setBarcodeFormat("CODE_128"); config.setContentTemplate("{/FILENAME}-{/VERSION}"); when(configMapper.selectList(any())).thenReturn(Collections.singletonList(config)); when(positionMapper.selectList(any())).thenReturn(Collections.singletonList(position())); jnpf.exception.DataException error = assertThrows(jnpf.exception.DataException.class, () -> resolver.resolve(user(), "tenant-1", version("玻思韬.docx"), types())); assertTrue(error.getMessage().contains("测试2")); assertTrue(error.getMessage().contains("请将条码格式改为二维码")); } @Test void historicalVersionUsesAllScopeWatermarksAndHistoricalStateText() { DmsVersionStateAuditRow state = new DmsVersionStateAuditRow(); state.setLifecycleStatus("SUPERSEDED"); when(versionStateMapper.selectAuditState("tenant-1", VERSION_ID)).thenReturn(state); DmsWatermarkConfigEntity allVersions = config(); allVersions.setVersionScope("ALL"); when(configMapper.selectList(any())).thenReturn(Collections.singletonList(allVersions)); when(positionMapper.selectList(any())).thenReturn(Collections.singletonList(position())); List result = resolver.resolve(user(), "tenant-1", version("manual.docx"), types()); assertEquals(1, result.size()); assertTrue(result.get(0).getContent().contains("历史版本")); @SuppressWarnings("unchecked") ArgumentCaptor> captor = ArgumentCaptor.forClass(LambdaQueryWrapper.class); verify(configMapper).selectList(captor.capture()); String sql = captor.getValue().getSqlSegment(); assertTrue(sql.contains("version_scope"), sql); assertTrue(!sql.contains("version_scope IN"), sql); } @Test void pendingVersionDoesNotResolveWatermarks() { DmsVersionStateAuditRow state = new DmsVersionStateAuditRow(); state.setLifecycleStatus("PENDING"); when(versionStateMapper.selectAuditState("tenant-1", VERSION_ID)).thenReturn(state); assertEquals(0, resolver.resolve(user(), "tenant-1", version("manual.docx"), types()).size()); verify(configMapper, never()).selectList(any()); } @Test void resolvesPreviewWatermarkForSupportedImages() { DmsVersionStateAuditRow state = new DmsVersionStateAuditRow(); state.setLifecycleStatus("EFFECTIVE"); when(versionStateMapper.selectAuditState("tenant-1", VERSION_ID)).thenReturn(state); when(configMapper.selectList(any())).thenReturn(Collections.singletonList(config())); when(positionMapper.selectList(any())).thenReturn(Collections.singletonList(position())); assertEquals(1, resolver.resolve(user(), "tenant-1", version("diagram.PNG"), types()).size()); assertEquals(1, resolver.resolve(user(), "tenant-1", version("photo.jpg"), types()).size()); DmsFileVersionEntity mimeOnlyImage = version("diagram"); mimeOnlyImage.setMimeType("image/png"); assertEquals(1, resolver.resolve(user(), "tenant-1", mimeOnlyImage, types()).size()); } @Test void resolvesWatermarksForPdfInEveryScene() { DmsVersionStateAuditRow state = new DmsVersionStateAuditRow(); state.setLifecycleStatus("EFFECTIVE"); when(versionStateMapper.selectAuditState("tenant-1", VERSION_ID)).thenReturn(state); when(configMapper.selectList(any())).thenReturn(Collections.singletonList(config())); when(positionMapper.selectList(any())).thenReturn(Collections.singletonList(position())); DmsFileVersionEntity pdf = version("manual.pdf"); assertEquals(1, resolver.resolve(user(), "tenant-1", pdf, types()).size()); assertEquals(1, resolver.resolveDownload(user(), "tenant-1", pdf, types()).size()); assertEquals(1, resolver.resolvePrint(user(), "tenant-1", pdf, types()).size()); } @Test void eachRuntimeEntryProducesItsOwnBarcodeWithIndependentPayloadAndLabelInEveryScene() { customAndOrdinaryConfigurations(); jnpf.dmsEntity.watermark.DmsWatermarkParameters parameters = new jnpf.dmsEntity.watermark.DmsWatermarkParameters(); parameters.setCustomBarcode(List.of(entry(5, "REC-005", "第五页"), entry(4, "记录 {/CUN} <&>", "文字 {/CDN} <&>"), entry(4, "REC-SECOND-004", "同页第二条"), entry(null, "EVERY-PAGE", "每页"))); parameters.setWatermarkBusinessTypes(List.of("RECORD")); for (List snapshots : List.of( resolver.resolve(user(), "tenant-1", version("manual.docx"), parameters), resolver.resolveDownload(user(), "tenant-1", version("manual.docx"), parameters), resolver.resolvePrint(user(), "tenant-1", version("manual.docx"), parameters))) { assertEquals(5, snapshots.size()); assertEquals("REC-005", snapshots.get(0).getContent()); assertEquals("第五页", snapshots.get(0).getBarcodeText()); assertEquals(List.of(5), snapshots.get(0).getRenderPages()); assertEquals(Boolean.TRUE, snapshots.get(0).getShowBarcodeText()); assertEquals("记录 {/CUN} <&>", snapshots.get(1).getContent()); assertEquals("文字 {/CDN} <&>", snapshots.get(1).getBarcodeText()); assertEquals(List.of(4), snapshots.get(1).getRenderPages()); assertEquals("REC-SECOND-004", snapshots.get(2).getContent()); assertEquals(List.of(4), snapshots.get(2).getRenderPages()); org.junit.jupiter.api.Assertions.assertNull(snapshots.get(3).getRenderPages()); org.junit.jupiter.api.Assertions.assertNull(snapshots.get(4).getRenderPages()); assertTrue(snapshots.get(4).getContent().contains("张三")); } } @Test void frozenCopySerialUsesExistingCustomBarcodeSourceWithOrWithoutRequiredGuard() { customAndOrdinaryConfigurations(); var request = new jnpf.dmsEntity.print.form.DmsFilePrintTicketRequest(); request.setCustomBarcode(List.of(entry(null, "20261003000001", "打印流水号: 20261003000001 计数流水号: 20260001"))); request.setWatermarkBusinessTypes(List.of("RECORD")); for (boolean required : List.of(false, true)) { request.setRequireSerialWatermark(required); var result = resolver.resolvePrint(user(), "tenant-1", version("manual.docx"), request); assertEquals(2, result.size()); assertEquals(Boolean.TRUE, result.getFirst().getCustomBarcode()); assertEquals("20261003000001", result.getFirst().getContent()); assertEquals("打印流水号: 20261003000001 计数流水号: 20260001", result.getFirst().getBarcodeText()); org.junit.jupiter.api.Assertions.assertNull(result.getFirst().getRenderPages()); assertEquals(1, result.getFirst().getPositions().size()); assertEquals("TEXT", result.get(1).getWatermarkType()); } } @Test void absentRuntimeTextSkipsCustomConfigurationAndPreservesOrdinaryWatermarks() { customAndOrdinaryConfigurations(); List snapshots = resolver.resolve(user(), "tenant-1", version("manual.docx"), types()); assertEquals(1, snapshots.size()); assertEquals("TEXT", snapshots.get(0).getWatermarkType()); } @Test void entryWithoutPageProducesEveryPageSnapshot() { customAndOrdinaryConfigurations(); jnpf.dmsEntity.watermark.DmsWatermarkParameters parameters = new jnpf.dmsEntity.watermark.DmsWatermarkParameters(); parameters.setCustomBarcode(List.of(entry(null, "REC-001", "Independent label"))); parameters.setWatermarkBusinessTypes(List.of("RECORD")); List snapshots = resolver.resolve(user(), "tenant-1", version("manual.docx"), parameters); assertEquals("REC-001", snapshots.get(0).getContent()); assertEquals("Independent label", snapshots.get(0).getBarcodeText()); org.junit.jupiter.api.Assertions.assertNull(snapshots.get(0).getRenderPages()); } private jnpf.dmsEntity.watermark.DmsWatermarkParameters.CustomBarcode entry(Integer page, String value, String text) { jnpf.dmsEntity.watermark.DmsWatermarkParameters.CustomBarcode entry = new jnpf.dmsEntity.watermark.DmsWatermarkParameters.CustomBarcode(); entry.setPage(page); entry.setBarValue(value); entry.setBarText(text); return entry; } private DmsWatermarkParameters types() { DmsWatermarkParameters parameters = new DmsWatermarkParameters(); parameters.setWatermarkBusinessTypes(List.of("RECORD")); return parameters; } @Test void emptyBusinessTypesDoNotQueryWatermarkConfigurations() { assertTrue(resolver.resolve(user(), "tenant-1", version("manual.docx")).isEmpty()); DmsWatermarkParameters empty = new DmsWatermarkParameters(); empty.setWatermarkBusinessTypes(List.of()); assertTrue(resolver.resolveDownload(user(), "tenant-1", version("manual.docx"), empty).isEmpty()); verify(configMapper, never()).selectList(any()); } @Test void multipleBusinessTypesSelectEachConfigurationOnce() { DictionaryDataEntity second = new DictionaryDataEntity(); second.setDictionaryTypeId("879384732964547653"); second.setEnCode("REPORT"); second.setEnabledMark(1); DictionaryDataEntity first = new DictionaryDataEntity(); first.setDictionaryTypeId("879384732964547653"); first.setEnCode("RECORD"); first.setEnabledMark(1); when(dictionaryDataApi.getList("879384732964547653", "true")).thenReturn(List.of(first, second)); DmsVersionStateAuditRow state = new DmsVersionStateAuditRow(); state.setLifecycleStatus("EFFECTIVE"); when(versionStateMapper.selectAuditState("tenant-1", VERSION_ID)).thenReturn(state); when(configMapper.selectList(any())).thenReturn(List.of(config())); when(positionMapper.selectList(any())).thenReturn(List.of(position())); DmsWatermarkParameters parameters = new DmsWatermarkParameters(); parameters.setWatermarkBusinessTypes(List.of("RECORD", "REPORT")); assertEquals(1, resolver.resolve(user(), "tenant-1", version("manual.docx"), parameters).size()); verify(configMapper).selectMatchingConfigIds(any(), any(), any()); } private void customAndOrdinaryConfigurations() { DmsVersionStateAuditRow state = new DmsVersionStateAuditRow(); state.setLifecycleStatus("EFFECTIVE"); when(versionStateMapper.selectAuditState("tenant-1", VERSION_ID)).thenReturn(state); DmsWatermarkConfigEntity custom = config(); custom.setWatermarkType("BARCODE"); custom.setBarcodeFormat("QR_CODE"); custom.setShowBarcodeText(false); custom.setContentTemplate(jnpf.dmsEntity.watermark.DmsWatermarkParameters.CUSTOM_BARCODE_TEMPLATE); when(configMapper.selectList(any())).thenReturn(List.of(custom, config())); when(positionMapper.selectList(any())).thenReturn(List.of(position())); } private UserInfo user() { UserInfo user = new UserInfo(); user.setUserId("user-1"); user.setUserName("张三"); user.setOrganize("质量部"); user.setOrganizeIds(Collections.singletonList("department-1")); return user; } private DmsFileVersionEntity version(String fileName) { DmsFileVersionEntity version = new DmsFileVersionEntity(); version.setId(VERSION_ID); version.setDocumentId(DOCUMENT_ID); version.setFileName(fileName); version.setVersionLabel("V1.0"); return version; } private DmsWatermarkConfigEntity config() { DmsWatermarkConfigEntity config = new DmsWatermarkConfigEntity(); config.setId(CONFIG_ID); config.setWatermarkType("TEXT"); config.setContentTemplate("{/CUN} / {/CDN} / {/FILENO} / {/VERSION} / {/FILESTATE}"); config.setFontFamily("SimHei"); config.setFontColor("#666666"); config.setFontSize(24); config.setRotationAngle(-30); config.setOpacity(30); config.setLayoutMode("POSITION"); config.setMarkWidthMm(new BigDecimal("80")); config.setMarkHeightMm(new BigDecimal("20")); return config; } private DmsWatermarkPositionEntity position() { DmsWatermarkPositionEntity position = new DmsWatermarkPositionEntity(); position.setConfigId(CONFIG_ID); position.setPaperType("A4"); position.setPaperWidthMm(new BigDecimal("210")); position.setPaperHeightMm(new BigDecimal("297")); position.setPortraitXMm(new BigDecimal("65")); position.setPortraitYMm(new BigDecimal("138")); position.setLandscapeXMm(new BigDecimal("108")); position.setLandscapeYMm(new BigDecimal("95")); return position; } private void initTable(Class type) { if (TableInfoHelper.getTableInfo(type) == null) { TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new Configuration(), ""), type); } } }