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);
|
}
|
}
|