# 【前端 BUG·管理后台】任务看板打开弹「获取看板详情/任务/成员失败」红 toast = 前端竞态重复发请求 + 取消被误报错

> **关联**: 无工单（前端 bug 走 changelog 通知 mmg）
> **服务**: 前端 hl-ui（**后端零改动**）| **提报时间**: 2026-06-05
>
> **存放目录**: `changelogs-v2/2026-06/`
> **影响范围**: 管理后台「任务看板」`/task/boards`（看板列表 / 看板详情视图）
> **类型**: 前端竞态 bug（mmg 修），**后端实测全 200 健康无需改动**

---

## ⚠️ 关键说明

打开任务看板时弹出红色错误 toast「**获取看板详情失败 / 获取看板任务失败 / 获取看板成员失败**」，**但看板数据其实正常加载出来了**（公司看板、各状态列、任务卡片都在）。

**根因 = 前端竞态，不是后端故障。** 看板组件挂载时对**同一个数据接口重复发请求**（`tasks` 发 3 次、`members` 发 2 次、`detail` 视时序也会重发），多余的在飞请求被前端 `AbortController` **主动取消**（浏览器 `net::ERR_ABORTED`）。前端 `.catch()` **没有把「请求被取消」和「请求真失败」区分开**，于是把取消也当成失败弹了红 toast。

后端侧：凡是被允许跑完的请求**全部返回 200**（看板列表/详情/任务/成员/状态 + token refresh，curl 80+ 次 + 真浏览器多轮复现，零服务端失败）。

---

## 1. 实证（真浏览器 DevTools Network，刚登录 token 全新，硬刷新复现）

打开 `/task/boards`（公司看板 `2022226454376366081`）后的请求序列：

| # | 请求 | 结果 |
|---|---|---|
| 1 | `GET /admin/task/boards`（看板列表） | **200** ✅ |
| 2 | `GET /admin/task/board/{id}`（看板详情，含 statuses） | **200** ✅ |
| 3 | `GET /admin/task/board/{id}/tasks` | **FAILED `net::ERR_ABORTED`** ❌ |
| 4 | `GET /admin/task/board/{id}/tasks` | **200** ✅（同一接口重发，成功）|
| 5 | `GET /admin/task/board/{id}/members` | **200** ✅ |
| 6 | `GET /admin/task/board/{id}/tasks` | **FAILED `net::ERR_ABORTED`** ❌ |
| 7 | `GET /admin/task/board/{id}/members` | **200** ✅（同一接口重发）|

要点：
- `tasks` 接口被触发 **3 次**（2 次被取消、1 次成功）；`members` 触发 **2 次**；`detail` 偶发重发被取消时就弹「获取看板详情失败」。
- `net::ERR_ABORTED` 是**客户端主动取消**（不是 4xx/5xx，服务端从没收到失败结果），典型来自 axios cancel / `AbortController.abort()` / 组件重渲染或重复触发同一 fetch。
- 浏览器 console **0 个 error**（说明取消已被 catch 捕获），但 catch 里仍调用了错误 toast。

后端实测对照（均 200，有数据）：

```
GET /admin/task/boards                         -> 200, 看板列表
GET /admin/task/board/2022226454376366081      -> 200, 详情含 4 个 statuses
GET /admin/task/board/2022226454376366081/tasks   -> 200, 任务分组
GET /admin/task/board/2022226454376366081/members -> 200, []
GET /admin/task/board/2022226454376366081/statuses-> 200, 4 条
POST /admin/auth/refresh (真 refreshToken)         -> 200（连打 30 次全 200）
```

---

## 2. 前端修法（mmg）

### 2.1 必修：catch 里放过「被取消」的请求，别弹错误 toast

错误处理里先判断是否为取消，是则**静默 return**，不弹 toast：

```js
import axios from 'axios'
// ...
.catch((e) => {
  // 请求被取消（组件重渲染/重复触发抢占）是预期行为，不是错误
  if (axios.isCancel(e) || e?.code === 'ERR_CANCELED' || e?.name === 'CanceledError') return
  ElMessage.error('获取看板任务失败')   // 只有真失败才提示
})
```

三处都要加：获取看板详情 / 获取看板任务 / 获取看板成员的 catch。

### 2.2 根治：去掉对同一接口的重复触发

`tasks` 发 3 次、`members` 发 2 次，说明同一 fetch 被多个来源同时触发（常见：`onMounted` 触发一次 + `watch(boardId)` 触发一次 + 默认「优先级/状态」筛选 radio 又触发一次）。建议：

- 数据加载**只保留单一触发源**（推荐由 `watch(boardId, { immediate: true })` 统一触发，去掉 `onMounted` 里的重复拉取）；
- 或对每类请求维护一个 `AbortController`，重发前先 `abort()` 旧的——**但这种「主动取消」必须配合 §2.1，把取消视为正常、不报错**。

### 2.3 注意（与本 bug 无关、但同页常见）

看板有实时通道（`/ws/task/info` + `/admin/task/board/ws-doc`）。若 WS 推送后又触发整页重新拉取，也会放大上面的「重复请求 + 取消」现象，一并按 §2.1/§2.2 处理。

---

## 3. 后端状态（无需改动）

- 任务看板全部读接口（list / detail / tasks / members / statuses）实测 200，数据正确。
- `POST /admin/auth/refresh`：网关已在 `JwtAuthFilter.SKIP_URLS` 白名单放行，带过期 access token + 真 refreshToken 实测 200，20 并发全 200。

### 3.1 附注：你截图里 `refresh` 的 502 是「另一桩、瞬时」

你之前截图 Network 里 `refresh` 显示 **502**，那是**独立的瞬时事件**：当时会话 access token 已过期 → 前端自动调 `/admin/auth/refresh` 续签 → 恰好撞上测试服 user-service（双实例 8081/8181）部署/重启抖动，网关 LB 轮到正在重启的实例返 502。**当前 30+ 次 refresh 全 200，复现不出，不是看板代码缺陷**，无需前端改动；若再遇到，刷新页面即恢复。

---

## 4. 关联

- 后端负责人：@wx（已实测确认后端健康，无改动）
- 前端对接（本 bug 修复）：**mmg**
- 复现路径：登录管理后台 → 任务看板（`/task/boards`）→ 打开「公司看板」即弹失败 toast，但数据正常。
