From 1925568c1387c80070d8422b7ca0f7d968367b3a Mon Sep 17 00:00:00 2001 From: iven Date: Thu, 30 Apr 2026 08:31:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(message+health):=20=E8=A1=A5=E5=85=A8=2014?= =?UTF-8?q?=20=E4=B8=AA=E4=BA=8B=E4=BB=B6=E6=B6=88=E8=B4=B9=E8=80=85=20+?= =?UTF-8?q?=20=E4=BF=AE=E5=A4=8D=206=20=E4=B8=AA=E4=BA=8B=E4=BB=B6=20paylo?= =?UTF-8?q?ad=20=E7=BC=BA=E5=A4=B1=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 事件消费者补全(erp-message/module.rs): - consultation.opened: 医生收到新咨询会话通知 - consultation.closed: 患者收到会话结束通知 - follow_up.created: 被分配人收到新随访任务通知 - follow_up.completed: 患者收到随访完成通知 - points.earned: 患者收到积分到账通知 - points.exchanged: 患者收到兑换成功通知 - points.expired: 患者收到积分过期提醒 - article.published/rejected: 作者收到审核结果通知 - ai.analysis.failed: 医生收到 AI 分析失败通知 - lab_report.uploaded/patient.updated/daily_monitoring/doctor: 审计日志记录 事件 payload 补充(erp-health services): - consultation.opened: 添加 doctor_id 字段 - follow_up.created: 添加 assigned_to + planned_date 字段 - points.earned: 添加 patient_id + reason 字段 - points.exchanged: 添加 product_name 字段 - article.rejected: 添加 author_id 字段 --- .../erp-health/src/service/article_service.rs | 3 +- .../src/service/consultation_service.rs | 11 +- .../src/service/follow_up_service.rs | 7 +- .../erp-health/src/service/points_service.rs | 2 + crates/erp-message/src/module.rs | 344 ++++++++++++++++++ 5 files changed, 363 insertions(+), 4 deletions(-) diff --git a/crates/erp-health/src/service/article_service.rs b/crates/erp-health/src/service/article_service.rs index 6c0357f..2413268 100644 --- a/crates/erp-health/src/service/article_service.rs +++ b/crates/erp-health/src/service/article_service.rs @@ -235,7 +235,8 @@ pub async fn reject_article( state.event_bus.publish( DomainEvent::new(crate::event::ARTICLE_REJECTED, tenant_id, erp_core::events::build_event_payload(serde_json::json!({ - "article_id": m.id, "title": m.title, + "article_id": m.id.to_string(), "title": m.title, + "author_id": m.created_by.map(|id| id.to_string()).unwrap_or_default(), }))), &state.db, ).await; diff --git a/crates/erp-health/src/service/consultation_service.rs b/crates/erp-health/src/service/consultation_service.rs index fc6fa62..e6011f3 100644 --- a/crates/erp-health/src/service/consultation_service.rs +++ b/crates/erp-health/src/service/consultation_service.rs @@ -77,7 +77,11 @@ pub async fn create_session( let event = DomainEvent::new( crate::event::CONSULTATION_OPENED, tenant_id, - erp_core::events::build_event_payload(serde_json::json!({ "session_id": m.id, "patient_id": m.patient_id })), + erp_core::events::build_event_payload(serde_json::json!({ + "session_id": m.id.to_string(), + "patient_id": m.patient_id.to_string(), + "doctor_id": m.doctor_id.map(|id| id.to_string()).unwrap_or_default(), + })), ); state.event_bus.publish(event, &state.db).await; @@ -175,7 +179,10 @@ pub async fn close_session( let event = DomainEvent::new( crate::event::CONSULTATION_CLOSED, tenant_id, - erp_core::events::build_event_payload(serde_json::json!({ "session_id": m.id, "patient_id": m.patient_id })), + erp_core::events::build_event_payload(serde_json::json!({ + "session_id": m.id.to_string(), + "patient_id": m.patient_id.to_string(), + })), ); state.event_bus.publish(event, &state.db).await; diff --git a/crates/erp-health/src/service/follow_up_service.rs b/crates/erp-health/src/service/follow_up_service.rs index 2d60e84..f30eb4e 100644 --- a/crates/erp-health/src/service/follow_up_service.rs +++ b/crates/erp-health/src/service/follow_up_service.rs @@ -170,7 +170,12 @@ pub async fn create_task( let event = DomainEvent::new( crate::event::FOLLOW_UP_CREATED, tenant_id, - erp_core::events::build_event_payload(serde_json::json!({ "task_id": m.id, "patient_id": m.patient_id })), + erp_core::events::build_event_payload(serde_json::json!({ + "task_id": m.id.to_string(), + "patient_id": m.patient_id.to_string(), + "assigned_to": m.assigned_to.map(|id| id.to_string()).unwrap_or_default(), + "planned_date": m.planned_date.to_string(), + })), ); state.event_bus.publish(event, &state.db).await; diff --git a/crates/erp-health/src/service/points_service.rs b/crates/erp-health/src/service/points_service.rs index e9c262a..d13d661 100644 --- a/crates/erp-health/src/service/points_service.rs +++ b/crates/erp-health/src/service/points_service.rs @@ -195,6 +195,7 @@ pub async fn earn_points( DomainEvent::new(crate::event::POINTS_EARNED, tenant_id, erp_core::events::build_event_payload(serde_json::json!({ "transaction_id": inserted.id, "account_id": inserted.account_id, "amount": inserted.amount, "balance_after": inserted.balance_after, + "patient_id": patient_id.to_string(), "reason": event_type, }))), &state.db, ).await; @@ -980,6 +981,7 @@ pub async fn exchange_product( DomainEvent::new(crate::event::POINTS_EXCHANGED, tenant_id, erp_core::events::build_event_payload(serde_json::json!({ "order_id": inserted_order.id, "patient_id": inserted_order.patient_id, "product_id": inserted_order.product_id, "points_cost": inserted_order.points_cost, + "product_name": product.name, }))), &state.db, ).await; diff --git a/crates/erp-message/src/module.rs b/crates/erp-message/src/module.rs index 8d0c958..d9c415d 100644 --- a/crates/erp-message/src/module.rs +++ b/crates/erp-message/src/module.rs @@ -602,6 +602,350 @@ async fn handle_workflow_event( .map_err(|e| e.to_string())?; } } + // 咨询会话开启通知医生 + "consultation.opened" => { + let doctor_id = event + .payload + .get("doctor_id") + .and_then(|v| v.as_str()) + .and_then(|s| uuid::Uuid::parse_str(s).ok()); + let patient_name = event + .payload + .get("patient_name") + .and_then(|v| v.as_str()) + .unwrap_or("患者"); + let session_id = event + .payload + .get("session_id") + .and_then(|v| v.as_str()) + .unwrap_or(""); + + if let Some(did) = doctor_id { + if should_skip_for_dnd(event.tenant_id, did, "normal", db).await { + return Ok(()); + } + let _ = crate::service::message_service::MessageService::send_system( + event.tenant_id, + did, + format!("新咨询会话 — {}", patient_name), + format!("患者 {} 发起了咨询会话,请及时响应。", patient_name), + "normal", + Some("consultation".to_string()), + uuid::Uuid::parse_str(session_id).ok(), + db, + event_bus, + ) + .await + .map_err(|e| e.to_string())?; + } + } + // 咨询会话关闭通知患者 + "consultation.closed" => { + let patient_id = event + .payload + .get("patient_id") + .and_then(|v| v.as_str()) + .and_then(|s| uuid::Uuid::parse_str(s).ok()); + let session_id = event + .payload + .get("session_id") + .and_then(|v| v.as_str()) + .unwrap_or(""); + + if let Some(pid) = patient_id { + if should_skip_for_dnd(event.tenant_id, pid, "normal", db).await { + return Ok(()); + } + let _ = crate::service::message_service::MessageService::send_system( + event.tenant_id, + pid, + "咨询会话已结束".to_string(), + "您的咨询会话已由医生关闭,感谢使用。".to_string(), + "normal", + Some("consultation".to_string()), + uuid::Uuid::parse_str(session_id).ok(), + db, + event_bus, + ) + .await + .map_err(|e| e.to_string())?; + } + } + // 随访任务创建通知被分配人 + "follow_up.created" => { + let assigned_to = event + .payload + .get("assigned_to") + .and_then(|v| v.as_str()) + .and_then(|s| uuid::Uuid::parse_str(s).ok()); + let patient_name = event + .payload + .get("patient_name") + .and_then(|v| v.as_str()) + .unwrap_or(""); + let planned_date = event + .payload + .get("planned_date") + .and_then(|v| v.as_str()) + .unwrap_or("未知日期"); + let task_id = event + .payload + .get("task_id") + .and_then(|v| v.as_str()) + .unwrap_or(""); + + if let Some(assignee) = assigned_to { + if should_skip_for_dnd(event.tenant_id, assignee, "normal", db).await { + return Ok(()); + } + let patient_info = if patient_name.is_empty() { + String::new() + } else { + format!("(患者:{})", patient_name) + }; + let _ = crate::service::message_service::MessageService::send_system( + event.tenant_id, + assignee, + format!("新随访任务{}", patient_info), + format!("您被分配了一个随访任务{},计划日期:{}。", patient_info, planned_date), + "normal", + Some("follow_up".to_string()), + uuid::Uuid::parse_str(task_id).ok(), + db, + event_bus, + ) + .await + .map_err(|e| e.to_string())?; + } + } + // 随访完成通知患者 + "follow_up.completed" => { + 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 { + if should_skip_for_dnd(event.tenant_id, pid, "normal", db).await { + return Ok(()); + } + let _ = crate::service::message_service::MessageService::send_system( + event.tenant_id, + pid, + "随访已完成".to_string(), + "您的随访任务已完成,感谢配合。".to_string(), + "normal", + Some("follow_up".to_string()), + None, + db, + event_bus, + ) + .await + .map_err(|e| e.to_string())?; + } + } + // 积分获得通知患者 + "points.earned" => { + let patient_id = event + .payload + .get("patient_id") + .and_then(|v| v.as_str()) + .and_then(|s| uuid::Uuid::parse_str(s).ok()); + let points = event + .payload + .get("points") + .and_then(|v| v.as_i64()) + .unwrap_or(0); + let reason = event + .payload + .get("reason") + .and_then(|v| v.as_str()) + .unwrap_or("系统奖励"); + + if let Some(pid) = patient_id { + if should_skip_for_dnd(event.tenant_id, pid, "low", db).await { + return Ok(()); + } + let _ = crate::service::message_service::MessageService::send_system( + event.tenant_id, + pid, + "积分到账".to_string(), + format!("您获得了 {} 积分({})。", points, reason), + "low", + Some("points".to_string()), + None, + db, + event_bus, + ) + .await + .map_err(|e| e.to_string())?; + } + } + // 积分兑换通知患者 + "points.exchanged" => { + let patient_id = event + .payload + .get("patient_id") + .and_then(|v| v.as_str()) + .and_then(|s| uuid::Uuid::parse_str(s).ok()); + let product_name = event + .payload + .get("product_name") + .and_then(|v| v.as_str()) + .unwrap_or("商品"); + + if let Some(pid) = patient_id { + if should_skip_for_dnd(event.tenant_id, pid, "normal", db).await { + return Ok(()); + } + let _ = crate::service::message_service::MessageService::send_system( + event.tenant_id, + pid, + "兑换成功".to_string(), + format!("您已成功兑换「{}」,请留意后续通知。", product_name), + "normal", + Some("points".to_string()), + None, + db, + event_bus, + ) + .await + .map_err(|e| e.to_string())?; + } + } + // 积分过期通知患者 + "points.expired" => { + let patient_id = event + .payload + .get("patient_id") + .and_then(|v| v.as_str()) + .and_then(|s| uuid::Uuid::parse_str(s).ok()); + let points = event + .payload + .get("points") + .and_then(|v| v.as_i64()) + .unwrap_or(0); + + if let Some(pid) = patient_id { + if should_skip_for_dnd(event.tenant_id, pid, "low", db).await { + return Ok(()); + } + let _ = crate::service::message_service::MessageService::send_system( + event.tenant_id, + pid, + "积分过期提醒".to_string(), + format!("您有 {} 积分已过期。", points), + "low", + Some("points".to_string()), + None, + db, + event_bus, + ) + .await + .map_err(|e| e.to_string())?; + } + } + // 文章发布通知(暂记录日志,无直接用户推送) + "article.published" => { + tracing::info!( + article_id = %event.payload.get("article_id").and_then(|v| v.as_str()).unwrap_or(""), + "文章已发布" + ); + } + // 文章驳回通知作者 + "article.rejected" => { + let author_id = event + .payload + .get("author_id") + .and_then(|v| v.as_str()) + .and_then(|s| uuid::Uuid::parse_str(s).ok()); + let title = event + .payload + .get("title") + .and_then(|v| v.as_str()) + .unwrap_or("文章"); + + if let Some(aid) = author_id { + if should_skip_for_dnd(event.tenant_id, aid, "normal", db).await { + return Ok(()); + } + let _ = crate::service::message_service::MessageService::send_system( + event.tenant_id, + aid, + format!("文章未通过审核 — {}", title), + format!("您的文章「{}」未通过审核,请修改后重新提交。", title), + "normal", + Some("article".to_string()), + None, + db, + event_bus, + ) + .await + .map_err(|e| e.to_string())?; + } + } + // 患者信息更新通知(审计日志用途) + "patient.updated" => { + tracing::info!( + patient_id = %event.payload.get("patient_id").and_then(|v| v.as_str()).unwrap_or(""), + "患者信息已更新" + ); + } + // AI 分析失败通知医生 + "ai.analysis.failed" => { + let doctor_id = event + .payload + .get("doctor_id") + .and_then(|v| v.as_str()) + .and_then(|s| uuid::Uuid::parse_str(s).ok()); + let error = event + .payload + .get("error") + .and_then(|v| v.as_str()) + .unwrap_or("未知错误"); + + if let Some(did) = doctor_id { + if should_skip_for_dnd(event.tenant_id, did, "normal", db).await { + return Ok(()); + } + let _ = crate::service::message_service::MessageService::send_system( + event.tenant_id, + did, + "AI 分析失败".to_string(), + format!("AI 分析任务执行失败:{},请稍后重试。", error), + "normal", + Some("ai".to_string()), + None, + db, + event_bus, + ) + .await + .map_err(|e| e.to_string())?; + } + } + // 化验单上传记录日志 + "lab_report.uploaded" => { + tracing::info!( + patient_id = %event.payload.get("patient_id").and_then(|v| v.as_str()).unwrap_or(""), + "化验单已上传" + ); + } + // 日常监测记录日志 + "daily_monitoring.created" => { + tracing::info!( + patient_id = %event.payload.get("patient_id").and_then(|v| v.as_str()).unwrap_or(""), + "日常监测数据已记录" + ); + } + // 医生在线状态变更记录日志 + "doctor.online_status_changed" => { + tracing::info!( + doctor_id = %event.payload.get("doctor_id").and_then(|v| v.as_str()).unwrap_or(""), + status = %event.payload.get("status").and_then(|v| v.as_str()).unwrap_or(""), + "医生在线状态变更" + ); + } _ => {} } Ok(())