liusheng
6 天以前 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
package com.ruoyi.inhospvisit.config;
 
import com.ruoyi.common.core.service.IConfigService;
import com.ruoyi.inhospvisit.api.InhospVisitFacade;
 
import java.util.List;
 
/**
 * 出入院随访模块开关校验(不依赖 JUnit,可直接用模块 classpath 运行)。
 *
 * <h3>为什么必须做这个</h3>
 * 模块原本有个 {@code InhospVisitContextCheck},但那份文件<b>整个被注释掉了</b>(412 行全是 {@code //}),
 * 所以模块实际上<b>没有任何可运行的开关校验</b> —— 这是本轮审计明确点出的缺口。
 * <p>
 * 照 {@code TemplateSwitchCheck} 的形状写,验证的是"**接上了没有**",不只是"方法能不能返回 boolean":
 * <ol>
 *   <li><b>参数表优先</b>:{@code sys_config} 有值时以它为准,同一份 yml 配置下可开可关;</li>
 *   <li><b>yml 兜底</b>:{@code sys_config} 没有这一行时回落 {@code inhospvisit.enabled}(默认 true);</li>
 *   <li><b>热更新</b>:改 {@code sys_config} 后缓存过期即生效(无需重启);</li>
 *   <li><b>拦截器真的拦</b>:关闭时 {@code preHandle} 抛明确业务错误;启用时放行;</li>
 *   <li><b>受管端点</b>:恰好 15 个,且 <b>包含</b>用户实机报过"关闭后仍能查"的
 *       {@code /smartor/patinhosp/selectPatMedInhospList}。</li>
 * </ol>
 */
public final class InhospVisitSwitchCheck {
 
    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 InhospVisitSwitchCheck() {
    }
 
    public static void main(String[] args) {
        System.out.println("=== 1) 参数表优先 / yml 兜底 ===");
        InhospVisitProperties ymlOn = new InhospVisitProperties();   // 默认 true
 
        check("sys_config 缺失 + yml 默认 -> 启用(fail-open)", "true",
                String.valueOf(new InhospVisitSwitch(ymlOn, SYS_MISSING).isEnabled()));
        check("sys_config=false 覆盖 yml 默认 -> 关闭", "false",
                String.valueOf(new InhospVisitSwitch(ymlOn, SYS_OFF).isEnabled()));
 
        InhospVisitProperties ymlOff = new InhospVisitProperties();
        ymlOff.setEnabled(false);
        check("sys_config=true 覆盖 yml=false -> 启用(参数表优先)", "true",
                String.valueOf(new InhospVisitSwitch(ymlOff, SYS_ON).isEnabled()));
        check("sys_config 缺失 + yml=false -> 关闭(兜底生效)", "false",
                String.valueOf(new InhospVisitSwitch(ymlOff, SYS_MISSING).isEnabled()));
 
        System.out.println();
        System.out.println("=== 2) 非 true/false 的脏值按「不是 false 即启用」处理 ===");
        check("值 = \"0\" -> 启用(不因脏值误关模块)", "true",
                String.valueOf(new InhospVisitSwitch(ymlOn, "0").isEnabled()));
        check("值 = \"FALSE\"(大写)-> 关闭", "false",
                String.valueOf(new InhospVisitSwitch(ymlOn, "FALSE").isEnabled()));
 
        System.out.println();
        System.out.println("=== 3) 热更新(10 秒缓存 + invalidateCache)===");
        final String[] value = {SYS_OFF};
        InhospVisitSwitch hot = hotSwitch(ymlOn, value);
        check("改之前(参数表=false)", "false", String.valueOf(hot.isEnabled()));
        value[0] = SYS_ON;
        check("缓存未过期时仍返回旧值(进程内 10 秒缓存)", "false", String.valueOf(hot.isEnabled()));
        hot.invalidateCache();
        check("缓存失效后读到新值(改成 true 立即生效)", "true", String.valueOf(hot.isEnabled()));
 
        System.out.println();
        System.out.println("=== 4) 拦截器:关闭时明确报错,启用时放行 ===");
        InhospVisitAvailabilityInterceptor offInterceptor =
                new InhospVisitAvailabilityInterceptor(new InhospVisitSwitch(ymlOn, SYS_OFF));
        InhospVisitAvailabilityInterceptor onInterceptor =
                new InhospVisitAvailabilityInterceptor(new InhospVisitSwitch(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("关闭时错误提示明确(含 inhospvisit.enabled=false 与「接口不可用」)", "true",
                String.valueOf(thrown.contains("inhospvisit.enabled=false") && thrown.contains("接口不可用")));
 
        System.out.println();
        System.out.println("=== 5) 受管范围:前缀通配(新加接口自动生效)===");
        List<String> guarded = InhospVisitWebConfig.guardedPathPatterns();
        check("受管模式只有 1 条(前缀通配,不再逐个枚举)", "1", String.valueOf(guarded.size()));
        check("受管模式 = /smartor/patinhosp/**", "true",
                String.valueOf(guarded.contains("/smartor/patinhosp/**")));
        // 前缀通配的语义:本模块前缀下**所有**端点都受管 —— 包括用户实机报过"关了还能查"的那个
        String prefix = InhospVisitWebConfig.MODULE_URL_PREFIX;
        for (String ep : new String[]{
                "/smartor/patinhosp/selectPatMedInhospList",
                "/smartor/patinhosp/selectPatMedInhospListCount",
                "/smartor/patinhosp/export",
                "/smartor/patinhosp/add",
                "/smartor/patinhosp/dealOutHospInfo",
                "/smartor/patinhosp/addOperationSubTask"}) {
            check("被前缀覆盖:" + ep, "true", String.valueOf(ep.startsWith(prefix + "/")));
        }
        // ⚠️ 通配的代价必须被显式承认:它不再能"静态枚举出管了哪些端点"。
        //    真实端点清单由 sql/_probe/verify_inhospvisit_switch.py 从控制器源码实测核对。
        check("前缀常量与通配模式一致(防止两者漂移)", "true",
                String.valueOf((prefix + "/**").equals(guarded.get(0))));
 
        System.out.println();
        System.out.println("=== 6) 门面:isEnabled() 自身不带守卫(否则问不出开关状态)===");
        // ⚠️ InhospVisitFacade 没有 2 参构造器:测试构造是
        //    (properties, InhospVisitApi delegate, String sysConfigValue)。
        //    delegate 传 null 表示"模块没被打进应用",此时 isEnabled() 为 false
        //    (它判的是"开关打开 **且** 内部实现在位")—— 这点与模板门面不同,故分别断言。
        InhospVisitFacade facadeOff = new InhospVisitFacade(ymlOn, null, SYS_OFF);
        check("关闭 + 无内部实现时 isEnabled() = false", "false",
                String.valueOf(facadeOff.isEnabled()));
        InhospVisitFacade facadeOnNoImpl = new InhospVisitFacade(ymlOn, null, SYS_ON);
        check("开启但无内部实现时 isEnabled() = false(判的是开关+实现在位)", "false",
                String.valueOf(facadeOnNoImpl.isEnabled()));
 
        System.out.println();
        if (failed == 0) {
            System.out.println("===== 出入院随访开关校验全部通过 =====");
        } else {
            System.out.println("===== 出入院随访开关校验失败 " + failed + " 项 =====");
            System.exit(1);
        }
    }
 
    /**
     * 造一个「{@code sys_config} 的值可以被中途改掉」的开关,用于验证缓存与热更新。
     */
    private static InhospVisitSwitch hotSwitch(InhospVisitProperties properties, final String[] configValue) {
        return new InhospVisitSwitch(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);
    }
}