package com.ruoyi.satisfaction.api; import com.ruoyi.common.core.service.ISatisfactionCategoryService; import com.ruoyi.satisfaction.config.SatisfactionProperties; import com.ruoyi.satisfaction.config.SatisfactionSwitch; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 满意度模块对外门面(常驻 Bean)。 *
* 这是其它模块访问满意度能力的唯一入口。 *
* 与 {@link SatisfactionApi} 实现类的关系: *
* @Autowired
* private SatisfactionFacade satisfactionFacade;
*
* if (satisfactionFacade.isEnabled()) {
* Map<String, Object> categories = satisfactionFacade.getSatisfactionCategories(orgid);
* }
*
*/
@Slf4j
@Component
public class SatisfactionFacade implements SatisfactionApi, ISatisfactionCategoryService {
private final SatisfactionProperties properties;
/**
* 运行时开关({@code sys_config} 优先,{@link SatisfactionProperties} 兜底)
*/
private final SatisfactionSwitch satisfactionSwitch;
/**
* 内部实现;模块关闭时为 {@code null}。
*
* 用 {@code ObjectProvider} 惰性获取:即使实现 Bean 不存在也不会导致注入失败。
*/
private final ObjectProvider
* ⚠️ 必须显式标注 {@code @Autowired}:本类有两个公开构造器
* (另一个是给测试/非 Spring 环境用的),若不标注,Spring 会因为"构造器不唯一"
* 而退回查找无参构造器,从而在启动时报
* {@code No default constructor found}。已由
* {@code sql/_probe/verify_spring_beans.py} 静态把关。
*/
@Autowired
public SatisfactionFacade(SatisfactionProperties properties,
SatisfactionSwitch satisfactionSwitch,
ObjectProvider
* 开关取自 {@code sys_config} 的模拟键值(见 {@link SatisfactionSwitch#SatisfactionSwitch}),
* 从而保证"开关校验"脚本无需 Spring/数据库即可运行。
*/
public SatisfactionFacade(SatisfactionProperties properties, SatisfactionApi delegate, String sysConfigValue) {
this(properties, new SatisfactionSwitch(properties, sysConfigValue), new FixedProvider(delegate));
}
/**
* 取运行时开关(供拦截器复用同一个实例、也供测试断言)
*/
public SatisfactionSwitch getSatisfactionSwitch() {
return satisfactionSwitch;
}
/**
* 取内部实现;模块关闭、实现缺失、或只找到自己时返回 {@code null}。
*
* ⚠️ 必须排除"自己":本类自身也实现了 {@link SatisfactionApi},而 Spring 在
* 按类型查找候选 Bean 时,若第一轮没找到别的实现(例如 ruoyi-satisfaction
* 没有被打进应用,或将来又有人用条件装配把它摘掉),会把自引用当作兜底候选返回。
* 那样 {@code delegateOrNull()} 会拿到 {@code this},随后
* {@code getSatisfactionCategories()} 里 {@code delegate.getXxx()} 就变成无限递归
* (StackOverflowError)。这里显式过滤掉自己,把"没有实现"如实视为不可用。
*
* ⚠️ 本方法不能替代开关判断:开关({@code sys_config: satisfaction.enabled})
* 关闭时 {@link DefaultSatisfactionApi} 依然存在,所以调用点一律先判 {@link #isEnabled()}。
*
* 用 {@code stream()} 而不是 {@code getIfAvailable()}:后者在候选多于一个时抛
* {@code NoUniqueBeanDefinitionException}(它继承自 {@code NoSuchBeanDefinitionException}),
* 最终会被吞成 null,掩盖"有人又加了一个实现"这种配置问题。
*/
private SatisfactionApi delegateOrNull() {
if (apiProvider == null) {
return null;
}
try {
return apiProvider.stream()
.filter(api -> api != this)
.findFirst()
.orElse(null);
} catch (Exception e) {
// 容器状态异常时按"未启用"处理,绝不让调用方(登录、随访导出)因为满意度而失败
log.warn("【满意度】获取内部实现失败,按未启用处理:{}", e.toString());
return null;
}
}
@Override
public boolean isEnabled() {
return satisfactionSwitch.isEnabled() && delegateOrNull() != null;
}
@Override
public Map