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}(运行时热读),因此这里验证三件事: *

    *
  1. 参数表优先:{@code sys_config} 有值时以它为准,同一份 yml 配置下可开可关;
  2. *
  3. yml 兜底:{@code sys_config} 没有这一行时回落 {@code satisfaction.enabled}(默认 true);
  4. *
  5. 关闭时不抛异常:门面返回安全空值,且满意度端点拿到的是「明确错误」而不是 404。
  6. *
*/ 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); } }