liusheng
5 天以前 0bbbd746e8c39d91ad7b47a4e36e29ab972dc98c
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
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
package com.ruoyi.template.api;
 
import com.ruoyi.common.core.service.TemplateModuleDisabledException;
import com.ruoyi.template.config.TemplateProperties;
import com.ruoyi.template.config.TemplateSwitch;
import com.ruoyi.template.domain.HeLibrary;
import com.ruoyi.template.domain.HeLibraryVO;
import com.ruoyi.template.domain.IvrLibaExtemplatescript;
import com.ruoyi.template.domain.IvrLibaTemplateVO;
import com.ruoyi.template.mapper.IvrLibaExtemplatescriptMapper;
import com.ruoyi.template.service.IHeLibraryService;
import com.ruoyi.template.service.IIvrLibaTemplateService;
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.List;
 
/**
 * 模板模块对外门面(<b>常驻 Bean</b>)—— 其它模块访问模板数据的<b>唯一入口</b>。
 *
 * <h3>它存在的理由(你定的跨模块数据访问原则)</h3>
 * 「别的模块要用数据,尽量去对应的模块调用,而不是在自己模块里写 SQL」。
 * 模板模块化之后,`he_library` / `svy_lib_template` / `ivr_liba_template` 等表的
 * 数据访问只在本模块内;外部需要读模板时,一律经本门面。
 *
 * <h3>⚠️ 开关关闭时:<b>门面同样不可访问</b></h3>
 * 你定的口径是"开关关闭,{@code TemplateFacade} 也不能访问"。
 * 所以本门面<b>每一个数据方法</b>进来先过 {@link #requireEnabled()},
 * 关闭时抛 {@link TemplateModuleDisabledException}(提示与 HTTP 拦截器一致):
 * <ul>
 *   <li><b>为什么不用返回空值代替抛异常</b>:返回空值会让调用方以为"这个模板确实不存在",
 *       于是"模块被关闭"和"数据查不到"再也分不开 —— 排查时最怕这个。
 *       抛异常把状态<b>显式</b>摆出来,要不要降级由调用方自己决定;</li>
 *   <li><b>为什么是独立异常类型</b>:{@code ServiceException} 是 final 的,
 *       无法精确捕获;见 {@link TemplateModuleDisabledException} 的说明;</li>
 *   <li><b>调用方必须配降级</b>:本门面的调用点全在非 HTTP 路径上
 *       ({@code RyTask} / {@code RedisMqReceiver} / {@code ServiceTaskServiceImpl} /
 *       {@code ChatWebSocketHandler}),门面一抛异常就会打断随访主流程。
 *       所以关掉模板后各调用点的行为是<b>逐个明确过</b>的,不是碰运气 ——
 *       详见 {@code doc/模板模块化_调研与实施方案.md} §7.11。</li>
 * </ul>
 *
 * <h3>⚠️ 例外:{@link #isEnabled()} 与 {@link #getTemplateSwitch()} 不受守卫</h3>
 * 这两个方法本身就是<b>查开关状态</b>用的(前者供调用方预判、后者供测试断言)。
 * 如果它们也抛异常,"开关是不是关着"就再也问不出来了 —— 会自我矛盾。
 *
 * <h3>⚠️ 取用方式:{@code ObjectProvider} + 降级</h3>
 * 模块内部服务可能因为非 Spring 构造/装配缺失而不在容器里。门面统一用
 * {@code ObjectProvider.stream().findFirst()} 惰性取,取不到就返回安全值并打 WARN。
 * <b>注意区分两种情况</b>:
 * <ul>
 *   <li><b>模块被关闭</b>({@code template.enabled=false})→ <b>抛异常</b>,见上;</li>
 *   <li><b>模块开着但服务装配缺失/非 Spring 构造</b> → <b>返回安全值</b>(null / 空列表 / 0),
 *       不抛异常,保证测试不必拉起容器、调用方也不被装配问题拖垮。</li>
 * </ul>
 * 这两条互不冲突:开关判断在方法<b>最前面</b>,服务取用在其后。
 */
