yxh
7 天以前 68ff33beff11310e830b0615b5a81f83fad0f683
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
package cn.lihu.jh.module.ecg.service.his;
 
import org.junit.Test;
 
import java.util.Arrays;
import java.util.Collections;
 
import static org.junit.Assert.*;
 
/**
 * 心电业务单判定({@link HisEcgFilter})回归测试。
 *
 * <p>全部测试数据取自 2026-09-17 生产日志中的真实报文值:
 * <ul>
 *   <li><b>心电单</b>:{@code S0201ECG} / {@code S0405} 中属心电的号与检查项目;</li>
 *   <li><b>非心电单</b>:同一时间窗内 {@code S0405} 推送的放射/超声单
 *       (实测占 96.5%,是本次过滤要拦掉的主体)。</li>
 * </ul>
 *
 * @author 心电排队装机系统
 */
public class HisEcgFilterTest {
 
    private HisEcgFilter filter = new HisEcgFilter();
 
    // ==================== 依据 A:申请单号后缀 ====================
 
    @Test
    public void testSuffix_shouldMatchEcgApplyNo() {
        // 日志实测的心电申请单号(S0201ECG 创建)
        assertTrue(filter.matchApplyNoSuffix("8826091720001101"));
        assertTrue(filter.matchApplyNoSuffix("8826081120716401"));
        assertTrue(filter.matchApplyNoSuffix("8826091520521101"));
        assertTrue(filter.matchApplyNoSuffix("8826091420571101"));
    }
 
    @Test
    public void testSuffix_shouldNotMatchRadiologyApplyNo() {
        // 日志实测的放射/超声申请单号(S0405 推送,纯数字流水号,无心电后缀)
        assertFalse(filter.matchApplyNoSuffix("2126091602867"));
        assertFalse(filter.matchApplyNoSuffix("2126091700001"));
        assertFalse(filter.matchApplyNoSuffix("2126091700038"));
        assertFalse(filter.matchApplyNoSuffix("2126091700190"));
    }
 
    @Test
    public void testSuffix_shouldHandleBlank() {
        assertFalse(filter.matchApplyNoSuffix(null));
        assertFalse(filter.matchApplyNoSuffix(""));
        assertFalse(filter.matchApplyNoSuffix("   "));
    }
 
    // ==================== 依据 B:检查项目代码 / 名称 ====================
 
    @Test
    public void testCheckCode_shouldMatchWithAndWithoutPrefix() {
        // 日志实测:S0201ECG 报文的 component2...code.code = "Y1202065"(床边心电图)
        // HisCheckCodeEnum 的 code 为去前缀后的 "1202065"
        assertTrue(filter.matchCheckCode("Y1202065"));
        assertTrue(filter.matchCheckCode("1202065"));
        // 其他实心电项目
        assertTrue(filter.matchCheckCode("Y691133607"));
        assertTrue(filter.matchCheckCode("Y559542128"));
    }
 
    @Test
    public void testCheckCode_shouldNotMatchRadiologyCode() {
        // 日志实测:放射/超声项目的 jcxmcode(不在心电枚举内)
        assertFalse(filter.matchCheckCode("1080"));   // 胸部CT平扫
        assertFalse(filter.matchCheckCode("1393"));   // 右肘正侧位
        assertFalse(filter.matchCheckCode("1006"));   // 上下颌骨
        assertFalse(filter.matchCheckCode("35689493")); // 彩超子宫附件
    }
 
    @Test
    public void testCheckName_shouldMatchEcgNames() {
        // S0201ECG 报文中出现的心电检查项目名称
        assertTrue(filter.matchCheckName("常规心电图"));
        assertTrue(filter.matchCheckName("24小时动态心电图"));
        assertTrue(filter.matchCheckName("床边心电图(常规+频谱)"));
        assertTrue(filter.matchCheckName("常规心电图-心电向量图"));
    }
 
    @Test
    public void testCheckName_shouldNotMatchRadiologyNames() {
        // S0405 中出现的其他科室项目名称
        assertFalse(filter.matchCheckName("胸部CT平扫"));
        assertFalse(filter.matchCheckName("颅脑MRI平扫(含DWI)"));
        assertFalse(filter.matchCheckName("彩超肝胆胰脾"));
        assertFalse(filter.matchCheckName("骨密度"));
        assertFalse(filter.matchCheckName("右腕关节正侧位"));
    }
 
    // ==================== 依据 C:执行科室(可配置白名单) ====================
 
