//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;
|
//
|
///**
|
// * 出入院随访模块的 <b>Spring 容器装配校验</b>(不依赖 JUnit、不依赖数据库)。
|
// *
|
// * <p>照抄 {@code SatisfactionContextCheck} 的做法:用真实的(但极简的)
|
// * {@link AnnotationConfigApplicationContext} 验证"Spring 能把 Bean 装起来" ——
|
// * 静态检查证明不了装配。满意度模块当时有<b>两个真实缺陷</b>只有真起容器才能发现:
|
// * <ol>
|
// * <li>门面有两个公开构造器却没标 {@code @Autowired} → 启动报 {@code No default constructor found};</li>
|
// * <li>门面把自己当实现 → 调用时无限递归 → {@code StackOverflowError}。</li>
|
// * </ol>
|
// * 本类把这两条都做成断言。
|
// *
|
// * <h3>运行方式(仓库内没有现成脚本)</h3>
|
// * <pre>
|
// * mvn -o -q -pl ruoyi-inhospvisit -am test-compile -DskipTests
|
// * # 然后用模块 classpath 直接跑 main(surefire 认不出它,因为没有 @Test)
|
// * </pre>
|
// * 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} 的抽象方法。
|
// * <p>
|
// * ⚠️ 这两个方法此前在本桩里<b>缺失</b>,导致本测试类从未编译成功
|
// * ({@code target/test-classes} 一直是空的)——即"装配校验"实际上一次都没跑起来过。
|
// * 这是被发现的既有缺陷,此处补上。
|
// */
|
// @Override
|
// public int addOperationSubTask(String config) {
|
// CALLS.incrementAndGet();
|
// return 1;
|
// }
|
//
|
// /**
|
// * 补齐 {@link InhospVisitApi} 的抽象方法(见 {@link #addOperationSubTask})。
|
// * <p>
|
// * 门诊随访属第二批 {@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<InhospVisitApi>},看容器按类型能找到谁。
|
// * <p>
|
// * 它不是 {@code InhospVisitApi} 的实现,所以不存在"自引用被排除"的情况,
|
// * 因此它看到的就是完整的候选集合 —— 用来证明"没有内部实现时容器会把门面自己当候选"。
|
// */
|
// public static class ProviderProbe {
|
//
|
// @org.springframework.beans.factory.annotation.Autowired
|
// private ObjectProvider<InhospVisitApi> provider;
|
//
|
// InhospVisitApi rawCandidate() {
|
// return provider.stream().findFirst().orElse(null);
|
// }
|
// }
|
//
|
// // ======================================================================
|
// // 基础出入院信息数据通路(InhospVisitDataService)
|
// //
|
// // 为什么必须单独验:
|
// // 门面同时实现了 InhospVisitDataService,而 Spring 的构造注入是按类型找候选。
|
// // 早先的写法(直接注入接口类型)在真实容器里会命中"正在创建的门面自己",
|
// // 抛 BeanCurrentlyInCreationException —— 这个缺陷【只有真起容器】才能发现,
|
// // 静态检查与编译都发现不了(满意度模块踩过同类坑)。
|
// // 因此这里用桩数据服务把这条通路钉住:
|
// // · 门面能装配(不自引用);
|
// // · 数据方法确实转发到数据服务,且【不受 inhospvisit.enabled 开关管辖】。
|
// // ======================================================================
|
//
|
// /**
|
// * 顶替 {@code DefaultInhospVisitDataService} 的桩:只记调用次数,不碰数据库。
|
// * <p>
|
// * ⚠️ 本桩只实现接口(<b>不继承</b> {@link DefaultInhospVisitDataService}):
|
// * 真实实现类带一个 {@code @Autowired PatMedInhospMapper} 字段,继承它就会把
|
// * "需要数据源"这个耦合带进本校验,而本校验的整个前提就是<b>不起数据库</b>
|
// * (极简上下文里没有 Mapper Bean,继承版会在 refresh 时抛 NoSuchBeanDefinitionException)。
|
// * <p>
|
// * 门面因此按【接口】{@code ObjectProvider<InhospVisitDataService>} 取候选并过滤自己,
|
// * 既能取到本桩,也不会自引用/循环依赖。
|
// */
|
// 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<com.ruoyi.inhospvisit.domain.PatMedInhosp> selectPatMedInhosp(
|
// com.ruoyi.inhospvisit.domain.PatMedInhosp q) {
|
// return java.util.Collections.emptyList();
|
// }
|
//
|
// @Override
|
// public java.util.List<com.ruoyi.inhospvisit.domain.PatMedInhosp> 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<java.util.Map<String, Object>> getTagnameByPatids(java.util.List<Long> patids) {
|
// return java.util.Collections.emptyList();
|
// }
|
//
|
// @Override
|
// public java.util.List<com.ruoyi.inhospvisit.domain.PatMedInhosp> selectPatMedInhospList(
|
// com.ruoyi.inhospvisit.domain.PatMedInhosp q) {
|
// return java.util.Collections.emptyList();
|
// }
|
//
|
// @Override
|
// public java.util.List<com.ruoyi.inhospvisit.domain.PatMedInhosp> selectPatMedInhospListBySerialnum(
|
// com.ruoyi.inhospvisit.domain.PatMedInhosp q) {
|
// return java.util.Collections.emptyList();
|
// }
|
//
|
// @Override
|
// public java.util.List<com.ruoyi.inhospvisit.domain.PatMedInhosp> 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<com.ruoyi.inhospvisit.domain.PatMedInhosp> 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<com.ruoyi.inhospvisit.domain.PatMedInhosp> 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;
|
// }
|
// }
|
//}
|