package jnpf.dmsService;
|
|
import jnpf.dmsIntegration.audit.DmsBusinessChangeAudit;
|
import static jnpf.dmsIntegration.audit.DmsBusinessChangeAudit.values;
|
|
import jnpf.base.UserInfo;
|
import jnpf.dmsEntity.fenfa.*;
|
import jnpf.dmsEntity.fenfa.DmsWenjianDayinFasongEntity.Batch;
|
import jnpf.dmsEntity.fenfa.form.DmsWenjianChongdaForm;
|
import jnpf.dmsMapper.*;
|
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.*;
|
|
@Service
|
@RequiredArgsConstructor
|
public class DmsWenjianChongdaService {
|
private final DmsBusinessChangeAudit businessAudit;
|
private final DmsCurrentIdentity identity;
|
private final DmsWenjianFenfaMapper taskMapper;
|
private final DmsWenjianDayinQuerenMapper versions;
|
private final DmsWenjianChongdaMapper mapper;
|
private final DmsWenjianBupinghengMapper documents;
|
private final DmsFileVersionMapper fileVersions;
|
private final DmsPermissionDecisionService permissions;
|
|
/** 可展示给同租户用户的占用摘要,不返回可补回执的令牌和原签名。 */
|
@Transactional(rollbackFor = Exception.class)
|
public jnpf.dmsEntity.fenfa.vo.DmsWenjianChongdaLockVO lockStatus(String recordId) throws DataException {
|
UserInfo actor = identity.requireUser();
|
String tenant = identity.tenantId(actor);
|
if (taskMapper.selectPrintForUpdate(tenant, recordId, null) == null)
|
throw new DataException("打印记录不存在或已删除");
|
var summary = new jnpf.dmsEntity.fenfa.vo.DmsWenjianChongdaLockVO();
|
summary.setPrintRecordId(recordId);
|
var attempt = mapper.selectActive(tenant, recordId);
|
if (attempt == null) return summary;
|
summary.setLocked(true); summary.setRequestId(attempt.getId());
|
summary.setUserId(attempt.getUserId()); summary.setCreatedAt(attempt.getCreatedAt());
|
summary.setReason(attempt.getReason());
|
var user = mapper.selectActor(tenant, attempt.getUserId());
|
summary.setUserName(user == null || blank(user.getUserName()) ? attempt.getUserId() : user.getUserName());
|
boolean owner = Objects.equals(actor.getUserId(), attempt.getUserId());
|
summary.setOwner(owner);
|
List<Batch> list = batches(attempt);
|
boolean unknown = list.stream().anyMatch(b -> "SENDING".equals(b.getState()));
|
int submitted = (int) list.stream().filter(b -> "SUBMITTED".equals(b.getState())).count();
|
summary.setSubmittedBatches(submitted); summary.setTotalBatches(list.size());
|
summary.setPhase("CONFIRMED".equals(attempt.getState()) ? "REGISTRATION_PENDING"
|
: unknown ? "UNKNOWN" : submitted > 0 ? "PARTIAL" : "READY");
|
summary.setCanCancel(owner && "OPEN".equals(attempt.getState()) && neverDispatched(list));
|
summary.setCanResume(owner && !unknown);
|
return summary;
|
}
|
|
/** 取消与begin共用副本行锁;只允许尚未取得任何派发许可的任务。 */
|
@Transactional(rollbackFor = Exception.class)
|
public DmsWenjianChongdaFasong cancel(DmsWenjianChongdaForm form) throws DataException {
|
var attempt = lockAttempt(form);
|
Map<String, Object> before = controlSnapshot(attempt);
|
if ("CANCELLED".equals(attempt.getState())) return attempt;
|
if (!"OPEN".equals(attempt.getState()) || !neverDispatched(batches(attempt)))
|
throw new DataException("任务已派发或结果未知,不能取消解锁;发送成功后请恢复登记");
|
attempt.setState("CANCELLED"); attempt.setCancelledAt(new Date()); saveAttempt(attempt);
|
recordControl("DMS_REPRINT_CANCEL", "重新打印取消", attempt, before);
|
return attempt;
|
}
|
|
private boolean neverDispatched(List<Batch> list) {
|
return list.stream().allMatch(b -> "READY".equals(b.getState()) && blank(b.getToken()));
|
}
|
|
@Transactional(rollbackFor = Exception.class)
|
public DmsWenjianChongdaFasong active(String recordId) throws DataException {
|
UserInfo actor = identity.requireUser();
|
String tenant = identity.tenantId(actor);
|
if (taskMapper.selectPrintForUpdate(tenant, recordId, null) == null)
|
throw new DataException("打印记录不存在或已删除");
|
DmsWenjianChongdaFasong attempt = mapper.selectActive(tenant, recordId);
|
if (attempt != null) requireOwner(attempt, actor);
|
return attempt;
|
}
|
|
@Transactional(rollbackFor = Exception.class)
|
public DmsWenjianChongdaFasong prepare(DmsWenjianChongdaForm form) throws DataException {
|
if (form == null || blank(form.getPrintRecordId())) throw new DataException("原打印记录不能为空");
|
requireUuid(form.getRequestId());
|
UserInfo actor = identity.requireUser();
|
String tenant = identity.tenantId(actor);
|
DmsWenjianDayinJiluEntity copy = taskMapper.selectPrintForUpdate(tenant, form.getPrintRecordId(), null);
|
if (copy == null) throw new DataException("原打印记录不存在或已删除");
|
DmsWenjianChongdaFasong prior = mapper.selectAttempt(tenant, form.getRequestId());
|
if (prior != null) {
|
requireOwner(prior, actor);
|
if (!Objects.equals(prior.getPrintRecordId(), copy.getId())) throw new DataException("重打关联不能变更");
|
if ("CANCELLED".equals(prior.getState())) throw new DataException("本次重打已取消,请关闭弹窗重新发起");
|
return prior;
|
}
|
DmsWenjianChongdaFasong active = mapper.selectActive(tenant, copy.getId());
|
if (active != null) {
|
requireOwner(active, actor);
|
throw new DataException("该副本存在未完成重打,请关闭弹窗并从原记录恢复");
|
}
|
eligible(copy);
|
if (blank(form.getReason()) || form.getReason().trim().length() > 255 || blank(form.getBizSign()))
|
throw new DataException("请填写255字以内重打原因并完成电子签名");
|
DmsWenjianDayinQuerenEntity version = versions.selectTaskVersion(tenant, copy.getDayinRenwuId());
|
if (version == null || blank(version.getFileVersionId()) || blank(copy.getDayinLiushuihao()))
|
throw new DataException("原文件版本或流水缺失,不能重打");
|
requirePrintPermission(tenant, version.getFileVersionId());
|
if (mapper.validSignature(tenant, actor.getUserId(), form.getBizSign(), copy.getId(), form.getRequestId(), form.getReason().trim()) == null)
|
throw new DataException("重打签名无效或不属于当前操作人");
|
DmsWenjianChongdaFasong attempt = new DmsWenjianChongdaFasong();
|
attempt.setId(form.getRequestId()); attempt.setTenantId(tenant); attempt.setPrintRecordId(copy.getId());
|
attempt.setTaskId(copy.getDayinRenwuId()); attempt.setFileVersionId(version.getFileVersionId());
|
attempt.setUserId(actor.getUserId()); attempt.setCopies(1); attempt.setState("OPEN");
|
attempt.setBatchesJson("[]"); attempt.setReason(form.getReason().trim());
|
attempt.setBizSign(form.getBizSign()); attempt.setCreatedAt(new Date());
|
DmsWenjianDayinHaoma number = new DmsWenjianDayinHaoma();
|
number.setSerialNumber(copy.getDayinLiushuihao()); number.setCountSerialNumber(copy.getDayinJishuLiushuihao());
|
attempt.setNumbersJson(JsonUtil.getObjectToString(Collections.singletonList(number)));
|
if (mapper.insertAttempt(attempt) != 1) throw new DataException("建立重打发送任务失败");
|
return attempt;
|
}
|
|
/** 查询原生保存结果也用于响应丢失恢复,不创建或修改业务单据。 */
|
@Transactional(rollbackFor = Exception.class)
|
public DmsWenjianChongdaFasong status(DmsWenjianChongdaForm form) throws DataException {
|
DmsWenjianChongdaFasong attempt = lockAttempt(form);
|
DmsWenjianChongdaFasong saved = mapper.findDocument(attempt.getTenantId(), attempt.getId());
|
if (saved != null) attempt.setDocumentId(saved.getDocumentId());
|
return attempt;
|
}
|
|
/** 原生主子表保存完成后只确认执行副作用;不重新打印、取号或生成发放。 */
|
@Transactional(rollbackFor = Exception.class)
|
public DmsWenjianChongdaFasong finish(DmsWenjianChongdaForm form) throws DataException {
|
DmsWenjianChongdaFasong attempt = lockAttempt(form);
|
Map<String, Object> before = controlSnapshot(attempt);
|
if ("CLOSED".equals(attempt.getState())) return attempt;
|
if (!"CONFIRMED".equals(attempt.getState()) || attempt.getSubmittedAt() == null)
|
throw new DataException("尚未收到全部重打成功回传,不能登记执行结果");
|
DmsWenjianBupinghengExecution doc = requireNativeDocument(attempt);
|
if (doc.getZhixingShijian() != null && !doc.getZhixingShijian().equals(attempt.getSubmittedAt()))
|
throw new DataException("登记执行时间与成功回传不一致,请核对原记录");
|
if (doc.getZhixingShijian() == null) {
|
if (mapper.completeDocument(attempt) != 1)
|
throw new DataException("重打已发送,登记确认失败,请只恢复登记");
|
businessAudit.record(attempt.getTenantId(), "DMS_REPRINT_EXECUTION_CONFIRMED", "回写重打执行结果",
|
"dms_wenjian_bupingheng_dengji", attempt.getDocumentId(),
|
values("zhixing_shijian", doc.getZhixingShijian()),
|
values("zhixing_shijian", attempt.getSubmittedAt()),
|
attempt.getReason(), controlContext(attempt));
|
}
|
attempt.setState("CLOSED"); saveAttempt(attempt);
|
recordControl("DMS_REPRINT_COMPLETE", "重新打印完成", attempt, before);
|
return attempt;
|
}
|
|
/** 发送成功后校验原生单据并绑定,不接管主子表CRUD。 */
|
private DmsWenjianBupinghengExecution requireNativeDocument(DmsWenjianChongdaFasong attempt) throws DataException {
|
DmsWenjianChongdaFasong saved = mapper.findDocument(attempt.getTenantId(), attempt.getId());
|
if (saved == null) throw new DataException("请先通过原生表单保存本次重打登记");
|
attempt.setDocumentId(saved.getDocumentId());
|
DmsWenjianBupinghengExecution doc = documents.lockDocument(attempt.getTenantId(), attempt.getDocumentId());
|
List<DmsWenjianBupinghengExecution.Item> items = documents.lockItems(attempt.getTenantId(), attempt.getDocumentId());
|
if (doc == null || !Objects.equals(doc.getBizSign(), attempt.getBizSign())
|
|| !Objects.equals(doc.getDengjiRenId(), attempt.getUserId()) || items == null || items.size() != 1)
|
throw new DataException("原生登记与本次重打的签名、执行人或明细不匹配");
|
DmsWenjianBupinghengExecution.Item item = items.get(0);
|
if (!"cuoshi3".equals(item.getCuoshi()) || !Objects.equals(item.getDayinJiluId(), attempt.getPrintRecordId())
|
|| !Objects.equals(item.getYuanyin(), attempt.getReason()) || !blank(item.getXinDayinRenwuId()))
|
throw new DataException("重打登记不得改变原记录、措施或原因");
|
return doc;
|
}
|
|
private DmsWenjianChongdaFasong lockAttempt(DmsWenjianChongdaForm form) throws DataException {
|
if (form == null || blank(form.getPrintRecordId())) throw new DataException("原打印记录不能为空");
|
requireUuid(form.getRequestId());
|
UserInfo actor = identity.requireUser(); String tenant = identity.tenantId(actor);
|
// 与prepare一致先锁副本;不因发送后的状态变化丢弃真实成功回执。
|
taskMapper.selectPrintForUpdate(tenant, form.getPrintRecordId(), null);
|
DmsWenjianChongdaFasong attempt = mapper.selectAttempt(tenant, form.getRequestId());
|
if (attempt == null || !Objects.equals(attempt.getPrintRecordId(), form.getPrintRecordId()))
|
throw new DataException("重打发送任务不存在或关联不匹配");
|
requireOwner(attempt, actor);
|
return attempt;
|
}
|
private void requireOwner(DmsWenjianChongdaFasong attempt, UserInfo actor) throws DataException {
|
if (!Objects.equals(attempt.getUserId(), actor.getUserId()))
|
throw new DataException("该副本存在其他用户的未完成重打,请由原发起人恢复");
|
}
|
private void eligible(DmsWenjianDayinJiluEntity copy) throws DataException {
|
if (copy == null || !("daifafang".equals(copy.getBizStatus()) || "daiqianshou".equals(copy.getBizStatus())))
|
throw new DataException("仅待发放、待签收的有效打印记录允许重打");
|
}
|
private void requirePrintPermission(String tenant, String versionId) throws DataException {
|
UUID id;
|
try { id = UUID.fromString(versionId); } catch (RuntimeException e) { throw new DataException("文件版本ID无效"); }
|
var version = fileVersions.selectReadableByTenantAndId(tenant, id);
|
if (version == null) throw new DataException("原文件版本不存在或已删除");
|
if (jnpf.dmsPermission.DmsFileControlPolicy.requiresPermissionCheck(version.getShifouShoukong()))
|
permissions.requireAllowed(jnpf.dmsEntity.permission.enums.DmsResourceType.FILE_VERSION, id,
|
jnpf.dmsEntity.permission.enums.DmsPermissionAction.PRINT);
|
}
|
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 boolean blank(String value) { return value == null || value.trim().isEmpty(); }
|
|
/** 计划持久化后才能发;已有计划只能恢复,不能被另一窗口替换。 */
|
@Transactional(rollbackFor = Exception.class)
|
public DmsWenjianChongdaFasong plan(DmsWenjianChongdaForm form, String fileVersionId, int pageCount) throws DataException {
|
DmsWenjianChongdaFasong attempt = lockAttempt(form);
|
Map<String, Object> before = controlSnapshot(attempt);
|
if (!batches(attempt).isEmpty() || !"OPEN".equals(attempt.getState())) return attempt;
|
if (!Objects.equals(attempt.getFileVersionId(), fileVersionId) || pageCount < 1 || pageCount > 10000)
|
throw new DataException("原版本或完整页数校验失败");
|
validatePlan(form.getBatches(), attempt, pageCount);
|
attempt.setPageCount(pageCount);
|
attempt.setBatchesJson(JsonUtil.getObjectToString(form.getBatches()));
|
saveAttempt(attempt);
|
recordControl("DMS_REPRINT_PLAN", "冻结重打发送计划", attempt, before);
|
return attempt;
|
}
|
|
@Transactional(rollbackFor = Exception.class)
|
public DmsWenjianChongdaFasong split(DmsWenjianChongdaForm form) throws DataException {
|
DmsWenjianChongdaFasong attempt = lockAttempt(form);
|
Map<String, Object> before = controlSnapshot(attempt);
|
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);
|
recordControl("DMS_REPRINT_BATCH_SPLIT", "调整未派发重打页段", attempt, before);
|
return attempt;
|
}
|
|
/** 非幂等派发许可:许可响应丢失也不能再次取得许可,必须保留结果未知。 */
|
@Transactional(rollbackFor = Exception.class)
|
public DmsWenjianChongdaFasong begin(DmsWenjianChongdaForm form) throws DataException {
|
DmsWenjianChongdaFasong attempt = lockAttempt(form);
|
Map<String, Object> before = controlSnapshot(attempt);
|
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("前一批次尚未确认,不能发送后续批次");
|
}
|
eligible(taskMapper.selectPrintForUpdate(attempt.getTenantId(), attempt.getPrintRecordId(), null));
|
// 新流程先打印后登记;兼容旧流程已保存的单据,仍核对一致性。
|
if (mapper.findDocument(attempt.getTenantId(), attempt.getId()) != null) {
|
DmsWenjianBupinghengExecution doc = requireNativeDocument(attempt);
|
if (doc.getZhixingShijian() != null) throw new DataException("本次登记已执行,不能再次发送重打");
|
}
|
requireUuid(form.getToken());
|
batch.setToken(form.getToken()); batch.setState("SENDING");
|
attempt.setBatchesJson(JsonUtil.getObjectToString(list)); saveAttempt(attempt);
|
recordControl("DMS_REPRINT_BATCH_BEGIN", "取得重打派发许可", attempt, before);
|
return attempt;
|
}
|
|
/** 包括迟到回传;保存失败可用同一令牌重试,绝不重新派发。 */
|
@Transactional(rollbackFor = Exception.class)
|
public DmsWenjianChongdaFasong submitted(DmsWenjianChongdaForm form) throws DataException {
|
DmsWenjianChongdaFasong attempt = lockAttempt(form);
|
Map<String, Object> before = controlSnapshot(attempt);
|
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));
|
if (list.stream().allMatch(item -> "SUBMITTED".equals(item.getState()))) {
|
attempt.setState("CONFIRMED");
|
attempt.setSubmittedAt(new Date());
|
}
|
saveAttempt(attempt);
|
recordControl("DMS_REPRINT_BATCH_RECEIPT", "确认重打提交回传", attempt, before);
|
return attempt;
|
}
|
|
private List<Batch> batches(DmsWenjianChongdaFasong 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, DmsWenjianChongdaFasong attempt, int pageCount) 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() || expectedPages != pageCount) throw new DataException("打印计划必须覆盖原版本全部页面");
|
}
|
|
|
private Map<String, Object> controlSnapshot(DmsWenjianChongdaFasong attempt) {
|
List<Map<String, Object>> batchStates = batches(attempt).stream().map(batch ->
|
values("batch_id", batch.getId(), "pages", batch.getPages(), "items", batch.getItems(),
|
"state", batch.getState())).toList();
|
return values("fasong_zhuangtai", attempt.getState(), "pici_neirong", batchStates,
|
"zong_yeshu", attempt.getPageCount(), "fasong_chenggong_shijian", attempt.getSubmittedAt(),
|
"dengji_id", attempt.getDocumentId(), "quxiao_shijian", attempt.getCancelledAt());
|
}
|
|
private Map<String, Object> controlContext(DmsWenjianChongdaFasong attempt) {
|
return values("request_id", attempt.getId(), "print_record_id", attempt.getPrintRecordId(),
|
"task_id", attempt.getTaskId(), "file_version_id", attempt.getFileVersionId(),
|
"copies", attempt.getCopies(), "numbers", JsonUtil.getJsonToList(attempt.getNumbersJson(), DmsWenjianDayinHaoma.class),
|
"document_id", attempt.getDocumentId(), "execution_user_id", attempt.getUserId(), "biz_sign", attempt.getBizSign());
|
}
|
|
private void recordControl(String action, String label, DmsWenjianChongdaFasong attempt, Map<String, Object> before) {
|
businessAudit.record(attempt.getTenantId(), action, label, "dms_wenjian_chongda_fasong", attempt.getId(),
|
before, controlSnapshot(attempt), attempt.getReason(), controlContext(attempt));
|
}
|
|
private void saveAttempt(DmsWenjianChongdaFasong attempt) throws DataException {
|
if (mapper.updateAttempt(attempt) != 1) throw new DataException("保存打印发送进度失败");
|
}
|
}
|