//package com.ruoyi.inhospvisit.api; // //import com.ruoyi.common.core.service.IConfigService; //import com.ruoyi.common.core.service.dto.VisitAgainSubtaskDto; //import com.ruoyi.inhospvisit.config.InhospVisitAvailabilityInterceptor; //import com.ruoyi.inhospvisit.config.InhospVisitProperties; //import com.ruoyi.inhospvisit.config.InhospVisitSwitch; //import com.ruoyi.inhospvisit.config.InhospVisitWebConfig; //import org.springframework.beans.factory.ObjectProvider; //import org.springframework.context.annotation.AnnotationConfigApplicationContext; // //import java.util.concurrent.atomic.AtomicInteger; // ///** // * 出入院随访模块的 Spring 容器装配校验(不依赖 JUnit、不依赖数据库)。 // * // *

照抄 {@code SatisfactionContextCheck} 的做法:用真实的(但极简的) // * {@link AnnotationConfigApplicationContext} 验证"Spring 能把 Bean 装起来" —— // * 静态检查证明不了装配。满意度模块当时有两个真实缺陷只有真起容器才能发现: // *

    // *
  1. 门面有两个公开构造器却没标 {@code @Autowired} → 启动报 {@code No default constructor found};
  2. // *
  3. 门面把自己当实现 → 调用时无限递归 → {@code StackOverflowError}。
  4. // *
// * 本类把这两条都做成断言。 // * // *

运行方式(仓库内没有现成脚本)