@Slf4j
@Component
public class TemplateFacade {
 
    private final TemplateProperties properties;
 
    /**
     * 运行时开关({@code sys_config} 优先,{@link TemplateProperties} 兜底)
     */
    private final TemplateSwitch templateSwitch;
 
    /**
     * 宣教资料库({@code he_library})读写。
     * <p>
     * ⚠️ 用 {@code ObjectProvider} 惰性取:本类有两个公开构造器,
     * 且容器里若没有该服务也不该让门面装配失败。
     */
    private final ObjectProvider<IHeLibraryService> heLibraryServiceProvider;
 
    /**
     * 语音库模板({@code ivr_liba_template})读写。
     */
    private final ObjectProvider<IIvrLibaTemplateService> ivrLibaTemplateServiceProvider;
 
    /**
     * 语音库外部模板脚本({@code ivr_liba_extemplatescript})。
     * <p>
     * ⚠️ 以 <b>Mapper</b> 形态注入:该项查询没有对应的 Service 方法,
     * 原调用方就是直调 Mapper。门面只做纯转发(见 {@link #findIvrLibaExtemplatescripts})。
     */
    private final ObjectProvider<IvrLibaExtemplatescriptMapper> ivrLibaExtemplatescriptMapperProvider;
 
    /**
     * Spring 使用的构造器。
     * <p>
     * ⚠️ <b>必须显式标注 {@code @Autowired}</b>:本类有两个公开构造器
     * (另一个是给测试/非 Spring 环境用的),若不标注,Spring 会因为"构造器不唯一"
     * 而退回查找无参构造器,从而在启动时报 {@code No default constructor found}。
     * —— 这是满意度模块<b>实机踩到</b>的缺陷,此处刻意保留同样的写法与注释。
     */
    @Autowired
    public TemplateFacade(TemplateProperties properties,
                          TemplateSwitch templateSwitch,
                          ObjectProvider<IHeLibraryService> heLibraryServiceProvider,
                          ObjectProvider<IIvrLibaTemplateService> ivrLibaTemplateServiceProvider,
                          ObjectProvider<IvrLibaExtemplatescriptMapper> ivrLibaExtemplatescriptMapperProvider) {
        this.properties = properties;
        this.templateSwitch = templateSwitch;
        this.heLibraryServiceProvider = heLibraryServiceProvider;
        this.ivrLibaTemplateServiceProvider = ivrLibaTemplateServiceProvider;
        this.ivrLibaExtemplatescriptMapperProvider = ivrLibaExtemplatescriptMapperProvider;
    }
 
    /**
     * 供<b>测试/非 Spring 环境</b>直接构造。
     * <p>
     * 不传任何服务:测试构造下调数据方法会返回安全值(null / 空列表)并打 WARN,
     * 保证开关校验类测试不必拉起数据库。
     */
    public TemplateFacade(TemplateProperties properties, String sysConfigValue) {
        this(properties, new TemplateSwitch(properties, sysConfigValue), null, null, null);
    }
 
    /**
     * 取运行时开关(供测试断言)
     */
    public TemplateSwitch getTemplateSwitch() {
        return templateSwitch;
    }
 
    /**
     * 模块被关闭时的统一提示。
     * <p>
     * ⚠️ 与 {@link com.ruoyi.template.config.TemplateAvailabilityInterceptor#DISABLED_MESSAGE}
     * <b>必须逐字一致</b>:同一个开关、同一个原因,不该因为走 HTTP 还是走门面而给出不同文案。
     * 测试 {@code TemplateSwitchCheck} 会断言两者相等。
     */
    public static final String DISABLED_MESSAGE =
            "模板功能未启用(参数配置 template.enabled=false),该接口不可用";
 
