From 00f615d8e525b7ddc4fe349b4f9a2c7e4af9cc1a Mon Sep 17 00:00:00 2001 From: iven Date: Tue, 28 Apr 2026 19:40:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(health):=20=E6=96=B0=E5=A2=9E=E8=A1=80?= =?UTF-8?q?=E5=8E=8B/=E8=A1=80=E7=B3=96=E4=B8=B4=E5=BA=8A=E9=98=88?= =?UTF-8?q?=E5=80=BC=E5=91=8A=E8=AD=A6=E8=A7=84=E5=88=99=20+=20alert=20eng?= =?UTF-8?q?ine=20=E7=9B=B4=E6=8E=A5=E6=9F=A5=20device=5Freadings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/erp-health/src/event.rs | 2 +- crates/erp-health/src/service/alert_engine.rs | 40 +++++++++++++++- crates/erp-health/src/service/seed.rs | 47 +++++++++++++++++++ 3 files changed, 87 insertions(+), 2 deletions(-) diff --git a/crates/erp-health/src/event.rs b/crates/erp-health/src/event.rs index 90339ec..168777a 100644 --- a/crates/erp-health/src/event.rs +++ b/crates/erp-health/src/event.rs @@ -137,7 +137,7 @@ pub fn register_handlers_with_state(state: crate::state::HealthState) { .and_then(|s| Uuid::parse_str(s).ok()); if let Some(pid) = patient_id { // 对所有设备类型触发评估 - for device_type in &["heart_rate", "blood_oxygen", "temperature"] { + for device_type in &["heart_rate", "blood_oxygen", "temperature", "blood_pressure", "blood_glucose"] { if let Err(e) = crate::service::alert_engine::evaluate_rules( &eval_state, event.tenant_id, pid, device_type, ).await { diff --git a/crates/erp-health/src/service/alert_engine.rs b/crates/erp-health/src/service/alert_engine.rs index 9e506ee..70799b2 100644 --- a/crates/erp-health/src/service/alert_engine.rs +++ b/crates/erp-health/src/service/alert_engine.rs @@ -5,7 +5,7 @@ use serde_json::json; use std::collections::HashSet; use uuid::Uuid; -use crate::entity::{alert_rules, alerts, vital_signs_hourly}; +use crate::entity::{alert_rules, alerts, device_readings, vital_signs_hourly}; use crate::error::{HealthError, HealthResult}; use crate::state::HealthState; @@ -62,6 +62,9 @@ pub async fn evaluate_rules( let condition_type = rule.condition_type.as_str(); let is_triggered = match condition_type { + "single_threshold" if matches!(device_type, "blood_pressure" | "blood_glucose") => { + evaluate_bp_glucose_threshold(&state.db, tenant_id, patient_id, device_type, params).await + } "single_threshold" => evaluate_single_threshold_in_memory(&hourly_records, params), "consecutive" => evaluate_consecutive_in_memory(&hourly_records, params), "trend" => evaluate_trend_in_memory(&hourly_records, params), @@ -213,3 +216,38 @@ async fn create_alert_and_notify( Ok(alert) } + +/// 血压/血糖告警:直接查 device_readings 最新值,支持 metric 过滤 +async fn evaluate_bp_glucose_threshold( + db: &DatabaseConnection, + tenant_id: Uuid, + patient_id: Uuid, + device_type: &str, + params: &serde_json::Value, +) -> bool { + let direction = params["direction"].as_str().unwrap_or("above"); + let threshold = params["value"].as_f64().unwrap_or(f64::MAX); + let metric = params["metric"].as_str(); + + let mut query = device_readings::Entity::find() + .filter(device_readings::Column::TenantId.eq(tenant_id)) + .filter(device_readings::Column::PatientId.eq(patient_id)) + .filter(device_readings::Column::DeviceType.eq(device_type)) + .filter(device_readings::Column::DeletedAt.is_null()) + .order_by_desc(device_readings::Column::MeasuredAt); + + if let Some(m) = metric { + query = query.filter(device_readings::Column::Metric.eq(m)); + } + + let latest = query.one(db).await.ok().flatten(); + let Some(record) = latest else { return false }; + + let val = record.raw_value.get("value").and_then(|v| v.as_f64()).unwrap_or(f64::MAX); + + match direction { + "above" => val > threshold, + "below" => val < threshold, + _ => false, + } +} diff --git a/crates/erp-health/src/service/seed.rs b/crates/erp-health/src/service/seed.rs index 93d2fb5..9e51ace 100644 --- a/crates/erp-health/src/service/seed.rs +++ b/crates/erp-health/src/service/seed.rs @@ -91,6 +91,53 @@ pub async fn seed_tenant_health( "urgent", 120, ), + // 血压告警规则 + ( + "血压收缩压偏高", + Some("收缩压 ≥ 140 mmHg"), + "blood_pressure", + "single_threshold", + json!({"direction": "above", "value": 140.0, "metric": "systolic"}), + "warning", + 60, + ), + ( + "血压收缩压危急", + Some("收缩压 ≥ 180 mmHg"), + "blood_pressure", + "single_threshold", + json!({"direction": "above", "value": 180.0, "metric": "systolic"}), + "critical", + 30, + ), + ( + "血压舒张压偏低", + Some("舒张压 < 60 mmHg"), + "blood_pressure", + "single_threshold", + json!({"direction": "below", "value": 60.0, "metric": "diastolic"}), + "critical", + 30, + ), + // 血糖告警规则 + ( + "空腹血糖偏高", + Some("空腹血糖 ≥ 7.0 mmol/L"), + "blood_glucose", + "single_threshold", + json!({"direction": "above", "value": 7.0}), + "warning", + 60, + ), + ( + "低血糖", + Some("血糖 < 3.9 mmol/L"), + "blood_glucose", + "single_threshold", + json!({"direction": "below", "value": 3.9}), + "critical", + 30, + ), ]; for (name, description, device_type, condition_type, condition_params, severity, cooldown) in