//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 装起来" —— // * 静态检查证明不了装配。满意度模块当时有两个真实缺陷只有真起容器才能发现: // *
// * 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
// * ⚠️ 本桩只实现接口(不继承 {@link DefaultInhospVisitDataService}):
// * 真实实现类带一个 {@code @Autowired PatMedInhospMapper} 字段,继承它就会把
// * "需要数据源"这个耦合带进本校验,而本校验的整个前提就是不起数据库
// * (极简上下文里没有 Mapper Bean,继承版会在 refresh 时抛 NoSuchBeanDefinitionException)。
// *
// * 门面因此按【接口】{@code ObjectProvider