use async_trait::async_trait; use chrono::Datelike; use erp_core::crypto::{self as pii, PiiCrypto}; use erp_core::error::{AppError, AppResult}; use erp_core::health_provider::{ AnomalyInfo, AppointmentSummaryDto, HealthDataProvider, HealthReportDto, LabItemDto, LabReportDto, MedicationSummaryDto, MetricTrendAnalysis, PatientSummaryDto, RegressionStats, ReportSectionDto, TimeRange, TrendAnalysisDto, TrendDirection, VitalSignDto, }; use num_traits::ToPrimitive; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect}; use uuid::Uuid; use crate::entity::{ appointment, diagnosis, health_record, lab_report, medication_record, patient, vital_signs, }; pub struct HealthDataProviderImpl { pub db: sea_orm::DatabaseConnection, pub crypto: PiiCrypto, } fn compute_age_group(birth_date: Option) -> String { let Some(bd) = birth_date else { return "未知".to_string(); }; let age = chrono::Utc::now().date_naive().year() - bd.year(); match age { a if a < 14 => "儿童", a if a < 36 => "青年", a if a < 56 => "中年", _ => "老年", } .to_string() } async fn find_patient( db: &sea_orm::DatabaseConnection, tenant_id: Uuid, patient_id: Uuid, ) -> AppResult { patient::Entity::find_by_id(patient_id) .filter(patient::Column::TenantId.eq(tenant_id)) .filter(patient::Column::DeletedAt.is_null()) .one(db) .await? .ok_or_else(|| AppError::NotFound(format!("患者 {patient_id} 不存在"))) } async fn find_lab_report( db: &sea_orm::DatabaseConnection, tenant_id: Uuid, report_id: Uuid, ) -> AppResult { lab_report::Entity::find_by_id(report_id) .filter(lab_report::Column::TenantId.eq(tenant_id)) .filter(lab_report::Column::DeletedAt.is_null()) .one(db) .await? .ok_or_else(|| AppError::NotFound(format!("化验报告 {report_id} 不存在"))) } fn parse_lab_items(items_json: &Option) -> Vec { let Some(arr) = items_json.as_ref().and_then(|v| v.as_array()) else { return vec![]; }; arr.iter() .filter_map(|item| { // 兼容两种存储格式:item_name/name let name = item .get("item_name") .or_else(|| item.get("name"))? .as_str()? .to_string(); // 兼容 value 为字符串或数字 let value = item .get("value") .and_then(|v| v.as_f64()) .or_else(|| item.get("value")?.as_str()?.parse::().ok()) .unwrap_or(0.0); let unit = item .get("unit") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(); // 兼容两种参考范围格式:reference_range(string) 或 reference_low/reference_high(number) let reference_range = item .get("reference_range") .and_then(|v| v.as_str()) .map(|s| s.to_string()) .unwrap_or_else(|| { let low = item .get("reference_low") .and_then(|v| v.as_f64()) .map(|l| l.to_string()); let high = item .get("reference_high") .and_then(|v| v.as_f64()) .map(|h| h.to_string()); match (low, high) { (Some(l), Some(h)) => format!("{l}-{h}"), (Some(l), None) => format!(">={l}"), (None, Some(h)) => format!("<={h}"), _ => "-".to_string(), } }); let is_abnormal = item .get("is_abnormal") .and_then(|v| v.as_bool()) .unwrap_or(false); Some(LabItemDto { name, value, unit, reference_range, is_abnormal, }) }) .collect() } fn report_type_to_department(report_type: &str) -> &str { match report_type { "kidney_function" => "肾内科", "blood_routine" => "血液科", "electrolyte" => "检验科", "liver_function" => "肝胆科", _ => "检验科", } } #[async_trait] impl HealthDataProvider for HealthDataProviderImpl { async fn get_lab_report(&self, tenant_id: Uuid, report_id: Uuid) -> AppResult { let report = find_lab_report(&self.db, tenant_id, report_id).await?; let patient = find_patient(&self.db, tenant_id, report.patient_id).await?; // 解密 items:加密时存储为 Value::String(ciphertext) let kek = self.crypto.kek(); let decrypted_items = report .items .as_ref() .and_then(|v| v.as_str()) .and_then(|s| pii::decrypt(kek, s).ok()) .and_then(|s| serde_json::from_str(&s).ok()) .or(report.items.clone()); Ok(LabReportDto { age_group: compute_age_group(patient.birth_date), sex: patient.gender.unwrap_or_else(|| "未知".to_string()), department: report_type_to_department(&report.report_type).to_string(), report_date: report.report_date.to_string(), items: parse_lab_items(&decrypted_items), }) } async fn get_vital_signs( &self, tenant_id: Uuid, patient_id: Uuid, metrics: &[String], range: &TimeRange, ) -> AppResult> { let _ = find_patient(&self.db, tenant_id, patient_id).await?; let start_date = range.start.date_naive(); let end_date = range.end.date_naive(); let records = vital_signs::Entity::find() .filter(vital_signs::Column::TenantId.eq(tenant_id)) .filter(vital_signs::Column::PatientId.eq(patient_id)) .filter(vital_signs::Column::DeletedAt.is_null()) .filter(vital_signs::Column::RecordDate.gte(start_date)) .filter(vital_signs::Column::RecordDate.lte(end_date)) .order_by_asc(vital_signs::Column::RecordDate) .all(&self.db) .await?; let metric_extractors: [(&str, Box Option>); 8] = [ ( "systolic_bp_morning", Box::new(|r| r.systolic_bp_morning.map(|v| v as f64)), ), ( "diastolic_bp_morning", Box::new(|r| r.diastolic_bp_morning.map(|v| v as f64)), ), ("heart_rate", Box::new(|r| r.heart_rate.map(|v| v as f64))), ( "weight", Box::new(|r| r.weight.map(|v| v.to_f64().unwrap_or(0.0))), ), ( "blood_sugar", Box::new(|r| r.blood_sugar.map(|v| v.to_f64().unwrap_or(0.0))), ), ( "body_temperature", Box::new(|r| r.body_temperature.map(|v| v.to_f64().unwrap_or(0.0))), ), ("spo2", Box::new(|r| r.spo2.map(|v| v as f64))), ( "urine_output_ml", Box::new(|r| r.urine_output_ml.map(|v| v as f64)), ), ]; let mut result = Vec::new(); for (metric_name, extractor) in &metric_extractors { if !metrics.is_empty() && !metrics.iter().any(|m| m == *metric_name) { continue; } let values: Vec<(String, f64)> = records .iter() .filter_map(|r| extractor(r).map(|v| (r.record_date.to_string(), v))) .collect(); if values.is_empty() { continue; } let unit = match *metric_name { "systolic_bp_morning" | "diastolic_bp_morning" => "mmHg", "heart_rate" => "bpm", "weight" => "kg", "blood_sugar" => "mmol/L", "body_temperature" => "°C", "spo2" => "%", "urine_output_ml" => "ml", _ => "", }; result.push(VitalSignDto { metric: metric_name.to_string(), values, unit: unit.to_string(), }); } Ok(result) } async fn get_patient_summary( &self, tenant_id: Uuid, patient_id: Uuid, ) -> AppResult { let patient = find_patient(&self.db, tenant_id, patient_id).await?; let diagnoses: Vec = diagnosis::Entity::find() .filter(diagnosis::Column::TenantId.eq(tenant_id)) .filter(diagnosis::Column::PatientId.eq(patient_id)) .filter(diagnosis::Column::DeletedAt.is_null()) .filter(diagnosis::Column::Status.eq("active")) .order_by_desc(diagnosis::Column::DiagnosedDate) .all(&self.db) .await? .iter() .map(|d| format!("{}({})", d.diagnosis_name, d.icd_code)) .collect(); let medications: Vec = medication_record::Entity::find() .filter(medication_record::Column::TenantId.eq(tenant_id)) .filter(medication_record::Column::PatientId.eq(patient_id)) .filter(medication_record::Column::DeletedAt.is_null()) .filter(medication_record::Column::IsCurrent.eq(true)) .all(&self.db) .await? .iter() .map(|m| { let mut s = m.medication_name.clone(); if let Some(ref dosage) = m.dosage { s.push_str(&format!(" {dosage}")); } s }) .collect(); let family_history = patient .medical_history_summary .as_ref() .map(|h| { h.split(';') .chain(h.split(';')) .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) .collect() }) .unwrap_or_default(); let last_checkup = health_record::Entity::find() .filter(health_record::Column::TenantId.eq(tenant_id)) .filter(health_record::Column::PatientId.eq(patient_id)) .filter(health_record::Column::DeletedAt.is_null()) .order_by_desc(health_record::Column::RecordDate) .one(&self.db) .await?; let last_checkup_date = last_checkup .map(|r| r.record_date.to_string()) .unwrap_or_else(|| "无".to_string()); Ok(PatientSummaryDto { age_group: compute_age_group(patient.birth_date), sex: patient.gender.unwrap_or_else(|| "未知".to_string()), chronic_conditions: diagnoses, medications, family_history, last_checkup_date, }) } async fn get_full_report( &self, tenant_id: Uuid, report_id: Uuid, ) -> AppResult { let record = health_record::Entity::find_by_id(report_id) .filter(health_record::Column::TenantId.eq(tenant_id)) .filter(health_record::Column::DeletedAt.is_null()) .one(&self.db) .await? .ok_or_else(|| AppError::NotFound(format!("健康报告 {report_id} 不存在")))?; let patient = find_patient(&self.db, tenant_id, record.patient_id).await?; let mut sections = Vec::new(); let findings: Vec = record .overall_assessment .as_ref() .map(|a| { a.split(';') .chain(a.split(';')) .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) .collect() }) .unwrap_or_default(); if !findings.is_empty() { sections.push(ReportSectionDto { title: "总体评估".to_string(), findings, abnormal_items: vec![], }); } let report_diagnoses = diagnosis::Entity::find() .filter(diagnosis::Column::TenantId.eq(tenant_id)) .filter(diagnosis::Column::PatientId.eq(record.patient_id)) .filter(diagnosis::Column::DeletedAt.is_null()) .filter( diagnosis::Column::HealthRecordId .eq(report_id) .or(diagnosis::Column::Status.eq("active")), ) .all(&self.db) .await?; if !report_diagnoses.is_empty() { let (abnormal, findings): (Vec<_>, Vec<_>) = report_diagnoses.iter().partition(|d| d.status == "active"); sections.push(ReportSectionDto { title: "诊断记录".to_string(), findings: findings .iter() .map(|d| { format!( "{}({}) — {}", d.diagnosis_name, d.icd_code, d.diagnosed_date ) }) .collect(), abnormal_items: abnormal .iter() .map(|d| format!("{}({})", d.diagnosis_name, d.icd_code)) .collect(), }); } let lab_reports = lab_report::Entity::find() .filter(lab_report::Column::TenantId.eq(tenant_id)) .filter(lab_report::Column::PatientId.eq(record.patient_id)) .filter(lab_report::Column::DeletedAt.is_null()) .filter( lab_report::Column::ReportDate.gte(record.record_date - chrono::Duration::days(30)), ) .filter(lab_report::Column::ReportDate.lte(record.record_date)) .order_by_desc(lab_report::Column::ReportDate) .all(&self.db) .await?; for lr in &lab_reports { let items = parse_lab_items(&lr.items); let abnormal: Vec = items .iter() .filter(|i| i.is_abnormal) .map(|i| format!("{} {}{}", i.name, i.value, i.unit)) .collect(); let findings: Vec = items .iter() .map(|i| format!("{}: {}{} ({})", i.name, i.value, i.unit, i.reference_range)) .collect(); if !findings.is_empty() { sections.push(ReportSectionDto { title: format!("化验报告 — {} ({})", lr.report_type, lr.report_date), findings, abnormal_items: abnormal, }); } } Ok(HealthReportDto { age_group: compute_age_group(patient.birth_date), sex: patient.gender.unwrap_or_else(|| "未知".to_string()), department: record.record_type.clone(), report_date: record.record_date.to_string(), sections, }) } async fn get_trend_analysis_data( &self, tenant_id: Uuid, patient_id: Uuid, metrics: &[String], range: &TimeRange, ) -> AppResult { let _ = find_patient(&self.db, tenant_id, patient_id).await?; let start_date = range.start.date_naive(); let end_date = range.end.date_naive(); let records = vital_signs::Entity::find() .filter(vital_signs::Column::TenantId.eq(tenant_id)) .filter(vital_signs::Column::PatientId.eq(patient_id)) .filter(vital_signs::Column::DeletedAt.is_null()) .filter(vital_signs::Column::RecordDate.gte(start_date)) .filter(vital_signs::Column::RecordDate.lte(end_date)) .order_by_asc(vital_signs::Column::RecordDate) .all(&self.db) .await?; let metric_extractors: [(&str, Box Option>); 8] = [ ( "systolic_bp_morning", Box::new(|r| r.systolic_bp_morning.map(|v| v as f64)), ), ( "diastolic_bp_morning", Box::new(|r| r.diastolic_bp_morning.map(|v| v as f64)), ), ("heart_rate", Box::new(|r| r.heart_rate.map(|v| v as f64))), ( "weight", Box::new(|r| r.weight.map(|v| v.to_f64().unwrap_or(0.0))), ), ( "blood_sugar", Box::new(|r| r.blood_sugar.map(|v| v.to_f64().unwrap_or(0.0))), ), ( "body_temperature", Box::new(|r| r.body_temperature.map(|v| v.to_f64().unwrap_or(0.0))), ), ("spo2", Box::new(|r| r.spo2.map(|v| v as f64))), ( "urine_output_ml", Box::new(|r| r.urine_output_ml.map(|v| v as f64)), ), ]; let mut metric_results = Vec::new(); for (metric_name, extractor) in &metric_extractors { if !metrics.is_empty() && !metrics.iter().any(|m| m == *metric_name) { continue; } // 构建时间序列数据 let time_series: Vec<(chrono::NaiveDate, f64)> = records .iter() .filter_map(|r| extractor(r).map(|v| (r.record_date, v))) .collect(); let data_point_count = time_series.len(); if data_point_count == 0 { continue; } // 线性回归 let regression = crate::service::trend_stats::compute_linear_regression(&time_series) .map(|r| RegressionStats { slope: r.slope, intercept: r.intercept, r_squared: r.r_squared, direction: match r.direction { crate::service::trend_stats::TrendDirection::Rising => { TrendDirection::Rising } crate::service::trend_stats::TrendDirection::Falling => { TrendDirection::Falling } crate::service::trend_stats::TrendDirection::Stable => { TrendDirection::Stable } }, daily_change: r.daily_change, period_change: r.period_change, }); // 异常检测(使用 2.0 倍标准差阈值) let anomaly_points = crate::service::trend_stats::detect_anomalies(&time_series, 2.0); let anomalies: Vec = anomaly_points .into_iter() .map(|a| AnomalyInfo { date: a.date.to_string(), value: a.value, mean: a.mean, std_dev: a.std_dev, deviation: a.deviation, }) .collect(); let unit = match *metric_name { "systolic_bp_morning" | "diastolic_bp_morning" => "mmHg", "heart_rate" => "bpm", "weight" => "kg", "blood_sugar" => "mmol/L", "body_temperature" => "°C", "spo2" => "%", "urine_output_ml" => "ml", _ => "", }; metric_results.push(MetricTrendAnalysis { metric: metric_name.to_string(), unit: unit.to_string(), data_point_count, regression, anomalies, }); } Ok(TrendAnalysisDto { patient_id, period_start: start_date.to_string(), period_end: end_date.to_string(), metrics: metric_results, }) } async fn get_upcoming_appointments( &self, tenant_id: Uuid, patient_id: Uuid, ) -> AppResult> { let _ = find_patient(&self.db, tenant_id, patient_id).await?; let today = chrono::Utc::now().date_naive(); let records = appointment::Entity::find() .filter(appointment::Column::TenantId.eq(tenant_id)) .filter(appointment::Column::PatientId.eq(patient_id)) .filter(appointment::Column::DeletedAt.is_null()) .filter(appointment::Column::AppointmentDate.gte(today)) .filter( appointment::Column::Status .is_in(vec!["scheduled".to_string(), "confirmed".to_string()]), ) .order_by_asc(appointment::Column::AppointmentDate) .order_by_asc(appointment::Column::StartTime) .limit(10) .all(&self.db) .await?; let result = records .into_iter() .map(|r| AppointmentSummaryDto { id: r.id, department: r.appointment_type, doctor_name: r .doctor_id .map_or("待定".to_string(), |_| "医生".to_string()), scheduled_at: format!("{} {}", r.appointment_date, r.start_time), status: r.status, }) .collect(); Ok(result) } async fn get_medication_list( &self, tenant_id: Uuid, patient_id: Uuid, ) -> AppResult> { let _ = find_patient(&self.db, tenant_id, patient_id).await?; let records = medication_record::Entity::find() .filter(medication_record::Column::TenantId.eq(tenant_id)) .filter(medication_record::Column::PatientId.eq(patient_id)) .filter(medication_record::Column::DeletedAt.is_null()) .filter(medication_record::Column::IsCurrent.eq(true)) .all(&self.db) .await?; let result = records .into_iter() .map(|m| MedicationSummaryDto { name: m.medication_name, dosage: m.dosage.unwrap_or_default(), frequency: m.frequency.unwrap_or_default(), }) .collect(); Ok(result) } }