从 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
29 KiB
小程序质量规范清单
最后更新: 2026-05-17 | 来源:164 条 git 提交历史 + 两轮深度审计,全量问题模式抽象
用途: 新页面/新模块开发时的自检清单,PR Review 的检查依据,新项目启动时的基线规范。每条规则都来自真实 bug 修复,不是理论推导。
目录
- #1 并发与请求层 — 限流器、长轮询、Token 刷新、缓存策略
- #2 导航与路由 — 页栈保护、reLaunch 去重、分包预加载
- #3 组件与渲染 — render body 定义、ScrollView 嵌套、懒加载、统一组件库
- #4 数据与内存 — 数组上限、去重索引一致性、Storage 清理
- #5 认证与会话 — 模块级缓存清理、logout 完整性、认证恢复
- #6 前后端接口契约 — 字段对齐、类型同步、API 路径一致
- #7 样式与布局 — CSS 变量主题、长者模式、设计 token、对齐原型
- #8 安全 — XSS 防护、输入验证、敏感数据、权限校验
- #9 错误处理与日志 — 静默吞错、生产日志、用户提示、ErrorBoundary
- #10 定时器与副作用清理 — setTimeout/setInterval 清理、useSafeTimeout
- #11 开发环境 — DevTools 卡死、source map、文件描述符限制
- #12 审计与交付 — 提交前检查、PR Review、里程碑审计
1 并发与请求层
规则 1.1 — 全局并发限制器
根因: 微信
wx.request并发上限为 10,超出排队。无限制发请求导致饥饿。 案例: ConcurrencyLimiter(8) 时长轮询占满 8 个槽位 25-30s,所有新请求排队超时。 提交:9d50ef774bffb4
- 所有 HTTP 请求必须经过全局
ConcurrencyLimiter - 并发上限 ≤ 12(微信限制 10,留 2 个给框架内部请求)
- Token 刷新必须直接调用
Taro.request()绕过限流器(否则 401 重试时死锁)
// ✅ 正确: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 切换 = 开发者工具卡死。 提交:
9d50ef75baa518
- 长轮询必须使用
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 触发大量重复。 提交:
6d151bb447126b
- GET 请求必须有响应缓存(TTL 60s),同 URL 短时间内命中缓存
- 同时进行的相同 GET 请求必须去重(inflight Promise 共享)
- 缓存按
patientId隔离,切换患者时自动失效 clearRequestCache()在 logout、切换患者时调用
规则 1.5 — 请求层错误处理
提交:
fcce2f54ca9027
catch块不允许完全静默(至少console.warn)- Token 刷新函数必须有死锁风险注释
- 网络超时/异常必须有用户友好提示(不显示原始 error message)
- 后端
error_code必须有前端映射表(ERROR_CODE_MAP)
2 导航与路由
规则 2.1 — 页栈溢出保护
根因: 微信
navigateTo页栈上限 10 层,超出navigateTo:fail。 案例: 患者列表→详情→报告→化验→趋势→咨询→...→第 10 层崩溃。 提交:59dd5ef74bffb4
- 所有
Taro.navigateTo必须替换为safeNavigateTo safeNavigateTo:页栈 ≥ 9 时自动降级为redirectTo- 新代码 review 时全局搜索
Taro.navigateTo,不应存在任何直接调用
规则 2.2 — 分包预加载与拆包策略
根因: 主包过大导致首次加载慢;单页分包浪费空间。 案例: consultation 原在主包,移出后主包减重。 提交:
59dd5ef4c38fcd
- 高频入口页必须配置
preloadRule(首页预加载核心分包) - 单页分包合并(多个单页合并为一个分包减少开销)
- 医生端独立分包(
pkg-doctor-core/pkg-doctor-clinical) - TabBar 页面必须在
app.config.ts正确声明
规则 2.3 — 页面生命周期防重入
根因:
useEffect+usePageData(useDidShow) 双重初始化。 案例: 分包页 navigateTo 后超时,双重 API 调用叠加。 提交:1fd2c7a59dd5ef
- 数据加载统一通过
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 之前、组件函数内部的模式
// ✅ 正确:组件定义在模块顶层,通过 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) - 组件使用
ReactAPI(如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 传递页面间数据,导致详情页显示上一个患者的信息。 提交:
0bf18223c828bf
- 禁止用 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。 提交:9d50ef7447126b
getHeaders()中不做同步 Token 刷新预检查(仅依赖 401 重试路径)- 刷新失败时必须清除所有认证相关 Storage
- 刷新进行中用 Promise 去重,防止多个并发请求同时触发刷新
- 刷新失败时设置
isLoggingOut标记,防止后续请求反复尝试刷新
规则 5.3 — 页面级认证恢复
根因: Tab 切换回来后 auth store 未恢复,页面显示为未登录状态。 案例: 首页 Tab 正常 → 切到咨询 Tab → 回到首页,用户信息消失。 提交:
6632985c314093
- 所有 TabBar 页面必须在
useDidShow中调用auth.restore() - 子页面依赖
usePageData自动恢复 - 不在
useEffect中重复恢复(避免双重初始化) - 退出登录后刷新必须清除
isLoggingOut标记(clearLoggingOut()),否则重新登录后请求被拒
规则 5.4 — 角色与权限
根因: 前端未区分医生/护士/患者角色,所有页面入口暴露给所有用户。 案例: 患者能看到医生端入口,点击后 403。 提交:
c38967a81c174a3dac6a9
- 医生端入口必须通过
isDoctor()/isNurse()守卫 - 页面组件使用
useDoctorClass()切换样式模式 - 精简菜单:未实现的模块入口必须移除,不显示空白页
6 前后端接口契约
规则 6.1 — 字段对齐
根因: 前端 TypeScript 接口与后端 DTO 字段名/类型不一致,运行时静默失败。 案例: 33 个接口端到端验证发现 8 个字段不对齐。 提交:
8316281c53f562c38967a
- 新增前端 service 函数时,必须对照后端 DTO 逐字段验证
- 后端 DTO 变更时,必须同步更新前端 TypeScript 接口
- 必填字段前端必须有默认值和校验
- 日期字段统一为
string(YYYY-MM-DD),数值字段注意number | undefined
规则 6.2 — API 路径一致
根因: 前端硬编码 API 路径,后端路由变更后前端 404。 案例: FHIR 端点从
/fhir迁移到/api/v1/fhir,前端请求 404。 提交:4ca90278316281
- API 路径使用
process.env.TARO_APP_API_URL+ 相对路径,不硬编码 - Service 层统一路径前缀,不在组件中直接拼接 URL
- 后端新增端点必须同步添加前端调用函数,不允许留空
规则 6.3 — 必填字段完整性
根因: 前端提交表单缺少后端必填字段,导致 400 错误。 案例: submitRecord 缺少
task_id字段,后端 CreateFollowUpRecordReq 拒绝。 提交:fbb28e6603af83
- 表单提交前检查所有后端 Required 字段是否已填
- 缺少字段时给出具体提示(如"缺少患者信息"),不是笼统的"操作失败"
- 隐式字段(如
patient_id、version)从路由参数或 store 获取,不依赖用户输入
7 样式与布局
规则 7.1 — CSS 变量主题
根因: 硬编码 px 值和颜色号,无法统一调整。 案例: 68 个 SCSS 文件手动 px 值,修改字号需要改 68 个文件。 提交:
890c132551d19d
- 所有颜色、字号、间距使用
var(--tk-*)设计 token - 不允许硬编码颜色值(如
#333、rgb(0,0,0)) - 深色模式通过 CSS 变量级联覆盖,不使用条件 class 切换
- 新增 design token 在
tokens.scss统一定义,不在页面中--custom-var
规则 7.2 — 长者模式
根因: 线性放大(所有 px 乘以 1.2)导致布局错乱。 案例: 长者模式字号过大,卡片内容溢出。 提交:
4335f7e257ca94
- 所有页面字号 ≥ 22px(长者模式),使用
.elder-modeCSS 变量覆盖 - 非线性放大:标题放大比例 > 正文(如 h1: 1.3x, body: 1.15x)
- 触摸目标 ≥ 44px
- 长者模式排除特定页面(如登录页,否则键盘遮挡)
规则 7.3 — 医生/患者双模式
提交:
95e219a
- 医生端使用
.doctor-mode靛蓝覆盖 - 共用页面通过
useDoctorClass()/useElderClass()切换 - 不为同一页面维护两套代码
规则 7.4 — 对齐设计原型
根因: 开发时凭感觉写 UI,与设计稿差距大。 案例: 文章列表/详情页原型对齐、首页渐变头部移除、Profile 积分卡片等分。 提交:
b8ce19fb84becf63d8b7a7b2c0336c42d5429d77e8
- 新页面开发前先看原型稿,记录关键字号/间距/颜色
- 完成后截图与原型对比(不是"差不多",是像素级)
- 组件变体(如 SegmentTabs pill)样式必须与原型一致
- 卡片布局必须用 ContentCard,不手写 div 模拟
规则 7.5 — 状态色统一
根因: 各页面自行定义状态颜色(绿色/红色/黄色),色调不一致。 提交:
4a95a83
- 状态色必须对齐设计系统色板(成功绿/警告黄/危险红/信息蓝)
- 使用 StatusTag 组件显示状态,不自行拼写 class 名
- CSS 变量定义一次,全局引用
8 安全
规则 8.1 — XSS 防护
根因: AI 报告内容直接用
dangerouslySetInnerHTML渲染,未过滤。 案例: AI 分析结果注入<script>标签。 提交:931edc38f35394
- 所有用户输入和外部内容(AI 生成、富文本)必须经过
sanitizeHtml - Markdown → HTML 必须经过 sanitize
- 不使用
dangerouslySetInnerHTML,用 Taro<RichText nodes={...}>替代
规则 8.2 — 输入验证
根因: 前端不做验证,后端 400 错误不明确。 案例: 患者创建空名称成功(后端无校验);日期格式不对(dayjs 对象未格式化)。 提交:
3424a33603af83
- 所有表单提交前验证必填字段
- 日期字段必须格式化为
YYYY-MM-DD字符串(不传 dayjs 对象) - 数值字段必须转为
number(不传字符串) - 后端返回 Validation 错误时,前端展示具体字段提示
规则 8.3 — 敏感数据
根因: Token 明文存储在 Storage,微信开发者工具可直接查看。 提交:
83fe89c447126b
- 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。 提交:3424a33c38967a
- 医生端接口必须使用医生端 service(
services/doctor/),不用患者端 service - 权限码拼写与后端 handler 一致(复数形式如
alerts.manage不是alert.manage) - 关键操作(删除、关闭会话)必须有二次确认弹窗
9 错误处理与日志
规则 9.1 — 生产日志保留策略
根因: 生产构建移除了所有 console,异常时无法排查。 提交:
59dd5efdc98394
- 生产构建保留
console.warn和console.error - 仅移除
console.log、console.info、console.debug - 关键路径(请求层、认证层)的异常必须有
console.warn
// config/prod.ts
pure_funcs: ['console.log', 'console.info', 'console.debug'],
规则 9.2 — 用户友好错误提示
提交:
4ca90273424a33
- 错误映射表
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必须使用useSafeTimeouthook,组件卸载自动清理 - 不允许裸写
setTimeout(除非在 try/catch 中且有 clearTimeout)
// ✅ 正确
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。 提交:
0f6f7a29c7ce93
config/dev.ts开启 filesystem 缓存(cacheDir)- 真机调试时关闭 source map(
sourceMapType: '') - 开启 prebundle 减少 Vite 按需编译开销
- 真机调试 EMFILE 时检查文件描述符限制
规则 11.2 — project.config.json
根因: 自动化端口未开启,MCP 无法连接。 提交:
c3140938f35394
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 — 快速自查脚本
# 在 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 |