feat(health): AI 行动分发器 — 风险分级路由到自动执行/医生审批/紧急确认
- dispatch_decision: 根据风险等级生成执行决策(low=自动, medium=24h审批, high=4h紧急) - handle_ai_suggestions: 遍历建议列表,按决策分发 - execute_action: 低风险自动发送预警/随访事件 - create_pending_action: 中高风险发送待审批事件 - 4 个单元测试覆盖:低/中/高/未知风险等级路由
This commit is contained in:
185
crates/erp-health/src/service/ai_action_dispatcher.rs
Normal file
185
crates/erp-health/src/service/ai_action_dispatcher.rs
Normal 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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod ai_action_dispatcher;
|
||||||
pub mod alert_engine;
|
pub mod alert_engine;
|
||||||
pub mod alert_rule_service;
|
pub mod alert_rule_service;
|
||||||
pub mod alert_service;
|
pub mod alert_service;
|
||||||
|
|||||||
Reference in New Issue
Block a user