liusheng
5 天以前 cc479e6a056785103996cc7ea67dbd5faa3e800e
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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
package com.ruoyi.template.config;
 
import com.ruoyi.common.core.service.IConfigService;
import com.ruoyi.common.core.service.TemplateModuleDisabledException;
import com.ruoyi.template.api.TemplateFacade;
 
import java.util.List;
 
/**
 * 模板模块开关校验(不依赖 JUnit,可直接用模块 classpath 运行)。
 *
 * <h3>为什么必须做这个</h3>
 * 开关光有 {@code TemplateSwitch} 类**不等于生效** —— 本轮之前就是这个状态:
 * {@code isEnabled()} 除了被门面转发一次之外,**没有任何地方调用它**,
 * 参数设成 false 什么也不会发生。
 * 所以这里验证的是"**接上了没有**",不只是"方法能不能返回 boolean":
 * <ol>
 *   <li><b>参数表优先</b>:{@code sys_config} 有值时以它为准,同一份 yml 配置下可开可关;</li>
 *   <li><b>yml 兜底</b>:{@code sys_config} 没有这一行时回落 {@code template.enabled}(默认 true);</li>
 *   <li><b>热更新</b>:改 {@code sys_config} 后,缓存过期即生效(无需重启);</li>
 *   <li><b>拦截器真的拦</b>:关闭时 {@code preHandle} 抛 {@code ServiceException} 且提示明确;
 *       启用时放行;</li>
 *   <li><b>受保护路径</b>:数量与内容符合预期,且<b>不含</b>已移出模块的运行时实例端点;</li>
 *   <li><b>门面也被拦</b>:关闭时 6 个数据方法全部抛
 *       {@code TemplateModuleDisabledException},且文案与 HTTP 拦截器逐字一致;</li>
 *   <li><b>两种"不可用"要分清</b>:<b>关闭</b> → 抛异常;
 *       <b>开着但服务装配缺失</b> → 返回安全值(null / 空列表 / 0)。
 *       前者是显式状态,后者是防装配问题拖垮调用方 —— 混为一谈就会让
 *       "模块被关闭"和"数据查不到"再也分不开。</li>
 * </ol>
 */
public final class TemplateSwitchCheck {
 
    private static int failed = 0;
 
    private static final String SYS_ON = "true";
    private static final String SYS_OFF = "false";
    private static final String SYS_MISSING = null;
 
    private TemplateSwitchCheck() {
    }
 
