feat: add redis cache and fanout support

This commit is contained in:
richjohnrambo
2026-08-25 20:54:03 +08:00
parent 07376c70e3
commit f900a08a01
68 changed files with 3949 additions and 5696 deletions
@@ -0,0 +1,595 @@
# IM Redis 三层数据分层改造方案
日期:2026-08-25
## 1. 背景与目标
当前 IM 服务需要接入 Redis,但不希望出现“进程内缓存一份、Redis 缓存一份、MySQL 再存一份”的重复状态,避免后续一致性维护困难。
本方案按业务数据特征拆成三层:
- 进程内:只放低频变更配置和本节点运行态。
- Redis:放小体量、高频读取、需要跨节点一致的热数据。
- MySQL:放最终持久化数据、事务数据、审计数据和大体量列表。
第一阶段不把消息正文作为 Redis 的长期存储。万人大群可以把 24 小时内热消息放入 Redis 作为读加速,私聊消息最终来源是租户库 `im_messages`,群聊消息最终来源是租户库 `im_group_messages`。
## 2. 核心原则
1. 同一类业务状态只确定一个主读取层,不在进程内和 Redis 同时维护两套可变状态。
2. 进程内缓存只承载低频配置或本机连接态,不承载跨节点业务状态。
3. 在线、离线、最近活跃等跨节点状态统一从 Redis 查询。
4. MySQL 仍是需要持久化、事务和审计的数据最终来源。
5. Redis 缓存 MySQL 数据时采用 cache-aside 模式,写入以 MySQL 事务成功为准。
6. Redis key 必须带租户标识,虽然业务库是一租户一库,但 Redis 是共享基础设施,需要避免不同租户数据串扰。
## 3. 三层数据分类
| 层级 | 数据类型 | 典型数据 | 读写特点 | 一致性要求 |
| --- | --- | --- | --- | --- |
| 进程内 | 静态或低频配置、本机运行态 | 系统配置、字典、租户配置、OSS/短信/内容安全/声网配置、本机 WebSocket 连接、topic channel | 启动加载,定时刷新;连接态只在本机维护 | 配置允许秒级延迟;连接态只代表本节点 |
| Redis | 小体量热数据、跨节点状态、短期状态 | 用户在线状态、用户基础信息、群基础信息、群成员集合、好友/黑名单集合、24 小时热消息、验证码、临时 token、限流计数 | 高频读,TTL 或主动失效 | 跨节点查询一致,允许短暂最终一致 |
| MySQL | 持久化和事务数据 | 消息、文件记录、群申请审批、举报、审核任务、操作日志、账务类数据、大分页列表 | 事务写入,分页查询,审计追溯 | 最终权威来源 |
## 4. 进程内层设计
### 4.1 适合放进程内的数据
- 系统配置:后台 hotgo 中的系统参数。
- 字典配置:`hg_sys_dict_type`、`hg_sys_dict_data` 等字典项。
- 租户配置:租户启停、基础配置、默认策略。
- 第三方配置:OSS、阿里云内容安全、短信、声网等接入参数。
- 本节点运行态:当前进程内的 WebSocket session、local topic channel、本机订阅关系、本机连接数。
### 4.2 刷新策略
- 服务启动时加载一次配置。
- 定时刷新,建议 60 秒。
- 后台配置变更后,后续可补充主动失效或广播刷新。
- 配置读取失败时保留旧值,并打印告警日志。
### 4.3 边界说明
进程内层不保存用户在线状态的全局结果。进程内只知道“本节点有哪些连接”,跨节点判断用户是否在线必须查 Redis。
## 5. Redis 层设计
### 5.1 第一阶段必须接入
#### 用户在线状态
在线状态是 Redis 第一优先级改造内容。原因是 WebSocket 长连接固定落在某个节点上,本机内存可以管理本机连接,但其他节点无法直接知道该用户是否在线。
建议 key:
```text
im:{tenant_id}:presence:user:{uid}
im:{tenant_id}:presence:user:{uid}:sessions
im:{tenant_id}:presence:node:{node_id}:users
```
`presence:user:{uid}` 建议使用 hash:
```text
online = 1
node_id = 当前节点 ID
session_count = 当前会话数
last_active_at = 最近活跃时间
last_seen_at = 最近可见时间
device = 设备类型
ua = 客户端 UA
```
生命周期:
1. WebSocket 登录成功后写入 Redis 在线状态。
2. 心跳或业务包刷新 TTL。
3. 断开连接时扣减 session 数,最后一个 session 断开后标记离线。
4. 节点异常退出时依赖 TTL 自动过期,避免永久在线。
5. 其他节点查询用户在线状态时只查 Redis。
TTL 建议:
- 在线状态 hash:60 到 120 秒。
- session 集合:60 到 120 秒,随心跳刷新。
- node 用户集合:可不强依赖 TTL,用于辅助排查和节点清理。
当前代码已经接入基础 PresenceStore:
- 登录成功后写入 `presence:user:{uid}`、`presence:user:{uid}:sessions`、`presence:node:{node_id}:users`。
- 客户端每次上行业务包刷新 `last_active_at` 和 TTL。
- session 清理时移除当前 session,最后一个 session 离线后将 `online` 置为 `0`。
- 对外提供 `store.GetUserPresence` 作为后续用户在线状态查询入口。
- 群 Topic 在线成员已同步维护 `topic:{topic}:online_members` 和 `topic:{topic}:node:{node_id}:online_members`,订阅/离开时更新,客户端心跳或任意上行业务包会刷新已订阅群的在线成员 TTL。
- 订阅列表中的用户在线状态已改为 Redis 优先读取;Redis 未启用或读取失败时,回退到原有进程内在线判断。
#### 短期状态
以下数据适合放 Redis,减少 MySQL 压力并天然支持过期:
- 注册验证码、登录验证码。
- 邀请码短期校验结果。
- 企业码登录临时 token。
- 登录失败次数、风控计数、接口限流计数。
- 幂等 key、短期操作锁。
### 5.2 第二阶段建议接入
#### 用户基础信息
适合缓存:
- 用户昵称、头像、状态签名。
- 用户公开资料。
- 用户账号到 UID 的映射。
不建议缓存:
- 密码 hash。
- 高敏认证字段。
- KYC 原始材料。
读写规则:
1. 查询先读 Redis。
2. 未命中读 MySQL。
3. MySQL 命中后写入 Redis。
4. 用户资料变更时先提交 MySQL,再删除或更新 Redis。
#### 群相关数据
群数据体量相对可控,且聊天过程中读取频繁,适合逐步放入 Redis:
- 群基础信息:群名、头像、简介、人数、群类型。
- 群配置:入群审批、禁言策略、邀请权限、公告权限。
- 群成员集合:成员 UID 集合。
- 群成员角色:群主、管理员、普通成员。
- 群成员状态:禁言、移出、黑名单。
建议 key:
```text
im:{tenant_id}:group:{group_id}:profile
im:{tenant_id}:group:{group_id}:settings
im:{tenant_id}:group:{group_id}:members
im:{tenant_id}:group:{group_id}:admins
im:{tenant_id}:group:{group_id}:muted
```
#### 关系数据
好友、黑名单、关注类关系适合用 Redis set 承载热读:
```text
im:{tenant_id}:relation:friends:{uid}
im:{tenant_id}:relation:blocklist:{uid}
```
用于快速判断:
- 是否好友。
- 是否被拉黑。
- 是否允许发起会话。
- 是否允许查看在线状态或最近在线时间。
### 5.3 第三阶段可选接入
- 首页 bootstrap 聚合结果:短 TTL 缓存,减少多表聚合查询。
- 群成员分页快照深度优化:基础 HTTP 结果快照已接入,后续大群可继续补 sorted set、稳定游标和主动失效。
- 未读数深度优化:基础总未读 Redis 热缓存已接入,后续可继续补 Topic 维度未读计数、bootstrap 聚合缓存和主动失效事件。
### 5.4 万人大群实时推送专项
万人大群的实时推送需要和 Redis 改造一起考虑。核心目标是避免普通小群模式下的全量 fanout 压垮服务端,同时保证在线用户实时可达、离线用户通过极光 Push 提醒、客户端重连后可按 seq 补齐消息。
该部分作为 Redis 改造的近期实现项处理,不再仅作为未实现增强项记录。
#### 数据分层
| 层级 | 承载内容 | 说明 |
| --- | --- | --- |
| 进程内 | 本节点在线连接、本节点 topic 订阅、本节点待发送队列 | 只负责当前节点连接和发送,不保存全局群成员在线状态。 |
| Redis | 群在线成员索引、用户所在节点、群分片、原子 seq 发号器、24 小时热消息、短期去重 key | 用于判断哪些在线用户需要实时推送、原子获取群消息 seq,以及近期高频消息读取加速。 |
| MySQL 租户库 | `im_group_messages` 群消息正文、群消息 seq 结果、消息索引、离线同步游标、审计数据 | 作为群消息最终存储,保存 Redis 分配后的 seq,不负责实时发号。 |
| 极光 Push | 离线提醒 | 只做提醒,不承载消息正文最终一致性。 |
#### 建议 Redis key
```text
im:{tenant_id}:presence:user:{uid}
im:{tenant_id}:topic:{topic}:online_members
im:{tenant_id}:topic:{topic}:node:{node_id}:online_members
im:{tenant_id}:topic:{topic}:shard:{shard_id}:members
im:{tenant_id}:topic:{topic}:last_seq
im:{tenant_id}:topic:{topic}:hot_msg_seqs
im:{tenant_id}:topic:{topic}:hot_msg:{seq}
im:{tenant_id}:topic:{topic}:uniq:msg:{from_uid}:{client_msg_id}
im:{tenant_id}:push:dedup:{topic}:{seq}:{uid}
```
说明:
- `topic:online_members` 保存当前群在线成员集合,可由用户上线、订阅群、退订群、断开连接维护。
- `topic:node:{node_id}:online_members` 用于跨节点推送时按节点拆分,减少广播范围。
- `topic:shard:{shard_id}:members` 可用于万人群成员分片,后续支持分批扫描和补偿。
- `topic:last_seq` 是群消息 seq 发号器,使用 Redis `INCR` 或 Lua 脚本原子获取下一个 seq。
- `topic:hot_msg_seqs` 使用 sorted set 保存 24 小时内热消息 seq,score 为 seq 或发送时间。
- `topic:hot_msg:{seq}` 保存单条热消息正文快照,TTL 建议 24 小时。
- `topic:uniq:msg:{from_uid}:{client_msg_id}` 保存群消息发送唯一 key,用于客户端重试幂等和服务端去重。
- `push:dedup` 用于极光 Push 或实时推送去重,TTL 建议按分钟级设置。
#### 唯一 key 与幂等规则
Redis 唯一 key 用于挡住短时间内的重复提交,MySQL 唯一索引作为最终兜底。不能只依赖 Redis 做永久唯一性,因为 Redis key 会过期,也可能被运维清理。
群消息发送唯一 key:
```text
im:{tenant_id}:topic:{topic}:uniq:msg:{from_uid}:{client_msg_id}
```
建议 value:
```json
{
"status": "pending|done|failed",
"seq": 12345,
"message_id": 987654321,
"created_at": "2026-08-25T12:00:00.000+08:00"
}
```
处理规则:
1. 客户端必须传 `client_msg_id`,同一发送人、同一 Topic 内唯一。
2. 服务端收到群消息后先用 `SET key value NX EX ttl` 抢占唯一 key,初始状态为 `pending`。
3. 抢占成功才继续 Redis `INCR` 获取群 `seq`。
4. 抢占失败时读取 key:
- `done`:直接返回已生成的 `seq/message_id`。
- `pending`:返回处理中或短暂等待后重试,避免重复发号。
- `failed`:允许客户端用同一个 `client_msg_id` 重试,或要求重新生成 `client_msg_id`。
5. 消息写入 `im_group_messages` 成功后,将唯一 key 更新为 `done`,记录 `seq/message_id`。
6. 消息写入失败时,将唯一 key 更新为 `failed` 并设置较短 TTL;已经获取的 `seq` 不回退,允许空洞。
7. `im_group_messages` 仍保留 `UNIQUE(topic, from_user, client_msg_id)`,防止 Redis key 过期后重复落库。
TTL 建议:
- `pending`:30 到 120 秒。
- `done`:24 小时到 7 天,按客户端重试窗口决定。
- `failed`:5 到 30 分钟。
- Push 去重 key:5 到 30 分钟。
#### Seq 发号规则
- 群消息 seq 由 Redis 维护,不从 MySQL 查询最大 seq。
- 每个 Topic 独立一个 `topic:last_seq`,使用 `INCR im:{tenant_id}:topic:{topic}:last_seq` 原子获取。
- 只要求单 Topic 内单调递增,不要求全局连续。
- 如果取到 seq 后消息持久化失败,该 seq 可以形成空洞;客户端按 seq 补拉时允许跳过服务端确认不存在的 seq。
- Topic 创建或 Redis key 丢失时,当前代码使用 topic 内存态 `lastID` 作为 Redis 初始化基线;该值来自 topic 加载结果或当前进程已确认的最新 seq,然后再执行原子 `INCR`。
- Redis 不可用时不能用数据库临时发号继续写大群消息,应返回可重试错误,避免多源发号造成 seq 冲突。
#### 发送流程
1. 服务端收到群消息后,先完成权限校验和禁言校验。
2. 使用 Redis 唯一 key 抢占 `topic + from_uid + client_msg_id`,防止客户端重试重复发送。
3. 抢占成功后,使用 Redis `INCR` 或 Lua 脚本从 `topic:last_seq` 原子获取下一个群 `seq`。
4. 消息正文带着该 `seq` 写入租户库 `im_group_messages`。
5. 落库成功后将唯一 key 更新为 `done`,记录 `seq/message_id`。
6. 对万人大群或高热群,将消息快照写入 `topic:hot_msg:{seq}`,并把 seq 加入 `topic:hot_msg_seqs`,TTL 保持 24 小时。
7. 查询 Redis 中该群在线成员,按 `node_id` 分组。
8. 本节点在线成员直接从进程内连接表推送。
9. 其他节点在线成员通过 Redis Pub/Sub 通知目标节点推送;当前代码使用 `im:0:group_fanout` 广播精简后的 `ServerComMessage`,订阅节点收到后投递到本机 `hub.routeSrv`,只发送给本节点已加载 Topic 的在线连接。
10. 不在线成员不做服务端全量离线消息 fanout,只更新离线游标或依赖消息 seq 补拉,并触发极光 Push。
11. 客户端重连后按 Topic seq 拉取缺失消息,优先读 Redis 热消息,未命中再回源租户库 `im_group_messages`。
#### 读取流程
客户端消息读取仍以 seq 为准,Redis 只优化 24 小时内的热读:
1. 在线实时收到消息后,客户端本地落库并更新本地最大 seq。
2. 重连、切换设备或发现 seq 不连续时,调用历史消息接口按 `topic + after_seq` 补拉。
3. 服务端先按 seq 范围读取 Redis 热消息。
4. Redis 命中的消息直接返回;缺失、过期或不连续的 seq 回源租户库 `im_group_messages`。
5. 回源命中后可按策略重新写入 Redis,保持 24 小时 TTL。
6. 极光 Push 唤醒后,客户端仍走正常登录和 seq 补拉链路。
#### 热消息缓存边界
- 只缓存 24 小时内高频读取的群消息,普通群可不开启或按群规模阈值开启。
- Redis 中的消息快照不作为审计和历史同步最终来源。
- 删除、撤回、编辑消息后,必须同步删除或更新对应 `topic:hot_msg:{seq}`,避免客户端读到旧正文。
- 大消息、文件二进制、音视频内容不直接放 Redis,只缓存消息元数据和 OSS object key。
- Redis 容量按租户和群设置上限,超过阈值时优先淘汰低热群或旧 seq。
- 当前代码只在 Redis 能完整命中一页群消息时直接返回;如果 Redis 缺失、过期、不连续或读取带有用户软删除视角,则回源 MySQL,避免缓存缺口导致消息缺失或读到用户已删除内容。
#### 离线 Push 口子
极光 Push 只用于离线提醒,建议 payload 只放最小必要信息:
```json
{
"type": "group_message",
"topic": "grp_xxx",
"seq": 12345,
"tenant_id": "tenant_xxx"
}
```
不建议在 Push payload 中放完整消息正文,避免隐私泄露、厂商通道截断和多端一致性问题。
#### 一致性边界
- Redis 的在线成员集合允许短暂不准,最终由心跳 TTL 和订阅变更修正。
- Redis 唯一 key 负责短期幂等和去重,MySQL 唯一索引负责最终唯一性兜底。
- seq 是否已分配以 Redis `topic:last_seq` 为准;群消息是否真实存在,以租户库 `im_group_messages` 为准。
- Redis 发号成功但消息持久化失败时,允许 seq 空洞,不回退 `topic:last_seq`。
- 实时推送失败不影响消息持久化,通过客户端 seq 补拉恢复;Redis 热消息缺失时回源租户库 `im_group_messages`。
- 极光 Push 失败不影响消息存储,只影响离线提醒,需要记录失败并可观测。
- Redis Pub/Sub 只负责在线实时投递,不作为可靠消息队列;节点短暂断线错过实时消息时,客户端仍以 Topic seq 补拉恢复。
## 6. MySQL 层设计
### 6.1 必须保留在 MySQL 的数据
- 私聊消息正文、私聊消息索引、离线同步游标。
- 群消息正文、群消息索引、群消息审计数据。
- 文件记录、OSS object key、文件安全审核结果。
- 群邀请、进群申请、审批记录。
- 群公告、公告确认、公告历史。
- 用户认证、KYC 记录、企业注册资料。
- 举报、投诉、内容安全审核任务。
- 操作日志、审计日志、管理后台变更记录。
- 需要事务一致性的业务状态。
### 6.2 消息表拆分
当前 `im_messages` 后续只作为私聊、文件助手等非群聊消息的主存储。群聊消息新增租户库业务表 `im_group_messages`,避免所有消息挤在一张表里,也方便后续针对大群按 `topic + seq`、时间、热度做分区、归档和缓存。
建议表结构:
```sql
CREATE TABLE IF NOT EXISTS im_group_messages (
id BIGINT NOT NULL AUTO_INCREMENT,
topic VARCHAR(64) NOT NULL COMMENT '群 Topic,如 grp_xxx',
group_id BIGINT NOT NULL COMMENT '业务群 ID,便于分区、治理和统计',
seqid BIGINT NOT NULL COMMENT '群内消息序号,由 Redis 原子发号',
from_user BIGINT NOT NULL COMMENT '发送人用户 ID',
client_msg_id VARCHAR(64) NULL COMMENT '客户端幂等消息 ID',
msg_type VARCHAR(32) NOT NULL DEFAULT 'text' COMMENT 'text/image/file/audio/video/system 等',
status VARCHAR(32) NOT NULL DEFAULT 'normal' COMMENT 'normal/recalled/deleted/blocked',
content JSON NULL COMMENT '消息正文或 Drafty 内容',
head JSON NULL COMMENT '消息头,包含 @、引用、客户端能力等',
mentioned_user_ids JSON NULL COMMENT '@ 用户列表,便于提醒和检索',
mention_all TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否 @ 全体',
reply_seqid BIGINT NULL COMMENT '回复的群消息 seq',
root_seqid BIGINT NULL COMMENT '线程或合并上下文根消息 seq',
file_count INT NOT NULL DEFAULT 0 COMMENT '附件数量',
content_size INT NOT NULL DEFAULT 0 COMMENT '正文 JSON 序列化后的字节数',
has_risk TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否命中风控或内容安全',
audit_status VARCHAR(32) NOT NULL DEFAULT 'pass' COMMENT 'pass/pending/reject',
version INT NOT NULL DEFAULT 1 COMMENT '编辑版本',
recalled_at DATETIME(3) NULL,
recalled_by BIGINT NULL,
deleted_at DATETIME(3) NULL,
expires_at DATETIME(3) NULL COMMENT '可选保留期,便于过期归档或清理',
created_at DATETIME(3) NOT NULL,
updated_at DATETIME(3) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY uk_im_group_messages_group_seq (group_id, seqid),
UNIQUE KEY uk_im_group_messages_topic_seq (topic, seqid),
UNIQUE KEY uk_im_group_messages_topic_sender_client_mid (topic, from_user, client_msg_id),
KEY idx_im_group_messages_group_created (group_id, created_at),
KEY idx_im_group_messages_group_status_seq (group_id, status, seqid),
KEY idx_im_group_messages_topic_created (topic, created_at),
KEY idx_im_group_messages_topic_from_seq (topic, from_user, seqid),
KEY idx_im_group_messages_audit (audit_status, created_at),
KEY idx_im_group_messages_expires (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
设计说明:
- `seqid` 使用 `BIGINT`,避免大群长期高频消息导致 `INT` 不够用。
- `group_id + seqid` 和 `topic + seqid` 都保持唯一,业务治理优先用 `group_id`,协议补拉仍可用 `topic`。
- `client_msg_id` 用于发送幂等,索引带上 `topic`,避免不同群复用客户端消息 ID 时互相冲突。
- `status` 标记普通、撤回、删除、拦截状态,拉历史时可以返回占位或过滤。
- `mentioned_user_ids`、`mention_all` 单独存,方便 @ 提醒、离线 Push 和后台治理,不必每次解析正文。
- `audit_status`、`has_risk` 为内容安全和风控留字段,高风险消息可先 pending 后再放行。
- `reply_seqid`、`root_seqid` 为引用回复、线程、合并转发上下文预留。
- 文件二进制不进表,表内只保存消息内容和 OSS object key 等元数据;群消息附件会同步登记到 `im_group_files`,并记录 `source_topic/source_seq` 追溯来源消息。
- 超大规模租户后续可按 `group_id` hash 分表,或按 `created_at/expires_at` 做归档分区。
读写规则:
1. 私聊消息继续写 `im_messages`。
2. 群聊消息写 `im_group_messages`。
3. 群消息发号由 Redis `topic:last_seq` 原子递增完成,`im_group_messages.seqid` 只保存结果。
4. Redis 24 小时热消息命中时直接返回;未命中按 `topic + seqid` 回源 `im_group_messages`。
5. 撤回、编辑、删除群消息时,以 `im_group_messages` 为准,并同步更新或删除 Redis 热消息。
### 6.3 消息正文说明
第一阶段不把消息正文作为 Redis 长期存储。万人大群和高热群允许缓存 24 小时内热消息,作为客户端重连补拉、最近历史翻页和多端同步的读加速。
消息正文主要读取场景:
- 新设备登录后的历史消息同步。
- 离线用户重新上线后的消息补拉。
- 客户端本地消息缺失后的重拉。
- 举报、风控、内容安全复查。
- 管理后台审计或客服处理。
这些场景的最终来源仍是 MySQL 的消息表。私聊回源 `im_messages`,群聊回源 `im_group_messages`。Redis 热消息只覆盖短期高频读取,命中则减少数据库压力,未命中必须回源。
## 7. 读写一致性规则
### 7.1 进程内配置
- 进程启动加载配置。
- 定时刷新配置。
- 配置读取失败时使用旧值。
- 配置变更允许短时间延迟生效。
### 7.2 Redis 运行态
- 在线状态以 Redis 为跨节点查询来源。
- 本机内存只用于管理本机连接。
- Redis 写入失败时,在线状态降级为未知或离线,不阻断基础连接能力。
- 心跳持续刷新 TTL,节点异常时依赖 TTL 自动回收。
### 7.3 Redis 缓存 MySQL 热数据
- 读:先 Redis,未命中再 MySQL,最后回填 Redis。
- 写:先 MySQL 事务提交,再删除或更新 Redis。
- 消息 seq:群聊先通过 Redis `INCR` 或 Lua 脚本原子获取 seq,再带 seq 写租户库 `im_group_messages`;私聊仍按现有 `im_messages` 链路处理。
- 消息热缓存:消息持久化成功后,再写 Redis 24 小时热消息;热消息写失败不影响消息发送成功。
- Redis 故障:直接回源 MySQL,记录降级日志和指标。
- 不允许因为 Redis 写失败回滚已经成功的 MySQL 业务事务,除非该业务本身强依赖 Redis 状态。
### 7.4 MySQL 持久化
- 需要审计、事务、历史追溯的数据直接写 MySQL。
- Redis 和进程内缓存只做读取加速或运行态存储。
- 私聊消息使用 `im_messages`,群聊消息使用 `im_group_messages`,避免单表无限膨胀。
## 8. 租户隔离
当前方案是一租户一库,业务表中不再保留 `tenant_id` 字段。但 Redis、进程内配置、日志指标仍属于共享运行环境,必须保留租户维度。
Redis key 统一包含租户标识:
```text
im:{tenant_id}:...
```
如果请求早期只有企业码或租户编码,应先解析到租户 ID 或租户库配置,再进入业务缓存读取。
## 9. 代码改造建议
### 9.1 基础设施
- 增加 Redis 配置:地址、密码、DB、连接池、超时、key 前缀。
- 增加 Redis client 初始化和健康检查。
- 增加统一 key builder,避免业务代码手写 key。
- 增加缓存指标和日志。
当前代码将 Redis 配置挂在 `store_config.redis` 下;不配置则不启用 Redis 群消息发号和唯一 key,群消息仍兼容旧的本机 `lastID + 1` 发号。
配置示例:
```json
{
"store_config": {
"redis": {
"addr": "127.0.0.1:6379",
"username": "",
"password": "",
"db": 0,
"key_prefix": "im",
"dial_timeout_ms": 2000,
"read_timeout_ms": 2000,
"write_timeout_ms": 2000,
"pending_ttl_seconds": 120,
"done_ttl_seconds": 86400,
"failed_ttl_seconds": 600,
"hot_message_ttl_seconds": 86400,
"presence_ttl_seconds": 120,
"cache_ttl_seconds": 300,
"short_state_ttl_seconds": 600,
"group_fanout_enabled": true
}
}
}
```
`group_fanout_enabled` 默认关闭。启用后,群消息本节点投递完成后会发布 Redis Pub/Sub,其他节点订阅后只向本节点已在线的同群连接推送;如果节点没有加载该 Topic,则读取 Redis 本节点群在线成员集合做兜底直投,本地会话订阅表作为兼容兜底,实时投递失败再由客户端 seq 补拉恢复。
当前第二阶段已接入:
- 用户资料单对象缓存:`store.Users.Get` 优先读 Redis,用户更新/删除后失效。
- Topic 单对象缓存:`store.Topics.Get` 优先读 Redis,Topic 更新、转让、删除后失效。
- 单条订阅关系缓存:`store.Subs.Get` 优先读 Redis,订阅更新/删除后 active/any 两种缓存同时失效。
- 群治理资料缓存:群设置 `GroupBusiness.GetSettings`、群成员 `GroupBusiness.GetMember` 优先读 Redis,更新、离群、角色变更后刷新或失效。
- 群成员分页快照:`GET /v1/im/groups/{topic}/members` 按租户、Topic、操作者权限、筛选条件、cursor 和 limit 缓存响应快照,未命中回源订阅列表。
- 用户总未读热缓存:`store.Users.GetUnreadCount` 先读 Redis,未命中回源 MySQL 并回填;在线用户 unread updater 更新内存计数时同步写 Redis。
- 好友/黑名单 Redis Set 热缓存:关系上下文判断先查 `friends:{uid}`、`blocklist:{uid}` 正向 set,命中后减少 MySQL 查询;未命中仍回源 MySQL,新增/删除好友、拉黑/解除拉黑会同步更新 set。
- 验证码短状态缓存:验证码创建、查询、验证、失败、消费会同步 Redis,`GetActive` 使用 `scene + phone_hash` 的 active key 做重发窗口热读。
- Redis 频控:验证码创建按 `tenant + scene + phone_hash` 使用 Redis 原子 `INCR + EXPIRE` 做 1 小时 5 次限制,手机号只使用 hash,不写明文。
- 动作 token 短状态缓存:创建时写 Redis,消费成功后删除缓存;最终消费仍以 MySQL 状态机为准,保留审计一致性。
- SeqId 收口:消息、Topic 游标、订阅读/收游标、消息范围、收藏/举报/群文件引用、通话消息引用、JSON/gRPC 协议结构均改为 `int64`;MySQL 新建表 schema 中对应字段改为 `BIGINT`。
- 进程内配置缓存:租户解析、注册短信开关、第三方配置读取已加本地 TTL 缓存,减少 HotGo 控制库频繁查询;缓存过期后按下一次读取刷新。
### 9.2 接口抽象
建议新增接口:
```go
type PresenceStore interface {
MarkOnline(ctx context.Context, tenantID string, uid string, sessionID string, meta PresenceMeta) error
Refresh(ctx context.Context, tenantID string, uid string, sessionID string) error
MarkOffline(ctx context.Context, tenantID string, uid string, sessionID string) error
Get(ctx context.Context, tenantID string, uid string) (Presence, error)
}
type ConfigCache interface {
GetDict(ctx context.Context, key string) (DictValue, bool)
GetTenantConfig(ctx context.Context, tenantID string) (TenantConfig, bool)
Refresh(ctx context.Context) error
}
type ReadModelCache interface {
GetUserProfile(ctx context.Context, tenantID string, uid string) (UserProfile, bool)
SetUserProfile(ctx context.Context, tenantID string, profile UserProfile) error
DeleteUserProfile(ctx context.Context, tenantID string, uid string) error
}
```
### 9.3 业务接入顺序
1. 先接入 Redis client 和配置。
2. 接入在线状态 PresenceStore。
3. 群消息接入 Redis `SET NX` 唯一 key 与 `INCR` 原子 seq 发号,群消息正文写入 `im_group_messages`。
4. WebSocket 登录、心跳、断开链路写入 PresenceStore。
5. 用户在线状态查询接口改为读 Redis。
6. 配置和字典改为进程内 TTL 缓存刷新。
7. 再逐步接入用户、群、好友关系热数据缓存。
## 10. 故障降级
| 场景 | 降级策略 |
| --- | --- |
| Redis 不可用,查询在线状态 | 返回未知或离线,记录告警 |
| Redis 不可用,读取用户/群缓存 | 回源 MySQL |
| Redis 不可用,验证码或短期 token | 涉及安全的接口应失败关闭 |
| Redis 不可用,万人大群发消息 | 无法原子获取群 seq,应返回可重试错误;不使用 MySQL 临时发号,避免 seq 冲突 |
| 配置刷新失败 | 使用进程内旧配置 |
| MySQL 不可用 | 持久化业务失败,不依赖 Redis 兜底 |
## 11. 监控指标
- Redis 连接状态。
- Redis 命令耗时。
- Redis 缓存命中率。
- 在线用户 key 数量。
- 心跳刷新失败次数。
- Redis 回源 MySQL 次数。
- 大群在线成员集合大小。
- 大群按节点分发耗时。
- 大群实时推送成功率和补拉率。
- 极光 Push 触发数、成功数和失败数。
- 配置刷新成功和失败次数。
- Presence TTL 过期导致的离线数量。
## 12. 第一阶段落地范围
第一阶段建议只做以下内容:
- Redis 基础配置和 client 初始化。
- 用户在线、离线、心跳刷新、跨节点查询。
- 验证码、临时 token、限流计数等短期状态迁移到 Redis。
- 系统配置、字典、租户配置、第三方配置改为进程内 TTL 缓存刷新。
- 万人大群实时推送纳入 Redis 改造近期实现范围,先完成 Redis key、在线成员索引、节点分片和跨节点实时分发链路。
- 明确消息正文不进入 Redis 长期存储;万人大群 24 小时热消息可进入 Redis 做读加速。
- 群成员分页快照和用户总未读 Redis 热缓存已完成基础接入。
后续再逐步推进好友/黑名单全量预热、群成员分页快照、未读数等缓存改造。
+33 -1
View File
@@ -1,6 +1,6 @@
# IM 产品原型需求文档
> 本文档根据 `/Users/superysh/im/第二项目/` 中的 156 张产品原型图整理。功能按业务域归类,每张图以原文件名作为标题,图片下方给出功能说明与界面内容描述。
> 本文档根据 `/Users/superysh/im/第二项目/` 中的 160 张产品原型图整理。功能按业务域归类,每张图以原文件名作为标题,图片下方给出功能说明与界面内容描述。
## 产品总体定位
@@ -18,6 +18,38 @@
提供企业租户入口。页面展示产品标识、企业码输入框和“进入企业”按钮;提交后应校验企业码是否存在、是否启用,并将用户导向对应企业的登录环境。
### 1.1.1 企业注册-登录入口.png
<img src="images/企业注册-登录入口.png" alt="企业注册-登录入口" width="360">
功能说明:
企业码登录页右上角提供“企业注册”入口,底部提供“遇到问题?”反馈入口。点击“企业注册”进入企业资料提交流程;点击“遇到问题?”进入问题反馈页。入口文案应保持可见且易点击,避免与状态栏或背景装饰重叠。
### 1.1.2 企业注册-填写注册信息.png
<img src="images/企业注册-填写注册信息.png" alt="企业注册-填写注册信息" width="360">
功能说明:
企业注册页按步骤展示“填写企业信息、确定信息、提交审核”,当前处于第 1 步。用户需填写 18 位统一社会信用代码,支持扫描识别;可选上传营业执照,并填写企业邮箱用于接收审核结果及企业通知。提交前应校验统一社会信用代码格式、邮箱格式、图片大小和文件类型,并提示资料提交后预计审核时长。
### 1.1.3 企业注册-提交成功提示.png
<img src="images/企业注册-提交成功提示.png" alt="企业注册-提交成功提示" width="360">
功能说明:
企业注册资料提交成功后,在当前页展示明确 Toast 提示“提交成功,我们会尽快联系你”。提交成功状态应防止重复提交,并记录申请单状态;审核结果需通过企业邮箱或后续通知渠道触达。页面底部审核时长文案应与实际运营规则一致。
### 1.1.4 企业注册-问题反馈.png
<img src="images/企业注册-问题反馈.png" alt="企业注册-问题反馈" width="360">
功能说明:
企业入口问题反馈页支持填写问题描述并上传最多 9 张截图,便于定位企业码、注册资料、审核通知等问题。提交时应携带设备、版本、租户线索和可选联系方式;图片上传需显示数量限制、失败重试和大小校验,提交成功后给出明确受理反馈。
### 1.2 登录页面.png
<img src="images/登录页面.png" alt="登录页面" width="360">
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+71 -23
View File
@@ -334,16 +334,60 @@ X-Tinode-APIKey: <预置的普通 API Key>
| 更新会话私有设置 | `{"set":{"id":"set-private-1","topic":"grp...","sub":{"user":"usr_me","private":{"pinned":true,"mute_notifications":false,"nickname":"群昵称","version":2}}}}` | `{"ctrl":{"id":"set-private-1","code":200,"text":"ok"}}` |
| 设置搜索标签 | `{"set":{"id":"set-tags-1","topic":"me","tags":["usr:alice"]}}` | 普通用户无权限时 `{"ctrl":{"id":"set-tags-1","code":403,"text":"permission denied","params":{"what":"tags"}}}` |
##### 0.5.4.6 系统事件、好友和群业务下行
##### 0.5.4.6 系统通知、好友和群业务下行
| 事件 | 服务端下行示例 | 客户端动作 |
| --- | --- | --- |
| 好友申请创建 | `{"data":{"topic":"sys","seq":101,"from":"usr_system","head":{"mime":"application/json","x-im-type":"system_event"},"content":{"event_type":"friend.request.created","request_id":"frq_1","applicant":{"user_id":"usr_a","display_name":"张三"},"message":"我是张三","action":{"type":"friend_request","request_id":"frq_1"}}}}` | 系统消息入库/展示,刷新 `friend-requests` 和 `bootstrap.entry_badges` |
| 好友关系变化 | `{"data":{"topic":"sys","seq":102,"head":{"x-im-type":"system_event"},"content":{"event_type":"friend_relationship_changed","peer_user_id":"usr_a","relationship":"friend","topic":"p2p..."}}}` | 刷新会话 context,解锁或锁定输入区 |
| 群邀请 | `{"data":{"topic":"sys","seq":103,"head":{"x-im-type":"system_event"},"content":{"event_type":"group_invitation","invitation_id":"ginv_1","group":{"topic":"grp...","name":"项目群"},"inviter":{"user_id":"usr_a","display_name":"张三"},"status":"pending","expires_at":"2026-08-27T09:00:00Z"}}}` | 展示邀请卡,点击后走邀请接受或入群申请接口 |
| 入群申请 | `{"data":{"topic":"grp...","seq":20,"head":{"x-im-type":"system_event"},"content":{"event_type":"group.join_request.created","request_id":"gjr_1","applicant":{"user_id":"usr_a","display_name":"张三"},"source":"qr"}}}` | 管理员刷新 `join-requests` 和 `summary.pending_join_request_count` |
| 群成员变化 | `{"data":{"topic":"grp...","seq":21,"head":{"x-im-type":"system_event"},"content":{"event_type":"group_membership_changed","operator":"usr_owner","target_users":["usr_a"],"action":"join","result":"joined"}}}` | 刷新群成员列表、群详情成员数和会话系统提示 |
| 工作台未读变化 | `{"data":{"topic":"sys","seq":104,"head":{"x-im-type":"system_event"},"content":{"event_type":"workbench.badge.updated","modules":[{"code":"meetings","unread_count":2}],"workbench_total_unread":2}}}` | 更新工作台 badge;必要时重新拉 `GET /v1/im/workbench` |
服务端主动下行分两类:Tinode 原生 Topic 消息使用 `{data}`、`{info}`、`{pres}` 等帧;IM 产品业务事件使用 `{info}` 帧下发,前端按 `info.what + info.event` 分发刷新。产品业务事件不占普通聊天 `seq`,不等同于系统通知列表记录;系统消息入口的未读数来自 `im_system_notifications` 和 `GET /v1/im/system-notifications`。
用户维度业务事件统一格式:
```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"
}
}
```
群维度业务事件统一格式:
```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"
}
}
```
| 事件 | 接收方 | 服务端下行示例 | 客户端动作 |
| --- | --- | --- | --- |
| 好友申请创建 | 被申请人 | `{"info":{"topic":"usr_target","from":"usr_applicant","what":"im","event":"friend.request.created","payload":{"user_id":"usr_applicant"}}}` | 刷新 `GET /v1/im/friend-requests` 和 `GET /v1/im/bootstrap` 的新朋友未处理数;系统通知列表也会新增记录 |
| 好友申请通过 | 申请人、通过人本人 | `{"info":{"topic":"usr_target","from":"usr_actor","what":"im","event":"friend.request.accepted","payload":{"user_id":"usr_actor"}}}` | 刷新新朋友列表、好友列表、P2P 会话 context;申请人当前有系统通知,通过人本人当前无系统通知 |
| 好友申请拒绝 | 申请人、拒绝人本人 | `{"info":{"topic":"usr_target","from":"usr_actor","what":"im","event":"friend.request.rejected","payload":{"user_id":"usr_actor"}}}` | 刷新新朋友列表和 `bootstrap.entry_badges`;申请人当前有系统通知,拒绝人本人当前无系统通知 |
| 删除好友 | 被删除方、操作人本人 | `{"info":{"topic":"usr_target","from":"usr_actor","what":"im","event":"friend.deleted","payload":{"user_id":"usr_actor"}}}` | 刷新好友列表、会话 context,重新判断输入区 `can_send`;被删除方当前有系统通知,操作人本人当前无系统通知 |
| 拉黑/取消拉黑 | 操作人本人 | `{"info":{"topic":"usr_actor","from":"usr_actor","what":"im","event":"relationship.blocked","payload":{"user_id":"usr_actor"}}}` | 刷新黑名单、好友列表和会话 context |
| 修改好友备注 | 操作人本人 | `{"info":{"topic":"usr_actor","from":"usr_actor","what":"im","event":"friend.remark.updated","payload":{"user_id":"usr_actor"}}}` | 刷新好友列表和会话头显示名 |
| 消息撤回 | Topic 在线成员 | `{"info":{"topic":"p2p_or_grp","from":"usr_actor","what":"im","event":"message.recalled","payload":{"topic":"p2p_or_grp","seq":12,"version":2}}}` | 按 `topic+seq+version` 重新拉取或本地标记已撤回 |
| 消息编辑 | Topic 在线成员 | `{"info":{"topic":"p2p_or_grp","from":"usr_actor","what":"im","event":"message.edited","payload":{"topic":"p2p_or_grp","seq":12,"version":2}}}` | 按 `topic+seq+version` 重新拉取消息详情或修订记录 |
| 群业务变化 | 群 Topic 在线成员 | `{"info":{"topic":"grp...","from":"usr_actor","what":"group","event":"group.member.joined","payload":{"topic":"grp...","user_id":"usr_xxx"}}}` | 按事件刷新群详情、成员列表、入群申请、公告、禁言或角色权限;除操作人外当前会写系统通知 |
| 会议变化 | 会议参会人 | `{"info":{"topic":"usr_target","from":"usr_actor","what":"im","event":"meeting.updated","payload":{"meeting_id":"mtg_1","version":2,"state":"scheduled"}}}` | 刷新会议列表/详情;除操作者外当前会写系统通知 |
| 安全风险确认 | 操作人本人 | `{"info":{"topic":"usr_actor","from":"usr_actor","what":"im","event":"security.risk.continue","payload":{"risk_challenge_id":"risk_1","decision":"continue"}}}` | 刷新安全验证页面;当前会写系统通知 |
业务事件写入 outbox 后会立即尝试投递;Hub 不可用或队列满时保持 pending,后台 poller 按在线租户重试。`registration.invite_used` 和 `security.password_reset` 是服务端内部消费事件,不作为普通业务事件下发给 App。
##### 0.5.4.7 WebRTC 通话信令
@@ -696,7 +740,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
- 查询:`GET /v1/im/security/realname` 返回 `status=unverified|pending|verified|rejected`、脱敏姓名/证件号和 `can_submit`。
- 提交:`POST /v1/im/security/realname/submissions`,Body `{"name":"张三","id_type":"cn_id","id_number":"..."}`,必须带幂等键。
- 成功:返回 `submission_id`、`status:"pending"`;前端禁用重复提交,通过 `GET /v1/im/security/realname` 或 `security.realname.updated` 系统事件更新状态。
- 成功:返回 `submission_id`、`status:"pending"`;前端禁用重复提交,当前以 `GET /v1/im/security/realname` 查询状态为准。后续如启用 `security.realname.updated`,应按 0.5.4.6 的 `info.what="im"` 业务事件下发。
- 安全:证件号传输和存储加密、展示脱敏;普通日志、搜索索引和消息中禁止出现原值;重复身份由租户策略决定并返回稳定错误。
### 1.9 实名认证-已实名.png
@@ -808,7 +852,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
**状态:需改造。**
- `GET /v1/im/system-notifications?cursor=` 返回 `id,event_type,title,summary,created_at,read_at,action`;新通知通过服务端专用系统 Topic 的 `system_event` 消息推送。
- `GET /v1/im/system-notifications?cursor=` 返回 `id,event_type,title,summary,created_at,read_at,action`;实时刷新以 0.5.4.6 的 WS `{info what:"im|group"}` 业务事件为准,系统通知列表记录不等同于每一条实时事件。
- 新设备登录事件内容为 `device_name,platform,ip_masked,login_at,security_action`;IP 只下发必要脱敏值。
- 已读:`POST /v1/im/system-notifications/{id}/read`,带幂等键;跳转目标只接受服务端白名单 `action.type` 和内部参数。
- 当前普通认证用户可向 `sys` Topic 发布的路径必须封禁。客户端不得渲染普通用户伪造的系统卡片为可信通知。
@@ -852,7 +896,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
- 订阅 P2P Topic 后用 `{pres}` 更新在线/离线,`last_seen` 仅在对方隐私允许时展示。
- `GET /v1/im/conversations/{topic}/context` 返回 `display_name,avatar_file_id,online_visibility,relationship`。
- 好友关系变化由服务端生成 `system_event{event_type:"friend_relationship_changed"}`,客户端不得根据本地动作自行永久插入系统记录。
- 好友关系变化由服务端生成 0.5.4.6 定义的 WS 业务事件,客户端不得根据本地动作自行永久插入系统通知记录。
### 3.3 个人对话框(我的状态).png
@@ -880,7 +924,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
**状态:新增好友审批与 Topic 联动。**
- 好友申请通过后,服务端在同一事务内建立双方关系和 P2P 订阅,并生成 `friend_relationship_changed:accepted` 系统事件。
- 好友申请通过后,服务端在同一事务内建立双方关系和 P2P 订阅,并向申请人、通过人本人下发 `info.what="im", event="friend.request.accepted"`;申请人当前会写入系统通知,通过人本人当前只收到实时业务事件。
- 客户端收到事件或重新查询 `context.relationship=friend` 后解锁输入区;不得仅因本地“通过”按钮成功动画解锁。
- 首次进入订阅 P2P Topic,再按 `since` 拉取申请上下文和正常消息。
@@ -1174,6 +1218,8 @@ X-Tinode-APIKey: <预置的普通 API Key>
| `GET /v1/im/topics/{topic}/messages/{seq}/context?before=20&after=20` | Query only | `{"topic":"grp...","anchor_seq":12,"items":[{"seq":11,"content":{"text":"上一条"}},{"seq":12,"content":{"text":"命中消息"}}]}` |
| `GET /v1/im/topics/{topic}/media?type=image&cursor=` | Query only | `{"topic":"grp...","items":[{"seq":12,"thumbnail_file_id":"file_thumb","file_id":"file_img","width":1080,"height":720,"ts":"2026-08-20T09:00:00Z","availability":"available"}],"next_cursor":""}` |
消息撤回和编辑成功后,Topic 在线成员会收到 0.5.4.6 定义的 `info.what="im"` 业务事件:`message.recalled` 或 `message.edited`。前端收到后按 `topic + seq + version` 刷新本地消息,不要等待用户重新进入会话才更新。
## 5. 通讯录、好友关系与黑名单
### 5.1 通讯录.png
@@ -1196,6 +1242,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
- 列表:`GET /v1/im/friend-requests?direction=inbound&status=all&cursor=`,返回 `request_id,applicant,message,source,status,created_at,handled_at`。
- 同意:`POST /v1/im/friend-requests/{request_id}/accept`;拒绝:`POST /v1/im/friend-requests/{request_id}/reject`,均带幂等键和可选 `version`。
- 成功返回最终 `status` 和 P2P `topic`。重复审批返回原结果;并发冲突返回 `REQUEST_ALREADY_HANDLED` 并附当前状态,前端刷新该项。
- 主动下行:创建申请时被申请人收到 `info.what="im", event="friend.request.created"`;通过/拒绝后申请人与处理人本人都收到 `friend.request.accepted/rejected`。前端收到后刷新新朋友列表、好友列表、会话 context 和 `bootstrap.entry_badges`。当前系统通知列表只给被申请人/申请人写记录,处理人本人主要靠实时业务事件刷新。
### 5.3 添加好友.png
@@ -1361,7 +1408,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
- 列表:`GET /v1/im/groups?category=owned|managed|joined&query=&cursor=`,返回 `topic,name,avatar_file_id,member_count,my_role,updated_at`。
- 创建:`POST /v1/im/groups`,Body `{"name":"...","description":"...","member_user_ids":[],"avatar_file_id":"..."}`;`description` 可省略或传空字符串,最长 200 字符;原子校验租户群数上限和群成员上限,返回 `topic,name,avatar_file_id,description,owner,member_count,version,permissions`。
- 分类由服务端 `my_role` 决定;创建成功或角色变化后通过系统事件刷新,不由前端自行移动分组。
- 分类由服务端 `my_role` 决定;创建成功或角色变化后通过 `info.what="group"` 业务事件刷新,不由前端自行移动分组。
### 6.2 空白页.png
@@ -1378,7 +1425,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
**状态:新增邀请卡片和加入接口。**
- 邀请卡下行 `system_event{event_type:"group_invitation",invitation_id,group,inviter,status,expires_at}`。
- 邀请卡展示以群邀请接口返回和系统通知记录为准;相关实时刷新使用 `info.what="group"` 群业务事件。
- 点击加入:`POST /v1/im/group-invitations/{invitation_id}/accept`,带幂等键;返回 `status=joined|pending_approval` 和 `topic`。
- 服务端校验邀请租户、有效期、群状态、成员资格和成员数上限;重复加入返回当前状态,过期返回 `INVITATION_EXPIRED`。
@@ -1389,8 +1436,8 @@ X-Tinode-APIKey: <预置的普通 API Key>
**状态:新增群成员事件。**
- 邀请:`POST /v1/im/groups/{topic}/invitations`,Body `{"user_ids":[],"message":""}`;按群策略直接加入或创建待审批申请。
- 加入、审批、拒绝等结果由服务端生成 `system_event{event_type:"group_membership_changed",operator,target_users,action,result}` 并写入群时间线。
- 客户端只渲染可信系统事件;服务端同时更新成员表、Topic 订阅和成员数,不允许三者不一致。
- 加入、审批、拒绝等结果由服务端生成 `info.what="group"` 群业务事件,例如 `group.member.joined`、`group.join_request.accepted/rejected`。
- 客户端只渲染服务端可信事件;服务端同时更新成员表、Topic 订阅和成员数,不允许三者不一致。
### 6.5 邀请进群申请.png
@@ -1419,7 +1466,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
- 查询最新公告:`GET /v1/im/groups/{topic}/announcements/latest`;发布/更新:`PUT /v1/im/groups/{topic}/announcements/current`,Body `{"content":"...","version":1}`。
- 返回 `announcement_id,content,author,version,published_at`。仅有权限角色可写,使用乐观锁防覆盖。
- 发布成功后服务端生成群系统事件;顶部摘要使用接口返回数据,不解析聊天文本猜测。
- 发布成功后服务端生成 `group.announcement.updated` 群业务事件;顶部摘要使用接口返回数据,不解析聊天文本猜测。
### 6.8 群公告状态/邀请进群申请同时存在.png
@@ -1603,7 +1650,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
**状态:新增。**
- `POST /v1/im/groups/{topic}/owner-transfer`,Body `{"target_user_id":"usr...","version":n}`,必须带幂等键和二次认证 `X-Action-Token`。
- 服务端事务内把旧群主降为管理员或成员、目标升为群主、更新权限和审计,再生成系统事件。
- 服务端事务内把旧群主降为管理员或成员、目标升为群主、更新权限和审计,再生成 `group.owner.transferred` 群业务事件。
- 成功返回 `owner,previous_owner,roles,version`;并发成员变化返回 `GROUP_VERSION_CONFLICT`,前端重新确认,不自动重试。
### 6.27 群文件.png
@@ -2001,7 +2048,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
- `GET /v1/im/workbench` 返回 `modules[{code,name,icon_file_id,route,unread_count,enabled,permission}]`、布局版本和企业配置。
- `GET /v1/im/bootstrap` 返回 `entry_badges.workbench_total_unread`,用于工作台一级入口总红点;总数只汇总当前用户可见且 `enabled=true` 的工作台模块。
- 服务端按当前 Token 的租户、组织角色和功能开关裁剪;前端只接受白名单内部 `route`,禁止直接执行服务端任意 URL。
- 未读数通过 `workbench.badge.updated` 系统事件增量更新;切租户必须重新加载,不复用另一企业工作台。
- 工作台一级总未读以 `GET /v1/im/bootstrap` 和 `GET /v1/im/workbench` 返回为准;若后续启用 `workbench.badge.updated`,应按 0.5.4.6 的 `info.what="im"` 业务事件下发。切租户必须重新加载,不复用另一企业工作台。
### 9.2 部门组织框架.png
@@ -2022,6 +2069,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
- 创建:`POST /v1/im/meetings`,Body `{"title":"...","start_at":"...","end_at":"...","location":"...","attendee_user_ids":[],"reminder_minutes":[]}`。
- 详情:`GET /v1/im/meetings/{meeting_id}` 返回会议、参会人的 `response=pending|accepted|declined|tentative`、权限和 `version`。
- 更新/取消:`PATCH /v1/im/meetings/{id}`、`POST /v1/im/meetings/{id}/cancel`;提醒未响应者 `POST /v1/im/meetings/{id}/reminders`。所有写操作带幂等键并生成通知和审计。
- 主动下行:参会人收到 `info.what="im"`,事件为 `meeting.created/updated/canceled/reminded/response.updated`,payload 包含 `meeting_id,version,state`;前端收到后刷新会议列表或会议详情。除操作者外,当前会写入系统通知列表。
### 9.4 会议通知-接收人状态.png
@@ -2030,7 +2078,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
**状态:新增。**
- 详情同 9.3;响应:`PUT /v1/im/meetings/{meeting_id}/my-response`,Body `{"response":"accepted|declined|tentative","version":n}`。
- 返回我的最终响应和会议版本,并实时通知发起人;重复提交同状态幂等。
- 返回我的最终响应和会议版本,并通过 `meeting.response.updated` 实时通知参会人;重复提交同状态幂等。
- 会议过期返回 `MEETING_CLOSED` 并只读展示;客户端本地日历写入是可选系统动作,不替代服务端响应。
### 9.5 日报.png
@@ -2410,7 +2458,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
- 高风险动作在原业务接口返回 `409 RISK_CONFIRMATION_REQUIRED`,`details={risk_challenge_id,level,notice_code,notice_params,expires_at,action}`。
- 客户端用 `notice_code` 映射经过审核的本地文案,不显示服务端规则、分数或命中特征;取消不重试原操作。
- 低风险提示也可由可信 `security.risk_notice` 系统事件触发。普通消息不得伪装成系统反诈弹窗。
- 低风险提示也可由服务端可信安全事件触发。当前风险确认/取消会按 0.5.4.6 下发 `security.risk.continue/cancel`;普通消息不得伪装成系统反诈弹窗。
### 11.2 反诈弹窗(二次提示安卓).png
@@ -2489,7 +2537,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
| --- | --- | --- |
| P0 | `/v1/im` 统一认证中间件、响应封装、幂等键、租户 Principal | 全部 HTTP 产品能力 |
| P0 | 企业码解析、验证码、密码重置、二次认证、Token `auth_version`、登录换新和撤销 | 1.1~1.11、10.7~10.8 |
| P0 | 普通用户禁止发布系统 Topic;系统事件可信生产通道 | 2.5、3.1~3.5、6.4、11.1 |
| P0 | 普通用户禁止发布系统 Topic;系统通知和业务事件可信生产通道 | 2.5、3.1~3.5、6.4、11.1 |
| P0 | 消息 `x-im-schema/type/client-mid` 校验和服务端幂等 | 2~4 章 |
| P0 | 文件资源级 ACL、Range、ETag、用途绑定和租户存储配额 | 3.6、4.10~4.14、6.27~7.16、11.4~11.5 |
| P1 | 好友申请、好友关系、黑名单状态机 | 第 3、5 章 |
@@ -0,0 +1,211 @@
# 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` | 除操作人外写入 |
| 成员加入 | 群成员 | `event="group.member.joined"` | `user_id`、`topic` | 除操作人外写入 |
| 入群申请创建 | 群成员/管理员可见链路 | `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`、禁言信息 | 除操作人外写入 |
| 成员离群 | 群成员 | `event="group.member.left"` | `user_id` | 除操作人外写入 |
| 群角色更新 | 群成员 | `event="group.role.updated"` | `user_id`、角色信息 | 除操作人外写入 |
| 群主转让 | 群成员 | `event="group.owner.transferred"` | `previous_owner`、`new_owner` | 除操作人外写入 |
前端建议动作:
- 群成员、角色、禁言、群主变化:刷新群详情、成员列表、当前用户群权限。
- 入群申请变化:管理员刷新入群申请列表和群 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. 好友申请通过后是否双方都写系统通知,需要产品确认;若确认双方都有,需要同步补代码。
+40 -34
View File
@@ -1,6 +1,6 @@
# IM 第一至十一章后续增强项
> 更新日期:2026-08-21
> 更新日期:2026-08-25
> 核对依据:`docs/IM-前后端对接文档.md`、`server/main.go` 当前 `/v1/im/*` 路由、`server/im_http.go` 当前 handler。
> 范围:第一章登录注册与安全、第二章消息首页/导航/系统通知、第三章单聊关系、第四章消息输入/转发/检索、第五章通讯录/好友/黑名单、第六章群聊创建/资料/治理、第七章文件/收藏/聊天记录检索、第八章实时语音与视频通话、第九章企业工作台与组织架构、第十章个人设置与客户端能力、第十一章反诈安全/举报/反馈。
> 用途:记录当前代码基线之外仍需排期的增强项。已经落地的 P0/P1 不再作为待办保留;已经抽入专项开发文档的项目,在本文中仅保留来源和现状说明,不再作为未分流待办处理。
@@ -14,42 +14,38 @@
3. `GET /v1/im/forward-targets` 已新增,返回文件助手和可转发 P2P/群会话目标。
4. 总经理信箱配置与提交的 500 已修复,`GET /v1/im/executive-mailbox/config`、`POST /v1/im/executive-mailbox/submissions` 已可联调。
5. 举报原因 500 已修复,`GET /v1/im/reports/abuse/reasons` 已按 `items` 返回。
6. `GET /v1/im/groups/{topic}/files` 和 `/announcements` 不再误落到群详情;未实现路径返回 404。
6. 群文件和群公告路径已从群详情兜底中拆出,群文件基础接口已落地,未实现的子路径会返回明确错误。
7. 注册数据模型已改为 `im_users.username`、`im_users.phone`、`im_user_profiles.email`,不再依赖用户 `tags` 保存手机号/邮箱。
8. 邀请码主数据在 HotGo 库,注册使用明细在 IM 租户库;邀请码可多人复用,不再“核销”。
9. 用户搜索、用户主页、用户二维码已支持 `user_id/username/public_account` 作为 identifier。
10. 第一批增强中的群文件列表/登记/删除、群邀请外部接受、实名更正申诉、组织全局成员接口、月报 reopen 已落地。
11. 第一批增强中的主动创建/获取 P2P 会话、会话设置、多端云草稿已落地。
### 0.2 当前仍明确缺口及分流状态
| 优先级 | 缺口 | 对接文档位置 | 当前代码状态 | 分流状态 |
| --- | --- | --- | --- | --- |
| P0 | 真实短信供应商发送、失败重试、发送审计 | 1.4、1.7、1.10 | 当前测试码 `123456` 可联调,但生产短信未完成 | 依赖第三方,暂留后续增强池 |
| P0 | 群文件完整接口 | 6.27、6.28 | `/groups/{topic}/files` 已不误兜底,但列表/登记/删除未实现 | 已抽入 `IM-增强项第一批开发.md` |
| P0 | 消息转发落地接口 | 4.18、4.19、4.20 | 仅 `forward-targets` 已实现,`POST /v1/im/messages/forward`、`GET /v1/im/forward-bundles/{id}` 未实现 | 已抽入 `IM-增强项第一批开发.md` |
| P0 | 登出与会话撤销 | 10.7 | `POST /v1/im/auth/logout`、`DELETE /v1/im/security/sessions/{id}` 未见路由 | 已抽入 `IM-增强项第一批开发.md` |
| P1 | 单聊主动建会话/会话设置/云草稿 | 3.2、4.5、4.21 | `GET /conversations/{topic}/context` 已有,`POST /conversations/p2p`、`/settings`、`PUT /topics/{topic}/draft` 未见路由 | 已抽入 `IM-增强项第一批开发.md` |
| P1 | 群邀请外部接受接口 | 6.8 | 群内 `/groups/{topic}/invitations` 已有,`POST /v1/im/group-invitations/{id}/accept` 未见路由 | 已抽入 `IM-增强项第一批开发.md` |
| P1 | 实名更正申诉 | 1.9、10.22 | `POST /v1/im/security/realname/appeals` 未见路由 | 已抽入 `IM-增强项第一批开发.md` |
| P1 | 组织全局成员接口 | 5.1、9.2 | 部门成员与组织搜索已具备,`GET /v1/im/organization/members` 未见路由 | 已抽入 `IM-增强项第一批开发.md` |
| P1 | 月报 reopen | 9.10 | 月报草稿/提交/详情已具备,`POST /reports/monthly/{id}/reopen` 未见路由 | 已抽入 `IM-增强项第一批开发.md` |
### 0.3 已抽入第一批开发的项目
以下项目已经从本文档的普通后续增强池中抽出,形成专项开发文档 `docs/IM-增强项第一批开发.md`。它们当前代码上仍未完全落地,但不再按“未分流的未实现项”处理,后续应以第一批开发文档作为实现、验收和同步更新依据。
以下项目已经从本文档的普通后续增强池中抽出,形成专项开发文档 `docs/IM-增强项第一批开发.md`。其中已落地项已移入各章“当前已完成基线”;仍未完全落地的项目继续以第一批开发文档作为实现、验收和同步更新依据。
1. 登出与指定会话撤销。
2. 普通消息转发。
3. 合并转发 Bundle 详情。
4. 群文件列表、登记和删除。
5. 主动创建/获取 P2P 会话。
6. 会话设置。
7. 多端云草稿。
8. 群邀请外部接受。
9. 实名更正申诉。
10. 组织全局成员接口。
11. 月报 reopen。
12. 列表 `has_more` 统一。
13. 风控 token 单次消费接入业务接口。
1. 仍待落地:登出与指定会话撤销。
2. 仍待落地:普通消息转发。
3. 仍待落地:合并转发 Bundle 详情。
4. 已落地:群文件列表、登记和删除。
5. 已落地:主动创建/获取 P2P 会话。
6. 已落地:会话设置。
7. 已落地:多端云草稿。
8. 已落地:群邀请外部接受。
9. 已落地:实名更正申诉。
10. 已落地:组织全局成员接口。
11. 已落地:月报 reopen。
12. 仍待统一回归:列表 `has_more` 统一。
13. 仍待落地:风控 token 单次消费接入业务接口。
## 1. 第一章:登录、注册与安全
@@ -70,6 +66,7 @@
13. 产品 HTTP 已统一校验 API Key、Schema 和 Request ID。
14. 登录态写接口已接入租户库幂等记录,支持请求摘要冲突检测和首次响应重放。
15. 风险确认可生成安全事件和系统通知。
16. 实名更正申诉 `POST /v1/im/security/realname/appeals` 已具备,申诉记录进入 pending 状态。
### 1.2 后续增强项
@@ -78,7 +75,7 @@
| P0 | 真实短信供应商发送通道 | 当前为测试码联调模式;生产必须接短信服务商、模板、签名、失败重试和发送审计。 | 真实手机号收到验证码;供应商失败返回稳定 JSON,告警可观测。 |
| P0 | 验证码多维限频 | 需要按租户、手机号、IP、设备、场景组合限频,并接 Redis 或落库。 | 高频请求返回 `VERIFICATION_RATE_LIMITED`,不同场景互不误伤。 |
| P0 | 登出与会话撤销 | 对接文档已有 `POST /v1/im/auth/logout` 和 `DELETE /v1/im/security/sessions/{id}`,代码未见路由。 | 当前设备退出不影响其他设备;删除指定 session 后该设备 token 立即失效。 |
| P1 | 实名审核与申诉 | 当前支持提交和状态查询,后续需要后台审核、驳回原因、通过状态变更和 `realname/appeals`。 | 审核通过后 `realname.status=verified`;驳回可再次提交;申诉有工单状态。 |
| P1 | 实名后台审核流转 | 当前支持实名提交、状态查询和申诉提交,后续需要后台审核、驳回原因、通过状态变更和申诉处理工单。 | 审核通过后 `realname.status=verified`;驳回可再次提交;申诉有处理状态。 |
| P1 | 安全事件通知覆盖扩展 | 风险确认已接入通知;密码重置、实名审核、独立密码变更、新设备登录仍需补齐。 | `/v1/im/system-notifications` 可看到对应事件且重试不重复入箱。 |
| P1 | API Key 轮换策略 | 当前 API Key 校验兼容现有逻辑,尚未实现最低可接受序号或撤销表。 | 旧序号 Key 可按配置禁用,新 Key 不影响正常客户端。 |
| P2 | 风控与反诈策略接入登录注册 | 安全动作后续应接入风险评分、反诈弹窗和二次确认策略。 | 高风险注册/登录/改密触发二次验证或反诈提示。 |
@@ -103,12 +100,15 @@
| 优先级 | 增强项 | 说明 | 建议验收点 |
| --- | --- | --- | --- |
| P0 | 消息变更查询占位策略 | 已撤回消息在普通拉取中应返回撤回占位,而不是仅靠客户端本地事件。 | 新设备登录拉历史时能看到撤回占位和版本。 |
| P0 | 万人大群离线提醒与重连补偿 | 离线只走极光 Push 提醒,客户端重连后按 Topic seq 拉取补偿。 | 离线用户收到极光提醒;重连后按 seq 补齐且不丢不重。 |
| P0 | 系统通知未读聚合实时化 | `bootstrap.entry_badges.system_unread_count` 已有,后续需确认真源与增量事件。 | 读通知后 bootstrap 和 WS badge 均一致。 |
| P1 | 列表 `has_more` 统一 | 部分接口仍只返回 `items,next_cursor`,对接文档要求可统一补 `has_more`。 | 前端所有列表分页逻辑可复用同一协议。 |
| P1 | 群回执隐私和角色策略 | 当前返回基础 read/recv 列表,后续需按租户策略、群规模、角色裁剪。 | 普通成员/管理员在不同策略下看到不同粒度回执。 |
| P1 | `message_policy` 租户配置化 | 当前 recall/edit 窗口和菜单能力是固定值,后续需要来自租户配置。 | 不同租户可配置不同撤回/编辑时限和菜单能力。 |
| P1 | 系统通知 action 白名单 | `action.type` 和参数需要统一注册、校验和版本化。 | 非白名单 action 写入失败;客户端只收到可识别跳转。 |
| P1 | 会话搜索排序与类型过滤 | 当前按订阅数据过滤,后续需支持会话类型、置顶、免打扰、最近活跃排序。 | 搜索结果与消息首页排序、筛选规则一致。 |
| P1 | 群成员分页快照深度优化 | 当前已接入 Redis 短 TTL 响应快照,后续大群需要 sorted set、稳定游标和成员变更主动失效。 | 万人大群成员翻页不重复不漏,角色/昵称/退群变更后缓存能及时失效。 |
| P1 | 未读数 Redis 深度优化 | 当前已接入用户总未读 Redis 热缓存,后续需要 Topic 维度未读计数、bootstrap 聚合缓存和主动失效事件。 | 首页红点、会话未读和多端 read/recv 后的未读数实时一致。 |
| P2 | 离线重连恢复编排测试 | 需要端到端覆盖 `hi -> token login -> sub me -> 恢复 Topic -> get data`。 | 断网重连后未丢消息、未重复消息、未错乱回执。 |
## 3. 第三章:单聊关系、权限与会话上下文
@@ -121,15 +121,16 @@
4. 拉黑/解除拉黑后关系上下文即时反映 `blocked_by_me`、`blocked_by_peer` 等状态。
5. 好友申请、通过/拒绝、删除、拉黑/解除和备注已生成稳定关系事件,相关对端通知可去重。
6. 非好友资料查询已执行 `profile_visibility`。
7. 主动创建/获取 P2P 会话 `POST /v1/im/conversations/p2p` 已具备。
8. 会话设置 `GET/PATCH /v1/im/conversations/{topic}/settings` 已具备。
9. 多端云草稿 `GET/PUT /v1/im/topics/{topic}/draft` 已具备。
### 3.2 后续增强项
| 优先级 | 增强项 | 说明 | 建议验收点 |
| --- | --- | --- | --- |
| P0 | P2P 策略统一入口 | 当前核心路径已校验好友/黑名单,后续需要把发消息、通话、名片、文件、历史查看统一收敛到同一策略服务。 | 任一 P2P 业务入口对同一关系状态返回一致权限。 |
| P1 | 主动建立 P2P 会话接口 | 对接文档有 `POST /v1/im/conversations/p2p`,当前未见路由。 | 好友关系成立时可显式获取/创建 P2P topic。 |
| P1 | 会话设置接口 | 对接文档有 `GET/PATCH /v1/im/conversations/{topic}/settings`,当前未见路由。 | 置顶、免打扰、备注、保留策略跨端同步。 |
| P1 | 多端草稿 | 对接文档有 `PUT /v1/im/topics/{topic}/draft`,当前仍建议本地加密保存。 | 同账号多端草稿同步,版本冲突可处理。 |
| P1 | 会话策略与草稿联调回归 | P2P 会话、会话设置和云草稿接口已具备,仍需覆盖多端、冲突、非好友/黑名单状态下的联调回归。 | 多端草稿同步、版本冲突和权限拒绝都符合协议。 |
| P1 | Pending 会话体验增强 | 待处理好友申请对应单聊页应携带申请摘要、来源、过期时间和快速处理入口。 | 进入 pending 会话可直接通过/拒绝。 |
| P1 | 黑名单历史消息策略 | 需明确拉黑后历史消息、资料、文件和已读回执可见范围。 | 被拉黑人在不同策略下看到一致的历史与资料裁剪结果。 |
| P1 | 好友关系审计 | 删除、拉黑、解除拉黑、申请处理等敏感关系操作写审计日志。 | 后台可按租户、操作者、目标用户查询操作流水。 |
@@ -199,14 +200,15 @@
8. 禁言候选、批量禁言、禁言列表和解除禁言接口已具备。
9. 群写操作已使用 `im_group_idempotency` 和 `im_group_events_outbox`,提交后通过 Hub 派发,失败保留 pending 重试。
10. WebSocket 群消息发布已校验成员禁言和全员禁言,支持过期恢复及管理员豁免。
11. 群文件和群公告未实现路径已不再误返回群详情。
11. 群文件列表、登记和删除接口 `GET/POST/DELETE /v1/im/groups/{topic}/files` 已具备。
12. 群邀请外部接受 `POST /v1/im/group-invitations/{id}/accept` 已具备。
### 6.2 后续增强项
| 优先级 | 增强项 | 说明 | 建议验收点 |
| --- | --- | --- | --- |
| P0 | 群文件索引与管理 | 对接文档要求 `GET/POST/DELETE /v1/im/groups/{topic}/files`,当前未实现,只修复了错误兜底。 | 群文件列表可搜索/分页,上传登记和删除权限正确,退群后不可下载旧 URL。 |
| P1 | 群邀请外部接受接口 | 对接文档有 `POST /v1/im/group-invitations/{id}/accept`,当前未见路由。 | 被邀请人可通过邀请卡入群或进入审批。 |
| P0 | 群文件权限与下载闭环 | 群文件基础接口已具备,后续需与文件 ACL、退群后下载阻断、审核状态、OSS 短链失效联动。 | 群文件列表可搜索/分页,上传登记和删除权限正确,退群后不可下载旧 URL。 |
| P1 | 群邀请外部接受体验回归 | 外部接受接口已具备,仍需覆盖邀请卡、多端重复接受、过期和审批模式切换。 | 被邀请人可通过邀请卡入群或进入审批;重复接受幂等返回稳定结果。 |
| P1 | 进群申请隐私与限频 | 申请入口需要按群治理、租户策略、用户状态、来源、IP/设备限频。 | 高频申请被限制;普通成员不可看到敏感申请人信息。 |
| P1 | 管理员/群主角色层级策略 | 管理员互相操作、管理员禁言、群主转让二次认证等需要完整策略矩阵。 | 群主、管理员、普通成员在每个治理动作上的权限稳定一致。 |
| P1 | 群解散与二维码吊销 | 群解散、二维码撤销、邀请失效需要状态联动和权限回收。 | 解散后 Topic、二维码、邀请、文件新增权限全部失效。 |
@@ -275,6 +277,8 @@
8. 日报/月报草稿、提交、详情、列表、统计与提醒基线已具备。
9. 总经理信箱配置与提交接口已具备,包含匿名策略、文案版本和幂等键。
10. 会议创建、变更、取消、提醒和参会回应已生成在线事件与系统通知,关键变更在事务内写 Outbox。
11. 组织全局成员接口 `GET /v1/im/organization/members` 已具备。
12. 月报 reopen `POST /v1/im/reports/monthly/{id}/reopen` 已具备。
### 9.2 后续增强项
@@ -284,9 +288,9 @@
| P0 | 工作台未读角标实时事件 | `workbench_total_unread` 已有,后续需要模块未读聚合和 `workbench.badge.updated` 增量事件。 | 会议、日报、信箱未读变化后工作台角标实时更新。 |
| P0 | 组织数据后台维护链路 | 当前前台读接口和表已具备,后续需要后台导入、增删改部门、调整成员和离职状态。 | 后台修改组织后前台接口版本变化,客户端可刷新。 |
| P0 | 组织权限与可见字段裁剪 | 成员手机号、邮箱、职位等字段需要按组织角色、隐私和企业策略裁剪。 | 普通员工和管理员看到的成员字段不同且不可绕过。 |
| P1 | 组织全局成员接口 | 对接文档提到 `GET /v1/im/organization/members`,当前可由部门成员和组织搜索替代,未见独立路由。 | 前端如需要全局成员列表,可直接分页查询并按部门过滤。 |
| P1 | 组织全局成员接口深度联调 | `GET /v1/im/organization/members` 已具备,仍需按部门、关键词、权限和分页做测试环境回归。 | 前端可直接分页查询并按部门过滤,字段裁剪符合权限。 |
| P1 | 会议列表完整联调 | 对接文档标注 `GET /v1/im/meetings?cursor=` 待补列表;代码有 `serveIMMeetings`,仍需用测试数据确认返回结构。 | 会议列表、详情、响应、取消端到端一致。 |
| P1 | 月报 reopen | 对接文档有 `POST /v1/im/reports/monthly/{id}/reopen`,当前未见路由。 | 已提交月报可按权限退回草稿并保留审计。 |
| P1 | 月报 reopen 审计与权限扩展 | reopen 接口已具备,后续需要后台权限、原因记录、审计和通知闭环。 | 已提交月报可按权限退回草稿并保留审计。 |
| P1 | 日报/月报后台统计口径 | 当前基础统计可用,后续需要按部门、周期、迟交、补交、未读等维度扩展。 | 管理员看板与业务口径一致。 |
| P1 | 总经理信箱后台流转 | 当前前台配置/提交已具备,后续需要后台处理、回复、转派、归档和通知。 | 信件全生命周期可追踪,匿名身份受保护。 |
| P1 | 办公协同审计 | 组织查询、会议变更、报告查看、信箱提交等敏感动作需要审计。 | 后台可按租户、用户、对象查询办公操作流水。 |
@@ -341,7 +345,9 @@
## 12. 统一处理建议
1. 第一优先级处理生产可用性 P0:真实短信、登出/会话撤销、群文件、普通消息转发/合并转发、文件安全、通话强状态机、工作台权限、风控 token 消费。
2. 第二优先级补齐前端文档缺口:`conversations/p2p`、会话设置、云草稿、群邀请外部接受、实名申诉、组织全局成员、月报 reopen。
1. 第一优先级处理生产可用性 P0:真实短信、登出/会话撤销、普通消息转发/合并转发、文件安全、通话强状态机、工作台权限、风控 token 消费。
2. 第二优先级补齐已落地接口的联调闭环:P2P 会话、会话设置、云草稿、群文件、群邀请外部接受、实名申诉、组织全局成员、月报 reopen。
3. 第三优先级统一协议和治理能力:所有列表补齐 `has_more`、稳定游标、审计流水、字段级隐私矩阵、租户配置化策略。
4. 每个增强项落地后同步更新本文档、`IM-系统设计文档.md`、`IM-前后端对接文档.md` 和接口审核报告,避免已完成项继续出现在待办清单。
4. 万人大群未实现增强项仅保留离线能力:离线只走极光 Push 提醒,客户端重连后按 Topic seq 拉取补偿;实时推送能力归入 `IM-Redis三层数据分层改造方案.md` 后续直接实现。
5. Redis 读性能深度优化继续保留:群成员分页 sorted set/稳定游标/主动失效、Topic 维度未读计数、bootstrap 聚合缓存和主动失效事件。
6. 每个增强项落地后同步更新本文档、`IM-系统设计文档.md`、`IM-前后端对接文档.md` 和接口审核报告,避免已完成项继续出现在待办清单。
Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

