liusheng
9 天以前 f0a09ee996ab2605869844d6a3d812709e2bf0fa
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
//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;
//        }
//    }
//}