刘光辉
5 小时以前 06f314545ac3514d7ce28939920866ae8188d31c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
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;
    }
}