liusheng
5 天以前 0bbbd746e8c39d91ad7b47a4e36e29ab972dc98c
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
package com.ruoyi.publicity.api;
 
import com.ruoyi.common.core.service.IConfigService;
import com.ruoyi.common.core.service.PublicityApi;
import com.ruoyi.publicity.config.PublicityProperties;
import com.ruoyi.publicity.config.PublicitySwitch;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
 
import java.util.concurrent.atomic.AtomicInteger;
 
/**
 * 宣教模块的 <b>Spring 容器装配校验</b>(不依赖 JUnit、不依赖数据库)。
 *
 * <p>与 {@code InhospVisitContextCheck} 同一套做法:用真实的(但极简的)
 * {@link AnnotationConfigApplicationContext} 验证"Spring 能把 Bean 装起来"。
 * 断言覆盖满意度模块实际踩过的两个坑:
 * <ol>
 *   <li>门面有两个公开构造器却没标 {@code @Autowired} → 启动报 {@code No default constructor found};</li>
 *   <li>门面把自己当实现 → 调用时无限递归 → {@code StackOverflowError}。</li>
 * </ol>
 *
 * <h3>运行方式</h3>
 * <pre>
 * mvn -o -q -pl ruoyi-publicity -am test-compile -DskipTests
 * # 然后用模块 classpath 直接跑 main(surefire 认不出它,因为没有 @Test)
 * </pre>
 */
