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<Target> targets, boolean draft) {
|
Objects.requireNonNull(operation);
|
transactions.execute(mapper -> {
|
var owned=mapper.chainOccupancies(tenantId,chainId);
|
if (draft && owned.isEmpty()) return null;
|
List<Target> submitted=List.copyOf(targets);
|
if (submitted.isEmpty()) throw new IllegalArgumentException("生命周期申请至少需要一个文件");
|
Map<String,Target> 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<String>();
|
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<decisions.size();i++) if(!decisions.get(i).allowed())
|
throw new BusinessFileIneligibleException(queries.get(i).fileNumber(),decisions.get(i).reasons());
|
for (int i=0;i<current.size();i++) {
|
var target=current.get(i); var row=new BusinessFileOccupancy();
|
row.setTenantId(tenantId);row.setFileNumber(target.getFileNumber());row.setOperation(operation.name());
|
row.setChainId(chainId);row.setApplicationId(applicationId);row.setWorkflowTaskId(taskId);
|
row.setFileId(target.getFileId());row.setBusinessVersionId(target.getBusinessVersionId());
|
row.setCenterVersionId(decisions.get(i).centerVersionId());row.setStage("SUBMITTED");
|
coordinator.acquire(row);
|
}
|
return null;
|
});
|
}
|
/** First numbered native audit; resource IDs do not exist until the final projection. */
|
public void submitCreation(String tenant, String application, String task, Collection<String> 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);
|
}
|
}
|