package com.ruoyi.satisfaction.api;
import com.ruoyi.common.core.service.IConfigService;
import com.ruoyi.common.exception.ServiceException;
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.service.strategy.CommonSatisfactionScoreStrategy;
import com.ruoyi.satisfaction.service.strategy.SatisfactionScoreStrategyResolver;
import com.ruoyi.satisfaction.service.strategy.SatisfactionSites;
import com.ruoyi.satisfaction.service.strategy.SltdSatisfactionScoreStrategy;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* 满意度模块的 真实 Spring 容器装配校验(不依赖 JUnit,也不依赖数据库)。
*
* 为什么需要这个检查:本模块的几个"静态校验"(Mapper 一致性、逐行等价、策略等价)都只能证明
* 代码内容正确,证明不了 Spring 能把 Bean 装起来。实际踩过两个坑:
*
* - {@link SatisfactionFacade} 有两个公开构造器,Spring 因"构造器不唯一"退回找无参构造器
* → 启动期 {@code No default constructor found};
* - {@code SatisfactionApi} 的按类型查找会把门面自己当兜底候选
* → 门面把请求转发给自己 → {@code StackOverflowError}。
*
* 所以这里直接起一个真实的 {@link AnnotationConfigApplicationContext},只注册本模块
* 不依赖 Mapper / 数据库的那些 Bean,验证装配与开关行为。
*
* 开关本身存在 {@code sys_config} 参数表里(键 {@code satisfaction.enabled},运行时热读),
* 因此这里用一个假的 {@link IConfigService} 模拟参数表,验证「参数表优先、yml 兜底」。
*/
public final class SatisfactionContextCheck {
private static int failed = 0;
private SatisfactionContextCheck() {
}
/**
* 假的内部实现(真实场景里是 DefaultSatisfactionApi,它需要 Mapper/Redis,起不了轻量容器)
*/
public static class StubApi implements SatisfactionApi {
/**
* 分类查询被调用的次数:用来证明"参数表关闭时门面确实没有转发给实现"
*/
static final java.util.concurrent.atomic.AtomicInteger CALLS = new java.util.concurrent.atomic.AtomicInteger();
@Override
public boolean isEnabled() {
return true;
}
@Override
public Map getSatisfactionCategories(String orgid) {
CALLS.incrementAndGet();
return Collections.singletonMap("fromStub", orgid);
}
@Override
public SatisfactionExportBundle getQuestionOptionRatioExport(List subtaskIds, String questiontext) {
return SatisfactionExportBundle.empty();
}
@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();
}
}
/**
* 探针:以"旁观者"身份注入 {@code ObjectProvider},看容器按类型能找到谁。
*
* 它不是 {@code SatisfactionApi} 的实现,所以不存在"自引用被排除"的情况,
* 因此它看到的就是完整的候选集合 —— 用来证明"模块关闭时容器会把门面自己当候选"。
*/
public static class ProviderProbe {
@org.springframework.beans.factory.annotation.Autowired
private org.springframework.beans.factory.ObjectProvider provider;
SatisfactionApi rawCandidate() {
return provider.getIfAvailable();
}
}
public static void main(String[] args) {
System.out.println("=== 场景 1:参数表 satisfaction.enabled=true,且存在内部实现(DefaultSatisfactionApi 的角色)===");
try (AnnotationConfigApplicationContext ctx = context(true, true, ContextCheckConfig.SYS_ON)) {
SatisfactionFacade facade = ctx.getBean(SatisfactionFacade.class);
check("门面能被 Spring 实例化(构造器不再歧义)", "true", String.valueOf(facade != null));
check("启用时 isEnabled()", "true", String.valueOf(facade.isEnabled()));
check("取到的是【内部实现】而不是门面自己(stub 把 orgid 原样回填)", "ORG1",
String.valueOf(facade.getSatisfactionCategories("ORG1").get("fromStub")));
check("门面同时是 ISatisfactionCategoryService",
"true", String.valueOf(ctx.getBean(com.ruoyi.common.core.service.ISatisfactionCategoryService.class) == facade));
// 容器里应有 2 个 SatisfactionApi:门面自己 + 内部实现(门面也实现该接口,这是设计使然)
check("容器内 SatisfactionApi 实现数 = 门面 1 + 内部实现 1", "2",
String.valueOf(ctx.getBeansOfType(SatisfactionApi.class).size()));
check("开关读取的键名是 sys_config 的 satisfaction.enabled", SatisfactionSwitch.CONFIG_KEY,
String.valueOf(SatisfactionSwitch.CONFIG_KEY));
SatisfactionScoreStrategyResolver resolver = ctx.getBean(SatisfactionScoreStrategyResolver.class);
check("站点策略解析器装配成功,支持站点 = [common, sltd]", "[common, sltd]",
String.valueOf(resolver.supportedSites()));
check("未知站点回落到通用口径", SatisfactionSites.COMMON,
resolver.resolve("不存在的站点").site());
}
System.out.println();
System.out.println("=== 场景 2:参数表 satisfaction.enabled=true,但【没有】内部实现(模块没被打进应用)===");
try (AnnotationConfigApplicationContext ctx = context(true, false, ContextCheckConfig.SYS_ON)) {
SatisfactionFacade facade = ctx.getBean(SatisfactionFacade.class);
check("无实现时 isEnabled() 为 false", "false", String.valueOf(facade.isEnabled()));
// 关键:不能把门面自己当实现,否则这里会 StackOverflowError
String result;
try {
result = String.valueOf(facade.getSatisfactionCategories("ORG1").isEmpty());
} catch (StackOverflowError e) {
result = "StackOverflowError";
}
check("无实现时分类查询返回空 Map(不会自引用递归)", "true", result);
check("无实现时占比导出包为空", "true",
String.valueOf(facade.getQuestionOptionRatioExport(Collections.singletonList(1L), null).isEmpty()));
check("无实现时满意度指数为全零 Map", "0",
String.valueOf(facade.getSatisfactionIndex("1", "D001", "ORG1", null, null).get("mydtmzl")));
// 反证:直接问容器"按类型有哪些 SatisfactionApi 候选",看它是否把门面自己算进去。
// 若这里返回的是门面自己,就说明"自引用兜底"确实存在,门面里的 self 过滤是必要的。
ProviderProbe probe = ctx.getBean(ProviderProbe.class);
check("无实现时,容器按类型只找到门面自己(证明自引用兜底存在)", "true",
String.valueOf(probe.rawCandidate() == facade));
check("但门面不会把'自己'当成实现(self 过滤生效)", "false",
String.valueOf(facade.isEnabled()));
}
System.out.println();
System.out.println("=== 场景 3:参数表 satisfaction.enabled=false(实现 Bean 依然存在,但门面必须不转发)===");
StubApi.CALLS.set(0);
try (AnnotationConfigApplicationContext ctx = context(true, true, ContextCheckConfig.SYS_OFF)) {
SatisfactionFacade facade = ctx.getBean(SatisfactionFacade.class);
check("关闭时 isEnabled() 为 false", "false", String.valueOf(facade.isEnabled()));
String result;
try {
result = String.valueOf(facade.getSatisfactionCategories("ORG1").isEmpty());
} catch (StackOverflowError e) {
result = "StackOverflowError";
}
check("关闭时分类查询返回空 Map(不会自递归)", "true", result);
check("关闭时不再调用内部实现(stub 调用次数 = 0)", "0",
String.valueOf(StubApi.CALLS.get()));
SatisfactionAvailabilityInterceptor interceptor = ctx.getBean(SatisfactionAvailabilityInterceptor.class);
String thrown = "(未抛异常)";
try {
interceptor.preHandle(null, null, null);
} catch (Exception e) {
thrown = e.getClass().getSimpleName() + "|" + e.getMessage();
}
check("关闭时拦截器抛 ServiceException", "true", String.valueOf(thrown.startsWith("ServiceException|")));
check("关闭时错误提示明确", "true",
String.valueOf(thrown.contains("satisfaction.enabled=false") && thrown.contains("接口不可用")));
check("关闭时受保护路径规则 7 条", "7",
String.valueOf(SatisfactionWebConfig.guardedPathPatterns().size()));
}
System.out.println();
System.out.println("=== 场景 4:参数表【没有】satisfaction.enabled 这一行时,回落 application.yml(默认 true)===");
try (AnnotationConfigApplicationContext ctx = context(true, true, ContextCheckConfig.SYS_MISSING)) {
SatisfactionFacade facade = ctx.getBean(SatisfactionFacade.class);
check("参数行缺失、yml 默认 true 时 isEnabled() 为 true", "true", String.valueOf(facade.isEnabled()));
}
try (AnnotationConfigApplicationContext ctx = context(false, true, ContextCheckConfig.SYS_MISSING)) {
SatisfactionFacade facade = ctx.getBean(SatisfactionFacade.class);
check("参数行缺失、yml=false 时 isEnabled() 为 false", "false", String.valueOf(facade.isEnabled()));
}
System.out.println();
System.out.println("=== 场景 5:容器里【没有】IConfigService 时(独立/测试环境)必须能启动且按 yml 兜底 ===");
try (AnnotationConfigApplicationContext ctx = context(true, true, null, false)) {
SatisfactionFacade facade = ctx.getBean(SatisfactionFacade.class);
check("无配置服务时门面仍能装配并启动", "true", String.valueOf(facade != null));
check("无配置服务时按 yml 兜底(true)", "true", String.valueOf(facade.isEnabled()));
}
System.out.println();
if (failed == 0) {
System.out.println("===== 满意度模块 Spring 装配校验全部通过 =====");
} else {
System.out.println("===== 满意度模块 Spring 装配校验失败 " + failed + " 项 =====");
System.exit(1);
}
}
/**
* 参数表的模拟取值
*/
static final class ContextCheckConfig {
/** 参数行存在且为 true */
static final String SYS_ON = "true";
/** 参数行存在且为 false */
static final String SYS_OFF = "false";
/** 参数行不存在 */
static final String SYS_MISSING = null;
private ContextCheckConfig() {
}
}
/**
* 起一个只含本模块"无 Mapper 依赖"Bean 的真实容器
*
* @param ymlEnabled application.yml 里 satisfaction.enabled 的取值(兜底用)
* @param withDelegate 是否注册内部实现(DefaultSatisfactionApi 的角色)
* @param sysConfigValue 模拟 sys_config 里 satisfaction.enabled 的键值,null=没有这一行
*/
private static AnnotationConfigApplicationContext context(boolean ymlEnabled, boolean withDelegate,
final String sysConfigValue) {
return context(ymlEnabled, withDelegate, sysConfigValue, true);
}
/**
* @param withConfigService 容器里是否注册 {@link IConfigService}(false 模拟独立/测试环境)
*/
private static AnnotationConfigApplicationContext context(boolean ymlEnabled, boolean withDelegate,
final String sysConfigValue, boolean withConfigService) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
SatisfactionProperties properties = new SatisfactionProperties();
properties.setEnabled(ymlEnabled);
ctx.registerBean(SatisfactionProperties.class, () -> properties);
ctx.register(SatisfactionFacade.class,
SatisfactionSwitch.class,
SatisfactionAvailabilityInterceptor.class,
SatisfactionWebConfig.class,
SatisfactionScoreStrategyResolver.class,
CommonSatisfactionScoreStrategy.class,
SltdSatisfactionScoreStrategy.class,
ProviderProbe.class);
if (withConfigService) {
// 假的参数配置服务:只认 satisfaction.enabled 这一个键,值由 sysConfigValue 决定
ctx.registerBean("stubConfigService", IConfigService.class, () -> (IConfigService) configKey ->
SatisfactionSwitch.CONFIG_KEY.equals(configKey) ? sysConfigValue : "");
}
if (withDelegate) {
ctx.registerBean("stubSatisfactionApi", SatisfactionApi.class, StubApi::new);
}
ctx.refresh();
return ctx;
}
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);
}
}