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)。 *

* 这是其它模块访问出入院随访能力的唯一入口。 * *

与内部实现的关系(照抄满意度模块,5 个坑全部规避)

* * *

⚠️ 本门面同时实现三个接口

* * *

使用示例

*
 * @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 preachformApiProvider; /** * 运行时开关({@code sys_config} 优先,{@link InhospVisitProperties} 兜底) */ private final InhospVisitSwitch inhospVisitSwitch; /** * 内部实现;模块关闭时为 {@code null}。 *

* 用 {@code ObjectProvider} 惰性获取:即使实现 Bean 不存在也不会导致注入失败。 */ private final ObjectProvider apiProvider; /** * 基础出入院信息数据访问(必存在,不受开关管辖)。 *

* ⚠️ 用 {@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 apiProvider, ObjectProvider dataServiceProvider, ObjectProvider preachformApiProvider) { this.properties = properties; this.inhospVisitSwitch = inhospVisitSwitch; this.apiProvider = apiProvider; this.dataService = firstDataServiceOrNull(dataServiceProvider); this.preachformApiProvider = preachformApiProvider; } /** * 取宣教单数据访问实现;容器里没有、状态异常、或只找到自己时返回 {@code null}。 *

* ⚠️ 与 {@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 findList( com.ruoyi.common.core.service.dto.VisitPreachformRow query) { return preachformUnavailable("findList") ? java.util.Collections.emptyList() : preachformOrNull().findList(query); } @Override public java.util.List findByTaskidAndOrgid( Long taskid, String orgid) { return preachformUnavailable("findByTaskidAndOrgid") ? java.util.Collections.emptyList() : preachformOrNull().findByTaskidAndOrgid(taskid, orgid); } @Override public boolean insert(com.ruoyi.common.core.service.dto.VisitPreachformRow row) { return !preachformUnavailable("insert") && preachformOrNull().insert(row); } @Override public boolean updateById(com.ruoyi.common.core.service.dto.VisitPreachformRow row) { return !preachformUnavailable("updateById") && preachformOrNull().updateById(row); } @Override public boolean updateByCondition(com.ruoyi.common.core.service.dto.VisitPreachformRow row) { return !preachformUnavailable("updateByCondition") && preachformOrNull().updateByCondition(row); } @Override public boolean softDeleteById(Long id) { return !preachformUnavailable("softDeleteById") && preachformOrNull().softDeleteById(id); } @Override public boolean softDeleteByIds(Long[] ids) { return !preachformUnavailable("softDeleteByIds") && preachformOrNull().softDeleteByIds(ids); } @Override public boolean softDeleteBySubid(Long subid) { return !preachformUnavailable("softDeleteBySubid") && preachformOrNull().softDeleteBySubid(subid); } /** * 取数据服务实现;容器里没有、状态异常、或只找到自己时返回 {@code null}。 *

* ⚠️ 必须过滤掉"自己",两个理由: *

    *
  1. 自引用:本类自身也实现了 {@link InhospVisitDataService}。若不过滤, * 容器在第一轮找不到别的实现时会把门面自己当候选返回,随后 * {@code dataService.selectPatMedInhospList(...)} 变成无限递归 * ({@code StackOverflowError})。这是满意度模块实机踩到的同类缺陷。
  2. *
  3. 循环依赖:不过滤时 Spring 会在构造本 Bean 的过程中解析到"正在创建的自己", * 直接抛 {@code BeanCurrentlyInCreationException}。实测确认过这一点。
  4. *
*

* 用 {@code stream()} 而不是 {@code getIfAvailable()}:后者在候选多于一个时抛 * {@code NoUniqueBeanDefinitionException},会被吞成 null 并掩盖"有人又加了一个实现"。 */ private InhospVisitDataService firstDataServiceOrNull( ObjectProvider provider) { if (provider == null) { return null; } try { return provider.stream() .filter(svc -> svc != this) .findFirst() .orElse(null); } catch (Exception e) { log.warn("【出入院随访】获取数据服务失败,基础出入院信息访问将降级:{}", e.toString()); return null; } } /** * 供测试/非 Spring 环境直接构造(直接给出内部实现,可为 null)。 *

* 开关取自 {@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 selectPatMedInhosp( com.ruoyi.inhospvisit.domain.PatMedInhosp patMedInhosp) { return dataUnavailable("selectPatMedInhosp") ? java.util.Collections.emptyList() : dataService.selectPatMedInhosp(patMedInhosp); } @Override public java.util.List selectOutInhospByPatnoAndOutDate( String patno, java.util.Date dayStart, java.util.Date dayEnd) { return dataUnavailable("selectOutInhospByPatnoAndOutDate") ? java.util.Collections.emptyList() : dataService.selectOutInhospByPatnoAndOutDate(patno, dayStart, dayEnd); } @Override public String getTagnameBypatid(Long patid) { return dataUnavailable("getTagnameBypatid") ? null : dataService.getTagnameBypatid(patid); } @Override public java.util.List> getTagnameByPatids(java.util.List patids) { return dataUnavailable("getTagnameByPatids") ? java.util.Collections.>emptyList() : dataService.getTagnameByPatids(patids); } @Override public java.util.List selectPatMedInhospList( com.ruoyi.inhospvisit.domain.PatMedInhosp patMedInhosp) { return dataUnavailable("selectPatMedInhospList") ? java.util.Collections.emptyList() : dataService.selectPatMedInhospList(patMedInhosp); } @Override public java.util.List selectPatMedInhospListBySerialnum( com.ruoyi.inhospvisit.domain.PatMedInhosp patMedInhosp) { return dataUnavailable("selectPatMedInhospListBySerialnum") ? java.util.Collections.emptyList() : dataService.selectPatMedInhospListBySerialnum(patMedInhosp); } @Override public java.util.List selectInhospRefreshCandidates(String orgid) { return dataUnavailable("selectInhospRefreshCandidates") ? java.util.Collections.emptyList() : dataService.selectInhospRefreshCandidates(orgid); } @Override public java.util.List selectPatMedInhospListByCondition( com.ruoyi.inhospvisit.domain.PatMedInhospVO patMedInhospVO) { return dataUnavailable("selectPatMedInhospListByCondition") ? java.util.Collections.emptyList() : dataService.selectPatMedInhospListByCondition(patMedInhospVO); } @Override public com.ruoyi.common.core.service.dto.PatMedRes selectPatMedInhospCount( com.ruoyi.common.core.service.dto.PatMedReq patMedReq) { return dataUnavailable("selectPatMedInhospCount") ? null : dataService.selectPatMedInhospCount(patMedReq); } @Override public java.util.List selectNeedProcessList() { return dataUnavailable("selectNeedProcessList") ? java.util.Collections.emptyList() : dataService.selectNeedProcessList(); } /** * 查询患者住院记录(通过入院申请单信息)。 *

* ⚠️ 降级时返回空列表(不是 null)—— 调用方的分页逻辑对 null 与空列表处理相同, * 但空列表能避免调用方额外判空。注意本方法在正常路径下也可能返回 {@code null} * (原实现的反向判断缺陷,见 {@code InhospVisitDataService} 的说明),那是业务行为,不是降级。 */ @Override public java.util.List selectPatMedInhospByApplication( com.ruoyi.inhospvisit.domain.PatMedInhosp patMedInhosp) { return dataUnavailable("selectPatMedInhospByApplication") ? java.util.Collections.emptyList() : dataService.selectPatMedInhospByApplication(patMedInhosp); } @Override public int insertPatMedInhosp(com.ruoyi.inhospvisit.domain.PatMedInhosp patMedInhosp) { return dataUnavailable("insertPatMedInhosp") ? 0 : dataService.insertPatMedInhosp(patMedInhosp); } @Override public int insertPatMedInhospBatch(java.util.List list) { return dataUnavailable("insertPatMedInhospBatch") ? 0 : dataService.insertPatMedInhospBatch(list); } @Override public int updatePatMedInhosp(com.ruoyi.inhospvisit.domain.PatMedInhosp patMedInhosp) { return dataUnavailable("updatePatMedInhosp") ? 0 : dataService.updatePatMedInhosp(patMedInhosp); } @Override public int deletePatMedInhospByInhospid(Long inhospid) { return dataUnavailable("deletePatMedInhospByInhospid") ? 0 : dataService.deletePatMedInhospByInhospid(inhospid); } @Override public int deletePatMedInhospByInhospno(String inhospno) { return dataUnavailable("deletePatMedInhospByInhospno") ? 0 : dataService.deletePatMedInhospByInhospno(inhospno); } @Override public int deletePatMedInhospByInhospids(Long[] inhospids) { return dataUnavailable("deletePatMedInhospByInhospids") ? 0 : dataService.deletePatMedInhospByInhospids(inhospids); } /** * 空结果(键与"启用但组装失败"时一致,保证调用方不会 NPE) */ public static com.ruoyi.common.core.service.dto.VisitAgainSubtaskDto emptyAgainDraft(String reason) { com.ruoyi.common.core.service.dto.VisitAgainSubtaskDto dto = new com.ruoyi.common.core.service.dto.VisitAgainSubtaskDto(); dto.setDraft(null); dto.setInhospFound(false); dto.setMissReason(reason); return dto; } /** * 固定的 {@code ObjectProvider} 实现:仅用于非 Spring 环境/测试。 */ private static final class FixedProvider implements ObjectProvider { private final InhospVisitApi api; private FixedProvider(InhospVisitApi api) { this.api = api; } @Override public InhospVisitApi getObject(Object... args) { return api; } @Override public InhospVisitApi getIfAvailable() { return api; } @Override public InhospVisitApi getIfUnique() { return api; } @Override public InhospVisitApi getObject() { return api; } @Override public java.util.stream.Stream stream() { return api == null ? java.util.stream.Stream.empty() : java.util.stream.Stream.of(api); } } }