fix(ai): AI 对话全链路修复 + 菜单配置 + 会话消息持久化

- 修复 ai_tenant_config Entity 表名错误(复数→单数)导致 budget_status 500
- 修复 ai_usage 表 SQL 引用不存在的 deleted_at 列
- 修复 risk_service SQL 列名/表名与实际数据库 schema 不匹配
- chat_handler provider 选择改为配置优先(default_provider→fallback chain)
- 新增 Ollama 非 FC provider 的 generate() 降级路径
- 新增 GET /ai/chat/sessions/{id}/messages 端点
- 前端 ChatPage 切换会话时从后端加载历史消息
- AiConfigPage 新增 default_provider 和 system_prompt 配置字段
- 迁移 000155-000156:AI 菜单调整 + AI 客服菜单 + 角色绑定
- 配额检查错误处理区分配额耗尽和 DB 异常
This commit is contained in:
iven
2026-05-19 21:36:01 +08:00
parent 8fbe1543cb
commit c6d4e76b62
12 changed files with 514 additions and 122 deletions

View File

@@ -138,7 +138,6 @@ impl CostService {
SELECT COALESCE(SUM(input_tokens + output_tokens), 0) AS total
FROM ai_usage
WHERE tenant_id = $1
AND deleted_at IS NULL
AND created_at >= DATE_TRUNC('month', CURRENT_DATE)
"#;

View File

@@ -175,66 +175,52 @@ impl RiskService {
) -> AppResult<serde_json::Value> {
use sea_orm::FromQueryResult;
// 最新一条体征数据(最近 30 天
// 体征数据:按 device_type 获取最近 30 天最新均值
// vital_signs_daily 实际列: device_type, date_bucket, avg_val, min_val, max_val, sample_count
#[derive(FromQueryResult)]
struct VitalRow {
systolic_bp_morning: Option<i32>,
diastolic_bp_morning: Option<i32>,
heart_rate: Option<i32>,
blood_sugar: Option<f64>,
weight: Option<f64>,
spo2: Option<i32>,
body_temperature: Option<f64>,
device_type: String,
avg_val: Option<f64>,
}
let vital: Option<VitalRow> = VitalRow::find_by_statement(
sea_orm::Statement::from_sql_and_values(
let vitals: Vec<VitalRow> =
VitalRow::find_by_statement(sea_orm::Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
"SELECT systolic_bp_morning, diastolic_bp_morning, heart_rate, blood_sugar, weight, spo2, body_temperature FROM vital_signs_daily WHERE tenant_id = $1 AND patient_id = $2 AND deleted_at IS NULL ORDER BY record_date DESC LIMIT 1",
r#"SELECT device_type, avg_val FROM vital_signs_daily
WHERE tenant_id = $1 AND patient_id = $2
AND date_bucket >= CURRENT_DATE - INTERVAL '30 days'
ORDER BY date_bucket DESC"#,
[tenant_id.into(), patient_id.into()],
),
)
.one(db)
.await?;
))
.all(db)
.await?;
// 最新化验报告异常计数(最近 90 天)
// 化验报告异常计数(最近 90 天)
// lab_report 表: is_abnormal 字段在 indicators JSON 内,通过 JSON 查询统计
#[derive(FromQueryResult)]
struct LabAbnormal {
report_type: String,
abnormal_count: i64,
}
let lab_abnormals: Vec<LabAbnormal> = LabAbnormal::find_by_statement(
sea_orm::Statement::from_sql_and_values(
let lab_abnormals: Vec<LabAbnormal> =
LabAbnormal::find_by_statement(sea_orm::Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
"SELECT report_type, COUNT(*) as abnormal_count FROM lab_reports WHERE tenant_id = $1 AND patient_id = $2 AND deleted_at IS NULL AND is_abnormal = true AND report_date >= NOW() - INTERVAL '90 days' GROUP BY report_type",
r#"SELECT report_type, COUNT(*) as abnormal_count FROM lab_report
WHERE tenant_id = $1 AND patient_id = $2
AND deleted_at IS NULL
AND report_date >= CURRENT_DATE - INTERVAL '90 days'
AND indicators @> '[{"is_abnormal":true}]'::jsonb
GROUP BY report_type"#,
[tenant_id.into(), patient_id.into()],
),
)
.all(db)
.await?;
))
.all(db)
.await
.unwrap_or_default();
let mut data = serde_json::Map::new();
if let Some(v) = vital {
if let Some(bp_sys) = v.systolic_bp_morning {
data.insert("systolic_bp_morning".into(), serde_json::json!(bp_sys));
}
if let Some(bp_dia) = v.diastolic_bp_morning {
data.insert("diastolic_bp_morning".into(), serde_json::json!(bp_dia));
}
if let Some(hr) = v.heart_rate {
data.insert("heart_rate".into(), serde_json::json!(hr));
}
if let Some(bs) = v.blood_sugar {
data.insert("blood_sugar".into(), serde_json::json!(bs));
}
if let Some(w) = v.weight {
data.insert("weight".into(), serde_json::json!(w));
}
if let Some(spo2) = v.spo2 {
data.insert("spo2".into(), serde_json::json!(spo2));
}
if let Some(temp) = v.body_temperature {
data.insert("body_temperature".into(), serde_json::json!(temp));
for v in &vitals {
if let Some(avg) = v.avg_val {
data.insert(format!("vital_{}", v.device_type), serde_json::json!(avg));
}
}