刘光辉
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
151
152
153
154
155
package jnpf.flowable.service.dev;
 
import com.alibaba.fastjson.JSON;
import jakarta.annotation.PreDestroy;
import jnpf.base.UserInfo;
import jnpf.config.ConfigValueUtil;
import jnpf.database.util.TenantDataSourceUtil;
import jnpf.exception.WorkFlowException;
import jnpf.util.UserProvider;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
 
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
 
/** Development-only, process-local jobs. Browser disconnection never cancels a running job. */
@Service
@RequiredArgsConstructor
@Slf4j
public class DevApprovalRuns {
    private final DevApprovalService service;
    private final DevApprovalSignatures signatures;
    private final ConfigValueUtil config;
    private final Map<String, Run> runs = new ConcurrentHashMap<>();
    private final Semaphore capacity = new Semaphore(4);
    private volatile boolean closing;
    private java.util.concurrent.Executor executor = command -> Thread.ofPlatform().name("workflow-dev-approval")
            .daemon(true).inheritInheritableThreadLocals(false).start(command);
 
    public record Progress(String runId, String status, int completed, String nodeName, String message) {}
    private static class Run {
        final String id = UUID.randomUUID().toString();
        final String owner;
        final String signId;
        final long created = System.currentTimeMillis();
        volatile boolean stop;
        volatile Progress progress;
        Run(String owner, String signId) {
            this.owner = owner; this.signId = signId;
            progress = new Progress(id, "RUNNING", 0, null, "后台审批已启动");
        }
        boolean active() { return Set.of("RUNNING", "STOPPING").contains(progress.status()); }
    }
 
    private UserInfo requireUser() throws WorkFlowException {
        if (closing || !service.allowed()) throw new WorkFlowException("开发后台审批不可用或无权限");
        return UserProvider.getUser();
    }
    private String key(String taskId, UserInfo user) {
        String tenant = user.getTenantId();
        return (tenant == null || tenant.isBlank() ? "0" : tenant) + ":" + taskId;
    }
    private void owner(Run run, UserInfo user) throws WorkFlowException {
        if (!run.owner.equals(user.getUserId())) throw new WorkFlowException("此后台任务不属于当前用户");
    }
 
    public synchronized Progress start(String taskId, String signId) throws WorkFlowException {
        UserInfo caller = requireUser();
        Run existing = runs.get(key(taskId, caller));
        if (existing != null && (existing.active() || Objects.equals(existing.signId, signId))) {
            owner(existing, caller);
            return existing.progress;
        }
        signatures.require(taskId, signId);
        var state = service.inspect(taskId, null);
        if (!state.isEnabled() || !"READY".equals(state.getStatus()))
            throw new WorkFlowException("流程当前不可自动审批:" + String.join(";", state.getProblems()));
        if (!capacity.tryAcquire()) throw new WorkFlowException("后台审批任务已满,请稍后再试");
        Run run = new Run(caller.getUserId(), signId);
        try {
            UserInfo captured = JSON.parseObject(JSON.toJSONString(caller), UserInfo.class);
            if (captured.getToken() == null || captured.getToken().isBlank()) captured.setToken(UserProvider.getToken());
            if (captured.getToken() == null || captured.getToken().isBlank()) throw new WorkFlowException("登录凭证不可用,请重新登录");
            runs.entrySet().removeIf(e -> !e.getValue().active() && System.currentTimeMillis() - e.getValue().created > 60 * 60 * 1000);
            if (runs.size() >= 200) throw new WorkFlowException("后台审批记录已满,请稍后再试");
            runs.put(key(taskId, caller), run);
            executor.execute(() -> execute(taskId, run, captured));
            return run.progress;
        } catch (Exception ex) {
            runs.remove(key(taskId, caller), run);
            capacity.release();
            throw ex;
        }
    }
 
    public Progress status(String taskId) throws WorkFlowException {
        UserInfo user = requireUser();
        Run run = runs.get(key(taskId, user));
        if (run == null) return new Progress(null, "IDLE", 0, null, null);
        owner(run, user);
        return run.progress;
    }
 
    public synchronized Progress stop(String taskId, String runId) throws WorkFlowException {
        UserInfo user = requireUser();
        Run run = runs.get(key(taskId, user));
        if (run == null || !run.id.equals(runId)) throw new WorkFlowException("后台任务已变化,请刷新状态");
        owner(run, user);
        if (run.active()) {
            run.stop = true;
            run.progress = new Progress(run.id, "STOPPING", run.progress.completed(), run.progress.nodeName(), "当前节点处理完后停止");
        }
        return run.progress;
    }
 
    private void execute(String taskId, Run run, UserInfo user) {
        int completed = 0;
        String node = null;
        Set<String> handled = new HashSet<>();
        try {
            UserProvider.setLocalLoginUser(user);
            if (config.isMultiTenancy()) TenantDataSourceUtil.switchTenant(user.getTenantId());
            while (!run.stop && !closing && completed < 100 && System.currentTimeMillis() - run.created < 25 * 60 * 1000) {
                if (!Boolean.TRUE.equals(UserProvider.isValidToken(user.getToken())))
                    throw new WorkFlowException("登录已失效,后台审批已停止,请重新登录并签名");
                // No signed HTTP preflight: approve performs one full preflight under the native node lock.
                var result = service.approve(taskId, null, run.signId);
                node = result.getNodeName();
                if (!Set.of("ADVANCED", "PASSED").contains(result.getStatus())) {
                    finish(run, "BLOCKED", completed, node, String.join(";", result.getProblems()));
                    return;
                }
                if (result.getOperatorId() != null) {
                    if (!handled.add(result.getOperatorId())) throw new WorkFlowException("待办未推进,后台审批已停止");
                    completed++;
                }
                if ("PASSED".equals(result.getStatus())) {
                    finish(run, "PASSED", completed, node, "流程已审核通过");
                    return;
                }
                run.progress = new Progress(run.id, run.stop ? "STOPPING" : "RUNNING", completed, node, null);
            }
            finish(run, "STOPPED", completed, node, run.stop || closing ? "后台审批已停止" : "达到本轮执行上限,请查看流程后重新签名");
        } catch (Exception ex) {
            log.error("开发后台审批中断 task={} run={} completed={}", taskId, run.id, completed, ex);
            finish(run, "FAILED", completed, node, ex instanceof WorkFlowException ? ex.getMessage() : "后台审批异常,请查看服务日志并刷新流程核对已完成节点");
        } finally {
            DevApprovalService.clearContext();
            UserProvider.clearLocalUser();
            TenantDataSourceUtil.clearLocalTenantInfo();
            capacity.release();
        }
    }
 
    private synchronized void finish(Run run, String status, int completed, String node, String message) {
        run.progress = new Progress(run.id, status, completed, node, message);
    }
 
    @PreDestroy public void shutdown() {
        closing = true;
        runs.values().forEach(run -> run.stop = true);
    }
}