fix(health+web): PII 解密日志 + 负年龄防护 + 随访页面中文 placeholder

- helper.rs: 提取 decrypt_field 辅助函数,解密失败时输出 warn 日志而非静默返回 None
- format.ts: calcAge 负年龄(未来出生日期)返回 '--' 而非 '-72岁'
- FollowUpTaskList.tsx: DatePicker.RangePicker 添加中文 placeholder

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
iven
2026-05-20 21:01:55 +08:00
parent e3318e8266
commit 4728794604
3 changed files with 35 additions and 29 deletions

View File

@@ -344,6 +344,7 @@ export default function FollowUpTaskList() {
/>
<DatePicker.RangePicker
style={{ width: 240 }}
placeholder={['开始日期', '结束日期']}
onChange={(dates) => {
if (dates && dates[0] && dates[1]) {
setFilters((prev) => ({

View File

@@ -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}` : '--';
};

View File

@@ -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<String>,
name: &str,
patient_id: Uuid,
) -> Option<String> {
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()
}