feat(health): 危急值告警 service — 创建/确认/升级扫描/列表查询
- handle_critical_alert_event: 消费事件创建告警记录 - acknowledge_alert: 医生确认 + 创建响应记录 - scan_escalation: 30min→L1, 60min→L2 分级升级 - list_pending_alerts / get_alert: 查询接口
This commit is contained in:
@@ -80,6 +80,9 @@ pub enum HealthError {
|
||||
#[error("随访模板不存在")]
|
||||
FollowUpTemplateNotFound,
|
||||
|
||||
#[error("危急值告警不存在")]
|
||||
CriticalAlertNotFound,
|
||||
|
||||
#[error("状态转换无效: {0}")]
|
||||
InvalidStatusTransition(String),
|
||||
|
||||
@@ -117,7 +120,8 @@ impl From<HealthError> for AppError {
|
||||
| HealthError::AlertRuleNotFound
|
||||
| HealthError::AlertNotFound
|
||||
| HealthError::DialysisPrescriptionNotFound
|
||||
| HealthError::FollowUpTemplateNotFound => AppError::NotFound(err.to_string()),
|
||||
| HealthError::FollowUpTemplateNotFound
|
||||
| HealthError::CriticalAlertNotFound => AppError::NotFound(err.to_string()),
|
||||
HealthError::ScheduleFull => AppError::Validation(err.to_string()),
|
||||
HealthError::InvalidStatusTransition(s) => AppError::Validation(s),
|
||||
HealthError::VersionMismatch => AppError::VersionMismatch,
|
||||
|
||||
221
crates/erp-health/src/service/critical_alert_service.rs
Normal file
221
crates/erp-health/src/service/critical_alert_service.rs
Normal file
@@ -0,0 +1,221 @@
|
||||
//! 危急值告警 Service — 创建告警、确认、升级
|
||||
|
||||
use chrono::Utc;
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::{ActiveValue::Set, QueryOrder, QuerySelect};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::entity::{critical_alert, critical_alert_response};
|
||||
use crate::error::{HealthError, HealthResult};
|
||||
use crate::state::HealthState;
|
||||
|
||||
/// 消费 health_data.critical_alert 事件,创建告警记录
|
||||
pub async fn handle_critical_alert_event(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
patient_id: Uuid,
|
||||
alert_type: &str,
|
||||
metric_name: &str,
|
||||
metric_value: &str,
|
||||
threshold_value: &str,
|
||||
created_by: Option<Uuid>,
|
||||
) -> HealthResult<Uuid> {
|
||||
let id = Uuid::now_v7();
|
||||
let now = Utc::now();
|
||||
let alert = critical_alert::ActiveModel {
|
||||
id: Set(id),
|
||||
tenant_id: Set(tenant_id),
|
||||
patient_id: Set(patient_id),
|
||||
alert_type: Set(alert_type.to_string()),
|
||||
metric_name: Set(metric_name.to_string()),
|
||||
metric_value: Set(metric_value.to_string()),
|
||||
threshold_value: Set(threshold_value.to_string()),
|
||||
severity: Set("critical".to_string()),
|
||||
status: Set("pending".to_string()),
|
||||
acknowledged_by: Set(None),
|
||||
acknowledged_at: Set(None),
|
||||
escalation_level: Set(0),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
created_by: Set(created_by),
|
||||
updated_by: Set(None),
|
||||
deleted_at: Set(None),
|
||||
version: Set(1),
|
||||
};
|
||||
critical_alert::Entity::insert(alert)
|
||||
.exec(&state.db)
|
||||
.await?;
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// 医生确认告警
|
||||
pub async fn acknowledge_alert(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
alert_id: Uuid,
|
||||
responder_id: Uuid,
|
||||
notes: Option<String>,
|
||||
) -> HealthResult<()> {
|
||||
let alert = critical_alert::Entity::find_by_id(alert_id)
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::CriticalAlertNotFound)?;
|
||||
|
||||
if alert.tenant_id != tenant_id {
|
||||
return Err(HealthError::CriticalAlertNotFound);
|
||||
}
|
||||
if alert.status != "pending" && alert.status != "escalated" {
|
||||
return Err(HealthError::InvalidStatusTransition(format!(
|
||||
"告警状态为 {},无法确认",
|
||||
alert.status
|
||||
)));
|
||||
}
|
||||
|
||||
let now = Utc::now();
|
||||
let mut active: critical_alert::ActiveModel = alert.into();
|
||||
active.status = Set("acknowledged".to_string());
|
||||
active.acknowledged_by = Set(Some(responder_id));
|
||||
active.acknowledged_at = Set(Some(now));
|
||||
active.updated_at = Set(now);
|
||||
active.updated_by = Set(Some(responder_id));
|
||||
active.version = Set(active.version.unwrap() + 1);
|
||||
critical_alert::Entity::update(active)
|
||||
.exec(&state.db)
|
||||
.await?;
|
||||
|
||||
// 创建响应记录
|
||||
let resp_id = Uuid::now_v7();
|
||||
let response = critical_alert_response::ActiveModel {
|
||||
id: Set(resp_id),
|
||||
tenant_id: Set(tenant_id),
|
||||
alert_id: Set(alert_id),
|
||||
responder_id: Set(responder_id),
|
||||
response_type: Set("acknowledge".to_string()),
|
||||
notes: Set(notes),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
created_by: Set(Some(responder_id)),
|
||||
updated_by: Set(None),
|
||||
deleted_at: Set(None),
|
||||
version: Set(1),
|
||||
};
|
||||
critical_alert_response::Entity::insert(response)
|
||||
.exec(&state.db)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 升级扫描 — 由定时任务每分钟调用
|
||||
pub async fn scan_escalation(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
) -> HealthResult<Vec<Uuid>> {
|
||||
let now = Utc::now();
|
||||
let mut escalated = Vec::new();
|
||||
|
||||
// Level 1: pending/escalated 且 created_at < 30min → escalation_level = 1
|
||||
let threshold_l1 = now - chrono::Duration::minutes(30);
|
||||
let alerts_l1 = critical_alert::Entity::find()
|
||||
.filter(critical_alert::Column::TenantId.eq(tenant_id))
|
||||
.filter(
|
||||
critical_alert::Column::Status
|
||||
.is_in(vec!["pending".to_string(), "escalated".to_string()]),
|
||||
)
|
||||
.filter(critical_alert::Column::CreatedAt.lt(threshold_l1))
|
||||
.filter(critical_alert::Column::EscalationLevel.lt(1))
|
||||
.filter(critical_alert::Column::DeletedAt.is_null())
|
||||
.all(&state.db)
|
||||
.await?;
|
||||
|
||||
for alert in alerts_l1 {
|
||||
let mut active: critical_alert::ActiveModel = alert.clone().into();
|
||||
active.escalation_level = Set(1);
|
||||
active.status = Set("escalated".to_string());
|
||||
active.updated_at = Set(now);
|
||||
active.version = Set(active.version.unwrap() + 1);
|
||||
critical_alert::Entity::update(active)
|
||||
.exec(&state.db)
|
||||
.await?;
|
||||
escalated.push(alert.id);
|
||||
}
|
||||
|
||||
// Level 2: pending/escalated 且 created_at < 60min → escalation_level = 2
|
||||
let threshold_l2 = now - chrono::Duration::minutes(60);
|
||||
let alerts_l2 = critical_alert::Entity::find()
|
||||
.filter(critical_alert::Column::TenantId.eq(tenant_id))
|
||||
.filter(
|
||||
critical_alert::Column::Status
|
||||
.is_in(vec!["pending".to_string(), "escalated".to_string()]),
|
||||
)
|
||||
.filter(critical_alert::Column::CreatedAt.lt(threshold_l2))
|
||||
.filter(critical_alert::Column::EscalationLevel.lt(2))
|
||||
.filter(critical_alert::Column::DeletedAt.is_null())
|
||||
.all(&state.db)
|
||||
.await?;
|
||||
|
||||
for alert in alerts_l2 {
|
||||
let mut active: critical_alert::ActiveModel = alert.clone().into();
|
||||
active.escalation_level = Set(2);
|
||||
active.updated_at = Set(now);
|
||||
active.version = Set(active.version.unwrap() + 1);
|
||||
critical_alert::Entity::update(active)
|
||||
.exec(&state.db)
|
||||
.await?;
|
||||
if !escalated.contains(&alert.id) {
|
||||
escalated.push(alert.id);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(escalated)
|
||||
}
|
||||
|
||||
/// 查询待处理告警列表
|
||||
pub async fn list_pending_alerts(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
page: u64,
|
||||
page_size: u64,
|
||||
) -> HealthResult<(Vec<critical_alert::Model>, u64)> {
|
||||
let limit = page_size.min(100);
|
||||
let offset = page.saturating_sub(1) * limit;
|
||||
|
||||
let query = critical_alert::Entity::find()
|
||||
.filter(critical_alert::Column::TenantId.eq(tenant_id))
|
||||
.filter(critical_alert::Column::DeletedAt.is_null())
|
||||
.filter(
|
||||
critical_alert::Column::Status
|
||||
.is_in(vec!["pending".to_string(), "escalated".to_string()]),
|
||||
);
|
||||
|
||||
let total = query
|
||||
.clone()
|
||||
.count(&state.db)
|
||||
.await?;
|
||||
|
||||
let items = query
|
||||
.order_by(critical_alert::Column::CreatedAt, sea_orm::Order::Desc)
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
.all(&state.db)
|
||||
.await?;
|
||||
|
||||
Ok((items, total))
|
||||
}
|
||||
|
||||
/// 查询告警详情
|
||||
pub async fn get_alert(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
alert_id: Uuid,
|
||||
) -> HealthResult<critical_alert::Model> {
|
||||
let alert = critical_alert::Entity::find_by_id(alert_id)
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::CriticalAlertNotFound)?;
|
||||
|
||||
if alert.tenant_id != tenant_id {
|
||||
return Err(HealthError::CriticalAlertNotFound);
|
||||
}
|
||||
Ok(alert)
|
||||
}
|
||||
@@ -7,6 +7,7 @@ pub mod article_service;
|
||||
pub mod article_tag_service;
|
||||
pub mod consultation_service;
|
||||
pub mod consent_service;
|
||||
pub mod critical_alert_service;
|
||||
pub mod critical_value_threshold_service;
|
||||
pub mod daily_monitoring_service;
|
||||
pub mod device_reading_service;
|
||||
|
||||
Reference in New Issue
Block a user