package jnpf.dmsService; import com.fasterxml.jackson.databind.ObjectMapper; import jnpf.base.ActionResult; import jnpf.base.entity.VisualdevEntity; import jnpf.dmsEntity.storage.approval.DmsApprovedBatch; import jnpf.dmsEntity.storage.approval.DmsApprovedItem; import jnpf.exception.DataException; import jnpf.flowable.WorkFlowApi; import jnpf.flowable.entity.RecordEntity; import jnpf.flowable.entity.TemplateJsonEntity; import jnpf.flowable.entity.TemplateNodeEntity; import jnpf.flowable.enums.NodeEnum; import jnpf.flowable.enums.RecordEnum; import jnpf.onlinedev.VisualdevOnlineApi; import jnpf.permission.OrganizeApi; import jnpf.permission.UserApi; import jnpf.permission.entity.OrganizeEntity; import jnpf.permission.entity.UserEntity; import org.junit.jupiter.api.Test; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.LinkedHashMap; import java.util.Map; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class DmsQianpiyeRenderSnapshotServiceTest { WorkFlowApi workflow = mock(WorkFlowApi.class); VisualdevOnlineApi forms = mock(VisualdevOnlineApi.class); UserApi users = mock(UserApi.class); OrganizeApi organizes = mock(OrganizeApi.class); DmsQianpiyeRenderSnapshotService service = new DmsQianpiyeRenderSnapshotService( workflow, forms, users, organizes, new ObjectMapper()); @Test void keepsOnlyTheLastSuccessfulNodeRunAndReusesTheSavedValuesOnRetry() { Date created = new Date(1_700_000_000_000L); Date oldTime = new Date(1_700_000_100_000L); Date newTime = new Date(1_700_000_200_000L); TemplateNodeEntity start = node("1", "start-node", NodeEnum.START.getType(), "提交申请", created); TemplateNodeEntity approve = node("2", "approve-node", NodeEnum.APPROVER.getType(), "部门审批", new Date(created.getTime() + 1)); mockFlowNodes(start, approve); when(workflow.getNodesByFlowId("flow-version")).thenReturn(Arrays.asList(approve, start)); when(workflow.getRecordList("task")).thenReturn(Arrays.asList( record("old", "run-1", "old-user", oldTime), record("new", "run-2", "new-user", newTime))); when(users.getUserName(anyList())).thenReturn(Arrays.asList( user("author", "申请人", "org-a"), user("old-user", "旧审批人", "org-old"), user("new-user", "新审批人", "org-new"))); when(organizes.getInfoById("org-a")).thenReturn(org("申请部门")); when(organizes.getInfoById("org-old")).thenReturn(org("旧部门")); when(organizes.getInfoById("org-new")).thenReturn(org("新部门")); DmsApprovedBatch first = batch(); service.attach(first, null, "author", created); Map values = first.getItems().get(0).getApprovalValuesByTag(); assertEquals("申请人", values.get("dms.qianpiye.flow.start.applicant")); assertEquals("新审批人", values.get("dms.qianpiye.flow.approve-1.approver")); assertEquals("新部门", values.get("dms.qianpiye.flow.approve-1.approverDepartment")); assertEquals("2023-11-15", values.get("dms.qianpiye.flow.start.applicantTime")); assertEquals("2023-11-15", values.get("dms.qianpiye.flow.approve-1.approveTime")); assertNotEquals("base-digest", first.getSourceDigest()); assertNotNull(first.getApprovalRenderSnapshot()); assertEquals("flow-version", first.getItems().get(0).getApprovalFlowVersionId()); DmsApprovedBatch retry = batch(); service.attach(retry, first.getApprovalRenderSnapshot(), "changed", new Date()); assertEquals(values, retry.getItems().get(0).getApprovalValuesByTag()); assertEquals(first.getSourceDigest(), retry.getSourceDigest()); verify(workflow).getNodesByFlowId("flow-version"); verify(workflow).getRecordList("task"); } @Test void skipsNonDocxFilesWhenNoApprovalPageIsConfigured() { DmsApprovedBatch batch = batch(); batch.getItems().get(0).setApprovalPageFileJson(null); batch.getItems().get(0).setFileJson("[{\"name\":\"image.png\",\"url\":\"image.png\"}]"); service.attach(batch, null, "author", new Date()); assertEquals("base-digest", batch.getSourceDigest()); verify(workflow, never()).getNodesByFlowId("flow-version"); } @Test void freezesValuesForOriginalDocxFilesWithoutAPageAndSharesThemWithNewRecordFiles() { mockFlowNodes(node("1", "start-node", NodeEnum.START.getType(), "提交申请", new Date())); when(workflow.getRecordList("task")).thenReturn(List.of()); when(users.getUserName(anyList())).thenReturn(List.of(user("author", "申请人", null))); DmsApprovedBatch batch = batch(); DmsApprovedItem main = batch.getItems().get(0); main.setApprovalPageFileJson(null); main.setFileJson("[{\"url\":\"/api/file/Image/annex/main.docx?download=1\"}]"); DmsApprovedItem record = new DmsApprovedItem(); record.setSourceKey("xinzeng_shenhe:a:jilu:1"); record.setChangeType("REVISE"); record.setFileJson("[{\"name\":\"record.DOCX\",\"url\":\"/api/file/Download\",\"fileId\":\"id\"}]"); DmsApprovedItem reused = new DmsApprovedItem(); reused.setChangeType("REUSE"); reused.setFileJson(record.getFileJson()); DmsApprovedItem retired = new DmsApprovedItem(); retired.setChangeType("RETIRE"); retired.setFileJson(record.getFileJson()); DmsApprovedItem image = new DmsApprovedItem(); image.setChangeType("CREATE"); image.setFileJson("[{\"name\":\"image.png\"}]"); batch.setItems(List.of(main, record, reused, retired, image)); service.attach(batch, null, "author", new Date()); assertNotNull(batch.getApprovalRenderSnapshot()); assertEquals("申请人", main.getApprovalValuesByTag().get("dms.qianpiye.flow.start.applicant")); assertEquals(main.getApprovalValuesByTag(), record.getApprovalValuesByTag()); assertEquals("flow-version", record.getApprovalFlowVersionId()); assertNull(main.getApprovalPageFileJson()); assertNull(record.getApprovalPageFileJson()); assertNull(reused.getApprovalValuesByTag()); assertNull(retired.getApprovalValuesByTag()); assertNull(image.getApprovalValuesByTag()); Map original = new LinkedHashMap<>(main.getApprovalValuesByTag()); batch.setSourceDigest("base-digest"); service.attach(batch, batch.getApprovalRenderSnapshot(), "changed", new Date()); assertEquals(original, main.getApprovalValuesByTag()); verify(workflow).getNodesByFlowId("flow-version"); } @Test void retriesPersistedValuesRegardlessOfJsonObjectOrderAndStillDetectsContentChanges() throws Exception { ObjectMapper mapper = new ObjectMapper(); DmsQianpiyeRenderSnapshotService.RenderSnapshot snapshot = new DmsQianpiyeRenderSnapshotService.RenderSnapshot(); snapshot.setProtocolVersion(1); snapshot.setTaskId("task"); snapshot.setFlowId("flow-version"); snapshot.setGeneratedAt(new Date(1_700_000_000_123L)); snapshot.getValuesByTag().put("dms.qianpiye.flow.start.applicant", "申请人"); snapshot.getValuesByTag().put("dms.qianpiye.form.bumen_id", "部门1"); String original = mapper.writeValueAsString(snapshot); DmsApprovedBatch first = batch(); service.attach(first, original, "author", new Date()); // jsonb returns object members in storage order, not the original TreeMap order. Map storedValues = new LinkedHashMap<>(); storedValues.put("dms.qianpiye.form.bumen_id", "部门1"); storedValues.put("dms.qianpiye.flow.start.applicant", "申请人"); snapshot.setValuesByTag(storedValues); String persisted = mapper.writeValueAsString(snapshot); assertNotEquals(original, persisted); DmsApprovedBatch retry = batch(); service.attach(retry, persisted, "changed", new Date()); assertEquals(first.getSourceDigest(), retry.getSourceDigest()); assertEquals(original, retry.getApprovalRenderSnapshot()); assertEquals(first.getItems().get(0).getApprovalValuesByTag(), retry.getItems().get(0).getApprovalValuesByTag()); storedValues.put("dms.qianpiye.flow.start.applicant", "另一申请人"); DmsApprovedBatch changed = batch(); service.attach(changed, mapper.writeValueAsString(snapshot), "author", new Date()); assertNotEquals(first.getSourceDigest(), changed.getSourceDigest()); DmsApprovedBatch changedSource = batch(); changedSource.setSourceDigest("changed-source-digest"); service.attach(changedSource, persisted, "author", new Date()); assertNotEquals(first.getSourceDigest(), changedSource.getSourceDigest()); verify(workflow, never()).getNodesByFlowId("flow-version"); } @Test void fillsConfiguredMainAndChildFormFieldsButNeverCopiesBizSign() { Date created = new Date(1_700_000_000_000L); TemplateNodeEntity start = node("1", "start-node", NodeEnum.START.getType(), "提交申请", created); start.setFormId("form-1"); mockFlowNodes(start); when(workflow.getRecordList("task")).thenReturn(Collections.emptyList()); when(users.getUserName(anyList())).thenReturn(Collections.singletonList( user("author", "申请人", "org-a"))); when(organizes.getInfoById("org-a")).thenReturn(org("申请部门")); VisualdevEntity form = new VisualdevEntity(); form.setFormData("{\"fields\":[" + "{\"__vModel__\":\"wenjian_bianhao\",\"__config__\":{}}," + "{\"__vModel__\":\"liushuihao\",\"__config__\":{\"jnpfKey\":\"inputNumber\"}}," + "{\"__vModel__\":\"shengxiao_riqi\",\"format\":\"yyyy-MM-dd\"," + "\"__config__\":{\"jnpfKey\":\"datePicker\"}}," + "{\"__vModel__\":\"biz_sign\",\"__config__\":{}}," + "{\"__vModel__\":\"jilu_mingxi\",\"__config__\":{\"children\":[" + "{\"__vModel__\":\"jilu_leixing\",\"__config__\":{}}," + "{\"__vModel__\":\"jilu_riqi\",\"format\":\"yyyy/MM/dd\"," + "\"__config__\":{\"jnpfKey\":\"datePicker\"}}]}}]}"); when(forms.getFormConfig("form-1")).thenReturn(form); Map row1 = new LinkedHashMap<>(); row1.put("jilu_leixing", "生产记录"); row1.put("jilu_riqi", "2026/09/18"); Map row2 = new LinkedHashMap<>(); row2.put("jilu_leixing", "检验记录"); row2.put("jilu_riqi", "2026/09/19"); Map data = new LinkedHashMap<>(); data.put("wenjian_bianhao", "SOP-001"); data.put("liushuihao", 1_789_660_800_000L); data.put("shengxiao_riqi", "2026-09-18"); data.put("biz_sign", "不得回填"); data.put("jilu_mingxi", Arrays.asList(row1, row2)); ActionResult response = new ActionResult<>(); response.setCode(200); response.setData(data); when(forms.displayInfo("form-1", "task")).thenReturn(response); DmsApprovedBatch batch = batch(); service.attach(batch, null, "author", created); Map values = batch.getItems().get(0).getApprovalValuesByTag(); assertEquals("SOP-001", values.get("dms.qianpiye.form.wenjian_bianhao")); assertEquals("1789660800000", values.get("dms.qianpiye.form.liushuihao")); assertEquals("2026-09-18", values.get("dms.qianpiye.form.shengxiao_riqi")); assertEquals("生产记录\n检验记录", values.get( "dms.qianpiye.form.jilu_mingxi.jilu_leixing")); assertEquals("2026/09/18\n2026/09/19", values.get( "dms.qianpiye.form.jilu_mingxi.jilu_riqi")); org.junit.jupiter.api.Assertions.assertFalse(values.containsKey("dms.qianpiye.form.biz_sign")); } @Test void freezesTargetVersionsForMainAndMixedRecordChangesInDisplayRowOrder() throws Exception { mockVersionForm(List.of( Map.of("id", "reused", "banbenhao", "wrong", "fujian_mingcheng", "沿用记录"), Map.of("f_id", "revised", "banbenhao", "D(09)", "fujian_mingcheng", "升版记录"), Map.of("id", "created", "banbenhao", "", "fujian_mingcheng", "新增记录"))); DmsApprovedBatch batch = batch(); DmsApprovedItem main = batch.getItems().get(0); main.setChangeType("REVISE"); main.setVersionLabel("D(01)"); DmsApprovedItem created = versionItem("created", "CREATE", "D(00)"); DmsApprovedItem revised = versionItem("revised", "REVISE", "D(10)"); DmsApprovedItem reused = versionItem("reused", "REUSE", "D(07)"); batch.setItems(List.of(main, created, revised, reused)); service.attach(batch, null, "", new Date()); Map values = main.getApprovalValuesByTag(); assertEquals("D(01)", values.get("dms.qianpiye.form.banbenhao")); assertEquals("D(07)\nD(10)\nD(00)", values.get("dms.qianpiye.form.jilu_mingxi.banbenhao")); assertEquals("沿用记录\n升版记录\n新增记录", values.get("dms.qianpiye.form.jilu_mingxi.fujian_mingcheng")); var rows = new ObjectMapper().readTree(values.get("dms.qianpiye.rows.common.jilu_mingxi")); assertEquals("D(07)", rows.get(0).path("banbenhao").asText()); assertEquals("D(10)", rows.get(1).path("banbenhao").asText()); assertEquals("D(00)", rows.get(2).path("banbenhao").asText()); assertEquals(values, created.getApprovalValuesByTag()); assertEquals(values, revised.getApprovalValuesByTag()); assertNull(reused.getApprovalValuesByTag()); // Related files keep their actual form version; they are not approval targets. assertEquals("D(03)", values.get("dms.qianpiye.form.guanlian_wenjian_mingxi.banbenhao")); String frozen = batch.getApprovalRenderSnapshot(); String digest = batch.getSourceDigest(); main.setVersionLabel("D(99)"); revised.setVersionLabel("D(98)"); batch.setSourceDigest("base-digest"); service.attach(batch, frozen, "", new Date()); assertEquals(values, main.getApprovalValuesByTag()); assertEquals(frozen, batch.getApprovalRenderSnapshot()); assertEquals(digest, batch.getSourceDigest()); verify(forms).displayInfo("version-form", "task"); } @Test void newMainVersionIsFilledEvenWhenTheFormShowsAnOldOrEmptyValue() { mockVersionForm(List.of()); DmsApprovedBatch batch = batch(); batch.getItems().get(0).setVersionLabel("D(00)"); service.attach(batch, null, "", new Date()); assertEquals("D(00)", batch.getItems().get(0).getApprovalValuesByTag().get("dms.qianpiye.form.banbenhao")); } @Test void unchangedMainVersionIsAvailableWhenOnlyARecordNeedsRendering() { mockVersionForm(List.of(Map.of("id", "created", "banbenhao", ""))); DmsApprovedBatch batch = batch(); DmsApprovedItem main = batch.getItems().get(0); main.setChangeType("REUSE"); main.setVersionLabel("D(07)"); DmsApprovedItem created = versionItem("created", "CREATE", "D(00)"); batch.setItems(List.of(main, created)); service.attach(batch, null, "", new Date()); assertNull(main.getApprovalValuesByTag()); assertEquals("D(07)", created.getApprovalValuesByTag().get("dms.qianpiye.form.banbenhao")); assertEquals("D(00)", created.getApprovalValuesByTag().get("dms.qianpiye.form.jilu_mingxi.banbenhao")); } @Test void rejectsMissingOrUnknownRecordIdsRatherThanFillingByRowPosition() { DmsApprovedBatch batch = batch(); batch.getItems().get(0).setVersionLabel("D(01)"); batch.setItems(List.of(batch.getItems().get(0), versionItem("expected", "REVISE", "D(02)"))); mockVersionForm(List.of(Map.of("banbenhao", "D(00)"))); assertThrows(DataException.class, () -> service.attach(batch, null, "", new Date())); mockVersionForm(List.of(Map.of("id", "unknown", "banbenhao", "D(00)"))); assertThrows(DataException.class, () -> service.attach(batch, null, "", new Date())); } @Test void rejectsMissingTargetVersionRatherThanKeepingTheOldFormVersion() { mockVersionForm(List.of()); assertThrows(DataException.class, () -> service.attach(batch(), null, "", new Date())); } private DmsApprovedItem versionItem(String rowId, String change, String label) { DmsApprovedItem item = new DmsApprovedItem(); item.setSourceKey("xiuding_shenhe:a:jilu:" + rowId); item.setSourceRowId(rowId); item.setChangeType(change); item.setVersionLabel(label); item.setFileJson("[{\"name\":\"record.docx\"}]"); return item; } private void mockVersionForm(List> rows) { TemplateNodeEntity start = node("1", "start", NodeEnum.START.getType(), "申请", new Date()); start.setFormId("version-form"); mockFlowNodes(start); when(workflow.getRecordList("task")).thenReturn(List.of()); VisualdevEntity form = new VisualdevEntity(); form.setFormData(""" {"fields":[ {"__vModel__":"banbenhao","__config__":{}}, {"__vModel__":"jilu_mingxi","__config__":{"jnpfKey":"table","children":[ {"__vModel__":"banbenhao","__config__":{}}, {"__vModel__":"fujian_mingcheng","__config__":{}}]}}, {"__vModel__":"guanlian_wenjian_mingxi","__config__":{"jnpfKey":"table","children":[ {"__vModel__":"banbenhao","__config__":{}}]}} ]} """); when(forms.getFormConfig("version-form")).thenReturn(form); ActionResult response = new ActionResult<>(); response.setCode(200); response.setData(Map.of("banbenhao", "D(00)", "jilu_mingxi", rows, "guanlian_wenjian_mingxi", List.of(Map.of("banbenhao", "D(03)")))); when(forms.displayInfo("version-form", "task")).thenReturn(response); } @Test void newFieldsNeedNoBackendRegistrationAndChildRowsKeepTheirEmptyCells() throws Exception { TemplateNodeEntity start = node("1", "start", NodeEnum.START.getType(), "申请", new Date()); start.setFormId("new-form"); mockFlowNodes(start); when(workflow.getRecordList("task")).thenReturn(Collections.emptyList()); VisualdevEntity form = new VisualdevEntity(); form.setFormData("{\"fields\":[{\"__vModel__\":\"new_department\",\"__config__\":{\"jnpfKey\":\"organizeSelect\"}}," + "{\"__vModel__\":\"distribution\",\"__config__\":{\"jnpfKey\":\"table\",\"children\":[" + "{\"__vModel__\":\"department\",\"__config__\":{\"jnpfKey\":\"organizeSelect\"}}," + "{\"__vModel__\":\"copies\",\"__config__\":{\"jnpfKey\":\"inputNumber\"}}]}}]}"); when(forms.getFormConfig("new-form")).thenReturn(form); Map data = new LinkedHashMap<>(); data.put("new_department", "质量部"); Map first = new LinkedHashMap<>(); first.put("department", "质量部"); first.put("copies", null); data.put("distribution", Arrays.asList(first, Map.of("department", "生产部", "copies", "20"))); ActionResult response = new ActionResult<>(); response.setCode(200); response.setData(data); when(forms.displayInfo("new-form", "task")).thenReturn(response); DmsApprovedBatch batch = batch(); service.attach(batch, null, "", new Date()); Map values = batch.getItems().get(0).getApprovalValuesByTag(); assertEquals("质量部", values.get("dms.qianpiye.form.new_department")); org.junit.jupiter.api.Assertions.assertFalse(values.keySet().stream() .anyMatch(tag -> tag.startsWith("dms.qianpiye.v2."))); org.junit.jupiter.api.Assertions.assertFalse(values.keySet().stream().anyMatch(tag -> tag.startsWith("dms.qianpiye.rows.") && !tag.startsWith("dms.qianpiye.rows.common."))); assertEquals("质量部\n生产部", values.get("dms.qianpiye.form.distribution.department")); assertEquals("\n20", values.get("dms.qianpiye.form.distribution.copies")); com.fasterxml.jackson.databind.JsonNode rows = new ObjectMapper().readTree(values.get("dms.qianpiye.rows.common.distribution")); assertEquals("", rows.get(0).path("copies").asText()); assertEquals("生产部", rows.get(1).path("department").asText()); verify(forms, never()).info("new-form", "task"); } @Test void fillsStepNamesAndActorsUsingTheSameConnectionOrderAsTheAnnotationCatalog() { List nodes = DmsQianpiyeFlowFixtures.steps("node"); mockFlowNodes(nodes.toArray(TemplateNodeEntity[]::new)); Collections.reverse(nodes); when(workflow.getNodesByFlowId("flow-version")).thenReturn(nodes); RecordEntity department = record("department", "run-department", "department-user", new Date(1000)); department.setNodeCode("node1"); RecordEntity qa = record("qa", "run-qa", "qa-user", new Date(2000)); qa.setNodeCode("node5"); when(workflow.getRecordList("task")).thenReturn(List.of(qa, department)); when(users.getUserName(anyList())).thenReturn(List.of( user("department-user", "部门审核人", null), user("qa-user", "QA审核人", null))); DmsApprovedBatch batch = batch(); service.attach(batch, null, "", new Date()); Map values = batch.getItems().get(0).getApprovalValuesByTag(); for (int index = 1; index < DmsQianpiyeFlowFixtures.STEP_NAMES.size(); index++) { assertEquals(DmsQianpiyeFlowFixtures.STEP_NAMES.get(index), values.get("dms.qianpiye.flow.approve-" + index + ".stepName")); } assertEquals("部门审核人", values.get("dms.qianpiye.flow.approve-1.approver")); assertEquals("QA审核人", values.get("dms.qianpiye.flow.approve-5.approver")); } private void mockFlowNodes(TemplateNodeEntity... nodes) { TemplateJsonEntity version = new TemplateJsonEntity(); version.setId("flow-version"); version.setFlowXml(DmsQianpiyeFlowFixtures.linearXml(Arrays.stream(nodes) .map(TemplateNodeEntity::getNodeCode).toArray(String[]::new))); when(workflow.getTemplateByVersionId("flow-version")).thenReturn("template"); when(workflow.getFlowIdsByTemplate("template")).thenReturn(List.of(version)); when(workflow.getNodesByFlowId("flow-version")).thenReturn(Arrays.asList(nodes)); } private DmsApprovedBatch batch() { DmsApprovedItem main = new DmsApprovedItem(); main.setSourceKey("xinzeng_shenhe:a:zhuwenjian"); main.setChangeType("CREATE"); main.setApprovalPageFileJson("[{\"url\":\"approval.docx\"}]"); DmsApprovedBatch batch = new DmsApprovedBatch(); batch.setTaskId("task"); batch.setFlowId("flow-version"); batch.setSourceDigest("base-digest"); batch.setItems(Collections.singletonList(main)); return batch; } private TemplateNodeEntity node(String id, String code, String type, String name, Date created) { TemplateNodeEntity node = new TemplateNodeEntity(); node.setId(id); node.setNodeCode(code); node.setNodeType(type); node.setNodeJson("{\"nodeName\":\"" + name + "\"}"); node.setCreatorTime(created); return node; } private RecordEntity record(String id, String nodeId, String userId, Date time) { RecordEntity record = new RecordEntity(); record.setId(id); record.setNodeCode("approve-node"); record.setNodeId(nodeId); record.setHandleId(userId); record.setHandleType(RecordEnum.AUDIT.getCode()); record.setStatus(0); record.setHandleTime(time); return record; } private UserEntity user(String id, String name, String organizeId) { UserEntity user = new UserEntity(); user.setId(id); user.setRealName(name); user.setOrganizeId(organizeId); return user; } private OrganizeEntity org(String name) { OrganizeEntity organize = new OrganizeEntity(); organize.setFullName(name); return organize; } }