From 1bece3d41fa92dcd7be8b854a599bc7ebb483c4f Mon Sep 17 00:00:00 2001 From: iven Date: Tue, 28 Apr 2026 11:43:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(health):=20=E5=8D=B1=E6=80=A5=E5=80=BC?= =?UTF-8?q?=E5=91=8A=E8=AD=A6=E6=B6=88=E8=B4=B9=E8=80=85=20=E2=80=94=20?= =?UTF-8?q?=E5=B9=82=E7=AD=89=E5=A4=84=E7=90=86=20+=20Handler=20+=20?= =?UTF-8?q?=E8=B7=AF=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - event.rs: 消费 health_data.critical_alert 事件创建告警记录 - handler: list/get/acknowledge 三个端点 - 路由: /health/critical-alerts, /health/critical-alerts/{id}/acknowledge - 权限: health.critical-alert.list / health.critical-alert.manage --- crates/erp-health/src/event.rs | 58 +++++++++++++ .../src/handler/critical_alert_handler.rs | 87 +++++++++++++++++++ crates/erp-health/src/handler/mod.rs | 1 + crates/erp-health/src/module.rs | 15 +++- 4 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 crates/erp-health/src/handler/critical_alert_handler.rs diff --git a/crates/erp-health/src/event.rs b/crates/erp-health/src/event.rs index 241c82d..4de0f07 100644 --- a/crates/erp-health/src/event.rs +++ b/crates/erp-health/src/event.rs @@ -156,4 +156,62 @@ pub fn register_handlers_with_state(state: crate::state::HealthState) { } } }); + + // health_data.critical_alert → 创建危急值告警记录 + let (mut critical_rx, _critical_handle) = state.event_bus.subscribe_filtered("health_data.".to_string()); + let critical_state = state.clone(); + tokio::spawn(async move { + loop { + match critical_rx.recv().await { + Some(event) if event.event_type == HEALTH_DATA_CRITICAL_ALERT => { + // 幂等检查 + if erp_core::events::is_event_processed(&critical_state.db, event.id, "critical_alert_consumer").await.unwrap_or(false) { + continue; + } + + let patient_id = event.payload.get("patient_id") + .and_then(|v| v.as_str()) + .and_then(|s| Uuid::parse_str(s).ok()); + let alert_type = event.payload.get("alert_type").and_then(|v| v.as_str()).unwrap_or("vital_sign"); + let metric_name = event.payload.get("metric_name").and_then(|v| v.as_str()).unwrap_or("unknown"); + let metric_value = event.payload.get("metric_value").and_then(|v| v.as_str()).unwrap_or(""); + let threshold_value = event.payload.get("threshold_value").and_then(|v| v.as_str()).unwrap_or(""); + + if let Some(pid) = patient_id { + match crate::service::critical_alert_service::handle_critical_alert_event( + &critical_state, + event.tenant_id, + pid, + alert_type, + metric_name, + metric_value, + threshold_value, + None, + ).await { + Ok(alert_id) => { + tracing::info!( + event_id = %event.id, + alert_id = %alert_id, + patient_id = %pid, + metric = %metric_name, + "危急值告警已创建" + ); + let _ = erp_core::events::mark_event_processed(&critical_state.db, event.id, "critical_alert_consumer").await; + } + Err(e) => { + tracing::error!( + event_id = %event.id, + patient_id = %pid, + error = %e, + "危急值告警创建失败" + ); + } + } + } + } + Some(_) => {} + None => break, + } + } + }); } diff --git a/crates/erp-health/src/handler/critical_alert_handler.rs b/crates/erp-health/src/handler/critical_alert_handler.rs new file mode 100644 index 0000000..7972e0b --- /dev/null +++ b/crates/erp-health/src/handler/critical_alert_handler.rs @@ -0,0 +1,87 @@ +use axum::extract::{FromRef, Path, Query, State}; +use axum::response::IntoResponse; +use axum::Extension; +use serde::Deserialize; +use utoipa::IntoParams; +use uuid::Uuid; + +use erp_core::error::AppError; +use erp_core::rbac::require_permission; +use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext}; + +use crate::service::critical_alert_service; +use crate::state::HealthState; + +#[derive(Debug, Deserialize, IntoParams)] +pub struct CriticalAlertListQuery { + pub page: Option, + pub page_size: Option, +} + +pub async fn list_critical_alerts( + State(state): State, + Extension(ctx): Extension, + Query(query): Query, +) -> Result +where + HealthState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "health.critical-alert.list")?; + let page = query.page.unwrap_or(1); + let page_size = query.page_size.unwrap_or(20); + + let (items, total) = critical_alert_service::list_pending_alerts( + &state, ctx.tenant_id, page, page_size, + ) + .await?; + + Ok(axum::Json(ApiResponse::ok(PaginatedResponse { + data: items, + total, + page, + page_size, + total_pages: total.div_ceil(page_size.max(1)), + }))) +} + +pub async fn get_critical_alert( + State(state): State, + Extension(ctx): Extension, + Path(id): Path, +) -> Result +where + HealthState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "health.critical-alert.list")?; + let alert = critical_alert_service::get_alert(&state, ctx.tenant_id, id).await?; + Ok(axum::Json(ApiResponse::ok(alert))) +} + +#[derive(Debug, serde::Deserialize, utoipa::ToSchema)] +pub struct AcknowledgeCriticalAlertRequest { + pub notes: Option, +} + +pub async fn acknowledge_critical_alert( + State(state): State, + Extension(ctx): Extension, + Path(id): Path, + axum::Json(body): axum::Json, +) -> Result +where + HealthState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "health.critical-alert.manage")?; + critical_alert_service::acknowledge_alert( + &state, + ctx.tenant_id, + id, + ctx.user_id, + body.notes, + ) + .await?; + Ok(axum::Json(ApiResponse::ok(serde_json::json!({"message": "告警已确认"})))) +} diff --git a/crates/erp-health/src/handler/mod.rs b/crates/erp-health/src/handler/mod.rs index 8843483..f30949c 100644 --- a/crates/erp-health/src/handler/mod.rs +++ b/crates/erp-health/src/handler/mod.rs @@ -6,6 +6,7 @@ pub mod article_handler; pub mod article_tag_handler; pub mod consultation_handler; pub mod consent_handler; +pub mod critical_alert_handler; pub mod critical_value_threshold_handler; pub mod daily_monitoring_handler; pub mod device_reading_handler; diff --git a/crates/erp-health/src/module.rs b/crates/erp-health/src/module.rs index 7d8c88e..dc5ec51 100644 --- a/crates/erp-health/src/module.rs +++ b/crates/erp-health/src/module.rs @@ -7,7 +7,7 @@ use erp_core::module::{ErpModule, PermissionDescriptor}; use crate::handler::{ alert_handler, alert_rule_handler, - appointment_handler, article_category_handler, article_handler, article_tag_handler, consultation_handler, consent_handler, critical_value_threshold_handler, daily_monitoring_handler, device_reading_handler, diagnosis_handler, dialysis_handler, dialysis_prescription_handler, doctor_handler, follow_up_handler, follow_up_template_handler, + appointment_handler, article_category_handler, article_handler, article_tag_handler, consultation_handler, consent_handler, critical_alert_handler, critical_value_threshold_handler, daily_monitoring_handler, device_reading_handler, diagnosis_handler, dialysis_handler, dialysis_prescription_handler, doctor_handler, follow_up_handler, follow_up_template_handler, health_data_handler, medication_record_handler, patient_handler, points_handler, stats_handler, }; @@ -635,6 +635,19 @@ impl HealthModule { "/health/alerts/{id}/resolve", axum::routing::put(alert_handler::resolve), ) + // 危急值告警路由 + .route( + "/health/critical-alerts", + axum::routing::get(critical_alert_handler::list_critical_alerts), + ) + .route( + "/health/critical-alerts/{id}", + axum::routing::get(critical_alert_handler::get_critical_alert), + ) + .route( + "/health/critical-alerts/{id}/acknowledge", + axum::routing::post(critical_alert_handler::acknowledge_critical_alert), + ) .route( "/health/alert-rules", axum::routing::get(alert_rule_handler::list_rules)