package jnpf.dmsBusinessOccupancy; import org.mybatis.spring.SqlSessionTemplate; import java.util.*; import static jnpf.dmsBusinessOccupancy.BusinessOccupancyException.Reason.*; /** Native revision-audit admission. Caller owns the native transaction; no form CRUD. */ public final class BusinessRevisionSubmissionService { private final BusinessOccupancyTransactions transactions; private final BusinessFileOccupancyCoordinator coordinator; private final BusinessFileExecutionService execution; private final BusinessFileEligibilityService eligibility; private record Item(String number,String file,String version,String center,String change) {} public BusinessRevisionSubmissionService(SqlSessionTemplate sessions) { transactions=new BusinessOccupancyTransactions(sessions); coordinator=new BusinessFileOccupancyCoordinator(sessions); execution=new BusinessFileExecutionService(sessions); eligibility=new BusinessFileEligibilityService(sessions); } /** Draft shortcut only. application/task are resolved by the native server, not posted IDs. */ public boolean hasSubmission(String tenant,String application,String task) { var mapper=transactions.reader(); return !mapper.boundAudit(tenant,application,task).isEmpty() || !mapper.chainOccupancies(tenant,task).isEmpty(); } /** detailId is an untrusted selector; its approved root and frozen receipt are resolved server-side. */ public void submit(String tenant,String application,String task,String detailId,String mainChange, List records,boolean draft) { if(draft && !hasSubmission(tenant,application,task)) return; check(tenant,application,task,detailId,mainChange,records,false); } /** Execution reads the complete persisted native audit; never accepts a client subset. */ public List require(String tenant,String auditId) { return transactions.execute(mapper->{ var audit=mapper.revisionAudit(tenant,auditId); if(audit==null) throw changed(); var records=mapper.revisionRecords(tenant,auditId); var result=check(tenant,auditId,audit.getAuditTaskId(),audit.getDetailId(),audit.getChange(),records,true); if(!audit.equals(mapper.revisionAudit(tenant,auditId)) || !records.equals(mapper.revisionRecords(tenant,auditId))) throw changed(); return result; }); } private List check(String tenant,String application,String task,String detailId, String mainChange,List records,boolean requireExisting) { if(!List.of("shengji","wubianhua").contains(Objects.toString(mainChange,""))) throw new IllegalArgumentException("修订主文件只能升级或沿用"); return transactions.execute(mapper->{ var source=mapper.revisionSource(tenant,detailId); if(source==null || source.getTaskId()==null || Objects.equals(source.getTaskId(),task)) throw changed(); var mains=mapper.fileTargets(tenant,List.of(source.getFileId())); if(mains.size()!=1) throw changed(); String mainNumber=mains.get(0).getFileNumber(); var before=items(mapper,tenant,source.getFileId(),records); var old=mapper.chainOccupancies(tenant,task); Set locks=new HashSet<>();locks.add(mainNumber); for(var item:before) if(!locks.add(item.number())) throw changed(); old.forEach(row->locks.add(row.getFileNumber())); var boundBefore=mapper.boundAudit(tenant,application,task); boundBefore.forEach(row->locks.add(row.getFileNumber())); if(!coordinator.tryLockNumbers(tenant,locks)) throw new BusinessOccupancyException(NUMBER_BUSY); if(!source.equals(mapper.revisionSource(tenant,detailId)) || !before.equals(items(mapper,tenant,source.getFileId(),records))) throw changed(); var root=execution.require(tenant,source.getTaskId(),source.getApplicationId(),source.getTaskId(), "REVISION",List.of(source.getFileId())).get(0); if(!Objects.equals(root.getFileNumber(),mainNumber) || !Objects.equals(root.getBusinessVersionId(),source.getBusinessVersionId())) throw changed(); var bound=mapper.boundAudit(tenant,application,task); if(bound.size()>1 || (!bound.isEmpty() && !Objects.equals(bound.get(0).getId(),root.getId()))) throw changed(); boolean own=Objects.equals(application,root.getAuditId()) && Objects.equals(task,root.getAuditTaskId()); if(!own && root.getAuditId()!=null && mapper.lockEndedRevisionAudit(tenant,root.getAuditId(),root.getAuditTaskId())==null) throw new BusinessOccupancyException(ALREADY_OCCUPIED); if(requireExisting && !own) throw changed(); String mainStage="AUDIT_"+mainChange; if(own && !Objects.equals(mainStage,root.getStage())) throw changed(); old=mapper.chainOccupancies(tenant,task); if(own || requireExisting || !old.isEmpty()) { if(!own || old.size()!=before.size()) throw changed(); for(var item:before) { var receipt=old.stream().filter(row->Objects.equals(row.getFileNumber(),item.number())) .findFirst().orElseThrow(BusinessRevisionSubmissionService::changed); if(receipt.getReleasedAt()!=null) throw new BusinessOccupancyException(CHAIN_ALREADY_RELEASED); if(!Objects.equals(receipt.getApplicationId(),application) || !Objects.equals(receipt.getWorkflowTaskId(),task) || !Objects.equals(receipt.getStage(),item.change()) || !Objects.equals(receipt.getOperation(),operation(item)) || !Objects.equals(receipt.getFileId(),item.file()) || !Objects.equals(receipt.getBusinessVersionId(),item.version()) || !Objects.equals(receipt.getCenterVersionId(),item.center())) throw changed(); } } else { for(var item:before) { if("xinjian".equals(item.change())) { if(mapper.creationNumberUsed(tenant,item.number())) throw changed(); } else { var decision=eligibility.recheckForSubmission(List.of(new BusinessFileEligibilityService.Query( tenant,item.number(),item.file(),item.version(),BusinessFileEligibilityService.Operation.REVISION))).get(0); if(!decision.allowed()) throw new BusinessFileIneligibleException(item.number(),decision.reasons()); } var receipt=new BusinessFileOccupancy(); receipt.setTenantId(tenant);receipt.setFileNumber(item.number());receipt.setOperation(operation(item)); receipt.setChainId(task);receipt.setApplicationId(application);receipt.setWorkflowTaskId(task); receipt.setFileId(item.file());receipt.setBusinessVersionId(item.version());receipt.setCenterVersionId(item.center()); receipt.setStage(item.change());coordinator.acquire(receipt); } if(mapper.bindRevisionAudit(root,application,task,mainStage)!=1) throw changed(); root=mapper.chain(tenant,mainNumber,source.getTaskId()); old=mapper.chainOccupancies(tenant,task); } List result=new ArrayList<>();result.add(root);result.addAll(old); return List.copyOf(result); }); } private static String operation(Item item) {return "xinjian".equals(item.change()) ? "CREATE" : "REVISION";} private static BusinessOccupancyException changed() {return new BusinessOccupancyException(TARGET_CHANGED);} /** RETIRE is inferred from effective native relationships, never accepted as posted input. */ private List items(BusinessFileOccupancyMapper mapper,String tenant,String mainFile,List records) { Map existing=new LinkedHashMap<>(); List result=new ArrayList<>(); for(var row:records) { String change=row.getChange(); if("xinjian".equals(change)) { // A new record cannot masquerade as a retained old record in the native source. if(row.getFileId()!=null || row.getBusinessVersionId()!=null) throw changed(); result.add(new Item(row.getFileNumber(),null,null,null,change)); } else { if(!List.of("shengji","wubianhua").contains(Objects.toString(change,"")) || row.getFileId()==null || row.getBusinessVersionId()==null || mainFile.equals(row.getFileId()) || existing.putIfAbsent(row.getFileId(),row)!=null) throw changed(); } } var related=mapper.revisionRelatedRecords(tenant,mainFile); if(new HashSet<>(related).size()!=related.size()) throw changed(); for(String file:related) if(!existing.containsKey(file)) { var row=new BusinessRevisionRecord();row.setFileId(file);row.setChange("RETIRE");existing.put(file,row); } if(!existing.isEmpty()) { var targets=mapper.fileTargets(tenant,new ArrayList<>(existing.keySet())); if(targets.size()!=existing.size()) throw changed(); for(var target:targets) { var row=existing.get(target.getFileId()); if(row.getBusinessVersionId()!=null && !Objects.equals(row.getBusinessVersionId(),target.getBusinessVersionId())) throw changed(); result.add(new Item(target.getFileNumber(),target.getFileId(),target.getBusinessVersionId(),target.getCenterVersionId(),row.getChange())); } } result.sort(Comparator.comparing(Item::number,Comparator.nullsFirst(Comparator.naturalOrder()))); return result; } }