Files
hms/crates/erp-health/src/fhir/converter.rs

475 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use crate::entity::{
appointment, consultation_session, device_readings, doctor_profile, follow_up_task,
lab_report, patient, patient_devices,
};
use crate::fhir::types::{device_type_to_category, device_type_to_loinc, device_type_to_unit};
/// HMS Patient → FHIR Patient (JSON Value)
pub fn patient_to_fhir(p: &patient::Model) -> serde_json::Value {
let mut result = serde_json::json!({
"resourceType": "Patient",
"id": p.id.to_string(),
"name": [{
"text": p.name,
}],
"meta": {
"source": "hms",
"lastUpdated": p.updated_at.to_rfc3339(),
},
});
if let Some(ref gender) = p.gender {
result["gender"] = serde_json::json!(match gender.as_str() {
"male" | "M" => "male",
"female" | "F" => "female",
"other" => "other",
_ => "unknown",
});
}
if let Some(bd) = p.birth_date {
result["birthDate"] = serde_json::json!(bd.to_string());
}
if let Some(ref id_number) = p.id_number {
// 加密密文v1| 前缀)不输出,明文做脱敏处理
let display_value = if id_number.starts_with("v1|") {
"[REDACTED]".to_string()
} else {
mask_sensitive(id_number)
};
result["identifier"] = serde_json::json!([{
"system": "urn:oid:2.16.156.10011.1.3",
"value": display_value,
}]);
}
result
}
/// HMS DeviceReading → FHIR Observation血压拆分为多个其余为单个
pub fn device_reading_to_fhir_observations(r: &device_readings::Model) -> Vec<serde_json::Value> {
let mut results = Vec::new();
let patient_ref = serde_json::json!({"reference": format!("Patient/{}", r.patient_id)});
let device_ref = r.device_id.as_ref().map(|d| {
serde_json::json!({"reference": format!("Device/{}", d)})
});
let measured = r.measured_at.to_rfc3339();
let category = device_type_to_category(&r.device_type);
let category_json = serde_json::json!([{
"coding": [{
"system": "http://terminology.hl7.org/CodeSystem/observation-category",
"code": category,
}]
}]);
match r.device_type.as_str() {
"blood_pressure" => {
let sys = r.raw_value.get("systolic").and_then(|v| v.as_f64());
let dia = r.raw_value.get("diastolic").and_then(|v| v.as_f64());
if let Some(val) = sys {
results.push(make_observation(
&r.id, "8480-6", "Systolic blood pressure",
category_json.clone(), &patient_ref, device_ref.as_ref(),
&measured, val, "mmHg", "mm[Hg]",
));
}
if let Some(val) = dia {
results.push(make_observation(
&r.id, "8462-4", "Diastolic blood pressure",
category_json, &patient_ref, device_ref.as_ref(),
&measured, val, "mmHg", "mm[Hg]",
));
}
}
_ => {
let (loinc_code, loinc_display) = device_type_to_loinc(&r.device_type)
.unwrap_or(("unknown", "Unknown"));
let (unit_display, unit_code) = device_type_to_unit(&r.device_type);
let val = extract_main_value(&r.device_type, &r.raw_value);
if let Some(v) = val {
results.push(make_observation(
&r.id, loinc_code, loinc_display,
category_json, &patient_ref, device_ref.as_ref(),
&measured, v, unit_display, unit_code,
));
}
}
}
results
}
fn make_observation(
reading_id: &uuid::Uuid, code: &str, display: &str,
category: serde_json::Value, subject: &serde_json::Value,
device: Option<&serde_json::Value>, effective: &str,
value: f64, unit_display: &str, unit_code: &str,
) -> serde_json::Value {
let mut obs = serde_json::json!({
"resourceType": "Observation",
"id": reading_id.to_string(),
"status": "final",
"category": category,
"code": {
"coding": [{
"system": "http://loinc.org",
"code": code,
"display": display,
}]
},
"subject": subject,
"effectiveDateTime": effective,
"valueQuantity": {
"value": value,
"unit": unit_display,
"system": "http://unitsofmeasure.org",
"code": unit_code,
},
"meta": { "source": "hms-device" },
});
if let Some(d) = device {
obs["device"] = d.clone();
}
obs
}
fn extract_main_value(device_type: &str, raw: &serde_json::Value) -> Option<f64> {
match device_type {
"heart_rate" => raw.get("heart_rate").and_then(|v| v.as_f64()),
"blood_oxygen" => raw.get("blood_oxygen").and_then(|v| v.as_f64()),
"temperature" => raw.get("temperature").and_then(|v| v.as_f64()),
"blood_glucose" => raw.get("blood_glucose").and_then(|v| v.as_f64()),
"steps" => raw.get("steps").and_then(|v| v.as_f64()),
"sleep" => raw.get("sleep_duration").and_then(|v| v.as_f64()),
"stress" => raw.get("stress_level").and_then(|v| v.as_f64()),
_ => raw.as_f64(),
}
}
/// HMS PatientDevice → FHIR Device
pub fn patient_device_to_fhir(d: &patient_devices::Model) -> serde_json::Value {
let mut device = serde_json::json!({
"resourceType": "Device",
"id": d.id.to_string(),
"identifier": [{
"system": "urn:hms:device-id",
"value": d.device_id,
}],
"status": "active",
"meta": { "source": "hms-device" },
});
if let Some(ref model) = d.device_model {
device["displayName"] = serde_json::json!(model);
}
if let Some(ref dt) = d.device_type {
device["type"] = serde_json::json!({
"coding": [{
"system": "urn:hms:device-type",
"code": dt,
}]
});
}
device
}
/// HMS DoctorProfile → FHIR Practitioner
pub fn doctor_to_fhir(d: &doctor_profile::Model) -> serde_json::Value {
let mut practitioner = serde_json::json!({
"resourceType": "Practitioner",
"id": d.id.to_string(),
"name": [{
"text": d.name,
}],
"meta": { "source": "hms-practitioner" },
});
let mut qualifications = Vec::new();
if let Some(ref title) = d.title {
qualifications.push(serde_json::json!({
"code": {
"coding": [{
"system": "urn:hms:doctor-title",
"display": title,
}]
}
}));
}
if let Some(ref dept) = d.department {
qualifications.push(serde_json::json!({
"code": {
"coding": [{
"system": "urn:hms:department",
"display": dept,
}]
}
}));
}
if !qualifications.is_empty() {
practitioner["qualification"] = serde_json::json!(qualifications);
}
if let Some(ref specialty) = d.specialty {
practitioner["specialty"] = serde_json::json!([{
"coding": [{
"system": "urn:hms:specialty",
"display": specialty,
}]
}]);
}
practitioner
}
/// HMS Appointment → FHIR Appointment
pub fn appointment_to_fhir(a: &appointment::Model) -> serde_json::Value {
let fhir_status = match a.status.as_str() {
"confirmed" | "completed" => a.status.as_str(),
"pending" => "proposed",
"cancelled" => "cancelled",
_ => "booked",
};
let mut participants = vec![
serde_json::json!({
"actor": {"reference": format!("Patient/{}", a.patient_id)},
"status": "accepted",
}),
];
if let Some(ref doctor_id) = a.doctor_id {
participants.push(serde_json::json!({
"actor": {"reference": format!("Practitioner/{}", doctor_id)},
"status": "accepted",
}));
}
let start = format!("{}T{}", a.appointment_date, a.start_time);
let end = format!("{}T{}", a.appointment_date, a.end_time);
serde_json::json!({
"resourceType": "Appointment",
"id": a.id.to_string(),
"status": fhir_status,
"participant": participants,
"start": start,
"end": end,
"meta": { "source": "hms-appointment" },
})
}
/// HMS LabReport → FHIR DiagnosticReport
pub fn lab_report_to_fhir(r: &lab_report::Model) -> serde_json::Value {
let fhir_status = match r.status.as_str() {
"reviewed" => "final",
"pending" => "preliminary",
_ => "registered",
};
serde_json::json!({
"resourceType": "DiagnosticReport",
"id": r.id.to_string(),
"status": fhir_status,
"code": {
"coding": [{
"system": "urn:hms:report-type",
"code": r.report_type,
}]
},
"subject": {"reference": format!("Patient/{}", r.patient_id)},
"effectiveDateTime": r.report_date.to_string(),
"meta": { "source": "hms-diagnostic-report" },
})
}
/// HMS ConsultationSession → FHIR Encounter
pub fn consultation_to_fhir(c: &consultation_session::Model) -> serde_json::Value {
let fhir_status = match c.status.as_str() {
"active" => "in-progress",
"closed" => "finished",
_ => "planned",
};
let class_code = match c.consultation_type.as_str() {
"online" => "VR",
"offline" => "AMB",
_ => "AMB",
};
serde_json::json!({
"resourceType": "Encounter",
"id": c.id.to_string(),
"status": fhir_status,
"class": {
"system": "http://terminology.hl7.org/CodeSystem/v3-ActCode",
"code": class_code,
},
"subject": {"reference": format!("Patient/{}", c.patient_id)},
"meta": { "source": "hms-encounter" },
})
}
/// HMS FollowUpTask → FHIR Task
pub fn follow_up_to_fhir(t: &follow_up_task::Model) -> serde_json::Value {
let fhir_status = match t.status.as_str() {
"pending" => "requested",
"in_progress" => "in-progress",
"completed" => "completed",
"cancelled" => "cancelled",
_ => "requested",
};
let display = t.content_template.as_deref()
.unwrap_or(&t.follow_up_type);
serde_json::json!({
"resourceType": "Task",
"id": t.id.to_string(),
"status": fhir_status,
"intent": "plan",
"focus": {
"display": display,
},
"for": {"reference": format!("Patient/{}", t.patient_id)},
"meta": { "source": "hms-task" },
})
}
/// 对敏感字符串做脱敏:保留前 1 位和后 4 位,中间用 * 替代
fn mask_sensitive(s: &str) -> String {
if s.len() <= 5 {
"*".repeat(s.len())
} else {
format!("{}{}{}", &s[..1], "*".repeat(s.len() - 5), &s[s.len() - 4..])
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_patient_to_fhir_basic() {
let p = patient::Model {
id: uuid::Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(),
tenant_id: uuid::Uuid::now_v7(),
user_id: None,
name: "\u{5f20}\u{4e09}".into(),
gender: Some("male".into()),
birth_date: chrono::NaiveDate::from_ymd_opt(1968, 5, 15),
blood_type: None,
id_number: Some("110101196805150001".into()),
id_number_hash: None,
allergy_history: None,
medical_history_summary: None,
emergency_contact_name: None,
emergency_contact_phone: None,
emergency_contact_phone_hash: None,
key_version: None,
status: "active".into(),
verification_status: "verified".into(),
source: None,
notes: None,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
created_by: None,
updated_by: None,
deleted_at: None,
version: 1,
};
let fhir = patient_to_fhir(&p);
assert_eq!(fhir["resourceType"], "Patient");
assert_eq!(fhir["id"], "00000000-0000-0000-0000-000000000001");
assert_eq!(fhir["gender"], "male");
assert_eq!(fhir["birthDate"], "1968-05-15");
assert_eq!(fhir["identifier"][0]["value"], "1*************0001");
}
#[test]
fn test_patient_to_fhir_encrypted_id_number_redacted() {
let p = patient::Model {
id: uuid::Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(),
tenant_id: uuid::Uuid::now_v7(),
user_id: None,
name: "测试".into(),
gender: None,
birth_date: None,
blood_type: None,
id_number: Some("v1|encrypted_payload_here".into()),
id_number_hash: None,
allergy_history: None,
medical_history_summary: None,
emergency_contact_name: None,
emergency_contact_phone: None,
emergency_contact_phone_hash: None,
key_version: None,
status: "active".into(),
verification_status: "pending".into(),
source: None,
notes: None,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
created_by: None,
updated_by: None,
deleted_at: None,
version: 1,
};
let fhir = patient_to_fhir(&p);
assert_eq!(fhir["identifier"][0]["value"], "[REDACTED]");
}
#[test]
fn test_mask_sensitive() {
assert_eq!(mask_sensitive("110101196805150001"), "1*************0001");
assert_eq!(mask_sensitive("12345"), "*****");
assert_eq!(mask_sensitive("123456"), "1*3456");
}
#[test]
fn test_heart_rate_to_fhir_observation() {
let reading = device_readings::Model {
id: uuid::Uuid::now_v7(),
tenant_id: uuid::Uuid::now_v7(),
patient_id: uuid::Uuid::now_v7(),
device_id: Some("band-001".into()),
device_type: "heart_rate".into(),
metric: None,
device_model: Some("Mi Band 8".into()),
raw_value: serde_json::json!({"heart_rate": 78}),
measured_at: chrono::Utc::now(),
created_at: chrono::Utc::now(),
deleted_at: None,
};
let observations = device_reading_to_fhir_observations(&reading);
assert_eq!(observations.len(), 1);
let obs = &observations[0];
assert_eq!(obs["resourceType"], "Observation");
assert_eq!(obs["code"]["coding"][0]["code"], "8867-4");
assert!((obs["valueQuantity"]["value"].as_f64().unwrap() - 78.0).abs() < f64::EPSILON);
}
#[test]
fn test_blood_pressure_to_fhir_observations() {
let reading = device_readings::Model {
id: uuid::Uuid::now_v7(),
tenant_id: uuid::Uuid::now_v7(),
patient_id: uuid::Uuid::now_v7(),
device_id: Some("bp-001".into()),
device_type: "blood_pressure".into(),
metric: None,
device_model: Some("Omron HEM-7322".into()),
raw_value: serde_json::json!({"systolic": 135, "diastolic": 88}),
measured_at: chrono::Utc::now(),
created_at: chrono::Utc::now(),
deleted_at: None,
};
let observations = device_reading_to_fhir_observations(&reading);
assert!(observations.len() >= 2);
}
}