package com.ruoyi.inhospvisit.api; import com.ruoyi.inhospvisit.config.InhospVisitProperties; import com.ruoyi.inhospvisit.config.InhospVisitSwitch; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** * 出入院随访模块对外门面(常驻 Bean)。 *
* 这是其它模块访问出入院随访能力的唯一入口。 * *
* @Autowired
* private InhospVisitFacade inhospVisitFacade;
*
* if (inhospVisitFacade.isEnabled()) {
* inhospVisitFacade.dealOutHospInfo(config);
* }
*
* // 基础出入院信息:不需要判开关
* List<PatMedInhosp> rows = inhospVisitFacade.selectPatMedInhospList(query);
*
*/
@Slf4j
@Component
public class InhospVisitFacade implements InhospVisitApi,
InhospVisitDataService,
com.ruoyi.common.core.service.PreachformApi,
com.ruoyi.common.core.service.PublicitySubtaskApi {
private final InhospVisitProperties properties;
/**
* 宣教单({@code service_subtask_preachform})数据访问。
* * ⚠️ 用 {@code ObjectProvider} 惰性取,而不是直接注入接口 —— 理由同 {@code dataService}: * 本类自身也实现了 {@code PreachformApi},按接口类型直接注入会命中"正在创建的自己" * ({@code BeanCurrentlyInCreationException})。 *
* 该表的归属模块是本模块;跨模块 SQL 收口后,外部只经 {@code PreachformApi} 访问它
* (此前 smartor 里另有一整份同表 Mapper 与 XML,已删除)。
*/
private final ObjectProvider
* 用 {@code ObjectProvider} 惰性获取:即使实现 Bean 不存在也不会导致注入失败。
*/
private final ObjectProvider
* ⚠️ 用 {@link ObjectProvider} 惰性获取,并在解析时过滤掉"自己":
* 本类自身也实现了 {@link InhospVisitDataService},若直接注入接口类型,
* Spring 解析候选时会命中正在创建的自己,抛
* {@code BeanCurrentlyInCreationException}(实测确认)。
* 详见 {@link #firstDataServiceOrNull(ObjectProvider)}。
*
* 用 {@code ObjectProvider} 而非直接注入还带来装配健壮性:容器里没有该实现
* (例如不起数据库的测试上下文)时,门面依然能装配,数据方法走降级分支,
* 而不会让整个应用启动失败。
*/
private final InhospVisitDataService dataService;
/**
* Spring 使用的构造器。
*
* ⚠️ 必须显式标注 {@code @Autowired}:本类有两个公开构造器
* (另一个是给测试/非 Spring 环境用的),若不标注,Spring 会因为"构造器不唯一"
* 而退回查找无参构造器,从而在启动时报 {@code No default constructor found}。
* —— 这是满意度模块实机踩到的缺陷,此处刻意保留同样的写法与注释。
*/
@Autowired
public InhospVisitFacade(InhospVisitProperties properties,
InhospVisitSwitch inhospVisitSwitch,
ObjectProvider
* ⚠️ 与 {@link #firstDataServiceOrNull} 同一手法与理由(自引用 / 循环依赖)。
*/
private com.ruoyi.common.core.service.PreachformApi preachformOrNull() {
if (preachformApiProvider == null) {
return null;
}
try {
return preachformApiProvider.stream()
.filter(svc -> svc != this)
.findFirst()
.orElse(null);
} catch (Exception e) {
log.warn("【出入院随访】获取宣教单数据服务失败,相关访问将降级:{}", e.toString());
return null;
}
}
/**
* 数据服务是否不可用(不可用时打 WARN 并返回 true)
*/
private boolean preachformUnavailable(String method) {
if (preachformOrNull() == null) {
log.warn("【出入院随访】宣教单数据服务不可用,{} 跳过。", method);
return true;
}
return false;
}
// ==================================================================
// 宣教单 service_subtask_preachform(PreachformApi)
//
// ⚠️ 与上面 6 个随访方法的关键区别:以下方法【不做开关判断】。
// 它是跨模块 SQL 收口后的数据访问入口(替代 smartor 里那份同表 Mapper),
// 属数据基础设施;开关只该控制"要不要生成随访"。
// ==================================================================
@Override
public com.ruoyi.common.core.service.dto.VisitPreachformRow findById(Long id) {
return preachformUnavailable("findById") ? null : preachformOrNull().findById(id);
}
@Override
public java.util.List
* ⚠️ 必须过滤掉"自己",两个理由:
*
* 用 {@code stream()} 而不是 {@code getIfAvailable()}:后者在候选多于一个时抛
* {@code NoUniqueBeanDefinitionException},会被吞成 null 并掩盖"有人又加了一个实现"。
*/
private InhospVisitDataService firstDataServiceOrNull(
ObjectProvider
* 开关取自 {@code sys_config} 的模拟键值,从而保证"开关校验"脚本无需 Spring/数据库即可运行。
*
* 数据服务不传:测试构造下不提供基础出入院信息访问与宣教单访问
* (调用会得到 null / 空集合 / false),保证"开关校验"类测试不必拉起数据库。
*/
public InhospVisitFacade(InhospVisitProperties properties, InhospVisitApi delegate, String sysConfigValue) {
this(properties, new InhospVisitSwitch(properties, sysConfigValue), new FixedProvider(delegate),
null, null);
}
/**
* 取运行时开关(供拦截器复用同一个实例、也供测试断言)
*/
public InhospVisitSwitch getInhospVisitSwitch() {
return inhospVisitSwitch;
}
/**
* 取内部实现;模块关闭、实现缺失、或只找到自己时返回 {@code null}。
*
* ⚠️ 必须排除"自己":本类自身也实现了 {@link InhospVisitApi},而 Spring 在
* 按类型查找候选 Bean 时,若第一轮没找到别的实现(例如 ruoyi-inhospvisit
* 没有被打进应用,或将来又有人用条件装配把它摘掉),会把自引用当作兜底候选返回。
* 那样 {@code delegateOrNull()} 会拿到 {@code this},随后
* {@code delegate.dealOutHospInfo(...)} 就变成无限递归({@code StackOverflowError})。
* 这里显式过滤掉自己,把"没有实现"如实视为不可用。
* —— 这是满意度模块实机踩到的缺陷。
*
* ⚠️ 本方法不能替代开关判断:开关关闭时 {@link DefaultInhospVisitApi} 依然存在,
* 所以调用点一律先判 {@link #isEnabled()}。
*
* 用 {@code stream()} 而不是 {@code getIfAvailable()}:后者在候选多于一个时抛
* {@code NoUniqueBeanDefinitionException}(它继承自 {@code NoSuchBeanDefinitionException}),
* 最终会被吞成 null,掩盖"有人又加了一个实现"这种配置问题。
*/
private InhospVisitApi delegateOrNull() {
if (apiProvider == null) {
return null;
}
try {
return apiProvider.stream()
.filter(api -> api != this)
.findFirst()
.orElse(null);
} catch (Exception e) {
// 容器状态异常时按"未启用"处理,绝不让调用方(定时任务、Controller)因为随访模块而失败
log.warn("【出入院随访】获取内部实现失败,按未启用处理:{}", e.toString());
return null;
}
}
@Override
public boolean isEnabled() {
return inhospVisitSwitch.isEnabled() && delegateOrNull() != null;
}
@Override
public int dealOutHospInfo(String config) {
if (!isEnabled()) {
log.warn("【出入院随访】模块未启用(参数配置 inhospvisit.enabled=false),dealOutHospInfo 跳过。config={}", config);
return 0;
}
return delegateOrNull().dealOutHospInfo(config);
}
@Override
public int dealOutHospInfoBySchedule(String config) {
if (!isEnabled()) {
log.warn("【出入院随访】模块未启用(参数配置 inhospvisit.enabled=false),dealOutHospInfoBySchedule 跳过。config={}", config);
return 0;
}
return delegateOrNull().dealOutHospInfoBySchedule(config);
}
@Override
public int addOperationSubTask(String config) {
if (!isEnabled()) {
log.warn("【出入院随访】模块未启用(参数配置 inhospvisit.enabled=false),addOperationSubTask 跳过。config={}", config);
return 0;
}
return delegateOrNull().addOperationSubTask(config);
}
@Override
public int dealOutpatientInfo() {
if (!isEnabled()) {
log.warn("【出入院随访】模块未启用(参数配置 inhospvisit.enabled=false),dealOutpatientInfo 跳过。");
return 0;
}
return delegateOrNull().dealOutpatientInfo();
}
@Override
public int rollTaskGroupCycle() {
if (!isEnabled()) {
log.warn("【出入院随访】模块未启用(参数配置 inhospvisit.enabled=false),rollTaskGroupCycle 跳过。");
return 0;
}
return delegateOrNull().rollTaskGroupCycle();
}
@Override
public com.ruoyi.common.core.service.dto.VisitAgainSubtaskDto buildAgainVisitDraft(String orgid, Long inhospid,
Long newTaskId, Long sendstate) {
if (!isEnabled()) {
log.warn("【出入院随访】模块未启用(参数配置 inhospvisit.enabled=false),buildAgainVisitDraft 返回空结果。"
+ "orgid={}, inhospid={}", orgid, inhospid);
return emptyAgainDraft("模块未启用");
}
return delegateOrNull().buildAgainVisitDraft(orgid, inhospid, newTaskId, sendstate);
}
@Override
public boolean generatePublicitySubtask(Long taskid, Long patid, Long inhospid, int type, String config) {
if (!isEnabled()) {
log.warn("【出入院随访】模块未启用(参数配置 inhospvisit.enabled=false),generatePublicitySubtask 跳过。"
+ "taskid={}, patid={}, type={}", taskid, patid, type);
return false;
}
return delegateOrNull().generatePublicitySubtask(taskid, patid, inhospid, type, config);
}
// ==================================================================
// 基础出入院信息读写(InhospVisitDataService)
//
// ⚠️ 与上面 6 个随访方法的关键区别:以下方法【不做开关判断】。
// 理由见 InhospVisitDataService 类注释 —— 关闭随访生成不该让
// HIS 采集/档案汇聚无法写入院记录。
//
// ⚠️ 每个方法都对 dataService 判空:只有在"测试构造器"下它才是 null;
// Spring 环境下由 @Autowired 保证非 null。
// 这里返回空值而非抛异常,保证调用方主流程不因本模块而失败。
// ==================================================================
private boolean dataUnavailable(String method) {
if (dataService == null) {
log.warn("【出入院随访】数据服务不可用(非 Spring 构造或装配缺失),{} 跳过。", method);
return true;
}
return false;
}
@Override
public com.ruoyi.inhospvisit.domain.PatMedInhosp selectPatMedInhospByInhospid(Long inhospid) {
return dataUnavailable("selectPatMedInhospByInhospid") ? null
: dataService.selectPatMedInhospByInhospid(inhospid);
}
@Override
public java.util.List
* ⚠️ 降级时返回空列表(不是 null)—— 调用方的分页逻辑对 null 与空列表处理相同,
* 但空列表能避免调用方额外判空。注意本方法在正常路径下也可能返回 {@code null}
* (原实现的反向判断缺陷,见 {@code InhospVisitDataService} 的说明),那是业务行为,不是降级。
*/
@Override
public java.util.List
*
*