Files
hms/docs/discussions/2026-04-27-miniprogram-audit-report.md
iven 1265935fa3 chore: 设计规格文档 + 销售数据 + 脚本工具 + 根目录 monorepo 配置
- docs/: 设计规格、讨论记录、销售数据、健康管理文档
- scripts/: 辅助脚本
- package.json + pnpm-lock.yaml: monorepo 根配置
2026-04-28 00:20:37 +08:00

331 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# HMS 小程序端审计报告
> 日期: 2026-04-27 | 审计范围: 代码审查 + API 链路实测 | 审计人: Claude
## 执行摘要
对 HMS 健康管理平台微信小程序端进行了全面审计,覆盖 **40 个页面、10+ 服务模块、2 个 Store**。通过代码静态分析 + 后端 API 实测,发现 **6 个严重问题、8 个中等问题、5 个低级问题**,以及 **3 个功能链路断链**
### 关键数字
| 指标 | 值 |
|------|-----|
| 审计页面 | 40 个(含 8 个医护端) |
| 审计服务 | 12 个 API 服务文件 |
| 审计组件 | 6 个 |
| 严重问题 (HIGH) | 6 个 |
| 中等问题 (MEDIUM) | 8 个 + 3 个功能断链 |
| 低级问题 (LOW) | 5 个 |
| 正面发现 | 8 项 |
---
## 一、功能链路断链(实测发现)
### F1. [HIGH] 今日体征数据未反映刚录入的数据
**实测过程:**
1. `POST /health/patients/{id}/vital-signs` 成功创建记录(`heart_rate: 72`
2. `GET /health/vital-signs/today` 仍返回所有字段 `null`
**根因分析:**
- `today` 端点依赖 JWT 中的 `user_id` 反查 `patient` 表的 `user_id` 字段
- 测试患者(张三)的 `user_id``null`API 返回 `"user_id":null`),即该患者未关联登录账号
- 今日体征接口无法定位到正确的患者
**前端影响:**
- 用户在健康录入页提交数据后,返回首页或健康 Tab"今日体征概览"区域仍然为空
- 用户体验严重断裂:**数据明明录入了,但看不到**
**修复建议:**
1. 前端:`today` 接口改用 `X-Patient-Id` header 传递当前选中患者的 ID代码已实现 header 注入,但后端 `today` 端点未读取)
2. 后端:`vital_signs_today` handler 应优先使用 `X-Patient-Id` header 中的 patient_id
### F2. [HIGH] 积分/签到功能对未关联患者的用户完全不可用
**实测过程:**
- `GET /health/points/account` → 404 `"当前用户未关联患者档案"`
- `GET /health/points/checkin/status` → 404
**根因:** 积分、签到、兑换等患者端功能都依赖 `user_id → patient` 的关联查询。管理员账号没有对应的患者档案。
**前端影响:**
- 积分商城 Tab 页5 个 Tab 之一)对未关联患者的用户显示为空白或报错
- **没有友好的降级提示**(如"请先完善个人档案"
**修复建议:**
1. 前端:积分商城/签到页面需增加"未关联患者档案"的降级 UI
2. Auth store `restore()` 时检查是否有 `currentPatient`,无则引导用户建档
### F3. [MEDIUM] 文章列表返回草稿状态的文章
**实测过程:**
- `GET /health/articles` 返回 4 篇文章,其中 `status: "draft"` 的文章也被返回
**前端影响:** 患者端文章列表可能显示未发布的草稿文章。
**修复建议:**
1. 前端:`article.ts` 服务请求时添加 `status=published` 过滤参数
2. 后端:患者端文章列表 API 应默认只返回 `published` 状态的文章
---
## 二、安全审计发现
### 2.1 严重 (HIGH)
| # | 问题 | 文件 | 影响 |
|---|------|------|------|
| H1 | **Token 刷新竞态条件** | `services/request.ts:57-65` | 多个 API 同时 401 时,各自独立调用 `tryRefreshToken()`,可能导致 refresh token 被消耗多次,锁死用户 |
| H2 | **静态加密密钥无密钥派生** | `utils/secure-storage.ts:4` | 所有用户共享同一编译时烘焙的密钥,反编译小程序包即可解密全部本地存储数据 |
| H3 | **非生产环境明文回退** | `utils/secure-storage.ts:11-33` | 开发模式下 token/PII 明文存储在 localStorage |
| H4 | **Token 冗余暴露在 React State** | `stores/auth.ts:31-32` | Zustand store 持有 `token`/`refreshToken` 副本,与 secure storage 冗余,增加攻击面 |
| H5 | **登录无防重复点击保护** | `stores/auth.ts:53-75` | `login()` 函数无 `if (loading) return` 守卫,快速双击可触发两次登录请求 |
| H6 | **Analytics 绕过请求层** | `services/analytics.ts:67-73` | 直接调用 `Taro.request()`,无 Authorization header无 token 刷新BASE_URL 硬编码重复 |
### 2.2 中等 (MEDIUM)
| # | 问题 | 文件 | 影响 |
|---|------|------|------|
| M1 | **Logout 清理不完整** | `stores/auth.ts:120-129` | 残留 `wechat_openid``tenant_id``analytics_queue``edit_patient` 在 storage 中 |
| M2 | **PII 未加密存储** | `stores/auth.ts:62-64` | `user` 对象(含 id/phone通过 `Taro.setStorageSync` 明文存储 |
| M3 | **开发日志含敏感数据** | `services/request.ts:50` | `console.log` 输出完整请求 body可能含血压、血糖等健康数据 |
| M4 | **解密失败静默返回空串** | `utils/secure-storage.ts:31-33` | 无法区分"未登录"和"数据被篡改"两种情况 |
| M5 | **聊天轮询闭包过时** | `consultation/detail` 两个页面 | `pollNewMessages` 捕获的 `session` 状态可能过时session 关闭后轮询可能继续 |
| M6 | **daily-monitoring 缺输入验证** | `health/daily-monitoring/index.tsx` | 无 Zod 验证,`parseFloat('999999')` 会被接受(对比 health/input 有完整 Zod |
| M7 | **OpenID 明文存储** | `stores/auth.ts:68` | WeChat OpenID 敏感标识符通过明文 storage 存储 |
| M8 | **身份证号明文传递** | `profile/family/index.tsx:46` | 编辑患者时 `edit_patient`(含 `id_number`)通过明文 storage 传递给编辑页 |
### 2.3 低 (LOW)
| # | 问题 | 说明 |
|---|------|------|
| L1 | 18 处 `any` 类型 | auth 流程最集中5 处 `as any`),绕过类型检查 |
| L2 | 聊天无指数退避 | 8s 固定间隔轮询,网络异常时产生大量无效请求 |
| L3 | 无客户端消息排序 | 假设服务端返回有序,无 `created_at` 排序 |
| L4 | devLogin 残留在产物中 | `services/auth.ts:47` 的开发登录函数未从生产构建中移除 |
| L5 | 大量静默 catch | 多处 `catch { }` 隐藏错误,影响生产问题排查 |
### 2.4 正面安全发现
- **XSS 防护完善**:零 `dangerouslySetInnerHTML`、零 `innerHTML`、零 `eval()`,所有用户内容通过 Taro `<Text>` 渲染
- **无硬编码密钥**:所有敏感配置通过环境变量注入
- **后端多租户隔离一致**:所有查询均含 `tenant_id` + `deleted_at IS NULL` 过滤
- **后端 CAS 并发控制**:预约、积分余额、库存、未读计数等关键操作使用乐观锁
- **后端 PII 加密存储**:身份证号、手机号、咨询消息等使用 AES + HMAC 可搜索加密
- **表单防重复提交**:所有表单使用 `submitting`/`loading` state + 按钮禁用
- **健康录入 Zod 验证**`health/input` 页面使用完整的 Zod schema + 阈值警告
- **URL 参数编码**`buildQuery` 正确过滤 undefined 并 `encodeURIComponent`
---
## 三、后端 API 数据结构实测
### 3.1 分页响应结构
后端统一返回结构(所有分页列表 API
```json
{
"success": true,
"data": {
"data": [...], // 实际数据数组
"total": 6,
"page": 1,
"page_size": 10,
"total_pages": 1
}
}
```
前端 `request.ts` 提取 `body.data` 后得到 `{ data: [...], total, page, ... }`
前端各 service 的泛型声明基本匹配此结构(如 `{ data: Patient[], total: number }`)。
**实测结论:前端 service 层的字段映射基本正确。** 但需注意:
- 部分页面可能直接用 `resp.items`(错误)而非 `resp.data`(正确)来访问列表数据
- 需要逐页验证页面层的消费代码
### 3.2 关键实体字段映射
| 实体 | 后端字段 | 前端期望 | 匹配状态 |
|------|----------|----------|----------|
| 商品积分价 | `points_cost` | `points_cost` | ✅ 匹配 |
| 咨询主题 | **无 `subject`/`title` 字段** | `subject` | ❌ 前端引用了不存在的字段 |
| 文章分类 | `category` (string) | `category` | ✅ 匹配 |
| 医生姓名 | `name` | `name` | ✅ 匹配 |
| 体征日期 | `record_date` | `record_date` | ✅ 匹配 |
### 3.3 咨询会话无 subject 字段
**实测发现:** 咨询会话实体后端字段为:
`id, patient_id, doctor_id, consultation_type, status, last_message_at, unread_count_patient, unread_count_doctor, created_at, updated_at, version`
**没有 `subject` 字段!** 前端会话列表页如果显示 `session.subject`,将渲染为 `undefined`
---
## 四、代码质量与优化
### 4.1 包体积问题
**实测发现:** `pages/health/trend/index.js` 体积 **455 KiB**,原因是全量引入 ECharts。
**优化建议:**
1. 使用 `echarts/charts` 按需引入(仅 LineChart
2. 或考虑小程序原生图表库(如 wx-charts
3. 预计可减少 ~80% 的趋势页体积
### 4.2 架构优化建议
| 优先级 | 建议 | 说明 |
|--------|------|------|
| P0 | Token 刷新加锁 | 实现单例 Promise 模式避免并发刷新 |
| P0 | 积分商城降级 UI | 未关联患者时显示引导而非空白 |
| P1 | daily-monitoring 加 Zod | 与 health/input 对齐验证标准 |
| P1 | Analytics 复用 request.ts | 消除独立的 `Taro.request` 调用 |
| P1 | 文章列表过滤草稿 | 患者端只展示 published 文章 |
| P2 | 聊天轮询 → WebSocket | 后端 SSE 基础设施已规划 |
| P2 | ECharts 按需引入 | 趋势页 455KiB → ~90KiB |
| P2 | 类型安全强化 | auth store 消除 `as any` |
| P3 | 统一错误处理 | 静默 catch → console.warn + 上报 |
---
## 五、缺失功能 / TODO 清单
| 功能 | 状态 | 说明 |
|------|------|------|
| 模板消息 | TODO | `wechat-templates.ts` 模板 ID 全部为空 |
| 用药提醒 | 仅本地 | 无后端同步,换设备即丢失 |
| 文章 Tab | 注册但未使用 | `pages/article/index` 在 pages 列表但不在 tabBar |
| 医生排班日历 | API 存在 | 后端 `doctor-schedules/calendar` 已实现,前端调用需验证 |
| AI 报告 | 页面存在 | 需验证 erp-ai 模块集成后的实际数据渲染 |
---
## 六、修复优先级建议
### 立即修复P0 — 影响用户体验/安全)
1. **F1: 今日体征数据不刷新** — 核心健康功能链路断裂
2. **H1: Token 刷新竞态** — 可能导致用户被锁死
3. **F2: 积分商城降级 UI** — Tab 页空白影响用户信任
### 短期修复P1 — 1-2 周内)
4. **F3: 文章列表过滤草稿**
5. **H5: 登录防重复点击**
6. **H6: Analytics 复用请求层**
7. **M6: daily-monitoring 加 Zod 验证**
8. **咨询会话 subject 字段缺失处理**
### 中期优化P2 — 迭代规划)
9. H2/H3: 存储加密加强
10. M1/M2/M7/M8: 存储清理 + PII 加密统一
11. 包体积优化ECharts 按需引入)
12. 类型安全强化(消除 `any`
---
## 七、附录API 实测数据
### 数据库当前状态
| 实体 | 记录数 |
|------|--------|
| 患者 | 6 |
| 医生 | 2 |
| 预约 | 1已完成 |
| 咨询会话 | 21 active, 1 waiting |
| 随访任务 | 53 pending, 2 completed |
| 文章 | 41 draft, 3 published |
| 文章分类 | 2+ |
| 商品 | 1Health Kit, 50 积分, 库存 100 |
| 医生排班 | 12026-04-28 AM, max 10, current 1 |
| 化验报告 | 0 |
| 线下活动 | 0 |
### 关键 ID用于后续测试
- 患者张三: `019dca49-1a88-7280-b44d-3ee5162b61ee` (user_id: null)
- 活跃咨询会话: `019dc2ac-235f-7550-a64c-3d66edfbf1ae`
- 已完成预约: doctor `019dc29b...`, date 2026-04-28
---
## 八、MCP 自动化页面渲染审计(实测补充)
> 使用 miniprogram-automator 通过 MCP 协议连接微信开发者工具,逐页导航验证渲染。
### 8.1 TabBar 页面5/5 通过)
| 页面路径 | 状态 |
|----------|------|
| pages/index/index | OK |
| pages/health/index | OK |
| pages/consultation/index | OK |
| pages/mall/index | OK |
| pages/profile/index | OK |
### 8.2 患者端 + 医护端子页面24/24 通过)
全部使用 `reLaunch` 逐页导航,无崩溃、无重定向到登录页:
| 页面路径 | 状态 |
|----------|------|
| pages/health/input/index | OK |
| pages/health/trend/index | OK |
| pages/health/daily-monitoring/index | OK |
| pages/appointment/index | OK |
| pages/appointment/create/index | OK |
| pages/article/index | OK |
| pages/ai-report/list/index | OK |
| pages/followup/detail/index | OK |
| pages/consultation/detail/index | OK |
| pages/mall/orders/index | OK |
| pages/profile/family/index | OK |
| pages/profile/reports/index | OK |
| pages/profile/followups/index | OK |
| pages/profile/medication/index | OK |
| pages/profile/settings/index | OK |
| pages/legal/user-agreement | OK |
| pages/legal/privacy-policy | OK |
| pages/doctor/index | OK |
| pages/doctor/patients/index | OK |
| pages/doctor/consultation/index | OK |
| pages/doctor/followup/index | OK |
| pages/doctor/report/index | OK |
| pages/events/index | OK |
| pages/device-sync/index | OK |
### 8.3 详情页(假 ID 优雅降级11/11 通过)
使用 UUID `00000000-0000-0000-0000-000000000000` 测试,所有页面不崩溃、显示空状态或加载中:
| 页面路径 | 状态 |
|----------|------|
| pages/appointment/detail/index?id=... | OK |
| pages/article/detail/index?id=... | OK |
| pages/report/detail/index?id=... | OK |
| pages/ai-report/detail/index?id=... | OK |
| pages/mall/detail/index?id=... | OK |
| pages/mall/exchange/index?id=... | OK |
| pages/profile/family-add/index | OK |
| pages/doctor/patients/detail/index?id=... | OK |
| pages/doctor/consultation/detail/index?id=... | OK |
| pages/doctor/followup/detail/index?id=... | OK |
| pages/doctor/report/detail/index?id=... | OK |
### 8.4 MCP 审计结论
**40/40 页面全部正常渲染**,无白屏、无崩溃、无意外重定向。详情页对无效 ID 参数均能优雅降级。
### 8.5 关于积分 API 404 的补充说明
上一轮审计中 `GET /health/points/account` 等 4 个端点返回 404经查
- 路由已正确注册在 `crates/erp-health/src/module.rs:454-484`
- 404 原因是 `resolve_patient_id()` 查找 admin 用户的关联患者档案失败
- 这是应用层 404"当前用户未关联患者档案"),不是路由缺失
- 属于预期行为:积分端点设计为患者端使用,管理员账号无患者档案