From 5f34e5715a6f7a986b7907d7d941ec46e1edc9ad Mon Sep 17 00:00:00 2001 From: iven Date: Wed, 20 May 2026 12:30:04 +0800 Subject: [PATCH] =?UTF-8?q?feat(health):=20AI=20=E4=B8=BB=E5=8A=A8?= =?UTF-8?q?=E5=B7=A1=E6=A3=80=E5=AE=9A=E6=97=B6=E4=BB=BB=E5=8A=A1=20?= =?UTF-8?q?=E2=80=94=20=E6=AF=8F=E6=97=A5=E6=89=AB=E6=8F=8F=E5=BC=82?= =?UTF-8?q?=E5=B8=B8=E6=82=A3=E8=80=85=E8=A7=A6=E5=8F=91=20AI=20=E5=88=86?= =?UTF-8?q?=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 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 --- crates/erp-health/src/module.rs | 47 +++++++++++++++++++ .../src/service/stats_service/mod.rs | 44 +++++++++++++++++ 2 files changed, 91 insertions(+) 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) +}