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

@@ -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/`)

View File

@@ -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` | meetingPhase 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 为直接加入成员)。

View File

@@ -1005,9 +1005,34 @@ services:
- 消息类型扩展图片/语音/文件消息
- 管理端消息管理功能
#### Phase 2e会议与通知 📋 待规划
- 多人音视频会议即时会议 + 预约会议
- 消息通知系统
#### Phase 2e会议与通知 🚧 规划完成,拆分为三子阶段(详见 `docs/plans/2026-04-20-phase2e-design.md`
- **Phase 2e-1通知系统** 🔜 开发中
- 统一通知中心好友/群聊事件 + 系统广播 + 会议通知类型预留
- 双通道持久化入库 + mini-toast
- 入口:「我的Tab 顶部铃铛 + 数字徽标
- 11 种通知类型枚举30 天保留期多端已读同步
- 跨模块 Pusher 接口contact/group 模块解耦集成
- **Phase 2e-2会议 MVP** 📋 待开发
- mediasoup Node.js 独立媒体服务 + mediasoup-client 前端集成
- 即时会议(≤ 8 )、会议号/密码音视频开关主持人控制
- WebSocket 信令复用现有 Hub
- 不含录制屏幕共享预约
- **Phase 2e-3会议增强** 📋 待开发
- 预约会议 + 定时提醒
- 会议邀请 2e-1 `meeting_invite` 通知
- 入会前设备预览
#### Phase 2f管理端扩展MVP 收尾)📋 待规划
*(从 Phase 2e 剥离出的管理端功能,保证前台用户体验先行)*
- 会议管理列表/详情/强制关闭/统计仪表板
- 通知广播发布 UI
- 管理端仪表板总览操作日志页系统配置管理
- 用户会议记录查询
### 第二期
- 屏幕共享

View File

