From 34981c30a78e8bbd7791131059a9210f9928b62c Mon Sep 17 00:00:00 2001
From: 刘光辉 <347230014@qq.com>
Date: 星期四, 17 九月 2026 09:24:09 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/master' into master

---
 jnpf-biz-common/jnpf-biz-common-onlyoffice/src/main/java/jnpf/bizcommon/onlyoffice/service/impl/OnlyOfficeServiceImpl.java |  700 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 700 insertions(+), 0 deletions(-)

diff --git a/jnpf-biz-common/jnpf-biz-common-onlyoffice/src/main/java/jnpf/bizcommon/onlyoffice/service/impl/OnlyOfficeServiceImpl.java b/jnpf-biz-common/jnpf-biz-common-onlyoffice/src/main/java/jnpf/bizcommon/onlyoffice/service/impl/OnlyOfficeServiceImpl.java
new file mode 100644
index 0000000..d24053e
--- /dev/null
+++ b/jnpf-biz-common/jnpf-biz-common-onlyoffice/src/main/java/jnpf/bizcommon/onlyoffice/service/impl/OnlyOfficeServiceImpl.java
@@ -0,0 +1,700 @@
+package jnpf.bizcommon.onlyoffice.service.impl;
+
+import cn.dev33.satoken.stp.StpUtil;
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.crypto.digest.DigestUtil;
+import cn.hutool.http.HttpResponse;
+import cn.hutool.http.HttpUtil;
+import cn.hutool.jwt.JWTUtil;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import jnpf.base.UserInfo;
+import jnpf.bizcommon.onlyoffice.entity.OnlyOfficeCallbackParam;
+import jnpf.bizcommon.onlyoffice.entity.OnlyOfficeConfigParam;
+import jnpf.bizcommon.onlyoffice.entity.OnlyOfficeProperties;
+import jnpf.bizcommon.onlyoffice.entity.OnlyOfficeSessionEntity;
+import jnpf.bizcommon.onlyoffice.mapper.OnlyOfficeSessionMapper;
+import jnpf.bizcommon.onlyoffice.service.OnlyOfficeService;
+import jnpf.constant.PermissionConst;
+import jnpf.exception.DataException;
+import jnpf.file.FileApi;
+import jnpf.file.FileUploadApi;
+import jnpf.util.UserProvider;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.util.ObjectUtils;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 缂栬緫鍣� config 鐨勭鍙戙��
+ *
+ * <p>鏈被鍙礋璐c�屾墦寮�鏂囨。銆嶈繖涓�娈点�備笁娈甸摼璺殑鍑嵁鍚勪笉鐩稿悓锛氭墦寮�璧扮敤鎴风櫥褰� token锛�
+ * DS 鍥炴簮璧� securityKey 绁ㄦ嵁锛孌S 鍥炶皟璧版湰绫荤鍙戠殑鍚屼竴鎶� JWT 瀵嗛挜銆�
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class OnlyOfficeServiceImpl implements OnlyOfficeService {
+
+    private final OnlyOfficeProperties properties;
+    private final OnlyOfficeSessionMapper sessionMapper;
+    private final OnlyOfficePluginResolver pluginResolver;
+    private final OnlyOfficeFillTicketIssuer fillTicketIssuer;
+    private final FileApi fileApi;
+    private final FileUploadApi fileUploadApi;
+
+    /** 鎵╁睍鍚� 鈫� DS documentType锛涙湭鍒楀嚭鐨勪竴寰嬫嫆缁濓紝閬垮厤鎶婁笉鏀寔鐨勬牸寮忓杺缁欑紪杈戝櫒 */
+    private static final Map<String, String> DOCUMENT_TYPES = new HashMap<>();
+
+    static {
+        putAll("word", "doc", "docx", "docm", "dot", "dotx", "odt", "ott", "rtf", "txt", "html", "htm");
+        putAll("cell", "xls", "xlsx", "xlsm", "xlt", "xltx", "ods", "ots", "csv");
+        putAll("slide", "ppt", "pptx", "pptm", "pot", "potx", "pps", "ppsx", "odp", "otp");
+        putAll("pdf", "pdf");
+    }
+
+    private static void putAll(String documentType, String... extensions) {
+        for (String extension : extensions) {
+            DOCUMENT_TYPES.put(extension, documentType);
+        }
+    }
+
+    /** 鍙紪杈戠殑鏍煎紡锛涘叾浣欙紙濡� pdf銆乼xt 涔嬪鐨勫彧璇绘牸寮忥級涓�寰嬮檷绾т负鍙 */
+    private static final Set<String> EDITABLE_EXTENSIONS = new HashSet<>(Arrays.asList(
+            "docx", "xlsx", "pptx", "docm", "xlsm", "pptm", "odt", "ods", "odp", "rtf", "txt", "csv"));
+
+    private static final String MODE_VIEW = "view";
+    private static final String MODE_EDIT = "edit";
+    private static final String ELN_TEMPLATE_ANNOTATE_SCENE = "eln.template.annotate";
+
+    @Override
+    public Map<String, Object> buildEditorConfig(OnlyOfficeConfigParam param) {
+        if (ObjectUtils.isEmpty(param.getFileId()) || ObjectUtils.isEmpty(param.getFileName())) {
+            throw new DataException("fileId 涓� fileName 涓嶈兘涓虹┖");
+        }
+        requireJwtSecret();
+
+        String extension = extensionOf(param.getFileName());
+        String documentType = DOCUMENT_TYPES.get(extension);
+        if (documentType == null) {
+            throw new DataException("OnlyOffice 涓嶆敮鎸佺殑鏂囦欢鏍煎紡锛�" + extension);
+        }
+
+        List<OnlyOfficePluginResolver.ResolvedPlugin> resolvedPlugins = pluginResolver.resolve(param);
+        UserInfo user = UserProvider.getUser();
+        OnlyOfficeSessionEntity session = loadOrCreateSession(param, documentType);
+        String mode = resolveMode(param, extension, session, user);
+        if (MODE_EDIT.equals(mode)) {
+            acquireLock(session, user.getUserId());
+        }
+
+        return assembleConfig(param, session, documentType, extension, mode, user, resolvedPlugins);
+    }
+
+    @Override
+    public Map<String, Object> getSaveStatus(String fileId) {
+        if (ObjectUtils.isEmpty(fileId)) {
+            throw new DataException("fileId 涓嶈兘涓虹┖");
+        }
+        OnlyOfficeSessionEntity session = sessionMapper.selectOne(Wrappers.<OnlyOfficeSessionEntity>lambdaQuery()
+                .eq(OnlyOfficeSessionEntity::getFileId, fileId)
+                .last("limit 1"));
+        Map<String, Object> status = new LinkedHashMap<>();
+        status.put("version", session == null || session.getVersionNo() == null ? 0 : session.getVersionNo());
+        status.put("lastSaveTime", session == null || session.getLastSaveTime() == null
+                ? null : session.getLastSaveTime().getTime());
+        return status;
+    }
+
+    // 鈹�鈹� 淇濆瓨鍥炶皟 鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+
+    @Override
+    public void handleCallback(OnlyOfficeCallbackParam param, String authorization) {
+        verifyCallbackToken(param, authorization);
+
+        if (ObjectUtils.isEmpty(param.getKey()) || param.getStatus() == null) {
+            throw new DataException("鍥炶皟缂哄皯 key 鎴� status");
+        }
+        OnlyOfficeSessionEntity session = sessionMapper.selectOne(Wrappers.<OnlyOfficeSessionEntity>lambdaQuery()
+                .eq(OnlyOfficeSessionEntity::getDocKey, param.getKey())
+                .last("limit 1"));
+        if (session == null) {
+            // 浼氳瘽琚竻鐞嗘垨 key 瀵逛笉涓娿�傛姏閿欒 Controller 璁版棩蹇楋紝浣嗕粛杩斿洖 error:0鈥斺��
+            // 杩斿洖闈� 0 浼氳 DS 鏃犻檺閲嶈瘯涓�涓案杩滀笉浼氭垚鍔熺殑鍥炶皟銆�
+            throw new DataException("鎵句笉鍒� doc_key 瀵瑰簲鐨勭紪杈戜細璇濓細" + param.getKey());
+        }
+
+        switch (param.getStatus()) {
+            case OnlyOfficeCallbackParam.Status.EDITING:
+                refreshLockHeartbeat(session);
+                break;
+            case OnlyOfficeCallbackParam.Status.READY_FOR_SAVING:
+                saveBack(session, param, true);
+                break;
+            case OnlyOfficeCallbackParam.Status.FORCE_SAVING:
+                // 寮哄埗淇濆瓨锛氳惤鐩樹絾涓嶇粨鏉熶細璇濓紝鏁呬笉杞崲 key銆佷笉閲婃斁閿�
+                saveBack(session, param, false);
+                break;
+            case OnlyOfficeCallbackParam.Status.CLOSED_NO_CHANGES:
+                releaseLock(session, OnlyOfficeSessionEntity.SessionStatus.IDLE);
+                break;
+            case OnlyOfficeCallbackParam.Status.SAVE_ERROR:
+            case OnlyOfficeCallbackParam.Status.FORCE_SAVE_ERROR:
+                log.error("OnlyOffice 淇濆瓨鍑洪敊锛宖ile={} key={} status={}",
+                        session.getFileId(), param.getKey(), param.getStatus());
+                releaseLock(session, OnlyOfficeSessionEntity.SessionStatus.SAVE_FAILED);
+                break;
+            default:
+                log.warn("鏈煡鐨� OnlyOffice 鍥炶皟 status={}锛宬ey={}", param.getStatus(), param.getKey());
+        }
+    }
+
+    /**
+     * 楠岀銆傚洖璋冭矾寰勫湪缃戝叧鐧藉悕鍗曞唴銆佸尶鍚嶅彲杈撅紝杩欐槸鍞竴鐨勫畨鍏ㄨ竟鐣岋紝
+     * 楠屼笉杩囦竴寰嬫姏閿欙紝涓嶅緱闄嶇骇鏀捐繃銆�
+     */
+    private void verifyCallbackToken(OnlyOfficeCallbackParam param, String authorization) {
+        String token = null;
+        if (!ObjectUtils.isEmpty(authorization)) {
+            token = authorization.startsWith("Bearer ") ? authorization.substring(7).trim() : authorization.trim();
+        }
+        if (ObjectUtils.isEmpty(token)) {
+            // 灏戞暟閮ㄧ讲鎶� token 鏀惧湪 body 鑰岄潪 Authorization 澶�
+            token = param.getToken();
+        }
+        if (ObjectUtils.isEmpty(token)) {
+            throw new DataException("鍥炶皟缂哄皯 JWT锛屾嫆缁濆鐞�");
+        }
+        if (!JWTUtil.verify(token, requireJwtSecret().getBytes(StandardCharsets.UTF_8))) {
+            throw new DataException("鍥炶皟 JWT 楠岀澶辫触锛屾嫆缁濆鐞�");
+        }
+    }
+
+    /**
+     * 鎶� DS 浜у嚭鐨勬柊鐗堟枃妗e啓鍥炴枃浠跺瓨鍌ㄣ��
+     *
+     * <p>鍘熷湴瑕嗙洊鍚屼竴涓� fileId鈥斺�旇繖鏍疯〃鍗曢噷瀛樼殑闄勪欢瀛楁 JSON 瀹屽叏涓嶇敤鏀瑰啓锛�
+     * 浠d环鍙槸鍓嶇渚х殑 fileSize 浼氳繃鏈燂紙闈� onSave 鍥炶皟鍒锋柊锛夈��
+     *
+     * @param rotateKey 鏄惁杞崲 doc_key 骞堕噴鏀鹃攣銆俿tatus 2锛堝叏鍛橀��鍑猴級涓� true锛�
+     *                  status 6锛堝己鍒朵繚瀛橈紝浠嶅湪缂栬緫锛変负 false
+     */
+    private void saveBack(OnlyOfficeSessionEntity session, OnlyOfficeCallbackParam param, boolean rotateKey) {
+        if (ObjectUtils.isEmpty(param.getUrl())) {
+            throw new DataException("鍥炶皟 status=" + param.getStatus() + " 浣嗙己灏戜笅杞� url");
+        }
+        String downloadUrl = rewriteToInternalHost(param.getUrl());
+        try (HttpResponse response = HttpUtil.createGet(downloadUrl).timeout(60_000).executeAsync()) {
+            if (!response.isOk()) {
+                throw new DataException("浠� DS 涓嬭浇缂栬緫鍚庢枃妗eけ璐ワ紝HTTP " + response.getStatus());
+            }
+            MultipartFile file = new InMemoryMultipartFile(session.getFileId(), response.bodyBytes());
+            String folderPath = fileApi.getPath(session.getFileType());
+            if (ObjectUtils.isEmpty(folderPath)) {
+                throw new DataException("鏂囦欢鏈嶅姟鏃犳硶瑙f瀽鏂囨。瀛樺偍鐩綍");
+            }
+            if (!folderPath.endsWith("/") && !folderPath.endsWith("\\")) {
+                folderPath += "/";
+            }
+            if (!fileUploadApi.replaceFile(file, folderPath, session.getFileId())) {
+                throw new DataException("鏂囦欢鏈嶅姟鍐欏洖鏂囨。澶辫触");
+            }
+        } catch (DataException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new DataException("鍐欏洖鏂囨。澶辫触锛�" + e.getMessage());
+        }
+
+        int nextVersion = session.getVersionNo() == null ? 1 : session.getVersionNo() + 1;
+        session.setLastSaveTime(new Date());
+        session.setLastEditorIds(param.getUsers() == null ? null : String.join(",", param.getUsers()));
+        if (rotateKey) {
+            // 鍐呭鍙樹簡蹇呴』鎹� key锛屽惁鍒欎笅娆℃墦寮� DS 浼氬懡涓嚜韬紦瀛樿繑鍥炴棫鍐呭
+            session.setVersionNo(nextVersion);
+            session.setDocKey(buildDocKey(session.getFileId(), nextVersion));
+            session.setLockUserId(null);
+            session.setLockHeartbeat(null);
+            session.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.IDLE);
+        }
+        sessionMapper.updateById(session);
+        log.info("OnlyOffice 淇濆瓨鍥炲啓瀹屾垚锛宖ile={} version={} rotateKey={}",
+                session.getFileId(), session.getVersionNo(), rotateKey);
+    }
+
+    private static final class InMemoryMultipartFile implements MultipartFile {
+
+        private final String fileName;
+        private final byte[] content;
+
+        private InMemoryMultipartFile(String fileName, byte[] content) {
+            this.fileName = fileName;
+            this.content = content;
+        }
+
+        @Override
+        public String getName() {
+            return "multipartFile";
+        }
+
+        @Override
+        public String getOriginalFilename() {
+            return fileName;
+        }
+
+        @Override
+        public String getContentType() {
+            return "application/octet-stream";
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return content.length == 0;
+        }
+
+        @Override
+        public long getSize() {
+            return content.length;
+        }
+
+        @Override
+        public byte[] getBytes() {
+            return content;
+        }
+
+        @Override
+        public InputStream getInputStream() {
+            return new ByteArrayInputStream(content);
+        }
+
+        @Override
+        public void transferTo(File destination) throws IOException {
+            Files.write(destination.toPath(), content);
+        }
+    }
+
+    /**
+     * DS 缁欑殑涓嬭浇 url 鐢ㄧ殑鏄畠鑷繁瑙嗚鐨勪富鏈哄悕锛屽悗绔笉涓�瀹氳兘鐩磋繛銆�
+     * 閰嶄簡 ds-internal-url 灏卞彧淇濈暀 path+query 鎹㈠埌鍚庣鍙揪鐨勪富鏈轰笂銆�
+     */
+    private String rewriteToInternalHost(String url) {
+        String base = properties.getDsInternalUrl();
+        if (ObjectUtils.isEmpty(base)) {
+            return url;
+        }
+        try {
+            URI origin = URI.create(url);
+            String pathAndQuery = origin.getRawPath()
+                    + (origin.getRawQuery() == null ? "" : "?" + origin.getRawQuery());
+            String rewritten = base.endsWith("/") ? base.substring(0, base.length() - 1) + pathAndQuery
+                    : base + pathAndQuery;
+            if (!rewritten.equals(url)) {
+                log.debug("DS 涓嬭浇鍦板潃閲嶅啓锛歿} 鈫� {}", url, rewritten);
+            }
+            return rewritten;
+        } catch (IllegalArgumentException e) {
+            log.warn("DS 涓嬭浇鍦板潃鏃犳硶瑙f瀽锛屽師鏍蜂娇鐢細{}", url);
+            return url;
+        }
+    }
+
+    private void refreshLockHeartbeat(OnlyOfficeSessionEntity session) {
+        session.setLockHeartbeat(new Date());
+        session.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.EDITING);
+        sessionMapper.updateById(session);
+    }
+
+    private void releaseLock(OnlyOfficeSessionEntity session, int status) {
+        session.setLockUserId(null);
+        session.setLockHeartbeat(null);
+        session.setSessionStatus(status);
+        sessionMapper.updateById(session);
+    }
+
+    // 鈹�鈹� 浼氳瘽涓庣紪杈戦攣 鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+
+    /**
+     * 鍙栧綋鍓嶆枃浠剁殑浼氳瘽锛涙病鏈夊垯寤轰竴鏉� version_no=1 鐨勬柊浼氳瘽銆�
+     * docKey 鐢� fileId + versionNo 娲剧敓锛屼繚璇佸悓涓�鐗堟湰閲嶅鎵撳紑鎷垮埌鐩稿悓 key锛堝惁鍒� DS 涓嶈涓烘槸鍚屼竴鏂囨。锛夈��
+     */
+    private OnlyOfficeSessionEntity loadOrCreateSession(OnlyOfficeConfigParam param, String documentType) {
+        OnlyOfficeSessionEntity session = sessionMapper.selectOne(Wrappers.<OnlyOfficeSessionEntity>lambdaQuery()
+                .eq(OnlyOfficeSessionEntity::getFileId, param.getFileId())
+                .eq(OnlyOfficeSessionEntity::getFileType, param.getFileType())
+                .last("limit 1"));
+        if (session != null) {
+            // 涓氬姟涓婁笅鏂囧彲鑳藉湪鍚庣画璋冪敤閲屾墠琛ュ叏锛堝鍏堜粠闄勪欢鍒楄〃鎵撳紑銆佸悗浠庝笟鍔¢〉闈㈡墦寮�锛�
+            if (!ObjectUtils.isEmpty(param.getBizModule())) {
+                session.setBizModule(param.getBizModule());
+            }
+            if (!ObjectUtils.isEmpty(param.getBizDataId())) {
+                session.setBizDataId(param.getBizDataId());
+            }
+            session.setFileName(param.getFileName());
+            session.setDocumentType(documentType);
+            sessionMapper.updateById(session);
+            return session;
+        }
+
+        session = new OnlyOfficeSessionEntity();
+        session.setId(IdUtil.getSnowflakeNextIdStr());
+        session.setFileId(param.getFileId());
+        session.setFileType(param.getFileType());
+        session.setFileName(param.getFileName());
+        session.setDocumentType(documentType);
+        session.setBizModule(param.getBizModule());
+        session.setBizDataId(param.getBizDataId());
+        session.setVersionNo(1);
+        session.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.IDLE);
+        session.setDocKey(buildDocKey(param.getFileId(), 1));
+        session.setDeleteMark(null);
+        sessionMapper.insert(session);
+        return session;
+    }
+
+    /**
+     * 鍐冲畾鏈�缁堟ā寮忋�傚墠绔紶鐨勫彧鏄湡鏈涘�硷紝浠ヤ笅鍥涚鎯呭喌涓�寰嬮檷绾т负鍙锛�
+     * 鏍煎紡鏈韩涓嶅彲缂栬緫銆佸綋鍓嶇敤鎴锋棤缂栬緫鏉冦�佷粬浜烘鎸佹湁缂栬緫閿併�丏S 涓婁竴娆′繚瀛樺け璐�
+     * 锛堥伩鍏嶅湪閿欒鐘舵�佷笂缁х画鍐欙級銆�
+     */
+    private String resolveMode(OnlyOfficeConfigParam param, String extension, OnlyOfficeSessionEntity session, UserInfo user) {
+        if (ObjectUtils.isEmpty(param.getBizScene())) {
+            return MODE_VIEW;
+        }
+        if (MODE_VIEW.equals(param.getMode())) {
+            return MODE_VIEW;
+        }
+        if (!EDITABLE_EXTENSIONS.contains(extension)) {
+            return MODE_VIEW;
+        }
+        if (!hasEditPermission(param.getBizModule(), user)) {
+            return MODE_VIEW;
+        }
+        if (Integer.valueOf(OnlyOfficeSessionEntity.SessionStatus.SAVE_FAILED).equals(session.getSessionStatus())) {
+            log.warn("鏂囨。 {} 涓婃淇濆瓨澶辫触锛屾湰娆¢檷绾у彧璇�", session.getFileId());
+            return MODE_VIEW;
+        }
+        if (isLockedByOthers(session, user.getUserId())) {
+            log.info("鏂囨。 {} 姝h {} 缂栬緫锛寋} 闄嶇骇鍙", session.getFileId(), session.getLockUserId(), user.getUserId());
+            return MODE_VIEW;
+        }
+        return MODE_EDIT;
+    }
+
+    /**
+     * 鑿滃崟/鎸夐挳绾х紪杈戞潈鍒ゅ畾銆傛潈闄愮爜鍦ㄧ櫥褰曟椂宸茬亴杩� Sa-Token 浼氳瘽锛屾澶勭函鏈湴鍒ゆ柇銆佹棤杩滅▼璋冪敤銆�
+     *
+     * <p>鍒や笉浜嗙殑鏃跺�欏彇鍝竴杈圭敱 {@code edit-permission.strict} 鍐冲畾锛氶粯璁ゆ斁琛岋紙淇濇寔閲岀▼纰� 3
+     * 涔嬪墠鐨勮涓猴紝閭f椂鏈�绠�璋冪敤骞朵笉浼� bizModule锛夛紝鐧昏榻愬叏鍚庡簲褰撴墦寮�涓ユ牸妯″紡鈥斺�斿惁鍒�
+     * 銆屾紡鐧昏銆嶄細琛ㄧ幇涓烘倓鎮勬斁琛岀紪杈戯紝鑰屼笉鏄兘琚汉鍙戠幇鐨勬姤閿欍��
+     */
+    private boolean hasEditPermission(String bizModule, UserInfo user) {
+        // 瓒呯鐩撮�氾紝涓� lims 鏃㈡湁鍒ゅ畾鍚屼竴鍙e緞锛圠imsJianceTaskServiceImpl#fetchAuditorOrganizes锛�
+        if (Boolean.TRUE.equals(user.getIsAdministrator())) {
+            return true;
+        }
+
+        OnlyOfficeProperties.EditPermission rule = properties.getEditPermission();
+        OnlyOfficeProperties.ModuleMapping mapping = ObjectUtils.isEmpty(bizModule)
+                ? null
+                : rule.getModules().stream()
+                        .filter(item -> bizModule.equals(item.getBizModule()))
+                        .findFirst()
+                        .orElse(null);
+
+        if (mapping == null || ObjectUtils.isEmpty(mapping.getModuleId())) {
+            if (rule.isStrict()) {
+                log.info("bizModule={} 鏈櫥璁板埌 onlyoffice.edit-permission.modules锛屼弗鏍兼ā寮忎笅闄嶇骇鍙", bizModule);
+                return false;
+            }
+            log.warn("bizModule={} 鏈櫥璁扮紪杈戞潈闄愭槧灏勶紝褰撳墠涓哄鏉炬ā寮忔晠鏀捐缂栬緫锛涚櫥璁伴綈鍏ㄥ悗璇峰紑鍚� edit-permission.strict", bizModule);
+            return true;
+        }
+
+        String moduleId = mapping.getModuleId();
+        // 鑿滃崟绾э細杩炲姛鑳芥湰韬兘娌℃巿鏉冨氨娌℃湁缁х画鍒ゆ柇鐨勫繀瑕�
+        if (!StpUtil.hasPermission(moduleId)) {
+            log.info("鐢ㄦ埛 {} 鏃犲姛鑳� {} 鐨勮彍鍗曟潈闄愶紝鏂囨。闄嶇骇鍙", user.getUserId(), moduleId);
+            return false;
+        }
+        // 鎸夐挳绾э細浠呭綋璇ヨ〃鍗曞惎鐢ㄤ簡鎸夐挳鏉冮檺鏃舵墠鏈夋剰涔夛紝鍚﹀垯娌′汉浼氭湁杩欎釜鏉冮檺鐮侊紙瑙� ModuleMapping 娉ㄩ噴锛�
+        if (mapping.isRequireBtnEdit() && !StpUtil.hasPermission(moduleId + "::" + PermissionConst.BTN_EDIT)) {
+            log.info("鐢ㄦ埛 {} 鏃犲姛鑳� {} 鐨勭紪杈戞寜閽潈闄愶紝鏂囨。闄嶇骇鍙", user.getUserId(), moduleId);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * 閿佹槸鍚﹁浠栦汉鎸佹湁銆傞攣姝e父鐢� status 2/4 鍥炶皟閲婃斁锛涙祻瑙堝櫒宕╂簝绛夊紓甯稿満鏅笅
+     * 闈犲績璺宠秴鏃跺厹搴曪紝鍚﹀垯鏂囨。浼氭案涔呴攣姝汇��
+     */
+    private boolean isLockedByOthers(OnlyOfficeSessionEntity session, String currentUserId) {
+        String holder = session.getLockUserId();
+        if (ObjectUtils.isEmpty(holder) || holder.equals(currentUserId)) {
+            return false;
+        }
+        Date heartbeat = session.getLockHeartbeat();
+        if (heartbeat == null) {
+            return false;
+        }
+        long timeoutMillis = properties.getLockTimeoutMinutes() * 60L * 1000L;
+        return System.currentTimeMillis() - heartbeat.getTime() < timeoutMillis;
+    }
+
+    private void acquireLock(OnlyOfficeSessionEntity session, String userId) {
+        session.setLockUserId(userId);
+        session.setLockHeartbeat(new Date());
+        session.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.EDITING);
+        sessionMapper.updateById(session);
+    }
+
+    // 鈹�鈹� config 缁勮 鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+
+    private Map<String, Object> assembleConfig(OnlyOfficeConfigParam param, OnlyOfficeSessionEntity session,
+                                               String documentType, String extension, String mode, UserInfo user,
+                                               List<OnlyOfficePluginResolver.ResolvedPlugin> resolvedPlugins) {
+        boolean editable = MODE_EDIT.equals(mode);
+        boolean hideComments = !editable || ELN_TEMPLATE_ANNOTATE_SCENE.equals(param.getBizScene());
+
+        Map<String, Object> permissions = new LinkedHashMap<>();
+        permissions.put("edit", editable);
+        // 鍙妯″紡鍏抽棴鍗忎綔鑿滃崟锛汦LN 鏍囨敞鍦烘櫙涔熼殣钘忔壒娉紝閬垮厤鎵规敞灞備笌妯℃澘瀛楁鏍囨敞娣峰湪涓�璧枫��
+        // 浠呰 permissions.comment=false 鍏充笉骞插噣锛屽繀椤婚厤涓� customization.comments=false銆�
+        // DS 鎶娿�岃兘鍐欐壒娉ㄣ�嶅拰銆岃兘鐪嬫壒娉ㄩ潰鏉裤�嶆媶鎴愪簡涓や釜鏍囧織锛屼笖鍚庤�呭帇鏍逛笉鐪� permissions
+        // 锛坵eb-apps/apps/documenteditor/main/app/controller/Main.js:1725-1727锛夛細
+        //   canComments     = permissions.comment && mode!=='view' && customization.comments!==false
+        //   canViewComments = canComments || customization.comments!==false   鈫� 杩欓噷娌℃湁 permissions
+        // 鑰屽乏鏍忛偅涓壒娉ㄦ寜閽殑鏄鹃殣鍙栫殑鏄� canViewComments锛坈ontroller/LeftMenu.js:206锛夈��
+        // 鎵�浠ュ彧鍏� permissions.comment 鐨勬晥鏋滄槸銆屾寜閽繕鍦ㄣ�佺偣寮�鍙兘鐪嬩笉鑳藉啓銆嶃��2026-08-08 瀹炶俯銆�
+        if (hideComments) {
+            permissions.put("comment", false);
+            // 妯℃澘鏍囨敞浼氱敤鍐呭鎺т欢鏇挎崲閫夊尯锛涚鐢ㄤ慨璁紝閬垮厤鍘熷瓧绗﹁璁板綍涓哄垹闄ゃ�佹柊鎺т欢琚褰曚负鏂板銆�
+            permissions.put("review", false);
+        }
+        permissions.put("chat", false);
+        // 涓嬭浇涓庢墦鍗伴粯璁ゅ叧闂細鍘绘帀銆屾枃浠� 鈫� 涓嬭浇涓恒�嶃�屾枃浠� 鈫� 鎵撳嵃銆嶄互鍙婇《鏍忓彸渚ч偅涓や釜鍥炬爣銆�
+        //
+        // 鈿狅笍 杩欎袱椤瑰彧鏄�**鏀舵帀鐣岄潰鍏ュ彛**锛屼笉绛変簬鏂囦欢鎷夸笉鍒帮細/api/file/Image/** 鍦ㄧ綉鍏崇櫧鍚嶅崟鍐呫��
+        // 鍙潬 securityKey 绁ㄦ嵁閴存潈锛岀煡閬� URL 鐨勪汉鐓ф牱鑳芥妸鍘熸枃浠舵媺璧般�傜湡姝g殑涓嬭浇绠℃帶蹇呴』钀藉湪
+        // 鏂囦欢鏈嶅姟閭d竴灞傦紝鍒妸杩欓噷鐨勫紑鍏冲綋瀹夊叏杈圭晫銆�
+        // 灏嗘潵瑕佹寜浜哄尯鍒嗭紝鐢� 搂6.1 閭e bizModule鈫抦oduleId 鐧昏琛ㄥ垽 moduleId::btn_download /
+        // btn_batchPrint 鍗冲彲锛圥ermissionConst 閲屽凡鏈夎繖涓や釜鐮侊級锛屾満鍒剁幇鎴愪笉鐢ㄦ柊閫犮��
+        permissions.put("download", properties.isAllowDownload());
+        permissions.put("print", properties.isAllowPrint());
+        // 鏀舵帀銆屼繚鎶ゃ�嶏細鍚屾椂鍘绘帀椤舵爮鐨勩�屼繚鎶ゃ�嶉〉绛句笌銆屾枃浠躲�嶅乏鑿滃崟閲岀殑銆屼繚鎶ゃ�嶆寜閽��
+        // 褰� permissions.protect 绠¤�屼笉鏄� customization鈥斺�攃ustomization.layout.toolbar.protect
+        // 灞炵櫧鏍囧弬鏁帮紝绀惧尯鐗堥暅鍍忎笉璁ゃ�侱S 鈮� 7.5 鏀寔鏈」锛堝綋鍓� 9.4.0.1锛夈��
+        //
+        // 鍏虫帀鐨勭悊鐢辨槸瀹冧笌鏈泦鎴愮殑妯″瀷鍐茬獊锛氬姞瀵�/鍙淇濇姢鍐欒繘鐨勬槸**鏂囨。鑷韩**锛�
+        // 鑰屾垜浠紪杈戝畬鏄師鍦拌鐩栧洖鍚屼竴涓� fileId锛屼竴鏃﹁鍔犲瘑锛屼笅娆℃墦寮� DS 瑕佸瘑鐮併��
+        // 鍚庣涔熸棤浠庤В锛岀瓑浜庢妸闄勪欢閿佹銆傝皝鑳界紪杈戠敱 搂6.1 鐨勬潈闄愬垽瀹� + 缂栬緫閿佸喅瀹氥��
+        permissions.put("protect", false);
+
+        Map<String, Object> document = new LinkedHashMap<>();
+        document.put("fileType", extension);
+        document.put("key", session.getDocKey());
+        document.put("title", param.getFileName());
+        document.put("url", buildFileFetchUrl(param, user));
+        document.put("permissions", permissions);
+
+        Map<String, Object> owner = new LinkedHashMap<>();
+        owner.put("id", user.getUserId());
+        owner.put("name", user.getUserName());
+
+        Map<String, Object> customization = buildCustomization(editable, !resolvedPlugins.isEmpty(), hideComments);
+
+        // 鍗忓悓涓嶅湪鏈湡鑼冨洿锛屼絾 DS 娌℃湁鍏抽棴鍗忓悓鐨勫紑鍏筹紙coEditing 鍙湁 fast/strict锛夈��
+        // strict + change:false 璁╄涓鸿嚦灏戝彲棰勬湡锛氭敼鍔ㄨ淇濆瓨鍚庢墠浜掔浉鍙锛屼笖绂佹帀鐣岄潰鍒囨崲鍏ュ彛
+        // 锛堢敤鎴峰湪鐣岄潰鏀硅繃妯″紡浼氬啓杩� localStorage 骞惰鐩栨澶勪紶鍊硷級銆傜湡姝g殑闅旂闈犵紪杈戦攣銆�
+        Map<String, Object> coEditing = new LinkedHashMap<>();
+        coEditing.put("mode", "strict");
+        coEditing.put("change", false);
+
+        Map<String, Object> editorConfig = new LinkedHashMap<>();
+        editorConfig.put("callbackUrl", properties.getCallbackBaseUrl() + "/api/biz/onlyoffice/callback");
+        editorConfig.put("lang", "zh");
+        editorConfig.put("mode", mode);
+        editorConfig.put("user", owner);
+        editorConfig.put("coEditing", coEditing);
+        editorConfig.put("customization", customization);
+        if (!resolvedPlugins.isEmpty()) {
+            Map<String, Object> plugins = new LinkedHashMap<>();
+            List<String> pluginsData = new ArrayList<>();
+            List<String> autostart = new ArrayList<>();
+            Map<String, Object> options = new LinkedHashMap<>();
+            for (OnlyOfficePluginResolver.ResolvedPlugin resolvedPlugin : resolvedPlugins) {
+                pluginsData.add(resolvedPlugin.getManifestUrl());
+                if (resolvedPlugin.isAutostart()) {
+                    autostart.add(resolvedPlugin.getGuid());
+                }
+                Map<String, Object> context = resolvedPlugin.runtimeContext(param.getFileId(), param.getBizDataId());
+                if (OnlyOfficeFillTicketIssuer.PLUGIN_CODE.equals(resolvedPlugin.getPluginCode())) {
+                    context.put("canFill", editable);
+                    context.put("scope", OnlyOfficeFillTicketIssuer.SCOPE);
+                    context.put("ticket", fillTicketIssuer.issue(param, user, resolvedPlugin.getGuid()));
+                } else if (ELN_TEMPLATE_ANNOTATE_SCENE.equals(param.getBizScene())) {
+                    context.put("canAnnotate", editable);
+                }
+                options.put(resolvedPlugin.getGuid(), context);
+            }
+            plugins.put("pluginsData", pluginsData);
+            plugins.put("options", options);
+            if (!autostart.isEmpty()) {
+                plugins.put("autostart", autostart);
+            }
+            List<String> disabledPluginGuids = resolvedPlugins.get(0).getDisabledPluginGuids();
+            if (!disabledPluginGuids.isEmpty()) {
+                plugins.put("disable", disabledPluginGuids);
+            }
+            editorConfig.put("plugins", plugins);
+        }
+
+        Map<String, Object> config = new LinkedHashMap<>();
+        config.put("document", document);
+        config.put("documentType", documentType);
+        config.put("editorConfig", editorConfig);
+
+        // token 鐨� payload 灏辨槸涓婇潰杩欎唤 config 鏈韩锛汥S 鏀跺埌鍚庣敤鍚屼竴瀵嗛挜楠岀锛�
+        // 闃叉娴忚鍣ㄧ鏀� permissions / callbackUrl銆傚繀椤绘渶鍚庢斁锛屼笉鑳芥妸鑷繁绠楄繘鍘汇��
+        config.put("token", JWTUtil.createToken(config, requireJwtSecret().getBytes(StandardCharsets.UTF_8)));
+        return config;
+    }
+
+    /**
+     * 缂栬緫鍣ㄧ晫闈㈣鍓��**蹇呴』鍦ㄥ悗绔嫾杩� config**锛歝ustomization 鍦� JWT 绛惧悕鑼冨洿鍐咃紝
+     * 鍓嶇寰� config 閲屽瀛楁浼氳 DS 楠岀澶辫触锛堝墠绔彧鑳借拷鍔� events锛屽嚱鏁颁笉鍙備笌搴忓垪鍖栵級銆�
+     *
+     * <p>涓ょ被鑰冭檻鍒嗗紑鐪嬶細
+     * <ul>
+     *   <li><b>瀹夊叏</b>锛歿@code macros}/{@code plugins}銆傞檮浠舵槸鐢ㄦ埛涓婁紶鐨勫鏉ユ枃妗o紝
+     *       鍙兘甯﹀畯锛涙彃浠讹紙鍚� AI 鍏ュ彛锛変細鎶婃枃妗e唴瀹瑰彂寰�绗笁鏂癸紝閮戒笉璇ラ粯璁ゅ紑鐫�銆�</li>
+     *   <li><b>鐣岄潰鍣煶</b>锛氬府鍔┿�佸弽棣堛�侀《鏍忔枃浠跺悕锛堝脊绐楁爣棰樺凡缁忔湁浜嗭級銆佸彸渚ч潰鏉裤��
+     *       杩欎簺绾睘瑙傛劅锛屾敼鍔ㄦ棤椋庨櫓銆�</li>
+     * </ul>
+     *
+     * <p>鈿狅笍 logo / about / layout 閭d竴绫诲睘**鐧芥爣瀹氬埗**锛岀ぞ鍖虹増闀滃儚澶ф鐜囦笉鐢熸晥锛屾晠姝ゅ涓嶈鈥斺��
+     * 闇�瑕佺殑璇濆緱鍏堝疄娴嬪啀鍔狅紝鍒啓浜嗕互涓虹敓鏁堛��
+     *
+     * @param editable 鏄惁鍙紪杈戯紱forcesave/autosave 鍙湪缂栬緫鎬佹湁鎰忎箟锛岄《鏍忓睍寮�涓庡惁涔熼殢瀹冨彉
+     */
+    private Map<String, Object> buildCustomization(boolean editable, boolean pluginsEnabled, boolean hideComments) {
+        Map<String, Object> customization = new LinkedHashMap<>();
+
+        // 鈹�鈹� 瀹夊叏 鈹�鈹�
+        // 澶栨潵 docx 鍙兘甯﹀畯锛氫笉鑷姩杩愯锛屼笖涓嶇粰鐢ㄦ埛寮�鐨勫叆鍙�
+        customization.put("macros", false);
+        customization.put("macrosMode", "disable");
+        // 浠呭悗绔櫧鍚嶅崟鍦烘櫙寮�鍚寚瀹氭彃浠讹紱绌哄満鏅粛鍏抽棴鍏ㄩ儴鎻掍欢鍏ュ彛銆�
+        customization.put("plugins", pluginsEnabled);
+
+        // 鈹�鈹� 鐣岄潰鏀舵暃 鈹�鈹�
+        // 榛樿鍏抽棴鎷煎啓妫�鏌ワ紱鐢ㄦ埛鍦ㄧ晫闈慨鏀瑰悗锛宭ocalStorage 浠嶄細瑕嗙洊杩欎釜鍒濆鍊笺��
+        Map<String, Object> features = new LinkedHashMap<>();
+        features.put("spellcheck", false);
+        customization.put("features", features);
+        if (hideComments) {
+            // 鎵规敞闈㈡澘鐨勫彟涓�鍗婂紑鍏筹紝涓� permissions.comment=false 蹇呴』鎴愬鍑虹幇銆�
+            customization.put("comments", false);
+            Map<String, Object> review = new LinkedHashMap<>();
+            review.put("trackChanges", false);
+            review.put("showReviewChanges", false);
+            customization.put("review", review);
+        }
+        customization.put("help", false);
+        customization.put("feedback", false);
+        // 宸︿笅瑙掗偅涓�屾彁鍑哄姛鑳藉缓璁�嶅綊 suggestFeature 绠★紝涓嶆槸 feedback锛堝疄娴嬪緱鐭ワ級
+        customization.put("suggestFeature", false);
+        // 椤舵爮涓嶅啀閲嶅鏄剧ず鏂囦欢鍚嶁�斺�旀垜浠殑寮圭獥鏍囬宸茬粡鏈変簡
+        customization.put("toolbarHideFileName", true);
+        customization.put("hideRightMenu", true);
+        // 椤舵爮褰㈡�佹寜妯″紡鍒嗭細缂栬緫鎬佸睍寮�鎴愬畬鏁村姛鑳藉尯锛堝伐鍏锋壘寰楀埌锛夛紝鍙鎬佹敹鎴愬崟琛岀揣鍑戠増
+        // 锛堟病鏈夊彲鐢ㄧ殑缂栬緫鎸夐挳锛屽睍寮�鍙槸鐧藉崰鍨傜洿绌洪棿锛夈�傝繖涓� DS 鑷繁鐨勯粯璁ゅ彇鍚戜竴鑷达紝
+        // 浣嗕粛鏄惧紡鍐欏嚭鈥斺�旈粯璁ゅ�奸殢鐗堟湰鍙樿繃锛屽埆鎸囨湜鐪佺暐銆�
+        //
+        // 鈿狅笍 鏈」浼氳娴忚鍣� localStorage 瑕嗙洊锛氱敤鎴锋墜鍔ㄦ姌鍙犺繃椤舵爮锛屼箣鍚庢墦寮�灏变竴鐩存槸鎶樺彔鐨勶紝
+        // 鍚庣浼犱粈涔堥兘涓嶇畻鏁般�傞獙璇佹椂璇锋崲鏃犵棔绐楀彛锛屽埆浠ヤ负閰嶇疆娌$敓鏁堛��
+        customization.put("compactToolbar", !editable);
+
+        customization.put("autosave", false);
+        if (editable) {
+            // forcesave锛氱偣淇濆瓨鍗宠Е鍙� status 6 鍥炶皟锛屼笉蹇呯瓑鍏ㄥ憳閫�鍑烘墠钀界洏
+            customization.put("forcesave", true);
+        }
+        return customization;
+    }
+
+    /**
+     * DS 鍥炴簮鎷夋枃妗g殑鍦板潃銆傝蛋 /api/file/Image/**鈥斺�旇璺緞鍦� GatewayWhite 纭紪鐮佺櫧鍚嶅崟鍐咃紝
+     * DS 娌℃湁鐧诲綍鎬佷篃鑳藉彇锛岄壌鏉冮潬 URL 涓婄殑 securityKey 绁ㄦ嵁銆�
+     *
+     * <p>馃敶 {@code &t=t} 涓嶈兘鐪侊紝涓斿畠涓�"鏃堕棿鎴�"鏃犲叧銆倇@code FileServiceImpl#flushFile} 閲�
+     * {@code redirect = StringUtil.isEmpty(t)}锛�**甯� securityKey 浣嗕笉甯� t 鏃讹紝璇ョ鐐逛笉鍚愭枃浠舵祦锛�
+     * 鑰屾槸 302 璺冲埌 ApiDomain 涓婄殑棰勮鍦板潃**銆侱S 浼氳窡鐫�璺筹紝鏈�缁堟嬁鍒�
+     * {@code {"code":400,"msg":"System abnormality"}} 骞舵妸杩欐 JSON 瀛樻垚 origin.docx锛�
+     * 琛ㄧ幇涓虹紪杈戝櫒涓�鐩磋浆鍦堬紙HTTP 200 璁� DS 浠ヤ负涓嬭浇鎴愬姛浜嗭級銆傚墠绔殑
+     * {@code getAuthMediaUrl(url, isRedirect=false)} 涔熸槸闈犺拷鍔� {@code &t=t} 鎷垮師濮嬫祦鐨勶紝姝ゅ鍚屾簮銆�
+     * 2026-08-08 瀹炶俯銆�
+     */
+    private String buildFileFetchUrl(OnlyOfficeConfigParam param, UserInfo user) {
+        return properties.getFileFetchBaseUrl()
+                + "/api/file/Image/" + param.getFileType() + "/" + param.getFileId()
+                + "?s=" + user.getSecurityKey() + "&t=t";
+    }
+
+    // 鈹�鈹� 宸ュ叿 鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+
+    /**
+     * 鍙栫鍚嶅瘑閽ワ紝椤哄甫鎸′綇銆屽崰浣嶇娌¤瑙f瀽銆嶈繖绉嶉潤榛樺け璐ャ��
+     *
+     * <p>閰嶇疆鍐欑殑鏄� {@code ${ONLYOFFICE_JWT_SECRET}}锛岃�� **Spring Boot 鐨� Binder 瀵硅В鏋愪笉浜嗙殑
+     * 鍗犱綅绗︽槸鍘熸牱鏀捐**锛堜笉鎶涘紓甯革級銆傛墍浠ュ彉閲忔病閫忎紶杩涘鍣ㄦ椂锛岃繖閲屾嬁鍒扮殑鏄瓧闈㈤噺
+     * {@code "${ONLYOFFICE_JWT_SECRET}"}鈥斺�旈潪绌恒�佽兘杩囩┖鍊兼牎楠屻�佽兘绛惧嚭 token锛屼絾 DS 楠岀蹇呯劧
+     * 澶辫触锛岃〃鐜颁负缂栬緫鍣ㄩ噷銆屾枃妗e畨鍏ㄤ护鐗岀殑鏍煎紡涓嶆纭�嶏紝涓斿悗绔鏃犲紓甯稿彲鏌ャ��
+     * 2026-08-08 瀹炶俯杩囦竴娆★紙compose 鍙妸璇ュ彉閲忓杺缁欎簡 cx-onlyoffice锛屾紡浜� cx-biz-common锛夈��
+     */
+    private String requireJwtSecret() {
+        String secret = properties.getJwtSecret();
+        if (ObjectUtils.isEmpty(secret)) {
+            throw new DataException("OnlyOffice 鏈厤缃� jwt-secret锛屾棤娉曠鍙�/鏍¢獙浠ょ墝");
+        }
+        if (secret.startsWith("${")) {
+            throw new DataException("OnlyOffice 鐨� jwt-secret 浠嶆槸鏈В鏋愮殑鍗犱綅绗︼紙" + secret
+                    + "锛夛紝璇存槑杩愯鐜娌℃湁娉ㄥ叆 ONLYOFFICE_JWT_SECRET鈥斺�斿鍣ㄦ爤妫�鏌� docker-compose.yml"
+                    + " 鐨� cx-biz-common environment锛屽涓绘爤妫�鏌� .env 鏄惁琚� start-all.sh 鍔犺浇");
+        }
+        return secret;
+    }
+
+    /**
+     * doc_key锛欴S 鐢ㄥ畠鍒ゆ柇銆屾槸涓嶆槸鍚屼竴浠芥枃妗g殑鍚屼竴涓増鏈�嶃��
+     * md5 鍙� 32 浣嶅崄鍏繘鍒讹紝澶╃劧婊¤冻 DS 瀵瑰瓧绗﹂泦 [0-9a-zA-Z_-] 涓庨暱搴� 鈮�128 鐨勮姹傘��
+     */
+    private String buildDocKey(String fileId, int versionNo) {
+        return DigestUtil.md5Hex(fileId + "_" + versionNo);
+    }
+
+    private String extensionOf(String fileName) {
+        int dot = fileName.lastIndexOf('.');
+        if (dot < 0 || dot == fileName.length() - 1) {
+            throw new DataException("鏃犳硶璇嗗埆鏂囦欢鎵╁睍鍚嶏細" + fileName);
+        }
+        return fileName.substring(dot + 1).toLowerCase();
+    }
+}

--
Gitblit v1.8.0