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.
Industry scenario
Section titled “Industry scenario”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.
Business pain points
Section titled “Business pain points”- 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-elseblock, 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.
Solution architecture
Section titled “Solution architecture”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 breakdown
Section titled “Rule method breakdown”| 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 |
Code implementation (complete & runnable)
Section titled “Code implementation (complete & runnable)”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.
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; }}Run & verified results
Section titled “Run & verified results”Run from the project root:
mvn test -Dtest=TriageServiceTestTest 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 |
Technical highlights
Section titled “Technical highlights”- Critical signals and complaint grading decoupled in two stages:
lifeThreatScoreonly 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 <= 9judges 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:
applyZoneAndResponsemaps I/II/III/IV to RESCUE/CRITICAL_BAY/OBS/WAITING and 0/10/30/120 minutes, with green-channel tags recorded inchannels, 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.