Files
im_server/docs/IM-服务端主动推送消息梳理.md
T

214 lines
12 KiB
Markdown

# IM 服务端主动推送消息梳理
> 本文先按当前代码实现梳理,后续确认产品口径后再合并到《IM-前后端对接文档》。
## 1. 术语和通道
服务端主动下行分两层:
1. Tinode 原生实时消息:通过 WebSocket/Long Polling 下发 `{data}`、`{info}`、`{pres}`、`{meta}` 等帧,覆盖聊天消息、回执、输入中、在线状态、通话信令。
2. IM 产品业务事件:HTTP 写操作或业务事务写入 `im_business_events_outbox` / 群业务 outbox 后,由服务端投递为 `{info}` 帧,前端据此刷新业务接口或局部状态。
当前产品业务事件不是聊天消息,不占普通会话消息 `seq`。系统通知列表是否出现记录,取决于是否额外写入 `im_system_notifications`。
## 2. 产品业务事件统一帧格式
### 2.1 用户维度事件
来自 `im_business_events_outbox`,投递到用户自己的在线会话。
```json
{
"info": {
"topic": "usr_target",
"from": "usr_actor",
"what": "im",
"event": "friend.request.accepted",
"payload": {
"user_id": "usr_actor"
},
"ts": "2026-08-25T10:00:00Z"
}
}
```
字段说明:
| 字段 | 说明 |
| --- | --- |
| `info.topic` | 目标用户 ID,即事件接收人 |
| `info.from` | 操作人用户 ID;系统事件可能为空 |
| `info.what` | 固定为 `im` |
| `info.event` | 产品事件类型 |
| `info.payload` | 事件载荷,按事件类型变化 |
投递机制:
- 写入 outbox 后会立即尝试投递。
- Hub 不可用或队列满时保持 pending,后续 poller 每秒按在线租户重试。
- 投递成功后标记 delivered。
- `registration.invite_used` 与 `security.password_reset` 是内部消费事件,不作为普通 WS 业务事件下发。
### 2.2 群维度事件
来自群业务 outbox,投递到群 Topic,在线群成员收到。
```json
{
"info": {
"topic": "grp_xxx",
"from": "usr_actor",
"what": "group",
"event": "group.member.joined",
"payload": {
"topic": "grp_xxx",
"user_id": "usr_xxx"
},
"ts": "2026-08-25T10:00:00Z"
}
}
```
当前群事件投递时,还会给除操作人外的群成员写入一条 `im_system_notifications`,标题为“群聊动态”,summary 暂为事件类型。
## 3. 好友和关系事件
| 触发点 | 接收方 | WS 事件 | payload | 系统通知 |
| --- | --- | --- | --- | --- |
| 创建好友申请 `POST /v1/im/friend-requests` | 被申请人 | `info.what="im"`, `event="friend.request.created"` | `{"user_id":"申请人"}` | 有,写给被申请人,标题“新的好友申请” |
| 同意好友申请 `POST /v1/im/friend-requests/{id}/accept` | 申请人 | `info.what="im"`, `event="friend.request.accepted"` | `{"user_id":"通过人"}` | 当前有,写给申请人,标题“好友申请已处理” |
| 同意好友申请 | 通过人本人 | `info.what="im"`, `event="friend.request.accepted"` | `{"user_id":"通过人"}` | 当前无 |
| 拒绝好友申请 `POST /v1/im/friend-requests/{id}/reject` | 申请人 | `info.what="im"`, `event="friend.request.rejected"` | `{"user_id":"拒绝人"}` | 当前有,写给申请人,标题“好友申请已处理” |
| 拒绝好友申请 | 拒绝人本人 | `info.what="im"`, `event="friend.request.rejected"` | `{"user_id":"拒绝人"}` | 当前无 |
| 删除好友 `DELETE /v1/im/friends/{user_id}` | 被删除方 | `info.what="im"`, `event="friend.deleted"` | `{"user_id":"操作人"}` | 有,标题“好友关系已解除” |
| 删除好友 | 操作人本人 | `info.what="im"`, `event="friend.deleted"` | `{"user_id":"操作人"}` | 当前无 |
| 修改好友备注 `PATCH /v1/im/friends/{user_id}` | 操作人本人 | `info.what="im"`, `event="friend.remark.updated"` | `{"user_id":"操作人"}` | 当前无 |
| 拉黑用户 `PUT /v1/im/blocks/{user_id}` | 操作人本人 | `info.what="im"`, `event="relationship.blocked"` | `{"user_id":"操作人"}` | 当前无 |
| 取消拉黑 `DELETE /v1/im/blocks/{user_id}` | 操作人本人 | `info.what="im"`, `event="relationship.unblocked"` | `{"user_id":"操作人"}` | 当前无 |
前端建议动作:
- 收到 `friend.request.created`:刷新新朋友列表 `GET /v1/im/friend-requests`,刷新 `GET /v1/im/bootstrap` 的 `entry_badges.new_friends_pending_count`。
- 收到 `friend.request.accepted/rejected`:刷新新朋友列表;accepted 时刷新好友列表、P2P 会话 context。
- 收到 `friend.deleted/relationship.blocked/relationship.unblocked`:刷新好友列表、黑名单列表、会话 context,重新判断输入区 `can_send`。
- 收到 `friend.remark.updated`:刷新好友列表和会话头显示名。
当前待确认点:
- 好友申请通过/拒绝、删除好友等操作人本人是否也要写入系统消息列表。当前代码只给对方或申请人写系统通知,操作人本人主要靠 WS 业务事件刷新。
- 对接文档现有示例写成 `sys` topic `{data}` 的 `system_event`,但当前代码实际实时推送为 `{info what:"im"}`。后续需要统一口径。
## 4. 消息编辑和撤回事件
| 触发点 | 接收方 | WS 事件 | payload | 系统通知 |
| --- | --- | --- | --- | --- |
| 撤回消息 `POST /v1/im/messages/{topic}/{seq}/recall` | Topic 在线成员 | `info.what="im"`, `event="message.recalled"` | `{"topic":"...","seq":1,"version":2}` | 无 |
| 编辑消息 `POST /v1/im/messages/{topic}/{seq}/revisions` | Topic 在线成员 | `info.what="im"`, `event="message.edited"` | `{"topic":"...","seq":1,"version":2}` | 无 |
前端建议动作:
- 收到 `message.recalled`:按 `topic + seq + version` 重新拉取或本地标记该消息为已撤回。
- 收到 `message.edited`:按 `topic + seq + version` 重新拉取消息详情或修订记录,更新气泡内容。
注意:
- 这两个事件当前 target_type 是 `topic`,投递到 Topic,而不是某一个用户。
- 数据库层和 HTTP 层都可能写入同类 outbox,依赖 `event_id` 幂等避免重复。
## 5. 群业务事件
| 触发点 | 接收方 | WS 事件 | payload 主要字段 | 系统通知 |
| --- | --- | --- | --- | --- |
| 创建群 | 群成员 | `info.what="group"`, `event="group.created"` | `topic`、群基础信息 | 除操作人外写入 |
| 更新群设置 | 群成员 | `event="group.settings.updated"` | `topic` | 除操作人外写入 |
| 成员资料/权限更新 | 群主、管理员、本人 | `event="group.member.updated"` | `user_id`、`member_sync=single` | 不写群系统消息 |
| 成员加入 | 群成员 | `event="group.member.joined"` | `user_id`、`topic`、`member_sync=single` | 写入群消息并参与个人未读与离线 Push |
| 入群申请创建 | 群主、管理员、申请人 | `event="group.join_request.created"` | `request_id`、`applicant_user_id`、`topic` | 普通成员不可见 |
| 入群申请处理 | 群主、管理员、申请人 | `event="group.join_request.accepted/rejected"` | `request_id`、`status`、`topic` | 普通成员不可见;接受时另发全群加入消息 |
| 群公告更新 | 群成员 | `event="group.announcement.updated"` | `topic`、公告信息 | 除操作人外写入 |
| 群禁言更新 | 群主、管理员、被操作人 | `event="group.mute.updated"` | `topic`、禁言信息、`member_sync` | 普通成员不可见 |
| 成员离群/移除 | 群主、管理员、离群成员 | `event="group.member.left"` | `user_id`、`member_sync=remove` | 普通成员不可见 |
| 群角色更新 | 群主、管理员、被操作人 | `event="group.role.updated"` | `user_id`、角色信息、`member_sync` | 普通成员不可见 |
| 群主转让 | 群成员 | `event="group.owner.transferred"` | `previous_owner`、`new_owner` | 除操作人外写入 |
前端建议动作:
- 首次进入、断线重连、本地无快照或收到 `member_sync=full`:分页拉取完整群成员。
- 收到 `member_sync=single`:调用 `GET /v1/im/groups/{topic}/members/{user_id}` 只刷新一个成员。
- 收到 `member_sync=remove`:从本地成员快照删除 `user_id`;若本地状态无法确认,再全量分页同步。
- 入群申请变化:管理员刷新入群申请列表和群 summary 待处理数。
- 群公告变化:刷新公告区。
- 成员加入/离开:刷新群成员数、成员列表和会话系统提示。
注意:
- 当前群事件实时帧 `what="group"`,不是 `what="im"`。
- 群系统通知的 summary 当前主要是事件类型,文案还比较粗,需要产品确认后优化。
## 6. 会议事件
| 触发点 | 接收方 | WS 事件 | payload | 系统通知 |
| --- | --- | --- | --- | --- |
| 创建会议 | 所有参会人 | `info.what="im"`, `event="meeting.created"` | `{"meeting_id":"...","version":1,"state":"scheduled"}` | 除操作者外写入,标题“会议通知” |
| 更新会议 | 所有参会人 | `event="meeting.updated"` | `meeting_id/version/state` | 除操作者外写入 |
| 取消会议 | 所有参会人 | `event="meeting.canceled"` | `meeting_id/version/state` | 除操作者外写入 |
| 发送会议提醒 | 待响应参会人 | `event="meeting.reminded"` | `meeting_id/version/state` | 除操作者外写入 |
| 更新参会响应 | 所有参会人 | `event="meeting.response.updated"` | `meeting_id/version/state` | 除操作者外写入 |
前端建议动作:
- 收到会议事件后刷新会议详情或会议列表。
- `meeting.reminded` 可触发本地提醒 UI。
## 7. 安全事件
| 触发点 | 接收方 | WS 事件 | payload | 系统通知 |
| --- | --- | --- | --- | --- |
| 风险挑战确认/取消 | 操作人本人 | `info.what="im"`, `event="security.risk.continue"` 或 `security.risk.cancel` | `{"risk_challenge_id":"...","decision":"continue|cancel"}` | 有,标题“安全操作确认” |
| 密码重置成功 | 无普通 WS 事件 | 内部事件 `security.password_reset` | `{"user_id":123}` | 当前用于清退该用户在线 session |
前端建议动作:
- 收到 `security.risk.*`:刷新安全验证页面状态。
- 密码重置后旧 token/session 会被服务端清退,客户端按登录失效流程处理。
## 8. 邀请码统计事件
| 触发点 | 接收方 | WS 事件 | payload | 系统通知 |
| --- | --- | --- | --- | --- |
| 注册成功使用邀请码 | 不下发给普通客户端 | 内部事件 `registration.invite_used` | `{"tenant_id":1,"invite_code_id":123,"user_id":456}` | 无 |
说明:
- 该事件当前由 IM 侧消费,用于把租户库 `im_registration_invite_uses` 聚合回 HotGo 控制库的邀请码统计。
- 普通 App 前端不需要监听。
## 9. Tinode 原生实时下行
这类不是产品 outbox 事件,但也是服务端主动下行,前端需要持续处理。
| 帧类型 | 触发点 | 接收方 | 前端动作 |
| --- | --- | --- | --- |
| `{data}` | 普通聊天消息、文件消息、通话邀请 `webrtc=started`、服务端替换通话结果消息 | Topic 订阅者 | 写入/更新会话时间线、更新未读 |
| `{info what:"read"}` | 对端已读回执 | Topic 订阅者 | 更新消息已读状态 |
| `{info what:"recv"}` | 对端送达回执 | Topic 订阅者 | 更新送达状态 |
| `{info what:"kp"}` | 对端输入中 | Topic 订阅者 | 显示输入中状态,超时自动消失 |
| `{info what:"call"}` | WebRTC `ringing/accept/offer/answer/ice-candidate/hang-up` | 通话对端 | 驱动通话状态机和 SDP/ICE 交换 |
| `{pres}` | 订阅、离线、在线、权限/订阅变化等 presence | 相关订阅者 | 更新在线状态、订阅状态、必要时刷新列表 |
| `{meta}` | 服务端响应 get/sub 中的 desc/sub/data/del/cred 等查询结果 | 请求方 | 合并服务端状态 |
| `{ctrl}` | 客户端请求结果 | 请求方 | 更新请求状态、错误提示、幂等结果 |
## 10. 当前文档与实现差异
当前《IM-前后端对接文档》部分位置把好友关系变化描述为 `sys` topic 的 `{data head.x-im-type="system_event"}`。代码当前更准确的模型是:
- 实时刷新事件:`{info what:"im"}` 或 `{info what:"group"}`。
- 系统通知列表:通过 `im_system_notifications` 暴露给 `GET /v1/im/system-notifications`,并不是所有实时事件都会写入。
- 聊天/系统 Topic `{data}` 仍用于普通消息时间线和 Tinode 原生 Topic 消息,不等同于业务 outbox 实时事件。
建议后续合并到对接文档时明确:
1. 前端必须监听所有 `{info}`,按 `what + event` 分发业务刷新。
2. 系统消息入口的未读数来自 `im_system_notifications`,不是 `{info}` 事件数量。
3. 好友申请通过后是否双方都写系统通知,需要产品确认;若确认双方都有,需要同步补代码。