Files
EchoChat/docs/api/frontend/notify.md
bujinyuan ba8c72aae0 docs(task13): 补齐 meeting/notify API 文档 + Playwright UI 回归结果
## API 文档(Task 13 前漏补)
- docs/api/frontend/meeting.md §9 邀请用户接口:展开 extra 8 字段清单
  (room_code / room_title / has_password / invite_token / inviter_id
   / inviter_name / inviter_avatar / expired_at),并说明前端如何据
   expired_at 判过期
- docs/api/frontend/meeting.md 后续任务关联:Task 13 标  + deep-link 路径
- docs/api/frontend/notify.md 概览:
  * "支持内联按钮的通知类型" 表(friend_request / meeting_invite)
  * "meeting_invite 专用 extra 字段" 详细清单

## Playwright UI 回归结果(2026-04-23 补做)
采用 "A 侧 curl 邀请 + B 侧 Playwright 登录" 策略:
- testuser1 邀请 testuser2 → 通知中心渲染 meeting_invite 卡片
- 新邀请卡片 → 双按钮「立即加入 / 稍后」
- 过期邀请卡片 → 合并为单 disabled「邀请已过期」按钮 
- 点「立即加入」→ 跳 /pages/meeting/preview?mode=join&code=516-162-828 
- preview 页正确渲染 "即将加入会议 516-162-828" + 设备面板 
- 截图:task13-notify-meeting-invite-cards.png
        task13-preview-page-after-invite-accept.png
- CURRENT_STATUS.md + implementation.plan.md 已同步补录

Made-with: Cursor
2026-04-22 17:34:29 +08:00

262 lines
7.7 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.

