刘光辉
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
package jnpf.dmsService;
 
import jnpf.audit.sdk.annotation.AuditLog;
import jnpf.base.Pagination;
import jnpf.base.UserInfo;
import jnpf.dmsEntity.fenfa.DmsWenjianDayinQuerenEntity;
import jnpf.dmsEntity.fenfa.DmsWenjianDayinRenwuEntity;
import jnpf.dmsEntity.fenfa.form.DmsWenjianDayinQuerenForm;
import jnpf.dmsEntity.fenfa.form.DmsWenjianDayinYulanForm;
import jnpf.dmsEntity.fenfa.DmsWenjianDayinHaoma;
import jnpf.dmsEntity.fenfa.vo.DmsWenjianDayinJiluVO;
import jnpf.dmsMapper.DmsWenjianDayinQuerenMapper;
import jnpf.dmsMapper.DmsWenjianFenfaMapper;
import jnpf.dmsPermission.DmsCurrentIdentity;
import jnpf.exception.DataException;
import jnpf.util.JsonUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
 
import java.util.*;
import jnpf.dmsEntity.fenfa.DmsWenjianDayinFasongEntity;
import jnpf.dmsEntity.fenfa.DmsWenjianDayinFasongEntity.Batch;
import jnpf.dmsEntity.fenfa.form.DmsWenjianDayinFasongForm;
 
@Service
@RequiredArgsConstructor
public class DmsWenjianDayinQuerenService {
    private final DmsCurrentIdentity identity;
    private final DmsWenjianFenfaMapper taskMapper;
    private final DmsWenjianDayinQuerenMapper mapper;
    private final DmsWenjianDayinTaizhangService ledger;
    private final DmsWenjianFenfaService distribution;
    private final DmsWenjianDayinHaomaService numbers;
 
    /** 只接收发送成功回传;事务同时提交正式记录、任务状态、分发和幂等凭据。 */
    @Transactional(rollbackFor = Exception.class)
    @AuditLog(action = "DMS_PRINT_CONFIRM", recordSuccess = false,
            bizCodeExpr = "#p0.requestId", targetTable = "dms_wenjian_dayin_queren", targetIdExpr = "#p0.requestId")
    public List<DmsWenjianDayinJiluVO> confirm(DmsWenjianDayinQuerenForm form) throws DataException {
        if (form == null || !"SUBMITTED".equals(form.getClientResult()) || form.getCopies() == null
                || form.getCopies() < 1 || form.getCopies() > 999) {
            throw new DataException("必须收到打印客户端提交成功回传及有效份数");
        }
        try {
            if (!UUID.fromString(form.getRequestId()).toString().equals(form.getRequestId())) {
                throw new IllegalArgumentException();
            }
        } catch (RuntimeException error) {
            throw new DataException("打印确认请求ID无效");
        }
        UserInfo actor = identity.requireUser();
        String tenantId = identity.tenantId(actor);
        // 同任务确认统一先锁任务,再读取幂等记录,兼容响应丢失和并发重复回调。
        DmsWenjianDayinRenwuEntity task = taskMapper.selectPrintTaskForUpdate(tenantId, form.getTaskId());
        if (task == null) throw new DataException("打印任务不存在或已删除");
        DmsWenjianDayinQuerenEntity saved = mapper.selectConfirmation(tenantId, form.getRequestId());
        if (saved != null) {
            if (!Objects.equals(saved.getTaskId(), form.getTaskId())
                    || !Objects.equals(saved.getUserId(), actor.getUserId())
                    || !Objects.equals(saved.getFileVersionId(), form.getFileVersionId())
                    || !Objects.equals(saved.getCopies(), form.getCopies())) {
                throw new DataException("相同打印确认请求ID不能用于不同打印任务或参数");
            }
            return JsonUtil.getJsonToList(saved.getResultJson(), DmsWenjianDayinJiluVO.class);
        }
        DmsWenjianDayinFasongEntity attempt = mapper.selectAttempt(tenantId, form.getRequestId());
        if (attempt == null) throw new DataException("请先建立技术发送任务并等待全部批次成功回传");
        requireOwner(attempt, actor, form.getTaskId(), form.getFileVersionId());
        if (!Objects.equals(attempt.getCopies(), form.getCopies()) || !"OPEN".equals(attempt.getState())
                || batches(attempt).isEmpty() || batches(attempt).stream().anyMatch(b -> !"SUBMITTED".equals(b.getState()))) {
            throw new DataException("打印批次尚未全部成功回传,不能创建正式记录");
        }
        Pagination pagination = new Pagination();
        pagination.setCurrentPage(1L);
        pagination.setPageSize(1L);
        if (ledger.list(pagination, null, "f_id", form.getTaskId()).isEmpty()) {
            throw new DataException("无当前打印任务的操作权限或任务已完成");
        }
        DmsWenjianDayinQuerenEntity version = mapper.selectTaskVersion(tenantId, form.getTaskId());
        if (version == null || !Objects.equals(version.getFileVersionId(), form.getFileVersionId())
                || task.getDayinFenshu() == null
                || form.getCopies() > task.getDayinFenshu() - taskMapper.countConsumedPrints(tenantId, form.getTaskId())) {
            throw new DataException("打印任务版本或份数已变化,请核对已发送结果,勿重复打印");
        }
        List<DmsWenjianDayinJiluVO> result = distribution.printReserved(form.getTaskId(), form.getCopies(),
                JsonUtil.getJsonToList(attempt.getNumbersJson(), jnpf.dmsEntity.fenfa.DmsWenjianDayinHaoma.class));
        DmsWenjianDayinQuerenEntity receipt = new DmsWenjianDayinQuerenEntity();
        receipt.setId(form.getRequestId());
        receipt.setTenantId(tenantId);
        receipt.setTaskId(form.getTaskId());
        receipt.setFileVersionId(form.getFileVersionId());
        receipt.setUserId(actor.getUserId());
        receipt.setCopies(form.getCopies());
        receipt.setResultJson(JsonUtil.getObjectToString(result));
        if (mapper.insertConfirmation(receipt) != 1) throw new DataException("保存打印确认凭据失败");
        attempt.setState("CONFIRMED");
        saveAttempt(attempt);
        return result;
    }
 
