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("保存打印发送进度失败");
|
}
|
}
|