liusheng
8 天以前 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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
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>
 * &#64;Autowired
 * private SatisfactionFacade satisfactionFacade;
 *
 * if (satisfactionFacade.isEnabled()) {
 *     Map&lt;String, Object&gt; 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);
        }
    }
}