feat(health): AI 行动分发器 — 风险分级路由到自动执行/医生审批/紧急确认

- dispatch_decision: 根据风险等级生成执行决策(low=自动, medium=24h审批, high=4h紧急)
- handle_ai_suggestions: 遍历建议列表,按决策分发
- execute_action: 低风险自动发送预警/随访事件
- create_pending_action: 中高风险发送待审批事件
- 4 个单元测试覆盖:低/中/高/未知风险等级路由
This commit is contained in:
iven
2026-05-01 08:34:04 +08:00
parent 4b3193fcd6
commit 69f9e1a61a
2 changed files with 186 additions and 0 deletions

View File

@@ -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<Duration>,
}
/// 根据风险等级和建议类型生成执行决策
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<Uuid>,
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<Uuid>,
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)));
}
}

View File

@@ -1,3 +1,4 @@
pub mod ai_action_dispatcher;
pub mod alert_engine;
pub mod alert_rule_service;
pub mod alert_service;