    /** 复用固定份号;历史任务在任务锁内补齐,预览不创建发送或正式打印记录。 */
    @Transactional(rollbackFor = Exception.class)
    public DmsWenjianDayinHaoma previewParameters(DmsWenjianDayinYulanForm form) throws DataException {
        if (form == null || form.getTaskId() == null || form.getTaskId().isBlank()
                || form.getFileVersionId() == null || form.getFileVersionId().isBlank()) {
            throw new DataException("打印预览参数无效");
        }
        UserInfo actor = identity.requireUser();
        String tenant = identity.tenantId(actor);
        DmsWenjianDayinRenwuEntity task = taskMapper.selectPrintTaskForUpdate(tenant, form.getTaskId());
        if (task == null) throw new DataException("打印任务不存在或已删除");
        DmsWenjianDayinFasongEntity active = mapper.selectActiveAttempt(tenant, form.getTaskId());
        List<DmsWenjianDayinHaoma> frozen;
        if (active != null) {
            requireOwner(active, actor, form.getTaskId(), form.getFileVersionId());
            // 与prepare恢复契约一致;CONFIRMED仍取原发送号,不跳到下一份。
            frozen = JsonUtil.getJsonToList(active.getNumbersJson(), DmsWenjianDayinHaoma.class);
        } else {
            requirePrintable(tenant, form.getTaskId(), form.getFileVersionId(), 1, task);
            int consumed = taskMapper.maxPrintSequence(tenant, task.getId());
            frozen = numbers.ensureTaskNumbers(tenant, task, consumed).stream()
                    .filter(n -> n.getTaskSequence() > consumed).limit(1).toList();
        }
        if (frozen == null || frozen.isEmpty() || frozen.get(0) == null) {
            throw new DataException("任务固定份号不足,不能重新取号");
        }
        DmsWenjianDayinHaoma number = frozen.get(0);
        if (number.getSerialNumber() == null || number.getSerialNumber().isBlank()
                || number.getCountSerialNumber() == null || !number.getCountSerialNumber().matches("[0-9]{8}")) {
            throw new DataException("任务固定流水号缺失,无法显示打印预览");
        }
        return number;
    }
 
