# 【前端待处理·管理后台】管理后台消息 SSE 鉴权重连与旧会话恢复

> **模块**：管理后台全局消息 / 在线状态 / 聊天信令 | **服务**：`hl-gateway` + `hl-user-service`<br>
> **类型**：前端待处理 + 联调告知 | **更新时间**：2026-07-19<br>
> **影响范围**：管理后台全局 SSE 连接、顶部未读角标、聊天、在线状态与抢单池信令<br>
> **状态**：后端已完成根因定位；前端尚未修复；接口契约未变

## 1. 结论与处理优先级

> ⚠️ 2026-07-19 测试环境启用 SSE 连接角色一致性校验后，发布前已签发且仍在有效期内的旧登录会话可能缺少当前角色标记。此时网关能够识别 access token，但用户服务会拒绝建立 SSE，前端当前实现会持续使用同一登录会话无限重连。

- 接口 URL、HTTP 方法、事件结构均未修改。
- 这不是 `token` Query 参数名写错；当前前端 URL 拼接方式与网关读取方式一致。
- **用户立即恢复方式**：退出当前账号，重新登录并选择当前角色，再建立 SSE。
- **前端必须处理**：Token/角色变化时主动重建连接、限制连续失败重试、给出重新登录提示，并消除默认 `message` 事件的重复注册。
- 本次现象包含后端发布前旧会话兼容问题；前端改造用于正确管理连接生命周期和避免无限重试，不代表把后端兼容责任转移给前端。

## 2. 当前接口契约

```http
GET /ws/admin-msg/stream?token=<accessToken>
Accept: text/event-stream
```

- 认证：管理后台 access token。
- 当前使用原生 `EventSource`，浏览器 API 不能自定义 `Authorization` Header，因此现有实现通过 Query 参数传递 token。
- `token` 必须使用当前 Store 中的 access token，并通过 `encodeURIComponent` 做 URL 编码。
- 成功建连后，请求应长期保持 `Pending`，响应类型为 `text/event-stream`。
- 首个握手事件：

```text
event: connected
data: ok
```

- 后续仍沿用现有具名事件，包括 `unread-count`、`im-chat`、`im-chat-read`、`presence` 和 `grab-pool-changed`；本次没有修改事件数据结构。

## 3. 已确认的问题链路

### 3.1 旧登录会话与新角色标记不兼容

测试环境运行链路已确认：

1. 网关可以从 `?token=` 读取并校验管理后台 JWT。
2. 网关向用户服务转发可信的管理员身份及角色信息。
3. 用户服务在下发任何 SSE 数据前校验“连接角色是否仍为当前登录角色”。
4. 发布前签发的旧登录会话没有初始化新角色标记时，校验按安全策略失败并关闭连接。
5. 新登录或重新选择角色会重新写入角色标记，因此重新登录后可恢复。

该校验采用 fail-closed（失败时拒绝）策略，目的是避免角色切换后旧 Token 继续接收不属于当前角色的消息。

### 3.2 前端当前会无限重试同一失败会话

当前 `src/composables/useAdminMessageSSE.js` 在 `EventSource.onerror` 后执行关闭和指数退避，但没有连续失败上限，也没有触发重新登录或鉴权恢复流程。

原生 `EventSource.onerror` 不暴露 HTTP 状态码和响应正文，前端不能仅凭 `onerror` 精确区分 401/403、服务异常和临时断网。因此不能把所有错误都直接判定为 Token 失效，但必须限制无休止重连。

### 3.3 默认 `message` 事件被重复注册

当前实现同时注册：

```js
es.onmessage = handleMessage
es.addEventListener('message', handleMessage)
```

两种写法都会监听默认 `message` 事件，并不是互斥兜底。后端发送默认 `message` 时，同一数据可能被处理两次，必须只保留一种注册方式。

## 4. 用户立即恢复步骤

1. 关闭当前页面产生的旧 SSE 连接。
2. 正常退出管理后台。
3. 重新登录，并重新选择当前需要使用的角色。
4. 进入主布局后重新建立 `/ws/admin-msg/stream`。
5. 在浏览器 Network 中确认请求保持 `Pending`，并收到一次 `connected` 事件。

不要通过手工复制、修改或在地址栏粘贴完整 Token 的方式恢复连接。

## 5. 【前端·管理后台】适配清单

### 5.1 让 SSE 生命周期跟随登录凭证和角色

- [ ] 监听 `userStore.token` 变化；值变化时先关闭旧 `EventSource`，再使用最新 Token 建立唯一的新连接。
- [ ] 角色切换成功并更新 Token 后，立即重建 SSE，不等待旧连接自行报错。
- [ ] 登出、主布局卸载或 Token 被清空时，关闭连接、清理重连定时器并禁止再次拉起。
- [ ] 保证全局最多只有一个管理后台消息 SSE 实例，避免布局重复挂载造成多连接。
- [ ] 重建连接时始终从 Store 现取 Token，不缓存旧登录会话中的 Token 字符串。

### 5.2 限制连续失败，避免无限重连

