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 apiProvider; /** * Spring 使用的构造器。 *

* ⚠️ 必须显式标注 {@code @Autowired}:本类有两个公开构造器 * (另一个是给测试/非 Spring 环境用的),若不标注,Spring 会因为"构造器不唯一" * 而退回查找无参构造器,从而在启动时报 * {@code No default constructor found}。已由 * {@code sql/_probe/verify_spring_beans.py} 静态把关。 */ @Autowired public SatisfactionFacade(SatisfactionProperties properties, SatisfactionSwitch satisfactionSwitch, ObjectProvider apiProvider) { this.properties = properties; this.satisfactionSwitch = satisfactionSwitch; this.apiProvider = apiProvider; } /** * 供测试/非 Spring 环境直接构造(直接给出内部实现,可为 null)。 *

* 开关取自 {@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 getSatisfactionCategories(String orgid) { if (!isEnabled()) { log.warn("【满意度】模块未启用(参数配置 satisfaction.enabled=false),getSatisfactionCategories 返回空结果。orgid={}", orgid); return Collections.emptyMap(); } return delegateOrNull().getSatisfactionCategories(orgid); } @Override public SatisfactionExportBundle getQuestionOptionRatioExport(List subtaskIds, String questiontext) { if (!isEnabled()) { log.warn("【满意度】模块未启用(参数配置 satisfaction.enabled=false),getQuestionOptionRatioExport 返回空导出包。subtaskIds 数量={}", subtaskIds == null ? 0 : subtaskIds.size()); return SatisfactionExportBundle.empty(); } return delegateOrNull().getQuestionOptionRatioExport(subtaskIds, questiontext); } @Override public boolean hasSatisfactionCategory(String orgid) { if (!isEnabled()) { return false; } return delegateOrNull().hasSatisfactionCategory(orgid); } @Override public Map getSatisfactionIndex(String type, String code, String orgid, Long ivrCategoryid, Long svyCategoryid) { if (!isEnabled()) { log.warn("【满意度】模块未启用(参数配置 satisfaction.enabled=false),getSatisfactionIndex 返回全零结果。type={}, code={}", type, code); // 返回与"启用时"完全相同的全零 Map(被迁移实现因原缺陷也恒定返回全零), // 这样随访统计页在模块关闭前后的显示结果一致。 return emptySatisfactionIndex(); } return delegateOrNull().getSatisfactionIndex(type, code, orgid, ivrCategoryid, svyCategoryid); } /** * 全零的满意度指数(键名与类型与被迁移实现一致:{@code mydtmzl}(Integer)、 * {@code mydtbl}(Integer)、{@code wcbl}(Double)) */ public static Map emptySatisfactionIndex() { Map map = new HashMap<>(); map.put("mydtmzl", 0); map.put("mydtbl", 0); map.put("wcbl", 0.0); return map; } /** * 固定的 ObjectProvider 实现:仅用于非 Spring 环境/测试。 */ private static final class FixedProvider implements ObjectProvider { private final SatisfactionApi api; private FixedProvider(SatisfactionApi api) { this.api = api; } @Override public SatisfactionApi getObject(Object... args) { return api; } @Override public SatisfactionApi getIfAvailable() { return api; } @Override public SatisfactionApi getIfUnique() { return api; } @Override public SatisfactionApi getObject() { return api; } @Override public java.util.stream.Stream stream() { return api == null ? java.util.stream.Stream.empty() : java.util.stream.Stream.of(api); } } }