fix(health+ai): 后端质量修复 — Phase 2d
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

H3: 设备数据摄入增加 tracing 日志(事务保护待 ConnectionTrait 重构)
M4: care_plan/shift/ble_gateway/vital_signs_daily 补全 tracing 入口日志
M1: AI 分析缓存命中检查 + 缓存结果 Stream 回放
H4: 透析→KDIGO 自动串联(dialysis_notifier 发布 ai.dialysis.kdigo_requested 事件)
This commit is contained in:
iven
2026-05-05 00:19:22 +08:00
parent 888fa108ef
commit 8d288cadfa
8 changed files with 90 additions and 4 deletions

View File

@@ -129,6 +129,21 @@ impl ErpModule for AiModule {
"收到 AI 分析请求事件(化验单上传触发,待 Prompt 模板就绪后实现自动分析)"
);
}
// H4: 透析记录→KDIGO 自动风险评估
Some(event) if event.event_type == "ai.dialysis.kdigo_requested" => {
let patient_id = event.payload.get("patient_id")
.and_then(|v| v.as_str())
.and_then(|s| uuid::Uuid::parse_str(s).ok());
let record_id = event.payload.get("dialysis_record_id")
.and_then(|v| v.as_str());
tracing::info!(
patient_id = ?patient_id,
record_id = ?record_id,
tenant_id = %event.tenant_id,
"透析→KDIGO 自动评估触发(待 eGFR 数据源接入后完成完整串联)"
);
}
Some(event) => {
tracing::debug!(
event_type = %event.event_type,

View File

@@ -52,6 +52,17 @@ impl AnalysisService {
let input_hash = self.compute_hash(&sanitized_data);
let provider_name = self.provider.name().to_string();
// 0. 缓存命中检查(相同输入 + prompt 版本 → 复用已有结果)
if let Some(cached) = self.find_cached(tenant_id, &input_hash, 1).await? {
tracing::info!(analysis = %cached.id, "AI 分析缓存命中,复用已有结果");
let content = cached.result_content.clone().unwrap_or_default();
let metadata = cached.result_metadata.clone().unwrap_or(serde_json::json!({}));
let stream = self.replay_cached(content, metadata);
return Ok((stream, cached.id, provider_name));
}
tracing::info!(analysis = %analysis_id, tenant = %tenant_id, r#type = %analysis_type.as_str(), "发起 AI 分析");
// 1. 渲染 Prompt
let user_prompt = self.renderer.render(&user_template, &sanitized_data)?;
@@ -82,6 +93,22 @@ impl AnalysisService {
Ok((stream, analysis_id, provider_name))
}
/// 将缓存结果构造为一次性 Stream模拟 SSE 单条返回)
fn replay_cached(
&self,
content: String,
metadata: serde_json::Value,
) -> Pin<Box<dyn Stream<Item = AiResult<String>> + Send>> {
use futures::stream;
let payload = serde_json::json!({
"content": content,
"metadata": metadata,
"cached": true,
});
let chunk = serde_json::to_string(&payload).unwrap_or_default();
Box::pin(stream::once(async move { Ok(chunk) }))
}
/// 更新分析记录为完成
pub async fn complete_analysis(
&self,