- [ ] 保留指数退避和最大间隔，但增加“连续失败次数/总时长”上限。
- [ ] **仅在收到后端 `connected` 事件后**清零连续失败计数；`EventSource.onopen` 不能作为鉴权成功依据，也不能清零计数。
- [ ] 达到上限后停止自动重试，并显示中性、可操作的提示，例如“消息连接连续失败，请检查网络或重新登录”。
- [ ] 用户完成重新登录、Token 刷新、角色切换或主动点击重试后，才开启新一轮连接。
- [ ] 临时断网恢复后仍允许重连；可结合 `online` 事件或显式重试入口恢复，而不是永久静默失效。

> 注意：由于原生 `EventSource` 无法在 `onerror` 中读取响应状态，前端不要根据一次 `onerror` 立即清空登录态。需要使用连续失败阈值，并结合普通鉴权接口结果或既有 Token 刷新状态判断。

### 5.3 消除重复消息处理

- [ ] `es.onmessage` 与 `es.addEventListener('message', ...)` 只保留一种。
- [ ] `connected`、`unread-count`、`im-chat`、`im-chat-read`、`presence`、`grab-pool-changed` 等具名事件继续分别注册。
- [ ] 验证单条默认 `message`、聊天信令和未读数信令都只被业务层消费一次。

### 5.4 失败信息与联调反馈

- [ ] 前端提示中不要展示 Token、完整 SSE URL、Cookie 或管理员标识。
- [ ] 如重新登录后仍失败，只反馈发生时间、页面、错误 `message` 和 `X-Trace-Id`。
- [ ] 若 Network 原始响应确实为“未提供有效的Token”，请附 `X-Trace-Id` 交后端继续检查路由/拦截器链；不要附 Token。

## 6. 验收场景

| 场景 | 期望结果 |
|---|---|
| 重新登录后首次进入主布局 | 只建立 1 条 SSE；请求保持 `Pending`；收到 1 次 `connected` |
| access token 刷新 | 旧连接关闭，使用新 Token 只重建 1 次 |
| 切换管理后台角色 | 旧角色连接立即关闭；新角色 Token 建立新连接；不接收旧角色后续数据 |
| 发布前旧会话无法建连 | 退避重试达到阈值后停止，并明确提示重新登录；不无限刷请求 |
| 只触发 `onopen`、未收到 `connected`、随后触发 `onerror` | 仍累计连续失败次数，不得被 `onopen` 反复清零 |
| 临时断网后恢复 | 在受控退避或用户重试后恢复连接，不产生并发 SSE |
| 收到默认 `message` | 同一事件只处理 1 次 |
| 正常登出 | SSE 和重连定时器均被清理，退出页不再发起连接 |
| 重新登录后仍失败 | 联调材料仅包含时间、页面、错误消息、`X-Trace-Id`，不包含 Token |

## 7. 后端状态与边界

- 当前接口路径、Query 参数名和 SSE 事件结构未变，不需要前端调整数据模型。
- 新登录/角色切换链路会写入当前角色标记，重新登录是当前可用的恢复手段。
- 发布前旧会话没有迁移标记是本次问题的触发条件；后端尚未交付旧会话兼容补丁。
- 角色一致性校验必须保留，不能为了兼容旧会话而允许旧角色 Token 接收消息。
- 若后续改为一次性 SSE Ticket、Fetch Streaming 或其他不在 URL 中携带 access token 的方案，将另发接口契约，不在本次前端适配范围内。

## 8. 安全要求

- 禁止把完整 Token、带 Token 的完整 SSE URL、Cookie 或真实管理员信息写入 Issue、PR、Changelog、日志和截图。
- Token 一旦通过聊天、工单或截图暴露，应立即停止使用和传播，通知后端/运维按当前鉴权策略显式吊销或拒绝该旧 Token，并验证它已无法访问；随后重新登录获取新 Token。
- 重新登录只是恢复 SSE 和获取新 Token，不等于旧 JWT 已自动吊销；尤其在仅校验 JWT 签名的环境中，必须单独完成旧 Token 的失效处置。
- 不得在前端代码中硬编码 Token，也不得把 Token 写入错误上报或埋点参数。

## 9. 影响范围

| 文件/能力 | 说明 |
|---|---|
| `src/composables/useAdminMessageSSE.js` | 连接、重连、事件监听和清理逻辑 |
| `src/layouts/BasicLayout.vue` | 主布局挂载、登出和 SSE 生命周期 |
| 角色切换流程 | Token 更新后主动重建 SSE |
| 顶部未读角标、聊天、在线状态、抢单池信令 | 共用同一 SSE，需防止连接缺失或事件重复消费 |

## 10. 发布说明

- 本文是前端联调和修复通知，不代表已修改或发布前端代码。
- 本文没有包含任何真实 Token、管理员 ID、Cookie 或其他敏感信息。
- 前端完成后应在 `mmg/hl-ui` 走自身 Issue、分支、PR、测试和发布流程。

## 11. 相关历史契约

| 文档 | 当前说明 |
|---|---|
| [内部员工站内信收件箱 + SSE 实时推送](../2026-06/04_3414_内部员工站内信收件箱-SSE实时推送-管理后台.md) | SSE 路径、Query 鉴权和事件契约仍有效；其中“断线自动重连”的建议被本文补充为有上限的受控重连，鉴权持续失败时不得无限请求 |
| [切角色 / 刷新令牌原子保存](../2026-06/38_4529_切角色与刷新token原子保存_前端必改-管理后台.md) | `token` 与 `refreshToken` 原子保存要求仍有效；保存新 access token 后还必须关闭旧 SSE 并主动重建 |
