feat(health+ai): P2 咨询联动 + AI 巡检消费 — 全链路打通

业务链路打通 5/5 断点全部完成:
- 咨询→随访:医生端新增"创建随访"按钮,从咨询会话直接创建随访任务
- 咨询→AI:医生端新增"AI 分析"按钮,对咨询上下文触发 AI 分析
- 告警→咨询:小程序告警详情页新增"在线咨询"快捷入口
- AI 巡检消费:erp-ai 新增 patrol_consumer,订阅 ai.patrol.requested 事件
- 前端联动:Web ConsultationDetail + 小程序 alerts 页面联动实现

后端:2 新 API + 2 handler + 1 service + AI event consumer
前端:Web 2 API + 1 页面改造 + 小程序 2 页面改造
测试:Web consultations.test.ts 9/9 通过
This commit is contained in:
iven
2026-05-20 17:50:49 +08:00
parent 5f34e5715a
commit fa1dc764a3
15 changed files with 888 additions and 8 deletions

View File

@@ -1,3 +1,5 @@
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
/// alert.triggered → 告警消息通知 + 告警聚合
pub fn spawn(state: &crate::state::HealthState) -> Vec<erp_core::events::SubscriptionHandle> {
let mut handles = Vec::new();
@@ -28,7 +30,54 @@ pub fn spawn(state: &crate::state::HealthState) -> Vec<erp_core::events::Subscri
.get("rule_name")
.and_then(|v| v.as_str())
.unwrap_or("健康告警");
if let Some(pid) = patient_id {
// 检查患者是否有活跃咨询会话active / waiting
let patient_uuid = uuid::Uuid::parse_str(pid).ok();
let active_session = if let Some(puid) = patient_uuid {
crate::entity::consultation_session::Entity::find()
.filter(
crate::entity::consultation_session::Column::PatientId.eq(puid),
)
.filter(
crate::entity::consultation_session::Column::TenantId
.eq(event.tenant_id),
)
.filter(
crate::entity::consultation_session::Column::DeletedAt
.is_null(),
)
.filter(
sea_orm::Condition::any()
.add(
crate::entity::consultation_session::Column::Status
.eq("active"),
)
.add(
crate::entity::consultation_session::Column::Status
.eq("waiting"),
),
)
.one(&alert_db)
.await
.ok()
.flatten()
} else {
None
};
let consultation_session_id =
active_session.as_ref().map(|s| s.id.to_string());
let mut params = serde_json::json!({
"rule_name": rule_name,
"severity": severity,
"suggested_action": "consult",
});
if let Some(ref sid) = consultation_session_id {
params["consultation_session_id"] = serde_json::json!(sid);
}
let notify_event = erp_core::events::DomainEvent::new(
"message.send",
event.tenant_id,
@@ -37,14 +86,16 @@ pub fn spawn(state: &crate::state::HealthState) -> Vec<erp_core::events::Subscri
"recipient_type": "patient",
"recipient_id": pid,
"template_key": if severity == "critical" { "CRITICAL_HEALTH_ALERT" } else { "HEALTH_DATA_ABNORMAL" },
"params": {
"rule_name": rule_name,
"severity": severity,
}
"params": params,
})),
);
alert_bus.publish(notify_event, &alert_db).await;
tracing::info!(patient_id = %pid, severity = %severity, "告警通知已发送");
tracing::info!(
patient_id = %pid,
severity = %severity,
consultation_session_id = ?consultation_session_id,
"告警通知已发送(含咨询联动建议)"
);
}
let _ = erp_core::events::mark_event_processed(
&alert_db,