package com.ruoyi.satisfaction.api;
|
|
import com.ruoyi.common.core.service.ISatisfactionCategoryService;
|
import com.ruoyi.satisfaction.config.SatisfactionAvailabilityInterceptor;
|
import com.ruoyi.satisfaction.config.SatisfactionProperties;
|
import com.ruoyi.satisfaction.config.SatisfactionSwitch;
|
import com.ruoyi.satisfaction.config.SatisfactionWebConfig;
|
import com.ruoyi.satisfaction.domain.ServiceSubtaskDetailRatioExport;
|
|
import java.util.Collections;
|
import java.util.List;
|
import java.util.Map;
|
|
/**
|
* 满意度开关与门面行为校验(不依赖 JUnit,可直接用模块 classpath 运行)。
|
* <p>
|
* 开关已从 {@code application.yml} 挪到 {@code sys_config} 参数表的
|
* {@code satisfaction.enabled}(运行时热读),因此这里验证三件事:
|
* <ol>
|
* <li><b>参数表优先</b>:{@code sys_config} 有值时以它为准,同一份 yml 配置下可开可关;</li>
|
* <li><b>yml 兜底</b>:{@code sys_config} 没有这一行时回落 {@code satisfaction.enabled}(默认 true);</li>
|
* <li><b>关闭时不抛异常</b>:门面返回安全空值,且满意度端点拿到的是「明确错误」而不是 404。</li>
|
* </ol>
|
*/
|
public final class SatisfactionSwitchCheck {
|
|
private static int failed = 0;
|
|
/** sys_config 里的「已开启」模拟值 */
|
private static final String SYS_ON = "true";
|
|
/** sys_config 里的「已关闭」模拟值 */
|
private static final String SYS_OFF = "false";
|
|
/** sys_config 里「没有这一行」 */
|
private static final String SYS_MISSING = null;
|
|
private SatisfactionSwitchCheck() {
|
}
|
|
/** 假实现:用于验证"启用"路径 */
|
static class StubApi implements SatisfactionApi {
|
@Override
|
public boolean isEnabled() {
|
return true;
|
}
|
|
@Override
|
public Map<String, Object> getSatisfactionCategories(String orgid) {
|
return Collections.singletonMap("voiceCategories", Collections.singletonList(orgid));
|
}
|
|
@Override
|
public SatisfactionExportBundle getQuestionOptionRatioExport(List<Long> subtaskIds, String questiontext) {
|
return new SatisfactionExportBundle(
|
Collections.singletonList(new ServiceSubtaskDetailRatioExport()),
|
ServiceSubtaskDetailRatioExport.class, workbook -> { });
|
}
|
|
@Override
|
public boolean hasSatisfactionCategory(String orgid) {
|
return true;
|
}
|
|
@Override
|
public Map<String, Object> getSatisfactionIndex(String type, String code, String orgid,
|
Long ivrCategoryid, Long svyCategoryid) {
|
return SatisfactionFacade.emptySatisfactionIndex();
|
}
|
}
|
|
public static void main(String[] args) throws Exception {
|
// ---------------- 启用:yml 兜底 ----------------
|
SatisfactionProperties on = new SatisfactionProperties(); // 默认 enabled=true
|
SatisfactionFacade onFacade = new SatisfactionFacade(on, new StubApi(), SYS_MISSING);
|
|
check("启用时 isEnabled()", "true", String.valueOf(onFacade.isEnabled()));
|
check("启用时分类查询有结果", "true",
|
String.valueOf(!onFacade.getSatisfactionCategories("ORG1").isEmpty()));
|
SatisfactionExportBundle onBundle = onFacade.getQuestionOptionRatioExport(Collections.singletonList(1L), null);
|
check("启用时占比导出包有行", "true", String.valueOf(!onBundle.isEmpty()));
|
check("启用时占比导出包带行类型与图表构建器", "true",
|
String.valueOf(onBundle.getRowClass() != null && onBundle.getChartBuilder() != null));
|
check("启用时 hasSatisfactionCategory", "true", String.valueOf(onFacade.hasSatisfactionCategory("ORG1")));
|
|
// ---------------- 启用:yml 为 false,但参数表说 true(参数表优先) ----------------
|
SatisfactionProperties ymlOff = new SatisfactionProperties();
|
ymlOff.setEnabled(false);
|
SatisfactionFacade sysOnFacade = new SatisfactionFacade(ymlOff, new StubApi(), SYS_ON);
|
check("参数表=true 覆盖 yml=false 时 isEnabled()", "true", String.valueOf(sysOnFacade.isEnabled()));
|
check("参数表=true 覆盖 yml=false 时分类查询有结果", "true",
|
String.valueOf(!sysOnFacade.getSatisfactionCategories("ORG1").isEmpty()));
|
|
// ---------------- 关闭:参数表 false,实现 Bean 依然存在 ----------------
|
SatisfactionFacade offFacade = new SatisfactionFacade(on, new StubApi(), SYS_OFF);
|
|
check("关闭时 isEnabled()", "false", String.valueOf(offFacade.isEnabled()));
|
check("关闭时分类查询返回空 Map(即使实现还在也不转发)", "true",
|
String.valueOf(offFacade.getSatisfactionCategories("ORG1").isEmpty()));
|
SatisfactionExportBundle offBundle = offFacade.getQuestionOptionRatioExport(Collections.singletonList(1L), null);
|
check("关闭时占比导出包为空", "true", String.valueOf(offBundle.isEmpty()));
|
check("关闭时占比导出包不带图表构建器(导出照常完成)", "true",
|
String.valueOf(offBundle.getChartBuilder() == null));
|
check("关闭时 hasSatisfactionCategory", "false", String.valueOf(offFacade.hasSatisfactionCategory("ORG1")));
|
|
// ---------------- 关闭:参数表缺失 + yml false(兜底生效) ----------------
|
SatisfactionFacade ymlOnlyOff = new SatisfactionFacade(ymlOff, new StubApi(), SYS_MISSING);
|
check("参数表缺失、yml=false 时 isEnabled()", "false", String.valueOf(ymlOnlyOff.isEnabled()));
|
|
// ---------------- 关闭:参数表缺失 + 无实现 Bean(模块根本不在依赖里) ----------------
|
SatisfactionFacade noImplFacade = new SatisfactionFacade(on, (SatisfactionApi) null, SYS_MISSING);
|
check("参数表缺失、无实现 Bean 时 isEnabled()", "false", String.valueOf(noImplFacade.isEnabled()));
|
|
// 关闭时反复调用不应抛异常
|
boolean noException = true;
|
try {
|
for (int i = 0; i < 3; i++) {
|
offFacade.getSatisfactionCategories(null);
|
offFacade.getQuestionOptionRatioExport(null, null);
|
offFacade.hasSatisfactionCategory(null);
|
}
|
} catch (Exception e) {
|
noException = false;
|
System.out.println(" 异常: " + e);
|
}
|
check("关闭时反复调用不抛异常", "true", String.valueOf(noException));
|
|
// ---------------- 登录链路的接口暴露(第 9 步) ----------------
|
// ruoyi-system 通过 ruoyi-common 的 ISatisfactionCategoryService 按类型查找实现,
|
// 因此门面必须实现该接口,才能"关闭时返回空 Map 而不影响登录"。
|
check("门面实现 ISatisfactionCategoryService(登录链路按接口查找)", "true",
|
String.valueOf(onFacade instanceof ISatisfactionCategoryService));
|
check("按接口调用(启用)返回非空", "true",
|
String.valueOf(!((ISatisfactionCategoryService) onFacade).getSatisfactionCategories("ORG1").isEmpty()));
|
check("按接口调用(关闭)返回空 Map 且不抛异常", "true",
|
String.valueOf(((ISatisfactionCategoryService) offFacade).getSatisfactionCategories("ORG1").isEmpty()));
|
|
// ---------------- 满意度指数(关闭时必须是可用的全零 Map,不能让调用方 NPE) ----------------
|
Map<String, Object> offIndex = offFacade.getSatisfactionIndex("1", "D001", "ORG1", null, null);
|
check("关闭时满意度指数返回全零 mydtmzl", "0", String.valueOf(offIndex.get("mydtmzl")));
|
check("关闭时满意度指数返回全零 mydtbl", "0", String.valueOf(offIndex.get("mydtbl")));
|
check("关闭时满意度指数返回全零 wcbl", "0.0", String.valueOf(offIndex.get("wcbl")));
|
check("关闭时满意度指数键齐全(不会让调用方 NPE)", "true",
|
String.valueOf(offIndex.containsKey("mydtmzl") && offIndex.containsKey("mydtbl")
|
&& offIndex.containsKey("wcbl")));
|
|
// ---------------- 开关热更新:改 sys_config 后自动生效(10 秒缓存过期) ----------------
|
SatisfactionProperties hot = new SatisfactionProperties();
|
final String[] configValue = { SYS_OFF };
|
SatisfactionSwitch hotSwitch = hotSwitch(hot, configValue);
|
check("热更新前(参数表=false)", "false", String.valueOf(hotSwitch.isEnabled()));
|
configValue[0] = SYS_ON;
|
check("缓存未过期时仍返回旧值(进程内 10 秒缓存)", "false", String.valueOf(hotSwitch.isEnabled()));
|
hotSwitch.invalidateCache();
|
check("缓存失效后读到新值(改成 true 立即生效)", "true", String.valueOf(hotSwitch.isEnabled()));
|
|
// ---------------- 关闭时调用满意度端点必须拿到「明确错误」而不是 404 ----------------
|
SatisfactionAvailabilityInterceptor offInterceptor =
|
new SatisfactionAvailabilityInterceptor(offFacade.getSatisfactionSwitch());
|
SatisfactionAvailabilityInterceptor onInterceptor =
|
new SatisfactionAvailabilityInterceptor(onFacade.getSatisfactionSwitch());
|
|
check("启用时拦截器放行", "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("关闭时拦截器抛 ServiceException", "true",
|
String.valueOf(thrown.startsWith("com.ruoyi.common.exception.ServiceException|")));
|
check("关闭时错误提示明确(含 satisfaction.enabled=false 与接口不可用)", "true",
|
String.valueOf(thrown.contains("satisfaction.enabled=false") && thrown.contains("接口不可用")));
|
|
// 被拦截的端点必须是本模块的 17 个,且不能碰到随访/其它业务的路径
|
List<String> guarded = SatisfactionWebConfig.guardedPathPatterns();
|
check("受开关保护的路径数量 = 1 个通配 + 5 个同德 + 1 个异常配置题 = 7 条规则", "7",
|
String.valueOf(guarded.size()));
|
check("受保护路径不含随访导出等其它业务端点", "false",
|
String.valueOf(guarded.contains("/smartor/serviceSubtask/patItemExport")
|
|| guarded.contains("/smartor/serviceSubtask/getSfStatisticsJoy")
|
|| guarded.contains("/smartor/subtaskAnswer/saveMYDQuestionAnswer")));
|
|
System.out.println();
|
if (failed == 0) {
|
System.out.println("===== 满意度开关与门面校验全部通过 =====");
|
} else {
|
System.out.println("===== 满意度开关与门面校验失败 " + failed + " 项 =====");
|
System.exit(1);
|
}
|
}
|
|
/**
|
* 造一个「{@code sys_config} 的值可以被中途改掉」的开关,用于验证缓存与热更新。
|
*/
|
private static SatisfactionSwitch hotSwitch(SatisfactionProperties properties, final String[] configValue) {
|
return new SatisfactionSwitch(properties, new org.springframework.beans.factory.ObjectProvider<com.ruoyi.common.core.service.IConfigService>() {
|
private final com.ruoyi.common.core.service.IConfigService service =
|
configKey -> configValue[0];
|
|
@Override
|
public com.ruoyi.common.core.service.IConfigService getObject(Object... args) {
|
return service;
|
}
|
|
@Override
|
public com.ruoyi.common.core.service.IConfigService getIfAvailable() {
|
return service;
|
}
|
|
@Override
|
public com.ruoyi.common.core.service.IConfigService getIfUnique() {
|
return service;
|
}
|
|
@Override
|
public com.ruoyi.common.core.service.IConfigService getObject() {
|
return service;
|
}
|
|
@Override
|
public java.util.stream.Stream<com.ruoyi.common.core.service.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);
|
}
|
}
|