刘光辉
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
package jnpf.dmsBusinessOccupancy;
 
import org.mybatis.spring.SqlSessionTemplate;
 
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
 
import static jnpf.dmsBusinessOccupancy.BusinessOccupancyException.Reason.*;
 
/**
 * Local primitive, explicitly wired by each business adapter after its transaction is verified.
 * Does not start/commit transactions, save native forms, infer workflow completion or alter expiry.
 * Call tryLockNumbers BEFORE resource/document locks, then reread business facts in that transaction.
 */
public final class BusinessFileOccupancyCoordinator {
    private final BusinessOccupancyTransactions transactions;
 
    public BusinessFileOccupancyCoordinator(SqlSessionTemplate sessions) {
        this.transactions = new BusinessOccupancyTransactions(sessions);
    }
 
    /** A UI hint only; callers must recheck under the numbered lock before any mutation. */
    public BusinessFileOccupancy current(String tenantId, String fileNumber) {
        identity(tenantId, 50); identity(fileNumber, 200);
        return transactions.reader().active(tenantId, fileNumber);
    }
 
    /**
     * False means waiting. The caller must end its transaction before retrying.
     * Partial locks remain until that transaction ends, as with all transaction-scoped DB locks.
     */
    public boolean tryLockNumbers(String tenantId, Collection<String> fileNumbers) {
        return transactions.execute(mapper -> lockNumbers(mapper, tenantId, fileNumbers));
    }
 
    private boolean lockNumbers(BusinessFileOccupancyMapper mapper, String tenantId, Collection<String> fileNumbers) {
        identity(tenantId, 50);
        if (fileNumbers == null || fileNumbers.isEmpty()) throw new IllegalArgumentException("文件编号不能为空");
        SortedSet<Long> keys = new TreeSet<>();
        for (String number : fileNumbers) {
            identity(number, 200);
            keys.add(lockKey(tenantId, number));
        }
        for (Long key : keys) if (!mapper.tryLock(key)) return false;
        return true;
    }
 
    /**
     * Expiry's lowest-priority gate. A false result is a skip, not a business failure.
     * On true the caller must reread current version/deadline and execute the ORIGINAL expiry
     * operation before ending this same transaction. This method never changes file state.
     */
    public boolean tryEnterExpiry(String tenantId, Collection<String> fileNumbers) {
        List<String> numbers = fileNumbers == null ? List.of() : List.copyOf(fileNumbers);
        return transactions.execute(mapper -> {
            if (!lockNumbers(mapper, tenantId, numbers)) return false;
            for (String number : numbers) if (mapper.active(tenantId, number) != null) return false;
            return true;
        });
    }
 
    /** Called only after authoritative eligibility checks while holding the same numbered lock. */
    public BusinessFileOccupancy acquire(BusinessFileOccupancy requested) {
        Objects.requireNonNull(requested);
        // Copy to keep the caller's mutable form/snapshot from changing identity mid-operation.
        BusinessFileOccupancy row = copy(requested);
        validate(row);
        return transactions.execute(mapper -> {
            requireLock(mapper, row.getTenantId(), row.getFileNumber());
            BusinessFileOccupancy previous = mapper.chain(row.getTenantId(), row.getFileNumber(), row.getChainId());
            if (previous != null) return sameActiveReceipt(previous, row);
            row.setId(UUID.randomUUID().toString());
            if (mapper.insert(row) != 1) throw new BusinessOccupancyException(ALREADY_OCCUPIED);
            return Objects.requireNonNull(mapper.chain(row.getTenantId(), row.getFileNumber(), row.getChainId()));
        });
    }
 
    /** Completion/termination evidence belongs to the trusted adapter, never a client-supplied flag. */
    public boolean release(String tenantId, String number, String chainId, String occupancyId, String reason) {
        identity(chainId, 50); identity(occupancyId, 50); identity(reason, Integer.MAX_VALUE);
        return transactions.execute(mapper -> {
            requireLock(mapper, tenantId, number);
            if (mapper.release(tenantId, number, chainId, occupancyId, reason) == 1) return true;
            BusinessFileOccupancy old = mapper.chain(tenantId, number, chainId);
            return old != null && occupancyId.equals(old.getId()) && old.getReleasedAt() != null;
        });
    }
 
