bigfix
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
---
|
||||
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>
|
||||
```
|
||||
Reference in New Issue
Block a user