    public static void main(String[] args) throws Exception {
        System.out.println("=== 1) 参数表优先 / yml 兜底 ===");
        TemplateProperties ymlOn = new TemplateProperties();          // 默认 true
 
        TemplateSwitch missSwitch = new TemplateSwitch(ymlOn, SYS_MISSING);
        check("sys_config 缺失 + yml 默认 -> 启用(fail-open)", "true",
                String.valueOf(missSwitch.isEnabled()));
 
        TemplateSwitch sysOff = new TemplateSwitch(ymlOn, SYS_OFF);
        check("sys_config=false 覆盖 yml 默认 -> 关闭", "false",
                String.valueOf(sysOff.isEnabled()));
 
        TemplateProperties ymlOff = new TemplateProperties();
        ymlOff.setEnabled(false);
        TemplateSwitch sysOn = new TemplateSwitch(ymlOff, SYS_ON);
        check("sys_config=true 覆盖 yml=false -> 启用(参数表优先)", "true",
                String.valueOf(sysOn.isEnabled()));
 
        TemplateSwitch bothOff = new TemplateSwitch(ymlOff, SYS_MISSING);
        check("sys_config 缺失 + yml=false -> 关闭(兜底生效)", "false",
                String.valueOf(bothOff.isEnabled()));
 
        System.out.println();
        System.out.println("=== 2) 热更新(10 秒缓存 + invalidateCache)===");
        TemplateProperties hot = new TemplateProperties();
        final String[] value = {SYS_OFF};
        TemplateSwitch hotSwitch = hotSwitch(hot, value);
        check("改之前(参数表=false)", "false", String.valueOf(hotSwitch.isEnabled()));
        value[0] = SYS_ON;
        check("缓存未过期时仍返回旧值(进程内 10 秒缓存)", "false",
                String.valueOf(hotSwitch.isEnabled()));
        hotSwitch.invalidateCache();
        check("缓存失效后读到新值(改成 true 立即生效)", "true",
                String.valueOf(hotSwitch.isEnabled()));
 
        System.out.println();
        System.out.println("=== 3) 拦截器:关闭时明确报错,启用时放行 ===");
        TemplateAvailabilityInterceptor offInterceptor =
                new TemplateAvailabilityInterceptor(new TemplateSwitch(ymlOn, SYS_OFF));
        TemplateAvailabilityInterceptor onInterceptor =
                new TemplateAvailabilityInterceptor(new TemplateSwitch(ymlOn, SYS_ON));
 
        check("启用时 preHandle 放行(request 传 null 也不 NPE)", "true",
                String.valueOf(onInterceptor.preHandle(null, null, null)));
 
        String thrown = "(未抛异常)";
        try {
            offInterceptor.preHandle(null, null, null);
        } catch (Exception e) {
            thrown = e.getClass().getName() + "|" + e.getMessage();
        }
        check("关闭时 preHandle 抛 ServiceException", "true",
                String.valueOf(thrown.startsWith("com.ruoyi.common.exception.ServiceException|")));
        check("关闭时错误提示明确(含 template.enabled=false 与「接口不可用」)", "true",
                String.valueOf(thrown.contains("template.enabled=false") && thrown.contains("接口不可用")));
 
        System.out.println();
        System.out.println("=== 4) 受保护路径 ===");
        List<String> guarded = TemplateWebConfig.guardedPathPatterns();
        check("受保护路径数量 = 66", "66", String.valueOf(guarded.size()));
        check("含宣教模板端点 /smartor/library/**", "true",
                String.valueOf(guarded.contains("/smartor/library/**")));
        check("含标签域端点 /base/tag/**", "true",
                String.valueOf(guarded.contains("/base/tag/**")));
        check("含刻意留在 smartor 的模板管理端点 /smartor/svyLibTemplate/**", "true",
                String.valueOf(guarded.contains("/smartor/svyLibTemplate/**")));
        check("不含已移出模块的运行时实例端点 /smartor/ivrtasksms/**", "false",
                String.valueOf(guarded.contains("/smartor/ivrtasksms/**")));
        check("不含已移出模块的运行时实例端点 /smartor/visitResult/**", "false",
                String.valueOf(guarded.contains("/smartor/visitResult/**")));
        check("不含已移出模块的运行时实例端点 /smartor/ivrtaskcallrecord/**", "false",
                String.valueOf(guarded.contains("/smartor/ivrtaskcallrecord/**")));
        check("不含非模板业务端点 /smartor/serviceSubtask/**", "false",
                String.valueOf(guarded.contains("/smartor/serviceSubtask/**")));
        check("不含非模板业务端点 /smartor/patinhosp/**", "false",
                String.valueOf(guarded.contains("/smartor/patinhosp/**")));
 
        System.out.println();
        System.out.println("=== 5) 门面:模块关闭时**同样拒绝访问** ===");
        // 刻意不构造真实服务(ObjectProvider 传 null):本组验证的是"开关守卫",
        // 守卫在方法最前面 —— 所以即使服务不可用,关闭时也必须先抛"模块未启用"。
        TemplateFacade offFacade = new TemplateFacade(ymlOn, SYS_OFF);
        check("门面 isEnabled() 仍可查询(守卫不拦它,否则就自我矛盾)", "false",
                String.valueOf(offFacade.isEnabled()));
 
        // 6 个数据方法逐个断言:关闭时必须抛 TemplateModuleDisabledException
        checkFacadeBlocked(offFacade, "findHeLibraryById", () -> offFacade.findHeLibraryById(1L));
        checkFacadeBlocked(offFacade, "findHeLibraryList", () -> offFacade.findHeLibraryList(null));
        checkFacadeBlocked(offFacade, "saveOrUpdateHeLibrary", () -> offFacade.saveOrUpdateHeLibrary(null));
        checkFacadeBlocked(offFacade, "findHeLibraryInfoByCondition",
                () -> offFacade.findHeLibraryInfoByCondition(null));
        checkFacadeBlocked(offFacade, "findIvrLibaTemplateInfoByCondition",
                () -> offFacade.findIvrLibaTemplateInfoByCondition(null));
        checkFacadeBlocked(offFacade, "findIvrLibaExtemplatescriptsByIds",
                () -> offFacade.findIvrLibaExtemplatescriptsByIds(java.util.Collections.emptyList()));
 
        // 文案必须与 HTTP 拦截器逐字一致(同一个开关不该因走哪条路而提示不同)
        check("门面提示语与拦截器常量逐字一致", "true",
                String.valueOf(TemplateFacade.DISABLED_MESSAGE.equals(TemplateAvailabilityInterceptor.DISABLED_MESSAGE)));
        check("提示语可读性(含 template.enabled=false 与「接口不可用」)", "true",
                String.valueOf(TemplateFacade.DISABLED_MESSAGE.contains("template.enabled=false")
                        && TemplateFacade.DISABLED_MESSAGE.contains("接口不可用")));
 
        System.out.println();
        System.out.println("=== 6) 门面:模块**开着**但服务装配缺失时仍是安全值(不抛异常)===");
        // 这条与第 5 组是两件不同的事,必须都成立:
        //   关闭            -> 抛异常(显式告知)
        //   开着但服务缺失  -> 返回 null/空列表(不让装配问题拖垮调用方)
        TemplateFacade onFacade = new TemplateFacade(ymlOn, SYS_ON);
        check("启用时 isEnabled()", "true", String.valueOf(onFacade.isEnabled()));
        boolean safeNoThrow = true;
        try {
            check("启用 + 服务不可用时 findHeLibraryById 返回 null", "null",
                    String.valueOf(onFacade.findHeLibraryById(1L)));
            check("启用 + 服务不可用时 findHeLibraryList 返回空列表", "true",
                    String.valueOf(onFacade.findHeLibraryList(null).isEmpty()));
            check("启用 + 服务不可用时 findIvrLibaExtemplatescriptsByIds 返回空列表", "true",
                    String.valueOf(onFacade.findIvrLibaExtemplatescriptsByIds(
                            java.util.Collections.emptyList()).isEmpty()));
            check("启用 + 服务不可用时 saveOrUpdateHeLibrary 返回 0", "0",
                    String.valueOf(onFacade.saveOrUpdateHeLibrary(null)));
        } catch (Exception e) {
            safeNoThrow = false;
            System.out.println("     异常: " + e);
        }
        check("启用 + 服务不可用时读方法不抛异常", "true", String.valueOf(safeNoThrow));
 
        System.out.println();
        if (failed == 0) {
            System.out.println("===== 模板开关与门面校验全部通过 =====");
        } else {
            System.out.println("===== 模板开关与门面校验失败 " + failed + " 项 =====");
            System.exit(1);
        }
    }
 
