跳转到内容

案例 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
规则方法 口径 分值/输出
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

将两个模型类放入 src/test/java/com/github/paohaijiao/demo/model/,规则接口放入 demo/,Service 放入 demo/service/,XML 脚本放入 src/test/resources/demo/,测试类放入 demo/,包名保持源码原样。工程根目录执行 mvn test -Dtest=TriageServiceTest 即可复现全部 6 个用例。

package com.github.paohaijiao.demo.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 急诊预检分诊要素
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public 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;
}
package com.github.paohaijiao.demo.model;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 急诊分诊决策
*/
@Data
public 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);
}
}
package com.github.paohaijiao.demo;
import com.github.paohaijiao.xml.param.Param;
/**
* 场景 34:急诊生命威胁评分、主诉基础分级、发热与时间窗规则
*/
public interface TriageRules {
/**
* 生命威胁信号累加(≥4 视为濒危 I 级):
* 其他体征危急/血氧&lt;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);
}
<?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>
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);
}
}
}
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;
}
}

在工程根目录执行:

Terminal window
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 起步、无重型中间件,可直接嵌入停不起机的急诊老系统与信创环境。