diff --git a/crates/erp-health/src/service/ai_action_dispatcher.rs b/crates/erp-health/src/service/ai_action_dispatcher.rs new file mode 100644 index 0000000..db1950b --- /dev/null +++ b/crates/erp-health/src/service/ai_action_dispatcher.rs @@ -0,0 +1,185 @@ +use std::time::Duration; + +use erp_core::events::EventBus; +use sea_orm::DatabaseConnection; +use uuid::Uuid; + +/// 执行模式 +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ExecutionMode { + AutoExecute, + DoctorReview, + UrgentConfirm, +} + +/// 分发决策 +#[derive(Debug, Clone)] +pub struct DispatchDecision { + pub execution_mode: ExecutionMode, + pub response_timeout: Option, +} + +/// 根据风险等级和建议类型生成执行决策 +pub fn dispatch_decision(risk_level: &str, _suggestion_type: &str) -> DispatchDecision { + match risk_level { + "low" => DispatchDecision { + execution_mode: ExecutionMode::AutoExecute, + response_timeout: None, + }, + "medium" => DispatchDecision { + execution_mode: ExecutionMode::DoctorReview, + response_timeout: Some(Duration::from_secs(86400)), + }, + "high" => DispatchDecision { + execution_mode: ExecutionMode::UrgentConfirm, + response_timeout: Some(Duration::from_secs(14400)), + }, + _ => DispatchDecision { + execution_mode: ExecutionMode::DoctorReview, + response_timeout: Some(Duration::from_secs(86400)), + }, + } +} + +/// 处理 AI 建议事件:根据风险等级分发到不同执行路径 +pub async fn handle_ai_suggestions( + db: &DatabaseConnection, + event_bus: &EventBus, + tenant_id: Uuid, + _analysis_id: Uuid, + patient_id: Uuid, + doctor_id: Option, + suggestions: &[serde_json::Value], + risk_level: &str, +) { + for suggestion in suggestions { + let suggestion_type = suggestion["type"].as_str().unwrap_or("alert"); + let decision = dispatch_decision(risk_level, suggestion_type); + + match decision.execution_mode { + ExecutionMode::AutoExecute => { + execute_action( + db, + event_bus, + tenant_id, + patient_id, + suggestion_type, + suggestion, + ) + .await; + } + ExecutionMode::DoctorReview | ExecutionMode::UrgentConfirm => { + create_pending_action( + db, + event_bus, + tenant_id, + patient_id, + doctor_id, + suggestion_type, + suggestion, + risk_level, + &decision, + ) + .await; + } + } + } +} + +async fn execute_action( + db: &DatabaseConnection, + event_bus: &EventBus, + tenant_id: Uuid, + patient_id: Uuid, + action_type: &str, + params: &serde_json::Value, +) { + match action_type { + "alert" => { + let event = erp_core::events::DomainEvent::new( + "health.ai_alert.sent", + tenant_id, + erp_core::events::build_event_payload(serde_json::json!({ + "patient_id": patient_id, + "alert_type": "ai_risk_warning", + "severity": params.get("severity").and_then(|v| v.as_str()).unwrap_or("warning"), + "message": params.get("message").and_then(|v| v.as_str()).unwrap_or(""), + "source": "ai_analysis", + })), + ); + event_bus.publish(event, db).await; + } + "followup" | "appointment" => { + let event = erp_core::events::DomainEvent::new( + "health.ai_action.auto_executed", + tenant_id, + erp_core::events::build_event_payload(serde_json::json!({ + "patient_id": patient_id, + "action_type": action_type, + "params": params, + })), + ); + event_bus.publish(event, db).await; + } + _ => {} + } +} + +async fn create_pending_action( + db: &DatabaseConnection, + event_bus: &EventBus, + tenant_id: Uuid, + patient_id: Uuid, + doctor_id: Option, + action_type: &str, + params: &serde_json::Value, + risk_level: &str, + decision: &DispatchDecision, +) { + let event = erp_core::events::DomainEvent::new( + "health.ai_action.pending_approval", + tenant_id, + erp_core::events::build_event_payload(serde_json::json!({ + "patient_id": patient_id, + "doctor_id": doctor_id, + "action_type": action_type, + "risk_level": risk_level, + "timeout_seconds": decision.response_timeout.map(|d| d.as_secs()), + "params": params, + })), + ); + event_bus.publish(event, db).await; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn route_low_risk_to_auto_execute() { + let decision = dispatch_decision("low", "alert"); + assert_eq!(decision.execution_mode, ExecutionMode::AutoExecute); + assert_eq!(decision.response_timeout, None); + } + + #[test] + fn route_medium_risk_to_doctor_review() { + let decision = dispatch_decision("medium", "followup"); + assert_eq!(decision.execution_mode, ExecutionMode::DoctorReview); + assert_eq!(decision.response_timeout, Some(Duration::from_secs(86400))); + } + + #[test] + fn route_high_risk_to_urgent_confirm() { + let decision = dispatch_decision("high", "alert"); + assert_eq!(decision.execution_mode, ExecutionMode::UrgentConfirm); + assert_eq!(decision.response_timeout, Some(Duration::from_secs(14400))); + } + + #[test] + fn route_unknown_defaults_to_doctor_review() { + let decision = dispatch_decision("unknown", "followup"); + assert_eq!(decision.execution_mode, ExecutionMode::DoctorReview); + assert_eq!(decision.response_timeout, Some(Duration::from_secs(86400))); + } +} diff --git a/crates/erp-health/src/service/mod.rs b/crates/erp-health/src/service/mod.rs index 3dbb065..7c244e3 100644 --- a/crates/erp-health/src/service/mod.rs +++ b/crates/erp-health/src/service/mod.rs @@ -1,3 +1,4 @@ +pub mod ai_action_dispatcher; pub mod alert_engine; pub mod alert_rule_service; pub mod alert_service;