刘光辉
5 小时以前 06f314545ac3514d7ce28939920866ae8188d31c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
package jnpf.dmsService.impl;
 
import jnpf.base.ActionResult;
import jnpf.base.BillRuleApi;
import jnpf.base.UserInfo;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import jnpf.audit.sdk.annotation.AuditLog;
import jnpf.dms.DmsResourceApi;
import jnpf.dmsEntity.DmsWenjianBanbenEntity;
import jnpf.dmsEntity.permission.form.DmsPersonalFolderFileForm;
import jnpf.dmsEntity.print.form.DmsWorkflowPrintFilesGrantForm;
import jnpf.dmsEntity.print.vo.DmsWorkflowPrintFilesGrantResultVO;
import jnpf.dmsEntity.preview.vo.DmsFilePageCountVO;
import jnpf.dmsEntity.recordprint.DmsJiluDayinLiushuiEntity;
import jnpf.dmsEntity.recordprint.DmsJiluDayinShenqingEntity;
import jnpf.dmsEntity.recordprint.DmsJiluDayinShenqingZixiangEntity;
import jnpf.dmsEntity.recordprint.form.DmsRecordPrintHuishouForm;
import jnpf.dmsEntity.recordprint.form.DmsRecordPrintHuishouItemForm;
import jnpf.dmsEntity.recordprint.form.DmsRecordPrintIssueForm;
import jnpf.dmsEntity.recordprint.form.DmsRecordPrintIssueItemForm;
import jnpf.dmsEntity.recordprint.vo.DmsRecordPrintConfirmVO;
import jnpf.dmsEntity.recordprint.vo.DmsRecordPrintPrepareVO;
import jnpf.dmsEntity.recordprint.vo.DmsRecordPrintSerialVO;
import jnpf.dmsMapper.DmsRecordPrintItemMapper;
import jnpf.dmsMapper.DmsRecordPrintRequestMapper;
import jnpf.dmsMapper.DmsJiluDayinLiushuiMapper;
import jnpf.dmsMapper.DmsWenjianBanbenMapper;
import jnpf.dmsPermission.DmsCurrentIdentity;
import jnpf.dmsService.DmsFilePageCountService;
import jnpf.dmsService.recordprint.DmsRecordPrintPageParser;
import jnpf.dmsService.recordprint.DmsRecordPrintProperties;
import jnpf.dmsService.recordprint.DmsRecordPrintReservation;
import jnpf.dmsService.recordprint.DmsRecordPrintReservationStore;
import jnpf.exception.DataException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.apache.ibatis.session.Configuration;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
 
import java.util.Date;
import java.util.List;
import java.util.UUID;
 
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;
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.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
 
@ExtendWith(MockitoExtension.class)
class DmsRecordPrintServiceImplTest {
 
    @Mock private DmsJiluDayinLiushuiMapper mapper;
    @Mock private DmsRecordPrintItemMapper itemMapper;
    @Mock private DmsRecordPrintRequestMapper requestMapper;
    @Mock private DmsWenjianBanbenMapper banbenMapper;
    @Mock private DmsResourceApi dmsResourceApi;
    @Mock private DmsCurrentIdentity identity;
    @Mock private BillRuleApi billRuleApi;
    @Mock private DmsFilePageCountService pageCountService;
    @Mock private DmsRecordPrintReservationStore reservationStore;
 
    private DmsRecordPrintServiceImpl service;
 
    @Test
    void completesPrintItemUsingEntity() {
        when(itemMapper.update(any(DmsJiluDayinShenqingZixiangEntity.class), any()))
                .thenReturn(1);
 
        service.complete(" item-1 ");
 
        ArgumentCaptor<DmsJiluDayinShenqingZixiangEntity> item =
                ArgumentCaptor.forClass(DmsJiluDayinShenqingZixiangEntity.class);
        verify(itemMapper).update(item.capture(), any());
        assertEquals("dayin_wancheng", item.getValue().getDayinZhuangtai());
    }
 
    @Test
    void rejectsMissingPrintItemOnCompletion() {
        when(itemMapper.update(any(DmsJiluDayinShenqingZixiangEntity.class), any()))
                .thenReturn(0);
 
        assertThrows(DataException.class, () -> service.complete("missing-item"));
    }
 
