Files
EchoChat/docs/plans/2026-04-20-phase2e-design.md
bujinyuan f1853f125d 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
2026-04-21 10:21:01 +08:00

359 lines
18 KiB
Markdown
Raw 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.

# 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 连接」约束与推迟项 |