Files
zt-closet-wxmini-frontend/.codebuddy/plans/fix-full-audit-issues-p0-p2_3b34adcf.md
T
2026-09-13 01:51:09 +08:00

12 KiB
Raw Blame History

name, overview, todos
name overview todos
fix-full-audit-issues-p0-p2 按审查报告修复小程序全部已确认缺陷:5 项阻断(支付重复下单、支付轮询定时器泄漏、401 无限重试、uploadFile 无 401 处理、衣柜分享无出口且失效时白屏)、12 项一般(通知红点不同步、运势空卡与定时器、列表竞态与判空、11000 未处理、token 过期判断、请求无超时、uuid 缺失卡 loading 等)、8 项轻微(提交锁、死代码、硬编码 key、状态枚举等)。
id content status
request-core 加固 request.ts 与 auth.ts:重试上限、11000、超时、过期预刷新、uploadFile 登录校验 completed
id content status
paged-util 新增 utils/paged.ts 的 normalizePaged 并接入全部列表判空 completed
id content status dependencies
mall-pay 修复商城支付:confirmPay 提交锁、轮询定时器随页面卸载清理、按钮 loading completed
request-core
id content status dependencies
share-detail 修复分享详情:uuid 兜底、错误态与去登录、过期校验、转发出口、定时器清理 completed
request-core
id content status dependencies
list-race 为衣橱、穿搭、分享、订单、通知、商城加请求竞态保护与 loadingMore 拆分 completed
paged-util
id content status dependencies
state-fix 修复运势错误态与重试、定时器与跨天、通知红点同步、衣物详情空态 completed
request-core
id content status dependencies
submit-locks 修复穿搭编辑与分享创建:跨分类已选不丢、选衣分页、提交防重复与失败提示 completed
paged-util
id content status dependencies
p2-cleanup 清理轻微项:资料保存锁与校验、跨零点、storage key、红点节流、倒计时、枚举、组件 data、死代码 completed
request-core
id content status dependencies
verify-all 执行 tsc 类型检查并逐条复核修复效果 completed
request-core
mall-pay
share-detail
list-race
state-fix
submit-locks
p2-cleanup

产品概述

微信小程序「纸条AI衣橱」。上一轮全链路审查输出了分级缺陷清单(阻断 5 项、一般 12 项、轻微 8 项),用户明确要求「全部修复」,即 P0 + P1 + P2 全量处理,不做取舍。

核心修复项

  • 支付与上传:商城下单加提交锁、支付轮询定时器随页面卸载清理、uploadFile 补齐登录校验与 401 处理、上传失败给明确提示
  • 请求内核:401 重试加上限防请求风暴、补 11000 登录失效码、全请求超时兜底、token 过期主动预刷新、静默请求不清登录态(已修,保持)
  • 衣柜分享闭环:分享详情页补 uuid 空值兜底、错误态、本地过期/封禁校验、转发出口(onShareAppMessage + 分享按钮)、定时器清理
  • 列表健壮性:统一分页空值兜底与请求竞态保护、拆分 loading 与 loadingMore,覆盖衣橱/穿搭/分享/订单/通知/商城
  • 选择页与提交:穿搭编辑、衣柜分享创建的衣物选择跨分类不丢、列表分页、提交防重复
  • 体验修复:运势页错误态与重试、定时器清理与跨天重算、通知已读后红点同步、衣物详情 uuid 缺失空态
  • 轻微清理:资料保存锁与校验、生日选择器跨零点、硬编码 storage key 收编、红点节流失败回滚、订单倒计时走动、分享状态枚举化、组件 data 补齐、死代码移除

效果验收

开发者工具清缓存重新编译后:无重复下单、无页面卸载后残留请求、弱网有超时提示、分享页可转发且无白屏、列表快速切换不串数据、运势失败可见可重试、通知已读后红点即时更新。

技术栈

微信小程序原生框架 + TypeScript(useCompilerPlugins: typescript,基础库 3.17.2,渲染引擎已切回 webview),无测试框架,通过微信开发者工具编译调试。类型检查命令:cd /root/zt-closet-wxmini-frontend && npx --yes -p typescript@5.4.5 tsc --noEmit -p tsconfig.json。

实现思路

