fix(health+ai+dialysis): 审计 P1 批次修复 — EventBus接入/盲索引去重/事件消费者补全
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

P1-2: erp-ai EventBus 接入
  - handler 层 SSE 流完成/失败时发布 ai.analysis.completed/failed 事件
  - build_sse_stream 新增 tenant_id 参数

P1-2: erp-dialysis EventBus 接入
  - create_dialysis_record 审计后发布 dialysis.record.created 事件

P1-5: message.sent 消费者改进
  - 从占位 tracing::info 升级为带 payload 详情的结构化日志

P1-7: 盲索引去重
  - create_patient 中新增 id_number HMAC 去重检查(查 blind_indexes 表)
  - 患者创建成功后写入 blind_indexes 表(id_number + phone)
  - 防止同租户重复建档

P1-1: 事件消费者补全
  - 新增 ai.analysis.completed 消费者(幂等处理 + 日志)
  - 新增 dialysis.record.created 消费者(幂等处理 + 日志)
This commit is contained in:
iven
2026-04-29 17:00:24 +08:00
parent dffa2dd47d
commit 30344d474f
4 changed files with 135 additions and 8 deletions

View File

@@ -72,7 +72,7 @@ where
let analysis_id_clone = analysis_id;
let state_clone = state.clone();
let sse_stream = build_sse_stream(stream, analysis_id_clone, state_clone, "lab_report");
let sse_stream = build_sse_stream(stream, analysis_id_clone, state_clone, "lab_report", ctx.tenant_id);
Ok(Sse::new(sse_stream).keep_alive(KeepAlive::default()))
}
@@ -140,7 +140,7 @@ where
let analysis_id_clone = analysis_id;
let state_clone = state.clone();
let sse_stream = build_sse_stream(stream, analysis_id_clone, state_clone, "trend");
let sse_stream = build_sse_stream(stream, analysis_id_clone, state_clone, "trend", ctx.tenant_id);
Ok(Sse::new(sse_stream).keep_alive(KeepAlive::default()))
}
@@ -197,7 +197,7 @@ where
let analysis_id_clone = analysis_id;
let state_clone = state.clone();
let sse_stream = build_sse_stream(stream, analysis_id_clone, state_clone, "checkup_plan");
let sse_stream = build_sse_stream(stream, analysis_id_clone, state_clone, "checkup_plan", ctx.tenant_id);
Ok(Sse::new(sse_stream).keep_alive(KeepAlive::default()))
}
@@ -254,7 +254,7 @@ where
let analysis_id_clone = analysis_id;
let state_clone = state.clone();
let sse_stream = build_sse_stream(stream, analysis_id_clone, state_clone, "report_summary");
let sse_stream = build_sse_stream(stream, analysis_id_clone, state_clone, "report_summary", ctx.tenant_id);
Ok(Sse::new(sse_stream).keep_alive(KeepAlive::default()))
}
@@ -452,6 +452,7 @@ fn build_sse_stream(
analysis_id: uuid::Uuid,
state: AiState,
analysis_type: &'static str,
tenant_id: uuid::Uuid,
) -> impl futures::Stream<Item = Result<Event, Infallible>> {
async_stream::stream! {
let mut full_content = String::new();
@@ -472,13 +473,34 @@ fn build_sse_stream(
let data = serde_json::to_string(&event).unwrap_or_default();
yield Ok(Event::default().event("error").data(data));
let _ = state.analysis.fail_analysis(analysis_id, e.to_string()).await;
// 发布 AI 分析失败事件
let fail_event = erp_core::events::DomainEvent::new(
"ai.analysis.failed",
tenant_id,
erp_core::events::build_event_payload(serde_json::json!({
"analysis_id": analysis_id,
"error": e.to_string(),
})),
);
state.event_bus.publish(fail_event, &state.db).await;
return;
}
}
}
let metadata = serde_json::json!({"analysis_type": analysis_type});
let _ = state.analysis.complete_analysis(analysis_id, full_content, metadata).await;
let _ = state.analysis.complete_analysis(analysis_id, full_content.clone(), metadata).await;
// 发布 AI 分析完成事件
let event = erp_core::events::DomainEvent::new(
"ai.analysis.completed",
tenant_id,
erp_core::events::build_event_payload(serde_json::json!({
"analysis_id": analysis_id,
"analysis_type": analysis_type,
})),
);
state.event_bus.publish(event, &state.db).await;
let done_event = AnalysisSseEvent::Done {
analysis_id,