刘光辉
8 小时以前 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
package jnpf.base.service;
 
import com.baomidou.dynamic.datasource.tx.TransactionContext;
import jnpf.base.ActionResult;
import jnpf.exception.DataException;
import jnpf.util.JsonUtil;
import jnpf.util.TicketUtil;
import jnpf.util.UserProvider;
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.web.context.request.async.DeferredResult;
import java.util.*;
 
/** Authorized, content-free change signals. Polling remains authoritative across nodes/restarts. */
@Component
public class UploadEditNotifications {
    private final Map<String, Long> versions = new LinkedHashMap<>();
    private final Set<Waiter> waiters = new HashSet<>();
    private long sequence = System.currentTimeMillis();
    private record Waiter(Map<String, Long> versions, DeferredResult<ActionResult<Boolean>> result) { }
 
    public synchronized long version(String tenant, String draft) {
        return versions.getOrDefault(tenant + ":" + draft, 0L);
    }
 
    public String ticket(Map<String, Long> observed) {
        Map<String, Object> proof = new LinkedHashMap<>();
        proof.put("purpose", "upload-edit-change-watch");
        proof.put("tenant", UploadEditDraftService.tenant(UserProvider.getUser()));
        proof.put("user", UserProvider.getLoginUserId());
        proof.put("expires", System.currentTimeMillis() + 60_000L);
        proof.put("versions", observed);
        return TicketUtil.createTicket(JsonUtil.getObjectToString(proof), 60L);
    }
 
    public DeferredResult<ActionResult<Boolean>> watch(String ticket) {
        String raw = ticket == null ? null : TicketUtil.parseTicket(ticket);
        Map<?, ?> proof = raw == null ? Map.of() : JsonUtil.getJsonToBean(raw, Map.class);
        String tenant = UploadEditDraftService.tenant(UserProvider.getUser());
        if (!"upload-edit-change-watch".equals(proof.get("purpose")) || !tenant.equals(proof.get("tenant"))
                || !Objects.equals(UserProvider.getLoginUserId(), proof.get("user"))
                || !(proof.get("expires") instanceof Number expiry) || expiry.longValue() < System.currentTimeMillis()
                || !(proof.get("versions") instanceof Map<?, ?> observed) || observed.isEmpty() || observed.size() > 200)
            throw new DataException("附件同步凭据已失效,请重新读取共享状态");
        Map<String, Long> expected = new LinkedHashMap<>();
        for (var entry : observed.entrySet()) {
            if (!(entry.getValue() instanceof Number number)) throw new DataException("附件同步凭据无效");
            expected.put(tenant + ":" + entry.getKey(), number.longValue());
        }
        DeferredResult<ActionResult<Boolean>> result = new DeferredResult<>(25_000L, ActionResult.success(false));
        Waiter waiter = new Waiter(expected, result);
        synchronized (this) {
            if (changed(expected)) result.setResult(ActionResult.success(true));
            else if (waiters.size() < 1024) waiters.add(waiter);
            else result.setResult(ActionResult.success(false));
        }
        result.onCompletion(() -> { synchronized (this) { waiters.remove(waiter); } });
        return result;
    }
 
    public void changedAfterCommit(String tenant, String draft) {
        Runnable publish = () -> publish(tenant + ":" + draft);
        TransactionSynchronization sync = new TransactionSynchronization() {
            @Override public void afterCommit() { publish.run(); }
        };
        if (TransactionContext.getXID() != null) TransactionContext.registerSynchronization(sync);
        else if (TransactionSynchronizationManager.isSynchronizationActive()) TransactionSynchronizationManager.registerSynchronization(sync);
        else publish.run();
    }
 
    private synchronized boolean changed(Map<String, Long> expected) {
        return expected.entrySet().stream().anyMatch(e -> !Objects.equals(versions.getOrDefault(e.getKey(), 0L), e.getValue()));
    }
    private synchronized void publish(String key) {
        versions.put(key, ++sequence);
        if (versions.size() > 4096) versions.remove(versions.keySet().iterator().next());
        // Completion callbacks may run immediately; iterate a snapshot.
        for (Waiter waiter : new ArrayList<>(waiters)) if (changed(waiter.versions())) {
            waiters.remove(waiter);
            waiter.result().setResult(ActionResult.success(true));
        }
    }
}