liusheng
9 天以前 f0a09ee996ab2605869844d6a3d812709e2bf0fa
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
package com.ruoyi.publicity.config;
 
import com.ruoyi.common.core.service.IConfigService;
import com.ruoyi.publicity.PublicityModule;
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>
 * 参数键名:publicity.enabled
 * 参数键值:true  = 启用(默认)
 *          false = 关闭(入院宣教与药品宣教都不再执行)
 * </pre>
 * 建参数的幂等 SQL 见 {@code sql/出入院随访开关_sys_config.sql}(与 inhospvisit 放同一份)。
 *
 * <h3>三层开关的关系(别混)</h3>
 * <ol>
 *   <li><b>本开关</b>({@code publicity.enabled})—— 整个宣教模块的总闸;</li>
 *   <li>{@code sys_config: in.helibrary.switch == "2"} —— <b>入院宣教</b>自身的业务开关
 *       (迁移前 {@code PatMedInhospServiceImpl:332},原样保留);</li>
 *   <li>站点 {@code spring.profiles.active == "sltd"} —— <b>药品宣教</b>仅省立同德执行
 *       (迁移前 {@code PatMedInhospServiceImpl:412},原样保留)。</li>
 * </ol>
 * 本类只负责第 1 条;第 2、3 条在 {@code PublicityService} 里按原逻辑判断。
 *
 * <h3>为什么读不到配置也按 true(fail-open)</h3>
 * 读 {@code sys_config} 要走数据库,数据库抖动时 fail-closed 会把宣教整体打死。
 * 而且宣教本身还有上面第 2、3 层业务判断兜着,多开一会儿的风险远小于把业务打死。
 */
@Slf4j
@Component
public class PublicitySwitch {
 
    /**
     * sys_config 里的参数键名(与 {@link PublicityModule#SWITCH_KEY} 保持一致)
     */
    public static final String CONFIG_KEY = PublicityModule.SWITCH_KEY;
 
    /**
     * 进程内缓存有效期(毫秒)
     */
    private static final long CACHE_TTL_MILLIS = 10_000L;
 
    private final PublicityProperties properties;
 
    private final ObjectProvider<IConfigService> configServiceProvider;
 
    private volatile Boolean cachedEnabled;
 
    private volatile long cachedAt;
 
    /**
     * Spring 使用的构造器。
     * <p>
     * ⚠️ <b>必须显式标注 {@code @Autowired}</b>:本类有两个公开构造器,
     * 不标注的话 Spring 会因"构造器不唯一"退回查找无参构造器,启动即报
     * {@code No default constructor found}。—— 这是满意度模块实机踩到的坑。
     */
    @Autowired
    public PublicitySwitch(PublicityProperties properties,
                           ObjectProvider<IConfigService> configServiceProvider) {
        this.properties = properties;
        this.configServiceProvider = configServiceProvider;
    }
 
    /**
     * 供<b>测试/非 Spring 环境</b>直接构造:{@code configValue} 就是 sys_config 里的键值,
     * 传 {@code null} 表示"参数行不存在"(此时回落到 {@link PublicityProperties})。
     */
    public PublicitySwitch(PublicityProperties properties, final String configValue) {
        this(properties, new FixedProvider(configValue));
        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;
    }
 
    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 中未配置 {},回落 publicity.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) {
            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);
        }
    }
}