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());
|
}
|
}
|