刘光辉
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
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
package jnpf.dmsService.impl;
 
import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import jnpf.audit.sdk.annotation.AuditLog;
import jnpf.base.ActionResult;
import jnpf.base.ActionResultCode;
import jnpf.base.BillRuleApi;
import jnpf.base.UserInfo;
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.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.DmsRecordPrintCopyVO;
import jnpf.dmsEntity.recordprint.vo.DmsRecordPrintConfirmVO;
import jnpf.dmsEntity.recordprint.vo.DmsRecordPrintPrepareVO;
import jnpf.dmsEntity.recordprint.vo.DmsRecordPrintSerialVO;
import jnpf.dmsEntity.preview.vo.DmsFilePageCountVO;
import jnpf.dmsMapper.DmsJiluDayinLiushuiMapper;
import jnpf.dmsMapper.DmsRecordPrintItemMapper;
import jnpf.dmsMapper.DmsRecordPrintRequestMapper;
import jnpf.dmsMapper.DmsWenjianBanbenMapper;
import jnpf.dmsPermission.DmsCurrentIdentity;
import jnpf.dmsService.DmsFilePageCountService;
import jnpf.dmsService.DmsRecordPrintService;
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 lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
 
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
 
/**
 * 记录文件打印准备业务实现。
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class DmsRecordPrintServiceImpl implements DmsRecordPrintService {
 
    private static final String PRINT_TYPE_BOUND = "chengce";
    private static final String PRINT_TYPE_LOOSE = "fensan";
    private static final String PRINT_STATUS_PROCESSING = "dayinzhong";
    private static final String PRINT_STATUS_COMPLETE = "dayin_wancheng";
    private static final String CHAZHAO_LEIXING_HUISHOU = "huishou";
    private static final String FLOWABLE_SERVICE_ID = "jnpf-flowable";
 
    private final DmsJiluDayinLiushuiMapper mapper;
    private final DmsRecordPrintItemMapper itemMapper;
    private final DmsRecordPrintRequestMapper requestMapper;
    private final DmsWenjianBanbenMapper banbenMapper;
    private final DmsResourceApi dmsResourceApi;
    private final DmsCurrentIdentity identity;
    private final BillRuleApi billRuleApi;
    private final DmsFilePageCountService pageCountService;
    private final DmsRecordPrintPageParser pageParser;
    private final DmsRecordPrintProperties properties;
    private final DmsRecordPrintReservationStore reservationStore;
 
    @Override
    public void complete(String printItemId) throws DataException {
        String id = requireValue(printItemId, "打印申请子项ID");
        DmsJiluDayinShenqingZixiangEntity item = new DmsJiluDayinShenqingZixiangEntity();
        item.setDayinZhuangtai(PRINT_STATUS_COMPLETE);
        if (itemMapper.update(item,
                Wrappers.<DmsJiluDayinShenqingZixiangEntity>lambdaUpdate()
                        .eq(DmsJiluDayinShenqingZixiangEntity::getId, id)
                        .isNull(DmsJiluDayinShenqingZixiangEntity::getDeleteMark)) != 1) {
            throw new DataException("未找到有效的记录打印申请子项:" + id);
        }
    }
 
    /**
     * 查询流程对应的记录打印申请和全部子项,并为申请人授予永久打印权限。
     *
     * @param flowId 流程实例 ID
     * @param flowTaskId 流程任务 ID
     * @return 文件打印授权结果
     * @throws DataException 申请、子项或业务版本无效,或者授权接口调用失败
     */
    @Override
    public DmsWorkflowPrintFilesGrantResultVO authorize(
            String flowId, String flowTaskId) throws DataException {
        String normalizedFlowId = requireValue(flowId, "流程ID(flow_id)");
        String normalizedFlowTaskId = requireValue(flowTaskId, "流程任务ID(flow_task_id)");
        DmsJiluDayinShenqingEntity request = findPrintRequest(
                normalizedFlowId, normalizedFlowTaskId);
        List<DmsJiluDayinShenqingZixiangEntity> items = findPrintItems(request.getId());
        Set<String> versionIds = extractVersionIds(items);
        List<DmsWenjianBanbenEntity> versions = findBusinessVersions(versionIds);
        DmsWorkflowPrintFilesGrantForm form = buildPrintGrantForm(request, versions);
 
        ActionResult<DmsWorkflowPrintFilesGrantResultVO> response;
        try {
            response = dmsResourceApi.grantWorkflowPrintFiles(FLOWABLE_SERVICE_ID, form);
        } catch (RuntimeException exception) {
            throw new DataException("调用DMS文件打印授权接口失败");
        }
        if (response == null || !ActionResultCode.SUCCESS.getCode().equals(response.getCode())
                || response.getData() == null) {
            String message = response == null ? null : response.getMsg();
            throw new DataException(isBlank(message) ? "记录文件打印授权失败或响应数据不完整" : message);
        }
        return response.getData();
    }
 
    /** 查询流程唯一对应的未删除记录打印申请。 */
    private DmsJiluDayinShenqingEntity findPrintRequest(String flowId, String flowTaskId) {
        List<DmsJiluDayinShenqingEntity> rows = requestMapper.selectList(
                Wrappers.<DmsJiluDayinShenqingEntity>lambdaQuery()
                        .eq(DmsJiluDayinShenqingEntity::getFlowId, flowId)
                        .eq(DmsJiluDayinShenqingEntity::getFlowTaskId, flowTaskId)
                        .isNull(DmsJiluDayinShenqingEntity::getDeleteMark));
        if (rows.isEmpty()) {
            throw new DataException("未找到记录打印申请:flow_id=" + flowId
                    + ", flow_task_id=" + flowTaskId);
        }
        if (rows.size() > 1) {
            throw new DataException("flow_id与flow_task_id关联了多条记录打印申请,无法授权");
        }
        return rows.get(0);
    }
 
    /** 根据申请主键查询全部未删除的记录打印子项。 */
    private List<DmsJiluDayinShenqingZixiangEntity> findPrintItems(String requestId) {
        List<DmsJiluDayinShenqingZixiangEntity> rows = itemMapper.selectList(
                Wrappers.<DmsJiluDayinShenqingZixiangEntity>lambdaQuery()
                        .eq(DmsJiluDayinShenqingZixiangEntity::getForeignId, requestId)
                        .isNull(DmsJiluDayinShenqingZixiangEntity::getDeleteMark));
        if (rows.isEmpty()) {
            throw new DataException("记录打印申请未关联打印子项,无法授权");
        }
        return rows;
    }
 
    /** 提取子项业务版本 ID,并按原顺序去重。 */
    private Set<String> extractVersionIds(List<DmsJiluDayinShenqingZixiangEntity> items) {
        Set<String> ids = new LinkedHashSet<>();
        for (DmsJiluDayinShenqingZixiangEntity item : items) {
            ids.add(requireValue(item.getBanbenId(), "记录打印子项版本ID(banben_id)"));
        }
        return ids;
    }
 
    /** 批量查询未删除的业务文件版本。 */
    private List<DmsWenjianBanbenEntity> findBusinessVersions(Set<String> versionIds) {
        List<DmsWenjianBanbenEntity> rows = banbenMapper.selectList(
                Wrappers.<DmsWenjianBanbenEntity>lambdaQuery()
                        .in(DmsWenjianBanbenEntity::getId, versionIds)
                        .isNull(DmsWenjianBanbenEntity::getDeleteMark));
        if (rows.size() != versionIds.size()) {
            throw new DataException("部分记录打印子项未找到对应的有效文件版本");
        }
        return rows;
    }
 
    /** 组装申请人的永久文件打印授权请求。 */
    private DmsWorkflowPrintFilesGrantForm buildPrintGrantForm(
            DmsJiluDayinShenqingEntity request, List<DmsWenjianBanbenEntity> versions) {
        List<DmsPersonalFolderFileForm.FileRelation> files = new ArrayList<>(versions.size());
        for (DmsWenjianBanbenEntity version : versions) {
            DmsPersonalFolderFileForm.FileRelation relation =
                    new DmsPersonalFolderFileForm.FileRelation();
            try {
                relation.setDocumentId(UUID.fromString(requireValue(
                        version.getWenjianZhongxinWenjianId(), "文件中心文件ID")));
                relation.setFileVersionId(UUID.fromString(requireValue(
                        version.getWenjianZhongxinBanbenId(), "文件中心版本ID")));
            } catch (IllegalArgumentException exception) {
                throw new DataException("文件中心文件ID或版本ID格式不正确:banben_id="
                        + version.getId());
            }
            relation.setValidTo(null);
            files.add(relation);
        }
        DmsWorkflowPrintFilesGrantForm form = new DmsWorkflowPrintFilesGrantForm();
        form.setUserId(requireValue(request.getShenqingren(), "记录打印申请人(shenqingren)"));
        form.setFiles(files);
        return form;
    }
 
    /** 校验必填文本并返回去除首尾空白后的值。 */
    private String requireValue(String value, String fieldName) {
        if (isBlank(value)) {
            throw new DataException(fieldName + "不能为空");
        }
        return value.trim();
    }
 
    /** 判断文本是否为空。 */
    private boolean isBlank(String value) {
        return value == null || value.trim().isEmpty();
    }
 
    /**
     * 校验整批流水后,使用同一时间完成发放和接收信息更新。
     *
     * @param form 电子签名及发放明细
     * @throws DataException 参数无效、流水不存在或条码已经发放
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    @AuditLog(
            action = "FAFANG",
            label = "记录打印发放",
            failureAction = "FAFANG_FAIL",
            failureLabel = "记录打印发放失败",
            bizModule = "记录打印发放",
            bizType = "DMS_RECORD_PRINT_FAFANG",
            bizCodeExpr = "#p0.data[0].fId",
            bizCodeRequired = false,
            targetTable = "dms_jilu_dayin_liushui",
            targetIdExpr = "#p0.data[0].fId",
            targetIdsExpr = "#p0.data.![fId]",
            diff = true,
            entityClass = DmsJiluDayinLiushuiEntity.class
    )
    public void issue(DmsRecordPrintIssueForm form) throws DataException {
        Map<String, DmsRecordPrintIssueItemForm> items = resolveIssueItems(form);
        List<DmsJiluDayinLiushuiEntity> records = mapper.selectList(
                Wrappers.<DmsJiluDayinLiushuiEntity>lambdaQuery()
                        .select(DmsJiluDayinLiushuiEntity::getId,
                                DmsJiluDayinLiushuiEntity::getLiushuihao,
                                DmsJiluDayinLiushuiEntity::getFafangren,
                                DmsJiluDayinLiushuiEntity::getDayinZixiangId)
                        .in(DmsJiluDayinLiushuiEntity::getLiushuihao, items.keySet())
                        .isNull(DmsJiluDayinLiushuiEntity::getDeleteMark));
        validateIssueRecords(items.keySet(), records);
 
        Date now = new Date();
        Map<String, Integer> issuedCounts = new LinkedHashMap<>();
        for (DmsJiluDayinLiushuiEntity record : records) {
            String printItemId = requireValue(record.getDayinZixiangId(), "打印申请子项ID");
            DmsRecordPrintIssueItemForm item = items.get(record.getLiushuihao());
            LambdaUpdateWrapper<DmsJiluDayinLiushuiEntity> update =
                    Wrappers.<DmsJiluDayinLiushuiEntity>lambdaUpdate()
                            .eq(DmsJiluDayinLiushuiEntity::getId, record.getId())
                            .isNull(DmsJiluDayinLiushuiEntity::getDeleteMark)
                            .and(wrapper -> wrapper
                                    .isNull(DmsJiluDayinLiushuiEntity::getFafangren)
                                    .or()
                                    .eq(DmsJiluDayinLiushuiEntity::getFafangren, ""))
                            .set(DmsJiluDayinLiushuiEntity::getFafangren,
                                    item.getFafangren().trim())
                            .set(DmsJiluDayinLiushuiEntity::getFafangBumen,
                                    item.getFafangBumen().trim())
                            .set(DmsJiluDayinLiushuiEntity::getJieshouren,
                                    item.getJieshouren().trim())
                            .set(DmsJiluDayinLiushuiEntity::getJieshouBumen,
                                    item.getJieshouBumen().trim())
                            .set(DmsJiluDayinLiushuiEntity::getFafangShijian, now)
                            .set(DmsJiluDayinLiushuiEntity::getJieshouShijian, now);
            if (mapper.update(null, update) != 1) {
                throw new DataException("该条码已签收或发放请求重复操作");
            }
            issuedCounts.merge(printItemId, 1, Integer::sum);
        }
        for (String printItemId : new TreeSet<>(issuedCounts.keySet())) {
            DmsJiluDayinShenqingZixiangEntity printItem =
                    mapper.selectPrintItemForUpdate(printItemId);
            if (printItem == null) {
                throw new DataException("未找到有效的记录打印申请子项:" + printItemId);
            }
            int total = parsePositiveInt(printItem.getDayinFenshu(), "打印份数");
            int issued = parseNonNegativeInt(printItem.getYiqianfaShuliang(), "已签发数量");
            try {
                issued = Math.addExact(issued, issuedCounts.get(printItemId));
            } catch (ArithmeticException exception) {
                throw new DataException("已签发数量超出允许范围:" + printItemId);
            }
            DmsJiluDayinShenqingZixiangEntity updated =
                    new DmsJiluDayinShenqingZixiangEntity();
            updated.setYiqianfaShuliang(String.valueOf(issued));
            updated.setWeiqianfaShuliang(String.valueOf(total - issued));
            if (itemMapper.update(updated,
                    Wrappers.<DmsJiluDayinShenqingZixiangEntity>lambdaUpdate()
                            .eq(DmsJiluDayinShenqingZixiangEntity::getId, printItemId)
                            .isNull(DmsJiluDayinShenqingZixiangEntity::getDeleteMark)) != 1) {
                throw new DataException("更新记录打印申请子项发放数量失败:" + printItemId);
            }
        }
    }
 
    /**
     * 校验整批流水后,使用同一时间完成归还和回收信息更新。
     *
     * @param form 电子签名及回收明细
     * @throws DataException 参数无效、流水不存在或条码已经回收
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    @AuditLog(
            action = "HUISHOU",
            label = "记录打印回收",
            failureAction = "HUIUSHOU_FAIL",
            failureLabel = "记录打印回收失败",
            bizModule = "记录打印回收",
            bizType = "DMS_RECORD_PRINT_HUISHOU",
            bizCodeExpr = "#p0.data[0].fId",
            bizCodeRequired = false,
            targetTable = "dms_jilu_dayin_liushui",
            targetIdExpr = "#p0.data[0].fId",
            targetIdsExpr = "#p0.data.![fId]",
            diff = true,
            entityClass = DmsJiluDayinLiushuiEntity.class
    )
    public void huishou(DmsRecordPrintHuishouForm form) throws DataException {
        Map<String, DmsRecordPrintHuishouItemForm> items = resolveHuishouItems(form);
        List<DmsJiluDayinLiushuiEntity> records = mapper.selectList(
                Wrappers.<DmsJiluDayinLiushuiEntity>lambdaQuery()
                        .select(DmsJiluDayinLiushuiEntity::getId,
                                DmsJiluDayinLiushuiEntity::getLiushuihao,
                                DmsJiluDayinLiushuiEntity::getGuihuanren,
                                DmsJiluDayinLiushuiEntity::getDayinZixiangId)
                        .in(DmsJiluDayinLiushuiEntity::getLiushuihao, items.keySet())
                        .isNull(DmsJiluDayinLiushuiEntity::getDeleteMark));
        validateIssueRecords(items.keySet(), records);
 
        Date now = new Date();
        Map<String, Integer> recoveredCounts = new LinkedHashMap<>();
        for (DmsJiluDayinLiushuiEntity record : records) {
            String printItemId = requireValue(record.getDayinZixiangId(), "打印申请子项ID");
            DmsRecordPrintHuishouItemForm item = items.get(record.getLiushuihao());
            LambdaUpdateWrapper<DmsJiluDayinLiushuiEntity> update =
                    Wrappers.<DmsJiluDayinLiushuiEntity>lambdaUpdate()
                            .eq(DmsJiluDayinLiushuiEntity::getId, record.getId())
                            .isNull(DmsJiluDayinLiushuiEntity::getDeleteMark)
                            .and(wrapper -> wrapper
                                    .isNull(DmsJiluDayinLiushuiEntity::getGuihuanren)
                                    .or()
                                    .eq(DmsJiluDayinLiushuiEntity::getGuihuanren, ""))
                            .set(DmsJiluDayinLiushuiEntity::getGuihuanren,
                                    item.getGuihuanren())
                            .set(DmsJiluDayinLiushuiEntity::getGuihuanBumen,
                                    item.getGuihuanBumen())
                            .set(DmsJiluDayinLiushuiEntity::getHuishouren,
                                    item.getHuishouren())
                            .set(DmsJiluDayinLiushuiEntity:: getHuishouBumen,
                                    item.getHuishouBumen())
                            .set(DmsJiluDayinLiushuiEntity:: getHuishouYuanyin,
                                    item.getHuishouYuanyin())
                            .set(DmsJiluDayinLiushuiEntity:: getHuishouLeixing,
                                    item.getHuishouLeixing())
                            .set(DmsJiluDayinLiushuiEntity::getGuihuanShijian, now)
                            .set(DmsJiluDayinLiushuiEntity::getHuishouShijian, now);
            if (mapper.update(null, update) != 1) {
                throw new DataException("该条码已回收请求重复操作");
            }
            recoveredCounts.merge(printItemId, 1, Integer::sum);
        }
        for (String printItemId : new TreeSet<>(recoveredCounts.keySet())) {
            DmsJiluDayinShenqingZixiangEntity printItem =
                    mapper.selectPrintItemForUpdate(printItemId);
            if (printItem == null) {
                throw new DataException("未找到有效的记录打印申请子项:" + printItemId);
            }
            int recovered = parseNonNegativeInt(printItem.getYihuishouShuliang(), "已回收数量");
            try {
                recovered = Math.addExact(recovered, recoveredCounts.get(printItemId));
            } catch (ArithmeticException exception) {
                throw new DataException("已回收数量超出允许范围:" + printItemId);
            }
            DmsJiluDayinShenqingZixiangEntity updated =
                    new DmsJiluDayinShenqingZixiangEntity();
            updated.setYihuishouShuliang(String.valueOf(recovered));
            if (itemMapper.update(updated,
                    Wrappers.<DmsJiluDayinShenqingZixiangEntity>lambdaUpdate()
                            .eq(DmsJiluDayinShenqingZixiangEntity::getId, printItemId)
                            .isNull(DmsJiluDayinShenqingZixiangEntity::getDeleteMark)) != 1) {
                throw new DataException("更新记录打印申请子项回收数量失败:" + printItemId);
            }
        }
    }
 
    /**
     * 校验请求及签名,并按流水号整理发放明细。
     *
     * @param form 发放请求
     * @return 流水号到发放明细的有序映射
     * @throws DataException 必填字段为空或流水号重复
     */
    private Map<String, DmsRecordPrintIssueItemForm> resolveIssueItems(
            DmsRecordPrintIssueForm form) throws DataException {
        if (form == null) {
            throw new DataException("发放参数不能为空");
        }
        if (form.getData() == null || form.getData().isEmpty()) {
            throw new DataException("发放数据不能为空");
        }
        Map<String, DmsRecordPrintIssueItemForm> items = new LinkedHashMap<>();
        for (DmsRecordPrintIssueItemForm item : form.getData()) {
            if (item == null) {
                throw new DataException("发放数据中不能包含空记录");
            }
            String serialNumber = requireText(item.getLiushuihao(), "流水号不能为空");
            requireText(item.getFafangren(), "发放人不能为空");
            requireText(item.getFafangBumen(), "发放部门不能为空");
            requireText(item.getJieshouren(), "接收人不能为空");
            requireText(item.getJieshouBumen(), "接收部门不能为空");
            item.setLiushuihao(serialNumber);
            if (items.put(serialNumber, item) != null) {
                throw new DataException("发放数据中不能包含重复流水号");
            }
        }
        return items;
    }
 
    /**
     * 校验请求及签名,并按流水号整理回收明细。
     *
     * @param form 回收请求
     * @return 流水号到回收明细的有序映射
     * @throws DataException 必填字段为空或流水号重复
     */
    private Map<String, DmsRecordPrintHuishouItemForm> resolveHuishouItems(
            DmsRecordPrintHuishouForm form) throws DataException {
        if (form == null) {
            throw new DataException("发放参数不能为空");
        }
        if (form.getData() == null || form.getData().isEmpty()) {
            throw new DataException("发放数据不能为空");
        }
        Map<String, DmsRecordPrintHuishouItemForm> items = new LinkedHashMap<>();
        for (DmsRecordPrintHuishouItemForm item : form.getData()) {
            if (item == null) {
                throw new DataException("发放数据中不能包含空记录");
            }
            String serialNumber = requireText(item.getLiushuihao(), "流水号不能为空");
            requireText(item.getGuihuanren(), "归还人人不能为空");
            requireText(item.getGuihuanBumen(), "归还部门不能为空");
            requireText(item.getHuishouren(), "回收人不能为空");
            requireText(item.getHuishouBumen(), "回收部门不能为空");
            item.setLiushuihao(serialNumber);
            if (items.put(serialNumber, item) != null) {
                throw new DataException("回收数据中不能包含重复流水号");
            }
        }
        return items;
    }
 
    /**
     * 校验所有请求流水均存在且尚未发放。
     *
     * @param serialNumbers 请求流水号
     * @param records       查询出的 A 表实体
     * @throws DataException 存在无效流水或已经发放的条码
     */
    private void validateIssueRecords(Set<String> serialNumbers,
                                      List<DmsJiluDayinLiushuiEntity> records)
            throws DataException {
        Set<String> foundSerialNumbers = new LinkedHashSet<>();
        for (DmsJiluDayinLiushuiEntity record : records) {
            foundSerialNumbers.add(record.getLiushuihao());
            if (!trim(record.getFafangren()).isEmpty()) {
                throw new DataException("该条码已签收或发放请勿重复操作");
            }
        }
        if (records.size() != serialNumbers.size()
                || !foundSerialNumbers.equals(serialNumbers)) {
            throw new DataException("存在未找到或已删除的打印流水号,无法发放");
        }
    }
 
    /**
     * 根据流水号读取 A 表实体,校验发放状态后读取对应 B 表实体。
     *
     * @param serialNumber 打印流水号
     * @return 流水记录及文件信息
     * @throws DataException 流水号为空、记录不存在或已经签收/发放
     */
    @Override
    public DmsRecordPrintSerialVO findBySerialNumber(String serialNumber, String chazhaoLeixing) throws DataException {
        String resolvedSerialNumber = requireText(serialNumber, "流水号不能为空");
        DmsJiluDayinLiushuiEntity record = mapper.selectOne(
                Wrappers.<DmsJiluDayinLiushuiEntity>lambdaQuery()
                        .select(DmsJiluDayinLiushuiEntity::getId,
                                DmsJiluDayinLiushuiEntity::getDayinZixiangId,
                                DmsJiluDayinLiushuiEntity::getLiushuihao,
                                DmsJiluDayinLiushuiEntity::getFafangren,
                                DmsJiluDayinLiushuiEntity::getHuishouren,
                                DmsJiluDayinLiushuiEntity::getJieshouren,
                                DmsJiluDayinLiushuiEntity::getJieshouBumen)
                        .eq(DmsJiluDayinLiushuiEntity::getLiushuihao,
                                resolvedSerialNumber)
                        .isNull(DmsJiluDayinLiushuiEntity::getDeleteMark));
        if (record == null) {
            throw new DataException("未找到该流水号对应的条码信息");
        }
        if (!trim(record.getFafangren()).isEmpty() && !CHAZHAO_LEIXING_HUISHOU.equals(chazhaoLeixing)) {
            throw new DataException("该条码已签收或发放请勿重复操作");
        }
        if (!trim(record.getHuishouren()).isEmpty() && CHAZHAO_LEIXING_HUISHOU.equals(chazhaoLeixing)) {
            throw new DataException("该条码已回签收请勿重复操作");
        }
 
        DmsJiluDayinShenqingZixiangEntity item = itemMapper.selectOne(
                Wrappers.<DmsJiluDayinShenqingZixiangEntity>lambdaQuery()
                        .select(DmsJiluDayinShenqingZixiangEntity::getId,
                                DmsJiluDayinShenqingZixiangEntity::getWenjianMingcheng,
                                DmsJiluDayinShenqingZixiangEntity::getWenjianBianhao,
                                DmsJiluDayinShenqingZixiangEntity::getBanbenhao)
                        .eq(DmsJiluDayinShenqingZixiangEntity::getId,
                                record.getDayinZixiangId())
                        .isNull(DmsJiluDayinShenqingZixiangEntity::getDeleteMark));
        if (item == null) {
            throw new DataException("未找到该流水号对应的打印申请子项");
        }
 
        DmsRecordPrintSerialVO result = new DmsRecordPrintSerialVO();
        result.setLiushuihao(record.getLiushuihao());
        result.setId(record.getId());
        result.setWenjianMingcheng(item.getWenjianMingcheng());
        result.setWenjianBianhao(item.getWenjianBianhao());
        result.setBanbenhao(item.getBanbenhao());
        result.setJieshouren(record.getJieshouren());
        result.setJieshouBumen(record.getJieshouBumen());
        return result;
    }
 
    /**
     * 锁定打印申请、计算打印单元并预占流水号,不写入打印业务表。
     *
     * @param printItemId B 表打印申请子项主键
     * @return 本次唯一打印内容所需的流水号映射
     * @throws DataException 任一步业务校验或持久化失败
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public DmsRecordPrintPrepareVO prepare(String printItemId) throws DataException {
        String resolvedId = requireText(printItemId, "打印申请子项ID不能为空");
        DmsJiluDayinShenqingZixiangEntity item = mapper.selectPrintItemForUpdate(resolvedId);
        validatePrintable(item, resolvedId);
 
        UserInfo user = identity.requireUser();
        String userId = requireText(user.getUserId(), "当前登录用户ID不能为空");
        String tenantId = identity.tenantId(user);
        DmsRecordPrintReservation existing = reservationStore.findByPrintItem(
                tenantId, resolvedId);
        if (existing != null) {
            validateReservationIdentity(existing, tenantId, userId);
            validateReservationVersion(existing, item);
            if (!DmsRecordPrintReservation.STATUS_PENDING.equals(existing.getStatus())
                    || existing.getExpiresAt() <= System.currentTimeMillis()) {
                throw new DataException("打印预占状态无效或已过期,请重新预览");
            }
            return buildPrepareResult(existing);
        }
 
        String printType = trim(item.getDayinLeixing());
        int printCount = parsePositiveInt(item.getDayinFenshu(), "打印份数");
        List<Integer> selectedPages = resolveSelectedPages(item, tenantId);
        int serialCount = calculateSerialCount(printType, printCount, selectedPages);
        List<String> serialNumbers = allocateSerialNumbers(serialCount);
 
        long ttlMillis = reservationTtlMillis();
        long now = System.currentTimeMillis();
        DmsRecordPrintReservation reservation = new DmsRecordPrintReservation();
        reservation.setReservationId(UUID.randomUUID().toString());
        reservation.setTenantId(tenantId);
        reservation.setUserId(userId);
        reservation.setPrintItemId(item.getId());
        reservation.setItemVersion(item.getVersion());
        reservation.setPrintType(printType);
        reservation.setPrintCount(printCount);
        reservation.setSelectedPages(new ArrayList<>(selectedPages));
        reservation.setSerialNumbers(new ArrayList<>(serialNumbers));
        reservation.setStatus(DmsRecordPrintReservation.STATUS_PENDING);
        reservation.setCreatedAt(now);
        reservation.setExpiresAt(now + ttlMillis);
        reservationStore.create(reservation, ttlMillis);
        return buildPrepareResult(reservation);
    }
 
    /**
     * 将预览阶段预占的流水号写入 A 表,并更新 B 表打印状态。
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public DmsRecordPrintConfirmVO confirm(String reservationId) throws DataException {
        String resolvedReservationId = requireText(
                reservationId, "打印预占标识不能为空");
        UserInfo user = identity.requireUser();
        String userId = requireText(user.getUserId(), "当前登录用户ID不能为空");
        String tenantId = identity.tenantId(user);
        DmsRecordPrintReservation reservation = reservationStore.findById(
                tenantId, resolvedReservationId);
        if (reservation == null || reservation.getExpiresAt() <= System.currentTimeMillis()) {
            throw new DataException("打印预占不存在或已过期,请重新预览");
        }
        validateReservationIdentity(reservation, tenantId, userId);
        if (DmsRecordPrintReservation.STATUS_CONFIRMED.equals(reservation.getStatus())) {
            return buildConfirmResult(reservation);
        }
        if (!DmsRecordPrintReservation.STATUS_PENDING.equals(reservation.getStatus())) {
            throw new DataException("打印预占状态无效,请重新预览");
        }
 
        DmsJiluDayinShenqingZixiangEntity item = mapper.selectPrintItemForUpdate(
                reservation.getPrintItemId());
        if (item == null) {
            throw new DataException("打印申请子项不存在");
        }
        List<DmsJiluDayinLiushuiEntity> existingRecords = findActivePrintRecords(item.getId());
        if (!existingRecords.isEmpty()) {
            DmsRecordPrintConfirmVO confirmed = resolveConfirmedRetry(
                    reservation, existingRecords, userId);
            markConfirmedAfterCommit(reservation);
            return confirmed;
        }
 
        validatePrintable(item, item.getId());
        validateReservationVersion(reservation, item);
        validateReservationLayout(reservation, item, tenantId);
 
        Date printTime = new Date();
        List<DmsJiluDayinLiushuiEntity> records = buildRecords(
                item, reservation.getSerialNumbers(), userId, printTime);
        int inserted = mapper.insertPrintRecords(records);
        if (inserted != records.size()) {
            throw new DataException("生成打印流水记录失败");
        }
        markPrintItemProcessing(item, userId, printTime);
        reservation.setRecordIds(recordIds(records));
        reservation.setStatus(DmsRecordPrintReservation.STATUS_CONFIRMED);
        markConfirmedAfterCommit(reservation);
        return buildConfirmResult(reservation);
    }
 
    /**
     * 校验打印申请存在、未打印且数据状态一致。
     *
     * @param item       B 表实体
     * @param printItemId 打印申请子项主键
     * @throws DataException 申请不存在、类型无效或已经打印
     */
    private void validatePrintable(DmsJiluDayinShenqingZixiangEntity item, String printItemId)
            throws DataException {
        if (item == null) {
            throw new DataException("打印申请子项不存在");
        }
        String printType = trim(item.getDayinLeixing());
        if (!PRINT_TYPE_BOUND.equals(printType) && !PRINT_TYPE_LOOSE.equals(printType)) {
            throw new DataException("打印类型无效:" + printType);
        }
        int activeRecords = mapper.countActivePrintRecords(printItemId);
        if (activeRecords > 0) {
            throw new DataException("该记录已经打印,不允许重复打印");
        }
        int printedCount = parseNonNegativeInt(item.getYidayinFenshu(), "已打印份数");
        if (printedCount > 0 || PRINT_STATUS_PROCESSING.equals(trim(item.getDayinZhuangtai()))) {
            throw new DataException("该记录已经打印,不允许重复打印");
        }
    }
 
    /**
     * 按打印类型解析分散打印页码,成册打印返回空集合。
     *
     * @param item B 表实体
     * @return 分散打印有序页码集合
     * @throws DataException 文件页数或页码集合无效
     */
    private List<Integer> resolveSelectedPages(DmsJiluDayinShenqingZixiangEntity item,
                                               String tenantId)
            throws DataException {
        if (PRINT_TYPE_BOUND.equals(trim(item.getDayinLeixing()))) {
            return Collections.emptyList();
        }
        String banbenId = requireText(item.getBanbenId(), "文件版本ID不能为空");
        DmsWenjianBanbenEntity banben = banbenMapper.selectById(tenantId, banbenId);
        if (banben == null) {
            throw new DataException("未找到文件版本:" + banbenId);
        }
        String fileCenterVersionId = requireText(
                banben.getWenjianZhongxinBanbenId(), "文件版本缺少文件中心版本ID");
        try {
            DmsFilePageCountVO pageCount = pageCountService.getPageCount(
                    UUID.fromString(fileCenterVersionId));
            return pageParser.parse(item.getYema(), pageCount.getPageCount());
        } catch (IllegalArgumentException exception) {
            throw new DataException("文件中心版本ID格式不正确:" + fileCenterVersionId);
        } catch (IOException exception) {
            throw new DataException("获取文件总页数失败");
        }
    }
 
    /**
     * 根据打印类型计算需要生成的流水号总数并校验上限。
     *
     * @param printType    打印类型
     * @param printCount   打印份数
     * @param selectedPages 分散打印有序页码集合
     * @return 流水号总数
     * @throws DataException 数量溢出或超过配置上限
     */
    private int calculateSerialCount(String printType, int printCount,
                                     List<Integer> selectedPages) throws DataException {
        final int serialCount;
        try {
            serialCount = PRINT_TYPE_BOUND.equals(printType)
                    ? printCount : Math.multiplyExact(printCount, selectedPages.size());
        } catch (ArithmeticException exception) {
            throw new DataException("打印流水号数量超出允许范围");
        }
        int maxSerialCount = properties.getMaxSerialCount();
        if (maxSerialCount <= 0) {
            throw new DataException("打印流水号数量上限配置无效");
        }
        if (serialCount <= 0 || serialCount > maxSerialCount) {
            throw new DataException("打印流水号数量必须在1至" + maxSerialCount + "之间");
        }
        return serialCount;
    }
 
    /**
     * 在 DMS 事务锁保护下调用单据规则接口生成本批全部流水号。
     *
     * @param count 流水号数量
     * @return 按生成顺序排列的流水号
     * @throws DataException 锁获取异常、规则不可用或返回重复号码
     */
    private List<String> allocateSerialNumbers(int count) throws DataException {
        Integer locked = mapper.lockSerialAllocation();
        if (locked == null || locked != 1) {
            throw new DataException("获取记录打印流水号批量取号锁失败");
        }
        String ruleCode = requireText(properties.getBillRuleCode(), "记录打印流水号规则编码不能为空");
        List<String> numbers = new ArrayList<>(count);
        Set<String> uniqueNumbers = new HashSet<>(count);
        for (int index = 0; index < count; index++) {
            ActionResult<String> result = billRuleApi.getBillNumber(ruleCode);
            String number = result == null ? "" : trim(result.getData());
            if (number.isEmpty()) {
                throw new DataException("打印流水号取号失败:单据规则[" + ruleCode + "]不可用");
            }
            if (!uniqueNumbers.add(number)) {
                throw new DataException("打印流水号规则返回重复号码:" + number);
            }
            numbers.add(number);
        }
        return numbers;
    }
 
    /**
     * 使用实体构造本次全部 A 表打印流水记录。
     *
     * @param item          B 表实体
     * @param serialNumbers 流水号集合
     * @param printUserId   打印人
     * @param printTime     本批统一打印时间
     * @return A 表实体集合
     * @throws DataException 打印申请主表主键为空
     */
    private List<DmsJiluDayinLiushuiEntity> buildRecords(
            DmsJiluDayinShenqingZixiangEntity item, List<String> serialNumbers,
            String printUserId, Date printTime) throws DataException {
        String printId = requireText(item.getForeignId(), "打印申请主表ID不能为空");
        List<DmsJiluDayinLiushuiEntity> records = new ArrayList<>(serialNumbers.size());
        for (String serialNumber : serialNumbers) {
            DmsJiluDayinLiushuiEntity record = new DmsJiluDayinLiushuiEntity();
            record.setId(IdUtil.getSnowflakeNextIdStr());
            record.setDayinId(printId);
            record.setDayinZixiangId(item.getId());
            record.setLiushuihao(serialNumber);
            record.setDayinren(printUserId);
            record.setDayinShijian(printTime);
            record.setDeleteMark(null);
            records.add(record);
        }
        return records;
    }
 
    /**
     * 使用 B 表实体统一回写打印人、打印时间和打印中状态。
     *
     * @param item        B 表实体
     * @param printUserId 打印人
     * @param printTime   与 A 表相同的打印时间
     * @throws DataException B 表状态未按预期更新
     */
    private void markPrintItemProcessing(DmsJiluDayinShenqingZixiangEntity item,
                                         String printUserId, Date printTime)
            throws DataException {
        item.setDayinren(printUserId);
        item.setDayinShijian(printTime);
        item.setDayinZhuangtai(PRINT_STATUS_PROCESSING);
        item.setYidayinFenshu(item.getDayinFenshu());
        if (mapper.markPrintItemProcessing(item) != 1) {
            throw new DataException("更新打印申请子项状态失败");
        }
    }
 
    /**
     * 按稳定顺序构造不含数据库记录主键的打印预览结果。
     */
    private DmsRecordPrintPrepareVO buildPrepareResult(
            DmsRecordPrintReservation reservation) {
        DmsRecordPrintPrepareVO result = new DmsRecordPrintPrepareVO();
        result.setReservationId(reservation.getReservationId());
        result.setExpiresAt(new Date(reservation.getExpiresAt()));
        result.setPrintItemId(reservation.getPrintItemId());
        result.setPrintType(reservation.getPrintType());
        result.setPrintCount(reservation.getPrintCount());
        result.setSelectedPages(new ArrayList<>(reservation.getSelectedPages()));
        result.setSerialCount(reservation.getSerialNumbers().size());
        result.setCopies(buildCopies(reservation, false));
        return result;
    }
 
    /**
     * 构造已经写入 A 表的打印确认结果。
     */
    private DmsRecordPrintConfirmVO buildConfirmResult(
            DmsRecordPrintReservation reservation) {
        DmsRecordPrintConfirmVO result = new DmsRecordPrintConfirmVO();
        result.setReservationId(reservation.getReservationId());
        result.setPrintItemId(reservation.getPrintItemId());
        result.setConfirmed(Boolean.TRUE);
        result.setCopies(buildCopies(reservation, true));
        return result;
    }
 
    /**
     * 根据预占中的稳定顺序构造打印单元。
     */
    private List<DmsRecordPrintCopyVO> buildCopies(
            DmsRecordPrintReservation reservation, boolean includeRecordIds) {
        List<String> serialNumbers = reservation.getSerialNumbers();
        List<String> recordIds = reservation.getRecordIds();
        if (includeRecordIds && (recordIds == null
                || recordIds.size() != serialNumbers.size())) {
            throw new DataException("打印确认结果缺少数据库记录主键");
        }
 
        List<DmsRecordPrintCopyVO> copies = new ArrayList<>(serialNumbers.size());
        int recordIndex = 0;
        for (int copyIndex = 1; copyIndex <= reservation.getPrintCount(); copyIndex++) {
            if (PRINT_TYPE_BOUND.equals(reservation.getPrintType())) {
                copies.add(toCopy(includeRecordIds ? recordIds.get(recordIndex) : null,
                        serialNumbers.get(recordIndex++), copyIndex, null));
                continue;
            }
            for (Integer pageNo : reservation.getSelectedPages()) {
                copies.add(toCopy(includeRecordIds ? recordIds.get(recordIndex) : null,
                        serialNumbers.get(recordIndex++), copyIndex, pageNo));
            }
        }
        if (recordIndex != serialNumbers.size()) {
            throw new DataException("打印预占的流水号数量与打印单元数量不一致");
        }
        return copies;
    }
 
    /**
     * 构造单个打印单元响应。
     */
    private DmsRecordPrintCopyVO toCopy(String recordId, String serialNumber,
                                        int copyIndex, Integer pageNo) {
        DmsRecordPrintCopyVO copy = new DmsRecordPrintCopyVO();
        copy.setRecordId(recordId);
        copy.setCopyIndex(copyIndex);
        copy.setPageNo(pageNo);
        copy.setSerialNo(serialNumber);
        return copy;
    }
 
    /**
     * 校验预占只能由创建它的租户和用户确认。
     */
    private void validateReservationIdentity(DmsRecordPrintReservation reservation,
                                               String tenantId, String userId)
            throws DataException {
        if (!Objects.equals(tenantId, reservation.getTenantId())) {
            throw new DataException("打印预占不属于当前租户");
        }
        if (!Objects.equals(userId, reservation.getUserId())) {
            throw new DataException("该打印申请正在被其他用户预览");
        }
    }
 
    /**
     * 校验预览后打印申请没有被修改。
     */
    private void validateReservationVersion(DmsRecordPrintReservation reservation,
                                              DmsJiluDayinShenqingZixiangEntity item)
            throws DataException {
        if (!Objects.equals(reservation.getItemVersion(), item.getVersion())) {
            throw new DataException("打印申请在预览后已经变更,请重新预览");
        }
    }
 
    /**
     * 重新计算打印布局,防止预览后参数发生变化。
     */
    private void validateReservationLayout(DmsRecordPrintReservation reservation,
                                             DmsJiluDayinShenqingZixiangEntity item,
                                             String tenantId) throws DataException {
        int printCount = parsePositiveInt(item.getDayinFenshu(), "打印份数");
        List<Integer> selectedPages = resolveSelectedPages(item, tenantId);
        String printType = trim(item.getDayinLeixing());
        int serialCount = calculateSerialCount(printType, printCount, selectedPages);
        if (!Objects.equals(reservation.getPrintType(), printType)
                || !Objects.equals(reservation.getPrintCount(), printCount)
                || !Objects.equals(reservation.getSelectedPages(), selectedPages)
                || reservation.getSerialNumbers() == null
                || reservation.getSerialNumbers().size() != serialCount
                || new HashSet<>(reservation.getSerialNumbers()).size() != serialCount) {
            throw new DataException("打印参数在预览后已经变更,请重新预览");
        }
        for (String serialNumber : reservation.getSerialNumbers()) {
            requireText(serialNumber, "打印预占中存在空流水号");
        }
    }
 
    /**
     * 查询申请已经写入的有效打印流水,用于确认请求幂等恢复。
     */
    private List<DmsJiluDayinLiushuiEntity> findActivePrintRecords(String printItemId) {
        List<DmsJiluDayinLiushuiEntity> records = mapper.selectList(
                Wrappers.<DmsJiluDayinLiushuiEntity>lambdaQuery()
                        .select(DmsJiluDayinLiushuiEntity::getId,
                                DmsJiluDayinLiushuiEntity::getLiushuihao,
                                DmsJiluDayinLiushuiEntity::getDayinren)
                        .eq(DmsJiluDayinLiushuiEntity::getDayinZixiangId, printItemId)
                        .isNull(DmsJiluDayinLiushuiEntity::getDeleteMark));
        return records == null ? Collections.<DmsJiluDayinLiushuiEntity>emptyList() : records;
    }
 
    /**
     * 已存在完全相同的一批流水时,将网络重试恢复为成功响应。
     */
    private DmsRecordPrintConfirmVO resolveConfirmedRetry(
            DmsRecordPrintReservation reservation,
            List<DmsJiluDayinLiushuiEntity> existingRecords, String userId)
            throws DataException {
        Map<String, DmsJiluDayinLiushuiEntity> bySerial = new LinkedHashMap<>();
        for (DmsJiluDayinLiushuiEntity record : existingRecords) {
            if (!Objects.equals(userId, record.getDayinren())
                    || bySerial.put(record.getLiushuihao(), record) != null) {
                throw new DataException("该记录已经打印,不允许重复打印");
            }
        }
        List<String> recordIds = new ArrayList<>();
        for (String serialNumber : reservation.getSerialNumbers()) {
            DmsJiluDayinLiushuiEntity record = bySerial.remove(serialNumber);
            if (record == null) {
                throw new DataException("该记录已经使用其他流水号打印,不允许重复打印");
            }
            recordIds.add(record.getId());
        }
        if (!bySerial.isEmpty()) {
            throw new DataException("该记录已经使用其他流水号打印,不允许重复打印");
        }
        reservation.setRecordIds(recordIds);
        reservation.setStatus(DmsRecordPrintReservation.STATUS_CONFIRMED);
        return buildConfirmResult(reservation);
    }
 
    private List<String> recordIds(List<DmsJiluDayinLiushuiEntity> records) {
        List<String> ids = new ArrayList<>(records.size());
        for (DmsJiluDayinLiushuiEntity record : records) {
            ids.add(record.getId());
        }
        return ids;
    }
 
    /**
     * 数据库提交后保留一段时间的确认结果,为网络重试提供幂等响应。
     */
    private void markConfirmedAfterCommit(DmsRecordPrintReservation reservation) {
        Runnable completion = () -> {
            try {
                long ttlMillis = confirmedResultTtlMillis();
                reservation.setExpiresAt(System.currentTimeMillis() + ttlMillis);
                reservationStore.markConfirmed(reservation, ttlMillis);
            } catch (RuntimeException exception) {
                log.error("更新打印预占确认状态失败,reservationId={}",
                        reservation.getReservationId(), exception);
            }
        };
        if (!TransactionSynchronizationManager.isSynchronizationActive()) {
            completion.run();
            return;
        }
        TransactionSynchronizationManager.registerSynchronization(
                new TransactionSynchronization() {
                    @Override
                    public void afterCommit() {
                        completion.run();
                    }
                });
    }
 
    private long reservationTtlMillis() throws DataException {
        int minutes = properties.getReservationTtlMinutes();
        if (minutes <= 0) {
            throw new DataException("打印预占有效期配置无效");
        }
        return TimeUnit.MINUTES.toMillis(minutes);
    }
 
    private long confirmedResultTtlMillis() throws DataException {
        int hours = properties.getConfirmedResultTtlHours();
        if (hours <= 0) {
            throw new DataException("打印确认结果有效期配置无效");
        }
        return TimeUnit.HOURS.toMillis(hours);
    }
 
    /**
     * 解析必须为正整数的字符串字段。
     *
     * @param value     原始字段值
     * @param fieldName 字段显示名
     * @return 正整数
     * @throws DataException 为空、非整数或小于 1
     */
    private int parsePositiveInt(String value, String fieldName) throws DataException {
        int number = parseInteger(value, fieldName, false);
        if (number <= 0) {
            throw new DataException(fieldName + "必须大于0");
        }
        return number;
    }
 
    /**
     * 解析允许为空或零的非负整数字段。
     *
     * @param value     原始字段值
     * @param fieldName 字段显示名
     * @return 非负整数,空值按 0 处理
     * @throws DataException 非整数或小于 0
     */
    private int parseNonNegativeInt(String value, String fieldName) throws DataException {
        String resolved = trim(value);
        if (resolved.isEmpty()) {
            return 0;
        }
        int number = parseInteger(resolved, fieldName, true);
        if (number < 0) {
            throw new DataException(fieldName + "不能小于0");
        }
        return number;
    }
 
    /**
     * 解析整数字符串。
     *
     * @param value      原始字段值
     * @param fieldName  字段显示名
     * @param allowEmpty 是否允许空值;空值由调用方提前处理
     * @return 整数
     * @throws DataException 字段为空或不是整数
     */
    private int parseInteger(String value, String fieldName, boolean allowEmpty)
            throws DataException {
        String resolved = trim(value);
        if (resolved.isEmpty() && !allowEmpty) {
            throw new DataException(fieldName + "不能为空");
        }
        try {
            return Integer.parseInt(resolved);
        } catch (NumberFormatException exception) {
            throw new DataException(fieldName + "必须为整数");
        }
    }
 
    /**
     * 校验并返回非空文本。
     *
     * @param value   原始值
     * @param message 为空时的错误消息
     * @return 去除首尾空白后的值
     * @throws DataException 值为空
     */
    private String requireText(String value, String message) throws DataException {
        String resolved = trim(value);
        if (resolved.isEmpty()) {
            throw new DataException(message);
        }
        return resolved;
    }
 
    /**
     * 将可空文本转换为去除首尾空白后的值。
     *
     * @param value 原始值
     * @return 非 null 文本
     */
    private String trim(String value) {
        return value == null ? "" : value.trim();
    }
}