    /**
     * 模板模块是否启用({@code sys_config: template.enabled},运行时热读)
     * <p>
     * ⚠️ 这是**查询开关状态**的方法,<b>刻意不受守卫</b>:调用方需要它来预判
     * "现在该不该调门面",如果它也抛异常就自我矛盾了。
     */
    public boolean isEnabled() {
        return templateSwitch.isEnabled();
    }
 
    /**
     * 门面数据方法的统一入口守卫:模块关闭时抛
     * {@link TemplateModuleDisabledException},由调用方决定如何降级。
     */
    private void requireEnabled() {
        if (!templateSwitch.isEnabled()) {
            log.warn("【模板】模块已关闭(参数配置 template.enabled=false),拒绝门面访问");
            throw new TemplateModuleDisabledException(DISABLED_MESSAGE);
        }
    }
 
    // ==========================================================================
    // 宣教资料库(he_library)
    // ==========================================================================
 
    /**
     * 按主键查宣教资料库。
     * <p>
     * <b>这是外部最需要的一个方法</b>:原实现在 3 处直连
     * {@code heLibraryMapper.selectHeLibraryById(serviceSubtask.getLibtemplateid())}
     * ({@code RyTask}、{@code RedisMqReceiver}、{@code ServiceTaskServiceImpl}),
     * 且只读 {@code getHetype()} / {@code getPreachcontent()} 两个字段。
     *
     * @param id 宣教资料库主键(原 {@code serviceSubtask.libtemplateid})
     * @return 宣教资料库;<b>不存在</b>或<b>服务不可用</b>时返回 {@code null}
     * @throws TemplateModuleDisabledException 模块被关闭({@code template.enabled=false})
     */
    public HeLibrary findHeLibraryById(Long id) {
        requireEnabled();
        IHeLibraryService svc = heLibraryService();
        if (svc == null) {
            log.warn("【模板】IHeLibraryService 不可用(非 Spring 构造或装配缺失),findHeLibraryById({}) 返回 null。", id);
            return null;
        }
        return svc.selectHeLibraryById(id);
    }
 
    /**
     * 查宣教资料库列表(按条件)。
     *
     * @param query 查询条件(传 null 表示不限制)
     * @return 列表;服务不可用时返回<b>空列表</b>(不是 null)
     * @throws TemplateModuleDisabledException 模块被关闭({@code template.enabled=false})
     */
    public List<HeLibrary> findHeLibraryList(HeLibrary query) {
        requireEnabled();
        IHeLibraryService svc = heLibraryService();
        if (svc == null) {
            log.warn("【模板】IHeLibraryService 不可用,findHeLibraryList 返回空列表。");
            return Collections.emptyList();
        }
        List<HeLibrary> rows = svc.selectHeLibraryList(query);
        return rows == null ? Collections.emptyList() : rows;
    }
 
    /**
     * 新增或修改宣教详情(原 {@code IHeLibraryService#saveOrUpdateScript})。
     *
     * @return 影响行数;服务不可用时返回 0
     * @throws TemplateModuleDisabledException 模块被关闭({@code template.enabled=false})
     */
    public Integer saveOrUpdateHeLibrary(HeLibraryVO vo) {
        requireEnabled();
        IHeLibraryService svc = heLibraryService();
        if (svc == null) {
            log.warn("【模板】IHeLibraryService 不可用,saveOrUpdateHeLibrary 返回 0。");
            return 0;
        }
        return svc.saveOrUpdateScript(vo);
    }
 
    /**
     * 按条件查模板详情(原 {@code IHeLibraryService#selectInfoByCondition})。
     *
     * @return 列表;服务不可用时返回空列表
     * @throws TemplateModuleDisabledException 模块被关闭({@code template.enabled=false})
     */
    public List<HeLibraryVO> findHeLibraryInfoByCondition(HeLibrary query) {
        requireEnabled();
        IHeLibraryService svc = heLibraryService();
        if (svc == null) {
            log.warn("【模板】IHeLibraryService 不可用,findHeLibraryInfoByCondition 返回空列表。");
            return Collections.emptyList();
        }
        List<HeLibraryVO> rows = svc.selectInfoByCondition(query);
        return rows == null ? Collections.emptyList() : rows;
    }
 
