From 5cb4e5e0ec60e29862f970f1a0d48ebf605d63f2 Mon Sep 17 00:00:00 2001 From: iven Date: Sun, 26 Apr 2026 03:54:45 +0800 Subject: [PATCH] =?UTF-8?q?fix(health):=20=E5=AE=A1=E8=AE=A1=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20=E2=80=94=20alert=20=E6=97=B6=E5=BA=8F=20+=20outbox?= =?UTF-8?q?=20=E5=B9=82=E7=AD=89=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. CRITICAL: check_vital_signs_alert 移至 insert 之后执行, 防止数据未持久化就触发告警 2. CRITICAL: send_system 添加 business_id 幂等检查, 防止 outbox relay 重放导致重复消息通知 3. 修复 consent_service unused_mut 警告 --- .../erp-health/src/service/consent_service.rs | 2 +- .../src/service/health_data_service.rs | 8 +++++-- .../src/service/message_service.rs | 24 +++++++++++++++++++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/crates/erp-health/src/service/consent_service.rs b/crates/erp-health/src/service/consent_service.rs index e1ad24f..6c913af 100644 --- a/crates/erp-health/src/service/consent_service.rs +++ b/crates/erp-health/src/service/consent_service.rs @@ -23,7 +23,7 @@ pub async fn list_consents( let limit = page_size.min(100); let offset = page.saturating_sub(1) * limit; - let mut query = consent::Entity::find() + let query = consent::Entity::find() .filter(consent::Column::TenantId.eq(tenant_id)) .filter(consent::Column::PatientId.eq(patient_id)) .filter(consent::Column::DeletedAt.is_null()); diff --git a/crates/erp-health/src/service/health_data_service.rs b/crates/erp-health/src/service/health_data_service.rs index 8c76b92..017e411 100644 --- a/crates/erp-health/src/service/health_data_service.rs +++ b/crates/erp-health/src/service/health_data_service.rs @@ -86,7 +86,7 @@ pub async fn create_vital_signs( .ok_or(HealthError::PatientNotFound)?; let now = Utc::now(); - check_vital_signs_alert(state, tenant_id, patient_id, operator_id, req.clone()).await; + let alert_req = req.clone(); let active = vital_signs::ActiveModel { id: Set(Uuid::now_v7()), tenant_id: Set(tenant_id), @@ -112,11 +112,15 @@ pub async fn create_vital_signs( }; let m = active.insert(&state.db).await?; + // 数据持久化成功后再触发危急值检测 + check_vital_signs_alert(state, tenant_id, patient_id, operator_id, alert_req).await; + audit_service::record( AuditLog::new(tenant_id, operator_id, "vital_signs.created", "vital_signs") .with_resource_id(m.id), &state.db, - ).await; + ) + .await; Ok(VitalSignsResp { id: m.id, patient_id: m.patient_id, record_date: m.record_date, diff --git a/crates/erp-message/src/service/message_service.rs b/crates/erp-message/src/service/message_service.rs index 88f890f..c3212e7 100644 --- a/crates/erp-message/src/service/message_service.rs +++ b/crates/erp-message/src/service/message_service.rs @@ -148,6 +148,9 @@ impl MessageService { } /// 系统发送消息(由事件处理器调用)。 + /// + /// 幂等保证:当 `business_id` 存在时,若同 tenant + recipient + business_id 的消息已存在, + /// 直接返回已有消息,避免 outbox relay 重放导致重复通知。 #[allow(clippy::too_many_arguments)] pub async fn send_system( tenant_id: Uuid, @@ -160,6 +163,27 @@ impl MessageService { db: &sea_orm::DatabaseConnection, event_bus: &EventBus, ) -> MessageResult { + // 幂等检查:防止 outbox relay 重放导致重复消息 + if let Some(bid) = business_id { + let existing = message::Entity::find() + .filter(message::Column::TenantId.eq(tenant_id)) + .filter(message::Column::RecipientId.eq(recipient_id)) + .filter(message::Column::BusinessId.eq(bid)) + .filter(message::Column::DeletedAt.is_null()) + .one(db) + .await + .map_err(|e| MessageError::Validation(e.to_string()))?; + + if let Some(m) = existing { + tracing::debug!( + message_id = %m.id, + business_id = %bid, + "消息已存在,跳过重复创建(幂等保护)" + ); + return Ok(Self::model_to_resp(&m)); + } + } + let id = Uuid::now_v7(); let now = Utc::now(); let system_user = Uuid::nil();