    @Test
    public void testDeptCode_notConfigured_shouldNotFilter() {
        // 未配置白名单时,依据 C 不参与判定,避免误判
        assertFalse(filter.matchDeptCode("YJ001"));
        assertFalse(filter.matchDeptCode("YJ002"));
    }
 
    @Test
    public void testDeptCode_configured_shouldMatch() {
        filter.setEcgDeptCodes(Arrays.asList("YJECG01", "YJECG02"));
        assertTrue(filter.matchDeptCode("YJECG01"));
        assertTrue(filter.matchDeptCode("yjecg02"));   // 忽略大小写
        assertFalse(filter.matchDeptCode("YJ001"));    // 放射科室不在白名单
    }
 
    @Test
    public void testDeptCode_shouldHandleEmptyWhitelistAndBlank() {
        filter.setEcgDeptCodes(Collections.emptyList());
        assertFalse(filter.matchDeptCode("YJ001"));
        filter.setEcgDeptCodes(Arrays.asList("YJECG01"));
        assertFalse(filter.matchDeptCode(null));
        assertFalse(filter.matchDeptCode(""));
    }
 
    // ==================== 综合判定 ====================
 
    @Test
    public void testJudge_ecgOrderBySuffix() {
        HisEcgFilter.Result r = filter.judge("8826091720001101", null, null, null);
        assertTrue(r.isEcg());
        assertTrue("应命中依据 A,实际=" + r.getReason(), r.getReason().startsWith("A:"));
    }
 
    @Test
    public void testJudge_ecgOrderByCheckCode() {
        // 申请单号不是心电后缀,但检查项目是心电 → 仍应判定为心电单
        HisEcgFilter.Result r = filter.judge("2126091700001", "Y1202065", null, null);
        assertTrue(r.isEcg());
        assertTrue("应命中依据 B,实际=" + r.getReason(), r.getReason().startsWith("B:"));
    }
 
    @Test
    public void testJudge_nonEcgRadiologyOrder() {
        // 典型放射单:CT 平扫 + 急诊CT室 → 三项都不命中
        HisEcgFilter.Result r = filter.judge("2126091602867", "1080", "胸部CT平扫", "YJ002");
        assertFalse("放射单不应被判为心电单", r.isEcg());
    }
 
    @Test
    public void testJudge_nonEcgUltrasoundOrder() {
        // 典型超声单
        HisEcgFilter.Result r = filter.judge("2126091700038", "35711720", "彩超宫颈管长度", "YJ001");
        assertFalse(r.isEcg());
    }
 
    @Test
    public void testIsEcgOrder_convenience() {
        assertTrue(filter.isEcgOrder("S0405", "8826091720001101", null, null, null));
        assertFalse(filter.isEcgOrder("S0405", "2126091602867", "1080", "胸部CT平扫", "YJ002"));
    }
 
    // ==================== 多值检查项目(S050401/S050501 的 ZDY.jcxmcode / jcxmname) ====================
 
    @Test
    public void testMatchCheckItems_shouldSplitCommaSeparated() {
        // 实测 S050401:非心电(DR 正侧位)
        assertFalse(filter.matchCheckItems("1393,1506", "右肘正侧位,左肩关节正侧位"));
        // 实测 S050501:非心电(彩超)
        assertFalse(filter.matchCheckItems("35672306,35672504,35672544,35672596",
                "彩超右下肢动脉,彩超右下肢静脉,彩超左下肢动脉,彩超左下肢静脉"));
    }
 
    @Test
    public void testMatchCheckItems_shouldMatchWhenAnyIsEcg() {
        // 混合场景:只要有一个是心电就算心电单
        assertTrue(filter.matchCheckItems("1393", "右肘正侧位,常规心电图"));
        assertTrue(filter.matchCheckItems("Y1202065", "彩超肝胆胰脾"));
    }
 
    // ==================== 关键字兜底(心电项目名均含「心电」) ====================
 
    @Test
    public void testCheckName_keywordFallback() {
        // 未收录进枚举的历史/新增长尾名称,只要含「心电」即认可
        assertTrue(filter.matchCheckName("十二导联心电检查"));
        assertTrue(filter.matchCheckName("心电监护"));
        assertTrue(filter.matchCheckName("运动心电"));
        // 枚举已收录的仍正常
        assertTrue(filter.matchCheckName("常规心电图"));
    }
 