    /**
     * 断言"模块关闭时某个门面方法被拒",且拒的是<b>正确的异常类型与文案</b>。
     * <p>
     * 只断言"抛了异常"是不够的:抛 {@code NullPointerException} 也算抛。
     * 所以这里同时校验类型、文案,以及"不是别的异常"。
     */
    private static void checkFacadeBlocked(TemplateFacade facade, String methodName, ThrowingCall call) {
        String outcome;
        try {
            Object value = call.invoke();
            outcome = "没抛异常,返回=" + value;
        } catch (TemplateModuleDisabledException e) {
            outcome = e.getMessage();
        } catch (Throwable t) {
            outcome = "抛了别的异常:" + t.getClass().getName();
        }
        check("关闭时门面 " + methodName + " 抛出明确「模块未启用」",
                TemplateFacade.DISABLED_MESSAGE, outcome);
    }
 
    /**
     * 供 {@link #checkFacadeBlocked} 用的可抛异常调用(避免为本校验引入 JUnit/函数式接口依赖)
     */
    private interface ThrowingCall {
        Object invoke() throws Exception;
    }
 
    /**
     * 造一个「{@code sys_config} 的值可以被中途改掉」的开关,用于验证缓存与热更新。
     */
    private static TemplateSwitch hotSwitch(TemplateProperties properties, final String[] configValue) {
        return new TemplateSwitch(properties,
                new org.springframework.beans.factory.ObjectProvider<IConfigService>() {
                    private final IConfigService service = configKey -> configValue[0];
 
                    @Override
                    public IConfigService getObject(Object... args) {
                        return service;
                    }
 
                    @Override
                    public IConfigService getIfAvailable() {
                        return service;
                    }
 
                    @Override
                    public IConfigService getIfUnique() {
                        return service;
                    }
 
                    @Override
                    public IConfigService getObject() {
                        return service;
                    }
 
                    @Override
                    public java.util.stream.Stream<IConfigService> stream() {
                        return java.util.stream.Stream.of(service);
                    }
                });
    }
 
    private static void check(String key, String expected, String actual) {
        boolean ok = expected.equals(actual);
        if (!ok) {
            failed++;
        }
        System.out.println((ok ? "[OK]   " : "[FAIL] ") + key + "  期望=" + expected + "  实际=" + actual);
    }
}