@@ -0,0 +1,294 @@
# Phase 2e-1 设计文档:统一通知中心
> **状态:** ✅ 已完成
> **上级设计:** [Phase 2e 整体路线图](./2026-04-20-phase2e-design.md)(本文档是其 §三「Phase 2e-1 详细设计」的专项展开版本,针对具体子阶段收敛)
> **实施计划:** [Phase 2e-1 实施计划](./2026-04-20-phase2e-1-implementation.plan.md)
> **验证报告:** [Phase 2e-1 测试验证报告](../../test-report-phase2e-1-notification.md)
> **分支:** `feature/phase2e-meeting-notification`
> **最后更新:** 2026-04-20实施完成 + code-reviewer 审查修订)
---
## 一、文档定位说明
项目历史惯例:每个 Phase 子阶段对应一份 `{phase}-design.md`(设计)+ `{phase}-implementation.plan.md`(实施)文档。
**Phase 2e 采用「总-分」结构**
- `2026-04-20-phase2e-design.md` —— Phase 2e 大阶段路线图 + 三个子阶段2e-1/2e-2/2e-3的总览与设计
- `2026-04-20-phase2e-1-design.md`**本文档** —— Phase 2e-1「统一通知中心」专用设计文档汇聚该子阶段的所有设计决策与实施后修订
- `2026-04-20-phase2e-2-design.md` / `2026-04-20-phase2e-3-design.md` —— 待 2e-2/2e-3 启动时分别补充
选择这种结构的原因三个子阶段共享部分设计背景跨模块通信模式、mediasoup 与通知的关联等),保留总体设计可避免重复;但每个子阶段完成后应有独立的 design 落档,记录实施过程中的约束变化与修订。
---
## 二、目标与范围
### 2.1 业务目标
参照微信,为 EchoChat 补齐**「统一通知中心」**,解决现有系统三类问题:
1. **通知分散**:好友申请、入群邀请、系统公告等事件散落在各模块 toast用户错过即丢失
2. **不可追溯**:无持久化,历史操作无法回查
3. **无统一入口**:用户无法集中查看所有待处理事项
### 2.2 交付范围(本期 P0
| 能力 | 说明 | 对应 §phase2e-design |
|------|------|----------------------|
| 11 种通知 type 枚举 | 10 种本期落地 + 2 种预留meeting_* | [§3.2](./2026-04-20-phase2e-design.md#32-通知类型枚举11-种含-2e-22e-3-预留) |
| `notify_notifications` 表 | 新增 1 张 PostgreSQL 表 + 3 索引 + 30 天清理 | [§3.3](./2026-04-20-phase2e-design.md#33-数据库设计新增-1-张表) |
| 5 个 REST 接口 | 4 用户端 + 1 管理员广播 | [§3.4](./2026-04-20-phase2e-design.md#34-后端-rest-api设计文档更新) |
| 2 个 WS 事件 | `notify.new`(新通知) + `notify.unread.total`(断线补偿) | [§3.5](./2026-04-20-phase2e-design.md#35-websocket-事件2-个单端连接架构) |
| 跨模块 Pusher 接口 | contact/group → notify 的单向注入 | [§3.6](./2026-04-20-phase2e-design.md#36-跨模块-pusher-接口沿用-phase-2a-接口注入标准) |
| 通知中心前端 UI | profile 铃铛入口 + 5 分类 Tab 列表 + 内联操作 | [§3.8/3.9](./2026-04-20-phase2e-design.md#38-前端页面) |
### 2.3 显式不做(推迟清单)
| 能力 | 推迟原因 | 去向 |
|------|----------|------|
| 多端已读同步(`notify.read.ack` | 现有 `ws.Hub` 单连接架构踢旧连接 | Phase 2f 或二期 |
| WS Hub 多端连接改造 | 技术债,需重构 `clients map[int64]*Client` | Phase 2f 或二期 |
| 管理端广播发布 UI | 后端接口已就绪,前端仅缺表单页 | Phase 2f |
| 通知分类开关push 偏好) | MVP 默认全开 | Phase 2f |
| 会议类通知meeting_invite/reminder| 依赖 2e-2/2e-3 落地 | Phase 2e-2/2e-3 |
| Playwright E2E 自动化 CI | 当前仅手动验证清单 | CI 基础设施建设阶段 |
---
## 三、关键架构决策
### 3.1 单端 WS 连接架构(实施过程中锁定的核心约束)
**背景**:设计初期规划「多端已读同步」,实施时发现 `backend/go-service/pkg/ws/hub.go``clients map[int64]*Client` 实际只支持单连接 —— 同一用户新登录会主动关闭旧连接。
**决策**
- **保持单端架构**Phase 2e-1 不做 `ws.Hub` 改造
- **移除** `notify.read.ack` 事件(原计划跨设备广播已读)
- 通知系统设计为「当前活跃连接设备」单端体验,多端改造延后
**影响**
- [设计文档 §3.1](./2026-04-20-phase2e-design.md#31-核心体验决策) 决策表「多端已读同步」项已改为「暂不支持」
- [§3.5](./2026-04-20-phase2e-design.md#35-websocket-事件2-个单端连接架构) WS 事件数从 3 个降为 2 个
- [§七](./2026-04-20-phase2e-design.md#七风险与应对) 对应的多端消息风暴风险天然规避
- [§九](./2026-04-20-phase2e-design.md#九后续规划清单必须留档) 推迟清单新增「WS Hub 多端连接支持改造」
### 3.2 跨模块通信模式(沿用 Phase 2a 接口注入标准)
```mermaid
flowchart LR
contactSvc[contact Service]
groupSvc[group Service]
adminCtrl[admin/notify Controller]
pusher[notify.Pusher interface]
notifySvc[notify Service]
notifyDAO[notify DAO]
db[(PostgreSQL)]
hub[ws.Hub]
contactSvc -->|Wire 注入| pusher
groupSvc -->|Wire 注入| pusher
adminCtrl -->|Wire 注入| pusher
pusher --> notifySvc
notifySvc --> notifyDAO
notifyDAO --> db
notifySvc -->|SendToUser| hub
```
**关键约束**
- **依赖方向单向**contact/group → notify**反向禁止**(未来 2e-2 meeting 模块同样遵守)
- Pusher 接口定义在 `app/notify/service/pusher.go`,实现同包
- Wire 绑定:`wire.Bind(new(contactService.NotifyPusher), new(*notifyService.NotifyService))`
- **降级策略**WS 推送失败不回滚数据库,下一次 `notify.unread.total` 补偿兜底
### 3.3 WS 连接建立/重连钩子
引入新接口 `ws.NotifyConnectHook`,在 `ws.Handler` 内连接建立成功后触发:
```go
type NotifyConnectHook interface {
OnUserConnected(ctx context.Context, userID int64) error
}
```
`notify.NotifyService` 实现,推送 `notify.unread.total` 作为权威值覆盖前端本地缓存,**解决断线期间错过的通知数据一致性问题**。
### 3.4 30 天清理任务
- `app/notify/task/cleanup_task.go``time.Ticker` 驱动,默认 24 小时执行一次
- **策略**:只删除已读 + 过期(`is_read=true AND created_at < NOW() - 30 days`
- 未读通知无论多久都保留,避免漏看历史重要事项
- 生命周期:随 `cmd/server/main.go` 启动,优雅关停
---
## 四、模块结构
### 4.1 后端(`backend/go-service/app/notify/`
```
notify/
├── constants/
│ └── notify_types.go # 11 种 type 常量 + 5 种 category 映射 + WS 事件名
├── model/
│ └── notification.go # GORM 模型
├── dao/
│ └── notification_dao.go # CRUD + 批量已读 + 统计 + 清理 + 全量用户列表
├── service/
│ ├── notify_service.go # 业务逻辑(含 UserInfoResolver/NotifyConnectHook 实现)
│ └── pusher.go # Pusher 接口 + Impl持久化+WS 推送)
├── controller/
│ └── notification_controller.go # 4 用户接口 + 1 管理员广播
├── task/
│ └── cleanup_task.go # 30 天清理 cron
├── provider.go # Wire NotifySet
└── router.go # 路由注册
```
### 4.2 前端(`frontend/src/`
```
frontend/src/
├── api/notify.js # REST API 封装
├── constants/notify.js # 前端常量type/category/图标/颜色/支持内联操作判定)
├── store/notify.js # Pinia Store分类缓存 + cursor 分页 + WS 监听)
├── components/notify/
│ └── NotifyItem.vue # 通用卡片(按 type 渲染 + 内联"接受/拒绝"按钮)
└── pages/notify/
└── index.vue # 通知中心主页5 分类 Tab + 骨架/空态/列表)
```
---
## 五、验收标准
- [x] 10 种业务通知类型全部正确触发并落库friend×3 + group×6 + system×1
- [x] 用户 A 给 B 发好友申请 → B 通知中心出现记录WS 实时收到 `notify.new`,铃铛徽标 +1
- [x] 群邀请/入群申请支持内联「接受/拒绝」按钮
- [x] 管理员 `POST /api/v1/admin/notifications/broadcast` 推送 `system_broadcast` → 在线用户实时收到
- [x] 断线重连 → 收到 `notify.unread.total`,徽标与后端查询一致
- [x] 30 天前已读通知被清理;未读无论多久都保留
- [x] Logout → `notifyStore.reset()` 清空,防止跨用户数据泄漏
- [x] `frontend/src/store/contact.js:155` 旧散落监听已删除,无重复提示
- [x] `frontend/src/store/group.js` `_onJoinRequest`/`_onJoinApproved``uni.showToast` 已移除
- [x] 代码审查(`code-reviewer` 子代理通过Blocker 全部修复
---
## 六、实施后的修订记录
### 6.1 设计变更(相对原设计的偏离)
| 项 | 原计划 | 实际落地 | 原因 |
|----|--------|----------|------|
| WS 事件数量 | 3 个(含 `notify.read.ack` | **2 个** | 单端 WS 架构约束 |
| 多端已读同步 | 后端主导 WS 广播 | **不支持** | 同上,推迟到 Phase 2f |
| Playwright E2E 自动化 | 每项核心场景都有脚本 | 改为 `test-report-*.md` 手动验证清单 | 缺少 E2E CI 基础设施 |
### 6.2 Code-Reviewer 审查发现2026-04-20
整体结论:**有条件通过**1 Blocker / 5 Major / 11 Minor / 10 亮点)
**🔴 Blocker已当场修复**
- `markAllRead` 前后端契约错位:前端将 `category` 放入 PUT body后端读 query string → 分类标记已读失效
- **修复**`frontend/src/api/notify.js` 改为 `PUT /api/v1/notifications/read-all?category=xxx`;同步 [Notify API 文档 §4](../api/frontend/notify.md#4-批量标记已读) 明确 Query 契约
- **验证**:后端 `go build ./...` 通过;纳入 Playwright 验证清单复测项
**🟡 Major / 🟢 Minor 项**:均不阻塞合入,已纳入 [Phase 2e 设计文档 §九](./2026-04-20-phase2e-design.md#九后续规划清单必须留档) Phase 2f 清理清单,包括:
- Response 字段与文档一致性细化
- 前端常量重复定义合并
- WS `notify.new` / `notify.unread.total` 事件竞态兜底
- NotifyItem 防重点击
- Broadcast 错误语义增强
- DDL 字段尺寸微调 / Pusher 签名与设计稿对齐 / WS 幂等去重 / goroutine 限流等
### 6.3 Playwright MCP 端到端验证2026-04-20
> 详细见 [测试验证报告 §八](../../test-report-phase2e-1-notification.md#八playwright-mcp-自动化验证与-bug-修复2026-04-20)
通过 Playwright MCP 驱动 H5 浏览器对 testuser1id=4完整走查登录 → 进入通知中心 → 构造好友申请 → 管理员广播 → 点击标记已读 → Deep-link 跳转 → 全部已读,**所有链路通过**。
**现场发现并修复 2 个 Bug**
| 级别 | Bug | 根因 | 修复 |
|------|-----|------|------|
| 🔴 Bug-1 | 点击通知 `PUT .../undefined/read` 400 | `NotifyItem` 自定义 emit 名 `tap` 与 uni-app 原生 DOM 事件冲突Event 对象覆盖 notify 参数 | emit 名改为 `item-tap` / `item-accept` / `item-reject`,父组件同步更新 |
| 🟡 Bug-2 | 标记已读后分类角标不递减,依赖下次 fetch 修复 | `markRead` 调用 `_patchAll``target.is_read` 已变 true`if (!target.is_read)` 永假 | 预先快照 `const wasUnread = !!target && !target.is_read` |
**涉及文件**
- `frontend/src/components/notify/NotifyItem.vue`
- `frontend/src/pages/notify/index.vue`
- `frontend/src/store/notify.js`
**验证结论**WS 实时推送admin 广播 → 1s 内前端自动插入列表 + 角标 +1、Deep-link 跳转(好友申请 → `/pages/contact/request`)、批量清零(「全部已读」按钮)、分类 Tab 独立统计等全部正常。前后端 `go build ./...` 与前端 lint 均通过。
### 6.4 TabBar「我的」聚合未读红点2026-04-21
#### 背景
实施完成后反馈:通知中心的未读数仅在 `/pages/profile/index` 页面内(铃铛 badge、菜单项 badge可见用户在其他 tabBar 页面(消息/联系人/会议)时无法感知「我的」模块有新事件,需被动切到"我的"才知道。这违反了"一级导航应承担未读提醒职责"的基本原则。
#### 设计决策
采用业界主流做法(微信/QQ/钉钉的"我"Tab 模式):**tabBar「我的」图标右上角显示纯红点无数字作为"我的"模块所有未读事件的聚合指示器**。
设计原则:
1. **信息层级分离**tabBar 是一级导航,只需 Boolean有/无未读);具体数字属于二级信息,进入页面后再展示
2. **聚合指示器可扩展**:红点来源是一个**开放集合**,当前只聚合 `notifyStore.unreadTotal`未来可无缝追加「资料待完善」「安全提醒」「新版本可用」等tabBar 无需感知具体来源
3. **语义清晰不混用**:保留现有 `getBadge(index)`(返回数字,用于消息/联系人 Tab新增 `hasDot(index)`(返回布尔,用于"我的" Tab模板优先渲染数字 badge无数字时再渲染红点
#### 实现摘要
| 层 | 改动 |
|---|---|
| `frontend/src/components/CustomTabBar.vue` | 引入 `useNotifyStore`,新增 `hasDot(index)` 方法;模板条件渲染 `.tab-dot` 元素补充小红点样式16rpx 红色圆点 + 2rpx 白色描边) |
核心代码(聚合逻辑集中在 `hasDot(3)`,未来扩展仅需追加 `|| 新来源`
```javascript
hasDot(index) {
if (index === 3) {
const notifyStore = useNotifyStore()
return notifyStore.unreadTotal > 0
// 未来扩展示例:
// || profileStore.hasProfileReminder
// || securityStore.hasSecurityAlert
// || appStore.hasNewVersion
}
return false
}
```
#### 验证Playwright
| 场景 | tabBar "我的" | 结果 |
|---|---|---|
| 有 3 条未读 · 消息页 | 红点亮 | ✅ |
| 有 3 条未读 · 我的页(选中态) | 红点亮 | ✅ |
| 点"全部已读"后 · 我的页 | 红点消失 | ✅ |
| 返回消息页 | 红点消失 | ✅ |
三层信息层级同步响应 `unreadTotal` 变化tabBar 红点Boolean / 铃铛 badge数字 3 / 通知中心菜单 badge数字 3
---
## 七、关联文档
- [Phase 2e 整体路线图](./2026-04-20-phase2e-design.md)(本文档的上级设计)
- [Phase 2e-1 实施计划](./2026-04-20-phase2e-1-implementation.plan.md)11 个 Task 拆分)
- [Phase 2e-1 测试验证报告](../../test-report-phase2e-1-notification.md)E2E 清单 + 审查修复记录)
- [Notify API 文档](../api/frontend/notify.md)5 REST + 2 WS 事件)
- [项目开发进度 · CURRENT_STATUS](../progress/CURRENT_STATUS.md)
- [项目上下文 · project-context.mdc](../../.cursor/rules/project-context.mdc)
---
## 八、变更记录
| 日期 | 变更 |
|------|------|
| 2026-04-20 | Phase 2e-1 设计文档首版落盘(由 Phase 2e 总设计 §三 专项展开 + 实施后修订合并) |
| 2026-04-20 | 锁定单端 WS 架构决策,移除 `notify.read.ack`code-reviewer Blocker 修复记录 |
| 2026-04-20 | 追加 §6.3 Playwright MCP 端到端验证成果,记录 2 个现场发现的交互 Bug 修复事件名冲突、markRead 竞态) |
| 2026-04-21 | 追加 §6.4 TabBar「我的」聚合未读红点设计与实现解决跨 tabBar 页面的未读感知问题(为后续我的模块功能扩展预留聚合入口) |

View File

@@ -0,0 +1,249 @@
# Phase 2e-1 实施计划:统一通知中心
> **状态:** ✅ 已完成
> **设计文档:** [Phase 2e 设计文档](./2026-04-20-phase2e-design.md)
> **验证报告:** [Phase 2e-1 测试验证报告](../../test-report-phase2e-1-notification.md)
> **分支:** `feature/phase2e-meeting-notification`
> **原规划文件Cursor 临时):** `.cursor/plans/phase_2e-1_通知系统实施_b2d43c9d.plan.md`(已被 `.gitignore`,本文档为其正式归档版本)
> **最后更新:** 2026-04-20Task 11 落盘同步)
---
## 一、范围锁定
### 包含
- **10 种业务通知类型**(本期落地触发):
- `friend_request` / `friend_accepted` / `friend_rejected`
- `group_invite` / `group_join_request` / `group_join_approved` / `group_join_rejected` / `group_kicked` / `group_role_changed`
- `system_broadcast`
- **2 种仅预留枚举**(业务由 2e-2/2e-3 对接):`meeting_invite``meeting_reminder`
- 单端 WS 连接架构(沿用现状):**不做多端已读同步**
- 双通道推送:持久化入库 + WS `notify.new` 实时推送 + 铃铛徽标 + mini-toast
- 通知中心 UI顶部 5 分类 Tab全部/好友/群聊/会议/系统)+ 下拉刷新 + 上拉加载
- 管理员广播:`POST /api/v1/admin/notifications/broadcast`(仅后端,前端 UI 推迟)
### 显式推迟(详见设计文档 §九)
- **WS Hub 多端连接支持改造** → Phase 2f / 二期
- `notify.read.ack` 跨设备广播 → 依赖上项,同步推迟
- 管理端广播发布 UI → Phase 2f
- 通知分类开关push 偏好设置)→ Phase 2f
- Playwright 自动化 CI → 待 CI 基础设施建设统一接入
---
## 二、关键架构决策
### 2.1 跨模块通信模式(沿用 Phase 2a 接口注入标准)
```mermaid
flowchart LR
contactSvc[contact Service]
groupSvc[group Service]
adminCtrl[admin/notify Controller]
pusher[notify.Pusher interface]
notifySvc[notify Service]
notifyDAO[notify DAO]
db[(PostgreSQL)]
hub[ws.Hub]
contactSvc -->|Wire 注入| pusher
groupSvc -->|Wire 注入| pusher
adminCtrl -->|Wire 注入| pusher
pusher --> notifySvc
notifySvc --> notifyDAO
notifyDAO --> db
notifySvc -->|SendToUser| hub
```
- `notify.Pusher` 接口定义在 `app/notify/service/pusher.go`,实现同包
- contact/group 通过 Wire 绑定 `NotifyPusher` 字段interface 类型)
- **单向依赖**contact/group → notify**反向禁止**Phase 2e-2 meeting 模块同样遵守)
- **降级策略**WS 推送失败不回滚入库,下一次 `notify.unread.total` 补偿兜底
### 2.2 单端 WS 推送2 个事件)
| 事件 | 方向 | 触发时机 | Payload |
|------|------|----------|---------|
| `notify.new` | S→C | Pusher 入库成功后立即推送 | 完整通知对象 |
| `notify.unread.total` | S→C | 连接建立/重连后钩子触发 | `{ total, by_category }` 权威值 |
**不引入** `notify.read.ack`WS Hub 单连接架构已天然规避多端同步风暴)。
### 2.3 数据库(新增 1 张表)
`notify_notifications` 表:`id / user_id / type / title / content / extra(JSONB) / actor_id / target_type / target_id / is_read / read_at / created_at`
索引:`(user_id, is_read, created_at DESC)``(user_id, type, created_at DESC)``created_at`(配合 30 天清理)。
---
## 三、文件与变更清单
### 3.1 后端新增(`backend/go-service/`
| 文件 | 作用 |
|------|------|
| `app/notify/constants/notify_types.go` | 11 种 type 常量 + 5 种 category 映射 + WS 事件名 |
| `app/notify/model/notification.go` | GORM 模型 |
| `app/notify/dao/notification_dao.go` | CRUD + 批量已读 + 未读统计 + 清理 + 全量用户列表 |
| `app/notify/service/notify_service.go` | 业务逻辑(创建/列表/标已读/广播) |
| `app/notify/service/pusher.go` | `Pusher` 接口 + Impl持久化 + WS 推送) |
| `app/notify/controller/notification_controller.go` | 4 用户接口 + 1 管理员广播接口 |
| `app/notify/router.go` | 路由注册 |
| `app/notify/provider.go` | Wire `NotifySet` |
| `app/notify/task/cleanup_task.go` | 30 天清理 cron默认每日 |
| `app/constants/notify.go` | 跨模块共享常量type / category / WS event |
| `app/dto/notify_dto.go` | 请求 / 响应 / 广播 DTO |
### 3.2 后端改造
| 文件 | 改动 |
|------|------|
| `app/provider/provider.go` | `App` 结构体新增 `NotificationController` / `NotifyCleanupTask` / `NotifyPusher``NewApp` 签名扩展 |
| `app/provider/wire.go` | 注册 `NotifySet`;绑定 `contactService.NotifyPusher` / `groupService.NotifyPusher` / `notifyService.UserInfoResolver` / `ws.NotifyConnectHook` |
| `app/provider/wire_gen.go` | 同步 Wire 生成产物 |
| `app/contact/service/contact_service.go` | **3 处** Pusher.PushSendFriendRequest / AcceptFriendRequest / RejectFriendRequest |
| `app/group/service/group_service.go` | **6 处** Pusher.PushInviteMembers / RequestJoin / ApproveJoin / RejectJoin / KickMember / ChangeRole |
| `app/ws/handler.go` | 连接建立/重连时触发 `NotifyConnectHook.OnUserConnected` → 推送 `notify.unread.total` |
| `cmd/server/main.go` | 启动 `app.NotifyCleanupTask.Start()` + `defer Stop()` |
| `router/router.go` | `notifyApp.RegisterRoutes(engine, app.NotifyController, jwtAuth)` |
| `deploy/docker/postgres/init.sql` | 追加 `notify_notifications` DDL + 3 个索引 |
### 3.3 前端新增(`frontend/src/`
| 文件 | 作用 |
|------|------|
| `api/notify.js` | REST API 封装getNotifications / getUnreadCount / markRead / markAllRead |
| `constants/notify.js` | 前端常量(与后端 type/category 对齐 + 图标/颜色/支持内联操作判定) |
| `store/notify.js` | Pinia Store分类缓存 + cursor 分页 + WS 监听 + reset |
| `components/notify/NotifyItem.vue` | 通用卡片(按 type 渲染 + 内联"接受/拒绝"按钮) |
| `pages/notify/index.vue` | 通知中心页5 分类 Tab + 骨架/空态/列表) |
### 3.4 前端改造
| 文件 | 改动 |
|------|------|
| `pages.json` | 注册 `pages/notify/index`custom navigationStyle |
| `App.vue` | 全局 WS 初始化时调用 `useNotifyStore().initWsListeners() + fetchUnreadCount()` |
| `pages/auth/login.vue` | 登录成功后同上初始化 |
| `pages/profile/index.vue` | 顶部铃铛图标 + 徽标 + 「通知中心」菜单项 + `onShow` 刷新未读 |
| `store/user.js` | `logout()` 内调用 `notifyStore.reset()`,防止跨用户数据泄漏 |
| `store/contact.js` | 删除原 `notify.friend.request` 散落监听(由 notify store 接管) |
| `store/group.js` | 清理 `_onJoinRequest` / `_onJoinApproved` 中的 `uni.showToast`(统一到 notify store |
### 3.5 文档
| 文件 | 操作 |
|------|------|
| `docs/plans/2026-04-20-phase2e-design.md` | §3.1/§3.5/§八/§九 修订「单端 WS 架构」约束 |
| `docs/plans/2026-04-20-phase2e-1-implementation.plan.md` | **本文档**(正式版归档) |
| `docs/api/frontend/notify.md` | 新增 notify API 文档5 REST + 2 WS 事件) |
| `docs/api/README.md` | 导航更新 |
| `docs/progress/CURRENT_STATUS.md` | 进度同步 |
| `.cursor/rules/project-context.mdc` | 跨模块通信模式 + 推迟清单更新 |
| `test-report-phase2e-1-notification.md` | E2E 验证报告 + Task 11 审查修复记录 |
---
## 四、Task 拆分11 个 Task按依赖分层
| # | Task | 依赖 | 交付产物 |
|---|------|------|----------|
| **Task 0** | 基础设施DDL + constants + DTO + model | — | 数据库表、常量、DTO、GORM 模型 |
| **Task 1** | Notify 模块骨架DAO + Service + Pusher 接口 + Wire | T0 | 完整模块雏形,可独立编译 |
| **Task 2** | REST API4 用户接口 + 1 管理员广播 | T1 | `/api/v1/notifications/*` + `/api/v1/admin/notifications/broadcast` |
| **Task 3** | WS 事件:`notify.new` + `notify.unread.total` 断线补偿 | T1 | Pusher 内部推送 + `ws.Handler` 钩子 |
| **Task 4** | contact 模块 Pusher 集成3 类好友事件) | T1 | 申请/接受/拒绝 → 3 处 Push |
| **Task 5** | group 模块 Pusher 集成6 类群聊事件) | T1 | 邀请/申请/批准/拒绝/踢人/角色变更 → 6 处 Push |
| **Task 6** | 30 天清理定时任务 | T1 | `CleanupTask` + 启动/停止集成 |
| **Task 7** | 前端 Pinia Store + API 封装 + WS 监听 | T2+T3 | `store/notify.js` + `api/notify.js` + 2 事件监听 |
| **Task 8** | 通知中心页 + NotifyItem 组件 + 5 分类 Tab | T7 | `pages/notify/index.vue` + 组件 |
| **Task 9** | profile 入口集成 + 冗余代码清理 | T8 | 铃铛徽标 + `contact.js:155` / `group.js:292` 清理 |
| **Task 10** | 端到端验证4 类场景 + 视觉回归) | T4+T5+T9 | `test-report-phase2e-1-notification.md` |
| **Task 11** | 设计文档修订 + `code-reviewer` 审查 + 所有文档同步 | T10 | 设计文档 §3.1/§3.5/§八/§九 修订 + 审查报告 |
### 依赖关系图
```mermaid
graph TD
T0[Task 0 基础设施] --> T1[Task 1 Notify 骨架]
T1 --> T2[Task 2 REST API]
T1 --> T3[Task 3 WS 事件]
T1 --> T4[Task 4 contact 集成]
T1 --> T5[Task 5 group 集成]
T1 --> T6[Task 6 清理任务]
T2 --> T7[Task 7 前端 Store]
T3 --> T7
T7 --> T8[Task 8 通知中心页]
T8 --> T9[Task 9 入口集成]
T4 --> T10[Task 10 E2E 验证]
T5 --> T10
T9 --> T10
T10 --> T11[Task 11 文档同步 + 审查]
```
---
## 五、端到端验收标准
- [x] 用户 A 给 B 发好友申请 → B「我的」铃铛徽标 +1通知中心出现记录WS 实时收到 `notify.new`
- [x] B 点击通知 → 跳转好友申请页;处理后通知自动标已读 → 徽标 -1
- [x] 张三邀请李四入群 → 李四收到卡片内联「接受/拒绝」,操作后群状态与通知状态一致
- [x] 管理员 `POST /api/v1/admin/notifications/broadcast` → 所有在线用户实时收到 `system_broadcast`
- [x] 断线重连:前端收到 `notify.unread.total`,徽标与后端 API 查询一致
- [x] 30 天前已读通知被清理任务删除;未读通知无论多久都不被清理
- [x] `frontend/src/store/contact.js:155` 旧处理已删除,无重复提示
- [x] Logout → `notifyStore.reset()` 清空,防止跨用户数据泄漏
- [x] ~~多端已读同步~~**架构限制不支持**,设计文档已修订
---
## 六、风险与应对
| 风险 | 等级 | 应对 | 实际结果 |
|------|------|------|----------|
| Pusher 同步调用阻塞业务 | 中 | WS 推送失败仅 warn 日志,不阻塞业务主流程 | ✅ 已落实 |
| contact/group 现有业务逻辑受影响 | 中 | 每个调用点保留原路径Pusher 仅旁路调用 | ✅ 无回归 |
| Wire 生成器循环依赖 | 高 | 严格单向 contact/group → notifyinterface 注入 | ✅ 无循环 |
| 前端 toast 与现有 `uni.showToast` 冲突 | 中 | 统一由 notify store 触发,其他模块不再直接调用 | ✅ 已清理 `contact.js:155` / `group.js:292` |
| 管理端广播权限 | 高 | `middleware.RequireRole(admin/super_admin)` | ✅ 已校验 |
| 代码审查漏网 | 中 | Task 11 调用 `code-reviewer` 子代理 | ✅ 发现 1 Blocker 已修复 |
---
## 七、实施后的连带动作
1. ✅ 修订 `docs/plans/2026-04-20-phase2e-design.md`
- §3.1 决策表「多端已读同步」改为「暂不支持WS Hub 单端架构)」
- §3.5 WS 事件表移除 `notify.read.ack`
- §八验收标准全部标记完成并注明限制
- §九推迟清单新增「WS Hub 多端连接支持改造」+「Playwright E2E CI」
- §十新增 2026-04-20 实施完成变更记录
2.`code-reviewer` 子代理审查(结论:有条件通过)
- 🔴 Blocker`markAllRead` 前端 body / 后端 query 契约错位 → 已修复(`frontend/src/api/notify.js` 改走 Query
- 🟡 Major × 5 / 🟢 Minor × 11不阻塞合入纳入 Phase 2f 清理清单
3. ✅ 所有规划 + 实施 + 验证文档在同一分支(`feature/phase2e-meeting-notification`)提交
4.`feature/phase2e-meeting-notification` 分支准备就绪,下一步可 PR 合并至主干或继续 Phase 2e-2
---
## 八、工期回顾
| 子任务 | 预估 | 实际 | 备注 |
|--------|------|------|------|
| 后端Task 0-6 | 2 人日 | ~2 人日 | Wire 绑定与 ws.Handler 钩子略超预期 |
| 前端Task 7-9 | 1.5 人日 | ~1.5 人日 | 分类缓存 + WS 去重实现顺利 |
| E2E + 审查 + 文档Task 10-11 | 0.5 人日 | ~1 人日 | 代码审查发现 Blocker 增加修复耗时 |
| **合计** | **4 人日** | **~4.5 人日** | 与设计文档预估基本一致 |
---
## 九、关联文档
- [Phase 2e 总体设计](./2026-04-20-phase2e-design.md)
- [Phase 2e-1 测试验证报告](../../test-report-phase2e-1-notification.md)
- [Notify API 文档](../api/frontend/notify.md)
- [项目开发进度 · CURRENT_STATUS](../progress/CURRENT_STATUS.md)
- [项目上下文 · project-context.mdc](../../.cursor/rules/project-context.mdc)

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

View File

@@ -1,11 +1,123 @@
# EchoChat 项目开发进度
> **最后更新**2026-04-20Phase 2d 完成 + UX 优化 + 已读状态刷新持久化
> **当前阶段**Phase 2d 全部完成 + Bug 修复 7 项 + UX 优化 2 项
> **当前分支**`feature/phase2c-group-read-receipt`Phase 2d 工作误用了 2c 分支,下一阶段 2e 直接新建分支开发)
> **实施计划**`docs/plans/2026-03-04-phase2d-implementation.plan.md`
> **设计文档**`docs/plans/2026-03-04-phase2d-design.md`
> **本次修复报告**`test-report-phase2d-bugfix.md`
> **最后更新**2026-04-21Phase 2e-1 通知系统 + TabBar 聚合红点 UX 完善
> **当前阶段**Phase 2e-1 通知系统 ✅ 已完成;下一步进入 Phase 2e-2 会议 MVP
> **当前分支**`feature/phase2e-meeting-notification`
> **Phase 2e 整体设计**`docs/plans/2026-04-20-phase2e-design.md`(三子阶段路线图 + 后续规划清单)
> **Phase 2e-1 专用设计**`docs/plans/2026-04-20-phase2e-1-design.md`(子阶段详细设计 + 实施后修订 + 审查记录)
> **Phase 2e-1 实施计划**`docs/plans/2026-04-20-phase2e-1-implementation.plan.md`11 个 Task 全部完成;原 `.cursor/plans/*` Cursor 临时文件已归档至本位置)
> **Phase 2e-1 验证报告**`test-report-phase2e-1-notification.md`
---
## ✅ 2026-04-20 Phase 2e-1 通知系统完成
**交付范围**:统一通知中心,覆盖好友 / 群聊 / 会议(预留枚举)/ 系统广播 四大类,共 11 种业务通知类型。
### 后端(`backend/go-service/`
| 模块 | 路径 | 作用 |
|---|---|---|
| 数据表 | `deploy/docker/postgres/init.sql` | 新增 `notify_notifications` + 3 索引 |
| 常量 | `app/constants/notify.go` | 11 种 type 常量 + 4 种 category + WS 事件名 |
| Model | `app/notify/model/notification.go` | GORM 模型 |
| DTO | `app/dto/notify_dto.go` | 请求/响应/广播 DTO |
| DAO | `app/notify/dao/notification_dao.go` | CRUD + 批量 + 未读统计 + 清理 + 全量用户列表 |
| Service | `app/notify/service/{notify_service.go,pusher.go}` | 业务逻辑 + Pusher 接口 + 持久化+推送降级 |
| Controller | `app/notify/controller/notification_controller.go` | 4 用户接口 + 1 管理员广播接口 |
| Router | `app/notify/router.go` | `/api/v1/notifications/*` + `/api/v1/admin/notifications/broadcast` |
| Cleanup Task | `app/notify/task/cleanup_task.go` | 30 天已读通知定时清理(默认每日) |
| Provider | `app/notify/provider.go` + `app/provider/{provider,wire,wire_gen}.go` | Wire 注入 + NotifyPusher / NotifyConnectHook 接口绑定 |
| WS 钩子 | `app/ws/handler.go` | 连接建立 → 推送 `notify.unread.total` 补偿 |
| contact 集成 | `app/contact/service/contact_service.go` | 3 处 Pusher.Pushfriend_request/accepted/rejected |
| group 集成 | `app/group/service/group_service.go` | 6 处 Pusher.Pushinvite/join_request/approved/rejected/kicked/role_changed |
| main.go | `cmd/server/main.go` | 启动 CleanupTask + defer Stop |
### 前端(`frontend/src/`
| 文件 | 作用 |
|---|---|
| `api/notify.js` | 4 个 REST 封装 |
| `constants/notify.js` | 前端 type / category / 图标 / 颜色常量 |
| `store/notify.js` | Pinia Store5 分类分页缓存 + 未读数 + WS 事件notify.new / notify.unread.total |
| `components/notify/NotifyItem.vue` | 通用通知卡片(按 type 渲染 + 内联接受/拒绝) |
| `pages/notify/index.vue` | 通知中心主页(顶部铃铛 + 5 Tab + 下拉刷新 + 无限滚动 + 全部已读) |
| `pages/profile/index.vue` | 新增铃铛入口 + 徽标 + 菜单项 |
| `pages.json` | 注册 `pages/notify/index` |
| `App.vue` / `pages/auth/login.vue` | 全局初始化 notifyStore 监听 + 未读数拉取 |
| `store/user.js` | logout 时调用 `notifyStore.reset()` 清缓存 |
| `store/contact.js` / `store/group.js` | 清理散落 toast 和冗余 `notify.friend.request` / `group.join.request` 直接处理 |
### 架构决策
- **单端 WS 连接架构**:沿用现有 `ws.Hub`**不做多端已读同步**。多端改造推迟到 Phase 2f/二期(已在设计文档 §3.1/§3.5/§九记录)。
- **跨模块依赖方向**`contact` / `group``notify`(严格单向),通过接口 `notifyService.Pusher` 注入,类似 Phase 2a `ws.FriendIDsGetter → contact.FriendshipDAO` 模式。
- **降级策略**Pusher 先入库、后推送WS 推送失败不回滚入库;入库失败仅 Warn 日志不影响业务。
- **游标分页**:通知列表使用 `before_id` + `limit` 替代传统 `page`,天然抗数据插入扰动。
- **30 天清理**:默认每 24 小时扫描一次,删除 `is_read=true AND created_at < NOW() - INTERVAL 30 DAY`;未读永久保留。
### Task 完成情况11/11
- [x] Task 0: 数据库 DDL + constants + DTO + model
- [x] Task 1: Notify 模块骨架DAO + Service + Pusher 接口 + Wire
- [x] Task 2: REST API4 用户 + 1 管理员)
- [x] Task 3: WS 事件 `notify.new` + `notify.unread.total`
- [x] Task 4: contact 集成3 类)
- [x] Task 5: group 集成6 类)
- [x] Task 6: 30 天清理定时任务
- [x] Task 7: 前端 Pinia Store + API + WS 监听
- [x] Task 8: 通知中心页 + NotifyItem + 5 分类 Tab
- [x] Task 9: profile 入口 + 冗余清理contact.js:155 / group.js:292
- [x] Task 10: E2E 验证清单(`test-report-phase2e-1-notification.md`
- [x] Task 11: 文档同步 + `code-reviewer` 子代理审查(本文件 + API 文档 + project-context.mdc + 设计文档 §3.1/§3.5/§八/§九 修订)
### Task 11 代码审查成果2026-04-20
- `code-reviewer` 整体结论:**有条件通过**1 Blocker / 5 Major / 11 Minor / 10 亮点)
- **Blocker 已当场修复**:前端 `markAllRead` 契约错位 —— 原将 `category` 放入 PUT body 而后端读 query已改为 `?category=xxx` Query 拼接,并同步 `docs/api/frontend/notify.md` §4
- **Major / Minor 项**:均不阻塞合入,已纳入 `docs/plans/2026-04-20-phase2e-design.md` §九 Phase 2f 清理清单
- 涉及修复文件:`frontend/src/api/notify.js``docs/api/frontend/notify.md``test-report-phase2e-1-notification.md` §七
- 验证:`cd backend/go-service && go build ./...` 通过
### Playwright MCP 端到端验证成果2026-04-20
- 使用 Playwright MCP 驱动 H5 浏览器完整走查通知中心:登录 → 进入 `/pages/notify/index` → 构造好友申请 → 管理员广播 → 点击通知 → 标记已读 → 跳转 → 全部已读,均通过
- **WS 实时推送链路通过**admin 广播后 1s 内页面自动插入通知到列表顶部,「全部/系统」角标实时变化,证明 `notify.new` 事件、前端 `_onNotifyNew` 处理器、UI 响应式更新三段链路一致
- **额外修复 2 个交互 Bug**Playwright 现场发现):
- 🔴 Bug-1`NotifyItem` 自定义 emit 事件名 `tap` 与 uni-app 原生 DOM 事件冲突,导致 Event 对象覆盖 notify 参数 → `PUT .../undefined/read` 400。修复emit 名改为 `item-tap` / `item-accept` / `item-reject`
- 🟡 Bug-2`store/notify.js#markRead``_patchAll` 后再判断 `!target.is_read` 永假unreadTotal 没有递减。修复:预先快照 `wasUnread`
- 修改文件:`frontend/src/components/notify/NotifyItem.vue``frontend/src/pages/notify/index.vue``frontend/src/store/notify.js`
- 详细过程见 `test-report-phase2e-1-notification.md` §八
### TabBar「我的」聚合未读红点2026-04-21
- **背景**:通知未读状态之前仅在 `/pages/profile/index` 内可见(铃铛 badge、菜单项 badge用户在其他 tabBar 页面(消息/联系人/会议)无法感知有新事件,必须被动切进"我的"才能发现
- **设计**:采用业界主流做法(微信/QQ/钉钉的"我"Tab 模式)—— tabBar「我的」图标右上角显示**纯红点(无数字)**作为"我的"模块**聚合未读指示器**
- 信息层级分离tabBar 一级导航只承载 Boolean有/无未读),具体数字留在二级页面
- 聚合开放集合:当前只聚合 `notifyStore.unreadTotal`,未来可无缝追加「资料待完善」「安全提醒」「新版本可用」等
- 语义清晰:保留原 `getBadge(index)`(数字,用于消息/联系人);新增 `hasDot(index)`(布尔,用于"我的");模板先数字后红点优先级渲染
- **实现文件**`frontend/src/components/CustomTabBar.vue`(新增 `hasDot` 方法 + `.tab-dot` 样式)
- **Playwright 回归**:有 3 条未读时消息页/我的页 tabBar 红点均亮起;点"全部已读"后 tabBar 红点消失、铃铛 badge 消失、菜单 badge 消失三层同步响应;所有场景均通过
- **详细设计**:见 `docs/plans/2026-04-20-phase2e-1-design.md` §6.4
---
---
## 📘 2026-04-20 Phase 2e 规划:会议与通知系统
**拆分理由**:原 Phase 2e会议+通知)工作量约 23-33 人日引入全新技术栈mediasoup + Node.js必须按风险梯度拆分。
| 子阶段 | 范围 | 周期 | 状态 |
|---|---|---|---|
| **2e-1 通知系统** | 统一通知中心11 种类型)+ 双通道推送 + 跨模块 Pusher 接口 | 3-4 天 | ✅ 已完成 |
| **2e-2 会议 MVP** | mediasoup Node 媒体服务 + 即时会议≤8人+ 基础音视频控制 | 10-14 天 | 📋 待开发 |
| **2e-3 会议增强** | 预约会议 + 定时提醒 + 会议邀请(复用 2e-1 通知) | 7-10 天 | 📋 待开发 |
**关键决策**
- 维持原 mediasoup SFU 架构(不改用 Mesh
- 会议 MVP 仅音视频通话(不含录制/屏幕共享/预约)
- 管理端扩展推迟到 Phase 2f新增阶段
- 通知系统覆盖全部 10+ 种业务事件,含 `meeting_invite`/`meeting_reminder` 类型预留
**后续规划清单**(含推迟项,见设计文档 §九):
- Phase 2f会议管理后台 + 通知广播发布 UI + 管理端仪表板等
- 第二期:屏幕共享、录制、虚拟背景、微信登录、互动直播
- 第三期微服务拆分、K8s、多 Worker 集群、AI 辅助
---