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; /** * 模板模块对外门面(常驻 Bean)—— 其它模块访问模板数据的唯一入口。 * *

它存在的理由(你定的跨模块数据访问原则)

* 「别的模块要用数据,尽量去对应的模块调用,而不是在自己模块里写 SQL」。 * 模板模块化之后,`he_library` / `svy_lib_template` / `ivr_liba_template` 等表的 * 数据访问只在本模块内;外部需要读模板时,一律经本门面。 * *

⚠️ 开关关闭时:门面同样不可访问

* 你定的口径是"开关关闭,{@code TemplateFacade} 也不能访问"。 * 所以本门面每一个数据方法进来先过 {@link #requireEnabled()}, * 关闭时抛 {@link TemplateModuleDisabledException}(提示与 HTTP 拦截器一致): * * *

⚠️ 例外:{@link #isEnabled()} 与 {@link #getTemplateSwitch()} 不受守卫

* 这两个方法本身就是查开关状态用的(前者供调用方预判、后者供测试断言)。 * 如果它们也抛异常,"开关是不是关着"就再也问不出来了 —— 会自我矛盾。 * *

⚠️ 取用方式:{@code ObjectProvider} + 降级

* 模块内部服务可能因为非 Spring 构造/装配缺失而不在容器里。门面统一用 * {@code ObjectProvider.stream().findFirst()} 惰性取,取不到就返回安全值并打 WARN。 * 注意区分两种情况: * * 这两条互不冲突:开关判断在方法最前面,服务取用在其后。 */ @Slf4j @Component public class TemplateFacade { private final TemplateProperties properties; /** * 运行时开关({@code sys_config} 优先,{@link TemplateProperties} 兜底) */ private final TemplateSwitch templateSwitch; /** * 宣教资料库({@code he_library})读写。 *

* ⚠️ 用 {@code ObjectProvider} 惰性取:本类有两个公开构造器, * 且容器里若没有该服务也不该让门面装配失败。 */ private final ObjectProvider heLibraryServiceProvider; /** * 语音库模板({@code ivr_liba_template})读写。 */ private final ObjectProvider ivrLibaTemplateServiceProvider; /** * 语音库外部模板脚本({@code ivr_liba_extemplatescript})。 *

* ⚠️ 以 Mapper 形态注入:该项查询没有对应的 Service 方法, * 原调用方就是直调 Mapper。门面只做纯转发(见 {@link #findIvrLibaExtemplatescripts})。 */ private final ObjectProvider ivrLibaExtemplatescriptMapperProvider; /** * Spring 使用的构造器。 *

* ⚠️ 必须显式标注 {@code @Autowired}:本类有两个公开构造器 * (另一个是给测试/非 Spring 环境用的),若不标注,Spring 会因为"构造器不唯一" * 而退回查找无参构造器,从而在启动时报 {@code No default constructor found}。 * —— 这是满意度模块实机踩到的缺陷,此处刻意保留同样的写法与注释。 */ @Autowired public TemplateFacade(TemplateProperties properties, TemplateSwitch templateSwitch, ObjectProvider heLibraryServiceProvider, ObjectProvider ivrLibaTemplateServiceProvider, ObjectProvider ivrLibaExtemplatescriptMapperProvider) { this.properties = properties; this.templateSwitch = templateSwitch; this.heLibraryServiceProvider = heLibraryServiceProvider; this.ivrLibaTemplateServiceProvider = ivrLibaTemplateServiceProvider; this.ivrLibaExtemplatescriptMapperProvider = ivrLibaExtemplatescriptMapperProvider; } /** * 供测试/非 Spring 环境直接构造。 *

* 不传任何服务:测试构造下调数据方法会返回安全值(null / 空列表)并打 WARN, * 保证开关校验类测试不必拉起数据库。 */ public TemplateFacade(TemplateProperties properties, String sysConfigValue) { this(properties, new TemplateSwitch(properties, sysConfigValue), null, null, null); } /** * 取运行时开关(供测试断言) */ public TemplateSwitch getTemplateSwitch() { return templateSwitch; } /** * 模块被关闭时的统一提示。 *

* ⚠️ 与 {@link com.ruoyi.template.config.TemplateAvailabilityInterceptor#DISABLED_MESSAGE} * 必须逐字一致:同一个开关、同一个原因,不该因为走 HTTP 还是走门面而给出不同文案。 * 测试 {@code TemplateSwitchCheck} 会断言两者相等。 */ public static final String DISABLED_MESSAGE = "模板功能未启用(参数配置 template.enabled=false),该接口不可用"; /** * 模板模块是否启用({@code sys_config: template.enabled},运行时热读) *

* ⚠️ 这是**查询开关状态**的方法,刻意不受守卫:调用方需要它来预判 * "现在该不该调门面",如果它也抛异常就自我矛盾了。 */ 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) // ========================================================================== /** * 按主键查宣教资料库。 *

* 这是外部最需要的一个方法:原实现在 3 处直连 * {@code heLibraryMapper.selectHeLibraryById(serviceSubtask.getLibtemplateid())} * ({@code RyTask}、{@code RedisMqReceiver}、{@code ServiceTaskServiceImpl}), * 且只读 {@code getHetype()} / {@code getPreachcontent()} 两个字段。 * * @param id 宣教资料库主键(原 {@code serviceSubtask.libtemplateid}) * @return 宣教资料库;不存在或服务不可用时返回 {@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 列表;服务不可用时返回空列表(不是 null) * @throws TemplateModuleDisabledException 模块被关闭({@code template.enabled=false}) */ public List findHeLibraryList(HeLibrary query) { requireEnabled(); IHeLibraryService svc = heLibraryService(); if (svc == null) { log.warn("【模板】IHeLibraryService 不可用,findHeLibraryList 返回空列表。"); return Collections.emptyList(); } List 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 findHeLibraryInfoByCondition(HeLibrary query) { requireEnabled(); IHeLibraryService svc = heLibraryService(); if (svc == null) { log.warn("【模板】IHeLibraryService 不可用,findHeLibraryInfoByCondition 返回空列表。"); return Collections.emptyList(); } List rows = svc.selectInfoByCondition(query); return rows == null ? Collections.emptyList() : rows; } // ========================================================================== // 语音库模板(ivr_liba_template / ivr_liba_extemplatescript) // ========================================================================== /** * 按条件查语音库模板详情(原 {@code IIvrLibaTemplateService#selectInfoByCondition})。 *

* 调用方:{@code ChatWebSocketHandler}(聊天里按条件取语音话术)。 * * @param query 查询条件 * @return 模板详情;查不到或服务不可用时返回 {@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})。 *

* 调用方:{@code ChatWebSocketHandler}。 *

* ⚠️ 这是**唯一**以 Mapper 形态暴露的方法 —— 因为原调用方就是直调 Mapper, * 而该查询没有对应的 Service 方法。纯转发,SQL 一行未改。 * * @param ids 外部模板ID列表(原调用方把逗号串拆成 {@code List}) * @return 脚本列表;服务不可用时返回空列表 * @throws TemplateModuleDisabledException 模块被关闭({@code template.enabled=false}) */ public List findIvrLibaExtemplatescriptsByIds(List ids) { requireEnabled(); IvrLibaExtemplatescriptMapper m = ivrLibaExtemplatescriptMapper(); if (m == null) { log.warn("【模板】IvrLibaExtemplatescriptMapper 不可用,findIvrLibaExtemplatescriptsByIds 返回空列表。"); return Collections.emptyList(); } List 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; } } }