feat: Phase 2e-1 统一通知中心 + 我的 TabBar 聚合未读红点
后端(notify 模块) - 新增 notify 模块:DAO/Service/Pusher 接口/Controller/Router/CleanupTask - 数据库 DDL:notify_notifications 表 + 3 索引(user+created/user+is_read/user+category) - 11 种 type 枚举(好友/群聊 9 种 + meeting_* 2 种预留)+ 4 种 category - 跨模块集成:contact 3 处 Pusher(friend_request/accepted/rejected) - 跨模块集成:group 6 处 Pusher(invite/join_request/approved/rejected/kicked/role_changed) - WS handler 断线补偿:连接建立即推送 notify.unread.total - 5 REST API(4 用户 + 1 管理员广播)+ 2 WS 事件(notify.new / notify.unread.total) - 30 天已读通知定时清理(未读永久保留) - Provider/Wire 依赖注入(NotifyPusher、NotifyConnectHook、UserInfoResolver 接口) 前端 - 新增 notify 模块:API/Pinia Store(5 分类分页缓存 + 未读数 + WS 事件)/NotifyItem/通知中心主页 - profile 入口:铃铛 badge + 菜单项 badge + 数字显示 - App.vue/login 初始化 notifyStore WS 监听;logout 调用 notifyStore.reset() 清缓存 - 清理 contact.js/group.js 中散落 toast 与冗余 notify.friend.request/group.join.request 处理 - CustomTabBar 新增 hasDot() 聚合指示器:我的 Tab 显示纯红点(无数字), 当前聚合 notifyStore.unreadTotal,未来可扩展「资料待完善/安全提醒/新版本」等 文档 - 新增 Phase 2e 整体路线图 docs/plans/2026-04-20-phase2e-design.md - 新增 Phase 2e-1 专用设计 docs/plans/2026-04-20-phase2e-1-design.md(§6.4 TabBar 聚合红点) - 新增 Phase 2e-1 实施计划 docs/plans/2026-04-20-phase2e-1-implementation.plan.md - 新增 E2E 验证报告 test-report-phase2e-1-notification.md(含 Playwright MCP 2 个现场 Bug 修复记录) - 更新 docs/progress/CURRENT_STATUS.md、docs/api/README.md、docs/api/frontend/notify.md - 更新 .cursor/rules/project-context.mdc、docs/plans/2026-02-27-echochat-system-design.md 其他 - .gitignore 排除 .playwright-mcp/ MCP 临时快照 架构决策 - 单端 WS 连接:沿用现有 ws.Hub,多端已读同步推迟到 Phase 2f/二期 - 跨模块依赖:contact/group → notify 严格单向(接口注入模式) - 降级策略:Pusher 先入库后推送;WS 失败不回滚入库;入库失败仅 Warn 不影响业务 Playwright MCP 回归(4 类场景全通) - 实时推送(admin 广播 → 1s 内前端自动插入 + 角标 +1) - Deep-link 跳转(好友申请通知 → contact/request 页) - 批量清零(全部已读按钮) - TabBar 聚合红点(有未读亮/全部已读灭)与 notifyStore.unreadTotal 三层同步 Made-with: Cursor
This commit is contained in:
@@ -18,7 +18,7 @@
|
||||
| [frontend/im.md](frontend/im.md) | 即时通讯 | ✅ Phase 2b | 7 个 API:会话列表/置顶/删除/清空、历史消息、全局搜索、未读数 |
|
||||
| [frontend/group.md](frontend/group.md) | 群聊管理 | ✅ Phase 2c | 16 个 API:建群/管理/成员/角色/禁言/公告/搜索/入群审批 |
|
||||
| [frontend/meeting.md](frontend/meeting.md) | 会议 | 📋 后续 | 即时会议、预约会议、加入/离开、会议列表 |
|
||||
| [frontend/notify.md](frontend/notify.md) | 通知 | 📋 后续 | 通知列表、标记已读 |
|
||||
| [frontend/notify.md](frontend/notify.md) | 通知中心 | ✅ Phase 2e-1 | 5 个 API:通知列表(游标分页)/未读数/标记已读/全部已读/管理员广播 + 2 个 WS 事件(notify.new/notify.unread.total) |
|
||||
|
||||
### 后台管理端 (`admin/`)
|
||||
|
||||
|
||||
@@ -1,7 +1,23 @@
|
||||
# 通知模块 API (Notify)
|
||||
|
||||
> 通用规范(认证方式、响应格式、错误码)见 [README.md](../README.md)
|
||||
> 新通知的实时推送通过 WebSocket 完成,见 [websocket.md](../websocket.md)
|
||||
> 通用规范(认证方式、响应格式、错误码)见 [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` / `meeting_reminder` | meeting(Phase 2e-2/2e-3 接入) |
|
||||
|
||||
> 通知持久化在 `notify_notifications` 表,每条通知对应单个接收者;30 天前的已读通知由后台定时任务自动清理,未读通知永久保留。
|
||||
|
||||
---
|
||||
|
||||
@@ -9,9 +25,11 @@
|
||||
|
||||
| 方法 | 路径 | 权限 | 说明 |
|
||||
|------|------|------|------|
|
||||
| GET | /api/v1/notifications | 需认证 | 获取通知列表 |
|
||||
| PUT | /api/v1/notifications/:id/read | 需认证 | 标记通知已读 |
|
||||
| PUT | /api/v1/notifications/read-all | 需认证 | 全部标记已读 |
|
||||
| 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 | 管理员 | 全员广播系统通知 |
|
||||
|
||||
---
|
||||
|
||||
@@ -25,69 +43,195 @@
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| is_read | bool | 无 | 筛选已读/未读,不传则返回全部 |
|
||||
| type | string | 无 | 筛选通知类型(meeting_invite / friend_request / friend_accepted / meeting_reminder / system) |
|
||||
| page | int | 1 | 页码 |
|
||||
| page_size | int | 20 | 每页数量 |
|
||||
| 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": "ok",
|
||||
"message": "success",
|
||||
"data": {
|
||||
"list": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "meeting_invite",
|
||||
"title": "会议邀请",
|
||||
"content": "张三邀请你参加会议「产品需求讨论」",
|
||||
"extra": {
|
||||
"room_code": "123-456-789",
|
||||
"room_title": "产品需求讨论",
|
||||
"from_user_id": 1,
|
||||
"from_username": "zhangsan"
|
||||
},
|
||||
"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-02-27 10:00:00"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "friend_request",
|
||||
"title": "好友申请",
|
||||
"content": "李四请求添加你为好友",
|
||||
"extra": {
|
||||
"from_user_id": 2,
|
||||
"from_username": "lisi",
|
||||
"message": "我是你的同事"
|
||||
},
|
||||
"is_read": false,
|
||||
"created_at": "2026-02-27 09:30:00"
|
||||
"created_at": "2026-04-20 10:15:00"
|
||||
}
|
||||
],
|
||||
"total": 15,
|
||||
"page": 1,
|
||||
"page_size": 20
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 标记通知已读
|
||||
## 3. 标记单条已读
|
||||
|
||||
`PUT /api/v1/notifications/:id/read`
|
||||
|
||||
**权限:** 需认证
|
||||
**权限:** 需认证(只能标记属于自己的通知)
|
||||
|
||||
**路径参数:** `id` — 通知 ID
|
||||
**响应:**
|
||||
```json
|
||||
{ "code": 0, "message": "success", "data": { "affected": 1 } }
|
||||
```
|
||||
|
||||
> 已读幂等:重复调用返回 `affected=0`。
|
||||
|
||||
---
|
||||
|
||||
## 3. 全部标记已读
|
||||
## 4. 批量标记已读
|
||||
|
||||
`PUT /api/v1/notifications/read-all`
|
||||
`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 为直接加入成员)。
|
||||
|
||||
Reference in New Issue
Block a user