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;
|
}
|
}
|
}
|