docs(spec): 修正 spec review 反馈的 6 个问题

- secureGet 修复移除无意义的 length > 0 验证
- P0-2 补充 auth.ts logout 中 current_patient_id 清理链路
- P1-1 补充 consultation.ts service 层类型修改
- P1-2 改为复用 input/index.tsx 已有的 num() 校验
- H4 修正医生端描述(非卡死,缺重试)
- C7 修正开发登录保护方案(IS_SIMULATOR 体验版泄漏)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
iven
2026-05-21 15:25:57 +08:00
parent ce0561001f
commit 43795b2fb7

View File

@@ -26,7 +26,7 @@
| C5 | 缺少紧急求助/SOS 入口 | `index.tsx` HomeDashboard | 患者突发状况无法快速求助 |
| C6 | Token 存储弱加密XOR + 硬编码 fallback | `secure-storage.ts:3` | 越狱设备可直接恢复 JWT token |
| C7 | 开发快速登录可能泄露到生产 | `login/index.tsx:8-9` | 攻击者获取 dev 凭证 |
| C8 | AI 聊天无后端持久化 | `messages/index.tsx` | Tab 切换可能丢失对话 |
| C8 | AI 聊天持久化需验证 | `messages/index.tsx` | wiki 标记已修复(后端消息加载 API 已添加),需验证实际效果 |
### HIGH11 项)
@@ -35,7 +35,7 @@
| H1 | 积分任务按钮无 onClick | `mall/index.tsx:187` |
| H2 | 告警无微信推送能力 | `alerts/index.tsx` |
| H3 | 体征录入校验不一致 | `health/index.tsx:112` vs `input/index.tsx:33` |
| H4 | 医生端工作台加载卡死 | `pkg-doctor-core/index.tsx` |
| H4 | 医生端工作台 API 失败无重试 | `pkg-doctor-core/index.tsx` |
| H5 | 家属代理操作无权限模型 | `family/index.tsx:47`, `auth.ts:226` |
| H6 | 首页医护人员 reLaunch 状态丢失 | `index.tsx:319-329` |
| H7 | PII 明文绕过 secure storage | `family/index.tsx:63` |
@@ -66,17 +66,13 @@ export function secureGet(key: string): string {
try {
const decoded = fromBase64(raw);
if (decoded) {
const decrypted = xorEncrypt(decoded, ENCRYPTION_KEY);
// 验证解密结果是否为有效数据JSON 或 JWT
if (decrypted.startsWith('{') || decrypted.startsWith('eyJ') || decrypted.length > 0) {
return decrypted;
}
return xorEncrypt(decoded, ENCRYPTION_KEY);
}
} catch {
// fallthrough — 可能是未加密的旧数据
}
// fallback: 兼容未加密的旧数据
// fallback: 兼容未加密的旧数据(明文 JSON/JWT 或其他值)
return raw;
}
```
@@ -86,10 +82,12 @@ export function secureGet(key: string): string {
### P0-2. 修复 Storage Key 前缀不一致
**文件**:
- `src/services/request.ts` 第 149 行: `Taro.getStorageSync('current_patient_id')``secureGet('current_patient_id')`
- `src/services/request.ts` 第 149 行: `Taro.getStorageSync('current_patient_id')``secureGet('current_patient_id')`(先修此处,确保内存缓存正确填充)
- `src/services/request.ts` 第 218 行: `Taro.removeStorageSync('current_patient_id')``secureRemove('current_patient_id')`
- `src/services/health.ts` 第 20 行: `Taro.getStorageSync('current_patient_id')` → 改用 `getCachedPatientId()``secureGet('current_patient_id')`
- `src/stores/auth.ts` 第 266 行: `secureRemove('analytics_queue')` `Taro.removeStorageSync('analytics_queue')`analytics 用明文存储)
- `src/services/health.ts` 第 20 行: 改用 `getCachedPatientId()`(依赖 request.ts 的内存缓存)
- `src/stores/auth.ts` 第 264-265 行: `secureRemove('current_patient')` / `secureRemove('current_patient_id')` 需同步确认写入路径一致
- `src/stores/auth.ts` 第 266 行: `secureRemove('analytics_queue')``Taro.removeStorageSync('analytics_queue')`analytics 使用明文存储,清理也必须用明文)
- `src/services/analytics.ts`: 确认 analytics_queue 包含 userId/patientIdlogout 时必须清除PII 泄漏风险)
### P0-3. 验证
@@ -103,36 +101,24 @@ export function secureGet(key: string): string {
### P1-1. 咨询创建添加症状描述
**文件**: `src/pages/consultation/create/index.tsx`
**文件**:
- `src/pages/consultation/create/index.tsx` — 添加 UI 字段
- `src/services/consultation.ts``createSession` 函数类型签名添加 `description` 参数
**改动**:
- 添加多行 `<Input type='textarea'>` 字段placeholder: "请描述您的症状或问题"
- 标记为建议填写(非必填),但未填写时 Toast 提醒"建议描述症状以便医生更快响应"
- `services/consultation.ts``createSession` 参数类型扩展,添加 `description?: string`
- 提交时将描述传递给后端 API后端 DTO 已支持 `description` 字段)
### P1-2. 体征录入统一范围校验
**新建文件**: `src/utils/vital-ranges.ts`
**文件**: `src/pages/health/index.tsx`, `src/pages/pkg-health/input/index.tsx`
```typescript
export const VITAL_RANGES = {
systolic_bp: { min: 60, max: 250, label: '收缩压' },
diastolic_bp: { min: 40, max: 150, label: '舒张压' },
heart_rate: { min: 20, max: 300, label: '心率' },
blood_glucose: { min: 1, max: 50, label: '血糖' },
weight: { min: 1, max: 500, label: '体重' },
} as const;
export function validateVital(type: keyof typeof VITAL_RANGES, value: number): string | null {
const range = VITAL_RANGES[type];
if (value < range.min || value > range.max) {
return `${range.label}应在 ${range.min}-${range.max} 之间`;
}
return null;
}
```
**修改文件**: `src/pages/health/index.tsx` 第 112-115 行,调用 `validateVital()` 替代简单的非空检查。`src/pages/pkg-health/input/index.tsx` 复用同一校验函数(移除内联校验逻辑)。
**改动**:
- 复用 `input/index.tsx` 中已有的 `num()` 校验函数(非创建新文件),提取到 `src/utils/vital-validation.ts`
- `health/index.tsx` 第 112-115 行,调用提取后的 `num()` 替代简单的非空检查
- 保留 `input/index.tsx` 中的动态阈值警告逻辑(`getWarnForIndicator`),不降级为静态范围
### P1-3. 积分任务入口修复
@@ -155,15 +141,15 @@ export function validateVital(type: keyof typeof VITAL_RANGES, value: number): s
**文件**: `src/pages/pkg-doctor-core/index.tsx`
**改动**:
- `loadDashboard` catch 中设置 `setDashboard({})` 而非仅打印 warn
- loading 为 false 且 dashboard 为 null 时,显示降级 UI统计卡片显示 `-`+ 错误提示 + 重试按钮
- 不再永久卡在 `<Loading />`
- `loadDashboard` catch 中 `setDashboard({})` 确保组件不卡在永久 Loading
- loading 为 false 且 dashboard 为空对象时,统计卡片显示 `-`(当前已实现)
- 添加错误状态提示和重试按钮(当前缺少)
### P1-5. 首页医护人员跳转优化
**文件**: `src/pages/index/index.tsx`
**改动**: 将 `shouldRedirect` 计算提前到组件顶层useMemo`useDidShow` 之前就确定跳转。跳转使用 `redirectTo` 替代 `reLaunch`保留分包预加载状态
**改动**: 将 `shouldRedirect` 计算提前到组件顶层useMemo`useDidShow` 之前就确定跳转。**注意**:首页是 TabBar 页面,`redirectTo` 替换 TabBar 页面可能导致 TabBar 消失,需改为在 `useDidShow` 中使用 `reLaunch`保留现有方式),但提前判断避免先渲染 Loading 再跳转
---
@@ -238,8 +224,10 @@ export function validateVital(type: keyof typeof VITAL_RANGES, value: number): s
### P3-2. 安全加固
**开发快速登录保护**:
- `login/index.tsx` 第 8-9 行: 添加编译时保护
- `config/prod.ts` 中确保 `TARO_APP_DEV_USER``TARO_APP_DEV_PASS` 注入为空字符串
- `login/index.tsx` 第 8-9 行: `IS_DEV``IS_SIMULATOR` 已有保护
- 实际风险:体验版(`envVersion === 'trial'`)也会显示开发登录按钮(`IS_SIMULATOR` 为 true
- 修复:第 9 行改为 `typeof __wxConfig !== 'undefined' && (__wxConfig as any).envVersion === 'develop'`(仅开发版显示)
- `config/index.ts``TARO_APP_DEV_USER/PASS` 已有空字符串 fallback无需额外修改 `prod.ts`
**Tenant ID 安全**:
- `request.ts` 第 161 行: `X-Tenant-Id` 改为从 JWT payload 的 `tid` 字段提取