Files
hms/docs/superpowers/specs/2026-05-21-miniprogram-comprehensive-improvement-design.md
iven 89fa322d7a docs(mp): 小程序安全优先全面改进路线图设计规格
四专家组五维度深度分析(架构7.25/安全6.0/UX7.4/工程6.2),
综合 6.7/10 → 目标 8.5/10。安全优先策略,4 Phase / 8 周,
覆盖 XOR→AES 替换、ARIA 可访问性、测试覆盖提升、Canvas 适老化。
2026-05-21 23:25:18 +08:00

23 KiB
Raw Blame History

小程序安全优先全面改进路线图

日期: 2026-05-21 | 策略: 安全优先 | 综合: 6.7/10 (B) → 目标: 8.5/10 (A-)

目录


1. 背景与动机

1.1 为什么现在做

2026-05-21 四专家组(安全/架构/UX/工程)对小程序进行了五维度深度分析,综合评分 6.7/10 (B)。发现 3 个 CRITICAL、4 个 HIGH、6 个 MEDIUM 级别问题,其中安全评分仅 6.0/10 (C+),可访问性评分 2/10测试覆盖率 ~6%。这些问题如果不解决,将阻碍产品进入医疗市场。

1.2 为什么选安全优先策略

三个候选策略对比:

策略 优势 风险 总工时
安全优先(选定) 先解决阻塞项,交付物可审计,风险低 工程改进可能延后 8-10 周
工程基础优先 基础先扎实,后续开发效率高 安全漏洞暴露窗口长 10-12 周
分层并行 节奏快 资源需求大,协调复杂 12-16 周

安全优先策略确保产品在合规审查前完成所有安全阻塞项。

1.3 分析数据来源

  • 168 个 TS/TSX 源文件 / 18,558 行代码 / 102 个 SCSS 文件
  • 42 个 Service 文件 / 4 个 Store / 10 个 Hook
  • 61 个页面15 主包 + 46 分包)
  • 9 个单元测试文件 / 4 个 E2E spec
  • 50+ 个与小程序相关的 git 提交

2. 五维度分析摘要

2.1 架构设计 — 7.25/10 (B+)

优势:

  • 四层架构清晰Service → Store → Hook → Page
  • BLE 子系统 adapter + manager + buffer + scheduler 拆分堪称模板
  • 内存泄漏防护到位generation counter / AbortController / BLE 监听器清理)
  • Zustand 使用规范缓存策略三层request/store/service

问题:

  • noImplicitAny: false 是 TypeScript 安全最大短板
  • health store 直接耦合 auth store
  • ErrorBoundary 功能有限(无分类/无上报)

2.2 安全 — 6.0/10 (C+)

CRITICAL

  • XOR 加密形同虚设 — 密钥硬编码在 bundle 中('hms-default-key'
  • .env.production 包含硬编码真实 Tenant ID
  • console.warn 生产环境泄露认证错误详情

HIGH

  • BLE 健康数据明文存储DataBuffer.ts最多 10000 条生理数据)
  • 健康阈值/Analytics 队列明文存储
  • 生产配置占位符(api.hms.example.com

2.3 UX/产品 — 7.4/10 (B+)

优势:

  • Design Token 体系业界上乘9/1011 级字号 + 12 结构 Token
  • 关怀模式 8.5/1058/58 页面覆盖CSS 变量级联
  • 错误状态组件覆盖完整ErrorBoundary/ErrorState/EmptyState/Loading

问题:

  • 可访问性 2/10 — 整个 0 ARIA 属性,医疗应用合规风险
  • TrendChart Canvas 未适老化(硬编码 10px 字号)
  • 医生端 reLaunch 状态丢失(搜索/滚动位置/表单草稿全清空)

2.4 工程质量 — 6.2/10 (B-)

优势:

  • 请求层设计优秀ResponseCache + ConcurrencyLimiter + Token 刷新去重)
  • 组件库完备35 个组件分 ui/patterns/独立三类)
  • 生产构建 console.log 全移除

问题:

  • 测试覆盖率 ~6%9 个测试文件/168 源文件)
  • 无 ESLint/Prettier 配置
  • 38 处 any 类型使用
  • 构建未做分包级代码分割

