diff --git a/apps/web/src/pages/health/FollowUpTaskList.tsx b/apps/web/src/pages/health/FollowUpTaskList.tsx index 85ec68b..25f6113 100644 --- a/apps/web/src/pages/health/FollowUpTaskList.tsx +++ b/apps/web/src/pages/health/FollowUpTaskList.tsx @@ -344,6 +344,7 @@ export default function FollowUpTaskList() { /> { if (dates && dates[0] && dates[1]) { setFilters((prev) => ({ diff --git a/apps/web/src/utils/format.ts b/apps/web/src/utils/format.ts index 64ab2a1..396b04b 100644 --- a/apps/web/src/utils/format.ts +++ b/apps/web/src/utils/format.ts @@ -12,5 +12,5 @@ export const formatRelative = (v: string | null | undefined): string => export const calcAge = (birthDate: string | null | undefined): string => { if (!birthDate) return '--'; const age = dayjs().diff(dayjs(birthDate), 'year'); - return `${age}岁`; + return age >= 0 ? `${age}岁` : '--'; }; diff --git a/crates/erp-health/src/service/patient_service/helper.rs b/crates/erp-health/src/service/patient_service/helper.rs index f5dc3a3..4f969ab 100644 --- a/crates/erp-health/src/service/patient_service/helper.rs +++ b/crates/erp-health/src/service/patient_service/helper.rs @@ -56,34 +56,20 @@ pub(crate) fn model_to_resp(m: patient::Model) -> PatientResp { /// 详情用 — 解密 Tier 1 字段 pub(crate) fn model_to_resp_decrypted(crypto: &PiiCrypto, m: patient::Model) -> PatientResp { let kek = crypto.kek(); - let decrypted_id_number = m - .id_number - .as_ref() - .map(|enc| pii::decrypt(kek, enc)) - .transpose() - .ok() - .flatten(); - let decrypted_allergy = m - .allergy_history - .as_ref() - .map(|enc| pii::decrypt(kek, enc)) - .transpose() - .ok() - .flatten(); - let decrypted_medical = m - .medical_history_summary - .as_ref() - .map(|enc| pii::decrypt(kek, enc)) - .transpose() - .ok() - .flatten(); - let decrypted_phone = m - .emergency_contact_phone - .as_ref() - .map(|enc| pii::decrypt(kek, enc)) - .transpose() - .ok() - .flatten(); + let decrypted_id_number = decrypt_field(kek, &m.id_number, "id_number", m.id); + let decrypted_allergy = decrypt_field(kek, &m.allergy_history, "allergy_history", m.id); + let decrypted_medical = decrypt_field( + kek, + &m.medical_history_summary, + "medical_history_summary", + m.id, + ); + let decrypted_phone = decrypt_field( + kek, + &m.emergency_contact_phone, + "emergency_contact_phone", + m.id, + ); PatientResp { id: m.id, user_id: m.user_id, @@ -105,3 +91,22 @@ pub(crate) fn model_to_resp_decrypted(crypto: &PiiCrypto, m: patient::Model) -> version: m.version, } } + +/// 解密单个 PII 字段,失败时输出 warn 日志并返回 None +fn decrypt_field( + kek: &[u8; 32], + field: &Option, + name: &str, + patient_id: Uuid, +) -> Option { + field + .as_ref() + .map(|enc| pii::decrypt(kek, enc)) + .transpose() + .map_err(|e| { + tracing::warn!(patient_id = %patient_id, field = name, error = %e, "PII decrypt failed"); + e + }) + .ok() + .flatten() +}