Skip to content

Case 34: Outpatient triage prioritization

This case corresponds to scenario 34 of JQuick-Java USE-CASES (requirement 41: outpatient triage / emergency priority grading), in the emergency medicine domain. The decision scores life-threat signals and grades chief complaints for emergency pre-examination patients, outputting a level I-IV triage conclusion, placement zone, target physician response time, and green-channel tags.

An emergency triage desk must complete priority determination within minutes of a patient’s arrival. The input model TriageRequest carries the patient ID, name, age, chief complaint code (CHEST_PAIN, STROKE_SIGN, FEVER, TRAUMA, ABDOMINAL, COLD, RASH, FOLLOW_UP), whether other vital signs such as blood pressure and heart rate have been flagged as critical values, oxygen saturation, consciousness level (ALERT / DROWSY / UNRESPONSIVE), whether there is active bleeding, whether there is breathing distress, body temperature, symptom onset duration (in half-hour units), whether the patient is an infant under 3 years old, and whether the patient is pregnant or postpartum.

The decision runs in two stages. The first stage is the life-threat hard score: other critical vital signs, oxygen saturation below 90%, and unresponsiveness each score 4 points; drowsiness scores 2; active bleeding and breathing distress each score 3; a cumulative score of 4 or more directly rules level I (critical). If not critical, the second stage routes by chief complaint: chest pain is fixed at level II and takes the chest-pain-center green channel; stroke signs are fixed at level II, with different green-channel text depending on whether the onset duration falls within the 4.5-hour (9 half-hours) IV thrombolysis window; fever is graded by temperature, rising to level I above 40°C and to level II for infants under 3 above 39°C; trauma with active bleeding rises to level II; abdominal pain rises to level II in three cases — pregnancy, accompanying bleeding, or age 65+; other pregnant/postpartum bleeding likewise rises to level II; all remaining complaints fall back to the script’s base grading.

The decision result is carried by TriageDecision: triage level I/II/III/IV, placement zone (RESCUE resuscitation room / CRITICAL_BAY critical bay or green channel / OBS observation area / WAITING waiting area), target physician response time (0/10/30/120 minutes), the life-threat signal score, and the channels list of green-channel and handling tags.

  • Triage standards change frequently with specialty-center construction: the time windows and response deadlines of the chest-pain, stroke, and maternal green channels come from specialty quality-control requirements, scattered across triage nurses’ experience and paper workflows, and are hard to sync into information systems.
  • The vital-signs and chief-complaint decision lines are coupled: critical signals must be unconditionally up front, while chest pain, stroke, fever, and abdominal pain each have their own escalation branches — written in one big if-else block, this is hard to maintain and hard to explain to quality-control departments.
  • Many special-population rules: infant high fever, pregnant abdominal pain/bleeding, and elderly high-risk abdominal pain each have independent escalation conditions; hardcoding makes the triage program increasingly messy across versions.
  • Triage results need traceable handling instructions: not only the level, but also executable green-channel tags like “first ECG within 10 minutes” and “urgent CT within the thrombolysis window” — quality-control inspections must be able to reconstruct the basis of each determination.
  • Emergency systems have minimal upgrade windows and are mostly JDK 8 legacy: any downtime in the triage program directly affects emergency care, so rules cannot be maintained through frequent releases, and heavyweight medical rule platforms requiring separate deployment are not an option.

This case uses the standard JQuick-Java six-piece set: input model, decision result model, Rules interface, XML rule script, Service orchestration, and JUnit tests. All code is verified on Oracle JDK 1.8.

Layer Responsibility Files
Domain model Carries emergency pre-examination attributes and the four-level triage decision (zone, response deadline, green-channel tags) TriageRequest.java, TriageDecision.java
Rule layer Life-threat signal accumulation, chief-complaint base grading, fever grading, stroke time-window check TriageRules.java, triage.xml
Orchestration layer Critical-condition short-circuit up front, per-complaint green-channel escalation branches, zone and response-deadline assembly TriageService.java
Tests 6 real-world emergency triage profiles TriageServiceTest.java
Rule method Criteria Score/output
lifeThreatScore Other critical vital signs, oxygen saturation below 90, unresponsive consciousness (consciousCode ≥ 2) each add 4 points; drowsiness adds 2; active bleeding and breathing distress each add 3; the Service rules level I (critical) at ≥ 4 points 0-18 points
complaintBaseLevel CHEST_PAIN and STROKE_SIGN base at level II; TRAUMA, ABDOMINAL, FEVER base at level III; all others at level IV II/III/IV
feverLevel Temperature ≥ 40.0 returns I; ≥ 39.0 returns II for infants, III for adults; ≥ 38.5 returns III; otherwise IV I/II/III/IV
withinStrokeWindow Onset duration (half-hour granularity) ≤ 9, i.e. within the stroke 4.5-hour IV thrombolysis treatment window boolean