// *
// * mvn -o -q -pl ruoyi-inhospvisit -am test-compile -DskipTests
// * # 然后用模块 classpath 直接跑 main(surefire 认不出它,因为没有 @Test)
// * 
// * classpath 可用 {@code mvn dependency:build-classpath -Dmdep.outputFile=...} 导出。 // */ //public class InhospVisitContextCheck { // // private static int failed = 0; // // private static void check(String name, String expected, String actual) { // if (expected.equals(actual)) { // System.out.println(" [OK] " + name + " = " + actual); // } else { // System.out.println(" [FAIL] " + name + " 期望=[" + expected + "] 实际=[" + actual + "]"); // failed++; // } // } // // public static void main(String[] args) { // System.out.println("=== 场景 1:参数表 inhospvisit.enabled=true,且存在内部实现 ==="); // try (AnnotationConfigApplicationContext ctx = context(true, true, SYS_ON)) { // InhospVisitFacade facade = ctx.getBean(InhospVisitFacade.class); // check("门面能被 Spring 实例化(构造器不再歧义)", "true", String.valueOf(facade != null)); // check("启用时 isEnabled()", "true", String.valueOf(facade.isEnabled())); // check("取到的是【内部实现】而不是门面自己", "1", // String.valueOf(facade.dealOutHospInfo("7"))); // check("门面同时是 PublicitySubtaskApi(下沉给宣教模块的那一个)", "true", // String.valueOf(ctx.getBean(com.ruoyi.common.core.service.PublicitySubtaskApi.class) == facade)); // // 容器里应有 2 个 InhospVisitApi:门面自己 + 内部实现(门面也实现该接口,这是设计使然) // check("容器内 InhospVisitApi 实现数 = 门面 1 + 内部实现 1", "2", // String.valueOf(ctx.getBeansOfType(InhospVisitApi.class).size())); // check("开关读取的键名是 sys_config 的 inhospvisit.enabled", "inhospvisit.enabled", // InhospVisitSwitch.CONFIG_KEY); // } // // System.out.println(); // System.out.println("=== 场景 2:参数表=true,但【没有】内部实现(模块没被打进应用)==="); // try (AnnotationConfigApplicationContext ctx = context(true, false, SYS_ON)) { // InhospVisitFacade facade = ctx.getBean(InhospVisitFacade.class); // check("无实现时 isEnabled() 为 false", "false", String.valueOf(facade.isEnabled())); // // 关键:不能把门面自己当实现,否则这里会 StackOverflowError // String result; // try { // result = String.valueOf(facade.dealOutHospInfo("7")); // } catch (StackOverflowError e) { // result = "StackOverflowError"; // } // check("无实现时 dealOutHospInfo 安全返回 0(不会自引用递归)", "0", result); // // String again; // try { // VisitAgainSubtaskDto dto = facade.buildAgainVisitDraft("ORG1", 1L, 2L, 2L); // again = String.valueOf(dto.isInhospFound()); // } catch (StackOverflowError e) { // again = "StackOverflowError"; // } // check("无实现时 buildAgainVisitDraft 返回「未找到出入院记录」而不是递归", "false", again); // // check("无实现时 generatePublicitySubtask 安全返回 false", "false", // String.valueOf(facade.generatePublicitySubtask(1L, 1L, 1L, 4, "7"))); // // // 反证:直接问容器"按类型有哪些候选",看它是否把门面自己算进去 // ProviderProbe probe = ctx.getBean(ProviderProbe.class); // check("无实现时,容器按类型只找到门面自己(证明自引用兜底存在)", "true", // String.valueOf(probe.rawCandidate() == facade)); // check("但门面不会把'自己'当成实现(self 过滤生效)", "false", // String.valueOf(facade.isEnabled())); // } // // System.out.println(); // System.out.println("=== 场景 3:参数表 inhospvisit.enabled=false(实现 Bean 依然存在,但门面必须不转发)==="); // StubApi.CALLS.set(0); // try (AnnotationConfigApplicationContext ctx = context(true, true, SYS_OFF)) { // InhospVisitFacade facade = ctx.getBean(InhospVisitFacade.class); // check("关闭时 isEnabled() 为 false", "false", String.valueOf(facade.isEnabled())); // String result; // try { // result = String.valueOf(facade.dealOutHospInfo("7")); // } catch (StackOverflowError e) { // result = "StackOverflowError"; // } // check("关闭时 dealOutHospInfo 返回 0(不会自递归)", "0", result); // check("关闭时不再调用内部实现(stub 调用次数 = 0)", "0", // String.valueOf(StubApi.CALLS.get())); // // // ------------------------------------------------------------------ // // 关键:基础出入院信息的读写【不受本开关管辖】 // // 理由见 InhospVisitDataService 类注释 —— HIS 采集与档案汇聚都依赖它写入院记录, // // 若把开关一并套上,关闭随访生成会连带让数据采集瘫痪,那是两件事。 // // ------------------------------------------------------------------ // StubDataService.CALLS.set(0); // String dataResult; // try { // dataResult = String.valueOf(facade.selectPatMedInhospByInhospid(123L) != null); // } catch (StackOverflowError e) { // dataResult = "StackOverflowError"; // } // check("关闭时【数据查询】仍可用(不受开关管辖)", "true", dataResult); // check("关闭时【数据查询】确实转发到数据服务(调用次数 = 1)", "1", // String.valueOf(StubDataService.CALLS.get())); // // StubDataService.CALLS.set(0); // String updResult; // try { // updResult = String.valueOf(facade.updatePatMedInhosp( // new com.ruoyi.inhospvisit.domain.PatMedInhosp())); // } catch (StackOverflowError e) { // updResult = "StackOverflowError"; // } // check("关闭时【数据写入】仍可用且返回真实行数(不受开关管辖)", "7", updResult); // check("关闭时【数据写入】确实转发到数据服务(调用次数 = 1)", "1", // String.valueOf(StubDataService.CALLS.get())); // // InhospVisitAvailabilityInterceptor interceptor = ctx.getBean(InhospVisitAvailabilityInterceptor.class); // String thrown = "(未抛异常)"; // try { // interceptor.preHandle(null, null, null); // } catch (Exception e) { // thrown = e.getClass().getSimpleName() + "|" + e.getMessage(); // } // check("关闭时拦截器抛 ServiceException", "true", String.valueOf(thrown.startsWith("ServiceException|"))); // check("关闭时错误提示明确(含键名与「接口不可用」)", "true", // String.valueOf(thrown.contains("inhospvisit.enabled=false") && thrown.contains("接口不可用"))); // check("关闭时错误提示说明定时任务不受开关控制", "true", // String.valueOf(thrown.contains("定时任务不受此开关控制"))); // // check("关闭时受保护路径 2 条(阶段一:不含 addOperationSubTask)", "2", // String.valueOf(InhospVisitWebConfig.guardedPathPatterns().size())); // check("受保护路径不含出入院记录 CRUD 端点", "false", // String.valueOf(InhospVisitWebConfig.guardedPathPatterns().stream() // .anyMatch(p -> p.contains("selectPatMedInhospList") || p.endsWith("/add") // || p.endsWith("/edit") || p.endsWith("/export")))); // } // // System.out.println(); // System.out.println("=== 场景 4:参数表【没有】inhospvisit.enabled 这一行时,回落 application.yml ==="); // try (AnnotationConfigApplicationContext ctx = context(true, true, SYS_MISSING)) { // InhospVisitFacade facade = ctx.getBean(InhospVisitFacade.class); // check("参数行缺失、yml 默认 true 时 isEnabled() 为 true", "true", String.valueOf(facade.isEnabled())); // } // try (AnnotationConfigApplicationContext ctx = context(false, true, SYS_MISSING)) { // InhospVisitFacade facade = ctx.getBean(InhospVisitFacade.class); // check("参数行缺失、yml=false 时 isEnabled() 为 false", "false", String.valueOf(facade.isEnabled())); // } // // System.out.println(); // System.out.println("=== 场景 5:容器里【没有】IConfigService 时(独立/测试环境)必须能启动且按 yml 兜底 ==="); // try (AnnotationConfigApplicationContext ctx = context(true, true, null, false)) { // InhospVisitFacade facade = ctx.getBean(InhospVisitFacade.class); // check("无配置服务时门面仍能装配并启动", "true", String.valueOf(facade != null)); // check("无配置服务时按 yml 兜底(true)", "true", String.valueOf(facade.isEnabled())); // } // // System.out.println(); // if (failed == 0) { // System.out.println("===== 出入院随访模块 Spring 装配校验全部通过 ====="); // } else { // System.out.println("===== 出入院随访模块 Spring 装配校验失败 " + failed + " 项 ====="); // System.exit(1); // } // } // // /** // * 参数表的模拟取值 // */ // static final class Cfg { // /** 参数行存在且为 true */ // static final String SYS_ON = "true"; // /** 参数行存在且为 false */ // static final String SYS_OFF = "false"; // /** 参数行不存在 */ // static final String SYS_MISSING = null; // } // // private static final String SYS_ON = Cfg.SYS_ON; // private static final String SYS_OFF = Cfg.SYS_OFF; // private static final String SYS_MISSING = Cfg.SYS_MISSING; // // private static AnnotationConfigApplicationContext context(boolean ymlEnabled, // boolean withImpl, // String sysConfigValue) { // return context(ymlEnabled, withImpl, sysConfigValue, true); // } // // private static AnnotationConfigApplicationContext context(boolean ymlEnabled, // boolean withImpl, // String sysConfigValue, // boolean withConfigService) { // AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); // InhospVisitProperties properties = new InhospVisitProperties(); // properties.setEnabled(ymlEnabled); // ctx.registerBean(InhospVisitProperties.class, () -> properties); // ctx.register(InhospVisitFacade.class, // InhospVisitSwitch.class, // InhospVisitAvailabilityInterceptor.class, // InhospVisitWebConfig.class, // ProviderProbe.class, // StubDataService.class); // if (withConfigService) { // // 假的参数配置服务:只认 inhospvisit.enabled 这一个键,值由 sysConfigValue 决定 // ctx.registerBean("stubConfigService", IConfigService.class, // () -> (IConfigService) configKey -> // InhospVisitSwitch.CONFIG_KEY.equals(configKey) ? sysConfigValue : ""); // } // if (withImpl) { // ctx.registerBean("stubInhospVisitApi", InhospVisitApi.class, StubApi::new); // } // ctx.refresh(); // return ctx; // } // // /** // * 顶替 {@code DefaultInhospVisitApi} 的桩:只回填调用次数与固定返回值,不碰数据库。 // * 这样本校验可以在没有数据源的环境里跑。 // */ // public static class StubApi implements InhospVisitApi { // // /** // * 调用次数:用来证明"开关关闭时门面确实没有转发给实现" // */ // static final AtomicInteger CALLS = new AtomicInteger(); // // @Override // public boolean isEnabled() { // return true; // } // // @Override // public int dealOutHospInfo(String config) { // CALLS.incrementAndGet(); // return 1; // } // // @Override // public int dealOutHospInfoBySchedule(String config) { // CALLS.incrementAndGet(); // return 1; // } // // /** // * 补齐 {@link InhospVisitApi} 的抽象方法。 // *

