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>
|
||||
where
|
||||
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());
|
||||
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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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<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