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

178 lines
12 KiB
Markdown
Raw Permalink 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.
---
name: fix-full-audit-issues-p0-p2
overview: 按审查报告修复小程序全部已确认缺陷:5 项阻断(支付重复下单、支付轮询定时器泄漏、401 无限重试、uploadFile 无 401 处理、衣柜分享无出口且失效时白屏)、12 项一般(通知红点不同步、运势空卡与定时器、列表竞态与判空、11000 未处理、token 过期判断、请求无超时、uuid 缺失卡 loading 等)、8 项轻微(提交锁、死代码、硬编码 key、状态枚举等)。
todos:
- id: request-core
content: 加固 request.ts 与 auth.ts:重试上限、11000、超时、过期预刷新、uploadFile 登录校验
status: completed
- id: paged-util
content: 新增 utils/paged.ts 的 normalizePaged 并接入全部列表判空
status: completed
- id: mall-pay
content: 修复商城支付:confirmPay 提交锁、轮询定时器随页面卸载清理、按钮 loading
status: completed
dependencies:
- request-core
- id: share-detail
content: 修复分享详情:uuid 兜底、错误态与去登录、过期校验、转发出口、定时器清理
status: completed
dependencies:
- request-core
- id: list-race
content: 为衣橱、穿搭、分享、订单、通知、商城加请求竞态保护与 loadingMore 拆分
status: completed
dependencies:
- paged-util
- id: state-fix
content: 修复运势错误态与重试、定时器与跨天、通知红点同步、衣物详情空态
status: completed
dependencies:
- request-core
- id: submit-locks
content: 修复穿搭编辑与分享创建:跨分类已选不丢、选衣分页、提交防重复与失败提示
status: completed
dependencies:
- paged-util
- id: p2-cleanup
content: 清理轻微项:资料保存锁与校验、跨零点、storage key、红点节流、倒计时、枚举、组件 data、死代码
status: completed
dependencies:
- request-core
- id: verify-all
content: 执行 tsc 类型检查并逐条复核修复效果
status: completed
dependencies:
- 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` 参数。
2. **页面状态机层**
- 新增 `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。
3. **防重复提交层**
- 商城 `confirmPay`、衣柜分享创建 `onConfirm`、穿搭编辑 `onSave`、资料保存 `onSaveProfile` 统一加 `submitting`/`saving` 布尔闸门,入口处 return,catch 与成功分支复位,按钮文案联动。
4. **错误可见层**
- 运势页新增 `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` 风格。
## 架构设计
```mermaid
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 类型同步精简)
```
## 关键代码结构
```ts
// 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>
```