fix: 全面 QA 审计修复 — 安全加固/代码质量/跨平台一致性/测试覆盖
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

Phase 0 安全热修复 (CRITICAL):
- 外部化微信 appid/secret 到 ERP__WECHAT__APPID/SECRET 环境变量
- 正确连接 HealthCrypto 到 ERP__HEALTH__AES_KEY/HMAC_KEY 环境变量
- 外部化小程序加密密钥到 TARO_APP_ENCRYPTION_KEY 环境变量
- 移除小程序 auth store 中的敏感信息 console.log

Phase 1 安全加固:
- 微信自动注册 display_name 添加 sanitize 防止 XSS
- 测试数据库凭据改为从 TEST_DB_URL 环境变量读取

Phase 2 代码质量:
- 提取 useThemeMode hook 消除 22 处重复暗色模式检测
- 提取共享健康常量到 constants/health.ts
- 拆分 patient_service.rs 脱敏函数到 masking.rs
- 移除未使用的 i18next/react-i18next 依赖
- 移除未使用的 api/errors.ts 和 erp-auth/anyhow 依赖

Phase 3 测试覆盖:
- 新增 5 个患者模块集成测试 (CRUD/租户隔离/验证/软删除)

Phase 4 跨平台一致性:
- 统一小程序 Patient.birthday → birth_date 匹配后端
- 统一小程序 Appointment.time_slot → start_time/end_time 匹配后端

Phase 5 架构:
- 微信登录添加多租户 TODO 注释
- 更新 wiki/infrastructure.md 环境变量文档
This commit is contained in:
iven
2026-04-25 10:00:49 +08:00
parent 07f4ba41ba
commit 945ccd64ba
56 changed files with 634 additions and 273 deletions

View File

@@ -27,7 +27,9 @@ interface DoctorItem {
}
interface TimeSlot {
time_slot: string;
start_time: string;
end_time: string;
label: string;
available_count: number;
}
@@ -85,7 +87,9 @@ export default function AppointmentCreate() {
const daySlots = schedules
.filter((s: any) => (s.date || s.appointment_date) === date)
.map((s: any) => ({
time_slot: s.time_slot || `${s.start_time || ''}-${s.end_time || ''}`,
start_time: s.start_time || '',
end_time: s.end_time || '',
label: `${s.start_time || ''}-${s.end_time || ''}`,
available_count: s.available_count ?? (s.max_patients ?? 10),
}));
setTimeSlots(daySlots);
@@ -105,11 +109,13 @@ export default function AppointmentCreate() {
setLoading(true);
try {
const selectedSlot = timeSlots.find((s) => s.label === timeSlot);
await createAppointment({
patient_id: currentPatient.id,
doctor_id: selectedDoctor.id,
appointment_date: appointmentDate,
time_slot: timeSlot,
start_time: selectedSlot?.start_time || timeSlot,
end_time: selectedSlot?.end_time || timeSlot,
reason: reason.trim() || undefined,
});
Taro.showToast({ title: '预约成功', icon: 'success' });
@@ -118,7 +124,7 @@ export default function AppointmentCreate() {
const tmplId = TEMPLATE_IDS.APPOINTMENT_REMINDER;
if (tmplId) {
try {
await Taro.requestSubscribeMessage({ tmplIds: [tmplId] });
await (Taro.requestSubscribeMessage as any)({ tmplIds: [tmplId] });
} catch { /* 用户拒绝 */ }
}
setTimeout(() => Taro.navigateBack(), 1500);
@@ -219,11 +225,11 @@ export default function AppointmentCreate() {
<View className='slot-grid'>
{timeSlots.map((slot) => (
<View
className={`slot-card ${getSlotStyle(slot.available_count)} ${timeSlot === slot.time_slot ? 'slot-selected' : ''}`}
key={slot.time_slot}
onClick={slot.available_count > 0 ? () => setTimeSlot(slot.time_slot) : undefined}
className={`slot-card ${getSlotStyle(slot.available_count)} ${timeSlot === slot.label ? 'slot-selected' : ''}`}
key={slot.label}
onClick={slot.available_count > 0 ? () => setTimeSlot(slot.label) : undefined}
>
<Text className='slot-time'>{slot.time_slot}</Text>
<Text className='slot-time'>{slot.label}</Text>
<Text className='slot-count'>{slot.available_count > 0 ? `剩余 ${slot.available_count}` : '已满'}</Text>
</View>
))}

View File

@@ -84,7 +84,7 @@ export default function AppointmentDetail() {
<Text className='header-title'></Text>
<View className='header-placeholder' />
</View>
<ErrorState message='未找到预约信息' />
<ErrorState text='未找到预约信息' />
</View>
);
}
@@ -117,7 +117,7 @@ export default function AppointmentDetail() {
</View>
<View className='info-item'>
<Text className='info-label'></Text>
<Text className='info-value'>{appointment.time_slot}</Text>
<Text className='info-value'>{appointment.start_time} - {appointment.end_time}</Text>
</View>
<View className='info-item'>
<Text className='info-label'></Text>

View File

@@ -107,7 +107,7 @@ export default function AppointmentList() {
</View>
<View className='info-row'>
<Text className='info-icon'>🕐</Text>
<Text className='info-text'>{item.time_slot}</Text>
<Text className='info-text'>{item.start_time} - {item.end_time}</Text>
</View>
</View>
</View>

View File

@@ -19,7 +19,7 @@ export default function FamilyAdd() {
const [genderIdx, setGenderIdx] = useState(
editData?.gender === 'female' ? 1 : 0
);
const [birthDate, setBirthDate] = useState(editData?.birthday || '');
const [birthDate, setBirthDate] = useState(editData?.birth_date || '');
const [submitting, setSubmitting] = useState(false);
useEffect(() => {

View File

@@ -25,14 +25,14 @@ export default function FamilyList() {
useDidShow(() => {
fetchPatients();
}, [fetchPatients]);
});
const handleSelect = (patient: Patient) => {
setCurrentPatient({
id: patient.id,
name: patient.name,
gender: patient.gender,
birthday: patient.birthday,
birth_date: patient.birth_date,
relation: patient.relation || '本人',
});
Taro.showToast({ title: `已切换为 ${patient.name}`, icon: 'success' });