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 运行)。
*
* 开关已从 {@code application.yml} 挪到 {@code sys_config} 参数表的
* {@code satisfaction.enabled}(运行时热读),因此这里验证三件事:
*
* - 参数表优先:{@code sys_config} 有值时以它为准,同一份 yml 配置下可开可关;
* - yml 兜底:{@code sys_config} 没有这一行时回落 {@code satisfaction.enabled}(默认 true);
* - 关闭时不抛异常:门面返回安全空值,且满意度端点拿到的是「明确错误」而不是 404。
*
*/
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 getSatisfactionCategories(String orgid) {
return Collections.singletonMap("voiceCategories", Collections.singletonList(orgid));
}
@Override
public SatisfactionExportBundle getQuestionOptionRatioExport(List subtaskIds, String questiontext) {
return new SatisfactionExportBundle(
Collections.singletonList(new ServiceSubtaskDetailRatioExport()),
ServiceSubtaskDetailRatioExport.class, workbook -> { });
}
@Override
public boolean hasSatisfactionCategory(String orgid) {
return true;
}
@Override
public Map 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 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 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() {
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 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);
}
}