package jnpf.dmsBusinessOccupancy; import org.mybatis.spring.SqlSessionTemplate; import java.util.*; import static jnpf.dmsBusinessOccupancy.BusinessOccupancyException.Reason.*; /** Trusted native submission adapter only. No endpoint and no client-supplied chain exemption. */ public final class BusinessFileSubmissionService { /** Null version is allowed only where the native form has no version field (extension). */ public record Target(String fileId, String businessVersionId) { public Target { if (fileId == null || fileId.isBlank()) throw new IllegalArgumentException("Missing resource ID"); } } private final BusinessOccupancyTransactions transactions; private final BusinessFileOccupancyCoordinator coordinator; private final BusinessFileEligibilityService eligibility; public BusinessFileSubmissionService(SqlSessionTemplate sessions) { transactions=new BusinessOccupancyTransactions(sessions); coordinator=new BusinessFileOccupancyCoordinator(sessions); eligibility=new BusinessFileEligibilityService(sessions); } public boolean hasOccupancy(String tenant,String task) { return !transactions.reader().chainOccupancies(tenant,task).isEmpty(); } /** Includes first drafts: serialize native number use with the existing name-change callback. */ public String guardCreationNumber(String tenant,String numberId,String fileName) { return transactions.execute(mapper->{ String number=mapper.lockIssuedNumber(tenant,numberId); if(number==null || number.isBlank()) throw new IllegalArgumentException("新增审核必须选择已发放编号"); if(!mapper.creationNumberReady(tenant,numberId,fileName)) throw new IllegalArgumentException("编号名称正在变更或已发生变化,请重新选择编号"); return number; }); } /** IDs must come from the native server task/application resolution, never form parameters. */ public void submit(String tenantId, String chainId, String applicationId, String taskId, BusinessFileEligibilityService.Operation operation, List targets, boolean draft) { Objects.requireNonNull(operation); transactions.execute(mapper -> { var owned=mapper.chainOccupancies(tenantId,chainId); if (draft && owned.isEmpty()) return null; List submitted=List.copyOf(targets); if (submitted.isEmpty()) throw new IllegalArgumentException("生命周期申请至少需要一个文件"); Map requested=new HashMap<>(); for (var item:submitted) { if (operation != BusinessFileEligibilityService.Operation.EXTENSION && (item.businessVersionId() == null || item.businessVersionId().isBlank())) throw new IllegalArgumentException("当前操作必须绑定申请中的文件版本"); if(requested.putIfAbsent(item.fileId(),item)!=null) throw new IllegalArgumentException("生命周期申请不能重复选择同一文件"); } var before=mapper.fileTargets(tenantId,new ArrayList<>(requested.keySet())); if (before.size()!=submitted.size()) throw new BusinessOccupancyException(TARGET_CHANGED); var numbers=new HashSet(); for (var item:before) if(!numbers.add(item.getFileNumber())) throw new BusinessOccupancyException(TARGET_CHANGED); for (var old:owned) numbers.add(old.getFileNumber()); if (!coordinator.tryLockNumbers(tenantId,numbers)) throw new BusinessOccupancyException(NUMBER_BUSY); var current=mapper.fileTargets(tenantId,new ArrayList<>(requested.keySet())); if (!before.equals(current)) throw new BusinessOccupancyException(TARGET_CHANGED); owned=mapper.chainOccupancies(tenantId,chainId); if (!owned.isEmpty()) { if (owned.size()!=current.size()) throw new BusinessOccupancyException(TARGET_CHANGED); for (var target:current) { var old=owned.stream().filter(o->Objects.equals(o.getFileNumber(),target.getFileNumber())) .findFirst().orElseThrow(()->new BusinessOccupancyException(TARGET_CHANGED)); if (old.getReleasedAt()!=null) throw new BusinessOccupancyException(CHAIN_ALREADY_RELEASED); if (!Objects.equals(old.getOperation(),operation.name()) || !Objects.equals(old.getApplicationId(),applicationId) || !Objects.equals(old.getWorkflowTaskId(),taskId) || !Objects.equals(old.getFileId(),target.getFileId()) || !Objects.equals(old.getBusinessVersionId(),target.getBusinessVersionId()) || !Objects.equals(old.getCenterVersionId(),target.getCenterVersionId())) throw new BusinessOccupancyException(TARGET_CHANGED); requireSubmittedVersion(requested.get(target.getFileId()),old.getBusinessVersionId()); } // Same trusted chain may continue while its own candidate is pending; never reacquire. return null; } if (draft) return null; var queries=current.stream().map(t->{ requireSubmittedVersion(requested.get(t.getFileId()),t.getBusinessVersionId()); return new BusinessFileEligibilityService.Query(tenantId,t.getFileNumber(),t.getFileId(), t.getBusinessVersionId(),operation); }).toList(); var decisions=eligibility.recheckForSubmission(queries); for (int i=0;i numbers, boolean draft) { transactions.execute(mapper -> { var owned=mapper.chainOccupancies(tenant,task); if(draft && owned.isEmpty()) return null; var requested=new HashSet<>(numbers); if(requested.isEmpty() || requested.size()!=numbers.size()) throw new IllegalArgumentException("新增审核必须包含唯一且非空的文件编号"); var locks=new HashSet<>(requested); owned.forEach(row->locks.add(row.getFileNumber())); if(!coordinator.tryLockNumbers(tenant,locks)) throw new BusinessOccupancyException(NUMBER_BUSY); owned=mapper.chainOccupancies(tenant,task); if(!owned.isEmpty()) { if(owned.size()!=requested.size()) throw new BusinessOccupancyException(TARGET_CHANGED); for(var row:owned) { if(row.getReleasedAt()!=null) throw new BusinessOccupancyException(CHAIN_ALREADY_RELEASED); if(!requested.contains(row.getFileNumber()) || !"CREATE".equals(row.getOperation()) || !Objects.equals(application,row.getApplicationId()) || !Objects.equals(task,row.getWorkflowTaskId())) throw new BusinessOccupancyException(TARGET_CHANGED); } return null; // own registered pending versions are allowed; the number set is frozen } for(String number:requested) { if(mapper.creationNumberUsed(tenant,number)) throw new BusinessOccupancyException(TARGET_CHANGED); var row=new BusinessFileOccupancy(); row.setTenantId(tenant);row.setFileNumber(number);row.setOperation("CREATE"); row.setChainId(task);row.setApplicationId(application);row.setWorkflowTaskId(task);row.setStage("SUBMITTED"); coordinator.acquire(row); } return null; }); } private static void requireSubmittedVersion(Target submitted,String expected) { if (submitted.businessVersionId()!=null && !Objects.equals(submitted.businessVersionId(),expected)) throw new BusinessOccupancyException(TARGET_CHANGED); } }