liusheng
9 天以前 a1e256f64930865ae2d170e333ee3f1a773c6c49
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
package com.ruoyi.satisfaction.api;
 
import com.ruoyi.common.core.service.IConfigService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.satisfaction.config.SatisfactionAvailabilityInterceptor;
import com.ruoyi.satisfaction.config.SatisfactionProperties;
import com.ruoyi.satisfaction.config.SatisfactionSwitch;
import com.ruoyi.satisfaction.config.SatisfactionWebConfig;
import com.ruoyi.satisfaction.service.strategy.CommonSatisfactionScoreStrategy;
import com.ruoyi.satisfaction.service.strategy.SatisfactionScoreStrategyResolver;
import com.ruoyi.satisfaction.service.strategy.SatisfactionSites;
import com.ruoyi.satisfaction.service.strategy.SltdSatisfactionScoreStrategy;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
 
import java.util.Collections;
import java.util.List;
import java.util.Map;
 
/**
 * 满意度模块的 <b>真实 Spring 容器</b>装配校验(不依赖 JUnit,也不依赖数据库)。
 * <p>
 * 为什么需要这个检查:本模块的几个"静态校验"(Mapper 一致性、逐行等价、策略等价)都只能证明
 * <b>代码内容</b>正确,证明不了 <b>Spring 能把 Bean 装起来</b>。实际踩过两个坑:
 * <ol>
 *   <li>{@link SatisfactionFacade} 有两个公开构造器,Spring 因"构造器不唯一"退回找无参构造器
 *       → 启动期 {@code No default constructor found};</li>
 *   <li>{@code SatisfactionApi} 的按类型查找会把门面<b>自己</b>当兜底候选
 *       → 门面把请求转发给自己 → {@code StackOverflowError}。</li>
 * </ol>
 * 所以这里直接起一个真实的 {@link AnnotationConfigApplicationContext},只注册本模块
 * <b>不依赖 Mapper / 数据库</b>的那些 Bean,验证装配与开关行为。
 * <p>
 * 开关本身存在 {@code sys_config} 参数表里(键 {@code satisfaction.enabled},运行时热读),
 * 因此这里用一个假的 {@link IConfigService} 模拟参数表,验证「参数表优先、yml 兜底」。
 */
public final class SatisfactionContextCheck {
 
    private static int failed = 0;
 
    private SatisfactionContextCheck() {
    }
 
    /**
     * 假的内部实现(真实场景里是 DefaultSatisfactionApi,它需要 Mapper/Redis,起不了轻量容器)
     */
    public static class StubApi implements SatisfactionApi {
 
        /**
         * 分类查询被调用的次数:用来证明"参数表关闭时门面确实没有转发给实现"
         */
        static final java.util.concurrent.atomic.AtomicInteger CALLS = new java.util.concurrent.atomic.AtomicInteger();
 
        @Override
        public boolean isEnabled() {
            return true;
        }
 
        @Override
        public Map<String, Object> getSatisfactionCategories(String orgid) {
            CALLS.incrementAndGet();
            return Collections.singletonMap("fromStub", orgid);
        }
 
        @Override
        public SatisfactionExportBundle getQuestionOptionRatioExport(List<Long> subtaskIds, String questiontext) {
            return SatisfactionExportBundle.empty();
        }
 
        @Override
        public boolean hasSatisfactionCategory(String orgid) {
            return true;
        }
 
        @Override
        public Map<String, Object> getSatisfactionIndex(String type, String code, String orgid,
                                                        Long ivrCategoryid, Long svyCategoryid) {
            return SatisfactionFacade.emptySatisfactionIndex();
        }
    }
 
    /**
     * 探针:以"旁观者"身份注入 {@code ObjectProvider<SatisfactionApi>},看容器按类型能找到谁。
     * <p>
     * 它不是 {@code SatisfactionApi} 的实现,所以不存在"自引用被排除"的情况,
     * 因此它看到的就是完整的候选集合 —— 用来证明"模块关闭时容器会把门面自己当候选"。
     */
    public static class ProviderProbe {
 