Place the two model classes under src/test/java/com/github/paohaijiao/demo/model/, the rules interface under demo/, the Service under demo/service/, the XML script under src/test/resources/demo/, and the test class under demo/, keeping the package names exactly as in the source. Run mvn test -Dtest=TriageServiceTest from the project root to reproduce all 6 cases.

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

Run from the project root:

Terminal window
mvn test -Dtest=TriageServiceTest

Test coverage (6 @Test methods covering critical-condition precedence, time-window branching, special-population escalation, and fallback to waiting):

Test method Business profile Key assertions / expected decision
shouldTriageChestPainWithVitalCrisisAsLevelOne P001, 56, chest pain with critical blood pressure/heart rate values, oxygen saturation 96 Life-threat score 4, level I, RESCUE, response 0 minutes; first tag contains “立即送抢救室并启动复苏团队”
shouldOpenStrokeGreenChannelWithWindowBranch P002/P003, 70, stroke signs: onset 2 hours (4 half-hours) vs 6 hours (12 half-hours) Both level II, CRITICAL_BAY, response 10 minutes; in-window tag contains “4.5 小时静脉溶栓时间窗内”, out-of-window tag contains “已超静脉溶栓时间窗”
shouldEscalateExtremeAndInfantFever P004, 30, fever 40.5°C; P005, 2-year-old infant, fever 39.2°C Extreme fever level I, RESCUE, tag contains “超高热 40.5℃”; infant level II, tag contains “热性惊厥”
shouldSendCommonColdToWaitingArea P006, 25, common cold, 37.8°C Level IV, WAITING area, response 120 minutes, channels empty
shouldEscalateElderlyAndPregnantAbdominalPain P007, 72, abdominal pain; P008, 28, pregnant with abdominal pain Both escalate to level II; tags contain “65 岁以上老年腹痛” and “孕产妇急救绿色通道” respectively
shouldPrioritizeActiveBleedingAndOrdinaryFever P009, 33, trauma with oxygen saturation 94 and active bleeding; P010, 30, fever 38.8°C Trauma: life-threat score 3 (below the critical line), escalated to level II via the complaint branch, tag contains “活动性出血,立即压迫止血”; ordinary fever: level III, OBS, response 30 minutes
  • Critical signals and complaint grading decoupled in two stages: lifeThreatScore only does objective accumulation, and the Service hard-gates level I at ≥ 4 points; green-channel rules for chest pain, stroke, fever, and abdominal pain all sit after the critical check, so critical patients are never delayed by low-priority complaint flows.
  • Time windows and thresholds externalized in XML, hot-updatable: the stroke 4.5-hour window (9 half-hours) and the fever 40/39/38.5 bands all live in triage.xml; when specialty-center quality standards change, editing the XML and rebuilding the proxy takes effect without releasing the emergency program.
  • Special-population escalation short-circuited on the Java side: escalation branches for infants, pregnant/postpartum patients, elderly 65+, and active bleeding are concentrated in the Service; forward early returns work around the script’s lack of ! negation, making branch order itself the handling priority.
  • Half-hour integer granularity carries the time window: onset duration is passed as an integer in half-hour units, and onsetHalfHours <= 9 judges the window directly — no minute/hour floating-point conversion error, keeping nurse-patient communication and quality-control criteria consistent.
  • Level, zone, and response deadline assembled as one: applyZoneAndResponse maps I/II/III/IV to RESCUE/CRITICAL_BAY/OBS/WAITING and 0/10/30/120 minutes, with green-channel tags recorded in channels, satisfying emergency quality-control audits.
  • Name-bound @Param + ASM proxy: script parameters are decoupled from the Java interface by name, and the proxy is singleton-reusable; starting from Java 8 with no heavyweight middleware, it embeds directly into legacy emergency systems and IT-innovation environments that cannot afford downtime.