# 通知模块 API (Notify)
> 通用规范(认证方式、响应格式、错误码)见 [README.md](../README.md)
> 新通知的实时推送通过 WebSocket 完成,见 [websocket.md](../websocket.md)
> 所属阶段Phase 2e-1已完成
---
## 概览
统一通知中心,覆盖四大业务类型:
| 分类 | 类型常量 | 触发模块 |
|---|---|---|
| 好友 | `friend_request` / `friend_accepted` / `friend_rejected` | contact |
| 群聊 | `group_invite` / `group_join_request` / `group_join_approved` / `group_join_rejected` / `group_kicked` / `group_role_changed` | group |
| 系统 | `system_broadcast` | admin 广播 |
| 会议 | `meeting_invite`Phase 2e-2 已接入) / `meeting_reminder`Phase 2e-3 预约会议接入) | meeting |
> 通知持久化在 `notify_notifications` 表每条通知对应单个接收者30 天前的已读通知由后台定时任务自动清理,未读通知永久保留。
### 支持内联按钮的通知类型
以下 type 在 `NotifyItem.vue` 会渲染"接受 / 拒绝"双按钮(文案因 type 而异),其余 type 仅展示点击跳转:
| type | 接受按钮 | 拒绝按钮 | 过期态 |
|---|---|---|---|
| `friend_request` | 同意 | 拒绝 | 无 |
| `meeting_invite` | 立即加入 | 稍后 | ✅ 过期合并为单个 disabled 按钮"邀请已过期",由 `extra.expired_at * 1000 < Date.now()` 判定 |
前端常量见 `frontend/src/constants/notify.js#NOTIFY_INLINE_ACTION_LABEL`
### `meeting_invite` 专用 extra 字段Phase 2e-2 Task 13
| 字段 | 类型 | 说明 |
|---|---|---|
| `room_code` | string | 9 位会议号 `XXX-XXX-XXX`,前端跳 preview 页使用 |
| `room_title` | string | 房间标题,用于卡片正文 |
| `has_password` | boolean | 是否需要密码,前端在 preview 页决定是否弹密码输入框 |
| `invite_token` | string | 32 位 hex token可选使用 `/meeting/invite-tokens/:token/redeem` 兑换 |
| `inviter_id` | int64 | 邀请人用户 ID |
| `inviter_name` | string | 邀请人昵称,用于卡片头部"XX 邀请你加入..." |
| `inviter_avatar` | string | 邀请人头像 URL |
| `expired_at` | int64 | Unix 秒,与 Redis `echo:meeting:invite:{token}` TTL600s同步前端据此灰显按钮 |
---
## 接口列表
| 方法 | 路径 | 权限 | 说明 |
|------|------|------|------|
| GET | /api/v1/notifications | 需认证 | 获取通知列表(游标分页) |
| GET | /api/v1/notifications/unread-count | 需认证 | 获取未读数统计 |
| PUT | /api/v1/notifications/:id/read | 需认证 | 标记单条已读 |
| PUT | /api/v1/notifications/read-all | 需认证 | 全部/按分类标记已读 |
| POST | /api/v1/admin/notifications/broadcast | 管理员 | 全员广播系统通知 |
---
## 1. 获取通知列表
`GET /api/v1/notifications`
**权限:** 需认证
**查询参数:**
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| category | string | `all` | 分类过滤:`all` / `friend` / `group` / `meeting` / `system` |
| is_read | bool | 无 | 是否已读:`true`/`false`,不传代表全部 |
| before_id | int64 | 无 | 游标:返回 id 小于此值的记录(按 id 降序) |
| limit | int | 20 | 页大小(最大 100 |
**成功响应:**
```json
{
"code": 0,
"message": "success",
"data": {
"list": [
{
"id": 128,
"type": "group_invite",
"category": "group",
"title": "",
"content": "张三 邀请你加入「产品交流群」",
"extra": "{\"group_id\":12,\"group_name\":\"产品交流群\",\"conversation_id\":45,\"inviter_id\":3,\"inviter_name\":\"张三\"}",
"actor_id": 3,
"actor_name": "张三",
"actor_avatar": "",
"target_type": "group",
"target_id": 12,
"is_read": false,
"created_at": "2026-04-20 10:15:00"
}
],
"has_more": true
}
}
```
> `extra` 是 JSON 字符串,前端按需 `JSON.parse` 解析。游标翻页使用最后一条 `id` 作为下一次请求的 `before_id`。
---
## 2. 获取未读数统计
`GET /api/v1/notifications/unread-count`
**权限:** 需认证
**成功响应:**
```json
{
"code": 0,
"message": "success",
"data": {
"total": 5,
"by_category": {
"friend": 1,
"group": 3,
"meeting": 0,
"system": 1
}
}
}
```
---
## 3. 标记单条已读
`PUT /api/v1/notifications/:id/read`
**权限:** 需认证(只能标记属于自己的通知)
**响应:**
```json
{ "code": 0, "message": "success", "data": { "affected": 1 } }
```
> 已读幂等:重复调用返回 `affected=0`。
---
## 4. 批量标记已读
`PUT /api/v1/notifications/read-all?category={category}`
**权限:** 需认证
**Query 参数:**
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| category | string | 否 | 分类:`friend`/`group`/`meeting`/`system`,不传代表全部 |
> ⚠️ 注意:`category` 通过 **Query String** 传递(非 Request Body。`PUT` 方法无需请求体。
**示例:**
- 全部标已读:`PUT /api/v1/notifications/read-all`
- 仅好友类标已读:`PUT /api/v1/notifications/read-all?category=friend`
**响应:**
```json
{ "code": 0, "message": "success", "data": { "affected": 3 } }
```
---
## 5. 管理员广播系统通知
`POST /api/v1/admin/notifications/broadcast`
**权限:** `RoleAdmin``RoleSuperAdmin`JWT + 角色校验)
**请求体:**
```json
{
"title": "系统维护通知",
"content": "今晚 02:00 - 04:00 将进行系统升级,请提前保存工作",
"target_type": "system",
"target_id": null,
"extra": null
}
```
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| title | string | 是 | 标题,最长 100 |
| content | string | 是 | 正文,最长 500 |
| target_type | string | 否 | 业务对象类型,默认 `system` |
| target_id | int64 | 否 | 业务对象 ID |
| extra | string | 否 | 扩展 JSON 字符串 |
**响应:**
```json
{ "code": 0, "message": "success", "data": { "affected": 128 } }
```
> 后端将遍历所有活跃用户批量写入通知,在线用户实时收到 `notify.new` WS 推送;离线用户重连后通过 `notify.unread.total` 触发未读数补偿。
---
## 6. WebSocket 事件
### `notify.new` — 新通知到达
| 方向 | 触发 |
|---|---|
| Server → Client | 每次通知写入成功后 |
**载荷**:与 REST 返回的单条通知对象字段一致。
```json
{
"event": "notify.new",
"data": {
"id": 129,
"type": "friend_request",
"category": "friend",
"title": "好友申请",
"content": "Bob 请求添加你为好友",
"actor_id": 5,
"actor_name": "Bob",
"target_type": "user",
"target_id": 5,
"is_read": false,
"created_at": "2026-04-20 10:17:21"
}
}
```
### `notify.unread.total` — 未读数补偿
| 方向 | 触发 |
|---|---|
| Server → Client | WebSocket 连接建立成功时 |
**载荷**
```json
{
"event": "notify.unread.total",
"data": {
"total": 5,
"by_category": { "friend": 1, "group": 3, "meeting": 0, "system": 1 }
}
}
```
> 前端以该值覆盖本地缓存的未读数,保证断线重连后铃铛徽标准确。
---
## 已知限制(单端连接架构)
- 当前 `ws.Hub` 仅支持同一用户单连接(新连接关闭旧连接),**不提供多端已读同步**;此限制已在 `docs/plans/2026-04-20-phase2e-design.md` §3.1/§3.5 修订。
- 管理端广播发布 UI 推迟到 Phase 2f后端接口已落地管理端页面未提供
- `group_invite` 的"接受/拒绝"内联操作当前语义:接受仅跳转会话;拒绝调用 `leaveGroup`(由于当前 InviteMembers 为直接加入成员)。