    /**
     * ⚠️ 关键回归:**不含「心电」二字的心电检查必须仍被判为心电**。
     *
     * <p>用户指出的反例。核对 {@code check_type} 表确认,本院 15 个心电类型中
     * 有 4 个名称不含「心电」,库中合计 2 万余条真实记录:
     * <pre>
     *   300 动态血压(9083 条)  500 动脉硬化检测(11205 条)
     *   600 食道电生理(60 条)  900 心室晚电位(0 条)
     * </pre>
     * 若仅用"含心电"判定,这些单会被**误判为非心电而丢弃**。
     */
    @Test
    public void testCheckName_ecgWithoutKeyword_shouldStillBeAccepted() {
        // —— 用户举的反例 ——
        assertTrue("动态血压(check_type=300,库中 9083 条)不应被丢弃",
                filter.matchCheckName("动态血压"));
        assertTrue("动脉硬化(check_type=500,库中 11205 条)不应被丢弃",
                filter.matchCheckName("动脉硬化"));
        assertTrue("运动试验(check_type=400)不应被丢弃",
                filter.matchCheckName("运动试验"));
        assertTrue("食道电生理(check_type=600)不应被丢弃",
                filter.matchCheckName("食道电生理"));
        // —— check_type 表中的准确名称 ——
        assertTrue(filter.matchCheckName("动脉硬化检测"));
        assertTrue(filter.matchCheckName("心室晚电位"));
        assertTrue(filter.matchCheckName("平板运动心电"));
        assertTrue(filter.matchCheckName("心电图药物负荷试验"));
        assertTrue(filter.matchCheckName("动态心电图+血压"));
        // —— 生产日志中的实际名称变体(S050401 实测)——
        assertTrue("实测 S050401 中 '24小时动态血压监测' 不含「心电」,靠白名单通过",
                filter.matchCheckName("24小时动态血压监测"));
        assertTrue(filter.matchCheckName("24小时动态心电图"));
        assertTrue(filter.matchCheckName("24小时动态心电监测"));
        assertTrue("实测 S050401 中 '心电图运动试验' 应对应 check_type=400",
                filter.matchCheckName("心电图运动试验"));
        // 平板运动的各种写法
        assertTrue(filter.matchCheckName("平板运动试验"));
        assertTrue(filter.matchCheckName("平板运动心电图"));
        assertTrue(filter.matchCheckName("活动平板试验"));
    }
 
    /**
     * 非心电排除词:名称含「心脏」但属其他科室者,必须判否。
     * ("彩超胎儿心脏""超声造影立体成像(右心)"均来自实测日志)
     */
    @Test
    public void testCheckName_nonEcgExcludeKeywords() {
        assertFalse("彩超胎儿心脏属超声科,不应被当作心电",
                filter.matchCheckName("彩超胎儿心脏"));
        assertFalse(filter.matchCheckName("超声造影立体成像(右心)"));
        assertFalse(filter.matchCheckName("彩超心脏(常规)"));
        assertFalse(filter.matchCheckName("彩超心脏(14岁以下)"));
        assertFalse(filter.matchCheckName("彩超床旁心包腔"));
    }
 
