---
title: 微信小程序(患者端)
updated: 2026-05-10
status: active
tags: [miniprogram, taro, wechat, patient]
---
# 微信小程序(患者端)
> 从 [[index]] 导航。关联: [[infrastructure]] [[erp-server]] [[erp-health]]
## 1. 设计决策
- **Taro 4.2 + React 18** — 跨平台小程序框架,React 生态
- **Zustand 状态管理** — 与 Web 前端一致的 store 模式
- **微信登录流程** — `Taro.login()` → code → 后端 `jscode2session` → openid → JWT
- **build 时注入环境变量** — `process.env` 在小程序运行时不存在,必须通过 `defineConstants` 编译时替换
- **不使用浏览器 Web API** — `btoa`/`atob` 等在小程序中不可用,用 Taro 原生 API 替代
- **Zod 输入验证** — 健康数据录入使用 Zod schema 验证
### 版本
Taro 4.2 / React 18 / TypeScript / Zustand 5 / Sass / Zod / ECharts 6(按需引入)
> **依赖统一(2026-04-27):** 移除 `echarts-taro3-react`(内嵌 Taro 3.0.8 + React 16,导致 webpack 模块 ID 不一致 → `Cannot read property 'call' of undefined`)。改为自定义 `EcCanvas` 组件 + `echarts/core` 按需引入。`taro.js` chunk 从 152KB 降至 133KB。
### 1.1 设计系统:温润东方风
> 自 2026-04-27 起采用。所有新增/修改页面必须遵循此设计系统,保持视觉一致性。
>
> 设计系统源文件:`apps/miniprogram/src/styles/variables.scss` + `mixins.scss` + `tokens.scss`
**设计哲学**:Kenya Hara 式东方极简。温润米底 + 单一赤土橙贯穿全场,留白呼吸感优先。
#### 色彩
| 角色 | 变量名 | 色值 | 用途 |
|------|--------|------|------|
| 强调色 | `$pri` | `#C4623A` | 按钮、链接、活跃 tab、品牌色 |
| 强调浅 | `$pri-l` | `#F0DDD4` | 图标底色、高亮背景 |
| 强调深 | `$pri-d` | `#8B3E1F` | 渐变终点、暗色变体 |
| 背景 | `$bg` | `#F5F0EB` | 页面底色(warm cream) |
| 卡片 | `$card` | `#FFFFFF` | 白色卡片 |
| 辅助底 | `$surface-alt` | `#EDE8E2` | 次级表面、分隔 |
| 主文字 | `$tx` | `#2D2A26` | warm black,标题和正文 |
| 次文字 | `$tx2` | `#7A756E` | 辅助说明、标签 |
| 淡文字 | `$tx3` | `#A8A29E` | 占位、时间戳、箭头 |
| 边框 | `$bd` / `$bd-l` | `#E8E2DC` / `#F0EBE5` | 分隔线、卡片边框 |
| 成功 | `$acc` / `$acc-l` | `#5B7A5E` / `#E8F0E8` | sage green 正常状态 |
| 警告 | `$wrn` / `$wrn-l` | `#C4873A` / `#FFF3E0` | warm amber 偏高/待确认 |
| 危险 | `$dan` / `$dan-l` | `#B54A4A` / `#FDEAEA` | muted red 异常/删除 |
#### 字体
| 用途 | 字体栈 | 说明 |
|------|--------|------|
| 标题 / 数据数字 | `Georgia, Times New Roman, serif` | 衬线体,传递专业与温度。用 `@include serif-number` |
| 正文 / 辅助 | `-apple-system, PingFang SC, sans-serif` | 系统无衬线,清晰可读 |
| 章节标题 | `@include section-title` | 30rpx / bold / serif,统一样式 |
#### 圆角 / 阴影 / 间距
| 元素 | 圆角 | 阴影 |
|------|------|------|
| 卡片 | `$r` 12px | `$shadow-md` (0 2px 12px rgba(45,42,38,0.08)) |
| 小元素(标签、输入框) | `$r-sm` 8px | `$shadow-sm` (0 1px 4px rgba(45,42,38,0.04)) |
| 浮层、弹窗 | `$r-lg` 16px | `$shadow-lg` (0 8px 32px rgba(45,42,38,0.12)) |
| 水平间距 | 页面两侧 24rpx,卡片内 24-28rpx | |
| 垂直间距 | 区块间 24rpx,卡片内标题与内容 20rpx | |
#### 组件规范
**按钮**:主按钮 `$pri` 实色 + 白字 + 圆角 12px + 阴影。次按钮 `$pri` 边框透明底。禁用 `$surface-alt` 底 + `$tx3` 字。
**状态标签**:用 `@include tag(背景色, 文字色)` 生成。不用粗边框、不用 emoji 状态图标。颜色映射:正常 → `$acc`/`$acc-l`,偏高 → `$wrn`/`$wrn-l`,异常 → `$dan`/`$dan-l`,默认 → `$tx2`/`$bd-l`。
**健康数据卡片**:2×2 网格,衬线大数字居中,小标签 + 状态标签在底部。数据用 `@include serif-number` 等宽数字。
**列表项**:白色卡片容器,每行 14-24rpx padding,`$bd-l` 底部分隔。右侧箭头用 `›` 字符。无左侧彩色 border accent。
**图标**:禁止用 emoji 作图标。用线性 SVG 或首字衬线体作为图标占位。图标底色用 `$pri-l`。
**禁止事项**:紫色渐变 / emoji 作图标 / 圆角卡片+左彩色 border / 无意义渐变背景 / 装饰性 icon 遍地配。
#### Design Token 系统
> 自 2026-05-09 起全面接入。68 个 SCSS 文件(59 页面 + 9 组件)全部使用 `var(--tk-*)` 引用字号和结构值。
>
> 源文件:`apps/miniprogram/src/styles/tokens.scss`
**架构原理**:通过 CSS 自定义属性(Custom Properties)实现单一真相源。`page {}` 定义正常值,`.elder-mode {}` 覆写关怀值,所有页面通过 `var(--tk-*)` 引用,关怀模式自动级联生效,无需逐页维护覆写。
**Taro 兼容性**:Taro 编译器正确处理 CSS 自定义属性中的 px→rpx 转换(如 `--tk-font-h1: 26px` → `--tk-font-h1: 52rpx`)。
##### 字号 Token(10 级)
| Token | 正常值 | 关怀值 | 倍率 | 语义 | 覆盖场景 |
|-------|--------|--------|------|------|----------|
| `--tk-font-hero` | 48px | 56px | ×1.17 | 装饰图标、空状态字符 | 大型装饰元素 |
| `--tk-font-h1` | 26px | 30px | ×1.15 | 页面/区块标题 | 99 处 |
| `--tk-font-h2` | 24px | 28px | ×1.17 | 副标题、日期 | 86 处 |
| `--tk-font-body-lg` | 28px | 34px | ×1.21 | 大正文、按钮 | 110 处 |
| `--tk-font-body` | 22px | 30px | ×1.36 | 正文、标签 | 92 处 |
| `--tk-font-body-sm` | 16px | 22px | ×1.38 | 中等正文、列表项 | 26 处 |
| `--tk-font-num` | 30px | 34px | ×1.13 | 数值显示 | 56 处 |
| `--tk-font-num-lg` | 34px | 40px | ×1.18 | 大数值、统计 | 21 处 |
| `--tk-font-cap` | 13px | 18px | ×1.38 | 说明文字、时间戳 | 54 处 |
| `--tk-font-micro` | 11px | 17px | ×1.55 | 角标、标签 | 22 处 |
##### 结构 Token
| Token | 正常值 | 关怀值 | 语义 |
|-------|--------|--------|------|
| `--tk-line-height` | 1.5 | 1.7 | 行高 |
| `--tk-touch-min` | 48px | 56px | 最小触控区域 |
| `--tk-btn-primary-h` | 56px | 64px | 主按钮高度 |
| `--tk-text-secondary` | #78716C | #5A554F | 辅助文字颜色(关怀模式提升对比度) |
##### 使用规则
**新增页面必须使用 token**,禁止硬编码 `font-size: Npx`:
```scss
// 正确
.title { font-size: var(--tk-font-h1); }
.desc { font-size: var(--tk-font-cap); color: var(--tk-text-secondary); }
.number { font-size: var(--tk-font-num); @include serif-number; }
// 错误
.title { font-size: 26px; } // 禁止
```
**特殊值例外**(仅允许以下场景硬编码):
- `72px` — 积分余额大数字(装饰性,不走 token)
- `80px` — 错误状态图标(组件内部装饰)
- `21px` — 长辈模式预览页(特殊 UI)
##### 关怀模式(Elder Mode)
通过 Zustand store (`stores/ui.ts`) 的 `DisplayMode` 控制。`useElderClass()` hook 在根 View 添加 `.elder-mode` class,触发所有 token 的关怀值级联。
`elder-mode.scss`(~120 行)仅保留结构布局覆写(grid 列数、padding、gap),所有字号/颜色覆写已通过 token 级联自动处理。
## 2. 关键文件 + 数据流
### 核心文件
| 文件 | 职责 |
|------|------|
| `apps/miniprogram/config/index.ts` | Taro 构建配置(defineConstants 注入环境变量) |
| `apps/miniprogram/src/services/request.ts` | HTTP 请求封装(401 自动刷新、并发限制、缓存、AbortSignal 取消、`resetForTesting()` 测试隔离) |
| `apps/miniprogram/src/services/auth.ts` | 微信登录/绑定手机号 API |
| `apps/miniprogram/src/stores/auth.ts` | 认证状态(login/bindPhone/restore) |
| `apps/miniprogram/src/stores/index.ts` | `resetAllStores()` 统一清理(解耦 store 间依赖) |
| `apps/miniprogram/src/hooks/usePageData.ts` | **统一页面数据加载 hook**(节流 + 下拉刷新 + 防重入) |
| `apps/miniprogram/src/pages/index/useHomeData.ts` | 首页数据 hook(从 424 行页面组件中提取) |
| `apps/miniprogram/src/pages/health/useHealthData.ts` | 健康页数据 hook(从 422 行页面组件中提取) |
| `apps/miniprogram/src/utils/secure-storage.ts` | token 安全存储(明文存储,保留接口兼容) |
| `apps/miniprogram/project.config.json` | 微信开发者工具配置(AppID、urlCheck) |
### 微信登录流程
```
用户点击"微信一键登录"
↓
Taro.login() → 临时 code
↓
POST /auth/wechat/login { code }
↓
后端调用微信 jscode2session → 获取 openid + session_key
↓
查询 wechat_users 表
├── 已绑定 → 返回 { bound: true, token: JWT } → 跳转首页
└── 未绑定 → 返回 { bound: false, openid } → 显示"授权手机号"按钮
↓ 用户授权手机号
POST /auth/wechat/bind-phone { openid, encrypted_data, iv }
↓
后端解密手机号 → 创建/关联用户 → 返回 JWT → 跳转首页
```
### 页面结构(59 个页面,6 个主包 + 10 个子包)
#### TabBar 页面(3 个)
| 页面路径 | 说明 |
|----------|------|
| `pages/index/index` | 首页(今日健康、快捷服务) |
| `pages/health/index` | 健康上报(Tab 页) |
| `pages/messages/index` | 消息中心(Tab 页) |
#### 患者端主页面
| 页面路径 | 说明 |
|----------|------|
| `pages/login/index` | 登录页(微信登录 + 协议勾选) |
| `pages/consultation/index` | 咨询列表 |
| `pages/consultation/detail/index` | 咨询详情 |
| `pages/mall/index` | 积分商城 |
| `pages/profile/index` | 个人中心 |
| `pages/appointment/index` | 预约列表 |
| `pages/appointment/create/index` | 预约挂号 |
| `pages/appointment/detail/index` | 预约详情 |
| `pages/legal/user-agreement` | 用户服务协议 |
| `pages/legal/privacy-policy` | 隐私政策 |
#### 健康子包(pkg-health,4 个)
| 页面路径 | 说明 |
|----------|------|
| `pages/pkg-health/trend/index` | 健康趋势(体征数据折线图) |
| `pages/pkg-health/input/index` | 健康数据录入(Zod 验证) |
| `pages/pkg-health/daily-monitoring/index` | 日常监测数据 |
| `pages/pkg-health/alerts/index` | 告警列表 |
#### 医生端子包(doctor,16 个)
| 页面路径 | 说明 |
|----------|------|
| `pages/doctor/index` | 医护首页/工作台 |
| `pages/doctor/patients/index` | 患者列表 |
| `pages/doctor/patients/detail/index` | 患者详情 |
| `pages/doctor/consultation/index` | 咨询管理 |
| `pages/doctor/consultation/detail/index` | 咨询详情 |
| `pages/doctor/followup/index` | 随访管理 |
| `pages/doctor/followup/detail/index` | 随访详情 |
| `pages/doctor/report/index` | 报告管理 |
| `pages/doctor/report/detail/index` | 报告详情 |
| `pages/doctor/alerts/index` | 告警管理 |
| `pages/doctor/alerts/detail/index` | 告警详情 |
| `pages/doctor/action-inbox/index` | 待办事项 |
| `pages/doctor/dialysis/index` | 透析管理 |
| `pages/doctor/dialysis/detail/index` | 透析详情 |
| `pages/doctor/dialysis/create/index` | 创建透析记录 |
| `pages/doctor/prescription/index` | 处方管理 |
| `pages/doctor/prescription/detail/index` | 处方详情 |
| `pages/doctor/prescription/create/index` | 开具处方 |
#### 商城子包(pkg-mall,3 个)
| 页面路径 | 说明 |
|----------|------|
| `pages/pkg-mall/exchange/index` | 积分兑换 |
| `pages/pkg-mall/orders/index` | 积分订单 |
| `pages/pkg-mall/detail/index` | 商品详情 |
#### 个人中心子包(pkg-profile,12 个)
| 页面路径 | 说明 |
|----------|------|
| `pages/pkg-profile/family/index` | 家庭成员管理 |
| `pages/pkg-profile/family-add/index` | 添加家庭成员 |
| `pages/pkg-profile/reports/index` | 我的报告 |
| `pages/pkg-profile/followups/index` | 我的随访 |
| `pages/pkg-profile/medication/index` | 用药记录 |
| `pages/pkg-profile/settings/index` | 设置 |
| `pages/pkg-profile/dialysis-records/index` | 透析记录 |
| `pages/pkg-profile/dialysis-records/detail/index` | 透析记录详情 |
| `pages/pkg-profile/dialysis-prescriptions/index` | 透析处方 |
| `pages/pkg-profile/dialysis-prescriptions/detail/index` | 透析处方详情 |
| `pages/pkg-profile/consents/index` | 知情同意 |
| `pages/pkg-profile/health-records/index` | 健康档案 |
| `pages/pkg-profile/diagnoses/index` | 诊断记录 |
#### 其他子包(9 个)
| 页面路径 | 说明 |
|----------|------|
| `pages/ai-report/list/index` | AI 分析报告列表 |
| `pages/ai-report/detail/index` | AI 分析报告详情 |
| `pages/article/index` | 健康资讯列表 |
| `pages/article/detail/index` | 文章详情 |
| `pages/report/detail/index` | 健康报告详情 |
| `pages/followup/detail/index` | 随访详情 |
| `pages/events/index` | 线下活动 |
| `pages/device-sync/index` | 设备数据同步 |
### Hook 层(7 个)
| Hook | 用途 |
|------|------|
| `usePageData` | **统一页面数据加载**:`useDidShow` 节流 + `usePullDownRefresh` + `loadingRef` 防重入 + `enabled` 条件守卫。44/58 页面已接入 |
| `useThrottledDidShow` | 带节流的 `useDidShow`(已迁移页面不再直接使用,保留兼容) |
| `useSafeTimeout` | 页面隐藏时自动 clearTimeout |
| `usePageRefresh` | 下拉刷新封装 |
| `usePagination` | 通用分页逻辑 |
| `useAuthRequired` | 登录态检查 |
| `useElderClass` | 长者模式 CSS class |
| `useLongPolling` | **通用长轮询**:generation counter 防重叠 + useDidShow/Hide 可见性控制 + 失败退避(delay = min(failCount×2s, 30s))+ enabled 条件守卫。咨询详情页(患者+医生端)已接入 |
### 服务层(10+ 个文件)
| 文件 | 覆盖 |
|------|------|
| `request.ts` | HTTP 封装(401 刷新、错误处理) |
| `auth.ts` | 微信登录/绑定手机号 |
| `health.ts` | 体征数据/健康趋势 |
| `patient.ts` | 患者信息/家庭成员 |
| `appointment.ts` | 预约挂号/详情/取消 |
| `followup.ts` | 随访任务/详情 |
| `article.ts` | 健康文章 |
| `report.ts` | 健康报告 |
| `analytics.ts` | 数据分析 |
| `wechat-templates.ts` | 微信模板消息 ID |
### 组件(10 个)
| 组件 | 用途 |
|------|------|
| `EcCanvas` | ECharts Canvas 包装(Taro 4 兼容,按需引入 echarts/core) |
| `EmptyState` | 空状态占位 |
| `ErrorBoundary` | 错误边界捕获 |
| `ErrorState` | 错误状态展示 |
| `FamilyPicker` | 家庭成员选择器 |
| `HealthCard` | 健康数据卡片 |
| `Loading` | 加载状态 |
| `StepIndicator` | 步骤指示器 |
| `TrendChart` | 趋势图表(基于 EcCanvas) |
| `WeekCalendar` | 周日历组件 |
### 集成契约
| 方向 | 模块 | 接口 | 触发时机 |
|------|------|------|---------|
| 调用 → | [[erp-server]] | `POST /auth/wechat/login` | 微信登录 |
| 调用 → | [[erp-server]] | `POST /auth/wechat/bind-phone` | 手机号绑定 |
| 调用 → | [[erp-health]] | `/api/v1/health/*` | 健康数据查询 |
| 调用 → | [[erp-server]] | `/api/v1/auth/refresh` | Token 刷新 |
| 调用 → | [[erp-health]] | `GET /public/banners` | 访客首页轮播图(无需认证) |
| 调用 → | [[erp-health]] | `GET /public/banner-image/{id}` | 轮播图图片下载(`wx.downloadFile`,无需认证) |
| 调用 → | [[erp-health]] | `GET /public/articles` | 访客首页文章列表(无需认证) |
| 调用 → | [[erp-health]] | `GET /public/articles/{id}` | 访客文章详情(无需认证) |
## 3. 代码逻辑
### 环境变量注入(关键)
小程序运行时没有 `process.env`,必须在 `config/index.ts` 中通过 `defineConstants` 编译时替换:
```typescript
defineConstants: {
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
'process.env.TARO_APP_API_URL': JSON.stringify(process.env.TARO_APP_API_URL || 'http://localhost:3000/api/v1'),
'process.env.TARO_APP_ENCRYPTION_KEY': JSON.stringify(process.env.TARO_APP_ENCRYPTION_KEY || ''),
'process.env.TARO_APP_DEFAULT_TENANT_ID': JSON.stringify(process.env.TARO_APP_DEFAULT_TENANT_ID || ''),
},
```
### Token 安全存储
`secure-storage.ts` 使用 XOR 混淆 + Base64 编码存储 token:
- **不使用 `btoa`/`atob`** — 小程序环境不支持
- 使用 `Taro.arrayBufferToBase64` / `Taro.base64ToArrayBuffer` 替代
- 加密密钥通过 `TARO_APP_ENCRYPTION_KEY` 环境变量注入
### 请求封装(request.ts)
- 401 自动尝试 refresh token,失败后跳转登录页
- 错误响应格式: `{ error: string, message: string }`(无 `success` 字段)
- 成功响应格式: `{ success: true, data: T }`
- 当前 URL 拼接方式构建查询参数(待重构为 params 对象)
### 健康数据录入验证
使用 Zod schema 验证用户输入的体征数据(血压、心率、体重、血糖),确保数据类型和范围合法。
## 4. 构建与调试
### 构建
```bash
cd apps/miniprogram && pnpm build:weapp # 生产构建
cd apps/miniprogram && npx taro build --type weapp # 等效
```
### 微信开发者工具配置
| 配置项 | 值 | 说明 |
|--------|-----|------|
| AppID | `wx20f4ef9cc2ec66c5` | 真实微信 AppID |
| urlCheck | `false` | 不校验合法域名(开发模式) |
| miniprogramRoot | `dist/` | 编译输出目录 |
### 后端微信配置
`crates/erp-server/config/default.toml`:
```toml
[wechat]
appid = "wx20f4ef9cc2ec66c5"
secret = "<通过环境变量 ERP__WECHAT__SECRET 设置>"
```
## 5. 活跃问题 + 陷阱
### 历史教训
| 问题 | 根因 | 修复 |
|------|------|------|
| 页面空白 `ReferenceError: process is not defined` | `process.env` 在运行时不存在 | `defineConstants` 编译时替换 |
| 登录成功但前端报失败 `btoa is not defined` | `secure-storage.ts` 使用 Web API | 改用 `Taro.arrayBufferToBase64` |
| 微信登录 500 `wechat_users.created_by 不存在` | 迁移创建的表缺少标准字段 | `ALTER TABLE` 补列 |
| 401 循环重定向 | 首页未登录时 `request.ts` 反复 redirectTo | 检查当前路径避免重复跳转 |
| `Cannot read property 'call' of undefined` | `echarts-taro3-react` 内嵌 Taro 3.0.8 + React 16,与 Taro 4.2 + React 18 产生双实例,webpack 模块 ID 不匹配 | 移除 `echarts-taro3-react`,改为自定义 `EcCanvas` + `echarts/core` 按需引入 |
### 待优化
| 问题 | 级别 | 说明 |
|------|------|------|
| ~~Auth token 日志输出~~ | ~~P0~~ 已解决 | terser `drop_console` 移除生产日志 |
| ~~Token 刷新竞态~~ | ~~P0~~ 已解决 | `refreshPromise` 单例 + GET 请求去重 |
| ~~ECharts 全量引入~~ | ~~P2~~ 已解决 | 分包后 echarts 514KB 仅在趋势页按需加载 |
| 积分商城降级 UI | P0 | 未关联患者档案时 Tab 页空白,需引导用户建档 |
| daily-monitoring 无 Zod 验证 | P1 | 对齐 health/input 的验证标准 |
| 文章列表返回草稿 | P1 | 患者端应只展示 `published` 状态文章 |
| URL 拼接构建查询参数 | P2 | `request.ts` 已支持 `buildQuery(params)` 但内部使用 |
| 生产配置 | P2 | `urlCheck`/`minified` 需区分环境 |
### 2026-04-30 审计发现
**三端对齐差距**(小程序 API 覆盖 76 个,Web 235 个,后端 328 个):
| 发现 | 严重性 | 说明 |
|------|--------|------|
| ~~晚间血压数据永久丢失~~ | ~~CRITICAL~~ **已修复** | 新增 `blood_pressure_evening` indicator_type,小程序录入页 + 日常监测页 + 后端 service + 集成测试均已覆盖 |
| ~~透析管理完全无入口~~ | ~~HIGH~~ **已修复** | 医生端新增 dialysis 3 页面 + prescription 3 页面,患者端新增 dialysis-records 2 页面 + dialysis-prescriptions 2 页面 |
| ~~知情同意完全无入口~~ | ~~HIGH~~ **已修复** | 患者端新增 `pkg-profile/consents` 页面 |
| 体温/血氧未映射 | MEDIUM | `body_temperature`/`spo2` 无 indicator_type |
| ~~健康记录小程序无入口~~ | ~~MEDIUM~~ **已修复** | 患者端新增 `pkg-profile/health-records` 页面 |
| ~~诊断记录小程序无入口~~ | ~~MEDIUM~~ **已修复** | 患者端新增 `pkg-profile/diagnoses` 页面 |
| 小程序无自动化测试 | HIGH | 已建立 MCP 自动化审计流程,4 角色 236 次探测通过率 96.2% |
**功能域完成度**(小程序端):
| 功能域 | 完成度 | 关键差距 |
|--------|--------|---------|
| 咨询管理 | 95% | 无导出(预期) |
| 预约管理 | 90% | 基本对齐 |
| 随访管理 | 70% | 仅列表+创建 |
| 患者管理 | 85% | 无删除(预期) |
| 健康数据 | 80% | ~~丢失晚间血压~~ 已修复 |
| 告警系统 | 80% | 查看+处理+医护端管理 |
| 透析管理 | 70% | 医护端管理+患者端查看记录/处方 |
| 知情同意 | 60% | 患者端查看,无签署流程 |
| 健康档案 | 60% | 患者端可查看健康档案和诊断 |
| 统计仪表盘 | 30% | 仅医护端 3 个统计 |
| AI 分析 | 30% | 仅历史查看 |
### 注意事项
- `Taro.login()` 的 code 一次性使用,每次调用会返回新 code
- `session_key` 缓存 5 分钟(TTL),过期需重新登录
- 微信开发者工具中 `getPhoneNumber` 需要真机调试或使用测试号
- Redis 不可达时限流降级为 fail-open,不影响登录
### 2026-05-14 全页面性能与稳定性审查(5 专家组 × 58 页面)
> 组织 5 个并行专家组,逐一审查全部 58 个页面 + stores/hooks/utils/components/services 基础设施层。
> 审查重点:开发者工具卡死、CPU 飙升、逻辑链路异常、内存泄漏。
#### 发现汇总
| 级别 | 数量 | 说明 |
|------|------|------|
| CRITICAL | 3 | 长轮询紧密递归、BLE 模块单例、TrendChart 同步 API |
| HIGH | 8 | 原生 HTML input、客户端过滤、Storage 渲染路径、messagesRef 不同步等 |
| MEDIUM | 15+ | 缺少 useThrottledDidShow、IIFE 渲染、loading 竞态等 |
#### CRITICAL 级别(已修复)
| # | 问题 | 文件 | 修复 |
|---|------|------|------|
| 1 | **长轮询 delay=0 紧密递归**:成功轮询后无间隔立即递归,后端快速响应时构成紧密循环,CPU 飙升 | `consultation/detail`(患者+医生端) | 成功路径加 3s 间隔 + 连续失败上限 50 次 |
| 2 | **BLE 模块级单例**:`BLEManager` 在模块顶层实例化,生命周期不与页面绑定;`liveReadings` 无上限增长 | `device-sync/index.tsx` | 改为 `useRef` 懒初始化 + `MAX_LIVE_READINGS=200` 上限 |
| 3 | **TrendChart 模块级同步 API**:`Taro.getSystemInfoSync()` 在模块加载时执行,阻塞首帧 | `components/TrendChart` | 改为延迟求值 `getDPR()` 函数 |
#### HIGH 级别(已修复)
| # | 问题 | 文件 | 修复 |
|---|------|------|------|
| 1 | **原生 HTML ``**:Taro 不支持原生 HTML 标签,日期选择器完全不可用 | `doctor/followup/detail` | 替换为 `` |
| 2 | **告警详情用列表+客户端过滤**:加载 100 条列表找单条告警,超过 100 条永远找不到 | `doctor/alerts/detail` | 新增 `getAlert(id)` API 函数,单条查询 |
| 3 | **透析列表客户端过滤**:服务端返回全状态数据,前端用 `activeTab` 过滤导致分页不准确 | `doctor/dialysis/index` | 传 `status` 参数给服务端,移除客户端过滤 |
| 4 | **`getStorageSync` 在渲染路径**:组件顶层调用同步 Storage 读取,每次渲染都执行 IPC | `report/detail` | 改用 `useAuthStore((s) => s.currentPatient)` |
| 5 | **`handleSend` 后 `messagesRef` 未同步**:发送消息后 state 更新但 ref 未更新,长轮询可能重复拉取 | `consultation/detail`(患者+医生端) | 在 `setMessages` 回调中同步更新 ref |
| 6 | **医护工作台无自动刷新**:返回工作台时数据不更新 | `doctor/index` | 添加 `useThrottledDidShow` 10s 节流刷新 |
#### MEDIUM 级别(已知,按需修复)
| 问题 | 文件 | 说明 |
|------|------|------|
| `ai-report/list` 缺少 `useThrottledDidShow` | `ai-report/list` | 只在挂载时加载,从详情页返回不刷新 |
| `events/list` 缺少分页 | `events/index` | 固定加载 50 条,无滚动加载更多 |
| `device-sync` tryAutoSync 无并发保护 | `device-sync` | 快速进出页面可能重复上传 |
| `health/index` loadTrend 无并发保护 | `health/index` | **已修复:** 添加 `loadingRef` 防重入 |
| `doctor/prescription` handleSearch loading 竞态 | `doctor/prescription` | handleSearch 和 useEffect 的 loadData 可能闪烁 |
#### 架构建议(已完成标注 ✅)
1. ~~**统一数据加载模式**~~ ✅ 已完成:所有列表/详情页使用 `usePageData` hook,44/58 页面已接入
2. ~~**长轮询通用化**~~ ✅ 已完成:抽取 `useLongPolling` hook(generation counter + useDidShow/Hide + 失败退避),患者端+医生端 consultation/detail 已接入
3. ~~**服务端过滤优先**~~ ✅ 已完成:所有列表页的 Tab 过滤传参给后端
4. ~~**BLE 管理器生命周期**~~ ✅ 已完成:改为 `useRef` 懒初始化
5. ~~**getStorageSync 出渲染路径**~~ ✅ 已完成:通过 Zustand store 获取
### 2026-05-15 患者端登录后卡死深度审查(3 专家组 × 请求链路 + 并发分析 + 端点可达性)
> 游客端卡死修复后,患者登录后又频繁出现开发者工具卡死。组织 3 个并行专家组深度审查。
> **根因:全局并发请求超过微信 10 个限制**,快速 Tab 切换时首页 5 个 + 健康页 4 个 + analytics 1 个 + 个人中心 2 个 = 12 个并发请求,超限排队阻塞 UI。
#### 审查发现
| 级别 | 问题 | 说明 |
|------|------|------|
| **根因** | **请求并发超微信 10 限制** | 快速 Tab 切换产生 12+ 并发请求,超出排队;慢请求占位导致后续全部阻塞 |
| HIGH | doRefresh 失败未设 isLoggingOut | Storage 清了但标志未设,401 路径可能重入 |
| HIGH | 401 失败后 reLaunch 首页而非登录页 | 首页 restoreAuth 又发请求,可能循环 |
| MEDIUM | 长轮询 useDidShow 重启时新旧循环重叠 | pollingRef 设 true 时旧 setTimeout 回调也会继续 |
| MEDIUM | 首页 loadReminders 无并发保护 | useThrottledDidShow + pullDownRefresh 可同时触发 |
| MEDIUM | health store refreshToday 无防重入 | 两个页面同时调用会发两次请求 |
| MEDIUM | 健康页 loadTrend/loadAiSuggestions 无防重入 | 下拉刷新 + useThrottledDidShow 双倍请求 |
#### 端点可达性验证
**所有 33 个小程序 service 端点后端均已实现**,无 ghost endpoint(特别确认 `/analytics/batch`、`/messages/unread-count`、`/ai/suggestions` 均存在)。
#### Tab 切换请求链路分析
每次 Tab 切换触发两层回调:
| 层级 | 操作 | 开销 |
|------|------|------|
| App 级 `useDidShow` | `restoreAuth()` + `restoreUI()` | 4-6 次同步 Storage IPC,约 10-50ms |
| 页面级 `useThrottledDidShow` | 页面数据加载 | 1-5 个异步 API 请求,timeout 15s |
**最坏情况(所有缓存过期):**
| 页面 | 并发请求数 |
|------|-----------|
| 首页 HomeDashboard | 5(refreshToday + 3 个 loadReminders + getUnreadCount) |
| 健康页 | 4(refreshToday + getTrend + listPendingSuggestions + getHealthThresholds) |
| 消息页 | 1(listConsultations 或 listNotifications) |
| 个人中心 | 2(getAccount + getCheckinStatus) |
| analytics flushEvents | 1(POST /analytics/batch,每 30s) |
| **合计** | **13 个** — 超限 3 个 |
#### 修复清单(6 项,全部已修复)
| # | 级别 | 文件 | 修复 |
|---|------|------|------|
| 1 | **HIGH** | `services/request.ts` | `doRefresh` 失败后设 `isLoggingOut=true` + 清理所有 Storage(含 `current_patient`)+ 清请求缓存 + 重置 headers 缓存 |
| 2 | **HIGH** | `services/request.ts` | 401 失败后跳 `/pages/login/index`(而非首页);重试成功后重置 `isLoggingOut` |
| 3 | **HIGH** | `services/request.ts` | **新增全局并发限制 `MAX_CONCURRENT=8`**:acquireSlot/releaseSlot 队列机制,防止超过微信 10 个请求限制;用 try/finally 确保所有路径释放槽位 |
| 4 | MEDIUM | `pages/consultation/detail` + `doctor/consultation/detail` | 长轮询改为 **generation counter**,`startLongPolling` 递增 generation,旧循环检测到 generation 变化自动退出,杜绝新旧循环重叠 |
| 5 | MEDIUM | `pages/index/index.tsx` | 首页 `loadReminders` 添加 `remindersLoadingRef` 防重入 |
| 6 | MEDIUM | `pages/health/index.tsx` + `stores/health.ts` | 健康页整体 `loadingRef` 防重入;health store `refreshToday` 添加全局 `refreshingToday` 去重 |
#### 并发限制器原理
```typescript
// request.ts — acquireSlot/releaseSlot 队列
const MAX_CONCURRENT = 8; // 留 2 个空位给系统请求
let activeRequests = 0;
const pendingQueue: Array<() => void> = [];
function acquireSlot(): Promise {
if (activeRequests < MAX_CONCURRENT) {
activeRequests++;
return Promise.resolve();
}
return new Promise((resolve) => { pendingQueue.push(resolve); });
}
function releaseSlot(): void {
activeRequests--;
const next = pendingQueue.shift();
if (next) { activeRequests++; next(); }
}
```
所有页面请求共享同一队列,保证同时最多 8 个网络请求在飞。超出自动排队,先到先得。
## 6. MCP 联调(微信开发者工具自动化)
> 通过 MCP (Model Context Protocol) 工具直接操控微信开发者工具中的小程序模拟器,实现页面导航、元素交互、数据读取等操作。
>
> **当前使用自建 MCP 服务器** `@hms/weapp-mcp`(`tools/weapp-mcp/`),基于 `@weapp-vite/miniprogram-automator@1.1.0`(社区活跃维护 fork)。
> 原有的 `@yfme/weapp-dev-mcp` 在 DevTools 2.01.2510290 版本后不兼容,已弃用。
### 6.1 前置条件
1. **后端运行** — `cargo run` 启动 `http://localhost:3000`
2. **小程序以 dev 模式构建** — 必须使用 `NODE_ENV=development` 构建(详见 §6.4)
```bash
cd apps/miniprogram
echo 'TARO_APP_API_URL=http://localhost:3000/api/v1
TARO_APP_DEFAULT_TENANT_ID=019d80da-7a2c-7820-b0a3-3d5266a3a324
TARO_APP_ENCRYPTION_KEY=' > .env
NODE_ENV=development npx taro build --type weapp
```
3. **微信开发者工具已登录** — 确保已扫码登录(CLI 命令需要登录态)
4. **微信开发者工具打开项目** — 手动打开,加载 `apps/miniprogram/dist/`
5. **自动化端口已开启** — `project.config.json` 中 `"automationAudits": true`
> ⚠️ **为什么必须 dev 模式?** 生产构建(`pnpm build:weapp`)设置 `NODE_ENV=production`,`secure-storage.ts` 的 `decrypt()` 在密钥为空时会抛异常。dev 模式允许空密钥走明文存储。
### 6.2 启动调试环境
> **铁律:只允许一个 DevTools 实例运行。** 多实例会导致 App 级别自动化命令全部超时(无法恢复,必须全部关闭重开)。
#### 方式 A:Launcher.launch() 自动启动(推荐)
通过 `@weapp-vite/miniprogram-automator` 的 `Launcher` 自动启动 DevTools 并开启自动化端口,无需手动操作。
```bash
# 步骤 1:确认没有残留的 DevTools 进程
tasklist | findstr wechatdevtools
# 如有残留:taskkill /F /IM wechatdevtools.exe
# 步骤 2:MCP 的 inject_auth 或 connect 工具会自动调用 Launcher.launch()
# 或者手动测试:
cd tools/weapp-mcp
node --input-type=module -e "
import { Launcher } from '@weapp-vite/miniprogram-automator';
const mp = await new Launcher().launch({
cliPath: 'D:/微信web开发者工具/cli.bat',
projectPath: 'G:/hms/apps/miniprogram/dist',
});
console.log('OK:', (await mp.systemInfo()).model);
// 不要 disconnect,保持连接供 MCP 使用
"
# 自动在端口 9420 开启自动化
```
#### 方式 B:手动启动 + CLI 开端口
```bash
# 步骤 1:确认没有残留的 DevTools 进程
taskkill /F /IM wechatdevtools.exe
# 步骤 2:手动打开微信开发者工具,加载 apps/miniprogram/dist/
# 等待模拟器完全加载,确认无报错
# 步骤 3:通过 CLI 开启自动化端口(端口 9420)
"D:/微信web开发者工具/cli.bat" auto --project G:/hms/apps/miniprogram/dist --auto-port 9420
# 看到 "✔ auto" 即成功
# 步骤 4:验证端口已开启
netstat -ano | findstr 9420
# 应看到 LISTENING 状态
```
成功后可通过 `mcp__weapp-local__connect` 连接。
### 6.3 常用 MCP 操作(weapp-local 工具集)
| 操作 | MCP 工具 | 说明 |
|------|----------|------|
| 连接 | `connect` | 连接自动化端口,返回系统信息 |
| 断开 | `disconnect` | 断开连接 |
| 查看当前页面 | `current_page` | 返回路径、尺寸,可选包含 data |
| 导航到页面 | `navigate` | `navigateTo` / `switchTab` / `reLaunch` / `redirectTo` / `navigateBack` |
| 查找元素 | `get_element` | CSS 选择器,支持 `[index=N]` 语法 |
| 查找多个元素 | `get_elements` | CSS 选择器数组 |
| 点击元素 | `tap` | CSS 选择器定位后点击 |
| 输入文本 | `input` | CSS 选择器 + 值 |
| 滚动 | `scroll` | scroll-view 的 CSS 选择器 + x/y 坐标 |
| 读取页面数据 | `page_data` | 获取当前页面 data 对象(可选路径) |
| 设置页面数据 | `page_data` | JSON data 参数 → setData |
| 读取组件数据 | `element_data` | 组件 CSS 选择器 + 可选路径 |
| 设置组件数据 | `element_data` | CSS 选择器 + JSON data → setData |
| 调用页面方法 | `page_call_method` | 调用当前页面上暴露的方法 |
| 执行 JS | `evaluate` | 在 AppService 上下文执行 JavaScript |
| 调用 wx API | `call_wx` | 如 `getStorageSync`、`setStorageSync` |
| Mock wx API | `mock_wx` | Mock 一个 wx API 返回指定结果 |
| 恢复 wx API | `restore_wx` | 恢复被 mock 的 wx API |
| 获取 WXML | `get_wxml` | 获取元素的 WXML 结构 |
| 截图 | `screenshot` | ⚠️ 当前版本超时,见 §6.6 |
| 日志 | `get_logs` | 获取 console 日志和异常 |
| 等待元素 | `wait_for` | 轮询等待选择器匹配 |
| 注入认证 | `inject_auth` | 一键获取后端 token + 注入 storage + 跳转首页 |
| 审计页面 | `audit_page` | 导航 → 检查加载 → 收集错误 → 健康报告 |
| 批量审计 | `audit_all` | 批量审计所有小程序页面 |
### 6.4 绕过微信登录
MCP 无法模拟微信 OAuth(`Taro.login()` 返回的 code 走真实微信 `jscode2session` 接口,DevTools 模拟器会返回 mock code 导致后端 500)。
#### 方案一:inject_auth 一键注入(推荐)
**原理:** 以 dev 模式重编译(空加密密钥),通过 MCP 的 `inject_auth` 工具自动完成:获取 admin token → evaluate 注入 storage → reLaunch 首页。
```
1. 准备:确保已按 §6.1 以 dev 模式构建,且已连接 MCP(connect)
2. 调用 inject_auth 工具(默认参数即可):
- 自动 POST /auth/login 获取 admin token
- 通过 evaluate() 注入 access_token / refresh_token / user_data / tenant_id / patient_id
- reLaunch 到 /pages/index/index
- 等待 2 秒后返回当前页面路径
3. 成功后即可正常操作各页面
```
**关键 ID(默认值):**
- `tenant_id`: `019d80da-7a2c-7820-b0a3-3d5266a3a324`
- `patient_id`: `019dcd34-bc4d-72c1-8c19-77ce1f4839d6`
#### 方案二:手动分步注入
```bash
# 1. 获取 admin token
curl -X POST http://localhost:3000/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"Admin@2026"}'
# 2. 通过 MCP call_wx 逐个写入 storage
# 3. 通过 MCP navigate reLaunch 到首页
```
> ⚠️ 长字符串 token 通过 `call_wx` 传输可能截断。建议用 `inject_auth` 工具(内部用 evaluate 直接执行,无传输问题)。
### 6.5 TabBar 页面列表
以下页面是 TabBar 页面,必须用 `switchTab` 导航(不能用 `navigateTo`):
- `pages/index/index` — 首页
- `pages/health/index` — 健康数据
- `pages/messages/index` — 消息中心
> 注意:咨询、商城、个人中心虽然显示为 Tab 样式,但实际不是 TabBar 页面(使用 `reLaunch` 导航)。
### 6.6 已知限制
| 问题 | 原因 | 替代方案 |
|------|------|----------|
| **screenshot 超时** | `App.captureScreenshot` 在 DevTools 2.01.2510290 不响应 | 用 `get_element` + `page_data` 获取 UI 状态 |
| **多实例导致全挂** | 两个 DevTools 实例抢端口,App 域命令全部超时且不可恢复 | `taskkill /F /IM wechatdevtools.exe` 后重开 |
| **navigateTo 超出 10 页栈** | 小程序 webview 限制最多 10 层页面栈 | 批量测试用 `reLaunch` 逐页导航 |
| **CLI 需要登录** | `cli.bat auto` 要求 DevTools 已扫码登录 | 在 DevTools 中先扫码登录 |
| **SummerCompiler 报错** | `project.config.json` 缺少必要字段 | 已配置 `packNpmManually`/`packNpmRelationList`/`ignoreUploadUnusedFiles`/`condition` |
| **require() 在 evaluate 不可用** | webpack 用数字 ID 注册模块 | 直接用 `wx.setStorageSync` / `wx.getStorageSync` |
| **auth 重定向** | request interceptor 检测 401 后跳转 login 并清空 storage | 确保 token 有效,reLaunch 后等待 2-3 秒 |
| **生产构建 decrypt 抛异常** | 空密钥 + `NODE_ENV=production` 时 `decrypt()` 直接 throw | 使用 `NODE_ENV=development` 构建 |
| **DevTools 频繁卡死** | Electron 多进程累积文件描述符泄漏,14+ 进程 3.5GB+ 内存 | 见下方 §6.9 DevTools 性能优化 |
### 6.9 DevTools 性能优化
微信开发者工具基于 Electron,多进程架构(主进程 + 渲染进程 + GPU + 插件)在长时间运行后会出现 `EMFILE: too many open files` 导致卡死。
**常见触发场景:**
- 频繁 MCP 调用(evaluate/reLaunch)导致 fd 累积
- Taro 热重载文件监视器与 DevTools 自带监视器重叠
- 长时间(>2 小时)不重启 DevTools
**优化措施:**
```powershell
# 1. 定期清理 DevTools 进程(推荐每 30 分钟或卡顿时执行)
taskkill /F /IM wechatdevtools.exe
# 然后重新打开项目
# 2. 检查进程资源占用
Get-Process wechatdevtools | Measure-Object WorkingSet64 -Sum | Select Count, @{N='MB';E={[math]::Round($_.Sum/1MB)}}
# 正常:< 1500MB | 需要重启:> 3000MB
# 3. 关闭不必要的 DevTools 功能
# 设置 → 编辑器设置 → 取消勾选「文件保存时自动编译」
# 设置 → 代理 → 关闭(不用代理时)
```
**MCP 联调最佳实践:**
- 每轮 MCP 测试结束后 `disconnect` 断开连接
- 批量测试分批执行,每批 ≤ 10 页后重启 DevTools
- 避免在 DevTools 编辑器中同时打开多个文件
### 6.7 MCP 服务器架构
```
tools/weapp-mcp/
├── src/
│ ├── index.ts # MCP 服务器入口,25 个工具注册
│ └── automator.ts # 连接管理器(@weapp-vite/miniprogram-automator)
├── package.json # @hms/weapp-mcp, 依赖 @weapp-vite/miniprogram-automator@1.1.0
└── tsconfig.json
```
`.mcp.json` 配置:
```json
{
"weapp-local": {
"command": "node",
"args": ["tools/weapp-mcp/dist/index.js"],
"env": {
"WEAPP_WS_ENDPOINT": "ws://localhost:9420",
"WEAPP_CLI_PATH": "D:/微信web开发者工具/cli.bat",
"WEAPP_PROJECT_PATH": "G:/hms/apps/miniprogram/dist",
"HMS_API_URL": "http://localhost:3000/api/v1"
}
}
}
```
修改后需 `cd tools/weapp-mcp && npm run build` 重新编译。
### 6.8 自动化审计脚本
| 脚本 | 用途 | 运行 |
|------|------|------|
| `tools/weapp-mcp/scripts/audit-pages.mjs` | **多角色分批审计**(推荐) | `node scripts/audit-pages.mjs --role admin` |
| `apps/miniprogram/inject-auth.cjs` | 注入明文 token(dev 模式) | `node inject-auth.cjs` |
| `apps/miniprogram/e2e-final.cjs` | 完整 E2E 链路验证(11 UI + 10 API) | `node e2e-final.cjs` |
#### 分批审计脚本用法
```bash
# 在 tools/weapp-mcp/ 目录下运行
cd tools/weapp-mcp
# 审计指定角色(admin/doctor/nurse/operator)
node scripts/audit-pages.mjs --role admin
# 自定义批次大小(默认 10 页/批,每批重启 DevTools 防止多实例超时)
node scripts/audit-pages.mjs --role doctor --batch-size 8
# 输出:
# - 控制台实时显示每页状态(OK / LOGIN_REDIRECT / ERROR)
# - JSON 报告保存到 docs/qa/role-test-results/MP-{role}-audit.json
```
**设计要点:**
- 每批 N 页后自动 `taskkill` 所有 DevTools 进程并重新 `Launcher.launch()`,避免多实例导致 App 级命令超时
- 通过 `mp.evaluate()` 注入 storage(`wx.setStorageSync`),写入 `access_token`/`refresh_token`/`user_data`/`user_roles`/`tenant_id`/`current_patient_id`
- 测试用户密码均为 `Admin@2026`(不是 `Test@2026`)
> ⚠️ 旧脚本 `audit-pages.cjs` / `audit-detail-pages.cjs` 基于 `miniprogram-automator`(已弃用),请使用新的 `audit-pages.mjs`。
### 6.9 审计结果
#### 2026-05-13 T40 UI 设计系统合规审计(60 页面)
基于 `docs/qa/T40-miniprogram-ui-audit-plan.md` 对全部 60 个页面进行设计系统合规审计,覆盖 Design Token、SCSS 变量、色彩/圆角/字号/组件规范。
**审计结果汇总:**
| 级别 | 页面数 | 占比 |
|------|--------|------|
| PASS | 31 | 52% |
| PASS_WITH_ISSUES | 27 | 45% |
| NEEDS_WORK | 2 | 3% |
**发现并修复的问题(全部已修复):**
| 类别 | 数量 | 修复内容 |
|------|------|----------|
| HIGH | 2 | 趋势页缺 Loading/EmptyState;文章列表缺 mixins 导入 |
| MEDIUM | 6 | 硬编码字号(72px→`--tk-font-display`);ErrorState 图标字号;AI报告离调色板颜色(#7c3aed/#f0e6ff→`$pri`/`$pri-l`);医生患者列表 inline style;咨询页 GuestGuard 统一;3处 TSX inline 颜色提取为 SCSS 类 |
| LOW — `#fff` 统一 | 44 | 新增 `$white` 变量,所有 `color: #fff` → `$white`,`background: #fff` → `$card` |
| LOW — 圆角统一 | 14 | `8px` → `$r-xs`(7文件);`20px` → `$r-lg`(2文件);`16px` → `$r`(1文件) |
| LOW — 静默 catch | 2 | article/health 的空 catch 块添加状态清理 |
| LOW — ErrorBoundary 重构 | 1 | 6 个 inline style 硬编码提取为 SCSS 类 + Design Token |
| LOW — 离调色板颜色 | 2 | `#0284C7`(冷蓝) → `$tx2`;`#94A3B8`(冷灰) → `$tx3` |
| LOW — `#FFFFFF` 统一 | 4 | index/exchange/mixins/variables 中 `#FFFFFF` → `$white` |
**设计系统新增:**
- `variables.scss`: 新增 `$white: #FFFFFF` 语义变量
- `tokens.scss`: 新增 `--tk-font-display` Token(72px/80px,大数字装饰用)
- `ErrorBoundary`: 新增 SCSS 文件,从 inline style 迁移到设计系统
**影响文件:** 25 个 SCSS + 10 个 TSX + 1 个新增 SCSS + 2 个样式系统文件
报告文件:`docs/qa/role-test-results/T40-ui-audit-results.md`
#### 2026-05-08 多角色自动化审计(59 页面 × 4 角色)
使用分批审计脚本对全部 59 个页面进行 4 角色全面审计(236 次页面探测):
| 角色 | OK | LOGIN_REDIRECT | ERROR | 通过率 |
|------|-----|----------------|-------|--------|
| admin(患者) | 57 | 0 | 2 | **96.6%** |
| doctor(医生) | 58 | 1 | 0 | **98.3%** |
| nurse(护士) | 57 | 0 | 2 | **96.6%** |
| operator(运营) | 55 | 0 | 4 | **93.2%** |
| **合计** | **227** | **1** | **8** | **96.2%** |
**关键结论:**
- 8 个 ERROR 全部是 DevTools 模拟器随机 timeout,非产品缺陷
- 1 个 LOGIN_REDIRECT 是 doctor 首页首次加载时序问题
- 所有 59 个页面在所有角色下均可正常渲染(排除测试工具因素)
- 测试用户密码均为 `Admin@2026`(之前误用 `Test@2026` 导致大量 LOGIN_REDIRECT)
报告文件:`docs/qa/role-test-results/MP-{role}-audit.json`
#### 2026-04-27 初次审计(40 页面,单角色)
通过 MCP 自动化工具对全部 40 个页面进行渲染审计:
| 类别 | 数量 | 结果 |
|------|------|------|
| TabBar 页面 | 5 | 5/5 OK |
| 患者端子页面 | 24 | 24/24 OK |
| 详情页(假 ID) | 11 | 11/11 优雅降级 |
| **合计** | **40** | **40/40 全部通过** |
详细审计报告见 `docs/discussions/2026-04-27-miniprogram-audit-report.md`。
## 7. 变更记录
| 日期 | 变更 |
|------|------|
| 2026-05-15 | **架构重构 P4:分包策略优化**:合并 4 个单页分包(report→pkg-profile/reports、followup→pkg-profile/followups、events→pkg-profile/events、device-sync→pkg-health);consultation/detail 移出主包到 pkg-consultation 分包;doctor 18 页拆分为 pkg-doctor-core(8 页:工作台+患者+咨询+随访)+ pkg-doctor-clinical(10 页:透析+处方+报告+告警);分包 10→8 个,主包页面 13→12 |
| 2026-05-15 | **架构重构 P3:长轮询通用化 useLongPolling**:抽取 `useLongPolling` hook(generation counter + useDidShow/Hide 可见性 + 失败退避 + enabled 守卫);患者端 + 医生端 consultation/detail 接入,删除 ~80 行重复代码;架构建议 #2 全部完成 ✅ |
| 2026-05-15 | **架构重构 P2:request.ts 模块级状态收编 + AbortSignal + Analytics 受控**:提取 `ConcurrencyLimiter` 类(并发限制)、`ResponseCache` 类(缓存+去重+patientId 绑定);新增 `resetForTesting()` 测试隔离函数;`api.get/post/put/delete` 支持 `AbortSignal` 请求取消;app.tsx Analytics 定时器改为 `useDidShow`/`useDidHide` 控制后台暂停;构建通过 + 测试 74/75 |
| 2026-05-15 | **患者端登录后卡死深度审查(3 专家组)**:根因 — 全局并发请求超微信 10 限制排队阻塞;端点可达性验证 33/33 全部存在;Tab 切换请求链路分析(最坏 13 并发);修复 HIGH×3(doRefresh 状态清理 + 401 跳转登录页 + 全局并发限制 MAX_CONCURRENT=8)+ MEDIUM×3(长轮询 generation counter + 首页/健康页 loadingRef 防重入 + refreshToday 去重) |
| 2026-05-15 | **全量审计修复(第二轮)**:修复 CRITICAL×1(pollingRef 未定义回归,咨询详情页 loadData 引用已删除的 pollingRef → 闭会话时 ReferenceError 崩溃);HIGH×3 — 401 重试递归占用双 slot 改为循环结构释放后重入、4 个医生端列表页(consultation/alerts/dialysis/prescription)添加 loadingRef 防重入、safeNavigateTo 页栈溢出保护(栈≥9 自动 redirectTo);新增 `safeNavigateTo` 工具函数(`utils/navigate.ts`) |
| 2026-05-15 | **setTimeout 无清理修复**:新增 `useSafeTimeout` hook(页面隐藏时自动 clearTimeout);10 个页面接入 — daily-monitoring(2)、exchange(4)、family-add、health/input、prescription detail/create、dialysis detail/create、appointment detail/create;所有 fire-and-forget 定时器替换为 safeSetTimeout |
| 2026-05-14 | **全页面性能与稳定性审查(5 专家组)**:审查 58 页面 + 基础设施层;修复 CRITICAL×3(长轮询紧密递归、BLE 模块单例、TrendChart 同步 API)+ HIGH×6(原生 HTML input、客户端过滤→服务端、Storage 渲染路径、messagesRef 同步、工作台刷新、告警单条查询);新增 §5 审查发现章节 + 架构建议 |
| 2026-05-13 | **T40 UI 设计系统合规审计+修复**:60 页面全覆盖审计(PASS 31 / PASS_WITH_ISSUES 27 / NEEDS_WORK 2);修复 HIGH×2 + MEDIUM×6 + LOW×67;新增 `$white` 变量 + `--tk-font-display` Token;44 处 `#fff` 统一为 `$white`;14 处圆角硬编码统一为变量;3 处 TSX inline 颜色提取为 SCSS 类;ErrorBoundary 重构为 SCSS;2 处静默 catch 修复;2 处离调色板颜色修正 |
| 2026-05-10 | **访客首页改造**:轮播图接入 `/public/banners` API + `wx.downloadFile` 下载图片到本地临时路径;文章列表接入 `/public/articles` API;文章详情页根据登录状态选择认证/公开 API(`getPublicArticleDetail`);`.env` 新增 `TARO_APP_DEFAULT_TENANT_ID`;集成契约新增 4 个公开端点 |
| 2026-05-09 | **Design Token 全面接入**:68 SCSS 文件全面迁移 `font-size: Npx` → `var(--tk-*)`;634 token 引用 / 3 个特殊硬编码;新增 §1.1 Design Token 系统文档 |
| 2026-05-08 | **多角色自动化审计**:4 角色(admin/doctor/nurse/operator)× 59 页面 = 236 次探测,综合通过率 96.2%;更新 §2 页面结构为 59 页面完整列表(含医生端 dialysis/prescription/action-inbox + 患者端 dialysis-records/prescriptions/consents/health-records/diagnoses);更新 §5 审计发现(透析/知情同意/诊断/健康记录标记为已修复);更新 §6.5 TabBar 为 3 个;新增 §6.8 分批审计脚本;更新 §6.9 多角色审计结果 |
| 2026-05-08 | **MCP 联调全面重写**:自建 MCP 服务器 `@hms/weapp-mcp` 替代 `@yfme/weapp-dev-mcp`;基于 `@weapp-vite/miniprogram-automator@1.1.0`;新增 §6.2 启动步骤(登录+单实例铁律);更新工具列表为 weapp-local 25 个工具;新增 inject_auth 一键注入;新增 §6.7 MCP 服务器架构说明;多实例冲突、CLI 登录、SummerCompiler 等已知限制 |
| 2026-05-01 | 审计发现更新:CRITICAL 晚间血压丢失 / HIGH 透析+知情同意完全空白 / 功能域完成度矩阵 |
| 2026-04-28 | **全面性能优化**:分包加载(6 分包,主包 517KB→275KB,vendors 192KB→36KB);GET 请求去重+60s TTL 缓存;points store 集中积分/签到状态;todaySummary 60s TTL;7 组件 React.memo;TrendChart 双重渲染修复;restoreAuth 提升 App 级别;prod terser drop_console;crypto-js 按需引入 |
| 2026-04-27 | **移除 echarts-taro3-react**:内嵌 Taro 3 + React 16 导致 webpack 模块加载失败,改为自定义 `EcCanvas` 组件 + `echarts/core` 按需引入;更新版本说明 + 历史教训 + 组件列表 |
| 2026-04-27 | **MCP 联调全面更新**:§6.1 增加 dev 构建前置条件,§6.4 重写为明文 token 注入法(评估两种方案),§6.6 补充 7 条已知限制,新增 §6.7 审计脚本说明 + §6.8 实测审计结果(40/40 页面通过);§5 补充 4 条审计发现 |
| 2026-04-27 | 新增 §6 MCP 联调章节:连接、操作列表、绕过登录、已知限制、e2e 脚本 |
| 2026-04-26 | 全面更新:40 页面(含 9 个医护端页面)、15 目录、5 个 Tab 页、积分商城、线下活动 |
| 2026-04-25 | 全面更新:20 页面、10 服务、9 组件、Zod 验证、加密密钥外部化说明 |
| 2026-04-24 | 创建小程序 wiki 页面,记录登录流程、环境配置、历史陷阱 |