// * ⚠️ 这两个方法此前在本桩里缺失,导致本测试类从未编译成功 // * ({@code target/test-classes} 一直是空的)——即"装配校验"实际上一次都没跑起来过。 // * 这是被发现的既有缺陷,此处补上。 // */ // @Override // public int addOperationSubTask(String config) { // CALLS.incrementAndGet(); // return 1; // } // // /** // * 补齐 {@link InhospVisitApi} 的抽象方法(见 {@link #addOperationSubTask})。 // *

// * 门诊随访属第二批 {@code ruoyi-outpatientvisit},桩里只记调用并返回 0。 // */ // @Override // public int dealOutpatientInfo() { // CALLS.incrementAndGet(); // return 0; // } // // @Override // public int rollTaskGroupCycle() { // CALLS.incrementAndGet(); // return 0; // } // // @Override // public VisitAgainSubtaskDto buildAgainVisitDraft(String orgid, Long inhospid, Long newTaskId, Long sendstate) { // CALLS.incrementAndGet(); // return InhospVisitFacade.emptyAgainDraft("stub"); // } // // @Override // public boolean generatePublicitySubtask(Long taskid, Long patid, Long inhospid, int type, String config) { // CALLS.incrementAndGet(); // return true; // } // } // // /** // * 探针:以"旁观者"身份注入 {@code ObjectProvider},看容器按类型能找到谁。 // *

