Files
zt-closet-wxmini-frontend/.codebuddy/plans/fix-401-relaunch-loop_32bcbbce.md
T

127 lines
7.6 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.
---
name: fix-401-relaunch-loop
overview: 修复新用户未登录时 /notification/unread 401 引发的并发 reLaunch 风暴(routeDone webviewId not found):请求层做登录守卫 + 登录失效跳转去重,custom-tab-bar 与页面侧停止无意义轮询与重复请求。
todos:
- id: request-guard
content: 改造 request.ts:未登录快速失败、401 收敛、新增 silentAuth 标记
status: completed
- id: redirect-idempotent
content: 在 app.ts 实现幂等登录失效跳转(单一入口+并发锁+已在登录页跳过)
status: completed
dependencies:
- request-guard
- id: tabbar-unread
content: 重构 custom-tab-bar:attached 只读缓存,新增 setUnread 与 syncUnread
status: completed
dependencies:
- request-guard
- id: page-guards
content: 为 notification、profile、wardrobe、login 页面加登录守卫并改用 setUnread
status: completed
dependencies:
- tabbar-unread
- redirect-idempotent
- id: verify
content: 通读改动链路并做编译/类型检查,确认无 401 刷屏与重复 reLaunch
status: completed
dependencies:
- page-guards
---
## 用户诉求
新用户(无 token)一打开小程序就疯狂报错:
- 控制台刷屏 `GET https://zt-closet-backend.lzcwy.cn/api/v1/notification/unread 401 (Unauthorized)`
- 大量 `[Page route 错误(system error)] routeDone with a webviewId 125 is not found`
需要定位根因并修复,消除 401 刷屏与路由报错。
## 根因(已由代码核实)
1. 未登录也发鉴权请求:`custom-tab-bar/index.ts` 的 `attached()` 无条件调用 `refreshUnread() → fetchUnread()`,无 `auth.isLoggedIn()` 守卫;`request()` 默认 `auth: true`,无 token 时服务端直接 401。
2. 401 会触发全局跳转:`request.ts` 中 `needAuth && (code 11001/11002/11004 || statusCode 401)` → `doRefresh()`(无 refresh_token 直接失败)→ `auth.clearTokens() + redirectToLogin() → wx.reLaunch('/pages/login/login')`。`showError: false` 只压掉 toast,压不掉跳转,所以"静默轮询"也会触发 reLaunch。
3. 并发放大:tabBar 有 5 个 tab 页,custom-tab-bar 每页一个实例,`attached` 各发一次;`profile.onShow → syncTabBar → tabBar.refreshUnread()` 再叠加;`wardrobe.onShow`、`profile.onShow` 还会各自发 `/clothes`、`/quota/info`、`/user/info`。多个请求同时 401 → 同时多次 `wx.reLaunch`,路由互相打断,导致 `routeDone with a webviewId 125 is not found`;且在登录页仍会继续 reLaunch。
4. 重复请求链:`notification.ts` 的 `fetchUnread()` 成功后调用 `tabBar.refreshUnread()`,后者内部又 `fetchUnread()`,每次 onShow 多打一次接口。
## 核心改造目标
- 未登录时不再发起鉴权请求,从源头消灭 401。
- 登录失效跳转收敛为单一入口并做幂等去重(含"已在登录页则跳过"),杜绝并发 `reLaunch`。
- 非关键轮询(未读红点)401 只失败不跳登录页。
- custom-tab-bar 提供 `setUnread(n)`,去掉 `refreshUnread → fetchUnread` 的二次请求。
- tab 页 `onShow` 增加登录守卫,登录后进入页面才拉数据。
## 技术栈
微信小程序原生框架 + TypeScript(沿用现有 `Page`/`Component` 写法、现有 `request.ts` 请求层、`auth.ts` 登录态工具),不做框架引入或架构调整。
## 实现方案
采用「请求层收敛 + 调用点守卫」两层治理,最小改动面:
1. **请求层(`utils/request.ts`)**
- 新增 `silentAuth?: boolean`(非关键请求,401 只 `reject` 不触发全局跳登录),用于未读红点等轮询。
- `auth: true` 且 `!auth.isLoggedIn()` 时:直接 `reject(new ApiError(11001, 'NOT_LOGIN'))`,**不发请求、不跳登录页**(避免未登录场景下的请求风暴)。
- 401 分支只做 `clearTokens()` + `reject`,跳转统一交给 `onAuthExpired` 处理器(不再在 request.ts 内直接 `wx.reLaunch`)。
- 保留 `doRefresh()` 单例锁逻辑不变(只处理"已登录但 token 过期"分支)。
2. **全局跳转去重(`app.ts`)**
- 在 `onLaunch` 注入的 `setAuthExpiredHandler` 内做单一入口:
- `isRedirecting` 布尔锁 + 时间窗(如 1000ms)内只执行一次;
- 读取 `getCurrentPages()`,若栈顶已是 `pages/login/login` 则直接返回;
- 执行 `clearTokens() + clearUserInfo() + wx.reLaunch('/pages/login/login')`,在 `reLaunch` 的 `complete` 回调里释放锁。
- 这是消除 `routeDone with a webviewId ... is not found` 的关键(并发 reLaunch 互相打断 webview 生命周期)。
3. **custom-tab-bar(`custom-tab-bar/index.ts`)**
- `attached()` / `refreshUnread()` 只做「读缓存 `zt_closet_unread` → `setData({ unread })`」,不再自动发请求。
- 新增 `setUnread(n)` 方法供外部(notification 页)直接更新红点,切断二次请求链。
- 新增 `syncUnread()`:仅在 `auth.isLoggedIn()` 时才请求 `/notification/unread`(`silentAuth: true`),供登录成功后/页面 onShow 主动调用。
4. **调用点守卫**
- `pages/notification/notification.ts`:`onShow` 仅在已登录时 `refresh() + fetchUnread()`;`fetchUnread()` 改为 `silentAuth: true`,成功后调用 `tabBar.setUnread(count)`(不再 `refreshUnread()`)。
- `pages/wardrobe/wardrobe.ts`、`pages/profile/profile.ts`:在 `refresh()` / `fetchUser()` / `fetchQuota()` 前加 `auth.isLoggedIn()` 守卫;`profile.syncTabBar()` 改为「已登录才 `setUnread` 缓存值 + 不触发请求」。
- `pages/login/login.ts`:登录成功后 `wx.setStorageSync('zt_closet_unread', 0)`,保证进入 tab 页时红点有初值。
## 实现要点(防回归)
- `request()` 的 `auth: false` 请求(`/user/wxlogin`)不受任何改动影响,登录流程保持原样。
- 「未登录直接 reject」不会弹出 toast:`showError` 默认 true 会提示,因此该分支应固定以 `showError: false` 语义处理,或显式不弹 toast,避免登录页被 toast 刷屏。
- 跳转锁必须放在 `app.ts` 的 handler 里(唯一入口),不要在各页面重复实现,避免多套锁不一致。
- 不改 `app.json` 页面顺序、不动 tabBar 配置,影响面仅 4~6 个文件。
## 架构关系
```mermaid
flowchart TD
A[tab 页 onShow / custom-tab-bar attached] --> B{auth.isLoggedIn?}
B -- 否 --> C[不发请求,红点用缓存]
B -- 是 --> D[request 鉴权请求]
D -- 401 --> E{silentAuth?}
E -- 是 --> F[仅 reject,不跳转]
E -- 否 --> G[clearTokens + onAuthExpired]
G --> H{幂等锁 / 已在登录页?}
H -- 是 --> I[跳过]
H -- 否 --> J[reLaunch login(单例)]
```
## 目录结构
```
miniprogram/
├── utils/
│ ├── request.ts # [MODIFY] 未登录快速失败;401 收敛为 reject + onAuthExpired;新增 silentAuth 非关键请求标记
│ └── auth.ts # [只读参考] 复用 isLoggedIn/clearTokens/clearUserInfo
├── app.ts # [MODIFY] onAuthExpired 幂等跳转:单一入口 + 并发锁 + 已在登录页跳过
├── custom-tab-bar/
│ └── index.ts # [MODIFY] attached 只读缓存;新增 setUnread(n) 与 syncUnread()(仅登录态请求)
└── pages/
├── notification/notification.ts # [MODIFY] 登录守卫;silentAuth 请求;成功后调用 tabBar.setUnread
├── profile/profile.ts # [MODIFY] onShow 登录守卫;syncTabBar 不再触发请求
└── wardrobe/wardrobe.ts # [MODIFY] refresh 前加登录守卫
└── login/login.ts # [MODIFY] 登录成功后初始化未读缓存为 0
```