分四层收敛,全部复用项目既有写法(Page 对象字面量 + setData + request 封装),不引入新模式:

  1. 请求内核层(最优先,其余修复依赖它)
  • 登录失效码收敛为常量集合 11000/11001/11002/11004 或 HTTP 401(utils/config.ts:118-121 已定义这些文案)。
  • doCall(retried) 加重试标记:刷新成功后仅允许重试一次,第二次仍命中失效码则直接清 token + handleAuthExpired() + reject,杜绝无限「刷新→重试」风暴。
  • auth.ts 新增 isTokenExpired():单位自动兼容(expireTime > 1e12 视为毫秒,否则秒),留 30s 提前量;isLoggedIn() 语义保持不变(仅判断 token 存在),避免误杀。request 在 needAuth 且 token 已过期时先 await doRefresh() 再发请求,省掉一次 401 往返。
  • 统一超时:业务请求与 refresh 15s,uploadFile 30s。
  • uploadFile 补齐:未登录快速失败(复用 NOT_LOGIN_CODE)、登录失效码 → 清 token + handleAuthExpired()、支持 showError/timeout 参数。
  1. 页面状态机层
  • 新增 utils/paged.ts 提供 normalizePaged,统一 (res && res.list) || [] 与 (res && res.total) || 0 兜底,消除 7 处重复且易漏的 res.list.map。
  • 列表页统一三件套:reqSeq(响应序号不匹配即丢弃)、loading(首次/刷新)与 loadingMore(上拉)分离、onUnload 定时器清理。涉及 wardrobe、outfit、closet-share、order、notification、mall、closet-share-create、outfit-edit。
  1. 防重复提交层
  • 商城 confirmPay、衣柜分享创建 onConfirm、穿搭编辑 onSave、资料保存 onSaveProfile 统一加 submitting/saving 布尔闸门,入口处 return,catch 与成功分支复位,按钮文案联动。
  1. 错误可见层
  • 运势页新增 error 态 + 重试按钮(wxml 内容区由 wx:else 改为 wx:elif 并补错误分支);衣物详情 uuid 缺失走空态;分享详情页 invalid 支持标题/描述变量与「去登录」出口;upload 失败对非 80000 错误给 toast。

实现要点(防回归)

  • 不得回退已有加固:redirectToLogin 幂等锁与「已在登录页跳过」、silentAuth 不清 token、request 未登录快速失败、attached 首帧 setData 延到 nextTick。
  • 分享详情 /closet-share/:uuid 是否免登录需后端确认,保守保持 auth: true,仅把 11001 单独映射为「去登录」按钮,避免擅自放开权限。
  • expire_time 单位在 types/models.ts 注释标注为秒,本次用「大于 1e12 视为毫秒」的兼容判断兜底,无需后端确认即可安全上线。
  • 定时器一律挂 this 并在 onUnload 清理;倒计时 tick 在 onHide 也要停,避免后台空转。
  • 空态统一复用 empty-state 组件(项目已在 horoscope/mall 使用,属性:icon/title/desc/buttonText/bind:action),不新增组件。
  • 日志与提示:不打印 token 等敏感信息;toast 文案沿用 utils/config.ts 的 ERROR_MESSAGES 风格。

架构设计

flowchart TD
  A[页面/组件发起请求] --> B{needAuth 且无 token?}
  B -- 是 --> C[reject NOT_LOGIN 不发请求]
  B -- 否 --> D{token 已过期?}
  D -- 是 --> E[doRefresh 预刷新]
  D -- 否 --> F[wx.request timeout 15s]
  E -- 成功 --> F
  E -- 失败 --> G[清 token + handleAuthExpired + 幂等跳登录]
  F -- 命中登录失效码 --> H{已重试过?}
  H -- 否 --> I[doRefresh 后重试一次]
  H -- 是 --> G
  F -- 其它业务码 --> J[toast + reject]
  K[uploadFile] --> B

目录结构

