Files
hms/wiki/miniprogram-quality-checklist.md
iven d26ea64ab2 docs(wiki): 全面重写小程序质量规范清单 — 12类44条规则,覆盖164条git历史
从 164 条小程序 git 提交中全量抽象问题模式:
- 新增 4 类规则:前后端接口契约(字段对齐/API路径/必填字段)、安全(XSS/输入验证/敏感数据/权限)、定时器与副作用清理(setTimeout)、开发环境(DevTools优化)
- 补充已有类别:请求缓存去重(1.4)、统一组件库(3.5-3.6)、认证恢复(5.3-5.4)、对齐原型(7.4-7.5)、ErrorBoundary(9.3)
- 溯源表从 13 条扩至 38 条,每条对应具体修复提交
- 自查脚本从 6 项扩至 10 项
- 统计概览:44 规则 / 66+ fix 提交 / 20 CRITICAL+HIGH
2026-05-17 20:33:25 +08:00

672 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 小程序质量规范清单
> **最后更新:** 2026-05-17 | 来源164 条 git 提交历史 + 两轮深度审计,全量问题模式抽象
>
> **用途:** 新页面/新模块开发时的自检清单PR Review 的检查依据,新项目启动时的基线规范。每条规则都来自真实 bug 修复,不是理论推导。
---
## 目录
1. [[#1 并发与请求层]] — 限流器、长轮询、Token 刷新、缓存策略
2. [[#2 导航与路由]] — 页栈保护、reLaunch 去重、分包预加载
3. [[#3 组件与渲染]] — render body 定义、ScrollView 嵌套、懒加载、统一组件库
4. [[#4 数据与内存]] — 数组上限、去重索引一致性、Storage 清理
5. [[#5 认证与会话]] — 模块级缓存清理、logout 完整性、认证恢复
6. [[#6 前后端接口契约]] — 字段对齐、类型同步、API 路径一致
7. [[#7 样式与布局]] — CSS 变量主题、长者模式、设计 token、对齐原型
8. [[#8 安全]] — XSS 防护、输入验证、敏感数据、权限校验
9. [[#9 错误处理与日志]] — 静默吞错、生产日志、用户提示、ErrorBoundary
10. [[#10 定时器与副作用清理]] — setTimeout/setInterval 清理、useSafeTimeout
11. [[#11 开发环境]] — DevTools 卡死、source map、文件描述符限制
12. [[#12 审计与交付]] — 提交前检查、PR Review、里程碑审计
---
## 1 并发与请求层
### 规则 1.1 — 全局并发限制器
> **根因:** 微信 `wx.request` 并发上限为 10超出排队。无限制发请求导致饥饿。
> **案例:** ConcurrencyLimiter(8) 时长轮询占满 8 个槽位 25-30s所有新请求排队超时。
> **提交:** `9d50ef7` `74bffb4`
- [ ] 所有 HTTP 请求必须经过全局 `ConcurrencyLimiter`
- [ ] 并发上限 ≤ 12微信限制 10留 2 个给框架内部请求)
- [ ] Token 刷新必须直接调用 `Taro.request()` 绕过限流器(否则 401 重试时死锁)
```typescript
// ✅ 正确Token 刷新绕过限流器
async function doRefresh(): Promise<boolean> {
const res = await Taro.request({ url: refreshTokenUrl, method: 'POST', ... });
}
// ❌ 错误Token 刷新走限流器,所有槽位被占时死锁
async function doRefresh(): Promise<boolean> {
return api.post('/auth/refresh', { refresh_token }); // 死锁!
}
```
### 规则 1.2 — 长轮询独立通道
> **根因:** 长轮询 hang 25-30s 占用并发槽位,与普通 API 竞争。
> **案例:** 咨询页长轮询 + Tab 切换 = 开发者工具卡死。
> **提交:** `9d50ef7` `5baa518`
- [ ] 长轮询必须使用 `requestUnlimited`(绕过限流器的独立通道)
- [ ] 必须有连续失败上限(默认 10 次),达到后停止
- [ ] 必须有 generation counter 模式,组件卸载/参数变化时旧轮询自动失效
- [ ] 成功轮询间隔 ≥ 3 秒(不能 `delay=0` 立即递归,否则 CPU 飙升)
### 规则 1.3 — reLaunch 去重
> **根因:** 401 时多个并发请求同时触发 `Taro.reLaunch('/pages/login/index')`。
> **案例:** 3 个请求同时 401 → 3 次 reLaunch → 页面栈混乱。
> **提交:** `9d50ef7`
- [ ] `reLaunch` 必须全局去重Promise 锁 + 2s 冷却期)
- [ ] 401 重试上限为 1 次,避免无限循环
### 规则 1.4 — 请求缓存与去重
> **根因:** Tab 切换时同一 API 被多次并发调用,浪费请求且竞态更新。
> **案例:** 首页/健康页每次 useDidShow 都重新请求,快速切换 Tab 触发大量重复。
> **提交:** `6d151bb` `447126b`
- [ ] GET 请求必须有响应缓存TTL 60s同 URL 短时间内命中缓存
- [ ] 同时进行的相同 GET 请求必须去重inflight Promise 共享)
- [ ] 缓存按 `patientId` 隔离,切换患者时自动失效
- [ ] `clearRequestCache()` 在 logout、切换患者时调用
### 规则 1.5 — 请求层错误处理
> **提交:** `fcce2f5` `4ca9027`
- [ ] `catch` 块不允许完全静默(至少 `console.warn`
- [ ] Token 刷新函数必须有死锁风险注释
- [ ] 网络超时/异常必须有用户友好提示(不显示原始 error message
- [ ] 后端 `error_code` 必须有前端映射表(`ERROR_CODE_MAP`
---
## 2 导航与路由
### 规则 2.1 — 页栈溢出保护
> **根因:** 微信 `navigateTo` 页栈上限 10 层,超出 `navigateTo:fail`。
> **案例:** 患者列表→详情→报告→化验→趋势→咨询→...→第 10 层崩溃。
> **提交:** `59dd5ef` `74bffb4`
- [ ] 所有 `Taro.navigateTo` 必须替换为 `safeNavigateTo`
- [ ] `safeNavigateTo`:页栈 ≥ 9 时自动降级为 `redirectTo`
- [ ] 新代码 review 时全局搜索 `Taro.navigateTo`,不应存在任何直接调用
### 规则 2.2 — 分包预加载与拆包策略
> **根因:** 主包过大导致首次加载慢;单页分包浪费空间。
> **案例:** consultation 原在主包,移出后主包减重。
> **提交:** `59dd5ef` `4c38fcd`
- [ ] 高频入口页必须配置 `preloadRule`(首页预加载核心分包)
- [ ] 单页分包合并(多个单页合并为一个分包减少开销)
- [ ] 医生端独立分包(`pkg-doctor-core` / `pkg-doctor-clinical`
- [ ] TabBar 页面必须在 `app.config.ts` 正确声明
### 规则 2.3 — 页面生命周期防重入
> **根因:** `useEffect` + `usePageData`(useDidShow) 双重初始化。
> **案例:** 分包页 navigateTo 后超时,双重 API 调用叠加。
> **提交:** `1fd2c7a` `59dd5ef`
- [ ] 数据加载统一通过 `usePageData` 单次回调,不额外添加 `useEffect` 初始化
- [ ] 需要跳过首次 mount 的场景用 `mountedRef` 守卫
- [ ] 加载状态用 `loadingRef` 防止并发重复加载(不用 `useState`,避免异步竞态)
- [ ] `usePageData` 支持 `throttleMs` 防抖(默认 5000ms
---
## 3 组件与渲染
### 规则 3.1 — 禁止在 render body 内定义组件
> **根因:** 每次父组件 render 创建新的组件引用React 销毁旧实例重建新实例。
> **案例:** dialysis/create 的 InputField 在 render 内定义14 个 Input 每次 render 全部销毁重建,输入焦点丢失。
> **提交:** `fcce2f5`
- [ ] 组件必须定义在函数组件外部(模块顶层)
- [ ] 如需访问父组件状态,通过 props 传入value/onChange 回调)
- [ ] Code review 时搜索 `const Xxx = () =>` 出现在 return 之前、组件函数内部的模式
```typescript
// ✅ 正确:组件定义在模块顶层,通过 props 传值
function InputField({ label, value, onChange }: Props) {
return <View>...</View>;
}
// ❌ 错误:组件定义在 render body 内
export default function MyForm() {
const InputField = ({ label, field }) => ( <View>...</View> ); // 每次 render 重建!
}
```
### 规则 3.2 — 禁止双重 ScrollView
> **根因:** PageShell 默认 `scroll=true`,内层再嵌套 `ScrollView scrollY`,两容器冲突。
> **案例:** ai-report/list 页面滚动不流畅。
> **提交:** `fcce2f5`
- [ ] 使用内层 `ScrollView` 时,外层 `PageShell` 必须设置 `scroll={false}`
- [ ] 或者不使用内层 `ScrollView`,只用 `PageShell` 自带滚动
### 规则 3.3 — 图片懒加载
> **提交:** `fcce2f5`
- [ ] 列表页、聊天消息中的 `<Image>` 必须添加 `lazyLoad` 属性
- [ ] 首屏可见图片不加 `lazyLoad`(避免延迟渲染)
- [ ] 图片 URL 必须使用 HTTPS微信强制要求
### 规则 3.4 — 列表渲染优化
> **提交:** `74bffb4`
- [ ] 长列表使用虚拟滚动或分页加载(`onScrollToLower`
- [ ] 渲染层消息上限(如 `MAX_RENDER_MESSAGES = 200`),超出截断并提示
- [ ] 状态层消息上限(如 `MAX_STATE_MESSAGES = 300`),防止内存增长
### 规则 3.5 — 统一组件库使用
> **根因:** 早期页面各自用原生 View/Text 手写布局60 页面风格不统一。
> **案例:** 迁移了 66 页面到 PageShell + ContentCard + StatusTag + LoadingCard + SearchSection + PaginationBar。
> **提交:** `80794c9` → `900c9ba`12 次迁移提交)
- [ ] 新页面必须使用统一组件库:`PageShell` / `ContentCard` / `StatusTag` / `LoadingCard` / `SearchSection` / `PaginationBar` / `EmptyState` / `ErrorState`
- [ ] 不允许在页面中手写 `.card` / `.list-item` 等重复的卡片样式
- [ ] 需要新组件时先扩展现有组件(添加 prop不要新建
- [ ] 组件必须有独立的 `.scss` 文件,不允许在页面 `.scss` 中覆盖组件内部样式
### 规则 3.6 — React 导入
> **根因:** Taro 4.x 的 JSX transform 不自动注入 React但某些组件需要 React 引用。
> **案例:** 迁移组件库后运行时报 `React is not defined`。
> **提交:** `1786f0d`
- [ ] 使用 JSX 的文件必须显式 `import React from 'react'`(即使 Taro 4.x 有自动 transform
- [ ] 组件使用 `React` API`React.memo``React.useRef`)时必须有导入
- [ ] SCSS 导入路径使用 `./index.scss`(相对当前组件),不使用 `@/components/...` 绝对路径到 scss
---
## 4 数据与内存
### 规则 4.1 — 数组必须有上限
> **根因:** 未设上限的数组在长时间运行或高频数据场景下无限增长。
> **案例:** BLE 设备 readings 数组持续追加,长时间连接后占大量内存。
> **提交:** `fcce2f5`
- [ ] 所有累积型数组必须设 MAX 常量(如 `MAX_LIVE_READINGS = 200`
- [ ] 追加时检查长度,超出时 `slice(-MAX)` 保留最新数据
- [ ] 适用于:设备数据缓存、聊天消息、日志队列等
### 规则 4.2 — 去重索引与数据一致
> **根因:** `trimToMax` 丢弃旧数据后,去重索引 `seenKeys` 仍保留已丢弃数据的 key。
> **案例:** DataBuffer seenKeys 膨胀,拒绝本应接受的新数据。
> **提交:** `fcce2f5`
- [ ] 数据裁剪时必须同步重建去重索引
- [ ] `flush()`/`clear()` 时必须清空索引
- [ ] 不要依赖 `Set` 自动清理——它不会自动 shrink
### 规则 4.3 — Storage 使用规范
> **根因:** Storage 滥用导致数据残留、切换用户数据泄漏。
> **案例:** 用 Storage 传递页面间数据,导致详情页显示上一个患者的信息。
> **提交:** `0bf1822` `3c828bf`
- [ ] **禁止用 Storage 传递页面间数据** — 必须通过 API 获取或 URL 参数
- [ ] logout 时必须清理所有业务相关 Storage key
- [ ] 带动态 key 的缓存(如 `ble_buffer_{id}_{bucket}`)必须用 `getStorageInfoSync().keys` 遍历清理
- [ ] 模块级缓存变量JS 内存中的 `let` 变量)也必须在 logout 时重置
- [ ] 敏感数据token使用 `secure-storage`(加密),不使用 `Taro.setStorageSync` 明文存储
---
## 5 认证与会话
### 规则 5.1 — 模块级缓存必须可清理
> **根因:** `auth.ts` 顶层的缓存变量 logout 后不清除restore() 恢复已登出用户数据。
> **案例:** 切换账号后看到上一个用户的头像和名称。
> **提交:** `fcce2f5`
- [ ] 所有模块级缓存变量(`let` 声明的 JSON/对象缓存)必须在 logout 时重置为初始值
- [ ] `restore()` 方法必须在开头读取最新缓存,不依赖残留值
- [ ] restore() 内部做变更检测,无变化时跳过 `set()` 避免不必要的重渲染
### 规则 5.2 — Token 刷新安全
> **根因:** `getHeaders()` 中做同步 Token 刷新预检查,所有请求被阻塞。
> **案例:** getHeaders 中 `await tryRefreshToken()` 导致并发请求排队 30s。
> **提交:** `9d50ef7` `447126b`
- [ ] `getHeaders()` 中不做同步 Token 刷新预检查(仅依赖 401 重试路径)
- [ ] 刷新失败时必须清除所有认证相关 Storage
- [ ] 刷新进行中用 Promise 去重,防止多个并发请求同时触发刷新
- [ ] 刷新失败时设置 `isLoggingOut` 标记,防止后续请求反复尝试刷新
### 规则 5.3 — 页面级认证恢复
> **根因:** Tab 切换回来后 auth store 未恢复,页面显示为未登录状态。
> **案例:** 首页 Tab 正常 → 切到咨询 Tab → 回到首页,用户信息消失。
> **提交:** `6632985` `c314093`
- [ ] 所有 TabBar 页面必须在 `useDidShow` 中调用 `auth.restore()`
- [ ] 子页面依赖 `usePageData` 自动恢复
- [ ] 不在 `useEffect` 中重复恢复(避免双重初始化)
- [ ] 退出登录后刷新必须清除 `isLoggingOut` 标记(`clearLoggingOut()`),否则重新登录后请求被拒
### 规则 5.4 — 角色与权限
> **根因:** 前端未区分医生/护士/患者角色,所有页面入口暴露给所有用户。
> **案例:** 患者能看到医生端入口,点击后 403。
> **提交:** `c38967a` `81c174a` `3dac6a9`
- [ ] 医生端入口必须通过 `isDoctor()` / `isNurse()` 守卫
- [ ] 页面组件使用 `useDoctorClass()` 切换样式模式
- [ ] 精简菜单:未实现的模块入口必须移除,不显示空白页
---
## 6 前后端接口契约
### 规则 6.1 — 字段对齐
> **根因:** 前端 TypeScript 接口与后端 DTO 字段名/类型不一致,运行时静默失败。
> **案例:** 33 个接口端到端验证发现 8 个字段不对齐。
> **提交:** `8316281` `c53f562` `c38967a`
- [ ] 新增前端 service 函数时,必须对照后端 DTO 逐字段验证
- [ ] 后端 DTO 变更时,必须同步更新前端 TypeScript 接口
- [ ] 必填字段前端必须有默认值和校验
- [ ] 日期字段统一为 `string``YYYY-MM-DD`),数值字段注意 `number | undefined`
### 规则 6.2 — API 路径一致
> **根因:** 前端硬编码 API 路径,后端路由变更后前端 404。
> **案例:** FHIR 端点从 `/fhir` 迁移到 `/api/v1/fhir`,前端请求 404。
> **提交:** `4ca9027` `8316281`
- [ ] API 路径使用 `process.env.TARO_APP_API_URL` + 相对路径,不硬编码
- [ ] Service 层统一路径前缀,不在组件中直接拼接 URL
- [ ] 后端新增端点必须同步添加前端调用函数,不允许留空
### 规则 6.3 — 必填字段完整性
> **根因:** 前端提交表单缺少后端必填字段,导致 400 错误。
> **案例:** submitRecord 缺少 `task_id` 字段,后端 CreateFollowUpRecordReq 拒绝。
> **提交:** `fbb28e6` `603af83`
- [ ] 表单提交前检查所有后端 Required 字段是否已填
- [ ] 缺少字段时给出具体提示(如"缺少患者信息"),不是笼统的"操作失败"
- [ ] 隐式字段(如 `patient_id``version`)从路由参数或 store 获取,不依赖用户输入
---
## 7 样式与布局
### 规则 7.1 — CSS 变量主题
> **根因:** 硬编码 px 值和颜色号,无法统一调整。
> **案例:** 68 个 SCSS 文件手动 px 值,修改字号需要改 68 个文件。
> **提交:** `890c132` `551d19d`
- [ ] 所有颜色、字号、间距使用 `var(--tk-*)` 设计 token
- [ ] 不允许硬编码颜色值(如 `#333``rgb(0,0,0)`
- [ ] 深色模式通过 CSS 变量级联覆盖,不使用条件 class 切换
- [ ] 新增 design token 在 `tokens.scss` 统一定义,不在页面中 `--custom-var`
### 规则 7.2 — 长者模式
> **根因:** 线性放大(所有 px 乘以 1.2)导致布局错乱。
> **案例:** 长者模式字号过大,卡片内容溢出。
> **提交:** `4335f7e` `257ca94`
- [ ] 所有页面字号 ≥ 22px长者模式使用 `.elder-mode` CSS 变量覆盖
- [ ] 非线性放大:标题放大比例 > 正文(如 h1: 1.3x, body: 1.15x
- [ ] 触摸目标 ≥ 44px
- [ ] 长者模式排除特定页面(如登录页,否则键盘遮挡)
### 规则 7.3 — 医生/患者双模式
> **提交:** `95e219a`
- [ ] 医生端使用 `.doctor-mode` 靛蓝覆盖
- [ ] 共用页面通过 `useDoctorClass()` / `useElderClass()` 切换
- [ ] 不为同一页面维护两套代码
### 规则 7.4 — 对齐设计原型
> **根因:** 开发时凭感觉写 UI与设计稿差距大。
> **案例:** 文章列表/详情页原型对齐、首页渐变头部移除、Profile 积分卡片等分。
> **提交:** `b8ce19f` `b84becf` `63d8b7a` `7b2c033` `6c42d54` `29d77e8`
- [ ] 新页面开发前先看原型稿,记录关键字号/间距/颜色
- [ ] 完成后截图与原型对比(不是"差不多",是像素级)
- [ ] 组件变体(如 SegmentTabs pill样式必须与原型一致
- [ ] 卡片布局必须用 ContentCard不手写 div 模拟
### 规则 7.5 — 状态色统一
> **根因:** 各页面自行定义状态颜色(绿色/红色/黄色),色调不一致。
> **提交:** `4a95a83`
- [ ] 状态色必须对齐设计系统色板(成功绿/警告黄/危险红/信息蓝)
- [ ] 使用 StatusTag 组件显示状态,不自行拼写 class 名
- [ ] CSS 变量定义一次,全局引用
---
## 8 安全
### 规则 8.1 — XSS 防护
> **根因:** AI 报告内容直接用 `dangerouslySetInnerHTML` 渲染,未过滤。
> **案例:** AI 分析结果注入 `<script>` 标签。
> **提交:** `931edc3` `8f35394`
- [ ] 所有用户输入和外部内容AI 生成、富文本)必须经过 `sanitizeHtml`
- [ ] Markdown → HTML 必须经过 sanitize
- [ ] 不使用 `dangerouslySetInnerHTML`,用 Taro `<RichText nodes={...}>` 替代
### 规则 8.2 — 输入验证
> **根因:** 前端不做验证,后端 400 错误不明确。
> **案例:** 患者创建空名称成功后端无校验日期格式不对dayjs 对象未格式化)。
> **提交:** `3424a33` `603af83`
- [ ] 所有表单提交前验证必填字段
- [ ] 日期字段必须格式化为 `YYYY-MM-DD` 字符串(不传 dayjs 对象)
- [ ] 数值字段必须转为 `number`(不传字符串)
- [ ] 后端返回 Validation 错误时,前端展示具体字段提示
### 规则 8.3 — 敏感数据
> **根因:** Token 明文存储在 Storage微信开发者工具可直接查看。
> **提交:** `83fe89c` `447126b`
- [ ] Access token / refresh token 必须使用 `secure-storage`AES-256-GCM 加密)
- [ ] 不在 URL 参数中传递 token
- [ ] 不在 `console.log` 中打印 token
- [ ] 请求头中的 token 通过 `getHeaders()` 统一注入,不在组件中手动拼接
### 规则 8.4 — 权限校验
> **根因:** 前端不做权限校验,依赖后端 403 拦截。
> **案例:** 患者端用了 `health.health-data.list` 权限码,应该是 `health.points.list`。
> **提交:** `3424a33` `c38967a`
- [ ] 医生端接口必须使用医生端 service`services/doctor/`),不用患者端 service
- [ ] 权限码拼写与后端 handler 一致(复数形式如 `alerts.manage` 不是 `alert.manage`
- [ ] 关键操作(删除、关闭会话)必须有二次确认弹窗
---
## 9 错误处理与日志
### 规则 9.1 — 生产日志保留策略
> **根因:** 生产构建移除了所有 console异常时无法排查。
> **提交:** `59dd5ef` `dc98394`
- [ ] 生产构建保留 `console.warn``console.error`
- [ ] 仅移除 `console.log``console.info``console.debug`
- [ ] 关键路径(请求层、认证层)的异常必须有 `console.warn`
```javascript
// config/prod.ts
pure_funcs: ['console.log', 'console.info', 'console.debug'],
```
### 规则 9.2 — 用户友好错误提示
> **提交:** `4ca9027` `3424a33`
- [ ] 错误映射表 `ERROR_CODE_MAP` 覆盖所有后端 error_code
- [ ] 不向用户展示原始 error message 或 HTTP 状态码
- [ ] 网络超时、服务器错误、权限不足各有独立提示文案
### 规则 9.3 — ErrorBoundary
> **根因:** 页面 JS 异常导致白屏,无降级 UI。
> **提交:** `a63043f`(第一版)`8f35394`(增强版)
- [ ] 全局 `ErrorBoundary` 包裹 App 根组件
- [ ] ErrorBoundary 必须有降级 UI"页面出错了,点击重试"
- [ ] 不在 ErrorBoundary 中吃掉异常(至少 `console.error`
### 规则 9.4 — Markdown 渲染安全
> **根因:** 简单正则无法正确处理 HTML 分组。
> **案例:** AI 报告每个 `<li>` 独立成列表。
> **提交:** `fcce2f5`
- [ ] Markdown → HTML 转换必须正确处理连续同类标签的分组
- [ ] 使用 `sanitizeHtml` 防止 XSS
- [ ] 复杂 Markdown 场景考虑引入成熟库(如 marked + DOMPurify
---
## 10 定时器与副作用清理
### 规则 10.1 — setTimeout 必须清理
> **根因:** `setTimeout(() => Taro.navigateBack(), 1000)` 在组件卸载后仍执行,操作已不存在的页面。
> **案例:** 10 个页面有此问题。
> **提交:** `fed1759`
- [ ] 所有 `setTimeout` 必须使用 `useSafeTimeout` hook组件卸载自动清理
- [ ] 不允许裸写 `setTimeout`(除非在 try/catch 中且有 clearTimeout
```typescript
// ✅ 正确
const { safeSetTimeout } = useSafeTimeout();
safeSetTimeout(() => Taro.navigateBack(), 1000);
// ❌ 错误
setTimeout(() => Taro.navigateBack(), 1000); // 组件卸载后仍执行!
```
### 规则 10.2 — useEffect 清理
- [ ] `useEffect` 中注册的事件监听、BLE 通知必须在 cleanup 中移除
- [ ] 异步操作必须有 `mountedRef``abort controller` 守卫
- [ ] 长轮询使用 generation counter不需要手动清理 timer
---
## 11 开发环境
### 规则 11.1 — DevTools 性能优化
> **根因:** Vite dev server 默认配置导致开发者工具卡顿。
> **案例:** filesystem 缓存未开启 → 每次编译从零开始source map → EMFILE。
> **提交:** `0f6f7a2` `9c7ce93`
- [ ] `config/dev.ts` 开启 filesystem 缓存(`cacheDir`
- [ ] 真机调试时关闭 source map`sourceMapType: ''`
- [ ] 开启 prebundle 减少 Vite 按需编译开销
- [ ] 真机调试 EMFILE 时检查文件描述符限制
### 规则 11.2 — project.config.json
> **根因:** 自动化端口未开启MCP 无法连接。
> **提交:** `c314093` `8f35394`
- [ ] `automationAudits` 必须为 `true`MCP 自动化测试前提)
- [ ] `urlCheck` 设为 `false`(开发环境不检查合法域名)
- [ ] `compileHotReLoad` 开启加速开发迭代
---
## 12 审计与交付
### 规则 12.1 — 提交前必检清单
每次提交小程序代码前,逐项确认:
- [ ] `grep -r "Taro.navigateTo" src/` — 无直接调用(全部用 `safeNavigateTo`
- [ ] `grep -r "const.*=.*() =>" src/` — render body 内无组件定义
- [ ] `grep -r "ScrollView" src/` — 无双重 ScrollView 嵌套
- [ ] 长轮询使用 `requestUnlimited` 而非 `api.get`
- [ ] 数组累积操作有 MAX 上限
- [ ] `<Image>` 列表使用 `lazyLoad`
- [ ] 新页面使用统一组件库PageShell / ContentCard 等)
- [ ] 前端 service 函数与后端 DTO 字段对齐
- [ ] `setTimeout` 使用 `useSafeTimeout`
- [ ] 表单提交验证必填字段
### 规则 12.2 — PR Review 检查点
| 检查项 | 搜索模式 | 期望结果 |
|--------|----------|----------|
| 直接导航 | `Taro.navigateTo` | 0 匹配 |
| render body 组件 | `const [A-Z].*=.*(=>\|{)` 在 return 前 | 0 匹配 |
| 双重滚动 | `PageShell` 子树含 `ScrollView` | 有 ScrollView 则 `scroll={false}` |
| 长轮询限流 | `pollFn:.*api\.(get\|post)` | 应使用 `requestUnlimited` |
| 无上限数组 | `.push(` 无后续 slice | 应有 MAX 截断 |
| 模块缓存泄漏 | logout 方法不含缓存重置 | 应清空所有模块级变量 |
| 硬编码样式 | `#[0-9a-f]{3,6}``\d+px` | 应使用 `var(--tk-*)` |
| Storage 传数据 | `setStorageSync.*detail` | 应通过 API 获取 |
| 裸 setTimeout | `setTimeout(` 无 useSafeTimeout | 应使用 hook |
| XSS 风险 | `dangerouslySetInnerHTML` | 应使用 `RichText` + sanitize |
| 明文 token | `setStorageSync.*token` | 应使用 `secureSet` |
### 规则 12.3 — 里程碑审计
每个功能里程碑完成后,执行:
- [ ] 全页面导航测试(覆盖 10 层页栈场景)
- [ ] Tab 快速切换测试(连续切换 5 次 Tab无卡死
- [ ] 弱网测试3G 模拟,长轮询不阻塞 UI
- [ ] 内存泄漏检查(长时间停留后页面不卡顿)
- [ ] 登录/登出流程(切换账号后数据正确)
- [ ] 多角色测试(患者/医生/护士/管理员各走一遍核心链路)
- [ ] 长者模式覆盖(所有页面 ≥ 22px触摸目标 ≥ 44px
- [ ] 设计原型对比(截图与原型稿像素级对比)
---
## 附录 A — 问题溯源表(全量)
以下每条规则对应的具体 bug 和修复提交,按规则编号排序:
| 规则 | 问题 | 严重度 | 首次修复提交 |
|------|------|--------|-------------|
| 1.1 并发限制器 | 长轮询占满槽位导致请求饥饿 | CRITICAL | `9d50ef7` |
| 1.2 长轮询通道 | 咨询页长轮询阻塞所有 API | CRITICAL | `9d50ef7` |
| 1.3 reLaunch 去重 | 401 多次 reLaunch 崩溃 | HIGH | `9d50ef7` |
| 1.4 请求缓存去重 | Tab 切换重复请求 + 竞态更新 | MEDIUM | `6d151bb` |
| 1.5 错误处理 | tryRefreshToken 静默吞异常 | HIGH | `a63043f` |
| 2.1 页栈保护 | 深层导航超 10 层失败 | HIGH | `59dd5ef` |
| 2.2 分包策略 | consultation 在主包增大体积 | MEDIUM | `4c38fcd` |
| 2.3 防重入 | useEffect + usePageData 双重加载 | MEDIUM | `1fd2c7a` |
| 3.1 render body | InputField 每次 render 重建 | HIGH | `fcce2f5` |
| 3.2 双重 ScrollView | ai-report/list 滚动冲突 | CRITICAL | `fcce2f5` |
| 3.3 图片懒加载 | 咨询详情图片无 lazyLoad | MEDIUM | `fcce2f5` |
| 3.4 列表优化 | 聊天消息无上限 DOM 爆炸 | MEDIUM | `74bffb4` |
| 3.5 统一组件库 | 60 页面各自手写布局 | HIGH | `80794c9` |
| 3.6 React 导入 | 组件库运行时 React is not defined | HIGH | `1786f0d` |
| 4.1 数组上限 | BLE readings 无限增长 | MEDIUM | `fcce2f5` |
| 4.2 索引一致 | DataBuffer seenKeys 不清理 | MEDIUM | `fcce2f5` |
| 4.3 Storage 传数据 | 详情页用 Storage 传递数据 | HIGH | `0bf1822` |
| 5.1 缓存清理 | auth.ts 模块缓存 logout 未清 | MEDIUM | `fcce2f5` |
| 5.2 Token 安全 | getHeaders 同步刷新阻塞 30s | CRITICAL | `9d50ef7` |
| 5.3 认证恢复 | Tab 切换后 auth store 未恢复 | HIGH | `6632985` |
| 5.4 角色权限 | 患者看到医生端入口 | MEDIUM | `c38967a` |
| 6.1 字段对齐 | 33 接口前端/后端字段不匹配 | HIGH | `8316281` |
| 6.2 API 路径 | FHIR 端点迁移后前端 404 | MEDIUM | `8316281` |
| 6.3 必填字段 | submitRecord 缺 task_id 后端 400 | HIGH | `fbb28e6` |
| 7.1 CSS 变量 | 68 SCSS 硬编码 px | MEDIUM | `890c132` |
| 7.2 长者模式 | 线性放大导致布局错乱 | MEDIUM | `4335f7e` |
| 7.4 对齐原型 | 文章页/首页/Profile 与原型差距大 | HIGH | `63d8b7a` |
| 7.5 状态色 | 各页面状态颜色不一致 | LOW | `4a95a83` |
| 8.1 XSS | AI 报告内容未过滤 | CRITICAL | `931edc3` |
| 8.2 输入验证 | 患者空名称创建成功 | HIGH | `603af83` |
| 8.3 敏感数据 | Token 明文存储 | HIGH | `447126b` |
| 8.4 权限码 | 积分端点权限码错误 403 | HIGH | `3424a33` |
| 9.1 生产日志 | safeReLaunch 静默吞错 | LOW | `fcce2f5` |
| 9.3 ErrorBoundary | 页面异常白屏无降级 | HIGH | `a63043f` |
| 9.4 Markdown | li 元素未合并到 ul | MEDIUM | `fcce2f5` |
| 10.1 setTimeout | 10 页面 setTimeout 无清理 | HIGH | `fed1759` |
| 11.1 DevTools | 开发者工具编译卡顿 | MEDIUM | `0f6f7a2` |
| 11.2 project.config | MCP 自动化端口未开启 | LOW | `c314093` |
## 附录 B — 快速自查脚本
```bash
# 在 apps/miniprogram/ 目录下运行
echo "=== 1. 直接 Taro.navigateTo ==="
grep -rn "Taro\.navigateTo" src/ --include="*.tsx" --include="*.ts"
echo "=== 2. render body 组件定义 ==="
grep -rn "const [A-Z].*=.*=>" src/ --include="*.tsx" | head -20
echo "=== 3. ScrollView 嵌套 ==="
grep -rn "ScrollView" src/ --include="*.tsx" -l | while read f; do
if grep -q "PageShell" "$f" && grep -q "ScrollView" "$f"; then
if ! grep -q "scroll={false}" "$f"; then
echo "⚠️ $f — PageShell + ScrollView 但无 scroll={false}"
fi
fi
done
echo "=== 4. 长轮询未用 requestUnlimited ==="
grep -rn "pollFn.*api\.\(get\|post\)" src/ --include="*.tsx"
echo "=== 5. 无上限数组累积 ==="
grep -rn "\.push(" src/ --include="*.ts" -A2 | grep -v "slice\|MAX\|limit" | head -20
echo "=== 6. Image 无 lazyLoad列表页==="
grep -rn "<Image" src/ --include="*.tsx" -l | while read f; do
if grep -q "\.map(" "$f" && ! grep -q "lazyLoad" "$f"; then
echo "⚠️ $f — 列表页 Image 无 lazyLoad"
fi
done
echo "=== 7. 硬编码样式 ==="
grep -rn "#[0-9a-fA-F]\{3,6\}" src/ --include="*.scss" | grep -v "var(--" | head -10
grep -rn "[0-9]\+px" src/ --include="*.scss" | grep -v "var(--\|token" | head -10
echo "=== 8. 裸 setTimeout ==="
grep -rn "setTimeout(" src/ --include="*.tsx" | grep -v "useSafeTimeout\|safeSetTimeout\|clearTimeout" | head -10
echo "=== 9. Storage 传数据 ==="
grep -rn "setStorageSync.*detail\|getStorageSync.*detail" src/ --include="*.tsx" --include="*.ts"
echo "=== 10. 明文 token ==="
grep -rn "setStorageSync.*token\|getStorageSync.*token" src/ --include="*.ts" | grep -v "secure"
echo "=== 检查完成 ==="
```
## 附录 C — 统计概览
| 类别 | 规则数 | 对应修复提交数 | 涉及 CRITICAL/HIGH |
|------|--------|---------------|-------------------|
| 1. 并发与请求层 | 5 | 6 | 3 |
| 2. 导航与路由 | 3 | 5 | 2 |
| 3. 组件与渲染 | 6 | 16 | 3 |
| 4. 数据与内存 | 3 | 3 | 1 |
| 5. 认证与会话 | 4 | 7 | 2 |
| 6. 前后端接口契约 | 3 | 4 | 3 |
| 7. 样式与布局 | 5 | 10 | 1 |
| 8. 安全 | 4 | 6 | 3 |
| 9. 错误处理与日志 | 4 | 5 | 1 |
| 10. 定时器与副作用 | 2 | 1 | 1 |
| 11. 开发环境 | 2 | 3 | 0 |
| 12. 审计与交付 | 3 | — | — |
| **合计** | **44** | **66+** | **20** |