刘光辉
4 小时以前 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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
package jnpf.dmsBusinessOccupancy;
 
import org.mybatis.spring.SqlSessionTemplate;
import java.util.*;
import static jnpf.dmsBusinessOccupancy.BusinessOccupancyException.Reason.*;
 
/** Trusted business execution only; caller owns the transaction and completion evidence. */
public final class BusinessFileExecutionService {
    private final BusinessOccupancyTransactions transactions;
    private final BusinessFileOccupancyCoordinator coordinator;
    private final BusinessFileEligibilityService eligibility;
    private final SqlSessionTemplate sessions;
    public BusinessFileExecutionService(SqlSessionTemplate sessions) {
        this.sessions=sessions;
        transactions=new BusinessOccupancyTransactions(sessions);
        coordinator=new BusinessFileOccupancyCoordinator(sessions);
        eligibility=new BusinessFileEligibilityService(sessions);
    }
    /** Locks the original numbers before document locks and verifies the frozen source versions. */
    public List<BusinessFileOccupancy> require(String tenant,String chain,String application,String workflowTask,String operation,
                                               Collection<String> fileIds) {
        return transactions.execute(mapper->{
            Set<String> files=new HashSet<>(fileIds);
            if(files.isEmpty() || files.size()!=fileIds.size()) throw new BusinessOccupancyException(TARGET_CHANGED);
            var all=mapper.chainOccupancies(tenant,chain);
            var receipts=all.stream().filter(r->files.contains(r.getFileId())).toList();
            if(receipts.size()!=files.size()) throw new BusinessOccupancyException(TARGET_CHANGED);
            if(!coordinator.tryLockNumbers(tenant,receipts.stream().map(BusinessFileOccupancy::getFileNumber).toList()))
                throw new BusinessOccupancyException(NUMBER_BUSY);
            var current=mapper.fileTargets(tenant,new ArrayList<>(files));
            if(current.size()!=files.size()) throw new BusinessOccupancyException(TARGET_CHANGED);
            List<BusinessFileOccupancy> result=new ArrayList<>();
            for(var old:receipts) {
                var row=mapper.chain(tenant,old.getFileNumber(),chain);
                if(row==null || row.getReleasedAt()!=null) throw new BusinessOccupancyException(CHAIN_ALREADY_RELEASED);
                if(!Objects.equals(row.getApplicationId(),application) || !Objects.equals(row.getWorkflowTaskId(),workflowTask)
                        || !Objects.equals(row.getOperation(),operation)) throw new BusinessOccupancyException(TARGET_CHANGED);
                var target=current.stream().filter(t->Objects.equals(t.getFileId(),row.getFileId())).findFirst()
                        .orElseThrow(()->new BusinessOccupancyException(TARGET_CHANGED));
                if(!Objects.equals(target.getFileNumber(),row.getFileNumber())
                        || !Objects.equals(target.getBusinessVersionId(),row.getBusinessVersionId())
                        || !Objects.equals(target.getCenterVersionId(),row.getCenterVersionId()))
                    throw new BusinessOccupancyException(TARGET_CHANGED);
                result.add(row);
            }
            return List.copyOf(result);
        });
    }
    /** Extension executes the whole application; subset execution is not a valid completion. */
    public List<BusinessFileOccupancy> requireExtension(String tenant,String application,String task,
                                                       Collection<String> fileIds) {
        return transactions.execute(mapper->{
            var frozen=mapper.chainOccupancies(tenant,task).stream()
                    .filter(row->Objects.equals(application,row.getApplicationId())
                            && Objects.equals(task,row.getWorkflowTaskId())
                            && "EXTENSION".equals(row.getOperation())).toList();
            if(frozen.isEmpty()) throw new BusinessOccupancyException(TARGET_CHANGED);
            if(!coordinator.tryLockNumbers(tenant,frozen.stream().map(BusinessFileOccupancy::getFileNumber).toList()))
                throw new BusinessOccupancyException(NUMBER_BUSY);
            var requested=new HashSet<>(fileIds);
            var locked=mapper.chainOccupancies(tenant,task).stream()
                    .filter(row->Objects.equals(application,row.getApplicationId())
                            && Objects.equals(task,row.getWorkflowTaskId())
                            && "EXTENSION".equals(row.getOperation())).toList();
            if(requested.isEmpty() || requested.size()!=fileIds.size() || locked.size()!=requested.size()
                    || !requested.equals(new HashSet<>(locked.stream().map(BusinessFileOccupancy::getFileId).toList()))
                    || !new HashSet<>(frozen.stream().map(BusinessFileOccupancy::getId).toList())
                        .equals(new HashSet<>(locked.stream().map(BusinessFileOccupancy::getId).toList())))
                throw new BusinessOccupancyException(TARGET_CHANGED);
            return require(tenant,task,application,task,"EXTENSION",fileIds);
        });
    }
    public List<BusinessFileOccupancy> requireRevision(String tenant,String auditId) {
        return new BusinessRevisionSubmissionService(sessions).require(tenant,auditId);
    }
    public List<BusinessFileOccupancy> requireCreation(String tenant,String auditId) {
        return transactions.execute(mapper->{
            var before=mapper.creationNumbers(tenant,auditId);
            var rows=requireCreation(tenant,auditId,before);
            var after=mapper.creationNumbers(tenant,auditId);
            if(before.size()!=after.size() || !new HashSet<>(before).equals(new HashSet<>(after)))
                throw new BusinessOccupancyException(TARGET_CHANGED);
            return rows;
        });
    }
    /** Reserve an external approval's own number; it has no issued-number detail row. */
    public List<BusinessFileOccupancy> requireExternalCreation(String tenant, String auditId,
                                                                String taskId, String fileNumber) {
        return transactions.execute(mapper -> {
            if (!coordinator.tryLockNumbers(tenant, List.of(fileNumber)))
                throw new BusinessOccupancyException(NUMBER_BUSY);
            BusinessFileOccupancy previous = mapper.chain(tenant, fileNumber, taskId);
            if (previous == null && mapper.hasBusinessNumber(tenant, fileNumber))
                throw new BusinessOccupancyException(TARGET_CHANGED);
            BusinessFileOccupancy requested = new BusinessFileOccupancy();
            requested.setTenantId(tenant); requested.setFileNumber(fileNumber);
            requested.setOperation("CREATE"); requested.setChainId(taskId);
            requested.setApplicationId(auditId); requested.setWorkflowTaskId(taskId);
            requested.setStage("EXECUTING");
            BusinessFileOccupancy receipt = coordinator.acquire(requested);
            return List.of(receipt);
        });
    }
    /** Every CREATE source item must be covered; called before activation and again before projection. */
    public List<BusinessFileOccupancy> requireCreation(String tenant,String auditId,Collection<String> numbers) {
        return transactions.execute(mapper->{
            var requested=new HashSet<>(numbers);
            if(requested.isEmpty() || requested.size()!=numbers.size()) throw new BusinessOccupancyException(TARGET_CHANGED);
            if(!coordinator.tryLockNumbers(tenant,requested)) throw new BusinessOccupancyException(NUMBER_BUSY);
            var rows=mapper.creationReceipts(tenant,auditId);
            if(rows.size()!=requested.size()) throw new BusinessOccupancyException(TARGET_CHANGED);
            for(var row:rows) {
                if(row.getReleasedAt()!=null) throw new BusinessOccupancyException(CHAIN_ALREADY_RELEASED);
                if(!requested.contains(row.getFileNumber())) throw new BusinessOccupancyException(TARGET_CHANGED);
            }
            return rows;
        });
    }
    public void guardNumbers(String tenant,String chain,Collection<String> numbers) {
        if(numbers.isEmpty()) return;
        transactions.execute(mapper->{
            if(!coordinator.tryLockNumbers(tenant,numbers)) throw new BusinessOccupancyException(NUMBER_BUSY);
            for(String number:numbers) {
                var row=mapper.active(tenant,number);
                if(row!=null && !Objects.equals(chain,row.getChainId())) throw new BusinessOccupancyException(ALREADY_OCCUPIED);
            }
            return null;
        });
    }
    /** Standalone center files retain their path; resource-backed files require the full audit batch. */
    public void guardStandaloneActivation(String tenant,String centerVersionId) {
        transactions.execute(mapper->{
            String number=mapper.centerFileNumber(tenant,centerVersionId);
            if(number!=null && !number.isBlank()) guardNumbers(tenant,null,List.of(number));
            if(!Objects.equals(number,mapper.centerFileNumber(tenant,centerVersionId))
                    || mapper.hasBusinessDocument(tenant,centerVersionId))
                throw new BusinessOccupancyException(TARGET_CHANGED);
            return null;
        });
    }
    /** Generic initial upload must not claim a number already owned by a business resource/chain. */
    public void guardStandaloneRegistration(String tenant,String fileNumber) {
        if(fileNumber==null || fileNumber.isBlank()) return;
        transactions.execute(mapper->{
            guardNumbers(tenant,null,List.of(fileNumber));
            if(mapper.hasBusinessNumber(tenant,fileNumber)) throw new BusinessOccupancyException(TARGET_CHANGED);
            return null;
        });
    }
    public void guardValidity(String tenant,String task,Collection<String> versions) {
        transactions.execute(mapper->{
            List<BusinessFileTarget> targets=new ArrayList<>();
            for(String version:versions) {
                var found=mapper.expiryTargets(tenant,version);
                if(found.isEmpty() && !mapper.hasCurrentBusinessReference(tenant,version)) continue;
                if(found.size()!=1) throw new BusinessOccupancyException(TARGET_CHANGED);
                targets.add(found.get(0));
            }
            if(targets.isEmpty()) return null;
            guardNumbers(tenant,task,targets.stream().map(BusinessFileTarget::getFileNumber).toList());
            if(task!=null) {
                var all=mapper.chainOccupancies(tenant,task);
                for(var target:targets) {
                    var row=all.stream().filter(r->Objects.equals(r.getFileId(),target.getFileId())).findFirst()
                            .orElseThrow(()->new BusinessOccupancyException(TARGET_CHANGED));
                    require(tenant,task,row.getApplicationId(),task,"EXTENSION",List.of(target.getFileId()));
                    if(!Objects.equals(target.getCenterVersionId(),row.getCenterVersionId()))
                        throw new BusinessOccupancyException(TARGET_CHANGED);
                }
            } else {
                var checks=eligibility.recheckForSubmission(targets.stream().map(t->
                        new BusinessFileEligibilityService.Query(tenant,t.getFileNumber(),t.getFileId(),t.getBusinessVersionId(),
                                BusinessFileEligibilityService.Operation.REVIEW_EXTENSION)).toList());
                if(checks.stream().anyMatch(d->!d.allowed())) throw new BusinessOccupancyException(TARGET_CHANGED);
            }
            return null;
        });
    }
    /** Cascading records must also respect number occupancy, even when not explicit application rows. */
    public void guardVersions(String tenant,String chain,Collection<String> versions) {
        transactions.execute(mapper->{
            List<BusinessFileTarget> targets=new ArrayList<>();
            for(String version:versions) {
                var found=mapper.expiryTargets(tenant,version);
                if(found.size()!=1) throw new BusinessOccupancyException(TARGET_CHANGED);
                targets.add(found.get(0));
            }
            if(targets.isEmpty()) throw new BusinessOccupancyException(TARGET_CHANGED);
            if(!coordinator.tryLockNumbers(tenant,targets.stream().map(BusinessFileTarget::getFileNumber).toList()))
                throw new BusinessOccupancyException(NUMBER_BUSY);
            for(var target:targets) {
                if(!List.of(target).equals(mapper.expiryTargets(tenant,target.getCenterVersionId())))
                    throw new BusinessOccupancyException(TARGET_CHANGED);
                var occupied=mapper.active(tenant,target.getFileNumber());
                if(occupied!=null && !Objects.equals(chain,occupied.getChainId()))
                    throw new BusinessOccupancyException(ALREADY_OCCUPIED);
            }
            return null;
        });
    }
    public List<BusinessFileOccupancy> terminationCandidates(String afterId,int limit) {
        return transactions.reader().terminationCandidates(afterId,limit);
    }
    /** Re-read authoritative terminal evidence under number and workflow locks; no timeout release. */
    public boolean releaseEnded(BusinessFileOccupancy candidate) {
        return transactions.execute(mapper->{
            if(!coordinator.tryLockNumbers(candidate.getTenantId(),List.of(candidate.getFileNumber()))) return false;
            if(mapper.lockEndedTask(candidate)==null) return false;
            return coordinator.release(candidate.getTenantId(),candidate.getFileNumber(),candidate.getChainId(),candidate.getId(),"ENDED_UNSUCCESSFUL");
        });
    }
    /** Call only after all native detail/resource/version projections succeeded in this transaction. */
    public void complete(List<BusinessFileOccupancy> receipts) {
        for(var row:receipts) if(!coordinator.release(row.getTenantId(),row.getFileNumber(),row.getChainId(),row.getId(),"EXECUTED"))
            throw new BusinessOccupancyException(TARGET_CHANGED);
    }
}