package com.ruoyi.inhospvisit.config;
|
|
import com.ruoyi.common.core.service.IConfigService;
|
import com.ruoyi.inhospvisit.InhospVisitModule;
|
import lombok.extern.slf4j.Slf4j;
|
import org.springframework.beans.factory.ObjectProvider;
|
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.stereotype.Component;
|
|
/**
|
* 出入院随访模块<b>总开关</b>:值存放在 {@code sys_config} 参数配置表中,<b>运行时读取、可热切换</b>。
|
*
|
* <h3>参数配置</h3>
|
* <pre>
|
* 参数键名:inhospvisit.enabled
|
* 参数键值:true = 启用(默认)
|
* false = 关闭
|
* </pre>
|
* 建参数的幂等 SQL 见 {@code sql/出入院随访开关_sys_config.sql}。
|
*
|
* <h3>⚠️ 本开关的作用范围(2025 更正)</h3>
|
* <ul>
|
* <li><b>HTTP 触发端点</b>:由 {@code InhospVisitAvailabilityInterceptor} 拦下,报明确业务错误;</li>
|
* <li><b>定时任务</b>:<b>受本开关控制</b>。{@code RyTask} 的 5 个入口已接开关 ——
|
* {@code dealOutHospInfo} / {@code dealOutHospInfoBySchedule} /
|
* {@code dealTaskGroupOutHosp} / {@code rollTaskGroupCycle} 走门面,
|
* {@code dealSubtaskDeadline} 走入口守卫;关闭时打
|
* 「【出入院随访】模块未启用(参数配置 inhospvisit.enabled=false),… 跳过」;</li>
|
* <li><b>基础出入院信息读写</b>({@code InhospVisitDataService}):<b>刻意不受本开关管</b> ——
|
* 关掉随访不该让 HIS 采集与出入院记录管理瘫痪。</li>
|
* </ul>
|
* <p>
|
* ⚠️ <b>本注释此前写的是"本开关不作用于定时任务"</b>,理由是"一次误操作就能让当天全院随访停摆,
|
* 而定时任务没有反馈渠道"。该说法<b>与事实不符且已更正</b>:
|
* 当时定时任务走 smartor 的遗留实现 {@code PatMedInhospServiceImpl},
|
* 它<b>从不调用门面的编排方法</b>(只用门面取数据,而数据方法按设计不受开关管),
|
* 所以真实状态是"开关对定时任务无效、且无任何提示"——
|
* 这比"受控"更糟:它让运维以为已经关掉了,实际还在生成随访。
|
* 现在定时任务走门面,关闭有明确日志,参数在「系统管理 → 参数设置」里可见可改。
|
* 要暂停定时任务仍可去「系统管理 → 定时任务」暂停 {@code sys_job},两条路都有效。
|
* 详见 {@code doc/模块开关有效性核查与修复.md}。
|
*
|
* <h3>取值优先级</h3>
|
* <ol>
|
* <li>{@code sys_config} 里 {@code inhospvisit.enabled}(<b>主用</b>);</li>
|
* <li>{@link InhospVisitProperties#isEnabled()}(<b>兜底</b>,默认 true)。</li>
|
* </ol>
|
*
|
* <h3>为什么读不到配置也按 true(fail-open)</h3>
|
* 读 {@code sys_config} 要走数据库,数据库抖动时 fail-closed 会把出入院随访整体打死 ——
|
* 那是把"参数读取"变成"业务停摆放大器"。所以读失败只打 WARN 并放行。
|
* <p>
|
* ⚠️ 注意本模块另有一个"类似但不该合并"的口径:yml 里的 {@code noLongTask}
|
* ({@code ServiceExternalServiceImpl.java:83-84})按 orgid 直接跳过长期任务生成。
|
* 语义相近但链路不同(它在 {@code ServiceExternalServiceImpl},不在随访扫描链上),
|
* <b>本次不合并</b>,避免行为变更。
|
*
|
* <h3>为什么有 10 秒缓存</h3>
|
* {@code SysConfigServiceImpl#selectConfigByKey} 当前<b>每次都查库</b>
|
* (Redis 缓存那段被注释掉了),而本开关每次随访请求都会被读到。
|
* 进程内 TTL 缓存把查库降到"每实例每 10 秒最多一次",代价是改动最多 10 秒生效。
|
*/
|
@Slf4j
|
@Component
|
public class InhospVisitSwitch {
|
|
/**
|
* sys_config 里的参数键名(与 {@link InhospVisitModule#SWITCH_KEY} 保持一致)
|
*/
|
public static final String CONFIG_KEY = InhospVisitModule.SWITCH_KEY;
|
|
/**
|
* 进程内缓存有效期(毫秒)
|
*/
|
private static final long CACHE_TTL_MILLIS = 10_000L;
|
|
private final InhospVisitProperties properties;
|
|
/**
|
* 参数配置服务(ruoyi-common 的接口,实现在 ruoyi-system)。
|
* <p>
|
* 用 {@code ObjectProvider} 惰性取:本模块只依赖 ruoyi-common,
|
* 在测试/独立部署等"容器里没有实现"的场景下不能因此启动失败。
|
*/
|
private final ObjectProvider<IConfigService> configServiceProvider;
|
|
private volatile Boolean cachedEnabled;
|
|
private volatile long cachedAt;
|
|
/**
|
* Spring 使用的构造器。
|
* <p>
|
* ⚠️ <b>必须显式标注 {@code @Autowired}</b>:本类有两个公开构造器
|
* (另一个给测试/非 Spring 环境用),不标注的话 Spring 会因"构造器不唯一"
|
* 退回查找无参构造器,启动即报 {@code No default constructor found}。
|
* —— 这是满意度模块实际踩到的坑,此处刻意保留同样的写法与注释。
|
*/
|
@Autowired
|
public InhospVisitSwitch(InhospVisitProperties properties,
|
ObjectProvider<IConfigService> configServiceProvider) {
|
this.properties = properties;
|
this.configServiceProvider = configServiceProvider;
|
}
|
|
/**
|
* 供<b>测试/非 Spring 环境</b>直接构造:{@code configValue} 就是 sys_config 里的键值,
|
* 传 {@code null} 表示"参数行不存在"(此时回落到 {@link InhospVisitProperties})。
|
*/
|
public InhospVisitSwitch(InhospVisitProperties properties, final String configValue) {
|
this(properties, 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 = properties.isEnabled();
|
log.debug("【出入院随访】sys_config 中未配置 {},回落 inhospvisit.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<IConfigService> {
|
|
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<IConfigService> stream() {
|
return configService == null ? java.util.stream.Stream.empty() : java.util.stream.Stream.of(configService);
|
}
|
}
|
}
|