feat(ai): Phase 2B 洞察→推送→反馈闭环 — 风险评分+通知+建议反馈

- 风险评分引擎 load_patient_data 实装(体征+化验异常)
- refresh_all_patients 高风险自动创建洞察+事件推送
- erp-message 订阅 copilot.insight.created 推送医护通知
- 每日 cron 增加洞察过期清理+建议过期清理
- POST /ai/suggestions/{id}/feedback 建议反馈端点
- SuggestionFeedbackService 反馈服务层
- 小程序健康页建议卡片增加采纳/忽略/咨询医生按钮
This commit is contained in:
iven
2026-05-19 01:19:09 +08:00
parent 2660f1afff
commit 9576e80175
10 changed files with 504 additions and 32 deletions

View File

@@ -193,6 +193,69 @@ where
}
}
#[derive(Debug, Deserialize, utoipa::ToSchema)]
pub struct SubmitFeedbackBody {
pub action: String, // "adopt" | "ignore" | "consult"
pub feedback_text: Option<String>,
}
/// 患者端提交建议反馈(采纳/忽略/咨询医生)
#[utoipa::path(
post,
path = "/ai/suggestions/{id}/feedback",
responses((status = 200, description = "提交建议反馈")),
tag = "AI 建议",
security(("bearer_auth" = [])),
)]
pub async fn submit_feedback<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
Path(id): Path<uuid::Uuid>,
Json(body): Json<SubmitFeedbackBody>,
) -> Result<Json<ApiResponse<serde_json::Value>>, erp_core::error::AppError>
where
AiState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "ai.suggestion.manage")?;
if !matches!(body.action.as_str(), "adopt" | "ignore" | "consult") {
return Err(erp_core::error::AppError::Validation(
"action 必须为 adopt、ignore 或 consult".into(),
));
}
let feedback_id =
crate::service::suggestion_feedback::SuggestionFeedbackService::submit_feedback(
&state.db,
ctx.tenant_id,
id,
ctx.user_id,
body.action.clone(),
body.feedback_text.clone(),
)
.await?;
// 发布反馈事件
let event = erp_core::events::DomainEvent::new(
"ai.suggestion.feedback",
ctx.tenant_id,
erp_core::events::build_event_payload(serde_json::json!({
"suggestion_id": id,
"action": body.action,
"feedback_text": body.feedback_text,
"user_id": ctx.user_id,
})),
);
state.event_bus.publish(event, &state.db).await;
Ok(Json(ApiResponse::ok(serde_json::json!({
"id": feedback_id,
"suggestion_id": id,
"action": body.action,
}))))
}
/// 发布建议状态变更事件
async fn publish_status_event(state: &AiState, suggestion: &crate::entity::ai_suggestion::Model) {
let event = erp_core::events::DomainEvent::new(