45 KiB
IM 系统设计文档
版本:v1.1
日期:2026-08-11
状态:后续开发基线
需求依据:IM 产品原型需求文档
关联文档:IM 产品需求服务端改造分析报告、IM 现有实现与新需求差异对照总结、IM 服务端多租户改造方案
| 版本 | 日期 | 变更 |
|---|---|---|
| v1.0 | 2026-08-11 | 建立总体架构和业务模块设计 |
| v1.1 | 2026-08-11 | 冻结 MySQL 范围;补齐认证、配额、消息变更、好友/群真源、审核、事务、Outbox、搜索和测试设计 |
1. 文档目的
本文作为后续 IM 产品开发的系统设计指导文件,用于统一服务端、客户端和测试团队对产品架构、模块边界、数据模型、接口风格、权限规则、消息事件和开发顺序的理解。
本文不替代详细接口文档和数据库 DDL。后续每个业务模块进入开发前,应在本文定义的边界内补充独立的 API 文档、状态机文档、表结构 SQL 和测试用例。
本文是产品业务开发的设计基线。关联分析报告用于记录调研过程;表名、唯一约束、状态机或技术选择与本文冲突时,以本文为准。设计发生变化时,必须先修改本文并记录版本,再修改接口和代码。
2. 设计目标
系统面向企业内部沟通与协作场景,核心目标如下:
- 支持企业码进入租户,所有账号、好友、群、消息、文件、组织和办公数据均按
tenant_id强隔离。 - 复用现有 Tinode 实时通信内核,保留 WebSocket 消息、Topic、Subscription、回执、文件、Push 和 WebRTC 信令能力。
- 在实时内核外新增产品业务层,承载好友关系、群治理、收藏、搜索、组织、会议、日报、月报、实名、青少年、举报和反诈等业务。
- 服务端必须成为权限和状态机的唯一可信执行方,客户端只负责交互展示和本地体验优化。
- 所有敏感动作具备幂等、防重、审计和可测试的错误码。
- 支持 iOS、Android 多端一致同步,网络断开后可增量恢复。
2.1 首期技术范围
- 产品版首期只支持 MySQL 8,共享服务、共享数据库,通过
tenant_id隔离租户。 - PostgreSQL、MongoDB 和 RethinkDB 不在首期产品业务支持范围内。启用产品模块但 Adapter 未实现完整租户业务接口时,服务必须拒绝启动,不能静默退回无租户接口。
- 实时消息继续使用现有 Tinode WebSocket 协议;事务型产品业务使用 HTTP API。
- 搜索首期使用 MySQL,达到第 11.3 节定义的切换条件后再引入独立搜索引擎。
3. 总体架构
系统采用“实时通信内核 + 产品业务服务 + 异步事件”的分层架构。
flowchart TB
APP["iOS / Android 客户端"]
subgraph Gateway["接入层"]
WS["WebSocket 实时协议"]
API["HTTP /v1/im/* 业务 API"]
FILE["HTTP 文件上传下载"]
end
subgraph Core["通信内核"]
SESSION["Session"]
HUB["Hub / Topic Actor"]
STORE["Tenant Store"]
CALL["WebRTC Call Signaling"]
end
subgraph Product["产品业务层"]
POLICY["策略引擎"]
REL["好友与黑名单"]
GROUP["群治理"]
FAV["收藏与转发"]
SEARCH["搜索索引"]
ORG["组织与工作台"]
SAFE["实名 / 青少年 / 举报 / 反诈"]
end
subgraph Infra["基础设施"]
DB["MySQL"]
CACHE["内存缓存 / 持久化缓存"]
OUTBOX["Outbox 异步事件"]
PUSH["Push / 短信"]
MEDIA["对象存储"]
PLUGIN["审核 / 风控插件"]
end
APP --> WS
APP --> API
APP --> FILE
WS --> SESSION --> HUB --> STORE --> DB
API --> Product --> DB
FILE --> MEDIA
HUB --> CALL
HUB --> POLICY
POLICY --> REL
POLICY --> GROUP
POLICY --> SAFE
Product --> OUTBOX --> PUSH
OUTBOX --> HUB
POLICY --> PLUGIN
4. 核心分层职责
| 层 | 职责 | 禁止事项 |
|---|---|---|
| 接入层 | 解析 Token、Session、文件认证和租户上下文;做基础参数校验 | 不从客户端自报字段信任内部 tenant_id |
| 通信内核 | 维护连接、Topic 路由、消息顺序、广播、回执、Push 触发和通话信令 | 不承载复杂审批、报表、组织树等事务业务 |
| 产品策略层 | 发消息前校验好友、黑名单、群禁言、青少年、文件权限和内容风险 | 不把策略只放客户端 |
| 业务服务层 | 处理好友申请、群审批、公告、收藏、组织、会议、日报、举报等 CRUD 和状态机 | 不把主数据塞进 Topic Public/Aux 任意 JSON |
| 存储层 | 所有业务表带 tenant_id,更新删除必须带租户条件 |
不保留无租户业务查询入口 |
| 异步层 | 系统消息、Push、搜索索引、审核任务、定时提醒和审计导出 | 不在 Topic 事件循环中执行耗时远程调用;核心审计记录仍在业务事务内落库 |
5. 多租户基础规则
多租户是本系统的底座,后续所有业务模块必须遵守:
- 首次
hi.tenant使用企业码解析租户,Session 固定TenantID。 - Token、认证记录、设备、Push、文件和插件调用均携带服务端可信
TenantID。 - 普通业务 Store 只能通过租户作用域访问,禁止全局查用户、Topic、消息、文件。
- 新增租户业务表必须包含
tenant_id BIGINT NOT NULL。平台级表和租户根表im_tenant例外;例外表必须在 DDL 评审中明确说明作用域。 - 唯一索引优先使用
(tenant_id, business_key)。 - 子表外键优先使用
(tenant_id, parent_id)复合约束。 - 跨租户通信首期禁止;搜索、加好友、入群、转发和文件访问均限制在当前租户。
- 越权访问统一返回 NotFound/Denied,不泄露目标对象是否存在于其他租户。
5.1 租户配置与配额
im_tenant_config 是租户配额的配置真源,首期使用以下字段:
| 字段 | 含义 | 0 的含义 |
|---|---|---|
max_users |
租户有效用户数上限 | 不限制 |
max_groups |
租户有效群数上限 | 不限制 |
max_group_members |
单群有效成员数上限 | 不限制 |
配额必须由服务端在事务内强制执行:
- 注册或启用用户时锁定租户配置记录,统计有效用户后再创建账号。
- 创建群时锁定租户配置记录,统计有效群后再创建 Topic 和群业务数据。
- 邀请、扫码入群或审批入群时锁定目标群业务记录,统计有效成员并校验
max_group_members后再写成员关系和 Subscription。 - 配额检查和资源创建必须在同一事务中完成,禁止先查询、提交后再创建。
- 超限返回稳定错误码,并记录
tenant_id、当前用量、限制值和操作类型;响应不得泄露其他租户信息。
6. 通道与接口风格
6.1 WebSocket 实时协议
继续使用现有 Tinode 协议承载以下实时能力:
| 能力 | 协议方向 | 说明 |
|---|---|---|
| 登录、注册、Token 登录 | client -> server | 已租户化,继续复用 |
| 发消息 | pub |
增加产品策略校验、消息类型和客户端幂等 ID |
| 拉消息、拉元数据 | get |
保留原序号查询;复杂搜索走 HTTP |
| 已读、送达、输入中 | note |
保留并增加隐私和节流规则 |
| 订阅/退订 Topic | sub / leave |
由好友、群审批等业务控制是否允许 |
| 通话信令 | note 或现有 call 信令 |
增加 call_id、通话类型和结束原因 |
| 系统消息 | 服务端发布的 system 类型消息 |
个人事件进入服务端管理的系统通知 Topic;群内事件进入对应群 Topic;普通用户不能发布该类型 |
6.2 HTTP 业务 API
新增业务 API 使用 /v1/im/* 前缀,按模块划分:
/v1/im/friends/*
/v1/im/groups/*
/v1/im/favorites/*
/v1/im/search/*
/v1/im/org/*
/v1/im/meetings/*
/v1/im/reports/*
/v1/im/profile/*
/v1/im/settings/*
/v1/im/security/*
/v1/im/reports-abuse/*
/v1/im/feedback/*
HTTP API 通用规则:
- HTTP 请求使用
Authorization: Bearer <token>;统一认证中间件校验 Token 后生成只读 Principal,并从中获取tenant_id和uid。请求参数中的同名字段不参与授权。 - 写接口必须支持幂等,使用
Idempotency-Key或业务唯一键。HTTP 幂等键作用域固定为tenant_id + uid + method + route + key;相同键但请求体摘要不同返回冲突错误。 - 列表接口统一使用游标分页,不使用不稳定的大 offset。
- 错误响应使用稳定业务错误码,客户端不解析数据库错误。
- 敏感接口记录审计日志。
6.3 文件 API
继续复用现有文件上传下载入口,但必须满足:
- 文件记录带
tenant_id。 - 上传路径使用
tenants/{tenant_id}/yyyy/mm/{file_id}。 - 下载前校验文件上传者或关联消息/Topic 读取权限。
- 群文件、收藏文件、举报证据和反馈附件复用同一文件基础设施,但业务用途单独记录。
- 客户端不可见内部
location。
7. 核心领域与认证设计
7.1 已有通信模型
| 模型 | 继续复用 | 新增约束 |
|---|---|---|
| User | 用户账号和基础资料 | 租户归属、实名状态、隐私设置、青少年状态 |
| Topic | 单聊、群聊、系统 Topic、收藏 Topic | 租户归属、产品业务类型、群治理策略 |
| Subscription | 用户与 Topic 的关系和 ACL | 好友/群状态不能只靠 ACL 表达 |
| Message | 话题内顺序消息 | 消息类型、幂等 ID、审核状态、搜索索引 |
| FileDef | 上传文件元数据 | 租户归属、业务用途、访问授权、扫描状态 |
7.2 新增产品模型
| 模块 | 目标表 | 主要职责 |
|---|---|---|
| 好友关系 | im_friendships、im_friend_requests、im_user_blocks |
好友申请、同意、拒绝、删除、拉黑 |
| 用户资料 | im_users、im_user_profiles、im_user_privacy_settings |
注册手机号、昵称、邮箱、生日、签名、可见性和添加方式 |
| 群治理 | im_group_settings、im_group_join_requests、im_group_announcements、im_group_mutes、im_group_admin_logs |
入群审批、公告、禁言、管理员、转让群主 |
| 群文件 | im_group_files |
群文件目录、搜索、删除权限 |
| 收藏 | im_favorites |
收藏去重、分类、来源、搜索和分享 |
| 搜索 | im_message_index 或外部搜索索引 |
关键词、类型、附件、链接、图片、视频检索 |
| 通话 | im_call_records、im_call_participants |
通话记录、状态、时长、结束原因 |
| 组织 | im_org_units、im_org_members |
部门树、成员、岗位、数据范围 |
| 工作台 | im_workbench_apps、im_workbench_user_apps |
企业应用入口和权限 |
| 会议 | im_meetings、im_meeting_participants |
会议通知、确认状态、提醒和取消 |
| 日报月报 | im_reports、im_report_read_states |
日报/月报填写、提交、查看和统计 |
| 总经理信箱 | im_manager_mailbox |
意见提交、匿名规则、处理状态 |
| 实名认证 | im_realname_verifications |
实名信息加密、审核状态、脱敏展示 |
| 青少年模式 | im_youth_mode_settings |
启用状态、PIN 哈希、策略版本 |
| 举报反馈 | im_abuse_reports、im_feedbacks、im_evidence_files |
举报、证据、反馈和处理状态 |
| 审计 | im_audit_logs |
敏感操作追加日志 |
| 异步事件 | im_outbox_events |
系统消息、Push、搜索索引、定时任务 |
新增以下基础模型,避免各业务模块重复实现认证、会话设置和消息变更:
| 模块 | 表 | 主要职责 |
|---|---|---|
| 账号安全 | im_user_security |
auth_version、密码更新时间、登录失败和锁定状态 |
| 短信验证 | im_verification_codes |
验证码用途、目标号码哈希、有效期、错误次数和发送频控 |
| 二次验证 | im_auth_challenges |
敏感操作挑战、一次性票据哈希、绑定操作和过期时间 |
| 登录设备 | im_login_sessions |
设备会话、Token 标识、最近活动和撤销状态 |
| 会话设置 | im_conversation_settings |
置顶、免打扰、隐藏和用户侧会话状态 |
| 群成员 | im_group_members |
群成员业务状态、角色、入群来源和操作者 |
| 消息版本 | im_message_revisions |
编辑前内容、版本、操作者和变更原因 |
MySQL 表主键约定:所有 im_* 基础表必须具有独立的 id 主键。im_user_security、im_kvmeta、用户资料/隐私、设备 Push Token、好友/黑名单、青少年模式以及群设置/公告已读/禁言等原先使用自然键或复合主键的表,统一改为服务端雪花算法生成的 BIGINT id 主键;原业务键保留唯一索引,查询和幂等语义不变。服务启动时会对当前租户库执行幂等迁移,为存量行补齐雪花 ID 后再替换主键。迁移期间不得同时运行不写入 id 的旧版本服务。
用户身份字段约定:im_users.username 是非空的主登录账号,im_users.phone 是手机号方式的注册号码,允许为 NULL;是否完成短信验证由已完成的 tel credential 判断。用户填写的普通用户名必须匹配 ^[a-z0-9]{1,10}$,服务端按原始输入校验;手机号方式写入的 E.164 账号和迁移生成的内部保留账号属于系统值例外。注册页面分别展示用户名和手机号,手机号非空时优先走手机号方式并忽略用户名输入:规范化后的 E.164 手机号同时写入 username 和 phone;只有手机号为空时才使用用户名方式并只写 username。短信关闭时,手机号输入搭配有效邀请码仍按手机号方式注册并写两个字段,但不建立已完成的 tel credential。im_user_profiles.email 仅是未验证的资料字段,不参与登录和密码找回。旧 im_users.tags 与 im_usertags 已废弃,启动迁移会先从已验证凭证和旧标签回填手机号/邮箱,再删除两者。群组发现仍使用 im_topics.tags 与 im_topictags,不受本次调整影响。
注册邀请码固定为 5 位 ASCII 大小写英文字母,使用密码学安全随机数生成并区分大小写。邀请码明文存入租户业务库,code 字段使用 ascii_bin 排序规则和唯一索引;注册时按完整明文精确查询,不允许大小写转换或模糊匹配。邀请码查看和导出必须授权并审计,普通日志不得记录邀请码。
7.3 企业码与登录流程
- 未认证客户端先通过
hi.tenant提交企业码;服务端只按im_tenant.code查找启用租户并把TenantID固定到 Session。 - 企业不存在、已停用或不允许登录时返回统一企业不可用错误,不能暴露内部状态和数据库信息。
- 后续注册、登录、验证码和找回密码全部使用该服务端租户上下文;不接受客户端在业务请求中重新指定租户。
- HTTP 场景使用
/v1/im/tenants/resolve完成企业码解析,只返回公开品牌信息和服务地址。首期不设计tenant_ticket;注册、登录等未认证请求由服务端根据企业码重新解析租户,认证成功后只信任正式用户 Token。 - 用户 Token 必须包含可信
tenant_id、uid、auth_version、签发时间和过期时间。Token 租户与当前接入租户不一致时拒绝认证。
7.4 验证码、密码重置和二次验证
验证码按 register、login、reset_password、sensitive_action 用途隔离,相同验证码不能跨用途使用。数据库只保存验证码强哈希和手机号规范化哈希,明文只在发送时短暂存在。
im_users.phone 非空的账号可以通过 reset_password 短信挑战自助找回密码;成功验证即证明当前持有该号码,不要求注册时已经存在完成状态的 tel credential,并可在重置成功后补建凭证。phone=NULL 的用户名账号不提供自助找回,只能由当前租户管理员重置。管理员不得跨租户操作,重置行为必须进入安全审计。
密码重置必须在同一事务中完成:
- 消费有效验证码或二次验证挑战。
- 更新密码强哈希和密码更新时间。
- 递增
im_user_security.auth_version。 - 撤销该用户现有
im_login_sessions。 - 写安全审计和 Outbox 通知。
二次验证成功后签发一次性操作票据。票据必须绑定 tenant_id + uid + action + target + expires_at,只保存票据哈希,成功使用后立即失效。青少年模式独立密码不与登录密码共用,哈希保存在 im_youth_mode_settings。
7.5 认证安全约束
- 用户名和手机号唯一约束均包含
tenant_id。 - 验证码发送和验证按租户、手机号、IP、设备分别限频。
- 登录错误使用统一提示;服务端仍按账号、IP 和设备记录失败次数并执行指数退避或临时锁定。
- 新设备登录、密码修改、会话撤销和异常登录通过 Outbox 发送安全通知。
- 手机号、验证码、密码、票据、Token 和证件信息不得写入普通日志或消息内容。
8. 消息体系设计
8.1 消息类型
服务端需要定义稳定的消息类型,不允许各端随意约定 JSON 字段。
产品消息协议 v1 使用现有 pub,固定字段放在 head:
| 字段 | 规则 |
|---|---|
x-im-schema |
产品消息必填,首版固定为 1 |
x-im-type |
产品消息必填,值必须在下表白名单中 |
x-im-client-mid |
普通客户端消息必填,对应服务端 client_msg_id |
x-im-reply-to |
引用消息时填写同 Topic 的 SeqId |
x-im-forward-mode |
转发时为 single 或 merged |
x-im-risk-state |
仅服务端生成;客户端提交时删除并记录安全日志 |
content 只保存对应消息类型的业务内容。每种类型进入开发前必须冻结字段、长度、必填条件和兼容规则;客户端未知字段应忽略,未知消息类型显示安全占位而不能崩溃。
| 类型 | 用途 | 关键字段 |
|---|---|---|
text |
普通文本和 Unicode 表情 | text、mentions |
image |
图片消息 | file_id、width、height、thumb_file_id |
voice |
语音消息 | file_id、duration_ms、waveform |
video |
视频消息 | file_id、duration_ms、thumb_file_id |
file |
普通文件 | file_id、name、size、mime |
contact_card |
名片 | user_id、display_name_snapshot、avatar_snapshot |
call |
通话结果消息 | call_id、call_type、result、duration_sec |
system |
系统提示 | event_type、actor_id、target_ids、params |
forward_single |
单条转发 | source_message_ref、snapshot |
forward_merged |
合并转发 | summary、items、source_refs |
8.2 消息幂等
客户端发消息必须带不超过 64 字符的 client_msg_id,推荐使用 UUID/ULID。服务端在当前租户和发送人范围内保证唯一,不把 Topic 放入唯一键,避免客户端误把同一个发送请求投递到多个会话:
UNIQUE(tenant_id, from_uid, client_msg_id)
断线重试时,如果服务端发现重复 client_msg_id,返回原消息的 Topic、seq_id、状态和响应摘要,不能再次插入消息。服务端生成的系统消息也必须生成稳定幂等 ID,并以 Outbox 事件 ID 作为去重来源。
8.3 发消息前策略校验
每条消息持久化前执行统一策略链:
- Session 租户和 Topic 租户一致。
- 发送人是同租户有效用户。
- Topic 存在且未删除。
- Subscription 具备写权限。
- 单聊检查好友关系、黑名单、删除状态和隐私策略。
- 群聊检查禁言、全员禁言、群状态和成员身份。
- 文件消息检查文件归属、扫描状态和目标 Topic 权限。
- 青少年模式和反诈策略检查。
- 内容审核通过或进入待审核策略。
8.4 消息编辑、引用、撤回与删除
消息在 Topic 中的 seq_id 和原发送人不可变。产品消息增加 message_type、client_msg_id、version、moderation_state、recalled_at 和 recalled_by 等明确字段,不能依赖客户端任意 JSON 决定服务端状态。
| 操作 | 服务端规则 | 多端同步 |
|---|---|---|
| 引用 | 保存 topic + seq_id 引用和发送时的安全摘要;禁止跨租户引用 |
新消息正常广播;原消息删除后仍显示受控摘要 |
| 编辑 | 仅原发送人、允许类型、规定时限内可编辑;事务内递增 version 并写 im_message_revisions |
广播带版本号的编辑事件,旧版本事件不得覆盖新版本 |
| 全员撤回 | 校验发送人或群管理员权限及撤回时限;保留受保护审计内容,普通查询只返回撤回占位 | 广播撤回事件,收藏、搜索和引用按保留策略更新 |
| 仅自己删除 | 使用现有用户侧删除日志,不改变其他成员可见性 | 只同步当前用户的其他设备 |
| 批量删除/撤回 | 限制单次数量,逐项授权,返回逐项结果 | 每个成功项产生独立事件和审计记录 |
编辑、撤回与索引更新必须使用乐观版本控制。请求携带期望 version,版本不一致返回冲突错误,客户端重新拉取当前消息状态。
8.5 内容审核时序
发送链路先执行本地权限、限频和硬规则,再按租户及消息类型选择以下一种审核模式:
| 模式 | 处理方式 | 适用场景 |
|---|---|---|
sync_block |
在短超时内同步审核,通过后持久化和广播;超时按租户配置拒绝或降级 | 高风险文本、小请求 |
async_hold |
以 pending_review 持久化但不广播;Outbox 审核通过后发布,拒绝后标记 rejected |
文件、图片和高风险内容 |
async_post_audit |
先发布,异步审核;命中后执行系统撤回并审计 | 租户明确允许的低风险内容 |
状态转换固定为:
pending_review -> approved -> published
pending_review -> rejected
published -> recalled_by_review
客户端提交的审核状态一律忽略。审核 Worker 必须幂等,同一消息只能完成一次终态转换。
9. 好友与黑名单设计
好友关系是单聊产品状态的真源,不直接用 P2P Topic 是否存在表示好友。
9.1 好友申请与关系状态
stateDiagram-v2
[*] --> none
none --> requested: 发起申请
requested --> accepted: 通过
requested --> rejected: 拒绝
requested --> expired: 过期
accepted --> friends: 事务创建好友关系
friends --> deleted: 删除好友关系
deleted --> requested: 重新申请
rejected --> requested: 重新申请
im_friend_requests 记录申请状态,im_friendships 记录双方好友关系,im_user_blocks 记录单向拉黑。拉黑不修改好友关系状态;取消拉黑只删除当前用户的黑名单记录,不自动恢复已经删除的好友关系。
9.2 服务端规则
- 申请、同意、拒绝、删除、拉黑必须幂等。
- 好友申请保存验证留言、来源、过期时间和处理人。
- 拉黑是单向行为,唯一约束为
(tenant_id, blocker_id, blocked_id);被拉黑人发消息、通话、名片、查看资料时按策略阻断。 - 删除好友后历史消息默认保留,但双方不能继续发送好友消息。
- 好友资料字段由关系和隐私设置共同决定。
- 接受申请必须在一个事务内更新申请、创建好友关系、创建或激活 P2P Topic/Subscription,并写入 Outbox。
- 单聊策略按“双方账号有效 -> 好友关系有效 -> 任一方向黑名单 -> 隐私/青少年/风控”的顺序判定。
10. 群治理设计
现有 Group Topic 继续作为群消息容器。im_group_settings 与 im_group_members 是群业务状态真源;Topic、Subscription 和 ACL 是通信内核投影。所有成员和角色变化必须通过产品业务接口完成,并在同一 MySQL 事务内同步业务真源与通信投影。
10.1 群角色
| 角色 | 业务真源 | 通信投影 | 权限 |
|---|---|---|---|
| 群主 | im_group_settings.owner_user_id + im_group_members.role=owner |
Topic Owner + Owner ACL | 全部群管理、转让、解散 |
| 管理员 | im_group_members.role=admin |
Admin ACL | 审批、公告、禁言、成员管理 |
| 普通成员 | im_group_members.role=member |
有效 Subscription | 收发消息、查看公告和群文件 |
| 游客/待审批 | im_group_join_requests |
无 Subscription | 不具备群消息读写权限 |
禁止客户端直接修改群 Topic Owner 或管理员 ACL。服务启动或定时一致性任务发现业务真源与通信投影不一致时,记录告警并以业务真源修复投影。
10.2 群能力
| 能力 | 设计 |
|---|---|
| 创建群 | 同一事务创建 Topic、群设置、群主成员记录、Subscription、ACL、审计和 Outbox |
| 入群邀请 | 写邀请/申请记录,审批通过后在同一事务写成员记录和 Subscription |
| 入群验证 | 群设置控制是否需要审批 |
| 群公告 | 公告表保存版本,群内发送系统消息 |
| 群二维码 | 签名短 Token,包含群 ID、租户、过期时间和用途 |
| 管理员 | 更新 im_group_members.role,同一事务同步 ACL |
| 禁言 | 全员禁言和成员禁言都在发消息前强制检查 |
| 群文件 | 文件消息成功后进入群文件索引,按群权限查询 |
| 转让群主 | 二次确认;同一事务更新 owner、双方角色、Topic Owner/ACL、审计和 Outbox |
| 解散群聊 | 标记群状态、踢出成员、停用二维码和审批入口 |
11. 收藏、转发与搜索设计
11.1 收藏
收藏使用独立 im_favorites 表作为查询和去重真源,不仅依赖 slf Topic。
收藏记录需要保存:
- 租户、收藏人、来源 Topic、来源 SeqId、来源消息 ID。
- 消息类型、摘要、文件 ID、发送人、发送时间。
- 收藏时间、去重键和删除状态。
- 原消息删除后的可见策略。
11.2 转发
逐条转发和合并转发都必须重新执行目标会话权限和文件权限。
| 转发类型 | 规则 |
|---|---|
| 逐条转发 | 每条源消息生成一条新消息;保持顺序;每条使用新 client_msg_id |
| 合并转发 | 生成不可变快照;限制条数、总大小和敏感字段;支持点击查看详情 |
| 文件转发 | 源文件和目标 Topic 必须同租户;必要时创建新的引用关系 |
11.3 搜索
搜索分三类:
- 用户和群搜索:使用租户内规范化 Tags,并执行隐私策略。
- 聊天记录搜索:
im_message_index保存tenant_id、Topic、SeqId、消息类型、发送人、时间、规范化检索文本和资源元数据,返回前二次校验 ACL。 - 文件/图片/视频/链接搜索:依赖消息类型索引和文件元数据索引。
首期固定使用 MySQL 8:结构化条件使用 B-Tree 复合索引,消息正文使用独立 im_message_index.search_text 和适合中文的 FULLTEXT/ngram 索引。分页游标使用稳定的 created_at + message_id,不能使用大 Offset。
搜索索引不是授权来源。查询必须先限定 tenant_id 和当前用户可读 Topic 集合,返回前再次校验 Topic Read ACL、成员状态、用户侧删除、全员撤回和审核状态。退群、拉黑或权限变更后必须立即阻断查询,即使异步索引尚未删除。
出现以下任一情况时重新评估 OpenSearch/Elasticsearch:单租户可检索消息达到千万级、MySQL 搜索 P95 超过 500ms、需要复杂中文相关性/高亮,或索引写入明显影响消息主库。切换时保持 HTTP 搜索契约不变。
12. 通话设计
现有 P2P WebRTC 信令继续复用,新增业务通话记录。
12.1 通话状态
stateDiagram-v2
[*] --> ringing: 发起呼叫
ringing --> accepted: 接听
ringing --> canceled: 发起方取消
ringing --> rejected: 接收方拒绝
ringing --> timeout: 超时
ringing --> busy: 对方忙线
accepted --> ended: 正常挂断
accepted --> interrupted: 异常断开
12.2 关键字段
im_call_records 至少包含:
tenant_id, call_id, topic, caller_id, callee_id, call_type,
state, start_at, accepted_at, ended_at, duration_sec,
end_reason, created_at, updated_at
通话结束后生成 call 类型消息,双方看到的文案由统一结果码生成。
13. 企业工作台设计
工作台是独立业务域,不能依赖聊天 Topic 保存主数据。
| 模块 | 入口 | 通知方式 |
|---|---|---|
| 组织架构 | /v1/im/org/* |
组织变更一般不发聊天消息,必要时发系统通知 |
| 会议通知 | /v1/im/meetings/* |
创建、变更、取消和提醒通过系统消息 + Push |
| 日报/月报 | /v1/im/reports/* |
未提交提醒、提交通知和管理员提醒通过 Outbox |
| 总经理信箱 | /v1/im/manager-mailbox/* |
提交成功给本人通知;处理结果按匿名规则通知 |
| 工作台应用 | /v1/im/workbench/apps |
返回租户配置和用户权限范围内的入口 |
组织数据权限用于日报、月报、会议统计和通讯录查询。管理员只能查看权限范围内成员数据。
14. 个人资料、隐私与青少年模式
14.1 个人资料
基础展示资料可以同步到 User Public,但可索引、可审计或敏感字段应进入独立表。
| 字段 | 存储建议 |
|---|---|
| 头像、昵称、签名 | User Public + im_user_profiles |
| 生日、地区、性别 | im_user_profiles,按隐私设置返回 |
| 注册手机号 | im_users.phone,仅账号安全流程内部使用 |
| 邮箱 | im_user_profiles.email,仅作为个人资料,不作为认证凭证 |
| 实名姓名、证件号 | 实名表加密存储,不进入 Public |
| 群昵称 | im_group_members,按需投影到 Subscription Private |
14.2 隐私设置
隐私设置影响以下入口:
- 账号/手机号/二维码/群聊添加好友。
- 个人主页字段展示。
- 在线状态和最后在线时间展示。
- 非好友消息和通话发起。
客户端可以隐藏入口,但最终阻断必须在服务端执行。
14.3 青少年模式
青少年模式状态保存在服务端,PIN 使用强哈希,不保存明文。
受限能力由租户策略决定,首期建议覆盖:
- 添加好友。
- 加群和创建群。
- 文件传输。
- 音视频通话。
- 工作台外部链接。
14.4 会话与通知设置
im_conversation_settings 以 (tenant_id, user_id, topic) 唯一,保存置顶、免打扰、隐藏等用户侧设置。它不改变其他成员的会话状态,也不能替代 Topic ACL。
Push 发送前同时检查租户通知策略、用户全局通知设置、会话免打扰、青少年模式和系统权限登记状态。客户端系统通知权限只用于展示引导,服务端保存的是用户偏好和最近一次上报状态,不把客户端上报当作可靠授权依据。
15. 安全、实名、举报与反诈
15.1 实名认证
实名认证采用独立状态机:
none -> pending -> approved
none -> pending -> rejected
approved -> appeal_pending -> approved/rejected
证件号、姓名等敏感字段加密存储,普通查询只返回脱敏状态。
15.2 反诈策略
反诈由“本地规则 + 外部风控插件”共同组成:
- 本地规则用于稳定阻断和提示,例如陌生人、敏感词、频繁加好友、异常文件。
- 外部插件用于内容审核、风险评分和策略建议。
- 插件调用遵守第 8.5 节的三种审核模式、超时和降级规则,不能在 Topic Actor 中执行无界远程等待。
- 风险提示和二次确认结果写审计。
15.3 举报与反馈
举报和反馈都按工单处理。
| 类型 | 特点 |
|---|---|
| 举报 | 绑定被举报对象、会话、消息、原因、证据和处理状态 |
| 意见反馈 | 绑定提交人、设备、版本、问题描述、附件和处理状态 |
举报证据应固化消息快照和文件引用,防止原消息删除后无法处理。
16. 系统消息与通知设计
需要产生异步副作用的业务状态变化,在同一数据库事务中写入 Outbox,由 Worker 生成系统消息、Push、索引或定时任务。没有异步副作用的更新不要求创建空事件。
16.1 系统消息格式
系统消息固定使用以下结构;业务模块只能登记 event_type 和参数 Schema,不能自行定义另一套消息信封:
{
"type": "system",
"event_type": "group_join_approved",
"template_version": 1,
"actor_id": "usr...",
"target_ids": ["usr..."],
"params": {
"group_id": "grp...",
"group_name": "项目群"
},
"dedupe_key": "tenant:1:group:grpxxx:join:request123"
}
16.2 事件类型
首期至少定义:
| 业务 | 事件 |
|---|---|
| 账号安全 | 新设备登录、密码修改、实名提醒 |
| 好友 | 新申请、通过、拒绝、删除 |
| 群 | 邀请、申请、审批、公告、禁言、管理员变更、群主转让、解散 |
| 文件 | 群文件上传、文件失效 |
| 通话 | 未接、拒绝、取消、忙线、通话结束 |
| 工作台 | 会议通知、会议变更、日报提醒、月报提醒、信箱处理 |
| 安全 | 反诈提示、举报受理、反馈受理 |
17. 权限模型
17.1 角色层级
| 层级 | 角色 |
|---|---|
| 平台 | platform_admin |
| 租户 | tenant_owner、tenant_admin |
| 组织 | 部门负责人、报表管理员 |
| 群 | 群主、管理员、成员 |
| 关系 | 好友、非好友、黑名单 |
| 账号 | 普通用户、实名用户、青少年模式用户 |
17.2 权限判定顺序
租户状态 -> 用户状态 -> 业务角色 -> Topic ACL -> 产品策略 -> 风控策略
任何一步失败,写操作不得继续。
18. 数据一致性与异步任务
18.1 事务边界
必须在同一事务中完成:
- 好友申请处理和关系状态更新。
- 群审批通过和 Subscription 写入。
- 群主转让和权限更新。
- 消息保存和附件引用建立。
- 文件记录从
uploaded切换为ready、附件引用和业务索引写入。 - 举报提交和证据引用。
业务层不能在 Handler 中连续调用多个独立 Store 方法来模拟事务。imstore 必须提供按用例划分的事务方法,例如 AcceptFriendRequest、ApproveGroupJoin、TransferGroupOwner 和 SaveMessageWithAttachments,方法内部同时更新产品业务表、Tinode 核心表、审计记录和 Outbox。
对象存储不参与 MySQL 事务,文件采用以下状态机:
pending -> uploaded -> ready
pending/uploaded -> failed
uploaded(超时且无引用) -> orphaned -> deleted
对象上传成功后,再在 MySQL 事务中把文件切换为 ready 并建立业务引用。若数据库事务失败,文件保持 uploaded,由带宽受控的租户级垃圾回收任务清理;消息只能引用 ready 文件。
18.2 Outbox
需要通知、Push、索引或审计的业务写操作,在事务内写入 im_outbox_events。异步 Worker 负责:
- 发送系统消息。
- 推送离线通知。
- 更新搜索索引。
- 调用外部审核/反诈。
- 发送会议、日报、月报定时提醒。
- 记录可观测指标。
im_outbox_events 至少包含:
id, tenant_id, aggregate_type, aggregate_id, event_type,
payload, dedupe_key, state, available_at, attempts,
locked_by, locked_at, last_error, created_at, processed_at
Outbox 处理规则:
dedupe_key在租户内唯一,生产事件和消费副作用都必须幂等。- Worker 使用
SELECT ... FOR UPDATE SKIP LOCKED分批领取事件,设置最大批量和租户公平调度,避免单个租户占满队列。 - 失败采用有上限的指数退避;超过上限进入
dead_letter,触发告警并支持人工重放。 - 系统消息通过集群安全的内部发布接口进入目标 Topic,不能直接假设 Outbox Worker 与目标 Topic 位于同一进程。
- 标记事件成功前必须确认目标副作用已获得稳定幂等结果;进程崩溃后的重复执行不得产生重复消息或 Push。
- 外部审核事件必须按第 8.5 节推进消息审核状态,不能绕过状态机直接广播。
19. API 与错误码规范
19.1 成功响应
{
"code": 0,
"data": {},
"request_id": "..."
}
19.2 错误响应
{
"code": 30001,
"message": "permission denied",
"reason": "GROUP_MUTED",
"request_id": "..."
}
错误码按模块分段:
| 范围 | 模块 |
|---|---|
| 10000-19999 | 通用、认证、租户 |
| 20000-29999 | 好友、黑名单、隐私 |
| 30000-39999 | 群、公告、群文件 |
| 40000-49999 | 消息、收藏、搜索、转发 |
| 50000-59999 | 文件、媒体、通话 |
| 60000-69999 | 组织、会议、日报、月报 |
| 70000-79999 | 实名、青少年、安全、举报、反馈 |
HTTP 状态码表达协议层结果,业务 code 表达稳定失败原因:参数错误使用 400,未认证使用 401,无权限使用 403,不可见资源使用 404,版本或幂等冲突使用 409,限频使用 429,服务端故障使用 5xx。客户端不得只判断 HTTP 状态而忽略业务 code。
20. 可观测性与审计
20.1 日志字段
认证后的请求日志至少包含:
request_id, tenant_id, uid, sid, topic, action, result, latency_ms
手机号、证件号、Token、验证码、文件签名和设备 Push Token 不写明文日志。
20.2 审计事件
必须审计:
- 租户、组织和管理员变更。
- 登录、密码修改、实名审核和二次验证。
- 好友删除、拉黑、黑名单解除。
- 群主转让、管理员变更、禁言、解散群。
- 文件下载、批量转发、收藏导出。
- 举报处理、反馈处理和反诈二次确认。
- 平台管理员代操作。
21. 开发阶段规划
阶段 0:基础底座收口
目标:让后续业务都站在正确地基上。
交付:
- 多租户核心链路完成并通过双租户隔离测试。
- 消息
client_msg_id幂等。 - 文件下载资源级授权。
- 系统消息结构和 Outbox 基础。
- 统一错误码和请求日志。
- 产品版明确只支持 MySQL 8,不支持的 Adapter 在启动时快速失败。
im_tenant_config三项配额在注册、建群和入群入口强制生效。- 建立专用业务事务 Store,禁止跨多个 Store 调用拼接关键事务。
阶段 1:账号、资料、好友和黑名单
交付:
- 注册、登录、找回密码、二次验证。
auth_version、登录设备、Token 撤销和安全通知。- 个人资料、会话设置和隐私设置。
- 好友申请、通讯录、主页、删除好友。
- 独立黑名单和单聊发送/通话策略。
阶段 2:群聊产品化
交付:
- 群列表、创建群、群资料、群头像。
- 入群审批、邀请、群二维码。
- 群公告、管理员、群主转让。
- 全员禁言、成员禁言、群文件。
- 群业务真源与 Topic/Subscription/ACL 投影一致性检查。
阶段 3:消息增强、收藏、搜索和转发
交付:
- 结构化消息类型。
- @成员、引用、编辑、撤回、批量删除及消息版本事件。
- 收藏分类和分享。
- 聊天记录、文件、图片、视频和链接搜索。
- 逐条转发和合并转发。
sync_block、async_hold和async_post_audit审核状态机。
阶段 4:通话和通知体验
交付:
call_id和通话记录。- 通话状态消息。
- 忙线、拒绝、未接、超时和异常断开。
- Push 文案、免打扰、锁屏隐私。
阶段 5:企业工作台
交付:
- 工作台应用配置。
- 组织架构。
- 会议通知和参会确认。
- 日报、月报。
- 总经理信箱。
阶段 6:安全合规
交付:
- 实名认证。
- 青少年模式。
- 反诈策略。
- 举报和意见反馈。
- 审计后台和处置流程。
22. 测试策略
22.1 自动化测试
每个模块至少包含:
- 单元测试:状态机、权限判断、错误码。
- Store 测试:租户隔离、唯一约束、分页、软删除。
- API 测试:参数校验、幂等、越权、并发。
- WebSocket 测试:消息顺序、回执、离线重连、系统消息。
- 安全测试:跨租户、跨用户、跨群、文件越权和枚举。
- 事务测试:在每个数据库写入点注入失败,验证业务真源、通信投影、审计和 Outbox 不出现部分提交。
- Worker 测试:重复消费、锁超时、进程崩溃、指数退避、死信和人工重放。
- Schema 测试:MySQL 初始化 DDL、升级 DDL 和 Adapter SQL 保持一致,唯一约束与外键真实生效。
- 性能测试:大群广播、并发发消息、重连风暴、文件上传下载、中文搜索和 Outbox 积压恢复。
性能测试不能只记录平均值。每次发布前根据部署规模给出 P95/P99 延迟、错误率、吞吐、数据库连接和队列积压报告;未确定正式 SLO 前,以当前稳定版本基线为回归门槛。
22.2 重点验收场景
| 场景 | 预期 |
|---|---|
| 两个租户使用相同手机号注册 | 均成功,互不影响 |
| A 租户尝试搜索 B 租户用户 | 返回空或通用 NotFound |
| 被拉黑用户发送消息 | 不进入对方会话,返回稳定失败原因 |
| 禁言成员发群消息 | 服务端拒绝,其他成员不可见 |
| 文件 URL 被其他用户重放 | 无权限返回 403/404 |
| 断线重试同一消息 | 不重复入库,返回原消息状态 |
同一发送人把相同 client_msg_id 投递到不同 Topic |
第二次返回首次投递结果,不写入另一 Topic |
并发注册超过 max_users |
成功数不超过配额,失败请求返回同一超限错误 |
并发审批入群超过 max_group_members |
最终有效成员数不超过配额 |
| 群主转让并发提交 | 最终只有一个成功,审计完整 |
| 群主转让在任一写入点失败 | owner、成员角色和 Topic ACL 全部回滚 |
async_hold 消息等待审核 |
审核前其他成员不可见,通过后只广播一次 |
| Outbox 发送成功但标记前进程崩溃 | 重启重放不产生重复系统消息或 Push |
| 用户退群后搜索旧群消息 | 即使索引尚在也不返回结果 |
| 举报消息后原消息被撤回 | 举报证据仍可处理 |
23. 开发约束
- 新业务表一律以
im_前缀命名。 - 所有租户业务表必须带
tenant_id;平台级表和im_tenant等根表例外,例外必须经过 DDL 评审。 - 新接口不使用无租户 Store。
- 不在 Topic Actor 中执行慢查询、外部 HTTP 或复杂报表统计。
- 不把敏感字段放入 User Public、Topic Public、普通消息 JSON 或日志。
- 新增消息类型必须先定义 schema,再实现客户端渲染。
- 所有危险操作必须有二次确认、幂等和审计。
- 所有批量操作必须限制数量并返回逐项结果。
- 所有列表必须分页。
- 所有外部服务调用必须有超时、重试上限和降级策略。
- 关键业务状态变化必须通过专用事务 Store 完成,不允许 Handler 直接修改 Topic ACL 绕过业务真源。
- 产品模块只允许在
TenantBusinessReady()为真且数据库版本满足要求时启动。
24. 待产品确认事项
以下问题会影响接口和表结构,开发前需要产品或业务方确认:
对应阶段冻结接口和 DDL 前必须确认本节事项。在产品未给出结论时,可以按本文其他章节声明的默认行为开发内部能力,但不得将未确认交互作为正式验收口径。
- 删除好友后是否同时删除双方会话,还是仅禁止继续发送。
- 拉黑后是否允许被拉黑人看到历史消息和个人资料。
- 群聊已读回执是否展示具体已读成员,是否只对管理员开放。
- 消息撤回时限和管理员撤回范围。
- 合并转发是否允许包含已撤回、已删除或涉敏消息。
- 群文件是否等同于群内所有文件消息,还是独立上传目录。
- 实名认证是否强制,未实名能否继续登录、聊天、加好友、入群。
- 青少年模式限制范围和默认策略。
- 总经理信箱是否匿名,以及哪些角色可以查看真实提交人。
- 日报/月报是否支持草稿、补交、撤回和修改。
25. 总结
后续开发应以现有 Tinode 服务端作为实时通信内核,在其外建设清晰的产品业务层。核心原则是:聊天实时链路保持轻,业务状态机走 HTTP 和数据库事务,状态变化通过系统消息和 Push 回到聊天体验中。
多租户、消息幂等、文件授权、系统消息和审计是所有功能之前的基础工程。好友、群治理、收藏搜索、通话、工作台和安全合规应按阶段推进,每个阶段都必须同时完成数据模型、接口、权限、通知和测试闭环。