diff --git a/crates/erp-health/src/module.rs b/crates/erp-health/src/module.rs index a1907d4..a3757ff 100644 --- a/crates/erp-health/src/module.rs +++ b/crates/erp-health/src/module.rs @@ -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() -> Router where crate::state::HealthState: axum::extract::FromRef, @@ -473,6 +516,10 @@ impl ErpModule for HealthModule { let _daily_agg_handle = Self::start_daily_aggregation(ctx.db.clone()); 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(()) } diff --git a/crates/erp-health/src/service/stats_service/mod.rs b/crates/erp-health/src/service/stats_service/mod.rs index 699c4d1..b846c15 100644 --- a/crates/erp-health/src/service/stats_service/mod.rs +++ b/crates/erp-health/src/service/stats_service/mod.rs @@ -31,3 +31,47 @@ pub use dashboard::get_module_status; pub use dashboard::get_points_recent_activity; pub use dashboard::get_system_health; 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, 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) +}