    // ==========================================================================
    // 语音库模板(ivr_liba_template / ivr_liba_extemplatescript)
    // ==========================================================================
 
    /**
     * 按条件查语音库模板详情(原 {@code IIvrLibaTemplateService#selectInfoByCondition})。
     * <p>
     * 调用方:{@code ChatWebSocketHandler}(聊天里按条件取语音话术)。
     *
     * @param query 查询条件
     * @return 模板详情;<b>查不到</b>或<b>服务不可用</b>时返回 {@code null}
     * @throws TemplateModuleDisabledException 模块被关闭({@code template.enabled=false})
     */
    public IvrLibaTemplateVO findIvrLibaTemplateInfoByCondition(IvrLibaTemplateVO query) {
        requireEnabled();
        IIvrLibaTemplateService svc = ivrLibaTemplateService();
        if (svc == null) {
            log.warn("【模板】IIvrLibaTemplateService 不可用,findIvrLibaTemplateInfoByCondition 返回 null。");
            return null;
        }
        return svc.selectInfoByCondition(query);
    }
 
    /**
     * 按 ID 列表查语音库外部模板脚本(原 {@code IvrLibaExtemplatescriptMapper#queryIvrLibaExtemplatescriptList})。
     * <p>
     * 调用方:{@code ChatWebSocketHandler}。
     * <p>
     * ⚠️ 这是**唯一**以 Mapper 形态暴露的方法 —— 因为原调用方就是直调 Mapper,
     * 而该查询没有对应的 Service 方法。纯转发,SQL 一行未改。
     *
     * @param ids 外部模板ID列表(原调用方把逗号串拆成 {@code List<Long>})
     * @return 脚本列表;服务不可用时返回空列表
     * @throws TemplateModuleDisabledException 模块被关闭({@code template.enabled=false})
     */
    public List<IvrLibaExtemplatescript> findIvrLibaExtemplatescriptsByIds(List<Long> ids) {
        requireEnabled();
        IvrLibaExtemplatescriptMapper m = ivrLibaExtemplatescriptMapper();
        if (m == null) {
            log.warn("【模板】IvrLibaExtemplatescriptMapper 不可用,findIvrLibaExtemplatescriptsByIds 返回空列表。");
            return Collections.emptyList();
        }
        List<IvrLibaExtemplatescript> rows = m.queryIvrLibaExtemplatescriptList(ids);
        return rows == null ? Collections.emptyList() : rows;
    }
 
    // ==========================================================================
 
    private IHeLibraryService heLibraryService() {
        if (heLibraryServiceProvider == null) {
            return null;
        }
        try {
            return heLibraryServiceProvider.stream().findFirst().orElse(null);
        } catch (Exception e) {
            log.warn("【模板】获取 IHeLibraryService 失败,按不可用处理:{}", e.toString());
            return null;
        }
    }
 
    private IIvrLibaTemplateService ivrLibaTemplateService() {
        if (ivrLibaTemplateServiceProvider == null) {
            return null;
        }
        try {
            return ivrLibaTemplateServiceProvider.stream().findFirst().orElse(null);
        } catch (Exception e) {
            log.warn("【模板】获取 IIvrLibaTemplateService 失败,按不可用处理:{}", e.toString());
            return null;
        }
    }
 
    private IvrLibaExtemplatescriptMapper ivrLibaExtemplatescriptMapper() {
        if (ivrLibaExtemplatescriptMapperProvider == null) {
            return null;
        }
        try {
            return ivrLibaExtemplatescriptMapperProvider.stream().findFirst().orElse(null);
        } catch (Exception e) {
            log.warn("【模板】获取 IvrLibaExtemplatescriptMapper 失败,按不可用处理:{}", e.toString());
            return null;
        }
    }
}