package com.ruoyi.satisfaction.config;
import com.ruoyi.common.core.service.IConfigService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 满意度模块总开关:值存放在 {@code sys_config} 参数配置表中,运行时读取、可热切换。
*
*
参数配置
*
* 参数键名:satisfaction.enabled
* 参数键值:true = 启用(默认)
* false = 关闭
*
* 位置:系统管理 → 参数设置 → 新增/修改。
*
* insert into sys_config (config_name, config_key, config_value, config_type, create_by, create_time, remark)
* values ('满意度模块开关', 'satisfaction.enabled', 'true', 'Y', 'admin', sysdate(), 'true启用,false关闭(默认true)');
*
*
* 取值优先级
*
* - {@code sys_config} 里 {@code satisfaction.enabled} 的值(主用,
* 来自本项目 {@code IConfigService} 接口,实现在 ruoyi-system 的 {@code SysConfigServiceImpl});
* - {@link SatisfactionProperties#isEnabled()}(兜底:仅当 sys_config 里没有这一行、
* 或非 Spring 环境取不到配置服务时使用,默认 true)。
*
*
* 为什么默认 true
* 历史行为就是"默认开启"。参数行缺失时按 true 处理,保证升级到本版本后
* 满意度功能不会因为少了一行配置而突然不可用(要关闭必须显式配置 false)。
*
* 为什么读不到配置时也按 true(fail-open)
* 读 {@code sys_config} 需要走数据库,数据库抖动时如果 fail-closed 会把整个满意度业务打死。
* 这里选择与"参数行缺失"一致的处理:只打 WARN 并放行,宁可多开一会儿,也不做数据库抖动放大器。
*
* 为什么有 10 秒缓存
* {@code SysConfigServiceImpl#selectConfigByKey} 当前每次都查库(Redis 缓存那段被注释掉了),
* 而本开关在每次满意度请求、每次登录取分类时都会被读到。这里加一个进程内 TTL 缓存,
* 把查库降到"每个实例每 10 秒最多一次"。代价是参数改动最多 10 秒生效(多实例部署时各自计时)。
*/
@Slf4j
@Component
public class SatisfactionSwitch {
/**
* sys_config 里的参数键名
*/
public static final String CONFIG_KEY = "satisfaction.enabled";
/**
* 进程内缓存有效期(毫秒)
*/
private static final long CACHE_TTL_MILLIS = 10_000L;
private final SatisfactionProperties satisfactionProperties;
/**
* 参数配置服务(ruoyi-common 的接口,实现在 ruoyi-system)。
*
* 用 {@code ObjectProvider} 惰性取:本模块只依赖 ruoyi-common,
* 在单元测试/独立部署等"容器里没有实现"的场景下不能因此启动失败。
*/
private final ObjectProvider configServiceProvider;
/**
* 最近一次解析结果(含是否来自 sys_config)
*/
private volatile Boolean cachedEnabled;
/**
* 最近一次解析的时间戳
*/
private volatile long cachedAt;
/**
* Spring 使用的构造器。
*
* ⚠️ 必须显式标注 {@code @Autowired}:本类有两个公开构造器
* (另一个给测试/非 Spring 环境用),不标注的话 Spring 会因"构造器不唯一"
* 退回查找无参构造器,启动即报 {@code No default constructor found}。
*/
@Autowired
public SatisfactionSwitch(SatisfactionProperties satisfactionProperties,
ObjectProvider configServiceProvider) {
this.satisfactionProperties = satisfactionProperties;
this.configServiceProvider = configServiceProvider;
}
/**
* 供测试/非 Spring 环境直接构造:{@code configValue} 就是 sys_config 里的键值,
* 传 {@code null} 表示"参数行不存在"(此时回落到 {@link SatisfactionProperties})。
*/
public SatisfactionSwitch(SatisfactionProperties satisfactionProperties, final String configValue) {
this(satisfactionProperties, new FixedProvider(configValue));
// 非 Spring 环境下没有"改配置后等 10 秒"的概念,直接禁用缓存,保证断言即时可见
this.cachedEnabled = resolve();
this.cachedAt = System.currentTimeMillis() + CACHE_TTL_MILLIS * 1000L;
}
/**
* 满意度功能是否启用(运行时热读,最多 10 秒延迟)
*/
public boolean isEnabled() {
long now = System.currentTimeMillis();
Boolean cached = this.cachedEnabled;
if (cached != null && now - this.cachedAt < CACHE_TTL_MILLIS) {
return cached;
}
boolean resolved = resolve();
this.cachedEnabled = resolved;
this.cachedAt = now;
return resolved;
}
/**
* 清掉进程内缓存,下次调用立即重新读 sys_config(供测试/需要立即生效的场景)
*/
public void invalidateCache() {
this.cachedEnabled = null;
}
/**
* 解析当前开关值:sys_config 优先,其次 yml 兜底
*/
private boolean resolve() {
String value = readFromSysConfig();
if (value != null) {
if (!"true".equalsIgnoreCase(value) && !"false".equalsIgnoreCase(value)) {
log.warn("【满意度】sys_config 中 {} 的值不是 true/false(实际=「{}」),将按「不是 false 即启用」处理",
CONFIG_KEY, value);
}
boolean enabled = !"false".equalsIgnoreCase(value);
log.debug("【满意度】开关取自 sys_config:{}={} -> enabled={}", CONFIG_KEY, value, enabled);
return enabled;
}
boolean enabled = satisfactionProperties.isEnabled();
log.debug("【满意度】sys_config 中未配置 {},回落 satisfaction.enabled={}", CONFIG_KEY, enabled);
return enabled;
}
/**
* 读 sys_config 的原始键值;没有实现、没有这一行、或读库失败都返回 {@code null}
*/
private String readFromSysConfig() {
if (configServiceProvider == null) {
return null;
}
try {
IConfigService configService = configServiceProvider.stream().findFirst().orElse(null);
if (configService == null) {
return null;
}
String value = configService.selectConfigByKey(CONFIG_KEY);
if (value == null || value.trim().isEmpty()) {
return null;
}
return value.trim();
} catch (Exception e) {
// 容器没有实现、数据源异常、SQL 报错……一律按"参数缺失"处理(fail-open),绝不影响主流程
log.warn("【满意度】读取 sys_config 的 {} 失败,本次按未配置处理(fail-open):{}",
CONFIG_KEY, e.toString());
return null;
}
}
/**
* 固定值的 {@code ObjectProvider}:只用于非 Spring 环境/测试
*/
private static final class FixedProvider implements ObjectProvider {
private final IConfigService configService;
private FixedProvider(final String configValue) {
this.configService = configValue == null ? null : configKey -> configValue;
}
@Override
public IConfigService getObject(Object... args) {
return configService;
}
@Override
public IConfigService getIfAvailable() {
return configService;
}
@Override
public IConfigService getIfUnique() {
return configService;
}
@Override
public IConfigService getObject() {
return configService;
}
@Override
public java.util.stream.Stream stream() {
return configService == null ? java.util.stream.Stream.empty() : java.util.stream.Stream.of(configService);
}
}
}