fix(health): 审计修复 — alert 时序 + outbox 幂等性
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

1. CRITICAL: check_vital_signs_alert 移至 insert 之后执行,
   防止数据未持久化就触发告警
2. CRITICAL: send_system 添加 business_id 幂等检查,
   防止 outbox relay 重放导致重复消息通知
3. 修复 consent_service unused_mut 警告
This commit is contained in:
iven
2026-04-26 03:54:45 +08:00
parent 4ab189283e
commit 5cb4e5e0ec
3 changed files with 31 additions and 3 deletions

View File

@@ -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());

View File

@@ -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,

View File

@@ -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<MessageResp> {
// 幂等检查:防止 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();