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<BusinessRevisionRecord> 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<BusinessFileOccupancy> 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<BusinessFileOccupancy> check(String tenant,String application,String task,String detailId,
|
String mainChange,List<BusinessRevisionRecord> 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<String> 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<BusinessFileOccupancy> 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<Item> items(BusinessFileOccupancyMapper mapper,String tenant,String mainFile,List<BusinessRevisionRecord> records) {
|
Map<String,BusinessRevisionRecord> existing=new LinkedHashMap<>();
|
List<Item> 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;
|
}
|
}
|