    /**
     * 初始化打印服务及当前登录用户。
     */
    @BeforeEach
    void setUp() throws Exception {
        if (TableInfoHelper.getTableInfo(DmsJiluDayinLiushuiEntity.class) == null) {
            TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new Configuration(), ""),
                    DmsJiluDayinLiushuiEntity.class);
        }
        if (TableInfoHelper.getTableInfo(DmsJiluDayinShenqingZixiangEntity.class) == null) {
            TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new Configuration(), ""),
                    DmsJiluDayinShenqingZixiangEntity.class);
        }
        DmsRecordPrintProperties properties = new DmsRecordPrintProperties();
        service = new DmsRecordPrintServiceImpl(mapper, itemMapper, requestMapper,
                banbenMapper, dmsResourceApi,
                identity, billRuleApi,
                pageCountService, new DmsRecordPrintPageParser(), properties,
                reservationStore);
    }
 
    /**
     * 验证流程申请中的全部业务版本会被组装为永久打印授权。
     */
    @Test
    void authorizesAllRecordPrintVersionsForApplicant() {
        DmsJiluDayinShenqingEntity request = new DmsJiluDayinShenqingEntity();
        request.setId("request-1");
        request.setShenqingren(" user-1 ");
        DmsJiluDayinShenqingZixiangEntity first = new DmsJiluDayinShenqingZixiangEntity();
        first.setBanbenId("version-1");
        DmsJiluDayinShenqingZixiangEntity second = new DmsJiluDayinShenqingZixiangEntity();
        second.setBanbenId("version-2");
        DmsWenjianBanbenEntity firstVersion = businessVersion("version-1");
        DmsWenjianBanbenEntity secondVersion = businessVersion("version-2");
        DmsWorkflowPrintFilesGrantResultVO expected =
                new DmsWorkflowPrintFilesGrantResultVO(2, 2);
        ActionResult<DmsWorkflowPrintFilesGrantResultVO> response = new ActionResult<>();
        response.setCode(200);
        response.setData(expected);
 
        when(requestMapper.selectList(any())).thenReturn(List.of(request));
        when(itemMapper.selectList(any())).thenReturn(List.of(first, second));
        when(banbenMapper.selectList(any())).thenReturn(List.of(firstVersion, secondVersion));
        when(dmsResourceApi.grantWorkflowPrintFiles(eq("jnpf-flowable"), any()))
                .thenReturn(response);
 
        DmsWorkflowPrintFilesGrantResultVO actual = service.authorize(" flow-1 ", " task-1 ");
 
        assertSame(expected, actual);
        ArgumentCaptor<DmsWorkflowPrintFilesGrantForm> captor =
                ArgumentCaptor.forClass(DmsWorkflowPrintFilesGrantForm.class);
        verify(dmsResourceApi).grantWorkflowPrintFiles(eq("jnpf-flowable"), captor.capture());
        assertEquals("user-1", captor.getValue().getUserId());
        assertEquals(2, captor.getValue().getFiles().size());
        for (DmsPersonalFolderFileForm.FileRelation relation : captor.getValue().getFiles()) {
            assertNull(relation.getValidTo());
        }
    }
 
    /** 验证不存在流程申请时不会调用授权接口。 */
    @Test
    void rejectsAuthorizationWhenRequestDoesNotExist() {
        when(requestMapper.selectList(any())).thenReturn(List.of());
 
        DataException exception = assertThrows(DataException.class,
                () -> service.authorize("flow-1", "task-1"));
 
        assertTrue(exception.getMessage().contains("未找到记录打印申请"));
        verify(dmsResourceApi, never()).grantWorkflowPrintFiles(any(), any());
    }
 
    /**
     * 验证流水查询由 A、B 表实体组装指定的四个返回字段。
     */
    @Test
    void findsUnissuedRecordBySerialNumber() throws Exception {
        DmsJiluDayinLiushuiEntity record = new DmsJiluDayinLiushuiEntity();
        record.setId("record-1");
        record.setLiushuihao("LS001");
        record.setDayinZixiangId("item-1");
        DmsJiluDayinShenqingZixiangEntity item = new DmsJiluDayinShenqingZixiangEntity();
        item.setWenjianMingcheng("检验记录");
        item.setBanbenhao("V2");
        when(mapper.selectOne(any())).thenReturn(record);
        when(itemMapper.selectOne(any())).thenReturn(item);
 
        DmsRecordPrintSerialVO result = service.findBySerialNumber(" LS001 ", "");
 
        assertEquals("LS001", result.getLiushuihao());
        assertEquals("record-1", result.getId());
        assertEquals("检验记录", result.getWenjianMingcheng());
        assertEquals("V2", result.getBanbenhao());
    }
 
    /**
     * 验证发放人有值时拒绝条码重复签收或发放。
     */
    @Test
    void rejectsSerialNumberWithIssuer() {
        DmsJiluDayinLiushuiEntity record = new DmsJiluDayinLiushuiEntity();
        record.setFafangren("user-1");
        when(mapper.selectOne(any())).thenReturn(record);
 
        DataException exception = assertThrows(DataException.class,
                () -> service.findBySerialNumber("LS001", ""));
 
        assertEquals("该条码已签收或发放请勿重复操作", exception.getMessage());
        verify(itemMapper, never()).selectOne(any());
    }
 
    /**
     * 验证批量发放使用条件更新写入双方信息和同一个操作时间。
     */
    @Test
    void issuesEveryRecordWithOneOperationTime() throws Exception {
        DmsJiluDayinLiushuiEntity first = issueRecord("record-1", "LS001", null);
        DmsJiluDayinLiushuiEntity second = issueRecord("record-2", "LS002", "");
        DmsJiluDayinShenqingZixiangEntity printItem = new DmsJiluDayinShenqingZixiangEntity();
        printItem.setDayinFenshu("5");
        printItem.setYiqianfaShuliang("1");
        when(mapper.selectList(any())).thenReturn(java.util.Arrays.asList(first, second));
        when(mapper.update(isNull(), any())).thenReturn(1);
        when(mapper.selectPrintItemForUpdate("item-1")).thenReturn(printItem);
        when(itemMapper.update(any(DmsJiluDayinShenqingZixiangEntity.class), any()))
                .thenReturn(1);
 
        service.issue(issueForm(issueItem("LS001", "issuer-1", "dept-1", "receiver-1", "dept-2"),
                issueItem("LS002", "issuer-2", "dept-3", "receiver-2", "dept-4")));
 
        ArgumentCaptor<LambdaUpdateWrapper<DmsJiluDayinLiushuiEntity>> captor =
                ArgumentCaptor.forClass(LambdaUpdateWrapper.class);
        verify(mapper, times(2)).update(isNull(), captor.capture());
        Date operationTime = null;
        for (LambdaUpdateWrapper<DmsJiluDayinLiushuiEntity> update : captor.getAllValues()) {
            String sqlSet = update.getSqlSet();
            assertTrue(sqlSet.contains("fafangren"));
            assertTrue(sqlSet.contains("fafang_bumen"));
            assertTrue(sqlSet.contains("jieshouren"));
            assertTrue(sqlSet.contains("jieshou_bumen"));
            assertTrue(sqlSet.contains("fafang_shijian"));
            assertTrue(sqlSet.contains("jieshou_shijian"));
            assertTrue(update.getSqlSegment().contains("f_delete_mark"));
            assertTrue(update.getSqlSegment().contains("fafangren"));
            for (Object value : update.getParamNameValuePairs().values()) {
                if (value instanceof Date) {
                    if (operationTime == null) {
                        operationTime = (Date) value;
                    } else {
                        assertSame(operationTime, value);
                    }
                }
            }
        }
        assertNotNull(operationTime);
        ArgumentCaptor<DmsJiluDayinShenqingZixiangEntity> updatedItem =
                ArgumentCaptor.forClass(DmsJiluDayinShenqingZixiangEntity.class);
        verify(itemMapper).update(updatedItem.capture(), any());
        assertEquals("3", updatedItem.getValue().getYiqianfaShuliang());
        assertEquals("2", updatedItem.getValue().getWeiqianfaShuliang());
        verify(mapper).selectPrintItemForUpdate("item-1");
    }
 
    @Test
    void rejectsIssueWhenPrintItemIsMissing() {
        when(mapper.selectList(any())).thenReturn(java.util.List.of(
                issueRecord("record-1", "LS001", null)));
        when(mapper.update(isNull(), any())).thenReturn(1);
 
        assertThrows(DataException.class,
                () -> service.issue(issueForm(
                        issueItem("LS001", "issuer-1", "dept-1", "receiver-1", "dept-2"))));
        verify(itemMapper, never()).update(any(DmsJiluDayinShenqingZixiangEntity.class), any());
    }
 
    @Test
    void updatesIssueCountsForEachPrintItem() {
        DmsJiluDayinLiushuiEntity first = issueRecord("record-1", "LS001", null);
        DmsJiluDayinLiushuiEntity second = issueRecord("record-2", "LS002", null);
        second.setDayinZixiangId("item-2");
        DmsJiluDayinShenqingZixiangEntity firstItem = new DmsJiluDayinShenqingZixiangEntity();
        firstItem.setDayinFenshu("3");
        DmsJiluDayinShenqingZixiangEntity secondItem = new DmsJiluDayinShenqingZixiangEntity();
        secondItem.setDayinFenshu("4");
        secondItem.setYiqianfaShuliang("2");
        when(mapper.selectList(any())).thenReturn(java.util.List.of(first, second));
        when(mapper.update(isNull(), any())).thenReturn(1);
        when(mapper.selectPrintItemForUpdate("item-1")).thenReturn(firstItem);
        when(mapper.selectPrintItemForUpdate("item-2")).thenReturn(secondItem);
        when(itemMapper.update(any(DmsJiluDayinShenqingZixiangEntity.class), any()))
                .thenReturn(1);
 
        service.issue(issueForm(issueItem("LS001", "issuer-1", "dept-1", "receiver-1", "dept-2"),
                issueItem("LS002", "issuer-2", "dept-3", "receiver-2", "dept-4")));
 
        ArgumentCaptor<DmsJiluDayinShenqingZixiangEntity> updatedItems =
                ArgumentCaptor.forClass(DmsJiluDayinShenqingZixiangEntity.class);
        verify(itemMapper, times(2)).update(updatedItems.capture(), any());
        assertEquals("1", updatedItems.getAllValues().get(0).getYiqianfaShuliang());
        assertEquals("2", updatedItems.getAllValues().get(0).getWeiqianfaShuliang());
        assertEquals("3", updatedItems.getAllValues().get(1).getYiqianfaShuliang());
        assertEquals("1", updatedItems.getAllValues().get(1).getWeiqianfaShuliang());
    }
 
    @Test
    void accumulatesRecoveredCountsByPrintItem() {
        DmsJiluDayinLiushuiEntity first = issueRecord("record-1", "LS001", null);
        DmsJiluDayinLiushuiEntity second = issueRecord("record-2", "LS002", null);
        DmsJiluDayinLiushuiEntity third = issueRecord("record-3", "LS003", null);
        third.setDayinZixiangId("item-2");
        DmsJiluDayinShenqingZixiangEntity firstItem = new DmsJiluDayinShenqingZixiangEntity();
        firstItem.setYihuishouShuliang("1");
        DmsJiluDayinShenqingZixiangEntity secondItem = new DmsJiluDayinShenqingZixiangEntity();
        when(mapper.selectList(any())).thenReturn(java.util.List.of(first, second, third));
        when(mapper.update(isNull(), any())).thenReturn(1);
        when(mapper.selectPrintItemForUpdate("item-1")).thenReturn(firstItem);
        when(mapper.selectPrintItemForUpdate("item-2")).thenReturn(secondItem);
        when(itemMapper.update(any(DmsJiluDayinShenqingZixiangEntity.class), any()))
                .thenReturn(1);
 
        service.huishou(huishouForm("LS001", "LS002", "LS003"));
 
        ArgumentCaptor<DmsJiluDayinShenqingZixiangEntity> updatedItems =
                ArgumentCaptor.forClass(DmsJiluDayinShenqingZixiangEntity.class);
        verify(itemMapper, times(2)).update(updatedItems.capture(), any());
        assertEquals("3", updatedItems.getAllValues().get(0).getYihuishouShuliang());
        assertEquals("1", updatedItems.getAllValues().get(1).getYihuishouShuliang());
        verify(mapper, times(3)).update(isNull(), any());
    }
 
    @Test
    void rejectsRecoveryWhenPrintItemIsMissing() {
        when(mapper.selectList(any())).thenReturn(java.util.List.of(
                issueRecord("record-1", "LS001", null)));
        when(mapper.update(isNull(), any())).thenReturn(1);
 
        assertThrows(DataException.class, () -> service.huishou(huishouForm("LS001")));
        verify(itemMapper, never()).update(any(DmsJiluDayinShenqingZixiangEntity.class), any());
    }
 
    /**
     * 验证任一流水已经发放时整批拒绝更新。
     */
    @Test
    void rejectsIssueBatchContainingIssuedRecord() {
        when(mapper.selectList(any())).thenReturn(java.util.Arrays.asList(
                issueRecord("record-1", "LS001", "issuer-0")));
 
        DataException exception = assertThrows(DataException.class,
                () -> service.issue(issueForm(
                        issueItem("LS001", "issuer-1", "dept-1", "receiver-1", "dept-2"))));
 
        assertEquals("该条码已签收或发放请勿重复操作", exception.getMessage());
        verify(mapper, never()).update(isNull(), any());
    }
 
    /**
     * 验证发放方法声明成功和失败审计动作。
     */
    @Test
    void issueMethodDeclaresAuditLog() throws Exception {
        AuditLog auditLog = DmsRecordPrintServiceImpl.class
                .getMethod("issue", DmsRecordPrintIssueForm.class)
                .getAnnotation(AuditLog.class);
 
        assertNotNull(auditLog);
        assertEquals("FAFANG", auditLog.action());
        assertEquals("FAFANG_FAIL", auditLog.failureAction());
        assertEquals("dms_jilu_dayin_liushui", auditLog.targetTable());
    }
 
    @Test
    void confirmMethodDoesNotAddPrintAudit() throws Exception {
        AuditLog auditLog = DmsRecordPrintServiceImpl.class
                .getMethod("confirm", String.class)
                .getAnnotation(AuditLog.class);
 
        // 打印确认不额外记录正式审计,保留实际流水与业务状态。
        assertNull(auditLog);
    }
 
    /**
     * 验证成册预览按份数预占流水,但不更新 A、B 表。
     */
    @Test
    void preparesBoundCopiesWithoutPersistingPrintRecords() throws Exception {
        DmsJiluDayinShenqingZixiangEntity item = item("chengce", "2", null);
        prepareSuccessfulReservation(item, "JL001", "JL002");
 
        DmsRecordPrintPrepareVO result = service.prepare("item-1");
 
        assertEquals(2, result.getSerialCount());
        assertEquals(2, result.getCopies().size());
        assertEquals("JL001", result.getCopies().get(0).getSerialNo());
        assertEquals("JL002", result.getCopies().get(1).getSerialNo());
        assertNull(result.getCopies().get(0).getPageNo());
        assertNull(result.getCopies().get(0).getRecordId());
        verify(pageCountService, never()).getPageCount(any());
        assertReservedMetadata(item, 2);
    }
 
    /**
     * 验证分散打印的流水号数量等于去重页数乘以打印份数。
     */
    @Test
    void preparesLoosePageCopiesByPageCountTimesPrintCount() throws Exception {
        DmsJiluDayinShenqingZixiangEntity item = item("fensan", "2", "3,1,3");
        UUID fileCenterVersionId = UUID.fromString("00000000-0000-0000-0000-000000000002");
        DmsWenjianBanbenEntity banben = new DmsWenjianBanbenEntity();
        banben.setWenjianZhongxinWenjianId("00000000-0000-0000-0000-000000000001");
        banben.setWenjianZhongxinBanbenId(fileCenterVersionId.toString());
        DmsFilePageCountVO pageCount = new DmsFilePageCountVO();
        pageCount.setPageCount(4);
        when(banbenMapper.selectById("tenant-1", "business-version-1")).thenReturn(banben);
        when(pageCountService.getPageCount(fileCenterVersionId)).thenReturn(pageCount);
        prepareSuccessfulReservation(item, "JL001", "JL002", "JL003", "JL004");
 
        DmsRecordPrintPrepareVO result = service.prepare("item-1");
 
        assertEquals(4, result.getSerialCount());
        assertEquals(java.util.Arrays.asList(1, 3), result.getSelectedPages());
        assertEquals(1, result.getCopies().get(0).getCopyIndex());
        assertEquals(1, result.getCopies().get(0).getPageNo());
        assertEquals(3, result.getCopies().get(1).getPageNo());
        assertEquals(2, result.getCopies().get(2).getCopyIndex());
        verify(pageCountService).getPageCount(fileCenterVersionId);
        assertReservedMetadata(item, 4);
    }
 
    /**
     * 验证重复打开预览时复用原预占,不再次消耗流水号。
     */
    @Test
    void reusesExistingReservationWithoutTakingNewSerialNumbers() throws Exception {
        DmsJiluDayinShenqingZixiangEntity item = item("chengce", "1", null);
        DmsRecordPrintReservation reservation = reservation(item, "JL001");
        UserInfo user = new UserInfo();
        user.setUserId("user-1");
        when(identity.requireUser()).thenReturn(user);
        when(identity.tenantId(user)).thenReturn("tenant-1");
        when(mapper.selectPrintItemForUpdate("item-1")).thenReturn(item);
        when(mapper.countActivePrintRecords("item-1")).thenReturn(0);
        when(reservationStore.findByPrintItem("tenant-1", "item-1"))
                .thenReturn(reservation);
 
        DmsRecordPrintPrepareVO result = service.prepare("item-1");
 
        assertEquals("reservation-1", result.getReservationId());
        assertEquals("JL001", result.getCopies().get(0).getSerialNo());
        verify(mapper, never()).lockSerialAllocation();
        verify(billRuleApi, never()).getBillNumber(any());
        verify(reservationStore, never()).create(any(), anyLong());
    }
 
    /**
     * 验证点击打印确认后才写入 A 表并更新 B 表。
     */
    @Test
    void confirmsReservedSerialNumbersAndPersistsPrintMetadata() throws Exception {
        DmsJiluDayinShenqingZixiangEntity item = item("chengce", "2", null);
        DmsRecordPrintReservation reservation = reservation(
                item, "JL001", "JL002");
        UserInfo user = new UserInfo();
        user.setUserId("user-1");
        when(identity.requireUser()).thenReturn(user);
        when(identity.tenantId(user)).thenReturn("tenant-1");
        when(reservationStore.findById("tenant-1", "reservation-1"))
                .thenReturn(reservation);
        when(mapper.selectPrintItemForUpdate("item-1")).thenReturn(item);
        when(mapper.selectList(any())).thenReturn(java.util.Collections.emptyList());
        when(mapper.countActivePrintRecords("item-1")).thenReturn(0);
        when(mapper.insertPrintRecords(any())).thenReturn(2);
        when(mapper.markPrintItemProcessing(any())).thenReturn(1);
 
        DmsRecordPrintConfirmVO result = service.confirm("reservation-1");
 
        assertEquals(Boolean.TRUE, result.getConfirmed());
        assertEquals(2, result.getCopies().size());
        assertEquals("JL001", result.getCopies().get(0).getSerialNo());
        assertNotNull(result.getCopies().get(0).getRecordId());
        assertPersistedMetadata(item, 2);
        verify(reservationStore).markConfirmed(any(DmsRecordPrintReservation.class), anyLong());
    }
 
    /**
     * 验证已确认预占的网络重试直接返回缓存结果,不重复写库。
     */
    @Test
    void returnsConfirmedReservationIdempotently() throws Exception {
        DmsJiluDayinShenqingZixiangEntity item = item("chengce", "1", null);
        DmsRecordPrintReservation reservation = reservation(item, "JL001");
        reservation.setStatus(DmsRecordPrintReservation.STATUS_CONFIRMED);
        reservation.setRecordIds(java.util.Collections.singletonList("record-1"));
        UserInfo user = new UserInfo();
        user.setUserId("user-1");
        when(identity.requireUser()).thenReturn(user);
        when(identity.tenantId(user)).thenReturn("tenant-1");
        when(reservationStore.findById("tenant-1", "reservation-1"))
                .thenReturn(reservation);
 
        DmsRecordPrintConfirmVO result = service.confirm("reservation-1");
 
        assertEquals("record-1", result.getCopies().get(0).getRecordId());
        verify(mapper, never()).selectPrintItemForUpdate(any());
        verify(mapper, never()).insertPrintRecords(any());
    }
 
    /**
     * 验证已经存在 A 表记录时,在取号之前拒绝重复打印。
     */
    @Test
    void rejectsRepeatedPrintBeforeTakingSerialNumber() {
        DmsJiluDayinShenqingZixiangEntity item = item("chengce", "1", null);
        when(mapper.selectPrintItemForUpdate("item-1")).thenReturn(item);
        when(mapper.countActivePrintRecords("item-1")).thenReturn(1);
 
        DataException exception = assertThrows(DataException.class,
                () -> service.prepare("item-1"));
 
        assertEquals("该记录已经打印,不允许重复打印", exception.getMessage());
        verify(mapper, never()).lockSerialAllocation();
        verify(billRuleApi, never()).getBillNumber(any());
    }
 
    /**
     * 配置一次成功打印需要的 Mapper 和流水号返回值。
     */
    private void prepareSuccessfulReservation(
            DmsJiluDayinShenqingZixiangEntity item, String... serialNumbers) throws Exception {
        UserInfo user = new UserInfo();
        user.setUserId("user-1");
        when(identity.requireUser()).thenReturn(user);
        when(identity.tenantId(user)).thenReturn("tenant-1");
        when(mapper.selectPrintItemForUpdate("item-1")).thenReturn(item);
        when(mapper.countActivePrintRecords("item-1")).thenReturn(0);
        when(mapper.lockSerialAllocation()).thenReturn(1);
        ActionResult<String>[] results = new ActionResult[serialNumbers.length];
        for (int index = 0; index < serialNumbers.length; index++) {
            results[index] = ActionResult.success("取号成功", serialNumbers[index]);
        }
        when(billRuleApi.getBillNumber("jiluDayinLiushuihao"))
                .thenReturn(results[0], java.util.Arrays.copyOfRange(results, 1, results.length));
    }
 
    /**
     * 断言预览只保存 Redis 预占,不写 A、B 表。
     */
    private void assertReservedMetadata(
            DmsJiluDayinShenqingZixiangEntity item, int expectedRecords) {
        ArgumentCaptor<DmsRecordPrintReservation> captor =
                ArgumentCaptor.forClass(DmsRecordPrintReservation.class);
        verify(reservationStore).create(captor.capture(), anyLong());
        DmsRecordPrintReservation reservation = captor.getValue();
        assertEquals(expectedRecords, reservation.getSerialNumbers().size());
        assertEquals("user-1", reservation.getUserId());
        assertEquals("tenant-1", reservation.getTenantId());
        assertEquals("item-1", reservation.getPrintItemId());
        assertEquals(DmsRecordPrintReservation.STATUS_PENDING, reservation.getStatus());
        verify(mapper, never()).insertPrintRecords(any());
        verify(mapper, never()).markPrintItemProcessing(any());
        assertNull(item.getDayinren());
        assertNull(item.getDayinShijian());
        assertEquals("daiqianfa", item.getDayinZhuangtai());
    }
 
    /**
     * 断言确认阶段写入的所有 A 表记录与 B 表使用同一打印元数据。
     */
    private void assertPersistedMetadata(
            DmsJiluDayinShenqingZixiangEntity item, int expectedRecords) {
        ArgumentCaptor<List<DmsJiluDayinLiushuiEntity>> captor =
                ArgumentCaptor.forClass(List.class);
        verify(mapper).insertPrintRecords(captor.capture());
        List<DmsJiluDayinLiushuiEntity> records = captor.getValue();
        assertEquals(expectedRecords, records.size());
        Date printTime = records.get(0).getDayinShijian();
        for (DmsJiluDayinLiushuiEntity record : records) {
            assertEquals("user-1", record.getDayinren());
            assertSame(printTime, record.getDayinShijian());
            assertEquals("item-1", record.getDayinZixiangId());
            assertEquals("print-1", record.getDayinId());
        }
        assertEquals("user-1", item.getDayinren());
        assertSame(printTime, item.getDayinShijian());
        assertEquals("dayinzhong", item.getDayinZhuangtai());
    }
 
    private DmsRecordPrintReservation reservation(
            DmsJiluDayinShenqingZixiangEntity item, String... serialNumbers) {
        DmsRecordPrintReservation reservation = new DmsRecordPrintReservation();
        reservation.setReservationId("reservation-1");
        reservation.setTenantId("tenant-1");
        reservation.setUserId("user-1");
        reservation.setPrintItemId(item.getId());
        reservation.setItemVersion(item.getVersion());
        reservation.setPrintType(item.getDayinLeixing());
        reservation.setPrintCount(Integer.valueOf(item.getDayinFenshu()));
        reservation.setSelectedPages(java.util.Collections.emptyList());
        reservation.setSerialNumbers(java.util.Arrays.asList(serialNumbers));
        reservation.setStatus(DmsRecordPrintReservation.STATUS_PENDING);
        reservation.setCreatedAt(System.currentTimeMillis());
        reservation.setExpiresAt(System.currentTimeMillis() + 60000L);
        return reservation;
    }
 
    /**
     * 构造可打印的 B 表实体。
     */
    private DmsJiluDayinShenqingZixiangEntity item(
            String printType, String printCount, String pages) {
        DmsJiluDayinShenqingZixiangEntity item = new DmsJiluDayinShenqingZixiangEntity();
        item.setId("item-1");
        item.setForeignId("print-1");
        item.setBanbenId("business-version-1");
        item.setDayinLeixing(printType);
        item.setDayinFenshu(printCount);
        item.setYema(pages);
        item.setYidayinFenshu("0");
        item.setDayinZhuangtai("daiqianfa");
        return item;
    }
 
    /** 构造已映射文件中心文件和版本的业务版本实体。 */
    private DmsWenjianBanbenEntity businessVersion(String id) {
        DmsWenjianBanbenEntity version = new DmsWenjianBanbenEntity();
        version.setId(id);
        version.setWenjianZhongxinWenjianId(UUID.randomUUID().toString());
        version.setWenjianZhongxinBanbenId(UUID.randomUUID().toString());
        return version;
    }
 
    /**
     * 构造打印流水实体。
     */
    private DmsJiluDayinLiushuiEntity issueRecord(
            String id, String serialNumber, String issuer) {
        DmsJiluDayinLiushuiEntity record = new DmsJiluDayinLiushuiEntity();
        record.setId(id);
        record.setLiushuihao(serialNumber);
        record.setFafangren(issuer);
        record.setDayinZixiangId("item-1");
        return record;
    }
 
    /**
     * 构造批量发放请求。
     */
    private DmsRecordPrintIssueForm issueForm(DmsRecordPrintIssueItemForm... items) {
        DmsRecordPrintIssueForm form = new DmsRecordPrintIssueForm();
        form.setData(java.util.Arrays.asList(items));
        return form;
    }
 
    private DmsRecordPrintHuishouForm huishouForm(String... serialNumbers) {
        DmsRecordPrintHuishouForm form = new DmsRecordPrintHuishouForm();
        java.util.List<DmsRecordPrintHuishouItemForm> items = new java.util.ArrayList<>();
        for (String serialNumber : serialNumbers) {
            DmsRecordPrintHuishouItemForm item = new DmsRecordPrintHuishouItemForm();
            item.setLiushuihao(serialNumber);
            item.setGuihuanren("returner");
            item.setGuihuanBumen("return-dept");
            item.setHuishouren("receiver");
            item.setHuishouBumen("receive-dept");
            items.add(item);
        }
        form.setData(items);
        return form;
    }
 
    /**
     * 构造单条发放明细。
     */
    private DmsRecordPrintIssueItemForm issueItem(
            String serialNumber, String issuer, String issuerDepartment,
            String receiver, String receiverDepartment) {
        DmsRecordPrintIssueItemForm item = new DmsRecordPrintIssueItemForm();
        item.setLiushuihao(serialNumber);
        item.setFafangren(issuer);
        item.setFafangBumen(issuerDepartment);
        item.setJieshouren(receiver);
        item.setJieshouBumen(receiverDepartment);
        return item;
    }
}