刘光辉
昨天 bb638871a7fb692d80f1b7a758f991dc0879002c
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
156
157
158
159
160
161
162
163
164
165
166
package jnpf.bizcommon.onlyoffice.entity;
 
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
 
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
 
/**
 * OnlyOffice 集成参数,绑定 {@code config/shared/jnpf-biz-common.yaml} 的 {@code onlyoffice.*}。
 *
 * <p>三个 base url 之所以分开而不是共用一个「网关地址」,是因为它们的**视角不同**:
 * {@code dsPublicUrl} 是浏览器视角(容器名浏览器解析不了),另两个是 DS 容器视角。
 * 宿主 java -jar 栈下后两者要覆盖成 host.docker.internal(DS 在容器、网关在宿主)。
 */
@Data
@Component
@ConfigurationProperties(prefix = "onlyoffice")
public class OnlyOfficeProperties {
 
    /** 浏览器加载编辑器 api.js 的地址,如 http://localhost:20000 */
    private String dsPublicUrl;
 
    /** DS 回源拉原始文档的网关地址,如 http://cx-gateway:30000 */
    private String fileFetchBaseUrl;
 
    /** DS 回调后端的网关地址,如 http://cx-gateway:30000 */
    private String callbackBaseUrl;
 
    /**
     * 后端反向拉取 DS 产物的地址(后端视角),如 http://cx-onlyoffice。
     *
     * <p>保存回调里 DS 给的下载 url 用的是**它自己视角**的主机名,后端不一定能直连;
     * 配了本项就只保留其 path+query 换到这个主机上。留空则原样使用 DS 给的 url。
     */
    private String dsInternalUrl;
 
    /**
     * 与 DS 容器 JWT_SECRET 同源。后端用它签发编辑器 config,并校验 DS 回调的
     * Authorization 头。回调端点在网关白名单内(平台补丁 #9),此密钥是其唯一安全边界。
     */
    private String jwtSecret;
 
    /** 编辑锁心跳超时(分钟)。锁正常由 status 2/4 回调释放,此项是浏览器崩溃等异常场景的兜底 */
    private Integer lockTimeoutMinutes = 30;
 
    /**
     * 是否允许在编辑器里下载文档(对应「文件 → 下载为」与顶栏右侧的下载图标)。
     *
     * <p>默认关闭。⚠️ 它只收界面入口,不是安全边界——{@code /api/file/Image/**} 在网关白名单内、
     * 只靠 securityKey 票据鉴权,知道 URL 的人照样能取到原文件。
     */
    private boolean allowDownload = false;
 
    /** 是否允许在编辑器里打印(对应「文件 → 打印」与顶栏右侧的打印图标)。默认关闭,同上注意事项 */
    private boolean allowPrint = false;
 
    /** 编辑权限判定,见 {@link EditPermission} */
    private EditPermission editPermission = new EditPermission();
 
    /** ONLYOFFICE 插件注册表与场景绑定 */
    private Plugins plugins = new Plugins();
 
    @Data
    public static class Plugins {
 
        /** 插件编码到部署定义的映射 */
        private Map<String, PluginDefinition> definitions = new LinkedHashMap<>();
 
        /** 允许启用插件的业务场景 */
        private List<PluginSceneMapping> scenes = new ArrayList<>();
    }
 
    @Data
    public static class PluginDefinition {
 
        /** 与插件 config.json 完全一致的 GUID */
        private String guid;
 
        /** 插件契约版本 */
        private String version;
 
        /** 浏览器可访问的插件资源基础地址,不含 release */
        private String publicBaseUrl;
 
        /** 不可变发布目录;正式版本为 0.1.0 */
        private String release = "0.1.0";
 
        /** 浏览器可访问的插件业务 API 基础地址 */
        private String apiBaseUrl;
    }
 
    @Data
    public static class PluginSceneMapping {
 
        private String bizScene;
 
        private String bizModule;
 
        private List<String> pluginCodes = new ArrayList<>();
 
        /** 打开编辑器后自动启动的插件编码,必须属于 pluginCodes */
        private String autostartPluginCode;
 
        /** 当前场景需要禁用的 Document Server 内置插件 GUID */
        private List<String> disabledPluginGuids = new ArrayList<>();
    }
 
    /**
     * 「谁能编辑」的判定规则。
     *
     * <p>粒度是**菜单/按钮级**,与平台在线表单的单条更新端点一致
     * ({@code VisualdevModelDataController#update} 只做 {@code StpUtil.checkPermission})。
     * 判定所需的权限码在登录时就已灌进 Sa-Token 会话,纯本地判断、零 Feign 调用。
     *
     * <p>为什么不做「记录级」(这一条检验单该不该给这个人改):那属于业务规则,
     * 依赖业务表字段(班组、负责人、流程状态),而 biz-common 的准入规则禁止它含单一
     * 业务模块的专属逻辑。需要记录级时应由业务模块自行判定后再打开编辑器。
     */
    @Data
    public static class EditPermission {
 
        /**
         * 未登记的 bizModule(或请求根本没带 bizModule)时的取向。
         *
         * <p>{@code false}(默认)= 放行编辑,只打一条 warn 日志。这是**兼容默认值**:
         * 里程碑 3 的最简调用 {@code openOfficeDocument({ file })} 并不传 bizModule,
         * 一上来就严格会让所有既有调用方突然变只读。
         *
         * <p>{@code true} = 一律只读。业务侧把该登记的都登记完之后应当打开它——
         * 否则「配置漏登记」这种失误会表现为「悄悄放行编辑」,而不是被人察觉的报错。
         */
        private boolean strict = false;
 
        /**
         * bizModule → 在线表单 moduleId 的登记表。
         *
         * <p>刻意用 List 而不是 Map:Spring Boot Binder 对 Map 是**合并**语义、对 List 是
         * **整体替换**,用 Map 的话删掉一条登记根本不生效(audit 的 layer0 注册表踩过这个坑,
         * 见 {@code AuditFormRegistry})。
         */
        private List<ModuleMapping> modules = new ArrayList<>();
    }
 
    @Data
    public static class ModuleMapping {
 
        /** 调用方传来的业务模块标识,与 {@code OnlyOfficeConfigParam.bizModule} 对应 */
        private String bizModule;
 
        /** 对应的在线表单功能主键(base_module.f_id),权限码就是拿它拼的 */
        private String moduleId;
 
        /**
         * 是否要求 {@code moduleId::btn_edit} 按钮权限,默认 true。
         *
         * <p>⚠️ 这条是个陷阱位:在线表单只有勾了「启用按钮权限」时,用户的权限表里才会有
         * {@code moduleId::btn_edit}。没启用却把本项留成 true,会导致**所有人都被降级只读**。
         * 该模块没启用按钮权限时填 false,此时只校验菜单级权限。
         */
        private boolean requireBtnEdit = true;
    }
}