public class PublicityContextCheck {
 
    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++;
        }
    }
 
    /** 参数表的模拟取值 */
    private static final String SYS_ON = "true";
    private static final String SYS_OFF = "false";
    private static final String SYS_MISSING = null;
 
    public static void main(String[] args) {
        System.out.println("=== 场景 1:参数表 publicity.enabled=true,且存在内部实现 ===");
        try (AnnotationConfigApplicationContext ctx = context(true, true, SYS_ON)) {
            PublicityFacade facade = ctx.getBean(PublicityFacade.class);
            check("门面能被 Spring 实例化(构造器不再歧义)", "true", String.valueOf(facade != null));
            check("启用时 isEnabled()", "true", String.valueOf(facade.isEnabled()));
            check("取到的是【内部实现】而不是门面自己", "1", String.valueOf(facade.dealInHospPublicity("7")));
            check("容器内 PublicityApi 实现数 = 门面 1 + 内部实现 1", "2",
                    String.valueOf(ctx.getBeansOfType(PublicityApi.class).size()));
            check("开关读取的键名是 sys_config 的 publicity.enabled", "publicity.enabled",
                    PublicitySwitch.CONFIG_KEY);
        }
 
        System.out.println();
        System.out.println("=== 场景 2:参数表=true,但【没有】内部实现(模块没被打进应用)===");
        try (AnnotationConfigApplicationContext ctx = context(true, false, SYS_ON)) {
            PublicityFacade facade = ctx.getBean(PublicityFacade.class);
            check("无实现时 isEnabled() 为 false", "false", String.valueOf(facade.isEnabled()));
            String result;
            try {
                result = String.valueOf(facade.dealInHospPublicity("7"));
            } catch (StackOverflowError e) {
                result = "StackOverflowError";
            }
            check("无实现时入院宣教安全返回 0(不会自引用递归)", "0", result);
            String result2;
            try {
                result2 = String.valueOf(facade.dealMedicalPublicity("7"));
            } catch (StackOverflowError e) {
                result2 = "StackOverflowError";
            }
            check("无实现时药品宣教安全返回 0(不会自引用递归)", "0", result2);
        }
 
        System.out.println();
        System.out.println("=== 场景 3:参数表 publicity.enabled=false(实现 Bean 依然存在,但门面必须不转发)===");
        StubApi.CALLS.set(0);
        try (AnnotationConfigApplicationContext ctx = context(true, true, SYS_OFF)) {
            PublicityFacade facade = ctx.getBean(PublicityFacade.class);
            check("关闭时 isEnabled() 为 false", "false", String.valueOf(facade.isEnabled()));
            check("关闭时入院宣教返回 0", "0", String.valueOf(facade.dealInHospPublicity("7")));
            check("关闭时药品宣教返回 0", "0", String.valueOf(facade.dealMedicalPublicity("7")));
            check("关闭时不再调用内部实现(stub 调用次数 = 0)", "0",
                    String.valueOf(StubApi.CALLS.get()));
        }
 
        System.out.println();
        System.out.println("=== 场景 4:参数表【没有】publicity.enabled 这一行时,回落 application.yml ===");
        try (AnnotationConfigApplicationContext ctx = context(true, true, SYS_MISSING)) {
            check("参数行缺失、yml 默认 true 时 isEnabled() 为 true", "true",
                    String.valueOf(ctx.getBean(PublicityFacade.class).isEnabled()));
        }
        try (AnnotationConfigApplicationContext ctx = context(false, true, SYS_MISSING)) {
            check("参数行缺失、yml=false 时 isEnabled() 为 false", "false",
                    String.valueOf(ctx.getBean(PublicityFacade.class).isEnabled()));
        }
 
        System.out.println();
        System.out.println("=== 场景 5:容器里【没有】IConfigService 时(独立/测试环境)必须能启动且按 yml 兜底 ===");
        try (AnnotationConfigApplicationContext ctx = context(true, true, null, false)) {
            PublicityFacade facade = ctx.getBean(PublicityFacade.class);
            check("无配置服务时门面仍能装配并启动", "true", String.valueOf(facade != null));
            check("无配置服务时按 yml 兜底(true)", "true", String.valueOf(facade.isEnabled()));
        }
 
        System.out.println();
        System.out.println("=== 场景 6:controller + 开关拦截器 + WebConfig(第 16 步新增)===");
        try (AnnotationConfigApplicationContext ctx = context(true, true, SYS_OFF)) {
            // 受保护路径:只有两个"会生成宣教"的端点,/status 刻意不挂
            String[] guarded = com.ruoyi.publicity.config.PublicityWebConfig.guardedPathPatterns();
            check("受保护路径 2 条(两个生成端点)", "2", String.valueOf(guarded.length));
            boolean hasIn = false, hasMed = false, hasStatus = false;
            for (String p : guarded) {
                if (com.ruoyi.publicity.config.PublicityWebConfig.DEAL_IN_HOSP_PUBLICITY_ENDPOINT.equals(p)) hasIn = true;
                if (com.ruoyi.publicity.config.PublicityWebConfig.DEAL_MEDICAL_PUBLICITY_ENDPOINT.equals(p)) hasMed = true;
                if (com.ruoyi.publicity.config.PublicityWebConfig.STATUS_ENDPOINT_PENDING.equals(p)) hasStatus = true;
            }
            check("含 /dealInHospPublicity", "true", String.valueOf(hasIn));
            check("含 /dealMedicalPublicity", "true", String.valueOf(hasMed));
            check("不含 /status(只读诊断入口必须一直可用)", "false", String.valueOf(hasStatus));
 
            // 关闭时拦截器必须抛明确业务异常(而不是静默放行)
            com.ruoyi.publicity.config.PublicityAvailabilityInterceptor it =
                    ctx.getBean(com.ruoyi.publicity.config.PublicityAvailabilityInterceptor.class);
            String thrown = "(未抛异常)";
            try {
                it.preHandle(null, null, null);
            } catch (Throwable t) {
                thrown = t.getClass().getSimpleName();
            }
            check("关闭时拦截器抛 ServiceException", "ServiceException", thrown);
            check("关闭时错误提示含键名 publicity.enabled",
                    "true", String.valueOf(
                            com.ruoyi.publicity.config.PublicityAvailabilityInterceptor.DISABLED_MESSAGE
                                    .contains("publicity.enabled")));
            check("关闭时错误提示点明与 in.helibrary.switch 是两回事",
                    "true", String.valueOf(
                            com.ruoyi.publicity.config.PublicityAvailabilityInterceptor.DISABLED_MESSAGE
                                    .contains("in.helibrary.switch")));
        }
        // 开启时必须放行
        try (AnnotationConfigApplicationContext ctx = context(true, true, SYS_ON)) {
            com.ruoyi.publicity.config.PublicityAvailabilityInterceptor it =
                    ctx.getBean(com.ruoyi.publicity.config.PublicityAvailabilityInterceptor.class);
            check("开启时拦截器放行", "true", String.valueOf(it.preHandle(null, null, null)));
        }
 
        System.out.println();
        if (failed == 0) {
            System.out.println("===== 宣教模块 Spring 装配校验全部通过 =====");
        } else {
            System.out.println("===== 宣教模块 Spring 装配校验失败 " + failed + " 项 =====");
            System.exit(1);
        }
    }
 
    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();
        PublicityProperties properties = new PublicityProperties();
        properties.setEnabled(ymlEnabled);
        ctx.registerBean(PublicityProperties.class, () -> properties);
        ctx.register(PublicityFacade.class, PublicitySwitch.class,
                com.ruoyi.publicity.config.PublicityAvailabilityInterceptor.class,
                com.ruoyi.publicity.config.PublicityWebConfig.class);
        if (withConfigService) {
            // 假的参数配置服务:只认 publicity.enabled 这一个键,值由 sysConfigValue 决定
            ctx.registerBean("stubConfigService", IConfigService.class,
                    () -> (IConfigService) configKey ->
                            PublicitySwitch.CONFIG_KEY.equals(configKey) ? sysConfigValue : "");
        }
        if (withImpl) {
            ctx.registerBean("stubPublicityApi", PublicityApi.class, StubApi::new);
        }
        ctx.refresh();
        return ctx;
    }
 
    /**
     * 顶替 {@code DefaultPublicityApi} 的桩:只回填调用次数与固定返回值,不碰数据库
     */
    public static class StubApi implements PublicityApi {
 
        /** 调用次数:用来证明"开关关闭时门面确实没有转发给实现" */
        static final AtomicInteger CALLS = new AtomicInteger();
 
        @Override
        public int dealInHospPublicity(String config) {
            CALLS.incrementAndGet();
            return 1;
        }
 
        @Override
        public int dealMedicalPublicity(String config) {
            CALLS.incrementAndGet();
            return 1;
        }
    }
}