案例 34:门诊分诊/急诊优先级分级
本案例对应 JQuick-Java USE-CASES 场景 34(诉求 41:门诊分诊/急诊优先级分级),属于医疗急诊领域,决策内容是对急诊预检患者做生命威胁信号评分与主诉分级,输出 I-IV 四级分诊结论、安置区域、医生目标响应时间与绿色通道标签。
急诊预检分诊台需要在患者到达的几分钟内完成优先级判定。入参模型 TriageRequest 承载患者号、姓名、年龄、主诉代码(CHEST_PAIN 胸痛、STROKE_SIGN 卒中征象、FEVER 发热、TRAUMA 外伤、ABDOMINAL 腹痛、COLD 普通感冒、RASH 皮疹、FOLLOW_UP 复诊开药),以及血压心率等其他生命体征是否已报危急值、血氧饱和度、意识状态(ALERT 清醒 / DROWSY 嗜睡 / UNRESPONSIVE 无反应)、是否活动性出血、是否呼吸困难、体温、症状发作时长(以半小时为单位)、是否 3 岁以下婴幼儿、是否孕产妇。
决策分两段。第一段是生命威胁硬评分:其他体征危急、血氧低于 90%、无反应各计 4 分,嗜睡计 2 分,活动性出血与呼吸困难各计 3 分,累加满 4 分直接判 I 级濒危。未濒危时进入第二段主诉路由:胸痛固定 II 级并走胸痛中心绿色通道;卒中征象固定 II 级,并按发作时长是否在 4.5 小时(9 个半小时)静脉溶栓时间窗内输出不同绿道文案;发热按体温分级,40℃ 以上升 I 级、3 岁以下婴幼儿 39℃ 以上升 II 级;外伤伴活动出血升 II 级;腹痛在孕产妇、伴出血或 65 岁以上三种情况下升 II 级;其他孕产妇出血同样升 II 级;其余主诉回落到脚本基础分级。
决策结果由 TriageDecision 承载:I/II/III/IV 分诊级别、安置区域(RESCUE 抢救室 / CRITICAL_BAY 危重处置区或绿道 / OBS 留观区 / WAITING 候诊区)、医生目标响应时间(0/10/30/120 分钟)、生命威胁信号分,以及 channels 绿色通道与处置标签清单。
- 分诊标准随专病中心建设频繁调整:胸痛、卒中、孕产妇等绿色通道的时间窗与响应时限来自各类专病质控要求,散落在分诊护士经验与纸质流程中,更新难以同步到信息系统。
- 生命体征与主诉两条判定线耦合:濒危信号必须一票前置,而胸痛、卒中、发热、腹痛又各有升级分叉,写在一个大段
if-else里既难维护也难以向质控部门解释每级判定依据。 - 特殊人群规则多:婴幼儿高热、孕产妇腹痛/出血、老年高危腹痛各有独立升级条件,硬编码导致分诊程序版本越积越乱。
- 分诊结果需要可追溯的处置指令:不仅要给出级别,还要同时给出“10 分钟内心电图”“在溶栓时间窗内急查 CT”等可执行绿道标签,质控检查时要还原当次判定依据。
- 急诊系统升级窗口极小且多为 JDK 8 老系统:分诊程序停机即影响急救,无法为规则维护频繁发版,也难以引入需要独立部署的重型医疗规则平台。
本案例采用 JQuick-Java 标准六件套:入参模型、决策结果模型、Rules 规则接口、XML 规则脚本、Service 编排服务、JUnit 测试,全部代码在 Oracle JDK 1.8 下实测通过。
| 层次 | 职责 | 对应文件 |
|---|---|---|
| 领域模型 | 承载急诊预检要素与四级分诊决策(区域、响应时限、绿道标签) | TriageRequest.java、TriageDecision.java |
| 规则层 | 生命威胁信号累加、主诉基础分级、发热分级、卒中时间窗判定 | TriageRules.java、triage.xml |
| 编排层 | 濒危前置短路、各主诉绿道升级分叉、区域与响应时限装配 | TriageService.java |
| 测试 | 6 组急诊分诊真实形态 | TriageServiceTest.java |
规则方法拆分
Section titled “规则方法拆分”| 规则方法 | 口径 | 分值/输出 |
|---|---|---|
lifeThreatScore |
其他体征危急、血氧低于 90、意识无反应(consciousCode ≥2)各加 4 分;嗜睡加 2 分;活动出血、呼吸困难各加 3 分;Service 侧以 ≥4 分判 I 级濒危 | 0-18 分 |
complaintBaseLevel |
CHEST_PAIN、STROKE_SIGN 基础 II 级;TRAUMA、ABDOMINAL、FEVER 基础 III 级;其余 IV 级 | II/III/IV |
feverLevel |
体温 ≥40.0 返回 I;≥39.0 时婴幼儿返回 II、成人返回 III;≥38.5 返回 III;否则 IV | I/II/III/IV |
withinStrokeWindow |
症状发作时长(半小时粒度)小于等于 9,即处于卒中 4.5 小时静脉溶栓救治时间窗 | boolean |
代码实现(完整可运行)
Section titled “代码实现(完整可运行)”将两个模型类放入 src/test/java/com/github/paohaijiao/demo/model/,规则接口放入 demo/,Service 放入 demo/service/,XML 脚本放入 src/test/resources/demo/,测试类放入 demo/,包名保持源码原样。工程根目录执行 mvn test -Dtest=TriageServiceTest 即可复现全部 6 个用例。
TriageRequest.java
Section titled “TriageRequest.java”package com.github.paohaijiao.demo.model;
import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;
/** * 急诊预检分诊要素 */@Data@NoArgsConstructor@AllArgsConstructorpublic class TriageRequest {
/** * 患者号 */ private String patientId;
/** * 患者姓名 */ private String patientName;
/** * 年龄 */ private int age;
/** * 主诉代码:CHEST_PAIN 胸痛 / STROKE_SIGN 卒中征象 / FEVER 发热 / * TRAUMA 外伤 / ABDOMINAL 腹痛 / COLD 普通感冒 / RASH 皮疹 / FOLLOW_UP 复诊开药 */ private String chiefComplaint;
/** * 血压/心率等其他生命体征是否已报危急值 */ private boolean vitalSignsAbnormal;
/** * 血氧饱和度(%) */ private int oxygenSaturation;
/** * 意识状态:ALERT 清醒 / DROWSY 嗜睡 / UNRESPONSIVE 无反应 */ private String consciousnessLevel;
/** * 是否活动性出血 */ private boolean bleedingActive;
/** * 是否呼吸困难 */ private boolean breathingDistress;
/** * 体温(℃) */ private double temperature;
/** * 症状发作时长(以半小时为单位,如 2 小时传 4) */ private int onsetHalfHours;
/** * 是否 3 岁以下婴幼儿 */ private boolean infant;
/** * 是否孕产妇 */ private boolean pregnant;}TriageDecision.java
Section titled “TriageDecision.java”package com.github.paohaijiao.demo.model;
import lombok.Data;
import java.util.ArrayList;import java.util.List;
/** * 急诊分诊决策 */@Datapublic class TriageDecision {
/** * 患者号 */ private String patientId;
/** * 分诊级别:I 濒危 / II 危重 / III 急症 / IV 非急症 */ private String triageLevel;
/** * 安置区域:RESCUE 抢救室 / CRITICAL_BAY 危重处置区/绿道 / OBS 留观区 / WAITING 候诊区 */ private String zone;
/** * 医生目标响应时间(分钟) */ private int responseMinutes;
/** * 生命威胁信号分(≥4 直接 I 级) */ private int lifeThreatScore;
/** * 绿色通道与处置标签 */ private List<String> channels = new ArrayList<String>();
public void addChannel(String channel) { this.channels.add(channel); }}TriageRules.java
Section titled “TriageRules.java”package com.github.paohaijiao.demo;
import com.github.paohaijiao.xml.param.Param;
/** * 场景 34:急诊生命威胁评分、主诉基础分级、发热与时间窗规则 */public interface TriageRules {
/** * 生命威胁信号累加(≥4 视为濒危 I 级): * 其他体征危急/血氧<90/无反应各 4 分,嗜睡 2 分,活动出血与呼吸困难各 3 分 */ int lifeThreatScore(@Param("vitalAbnormal") int vitalAbnormal, @Param("spo2") int spo2, @Param("consciousCode") int consciousCode, @Param("bleeding") int bleeding, @Param("distress") int distress);
/** * 主诉基础分级(CHEST_PAIN/STROKE_SIGN 为 II,TRAUMA/ABDOMINAL/FEVER 为 III,其余 IV) */ String complaintBaseLevel(@Param("complaint") String complaint);
/** * 发热分级,婴幼儿 39℃ 升至 II */ String feverLevel(@Param("temperature") double temperature, @Param("infant") boolean infant);
/** * 是否处于卒中 4.5 小时(9 个半小时)救治时间窗 */ boolean withinStrokeWindow(@Param("onsetHalfHours") int onsetHalfHours);}triage.xml
Section titled “triage.xml”<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE javas PUBLIC "-//PAOHAIJIAO//DTD API JAVA 1.0//EN" "classpath:paohaijiao/dtd/Jquick-java.dtd"><javas namespace="com.github.paohaijiao.demo.TriageRules">
<java name="lifeThreatScore" returnClass="int"> <![CDATA[ int def lifeThreatScore(int:vitalAbnormal, int:spo2, int:consciousCode, int:bleeding, int:distress) { int score = 0; if (vitalAbnormal >= 1) { score = score + 4; } if (spo2 < 90) { score = score + 4; } if (consciousCode >= 2) { score = score + 4; } else if (consciousCode == 1) { score = score + 2; } if (bleeding >= 1) { score = score + 3; } if (distress >= 1) { score = score + 3; } return score; } ]]> </java>
<java name="complaintBaseLevel" returnClass="java.lang.String"> <![CDATA[ str def complaintBaseLevel(str:complaint) { if (complaint == "CHEST_PAIN") { return "II"; } else if (complaint == "STROKE_SIGN") { return "II"; } else if (complaint == "TRAUMA") { return "III"; } else if (complaint == "ABDOMINAL") { return "III"; } else if (complaint == "FEVER") { return "III"; } else { return "IV"; } } ]]> </java>
<java name="feverLevel" returnClass="java.lang.String"> <![CDATA[ str def feverLevel(double:temperature, boolean:infant) { if (temperature >= 40.0) { return "I"; } else if (temperature >= 39.0) { if (infant) { return "II"; } else { return "III"; } } else if (temperature >= 38.5) { return "III"; } else { return "IV"; } } ]]> </java>
<java name="withinStrokeWindow" returnClass="boolean"> <![CDATA[ boolean def withinStrokeWindow(int:onsetHalfHours) { if (onsetHalfHours <= 9) { return true; } else { return false; } } ]]> </java>
</javas>TriageService.java
Section titled “TriageService.java”package com.github.paohaijiao.demo.service;
import com.github.paohaijiao.demo.TriageRules;import com.github.paohaijiao.demo.model.TriageDecision;import com.github.paohaijiao.demo.model.TriageRequest;
/** * 场景 34:门诊/急诊预检四级分诊 */public class TriageService {
private final TriageRules rules;
public TriageService(TriageRules rules) { this.rules = rules; }
public TriageDecision triage(TriageRequest req) { TriageDecision d = new TriageDecision(); d.setPatientId(req.getPatientId());
int consciousCode = "UNRESPONSIVE".equals(req.getConsciousnessLevel()) ? 2 : "DROWSY".equals(req.getConsciousnessLevel()) ? 1 : 0; int score = rules.lifeThreatScore(req.isVitalSignsAbnormal() ? 1 : 0, req.getOxygenSaturation(), consciousCode, req.isBleedingActive() ? 1 : 0, req.isBreathingDistress() ? 1 : 0); d.setLifeThreatScore(score);
String level; if (score >= 4) { level = "I"; d.addChannel("生命体征濒危,立即送抢救室并启动复苏团队"); } else { level = classifyByComplaint(req, d); }
applyZoneAndResponse(d, level); return d; }
private String classifyByComplaint(TriageRequest req, TriageDecision d) { String complaint = req.getChiefComplaint();
if ("CHEST_PAIN".equals(complaint)) { d.addChannel("胸痛中心绿色通道,10 分钟内完成首份心电图与肌钙蛋白采血"); return "II"; }
if ("STROKE_SIGN".equals(complaint)) { if (rules.withinStrokeWindow(req.getOnsetHalfHours())) { d.addChannel("卒中绿色通道,发病在 4.5 小时静脉溶栓时间窗内,急查 CT 与凝血"); } else { d.addChannel("卒中绿色通道,已超静脉溶栓时间窗,神经科即刻评估取栓指征"); } return "II"; }
if ("FEVER".equals(complaint)) { String feverLevel = rules.feverLevel(req.getTemperature(), req.isInfant()); if ("I".equals(feverLevel)) { d.addChannel("超高热 " + req.getTemperature() + "℃,立即抢救降温并排查感染性休克"); return "I"; } if ("II".equals(feverLevel)) { d.addChannel("3 岁以下婴幼儿 39℃ 以上高热,优先就诊并警惕热性惊厥"); return "II"; } return feverLevel; }
if ("TRAUMA".equals(complaint)) { if (req.isBleedingActive()) { d.addChannel("外伤伴活动性出血,立即压迫止血并评估缝合/输血"); return "II"; } return "III"; }
if ("ABDOMINAL".equals(complaint)) { if (req.isPregnant()) { d.addChannel("孕产妇腹痛,启动孕产妇急救绿色通道并通知产科急会诊"); return "II"; } if (req.isBleedingActive()) { d.addChannel("腹痛伴活动性出血,立即评估消化道出血/宫外孕"); return "II"; } if (req.getAge() >= 65) { d.addChannel("65 岁以上老年腹痛,按高危腹痛优先排查血管性与外科急腹症"); return "II"; } return "III"; }
if (req.isPregnant() && req.isBleedingActive()) { d.addChannel("孕产妇出血,启动孕产妇急救绿色通道并通知产科"); return "II"; }
return rules.complaintBaseLevel(complaint); }
private void applyZoneAndResponse(TriageDecision d, String level) { d.setTriageLevel(level); if ("I".equals(level)) { d.setZone("RESCUE"); d.setResponseMinutes(0); } else if ("II".equals(level)) { d.setZone("CRITICAL_BAY"); d.setResponseMinutes(10); } else if ("III".equals(level)) { d.setZone("OBS"); d.setResponseMinutes(30); } else { d.setZone("WAITING"); d.setResponseMinutes(120); } }}TriageServiceTest.java
Section titled “TriageServiceTest.java”package com.github.paohaijiao.demo;
import com.github.paohaijiao.demo.model.TriageDecision;import com.github.paohaijiao.demo.model.TriageRequest;import com.github.paohaijiao.demo.service.TriageService;import com.github.paohaijiao.xml.JQuickJava;import org.junit.Assert;import org.junit.Before;import org.junit.Test;
/** * 场景 34:门诊分诊/急诊优先级分级 * <p> * 6 组形态:胸痛濒危 I 级、卒中绿道时间窗分叉、超高热与婴幼儿高热、 * 普通感冒候诊、老年/孕妇腹痛升级、外伤出血与普通发热。 */public class TriageServiceTest {
private TriageService service;
@Before public void setUp() { TriageRules rules = JQuickJava.create() .createApi(TriageRules.class, "demo/triage.xml"); this.service = new TriageService(rules); }
/** * 胸痛合并血压心率危急值 → I 级濒危,立即抢救室 */ @Test public void shouldTriageChestPainWithVitalCrisisAsLevelOne() { TriageRequest req = base("P001", "CHEST_PAIN", 56); req.setVitalSignsAbnormal(true); req.setOxygenSaturation(96);
TriageDecision d = service.triage(req);
Assert.assertEquals(4, d.getLifeThreatScore()); Assert.assertEquals("I", d.getTriageLevel()); Assert.assertEquals("RESCUE", d.getZone()); Assert.assertEquals(0, d.getResponseMinutes()); Assert.assertTrue(d.getChannels().get(0).contains("立即送抢救室并启动复苏团队")); }
/** * 清醒卒中 2 小时 → II 级绿道在窗;6 小时 → 超窗标签 */ @Test public void shouldOpenStrokeGreenChannelWithWindowBranch() { TriageRequest inWindow = base("P002", "STROKE_SIGN", 70); inWindow.setOnsetHalfHours(4); TriageDecision d1 = service.triage(inWindow); Assert.assertEquals("II", d1.getTriageLevel()); Assert.assertEquals("CRITICAL_BAY", d1.getZone()); Assert.assertEquals(10, d1.getResponseMinutes()); Assert.assertTrue(d1.getChannels().get(0).contains("4.5 小时静脉溶栓时间窗内"));
TriageRequest outWindow = base("P003", "STROKE_SIGN", 70); outWindow.setOnsetHalfHours(12); TriageDecision d2 = service.triage(outWindow); Assert.assertEquals("II", d2.getTriageLevel()); Assert.assertTrue(d2.getChannels().get(0).contains("已超静脉溶栓时间窗")); }
/** * 超高热 40.5℃ → I 级;婴幼儿 39.2℃ → II 级 */ @Test public void shouldEscalateExtremeAndInfantFever() { TriageRequest extreme = base("P004", "FEVER", 30); extreme.setTemperature(40.5); TriageDecision d1 = service.triage(extreme); Assert.assertEquals("I", d1.getTriageLevel()); Assert.assertEquals("RESCUE", d1.getZone()); Assert.assertTrue(d1.getChannels().get(0).contains("超高热 40.5℃"));
TriageRequest infant = base("P005", "FEVER", 2); infant.setTemperature(39.2); infant.setInfant(true); TriageDecision d2 = service.triage(infant); Assert.assertEquals("II", d2.getTriageLevel()); Assert.assertTrue(d2.getChannels().get(0).contains("热性惊厥")); }
/** * 普通感冒低热成人 → IV 级候诊 */ @Test public void shouldSendCommonColdToWaitingArea() { TriageRequest req = base("P006", "COLD", 25); req.setTemperature(37.8);
TriageDecision d = service.triage(req);
Assert.assertEquals("IV", d.getTriageLevel()); Assert.assertEquals("WAITING", d.getZone()); Assert.assertEquals(120, d.getResponseMinutes()); Assert.assertEquals(0, d.getChannels().size()); }
/** * 72 岁腹痛与孕妇腹痛均升至 II 级 */ @Test public void shouldEscalateElderlyAndPregnantAbdominalPain() { TriageRequest elderly = base("P007", "ABDOMINAL", 72); TriageDecision d1 = service.triage(elderly); Assert.assertEquals("II", d1.getTriageLevel()); Assert.assertTrue(d1.getChannels().get(0).contains("65 岁以上老年腹痛"));
TriageRequest pregnant = base("P008", "ABDOMINAL", 28); pregnant.setPregnant(true); TriageDecision d2 = service.triage(pregnant); Assert.assertEquals("II", d2.getTriageLevel()); Assert.assertTrue(d2.getChannels().get(0).contains("孕产妇急救绿色通道")); }
/** * 外伤活动出血(生命体征尚稳)→ II 级止血;成人 38.8℃ 发热 → III 级留观 */ @Test public void shouldPrioritizeActiveBleedingAndOrdinaryFever() { TriageRequest trauma = base("P009", "TRAUMA", 33); trauma.setOxygenSaturation(94); trauma.setBleedingActive(true); TriageDecision d1 = service.triage(trauma); Assert.assertEquals(3, d1.getLifeThreatScore()); Assert.assertEquals("II", d1.getTriageLevel()); Assert.assertTrue(d1.getChannels().get(0).contains("活动性出血,立即压迫止血"));
TriageRequest fever = base("P010", "FEVER", 30); fever.setTemperature(38.8); TriageDecision d2 = service.triage(fever); Assert.assertEquals("III", d2.getTriageLevel()); Assert.assertEquals("OBS", d2.getZone()); Assert.assertEquals(30, d2.getResponseMinutes()); }
private TriageRequest base(String id, String complaint, int age) { TriageRequest r = new TriageRequest(); r.setPatientId(id); r.setPatientName("测试患者"); r.setAge(age); r.setChiefComplaint(complaint); r.setVitalSignsAbnormal(false); r.setOxygenSaturation(98); r.setConsciousnessLevel("ALERT"); r.setBleedingActive(false); r.setBreathingDistress(false); r.setTemperature(36.8); r.setOnsetHalfHours(2); r.setInfant(false); r.setPregnant(false); return r; }}在工程根目录执行:
mvn test -Dtest=TriageServiceTest测试覆盖形态(6 个 @Test 方法,覆盖濒危前置、时间窗分叉、特殊人群升级与回落候诊):
| 测试方法 | 业务形态 | 关键断言/期望决策 |
|---|---|---|
shouldTriageChestPainWithVitalCrisisAsLevelOne |
P001 56 岁胸痛,血压/心率已报危急值,血氧 96 | 生命威胁分 4、I 级、RESCUE 抢救室、响应 0 分钟,首条标签含“立即送抢救室并启动复苏团队” |
shouldOpenStrokeGreenChannelWithWindowBranch |
P002/P003 70 岁卒中征象:发作 2 小时(4 个半小时)与 6 小时(12 个半小时) | 两者均 II 级、CRITICAL_BAY、响应 10 分钟;在窗标签含“4.5 小时静脉溶栓时间窗内”,超窗标签含“已超静脉溶栓时间窗” |
shouldEscalateExtremeAndInfantFever |
P004 30 岁发热 40.5℃;P005 2 岁婴幼儿发热 39.2℃ | 超高热 I 级、RESCUE、标签含“超高热 40.5℃”;婴幼儿 II 级、标签含“热性惊厥” |
shouldSendCommonColdToWaitingArea |
P006 25 岁普通感冒、37.8℃ | IV 级、WAITING 候诊区、响应 120 分钟、channels 0 条 |
shouldEscalateElderlyAndPregnantAbdominalPain |
P007 72 岁腹痛;P008 28 岁孕妇腹痛 | 均升 II 级;标签分别含“65 岁以上老年腹痛”和“孕产妇急救绿色通道” |
shouldPrioritizeActiveBleedingAndOrdinaryFever |
P009 33 岁外伤、血氧 94、伴活动性出血;P010 30 岁发热 38.8℃ | 外伤生命威胁分 3(未达濒危线)经主诉升 II 级、标签含“活动性出血,立即压迫止血”;普通发热 III 级、OBS、响应 30 分钟 |
- 濒危信号与主诉分级双段解耦:
lifeThreatScore只做客观累加,Service 以 ≥4 分硬前置 I 级;胸痛、卒中、发热、腹痛等绿道规则全部位于濒危判定之后,危重患者不会被低优先级主诉流程耽搁。 - 时间窗与阈值 XML 外置、热更新:卒中 4.5 小时窗(9 个半小时)、发热 40/39/38.5 三档线都在
triage.xml,专病中心质控标准调整时改 XML 重建代理即生效,急诊程序免发版。 - 特殊人群升级在 Java 侧短路:婴幼儿、孕产妇、65 岁以上老年、活动出血等升级分叉集中在 Service,配合正向提前
return规避脚本不支持!取反的限制,分支顺序即处置优先级。 - 半小时整数粒度承载时间窗:发作时长以“半小时”为最小单位传整数,
onsetHalfHours <= 9直接判窗,不存在分钟/小时浮点换算误差,护患沟通与质控口径一致。 - 级别、区域、响应时限一体装配:
applyZoneAndResponse把 I/II/III/IV 映射到 RESCUE/CRITICAL_BAY/OBS/WAITING 与 0/10/30/120 分钟,绿道标签channels同步留痕,满足急诊质控审计。 @Param按名绑定 + ASM 代理:脚本参数与 Java 接口按名解耦,代理可单例复用;Java 8 起步、无重型中间件,可直接嵌入停不起机的急诊老系统与信创环境。