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;
|
|
/**
|
* 满意度模块对外门面(<b>常驻 Bean</b>)。
|
* <p>
|
* 这是其它模块访问满意度能力的<b>唯一入口</b>。
|
* <p>
|
* 与 {@link SatisfactionApi} 实现类的关系:
|
* <ul>
|
* <li>实现类 {@link DefaultSatisfactionApi} <b>常驻</b>(不再受启动期条件装配控制);</li>
|
* <li>开关存放在 {@code sys_config} 参数表的 {@code satisfaction.enabled},
|
* 由 {@link SatisfactionSwitch} <b>运行时</b>读取(10 秒进程内缓存),
|
* 改完参数设置无需重启;</li>
|
* <li>本门面<b>永远</b>是 Spring Bean,因此调用方(随访导出、登录链路)
|
* 的 {@code @Autowired} 在任何开关状态下都<b>不会启动失败</b>;</li>
|
* <li>关闭时各方法返回失败/空值并打 WARN,<b>不抛异常</b>,
|
* 保证调用方主流程(如导出、登录)照常完成。</li>
|
* </ul>
|
*
|
* <h3>使用示例</h3>
|
* <pre>
|
* @Autowired
|
* private SatisfactionFacade satisfactionFacade;
|
*
|
* if (satisfactionFacade.isEnabled()) {
|
* Map<String, Object> categories = satisfactionFacade.getSatisfactionCategories(orgid);
|
* }
|
* </pre>
|
*/
|
@Slf4j
|
@Component
|
public class SatisfactionFacade implements SatisfactionApi, ISatisfactionCategoryService {
|
|
private final SatisfactionProperties properties;
|
|
/**
|
* 运行时开关({@code sys_config} 优先,{@link SatisfactionProperties} 兜底)
|
*/
|
private final SatisfactionSwitch satisfactionSwitch;
|
|
/**
|
* 内部实现;模块关闭时为 {@code null}。
|
* <p>
|
* 用 {@code ObjectProvider} 惰性获取:即使实现 Bean 不存在也不会导致注入失败。
|
*/
|
private final ObjectProvider<SatisfactionApi> apiProvider;
|
|
/**
|
* Spring 使用的构造器。
|
* <p>
|
* ⚠️ <b>必须显式标注 {@code @Autowired}</b>:本类有两个公开构造器
|
* (另一个是给测试/非 Spring 环境用的),若不标注,Spring 会因为"构造器不唯一"
|
* 而退回查找无参构造器,从而在启动时报
|
* {@code No default constructor found}。已由
|
* {@code sql/_probe/verify_spring_beans.py} 静态把关。
|
*/
|
@Autowired
|
public SatisfactionFacade(SatisfactionProperties properties,
|
SatisfactionSwitch satisfactionSwitch,
|
ObjectProvider<SatisfactionApi> apiProvider) {
|
this.properties = properties;
|
this.satisfactionSwitch = satisfactionSwitch;
|
this.apiProvider = apiProvider;
|
}
|
|
/**
|
* 供<b>测试/非 Spring 环境</b>直接构造(直接给出内部实现,可为 null)。
|
* <p>
|
* 开关取自 {@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}。
|
* <p>
|
* ⚠️ <b>必须排除"自己"</b>:本类自身也实现了 {@link SatisfactionApi},而 Spring 在
|
* 按类型查找候选 Bean 时,若第一轮没找到别的实现(例如 ruoyi-satisfaction
|
* 没有被打进应用,或将来又有人用条件装配把它摘掉),会<b>把自引用当作兜底候选</b>返回。
|
* 那样 {@code delegateOrNull()} 会拿到 {@code this},随后
|
* {@code getSatisfactionCategories()} 里 {@code delegate.getXxx()} 就变成无限递归
|
* (StackOverflowError)。这里显式过滤掉自己,把"没有实现"如实视为不可用。
|
* <p>
|
* ⚠️ <b>本方法不能替代开关判断</b>:开关({@code sys_config: satisfaction.enabled})
|
* 关闭时 {@link DefaultSatisfactionApi} 依然存在,所以调用点一律先判 {@link #isEnabled()}。
|
* <p>
|
* 用 {@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<String, Object> 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<Long> 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<String, Object> 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<String, Object> emptySatisfactionIndex() {
|
Map<String, Object> 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<SatisfactionApi> {
|
|
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<SatisfactionApi> stream() {
|
return api == null ? java.util.stream.Stream.empty() : java.util.stream.Stream.of(api);
|
}
|
}
|
}
|