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