    /**
     * 用实测日志中出现的 111 个非心电项目名称做回归:
     * 这些是 S0405 / S050401 / S050501 里推送的放射、超声、内镜等项目,
     * 必须**全部**判为非心电,否则会被误当作心电申请单采集。
     */
    @Test
    public void testRealNonEcgNames_shouldAllBeRejected() {
        String[] nonEcgNames = {
            "彩超肝胆胰脾", "彩超产检(单胎)", "彩超心脏(常规)", "彩超颈动脉",
            "彩超甲状腺及颈部淋巴结", "彩超早孕(经阴5-11w)", "彩超产检1",
            "彩色多普勒超声检查(弹性成像)", "彩超早孕(经腹5-11w)", "彩超子宫附件(经腹)",
            "彩超子宫附件(经阴道)", "彩超双肾输尿管膀胱", "彩超椎动脉", "彩超肾上腺",
            "彩超肾动脉", "彩超产检(双胎)", "彩超双乳及腋窝淋巴结", "彩超阑尾",
            "彩超宫颈管长度", "超声造影立体成像(右心)", "彩超右下肢动脉", "彩超睾丸",
            "彩超卵泡监测(经阴道)", "彩超床旁胸腔", "彩超床旁左下肢静脉",
            "彩超床旁心包腔", "彩超床旁双肾输尿管膀胱", "彩超腹膜后淋巴结",
            "彩超床旁肝胆胰脾", "彩超胸腔", "彩超胎儿系统性筛查", "彩超盆腔超声(经阴道)",
            "彩超涎腺(颌下腺、腮腺)", "彩超心脏(经食管-卵圆孔)", "彩超子宫动脉(经阴道)",
            "彩超甲状腺", "彩超腹股沟淋巴结(双侧)", "彩超心脏(14岁以下)",
            "彩超体表包块1(请标注1块包块具体位置)",
            "胸部平扫", "胸部CT平扫", "颅脑CT平扫", "上腹部CT平扫", "口腔CT",
            "下腹部CT平扫", "盆腔CT平扫", "胸部高分辨(HRCT)", "胸部CT增强",
            "颅脑MRI平扫(含DWI)", "颅脑MRI平扫", "颅脑平扫", "颅脑平扫+DWI",
            "腰椎平扫", "颈椎平扫", "腰椎+二、三维", "右肩关节平扫", "左膝关节平扫",
            "右膝关节平扫", "右足平扫", "全腹部平扫+增强", "全腹部平扫(平扫三个部位)",
            "上腹部增强", "前列腺增强", "肝脏增强", "乳腺增强", "盆腔增强",
            "冠脉CTA", "脑CTA", "颈CTA", "颅脑TOF MRA",
            "胸部正位", "胸部正侧位", "胸部正斜位(起搏器)", "小儿胸片",
            "右膝关节", "左膝关节正侧位", "右膝关节正侧位", "右股骨全长正侧位",
            "左胫腓骨全长正侧位", "骨盆正位", "腰椎正侧位", "腰椎过屈过伸",
            "右肘正侧位", "右手指正侧位", "右腕关节正侧位", "右腕关节",
            "右踝关节正侧位", "左肩关节正斜位", "右尺桡骨正侧位", "右肱骨正侧位",
            "全下肢正位", "左胫腓骨", "上下颌骨", "乳突水平", "颈椎", "腰椎",
            "腹部平片(卧位)KUB", "双肾输尿管全长", "骨密度",
            "肺通气功能检查", "肺弥散功能检查", "支气管激发试验检查", "支气管舒张试验",
            "一氧化氮呼气测定", "无痛胃镜", "电子胃镜检查", "无痛肠镜",
            "无创肝弹性测定", "心脏功能"
        };
        java.util.List<String> wrong = new java.util.ArrayList<>();
        for (String name : nonEcgNames) {
            if (filter.matchCheckName(name)) {
                wrong.add(name);
            }
        }
        assertTrue("以下非心电项目被误判为心电: " + wrong, wrong.isEmpty());
    }
 
    // ==================== judgeWithZdy(S050401/S050501 专用入口) ====================
 
    @Test
    public void testJudgeWithZdy_shouldRejectRadiology() {
        java.util.Map<String, Object> zdy = new java.util.HashMap<>();
        zdy.put("jcxmcode", "1393,1506");
        zdy.put("jcxmname", "右肘正侧位,左肩关节正侧位");
        zdy.put("roomname", "2号机房(DR)");
        HisEcgFilter.Result r = filter.judgeWithZdy("2126091700001", "ZY012", zdy);
        assertFalse("DR 正侧位不应判为心电单: " + r.getReason(), r.isEcg());
    }
 
    @Test
    public void testJudgeWithZdy_shouldRejectUltrasound() {
        java.util.Map<String, Object> zdy = new java.util.HashMap<>();
        zdy.put("jcxmcode", "35672306,35672504");
        zdy.put("jcxmname", "彩超右下肢动脉,彩超右下肢静脉");
        zdy.put("roomname", "5号诊室");
        assertFalse(filter.judgeWithZdy("2126091700038", "ZY012", zdy).isEcg());
    }
 
    @Test
    public void testJudgeWithZdy_shouldAcceptEcgOrder() {
        // 心电单:名称含「心电」
        java.util.Map<String, Object> zdy = new java.util.HashMap<>();
        zdy.put("jcxmcode", "1202065");
        zdy.put("jcxmname", "床边心电图(常规+频谱)");
        zdy.put("roomname", "心电检查室");
        HisEcgFilter.Result r = filter.judgeWithZdy("2126091700001", "ZY012", zdy);
        assertTrue("心电项目应判为心电单: " + r.getReason(), r.isEcg());
    }
 
    @Test
    public void testJudgeWithZdy_shouldAcceptByApplyNoSuffix() {
        // 即便检查项目取不到,只要申请单号带心电后缀也认可
        java.util.Map<String, Object> zdy = new java.util.HashMap<>();
        assertTrue(filter.judgeWithZdy("8826091720001101", null, zdy).isEcg());
        assertTrue(filter.judgeWithZdy("8826081120716401", null, null).isEcg());
    }
}