    /** 同任务排他:刷新和其他窗口必须恢复原发送,不创建第二个发送任务。 */
    @Transactional(rollbackFor = Exception.class)
    @AuditLog(action = "DMS_PRINT_PREPARE", recordSuccess = false, bizCodeExpr = "#p0.taskId")
    public DmsWenjianDayinFasongEntity prepare(DmsWenjianDayinFasongForm form) throws DataException {
        validateRequest(form);
        UserInfo actor = identity.requireUser();
        String tenant = identity.tenantId(actor);
        DmsWenjianDayinRenwuEntity task = taskMapper.selectPrintTaskForUpdate(tenant, form.getTaskId());
        if (task == null) throw new DataException("打印任务不存在或已删除");
        DmsWenjianDayinFasongEntity active = mapper.selectActiveAttempt(tenant, form.getTaskId());
        if (active != null) {
            requireOwner(active, actor, form.getTaskId(), form.getFileVersionId());
            return active; // 恢复原份数,绝不以新输入覆盖已开始发送的任务。
        }
        if (mapper.selectAttempt(tenant, form.getRequestId()) != null) throw new DataException("发送请求已结束,请刷新台账");
        requirePrintable(tenant, form.getTaskId(), form.getFileVersionId(), form.getCopies(), task);
        DmsWenjianDayinFasongEntity attempt = new DmsWenjianDayinFasongEntity();
        attempt.setId(form.getRequestId()); attempt.setTenantId(tenant); attempt.setTaskId(form.getTaskId());
        attempt.setFileVersionId(form.getFileVersionId()); attempt.setUserId(actor.getUserId());
        attempt.setCopies(form.getCopies()); attempt.setState("OPEN"); attempt.setBatchesJson("[]");
        int consumed = taskMapper.maxPrintSequence(tenant, task.getId());
        List<jnpf.dmsEntity.fenfa.DmsWenjianDayinHaoma> frozen = numbers.ensureTaskNumbers(tenant, task, consumed).stream()
                .filter(n -> n.getTaskSequence() > consumed).limit(form.getCopies()).toList();
        if (frozen.size() != form.getCopies()) throw new DataException("任务固定份号不足,不能重新取号");
        attempt.setNumbersJson(JsonUtil.getObjectToString(frozen));
        if (mapper.insertAttempt(attempt) != 1) throw new DataException("建立打印发送任务失败");
        return attempt;
    }
 
    /** 计划持久化后才能发;已有计划只能恢复,不能被另一窗口替换。 */
    @Transactional(rollbackFor = Exception.class)
    @AuditLog(action = "DMS_PRINT_PLAN", recordSuccess = false, bizCodeExpr = "#p0.requestId")
    public DmsWenjianDayinFasongEntity plan(DmsWenjianDayinFasongForm form) throws DataException {
        DmsWenjianDayinFasongEntity attempt = lockAttempt(form);
        if (!batches(attempt).isEmpty() || !"OPEN".equals(attempt.getState())) return attempt;
        validatePlan(form.getBatches(), attempt);
        attempt.setBatchesJson(JsonUtil.getObjectToString(form.getBatches()));
        saveAttempt(attempt);
        return attempt;
    }
 
    @Transactional(rollbackFor = Exception.class)
    @AuditLog(action = "DMS_PRINT_BATCH_SPLIT", recordSuccess = false, bizCodeExpr = "#p0.requestId")
    public DmsWenjianDayinFasongEntity split(DmsWenjianDayinFasongForm form) throws DataException {
        DmsWenjianDayinFasongEntity attempt = lockAttempt(form);
        List<Batch> list = batches(attempt);
        Batch batch = findBatch(list, form.getBatchId());
        if (!"OPEN".equals(attempt.getState()) || !"READY".equals(batch.getState())) throw new DataException("已发送的批次不能拆分");
        List<Batch> parts = form.getBatches();
        if (parts == null || parts.size() != 2) throw new DataException("拆分计划无效");
        validateBatch(parts.get(0)); validateBatch(parts.get(1));
        List<Integer> pages = new ArrayList<>(parts.get(0).getPages()); pages.addAll(parts.get(1).getPages());
        if (!parts.get(0).getItems().equals(batch.getItems()) || !parts.get(1).getItems().equals(batch.getItems())
                || !pages.equals(batch.getPages())) throw new DataException("拆分页段与原批次不一致");
        int index = list.indexOf(batch); list.remove(index); list.addAll(index, parts);
        if (list.stream().map(Batch::getId).distinct().count() != list.size()) throw new DataException("批次ID重复");
        attempt.setBatchesJson(JsonUtil.getObjectToString(list)); saveAttempt(attempt);
        return attempt;
    }
 