@@ -0,0 +1,12 @@
-- IM SeqId BIGINT upgrade for existing MySQL tenant databases.
-- New schemas have been updated in server/db/mysql/adapter.go and group_schema.go.
ALTER TABLE im_topics MODIFY seqid BIGINT NOT NULL DEFAULT 0;
ALTER TABLE im_subscriptions MODIFY recvseqid BIGINT DEFAULT 0;
ALTER TABLE im_subscriptions MODIFY readseqid BIGINT DEFAULT 0;
ALTER TABLE im_messages MODIFY seqid BIGINT NOT NULL;
ALTER TABLE im_message_revisions MODIFY seqid BIGINT NOT NULL;
ALTER TABLE im_favorites MODIFY source_seq BIGINT NOT NULL;
ALTER TABLE im_abuse_reports MODIFY target_seq BIGINT NOT NULL DEFAULT 0;
ALTER TABLE im_call_records MODIFY seq_id BIGINT NOT NULL;
ALTER TABLE im_group_files MODIFY source_seq BIGINT NOT NULL DEFAULT 0;
+37
View File
@@ -0,0 +1,37 @@
CREATE TABLE IF NOT EXISTS im_group_messages (
id BIGINT NOT NULL AUTO_INCREMENT,
topic VARCHAR(64) NOT NULL COMMENT '群 Topic,如 grp_xxx',
group_id BIGINT NOT NULL COMMENT '业务群 ID,便于分区、治理和统计',
seqid BIGINT NOT NULL COMMENT '群内消息序号,由 Redis 原子发号',
from_user BIGINT NOT NULL COMMENT '发送人用户 ID',
client_msg_id VARCHAR(64) NULL COMMENT '客户端幂等消息 ID',
msg_type VARCHAR(32) NOT NULL DEFAULT 'text' COMMENT 'text/image/file/audio/video/system 等',
status VARCHAR(32) NOT NULL DEFAULT 'normal' COMMENT 'normal/recalled/deleted/blocked',
content JSON NULL COMMENT '消息正文或 Drafty 内容',
head JSON NULL COMMENT '消息头,包含 @、引用、客户端能力等',
mentioned_user_ids JSON NULL COMMENT '@ 用户列表,便于提醒和检索',
mention_all TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否 @ 全体',
reply_seqid BIGINT NULL COMMENT '回复的群消息 seq',
root_seqid BIGINT NULL COMMENT '线程或合并上下文根消息 seq',
file_count INT NOT NULL DEFAULT 0 COMMENT '附件数量',
content_size INT NOT NULL DEFAULT 0 COMMENT '正文 JSON 序列化后的字节数',
has_risk TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否命中风控或内容安全',
audit_status VARCHAR(32) NOT NULL DEFAULT 'pass' COMMENT 'pass/pending/reject',
version INT NOT NULL DEFAULT 1 COMMENT '编辑版本',
recalled_at DATETIME(3) NULL,
recalled_by BIGINT NULL,
deleted_at DATETIME(3) NULL,
expires_at DATETIME(3) NULL COMMENT '可选保留期,便于过期归档或清理',
created_at DATETIME(3) NOT NULL,
updated_at DATETIME(3) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY uk_im_group_messages_group_seq (group_id, seqid),
UNIQUE KEY uk_im_group_messages_topic_seq (topic, seqid),
UNIQUE KEY uk_im_group_messages_topic_sender_client_mid (topic, from_user, client_msg_id),
KEY idx_im_group_messages_group_created (group_id, created_at),
KEY idx_im_group_messages_group_status_seq (group_id, status, seqid),
KEY idx_im_group_messages_topic_created (topic, created_at),
KEY idx_im_group_messages_topic_from_seq (topic, from_user, seqid),
KEY idx_im_group_messages_audit (audit_status, created_at),
KEY idx_im_group_messages_expires (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@@ -0,0 +1,110 @@
# IM 第三方接入本地测试结果
测试日期:2026-08-24
测试环境:本地环境,MySQL `127.0.0.1:3306`,控制库 `hotgo`,业务库 `tinode`。
## 1. 测试结论
| 项 | 结果 | 说明 |
| --- | --- | --- |
| 第三方相关普通单元测试 | 通过 | OSS、文件安全配置、阿里云内容安全封装测试通过 |
| MySQL build tag 单元测试 | 通过 | 文件审核表结构、MySQL 适配相关测试通过 |
| 本地 API handler 测试 | 通过 | 群文件、群邀请接受、实名申诉、组织成员、月报 reopen、反馈/举报、信箱、会话设置、云草稿等接口级测试通过 |
| 本地 MySQL 连通性 | 通过 | 可连接 `hotgo` 与 `tinode` |
| 第三方配置读取 | 通过 | `im_third_party` 配置存在 18 条,已按本地第三方依赖配置启用 OSS、内容审核和恶意文件检测 |
| 文件审核字典 | 通过 | provider 2 条、task type 2 条、status 5 条 |
| 文件审核任务表 | 通过 | `tinode.im_file_review_tasks` 存在,且不包含 `tenant_id` |
| 真实 OSS 上传/下载 | 通过 | 使用普通 Bucket endpoint 完成 put/get/sign URL,并清理测试对象 |
| 真实内容安全 | 通过 | 使用阿里云内容安全增强版 `FileModeration + document_detection` 提交并轮询,返回 `clean/none` |
| 真实恶意文件检测 | 阻塞 | SDK 请求 `tds.aliyuncs.com` 时本地网络连接被 reset,`curl` 直连同样失败 |
## 2. 执行命令
```bash
go test -count=1 ./server/media/oss ./server/filesafety ./server/filesafety/aliyun ./server/db/mysql
go test -count=1 -tags mysql ./server/media/oss ./server/filesafety ./server/filesafety/aliyun ./server/db/mysql
go test -count=1 ./server -run 'TestServeIM(GroupFiles|GroupInvitationAccept|RealNameAppeal|OrganizationMembers|MonthlyReportReopen|DailyReport|MonthlyReport|Mailbox|Feedback|Abuse|ConversationSettings|TopicDraft|P2PConversation|ForwardTargets)'
```
## 3. 命令结果
| 命令 | 结果 |
| --- | --- |
| `go test -count=1 ./server/media/oss ./server/filesafety ./server/filesafety/aliyun ./server/db/mysql` | 通过 |
| `go test -count=1 -tags mysql ./server/media/oss ./server/filesafety ./server/filesafety/aliyun ./server/db/mysql` | 通过 |
| `go test -count=1 ./server -run ...` | 通过 |
## 4. 本地数据库核验
| 核验项 | 结果 |
| --- | --- |
| MySQL ping | 通过 |
| `hotgo` 数据库 | 存在 |
| `tinode` 数据库 | 存在 |
| `hotgo.hg_sys_config` 中 `im_third_party` 配置数量 | 18 |
| `oss_enabled` | 1 |
| `file_review_content_enabled` | 1 |
| `file_review_malware_enabled` | 1 |
| OSS 敏感配置 | 已配置,报告不输出具体值 |
| 阿里云审核敏感配置 | 已配置,报告不输出具体值 |
| `im_file_review_provider` 字典 | 2 条 |
| `im_file_review_task_type` 字典 | 2 条 |
| `im_file_review_status` 字典 | 5 条 |
| `tinode.im_file_review_tasks` | 存在 |
| `im_file_review_tasks.tenant_id` | 不存在,符合租户分库方案 |
| `im_file_review_tasks` 当前记录数 | 0 |
## 5. 覆盖情况
| 测试范围 | 本次覆盖 | 结果 |
| --- | --- | --- |
| OSS 配置读取与对象路径规则 | 已覆盖单元测试 | 通过 |
| `im_third_party` 配置读取 | 已覆盖单元测试和本地库只读核验 | 通过 |
| `im_file_review_tasks` 表结构 | 已覆盖 MySQL tag 测试和本地库只读核验 | 通过 |
| 上传完成创建审核任务 | 已覆盖代码路径和表结构测试;本地无真实上传任务 | 部分通过 |
| 群文件 API handler | 已覆盖本地 handler 测试 | 通过 |
| 举报/反馈/信箱附件相关 API handler | 已覆盖本地 handler 测试 | 通过 |
| 日报/月报附件相关 API handler | 已覆盖本地 handler 测试 | 通过 |
| P2P 会话、会话设置、云草稿 | 已覆盖本地 handler 测试 | 通过 |
| 真实 OSS API | 已上传、读取、生成签名 URL、清理测试对象 | 通过 |
| 真实内容安全 API | 已提交 OSS 文件审核并轮询结果 | 通过 |
| 真实恶意文件检测 API | 已使用官方 SAS SDK 提交检测,受本地网络限制未到达服务端 | 阻塞 |
| 风险审核结果影响下载/展示 | worker 和策略闭环未完成 | 未执行 |
## 6. 真实第三方调用补测
补测时间:2026-08-24
### 6.1 补测操作
1. 从本地第三方依赖文档读取测试账号、OSS Bucket 和地域信息。
2. 将本地 `hotgo.hg_sys_config` 的 `im_third_party` 配置更新为启用状态。
3. 使用普通 Bucket endpoint `oss-cn-shenzhen.aliyuncs.com` 上传测试 TXT 文件。
4. 对同一 OSS 对象执行读取、签名 URL 生成和测试对象清理。
5. 使用阿里云内容安全增强版 `FileModeration + document_detection` 提交该 OSS 对象并轮询结果。
6. 使用阿里云云安全中心官方 SAS SDK 调用 `CreateFileDetect` 和 `GetFileDetectResult`。
### 6.2 补测结果
| 项 | 结果 | 说明 |
| --- | --- | --- |
| 本地配置启用 | 通过 | `oss_enabled`、`file_review_content_enabled`、`file_review_malware_enabled` 已置为 1 |
| OSS `PutObject` | 通过 | 测试文件成功写入 OSS |
| OSS `GetObject` | 通过 | 测试文件可读取且内容一致 |
| OSS `SignURL` | 通过 | 可生成短时下载 URL |
| OSS 测试对象清理 | 通过 | `codex-live/` 测试前缀已清理 |
| 内容安全提交 | 通过 | `FileModeration` 返回任务 ID |
| 内容安全轮询 | 通过 | 轮询结果为 `status=clean,risk=none` |
| 云安全中心提交 | 阻塞 | 本地网络访问 `https://tds.aliyuncs.com` 被 reset,未到达业务响应 |
### 6.3 SAS 阻塞说明
云安全中心 SDK 内置中国区 endpoint 为 `tds.aliyuncs.com`。本地环境中该域名解析到 `198.18.1.113`,无论 SDK 调用还是 `curl -I https://tds.aliyuncs.com/` 都返回 connection reset。备用 endpoint `tds.ap-southeast-1.aliyuncs.com` 在当前网络下同样 reset。因此本轮无法判断 SAS 账号权限或检测结果,只能确认本地网络到 SAS endpoint 不通。
## 7. 遗留问题
1. 本地网络无法访问云安全中心恶意文件检测 endpoint `tds.aliyuncs.com`,需要换网络、服务器环境或排查网络策略后复测。
2. 文件安全 worker 尚未完成,pending 审核任务不会自动提交第三方、轮询或回写状态。
3. 下载和业务展示侧尚未完整按 `im_file_review_tasks` 风险状态阻断或告警。
4. 纯文本内容安全不属于文件审核链路,群简介、签名、举报/反馈正文、日报/月报正文仍需独立文本审核方案。
@@ -2,46 +2,46 @@
## 1. 测试范围
- 测试时间:`2026-08-21 00:39:01 +0800`
- 测试时间:`2026-08-24 11:15:16 +0800`
- HTTP:`https://api.118im.com`
- WebSocket:`wss://api.118im.com/v0/channels`
- 租户:`default`
- 用户 A:`usr7-zA3UMrw7c` / `wsa459000` / `15794459000`
- 用户 B:`usrnDXQ3A2oQP0` / `wsb459000` / `15894459000`
- 用户 A:`usrO4KoysjOOVE` / `wsa043000` / `15757043000`
- 用户 B:`usricjD1vg9CXU` / `wsb043000` / `15857043000`
- API Key 与 Token 已脱敏,不在文档中展开。
## 2. 总体结论
本轮专项验证 Tinode WebSocket 真实协议链路,共执行 `29` 项,通过 `27` 项,未通过 `2` 项。
本轮专项验证 Tinode WebSocket 真实协议链路,共执行 `29` 项,通过 `28` 项,未通过 `1` 项。
| ID | 场景 | 方法 | 目标 | 结果 | 备注 |
| --- | --- | --- | --- | --- | --- |
| `WS-01` | 注册用户 A: HTTP 验证码 + WS acc | `WS` | `/v0/channels` | 通过 | user=usr7-zA3UMrw7c token=AwEA...18Om |
| `WS-02` | 注册用户 B: HTTP 验证码 + WS acc | `WS` | `/v0/channels` | 通过 | user=usrnDXQ3A2oQP0 token=AwEA...NX6b |
| `WS-03` | HTTP 建立好友关系生成 P2P Topic | `HTTP` | `/v1/im/friend-requests` | 通过 | request_id=frq_KAOkEfarCOA topic=p2p7-zA3UMrw7ecNdDcDahA_Q |
| `WS-04` | A token login + sub me | `WS` | `/v0/channels` | 通过 | login user=usr7-zA3UMrw7c |
| `WS-05` | B token login + sub me | `WS` | `/v0/channels` | 通过 | login user=usrnDXQ3A2oQP0 |
| `WS-06` | A/B 订阅 P2P Topic | `WS` | `p2p7-zA3UMrw7ecNdDcDahA_Q` | 通过 | topic=p2p7-zA3UMrw7ecNdDcDahA_Q |
| `WS-07` | A pub 文本消息,B 收到 data | `WS` | `p2p7-zA3UMrw7ecNdDcDahA_Q` | 通过 | seq=1 bob_topic=usr7-zA3UMrw7c |
| `WS-08` | B 发送 recv/read 回执 | `WS` | `p2p7-zA3UMrw7ecNdDcDahA_Q` | 通过 | what=read seq=1 |
| `WS-01` | 注册用户 A: HTTP 验证码 + WS acc | `WS` | `/v0/channels` | 通过 | user=usrO4KoysjOOVE token=AwEA...S/Xa |
| `WS-02` | 注册用户 B: HTTP 验证码 + WS acc | `WS` | `/v0/channels` | 通过 | user=usricjD1vg9CXU token=AwEA...Reda |
| `WS-03` | HTTP 建立好友关系生成 P2P Topic | `HTTP` | `/v1/im/friend-requests` | 通过 | request_id=frq_xYKajMBbqQM topic=p2pO4KoysjOOVGJyMPW-D0JdQ |
| `WS-04` | A token login + sub me | `WS` | `/v0/channels` | 通过 | login user=usrO4KoysjOOVE |
| `WS-05` | B token login + sub me | `WS` | `/v0/channels` | 通过 | login user=usricjD1vg9CXU |
| `WS-06` | A/B 订阅 P2P Topic | `WS` | `p2pO4KoysjOOVGJyMPW-D0JdQ` | 通过 | topic=p2pO4KoysjOOVGJyMPW-D0JdQ |
| `WS-07` | A pub 文本消息,B 收到 data | `WS` | `p2pO4KoysjOOVGJyMPW-D0JdQ` | 通过 | seq=1 bob_topic=usrO4KoysjOOVE |
| `WS-08` | B 发送 recv/read 回执 | `WS` | `p2pO4KoysjOOVGJyMPW-D0JdQ` | 通过 | what=read seq=1 |
| `WS-09` | basic login + sub me | `WS` | `/v0/channels` | 通过 | basic login ok |
| `WS-10` | 重复 login 被拒绝 | `WS` | `/v0/channels` | 通过 | code=409 text=already authenticated |
| `WS-11` | 订阅 fnd 搜索 Topic | `WS` | `fnd` | 通过 | sub fnd ok |
| `WS-12` | get me desc/sub/tags/cred | `WS` | `me` | 通过 | get me ok |
| `WS-13` | get P2P desc/sub/data/del | `WS` | `p2p7-zA3UMrw7ecNdDcDahA_Q` | 通过 | get p2p ok |
| `WS-14` | set P2P private 设置 | `WS` | `p2p7-zA3UMrw7ecNdDcDahA_Q` | 通过 | set private ok |
| `WS-15` | pub 产品 x-im 基础头 | `WS` | `p2p7-zA3UMrw7ecNdDcDahA_Q` | 通过 | seq=2 bob_topic=usr7-zA3UMrw7c |
| `WS-16` | pub 产品 x-im-client-mid 幂等头 | `WS` | `p2p7-zA3UMrw7ecNdDcDahA_Q` | 未通过 | pub ctrl: ctrl id=pub-003857.244320000 code=500 text=internal error |
| `WS-17` | note kp 输入中通知 | `WS` | `usr7-zA3UMrw7c` | 通过 | what=kp |
| `WS-18` | del 当前用户消息视图 | `WS` | `p2p7-zA3UMrw7ecNdDcDahA_Q` | 通过 | deleted seq=1 |
| `WS-19` | leave P2P detach 后重新订阅 | `WS` | `p2p7-zA3UMrw7ecNdDcDahA_Q` | 通过 | leave/resub ok |
| `WS-20` | HTTP 创建群供 WS 群协议测试 | `HTTP` | `/v1/im/groups` | 通过 | topic=grpURzKYanXB1Y |
| `WS-21` | A/B 订阅群 Topic | `WS` | `grpURzKYanXB1Y` | 通过 | sub group ok |
| `WS-22` | 群 pub 文本广播 | `WS` | `grpURzKYanXB1Y` | 通过 | seq=1 topic=grpURzKYanXB1Y |
| `WS-23` | get 群 desc/sub/data | `WS` | `grpURzKYanXB1Y` | 通过 | get group ok |
| `WS-13` | get P2P desc/sub/data/del | `WS` | `p2pO4KoysjOOVGJyMPW-D0JdQ` | 通过 | get p2p ok |
| `WS-14` | set P2P private 设置 | `WS` | `p2pO4KoysjOOVGJyMPW-D0JdQ` | 通过 | set private ok |
| `WS-15` | pub 产品 x-im 基础头 | `WS` | `p2pO4KoysjOOVGJyMPW-D0JdQ` | 通过 | seq=2 bob_topic=usrO4KoysjOOVE |
| `WS-16` | pub 产品 x-im-client-mid 幂等头 | `WS` | `p2pO4KoysjOOVGJyMPW-D0JdQ` | 通过 | seq=3 bob_topic=usrO4KoysjOOVE |
| `WS-17` | note kp 输入中通知 | `WS` | `usrO4KoysjOOVE` | 通过 | what=kp |
| `WS-18` | del 当前用户消息视图 | `WS` | `p2pO4KoysjOOVGJyMPW-D0JdQ` | 通过 | deleted seq=1 |
| `WS-19` | leave P2P detach 后重新订阅 | `WS` | `p2pO4KoysjOOVGJyMPW-D0JdQ` | 通过 | leave/resub ok |
| `WS-20` | HTTP 创建群供 WS 群协议测试 | `HTTP` | `/v1/im/groups` | 通过 | topic=grpNJqhCtAZwYM |
| `WS-21` | A/B 订阅群 Topic | `WS` | `grpNJqhCtAZwYM` | 通过 | sub group ok |
| `WS-22` | 群 pub 文本广播 | `WS` | `grpNJqhCtAZwYM` | 通过 | seq=1 topic=grpNJqhCtAZwYM |
| `WS-23` | get 群 desc/sub/data | `WS` | `grpNJqhCtAZwYM` | 通过 | get group ok |
| `WS-24` | 文件上传 /v0/file/u | `HTTP` | `/v0/file/u/` | 通过 | url=/v0/....bin |
| `WS-25` | WS 文件消息下发 | `WS` | `p2p7-zA3UMrw7ecNdDcDahA_Q` | 通过 | seq=3 bob_topic=usr7-zA3UMrw7c |
| `WS-26` | WebRTC 通话 invite pub | `WS` | `p2p7-zA3UMrw7ecNdDcDahA_Q` | 未通过 | pub ctrl: ctrl id=pub-003859.867810000 code=501 text=not implemented |
| `WS-25` | WS 文件消息下发 | `WS` | `p2pO4KoysjOOVGJyMPW-D0JdQ` | 通过 | seq=4 bob_topic=usrO4KoysjOOVE |
| `WS-26` | WebRTC 通话 invite pub | `WS` | `p2pO4KoysjOOVGJyMPW-D0JdQ` | 未通过 | pub ctrl: ctrl id=pub-111508.550364000 code=501 text=not implemented |
| `WS-27` | 未 hi 直接 login 被拒绝 | `WS` | `/v0/channels` | 通过 | code=409 text=command out of sequence |
| `WS-28` | hi 后切换 tenant 被拒绝 | `WS` | `/v0/channels` | 通过 | code=409 text=command out of sequence |
| `WS-29` | 非法 API Key 拒绝建连 | `WS` | `/v0/channels` | 通过 | http_status=403 |
@@ -53,6 +53,6 @@
## 4. 测试产生的数据
- P2P Topic:`p2p7-zA3UMrw7ecNdDcDahA_Q`
- Bob 收到的 Topic:`usr7-zA3UMrw7c`
- P2P Topic:`p2pO4KoysjOOVGJyMPW-D0JdQ`
- Bob 收到的 Topic:`usrO4KoysjOOVE`
- 消息 Seq:`1`