- docs/: 设计规格、讨论记录、销售数据、健康管理文档 - scripts/: 辅助脚本 - package.json + pnpm-lock.yaml: monorepo 根配置
14 KiB
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] 今日体征数据未反映刚录入的数据
实测过程:
POST /health/patients/{id}/vital-signs成功创建记录(heart_rate: 72)GET /health/vital-signs/today仍返回所有字段null
根因分析:
today端点依赖 JWT 中的user_id反查patient表的user_id字段- 测试患者(张三)的
user_id为null(API 返回"user_id":null),即该患者未关联登录账号 - 今日体征接口无法定位到正确的患者
前端影响:
- 用户在健康录入页提交数据后,返回首页或健康 Tab,"今日体征概览"区域仍然为空
- 用户体验严重断裂:数据明明录入了,但看不到
修复建议:
- 前端:
today接口改用X-Patient-Idheader 传递当前选中患者的 ID(代码已实现 header 注入,但后端today端点未读取) - 后端:
vital_signs_todayhandler 应优先使用X-Patient-Idheader 中的 patient_id
F2. [HIGH] 积分/签到功能对未关联患者的用户完全不可用
实测过程:
GET /health/points/account→ 404"当前用户未关联患者档案"GET /health/points/checkin/status→ 404
根因: 积分、签到、兑换等患者端功能都依赖 user_id → patient 的关联查询。管理员账号没有对应的患者档案。
前端影响:
- 积分商城 Tab 页(5 个 Tab 之一)对未关联患者的用户显示为空白或报错
- 没有友好的降级提示(如"请先完善个人档案")
修复建议:
- 前端:积分商城/签到页面需增加"未关联患者档案"的降级 UI
- Auth store
restore()时检查是否有currentPatient,无则引导用户建档
F3. [MEDIUM] 文章列表返回草稿状态的文章
实测过程:
GET /health/articles返回 4 篇文章,其中status: "draft"的文章也被返回
前端影响: 患者端文章列表可能显示未发布的草稿文章。
修复建议:
- 前端:
article.ts服务请求时添加status=published过滤参数 - 后端:患者端文章列表 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/loadingstate + 按钮禁用 - 健康录入 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。
优化建议:
- 使用
echarts/charts按需引入(仅 LineChart) - 或考虑小程序原生图表库(如 wx-charts)
- 预计可减少 ~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 — 影响用户体验/安全)
- F1: 今日体征数据不刷新 — 核心健康功能链路断裂
- H1: Token 刷新竞态 — 可能导致用户被锁死
- F2: 积分商城降级 UI — Tab 页空白影响用户信任
短期修复(P1 — 1-2 周内)
- F3: 文章列表过滤草稿
- H5: 登录防重复点击
- H6: Analytics 复用请求层
- M6: daily-monitoring 加 Zod 验证
- 咨询会话 subject 字段缺失处理
中期优化(P2 — 迭代规划)
- H2/H3: 存储加密加强
- M1/M2/M7/M8: 存储清理 + PII 加密统一
- 包体积优化(ECharts 按需引入)
- 类型安全强化(消除
any)
七、附录:API 实测数据
数据库当前状态
| 实体 | 记录数 |
|---|---|
| 患者 | 6 |
| 医生 | 2 |
| 预约 | 1(已完成) |
| 咨询会话 | 2(1 active, 1 waiting) |
| 随访任务 | 5(3 pending, 2 completed) |
| 文章 | 4(1 draft, 3 published) |
| 文章分类 | 2+ |
| 商品 | 1(Health Kit, 50 积分, 库存 100) |
| 医生排班 | 1(2026-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("当前用户未关联患者档案"),不是路由缺失
- 属于预期行为:积分端点设计为患者端使用,管理员账号无患者档案