    /** Compare-and-set prevents a delayed callback regressing a newer stage. */
    public boolean advance(String tenantId, String number, String chainId, String occupancyId,
                           String expectedStage, String nextStage) {
        identity(chainId, 50); identity(occupancyId, 50);
        identity(expectedStage, 100); identity(nextStage, 100);
        return transactions.execute(mapper -> {
            requireLock(mapper, tenantId, number);
            if (mapper.advance(tenantId, number, chainId, occupancyId, expectedStage, nextStage) == 1) return true;
            BusinessFileOccupancy old = mapper.chain(tenantId, number, chainId);
            return old != null && occupancyId.equals(old.getId()) && old.getReleasedAt() == null
                    && nextStage.equals(old.getStage());
        });
    }
 
    private void requireLock(BusinessFileOccupancyMapper mapper, String tenantId, String number) {
        if (!lockNumbers(mapper, tenantId, List.of(number)))
            throw new BusinessOccupancyException(NUMBER_BUSY);
    }
 
    private static BusinessFileOccupancy sameActiveReceipt(BusinessFileOccupancy old, BusinessFileOccupancy row) {
        if (old.getReleasedAt() != null) throw new BusinessOccupancyException(CHAIN_ALREADY_RELEASED);
        if (!Objects.equals(old.getOperation(), row.getOperation())
                || !Objects.equals(old.getApplicationId(), row.getApplicationId())
                || !Objects.equals(old.getFileId(), row.getFileId())
                || !Objects.equals(old.getBusinessVersionId(), row.getBusinessVersionId())
                || !Objects.equals(old.getCenterVersionId(), row.getCenterVersionId())
                || !Objects.equals(old.getWorkflowTaskId(), row.getWorkflowTaskId()))
            throw new BusinessOccupancyException(TARGET_CHANGED);
        // Do not rewind the stage on a repeated submission.
        return old;
    }
 
    private static BusinessFileOccupancy copy(BusinessFileOccupancy source) {
        BusinessFileOccupancy row = new BusinessFileOccupancy();
        row.setTenantId(source.getTenantId()); row.setFileNumber(source.getFileNumber());
        row.setOperation(source.getOperation()); row.setChainId(source.getChainId());
        row.setApplicationId(source.getApplicationId()); row.setWorkflowTaskId(source.getWorkflowTaskId());
        row.setFileId(source.getFileId()); row.setBusinessVersionId(source.getBusinessVersionId());
        row.setCenterVersionId(source.getCenterVersionId()); row.setStage(source.getStage());
        return row;
    }
 
    private static void validate(BusinessFileOccupancy row) {
        identity(row.getTenantId(), 50); identity(row.getFileNumber(), 200);
        identity(row.getOperation(), 100); identity(row.getChainId(), 50);
        identity(row.getApplicationId(), 50); identity(row.getStage(), 100);
        if (row.getFileId() == null && "CREATE".equals(row.getOperation())) {
            if (row.getBusinessVersionId() != null || row.getCenterVersionId() != null)
                throw new IllegalArgumentException("首次建档尚无文件资源,不能携带来源版本");
        } else identity(row.getFileId(), 50);
        optionalIdentity(row.getWorkflowTaskId()); optionalIdentity(row.getBusinessVersionId());
        optionalIdentity(row.getCenterVersionId());
    }
 
    private static void optionalIdentity(String value) { if (value != null) identity(value, 50); }
 
    private static void identity(String value, int maxLength) {
        if (value == null || value.isBlank() || !value.equals(value.strip())
                || value.codePointCount(0, value.length()) > maxLength || value.indexOf('\0') >= 0)
            throw new IllegalArgumentException("业务占用标识不能为空、超长或包含首尾空白");
    }
 
    // Stable across JVMs; length-prefixing prevents tenant/number separator ambiguity.
    // Hash collisions only serialize unrelated numbers. Table uniqueness uses the full exact number.
    static long lockKey(String tenantId, String number) {
        try {
            MessageDigest hash = MessageDigest.getInstance("SHA-256");
            hash.update("dms-business-file-occupancy:v1\0".getBytes(StandardCharsets.UTF_8));
            for (String part : List.of(tenantId, number)) {
                byte[] bytes = part.getBytes(StandardCharsets.UTF_8);
                hash.update(ByteBuffer.allocate(4).putInt(bytes.length).array());
                hash.update(bytes);
            }
            return ByteBuffer.wrap(hash.digest()).getLong();
        } catch (NoSuchAlgorithmException e) { throw new IllegalStateException(e); }
    }
}