feat(health): AI 主动巡检定时任务 — 每日扫描异常患者触发 AI 分析
- 新增 start_ai_patrol 定时任务(启动延迟 10 分钟 + 每 24 小时执行) - 新增 get_patrol_candidates 函数:查询最近 7 天有未处理告警的患者 - 每个候选患者发布 ai.patrol.requested 事件(含 patient_id/doctor_id/reason) - AI 模块可订阅此事件执行自动化分析(erp-ai 侧消费) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -134,6 +134,49 @@ impl HealthModule {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 启动 AI 主动巡检(每 24 小时运行一次),扫描有异常数据的患者并发布 AI 分析请求事件
|
||||||
|
pub fn start_ai_patrol(
|
||||||
|
db: sea_orm::DatabaseConnection,
|
||||||
|
event_bus: erp_core::events::EventBus,
|
||||||
|
) -> tokio::task::JoinHandle<()> {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
// 首次延迟 10 分钟启动,等待其他服务就绪
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(600)).await;
|
||||||
|
let mut interval = tokio::time::interval(std::time::Duration::from_secs(24 * 3600));
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
_ = interval.tick() => {
|
||||||
|
match crate::service::stats_service::get_patrol_candidates(&db).await {
|
||||||
|
Ok(patients) => {
|
||||||
|
tracing::info!(count = patients.len(), "AI 主动巡检:发现待分析患者");
|
||||||
|
for (tenant_id, patient_id, doctor_id, reason) in &patients {
|
||||||
|
let patrol_event = erp_core::events::DomainEvent::new(
|
||||||
|
"ai.patrol.requested",
|
||||||
|
*tenant_id,
|
||||||
|
erp_core::events::build_event_payload(serde_json::json!({
|
||||||
|
"patient_id": patient_id.to_string(),
|
||||||
|
"doctor_id": doctor_id.map(|d| d.to_string()),
|
||||||
|
"source": "daily_patrol",
|
||||||
|
"reason": reason,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
event_bus.publish(patrol_event, &db).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(error = %e, "AI 主动巡检:获取待分析患者失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = tokio::signal::ctrl_c() => {
|
||||||
|
tracing::info!("AI 主动巡检任务收到关闭信号,正在停止");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn public_routes<S>() -> Router<S>
|
pub fn public_routes<S>() -> Router<S>
|
||||||
where
|
where
|
||||||
crate::state::HealthState: axum::extract::FromRef<S>,
|
crate::state::HealthState: axum::extract::FromRef<S>,
|
||||||
@@ -473,6 +516,10 @@ impl ErpModule for HealthModule {
|
|||||||
let _daily_agg_handle = Self::start_daily_aggregation(ctx.db.clone());
|
let _daily_agg_handle = Self::start_daily_aggregation(ctx.db.clone());
|
||||||
tracing::info!(module = "health", "Daily aggregation task started");
|
tracing::info!(module = "health", "Daily aggregation task started");
|
||||||
|
|
||||||
|
// 启动 AI 主动巡检(每天扫描异常患者,发布 ai.patrol.requested 事件)
|
||||||
|
let _patrol_handle = Self::start_ai_patrol(ctx.db.clone(), ctx.event_bus.clone());
|
||||||
|
tracing::info!(module = "health", "AI patrol task started");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,3 +31,47 @@ pub use dashboard::get_module_status;
|
|||||||
pub use dashboard::get_points_recent_activity;
|
pub use dashboard::get_points_recent_activity;
|
||||||
pub use dashboard::get_system_health;
|
pub use dashboard::get_system_health;
|
||||||
pub use dashboard::get_user_activity;
|
pub use dashboard::get_user_activity;
|
||||||
|
|
||||||
|
// ── AI 主动巡检 ──
|
||||||
|
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QueryOrder};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
/// 获取 AI 主动巡检候选患者:最近 7 天有异常告警但未做 AI 分析的患者
|
||||||
|
pub async fn get_patrol_candidates(
|
||||||
|
db: &DatabaseConnection,
|
||||||
|
) -> Result<Vec<(Uuid, Uuid, Option<Uuid>, String)>, sea_orm::DbErr> {
|
||||||
|
let seven_days_ago = chrono::Utc::now() - chrono::Duration::days(7);
|
||||||
|
|
||||||
|
// 查找最近 7 天有告警记录的患者(去重,每个患者取最新一条)
|
||||||
|
let alerts = crate::entity::alerts::Entity::find()
|
||||||
|
.filter(crate::entity::alerts::Column::CreatedAt.gte(seven_days_ago))
|
||||||
|
.filter(crate::entity::alerts::Column::DeletedAt.is_null())
|
||||||
|
.filter(crate::entity::alerts::Column::Status.is_in(["active", "acknowledged", "new"]))
|
||||||
|
.order_by_desc(crate::entity::alerts::Column::CreatedAt)
|
||||||
|
.all(db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// 按患者去重,保留最新告警
|
||||||
|
let mut seen = std::collections::HashSet::new();
|
||||||
|
let mut candidates = Vec::new();
|
||||||
|
for alert in alerts {
|
||||||
|
if seen.contains(&alert.patient_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seen.insert(alert.patient_id);
|
||||||
|
|
||||||
|
// 查找管床医生
|
||||||
|
let doctor = crate::entity::patient_doctor_relation::Entity::find()
|
||||||
|
.filter(crate::entity::patient_doctor_relation::Column::PatientId.eq(alert.patient_id))
|
||||||
|
.filter(crate::entity::patient_doctor_relation::Column::TenantId.eq(alert.tenant_id))
|
||||||
|
.filter(crate::entity::patient_doctor_relation::Column::DeletedAt.is_null())
|
||||||
|
.one(db)
|
||||||
|
.await?
|
||||||
|
.map(|r| r.doctor_id);
|
||||||
|
|
||||||
|
let reason = format!("告警未处理: {} (severity: {})", alert.title, alert.severity);
|
||||||
|
candidates.push((alert.tenant_id, alert.patient_id, doctor, reason));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(candidates)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user