3. 总体架构与 Phase 规划

3.1 总工时

领域 Phase 0 Phase 1 Phase 2 Phase 3 合计
安全 7.5d 1dconsent 拦截) 5d 1.5d 15d
工程 4.5d 7.5d 2dToken 生成脚本) 4d 18d
UX 4.5d 4d 1.5d 10d
Phase 合计 12d 13d 11d 7d 43d

注:部分任务可并行执行(安全 + 工程 + UX 不同人员),实际日历时间约 8 周。

3.2 依赖关系图

Phase 0周 1-2
├── TypeScript strict ──────────┐
├── ESLint 工具链 ──────────────┤
├── secure-storage 接口抽象 ────┤
├── XOR → AES 替换 ────────────┤
└── 安全 P0 修复 ───────────────┤
                                ↓
Phase 1周 3-4               │
├── secure-storage 测试 ────────┤
├── request.ts 测试 ────────────┤
├── auth store 测试 ────────────┤
└── ARIA 可访问性基础 ──────────┤
                                ↓
Phase 2周 5-6               │
├── Analytics/阈值加密 ─────────┤
├── Patient DTO 最小化 ─────────┤
├── Canvas 适老化 ──────────────┤
└── 表单跳焦 + Loading 统一 ───┤
                                ↓
Phase 3周 7-8               │
├── API 请求签名 ──────────────┤
├── any 类型清零 ──────────────┤
├── 构建优化 ──────────────────┤
└── CI 集成 ───────────────────┘

3.3 关键里程碑

里程碑 时间 交付物
M0 第 2 周末 安全 CRITICAL 全部修复、TypeScript strict 开启、ESLint 配置生效
M1 第 4 周末 核心模块测试覆盖 ≥80%、ARIA 基础框架完成
M2 第 6 周末 AES 加密替换完成、Canvas 适老化完成
M3 第 8 周末 any 类型清零、CI 流水线运行、综合评分 ≥8.5

4. Phase 0安全 P0 + 工程基础(周 1-2

4.1 安全 P0 修复

S0-1: 替换 XOR 为 AES-256-GCM 加密存储 [CRITICAL, 3d]

现状: secure-storage.ts 使用 XOR 编码,密钥 'hms-default-key' 硬编码。.envTARO_APP_ENCRYPTION_KEY= 为空值,生产 fallback 到硬编码密钥。

方案:

  1. 安装 @noble/ciphers(纯 JS AES-GCM无 Web Crypto 依赖)
  2. 后端 login/refresh 响应新增 storage_key 字段256-bit与 access_token 同生命周期)
  3. 小程序侧 storage_key 仅存内存变量,不持久化
  4. 实现 AesSecureStoragecrypto.getRandomValues 生成 IV → AES-GCM 加密 → Base64 编码 → Storage
  5. 冷启动无 storage_key 时,secureGet 返回空,用户需重新登录

影响: 冷启动需重新登录UX 略降。缓解refresh token 有效期内自动恢复。

S0-2: 移除生产硬编码 Tenant ID [CRITICAL, 1d]

方案:

  1. .env.productionTARO_APP_DEFAULT_TENANT_ID 设为空字符串
  2. 访客首页(未登录)无轮播图时展示静态引导图

S0-3: 生产环境 console 脱敏 [CRITICAL, 1d]

方案:

  1. 创建 utils/logger.ts,区分 __DEV__ 和生产模式
  2. 生产模式仅输出错误类别码(如 [auth] LOGIN_FAILED),不附带 error 对象
  3. prod.tsterserOption.compress.pure_funcs 增加 'console.warn'

S0-4: 开发快速登录生产构建移除 [HIGH, 0.5d]

方案:

  1. config/index.tsdefineConstants 中,生产环境硬编码 TARO_APP_DEV_USER/PASS 为空字符串
  2. login 页面组件条件编译确保生产构建中整段 JSX 被 tree-shaking 移除

S0-5: BLE 数据缓冲加密存储 [HIGH, 2d]

方案:

  1. DataBuffer 构造函数接收加密函数引用DI
  2. persistCurrentBucketrestore 通过注入的 encryptFn/decryptFn 处理
  3. 复用 S0-1 的 AES-GCM 会话密钥

