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

14 KiB
Raw Blame History

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_idnullAPI 返回 "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_openidtenant_idanalytics_queueedit_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

{
  "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 周内)

  1. F3: 文章列表过滤草稿
  2. H5: 登录防重复点击
  3. H6: Analytics 复用请求层
  4. M6: daily-monitoring 加 Zod 验证
  5. 咨询会话 subject 字段缺失处理

中期优化P2 — 迭代规划)

  1. H2/H3: 存储加密加强
  2. M1/M2/M7/M8: 存储清理 + PII 加密统一
  3. 包体积优化ECharts 按需引入)
  4. 类型安全强化(消除 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"当前用户未关联患者档案"),不是路由缺失
  • 属于预期行为:积分端点设计为患者端使用,管理员账号无患者档案