package jnpf.dmsService.impl; import jnpf.base.ActionResult; import jnpf.base.BillRuleApi; import jnpf.base.UserInfo; import jnpf.dmsEntity.fenfa.DmsWenjianDayinJiluEntity; import jnpf.dmsEntity.fenfa.DmsWenjianDayinRenwuEntity; import jnpf.dmsEntity.fenfa.DmsWenjianFenfaFanweiEntity; import jnpf.dmsEntity.fenfa.DmsWenjianFenfaRenwuEntity; import jnpf.dmsEntity.fenfa.form.DmsWenjianQianshouFafangForm; import jnpf.dmsEntity.fenfa.form.DmsWenjianHuishouForm; import jnpf.dmsEntity.fenfa.form.DmsWenjianHuishouMingxiForm; import jnpf.dmsEntity.fenfa.vo.DmsWenjianJiaojieXiangqingVO; import jnpf.dmsEntity.fenfa.vo.DmsWenjianHuishouMingxiVO; import jnpf.dmsEntity.fenfa.vo.DmsWenjianHuishouResultVO; import jnpf.dmsEntity.fenfa.vo.DmsWenjianXiaohuiMingxiVO; import jnpf.dmsIntegration.audit.DmsAuditAdapter; import jnpf.dmsIntegration.audit.DmsWenjianHuishouAuditEvent; import jnpf.dmsMapper.DmsWenjianFenfaMapper; import jnpf.dmsPermission.DmsCurrentIdentity; import jnpf.exception.DataException; import jnpf.permission.OrganizeApi; import jnpf.permission.UserApi; import jnpf.permission.entity.OrganizeEntity; import jnpf.permission.entity.UserEntity; import jnpf.permission.model.user.UserRelationIds; import org.junit.jupiter.api.BeforeEach; 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 java.util.Collections; import java.util.Arrays; 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.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class DmsWenjianFenfaServiceImplTest { @Mock private DmsCurrentIdentity identity; @Mock private DmsWenjianFenfaMapper mapper; @Mock private BillRuleApi billRuleApi; @Mock private OrganizeApi organizeApi; @Mock private UserApi userApi; @Mock private DmsAuditAdapter auditAdapter; @Mock private jnpf.dmsService.DmsWenjianDayinHaomaService numbers; @Mock private jnpf.dmsService.DmsWenjianChongdaGuard reprintGuard; @InjectMocks private DmsWenjianFenfaServiceImpl service; private UserInfo user; @BeforeEach void setUp() { user = new UserInfo(); user.setUserId("user-1"); user.setTenantId("tenant-1"); user.setOrganizeId("org-1"); user.setIsAdministrator(false); } @Test void approvalCreatesOnlyOneIdempotentPrintTaskForEachValidScope() throws Exception { when(mapper.selectNewScopes("tenant-1", "audit-1")) .thenReturn(Collections.singletonList(scope("scope-1", 2))); when(mapper.selectAuditPrintSnapshot("tenant-1", "xinzeng", "audit-1")) .thenReturn(snapshot()); when(mapper.insertPrintTaskIfAbsent(any())).thenReturn(1); service.ensureNewPrintTasks("tenant-1", "audit-1", "file-1", "user-1"); ArgumentCaptor printTaskCaptor = ArgumentCaptor.forClass(DmsWenjianDayinRenwuEntity.class); verify(mapper).insertPrintTaskIfAbsent(printTaskCaptor.capture()); assertEquals("wenjian_xinzeng_shenhe", printTaskCaptor.getValue().getLaiyuanLeixing()); assertEquals("audit-1", printTaskCaptor.getValue().getLaiyuanDanjuId()); assertEquals("scope-1", printTaskCaptor.getValue().getLaiyuanMingxiId()); assertEquals(2, printTaskCaptor.getValue().getDayinFenshu()); assertEquals("DOC-001", printTaskCaptor.getValue().getWenjianBianhao()); verify(numbers).ensureTaskNumbers("tenant-1", printTaskCaptor.getValue(), 0); verify(mapper, never()).insertDistributionTaskIfAbsent(any()); } @Test void approvalRejectsNonPositiveDistributionCopies() { when(mapper.selectNewScopes("tenant-1", "audit-1")) .thenReturn(Collections.singletonList(scope("scope-1", 0))); when(mapper.selectAuditPrintSnapshot("tenant-1", "xinzeng", "audit-1")) .thenReturn(snapshot()); DataException exception = assertThrows(DataException.class, () -> service.ensureNewPrintTasks( "tenant-1", "audit-1", "file-1", "user-1")); assertEquals("分发份数必须大于0:0", exception.getMessage()); verify(mapper, never()).insertPrintTaskIfAbsent(any()); } @Test void confirmationWritesBothFrozenSerialsWithoutAllocatingAgain() throws Exception { prepareIdentity(); var task = printTask("task-1"); when(mapper.selectPrintTaskForUpdate("tenant-1", "task-1")).thenReturn(task); when(mapper.insertPrintRecord(any())).thenReturn(1); when(mapper.updateTaskStatus(any(), any(), any(), any(), any())).thenReturn(1); when(mapper.selectDistributionTaskByPrintTaskForUpdate("tenant-1", "task-1")) .thenReturn(distributionTask()); var number = new jnpf.dmsEntity.fenfa.DmsWenjianDayinHaoma(); number.setId("number-1"); number.setTaskSequence(1); number.setCounterId("counter-1"); number.setYear(2026); number.setSequence(1); number.setSerialNumber("20261003000001"); number.setCountSerialNumber("20260001"); var frozen = Collections.singletonList(number); var result = service.printReserved("task-1", 1, frozen); verify(numbers).requireReservedNumbers("tenant-1", task, 0, frozen); var record = ArgumentCaptor.forClass(DmsWenjianDayinJiluEntity.class); verify(mapper).insertPrintRecord(record.capture()); assertEquals("20261003000001",record.getValue().getDayinLiushuihao()); assertEquals("20260001",record.getValue().getDayinJishuLiushuihao()); assertEquals("counter-1",record.getValue().getDayinJishuId()); assertEquals("20260001",result.getFirst().getDayinJishuLiushuihao()); verify(billRuleApi, never()).getBillNumber(any()); } @Test void rejectsTaskThatAlreadyHasPrintRecordsBeforeTakingNumber() { prepareIdentity(); DmsWenjianDayinRenwuEntity task = printTask("task-1"); when(mapper.selectPrintTaskForUpdate("tenant-1", "task-1")).thenReturn(task); when(mapper.countConsumedPrints("tenant-1", "task-1")).thenReturn(1); DataException exception = assertThrows(DataException.class, () -> service.print("task-1", 2)); assertEquals("本次打印份数超过剩余份数或无效,剩余:1", exception.getMessage()); verify(billRuleApi, never()).getBillNumber(any()); verify(auditAdapter, never()).print(any()); } @Test void newAuditValidatesLaterRowsBeforeInsertingAnyTask() { when(mapper.selectNewScopes("tenant-1", "audit-1")) .thenReturn(Arrays.asList(scope("first", 1), scope("second", 0))); when(mapper.selectAuditPrintSnapshot("tenant-1", "xinzeng", "audit-1")) .thenReturn(snapshot()); assertThrows(DataException.class, () -> service.ensureNewPrintTasks("tenant-1", "audit-1", "file-1", "user-1")); verify(mapper, never()).insertPrintTaskIfAbsent(any()); } @Test void revisionAuditValidatesLaterRowsBeforeInsertingAnyTask() { when(mapper.selectRevisionScopes("tenant-1", "audit-1")) .thenReturn(Arrays.asList(scope("first", 1), scope("second", -1))); when(mapper.selectAuditPrintSnapshot("tenant-1", "xiuding", "audit-1")) .thenReturn(snapshot()); assertThrows(DataException.class, () -> service.ensureRevisionPrintTasks("tenant-1", "audit-1", "file-1", "user-1")); verify(mapper, never()).insertPrintTaskIfAbsent(any()); } @Test void printsCopiesWithPlatformSerialsAndMarksTaskPrinted() throws Exception { prepareIdentity(); when(mapper.selectPrintTaskForUpdate("tenant-1", "task-1")) .thenReturn(printTask("task-1")); when(mapper.countConsumedPrints("tenant-1", "task-1")).thenReturn(0); when(mapper.maxPrintSequence("tenant-1", "task-1")).thenReturn(0); when(billRuleApi.getBillNumber("wenjianDayinLiushuihao")) .thenReturn(ActionResult.success("取号成功", "20260825000001"), ActionResult.success("取号成功", "20260825000002")); when(billRuleApi.getBillNumber("wenjianFenfaBianhao")) .thenReturn(ActionResult.success("取号成功", "FF20260825000001")); when(mapper.insertPrintRecord(any())).thenReturn(1); when(mapper.updateTaskStatus(any(), any(), any(), any(), any())).thenReturn(1); when(mapper.insertDistributionTaskIfAbsent(any())).thenReturn(1); java.util.List printed = service.print("task-1", 2); assertEquals(2, printed.size()); assertEquals("org-1", printed.get(0).getDayinBumenId()); ArgumentCaptor captor = ArgumentCaptor.forClass(DmsWenjianDayinJiluEntity.class); verify(mapper, org.mockito.Mockito.times(2)).insertPrintRecord(captor.capture()); assertEquals(1, captor.getAllValues().get(0).getDayinXuhao()); assertEquals(2, captor.getAllValues().get(1).getDayinXuhao()); assertEquals("org-1", captor.getAllValues().get(0).getDayinBumenId()); assertNull(captor.getAllValues().get(0).getQianshouBumenId()); verify(mapper).updateTaskStatus( org.mockito.ArgumentMatchers.eq("tenant-1"), org.mockito.ArgumentMatchers.eq("task-1"), org.mockito.ArgumentMatchers.eq("yidayin"), org.mockito.ArgumentMatchers.eq("user-1"), any()); ArgumentCaptor taskCaptor = ArgumentCaptor.forClass(DmsWenjianFenfaRenwuEntity.class); verify(mapper).insertDistributionTaskIfAbsent(taskCaptor.capture()); assertEquals("task-1", taskCaptor.getValue().getDayinRenwuId()); assertEquals("FF20260825000001", taskCaptor.getValue().getFenfaBianhao()); assertEquals("zidongfenfa", taskCaptor.getValue().getFafangLeixing()); assertEquals("daifafang", taskCaptor.getValue().getBizStatus()); verify(auditAdapter, never()).print(any()); } @Test void partialPrintCreatesDistributionAndRemainingPrintReusesIt() throws Exception { prepareIdentity(); DmsWenjianDayinRenwuEntity task = printTask("task-1"); task.setDayinFenshu(10); when(mapper.selectPrintTaskForUpdate("tenant-1", "task-1")).thenReturn(task); when(mapper.countConsumedPrints("tenant-1", "task-1")).thenReturn(0); when(billRuleApi.getBillNumber("wenjianDayinLiushuihao")) .thenReturn(ActionResult.success("取号成功", "serial")); when(mapper.insertPrintRecord(any())).thenReturn(1); when(mapper.updateTaskStatus(any(), any(), any(), any(), any())).thenReturn(1); when(billRuleApi.getBillNumber("wenjianFenfaBianhao")) .thenReturn(ActionResult.success("取号成功", "distribution")); when(mapper.insertDistributionTaskIfAbsent(any())).thenReturn(1); assertEquals(3, service.print("task-1", 3).size()); verify(mapper).updateTaskStatus(org.mockito.ArgumentMatchers.eq("tenant-1"), org.mockito.ArgumentMatchers.eq("task-1"), org.mockito.ArgumentMatchers.eq("bufendayin"), org.mockito.ArgumentMatchers.eq("user-1"), any()); verify(mapper).insertDistributionTaskIfAbsent(any()); task.setBizStatus("bufendayin"); when(mapper.countConsumedPrints("tenant-1", "task-1")).thenReturn(3); when(mapper.maxPrintSequence("tenant-1", "task-1")).thenReturn(3); when(mapper.selectDistributionTaskByPrintTaskForUpdate("tenant-1", "task-1")) .thenReturn(distributionTask()); assertEquals(7, service.print("task-1", 7).size()); verify(mapper).insertDistributionTaskIfAbsent(any()); verify(billRuleApi).getBillNumber("wenjianFenfaBianhao"); verify(mapper).updateTaskStatus(eq("tenant-1"), eq("task-1"), eq("yidayin"), eq("user-1"), any()); verify(mapper, never()).countActivePrints(any(), any()); } @Test void voidedCopiesDoNotRestoreAllowance() { prepareIdentity(); DmsWenjianDayinRenwuEntity task = printTask("task-1"); task.setBizStatus("bufendayin"); when(mapper.selectPrintTaskForUpdate("tenant-1", "task-1")).thenReturn(task); when(mapper.countConsumedPrints("tenant-1", "task-1")).thenReturn(2); assertThrows(DataException.class, () -> service.print("task-1", 1)); verify(mapper, never()).countActivePrints(any(), any()); verify(billRuleApi, never()).getBillNumber(any()); } @Test void batchPrintingPrintsEverySelectedTaskInFull() throws Exception { prepareIdentity(); DmsWenjianDayinRenwuEntity firstTask = printTask("task-1"); DmsWenjianDayinRenwuEntity secondTask = printTask("task-2"); when(mapper.selectPrintTaskForUpdate("tenant-1", "task-1")).thenReturn(firstTask); when(mapper.selectPrintTaskForUpdate("tenant-1", "task-2")).thenReturn(secondTask); when(mapper.countConsumedPrints("tenant-1", "task-1")).thenReturn(0); when(mapper.countConsumedPrints("tenant-1", "task-2")).thenReturn(0); when(mapper.maxPrintSequence("tenant-1", "task-1")).thenReturn(0); when(mapper.maxPrintSequence("tenant-1", "task-2")).thenReturn(0); when(billRuleApi.getBillNumber("wenjianDayinLiushuihao")) .thenReturn(ActionResult.success("取号成功", "serial-2"), ActionResult.success("取号成功", "serial-3"), ActionResult.success("取号成功", "serial-4"), ActionResult.success("取号成功", "serial-5")); when(billRuleApi.getBillNumber("wenjianFenfaBianhao")) .thenReturn(ActionResult.success("取号成功", "FF-1"), ActionResult.success("取号成功", "FF-2")); when(mapper.insertPrintRecord(any())).thenReturn(1); when(mapper.updateTaskStatus(any(), any(), any(), any(), any())).thenReturn(1); when(mapper.insertDistributionTaskIfAbsent(any())).thenReturn(1); assertEquals(4, service.printRemaining(Arrays.asList("task-2", "task-1")).size()); verify(mapper, org.mockito.Mockito.times(4)).insertPrintRecord(any()); verify(mapper, org.mockito.Mockito.times(2)) .updateTaskStatus(any(), any(), any(), any(), any()); verify(mapper, org.mockito.Mockito.times(2)) .insertDistributionTaskIfAbsent(any()); verify(auditAdapter, never()).print(any()); } @Test void activeReprintStopsHandoverBeforeTaskUpdates() throws Exception { when(identity.requireUser()).thenReturn(user);when(identity.tenantId(user)).thenReturn("tenant-1"); var record=new DmsWenjianDayinJiluEntity();record.setId("print-1"); when(mapper.selectPrintForUpdate("tenant-1","print-1",null)).thenReturn(record); org.mockito.Mockito.doThrow(new DataException("locked")).when(reprintGuard).requireUnlocked("tenant-1","print-1"); assertThrows(DataException.class,()->service.handoverByRecordId("print-1",handoverForm())); verify(mapper,never()).selectPrintTaskForUpdate(any(),any());verifyNoAuditAfterLocked(); } private void verifyNoAuditAfterLocked() { org.mockito.Mockito.verifyNoInteractions(auditAdapter); } @Test void handoverWritesIssueAndReceiptOnlyForSelectedPrint() throws Exception { user.setOrganizeId("company-org-1"); prepareIdentity(); DmsWenjianDayinJiluEntity record = new DmsWenjianDayinJiluEntity(); record.setId("print-1"); record.setDayinRenwuId("task-1"); record.setBizStatus("daifafang"); when(mapper.selectPrintForUpdate("tenant-1", "print-1", null)).thenReturn(record); when(mapper.selectDistributionTaskByPrintTaskForUpdate("tenant-1", "task-1")) .thenReturn(distributionTask()); when(mapper.selectPrintTaskForUpdate("tenant-1", "task-1")) .thenReturn(printTask("task-1")); when(userApi.getUserObjectIdList("user-1")) .thenReturn(userRelationIds("org-1")); when(userApi.getUserName(Collections.singletonList("receiver-1"))) .thenReturn(Collections.singletonList(user("receiver-1", "signer-org-1", 1))); when(userApi.getUserObjectIdList("receiver-1")) .thenReturn(userRelationIds("signer-org-1")); when(mapper.handoverPrintRecord( any(), any(), any(), any(), any(), any(), any(), any(), any(), any())).thenReturn(1); when(mapper.completeDistributionTaskHandoverIfFinished( any(), any(), any(), any(), any(), any(), any(), any())).thenReturn(0); DmsWenjianQianshouFafangForm form = new DmsWenjianQianshouFafangForm(); form.setBizUserId("user-1"); form.setBizReviewUser("receiver-1"); form.setBizSign("issuer-signature-data"); form.setBizReviewSign("receiver-signature-data"); form.setBeizhuShuoming("单份交接"); service.handoverByRecordId("print-1", form); verify(mapper).handoverPrintRecord( org.mockito.ArgumentMatchers.eq("tenant-1"), org.mockito.ArgumentMatchers.eq("print-1"), org.mockito.ArgumentMatchers.eq("user-1"), org.mockito.ArgumentMatchers.eq("org-1"), org.mockito.ArgumentMatchers.eq("receiver-1"), org.mockito.ArgumentMatchers.eq("signer-org-1"), org.mockito.ArgumentMatchers.eq("issuer-signature-data"), org.mockito.ArgumentMatchers.eq("receiver-signature-data"), org.mockito.ArgumentMatchers.eq("单份交接"), any()); verify(mapper).completeDistributionTaskHandoverIfFinished( org.mockito.ArgumentMatchers.eq("tenant-1"), org.mockito.ArgumentMatchers.eq("distribution-1"), org.mockito.ArgumentMatchers.eq("task-1"), org.mockito.ArgumentMatchers.eq("receiver-1"), org.mockito.ArgumentMatchers.eq("user-1"), org.mockito.ArgumentMatchers.eq("org-1"), org.mockito.ArgumentMatchers.eq("单份交接"), any()); } @Test void handoverDetailUsesCurrentTenantAndRejectsMissingRecord() throws Exception { when(identity.tenantId()).thenReturn("tenant-1"); DmsWenjianJiaojieXiangqingVO detail = new DmsWenjianJiaojieXiangqingVO(); detail.setId("print-1"); detail.setWenjianBianhao("DOC-001"); detail.setJieshouBumenId("org-2"); detail.setJieshouBumen("信息管理部"); OrganizeEntity organization = new OrganizeEntity(); organization.setId("org-2"); organization.setOrgNameTree("玻思韬/信息管理部"); when(mapper.selectHandoverDetail("tenant-1", "print-1")).thenReturn(detail); when(organizeApi.getInfoById("org-2")).thenReturn(organization); assertEquals(detail, service.handoverDetail("print-1")); assertEquals("玻思韬/信息管理部", detail.getJieshouBumen()); when(mapper.selectHandoverDetail("tenant-1", "missing")).thenReturn(null); DataException exception = assertThrows(DataException.class, () -> service.handoverDetail("missing")); assertEquals("未找到对应的有效打印记录", exception.getMessage()); } @Test void barcodeHandoverUsesTheSameAtomicOperation() throws Exception { prepareIdentity(); DmsWenjianDayinJiluEntity record = new DmsWenjianDayinJiluEntity(); record.setId("print-1"); record.setDayinRenwuId("task-1"); record.setBizStatus("daifafang"); when(mapper.selectPrintForUpdate("tenant-1", null, "serial-1")) .thenReturn(record); when(mapper.selectDistributionTaskByPrintTaskForUpdate("tenant-1", "task-1")) .thenReturn(distributionTask()); when(mapper.selectPrintTaskForUpdate("tenant-1", "task-1")) .thenReturn(printTask("task-1")); when(userApi.getUserObjectIdList("user-1")) .thenReturn(userRelationIds("org-1")); when(userApi.getUserName(Collections.singletonList("receiver-1"))) .thenReturn(Collections.singletonList(user("receiver-1", "signer-org-1", 1))); when(userApi.getUserObjectIdList("receiver-1")) .thenReturn(userRelationIds("signer-org-1")); when(mapper.handoverPrintRecord( any(), any(), any(), any(), any(), any(), any(), any(), any(), any())).thenReturn(1); when(mapper.completeDistributionTaskHandoverIfFinished( any(), any(), any(), any(), any(), any(), any(), any())).thenReturn(1); DmsWenjianQianshouFafangForm form = new DmsWenjianQianshouFafangForm(); form.setBizUserId("user-1"); form.setBizReviewUser("receiver-1"); form.setBizSign("issuer-signature-data"); form.setBizReviewSign("receiver-signature-data"); service.handoverBySerialNumber("serial-1", form); verify(mapper).handoverPrintRecord( org.mockito.ArgumentMatchers.eq("tenant-1"), org.mockito.ArgumentMatchers.eq("print-1"), org.mockito.ArgumentMatchers.eq("user-1"), org.mockito.ArgumentMatchers.eq("org-1"), org.mockito.ArgumentMatchers.eq("receiver-1"), org.mockito.ArgumentMatchers.eq("signer-org-1"), org.mockito.ArgumentMatchers.eq("issuer-signature-data"), org.mockito.ArgumentMatchers.eq("receiver-signature-data"), org.mockito.ArgumentMatchers.isNull(), any()); } @Test void handoverIsIdempotentForAlreadyReceivedPrint() throws Exception { prepareIdentity(); DmsWenjianDayinJiluEntity record = new DmsWenjianDayinJiluEntity(); record.setId("print-1"); record.setDayinRenwuId("task-1"); record.setBizStatus("yiqianshou"); when(mapper.selectPrintForUpdate("tenant-1", "print-1", null)).thenReturn(record); DmsWenjianFenfaRenwuEntity task = distributionTask(); task.setBizStatus("yiwancheng"); when(mapper.selectDistributionTaskByPrintTaskForUpdate("tenant-1", "task-1")) .thenReturn(task); when(mapper.selectPrintTaskForUpdate("tenant-1", "task-1")).thenReturn(printTask("task-1")); service.handoverByRecordId("print-1", handoverForm()); verify(mapper, never()).handoverPrintRecord( any(), any(), any(), any(), any(), any(), any(), any(), any(), any()); verify(mapper, never()).completeDistributionTaskHandoverIfFinished( any(), any(), any(), any(), any(), any(), any(), any()); } @Test void handoverIsIdempotentAfterTheReceiptLifecycleHasAdvanced() throws Exception { prepareIdentity(); DmsWenjianDayinJiluEntity record = new DmsWenjianDayinJiluEntity(); record.setId("print-1"); record.setDayinRenwuId("task-1"); record.setBizStatus("yixiaohui"); when(mapper.selectPrintForUpdate("tenant-1", "print-1", null)).thenReturn(record); DmsWenjianFenfaRenwuEntity task = distributionTask(); task.setBizStatus("yiwancheng"); when(mapper.selectDistributionTaskByPrintTaskForUpdate("tenant-1", "task-1")) .thenReturn(task); when(mapper.selectPrintTaskForUpdate("tenant-1", "task-1")).thenReturn(printTask("task-1")); service.handoverByRecordId("print-1", handoverForm()); verify(mapper, never()).handoverPrintRecord( any(), any(), any(), any(), any(), any(), any(), any(), any(), any()); verify(mapper, never()).completeDistributionTaskHandoverIfFinished( any(), any(), any(), any(), any(), any(), any(), any()); } @Test void handoverRejectsConfirmUserDifferentFromCurrentOperator() { prepareIdentity(); DmsWenjianDayinJiluEntity record = new DmsWenjianDayinJiluEntity(); record.setId("print-1"); record.setDayinRenwuId("task-1"); record.setBizStatus("daifafang"); when(mapper.selectPrintForUpdate("tenant-1", "print-1", null)).thenReturn(record); when(mapper.selectDistributionTaskByPrintTaskForUpdate("tenant-1", "task-1")) .thenReturn(distributionTask()); when(mapper.selectPrintTaskForUpdate("tenant-1", "task-1")) .thenReturn(printTask("task-1")); DmsWenjianQianshouFafangForm form = new DmsWenjianQianshouFafangForm(); form.setBizUserId("another-user"); form.setBizReviewUser("receiver-1"); form.setBizSign("issuer-signature-data"); form.setBizReviewSign("receiver-signature-data"); DataException exception = assertThrows(DataException.class, () -> service.handoverByRecordId("print-1", form)); assertEquals("发放人必须是当前操作人", exception.getMessage()); verify(mapper, never()).handoverPrintRecord( any(), any(), any(), any(), any(), any(), any(), any(), any(), any()); } @Test void handoverRejectsMissingDisabledOrDepartmentlessSigner() { for (java.util.List resolved : Arrays.asList( Collections.emptyList(), Collections.singletonList(user("receiver-1", "signer-org-1", 0)), Collections.singletonList(user("receiver-1", null, 1)))) { org.mockito.Mockito.reset(mapper, userApi); prepareIdentity(); prepareHandoverRecord(); when(userApi.getUserObjectIdList("user-1")) .thenReturn(userRelationIds("org-1")); when(userApi.getUserName(Collections.singletonList("receiver-1"))) .thenReturn(resolved); DataException exception = assertThrows(DataException.class, () -> service.handoverByRecordId("print-1", handoverForm())); if (resolved.isEmpty() || Integer.valueOf(0).equals(resolved.get(0).getEnabledMark())) { assertEquals("所选签收人不存在或已停用", exception.getMessage()); } else { assertEquals("所选签收人未设置所属部门", exception.getMessage()); } verify(mapper, never()).handoverPrintRecord( any(), any(), any(), any(), any(), any(), any(), any(), any(), any()); } } @Test void handoverRejectsNonAdministratorWithoutRelatedDepartment() { user.setOrganizeId(null); prepareIdentity(); prepareHandoverRecord(); when(userApi.getUserObjectIdList("user-1")) .thenReturn(UserRelationIds.builder().build()); DataException exception = assertThrows(DataException.class, () -> service.handoverByRecordId("print-1", handoverForm())); assertEquals("当前发放人未设置所属部门", exception.getMessage()); verify(userApi).getUserObjectIdList("user-1"); verify(userApi, never()).getUserName(any()); verify(mapper, never()).handoverPrintRecord( any(), any(), any(), any(), any(), any(), any(), any(), any(), any()); } @Test void handoverAllowsAdministratorWithoutRelatedDepartment() throws Exception { user.setOrganizeId(null); user.setIsAdministrator(true); prepareIdentity(); prepareHandoverRecord(); when(userApi.getUserObjectIdList("user-1")) .thenReturn(UserRelationIds.builder().build()); when(userApi.getUserName(Collections.singletonList("receiver-1"))) .thenReturn(Collections.singletonList(user("receiver-1", "signer-org-1", 1))); when(userApi.getUserObjectIdList("receiver-1")) .thenReturn(userRelationIds("signer-org-1")); when(mapper.handoverPrintRecord( any(), any(), any(), any(), any(), any(), any(), any(), any(), any())).thenReturn(1); when(mapper.completeDistributionTaskHandoverIfFinished( any(), any(), any(), any(), any(), any(), any(), any())).thenReturn(0); service.handoverByRecordId("print-1", handoverForm()); verify(mapper).handoverPrintRecord( org.mockito.ArgumentMatchers.eq("tenant-1"), org.mockito.ArgumentMatchers.eq("print-1"), org.mockito.ArgumentMatchers.eq("user-1"), org.mockito.ArgumentMatchers.isNull(), org.mockito.ArgumentMatchers.eq("receiver-1"), org.mockito.ArgumentMatchers.eq("signer-org-1"), org.mockito.ArgumentMatchers.eq("issuer-signature-data"), org.mockito.ArgumentMatchers.eq("receiver-signature-data"), org.mockito.ArgumentMatchers.isNull(), any()); } @Test void signerDepartmentEndpointUsesTheSameAuthoritativeResolution() throws Exception { when(userApi.getUserName(Collections.singletonList("receiver-1"))) .thenReturn(Collections.singletonList(user("receiver-1", "company-org-1", 1))); when(userApi.getUserObjectIdList("receiver-1")) .thenReturn(userRelationIds("department-org-1")); assertEquals("department-org-1", service.signerDepartmentId("receiver-1")); } @Test void signerDepartmentPrefersDefaultOrganizationWhenItIsAUserRelation() throws Exception { when(userApi.getUserName(Collections.singletonList("receiver-1"))) .thenReturn(Collections.singletonList(user("receiver-1", "department-org-2", 1))); when(userApi.getUserObjectIdList("receiver-1")) .thenReturn(UserRelationIds.builder() .organize(Arrays.asList("department-org-1", "department-org-2")) .build()); assertEquals("department-org-2", service.signerDepartmentId("receiver-1")); } @Test void recycleUsesServerIdentityAndTimeForEveryDetail() throws Exception { prepareIdentity(); when(mapper.insertRecycle(any())).thenReturn(1); when(mapper.selectRecyclePrintsForUpdate("tenant-1", Arrays.asList("print-1", "print-2"))) .thenReturn(Arrays.asList(recycleRecord("print-1", "serial-1", "daihuishou"), recycleRecord("print-2", "serial-2", "daihuishou"))); when(mapper.markPrintsRecycled(any(), any(), any(), any())).thenReturn(2); when(mapper.insertRecycleDetail(any())).thenReturn(1); DmsWenjianHuishouResultVO result = service.recycle(recycleForm("print-2", "print-1")); org.junit.jupiter.api.Assertions.assertTrue(result.getId().matches("\\d+")); assertEquals(2, result.getHuishouFenshu()); org.junit.jupiter.api.Assertions.assertFalse(result.isRepeated()); ArgumentCaptor recycleCaptor = ArgumentCaptor.forClass(jnpf.dmsEntity.fenfa.DmsWenjianHuishouEntity.class); verify(mapper).insertRecycle(recycleCaptor.capture()); assertEquals(result.getId(), recycleCaptor.getValue().getId()); assertEquals("xiaohui", recycleCaptor.getValue().getChuliFangshi()); ArgumentCaptor details = ArgumentCaptor.forClass(jnpf.dmsEntity.fenfa.DmsWenjianHuishouMingxiEntity.class); verify(mapper, org.mockito.Mockito.times(2)).insertRecycleDetail(details.capture()); org.junit.jupiter.api.Assertions.assertTrue( details.getAllValues().get(0).getId().matches("\\d+")); assertEquals(result.getId(), details.getAllValues().get(0).getForeignId()); assertEquals("user-1", details.getAllValues().get(0).getHuishouRenId()); assertEquals("serial-1", details.getAllValues().get(0).getDayinLiushuihao()); ArgumentCaptor auditCaptor = ArgumentCaptor.forClass(DmsWenjianHuishouAuditEvent.class); verify(auditAdapter).recycle(auditCaptor.capture()); assertEquals(result.getId(), auditCaptor.getValue().getHuishouDanjuId()); assertEquals(2, auditCaptor.getValue().getDetails().size()); assertEquals(result.getId(), auditCaptor.getValue().getDetails().get(0).getForeignId()); assertEquals("recycle-signature-data", auditCaptor.getValue().getRelatedSigns().get(0).getSignId()); verify(mapper).markPrintsRecycled( org.mockito.ArgumentMatchers.eq("tenant-1"), org.mockito.ArgumentMatchers.eq(Arrays.asList("print-1", "print-2")), org.mockito.ArgumentMatchers.eq("user-1"), any()); } @Test void destructionCandidateUsesTenantAndRequiresPendingDestructionStatus() throws Exception { when(identity.tenantId()).thenReturn("tenant-1"); DmsWenjianXiaohuiMingxiVO candidate = new DmsWenjianXiaohuiMingxiVO(); candidate.setDayinJiluId("print-1"); candidate.setDayinLiushuihao("serial-1"); candidate.setBizStatus("daixiaohui"); when(mapper.selectDestructionCandidateBySerialNumber("tenant-1", "serial-1")) .thenReturn(candidate); assertEquals(candidate, service.destructionCandidate(" serial-1 ")); verify(mapper).selectDestructionCandidateBySerialNumber("tenant-1", "serial-1"); when(mapper.selectDestructionCandidateBySerialNumber("tenant-1", "missing")) .thenReturn(null); DataException exception = assertThrows(DataException.class, () -> service.destructionCandidate("missing")); assertEquals("未找到待销毁的文件副本", exception.getMessage()); } @Test void recycleCallbackBackedCopyPreservesUnsignedFieldsAndClosesDistribution() throws Exception { prepareIdentity(); when(mapper.insertRecycle(any())).thenReturn(1); DmsWenjianHuishouMingxiVO copy = recycleRecord("print-1", "serial-1", "daifafang"); copy.setEarlyRecycleEligible(true); copy.setDayinRenwuId("task-1"); copy.setQianshouRen(null); copy.setQianshouBumen(null); copy.setQianshouShijian(null); when(mapper.selectRecyclePrintsForUpdate("tenant-1", Arrays.asList("print-1"))) .thenReturn(Collections.singletonList(copy)); when(mapper.selectPrintTaskForUpdate("tenant-1", "task-1")).thenReturn(printTask("task-1")); when(mapper.markPrintsRecycled(any(), any(), any(), any())).thenReturn(1); when(mapper.insertRecycleDetail(any())).thenReturn(1); service.recycle(recycleForm("print-1")); verify(reprintGuard).requireUnlocked("tenant-1", "print-1"); verify(mapper).completeDistributionAfterRecycle(eq("tenant-1"), eq("task-1"), eq("user-1"), any()); ArgumentCaptor audit = ArgumentCaptor.forClass(DmsWenjianHuishouAuditEvent.class); verify(auditAdapter).recycle(audit.capture()); assertEquals("daifafang", audit.getValue().getBeforeStatuses().get("print-1")); assertNull(audit.getValue().getDetails().get(0).getQianshouRen()); assertNull(audit.getValue().getDetails().get(0).getQianshouShijian()); } @Test void recycleRejectsAnActiveReprintBeforeStateChanges() throws Exception { prepareIdentity(); when(mapper.insertRecycle(any())).thenReturn(1); DmsWenjianHuishouMingxiVO copy = recycleRecord("print-1", "serial-1", "daifafang"); copy.setEarlyRecycleEligible(true); when(mapper.selectRecyclePrintsForUpdate("tenant-1", Arrays.asList("print-1"))) .thenReturn(Collections.singletonList(copy)); org.mockito.Mockito.doThrow(new DataException("重打占用")) .when(reprintGuard).requireUnlocked("tenant-1", "print-1"); assertThrows(DataException.class, () -> service.recycle(recycleForm("print-1"))); verify(mapper, never()).markPrintsRecycled(any(), any(), any(), any()); verify(mapper, never()).insertRecycleDetail(any()); verify(auditAdapter, never()).recycle(any()); } @Test void recycleRejectsWhenTheGeneratedMainRecordCannotBeInserted() { prepareIdentity(); when(mapper.insertRecycle(any())).thenReturn(0); DataException exception = assertThrows(DataException.class, () -> service.recycle(recycleForm("print-1"))); assertEquals("保存回收单失败", exception.getMessage()); verify(mapper, never()).selectRecyclePrintsForUpdate(any(), any()); verify(auditAdapter, never()).recycle(any()); } @Test void recycleRejectsMixedStatusesBeforeUpdatingAnyRecord() { prepareIdentity(); when(mapper.insertRecycle(any())).thenReturn(1); when(mapper.selectRecyclePrintsForUpdate("tenant-1", Arrays.asList("print-1", "print-2"))) .thenReturn(Arrays.asList(recycleRecord("print-1", "serial-1", "daihuishou"), recycleRecord("print-2", "serial-2", "daixiaohui"))); DataException exception = assertThrows(DataException.class, () -> service.recycle(recycleForm("print-1", "print-2"))); assertEquals("文件副本尚未满足回收条件或已处理:serial-2", exception.getMessage()); verify(mapper, never()).markPrintsRecycled(any(), any(), any(), any()); verify(mapper, never()).insertRecycleDetail(any()); } @Test void recycleRejectsDuplicateDetailBeforeCreatingMainRecord() { DataException exception = assertThrows(DataException.class, () -> service.recycle(recycleForm("print-1", "print-1"))); assertEquals("打印记录不能重复回收:print-1", exception.getMessage()); verify(mapper, never()).insertRecycle(any()); verify(auditAdapter, never()).recycle(any()); } private void prepareIdentity() { when(identity.requireUser()).thenReturn(user); when(identity.tenantId(user)).thenReturn("tenant-1"); } private UserRelationIds userRelationIds(String organizationId) { return UserRelationIds.builder() .organize(Collections.singletonList(organizationId)) .build(); } private void prepareHandoverRecord() { DmsWenjianDayinJiluEntity record = new DmsWenjianDayinJiluEntity(); record.setId("print-1"); record.setDayinRenwuId("task-1"); record.setBizStatus("daifafang"); when(mapper.selectPrintForUpdate("tenant-1", "print-1", null)).thenReturn(record); when(mapper.selectDistributionTaskByPrintTaskForUpdate("tenant-1", "task-1")) .thenReturn(distributionTask()); when(mapper.selectPrintTaskForUpdate("tenant-1", "task-1")) .thenReturn(printTask("task-1")); } private DmsWenjianQianshouFafangForm handoverForm() { DmsWenjianQianshouFafangForm form = new DmsWenjianQianshouFafangForm(); form.setBizUserId("user-1"); form.setBizReviewUser("receiver-1"); form.setBizSign("issuer-signature-data"); form.setBizReviewSign("receiver-signature-data"); return form; } private DmsWenjianHuishouForm recycleForm(String... recordIds) { DmsWenjianHuishouForm form = new DmsWenjianHuishouForm(); form.setBizSign("recycle-signature-data"); java.util.List details = new java.util.ArrayList<>(); for (String recordId : recordIds) { DmsWenjianHuishouMingxiForm detail = new DmsWenjianHuishouMingxiForm(); detail.setDayinJiluId(recordId); details.add(detail); } form.setHuishouMingxi(details); return form; } private DmsWenjianHuishouMingxiVO recycleRecord( String id, String serialNumber, String status) { DmsWenjianHuishouMingxiVO result = new DmsWenjianHuishouMingxiVO(); result.setDayinJiluId(id); result.setDayinLiushuihao(serialNumber); result.setWenjianBianhao("DOC-001"); result.setWenjianMingcheng("文件一"); result.setBanbenhao("D(00)"); result.setBizStatus(status); return result; } private UserEntity user(String id, String departmentId, Integer enabledMark) { UserEntity result = new UserEntity(); result.setId(id); result.setOrganizeId(departmentId); result.setEnabledMark(enabledMark); return result; } private DmsWenjianFenfaFanweiEntity scope(String id, Integer copies) { DmsWenjianFenfaFanweiEntity scope = new DmsWenjianFenfaFanweiEntity(); scope.setId(id); scope.setBumenId("target-org-1"); scope.setWenjianLeibie("受控副本"); scope.setFenshu(copies); return scope; } private DmsWenjianDayinRenwuEntity printTask(String id) { DmsWenjianDayinRenwuEntity task = new DmsWenjianDayinRenwuEntity(); task.setId(id); task.setTenantId("tenant-1"); task.setLaiyuanLeixing("wenjian_xinzeng_shenhe"); task.setLaiyuanDanjuId("audit-1"); task.setJieshouBumenId("target-org-1"); task.setDayinFenshu(2); task.setBizStatus("daidayin"); return task; } private DmsWenjianFenfaRenwuEntity distributionTask() { DmsWenjianFenfaRenwuEntity task = new DmsWenjianFenfaRenwuEntity(); task.setId("distribution-1"); task.setDayinRenwuId("task-1"); task.setBizStatus("daifafang"); return task; } private DmsWenjianDayinRenwuEntity snapshot() { DmsWenjianDayinRenwuEntity snapshot = new DmsWenjianDayinRenwuEntity(); snapshot.setWenjianBianhao("DOC-001"); snapshot.setWenjianMingcheng("文件一"); snapshot.setBanbenhao("D(00)"); return snapshot; } }