feat(health): 业务链路打通 — 告警自动随访 + 健康数据积分激励
- 迁移 000157: follow_up_task 新增 source_type/source_id 字段追踪任务来源 - 迁移 000157: points_rule 新增 health_data_report/lab_report_upload/streak_7_days 种子规则 - P0-2: follow_up 事件处理器新增 health_data.critical_alert 消费,告警触发时自动创建随访任务 - 自动查找管床医生分配,critical 级别 1 天内、warning 级别 3 天内 - 新增 alert_auto 随访类型,source_type 标记来源为 critical_alert - P1-1: points 事件处理器新增 daily_monitoring.created 消费,日常监测上报自动获取积分 - P1-1: points 事件处理器新增 lab_report.uploaded 消费,化验报告上传自动获取积分 - 更新 HMS 系统设计思路文档 v2.0(实体数/链路图/业务链路章节全面更新) - 新增业务链路打通讨论记录 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
/// follow_up.overdue → 升级通知 + follow_up.created → 通知执行人
|
||||
/// health_data.critical_alert → 自动创建告警随访任务
|
||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
|
||||
|
||||
pub fn spawn(state: &crate::state::HealthState) -> Vec<erp_core::events::SubscriptionHandle> {
|
||||
let mut handles = Vec::new();
|
||||
|
||||
@@ -139,5 +142,134 @@ pub fn spawn(state: &crate::state::HealthState) -> Vec<erp_core::events::Subscri
|
||||
}
|
||||
});
|
||||
|
||||
// health_data.critical_alert → 自动创建告警随访任务
|
||||
let (mut critical_rx, critical_handle) = state
|
||||
.event_bus
|
||||
.subscribe_filtered("health_data.".to_string());
|
||||
handles.push(critical_handle);
|
||||
let critical_state = state.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match critical_rx.recv().await {
|
||||
Some(event) if event.event_type == super::HEALTH_DATA_CRITICAL_ALERT => {
|
||||
if erp_core::events::is_event_processed(
|
||||
&critical_state.db,
|
||||
event.id,
|
||||
"critical_alert_followup_creator",
|
||||
)
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let patient_id = event
|
||||
.payload
|
||||
.get("patient_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| uuid::Uuid::parse_str(s).ok());
|
||||
|
||||
let Some(pid) = patient_id else {
|
||||
let _ = erp_core::events::mark_event_processed(
|
||||
&critical_state.db,
|
||||
event.id,
|
||||
"critical_alert_followup_creator",
|
||||
)
|
||||
.await;
|
||||
continue;
|
||||
};
|
||||
|
||||
let tenant_id = event.tenant_id;
|
||||
let alert_obj = event.payload.get("alert");
|
||||
let metric_name = alert_obj
|
||||
.and_then(|a| a.get("indicator"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("unknown");
|
||||
let metric_value = alert_obj
|
||||
.and_then(|a| a.get("value"))
|
||||
.and_then(|v| v.as_f64())
|
||||
.map(|v| v.to_string())
|
||||
.unwrap_or_default();
|
||||
let severity = alert_obj
|
||||
.and_then(|a| a.get("level"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("critical");
|
||||
|
||||
// 查找管床医生
|
||||
let assigned_doctor: Option<uuid::Uuid> =
|
||||
crate::entity::patient_doctor_relation::Entity::find()
|
||||
.filter(
|
||||
crate::entity::patient_doctor_relation::Column::PatientId.eq(pid),
|
||||
)
|
||||
.filter(
|
||||
crate::entity::patient_doctor_relation::Column::TenantId
|
||||
.eq(tenant_id),
|
||||
)
|
||||
.filter(
|
||||
crate::entity::patient_doctor_relation::Column::DeletedAt.is_null(),
|
||||
)
|
||||
.one(&critical_state.db)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|r| r.doctor_id);
|
||||
|
||||
let planned_date = chrono::Local::now().date_naive()
|
||||
+ chrono::Duration::days(if severity == "critical" { 1 } else { 3 });
|
||||
|
||||
let content = format!(
|
||||
"系统自动创建:患者健康数据异常告警({}: {},严重程度: {})",
|
||||
metric_name, metric_value, severity
|
||||
);
|
||||
|
||||
let req = crate::dto::follow_up_dto::CreateFollowUpTaskReq {
|
||||
patient_id: pid,
|
||||
assigned_to: assigned_doctor,
|
||||
follow_up_type: "alert_auto".to_string(),
|
||||
planned_date,
|
||||
content_template: Some(content),
|
||||
related_appointment_id: None,
|
||||
source_type: Some("critical_alert".to_string()),
|
||||
source_id: Some(event.id),
|
||||
};
|
||||
|
||||
match crate::service::follow_up_service::create_task(
|
||||
&critical_state,
|
||||
tenant_id,
|
||||
None,
|
||||
req,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(task) => {
|
||||
tracing::info!(
|
||||
patient_id = %pid,
|
||||
task_id = %task.id,
|
||||
metric = %metric_name,
|
||||
"告警自动随访任务已创建"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
patient_id = %pid,
|
||||
error = %e,
|
||||
"告警自动随访任务创建失败"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let _ = erp_core::events::mark_event_processed(
|
||||
&critical_state.db,
|
||||
event.id,
|
||||
"critical_alert_followup_creator",
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Some(_) => {}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
handles
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/// points.earned/exchanged/expired → 积分变动通知
|
||||
/// daily_monitoring.created → 健康数据上报积分
|
||||
/// lab_report.uploaded → 化验报告上传积分
|
||||
pub fn spawn(state: &crate::state::HealthState) -> Vec<erp_core::events::SubscriptionHandle> {
|
||||
let mut handles = Vec::new();
|
||||
|
||||
@@ -120,5 +122,146 @@ pub fn spawn(state: &crate::state::HealthState) -> Vec<erp_core::events::Subscri
|
||||
}
|
||||
});
|
||||
|
||||
// daily_monitoring.created → 健康数据上报积分
|
||||
let (mut dm_rx, dm_handle) = state
|
||||
.event_bus
|
||||
.subscribe_filtered("daily_monitoring.".to_string());
|
||||
handles.push(dm_handle);
|
||||
let dm_state = state.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match dm_rx.recv().await {
|
||||
Some(event) if event.event_type == super::DAILY_MONITORING_CREATED => {
|
||||
if erp_core::events::is_event_processed(
|
||||
&dm_state.db,
|
||||
event.id,
|
||||
"daily_monitoring_points",
|
||||
)
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let patient_id = event
|
||||
.payload
|
||||
.get("patient_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| uuid::Uuid::parse_str(s).ok());
|
||||
|
||||
if let Some(pid) = patient_id {
|
||||
match crate::service::points_service::earn_points(
|
||||
&dm_state,
|
||||
event.tenant_id,
|
||||
pid,
|
||||
"health_data_report",
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(tx) => {
|
||||
tracing::info!(
|
||||
patient_id = %pid,
|
||||
points = tx.amount,
|
||||
"健康数据上报积分已发放"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
// 无匹配规则时不告警(租户可能未配置该规则)
|
||||
let err_str = e.to_string();
|
||||
if !err_str.contains("无匹配的积分规则") {
|
||||
tracing::warn!(
|
||||
patient_id = %pid,
|
||||
error = %e,
|
||||
"健康数据上报积分发放失败"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = erp_core::events::mark_event_processed(
|
||||
&dm_state.db,
|
||||
event.id,
|
||||
"daily_monitoring_points",
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Some(_) => {}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// lab_report.uploaded → 化验报告上传积分
|
||||
let (mut lr_rx, lr_handle) = state
|
||||
.event_bus
|
||||
.subscribe_filtered("lab_report.".to_string());
|
||||
handles.push(lr_handle);
|
||||
let lr_state = state.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match lr_rx.recv().await {
|
||||
Some(event) if event.event_type == super::LAB_REPORT_UPLOADED => {
|
||||
if erp_core::events::is_event_processed(
|
||||
&lr_state.db,
|
||||
event.id,
|
||||
"lab_report_points",
|
||||
)
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let patient_id = event
|
||||
.payload
|
||||
.get("patient_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| uuid::Uuid::parse_str(s).ok());
|
||||
|
||||
if let Some(pid) = patient_id {
|
||||
match crate::service::points_service::earn_points(
|
||||
&lr_state,
|
||||
event.tenant_id,
|
||||
pid,
|
||||
"lab_report_upload",
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(tx) => {
|
||||
tracing::info!(
|
||||
patient_id = %pid,
|
||||
points = tx.amount,
|
||||
"化验报告上传积分已发放"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
let err_str = e.to_string();
|
||||
if !err_str.contains("无匹配的积分规则") {
|
||||
tracing::warn!(
|
||||
patient_id = %pid,
|
||||
error = %e,
|
||||
"化验报告上传积分发放失败"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = erp_core::events::mark_event_processed(
|
||||
&lr_state.db,
|
||||
event.id,
|
||||
"lab_report_points",
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Some(_) => {}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
handles
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user