        @org.springframework.beans.factory.annotation.Autowired
        private org.springframework.beans.factory.ObjectProvider<SatisfactionApi> provider;
 
        SatisfactionApi rawCandidate() {
            return provider.getIfAvailable();
        }
    }
 
    public static void main(String[] args) {
        System.out.println("=== 场景 1:参数表 satisfaction.enabled=true,且存在内部实现(DefaultSatisfactionApi 的角色)===");
        try (AnnotationConfigApplicationContext ctx = context(true, true, ContextCheckConfig.SYS_ON)) {
            SatisfactionFacade facade = ctx.getBean(SatisfactionFacade.class);
            check("门面能被 Spring 实例化(构造器不再歧义)", "true", String.valueOf(facade != null));
            check("启用时 isEnabled()", "true", String.valueOf(facade.isEnabled()));
            check("取到的是【内部实现】而不是门面自己(stub 把 orgid 原样回填)", "ORG1",
                    String.valueOf(facade.getSatisfactionCategories("ORG1").get("fromStub")));
            check("门面同时是 ISatisfactionCategoryService",
                    "true", String.valueOf(ctx.getBean(com.ruoyi.common.core.service.ISatisfactionCategoryService.class) == facade));
            // 容器里应有 2 个 SatisfactionApi:门面自己 + 内部实现(门面也实现该接口,这是设计使然)
            check("容器内 SatisfactionApi 实现数 = 门面 1 + 内部实现 1", "2",
                    String.valueOf(ctx.getBeansOfType(SatisfactionApi.class).size()));
            check("开关读取的键名是 sys_config 的 satisfaction.enabled", SatisfactionSwitch.CONFIG_KEY,
                    String.valueOf(SatisfactionSwitch.CONFIG_KEY));
 
            SatisfactionScoreStrategyResolver resolver = ctx.getBean(SatisfactionScoreStrategyResolver.class);
            check("站点策略解析器装配成功,支持站点 = [common, sltd]", "[common, sltd]",
                    String.valueOf(resolver.supportedSites()));
            check("未知站点回落到通用口径", SatisfactionSites.COMMON,
                    resolver.resolve("不存在的站点").site());
        }
 
        System.out.println();
        System.out.println("=== 场景 2:参数表 satisfaction.enabled=true,但【没有】内部实现(模块没被打进应用)===");
        try (AnnotationConfigApplicationContext ctx = context(true, false, ContextCheckConfig.SYS_ON)) {
            SatisfactionFacade facade = ctx.getBean(SatisfactionFacade.class);
            check("无实现时 isEnabled() 为 false", "false", String.valueOf(facade.isEnabled()));
            // 关键:不能把门面自己当实现,否则这里会 StackOverflowError
            String result;
            try {
                result = String.valueOf(facade.getSatisfactionCategories("ORG1").isEmpty());
            } catch (StackOverflowError e) {
                result = "StackOverflowError";
            }
            check("无实现时分类查询返回空 Map(不会自引用递归)", "true", result);
            check("无实现时占比导出包为空", "true",
                    String.valueOf(facade.getQuestionOptionRatioExport(Collections.singletonList(1L), null).isEmpty()));
            check("无实现时满意度指数为全零 Map", "0",
                    String.valueOf(facade.getSatisfactionIndex("1", "D001", "ORG1", null, null).get("mydtmzl")));
 
            // 反证:直接问容器"按类型有哪些 SatisfactionApi 候选",看它是否把门面自己算进去。
            // 若这里返回的是门面自己,就说明"自引用兜底"确实存在,门面里的 self 过滤是必要的。
            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:参数表 satisfaction.enabled=false(实现 Bean 依然存在,但门面必须不转发)===");
        StubApi.CALLS.set(0);
        try (AnnotationConfigApplicationContext ctx = context(true, true, ContextCheckConfig.SYS_OFF)) {
            SatisfactionFacade facade = ctx.getBean(SatisfactionFacade.class);
            check("关闭时 isEnabled() 为 false", "false", String.valueOf(facade.isEnabled()));
            String result;
            try {
                result = String.valueOf(facade.getSatisfactionCategories("ORG1").isEmpty());
            } catch (StackOverflowError e) {
                result = "StackOverflowError";
            }
            check("关闭时分类查询返回空 Map(不会自递归)", "true", result);
            check("关闭时不再调用内部实现(stub 调用次数 = 0)", "0",
                    String.valueOf(StubApi.CALLS.get()));
 
            SatisfactionAvailabilityInterceptor interceptor = ctx.getBean(SatisfactionAvailabilityInterceptor.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("satisfaction.enabled=false") && thrown.contains("接口不可用")));
 
            check("关闭时受保护路径规则 7 条", "7",
                    String.valueOf(SatisfactionWebConfig.guardedPathPatterns().size()));
        }
 
        System.out.println();
        System.out.println("=== 场景 4:参数表【没有】satisfaction.enabled 这一行时,回落 application.yml(默认 true)===");
        try (AnnotationConfigApplicationContext ctx = context(true, true, ContextCheckConfig.SYS_MISSING)) {
            SatisfactionFacade facade = ctx.getBean(SatisfactionFacade.class);
            check("参数行缺失、yml 默认 true 时 isEnabled() 为 true", "true", String.valueOf(facade.isEnabled()));
        }
        try (AnnotationConfigApplicationContext ctx = context(false, true, ContextCheckConfig.SYS_MISSING)) {
            SatisfactionFacade facade = ctx.getBean(SatisfactionFacade.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)) {
            SatisfactionFacade facade = ctx.getBean(SatisfactionFacade.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 ContextCheckConfig {
        /** 参数行存在且为 true */
        static final String SYS_ON = "true";
        /** 参数行存在且为 false */
        static final String SYS_OFF = "false";
        /** 参数行不存在 */
        static final String SYS_MISSING = null;
 
        private ContextCheckConfig() {
        }
    }
 
    /**
     * 起一个只含本模块"无 Mapper 依赖"Bean 的真实容器
     *
     * @param ymlEnabled        application.yml 里 satisfaction.enabled 的取值(兜底用)
     * @param withDelegate      是否注册内部实现(DefaultSatisfactionApi 的角色)
     * @param sysConfigValue    模拟 sys_config 里 satisfaction.enabled 的键值,null=没有这一行
     */
    private static AnnotationConfigApplicationContext context(boolean ymlEnabled, boolean withDelegate,
                                                              final String sysConfigValue) {
        return context(ymlEnabled, withDelegate, sysConfigValue, true);
    }
 
    /**
     * @param withConfigService 容器里是否注册 {@link IConfigService}(false 模拟独立/测试环境)
     */
    private static AnnotationConfigApplicationContext context(boolean ymlEnabled, boolean withDelegate,
                                                              final String sysConfigValue, boolean withConfigService) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
        SatisfactionProperties properties = new SatisfactionProperties();
        properties.setEnabled(ymlEnabled);
        ctx.registerBean(SatisfactionProperties.class, () -> properties);
        ctx.register(SatisfactionFacade.class,
                SatisfactionSwitch.class,
                SatisfactionAvailabilityInterceptor.class,
                SatisfactionWebConfig.class,
                SatisfactionScoreStrategyResolver.class,
                CommonSatisfactionScoreStrategy.class,
                SltdSatisfactionScoreStrategy.class,
                ProviderProbe.class);
        if (withConfigService) {
            // 假的参数配置服务:只认 satisfaction.enabled 这一个键,值由 sysConfigValue 决定
            ctx.registerBean("stubConfigService", IConfigService.class, () -> (IConfigService) configKey ->
                    SatisfactionSwitch.CONFIG_KEY.equals(configKey) ? sysConfigValue : "");
        }
        if (withDelegate) {
            ctx.registerBean("stubSatisfactionApi", SatisfactionApi.class, StubApi::new);
        }
        ctx.refresh();
        return ctx;
    }
 
    private static void check(String key, String expected, String actual) {
        boolean ok = expected.equals(actual);
        if (!ok) {
            failed++;
        }
        System.out.println("[" + (ok ? "OK" : "FAIL") + "]   " + key + "  期望=" + expected + "  实际=" + actual);
    }
}