diff --git a/crates/erp-ai/src/copilot/engine.rs b/crates/erp-ai/src/copilot/engine.rs index 6c9541e..d7e4d65 100644 --- a/crates/erp-ai/src/copilot/engine.rs +++ b/crates/erp-ai/src/copilot/engine.rs @@ -1,4 +1,4 @@ -use crate::copilot::rules::{RuleData, evaluate_rules}; +use crate::copilot::rules::{MatchedRuleData, RuleData, evaluate_rules}; use crate::copilot::scoring::{RiskScore, calculate_risk}; use serde_json::Value; @@ -12,3 +12,84 @@ impl CopilotEngine { calculate_risk(matched) } } + +/// 根据规则匹配结果生成异常洞察 +/// 仅 warning 和 critical 级别生成告警洞察,info 级别仅在档案内展示 +pub fn generate_anomaly_insights( + patient_id: &str, + matched: &[MatchedRuleData], +) -> Vec { + matched + .iter() + .filter(|(_, _, _, severity, _)| severity == "warning" || severity == "critical") + .map(|(rule_id, name, score, severity, suggestion)| { + serde_json::json!({ + "patient_id": patient_id, + "insight_type": "anomaly", + "source": "rule", + "severity": severity, + "title": name, + "content": { + "rule_id": rule_id.to_string(), + "score": score, + "suggestion": suggestion, + }, + }) + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_generate_anomaly_insight_critical() { + let matched = vec![( + uuid::Uuid::now_v7(), + "高钾血症风险".into(), + 4, + "critical".into(), + Some("立即通知主治医生".into()), + )]; + let insights = generate_anomaly_insights("patient-123", &matched); + assert_eq!(insights.len(), 1); + assert_eq!(insights[0]["severity"], "critical"); + assert_eq!(insights[0]["insight_type"], "anomaly"); + } + + #[test] + fn test_generate_anomaly_insight_filters_info() { + let matched = vec![( + uuid::Uuid::now_v7(), + "体重轻微波动".into(), + 1, + "info".into(), + None, + )]; + let insights = generate_anomaly_insights("patient-123", &matched); + assert!(insights.is_empty()); + } + + #[test] + fn test_generate_anomaly_insight_warning_and_critical() { + let matched = vec![ + ( + uuid::Uuid::now_v7(), + "eGFR下降".into(), + 3, + "warning".into(), + Some("建议调整".into()), + ), + ( + uuid::Uuid::now_v7(), + "透析质量危急".into(), + 5, + "critical".into(), + Some("紧急评估".into()), + ), + ]; + let insights = generate_anomaly_insights("patient-123", &matched); + assert_eq!(insights.len(), 2); + } +} diff --git a/crates/erp-ai/src/event/copilot_consumer.rs b/crates/erp-ai/src/event/copilot_consumer.rs index 8f750e4..fdcc5d5 100644 --- a/crates/erp-ai/src/event/copilot_consumer.rs +++ b/crates/erp-ai/src/event/copilot_consumer.rs @@ -58,8 +58,45 @@ async fn process_event(db: &sea_orm::DatabaseConnection, event: &erp_core::event }, None => return, }; - let _ = - crate::service::risk_service::RiskService::compute_risk(db, tenant_id, patient_id).await; + // 风险评分计算 + if let Ok(risk) = + crate::service::risk_service::RiskService::compute_risk(db, tenant_id, patient_id).await + { + // 异常检测:告警级规则匹配生成洞察 + let matched_with_severity: Vec<_> = risk + .matched_rules + .into_iter() + .map(|r| (r.rule_id, r.name, r.score, r.severity, r.suggestion)) + .collect(); + let anomaly_insights = crate::copilot::engine::generate_anomaly_insights( + &patient_id.to_string(), + &matched_with_severity, + ); + for insight_data in anomaly_insights { + let severity = insight_data["severity"] + .as_str() + .unwrap_or("warning") + .to_string(); + let title = insight_data["title"] + .as_str() + .unwrap_or("异常告警") + .to_string(); + let _ = crate::service::insight_service::InsightService::create_insight( + db, + tenant_id, + patient_id, + "anomaly".into(), + "rule".into(), + Some(severity), + title, + insight_data, + None, + 168, // 7 天过期 + None, + ) + .await; + } + } let _ = erp_core::events::mark_event_processed(db, event.id, "copilot_consumer").await; }