zt-closet-wxmini-frontend/
├── miniprogram/
│   ├── utils/
│   │   ├── request.ts                 # [MODIFY] 登录失效码集合常量;doCall(retried) 重试上限;11000 并入;统一 timeout;needAuth 过期预刷新;uploadFile 补登录校验/401/timeout/showError
│   │   ├── auth.ts                    # [MODIFY] 新增 isTokenExpired()(秒/毫秒兼容 + 30s 提前量),isLoggedIn 语义不变
│   │   ├── paged.ts                   # [NEW] normalizePaged(res) 统一 list/total 空值兜底,供 7 处列表复用
│   │   └── config.ts                  # [MODIFY] STORAGE_KEYS 新增 OPEN_COLLECT,收编硬编码 'zt_open_collect'
│   ├── custom-tab-bar/index.ts        # [MODIFY] 节流打点改为请求成功后记录、失败回滚 lastSyncAt=0
│   ├── components/navigation-bar/navigation-bar.ts  # [MODIFY] data 补齐 ios/innerPaddingRight/leftWidth/safeAreaTop
│   ├── pages/
│   │   ├── mall/mall.ts               # [MODIFY] confirmPay 加 submitting 锁;pollOrder timer 挂 this + onUnload 清理;fetchGoods/fetchFlow 用 normalizePaged
│   │   ├── mall/mall.wxml             # [MODIFY] 支付按钮 loading 文案与禁止连点
│   │   ├── upload/upload.ts           # [MODIFY] catch 分支对非 80000 错误给 toast,保留额度弹窗
│   │   ├── closet-share-detail/closet-share-detail.ts   # [MODIFY] uuid 空值兜底;本地过期/封禁校验;非 901xx 与 11001 分别落错误态/去登录态;onShareAppMessage;取消分享 timer 挂 this + onUnload 清理
│   │   ├── closet-share-detail/closet-share-detail.wxml # [MODIFY] empty-state 标题/描述变量化 + 去登录按钮 + open-type=share 分享按钮
│   │   ├── horoscope/horoscope.ts     # [MODIFY] 新增 error 态与 onRetry;重试 timer 挂 this + onUnload 清理;跨天(stat_date 比对今日)重拉
│   │   ├── horoscope/horoscope.wxml   # [MODIFY] 内容区改 wx:elif 并补错误态分支与重试按钮
│   │   ├── wardrobe/wardrobe.ts       # [MODIFY] reqSeq + loadingMore + normalizePaged
│   │   ├── outfit/outfit.ts           # [MODIFY] 同上;storage key 改用 STORAGE_KEYS.OPEN_COLLECT
│   │   ├── closet-share/closet-share.ts  # [MODIFY] 同上;decorate 改用 ShareStatus 枚举
│   │   ├── closet-share/closet-share.wxml # [MODIFY] 已封禁状态文案分支
│   │   ├── order/order.ts             # [MODIFY] 同上;待支付倒计时 1s tick + onUnload/onHide 清理
│   │   ├── notification/notification.ts  # [MODIFY] 红点同步改为从 getCurrentPages 首个 tab 页取 getTabBar(),取不到仅写 storage;列表用 normalizePaged
│   │   ├── clothes-detail/clothes-detail.ts  # [MODIFY] uuid 为空直接 notFound 态,不再永久 loading
│   │   ├── clothes-detail/clothes-detail.wxml # [MODIFY] 增加 notFound 空态
│   │   ├── outfit-edit/outfit-edit.ts # [MODIFY] selectedMap 累积跨分类已选;选衣列表触底分页;onSave 加 saving 锁
│   │   ├── closet-share-create/closet-share-create.ts # [MODIFY] 选衣列表触底分页;onConfirm 加 submitting 锁 + 失败 toast
│   │   ├── profile/profile.ts         # [MODIFY] showActionSheet 补 fail;today 改 onShow 重算;onSaveProfile 加 saving 锁 + trim + 无变更关闭 + 复位 avatarChanged;storage key 收编
│   │   └── outfit-detail/outfit-detail.ts # [MODIFY] 移除未使用的 OutfitSource 导入
│   └── app.ts                         # [MODIFY] 移除零引用的 globalData.userInfo(与 IAppOption 类型同步精简)

关键代码结构

// miniprogram/utils/paged.ts —— 统一分页兜底,杜绝 list 为 null 时抛错被空 catch 吞掉
export interface NormalizedPaged<T> { list: T[]; total: number }
export function normalizePaged<T>(res: any): NormalizedPaged<T>

// miniprogram/utils/auth.ts —— token 过期判断(秒/毫秒兼容,30s 提前量)
export function isTokenExpired(): boolean
// isLoggedIn() 语义保持不变:仅判断 access token 是否存在

// miniprogram/utils/request.ts —— 登录失效码收敛 + 重试上限
export const AUTH_EXPIRED_CODES: number[]   // 11000 / 11001 / 11002 / 11004
export interface UploadOptions { showError?: boolean; timeout?: number }
export function uploadFile<T = any>(filePath: string, name?: string, url?: string, options?: UploadOptions): Promise<T>