4.2 工程基础

E0-1: 开启 TypeScript strict [2d]

方案:

  1. tsconfig.json 设置 noImplicitAny: true
  2. 修复编译错误,优先修复 request.tsBLEManager.tsauth.ts
  3. 临时 // @ts-expect-error 标注无法立即修复的位置

E0-2: ESLint + Prettier 工具链 [1d]

方案:

  1. 安装 eslint@9flat config@typescript-eslint/parser@typescript-eslint/eslint-plugineslint-plugin-react-hooksprettier
  2. 核心 ruleset@typescript-eslint/no-explicit-any: errorreact-hooks/exhaustive-deps: error
  3. Prettier 配置:{ semi: true, singleQuote: true, trailingComma: "all", printWidth: 100 }

E0-3: ErrorBoundary 安全加固 [1d]

方案:

  1. 添加错误分类NetworkError / RenderError / BusinessError
  2. 结构化日志输出(含 user_id / tenant_id / page_path
  3. 重试次数按错误类型区分

E0-4: DataSyncScheduler 并发安全 [0.5d]

方案: 添加 isSyncing 互斥标志interval callback 先检查再执行。

4.3 Phase 0 验收标准

  • secure-storage.ts 使用 AES-256-GCMXOR 仅作为迁移读取路径
  • .env.production 不包含真实 Tenant ID
  • 生产构建中 console.warn 被移除或脱敏
  • 开发快速登录按钮在生产构建中不存在
  • BLE DataBuffer 使用加密存储
  • tsconfig.jsonnoImplicitAny: truenpx tsc --noEmit 零错误
  • npx eslint src/ 零 error
  • cargo check + cargo test 全 workspace 通过(后端配合修改)

5. Phase 1测试覆盖 + UX 合规(周 3-4

5.1 测试覆盖提升

T1-1: secure-storage 单元测试 [2d]

覆盖AES-GCM 加解密对称性、Base64 边界(空字符串/超长/中文/emoji、空 value 触发 remove、明文 fallback 读取兼容性、migrateLegacyStorage 迁移逻辑。

T1-2: request.ts 核心路径测试 [2d]

覆盖ConcurrencyLimiter acquire/release 队列行为、401 重试 + token 刷新去重、ResponseCache 命中/淘汰/inflight 去重、safeReLaunch 去重、AbortSignal 取消。

T1-3: auth store 测试 [2d]

覆盖:restore() 状态恢复、login/credentialLogin 流程、logout 清理完整性、bindPhone 流程、角色判断函数。

T1-4: DataSyncScheduler + BLEManager 测试 [1.5d]

覆盖:needsSync 时间判断、startPeriodicCheck 不并发、BLE 连接生命周期。

5.2 UX 合规

U1-1: 核心 ARIA 角色标注 [2d]

为 TabBarrole="tablist"/role="tab" + aria-selected)、表单输入(aria-label/aria-describedby/aria-invalid)、按钮(aria-disabled)、加载状态(role="status" + aria-live="polite")添加语义角色。覆盖约 15 个核心组件。

U1-2: 表单可访问性增强 [1d]

FormInput 组件的 <Input> 元素添加 aria-labelaria-describedby(错误提示关联)、aria-invalid。体征录入页数值输入添加 aria-valuemin/max/now

U1-3: 动态内容 aria-live [0.5d]

LoadingCard、EmptyState、ErrorState、TrendChart tooltip 添加 aria-live 区域,确保内容变化时读屏器自动播报。

U1-4: 焦点管理基础 [1d]

所有可交互元素添加 focusin/focusout 视觉反馈样式,确保焦点环可见。

5.3 Phase 1 验收标准

  • 单元测试文件 ≥6 个secure-storage / request / auth / scheduler / BLE / components
  • 核心安全模块行覆盖率 ≥80%
  • npx vitest run 全部通过
  • 核心交互组件有 ARIA 角色标注
  • FormInput 组件有完整的可访问性属性

6. Phase 2加密替换 + Canvas 适老(周 5-6

6.1 安全加固

S2-1: Analytics 队列 PII 清理 [2d]

方案:

  1. AnalyticsEvent 接口移除 userIdpatientId 字段
  2. 服务端从 JWT 提取用户身份,不从客户端 payload 接收
  3. Storage 中的队列仅保留 event + timestamp + properties
  4. properties 运行时过滤禁止包含 user/patient 标识

S2-2: 健康阈值缓存加密 [1d]

复用 Phase 0 的 AES-GCM 加密函数,getHealthThresholds 写入缓存时加密,读取时解密。缓存 TTL 24h 不变。

S2-3: Patient DTO 字段最小化 [2d]

方案:

  1. 后端拆分 PatientSummary列表用5-8 非敏感字段)和 PatientDetail(详情用)
  2. 小程序 loadPatients 调用 summary 端点
  3. current_patient 存储过滤掉 id_card_numberphone_hash 等字段

6.2 Canvas 适老化

U2-1: TrendChart Canvas 适老化 [2d]

方案:

  1. 创建 useCanvasTokens() hook从 CSS 变量提取字号/颜色供 Canvas 使用
  2. 关怀模式下:字号提升到 14-16px、颜色对比度 WCAG AA、异常点放大到 7-8px
  3. 参考区间带添加斜线纹理增强区分度
  4. 数据点 tooltip 改为常驻显示(老年用户难以精确触摸小点)

U2-2: 表单自动跳焦 + 历史参考 [1.5d]

方案:

  1. 体征录入页 returnKeyType="next" + onConfirm 链式跳焦
  2. 显示上次测量值作为参考提示
  3. 体重录入显示上次记录

U2-3: Loading/骨架屏统一 [0.5d]

规范:列表页用 LoadingCard(card/list)、详情页用 LoadingCard(detail)、操作反馈用 Loading spinner。

6.3 Phase 2 验收标准

  • Analytics 队列无 PII 字段
  • 健康阈值缓存加密存储
  • Patient DTO 列表端点不含敏感字段
  • TrendChart 在关怀模式下字号 ≥14px、异常点可见
  • useCanvasTokens() hook 在所有 Canvas 组件中使用
  • 血压录入支持自动跳焦

7. Phase 3全面提升 + CI周 7-8

7.1 安全

S3-1: API 请求签名验证 [1.5d]

方案:

  1. 后端 login/refresh 下发 signing_key256-bit仅存内存
  2. request.ts 计算 HMAC-SHA256(method + path + body_hash + timestamp + nonce)
  3. 请求头添加 X-SignatureX-TimestampX-Nonce
  4. 后端中间件校验签名timestamp 偏移 >5min 拒绝

7.2 工程

E3-1: 消灭所有 any 类型 [1d]

  • catch (err: any)catch (err: unknown) + 类型守卫
  • BLE 回调参数提取具体接口
  • method as any 使用 Taro RequestMethod 类型

E3-2: 大文件拆分 [1d]

8 个 >300 行文件拆分至 ≤250 行。提取 hooksuseDailyMonitoringuseTrendChart)和子组件。

E3-3: 构建优化 [1d]

  1. splitChunks 按分包拆分公共依赖
  2. package.json 添加 "sideEffects": false
  3. webpack-bundle-analyzer 分析产物

E3-4: CI 集成 [1d]

PR 触发TypeScript 编译 + ESLint + 单元测试。阻断条件:编译失败/ESLint error/测试失败。

7.3 UX

U3-1: 医生端导航状态保持 [1d]

为高频页面实现状态持久化(离开时保存关键状态到 session返回时恢复

U3-2: 微交互统一 [0.5d]

统一触觉反馈、动画时序200ms ease-out、加载到内容的 fade-in 过渡。

7.4 Phase 3 验收标准

  • grep -rn ': any' src/ 返回 0 结果
  • 所有源文件 ≤250 行
  • 主包体积相比优化前减少 ≥15%
  • CI 流水线在 PR 上自动运行
  • API 签名在登录/预约请求中生效
  • 医生端 Tab 切换后搜索状态保持

8. 医疗合规特殊要求

8.1 PIPL 合规

要求 Phase 技术措施
敏感个人信息加密存储 Phase 0 AES-256-GCM 替换 XOR
数据最小化 Phase 2 Patient DTO 拆分,仅返回必要字段
用户同意管理 Phase 1 consent 模块已有 CRUD需新增 handler 层拦截器检查同意状态
数据出境评估 Phase 2+ AI 分析 PII 脱敏 + data_residency: cn_only
安全事件响应 Phase 3 安全审计日志 + 异常登录检测

8.2 无障碍合规

《无障碍环境建设法》(2023) 要求公共服务应用提供无障碍支持。医疗健康管理平台属于覆盖范围。

要求 Phase 技术措施
ARIA 语义标注 Phase 1 TabBar/表单/按钮/状态组件
屏幕阅读器支持 Phase 1 aria-live 动态区域
键盘焦点管理 Phase 1 focusin/focusout 视觉反馈
色彩对比度 Phase 0+ Canvas 适老化 + Token 审计

9. 风险评估与缓解

风险 影响 概率 缓解方案
AES 替换后冷启动需重新登录 UX 下降 确定 refresh token 自动恢复,用户无感知
AES-GCM 在小程序性能 延迟增加 @noble/ciphers 实测 <1ms/次
访客首页无轮播图 功能降级 确定 改为静态引导图
BLE 冷启动丢弃缓冲数据 数据丢失 BLE 缓冲是降级能力,同步后清空
前后端 DTO 拆分范围大 接口不稳定 渐进式,保持旧端点兼容
请求签名计算增加延迟 性能 HMAC-SHA256 <0.5ms/次
客户端时钟偏移 签名失败 后端容忍 5min 偏移 + UI 提示校准
PIPL 数据出境限制 AI 功能 功能受限 境外 Provider 禁用,仅用 Ollama

10. 验收标准

全局验收Phase 3 结束后)

维度 当前 目标 验收方法
安全评分 6.0 ≥8.5 安全审计复查
架构评分 7.25 ≥8.5 TypeScript strict + 0 any
UX 评分 7.4 ≥8.5 ARIA 覆盖 + Canvas 适老
工程评分 6.2 ≥8.0 测试 ≥50% + ESLint 0 error + CI
测试覆盖率 ~6% ≥50% vitest --coverage
any 使用 38 处 0 grep -rn ': any' src/
ARIA 属性 0 个 ≥50 grep -rn 'aria-' src/
CI 阻断 全覆盖 PR 自动检查

11. 技术选型

加密:@noble/ciphers + 微信小程序 polyfill

纯 JS AES-GCM 实现,无 Web Crypto 依赖,便于测试和跨环境兼容。体积 ~10KB gzipped。

关键:微信小程序环境 polyfill 策略

微信小程序没有标准 crypto.getRandomValues(),平台提供 wx.getRandomValues()(异步)或旧版 Taro.arrayBufferToBase64 + Math.random 混合方案。

启动时 polyfill

// utils/crypto-polyfill.ts
import Taro from '@tarojs/taro';

// 微信小程序 crypto.getRandomValues polyfill
if (!globalThis.crypto?.getRandomValues) {
  globalThis.crypto = {
    getRandomValues: (arr: Uint8Array) => {
      const buf = new ArrayBuffer(arr.length);
      // wx.getRandomValuesSync 在基础库 2.17.3+ 可用
      if (typeof wx !== 'undefined' && wx.getRandomValuesSync) {
        const res = wx.getRandomValuesSync(buf);
        arr.set(new Uint8Array(res));
      } else {
        // fallback: Math.random仅用于非安全关键场景
        for (let i = 0; i < arr.length; i++) {
          arr[i] = Math.floor(Math.random() * 256);
        }
      }
      return arr;
    },
  } as any;
}

app.tsx 首行导入此 polyfill确保 @noble/ciphers 能正常工作。

替代方案: 使用 wecipher 库,已内置微信小程序兼容性封装。

ESLintFlat Config v9+

// eslint.config.mjs
import tseslint from '@typescript-eslint/eslint-plugin';
import tsparser from '@typescript-eslint/parser';
import reactHooks from 'eslint-plugin-react-hooks';
import jsxA11y from 'eslint-plugin-jsx-a11y';

export default [
  { files: ['src/**/*.{ts,tsx}'] },
  { languageOptions: { parser: tsparser } },
  tseslint.configs.recommended,
  { rules: { '@typescript-eslint/no-explicit-any': 'error' } },
  reactHooks.configs.recommended,
  jsxA11y.configs.recommended,
];

测试Vitest已安装

environment: 'node'Mock 策略三层:基础设施 MockTaro API→ 服务 Mockrequest→ 组件 Mock子组件

Prettier

{ "semi": true, "singleQuote": true, "trailingComma": "all", "printWidth": 100, "tabWidth": 2 }

Canvas Token 桥接

微信小程序没有 document/getComputedStyleCanvas 无法直接读取 CSS 变量。使用构建时生成 + 运行时注入方案:

// scripts/generate-tokens.ts — 构建时从 tokens.scss 解析生成
// 输出 src/styles/token-values.ts

// src/styles/token-values.ts — 自动生成,勿手动编辑
export const TOKEN_VALUES = {
  fontBody: '16px',
  fontBodySm: '14px',
  fontCap: '13px',
  pri: '#C4623A',
  tx: '#2D2A26',
  // ...
} as const;

// 导出关怀模式/医生模式的覆盖值
export const ELDER_TOKEN_OVERRIDES = {
  fontBody: '22px',
  fontBodySm: '20px',
  // ...
} as const;

export const DOCTOR_TOKEN_OVERRIDES = {
  pri: '#3A6B8C',
  // ...
} as const;
// hooks/useCanvasTokens.ts — 运行时 Hook
import { TOKEN_VALUES, ELDER_TOKEN_OVERRIDES, DOCTOR_TOKEN_OVERRIDES } from '@/styles/token-values';
import { useUIStore } from '@/stores/ui';
import { useAuthStore } from '@/stores/auth';

export function useCanvasTokens() {
  const mode = useUIStore((s) => s.mode);
  const isDoctor = useAuthStore((s) => s.isDoctor);

  return useMemo(() => {
    let tokens = { ...TOKEN_VALUES };
    if (mode === 'elder') tokens = { ...tokens, ...ELDER_TOKEN_OVERRIDES };
    if (isDoctor) tokens = { ...tokens, ...DOCTOR_TOKEN_OVERRIDES };
    return tokens;
  }, [mode, isDoctor]);
}

构建流程在 npm run build:weapp 前自动执行 generate-tokens.ts,确保 JS 常量与 SCSS 变量同步。


附录 A回滚策略

Phase 0 引入了破坏性变更,以下为各变更的回滚方案:

AES 加密替换回滚

AesSecureStorage 保留 XOR 读取路径作为 fallbacksecureGet 尝试 AES 解密失败时,自动尝试 XOR 解密。如果需要完全回滚,将 SecureStorageFactory.create() 切回 XorSecureStorage 即可。Storage 中已有数据无需迁移——旧数据用 XOR 读取,新数据用 AES 写入,自然过渡。

TypeScript strict 回滚

tsconfig.jsonnoImplicitAny 可随时改回 false。已修复的类型标注保留不影响运行时,只是编译检查宽松度变化。

ESLint 阻断回滚

紧急修复时可临时禁用 CI 中的 ESLint 阻断条件(--max-warnings=-1--max-warnings=999)。修复完成后恢复严格模式。

BLE 加密回滚

DataBuffer 的 DI 设计允许注入明文存储函数(identity 函数),回滚时替换构造函数参数即可。


附录 B专家组成员

专家 角色 分析维度
Security Engineer 安全审计 加密/PII/网络/合规
Senior Developer 架构+工程 TypeScript/依赖/错误处理/构建
UX Architect 用户体验 Token/关怀模式/可访问性/交互
Frontend Developer 工程质量 测试/代码规范/性能/CI

附录 B分析数据来源

  • 168 TS/TSX 源文件逐文件审查
  • 102 SCSS 文件 Design Token 执行度检查
  • 42 Service 文件 API 契约验证
  • 50+ git 提交历史分析
  • wiki/miniprogram.md54KB+ wiki/miniprogram-quality-checklist.md29KB
  • docs/qa/ 下 10+ 份 QA 文档交叉验证