    /** 非幂等派发许可:许可响应丢失也不能再次取得许可,必须保留结果未知。 */
    @Transactional(rollbackFor = Exception.class)
    @AuditLog(action = "DMS_PRINT_BATCH_BEGIN", recordSuccess = false, bizCodeExpr = "#p0.requestId")
    public DmsWenjianDayinFasongEntity begin(DmsWenjianDayinFasongForm form) throws DataException {
        DmsWenjianDayinFasongEntity attempt = lockAttempt(form);
        List<Batch> list = batches(attempt);
        Batch batch = findBatch(list, form.getBatchId());
        if (!"OPEN".equals(attempt.getState()) || !"READY".equals(batch.getState())) {
            throw new DataException("该批次已发送或发送结果未知,请恢复原任务,禁止重复发送");
        }
        for (Batch previous : list) {
            if (previous == batch) break;
            if (!"SUBMITTED".equals(previous.getState())) throw new DataException("前一批次尚未确认,不能发送后续批次");
        }
        requirePrintable(attempt.getTenantId(), attempt.getTaskId(), attempt.getFileVersionId(), attempt.getCopies(),
                taskMapper.selectPrintTaskForUpdate(attempt.getTenantId(), attempt.getTaskId()));
        requireUuid(form.getToken());
        batch.setToken(form.getToken()); batch.setState("SENDING");
        attempt.setBatchesJson(JsonUtil.getObjectToString(list)); saveAttempt(attempt);
        return attempt;
    }
 
    /** 包括迟到回传;保存失败可用同一令牌重试,绝不重新派发。 */
    @Transactional(rollbackFor = Exception.class)
    @AuditLog(action = "DMS_PRINT_BATCH_RECEIPT", recordSuccess = false, bizCodeExpr = "#p0.requestId")
    public DmsWenjianDayinFasongEntity submitted(DmsWenjianDayinFasongForm form) throws DataException {
        DmsWenjianDayinFasongEntity attempt = lockAttempt(form);
        List<Batch> list = batches(attempt);
        Batch batch = findBatch(list, form.getBatchId());
        requireUuid(form.getToken());
        if (!Objects.equals(form.getToken(), batch.getToken())
                || !("SENDING".equals(batch.getState()) || "SUBMITTED".equals(batch.getState()))) {
            throw new DataException("打印批次成功回传与发送任务不匹配");
        }
        if ("SUBMITTED".equals(batch.getState())) return attempt;
        batch.setState("SUBMITTED"); attempt.setBatchesJson(JsonUtil.getObjectToString(list)); saveAttempt(attempt);
        return attempt;
    }
 
    /** 前端已收到正式确认才结束技术任务;确认响应丢失时仍能恢复。 */
    @Transactional(rollbackFor = Exception.class)
    @AuditLog(action = "DMS_PRINT_FINISH", recordSuccess = false, bizCodeExpr = "#p0.requestId")
    public void finish(DmsWenjianDayinFasongForm form) throws DataException {
        DmsWenjianDayinFasongEntity attempt = lockAttempt(form);
        if ("CLOSED".equals(attempt.getState())) return;
        if (!"CONFIRMED".equals(attempt.getState())) throw new DataException("正式打印记录尚未确认,不能结束发送任务");
        attempt.setState("CLOSED"); saveAttempt(attempt);
    }
 
    private void validateRequest(DmsWenjianDayinFasongForm form) throws DataException {
        if (form == null || form.getTaskId() == null || form.getTaskId().trim().isEmpty()
                || form.getFileVersionId() == null || form.getFileVersionId().trim().isEmpty()
                || form.getCopies() == null || form.getCopies() < 1 || form.getCopies() > 999) throw new DataException("打印发送参数无效");
        requireUuid(form.getRequestId());
    }
 
    private void requireUuid(String value) throws DataException {
        try { if (!UUID.fromString(value).toString().equals(value)) throw new IllegalArgumentException(); }
        catch (RuntimeException ex) { throw new DataException("打印发送关联ID无效"); }
    }
 
