--- title: 微信小程序(患者端) updated: 2026-04-26 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 ## 2. 关键文件 + 数据流 ### 核心文件 | 文件 | 职责 | |------|------| | `apps/miniprogram/config/index.ts` | Taro 构建配置(defineConstants 注入环境变量) | | `apps/miniprogram/src/services/request.ts` | HTTP 请求封装(401 自动刷新、错误处理) | | `apps/miniprogram/src/services/auth.ts` | 微信登录/绑定手机号 API | | `apps/miniprogram/src/stores/auth.ts` | 认证状态(login/bindPhone/restore) | | `apps/miniprogram/src/utils/secure-storage.ts` | token 安全存储(XOR + Base64 混淆) | | `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 → 跳转首页 ``` ### 页面结构(40 个页面,15 个目录) #### 患者端页面 | 页面路径 | 说明 | |----------|------| | `pages/login/index` | 登录页(微信登录 + 协议勾选) | | `pages/index/index` | 首页(今日健康、快捷服务) | | `pages/health/index` | 健康上报(Tab 页) | | `pages/health/input/index` | 健康数据录入(Zod 验证) | | `pages/health/trend/index` | 健康趋势(体征数据折线图) | | `pages/health/daily-monitoring/index` | 日常监测数据 | | `pages/appointment/index` | 预约列表 | | `pages/appointment/create/index` | 预约挂号 | | `pages/appointment/detail/index` | 预约详情 | | `pages/article/index` | 健康资讯列表 | | `pages/article/detail/index` | 文章详情 | | `pages/report/detail/index` | 健康报告详情 | | `pages/ai-report/list/index` | AI 分析报告列表 | | `pages/ai-report/detail/index` | AI 分析报告详情 | | `pages/followup/detail/index` | 随访详情 | | `pages/consultation/index` | 咨询列表(Tab 页) | | `pages/consultation/detail/index` | 咨询详情 | | `pages/mall/index` | 积分商城(Tab 页) | | `pages/mall/detail/index` | 商品详情 | | `pages/mall/exchange/index` | 积分兑换 | | `pages/mall/orders/index` | 积分订单 | | `pages/events/index` | 线下活动 | | `pages/profile/index` | 个人中心(Tab 页) | | `pages/profile/family/index` | 家庭成员管理 | | `pages/profile/family-add/index` | 添加家庭成员 | | `pages/profile/reports/index` | 我的报告 | | `pages/profile/followups/index` | 我的随访 | | `pages/profile/medication/index` | 用药记录 | | `pages/profile/settings/index` | 设置 | | `pages/legal/user-agreement` | 用户服务协议 | | `pages/legal/privacy-policy` | 隐私政策 | #### 医护端页面(8 个) | 页面路径 | 说明 | |----------|------| | `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` | 报告详情 | ### 服务层(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 | ### 组件(9 个) | 组件 | 用途 | |------|------| | `EmptyState` | 空状态占位 | | `ErrorBoundary` | 错误边界捕获 | | `ErrorState` | 错误状态展示 | | `FamilyPicker` | 家庭成员选择器 | | `HealthCard` | 健康数据卡片 | | `Loading` | 加载状态 | | `StepIndicator` | 步骤指示器 | | `TrendChart` | 趋势图表(ECharts) | | `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 刷新 | ## 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 || ''), }, ``` ### 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 | 检查当前路径避免重复跳转 | ### 待优化 | 问题 | 级别 | 说明 | |------|------|------| | URL 拼接构建查询参数 | P2 | `request.ts` 应支持 params 对象 | | 加密密钥硬编码 | P0 | 需外部化到 `TARO_APP_ENCRYPTION_KEY` 环境变量 | | Auth token 日志输出 | P0 | 生产环境需移除 console.log | | 生产配置 | P2 | `urlCheck`/`minified` 需区分环境 | ### 注意事项 - `Taro.login()` 的 code 一次性使用,每次调用会返回新 code - `session_key` 缓存 5 分钟(TTL),过期需重新登录 - 微信开发者工具中 `getPhoneNumber` 需要真机调试或使用测试号 - Redis 不可达时限流降级为 fail-open,不影响登录 ## 6. MCP 联调(微信开发者工具自动化) > 通过 MCP (Model Context Protocol) 工具直接操控微信开发者工具中的小程序模拟器,实现页面导航、元素交互、数据读取等操作。 ### 6.1 前置条件 1. **后端运行** — `cargo run` 启动 `http://localhost:3000` 2. **小程序已构建** — `cd apps/miniprogram && pnpm build:weapp` 3. **微信开发者工具已打开项目** — 加载 `apps/miniprogram/dist/` 4. **自动化端口已开启** — `project.config.json` 中 `"automationAudits": true`,端口 9420 ### 6.2 建立连接 ``` 调用 mp_ensureConnection → 自动连接 ws://localhost:9420 ``` 成功后返回系统信息(SDK 版本、设备型号、屏幕尺寸等)。 ### 6.3 常用 MCP 操作 | 操作 | MCP 工具 | 说明 | |------|----------|------| | 查看当前页面 | `mp_currentPage` | 返回路径、尺寸、数据 | | 导航到页面 | `mp_navigate` | `navigateTo` / `switchTab` / `reLaunch` | | 返回上一页 | `mp_navigate` | `transition: "navigateBack"` | | 截图 | `mp_screenshot` | ⚠️ 当前版本超时,见下方限制 | | 读取 storage | `mp_callWx` | method: `getStorageSync` | | 写入 storage | `mp_callWx` | method: `setStorageSync` | | 查找元素 | `page_getElement` | CSS 选择器,返回标签/文本/尺寸 | | 查找多个元素 | `page_getElements` | CSS 选择器数组 | | 点击元素 | `element_tap` | CSS 选择器定位后点击 | | 输入文本 | `element_input` | CSS 选择器 + 值 | | 读取组件数据 | `element_getData` | 获取自定义组件的 data | | 设置组件数据 | `element_setData` | 修改自定义组件的 data | | 读取页面数据 | `page_getData` | 获取当前页面 data 对象 | | 设置页面数据 | `page_setData` | 修改当前页面 data | | 调用页面方法 | `page_callMethod` | 调用当前页面上暴露的方法 | | 等待元素出现 | `page_waitElement` | 轮询等待选择器匹配 | ### 6.4 绕过微信登录 MCP 无法模拟微信 OAuth,需要通过 `mp_callWx` 直接写入加密 auth 数据: ``` 1. 调用后端 API 获取 admin token: POST /api/v1/auth/login { username: "admin", password: "Admin@2026" } 2. 用 CryptoJS.AES.encrypt(token, ENC_KEY) 加密 3. 通过 mp_callWx 写入 storage: setStorageSync("access_token", encrypted_token) setStorageSync("refresh_token", encrypted_refresh) setStorageSync("user_data", encrypted_user_json) setStorageSync("user_roles", encrypted_roles_json) setStorageSync("tenant_id", encrypted_tenant_id) setStorageSync("current_patient", patient_object) setStorageSync("current_patient_id", patient_id) 4. reLaunch 到首页 ``` **ENC_KEY**: `0a17b71d46064b06f993c9c202b342425e311a79f5be026d830562e7ad51f522` > ⚠️ 注意:写完 storage 后必须立即 reLaunch,否则 app 的 API 请求会因 token 无效触发 401 → logout 清空 storage。 ### 6.5 TabBar 页面列表 以下页面是 TabBar 页面,必须用 `switchTab` 导航(不能用 `navigateTo`): - `pages/index/index` — 首页 - `pages/health/index` — 健康数据 - `pages/consultation/index` — 咨询 - `pages/mall/index` — 积分商城 - `pages/profile/index` — 个人中心 ### 6.6 已知限制 | 问题 | 原因 | 替代方案 | |------|------|----------| | **mp_screenshot 超时** | `miniprogram-automator` 的 `screenshot()` 方法在当前 DevTools 版本卡死 | 用 `page_getElement` + `page_getData` 获取 UI 状态 | | **navigateTo 超时** | 某些页面的 API 调用阻塞了页面渲染回调 | 页面实际已加载,用 `mp_currentPage` 确认路径 | | **auth 重定向** | app 的 request interceptor 检测 401 后自动跳转 login 并清空 storage | 确保 token 有效,reLaunch 后等待足够时间 | | **getStorageSync 返回空** | reLaunch 触发 app 初始化,401 → logout 清空了 storage | 检查 token 是否过期,必要时重新注入 | ### 6.7 端到端验证脚本 `apps/miniprogram/e2e-final.cjs` 使用 `miniprogram-automator` Node.js 库执行完整链路验证: - 11 条 UI 链路(页面导航 + 元素交互) - 10 个 API 数据闭环验证 - 加密 storage 注入绕过微信登录 - 超时保护避免卡死 运行:`cd apps/miniprogram && node e2e-final.cjs` ## 7. 变更记录 | 日期 | 变更 | |------|------| | 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 页面,记录登录流程、环境配置、历史陷阱 |