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

7.7 KiB
Raw Permalink Blame History

通知模块 API (Notify)

通用规范(认证方式、响应格式、错误码)见 README.md
新通知的实时推送通过 WebSocket 完成,见 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_invitePhase 2e-2 已接入) / meeting_reminderPhase 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

成功响应:

{
    "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

权限: 需认证

成功响应:

{
    "code": 0,
    "message": "success",
    "data": {
        "total": 5,
        "by_category": {
            "friend": 1,
            "group": 3,
            "meeting": 0,
            "system": 1
        }
    }
}

3. 标记单条已读

PUT /api/v1/notifications/:id/read

权限: 需认证(只能标记属于自己的通知)

响应:

{ "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 BodyPUT 方法无需请求体。

示例:

  • 全部标已读:PUT /api/v1/notifications/read-all
  • 仅好友类标已读:PUT /api/v1/notifications/read-all?category=friend

响应:

{ "code": 0, "message": "success", "data": { "affected": 3 } }

5. 管理员广播系统通知

POST /api/v1/admin/notifications/broadcast

权限: RoleAdminRoleSuperAdminJWT + 角色校验)

请求体:

{
    "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 字符串

响应:

{ "code": 0, "message": "success", "data": { "affected": 128 } }

后端将遍历所有活跃用户批量写入通知,在线用户实时收到 notify.new WS 推送;离线用户重连后通过 notify.unread.total 触发未读数补偿。


6. WebSocket 事件

notify.new — 新通知到达

方向 触发
Server → Client 每次通知写入成功后

载荷:与 REST 返回的单条通知对象字段一致。

{
  "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 连接建立成功时

载荷

{
  "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 为直接加入成员)。