// * 它不是 {@code InhospVisitApi} 的实现,所以不存在"自引用被排除"的情况, // * 因此它看到的就是完整的候选集合 —— 用来证明"没有内部实现时容器会把门面自己当候选"。 // */ // public static class ProviderProbe { // // @org.springframework.beans.factory.annotation.Autowired // private ObjectProvider provider; // // InhospVisitApi rawCandidate() { // return provider.stream().findFirst().orElse(null); // } // } // // // ====================================================================== // // 基础出入院信息数据通路(InhospVisitDataService) // // // // 为什么必须单独验: // // 门面同时实现了 InhospVisitDataService,而 Spring 的构造注入是按类型找候选。 // // 早先的写法(直接注入接口类型)在真实容器里会命中"正在创建的门面自己", // // 抛 BeanCurrentlyInCreationException —— 这个缺陷【只有真起容器】才能发现, // // 静态检查与编译都发现不了(满意度模块踩过同类坑)。 // // 因此这里用桩数据服务把这条通路钉住: // // · 门面能装配(不自引用); // // · 数据方法确实转发到数据服务,且【不受 inhospvisit.enabled 开关管辖】。 // // ====================================================================== // // /** // * 顶替 {@code DefaultInhospVisitDataService} 的桩:只记调用次数,不碰数据库。 // *

// * ⚠️ 本桩只实现接口(不继承 {@link DefaultInhospVisitDataService}): // * 真实实现类带一个 {@code @Autowired PatMedInhospMapper} 字段,继承它就会把 // * "需要数据源"这个耦合带进本校验,而本校验的整个前提就是不起数据库 // * (极简上下文里没有 Mapper Bean,继承版会在 refresh 时抛 NoSuchBeanDefinitionException)。 // *

// * 门面因此按【接口】{@code ObjectProvider} 取候选并过滤自己, // * 既能取到本桩,也不会自引用/循环依赖。 // */ // public static class StubDataService implements InhospVisitDataService { // // static final AtomicInteger CALLS = new AtomicInteger(); // // @Override // public com.ruoyi.inhospvisit.domain.PatMedInhosp selectPatMedInhospByInhospid(Long inhospid) { // CALLS.incrementAndGet(); // com.ruoyi.inhospvisit.domain.PatMedInhosp row = new com.ruoyi.inhospvisit.domain.PatMedInhosp(); // row.setInhospid(inhospid); // return row; // } // // @Override // public int updatePatMedInhosp(com.ruoyi.inhospvisit.domain.PatMedInhosp patMedInhosp) { // CALLS.incrementAndGet(); // return 7; // } // // // ---- 其余方法:本校验不涉及,返回安全空值 ---- // // @Override // public java.util.List selectPatMedInhosp( // com.ruoyi.inhospvisit.domain.PatMedInhosp q) { // return java.util.Collections.emptyList(); // } // // @Override // public java.util.List selectOutInhospByPatnoAndOutDate( // String patno, java.util.Date dayStart, java.util.Date dayEnd) { // return java.util.Collections.emptyList(); // } // // @Override // public String getTagnameBypatid(Long patid) { // return null; // } // // @Override // public java.util.List> getTagnameByPatids(java.util.List patids) { // return java.util.Collections.emptyList(); // } // // @Override // public java.util.List selectPatMedInhospList( // com.ruoyi.inhospvisit.domain.PatMedInhosp q) { // return java.util.Collections.emptyList(); // } // // @Override // public java.util.List selectPatMedInhospListBySerialnum( // com.ruoyi.inhospvisit.domain.PatMedInhosp q) { // return java.util.Collections.emptyList(); // } // // @Override // public java.util.List selectPatMedInhospListByCondition( // com.ruoyi.inhospvisit.domain.PatMedInhospVO vo) { // return java.util.Collections.emptyList(); // } // // @Override // public com.ruoyi.common.core.service.dto.PatMedRes selectPatMedInhospCount( // com.ruoyi.common.core.service.dto.PatMedReq req) { // return null; // } // // @Override // public java.util.List selectNeedProcessList() { // return java.util.Collections.emptyList(); // } // // @Override // public int insertPatMedInhosp(com.ruoyi.inhospvisit.domain.PatMedInhosp p) { // return 0; // } // // @Override // public int insertPatMedInhospBatch(java.util.List list) { // return 0; // } // // @Override // public int deletePatMedInhospByInhospid(Long inhospid) { // return 0; // } // // @Override // public int deletePatMedInhospByInhospno(String inhospno) { // return 0; // } // // @Override // public int deletePatMedInhospByInhospids(Long[] inhospids) { // return 0; // } // } //}