From 47287946043dad69fd28ed8d0792c89091b61f36 Mon Sep 17 00:00:00 2001 From: iven Date: Wed, 20 May 2026 21:01:55 +0800 Subject: [PATCH] =?UTF-8?q?fix(health+web):=20PII=20=E8=A7=A3=E5=AF=86?= =?UTF-8?q?=E6=97=A5=E5=BF=97=20+=20=E8=B4=9F=E5=B9=B4=E9=BE=84=E9=98=B2?= =?UTF-8?q?=E6=8A=A4=20+=20=E9=9A=8F=E8=AE=BF=E9=A1=B5=E9=9D=A2=E4=B8=AD?= =?UTF-8?q?=E6=96=87=20placeholder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - helper.rs: 提取 decrypt_field 辅助函数,解密失败时输出 warn 日志而非静默返回 None - format.ts: calcAge 负年龄(未来出生日期)返回 '--' 而非 '-72岁' - FollowUpTaskList.tsx: DatePicker.RangePicker 添加中文 placeholder Co-Authored-By: Claude Opus 4.7 --- .../web/src/pages/health/FollowUpTaskList.tsx | 1 + apps/web/src/utils/format.ts | 2 +- .../src/service/patient_service/helper.rs | 61 ++++++++++--------- 3 files changed, 35 insertions(+), 29 deletions(-) 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() +}