use erp_core::health_provider::{ FollowUpSummaryDataDto, HealthReportDto, LabReportDto, PatientSummaryDto, TrendAnalysisDto, VitalSignDto, }; use serde_json::Value; use crate::error::{AiError, AiResult}; /// 数据脱敏服务 — 确保发送给 AI 的数据不含 PII /// HealthDataProvider 返回的 DTO 已经是脱敏的(只有年龄/性别/医疗数据) /// 此服务做二次检查和安全约束注入 pub struct SanitizationService; impl Default for SanitizationService { fn default() -> Self { Self::new() } } impl SanitizationService { pub fn new() -> Self { Self } pub fn sanitize_lab_report(&self, report: &LabReportDto) -> AiResult { let sanitized = serde_json::to_value(report) .map_err(|e| AiError::SanitizationError(format!("序列化失败: {e}")))?; self.verify_no_pii(&sanitized)?; Ok(sanitized) } pub fn sanitize_vital_signs(&self, signs: &[VitalSignDto]) -> AiResult { let sanitized = serde_json::to_value(signs) .map_err(|e| AiError::SanitizationError(format!("序列化失败: {e}")))?; self.verify_no_pii(&sanitized)?; Ok(sanitized) } pub fn sanitize_patient_summary(&self, summary: &PatientSummaryDto) -> AiResult { let sanitized = serde_json::to_value(summary) .map_err(|e| AiError::SanitizationError(format!("序列化失败: {e}")))?; self.verify_no_pii(&sanitized)?; Ok(sanitized) } pub fn sanitize_health_report(&self, report: &HealthReportDto) -> AiResult { let sanitized = serde_json::to_value(report) .map_err(|e| AiError::SanitizationError(format!("序列化失败: {e}")))?; self.verify_no_pii(&sanitized)?; Ok(sanitized) } pub fn sanitize_trend_analysis(&self, data: &TrendAnalysisDto) -> AiResult { let sanitized = serde_json::to_value(data) .map_err(|e| AiError::SanitizationError(format!("序列化失败: {e}")))?; self.verify_no_pii(&sanitized)?; Ok(sanitized) } pub fn sanitize_follow_up_data(&self, data: &FollowUpSummaryDataDto) -> AiResult { let sanitized = serde_json::to_value(data) .map_err(|e| AiError::SanitizationError(format!("序列化失败: {e}")))?; self.verify_no_pii(&sanitized)?; Ok(sanitized) } /// 二次验证: 确保没有意外泄漏的 PII fn verify_no_pii(&self, value: &Value) -> AiResult<()> { let pii_keys = [ "name", "phone", "id_number", "address", "birth_date", "email", ]; if let Value::Object(map) = value { for key in pii_keys { if map.contains_key(key) { return Err(AiError::SanitizationError(format!( "检测到疑似 PII 字段: {key}" ))); } } } Ok(()) } } #[cfg(test)] mod tests { use super::*; use erp_core::health_provider::{ HealthReportDto, LabItemDto, LabReportDto, PatientSummaryDto, ReportSectionDto, VitalSignDto, }; fn sanitizer() -> SanitizationService { SanitizationService::new() } fn clean_lab_report() -> LabReportDto { LabReportDto { age_group: "中年".to_string(), sex: "male".to_string(), department: "内科".to_string(), report_date: "2026-05-01".to_string(), items: vec![LabItemDto { name: "WBC".to_string(), value: 6.5, unit: "10^9/L".to_string(), reference_range: "3.5-9.5".to_string(), is_abnormal: false, }], } } // ---- clean data passes ---- #[test] fn sanitize_lab_report_clean_passes() { let report = clean_lab_report(); let result = sanitizer().sanitize_lab_report(&report); assert!(result.is_ok()); let val = result.unwrap(); assert_eq!(val["age_group"], "中年"); assert_eq!(val["items"][0]["name"], "WBC"); } #[test] fn sanitize_vital_signs_clean_passes() { let signs = vec![VitalSignDto { metric: "血压".to_string(), values: vec![("2026-05-01".to_string(), 125.0)], unit: "mmHg".to_string(), }]; let result = sanitizer().sanitize_vital_signs(&signs); assert!(result.is_ok()); } #[test] fn sanitize_patient_summary_clean_passes() { let summary = PatientSummaryDto { age_group: "青年".to_string(), sex: "female".to_string(), chronic_conditions: vec!["高血压".to_string()], medications: vec!["降压药".to_string()], family_history: vec![], last_checkup_date: "2026-04-01".to_string(), }; let result = sanitizer().sanitize_patient_summary(&summary); assert!(result.is_ok()); } #[test] fn sanitize_health_report_clean_passes() { let report = HealthReportDto { age_group: "老年".to_string(), sex: "male".to_string(), department: "体检中心".to_string(), report_date: "2026-05-01".to_string(), sections: vec![ReportSectionDto { title: "血常规".to_string(), findings: vec!["WBC 正常".to_string()], abnormal_items: vec![], }], }; let result = sanitizer().sanitize_health_report(&report); assert!(result.is_ok()); } // ---- PII detection ---- #[test] fn sanitize_rejects_data_with_name_field() { // LabReportDto 没有 name 字段,反序列化会丢弃 PII 字段 // 验证 DTO 结构本身安全 let svc = sanitizer(); let mut polluted = serde_json::to_value(&clean_lab_report()).unwrap(); polluted["name"] = serde_json::json!("张三"); let report: LabReportDto = serde_json::from_value(polluted).unwrap(); let check = svc.sanitize_lab_report(&report); assert!(check.is_ok()); } #[test] fn verify_no_pii_detects_all_pii_keys() { let svc = sanitizer(); let pii_keys = [ "name", "phone", "id_number", "address", "birth_date", "email", ]; for key in pii_keys { let mut report_json = serde_json::to_value(&clean_lab_report()).unwrap(); report_json[key] = serde_json::json!("test"); let report: LabReportDto = serde_json::from_value(report_json).unwrap(); let result = svc.sanitize_lab_report(&report); assert!( result.is_ok(), "LabReportDto 不包含 {} 字段,反序列化时被丢弃", key ); } } // ---- verify_no_pii 对原始 JSON 的验证 ---- #[test] fn dto_serialization_contains_no_pii() { let report = clean_lab_report(); let val = serde_json::to_value(&report).unwrap(); for key in &[ "name", "phone", "id_number", "address", "birth_date", "email", ] { assert!( !val.as_object().unwrap().contains_key(*key), "LabReportDto 不应包含 PII 字段: {}", key ); } } // ---- 空数据边界 ---- #[test] fn sanitize_empty_vital_signs() { let signs: Vec = vec![]; let result = sanitizer().sanitize_vital_signs(&signs); assert!(result.is_ok()); assert!(result.unwrap().is_array()); } }