From ce0561001f2a8a5e1926e591c56271bf16bd9d34 Mon Sep 17 00:00:00 2001 From: iven Date: Thu, 21 May 2026 15:09:53 +0800 Subject: [PATCH] =?UTF-8?q?docs(spec):=20=E5=B0=8F=E7=A8=8B=E5=BA=8F?= =?UTF-8?q?=E4=B8=8A=E7=BA=BF=E5=89=8D=E5=85=A8=E9=9D=A2=E6=94=B9=E8=BF=9B?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E8=A7=84=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 5 Phase / 14 天改进方案,基于 MCP 实操审查 + 3 专家组并行分析 (UX/前端工程/产品安全),涵盖 8 CRITICAL / 11 HIGH / 22 MEDIUM 问题 Co-Authored-By: Claude Opus 4.7 --- ...-21-miniprogram-production-ready-design.md | 308 ++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-21-miniprogram-production-ready-design.md diff --git a/docs/superpowers/specs/2026-05-21-miniprogram-production-ready-design.md b/docs/superpowers/specs/2026-05-21-miniprogram-production-ready-design.md new file mode 100644 index 0000000..4d30cca --- /dev/null +++ b/docs/superpowers/specs/2026-05-21-miniprogram-production-ready-design.md @@ -0,0 +1,308 @@ +# 小程序上线前全面改进设计规格 + +> 日期: 2026-05-21 | 状态: Draft | 分支: feat/media-library-banner +> 方法: MCP 实操审查 + 3 专家组并行分析(UX/前端工程/产品安全) + +## 背景 + +通过 MCP 连接微信开发者工具实际操作 59 个页面,配合 3 个专家组(UX/前端工程/产品安全)并行源码级审查,发现 **8 CRITICAL / 11 HIGH / 22 MEDIUM** 问题。本规格定义 5 个 Phase 的改进方案,目标 1-2 周内达到可公开测试标准。 + +## 审查方法 + +- **MCP 实操**: 连接微信 DevTools(iPhone 12 Pro 模拟器),注入 admin 认证,逐页检查加载/交互/错误 +- **自动化审计**: `audit_all` 批量检测 59 页面健康状态(35 OK / 1 CRASH / 23 路径不匹配) +- **3 专家组并行**: 每组阅读 13-20 个核心源文件,按 CRITICAL/HIGH/MEDIUM/LOW 分级输出 + +## 发现汇总(去重后 TOP 问题) + +### CRITICAL(8 项) + +| # | 问题 | 文件 | 影响 | +|---|------|------|------| +| C1 | `secureGet` 解密逻辑失效 | `secure-storage.ts:60` | 用户关闭小程序后重开必须重新登录 | +| C2 | Storage key 前缀不一致 | `request.ts:149`, `health.ts:20` | `current_patient_id` 写加密读明文,X-Patient-Id header 缺失 | +| C3 | `analytics_queue` 前缀不一致 | `analytics.ts:38` vs `auth.ts:266` | logout 无法清除分析队列,PII 泄漏风险 | +| C4 | 咨询创建无症状描述输入 | `consultation/create/index.tsx` | 医生收到空咨询请求 | +| C5 | 缺少紧急求助/SOS 入口 | `index.tsx` HomeDashboard | 患者突发状况无法快速求助 | +| C6 | Token 存储弱加密(XOR + 硬编码 fallback) | `secure-storage.ts:3` | 越狱设备可直接恢复 JWT token | +| C7 | 开发快速登录可能泄露到生产 | `login/index.tsx:8-9` | 攻击者获取 dev 凭证 | +| C8 | AI 聊天无后端持久化 | `messages/index.tsx` | Tab 切换可能丢失对话 | + +### HIGH(11 项) + +| # | 问题 | 文件 | +|---|------|------| +| H1 | 积分任务按钮无 onClick | `mall/index.tsx:187` | +| H2 | 告警无微信推送能力 | `alerts/index.tsx` | +| H3 | 体征录入校验不一致 | `health/index.tsx:112` vs `input/index.tsx:33` | +| H4 | 医生端工作台加载卡死 | `pkg-doctor-core/index.tsx` | +| H5 | 家属代理操作无权限模型 | `family/index.tsx:47`, `auth.ts:226` | +| H6 | 首页医护人员 reLaunch 状态丢失 | `index.tsx:319-329` | +| H7 | PII 明文绕过 secure storage | `family/index.tsx:63` | +| H8 | 化验报告缺少指标解读 | `reports/index.tsx:57` | +| H9 | 趋势图缺少交互和 AI 解读 | `trend/index.tsx` | +| H10 | 预约无防恶意占用机制 | `appointment/create/index.tsx` | +| H11 | API 无请求签名 | `request.ts:154` | + +--- + +## Phase 0(Day 1):基础设施修复 + +### P0-1. 修复 secureGet 解密逻辑 + +**文件**: `src/utils/secure-storage.ts` + +**问题**: `secureGet` 第 60 行 `if (raw.startsWith('{') || raw.startsWith('eyJ'))` 条件判断错误。`secureSet` 存储的 XOR 加密 + base64 编码数据通常以 `A-Za-z0-9` 开头,不以 `{` 或 `eyJ` 开头,导致解密分支永远不执行。 + +**修复**: + +```typescript +export function secureGet(key: string): string { + const prefixedKey = STORAGE_PREFIX + key; + const raw = Taro.getStorageSync(prefixedKey); + if (!raw || typeof raw !== 'string') return ''; + + // 始终尝试 base64 解码 + XOR 解密(secureSet 的写入格式) + try { + const decoded = fromBase64(raw); + if (decoded) { + const decrypted = xorEncrypt(decoded, ENCRYPTION_KEY); + // 验证解密结果是否为有效数据(JSON 或 JWT) + if (decrypted.startsWith('{') || decrypted.startsWith('eyJ') || decrypted.length > 0) { + return decrypted; + } + } + } catch { + // fallthrough — 可能是未加密的旧数据 + } + + // fallback: 兼容未加密的旧数据 + return raw; +} +``` + +**验证**: 登录 → 完全关闭小程序 → 重新打开 → 确认自动恢复登录态(非访客页面) + +### P0-2. 修复 Storage Key 前缀不一致 + +**文件**: +- `src/services/request.ts` 第 149 行: `Taro.getStorageSync('current_patient_id')` → `secureGet('current_patient_id')` +- `src/services/request.ts` 第 218 行: `Taro.removeStorageSync('current_patient_id')` → `secureRemove('current_patient_id')` +- `src/services/health.ts` 第 20 行: `Taro.getStorageSync('current_patient_id')` → 改用 `getCachedPatientId()` 或 `secureGet('current_patient_id')` +- `src/stores/auth.ts` 第 266 行: `secureRemove('analytics_queue')` → `Taro.removeStorageSync('analytics_queue')`(analytics 用明文存储) + +### P0-3. 验证 + +- 注入认证 → reLaunch → 首页正确显示登录态 +- 医生端工作台数据正常加载 +- 请求头 `X-Patient-Id` 正确携带 + +--- + +## Phase 1(Day 2-4):核心体验修复 + +### P1-1. 咨询创建添加症状描述 + +**文件**: `src/pages/consultation/create/index.tsx` + +**改动**: +- 添加多行 `` 字段(placeholder: "请描述您的症状或问题") +- 标记为建议填写(非必填),但未填写时 Toast 提醒"建议描述症状以便医生更快响应" +- 提交时将描述传递给后端 API(后端 DTO 已支持 `description` 字段) + +### P1-2. 体征录入统一范围校验 + +**新建文件**: `src/utils/vital-ranges.ts` + +```typescript +export const VITAL_RANGES = { + systolic_bp: { min: 60, max: 250, label: '收缩压' }, + diastolic_bp: { min: 40, max: 150, label: '舒张压' }, + heart_rate: { min: 20, max: 300, label: '心率' }, + blood_glucose: { min: 1, max: 50, label: '血糖' }, + weight: { min: 1, max: 500, label: '体重' }, +} as const; + +export function validateVital(type: keyof typeof VITAL_RANGES, value: number): string | null { + const range = VITAL_RANGES[type]; + if (value < range.min || value > range.max) { + return `${range.label}应在 ${range.min}-${range.max} 之间`; + } + return null; +} +``` + +**修改文件**: `src/pages/health/index.tsx` 第 112-115 行,调用 `validateVital()` 替代简单的非空检查。`src/pages/pkg-health/input/index.tsx` 复用同一校验函数(移除内联校验逻辑)。 + +### P1-3. 积分任务入口修复 + +**文件**: `src/pages/mall/index.tsx` 第 187-192 行 + +**方案**: 暂时隐藏未实现的"积分任务"入口,仅保留"签到打卡"和"兑换记录"。后续实现后恢复显示。 + +```diff +- +- +- +- +- 积分任务 +- ++ {/* TODO: 积分任务功能待实现后恢复 */} +``` + +### P1-4. 医生端首页加载优化 + +**文件**: `src/pages/pkg-doctor-core/index.tsx` + +**改动**: +- `loadDashboard` catch 中设置 `setDashboard({})` 而非仅打印 warn +- loading 为 false 且 dashboard 为 null 时,显示降级 UI(统计卡片显示 `-`)+ 错误提示 + 重试按钮 +- 不再永久卡在 `` + +### P1-5. 首页医护人员跳转优化 + +**文件**: `src/pages/index/index.tsx` + +**改动**: 将 `shouldRedirect` 计算提前到组件顶层(useMemo),在 `useDidShow` 之前就确定跳转。跳转使用 `redirectTo` 替代 `reLaunch`,保留分包预加载状态。 + +--- + +## Phase 2(Day 5-8):功能补全 + +### P2-1. 紧急求助入口 + +**文件**: `src/pages/index/index.tsx` HomeDashboard 组件 + +**改动**: +- 添加固定悬浮 SOS 按钮(右下角,红色圆形,直径 56px) +- 点击弹出确认弹窗:"是否拨打机构急救电话 xxx-xxxx?" +- 电话号码从后端配置 API 获取,fallback 为租户配置的电话 +- 仅在患者登录状态显示(admin/doctor 不显示) + +### P2-2. 告警微信推送订阅 + +**文件**: `src/pages/pkg-health/alerts/index.tsx` + +**改动**: +- 页面加载时调用 `Taro.requestSubscribeMessage({ tmplIds: [CRITICAL_ALERT_TEMPLATE_ID] })` +- 复用 `process.env.TARO_APP_WX_TEMPLATE_CRITICAL_ALERT` 模板 ID +- 用户拒绝时不阻塞页面使用,仅在 `subscribeStatus` 中记录 +- critical 级别告警列表项显示"已开启推送"或"点击开启推送"标识 + +### P2-3. 趋势图交互增强 + +**文件**: `src/pages/pkg-health/trend/index.tsx` + +**改动**: +- ECharts 配置添加 `tooltip` 组件(`trigger: 'axis'`),点击/长按显示具体日期和数值 +- 血压趋势图添加收缩压+舒张压双线(series 两条线) +- 阈值区间以阴影区域显示(markArea) + +### P2-4. 个人中心菜单优化 + +**文件**: `src/pages/profile/index.tsx` + +**改动**: +- 17 项菜单分为 4 组,每组有分组标题: + +``` +健康档案: 健康记录 / 我的报告 / AI 分析 / 诊断记录 / 用药记录 +就诊服务: 我的预约 / 我的随访 / 在线咨询 +透析管理: 透析记录 / 透析处方 / 知情同意 +账号设置: 积分商城 / 线下活动 / 就诊人管理 / 长辈模式 / 设备同步 / 设置 +``` + +- 图标:汉字替换为 `` 或用 antd-mini 图标 +- 每组之间添加 12px 间距和分割线 + +### P2-5. 家属管理安全修复 + +**文件**: `src/pages/pkg-profile/family/index.tsx` + +**改动**: +- `edit_patient` 数据改用 `secureSet('edit_patient', JSON.stringify(patient))` 存储 +- 编辑页从 API 获取最新患者数据(`GET /health/patients/{id}`),不完全依赖 Storage 中的旧数据 +- Storage 数据添加 5 分钟过期检查 + +--- + +## Phase 3(Day 9-12):品质打磨 + +### P3-1. 老年模式交互增强 + +- 所有可点击元素最小尺寸 56px(通过 `elder-mode` CSS 变量覆盖) +- 体征录入页:数值输入区增加 +/- 按钮辅助(每次步进 1) +- TabBar 图标字号 elder-mode 下放大 120% +- 考虑 P2 迭代添加语音输入(本次不实现,仅预留接口) + +### P3-2. 安全加固 + +**开发快速登录保护**: +- `login/index.tsx` 第 8-9 行: 添加编译时保护 +- `config/prod.ts` 中确保 `TARO_APP_DEV_USER` 和 `TARO_APP_DEV_PASS` 注入为空字符串 + +**Tenant ID 安全**: +- `request.ts` 第 161 行: `X-Tenant-Id` 改为从 JWT payload 的 `tid` 字段提取 +- 不再从 Storage 读取 tenant_id + +**密码输入框**: +- `login/index.tsx` 第 160 行: `type="text"` 改为 `type="safe-password"` + +**预约防恶意占用**: +- `appointment/create/index.tsx`: 备注字段 maxlength=200 +- 提交按钮添加 loading 态防重复点击 + +### P3-3. 空状态和错误处理统一 + +- 所有列表页空状态统一使用 `EmptyState` 组件 +- Loading 组件拆分:`` 仅用于加载中,新增 `` 组件(无旋转动画) +- API 失败统一显示 `` + +### P3-4. ErrorBoundary 增强 + +- App 级 ErrorBoundary 添加最大重试次数(3 次),超限显示"请重启小程序" +- 咨询详情、健康数据、AI 聊天 3 个高频页面添加独立 ErrorBoundary +- `componentDidCatch` 调用 `trackEvent('error_boundary', { error: String(error) })` + +### P3-5. 访客首页优化 + +- "查看全部"改为跳转 `/pages/article/index`(文章列表页) +- "健康资讯"区域调用公开 API(`/api/v1/health/public/articles`)展示真实文章 +- 3 个静态卡片(健康数据管理/积分商城/专业科普)改为动态文章数据 + +--- + +## Phase 4(Day 13-14):全量回归测试 + +### 验证清单 + +- [ ] MCP `audit_all` 59 页面全部 OK(0 CRASH / 0 ERROR) +- [ ] 4 角色测试:admin / doctor / patient / family +- [ ] 长者模式全页面验证(58/58 页面可操作) +- [ ] 并发测试:快速 Tab 切换 10 次 + 长轮询运行 + 5 个并发 API +- [ ] 生产构建:`pnpm build` 通过 + 主包 < 2MB + 分包各 < 2MB +- [ ] secureGet 往返测试:登录 → 关闭 → 重开 → 确认自动恢复 +- [ ] 紧急求助功能端到端测试 +- [ ] 告警推送订阅流程测试 + +--- + +## 预期交付物 + +| Phase | 天数 | 交付物 | +|-------|------|--------| +| Phase 0 | 1 | secureGet 修复 + Storage key 一致性 | +| Phase 1 | 3 | 核心体验修复(咨询/体征/商城/医生端/跳转) | +| Phase 2 | 4 | 功能补全(SOS/推送/趋势图/菜单/家属) | +| Phase 3 | 4 | 品质打磨(老年模式/安全/空状态/ErrorBoundary) | +| Phase 4 | 2 | 全量回归测试报告 | +| **合计** | **14** | **可公开测试版本** | + +## 风险和缓解 + +| 风险 | 概率 | 缓解措施 | +|------|------|----------| +| secureGet 修复影响旧版本用户 | 中 | `migrateLegacyStorage` 兼容两种格式 | +| 后端缺少部分 API(告警推送模板、紧急电话) | 中 | Phase 2 后端同步开发,前端先 mock | +| ECharts 双线血压趋势性能问题 | 低 | 限制数据点数量(最多 30 天) | +| 微信审核拒绝(SOS 按钮、订阅消息) | 低 | SOS 改为"联系机构"文案,订阅消息仅 critical 级别 |