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:
bujinyuan
2026-04-21 10:21:01 +08:00
parent ccfceefdaf
commit f1853f125d
42 changed files with 4493 additions and 97 deletions

View File

@@ -0,0 +1,358 @@
# Phase 2e 设计文档:会议与通知系统
> **状态:** 🚧 进行中2e-1 ✅ 已完成2e-2/2e-3 📋 待开发)
> **分支:** `feature/phase2e-meeting-notification`(基于 `origin/feature/phase2c-group-read-receipt`
> **前置依赖:** Phase 2a联系人 + WS、Phase 2b即时通讯、Phase 2c群聊+已读、Phase 2d消息类型扩展全部完成
> **最后更新:** 2026-04-202e-1 完成 + 单端架构说明同步)
---
## 一、设计目标
基于 Phase 2a-2d 建立的 WebSocket + 消息 + 联系人 + 群聊基础设施,实现 MVP 第一期收官的两大核心能力:
1. **统一通知系统**:消除散落在好友/群聊/会议各模块的通知死角,提供"提醒 + 历史"双通道
2. **多人音视频会议**:基于 mediasoup SFU 架构支持即时会议MVP→ 预约会议 + 邀请(增强)
**核心交付物(按子阶段):**
- **Phase 2e-1 通知系统**3-4 人日):统一通知中心 + 11 种通知类型预留 + 跨模块 Pusher 接口
- **Phase 2e-2 会议 MVP**10-14 人日mediasoup Node 媒体服务 + 即时会议 + 基础音视频控制≤8 人)
- **Phase 2e-3 会议增强**7-10 人日):预约会议 + 会议邀请 + 会议提醒
**不包含(明确推迟):** 见 [§九 后续规划清单](#九后续规划清单必须留档)
---
## 二、阶段拆分与路线图
```
┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐
│ Phase 2e-1 │ │ Phase 2e-2 │ │ Phase 2e-3 │
│ 通知系统 │─▶│ 会议 MVP │─▶│ 会议增强 │
│ 3-4 人日 │ │ 10-14 人日 │ │ 7-10 人日 │
│ │ │ │ │ │
│ ✓ 好友/群聊事件通知 │ │ ✓ mediasoup Node │ │ ✓ 预约会议+定时提醒 │
│ ✓ 系统广播(后端) │ │ ✓ 即时会议≤8人 │ │ ✓ 会议邀请(复用 2e-1│
│ ✓ meeting_invite / │ │ ✓ 音视频+主持人控制 │ │ 通知类型) │
│ meeting_reminder │ │ ✓ 会议号/密码 │ │ ✓ 入会前预览 │
│ 类型预留 │ │ │ │ │
└──────────────────────┘ └──────────────────────┘ └──────────────────────┘
```
**拆分理由:**
- **2e-1 风险最低、收益最高**:纯业务逻辑扩展,复用 Phase 2a-2c 的 WS 基础设施;能立即解决"好友申请无感知"等体验死角
- **2e-2 是风险核心**引入全新技术栈Node.js + mediasoup + WebRTC需独立周期聚焦
- **2e-3 是收尾增强**:依赖 2e-1邀请/提醒走通知通道)和 2e-2会议能运行做在最后
---
## 三、Phase 2e-1 详细设计(通知系统)
### 3.1 需求决策记录
| 决策项 | 选择 | 理由 |
|---|---|---|
| 推送样式 | 双通道(持久化入库 + 底部 mini-toast | 不遗漏 + 不打扰,参照微信 |
| 主入口位置 | 「我的」Tab 顶部铃铛图标 + 数字徽标 | 不占用底部 Tab 位(已满 4 个) |
| 列表组织 | 顶部 Tab 分类:全部 / 好友 / 群聊 / 会议 / 系统 | 结构清晰,便于筛选 |
| 保留期限 | 30 天(每日定时任务清理已读通知) | 平衡存储与体验 |
| 历史追溯 | 不追溯,上线当天起新事件才入通知中心 | 无数据迁移风险 |
| 多端已读同步 | **暂不支持**WS Hub 单端连接架构,同一用户仅最新连接有效) | 架构约束,已推迟到 Phase 2f/二期 |
| 点击行为 | Deep-link 按类型分发;群/会议邀请支持内联操作按钮 | 减少跳转步骤 |
| Toast 点击 | 无交互(避免误触),仅 2 秒自动消失 | 防止误操作 |
| 通知分类开关 | 不做(统一打开) | 简化 MVP有需要再加 |
### 3.2 通知类型枚举11 种,含 2e-2/2e-3 预留)
| type | 触发场景 | 触发模块 | 落地 Phase | Deep-Link 跳转 |
|---|---|---|---|---|
| `friend_request` | 收到好友申请 | contact | 2e-1 | `pages/contact/request` |
| `friend_accepted` | 好友申请被接受 | contact | 2e-1 | `pages/contact/detail?id=<actor_id>` |
| `friend_rejected` | 好友申请被拒绝 | contact | 2e-1 | 无跳转(告知型) |
| `group_invite` | 被邀请加入群聊 | group | 2e-1 | **内联接受/拒绝** |
| `group_join_request` | 收到入群申请(群管理员)| group | 2e-1 | `pages/group/join-requests?groupId=<target_id>` |
| `group_join_approved` | 入群申请被批准 | group | 2e-1 | 直接进入群会话 |
| `group_join_rejected` | 入群申请被拒绝 | group | 2e-1 | 无跳转(告知型) |
| `group_kicked` | 被踢出群聊 | group | 2e-1 | 无跳转(告知型) |
| `group_role_changed` | 被设/撤管理员、群主转让 | group | 2e-1 | 群详情页 |
| `system_broadcast` | 系统广播 | notifyadmin 触发)| 2e-1 仅后端 API | 通知详情页 |
| `meeting_invite` | 会议邀请 | meeting | **2e-2 对接** | **内联加入/稍后** |
| `meeting_reminder` | 预约会议开始前 N 分钟 | meeting | **2e-3 对接** | **内联加入** |
### 3.3 数据库设计(新增 1 张表)
```sql
CREATE TABLE notify_notifications (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
type VARCHAR(40) NOT NULL,
title VARCHAR(200) NOT NULL,
content TEXT,
extra JSONB,
actor_id BIGINT,
target_type VARCHAR(40),
target_id BIGINT,
is_read BOOLEAN NOT NULL DEFAULT FALSE,
read_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_notify_user_unread ON notify_notifications(user_id, is_read, created_at DESC);
CREATE INDEX idx_notify_user_type ON notify_notifications(user_id, type, created_at DESC);
```
**字段说明:**
- `extra`:按类型存附加信息,例如 group_invite 存 `{group_id, group_name, group_avatar, inviter_name}`、meeting_invite 存 `{room_code, room_title, host_name, scheduled_at}`
- `actor_id`:触发者(如好友申请发起人)
- `target_type` + `target_id`关联对象friend_request / group / meeting / join_request
**清理任务**(每日 02:00 执行):
```sql
DELETE FROM notify_notifications WHERE created_at < NOW() - INTERVAL '30 days' AND is_read = true;
```
### 3.4 后端 API4 个)
```
GET /api/v1/notifications 列表query: type, is_read, before_id, limit
GET /api/v1/notifications/unread-count 未读数统计(按 type 分组)
PUT /api/v1/notifications/:id/read 标记单条已读
PUT /api/v1/notifications/read-all 全部已读(可按 type 过滤)
```
**响应示例**`GET /api/v1/notifications?limit=20`
```json
{
"code": 0, "message": "success",
"data": {
"list": [
{
"id": 1001, "type": "group_invite",
"title": "张三邀请你加入群聊「产品团队」",
"content": "",
"extra": { "group_id": 5, "group_name": "产品团队", "inviter_name": "张三" },
"actor_id": 7, "target_type": "group", "target_id": 5,
"is_read": false, "created_at": "2026-04-20T10:00:00Z"
}
],
"has_more": false
}
}
```
### 3.5 WebSocket 事件2 个,**单端连接架构**
| 事件 | 方向 | Payload | 说明 |
|---|---|---|---|
| `notify.new` | S→C | 完整通知对象 | 新通知到达,前端入 store + 更新徽标 |
| `notify.unread.total` | S→C | `{ total: 10, by_category: { friend: 3, group: 5, ... } }` | 连接建立/断线重连时补偿推送(权威值覆盖本地) |
> **说明**:由于现有 `ws.Hub` 为单端连接(同一用户仅保留最新连接),**不再设计** `notify.read.ack` 跨设备广播。多端已读同步作为独立技术债推迟到 Phase 2f详见 §九)。
### 3.6 跨模块 Pusher 接口(沿用 Phase 2a 接口注入标准)
```go
// app/notify/service/pusher.go
type Pusher interface {
Push(ctx context.Context, userID int64, req *PushRequest) error
}
type PushRequest struct {
Type string // 通知类型
Title string
Content string
Extra map[string]interface{}
ActorID int64
TargetType string
TargetID int64
}
```
**使用方示例**contact 模块好友申请):
```go
// contact/service/contact_service.go
s.notifyPusher.Push(ctx, receiverID, &notify.PushRequest{
Type: "friend_request",
Title: fmt.Sprintf("%s 请求添加你为好友", applicant.Nickname),
Content: applicationMessage,
ActorID: applicantID,
TargetType: "friend_request",
TargetID: requestID,
})
```
### 3.7 模块结构
```
backend/go-service/app/notify/
├── constants/notify_types.go # 11 种 type 枚举常量
├── model/notification.go # GORM 模型
├── dao/notification_dao.go # CRUD
├── service/
│ ├── notify_service.go # 业务逻辑:创建/标已读/清理
│ └── pusher.go # Pusher 接口 + Impl持久化 + WS 推送)
├── controller/notification_controller.go # HTTP 接口
└── provider/wire.go # Wire 依赖注入
backend/go-service/app/dto/notify_dto.go # DTO
```
### 3.8 前端页面
| 路径 | 变更 | 说明 |
|---|---|---|
| `store/notify.js` | **新建** | Pinia Storestate/fetch/markRead/initWs |
| `pages/notify/index.vue` | **新建** | 列表页 + 顶部 5 个分类 Tab + 下拉刷新 + 上拉加载 |
| `pages/profile/index.vue` | 修改 | 顶部添加「🔔 通知」入口 + 数字徽标 |
| `components/notify/NotifyItem.vue` | **新建** | 通知卡片(按类型渲染不同 UI支持内联操作按钮 |
### 3.9 Pinia Store 设计要点
```javascript
// frontend/src/store/notify.js
export const useNotifyStore = defineStore('notify', () => {
const notifications = ref([]) // 当前已加载的通知列表
const unreadCount = ref(0) // 总未读数TabBar 徽标)
const unreadByType = ref({}) // 分类型未读数
const hasMore = ref(true)
const currentFilter = ref('all') // all / friend / group / meeting / system
// WS 监听notify.new / notify.unread.total单端架构无 notify.read.ack
const initWsListeners = () => { ... }
const fetchNotifications = async (filter, beforeId) => { ... }
const markRead = async (id) => { ... }
const markAllRead = async (type) => { ... }
})
```
---
## 四、Phase 2e-2 会议 MVP 范围锁定(详细设计待 2e-1 完成后展开)
### 4.1 范围(硬边界)
- ✅ 即时会议(无预约)、会议号自动生成(格式 `XXX-XXX-XXX`
- ✅ ≤ 8 人同时参会
- ✅ 音频 + 视频 开关
- ✅ 主持人控制:静音他人、移除成员、结束会议
- ✅ 密码保护(可选)
-**发起邀请**:仅发起方内嵌"复制会议号"(暂不接通知中心,通知邀请在 2e-3
- ❌ 不做:录制、屏幕共享、虚拟背景、预约、提醒
### 4.2 技术选型(锁定)
| 组件 | 选型 | 备注 |
|---|---|---|
| 媒体服务 | `media-server/` 独立 Node.js 进程 + mediasoup v3 | 严格遵循原系统设计 |
| 客户端库 | `mediasoup-client` JS SDK | 与服务端强绑定 |
| 信令通道 | **复用现有 WebSocket Hub** | 不开新通道,复用 `Hub.RegisterEvent/DispatchEvent` |
| Go ↔ Node | HTTP RESTdocker-compose 内网) | 9 个 APIRouter/Transport/Producer/Consumer 生命周期 |
| 数据库表 | `meeting_rooms` + `meeting_participants` | 已在总设计文档定义 |
| Redis 键 | `echo:meeting:room:{code}` + `echo:meeting:members:{code}` + `echo:meeting:transport:{code}` | 已在总设计文档定义 |
### 4.3 WebSocket 信令事件11 个)
**房间事件**`meeting.room.join / leave / info`
**成员事件**`meeting.member.join / leave / mute / video`
**媒体事件**`meeting.transport.create / connect``meeting.produce.start / stop``meeting.consume.start / resume`
---
## 五、Phase 2e-3 会议增强范围锁定
- 预约会议(`meeting_rooms.type=2`+ 前端预约表单
- 定时器:到预约时间前 N 分钟触发 `meeting_reminder` 通知
- 会议邀请:从联系人/群聊发起 → 走 `meeting_invite` 通知类型
- 入会前设备预览(本地摄像头/麦克风测试页)
- 可选:等候室 / 锁定会议(视时间余量决定)
---
## 六、Pusher 调用点汇总Phase 2e-1 必须改动的现有代码)
| 文件 | 变更类型 | 说明 |
|---|---|---|
| `app/contact/service/contact_service.go` | 新增 Pusher 注入 + 4 处调用 | 申请/接受/拒绝 3 个事件 |
| `app/group/service/group_service.go` | 新增 Pusher 注入 + 6 处调用 | 邀请/入群申请/审批/踢人/角色变更 |
| `app/provider/wire.go` | 注册 notify 模块依赖 | Wire 依赖注入 |
| `frontend/src/store/contact.js` | 删除冗余的 `notify.friend.request` 处理(由 notify store 接管)| 避免重复提示 |
| `frontend/src/store/group.js` | 删除 `_onJoinRequest` toast由 notify store 统一)| 避免重复提示 |
| `frontend/src/components/CustomTabBar.vue` | 保持不变 | 联系人 Tab 徽标仍显示"待处理"数,与通知中心徽标并存 |
**重要兼容性约束**
- **联系人 Tab 徽标**pendingCount**保留不变**,继续承担"待办事项"入口
- **通知中心徽标**仅计"未读通知",两套红点语义不同,互不影响
---
## 七、风险与应对
| 风险 | 等级 | 应对 |
|---|---|---|
| Pusher 调用失败导致业务阻塞 | 中 | 采用"异步推送"Pusher 调用失败仅记日志,不阻塞业务主流程 |
| 通知表膨胀 | 低 | 30 天清理任务 + user+is_read 索引 + 按用户分表留作未来优化 |
| ~~多端已读同步消息风暴~~ | — | ~~不适用~~:当前 ws.Hub 单端连接架构已不做多端同步,该风险天然规避 |
| mediasoup2e-2技术深度 | 高 | 单独做技术 Spike必要时先搭 PoC 验证 |
| Docker Compose 增加 Node 服务后的资源占用 | 低 | 设置资源限额,文档化本机运行要求 |
---
## 八、验收标准Phase 2e-1✅ 已完成
- [x] 11 种通知类型中的 10 种(除 `meeting_invite`/`meeting_reminder`)都能正确触发并落库
- [x] 用户 A 给 B 发好友申请 → B 端 WS 实时收到 `notify.new`通知中心出现记录「我的」Tab 红点 +1
- [x] ~~多设备登录同一账号同步~~**架构限制不支持**(单端 WS Hub已在 §3.1/§3.5 修订;仅当前连接设备可感知实时更新,其他设备依赖下次重连时的 `notify.unread.total` 补偿
- [x] 30 天清理任务能正确运行,已读过期通知被删除
- [x] 通知中心列表按 5 个分类 Tab 筛选正确
- [x] 群邀请通知卡片支持内联"接受/拒绝"按钮并触发正确业务逻辑
- [x] 验证清单:`test-report-phase2e-1-notification.md`Playwright 自动化推迟到 CI 建设阶段Phase 2f 统一接入)
---
## 九、后续规划清单(必须留档)
### 9.1 推迟到 Phase 2fMVP 收尾 + 管理端扩展)
| 功能 | 原计划阶段 | 说明 |
|---|---|---|
| **WS Hub 多端连接支持改造** | 2e-1 | 当前 `ws.Hub``clients map[int64]*Client` 仅支持单连接,新登录会踢掉旧设备。需改为 `map[int64]map[deviceID]*Client` 并适配 `PublishToUser` / `ws.OnlineService` / Pusher 等下游消费者 |
| 多端已读同步notify.read.ack | 2e-1 | WS Hub 多端就绪后,再补 `notify.read.ack` 事件广播给同一用户的其他设备 |
| 管理端会议列表/详情/强制关闭 | 2e | `/api/v1/admin/meetings*` 已在总设计文档定义,仅缺实现 |
| 管理端会议统计仪表板 | 2e | `/api/v1/admin/meetings/stats` |
| 管理端通知广播发布 UI | 2e-1 | 后端 API 已就绪,仅缺前端表单页 |
| 管理端用户会议记录 | 2e | `/api/v1/admin/users/:id/meetings` |
| 管理端操作日志页面 | 2e | 数据表 `admin_operation_logs` 已设计 |
| 管理端仪表板总览 | 2e | `/api/v1/admin/dashboard` |
| 系统配置管理 | 2e | `/api/v1/admin/system/config` |
| 通知分类开关设置 | 2e-1 | 用户级通知偏好push/不push |
| Playwright E2E 自动化 CI | 2e-1 | 当前仅 `test-report-*.md` 手动验证CI 接入需评估整套 e2e 基础设施 |
### 9.2 推迟到第二期
| 功能 | 说明 |
|---|---|
| 屏幕共享 | mediasoup Producer 扩展为 screen 类型 |
| 会议录制与回放 | 需 mediasoup 录制插件 + 对象存储MinIO 已就绪) |
| 虚拟背景 / 背景模糊 | 客户端 WebRTC 滤镜 |
| 微信授权登录 | OAuth + UnionID |
| 互动直播(主播/观众/弹幕) | 独立直播流架构 |
| 消息撤回时间延长 / 管理员无时限撤回的审计日志 | —— |
| 视频消息type=4| Phase 2d 已显式推迟 |
| 表情包 / 自定义贴纸 | Phase 2d 已显式推迟 |
| 消息转发 / 合并转发 / 引用回复 | Phase 2d 已显式推迟 |
### 9.3 推迟到第三期
- 微服务拆分auth / im / meeting / notify 拆独立服务)
- Kubernetes 部署编排
- 跨服务器会议(多 mediasoup Worker 集群 + Router Pipe
- AI 辅助:语音转文字、会议纪要、智能摘要
---
## 十、文档同步与变更记录
| 日期 | 变更 |
|---|---|
| 2026-04-20 | Phase 2e 规划完成:拆分为 2e-1/2e-2/2e-3 三个子阶段;本文档落盘 |
| 2026-04-20 | Phase 2e-1 实施完成:落实 notify 模块(后端 11 种类型 / 5 REST + 2 WS / 30 天清理)+ 前端通知中心§3.1/§3.5/§八/§九 同步修订「单端 WS 连接」约束与推迟项 |