刘光辉
118 分钟以前 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
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);
    }
}