刘光辉
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
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
package jnpf.bizcommon.onlyoffice.service.impl;
 
import cn.hutool.core.net.URLEncodeUtil;
import cn.hutool.jwt.JWTUtil;
import jnpf.base.UserInfo;
import jnpf.bizcommon.onlyoffice.entity.OnlyOfficeCallbackParam;
import jnpf.bizcommon.onlyoffice.entity.OnlyOfficeConfigParam;
import jnpf.bizcommon.onlyoffice.entity.OnlyOfficeProperties;
import jnpf.constant.GlobalConst;
import jnpf.dmsEntity.compare.DmsCompareTicket;
import jnpf.dmsEntity.preview.vo.DmsFilePreviewTicketVO;
import jnpf.exception.DataException;
import jnpf.util.TicketUtil;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
 
@Service
public class OnlyOfficeCompareService {
    private final OnlyOfficeProperties properties;
    private final DmsDocumentCompareConfigBuilder builder;
    private final DmsOnlyOfficePreviewTicketValidator validator;
    private final TicketReader tickets;
    private final OnlyOfficeComparisonRenderer renderer;
 
    @org.springframework.beans.factory.annotation.Autowired
    public OnlyOfficeCompareService(OnlyOfficeProperties properties, DmsDocumentCompareConfigBuilder builder,
                                     DmsOnlyOfficePreviewTicketValidator validator, OnlyOfficeComparisonRenderer renderer) {
        this(properties, builder, validator, TicketUtil::parseTicket, renderer);
    }
 
    OnlyOfficeCompareService(OnlyOfficeProperties properties, DmsDocumentCompareConfigBuilder builder,
                            DmsOnlyOfficePreviewTicketValidator validator, TicketReader tickets, OnlyOfficeComparisonRenderer renderer) {
        this.properties = properties;
        this.builder = builder;
        this.validator = validator;
        this.tickets = tickets;
        this.renderer = renderer;
    }
 
    public Map<String, Object> createConfig(String ticketValue, UserInfo user) {
        DmsCompareTicket ticket;
        try {
            ticket = DmsCompareTicket.fromRaw(tickets.read(ticketValue));
        } catch (RuntimeException e) {
            throw new DataException("比较会话已过期,请重新选择版本");
        }
        String tenant = user == null || user.getTenantId() == null || user.getTenantId().trim().isEmpty()
                ? GlobalConst.DEFAULT_TENANT_VALUE : user.getTenantId().trim();
        if (user == null || !ticket.getTenantId().equals(tenant) || !ticket.getUserId().equals(user.getUserId())) {
            throw new DataException("比较票据与当前用户或租户不匹配");
        }
        validateSource(ticket.getHistorical(), user);
        validateSource(ticket.getCurrent(), user);
        String key = DmsDocumentCompareConfigBuilder.KEY_PREFIX + UUID.randomUUID();
        String title = ticket.getCurrent().getFileName() + "|" + ticket.getHistoricalLabel() + " → " + ticket.getCurrentLabel();
        secret(); // Fail before invoking DS if signing is unavailable.
        String resultPath = renderer.generate(sourceUrl(ticket.getHistorical()), sourceUrl(ticket.getCurrent()));
        if (ticket.getExpiresAt() <= System.currentTimeMillis()) {
            throw new DataException("比较会话已过期,请重新选择版本");
        }
        Map<String, Object> resultClaims = new LinkedHashMap<>();
        resultClaims.put("scope", "dms:onlyoffice:compare-result");
        resultClaims.put("key", key);
        resultClaims.put("path", resultPath);
        resultClaims.put("expiresAt", ticket.getExpiresAt());
        String resultTicket = JWTUtil.createToken(resultClaims, secret());
        String resultUrl = properties.getFileFetchBaseUrl() + "/api/biz/onlyoffice/compare/result?ticket="
                + URLEncodeUtil.encode(resultTicket, GlobalConst.DEFAULT_CHARSET);
        Map<String, Object> config = builder.comparisonConfig(key, title, resultUrl,
                properties.getCallbackBaseUrl() + "/api/biz/onlyoffice/compare/callback", user);
        config.put("token", JWTUtil.createToken(config, secret()));
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("config", config);
        result.put("title", title);
        result.put("expiresAt", ticket.getExpiresAt());
        return result;
    }
 
    public byte[] readResult(String ticket) {
        try {
            if (ticket == null || !JWTUtil.verify(ticket, secret())) throw new IllegalArgumentException();
            cn.hutool.json.JSONObject payload = JWTUtil.parseToken(ticket).getPayloads();
            if (!"dms:onlyoffice:compare-result".equals(payload.getStr("scope"))
                    || payload.getLong("expiresAt", 0L) <= System.currentTimeMillis()) {
                throw new IllegalArgumentException();
            }
            return renderer.readResult(payload.getStr("path"));
        } catch (Exception e) {
            throw new DataException("比较结果票据无效或已过期");
        }
    }
 
    /** No file/session repository or write-back dependency: DS owns and expires its temporary copy. */
    public void acknowledgeCallback(OnlyOfficeCallbackParam body, String authorization) {
        String token = authorization == null ? body.getToken()
                : authorization.replaceFirst("^Bearer ", "").trim();
        if (token == null || !JWTUtil.verify(token, secret())) throw new DataException("比较回调验签失败");
        if (body.getKey() == null || !body.getKey().startsWith(DmsDocumentCompareConfigBuilder.KEY_PREFIX)) {
            throw new DataException("无效的比较会话");
        }
        // Intentionally never download body.url or call the normal OnlyOffice save handler.
    }
 
    private void validateSource(DmsFilePreviewTicketVO source, UserInfo user) {
        if (source.getFileName() == null || !source.getFileName().toLowerCase(java.util.Locale.ROOT).endsWith(".docx")) {
            throw new DataException("版本比较目前仅支持 DOCX 文件");
        }
        OnlyOfficeConfigParam param = new OnlyOfficeConfigParam();
        param.setFileId(source.getFileVersionId().toString());
        param.setFileName(source.getFileName());
        param.setDmsAccessTicket(source.getAccessTicket());
        validator.validate(param, user);
    }
 
    private String sourceUrl(DmsFilePreviewTicketVO source) {
        return properties.getFileFetchBaseUrl() + "/api/file/Dms/OnlyOffice/" + source.getFileVersionId()
                + "?ticket=" + URLEncodeUtil.encode(source.getAccessTicket(), GlobalConst.DEFAULT_CHARSET);
    }
 
    private byte[] secret() {
        String value = properties.getJwtSecret();
        if (value == null || value.trim().isEmpty() || value.contains("${")) {
            throw new DataException("OnlyOffice 签名密钥未正确配置");
        }
        return value.getBytes(StandardCharsets.UTF_8);
    }
 
    @FunctionalInterface interface TicketReader { Object read(String ticket); }
}