    private DmsWenjianDayinFasongEntity lockAttempt(DmsWenjianDayinFasongForm form) throws DataException {
        validateRequest(form);
        UserInfo actor = identity.requireUser(); String tenant = identity.tenantId(actor);
        if (taskMapper.selectPrintTaskForUpdate(tenant, form.getTaskId()) == null) throw new DataException("打印任务不存在或已删除");
        DmsWenjianDayinFasongEntity attempt = mapper.selectAttempt(tenant, form.getRequestId());
        if (attempt == null) throw new DataException("打印发送任务不存在");
        requireOwner(attempt, actor, form.getTaskId(), form.getFileVersionId());
        if (!Objects.equals(attempt.getCopies(), form.getCopies())) throw new DataException("打印发送份数不能改变");
        return attempt;
    }
 
    private void requireOwner(DmsWenjianDayinFasongEntity attempt, UserInfo actor, String taskId, String versionId) throws DataException {
        if (!Objects.equals(attempt.getUserId(), actor.getUserId())) throw new DataException("该打印任务有其他操作人的未结束发送,请先核对原任务");
        if (!Objects.equals(attempt.getTaskId(), taskId) || !Objects.equals(attempt.getFileVersionId(), versionId)) throw new DataException("打印发送任务版本不匹配");
    }
 
    private void requirePrintable(String tenant, String taskId, String versionId, int copies, DmsWenjianDayinRenwuEntity task) throws DataException {
        Pagination pagination = new Pagination(); pagination.setCurrentPage(1L); pagination.setPageSize(1L);
        if (ledger.list(pagination, null, "f_id", taskId).isEmpty()) throw new DataException("无当前打印任务的操作权限或任务已完成");
        DmsWenjianDayinQuerenEntity version = mapper.selectTaskVersion(tenant, taskId);
        if (version == null || !Objects.equals(version.getFileVersionId(), versionId) || task.getDayinFenshu() == null
                || copies > task.getDayinFenshu() - taskMapper.countConsumedPrints(tenant, taskId)) throw new DataException("打印任务版本或剩余份数已变化,请刷新台账");
    }
 
    private List<Batch> batches(DmsWenjianDayinFasongEntity attempt) {
        return JsonUtil.getJsonToList(attempt.getBatchesJson(), Batch.class);
    }
 
    private Batch findBatch(List<Batch> list, String id) throws DataException {
        for (Batch batch : list) if (Objects.equals(batch.getId(), id)) return batch;
        throw new DataException("打印批次不存在,请恢复原任务");
    }
 
    private void validateBatch(Batch batch) throws DataException {
        if (batch == null) throw new DataException("打印批次无效");
        requireUuid(batch.getId());
        if (batch.getPages() == null || batch.getPages().isEmpty() || batch.getPages().size() > 100
                || batch.getItems() == null || batch.getItems().size() != 1 || batch.getItems().get(0) == null || batch.getItems().get(0) < 0) throw new DataException("打印页段无效");
        batch.setState("READY"); batch.setToken(null);
    }
 
    private void validatePlan(List<Batch> list, DmsWenjianDayinFasongEntity attempt) throws DataException {
        if (list == null || list.isEmpty() || list.size() > 100000) throw new DataException("打印计划无效");
        Set<String> ids = new HashSet<>();
        int cursor = 0, expectedPages = 0;
        for (int copy = 0; copy < attempt.getCopies(); copy++) {
            int page = 1;
            while (cursor < list.size() && Collections.singletonList(copy).equals(list.get(cursor).getItems())) {
                Batch batch = list.get(cursor++);
                validateBatch(batch);
                if (!ids.add(batch.getId())) throw new DataException("批次ID重复");
                for (Integer number : batch.getPages()) {
                    if (!Objects.equals(number, page++) || page > 10001) throw new DataException("打印计划必须连续覆盖完整文档,不能重复或跳页");
                }
            }
            if (page == 1 || (copy > 0 && page - 1 != expectedPages)) throw new DataException("每份必须包含相同完整页段");
            expectedPages = page - 1;
        }
        if (cursor != list.size()) throw new DataException("打印计划份数无效");
    }
 
    private void saveAttempt(DmsWenjianDayinFasongEntity attempt) throws DataException {
        if (mapper.updateAttempt(attempt) != 1) throw new DataException("保存打印发送进度失败");
    }
}