刘光辉
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
package jnpf.bizcommon.onlyoffice.service.impl;
 
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.jwt.JWTUtil;
import jnpf.base.ActionResult;
import jnpf.base.ActionResultCode;
import jnpf.base.model.uploadedit.UploadEditLease;
import jnpf.base.model.uploadedit.UploadEditSaveCommand;
import jnpf.bizcommon.onlyoffice.entity.OnlyOfficeCallbackParam;
import jnpf.bizcommon.onlyoffice.entity.OnlyOfficeSessionEntity;
import jnpf.exception.DataException;
import jnpf.onlinedev.UploadEditApi;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.Objects;
 
@Component
@RequiredArgsConstructor
public class UploadEditSessionSupport {
    private final UploadEditApi api;
    private final UploadEditStorage storage;
 
    public static boolean matches(OnlyOfficeSessionEntity session) {
        return session != null && UploadEditSceneConfigBuilder.SCENE.equals(session.getBizScene())
                && UploadEditSceneConfigBuilder.MODULE.equals(session.getBizModule());
    }
 
    public void requireStatusAccess(String tenantId, String ownerId, String leaseId) {
        if (!Boolean.TRUE.equals(require(api.canReadStatus(tenantId, ownerId, leaseId)))) {
            throw new DataException("编辑租约已失效或不可访问");
        }
    }
 
    public UploadEditLease lease(String tenantId, String ownerId, String leaseId) {
        UploadEditLease lease = require(api.lease(tenantId, ownerId, leaseId));
        if (lease == null || lease.getFile() == null) throw new DataException("附件编辑租约不可用");
        return lease;
    }
 
    public void save(OnlyOfficeSessionEntity session, OnlyOfficeCallbackParam callback, byte[] candidate) {
        // 回调到达时重新检查当前流程权限,不能复用打开时的权限结论。
        UploadEditLease lease = lease(session.getTenantId(), session.getLockUserId(), session.getFileId());
        // 普通未保存附件维持原行为;只有服务端核验的DMS修订来源可另存临时副本。
        if (!lease.isTracked() && !lease.isRevisionTemporary()) return;
        String operationId = DigestUtil.sha256Hex(callback.getKey() + "\n" + callback.getStatus() + "\n" + callback.getUrl());
        if (operationId.equals(lease.getLastOperationId())) return;
        Map<String, Object> file = storage.save(session.getTenantId(), session.getFileId(), savedFileName(session.getFileName(), callback.getFiletype()), candidate);
        UploadEditSaveCommand command = new UploadEditSaveCommand();
        command.setTenantId(session.getTenantId());
        command.setOwnerId(lease.getOwnerId());
        command.setLeaseId(lease.getLeaseId());
        command.setOperationId(operationId);
        command.setFile(file);
        if (!Boolean.TRUE.equals(require(api.save(command)))) throw new DataException("附件已替换、放弃或采用,本次编辑产物不再生效");
    }
 
    static String savedFileName(String sourceName, String outputType) {
        boolean sourceDoc = sourceName != null && sourceName.toLowerCase(java.util.Locale.ROOT).endsWith(".doc");
        // DOC 可能由 Document Server 转成 DOCX;缺少实际输出类型时不能冒用源后缀。
        if (outputType == null || outputType.isBlank()) {
            if (sourceDoc) throw new DataException("DOC 编辑产物缺少输出格式,无法安全保存");
            return sourceName;
        }
        String extension = outputType.toLowerCase(java.util.Locale.ROOT);
        if ((!"docx".equals(extension) && !(sourceDoc && "doc".equals(extension))) || sourceName == null) {
            throw new DataException("附件编辑产物格式不受支持");
        }
        if (sourceName.toLowerCase(java.util.Locale.ROOT).endsWith("." + extension)) return sourceName;
        return sourceName.substring(0, sourceName.lastIndexOf('.')) + "." + extension;
    }
 
    /** 已经过签名验证;进一步绑定请求体,拒绝拿编辑器配置 JWT 冒充保存回调。 */
    public static void requireBoundCallback(OnlyOfficeCallbackParam callback, String authorization) {
        String token = authorization == null || authorization.isBlank() ? callback.getToken()
                : authorization.startsWith("Bearer ") ? authorization.substring(7).trim() : authorization.trim();
        Object payload = JWTUtil.parseToken(token).getPayload("payload");
        Map<?, ?> claims = payload instanceof Map ? (Map<?, ?>) payload : JWTUtil.parseToken(token).getPayloads();
        if (!Objects.equals(callback.getKey(), claims.get("key"))
                || !(claims.get("status") instanceof Number) || !Objects.equals(callback.getStatus(), ((Number) claims.get("status")).intValue())
                || !Objects.equals(callback.getUrl(), claims.get("url"))
                || !Objects.equals(callback.getFiletype(), claims.get("filetype"))) {
            throw new DataException("保存回调与已签名内容不一致");
        }
    }
 
    private static <T> T require(ActionResult<T> result) {
        if (result == null || !ActionResultCode.SUCCESS.getCode().equals(result.getCode())) {
            throw new DataException(result == null ? "附件编辑服务不可用" : result.getMsg());
        }
        return result.getData();
    }
}