chore: seed migration objects batch 1
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
# IM 后台未实现功能清单
|
||||
|
||||
> 编写日期:2026-08-16
|
||||
> 目的:记录当前 HotGo IM 后台已经完成的能力、尚未实现的后台功能,以及后续应优先通过 `im_server` 管理 API 完成的写操作。
|
||||
|
||||
## 1. 当前已实现
|
||||
|
||||
### 1.1 平台控制库
|
||||
|
||||
已实现:
|
||||
|
||||
| 模块 | 当前能力 |
|
||||
| --- | --- |
|
||||
| IM 租户管理 | 租户列表、详情、新增/编辑、状态切换 |
|
||||
| IM 租户配额配置 | `im_tenant_config` 随租户编辑维护 |
|
||||
| 租户业务库路由 | 后台按 `im_tenant.db_dsn` 或 `db_name` 动态连接租户库 |
|
||||
| 后台菜单 | 已导入 IM 管理根菜单及各模块权限点 |
|
||||
|
||||
未完成:
|
||||
|
||||
| 功能 | 说明 |
|
||||
| --- | --- |
|
||||
| 租户 provisioning 流程 | 创建租户业务库、执行 schema、初始化系统账号和默认 Topic 尚未实现 |
|
||||
| 租户删除流程 | 删除前检查、异步归档、业务库删除/保留策略未实现 |
|
||||
| 平台级审计写入 | 租户创建、停用、恢复、删除等平台操作还未写平台审计 |
|
||||
| 租户用量统计 | 用户数、群数、文件容量、消息量统计接口未实现 |
|
||||
|
||||
### 1.2 租户业务库只读后台
|
||||
|
||||
已实现:
|
||||
|
||||
| 模块 | 当前能力 |
|
||||
| --- | --- |
|
||||
| IM 用户管理 | 按租户查看用户列表、用户详情 |
|
||||
| IM 群组管理 | 群列表、群详情、群成员列表 |
|
||||
| 群治理只读视图 | 查看 `im_group_settings`、入群申请、群邀请、群公告、禁言记录、群管理日志 |
|
||||
| 实名审核 | 实名提交列表、详情、通过、拒绝 |
|
||||
| 举报反馈工单 | 举报/反馈列表、详情、状态更新 |
|
||||
| 租户审计日志 | `im_audit_logs` 列表、详情,只读 |
|
||||
|
||||
注意:
|
||||
|
||||
1. 群成员列表优先读取 `im_group_members`,如果租户库尚未回填业务真源,会回退读取 `im_subscriptions`。
|
||||
2. 群治理写操作暂未在 HotGo 后台直接实现,避免绕过 `im_server` 的 Topic、Subscription、ACL、审计和 Outbox 状态机。
|
||||
3. 举报反馈当前只更新工单状态,尚未实现“警告、禁言、封禁、撤回消息、通知用户”等处置动作。
|
||||
|
||||
## 2. P0 未实现
|
||||
|
||||
### 2.1 后台认证、租户 Principal 与数据权限
|
||||
|
||||
未实现:
|
||||
|
||||
| 功能 | 说明 |
|
||||
| --- | --- |
|
||||
| 租户级 Principal | 后台 Token 中可信 `tenant_id`、角色、权限版本尚未完整建模 |
|
||||
| 企业后台权限边界 | 普通租户管理员只能访问本租户数据的服务端强校验还未系统化 |
|
||||
| 跨租户操作二次验证 | 平台管理员切换租户、导出、代操作缺少二次确认 |
|
||||
| 数据范围权限 | 部门级、群级、安全审计员的数据范围过滤未实现 |
|
||||
|
||||
### 2.2 审计写入
|
||||
|
||||
未实现:
|
||||
|
||||
| 功能 | 说明 |
|
||||
| --- | --- |
|
||||
| 平台级审计表 | 控制库中的平台审计表和查询页面尚未实现 |
|
||||
| 租户级审计写入 | 用户、群、实名、举报、反馈、文件等敏感操作尚未统一写 `im_audit_logs` |
|
||||
| 审计导出 | 审计导出、导出审计、脱敏策略未实现 |
|
||||
| 审计不可篡改策略 | 追加写、防删除、防修改、保留周期尚未实现 |
|
||||
|
||||
### 2.3 Outbox 与后台任务
|
||||
|
||||
未实现:
|
||||
|
||||
| 功能 | 说明 |
|
||||
| --- | --- |
|
||||
| 租户级 Outbox 查询 | `im_outbox_events`、`im_group_events_outbox` 后台查看未实现 |
|
||||
| Outbox 重试/死信 | 人工重放、死信处理、幂等检查未实现 |
|
||||
| 任务状态监控 | 文件 GC、索引重建、Push、外部审核任务状态未实现 |
|
||||
|
||||
## 3. P1 未实现
|
||||
|
||||
### 3.1 群治理写操作
|
||||
|
||||
这些操作必须优先通过 `im_server` 管理 API 实现,HotGo 后台只调用 API,不直接改业务表:
|
||||
|
||||
| 功能 | 涉及表 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 修改群资料 | `im_group_settings`、`im_topics` | 群名、头像、简介,需同步通信投影 |
|
||||
| 修改治理配置 | `im_group_settings` | 入群审批、全员禁言、上传文件、成员私聊、成员邀请 |
|
||||
| 入群审批 | `im_group_join_requests`、`im_group_members`、`im_subscriptions` | 同意/拒绝,需版本号、幂等、配额校验 |
|
||||
| 邀请处理 | `im_group_invitations`、`im_group_join_requests` | 接受邀请后可能直接入群或进入审批 |
|
||||
| 群公告发布/更新/删除 | `im_group_announcements`、`im_group_announcement_reads` | 需要乐观锁、系统消息、Outbox |
|
||||
| 成员禁言/解除 | `im_group_mutes` | 需写管理日志、审计和群系统事件 |
|
||||
| 管理员管理 | `im_group_members`、`im_subscriptions` | 添加/移除管理员并同步 ACL |
|
||||
| 群主转让 | `im_group_settings`、`im_group_members`、`im_topics` | 二次确认,同步 Owner 和 ACL |
|
||||
| 解散群 | `im_group_settings`、`im_topics`、邀请、二维码、文件入口 | 需撤销入口、写审计、发系统事件 |
|
||||
| 群二维码 | `im_group_qrcodes` | 生成、撤销、过期、扫码解析管理 |
|
||||
|
||||
当前 HotGo 后台只读查看这些表,不做上述写操作。
|
||||
|
||||
### 3.2 文件与存储治理
|
||||
|
||||
未实现:
|
||||
|
||||
| 功能 | 说明 |
|
||||
| --- | --- |
|
||||
| 文件列表 | 按租户、用途、状态、MIME、上传人查询 |
|
||||
| 文件详情 | 文件元数据、引用消息、引用群、ACL、扫描状态 |
|
||||
| 文件下载审计 | 后台下载必须写审计 |
|
||||
| 隔离/删除文件 | 必须通过 `im_server` API 处理引用、消息、收藏、举报证据 |
|
||||
| 文件策略 | 单文件大小、总容量、允许 MIME、保留期、病毒扫描策略 |
|
||||
| 群文件治理 | `im_group_files` 列表、删除、下载权限未实现 |
|
||||
|
||||
### 3.3 组织架构与企业通讯录
|
||||
|
||||
未实现:
|
||||
|
||||
| 功能 | 说明 |
|
||||
| --- | --- |
|
||||
| 部门管理 | 部门树、移动部门、防环、删除校验 |
|
||||
| 成员归属 | 用户与部门、岗位、负责人关系 |
|
||||
| 组织数据权限 | 按部门/下级部门限制日报、会议、通讯录可见范围 |
|
||||
|
||||
### 3.4 用户与账号安全写操作
|
||||
|
||||
未实现:
|
||||
|
||||
| 功能 | 说明 |
|
||||
| --- | --- |
|
||||
| 停用/恢复用户 | 需同步登录、Token、WS、审计 |
|
||||
| 重置密码/安全项 | 必须二次确认并写审计 |
|
||||
| 设备管理 | 查看、踢下线、撤销设备 Token |
|
||||
| 用户导出 | 需要导出审计和字段脱敏 |
|
||||
| 黑名单/好友关系后台 | 查询、处置、修复关系未实现 |
|
||||
|
||||
## 4. P2 未实现
|
||||
|
||||
### 4.1 反诈与内容审核
|
||||
|
||||
未实现:
|
||||
|
||||
| 功能 | 说明 |
|
||||
| --- | --- |
|
||||
| 风控策略配置 | 高风险动作、提示文案、二次确认、阻断策略 |
|
||||
| 内容审核策略 | `sync_block`、`async_hold`、`async_post_audit` 配置 |
|
||||
| 审核插件配置 | 平台统一审核服务、租户专属审核服务 |
|
||||
| 风险事件列表 | 确认、取消、过期、阻断记录 |
|
||||
| 审核任务列表 | 待审核消息/文件/图片/视频任务 |
|
||||
| 审核通过/拒绝 | 必须通过 `im_server` 推进状态机 |
|
||||
| 审核撤回消息 | 已发布违规消息系统撤回、审计、Outbox 未实现 |
|
||||
|
||||
### 4.2 举报反馈完整处置
|
||||
|
||||
当前已实现列表、详情、状态更新。未实现:
|
||||
|
||||
| 功能 | 说明 |
|
||||
| --- | --- |
|
||||
| 举报原因配置 | 按用户、群、消息目标类型配置原因 |
|
||||
| 工单分派 | 分派给处理人或安全团队 |
|
||||
| 工单回复 | 处理结果回复举报人/反馈人 |
|
||||
| 标记重复 | 合并重复举报或反馈 |
|
||||
| 处置动作 | 警告、禁言、封禁用户、解散群、撤回消息 |
|
||||
| 结果通知 | 通过系统通知或客户端查询展示处理结果 |
|
||||
| 证据文件管理 | `im_evidence_files` 或附件引用的 ACL、保留期、下载审计 |
|
||||
|
||||
### 4.3 实名审核完善
|
||||
|
||||
当前已实现列表、详情、通过、拒绝。未实现:
|
||||
|
||||
| 功能 | 说明 |
|
||||
| --- | --- |
|
||||
| 实名策略配置 | 是否强制实名、允许跳过、提醒周期、重复身份策略 |
|
||||
| 要求补充材料 | 当前只有通过/拒绝,没有补充材料状态 |
|
||||
| 敏感原文查看流程 | 需要权限、二次验证、审计,普通管理员不可查看 |
|
||||
| 审核结果通知 | 审核后通过系统事件通知用户 |
|
||||
| 审核操作审计 | 当前未统一写租户审计 |
|
||||
|
||||
### 4.4 会议、日报、月报
|
||||
|
||||
未实现:
|
||||
|
||||
| 功能 | 说明 |
|
||||
| --- | --- |
|
||||
| 会议管理 | 会议列表、详情、参与人响应、提醒 |
|
||||
| 日报/月报列表 | 按部门、成员、状态、时间查询 |
|
||||
| 统计 | 日报/月报提交率、阅读率、部门统计 |
|
||||
| 提醒 | 未提交、未读提醒,需限流和逐项结果 |
|
||||
|
||||
### 4.5 总经理信箱
|
||||
|
||||
未实现:
|
||||
|
||||
| 功能 | 说明 |
|
||||
| --- | --- |
|
||||
| 信箱配置 | 是否启用、匿名、必填字段、附件策略、提示文案 |
|
||||
| 提交列表/详情 | 匿名模式下处理人不可见提交人 |
|
||||
| 分派/回复/关闭 | 需要审计和通知 |
|
||||
|
||||
### 4.6 工作台应用配置
|
||||
|
||||
未实现:
|
||||
|
||||
| 功能 | 说明 |
|
||||
| --- | --- |
|
||||
| 应用模板 | 平台应用模板、默认权限、分类 |
|
||||
| 租户应用配置 | 启用/停用、排序、权限、外链安全 |
|
||||
| 外部链接审计 | 工作台外链访问和安全策略 |
|
||||
|
||||
### 4.7 Push、短信、TURN、对象存储配置
|
||||
|
||||
未实现:
|
||||
|
||||
| 功能 | 说明 |
|
||||
| --- | --- |
|
||||
| Push 配置 | 租户级 Push 通道和测试 |
|
||||
| 短信配置 | 租户短信模板、签名、发送日志 |
|
||||
| TURN 配置 | 音视频 TURN 服务配置和连通性检查 |
|
||||
| 对象存储配置 | 租户桶、路径、回源、签名、上传策略 |
|
||||
| 外部服务变更审计 | 修改配置必须二次确认和审计 |
|
||||
|
||||
## 5. 后续开发建议顺序
|
||||
|
||||
建议按以下顺序继续:
|
||||
|
||||
1. **审计写入基础设施**:先实现统一 `im_audit_logs` 写入工具和平台审计表。
|
||||
2. **im_server 管理 API 对接层**:HotGo 后台新增统一 client,所有群治理、文件治理、用户处置写操作都走它。
|
||||
3. **群治理写操作**:入群审批、治理配置、公告、禁言、管理员、群主转让、解散。
|
||||
4. **文件治理**:文件列表、群文件、下载审计、隔离/删除、文件策略。
|
||||
5. **Outbox 与任务后台**:事件查看、重试、死信、任务监控。
|
||||
6. **内容审核与风险处置**:审核任务、风险事件、违规消息撤回。
|
||||
7. **组织/会议/日报/月报/工作台**:企业协同类功能。
|
||||
|
||||
## 6. 开发边界提醒
|
||||
|
||||
1. 租户业务库是“一个租户一个库”,业务表不带 `tenant_id`。
|
||||
2. HotGo 后台直连租户库适合只读查询、诊断和报表。
|
||||
3. 会影响用户、群、消息、文件、`im_topics`、`im_subscriptions`、ACL、Outbox 的写操作,应优先通过 `im_server` 管理 API。
|
||||
4. 敏感操作必须具备:权限校验、二次确认、幂等键或版本号、审计、Outbox 通知。
|
||||
5. 后台前端隐藏按钮只是体验优化,后端接口仍必须做权限和租户边界校验。
|
||||
@@ -0,0 +1,883 @@
|
||||
# IM 后台需求文档
|
||||
|
||||
> 编写日期:2026-08-12
|
||||
> 依据文档:`IM-产品需求服务端改造分析报告.md`、`IM-独立库多租户改造方案.md`、`IM-多租户第5至12章代码改造与测试方案.md`、`IM-前后端对接文档.md`、`IM-系统设计文档.md`
|
||||
> 目标读者:后台产品、后端、管理端前端、测试
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
本文从现有 IM 产品与服务端设计文档中提取“后台管理端”需求,用于后续后台系统开发。本文只描述后台能力,不重复普通移动端聊天、通讯录、消息收发等客户端功能。
|
||||
|
||||
后台系统分为两类:
|
||||
|
||||
| 后台 | 使用对象 | 数据范围 |
|
||||
| --- | --- | --- |
|
||||
| 平台运营后台 | 平台管理员、运维、安全负责人 | 可管理全部租户,但跨租户操作必须二次验证和审计 |
|
||||
| 企业管理后台 | 租户负责人、企业管理员、安全审计员、组织管理员 | 只能管理当前租户内的数据 |
|
||||
|
||||
所有后台能力必须建立在多租户隔离之上:当前多租户采用“控制库 + 每租户独立业务库”方案。后台 Token 必须包含可信 `tenant_id`、`uid`、角色、权限版本;其中 `tenant_id` 只作为运行时租户上下文和选库依据,不要求租户业务库内业务表保存 `tenant_id` 字段。平台后台的跨租户操作必须走独立后台 API,禁止通过普通 WebSocket `AsUser` 任意代操作用户。
|
||||
|
||||
后台系统与 `im_server` 的职责边界如下:
|
||||
|
||||
1. HotGo 后台负责管理平台控制库、后台页面、菜单权限、平台运营流程和必要的数据展示。
|
||||
2. 租户内用户、群、文件、实名、举报、反馈、Outbox 等业务动作优先通过 `im_server` 管理 API 执行,避免绕过 `im_server` 的状态机、审计、Outbox、权限校验和租户选库逻辑。
|
||||
3. HotGo 后台如需直连租户业务库,只能用于只读报表、诊断或经过评审的维护任务;写操作必须明确说明为什么不能调用 `im_server` API。
|
||||
|
||||
## 2. 后台角色与权限
|
||||
|
||||
### 2.1 角色
|
||||
|
||||
| 角色 | 作用域 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `platform_admin` | 平台级 | 租户生命周期、平台配置、运维诊断、跨租户审计 |
|
||||
| `tenant_owner` | 租户级 | 企业最高管理员,拥有本租户全部管理权限 |
|
||||
| `tenant_admin` | 租户级 | 组织、用户、群、安全策略和工作台配置 |
|
||||
| `security_auditor` | 租户级 | 实名、举报、反馈、审计读取和安全处置 |
|
||||
| `org_admin` | 租户级或部门级 | 部门、成员、岗位、组织数据范围 |
|
||||
| `report_admin` | 部门级 | 日报、月报、会议统计与提醒 |
|
||||
| `group_admin` | 群级 | 群成员、公告、禁言、群文件等群治理 |
|
||||
|
||||
### 2.2 权限原则
|
||||
|
||||
1. 平台管理员可以选择租户上下文进行运维,但所有跨租户读取、导出、代操作必须记录审计。
|
||||
2. 企业管理员只能访问自己租户的数据,不能通过 URL、Header、Body 指定其他 `tenant_id`、`db_name` 或 `db_dsn`。
|
||||
3. 后台菜单和按钮由服务端返回权限裁剪结果,前端隐藏只是体验优化,服务端仍必须二次鉴权。
|
||||
4. 敏感操作必须支持二次认证、幂等键、版本号或 `If-Match`,并写入审计。
|
||||
5. 普通租户管理员不能修改或删除审计日志。
|
||||
|
||||
## 3. 后台功能总览
|
||||
|
||||
| 模块 | 平台后台 | 企业后台 | 优先级 |
|
||||
| --- | --- | --- | --- |
|
||||
| 租户管理 | 是 | 否 | P0 |
|
||||
| 租户配置与配额 | 是 | 部分只读或申请变更 | P0 |
|
||||
| 管理员与角色权限 | 是 | 是 | P0 |
|
||||
| 用户与账号安全 | 支持跨租户运维 | 是 | P0 |
|
||||
| 组织架构 | 否 | 是 | P1 |
|
||||
| 群治理 | 运维只读/处置 | 是 | P1 |
|
||||
| 工作台应用配置 | 平台模板 | 租户配置 | P2 |
|
||||
| 会议、日报、月报 | 否 | 是 | P2 |
|
||||
| 总经理信箱 | 否 | 是 | P2 |
|
||||
| 实名认证审核 | 平台可审计 | 是 | P2 |
|
||||
| 反诈与内容审核 | 平台策略/插件 | 租户策略/处置 | P2 |
|
||||
| 举报与反馈工单 | 平台可审计 | 是 | P2 |
|
||||
| 文件与存储治理 | 平台运维 | 租户范围 | P1 |
|
||||
| Push、短信、TURN、对象存储配置 | 是 | 租户专属配置 | P1 |
|
||||
| 审计日志 | 是 | 只读本租户 | P0 |
|
||||
| Outbox 与后台任务 | 是 | 租户任务可见 | P1 |
|
||||
|
||||
## 4. 平台后台需求
|
||||
|
||||
### 4.1 租户管理
|
||||
|
||||
后台需要支持租户的创建、查看、编辑、停用、恢复、删除前置检查和删除归档。
|
||||
|
||||
租户状态:
|
||||
|
||||
| 状态 | 登录 | 读消息 | 写业务 | 管理操作 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `provisioning` | 禁止 | 禁止 | 禁止 | 平台管理员可配置 |
|
||||
| `active` | 允许 | 允许 | 允许 | 按角色允许 |
|
||||
| `suspended` | 禁止新登录 | 可配置只读 | 禁止 | 平台管理员可恢复或导出 |
|
||||
| `deleting` | 禁止 | 禁止 | 禁止 | 只允许删除任务 |
|
||||
| `deleted` | 禁止 | 禁止 | 禁止 | 只保留审计 |
|
||||
|
||||
核心字段:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `id` | 内部租户 ID,普通客户端不返回 |
|
||||
| `code` | 企业码,全局唯一,最长 64 字符 |
|
||||
| `name` | 企业名称 |
|
||||
| `tenant_desc` | 企业说明 |
|
||||
| `db_name` | 租户业务库名称;适合同 MySQL 实例下按库隔离,如 `im_tenant_1001` |
|
||||
| `db_dsn` | 租户业务库完整 DSN;适合独立 MySQL 实例或特殊部署,优先级高于 `db_name` |
|
||||
| `state` | 租户状态 |
|
||||
| `created_by/updated_by` | 操作人 |
|
||||
| `created_at/updated_at` | 时间 |
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/admin/v1/tenants` | 租户列表,支持状态、企业码、名称过滤 |
|
||||
| `POST` | `/admin/v1/tenants` | 创建租户 |
|
||||
| `GET` | `/admin/v1/tenants/{tenant_id}` | 租户详情 |
|
||||
| `PATCH` | `/admin/v1/tenants/{tenant_id}` | 修改名称、说明等基础资料 |
|
||||
| `POST` | `/admin/v1/tenants/{tenant_id}/suspend` | 停用租户 |
|
||||
| `POST` | `/admin/v1/tenants/{tenant_id}/restore` | 恢复租户 |
|
||||
| `POST` | `/admin/v1/tenants/{tenant_id}/deletion-check` | 删除前检查 |
|
||||
| `DELETE` | `/admin/v1/tenants/{tenant_id}` | 进入删除流程 |
|
||||
|
||||
独立库模式下,创建租户必须按 provisioning 流程执行:
|
||||
|
||||
1. 在控制库创建 `im_tenant` 记录,初始状态为 `provisioning`。
|
||||
2. 生成或填写租户业务库路由信息,优先使用 `db_dsn`,否则使用主 DSN + `db_name`。
|
||||
3. 创建租户业务库并执行 `tenant_schema.sql`。
|
||||
4. 初始化默认系统数据,例如 `kvmeta.version`、系统账号、`sys` Topic 和必要的默认配置。
|
||||
5. 回写 `im_tenant.db_name/db_dsn`,状态改为 `active`。
|
||||
6. 任一步失败时保留 `provisioning` 或进入明确失败状态,后台提供错误详情和重试入口;禁止创建失败的租户进入登录流程。
|
||||
|
||||
验收点:
|
||||
|
||||
1. 租户 `code` 全局唯一。
|
||||
2. 停用后旧 Token、WS 登录、HTTP 产品接口均拒绝。
|
||||
3. 删除必须进入异步任务,不允许直接硬删租户业务库。
|
||||
4. 所有状态变更写平台审计。
|
||||
|
||||
### 4.2 租户配置与配额
|
||||
|
||||
首期配置真源是 `im_tenant_config`。
|
||||
|
||||
| 配置 | 说明 | 默认 |
|
||||
| --- | --- | --- |
|
||||
| `max_users` | 有效用户数上限 | `0` 表示不限 |
|
||||
| `max_groups` | 有效群数量上限 | `0` 表示不限 |
|
||||
| `max_group_members` | 单群有效成员上限 | `0` 表示不限 |
|
||||
|
||||
后续可扩展配置:
|
||||
|
||||
| 配置域 | 示例 |
|
||||
| --- | --- |
|
||||
| 功能开关 | 加好友、建群、用户搜索、音视频、群文件、日报月报 |
|
||||
| 安全策略 | 实名要求、青少年限制、风控模式、数据保留期 |
|
||||
| 消息策略 | 文本长度、媒体类型、消息频控、撤回时限、编辑时限 |
|
||||
| 文件策略 | 单文件大小、总存储配额、允许 MIME、病毒扫描 |
|
||||
| Push 配置 | APNS/FCM 凭证、模板、锁屏隐私 |
|
||||
| 外部服务 | 短信、REST 认证、TURN、对象存储、审核插件 |
|
||||
| 企业品牌 | Logo、主题色、企业描述、客户端下载地址 |
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/admin/v1/tenants/{tenant_id}/config` | 查询租户配置 |
|
||||
| `PATCH` | `/admin/v1/tenants/{tenant_id}/config` | 修改配额和策略 |
|
||||
| `GET` | `/admin/v1/tenants/{tenant_id}/usage` | 查询用户数、群数、文件容量、消息量 |
|
||||
| `GET` | `/admin/v1/tenants/{tenant_id}/features` | 查询功能开关 |
|
||||
| `PATCH` | `/admin/v1/tenants/{tenant_id}/features` | 修改功能开关 |
|
||||
|
||||
验收点:
|
||||
|
||||
1. 注册、创建群、入群审批必须在 `im_server` 对应租户业务库事务内校验配额。
|
||||
2. 超限返回稳定错误码,并记录当前用量、限制值、租户和操作。
|
||||
3. 修改配置只影响当前租户,不能污染全平台缓存。
|
||||
|
||||
### 4.3 平台管理员管理
|
||||
|
||||
需求:
|
||||
|
||||
1. 创建、停用、恢复平台管理员。
|
||||
2. 分配平台角色和权限。
|
||||
3. 平台管理员登录、二次认证、Token 撤销。
|
||||
4. 查看平台管理员操作审计。
|
||||
|
||||
敏感操作:
|
||||
|
||||
| 操作 | 要求 |
|
||||
| --- | --- |
|
||||
| 创建平台管理员 | 二次确认和审计 |
|
||||
| 授权 `platform_admin` | 二次认证 |
|
||||
| 跨租户代操作 | 必须填写原因,记录目标租户和目标对象 |
|
||||
| 导出数据 | 审批或二次认证,记录导出范围 |
|
||||
|
||||
### 4.4 平台运维与任务
|
||||
|
||||
需求:
|
||||
|
||||
1. 查看运行节点、数据库版本、Adapter 类型和多租户能力。
|
||||
2. 非 MySQL 或未完成独立库租户路由能力的 Adapter 在产品模式下必须拒绝启动。
|
||||
3. 查看 Outbox 事件积压、失败、死信和重试。
|
||||
4. 查看文件垃圾回收、搜索索引任务、Push 投递、外部审核调用状态。
|
||||
5. 支持按租户维度查看任务,但避免 Prometheus 高基数标签。
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/admin/v1/system/health` | 系统健康 |
|
||||
| `GET` | `/admin/v1/system/nodes` | 节点列表 |
|
||||
| `GET` | `/admin/v1/system/outbox-events` | Outbox 查询 |
|
||||
| `POST` | `/admin/v1/system/outbox-events/{id}/retry` | 重试事件 |
|
||||
| `POST` | `/admin/v1/system/outbox-events/{id}/dead-letter` | 标记死信 |
|
||||
| `GET` | `/admin/v1/system/audit-logs` | 平台审计 |
|
||||
|
||||
## 5. 企业后台需求
|
||||
|
||||
### 5.1 企业管理员与角色权限
|
||||
|
||||
企业后台需要支持本租户内管理员管理。
|
||||
|
||||
角色:
|
||||
|
||||
| 角色 | 权限 |
|
||||
| --- | --- |
|
||||
| `tenant_owner` | 全部租户管理权限 |
|
||||
| `tenant_admin` | 用户、组织、群、安全、工作台配置 |
|
||||
| `security_auditor` | 实名、举报、反馈、审计读取 |
|
||||
| `org_admin` | 组织架构和成员维护 |
|
||||
| `report_admin` | 日报、月报、会议统计 |
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/admin/v1/tenant/admins` | 企业管理员列表 |
|
||||
| `POST` | `/admin/v1/tenant/admins` | 添加管理员 |
|
||||
| `PATCH` | `/admin/v1/tenant/admins/{user_id}` | 修改角色 |
|
||||
| `DELETE` | `/admin/v1/tenant/admins/{user_id}` | 移除管理员 |
|
||||
| `GET` | `/admin/v1/tenant/roles` | 可用角色和权限点 |
|
||||
|
||||
验收点:
|
||||
|
||||
1. Tenant Admin 只能管理当前租户。
|
||||
2. 授权、移除、代操作必须审计。
|
||||
3. 不允许最后一个 `tenant_owner` 被删除或停用。
|
||||
|
||||
### 5.2 用户与账号安全管理
|
||||
|
||||
需求:
|
||||
|
||||
1. 用户列表、搜索、详情。
|
||||
2. 创建、启用、停用、离职、恢复用户。
|
||||
3. 重置密码、撤销 Token、踢下线。
|
||||
4. 查看登录设备、登录 Session、安全事件。
|
||||
5. 查看用户实名状态、青少年状态、隐私设置摘要。
|
||||
6. 支持同手机号在不同租户独立存在,本租户后台只看到本租户用户。
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/admin/v1/tenant/users` | 用户列表 |
|
||||
| `POST` | `/admin/v1/tenant/users` | 创建用户 |
|
||||
| `GET` | `/admin/v1/tenant/users/{user_id}` | 用户详情 |
|
||||
| `PATCH` | `/admin/v1/tenant/users/{user_id}` | 修改资料或状态 |
|
||||
| `POST` | `/admin/v1/tenant/users/{user_id}/reset-password` | 重置密码 |
|
||||
| `POST` | `/admin/v1/tenant/users/{user_id}/revoke-sessions` | 撤销会话和 Token |
|
||||
| `GET` | `/admin/v1/tenant/users/{user_id}/devices` | 登录设备 |
|
||||
| `GET` | `/admin/v1/tenant/users/{user_id}/security-events` | 安全事件 |
|
||||
|
||||
验收点:
|
||||
|
||||
1. 停用用户不影响其他租户相同手机号或相同设备用户。
|
||||
2. 重置密码必须递增 `auth_version` 并撤销旧 Token。
|
||||
3. 查看敏感信息必须脱敏,证件号、手机号、Token 不写日志。
|
||||
4. 用户管理接口必须通过 `tenant_id` 选择对应租户业务库;不得在控制库中查找租户内用户主数据。
|
||||
|
||||
### 5.3 组织架构管理
|
||||
|
||||
需求:
|
||||
|
||||
1. 部门树管理:新增、编辑、移动、删除部门。
|
||||
2. 部门成员管理:添加、移除、调岗、设置负责人。
|
||||
3. 组织搜索:按部门、用户、岗位搜索。
|
||||
4. 离职用户处理:不可联系或从结果移除。
|
||||
5. 组织数据权限:管理员只能管理权限范围内部门。
|
||||
|
||||
建议数据表:
|
||||
|
||||
| 表 | 说明 |
|
||||
| --- | --- |
|
||||
| `im_org_units` | 部门树 |
|
||||
| `im_org_members` | 部门成员、岗位、负责人 |
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/admin/v1/tenant/org/departments` | 部门树 |
|
||||
| `POST` | `/admin/v1/tenant/org/departments` | 创建部门 |
|
||||
| `PATCH` | `/admin/v1/tenant/org/departments/{id}` | 修改或移动部门 |
|
||||
| `DELETE` | `/admin/v1/tenant/org/departments/{id}` | 删除部门 |
|
||||
| `GET` | `/admin/v1/tenant/org/departments/{id}/members` | 部门成员 |
|
||||
| `POST` | `/admin/v1/tenant/org/departments/{id}/members` | 添加成员 |
|
||||
| `DELETE` | `/admin/v1/tenant/org/departments/{id}/members/{user_id}` | 移除成员 |
|
||||
| `GET` | `/admin/v1/tenant/org/search` | 组织搜索 |
|
||||
|
||||
验收点:
|
||||
|
||||
1. 部门 `parent_id`、负责人、成员必须来自当前租户业务库。
|
||||
2. 移动部门不能形成环。
|
||||
3. 删除部门需要校验子部门和成员处理策略。
|
||||
|
||||
### 5.4 群治理管理
|
||||
|
||||
企业后台和群主/群管理员都需要群治理能力。后台侧侧重全租户范围查询、策略配置、违规处置和审计。
|
||||
|
||||
需求:
|
||||
|
||||
1. 群列表、群详情、群成员列表。
|
||||
2. 群资料编辑:名称、头像、简介。
|
||||
3. 群治理配置:入群审批、全员禁言、成员上传文件、成员私聊、成员邀请。
|
||||
4. 群管理员管理:添加、删除管理员,管理员数量限制。
|
||||
5. 禁言管理:添加、解除、过期自动失效。
|
||||
6. 群主转让、解散群聊。
|
||||
7. 群二维码管理:生成、撤销、过期。
|
||||
8. 群文件管理:列表、搜索、删除、下载权限。
|
||||
9. 群操作日志:角色变化、禁言、公告、解散。
|
||||
|
||||
建议数据表:
|
||||
|
||||
| 表 | 说明 |
|
||||
| --- | --- |
|
||||
| `im_group_settings` | 群治理配置 |
|
||||
| `im_group_members` | 群成员和角色真源 |
|
||||
| `im_group_join_requests` | 入群申请 |
|
||||
| `im_group_announcements` | 群公告 |
|
||||
| `im_group_mutes` | 禁言记录 |
|
||||
| `im_group_admin_logs` | 群管理日志 |
|
||||
| `im_group_files` | 群文件索引 |
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/admin/v1/tenant/groups` | 群列表 |
|
||||
| `GET` | `/admin/v1/tenant/groups/{topic}` | 群详情 |
|
||||
| `PATCH` | `/admin/v1/tenant/groups/{topic}` | 修改群资料 |
|
||||
| `GET/PATCH` | `/admin/v1/tenant/groups/{topic}/governance` | 群治理配置 |
|
||||
| `GET` | `/admin/v1/tenant/groups/{topic}/members` | 成员列表 |
|
||||
| `POST` | `/admin/v1/tenant/groups/{topic}/admins/batch` | 添加管理员 |
|
||||
| `DELETE` | `/admin/v1/tenant/groups/{topic}/admins/{user_id}` | 删除管理员 |
|
||||
| `POST` | `/admin/v1/tenant/groups/{topic}/mutes/batch` | 添加禁言 |
|
||||
| `DELETE` | `/admin/v1/tenant/groups/{topic}/mutes/{user_id}` | 解除禁言 |
|
||||
| `POST` | `/admin/v1/tenant/groups/{topic}/owner-transfer` | 群主转让 |
|
||||
| `DELETE` | `/admin/v1/tenant/groups/{topic}` | 解散群 |
|
||||
| `GET` | `/admin/v1/tenant/groups/{topic}/files` | 群文件 |
|
||||
| `DELETE` | `/admin/v1/tenant/groups/{topic}/files/{group_file_id}` | 删除群文件 |
|
||||
|
||||
验收点:
|
||||
|
||||
1. 群业务真源与 Topic/Subscription/ACL 投影必须在同一事务内更新。
|
||||
2. 管理员不能越级操作群主,普通管理员之间操作规则由治理策略决定。
|
||||
3. 解散群必须撤销 Topic、二维码、邀请和文件新增权限。
|
||||
4. 群治理写操作必须由 `im_server` 在目标租户业务库内完成,HotGo 后台不能直接改 Topic、Subscription 或 ACL 绕过业务真源。
|
||||
|
||||
### 5.5 工作台应用配置
|
||||
|
||||
需求:
|
||||
|
||||
1. 工作台模块配置:启用、禁用、排序、图标、名称、路由。
|
||||
2. 根据组织角色、权限和功能开关裁剪用户可见模块。
|
||||
3. 维护会议、日报、月报、总经理信箱等入口。
|
||||
4. 工作台未读角标由系统事件更新。
|
||||
|
||||
建议数据表:
|
||||
|
||||
| 表 | 说明 |
|
||||
| --- | --- |
|
||||
| `im_workbench_apps` | 租户工作台应用配置 |
|
||||
| `im_workbench_user_apps` | 用户或角色级工作台配置 |
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/admin/v1/tenant/workbench/apps` | 应用列表 |
|
||||
| `POST` | `/admin/v1/tenant/workbench/apps` | 新增应用 |
|
||||
| `PATCH` | `/admin/v1/tenant/workbench/apps/{code}` | 修改应用 |
|
||||
| `DELETE` | `/admin/v1/tenant/workbench/apps/{code}` | 删除或禁用应用 |
|
||||
| `POST` | `/admin/v1/tenant/workbench/apps/reorder` | 排序 |
|
||||
|
||||
验收点:
|
||||
|
||||
1. 客户端只接受白名单内部 `route`。
|
||||
2. 禁用模块后普通用户不可通过直接 URL 访问。
|
||||
|
||||
### 5.6 会议管理
|
||||
|
||||
需求:
|
||||
|
||||
1. 创建、编辑、取消会议。
|
||||
2. 管理参会人、提醒未响应者。
|
||||
3. 查看参会响应:`pending`、`accepted`、`declined`、`tentative`。
|
||||
4. 会议过期后只读展示。
|
||||
5. 生成系统通知和 Push。
|
||||
|
||||
建议数据表:
|
||||
|
||||
| 表 | 说明 |
|
||||
| --- | --- |
|
||||
| `im_meetings` | 会议主表 |
|
||||
| `im_meeting_participants` | 参会人响应 |
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/admin/v1/tenant/meetings` | 会议列表 |
|
||||
| `POST` | `/admin/v1/tenant/meetings` | 创建会议 |
|
||||
| `GET` | `/admin/v1/tenant/meetings/{id}` | 会议详情 |
|
||||
| `PATCH` | `/admin/v1/tenant/meetings/{id}` | 更新会议 |
|
||||
| `POST` | `/admin/v1/tenant/meetings/{id}/cancel` | 取消会议 |
|
||||
| `POST` | `/admin/v1/tenant/meetings/{id}/reminders` | 提醒未响应者 |
|
||||
|
||||
### 5.7 日报与月报管理
|
||||
|
||||
需求:
|
||||
|
||||
1. 日报/月报模板和校验规则配置。
|
||||
2. 普通用户草稿、提交、修订。
|
||||
3. 管理员按部门查看统计和明细。
|
||||
4. 提醒未提交人员。
|
||||
5. 查看详情时记录已读状态。
|
||||
6. 附件使用文件 ACL,不能通过旧 URL 越权下载。
|
||||
|
||||
建议数据表:
|
||||
|
||||
| 表 | 说明 |
|
||||
| --- | --- |
|
||||
| `im_reports` | 日报/月报统一表 |
|
||||
| `im_report_read_states` | 阅读状态 |
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/admin/v1/tenant/reports/daily/statistics` | 日报统计 |
|
||||
| `GET` | `/admin/v1/tenant/reports/daily` | 日报列表 |
|
||||
| `GET` | `/admin/v1/tenant/reports/daily/{id}` | 日报详情 |
|
||||
| `POST` | `/admin/v1/tenant/reports/daily/reminders` | 日报提醒 |
|
||||
| `GET` | `/admin/v1/tenant/reports/monthly/statistics` | 月报统计 |
|
||||
| `GET` | `/admin/v1/tenant/reports/monthly` | 月报列表 |
|
||||
| `GET` | `/admin/v1/tenant/reports/monthly/{id}` | 月报详情 |
|
||||
| `POST` | `/admin/v1/tenant/reports/monthly/reminders` | 月报提醒 |
|
||||
|
||||
验收点:
|
||||
|
||||
1. 管理员只能看组织权限范围内成员。
|
||||
2. 并发保存必须使用版本号,冲突返回当前最新版本。
|
||||
3. 提醒接口需要限流并返回逐项结果。
|
||||
|
||||
### 5.8 总经理信箱
|
||||
|
||||
需求:
|
||||
|
||||
1. 配置是否启用、是否允许匿名、必填字段、附件策略、提示文案。
|
||||
2. 用户提交意见或建议。
|
||||
3. 后台查看、分派、处理、回复和关闭。
|
||||
4. 匿名模式下业务接收方不可见提交人;安全审计是否保留内部追踪由企业政策决定。
|
||||
5. 文案变更后,客户端提交时可要求重新确认。
|
||||
|
||||
建议数据表:
|
||||
|
||||
| 表 | 说明 |
|
||||
| --- | --- |
|
||||
| `im_manager_mailbox` | 信箱提交和处理记录 |
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/admin/v1/tenant/executive-mailbox/config` | 查询配置 |
|
||||
| `PATCH` | `/admin/v1/tenant/executive-mailbox/config` | 修改配置 |
|
||||
| `GET` | `/admin/v1/tenant/executive-mailbox/submissions` | 提交列表 |
|
||||
| `GET` | `/admin/v1/tenant/executive-mailbox/submissions/{id}` | 提交详情 |
|
||||
| `POST` | `/admin/v1/tenant/executive-mailbox/submissions/{id}/assign` | 分派 |
|
||||
| `POST` | `/admin/v1/tenant/executive-mailbox/submissions/{id}/reply` | 回复 |
|
||||
| `POST` | `/admin/v1/tenant/executive-mailbox/submissions/{id}/close` | 关闭 |
|
||||
|
||||
## 6. 安全、审核与工单后台
|
||||
|
||||
### 6.1 实名认证审核
|
||||
|
||||
需求:
|
||||
|
||||
1. 查看实名认证列表:未认证、待审核、已通过、已拒绝、申诉中。
|
||||
2. 查看脱敏信息,具备权限时按流程查看敏感原文。
|
||||
3. 审核通过、拒绝、要求补充材料。
|
||||
4. 实名策略配置:是否强制实名、允许跳过、提醒周期、重复身份策略。
|
||||
5. 审核结果通过系统事件通知用户。
|
||||
|
||||
建议数据表:
|
||||
|
||||
| 表 | 说明 |
|
||||
| --- | --- |
|
||||
| `im_realname_verifications` | 实名信息、审核状态、脱敏展示 |
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/admin/v1/tenant/security/realname/submissions` | 实名审核列表 |
|
||||
| `GET` | `/admin/v1/tenant/security/realname/submissions/{id}` | 审核详情 |
|
||||
| `POST` | `/admin/v1/tenant/security/realname/submissions/{id}/approve` | 通过 |
|
||||
| `POST` | `/admin/v1/tenant/security/realname/submissions/{id}/reject` | 拒绝 |
|
||||
| `GET/PATCH` | `/admin/v1/tenant/security/realname/policy` | 实名策略 |
|
||||
|
||||
验收点:
|
||||
|
||||
1. 姓名、证件号加密存储,普通日志不出现原文。
|
||||
2. 审核操作必须审计。
|
||||
3. 普通管理员无权读取敏感原文。
|
||||
|
||||
### 6.2 青少年模式策略
|
||||
|
||||
需求:
|
||||
|
||||
1. 配置青少年模式默认策略和限制范围。
|
||||
2. 查看用户青少年模式状态。
|
||||
3. 处理用户忘记独立密码后的重置流程。
|
||||
|
||||
限制范围首期建议包括:
|
||||
|
||||
1. 添加好友。
|
||||
2. 加群和创建群。
|
||||
3. 文件传输。
|
||||
4. 音视频通话。
|
||||
5. 工作台外部链接。
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET/PATCH` | `/admin/v1/tenant/security/youth-mode/policy` | 青少年策略 |
|
||||
| `GET` | `/admin/v1/tenant/security/youth-mode/users` | 用户状态列表 |
|
||||
| `POST` | `/admin/v1/tenant/security/youth-mode/users/{user_id}/reset-pin` | 重置独立密码 |
|
||||
|
||||
### 6.3 反诈与内容审核
|
||||
|
||||
需求:
|
||||
|
||||
1. 风控策略配置:高风险动作、提示文案、二次确认、阻断策略。
|
||||
2. 审核插件配置:平台统一审核服务或租户专属审核服务。
|
||||
3. 审核模式配置:`sync_block`、`async_hold`、`async_post_audit`。
|
||||
4. 查看风险事件、确认、取消、过期、阻断记录。
|
||||
5. 对已发布违规消息执行系统撤回。
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET/PATCH` | `/admin/v1/tenant/security/risk-policy` | 风控策略 |
|
||||
| `GET/PATCH` | `/admin/v1/tenant/security/moderation-policy` | 内容审核策略 |
|
||||
| `GET` | `/admin/v1/tenant/security/risk-events` | 风险事件 |
|
||||
| `GET` | `/admin/v1/tenant/security/moderation-tasks` | 审核任务 |
|
||||
| `POST` | `/admin/v1/tenant/security/moderation-tasks/{id}/approve` | 审核通过 |
|
||||
| `POST` | `/admin/v1/tenant/security/moderation-tasks/{id}/reject` | 审核拒绝 |
|
||||
| `POST` | `/admin/v1/tenant/messages/{message_id}/recall-by-review` | 审核撤回 |
|
||||
|
||||
验收点:
|
||||
|
||||
1. 客户端提交的审核状态一律忽略。
|
||||
2. 插件返回 `REPLACE` 或 `RESPOND` 时不能修改 TenantID。
|
||||
3. 风控确认 Token 单次、短期、绑定原动作。
|
||||
|
||||
### 6.4 举报工单
|
||||
|
||||
需求:
|
||||
|
||||
1. 配置举报原因,按目标类型区分用户、群、消息。
|
||||
2. 举报列表、详情、证据、目标快照。
|
||||
3. 工单状态:`received`、`processing`、`resolved`、`rejected`、`closed`。
|
||||
4. 处置动作:警告、禁言、封禁用户、解散群、撤回消息、标记无效。
|
||||
5. 重复举报合并策略。
|
||||
6. 处理结果通知举报人,必要时通知被处置对象。
|
||||
|
||||
建议数据表:
|
||||
|
||||
| 表 | 说明 |
|
||||
| --- | --- |
|
||||
| `im_abuse_reports` | 举报工单 |
|
||||
| `im_evidence_files` | 证据文件 |
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET/PATCH` | `/admin/v1/tenant/reports-abuse/reasons` | 举报原因配置 |
|
||||
| `GET` | `/admin/v1/tenant/reports-abuse` | 举报列表 |
|
||||
| `GET` | `/admin/v1/tenant/reports-abuse/{id}` | 举报详情 |
|
||||
| `POST` | `/admin/v1/tenant/reports-abuse/{id}/assign` | 分派 |
|
||||
| `POST` | `/admin/v1/tenant/reports-abuse/{id}/resolve` | 处置 |
|
||||
| `POST` | `/admin/v1/tenant/reports-abuse/{id}/close` | 关闭 |
|
||||
|
||||
验收点:
|
||||
|
||||
1. 举报人必须能看到被举报目标,后台详情使用提交时固化快照。
|
||||
2. 证据文件使用独立 ACL 和保留期。
|
||||
3. 原消息删除或撤回后,举报证据仍可处理。
|
||||
|
||||
### 6.5 意见反馈工单
|
||||
|
||||
需求:
|
||||
|
||||
1. 配置反馈类别、最大正文、附件规则、是否允许联系方式、日志授权说明。
|
||||
2. 反馈列表、详情、附件、设备和版本信息。
|
||||
3. 分派、回复、关闭、标记重复。
|
||||
4. 处理结果通过系统通知或客户端查询展示。
|
||||
|
||||
建议数据表:
|
||||
|
||||
| 表 | 说明 |
|
||||
| --- | --- |
|
||||
| `im_feedbacks` | 反馈工单 |
|
||||
| `im_evidence_files` | 反馈附件 |
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET/PATCH` | `/admin/v1/tenant/feedback/config` | 反馈配置 |
|
||||
| `GET` | `/admin/v1/tenant/feedbacks` | 反馈列表 |
|
||||
| `GET` | `/admin/v1/tenant/feedbacks/{id}` | 反馈详情 |
|
||||
| `POST` | `/admin/v1/tenant/feedbacks/{id}/assign` | 分派 |
|
||||
| `POST` | `/admin/v1/tenant/feedbacks/{id}/reply` | 回复 |
|
||||
| `POST` | `/admin/v1/tenant/feedbacks/{id}/close` | 关闭 |
|
||||
|
||||
## 7. 文件、搜索与数据治理后台
|
||||
|
||||
### 7.1 文件治理
|
||||
|
||||
需求:
|
||||
|
||||
1. 按租户查看文件用量、文件列表、状态和业务用途。
|
||||
2. 支持群文件、收藏文件、举报证据、反馈附件、日报/月报附件分类管理。
|
||||
3. 文件下载审计。
|
||||
4. 清理未关联文件、过期文件和违规文件。
|
||||
5. 配置文件大小、MIME、总存储配额、保留期。
|
||||
|
||||
文件状态建议:
|
||||
|
||||
```text
|
||||
pending -> uploaded -> ready
|
||||
pending/uploaded -> failed
|
||||
uploaded(超时且无引用) -> orphaned -> deleted
|
||||
```
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/admin/v1/tenant/files` | 文件列表 |
|
||||
| `GET` | `/admin/v1/tenant/files/{file_id}` | 文件详情 |
|
||||
| `POST` | `/admin/v1/tenant/files/{file_id}/quarantine` | 隔离文件 |
|
||||
| `DELETE` | `/admin/v1/tenant/files/{file_id}` | 删除文件 |
|
||||
| `GET/PATCH` | `/admin/v1/tenant/files/policy` | 文件策略 |
|
||||
|
||||
验收点:
|
||||
|
||||
1. 文件下载不能只靠 FileID,必须校验上传者或关联消息/Topic 读取权限。
|
||||
2. 后台删除文件必须审计,并处理消息、收藏、举报等引用。
|
||||
|
||||
### 7.2 搜索治理
|
||||
|
||||
需求:
|
||||
|
||||
1. 消息索引状态查看。
|
||||
2. 重建指定租户、Topic 或时间段索引。
|
||||
3. 搜索敏感内容审计。
|
||||
4. 查看搜索性能和失败任务。
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/admin/v1/tenant/search/index-status` | 索引状态 |
|
||||
| `POST` | `/admin/v1/tenant/search/reindex` | 重建索引 |
|
||||
| `GET` | `/admin/v1/tenant/search/audit-logs` | 搜索审计 |
|
||||
|
||||
验收点:
|
||||
|
||||
1. 搜索索引不是授权来源,返回前仍需 Topic Read ACL 二次校验。
|
||||
2. 用户退群或权限变化后必须立即阻断搜索结果。
|
||||
|
||||
## 8. 通知、Push 与外部服务后台
|
||||
|
||||
### 8.1 系统消息模板
|
||||
|
||||
需求:
|
||||
|
||||
1. 管理系统事件类型和模板版本。
|
||||
2. 配置账号安全、好友、群、文件、通话、工作台、安全等通知模板。
|
||||
3. Outbox 事件驱动系统消息、Push、索引和定时任务。
|
||||
|
||||
首期事件:
|
||||
|
||||
| 业务 | 事件 |
|
||||
| --- | --- |
|
||||
| 账号安全 | 新设备登录、密码修改、实名提醒 |
|
||||
| 好友 | 新申请、通过、拒绝、删除 |
|
||||
| 群 | 邀请、申请、审批、公告、禁言、管理员变更、群主转让、解散 |
|
||||
| 文件 | 群文件上传、文件失效 |
|
||||
| 通话 | 未接、拒绝、取消、忙线、通话结束 |
|
||||
| 工作台 | 会议通知、会议变更、日报提醒、月报提醒、信箱处理 |
|
||||
| 安全 | 反诈提示、举报受理、反馈受理 |
|
||||
|
||||
### 8.2 Push、短信、TURN、对象存储
|
||||
|
||||
需求:
|
||||
|
||||
1. 租户级 Push 模板、App 包名、APNS/FCM 凭证引用。
|
||||
2. 短信服务配置和模板。
|
||||
3. TURN 配置。
|
||||
4. 对象存储 Bucket、Prefix、签名策略。
|
||||
5. 外部 REST 认证服务配置。
|
||||
6. 外部审核和风控插件配置。
|
||||
|
||||
验收点:
|
||||
|
||||
1. 租户没有配置时按明确策略失败,不得静默使用其他租户配置。
|
||||
2. 密钥只保存引用,不把明文 Secret 放入普通业务 JSON。
|
||||
3. 外部回调必须校验服务端签名的 TenantContext 和 nonce。
|
||||
|
||||
## 9. 审计与日志后台
|
||||
|
||||
### 9.1 审计范围
|
||||
|
||||
以下操作必须写审计:
|
||||
|
||||
1. 租户创建、停用、恢复、删除。
|
||||
2. 平台管理员和租户管理员授权、移除、代操作。
|
||||
3. 用户停用、离职、密码重置、数据导出。
|
||||
4. 群主转让、管理员变更、禁言、解散群。
|
||||
5. 实名审核、举报处置、反馈处理、匿名信箱读取。
|
||||
6. 文件下载、删除、批量导出、搜索敏感内容。
|
||||
7. 反诈二次确认、内容审核处置。
|
||||
8. 外部服务配置修改。
|
||||
|
||||
独立库模式下审计存储分为两类:
|
||||
|
||||
1. 平台级审计存控制库,覆盖租户生命周期、平台管理员、跨租户运维、租户业务库创建/删除/导出等操作。
|
||||
2. 租户级业务审计优先存对应租户业务库,覆盖用户、群、文件、实名、举报、反馈、办公协同等操作;后台查询时通过 `tenant_id` 选择租户业务库。
|
||||
|
||||
平台后台可汇总查询租户级审计,但跨租户聚合、导出和代操作必须记录平台级审计。
|
||||
|
||||
### 9.2 审计字段
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `tenant_id` | 租户 |
|
||||
| `actor_uid` | 操作人 |
|
||||
| `actor_role` | 操作角色 |
|
||||
| `action` | 操作类型 |
|
||||
| `target_type` | 目标类型 |
|
||||
| `target_id` | 目标 ID |
|
||||
| `before_snapshot` | 变更前摘要 |
|
||||
| `after_snapshot` | 变更后摘要 |
|
||||
| `reason` | 操作原因 |
|
||||
| `request_id` | 请求 ID |
|
||||
| `ip` | 操作 IP,必要时脱敏 |
|
||||
| `user_agent` | 操作客户端 |
|
||||
| `created_at` | 操作时间 |
|
||||
|
||||
审计表只允许追加,不允许普通管理员修改或删除。
|
||||
|
||||
建议接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/admin/v1/tenant/audit-logs` | 租户审计 |
|
||||
| `GET` | `/admin/v1/audit-logs` | 平台审计 |
|
||||
| `POST` | `/admin/v1/audit-logs/export` | 审计导出 |
|
||||
|
||||
## 10. 后台公共能力
|
||||
|
||||
### 10.1 认证与会话
|
||||
|
||||
1. 后台登录必须使用独立后台入口,不与普通 App 登录混用权限。
|
||||
2. 后台 Token 包含 `tenant_id`、`uid`、角色、权限版本、`auth_version`。
|
||||
3. 支持刷新、撤销、踢下线、登录设备管理。
|
||||
4. 高危操作需要二次认证或 `X-Action-Token`。
|
||||
|
||||
### 10.2 API 规范
|
||||
|
||||
后台 API 建议统一使用 `/admin/v1/*`。该路径是 `im_server` 管理 API 的目标契约;在 HotGo 中落地页面和接口时,可以按 HotGo 现有 `/admin/...` 路由规范做适配,但必须保持权限语义、请求字段、幂等、审计和错误码一致。
|
||||
|
||||
成功响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {},
|
||||
"request_id": "01K...",
|
||||
"server_time": "2026-08-12T05:30:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
失败响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 30001,
|
||||
"reason": "FORBIDDEN",
|
||||
"message": "permission denied",
|
||||
"details": {},
|
||||
"request_id": "01K...",
|
||||
"server_time": "2026-08-12T05:30:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
公共要求:
|
||||
|
||||
1. 写操作必须带 `Idempotency-Key`。
|
||||
2. 更新操作必须带版本号或 `If-Match`。
|
||||
3. 列表统一使用 `limit + cursor`。
|
||||
4. 请求必须带 `X-Request-ID`。
|
||||
5. 不能从请求 Body、URL Query 或普通 Header 信任客户端自报 `tenant_id`。
|
||||
6. 手机号、证件号、验证码、Token、设备 Token、文件签名不写日志。
|
||||
|
||||
### 10.3 数据表公共规则
|
||||
|
||||
1. 新业务表统一使用 `im_` 前缀。
|
||||
2. 控制库只保存平台级租户元数据、平台级审计、平台任务和全局配置,例如 `im_tenant`、`im_tenant_config`。
|
||||
3. 租户业务库保存租户内业务表,例如用户安全、好友、群治理、收藏、实名、举报、反馈、组织、会议、日报月报等;这些表不包含 `tenant_id` 字段。
|
||||
4. 租户业务表唯一索引按租户库内业务键设计,例如 `business_key`、`user_id + peer_id`、`topic + seqid`,不使用 `(tenant_id, business_key)`。
|
||||
5. 子表外键按租户库内父子关系设计,不使用 `(tenant_id, parent_id)` 复合外键。
|
||||
6. 更新、删除、聚合、清理任务必须先从可信 Principal 或平台运维上下文取得 `tenant_id`,再路由到对应租户业务库执行。
|
||||
7. 禁止通过 URL、Header、Body 传入的 `db_name`、`db_dsn` 或 `tenant_id` 决定业务库;业务库路由只能来自控制库和服务端可信上下文。
|
||||
|
||||
### 10.4 Outbox 与后台任务存储
|
||||
|
||||
Outbox 和后台任务同样按作用域拆分:
|
||||
|
||||
1. 平台级 Outbox 存控制库,用于租户创建、租户停用、建库初始化、跨租户导出、平台通知等。
|
||||
2. 租户级 Outbox 存租户业务库,用于好友、群、文件、实名、举报、反馈、会议、日报月报等业务事件。
|
||||
3. 后台查询 Outbox 时,平台后台可以先按租户筛选,再路由到对应业务库;企业后台只能查询当前租户业务库内事件。
|
||||
4. 重试、死信和人工重放必须幂等,并写入对应作用域的审计。
|
||||
|
||||
## 11. 开发优先级建议
|
||||
|
||||
### P0:后台地基
|
||||
|
||||
1. `/admin/v1` 认证中间件、RBAC、租户 Principal。
|
||||
2. 平台租户管理:创建、停用、恢复、配置配额。
|
||||
3. 租户业务库 provisioning:建库、执行 schema、初始化系统数据、回写 `db_name/db_dsn`、失败重试。
|
||||
4. `im_server` 管理 API 对接和租户选库校验。
|
||||
5. 企业管理员与角色权限。
|
||||
6. 用户管理:停用、重置密码、撤销 Session。
|
||||
7. 审计日志基础表和查询。
|
||||
8. Outbox 查询与重试基础能力。
|
||||
|
||||
### P1:企业治理
|
||||
|
||||
1. 组织架构管理。
|
||||
2. 群治理后台:群列表、群成员、管理员、禁言、解散、群文件。
|
||||
3. 文件策略、文件列表、文件审计。
|
||||
4. 外部服务配置:Push、短信、对象存储、TURN。
|
||||
5. 功能开关和配额用量面板。
|
||||
|
||||
### P2:办公与安全业务
|
||||
|
||||
1. 工作台配置。
|
||||
2. 会议管理。
|
||||
3. 日报、月报统计和提醒。
|
||||
4. 总经理信箱。
|
||||
5. 实名认证审核。
|
||||
6. 青少年策略。
|
||||
7. 反诈、内容审核、举报、反馈工单。
|
||||
|
||||
## 12. 验收清单
|
||||
|
||||
- [ ] 平台后台与企业后台权限边界清晰,跨租户操作必须审计。
|
||||
- [ ] 所有后台列表、详情、更新、删除都按租户隔离。
|
||||
- [ ] 租户停用后普通用户登录和 HTTP 产品接口拒绝。
|
||||
- [ ] 用户停用、重置密码、撤销 Token 不影响其他租户同手机号用户。
|
||||
- [ ] 创建用户、建群、入群审批配额在事务内强制执行。
|
||||
- [ ] 群主转让、禁言、管理员变更、解散群写审计并同步系统消息。
|
||||
- [ ] 实名、举报、反馈、信箱等敏感数据脱敏展示并按权限读取。
|
||||
- [ ] 文件下载、删除、导出均有资源级授权和审计。
|
||||
- [ ] 外部服务密钥只保存引用,不写普通日志。
|
||||
- [ ] Outbox 事件可查询、重试、死信处理,重复执行不产生重复副作用。
|
||||
- [ ] 审计表只允许追加,普通管理员不可修改或删除。
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,852 @@
|
||||
# IM 产品需求服务端改造分析报告
|
||||
|
||||
> 分析日期:2026-08-09
|
||||
> 分析对象:`chat-0.25.3` 当前工作区源码
|
||||
> 需求依据:[IM-产品原型需求文档](IM-产品原型需求文档.md)
|
||||
> 报告范围:服务端能力、数据模型、协议/API、安全、实施计划和验收建议
|
||||
|
||||
## 1. 报告结论
|
||||
|
||||
当前项目是一个以 Tinode 为核心的 Go 即时通讯服务端,已经具备稳定的实时通信底座:账号登录、单聊、群聊、订阅与访问控制、消息收发、已读/送达回执、输入状态、消息删除、文件上传、推送、点对点 WebRTC 信令,以及可扩展的 gRPC 插件机制。
|
||||
|
||||
原型中的基础聊天能力不需要推倒重做,建议继续复用现有 Topic、Subscription、Message 和 Session 模型。在此基础上增加产品业务层,负责好友关系、群治理、内容检索、企业组织、会议/日报/月报、实名认证、青少年模式、举报与安全审计等业务。
|
||||
|
||||
本次分析得到以下关键结论:
|
||||
|
||||
1. **现有服务端可以承担实时通信内核,但不能直接满足完整产品需求。** 当前代码主要解决消息路由、会话和 ACL,不包含完整的社交关系、企业协同和安全治理领域模型。
|
||||
2. **应将“实时消息能力”和“产品业务能力”分层。** 聊天消息继续走 WebSocket;需要事务、查询、审批和管理的业务走 HTTP API;状态变化通过结构化系统消息回推 WebSocket。
|
||||
3. **必须先确定企业租户模式。** 当前 User、Topic、Subscription、Message 和 FileDef 均没有 `tenant_id`。如果多个企业共用一套数据库,租户隔离必须作为所有功能之前的基础改造。
|
||||
4. **消息幂等和文件授权是上线前必须修复的基础问题。** 发布消息处仍有“重复消息 ID 检查”的 TODO;文件下载只校验登录身份,没有验证用户是否有权访问对应消息或话题。
|
||||
5. **仓库部分旧文档与当前源码存在漂移。** 旧文档提到的 `x-im-*` 扩展、`im_*` 业务表、数据库版本 119 和 `tools/wsmock`,在当前源码中均未发现。本报告分析时各数据库适配器版本为 116;多租户第一阶段已把 MySQL 提升到 118,其他适配器仍为 116。本报告以可执行源码为准,不把文档设想视为已实现能力。
|
||||
|
||||
建议按“基础安全与协议 → 好友和群治理 → 搜索与文件 → 通话与设置 → 企业工作台”的顺序实施。若团队和测试资源稳定,完整服务端改造建议拆成 4 个产品阶段、6 至 9 个迭代交付。
|
||||
|
||||
## 2. 分析范围与判定口径
|
||||
|
||||
### 2.1 需求范围
|
||||
|
||||
本报告覆盖原型文档的 11 个功能域:
|
||||
|
||||
1. 账号注册、登录与身份安全;
|
||||
2. 消息首页、全局导航与系统通知;
|
||||
3. 单聊会话、会话建立与消息状态;
|
||||
4. 消息输入、媒体内容与消息操作;
|
||||
5. 通讯录、好友关系与黑名单;
|
||||
6. 群聊创建、资料维护与成员治理;
|
||||
7. 文件下载、收藏管理与聊天记录检索;
|
||||
8. 实时语音与视频通话;
|
||||
9. 企业工作台与办公协同;
|
||||
10. 个人资料、通用设置、隐私与青少年模式;
|
||||
11. 反诈安全、举报与意见反馈。
|
||||
|
||||
### 2.2 能力分类
|
||||
|
||||
文中的需求按以下四类判定:
|
||||
|
||||
| 分类 | 含义 | 处理方式 |
|
||||
| --- | --- | --- |
|
||||
| 已支持 | 当前代码已有可用服务端能力 | 直接复用并补验收测试 |
|
||||
| 扩展支持 | 现有核心模型可承载,但缺少业务字段、校验或接口 | 在现有链路中增加明确的领域逻辑 |
|
||||
| 全新建设 | 当前没有对应模型和服务 | 新增业务模块、表、API 和通知桥接 |
|
||||
| 客户端为主 | 服务端只需提供数据或开关,交互由 App 完成 | 避免在服务端重复实现 UI 状态 |
|
||||
|
||||
### 2.3 源码核查范围
|
||||
|
||||
重点核查了以下目录与文件:
|
||||
|
||||
- `server/session.go`:客户端消息分发、发布、订阅和元数据处理;
|
||||
- `server/topic.go`:Topic 生命周期、ACL、消息广播、回执和元数据读写;
|
||||
- `server/datamodel.go`:WebSocket 客户端/服务端协议模型;
|
||||
- `server/calls.go`:WebRTC 呼叫信令;
|
||||
- `server/hdl_files.go`:文件上传与下载;
|
||||
- `server/plugins.go`:gRPC 插件和消息拦截;
|
||||
- `server/store/store.go`、`server/store/types/types.go`:存储接口和核心实体;
|
||||
- `server/db/mysql/adapter.go`、`server/db/mysql/schema.sql`:当前 MySQL 实现;
|
||||
- `server/auth`、`server/validate`、`server/push`:认证、验证码和推送;
|
||||
- 根目录与 `docs` 目录的 Markdown 文档。
|
||||
|
||||
## 3. 当前服务端真实能力
|
||||
|
||||
### 3.1 核心架构
|
||||
|
||||
当前服务端采用“Session 接收协议包 → Hub/Topic 路由 → Store 持久化 → Topic 广播/Push”的模型。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
C["移动端 / Web 客户端"] -->|"WebSocket / Long Poll / gRPC"| S["Session"]
|
||||
S --> P["Plugin Firehose"]
|
||||
P --> H["Hub"]
|
||||
H --> T["Topic Actor"]
|
||||
T --> ST["Store Adapter"]
|
||||
ST --> DB["MySQL / PostgreSQL / MongoDB / RethinkDB"]
|
||||
T --> PU["Push Handlers"]
|
||||
T --> C
|
||||
C -->|"HTTP"| F["File Handler"]
|
||||
F --> ST
|
||||
```
|
||||
|
||||
Topic 以串行事件循环处理同一话题内的状态和消息,适合维持顺序号、订阅状态和广播一致性。扩展时不应在 Topic 事件循环中执行耗时的远程 RPC、全文搜索或复杂审批事务,否则会阻塞该话题的全部消息。
|
||||
|
||||
### 3.2 已有能力清单
|
||||
|
||||
| 能力 | 当前实现 | 主要源码位置 | 复用判断 |
|
||||
| --- | --- | --- | --- |
|
||||
| 账号创建与登录 | `acc`、`login` 协议;basic、token、REST 等认证方式 | `server/session.go`、`server/auth` | 已支持基础能力 |
|
||||
| 手机验证码 | 电话号码校验、验证码、重试控制,可配置 Twilio | `server/validate/tel/validate.go` | 可用于注册/找回密码,需补产品流程 |
|
||||
| 单聊与群聊 | P2P、group、channel Topic | `server/topic.go` | 已支持 |
|
||||
| 会话成员与 ACL | `ModeWant`、`ModeGiven`,包含 Join、Read、Write、Admin、Owner 等权限 | `server/topic.go`、`server/store/types` | 已支持底层权限 |
|
||||
| 消息收发 | 消息持久化、Topic 内递增序号、广播、离线推送 | `server/topic.go:997`、`server/store/store.go` | 已支持 |
|
||||
| 已读/送达/输入中 | `note` 的 `read`、`recv`、`kp` 等事件 | `server/datamodel.go:309`、`server/topic.go:1137` | 已支持 |
|
||||
| 消息扩展内容 | `head` 和 `content` 为通用 JSON;支持 Drafty | `server/datamodel.go:265`、`server/store/types/types.go:1213` | 能承载,缺少产品级规范与校验 |
|
||||
| 用户/群资料扩展 | Public、Trusted、Private、Aux、Tags | `server/datamodel.go`、`server/topic.go:2202` | 能承载少量扩展信息 |
|
||||
| 收藏话题 | 存在 `slf` Topic 类别 | `server/topic.go`、`server/store/types` | 可复用,但缺少完整收藏模型与检索 |
|
||||
| 文件 | HTTP 上传、元数据保存、附件与消息关联 | `server/hdl_files.go`、`server/store/store.go` | 基础可用,下载授权需修复 |
|
||||
| 音视频信令 | 邀请、接听、Offer、Answer、ICE、挂断;单聊中维护一个当前呼叫 | `server/calls.go` | 可复用,需产品化扩展 |
|
||||
| 推送 | 可插拔 Push Handler,包含消息和通话字段 | `server/push.go`、`server/push` | 已支持基础能力 |
|
||||
| 外部风控/审核 | gRPC 插件可继续、丢弃、替换或直接响应协议包 | `server/plugins.go:353` | 适合接外部审核,不替代内部授权 |
|
||||
|
||||
### 3.3 当前核心数据模型
|
||||
|
||||
当前模型聚焦通信内核:
|
||||
|
||||
| 实体 | 关键字段 | 缺少的产品概念 |
|
||||
| --- | --- | --- |
|
||||
| User | 状态、访问权限、Public、Trusted、Tags、设备 | 企业、部门、好友关系、实名认证状态、青少年策略 |
|
||||
| Topic | Owner、默认 ACL、Public、Trusted、Tags、Aux | 入群策略、群公告、禁言规则、群二维码、企业归属 |
|
||||
| Subscription | Topic、User、ModeWant、ModeGiven、Private | 备注、置顶、通知策略、单独禁言、好友状态 |
|
||||
| Message | Topic、SeqId、From、Head、Content | 客户端幂等 ID、消息类型索引、全文检索字段、审核状态 |
|
||||
| FileDef | 文件位置、状态、上传用户等 | 资源级访问策略、业务用途、病毒扫描状态 |
|
||||
|
||||
MySQL 当前消息表只对 `(topic, seqid)` 建唯一索引,没有消息类型索引、客户端消息 ID 唯一索引或全文检索索引。分析基线中各适配器均为 schema 116;当前 MySQL 已因租户主表升级到 118,PostgreSQL、MongoDB、RethinkDB 仍为 116。
|
||||
|
||||
### 3.4 当前主要缺口
|
||||
|
||||
1. `MsgGetQuery` 只支持 desc、sub、data、del,消息查询只有序号范围和数量,没有关键词、媒体类型、发送人、时间区间等检索条件。
|
||||
2. 发布消息时没有客户端消息幂等校验,`server/session.go:691` 仍保留重复消息 ID 检查 TODO。
|
||||
3. 当前群成员模型能表达 ACL,但不能完整表达申请加入、审批记录、过期状态和审批理由。直接用 `ModeGiven` 表示待审批会与当前订阅逻辑冲突。
|
||||
4. 当前群“静音”更接近通知/Presence 控制,不等同于禁止指定成员发言;服务端缺少群禁言策略。
|
||||
5. 文件下载只校验 API Key 和登录用户,没有根据文件关联消息、Topic ACL、发送者或接收者判断访问权限。
|
||||
6. WebRTC 只维护 P2P Topic 的一个当前呼叫,缺少稳定的业务 `call_id`、跨设备抢答、通话详单、失败原因规范和群通话能力。
|
||||
7. 没有企业、组织、部门、会议、日报、月报、信箱等领域模型。
|
||||
8. 没有实名认证审批、反诈策略、举报工单、反馈工单、青少年模式的服务端强制规则。
|
||||
9. 没有租户字段;共享数据库的企业 SaaS 部署会有数据越界风险。
|
||||
|
||||
## 4. 总体改造原则
|
||||
|
||||
### 4.1 保留通信内核,新增产品业务层
|
||||
|
||||
不建议将会议、日报、实名认证等业务硬塞进 Topic 的 Public/Aux JSON。Aux 适合兼容性元数据,不适合作为需要索引、唯一约束、状态机、审批记录和审计日志的主数据。
|
||||
|
||||
推荐分为四层:
|
||||
|
||||
1. **Tinode 实时内核**:Session、Hub、Topic、ACL、Message、Push、WebRTC 信令;
|
||||
2. **IM 产品策略层**:结构化消息解析、发消息前鉴权、好友/黑名单/群禁言/青少年模式校验;
|
||||
3. **业务服务层**:好友申请、群审批、组织、会议、报告、实名认证、举报等 HTTP API;
|
||||
4. **业务存储与事件层**:业务表、事务、Outbox、审计日志、搜索索引。
|
||||
|
||||
### 4.2 目标架构
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
APP["iOS / Android / Web"]
|
||||
|
||||
subgraph Gateway["同一 Go 服务进程或同域网关"]
|
||||
WS["现有实时协议入口"]
|
||||
API["新增 /v1/im/* 业务 API"]
|
||||
FILE["文件上传/下载"]
|
||||
end
|
||||
|
||||
subgraph Core["通信与产品策略"]
|
||||
SESSION["Session / Hub / Topic"]
|
||||
POLICY["IM Policy Engine"]
|
||||
CALL["Call Signaling"]
|
||||
NOTIFY["Notification Bridge"]
|
||||
end
|
||||
|
||||
subgraph Business["业务模块"]
|
||||
REL["好友与黑名单"]
|
||||
GROUP["群治理"]
|
||||
ORG["组织与工作台"]
|
||||
SAFETY["实名 / 青少年 / 举报 / 风控"]
|
||||
SEARCH["消息与文件索引"]
|
||||
end
|
||||
|
||||
subgraph Infra["存储与外部系统"]
|
||||
DB["核心表 + im_* 业务表"]
|
||||
OUTBOX["Outbox / 异步任务"]
|
||||
MOD["外部审核与反诈服务"]
|
||||
PUSH["Push / SMS / 邮件"]
|
||||
MEDIA["对象存储 / 转码 / 病毒扫描"]
|
||||
end
|
||||
|
||||
APP --> WS
|
||||
APP --> API
|
||||
APP --> FILE
|
||||
WS --> SESSION
|
||||
SESSION --> POLICY
|
||||
POLICY --> DB
|
||||
API --> Business
|
||||
Business --> DB
|
||||
Business --> OUTBOX
|
||||
OUTBOX --> NOTIFY
|
||||
NOTIFY --> SESSION
|
||||
SESSION --> CALL
|
||||
POLICY --> MOD
|
||||
FILE --> MEDIA
|
||||
SESSION --> PUSH
|
||||
```
|
||||
|
||||
### 4.3 WebSocket 与 HTTP 的职责边界
|
||||
|
||||
| 场景 | 推荐通道 | 原因 |
|
||||
| --- | --- | --- |
|
||||
| 发消息、回执、输入中、在线事件、通话信令 | 现有 WebSocket | 低延迟、已有 Topic 路由和广播 |
|
||||
| 好友申请/审批、群申请/审批 | HTTP 写操作 + WS 通知 | 需要幂等、状态机、审计和列表查询 |
|
||||
| 组织架构、日报/月报、会议管理 | HTTP API | 强查询和事务型业务,不应阻塞 Topic |
|
||||
| 消息关键词/类型搜索 | HTTP 或扩展后的 `get` | 需要独立索引、分页游标和查询权限 |
|
||||
| 实名认证、举报、反馈、版本配置 | HTTP API | 涉及附件、敏感字段、工单和后台管理 |
|
||||
| 文件上传/下载 | HTTP | 沿用当前链路,但补资源级授权 |
|
||||
| 业务状态变化提醒 | 结构化系统消息/系统 Topic | 复用实时和推送能力 |
|
||||
|
||||
## 5. 11 类原型需求的服务端适配方案
|
||||
|
||||
### 5.1 账号注册、登录与身份安全
|
||||
|
||||
| 原型能力 | 现状 | 改造方案 | 优先级 |
|
||||
| --- | --- | --- | --- |
|
||||
| 企业码登录 | 无企业模型 | 新增企业码解析接口;企业码映射到企业、部署地址和登录策略 | P0 |
|
||||
| 手机号/密码登录 | 认证框架已有 | 固化账号标识规范、失败次数、锁定时长、设备记录和安全审计 | P0 |
|
||||
| 注册和验证码倒计时 | 电话验证器已有 | 新增注册编排接口;倒计时由客户端显示,服务端返回 `retry_after`、过期时间和重试上限 | P0 |
|
||||
| 忘记密码 | 有认证与验证器,无完整流程 | 增加发送验证码、校验票据、重置密码三个阶段;票据一次性、短时有效 | P0 |
|
||||
| 设置独立密码 | 可更新账号,但无产品流程 | 增加二次验证和密码策略;写审计日志并撤销旧 Token | P1 |
|
||||
| 实名认证 | 无 | 新建实名申请、状态查询、审核回调;敏感字段加密,客户端仅返回脱敏结果 | P1 |
|
||||
|
||||
企业码有两种实现方式:
|
||||
|
||||
- **推荐第一阶段:一企一实例/一企一库。** 企业码只负责发现该企业的服务地址和认证方式。现有核心表无需立即加入 `tenant_id`,隔离边界清晰。
|
||||
- **共享 SaaS:多企业共用服务和数据库。** 必须先完成第 6 节的全链路租户隔离,再开放企业码登录。
|
||||
|
||||
建议新增接口:
|
||||
|
||||
```text
|
||||
POST /v1/im/auth/enterprise/resolve
|
||||
POST /v1/im/auth/sms/send
|
||||
POST /v1/im/auth/password/reset-ticket
|
||||
POST /v1/im/auth/password/reset
|
||||
POST /v1/im/identity/verifications
|
||||
GET /v1/im/identity/verifications/current
|
||||
```
|
||||
|
||||
不要把身份证号、证件照片地址写入 User.Public、User.Trusted 或消息内容。实名数据应单独加密存储,并限制后台角色、访问理由和留痕。
|
||||
|
||||
### 5.2 消息首页、全局导航与系统通知
|
||||
|
||||
| 原型能力 | 现状 | 改造方案 | 优先级 |
|
||||
| --- | --- | --- | --- |
|
||||
| 会话列表 | Subscription 已有未读、时间等信息 | 扩展每用户会话设置:置顶、免打扰、草稿、隐藏;草稿可优先本地保存 | P1 |
|
||||
| 离线/重连 | Session 支持连接和队列 | 客户端指数退避;服务端增加连接状态指标、消息同步游标和限流 | P0 |
|
||||
| 长按置顶/删除/免打扰 | 部分可由 Private/订阅状态承载 | 明确 `im_conversation_settings`,避免 Private JSON 无法查询和约束 | P1 |
|
||||
| 系统消息 | 可创建系统 Topic/系统账号 | 建立系统消息类型字典、模板版本、跳转参数和去重键 | P1 |
|
||||
|
||||
系统消息应使用正常的 Message 存储和 Push 链路,而不是只发临时 `info` 包。这样用户离线后仍可拉取、已读和跨设备同步。
|
||||
|
||||
### 5.3 单聊会话、会话建立与消息状态
|
||||
|
||||
| 原型能力 | 现状 | 改造方案 | 优先级 |
|
||||
| --- | --- | --- | --- |
|
||||
| 单聊建立 | P2P Topic 已支持 | 在创建/订阅前检查好友、黑名单和隐私策略 | P0 |
|
||||
| 在线、输入中 | Presence、`kp` 已支持 | 统一客户端节流和过期时间;按隐私设置决定是否透出 | P1 |
|
||||
| 已发送、已送达、已读 | Topic 序号和 `recv/read` 已支持 | 定义产品状态映射;群聊展示规则单独定义 | P0 |
|
||||
| 拉黑后限制沟通 | 无完整关系表 | 关系表作为真源;发布、建会话、呼叫、文件访问均执行同一策略 | P0 |
|
||||
| 删除消息/撤回 | 有消息删除能力 | 区分“仅自己删除”和“全员撤回”;增加撤回时限、操作者和审计事件 | P1 |
|
||||
| 名片消息 | 通用 JSON 可承载 | 定义 `contact_card` 类型,只保存稳定用户 ID 和展示快照,打开时重新鉴权 | P1 |
|
||||
|
||||
“拉黑”不能只修改客户端按钮或 Topic 私有字段。策略层至少要拦截:新建单聊、发送消息、发送好友请求、音视频呼叫、读取对方敏感资料。
|
||||
|
||||
### 5.4 消息输入、媒体内容与消息操作
|
||||
|
||||
当前 `head`、`content` 接受通用 JSON,技术上可以传文本、图片、语音、视频、文件、名片、合并转发等内容,但服务端没有类型白名单、字段校验、幂等、审核和索引。建议增加明确的消息信封:
|
||||
|
||||
```json
|
||||
{
|
||||
"pub": {
|
||||
"id": "client-request-id",
|
||||
"topic": "grp...",
|
||||
"head": {
|
||||
"x-im-schema": 1,
|
||||
"x-im-type": "image",
|
||||
"x-im-client-mid": "01J...",
|
||||
"x-im-reply-to": 128
|
||||
},
|
||||
"content": {
|
||||
"file_id": "f...",
|
||||
"width": 1080,
|
||||
"height": 1440,
|
||||
"thumbnail_file_id": "f..."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
服务端改造要求:
|
||||
|
||||
1. 在 `server/datamodel.go` 定义扩展字段常量和允许类型,不再由各客户端自由命名。
|
||||
2. 在 `server/session.go` 发布入口解析 `x-im-client-mid`,按“租户 + 发送者 + client_mid”去重;重复请求返回第一次成功的 Topic/SeqId。
|
||||
3. 在 `server/topic.go` 持久化前执行类型、大小、引用消息、附件所有权、好友/黑名单、禁言、青少年和风控策略。
|
||||
4. 文本继续使用 Drafty;图片、视频、语音、文件只在消息里保存 `file_id` 和必要元数据,不信任客户端提交的任意下载 URL。
|
||||
5. `@成员` 保存稳定用户 ID 列表;服务端校验被提醒者确实是当前群成员,并生成定向推送。
|
||||
6. 合并转发生成不可变快照,保留原作者展示名、时间和内容类型,但不得泄露原会话 ID 或无权访问的附件。
|
||||
7. 多选、键盘、表情面板、录音进度、上传进度属于客户端交互;服务端只负责资源、消息和失败状态。
|
||||
|
||||
推荐消息类型首批固定为:
|
||||
|
||||
```text
|
||||
text, image, voice, video, file, contact_card,
|
||||
location, merged_forward, system_notice, call_record
|
||||
```
|
||||
|
||||
转发应区分两种语义:
|
||||
|
||||
- **逐条转发**:为每条内容创建新消息,原附件通过授权后重新建立引用;
|
||||
- **合并转发**:创建一个 `merged_forward` 快照,子项限制数量和总大小,服务端清理敏感字段。
|
||||
|
||||
### 5.5 通讯录、好友关系与黑名单
|
||||
|
||||
当前 Subscription 表示“用户订阅了哪个 Topic”,不能替代双边好友关系和申请状态机。建议新增:
|
||||
|
||||
- `im_user_relations`:双方关系、备注、黑名单、免打扰等当前状态;
|
||||
- `im_friend_requests`:申请、处理人、验证信息、状态、过期时间和幂等键;
|
||||
- `im_privacy_settings`:允许通过手机号/ID/二维码查找、加好友验证方式等。
|
||||
|
||||
好友流程:
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Pending: 发起申请
|
||||
Pending --> Accepted: 接受
|
||||
Pending --> Rejected: 拒绝
|
||||
Pending --> Cancelled: 发起人撤回
|
||||
Pending --> Expired: 超时
|
||||
Accepted --> Deleted: 任一方删除
|
||||
Accepted --> Blocked: 任一方拉黑
|
||||
Blocked --> Accepted: 取消拉黑且关系仍保留
|
||||
```
|
||||
|
||||
实现要点:
|
||||
|
||||
1. 精准用户 ID 搜索必须受隐私设置、租户和账号状态限制;手机号搜索应使用规范化哈希进行匹配,禁止模糊枚举。
|
||||
2. 接受好友申请和建立双方关系必须在同一数据库事务内完成,并通过 Outbox 异步创建/激活 P2P Topic、发送系统通知。
|
||||
3. 删除好友是否删除 P2P 历史需产品确认。建议只解除关系,不物理删除历史;双方分别控制会话是否可见。
|
||||
4. 拉黑属于单方策略,服务端立即生效。是否同时解除好友关系应作为明确产品规则,不由客户端猜测。
|
||||
5. 关系变化必须使 ACL/策略缓存失效,避免短时间内继续发消息。
|
||||
|
||||
建议接口:
|
||||
|
||||
```text
|
||||
GET /v1/im/contacts
|
||||
GET /v1/im/users/exact-search?id=...
|
||||
POST /v1/im/friend-requests
|
||||
GET /v1/im/friend-requests
|
||||
POST /v1/im/friend-requests/{id}/accept
|
||||
POST /v1/im/friend-requests/{id}/reject
|
||||
DELETE /v1/im/contacts/{user_id}
|
||||
PUT /v1/im/contacts/{user_id}/block
|
||||
DELETE /v1/im/contacts/{user_id}/block
|
||||
GET /v1/im/blocks
|
||||
```
|
||||
|
||||
### 5.6 群聊创建、资料维护与成员治理
|
||||
|
||||
现有群 Topic、Owner/Admin ACL 和所有者转移可以复用。需要新增群业务规则和审批数据。
|
||||
|
||||
| 原型能力 | 可复用基础 | 新增服务端能力 |
|
||||
| --- | --- | --- |
|
||||
| 创建群、群资料 | Group Topic、Public/Trusted/Aux | 资料字段规范、头像资源校验、敏感词审核 |
|
||||
| 加入群聊 | Subscription/Join ACL | 入群方式、申请表、审批状态机、人数和频率限制 |
|
||||
| 邀请进群 | Admin/Share ACL | 邀请记录、邀请权限、被邀请人确认、批量幂等 |
|
||||
| 群公告 | 消息可承载 | 公告主表、版本、置顶、已读回执、发布权限 |
|
||||
| 管理员 | Admin ACL | 管理员人数上限、操作审计、角色显示 |
|
||||
| 禁言成员 | 无对应强制策略 | 单人/全员禁言表和发布前校验,管理员豁免规则 |
|
||||
| 转让群主 | 已有 Owner 转移逻辑 | 二次确认、风险校验、审计和通知 |
|
||||
| 群二维码 | 无 | 签名短 Token、有效期、次数限制、撤销和入群策略 |
|
||||
| 群文件 | 文件与消息已有关联 | 群文件索引、上传者/类型/时间筛选、删除权限 |
|
||||
|
||||
当前订阅代码在新订阅者没有 Join 权限时会拒绝加入,因此不能仅用“`ModeGiven` 不含 Join”来保存待审批申请。正确做法是先写 `im_group_join_requests`,审批通过后再创建或更新 Subscription。
|
||||
|
||||
群禁言必须在消息持久化前检查,建议规则优先级为:
|
||||
|
||||
```text
|
||||
账号停用 > 黑名单/关系限制 > 全员禁言 > 单人禁言 > Topic Write ACL > 允许发送
|
||||
```
|
||||
|
||||
群二维码只应包含服务端生成的短 Token,不应直接暴露可永久加入的 Topic ID。Token 至少绑定群、创建人、过期时间、最大使用次数和当前入群策略,并使用服务端密钥签名或只保存其哈希。
|
||||
|
||||
### 5.7 文件下载、收藏管理与聊天记录检索
|
||||
|
||||
#### 文件访问安全
|
||||
|
||||
这是当前代码中最明确的安全缺口。`server/hdl_files.go` 的下载流程验证 API Key 和已登录用户后即可读取 URL 对应文件,没有验证该用户是否是上传者、是否属于关联消息的 Topic、该消息是否已撤回,或文件是否为公开资源。
|
||||
|
||||
建议新增统一的 `FileCanAccess(uid, fileID, action)`:
|
||||
|
||||
1. 查询 FileDef 和 file-message links;
|
||||
2. 上传未完成的文件只允许上传者访问;
|
||||
3. 已关联消息的文件要求用户对至少一个关联 Topic 具有 Read 权限;
|
||||
4. 群文件删除要求上传者、群管理员或群主权限;
|
||||
5. 转发附件时重新建立授权关联,不暴露底层存储永久地址;
|
||||
6. 使用短时签名下载 URL,记录下载审计;
|
||||
7. 对上传文件执行 MIME 嗅探、扩展名一致性、病毒扫描、图片/视频解码安全检查。
|
||||
|
||||
#### 收藏
|
||||
|
||||
`slf` Topic 可以继续承担“收藏内容跨设备同步”的消息容器,但建议增加 `im_favorites` 索引表,保存源消息、收藏消息、类型、创建时间和已清理的展示摘要。源消息删除后,收藏快照是否保留需按合规策略处理。
|
||||
|
||||
#### 聊天记录检索
|
||||
|
||||
当前 `MessageGetAll` 仅支持 Topic、序号范围和数量,不支持关键词或类型。建议新增 `im_message_index`:
|
||||
|
||||
| 字段 | 用途 |
|
||||
| --- | --- |
|
||||
| tenant_id / topic / seq_id | 权限过滤和定位原消息 |
|
||||
| sender_id / message_type | 发送人和类型筛选 |
|
||||
| sent_at | 时间范围和排序 |
|
||||
| searchable_text | 文本、文件名、链接标题等可搜索文本 |
|
||||
| file_id / mime_group | 文件、图片、视频分类 |
|
||||
| deleted / moderation_state | 排除已删除或不可见内容 |
|
||||
|
||||
若仅支持 MySQL,可使用 InnoDB FULLTEXT 配合 `ngram` 分词;若需要更强中文检索、跨话题检索和高数据量,建议使用 OpenSearch/Elasticsearch。无论使用哪种索引,查询结果都必须再次以当前 Topic Read ACL、消息删除范围和租户进行过滤,不能把搜索索引当作授权来源。
|
||||
|
||||
需要提供全量回填任务和增量 Outbox 消费者,避免上线后只能搜索新消息。
|
||||
|
||||
### 5.8 实时语音与视频通话
|
||||
|
||||
现有 `server/calls.go` 已支持 P2P 呼叫的邀请、接听、Offer、Answer、ICE Candidate 和挂断,可作为首期基础。改造建议:
|
||||
|
||||
1. 为每次呼叫生成不可复用的 `call_id`,不要只用 Topic 和消息 SeqId 表示一次通话;
|
||||
2. 增加 `voice/video` 类型、主叫/被叫设备、创建/响铃/接听/结束时间、结束方、标准结束原因;
|
||||
3. 增加跨设备抢答:一台设备接听后,其他设备收到 `answered_elsewhere`;
|
||||
4. 呼叫前执行好友、黑名单、隐私、账号状态和青少年策略;
|
||||
5. 呼叫超时、拒接、忙线、取消分别生成一致的通话记录消息和推送;
|
||||
6. 配置可用的 STUN/TURN,监控 ICE 成功率和呼叫建立耗时;
|
||||
7. 麦克风、扬声器、摄像头开关和悬浮窗主要由客户端管理,服务端只同步必要的媒体状态事件;
|
||||
8. 群音视频不应扩展当前单个 `videoCall` 结构硬做,建议另建会议房间并接入 SFU。
|
||||
|
||||
### 5.9 企业工作台与办公协同
|
||||
|
||||
该部分属于全新业务域,当前服务端没有组织、部门、会议、日报、月报或信箱代码。建议它们作为独立业务模块,通过 Tinode 系统消息发送提醒,而不是修改聊天核心协议承载所有 CRUD。
|
||||
|
||||
#### 组织架构
|
||||
|
||||
- `im_org_units`:企业、部门、层级、排序、负责人、状态;
|
||||
- `im_org_members`:用户、部门、职位、主部门、在职状态和可见范围;
|
||||
- 支持树形查询、按姓名/部门搜索、部门成员分页;
|
||||
- 组织同步应有版本号,客户端可增量刷新;
|
||||
- 离职用户需在一个事务编排中禁用账号、退出企业群、撤销 Token 和处理数据归属。
|
||||
|
||||
#### 会议通知
|
||||
|
||||
- `im_meetings`:主题、时间、地点/会议链接、发起人、提醒策略、状态;
|
||||
- `im_meeting_participants`:参与人、回复状态、回复时间;
|
||||
- 创建/变更/取消写 Outbox,由通知桥接器向参与者系统 Topic 发结构化消息;
|
||||
- 定时提醒由任务调度器执行,任务必须使用幂等键避免重复推送;
|
||||
- 若未来增加在线视频会议,会议业务 ID 与通话房间 ID 分离。
|
||||
|
||||
#### 日报与月报
|
||||
|
||||
- 统一使用 `im_work_reports`,字段包含类型、周期、作者、部门、正文、附件、状态、提交时间;
|
||||
- 管理员列表以权限范围过滤,不以客户端传入部门为准;
|
||||
- 提交后是否可编辑、补交和撤回需形成明确状态机;
|
||||
- 汇总统计异步生成,不在请求中扫描所有报告。
|
||||
|
||||
#### 总经理信箱
|
||||
|
||||
- 使用 `im_mailbox_messages`,支持实名/匿名展示策略、正文、附件、处理状态和回复;
|
||||
- “匿名”只能对业务查看者隐藏,安全审计角色仍可在合法授权下追溯;
|
||||
- 附件走统一私有文件授权;
|
||||
- 每次查看和导出敏感信件都写审计日志。
|
||||
|
||||
### 5.10 个人资料、通用设置、隐私与青少年模式
|
||||
|
||||
| 能力 | 服务端责任 | 客户端责任 |
|
||||
| --- | --- | --- |
|
||||
| 昵称、生日、签名、头像 | 校验、敏感词、可见范围、版本和审计 | 编辑和展示 |
|
||||
| 个人二维码 | 生成短时签名 Token、隐私校验 | 展示、扫码 |
|
||||
| 清空缓存 | 无需删除服务端消息 | 清理本地缓存和下载文件 |
|
||||
| 版本更新 | 返回平台、渠道、最低版本、推荐版本、下载地址和更新文案 | 下载、安装、商店跳转 |
|
||||
| 隐私设置 | 作为服务端授权条件强制执行 | 提供开关界面 |
|
||||
| 青少年模式 | 服务端保存状态、PIN 哈希、策略版本并强制限制 | UI 切换和本地内容隐藏 |
|
||||
| 推送/相机/麦克风权限 | 保存推送 Token 和提醒偏好 | 调用系统权限 API |
|
||||
|
||||
青少年模式不能只是客户端开关,否则卸载重装或换设备即可绕过。建议 `im_teen_mode` 保存启用状态、监护 PIN 的强哈希、失败锁定、允许时段、功能策略版本和更新时间。服务端在登录、搜索、加好友、发消息、通话和工作台入口统一检查策略。
|
||||
|
||||
版本接口示例:
|
||||
|
||||
```text
|
||||
GET /v1/im/config/app-version?platform=android&channel=official&version=1.4.0
|
||||
```
|
||||
|
||||
响应需区分 `latest`、`recommended`、`minimum_supported` 和 `force_update`。客户端系统权限文案和弹窗不需要服务端逐页参与,但可以通过配置中心提供版本化文案。
|
||||
|
||||
### 5.11 反诈安全、举报与意见反馈
|
||||
|
||||
建议形成三层控制:
|
||||
|
||||
1. **同步硬规则**:账号状态、黑名单、限频、群禁言、文件大小/类型等,必须在服务端本地快速判断;
|
||||
2. **内容与风险审核**:通过现有 gRPC Plugin Firehose 或独立审核服务检查文本、图片和链接;
|
||||
3. **事后处置**:举报工单、证据固化、审核、处罚、申诉和审计。
|
||||
|
||||
现有插件在协议分发之前同步执行,严格审核会直接增加消息延迟。外部审核调用必须设置较短超时、熔断、指标和明确的 fail-open/fail-closed 策略。高风险场景可以先将消息标为 `pending_review`,审核通过后再广播;低风险场景可先发后审并支持撤回。
|
||||
|
||||
新增数据模型:
|
||||
|
||||
- `im_reports`:举报对象、原因、描述、提交人、状态、处理人、结论;
|
||||
- `im_report_evidence`:消息快照、文件引用、校验哈希;
|
||||
- `im_feedback`:意见类型、正文、联系方式、附件、处理状态;
|
||||
- `im_risk_events`:命中规则、风险等级、处置、模型/规则版本;
|
||||
- `im_audit_logs`:操作者、动作、对象、结果、IP、设备和关联请求 ID。
|
||||
|
||||
举报消息时不要只保存可变的 Topic/SeqId,应在用户仍有读取权限时由服务端固化必要证据快照和内容哈希。后台读取证据需要单独权限和完整审计。
|
||||
|
||||
## 6. 企业租户隔离决策
|
||||
|
||||
### 6.1 当前事实
|
||||
|
||||
当前 User、Topic、Subscription、Message 和 FileDef 没有 `tenant_id`。用户标签和 Topic 标签也没有企业隔离维度。仅在登录入口识别企业码,不能保证后续搜索、消息、文件和后台查询不会跨企业。
|
||||
|
||||
### 6.2 推荐方案:第一阶段一企一实例/一企一库
|
||||
|
||||
企业码网关返回企业专属服务地址。每个实例使用独立数据库、对象存储前缀、推送配置和审核配置。这种方案最贴合当前架构,改造量和误配风险最低,也便于企业级数据迁移与删除。
|
||||
|
||||
需要新增:
|
||||
|
||||
- 企业码目录服务;
|
||||
- 企业到服务地址/认证策略的映射;
|
||||
- 配置和密钥隔离;
|
||||
- 统一运维、监控和版本发布能力。
|
||||
|
||||
### 6.3 共享 SaaS 的必要改造
|
||||
|
||||
如果明确要求多个企业共用一套服务和数据库,则以下工作必须先于产品功能:
|
||||
|
||||
1. 为核心表和所有业务表增加 `tenant_id`;
|
||||
2. 所有唯一索引包含租户维度,例如用户登录标识、Topic、客户端消息 ID;
|
||||
3. Session 完成认证后固定 tenant,禁止客户端包覆盖;
|
||||
4. Topic 查找、用户查找、订阅、消息读写、文件访问和搜索全部强制带 tenant;
|
||||
5. 内存缓存、Hub 路由键、集群路由和 Push 任务均包含 tenant;
|
||||
6. 系统账号和系统 Topic 按企业创建;
|
||||
7. 后台管理账号采用企业范围和数据范围授权;
|
||||
8. 增加跨租户攻击自动化测试,覆盖枚举 ID、文件 URL、搜索和转发。
|
||||
|
||||
这不是加一个字段的局部修改,而是横跨认证、存储接口、全部数据库适配器、缓存、集群路由和测试的基础工程。
|
||||
|
||||
## 7. 协议与 API 改造
|
||||
|
||||
### 7.1 保持现有协议兼容
|
||||
|
||||
继续支持现有 `hi`、`acc`、`login`、`sub`、`leave`、`pub`、`get`、`set`、`del`、`note`。新客户端通过 `x-im-schema` 声明产品扩展版本;旧客户端没有扩展字段时仍按现有路径工作。
|
||||
|
||||
推荐新增固定消息头:
|
||||
|
||||
| 字段 | 必填条件 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `x-im-schema` | 产品消息必填 | 扩展协议版本 |
|
||||
| `x-im-type` | 产品消息必填 | 消息类型枚举 |
|
||||
| `x-im-client-mid` | 普通消息必填 | 客户端生成的幂等 ID |
|
||||
| `x-im-reply-to` | 回复消息时 | 被回复消息 SeqId |
|
||||
| `x-im-forward-mode` | 转发时 | `single` 或 `merged` |
|
||||
| `x-im-risk-state` | 仅服务端写 | 审核状态,客户端提交值必须忽略 |
|
||||
|
||||
### 7.2 消息幂等
|
||||
|
||||
新增唯一约束:
|
||||
|
||||
```text
|
||||
(tenant_id, sender_id, client_mid) UNIQUE
|
||||
```
|
||||
|
||||
一企一库时可省略物理 `tenant_id`,但代码接口仍建议预留租户上下文。幂等记录需要保存最终 Topic、SeqId 和响应摘要。消息保存、Topic 最新序号、订阅更新目前通过多次 Store 调用完成;新增幂等时应明确事务边界,避免数据库写成功但回复丢失后重试产生两条消息。
|
||||
|
||||
### 7.3 查询扩展
|
||||
|
||||
不建议继续把搜索条件塞进未定义的任意 JSON。可在 `MsgGetQuery` 中增加明确字段:
|
||||
|
||||
```go
|
||||
type MsgSearchQuery struct {
|
||||
Query string `json:"query,omitempty"`
|
||||
Types []string `json:"types,omitempty"`
|
||||
From string `json:"from,omitempty"`
|
||||
SinceTime string `json:"since_time,omitempty"`
|
||||
BeforeTime string `json:"before_time,omitempty"`
|
||||
Cursor string `json:"cursor,omitempty"`
|
||||
Limit int `json:"limit,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
如果搜索由独立服务承担,则提供 `/v1/im/search/messages`,返回 Topic、SeqId 和安全摘要,客户端再通过原生 `get data` 拉取上下文。任何方案都必须在服务端重新做 Topic ACL 检查。
|
||||
|
||||
### 7.4 业务 API 约定
|
||||
|
||||
新增 `/v1/im/*` 路由,复用 Tinode Token 的认证结果。应把 `hdl_files.go` 中现有的请求认证逻辑抽取成通用 HTTP 中间件,而不是让每个 Handler 自行解析 Token。
|
||||
|
||||
所有写接口统一支持:
|
||||
|
||||
- `X-Request-ID`:链路追踪;
|
||||
- `Idempotency-Key`:创建和审批的幂等控制;
|
||||
- 统一错误结构:`code`、`message`、`details`、`request_id`;
|
||||
- 游标分页,不使用数据增长时不稳定的大 Offset;
|
||||
- 服务端从认证上下文取用户和租户,不接受正文伪造;
|
||||
- 状态变更写审计日志和 Outbox。
|
||||
|
||||
## 8. 建议新增的数据模型
|
||||
|
||||
以下是逻辑模型,正式建表前需补充字段长度、外键策略、冷热分层和数据保留周期。
|
||||
|
||||
| 表 | 核心字段 | 关键约束/索引 | 作用 |
|
||||
| --- | --- | --- | --- |
|
||||
| `im_user_relations` | user_id、peer_id、state、remark、blocked、muted | 双向记录唯一;peer/state 索引 | 好友、备注、拉黑和通知关系 |
|
||||
| `im_friend_requests` | id、from、to、message、state、expires_at | 待处理申请去重;to/state/time | 好友申请状态机 |
|
||||
| `im_conversation_settings` | user_id、topic、pinned、muted、hidden | user/topic 唯一 | 会话个性化设置 |
|
||||
| `im_privacy_settings` | user_id、search flags、friend policy | user 唯一 | 精准搜索和加好友授权 |
|
||||
| `im_group_settings` | topic、join_policy、invite_policy、mute_all | topic 唯一 | 群业务规则 |
|
||||
| `im_group_join_requests` | id、topic、applicant、inviter、state | topic/applicant/pending 去重 | 入群申请与审批 |
|
||||
| `im_group_mutes` | topic、user、expires_at、operator | topic/user 唯一;expires 索引 | 单人禁言 |
|
||||
| `im_group_announcements` | id、topic、version、content、publisher | topic/status/time | 群公告版本 |
|
||||
| `im_group_announcement_reads` | announcement_id、user、read_at | announcement/user 唯一 | 公告已读 |
|
||||
| `im_group_invite_tokens` | token_hash、topic、expires、max_uses | token_hash 唯一;expires | 群二维码/邀请链接 |
|
||||
| `im_message_idempotency` | sender、client_mid、topic、seq_id | sender/client_mid 唯一 | 消息去重 |
|
||||
| `im_message_index` | topic、seq_id、type、text、file_id、sent_at | topic/seq 唯一;搜索索引 | 消息、文件和媒体检索 |
|
||||
| `im_favorites` | user、favorite_seq、source topic/seq、type | user/time;源消息索引 | 收藏筛选与分享 |
|
||||
| `im_identity_verifications` | user、encrypted PII、state、review | user/current-state;request id | 实名认证和审计 |
|
||||
| `im_org_units` | id、parent_id、name、path、state | parent/sort;path | 企业组织树 |
|
||||
| `im_org_members` | user、org_unit、title、primary | unit/state;user | 部门成员关系 |
|
||||
| `im_meetings` | id、organizer、title、start/end、state | organizer/time;state/time | 会议通知 |
|
||||
| `im_meeting_participants` | meeting、user、response | meeting/user 唯一 | 参会状态 |
|
||||
| `im_work_reports` | type、period、author、department、content、state | author/type/period;dept/time | 日报/月报 |
|
||||
| `im_mailbox_messages` | id、sender、content、anonymous、state | state/time;sender/time | 总经理信箱 |
|
||||
| `im_user_settings` | user、setting key/value、version | user/key 唯一 | 跨设备设置 |
|
||||
| `im_teen_mode` | user、enabled、pin_hash、policy_version | user 唯一 | 青少年策略 |
|
||||
| `im_reports` | reporter、target_type/id、reason、state | state/time;target | 举报工单 |
|
||||
| `im_report_evidence` | report、snapshot、file、hash | report | 证据固化 |
|
||||
| `im_feedback` | user、category、content、state | state/time;user/time | 意见反馈 |
|
||||
| `im_risk_events` | actor、object、rule、level、action | actor/time;object | 风控记录 |
|
||||
| `im_audit_logs` | actor、action、object、result、request_id | actor/time;object/time | 管理和敏感操作审计 |
|
||||
| `im_outbox` | aggregate、event_type、payload、state | state/next_retry;dedupe key | 事务后可靠通知 |
|
||||
|
||||
所有表在共享 SaaS 模式下都必须包含 `tenant_id`,并将它放入唯一约束和查询前缀。软删除字段不能代替状态机和审计记录。
|
||||
|
||||
## 9. 代码级改造清单
|
||||
|
||||
### 9.1 现有文件修改
|
||||
|
||||
| 文件 | 建议修改 |
|
||||
| --- | --- |
|
||||
| `server/datamodel.go` | 定义产品消息头、消息类型、查询过滤和错误码;保持旧协议兼容 |
|
||||
| `server/session.go` | 发布前幂等校验;调用产品策略层;统一映射业务错误;认证后绑定租户上下文 |
|
||||
| `server/topic.go` | 发布前关系/禁言/青少年策略;结构化系统消息;群管理状态变化后的缓存失效 |
|
||||
| `server/calls.go` | 引入 call_id、设备维度、结束原因、抢答与通话详单;群通话走独立模块 |
|
||||
| `server/push.go` | 按消息类型生成 Push;支持 @提醒、审批、会议和审核状态;避免敏感内容出现在锁屏 |
|
||||
| `server/hdl_files.go` | 抽取通用 HTTP 认证;下载增加 FileCanAccess;签名 URL;扫描状态检查 |
|
||||
| `server/plugins.go` | 为外部审核增加超时、指标、熔断和场景化失败策略 |
|
||||
| `server/main.go` | 注册 `/v1/im/*` 路由、业务模块生命周期、健康检查和后台任务 |
|
||||
| `server/store/store.go` | 增加幂等、文件授权和搜索所需的核心接口,明确事务边界 |
|
||||
| `server/store/types/types.go` | 增加明确的扩展类型;若共享 SaaS,核心实体加入 TenantId |
|
||||
| `server/db/mysql/adapter.go` | schema 升级、索引、幂等和核心查询实现 |
|
||||
| `server/db/mysql/schema.sql` | 与 adapter 中建表 SQL 同步,禁止只改其中一份 |
|
||||
| `tinode.conf` | 产品模块、租户、搜索、审核、TURN、文件扫描和限流配置 |
|
||||
|
||||
### 9.2 建议新增包
|
||||
|
||||
```text
|
||||
server/imapi/ HTTP 路由、请求校验、响应和认证中间件
|
||||
server/impolicy/ 好友、黑名单、群禁言、隐私、青少年等同步策略
|
||||
server/immessage/ 消息类型注册、结构校验、摘要与转发净化
|
||||
server/imbiz/ 好友、群治理、组织、会议、报告、实名、举报服务
|
||||
server/imstore/ 产品业务存储接口和 MySQL 实现
|
||||
server/imnotify/ Outbox 消费、系统消息和 Push 桥接
|
||||
server/imsearch/ 索引写入、回填、检索和 ACL 二次过滤
|
||||
server/imjobs/ 过期申请、会议提醒、清理、索引回填等后台任务
|
||||
```
|
||||
|
||||
### 9.3 数据库适配策略
|
||||
|
||||
当前项目同时包含 MySQL、PostgreSQL、MongoDB 和 RethinkDB 适配器。新增所有产品能力到公共 Store 接口会要求四套实现同步完成,改造面很大。
|
||||
|
||||
建议先明确产品正式支持的数据库:
|
||||
|
||||
- 如果生产只使用 MySQL,则产品业务 Store 首期实现 MySQL,并在启用 `im_product` 配置且底层不支持时拒绝启动,不能静默降级;
|
||||
- 核心消息幂等、文件授权等通信安全能力应进入公共接口,或明确产品版只支持 MySQL;
|
||||
- schema 版本从 116 升级时,四个适配器必须分别维护自己的版本,不能把 MySQL 已升级误写成所有数据库都支持;
|
||||
- `adapter.go` 中的建表 SQL 与 `schema.sql` 必须通过测试保持一致。
|
||||
|
||||
## 10. 事务、一致性与异步通知
|
||||
|
||||
好友审批、入群审批、会议创建等操作通常同时涉及业务表、Topic/Subscription 和通知。若直接依次调用,任何一步失败都会出现“状态已接受但没有会话”或“会议创建了但没人收到通知”。
|
||||
|
||||
建议采用本地事务 + Outbox:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant A as App
|
||||
participant API as IM Business API
|
||||
participant DB as Database
|
||||
participant W as Outbox Worker
|
||||
participant T as Tinode Topic
|
||||
|
||||
A->>API: 接受好友/入群申请(Idempotency-Key)
|
||||
API->>DB: 开启事务
|
||||
API->>DB: 更新申请与关系/成员状态
|
||||
API->>DB: 写入 Outbox 事件
|
||||
API->>DB: 提交事务
|
||||
API-->>A: 返回确定的业务状态
|
||||
W->>DB: 锁定未处理事件
|
||||
W->>T: 创建/更新订阅并发送系统通知
|
||||
W->>DB: 标记完成或计划重试
|
||||
```
|
||||
|
||||
Outbox 消费需要事件去重;状态变更 API 自身也需要 Idempotency-Key。对于必须与核心 Topic 数据原子提交的操作,应把它们放到同一数据库事务接口,而不是依赖最终一致性掩盖授权窗口。
|
||||
|
||||
## 11. 安全与合规基线
|
||||
|
||||
服务端改造至少应包含:
|
||||
|
||||
1. **认证安全**:密码强哈希、验证码限频、设备和 IP 风险、Token 撤销、失败锁定;
|
||||
2. **授权安全**:所有对象查询从服务端认证上下文获得租户和用户;文件、搜索、转发必须二次校验 ACL;
|
||||
3. **数据安全**:身份证等 PII 字段加密、密钥轮换、响应脱敏、日志禁止明文;
|
||||
4. **内容安全**:文本、图片、链接和文件审核;病毒扫描;风险策略版本化;
|
||||
5. **审计安全**:管理员、群主、实名审核、举报处置、匿名信箱访问全部留痕;
|
||||
6. **接口安全**:限流、请求大小、游标签名、幂等、防重放、统一输入校验;
|
||||
7. **隐私删除**:明确账号注销、聊天记录、收藏、举报证据和办公数据的不同保留周期;
|
||||
8. **推送隐私**:锁屏 Push 默认不携带敏感正文,由用户设置控制预览;
|
||||
9. **可观测性**:请求 ID、用户/租户匿名化标识、消息处理延迟、拒绝原因和插件超时指标。
|
||||
|
||||
## 12. 分阶段实施建议
|
||||
|
||||
### 阶段 P0:基线和关键决策
|
||||
|
||||
- 确定一企一实例还是共享 SaaS;
|
||||
- 确定产品版支持的数据库;
|
||||
- 清理旧设计文档与源码的版本漂移,建立 schema/协议单一事实来源;
|
||||
- 建立数据库迁移、回滚、CI 和环境配置基线;
|
||||
- 修复文件下载授权;
|
||||
- 实现消息客户端 ID 幂等;
|
||||
- 定义消息类型、错误码和兼容策略。
|
||||
|
||||
完成标准:安全缺口关闭;重复重试不会产生重复消息;旧客户端仍可通信;开发和测试环境可重复升级/回滚。
|
||||
|
||||
### 阶段 P1:账号、关系和基础聊天产品化
|
||||
|
||||
- 企业码发现、注册、验证码和忘记密码流程;
|
||||
- 实名认证基础流程;
|
||||
- 好友申请、联系人、黑名单和隐私设置;
|
||||
- 结构化文本/图片/语音/视频/文件/名片消息;
|
||||
- 会话置顶、免打扰、系统消息;
|
||||
- @提醒、回复、逐条转发、合并转发;
|
||||
- 基础风控和审计。
|
||||
|
||||
完成标准:原型第 1 至 5 类主流程可由真实客户端端到端完成。
|
||||
|
||||
### 阶段 P2:群治理、收藏、搜索和文件
|
||||
|
||||
- 入群申请、邀请审批、群公告;
|
||||
- 管理员、群主转让、全员/成员禁言;
|
||||
- 群二维码和群文件;
|
||||
- 收藏索引;
|
||||
- 消息关键词、文件、链接、图片和视频检索;
|
||||
- 历史数据回填和搜索 ACL 验证。
|
||||
|
||||
完成标准:原型第 6、7 类全部服务端流程可验收,搜索不会返回无权消息或附件。
|
||||
|
||||
### 阶段 P3:通话、设置和安全治理
|
||||
|
||||
- call_id、通话记录、跨设备抢答、TURN 指标;
|
||||
- 个人资料和二维码;
|
||||
- 版本配置、隐私设置、青少年模式强制策略;
|
||||
- 反诈提示策略、举报、证据、反馈和后台处置;
|
||||
- 内容审核超时/降级策略演练。
|
||||
|
||||
完成标准:原型第 8、10、11 类服务端能力可验收,关键安全策略无法通过换设备绕过。
|
||||
|
||||
### 阶段 P4:企业工作台
|
||||
|
||||
- 组织与部门;
|
||||
- 会议通知和提醒;
|
||||
- 日报、月报和管理视图;
|
||||
- 总经理信箱;
|
||||
- 离职编排、数据权限和敏感操作审计。
|
||||
|
||||
完成标准:原型第 9 类完成业务闭环,组织数据权限和通知可靠性通过测试。
|
||||
|
||||
## 13. 测试与验收方案
|
||||
|
||||
### 13.1 自动化测试层次
|
||||
|
||||
| 测试层 | 重点 |
|
||||
| --- | --- |
|
||||
| 单元测试 | 消息类型校验、状态机、权限矩阵、Token/二维码、错误映射 |
|
||||
| Store 集成测试 | schema 升降级、唯一约束、事务、并发审批、Outbox 重试 |
|
||||
| WebSocket 端到端 | 登录、订阅、消息、重复发送、回执、禁言、拉黑、通话信令 |
|
||||
| HTTP API 测试 | 好友/群审批、组织、报告、实名、举报、文件授权 |
|
||||
| 搜索测试 | 中文关键词、类型筛选、删除同步、权限变化、历史回填 |
|
||||
| 安全测试 | 跨用户/跨租户、文件 URL 猜测、ID 枚举、重放、越权审批 |
|
||||
| 故障测试 | 审核超时、Push 失败、Outbox 重试、数据库瞬断、服务重启 |
|
||||
| 真实设备测试 | Push、相机/麦克风、弱网重连、WebRTC NAT/TURN、跨设备抢答 |
|
||||
|
||||
### 13.2 必须覆盖的关键用例
|
||||
|
||||
1. 同一个 `x-im-client-mid` 并发重试 10 次,只产生一条消息并返回同一个 SeqId;
|
||||
2. 拉黑后已有长连接不能继续发消息或呼叫;
|
||||
3. 被禁言成员通过旧客户端或直接构造协议包仍被服务端拒绝;
|
||||
4. 非群成员持有文件 URL 时无法下载,退出群后访问策略符合产品约定;
|
||||
5. 搜索结果不包含已撤回、仅对本人删除、无 Read ACL 或其他租户的消息;
|
||||
6. 入群审批重复点击不会重复创建 Subscription;
|
||||
7. 群主转让、管理员变更和禁言都有审计记录且权限立即生效;
|
||||
8. 实名信息不会出现在日志、Push、Public/Trusted 和普通后台响应;
|
||||
9. 青少年模式换设备、重装或使用旧客户端仍受服务端限制;
|
||||
10. 外部审核服务超时后,系统严格按配置的降级策略处理并产生告警;
|
||||
11. Outbox 消费进程崩溃重启后,通知不丢失且业务副作用不重复;
|
||||
12. 多租户模式下所有 API、消息、文件、搜索和后台查询均完成负向越权测试。
|
||||
|
||||
### 13.3 当前基线验证
|
||||
|
||||
本次分析执行了以下不依赖外部数据库的测试:
|
||||
|
||||
```bash
|
||||
go test -count=1 ./server ./server/drafty ./server/media ./server/store/types ./server/db/common
|
||||
```
|
||||
|
||||
上述包当前全部通过。MySQL/PostgreSQL/MongoDB/RethinkDB 的真实数据库集成测试未在本次报告中执行,改造前应在 CI 中配置正式支持的数据库并固定运行。
|
||||
|
||||
## 14. 风险与待确认事项
|
||||
|
||||
| 决策 | 不确认的风险 | 建议 |
|
||||
| --- | --- | --- |
|
||||
| 企业隔离模式 | 后期补 tenant 会重写大量查询和数据 | P0 首先决策,优先一企一实例 |
|
||||
| 正式支持的数据库 | 四种适配器同步开发显著拖慢交付 | 产品业务首期明确 MySQL |
|
||||
| 删除好友后的历史 | 客户端与服务端理解不同导致数据争议 | 默认保留历史、禁止新消息 |
|
||||
| 收藏是否为独立副本 | 源消息删除后的合规行为不明确 | 明确快照和保留策略 |
|
||||
| 全员撤回时限 | 无规则会导致历史任意篡改 | 配置时限、角色和审计 |
|
||||
| 匿名信箱的匿名边界 | 绝对匿名与合规追溯冲突 | 业务匿名、受控审计可追溯 |
|
||||
| 审核失败策略 | 失败开放有风险,失败关闭影响可用性 | 按场景分级并可观测 |
|
||||
| 群音视频范围 | 当前 P2P 模型无法自然扩展 | 若需要多人通话,独立评估 SFU |
|
||||
| 搜索技术选型 | MySQL 与独立搜索引擎成本差异大 | 依据消息量、中文检索和运维能力压测决定 |
|
||||
|
||||
## 15. 建议的第一批工程任务
|
||||
|
||||
建议立刻拆分以下可执行任务:
|
||||
|
||||
1. 建立“源码真实基线”文档,删除或标记未实现的 `x-im-*`、schema 119 和测试工具说明;
|
||||
2. 编写消息扩展协议 v1,包括类型、字段、大小、错误码和兼容矩阵;
|
||||
3. 设计并实现消息幂等,补并发和断线重试测试;
|
||||
4. 修复文件下载授权,补上传者、群成员、退群用户和泄露 URL 测试;
|
||||
5. 决定租户和数据库支持范围,冻结第一版 schema;
|
||||
6. 新建 `imapi`、`impolicy`、`imbiz`、`imstore`、`imnotify` 的最小骨架;
|
||||
7. 以“好友申请 → 接受 → 创建会话 → 发消息 → 拉黑”为第一条端到端业务链;
|
||||
8. 以“群申请 → 审批 → 禁言 → 公告”为第二条端到端业务链;
|
||||
9. 引入 Outbox 和审计日志,所有后续业务复用;
|
||||
10. 在 CI 中加入服务端单元测试、MySQL 集成测试和 WebSocket 协议回归测试。
|
||||
|
||||
## 16. 最终建议
|
||||
|
||||
本项目具备可继续演进的实时通信核心,最合理的路线不是重写聊天服务,而是把 Tinode 明确定位为通信内核,在它外部增加可事务、可检索、可审计的产品业务层,并只在发布、订阅、文件和通话等关键入口加入同步策略校验。
|
||||
|
||||
实施时应优先解决租户、幂等和文件授权,再做好友与群业务。企业工作台与通信内核保持模块边界,通过 HTTP API 管理业务状态、通过 Outbox 和系统 Topic 可靠通知。这样既能复用现有 Topic/ACL/Push/WebRTC 能力,也能避免把大量办公和审批逻辑耦合进消息路由主链路。
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,659 @@
|
||||
# IM 服务端多租户第 5 至 12 章代码改造与测试方案
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
本文是《IM-服务端多租户改造方案》第 5 章至第 12 章的代码实施方案,覆盖租户识别与认证、Session 与 Topic、Store 与 Adapter、MySQL 数据访问、缓存与集群、文件、搜索,以及 Push、插件和外部服务。
|
||||
|
||||
本文只规划代码改造和测试,不包含数据库建表、数据迁移、历史数据回填、双写、灰度迁移或旧 Token 兼容。实施前提是本地 MySQL 已经更新为最终多租户表结构,所有业务表的 `tenant_id`、复合索引和复合外键均已就绪。
|
||||
|
||||
## 2. 实施范围与原则
|
||||
|
||||
### 2.1 本期范围
|
||||
|
||||
- WebSocket 握手解析企业码并固定 Session TenantID;
|
||||
- 注册、登录、Token、验证码、REST 认证和密码找回租户化;
|
||||
- User、Topic、Subscription、Message、Device、File、Cache 的 Store 与 MySQL 查询租户化;
|
||||
- Hub、本地缓存、Session Store 和集群路由键租户化;
|
||||
- 文件上传、下载、关联、删除和对象存储路径租户化;
|
||||
- 用户、Topic、Tags、消息搜索和组织数据租户化;
|
||||
- Push、Plugin Firehose 及外部服务调用携带可信 TenantID;
|
||||
- 单元测试、MySQL 集成测试、双租户隔离测试和集群测试。
|
||||
|
||||
### 2.2 明确不做
|
||||
|
||||
- 不实现 `tenant_ticket`,首次 `hi` 继续直接提交公开企业码;
|
||||
- 不兼容无 TenantID 的旧 Token;
|
||||
- 不设置默认租户;
|
||||
- 不提供无租户业务 Store;
|
||||
- 不处理历史数据;
|
||||
- 本期产品运行时只支持 MySQL。MongoDB、PostgreSQL 和 RethinkDB 不得以未隔离模式启动。
|
||||
|
||||
### 2.3 核心安全不变量
|
||||
|
||||
以下规则应通过类型、统一入口和测试共同保证:
|
||||
|
||||
```text
|
||||
tenant_id 只能由服务端根据 hi.tenant、已签名 Token 或已认证 Session 得到
|
||||
已认证 Session 的 tenant_id != 0、uid != 0
|
||||
认证结果 tenant_id == Session tenant_id
|
||||
业务对象 tenant_id == Session tenant_id
|
||||
内部消息、缓存键、集群路由和外部调用均携带同一个 tenant_id
|
||||
任何跨租户访问统一返回 NotFound/Denied,不泄露目标对象是否存在
|
||||
```
|
||||
|
||||
禁止从 Topic Tags、请求 Body、URL Query 或普通 Header 中读取客户端自报的内部 `tenant_id` 作为授权依据。
|
||||
|
||||
## 3. 当前实现基线
|
||||
|
||||
| 能力 | 当前状态 | 本方案处理 |
|
||||
| --- | --- | --- |
|
||||
| `TenantID`、租户实体和状态 | 已实现 | 保留并补充通用 TenantKey 类型 |
|
||||
| `hi.tenant` 企业码 | 已实现 | 保留;补充后续链路测试 |
|
||||
| Session 固定 TenantID | 已实现 | 补充认证后用户归属校验 |
|
||||
| MySQL `TenantGetByCode` | 已实现 | 保留 |
|
||||
| `ClientComMessage.TenantID` | 已实现 | 扩展到服务端消息、集群和后台任务 |
|
||||
| protobuf `ClientHi.tenant` | 已实现 | 补充内部 TenantID 字段或 RPC 元数据 |
|
||||
| `AuthContext`、`auth.Rec.TenantID` | 未实现 | 第 5 章实施 |
|
||||
| Token 携带 TenantID | 未实现 | 升级为 Token v2,拒绝旧格式 |
|
||||
| Store/Adapter 业务接口租户化 | 未实现 | 第 7 章实施 |
|
||||
| MySQL 业务 SQL 带租户条件 | 未实现 | 第 8 章实施 |
|
||||
| Hub/Cluster/Cache 路由键租户化 | 未实现 | 第 9 章实施 |
|
||||
| 文件、搜索、Push、插件租户化 | 未实现 | 第 10 至 12 章实施 |
|
||||
|
||||
## 4. 总体代码结构
|
||||
|
||||
### 4.1 租户作用域类型
|
||||
|
||||
在 `server/store/types` 增加统一键类型,所有内部路由和缓存只通过这些类型生成 Key:
|
||||
|
||||
```go
|
||||
type TopicKey struct {
|
||||
TenantID TenantID
|
||||
Topic string
|
||||
}
|
||||
|
||||
func (k TopicKey) RoutingKey() (string, error)
|
||||
|
||||
type UserKey struct {
|
||||
TenantID TenantID
|
||||
UID Uid
|
||||
}
|
||||
|
||||
func (k UserKey) CacheKey() (string, error)
|
||||
```
|
||||
|
||||
`TenantID == ZeroTenantID`、Topic 为空或 UID 为零时生成 Key 必须报错。业务代码不得自行拼接 `tenantID + ":" + topic`。
|
||||
|
||||
### 4.2 租户作用域 Store
|
||||
|
||||
推荐在 `server/store/store.go` 增加:
|
||||
|
||||
```go
|
||||
type TenantStore struct {
|
||||
TenantID types.TenantID
|
||||
Users TenantUsersStore
|
||||
Topics TenantTopicsStore
|
||||
Subs TenantSubsStore
|
||||
Messages TenantMessagesStore
|
||||
Devices TenantDevicesStore
|
||||
Files TenantFilesStore
|
||||
PCache TenantCacheStore
|
||||
}
|
||||
|
||||
func ForTenant(tenantID types.TenantID) (*TenantStore, error)
|
||||
```
|
||||
|
||||
Session、Topic、HTTP Handler 和后台任务先取得 `TenantStore`,再访问业务数据。`ForTenant(0)` 必须返回错误。租户注册表和平台管理操作继续使用独立的 `Tenants`/`PlatformStore`,不能混入业务 Store。
|
||||
|
||||
底层 MySQL Adapter 的业务方法显式接收 `tenantID`。为了避免未完成的数据库 Adapter 继续工作,启动时检查配置的 Adapter 是否完整实现租户能力;非 MySQL Adapter 返回 `ErrUnsupportedTenant` 并终止启动。
|
||||
|
||||
### 4.3 TenantID 的传播方向
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
HI["hi.tenant 企业码"] --> S["Session.tenantID"]
|
||||
S --> A["AuthContext"]
|
||||
A --> R["auth.Rec.TenantID"]
|
||||
S --> C["Client/Server 内部消息"]
|
||||
C --> T["TopicKey / UserKey"]
|
||||
T --> DB["TenantStore / MySQL"]
|
||||
T --> CL["Cluster RPC"]
|
||||
C --> F["File / Search / Push / Plugin"]
|
||||
```
|
||||
|
||||
传播只允许从左向右。认证器、插件、数据库结果和外部回调只能验证 TenantID,不能替换 Session 已绑定的 TenantID。
|
||||
|
||||
## 5. 第 5 章:租户识别与认证流程
|
||||
|
||||
### 5.1 修改目标
|
||||
|
||||
已完成的 `hi.tenant` 解析继续作为连接租户入口。本章补齐认证上下文、认证记录、Token v2、凭证查询和管理员边界。
|
||||
|
||||
### 5.2 代码修改
|
||||
|
||||
#### 5.2.1 认证接口
|
||||
|
||||
修改 `server/auth/auth.go`:
|
||||
|
||||
```go
|
||||
type AuthContext struct {
|
||||
TenantID types.TenantID
|
||||
RemoteAddr string
|
||||
}
|
||||
|
||||
type Rec struct {
|
||||
TenantID types.TenantID `json:"tenant_id,omitempty"`
|
||||
// 原有字段保持不变
|
||||
}
|
||||
```
|
||||
|
||||
将 `AuthHandler` 的以下方法改为接收 `AuthContext` 或 TenantID:
|
||||
|
||||
```go
|
||||
AddRecord(ctx AuthContext, rec *Rec, secret []byte)
|
||||
UpdateRecord(ctx AuthContext, rec *Rec, secret []byte)
|
||||
Authenticate(ctx AuthContext, secret []byte)
|
||||
IsUnique(ctx AuthContext, secret []byte)
|
||||
DelRecords(tenantID types.TenantID, uid types.Uid)
|
||||
GetResetParams(tenantID types.TenantID, uid types.Uid)
|
||||
```
|
||||
|
||||
同步修改:
|
||||
|
||||
- `server/auth/basic/auth_basic.go`;
|
||||
- `server/auth/code/auth_code.go`;
|
||||
- `server/auth/rest/auth_rest.go`;
|
||||
- `server/auth/token/auth_token.go`;
|
||||
- `server/auth/anon/auth_anon.go`;
|
||||
- `server/auth/mock_auth`;
|
||||
- `server/session.go` 中注册、登录、临时认证和凭证校验调用点;
|
||||
- HTTP 文件认证调用点。
|
||||
|
||||
所有认证器入口先校验 `ctx.TenantID != 0`。认证结果没有 TenantID,或 `rec.TenantID != ctx.TenantID`,统一返回认证失败并记录安全日志。
|
||||
|
||||
#### 5.2.2 Basic、Code、REST 认证
|
||||
|
||||
- Basic 登录名按 `(tenant_id, scheme, unique)` 查询;
|
||||
- 注册时唯一性检查只在当前租户执行;
|
||||
- Code 认证的验证码记录、次数和过期时间按租户隔离;
|
||||
- REST 认证请求携带服务端签名的 TenantID,响应必须原样返回 TenantID;
|
||||
- 密码找回先从 Session/签名上下文得到 TenantID,再查询 Credential,禁止全库按手机号或邮箱查找。
|
||||
|
||||
#### 5.2.3 Token v2
|
||||
|
||||
修改 `server/auth/token/auth_token.go`。当前 Token 是 50 字节且没有版本和 TenantID,替换为明确版本的 v2 布局:
|
||||
|
||||
```text
|
||||
[1:version][8:tenant_id][8:uid][4:expires][2:auth_level]
|
||||
[2:serial][2:features][32:HMAC-SHA256]
|
||||
```
|
||||
|
||||
实施规则:
|
||||
|
||||
- `version` 固定为 2;
|
||||
- `tenant_id` 和其余 Token 字段一起参与 HMAC;
|
||||
- `GenSecret` 要求 `rec.TenantID != 0`;
|
||||
- `Authenticate` 校验版本、精确长度、签名、租户、过期时间和 serial;
|
||||
- v1/当前 50 字节 Token 直接返回 `ErrMalformed` 或 `ErrFailed`;
|
||||
- Token TenantID 与 `AuthContext.TenantID` 不一致时返回认证失败;
|
||||
- 认证成功返回包含 TenantID 的 `auth.Rec`。
|
||||
|
||||
本项目没有历史数据,因此不增加双版本解析和兼容开关。
|
||||
|
||||
#### 5.2.4 用户归属和管理员
|
||||
|
||||
登录取得 UID 后,使用当前 `TenantStore.Users.Get(uid)` 再确认 User 属于相同租户,然后才写入 `Session.uid`。Tenant Admin 角色通过租户内角色/权限数据判断,不能用全局 `AuthLevelRoot` 代替。普通 WebSocket 请求中的 `AsUser` 必须同时验证角色和目标 User 的 TenantID。
|
||||
|
||||
### 5.3 测试用例
|
||||
|
||||
| 编号 | 层级 | 场景与操作 | 预期结果 |
|
||||
| --- | --- | --- | --- |
|
||||
| T5-HI-001 | 单元 | 使用有效且 active 的 `acme` 执行首次 `hi` | Session 固定 Tenant A,响应只含 code/name |
|
||||
| T5-HI-002 | 单元 | `hi` 不传 tenant、传空白或超过 64 字符 | 握手失败,Session 仍未绑定 |
|
||||
| T5-HI-003 | 单元 | 使用不存在或 suspended 租户 | 分别返回 NotFound/Denied,不绑定 Session |
|
||||
| T5-HI-004 | 单元 | 已绑定 A 的 Session 再传 B | 拒绝且 Session 仍为 A |
|
||||
| T5-AUTH-001 | MySQL 集成 | A、B 分别创建相同登录名 `alice` | 两条记录均成功,分别登录到各自用户 |
|
||||
| T5-AUTH-002 | MySQL 集成 | A 使用 B 的 `alice` 密码登录 | 认证失败,不泄露 B 用户存在性 |
|
||||
| T5-AUTH-003 | 单元 | Authenticator 返回 B 的 `auth.Rec` 给 A Session | 登录失败并产生安全日志 |
|
||||
| T5-AUTH-004 | 单元 | 为 A 用户生成 Token v2,再在 A Session 登录 | 成功,TenantID/UID 完整恢复 |
|
||||
| T5-AUTH-005 | 单元 | A Token 在 B Session 使用 | 认证失败,B Session 不写入 UID |
|
||||
| T5-AUTH-006 | 单元 | 修改 Token 中 TenantID 或版本字节 | HMAC/版本校验失败 |
|
||||
| T5-AUTH-007 | 单元 | 使用当前 50 字节旧 Token | 明确拒绝,不进入兼容分支 |
|
||||
| T5-AUTH-008 | MySQL 集成 | A、B 使用相同手机号发起密码找回 | 只命中当前租户用户和验证码 |
|
||||
| T5-AUTH-009 | 权限 | A 的 tenant_admin 代操作 B 用户 | 拒绝;平台管理 API 不受普通 WS 入口影响 |
|
||||
|
||||
## 6. 第 6 章:Session、消息和 Topic 改造
|
||||
|
||||
### 6.1 修改目标
|
||||
|
||||
TenantID 从 Session 贯穿到内部双向消息、Topic 生命周期和所有 P2P/群聊权限检查。客户端 Topic 名和对外协议保持不变。
|
||||
|
||||
### 6.2 代码修改
|
||||
|
||||
#### 6.2.1 内部消息和 Session
|
||||
|
||||
- 保留 `Session.tenantID` 和 `ClientComMessage.TenantID`;
|
||||
- 给 `ServerComMessage` 增加不序列化给普通客户端的 `TenantID`;
|
||||
- Session 收到每条客户端消息时覆盖内部 TenantID,忽略反序列化对象中可能存在的值;
|
||||
- 所有 Topic、Hub、Cluster 和后台生成消息必须显式填写 TenantID;
|
||||
- 登录成功前验证 `User.tenant_id == Session.tenantID`;
|
||||
- PROXY、MULTIPLEX Session 同样必须有 TenantID。
|
||||
|
||||
#### 6.2.2 Topic 对象和路由键
|
||||
|
||||
修改 `server/topic.go`、`server/hub.go`、`server/session.go` 及相关辅助类型:
|
||||
|
||||
- `Topic` 增加初始化后不可变的 `tenantID`;
|
||||
- Hub `topicGet/topicPut/topicDel` 参数从裸字符串改为 `types.TopicKey`;
|
||||
- Session 订阅表、Topic Proxy、Multiplex Session 使用 `RoutingKey()`;
|
||||
- 调试输出可显示 TenantID 和外部 Topic 名,但不能把内部前缀回传客户端;
|
||||
- 数据库中的 Topic 名仍保存原始名称,不保存 `tenantID:` 前缀。
|
||||
|
||||
#### 6.2.3 特殊 Topic、P2P 和群聊
|
||||
|
||||
- `sys` 按 `(tenant_id, "sys")` 懒加载,不再创建全局唯一实例;
|
||||
- `me`、`fnd`、`slf` 使用 Session TenantStore 加载;
|
||||
- 创建 P2P 前加载双方 User,并验证双方与 Session 同租户;
|
||||
- 创建群、邀请、审批、踢人、转让群主、发送系统通知时验证 Topic、操作者和目标成员同租户;
|
||||
- Subscription、Outbox、系统消息和附件关联全部沿用 Topic TenantID。
|
||||
|
||||
### 6.3 测试用例
|
||||
|
||||
| 编号 | 层级 | 场景与操作 | 预期结果 |
|
||||
| --- | --- | --- | --- |
|
||||
| T6-MSG-001 | 单元 | A Session 发送普通消息 | `ClientComMessage.TenantID` 被服务端写为 A |
|
||||
| T6-MSG-002 | 单元 | 构造内部消息自带 B TenantID 后交给 A Session | 值被覆盖或请求被拒绝,不能进入 B |
|
||||
| T6-MSG-003 | 单元 | Topic 产生服务端响应/通知 | `ServerComMessage.TenantID` 等于 Topic TenantID |
|
||||
| T6-TOPIC-001 | 单元 | A、B 同时加载裸名称 `sys` | Hub 中存在两个不同 Topic 实例 |
|
||||
| T6-TOPIC-002 | 单元 | A、B 使用相同 Group Topic 名 | 内部 RoutingKey 不同,对外名称不变 |
|
||||
| T6-P2P-001 | 集成 | A 用户邀请 A 用户创建 P2P | 创建成功,Topic/Subs 均为 A |
|
||||
| T6-P2P-002 | 集成 | A 用户邀请 B 用户创建 P2P | 返回通用 NotFound/Denied,不产生 Topic/Subs |
|
||||
| T6-GRP-001 | 集成 | A 管理员邀请 B 用户加入 A 群 | 拒绝且没有部分写入 |
|
||||
| T6-GRP-002 | 集成 | A 用户用已知 B Topic 名订阅、读、写、删 | 所有操作均失败,B 数据不变化 |
|
||||
| T6-SYS-001 | 集成 | A 发布系统通知 | 只有 A 的 `sys` 订阅者收到 |
|
||||
|
||||
## 7. 第 7 章:Store 与 Adapter 改造
|
||||
|
||||
### 7.1 修改目标
|
||||
|
||||
消除业务代码中的无租户持久化入口,使漏传 TenantID 在编译期或统一 Store 入口立即失败,而不是依赖开发者记得追加 SQL 条件。
|
||||
|
||||
### 7.2 代码修改
|
||||
|
||||
#### 7.2.1 Store 接口
|
||||
|
||||
修改 `server/store/store.go`,为以下域建立 TenantStore Mapper:
|
||||
|
||||
- Users:Create/Get/GetAll/Update/Delete/State/Tags/Topics/Subs/Unread;
|
||||
- Auth:Add/Get/Update/Delete/Unique lookup;
|
||||
- Credentials:Upsert/Get/Confirm/Fail/Delete/Reset lookup;
|
||||
- Topics:Create/Get/Share/Delete/Update/Owner/Find;
|
||||
- Subscriptions:Create/Get/Update/Delete/按 User 或 Topic 查询;
|
||||
- Messages:Save/Get/Delete/GetDeleted/Topic SeqId;
|
||||
- Devices:Upsert/Get/Delete/Push Channel;
|
||||
- Files:Start/Finish/Get/Link/DeleteUnused/Access;
|
||||
- Persistent Cache:Get/Upsert/Delete/Expire。
|
||||
|
||||
现有 `store.Users`、`store.Topics` 等全局 Mapper 不再供 Session/Topic/Handler 使用。完成调用点替换后删除或限制为测试期私有入口,避免新代码绕过 TenantStore。
|
||||
|
||||
#### 7.2.2 Adapter 接口
|
||||
|
||||
修改 `server/db/adapter.go`,所有租户业务方法显式接收 `types.TenantID`,例如:
|
||||
|
||||
```go
|
||||
UserGet(tenantID types.TenantID, uid types.Uid)
|
||||
AuthGetUniqueRecord(tenantID types.TenantID, unique string)
|
||||
TopicGet(tenantID types.TenantID, topic string)
|
||||
SubscriptionGet(tenantID types.TenantID, topic string, uid types.Uid, keepDeleted bool)
|
||||
MessageGetAll(tenantID types.TenantID, topic string, uid types.Uid, opts *types.QueryOpt)
|
||||
FileGet(tenantID types.TenantID, fileID string)
|
||||
```
|
||||
|
||||
数据对象 `User`、`Topic`、`Subscription`、`Message`、`DelMessage`、`Credential`、`DeviceDef` 和 `FileDef` 增加内部 TenantID。Create/Save 时同时校验“方法参数 TenantID、父对象 TenantID、待写对象 TenantID”一致。
|
||||
|
||||
#### 7.2.3 非 MySQL Adapter
|
||||
|
||||
公共接口变化会影响 `server/db/postgres`、`server/db/mongodb`、`server/db/rethinkdb` 和 Mock。处理方式:
|
||||
|
||||
- Mock 完整实现租户参数,用于单元测试;
|
||||
- MySQL 完整实现业务逻辑;
|
||||
- 其他 Adapter 实现相同签名但返回 `ErrUnsupportedTenant`;
|
||||
- 服务启动阶段检测并拒绝使用不支持租户业务接口的 Adapter;
|
||||
- 不允许退回旧的无租户查询。
|
||||
|
||||
### 7.3 测试用例
|
||||
|
||||
| 编号 | 层级 | 场景与操作 | 预期结果 |
|
||||
| --- | --- | --- | --- |
|
||||
| T7-STORE-001 | 单元 | 调用 `store.ForTenant(0)` | 返回错误,不创建 Mapper |
|
||||
| T7-STORE-002 | 单元 | A TenantStore 查询 B UID | 返回 NotFound/Denied |
|
||||
| T7-STORE-003 | 单元 | Create 参数为 A、对象 TenantID 为 B | 写入前失败,Adapter 不被调用 |
|
||||
| T7-STORE-004 | 单元 | Mock 断言 Session 所有 Store 调用 | 每次调用都收到 Session TenantID |
|
||||
| T7-STORE-005 | 构建 | 搜索生产代码中的全局 `store.Users/Topics/...` | Session/Topic/Handler 路径不存在绕过入口 |
|
||||
| T7-ADP-001 | 启动 | 配置 PostgreSQL/MongoDB/RethinkDB | 启动失败并明确提示不支持多租户 |
|
||||
| T7-ADP-002 | 单元 | Adapter 收到零 TenantID | 统一返回 `ErrInvalidTenant`,不执行 SQL |
|
||||
|
||||
## 8. 第 8 章:MySQL 代码改造
|
||||
|
||||
### 8.1 前提
|
||||
|
||||
本地数据库已经更新,本章不执行任何 schema 修改。代码直接以最终多租户表结构为准,测试负责验证 SQL 使用 `tenant_id`、唯一约束和复合外键符合预期。
|
||||
|
||||
### 8.2 代码修改
|
||||
|
||||
修改 `server/db/mysql/adapter.go` 中所有业务 SQL:
|
||||
|
||||
- `SELECT` 必须包含 `WHERE tenant_id = ?`;
|
||||
- `UPDATE`、`DELETE` 必须同时包含 TenantID 和业务主键;
|
||||
- `INSERT` 明确写入 `tenant_id`,不能依赖数据库默认值;
|
||||
- JOIN 同时关联父子表 TenantID,例如 `child.tenant_id = parent.tenant_id`;
|
||||
- 唯一查询按复合业务键执行,如 `(tenant_id, uname)`;
|
||||
- 聚合、未读数、序号、清理任务和批处理同样按租户过滤;
|
||||
- 受影响行数为 0 时直接返回 NotFound/Denied,不再发起无租户探测查询;
|
||||
- 租户级垃圾清理采用 `tenantID + limit`,由平台调度器遍历 active 租户。
|
||||
|
||||
建议按域逐段改造并提交,每段同时更新对应 MySQL 测试:
|
||||
|
||||
1. User、Auth、Credential;
|
||||
2. Topic、Subscription、Tags;
|
||||
3. Message、DelLog、未读数;
|
||||
4. Device;
|
||||
5. FileUpload、FileMessageLink;
|
||||
6. Persistent Cache。
|
||||
|
||||
### 8.3 SQL 审计要求
|
||||
|
||||
新增测试辅助函数或代码审查清单,对 `adapter.go` 中业务 SQL 做静态检查。以下情况必须阻止合并:
|
||||
|
||||
- 业务表查询完全没有 `tenant_id`;
|
||||
- JOIN 只按 ID 关联,没有 TenantID;
|
||||
- Update/Delete 只按全局 ID;
|
||||
- `IN (...)` 批量查询没有 TenantID 前置条件;
|
||||
- 后台清理一次扫描全部租户业务数据;
|
||||
- 为判断“其他租户是否存在该对象”而执行无租户查询。
|
||||
|
||||
### 8.4 测试用例
|
||||
|
||||
| 编号 | 层级 | 场景与操作 | 预期结果 |
|
||||
| --- | --- | --- | --- |
|
||||
| T8-DB-001 | MySQL 集成 | A、B 插入相同 `auth.uname` | 均成功;同租户重复失败 |
|
||||
| T8-DB-002 | MySQL 集成 | A、B 插入相同 Topic name/tag | 均成功;查询只返回当前租户 |
|
||||
| T8-DB-003 | MySQL 集成 | A 查询 B 的 UID、Topic、Message、File | 均返回空/NotFound |
|
||||
| T8-DB-004 | MySQL 集成 | A 更新/删除 B 对象的业务 ID | 影响行数为 0,B 数据不变 |
|
||||
| T8-DB-005 | MySQL 集成 | 尝试写入 A Subscription 关联 B Topic/User | 代码校验或复合外键拒绝,事务回滚 |
|
||||
| T8-DB-006 | MySQL 集成 | A 消息关联 B 文件 | 代码校验或复合外键拒绝 |
|
||||
| T8-DB-007 | MySQL 集成 | A、B 分别统计未读数和设备数 | 结果只包含本租户数据 |
|
||||
| T8-DB-008 | MySQL 集成 | A 的批量删除、过期和垃圾清理 | B 的 Message/File/Cache 不变化 |
|
||||
| T8-DB-009 | 静态审计 | 扫描全部业务 SQL | 无缺失 TenantID 的读写和 JOIN |
|
||||
|
||||
## 9. 第 9 章:缓存与集群改造
|
||||
|
||||
### 9.1 代码修改
|
||||
|
||||
#### 9.1.1 Hub 与 User Cache
|
||||
|
||||
修改 `server/hub.go`:
|
||||
|
||||
- `topicGet/topicPut/topicDel` 使用 `TopicKey.RoutingKey()`;
|
||||
- 放入 Hub 前验证请求、Topic 对象和数据库行 TenantID 一致;
|
||||
- `topicUnreg`、rehash、stats 和 debug 结构增加 TenantID。
|
||||
|
||||
修改 User Cache、未读数、Pending Push 和 EvictUser 相关代码:
|
||||
|
||||
- 使用 `UserKey{TenantID, UID}`;
|
||||
- `EvictUser(tenantID, uid, skipSID)`;
|
||||
- 用户状态、设备和未读数更新都携带 TenantID。
|
||||
|
||||
Session ID 保持全局随机,但通过 SID 取 Session 后,使用方必须读取并验证 Session TenantID,不能再只取 UID。
|
||||
|
||||
#### 9.1.2 集群
|
||||
|
||||
修改 `server/cluster.go`:
|
||||
|
||||
- `ClusterSess`、`ClusterSessUpdate`、`ClusterReq`、`ClusterRoute`、`ClusterResp` 增加 TenantID;
|
||||
- `nodeForTopic`、`isRemoteTopic` 接收 `TopicKey` 并按 RoutingKey 做一致性哈希;
|
||||
- Proxy/Master 两端重算 RoutingKey,并验证 RPC、Session、Client/Server Message TenantID 一致;
|
||||
- User Cache RPC 和 Topic rehash 携带 TenantID;
|
||||
- Cluster 协议版本增加多租户能力位,旧节点拒绝加入。
|
||||
|
||||
#### 9.1.3 持久化缓存
|
||||
|
||||
增加统一 `TenantCacheKey(tenantID, namespace, key)`。租户配置、验证码、未读数和业务临时状态均使用该函数。平台级 schema/version 缓存使用独立 Platform Cache 接口。
|
||||
|
||||
### 9.2 测试用例
|
||||
|
||||
| 编号 | 层级 | 场景与操作 | 预期结果 |
|
||||
| --- | --- | --- | --- |
|
||||
| T9-HUB-001 | 单元 | A、B Put/Get 相同 Topic 名 | 分别命中各自对象,无覆盖 |
|
||||
| T9-HUB-002 | 单元 | 将 B Topic 放入 A TopicKey | 拒绝并记录安全告警 |
|
||||
| T9-USER-001 | 单元 | Evict A 的同值 UID | 只断开 A Session,B Session 保持 |
|
||||
| T9-CACHE-001 | MySQL 集成 | A、B 写同 namespace/key | 值独立;过期 A 不删除 B |
|
||||
| T9-CL-001 | 单元 | A、B 相同 Topic 计算节点 | 使用不同 RoutingKey;结果按各自 Key 稳定 |
|
||||
| T9-CL-002 | 集群集成 | RPC TenantID 与消息 TenantID 不一致 | 接收端拒绝,不创建 Proxy Topic |
|
||||
| T9-CL-003 | 集群集成 | A Session 伪造 B RcptTo | Master 重算路由后拒绝 |
|
||||
| T9-CL-004 | 集群集成 | 节点重哈希 | A、B 同名 Topic 均完整迁移且不合并 |
|
||||
| T9-CL-005 | 启动 | 多租户节点与旧协议节点组网 | 握手失败,不能形成混合版本集群 |
|
||||
|
||||
## 10. 第 10 章:文件与对象存储改造
|
||||
|
||||
### 10.1 代码修改
|
||||
|
||||
在 `server/hdl_files.go` 增加:
|
||||
|
||||
```go
|
||||
type RequestPrincipal struct {
|
||||
TenantID types.TenantID
|
||||
UID types.Uid
|
||||
Roles []string
|
||||
}
|
||||
```
|
||||
|
||||
`authFileRequest` 改为返回 `RequestPrincipal`:
|
||||
|
||||
- Token 路径从 Token v2 得到 TenantID;
|
||||
- SID 路径从 Session 得到 TenantID;
|
||||
- 两条路径都要求 TenantID 和 UID 非零;
|
||||
- API Key 只做应用准入,不推导 TenantID。
|
||||
|
||||
Store/Adapter 增加 `FileCanAccess(tenantID, uid, fileID, action)`,并改造上传、下载、断点续传、关联、删除和垃圾回收:
|
||||
|
||||
- 先使用 TenantID 加载 File;
|
||||
- 未完成上传只允许同租户上传者访问;
|
||||
- 已关联消息时校验同租户 Subscription 和 Read 权限;
|
||||
- 删除验证上传者、群主或租户管理员角色;
|
||||
- 转发时验证源文件、源消息和目标 Topic 同租户;
|
||||
- 签名 URL 覆盖 TenantID、FileID、Action 和 Expires;
|
||||
- 新对象路径使用 `tenants/{tenant_id}/yyyy/mm/{file_id}`;
|
||||
- Location 仅供服务端使用,不直接返回客户端。
|
||||
|
||||
### 10.2 测试用例
|
||||
|
||||
| 编号 | 层级 | 场景与操作 | 预期结果 |
|
||||
| --- | --- | --- | --- |
|
||||
| T10-AUTH-001 | 单元 | Token 和 SID 分别认证同一用户 | 得到相同 TenantID/UID Principal |
|
||||
| T10-AUTH-002 | 单元 | 无租户旧 Token、无效 SID 或零 UID | 认证失败,不进入 File Store |
|
||||
| T10-UP-001 | 集成 | A 上传文件 | DB TenantID 为 A,路径以 `tenants/A/` 开头 |
|
||||
| T10-DL-001 | 集成 | A 用户下载 A 群内有权限文件 | 成功 |
|
||||
| T10-DL-002 | 集成 | A 使用已知 B FileID 下载 | 通用 NotFound/Denied,不返回元数据/Location |
|
||||
| T10-LINK-001 | 集成 | A 消息关联 B FileID | 失败,文件 use_count 不变化 |
|
||||
| T10-FWD-001 | 集成 | A 转发 B 文件到 A Topic | 失败且不创建消息 |
|
||||
| T10-SIGN-001 | 单元 | 修改签名 URL 中 TenantID/FileID/Expires | 验签失败 |
|
||||
| T10-GC-001 | MySQL 集成 | 清理 A 未使用文件 | B 文件记录和对象不被删除 |
|
||||
|
||||
## 11. 第 11 章:搜索、Tags 与组织数据
|
||||
|
||||
### 11.1 代码修改
|
||||
|
||||
修改 `server/store/store.go`、`server/db/adapter.go` 和 `server/db/mysql/adapter.go`:
|
||||
|
||||
- `Find`、`FindOne`、User/Topic Tag 增删改查显式接收 TenantID;
|
||||
- 精确账号、手机号、邮箱查询只在当前租户执行;
|
||||
- 结果对象再次验证 TenantID 后才返回;
|
||||
- 无结果和命中其他租户统一表现为空/NotFound。
|
||||
|
||||
消息搜索索引文档必须包含 TenantID,文档键或路由键使用 `tenant_id + topic + seq_id`。查询时先强制加 TenantID,再按当前用户在同租户 Topic 的 Read ACL 二次过滤。TenantID 来自认证上下文,不接受 Query 参数覆盖。
|
||||
|
||||
组织表 Repository 均以 TenantID 为首参数。新增/移动部门、设置负责人、添加成员时加载父部门和 User,验证它们属于同一租户。涉及多表写入时使用事务,任何归属不一致都整体回滚。
|
||||
|
||||
### 11.2 测试用例
|
||||
|
||||
| 编号 | 层级 | 场景与操作 | 预期结果 |
|
||||
| --- | --- | --- | --- |
|
||||
| T11-FIND-001 | MySQL 集成 | A、B 用户具有相同 `email:`/`tel:` Tag | 各租户只返回自己的用户 |
|
||||
| T11-FIND-002 | MySQL 集成 | A 搜索只有 B 存在的 Tag | 返回空结果,不提示跨租户命中 |
|
||||
| T11-FIND-003 | MySQL 集成 | A、B 使用相同 Topic Tag | 排名和结果集合互不影响 |
|
||||
| T11-MSG-001 | 搜索集成 | A、B 消息正文相同 | A 查询结果中不出现 B 文档 |
|
||||
| T11-MSG-002 | 搜索集成 | A 用户搜索 A 内无 Read 权限 Topic | ACL 二次过滤移除结果 |
|
||||
| T11-MSG-003 | 安全 | Query 参数提交 B TenantID | 参数被忽略或请求被拒绝,仍使用 Token TenantID |
|
||||
| T11-ORG-001 | MySQL 集成 | A 部门添加 A 用户 | 成功,组织关系为 A |
|
||||
| T11-ORG-002 | MySQL 集成 | A 部门添加 B 用户/负责人 | 失败,事务无部分写入 |
|
||||
| T11-ORG-003 | MySQL 集成 | A 部门设置 B 部门为 parent | 失败,部门树不变化 |
|
||||
|
||||
## 12. 第 12 章:Push、插件和外部服务
|
||||
|
||||
### 12.1 Push
|
||||
|
||||
修改 `server/push/push.go`:
|
||||
|
||||
```go
|
||||
type Receipt struct { TenantID types.TenantID /* ... */ }
|
||||
type ChannelReq struct { TenantID types.TenantID /* ... */ }
|
||||
type Payload struct { TenantID types.TenantID `json:"-"` /* ... */ }
|
||||
```
|
||||
|
||||
- Push 构造入口要求 TenantID 非零;
|
||||
- 设备按 `(tenant_id, uid)` 查询;
|
||||
- Channel 使用统一函数生成租户前缀,不能直接拼接客户端值;
|
||||
- Push Handler 根据 TenantID 选择模板、App 包和密钥引用;
|
||||
- 未读数使用 UserKey;
|
||||
- 日志和指标带 TenantID,但不得输出完整设备 Token。
|
||||
|
||||
### 12.2 Plugin Firehose
|
||||
|
||||
修改 `server/plugins.go` 和 protobuf:
|
||||
|
||||
- `pluginGenerateClientReq` 写入 Session TenantID;
|
||||
- Plugin 请求包含 TenantID 和可选租户策略版本;
|
||||
- `REPLACE` 响应反序列化后,服务端强制恢复原 TenantID;
|
||||
- 替换后的目标 Topic 重新执行同租户校验;
|
||||
- `RESPOND` 生成的服务端消息也强制使用原 TenantID;
|
||||
- 插件超时、失败开放/关闭、审计和指标按租户配置执行。
|
||||
|
||||
### 12.3 REST、短信、TURN 和对象存储
|
||||
|
||||
- 外部请求使用服务端签名的 TenantContext,至少覆盖 TenantID、请求用途、过期时间和 nonce;
|
||||
- 回调响应必须返回 TenantID/nonce,服务端验证与原请求一致;
|
||||
- 秘钥管理只保存租户配置中的 Secret 引用,业务 JSON 不保存明文 Secret;
|
||||
- 短信模板、Push 凭证、对象存储 Bucket/Prefix 和 TURN 配置按 TenantID 选择;
|
||||
- 租户没有配置时按明确策略失败,不得静默使用其他租户配置。
|
||||
|
||||
### 12.4 测试用例
|
||||
|
||||
| 编号 | 层级 | 场景与操作 | 预期结果 |
|
||||
| --- | --- | --- | --- |
|
||||
| T12-PUSH-001 | 单元 | A 消息生成 Push Receipt | Receipt/Payload/Channel 均为 A |
|
||||
| T12-PUSH-002 | 集成 | A、B 有同值 UID 和不同设备 | A Push 只读取并投递 A 设备 |
|
||||
| T12-PUSH-003 | 单元 | A、B 使用相同逻辑 Channel | 生成的物理 Channel 不碰撞 |
|
||||
| T12-PUSH-004 | 单元 | TenantID 为零或找不到租户凭证 | 明确失败,不回退到其他租户凭证 |
|
||||
| T12-PLG-001 | 单元 | A 请求发送到 Firehose | Plugin Request TenantID 为 A |
|
||||
| T12-PLG-002 | 安全 | Plugin `REPLACE` 返回 B TenantID | 服务端拒绝或恢复 A,并重新校验 Topic |
|
||||
| T12-PLG-003 | 安全 | Plugin `RESPOND` 返回 B TenantID | 响应不能进入 B 路由 |
|
||||
| T12-EXT-001 | 单元 | 篡改外部请求 TenantContext | 签名校验失败 |
|
||||
| T12-EXT-002 | 集成 | A 请求收到带 B TenantID/错误 nonce 的回调 | 回调拒绝,不更新任何业务状态 |
|
||||
| T12-EXT-003 | 配置 | A 未配置短信/TURN/对象存储 | 按 A 策略失败,不使用 B 或全局 Secret |
|
||||
|
||||
## 13. 实施顺序与提交边界
|
||||
|
||||
### 阶段 A:基础类型和认证
|
||||
|
||||
1. 增加 TopicKey、UserKey、AuthContext 和 `auth.Rec.TenantID`;
|
||||
2. 修改全部认证器和 Mock;
|
||||
3. 实现 Token v2;
|
||||
4. 完成第 5 章测试。
|
||||
|
||||
完成条件:注册、登录、Token 和密码找回全部绑定租户,旧 Token 明确拒绝。
|
||||
|
||||
### 阶段 B:Store、MySQL 与单机消息链路
|
||||
|
||||
1. 增加 TenantStore;
|
||||
2. 改造 Adapter 接口和 MySQL User/Auth/Credential;
|
||||
3. 改造 Topic/Subscription/Message/Device/File/Cache;
|
||||
4. 改造 Session、Topic 和 Hub 调用点;
|
||||
5. 完成第 6 至 8 章测试。
|
||||
|
||||
完成条件:单机模式下两个租户可使用同名登录名、Topic 和 Tag,所有交叉读写失败。
|
||||
|
||||
### 阶段 C:缓存、集群和文件
|
||||
|
||||
1. Hub/User/PCache Key 租户化;
|
||||
2. Cluster RPC 和一致性哈希租户化;
|
||||
3. 文件 Principal、ACL、路径和签名租户化;
|
||||
4. 完成第 9、10 章测试。
|
||||
|
||||
完成条件:双节点路由和文件链路不存在跨租户缓存命中或资源访问。
|
||||
|
||||
### 阶段 D:搜索、Push、插件和外部服务
|
||||
|
||||
1. Find/Tags/消息索引/组织 Repository;
|
||||
2. Push 数据结构、设备查询和凭证选择;
|
||||
3. Plugin protobuf 与 REPLACE/RESPOND 校验;
|
||||
4. 外部 TenantContext 签名与回调验证;
|
||||
5. 完成第 11、12 章测试。
|
||||
|
||||
完成条件:所有出站和异步链路均可按 TenantID 追踪,并通过反向篡改测试。
|
||||
|
||||
## 14. 测试环境与公共数据
|
||||
|
||||
### 14.1 测试租户
|
||||
|
||||
在专用测试库中准备以下数据,不使用开发人员的日常业务数据:
|
||||
|
||||
| 数据 | Tenant A | Tenant B |
|
||||
| --- | --- | --- |
|
||||
| tenant_id | 1001 | 1002 |
|
||||
| code | acme | beta |
|
||||
| state | active | active |
|
||||
| 登录名 | alice | alice |
|
||||
| 手机号 | 13800000000 | 13800000000 |
|
||||
| 逻辑 Topic | sys、grpShared | sys、grpShared |
|
||||
| Tag | email:alice@example.com | email:alice@example.com |
|
||||
| 文件 | file-shared-name | file-shared-name |
|
||||
|
||||
另外准备一个 `suspended` 租户用于握手和已签 Token 的停用测试。测试数据必须由测试事务或 fixture 创建并清理,不能依赖固定自增 ID。
|
||||
|
||||
### 14.2 测试层次
|
||||
|
||||
- 单元测试:Key、AuthContext、Token、Session、Hub、Cluster RPC、Plugin 和签名;
|
||||
- MySQL 集成测试:每个 Adapter 方法至少包含“同租户成功 + 跨租户失败”;
|
||||
- 服务集成测试:两个真实 WebSocket Session 完成握手、登录、建群、发消息和文件访问;
|
||||
- 双节点集群测试:同名 Topic 路由、Proxy、重哈希和节点版本检查;
|
||||
- 安全测试:篡改 TenantID、Token、RPC、Plugin 响应和外部回调;
|
||||
- 并发测试:A、B 同时创建同名业务键,验证唯一约束只在租户内生效。
|
||||
|
||||
## 15. 验证命令
|
||||
|
||||
具体数据库连接参数沿用仓库现有 MySQL 测试配置,不在文档中写死账号密码。实施过程中至少执行:
|
||||
|
||||
```bash
|
||||
go test ./server/auth/... ./server/store/... ./pbx
|
||||
go test ./server
|
||||
go test -tags mysql ./server/db/mysql/tests
|
||||
go test -race ./server ./server/auth/... ./server/store/...
|
||||
go build -tags mysql ./server/...
|
||||
```
|
||||
|
||||
如果仓库现有 MySQL 测试要求特定工作目录或配置文件,应沿用现有测试入口。集群、对象存储、Push 和外部服务使用本地 fake/mock,另在测试环境执行真实双节点和真实供应商沙箱验收。
|
||||
|
||||
## 16. 完成标准
|
||||
|
||||
- [ ] 第 5 至 12 章所有生产调用链都能追踪到可信 TenantID 来源;
|
||||
- [ ] 已认证 Session、User、Topic、消息和认证结果 TenantID 始终一致;
|
||||
- [ ] 业务 Store 不存在可被生产代码调用的无租户入口;
|
||||
- [ ] MySQL 所有业务读写、JOIN、批处理和清理均包含 TenantID;
|
||||
- [ ] Hub、User Cache、PCache 和 Cluster 路由均使用租户 Key;
|
||||
- [ ] Token、文件签名、Plugin 和外部上下文覆盖 TenantID 并通过篡改测试;
|
||||
- [ ] A/B 双租户同名数据测试全部通过;
|
||||
- [ ] 所有跨租户读、写、邀请、关联、删除和回调测试全部失败且不泄露对象存在性;
|
||||
- [ ] 非 MySQL Adapter 无法以未隔离模式启动;
|
||||
- [ ] 单元测试、MySQL 集成测试、race test 和 MySQL build 全部通过;
|
||||
- [ ] 日志、指标和安全审计含 TenantID,但不输出密码、Token、设备 Token 或租户 Secret。
|
||||
@@ -0,0 +1,713 @@
|
||||
-- IM product database changes summary, chapters 1-11.
|
||||
-- Target:
|
||||
-- public backend database: hotgo
|
||||
-- default tenant business database: tinode
|
||||
-- Notes:
|
||||
-- 1. Public tenant registry tables live in hotgo.
|
||||
-- 2. Tenant-scoped business tables live in each tenant database and do not include tenant_id.
|
||||
-- 3. This file is for review and manual execution. Existing legacy columns/indexes should be checked before ALTER/DROP.
|
||||
-- 4. Table definitions are aligned with server/db/mysql/adapter.go plus im_verification_codes used by chapter 1 verification APIs.
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS hotgo CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
|
||||
CREATE DATABASE IF NOT EXISTS tinode CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- Public database: tenant registry.
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS hotgo.im_tenant (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
code VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
tenant_desc VARCHAR(256),
|
||||
db_name VARCHAR(128) NOT NULL DEFAULT '',
|
||||
db_dsn VARCHAR(1024) NOT NULL DEFAULT '',
|
||||
state SMALLINT NOT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_by BIGINT NOT NULL,
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
updated_by BIGINT NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_tenant_code (code),
|
||||
INDEX idx_im_tenant_db_name (db_name)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS hotgo.im_tenant_config (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
max_users BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
|
||||
max_groups BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
|
||||
max_group_members INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_by BIGINT NOT NULL,
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
updated_by BIGINT NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_tenant_config_tenant_id (tenant_id),
|
||||
CONSTRAINT fk_im_tenant_config_tenant FOREIGN KEY (tenant_id) REFERENCES hotgo.im_tenant(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- Tenant database: chapter 1, tenant-aware login, verification, security.
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_user_security (
|
||||
user_id BIGINT NOT NULL,
|
||||
auth_version INT NOT NULL DEFAULT 1,
|
||||
password_updated_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_verification_codes (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
challenge_id VARCHAR(64) NOT NULL,
|
||||
scene VARCHAR(32) NOT NULL,
|
||||
action VARCHAR(64) NOT NULL DEFAULT '',
|
||||
phone_hash CHAR(64) NOT NULL,
|
||||
phone_mask VARCHAR(32) NOT NULL,
|
||||
target_user BIGINT,
|
||||
code_hash CHAR(64) NOT NULL,
|
||||
verification_token_hash CHAR(64),
|
||||
state SMALLINT NOT NULL,
|
||||
attempts INT NOT NULL DEFAULT 0,
|
||||
max_attempts INT NOT NULL,
|
||||
expires_at DATETIME(3) NOT NULL,
|
||||
resend_after DATETIME(3) NOT NULL,
|
||||
verified_at DATETIME(3),
|
||||
consumed_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_verification_challenge (challenge_id),
|
||||
INDEX idx_im_verification_scene_phone_state (scene, phone_hash, state, expires_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_realname_verifications (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
user_id BIGINT NOT NULL,
|
||||
submission_id VARCHAR(64) NOT NULL,
|
||||
status VARCHAR(16) NOT NULL,
|
||||
name_cipher TEXT NOT NULL,
|
||||
id_type VARCHAR(32) NOT NULL,
|
||||
id_number_cipher TEXT NOT NULL,
|
||||
id_number_hash CHAR(64) NOT NULL,
|
||||
masked_name VARCHAR(64) NOT NULL,
|
||||
masked_id_number VARCHAR(64) NOT NULL,
|
||||
review_reason VARCHAR(256),
|
||||
verified_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_realname_submission (submission_id),
|
||||
INDEX idx_im_realname_user_latest (user_id, created_at),
|
||||
INDEX idx_im_realname_id_hash (id_number_hash)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_action_tokens (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
user_id BIGINT NOT NULL,
|
||||
token_id VARCHAR(64) NOT NULL,
|
||||
action VARCHAR(64) NOT NULL,
|
||||
target VARCHAR(128) NOT NULL DEFAULT '',
|
||||
token_hash CHAR(64) NOT NULL,
|
||||
state SMALLINT NOT NULL,
|
||||
expires_at DATETIME(3) NOT NULL,
|
||||
consumed_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_action_tokens_token (token_id),
|
||||
INDEX idx_im_action_tokens_user_action (user_id, action, target, state, expires_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_youth_mode_settings (
|
||||
user_id BIGINT NOT NULL,
|
||||
purpose VARCHAR(64) NOT NULL,
|
||||
password_hash VARBINARY(255) NOT NULL,
|
||||
enabled TINYINT NOT NULL DEFAULT 0,
|
||||
policy_version INT NOT NULL DEFAULT 1,
|
||||
restrictions JSON,
|
||||
enabled_at DATETIME(3),
|
||||
failed_attempts INT NOT NULL DEFAULT 0,
|
||||
locked_until DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (user_id, purpose)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- Tenant database: chapters 2-7, notifications, messages, friends, files, favorites.
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_system_notifications (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
user_id BIGINT NOT NULL,
|
||||
event_id VARCHAR(64) NOT NULL,
|
||||
event_type VARCHAR(64) NOT NULL,
|
||||
title VARCHAR(128) NOT NULL,
|
||||
summary VARCHAR(512) NOT NULL,
|
||||
action JSON,
|
||||
read_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_system_notifications_event (user_id, event_id),
|
||||
INDEX idx_im_system_notifications_inbox (user_id, id DESC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_message_revisions (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
createdat DATETIME(3) NOT NULL,
|
||||
updatedat DATETIME(3) NOT NULL,
|
||||
topic CHAR(25) NOT NULL,
|
||||
seqid INT NOT NULL,
|
||||
version INT NOT NULL,
|
||||
actor BIGINT NOT NULL,
|
||||
head JSON,
|
||||
content JSON,
|
||||
reason VARCHAR(256) NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_message_revisions_version (topic, seqid, version)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_friend_requests (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
request_id VARCHAR(64) NOT NULL,
|
||||
applicant_id BIGINT NOT NULL,
|
||||
target_id BIGINT NOT NULL,
|
||||
message VARCHAR(200) NOT NULL DEFAULT '',
|
||||
source VARCHAR(32) NOT NULL DEFAULT '',
|
||||
source_topic VARCHAR(64) NOT NULL DEFAULT '',
|
||||
status VARCHAR(16) NOT NULL,
|
||||
idempotency_key VARCHAR(128) NOT NULL DEFAULT '',
|
||||
expires_at DATETIME(3) NOT NULL,
|
||||
handled_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_friend_requests_request (request_id),
|
||||
UNIQUE INDEX uk_im_friend_requests_idempotency (applicant_id, idempotency_key),
|
||||
INDEX idx_im_friend_requests_pair (applicant_id, target_id, status, created_at),
|
||||
INDEX idx_im_friend_requests_inbox (target_id, status, created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_friendships (
|
||||
user_id BIGINT NOT NULL,
|
||||
peer_id BIGINT NOT NULL,
|
||||
status VARCHAR(16) NOT NULL,
|
||||
remark VARCHAR(64) NOT NULL DEFAULT '',
|
||||
topic CHAR(25) NOT NULL DEFAULT '',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (user_id, peer_id),
|
||||
INDEX idx_im_friendships_peer (peer_id, status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_user_blocks (
|
||||
blocker_id BIGINT NOT NULL,
|
||||
blocked_id BIGINT NOT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (blocker_id, blocked_id),
|
||||
INDEX idx_im_user_blocks_blocked (blocked_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_favorites (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
favorite_id VARCHAR(64) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
item_type VARCHAR(16) NOT NULL,
|
||||
source_topic CHAR(25) NOT NULL,
|
||||
source_seq INT NOT NULL,
|
||||
snapshot JSON,
|
||||
attachment_refs JSON,
|
||||
availability VARCHAR(16) NOT NULL DEFAULT 'available',
|
||||
idempotency_key VARCHAR(128) NOT NULL DEFAULT '',
|
||||
collected_at DATETIME(3) NOT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_favorites_favorite (favorite_id),
|
||||
UNIQUE INDEX uk_im_favorites_source (user_id, source_topic, source_seq),
|
||||
UNIQUE INDEX uk_im_favorites_idempotency (user_id, ((NULLIF(idempotency_key, '')))),
|
||||
INDEX idx_im_favorites_inbox (user_id, id DESC),
|
||||
INDEX idx_im_favorites_type (user_id, item_type, id DESC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- Existing Tinode tables must match the current schema for chapters 2-7:
|
||||
-- topics.aux, fileuploads.etag, and filemsglinks.topic/userid are required by current code.
|
||||
|
||||
SET @im_sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'client_msg_id') = 0,
|
||||
'ALTER TABLE tinode.messages ADD COLUMN client_msg_id VARCHAR(64) AFTER `from`',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE im_stmt FROM @im_sql;
|
||||
EXECUTE im_stmt;
|
||||
DEALLOCATE PREPARE im_stmt;
|
||||
|
||||
SET @im_sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'version') = 0,
|
||||
'ALTER TABLE tinode.messages ADD COLUMN version INT NOT NULL DEFAULT 1 AFTER client_msg_id',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE im_stmt FROM @im_sql;
|
||||
EXECUTE im_stmt;
|
||||
DEALLOCATE PREPARE im_stmt;
|
||||
|
||||
SET @im_sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'recalled_at') = 0,
|
||||
'ALTER TABLE tinode.messages ADD COLUMN recalled_at DATETIME(3) AFTER version',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE im_stmt FROM @im_sql;
|
||||
EXECUTE im_stmt;
|
||||
DEALLOCATE PREPARE im_stmt;
|
||||
|
||||
SET @im_sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'recalled_by') = 0,
|
||||
'ALTER TABLE tinode.messages ADD COLUMN recalled_by BIGINT AFTER recalled_at',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE im_stmt FROM @im_sql;
|
||||
EXECUTE im_stmt;
|
||||
DEALLOCATE PREPARE im_stmt;
|
||||
|
||||
SET @im_sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'messages' AND INDEX_NAME = 'messages_sender_client_mid') = 0,
|
||||
'CREATE UNIQUE INDEX messages_sender_client_mid ON tinode.messages(`from`, client_msg_id)',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE im_stmt FROM @im_sql;
|
||||
EXECUTE im_stmt;
|
||||
DEALLOCATE PREPARE im_stmt;
|
||||
|
||||
-- ============================================================
|
||||
-- Tenant database: chapter 8, realtime call records.
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_call_records (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
call_id VARCHAR(64) NOT NULL,
|
||||
topic CHAR(25) NOT NULL,
|
||||
seq_id INT NOT NULL,
|
||||
caller_id BIGINT NOT NULL,
|
||||
callee_id BIGINT NOT NULL,
|
||||
call_type VARCHAR(16) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL,
|
||||
end_reason VARCHAR(64) NOT NULL DEFAULT '',
|
||||
duration_ms BIGINT NOT NULL DEFAULT 0,
|
||||
started_at DATETIME(3) NOT NULL,
|
||||
accepted_at DATETIME(3),
|
||||
finished_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_call_records_call (call_id),
|
||||
UNIQUE INDEX uk_im_call_records_topic_seq (topic, seq_id),
|
||||
INDEX idx_im_call_records_caller (caller_id, id DESC),
|
||||
INDEX idx_im_call_records_callee (callee_id, id DESC),
|
||||
INDEX idx_im_call_records_status (status, updated_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- Tenant database: chapter 9, workbench and office collaboration.
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_workbench_apps (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
code VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
icon_file_id VARCHAR(64) NOT NULL DEFAULT '',
|
||||
route VARCHAR(128) NOT NULL,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
unread_key VARCHAR(64) NOT NULL DEFAULT '',
|
||||
enabled TINYINT NOT NULL DEFAULT 1,
|
||||
permission VARCHAR(64) NOT NULL DEFAULT 'enabled',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_workbench_apps_code (code),
|
||||
INDEX idx_im_workbench_apps_enabled_order (enabled, sort_order)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_org_departments (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
department_id VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
parent_id VARCHAR(64) NOT NULL DEFAULT 'root',
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
state VARCHAR(16) NOT NULL DEFAULT 'active',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_org_departments_department (department_id),
|
||||
INDEX idx_im_org_departments_parent (parent_id, state, sort_order, id),
|
||||
INDEX idx_im_org_departments_name (name)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_org_members (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
department_id VARCHAR(64) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
display_name VARCHAR(128) NOT NULL DEFAULT '',
|
||||
position VARCHAR(128) NOT NULL DEFAULT '',
|
||||
avatar_file_id VARCHAR(64) NOT NULL DEFAULT '',
|
||||
mobile VARCHAR(32) NOT NULL DEFAULT '',
|
||||
email VARCHAR(128) NOT NULL DEFAULT '',
|
||||
employment_state VARCHAR(16) NOT NULL DEFAULT 'active',
|
||||
contactable TINYINT NOT NULL DEFAULT 1,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_org_members_department_user (department_id, user_id),
|
||||
INDEX idx_im_org_members_department (department_id, employment_state, sort_order, id),
|
||||
INDEX idx_im_org_members_user (user_id),
|
||||
INDEX idx_im_org_members_display_name (display_name),
|
||||
CONSTRAINT fk_im_org_members_department FOREIGN KEY (department_id) REFERENCES tinode.im_org_departments(department_id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_meetings (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
meeting_id VARCHAR(64) NOT NULL,
|
||||
organizer_id BIGINT NOT NULL,
|
||||
title VARCHAR(200) NOT NULL,
|
||||
start_at DATETIME(3) NOT NULL,
|
||||
end_at DATETIME(3) NOT NULL,
|
||||
location VARCHAR(256) NOT NULL DEFAULT '',
|
||||
state VARCHAR(16) NOT NULL DEFAULT 'scheduled',
|
||||
reminder_minutes JSON,
|
||||
idempotency_key VARCHAR(128),
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
canceled_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_meetings_meeting (meeting_id),
|
||||
UNIQUE INDEX uk_im_meetings_idempotency (organizer_id, idempotency_key),
|
||||
INDEX idx_im_meetings_organizer (organizer_id, start_at),
|
||||
INDEX idx_im_meetings_state_time (state, start_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_meeting_participants (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
meeting_id VARCHAR(64) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
response VARCHAR(16) NOT NULL DEFAULT 'pending',
|
||||
response_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_meeting_participants_user (meeting_id, user_id),
|
||||
INDEX idx_im_meeting_participants_user_time (user_id, meeting_id),
|
||||
INDEX idx_im_meeting_participants_response (meeting_id, response),
|
||||
CONSTRAINT fk_im_meeting_participants_meeting FOREIGN KEY (meeting_id) REFERENCES tinode.im_meetings(meeting_id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_daily_reports (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
report_id VARCHAR(64) NOT NULL,
|
||||
author_id BIGINT NOT NULL,
|
||||
report_date DATE NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
attachments JSON,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'draft',
|
||||
read_state VARCHAR(16) NOT NULL DEFAULT 'unread',
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
submitted_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_daily_reports_report (report_id),
|
||||
UNIQUE INDEX uk_im_daily_reports_author_date (author_id, report_date),
|
||||
INDEX idx_im_daily_reports_author_id (author_id, id DESC),
|
||||
INDEX idx_im_daily_reports_date_status (report_date, status, id DESC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_monthly_reports (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
report_id VARCHAR(64) NOT NULL,
|
||||
author_id BIGINT NOT NULL,
|
||||
report_month CHAR(7) NOT NULL,
|
||||
work_summary TEXT NOT NULL,
|
||||
next_plan TEXT NOT NULL,
|
||||
attachments JSON,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'draft',
|
||||
read_state VARCHAR(16) NOT NULL DEFAULT 'unread',
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
submitted_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_monthly_reports_report (report_id),
|
||||
UNIQUE INDEX uk_im_monthly_reports_author_month (author_id, report_month),
|
||||
INDEX idx_im_monthly_reports_author_id (author_id, id DESC),
|
||||
INDEX idx_im_monthly_reports_month_status (report_month, status, id DESC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_mailbox_config (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
enabled TINYINT NOT NULL DEFAULT 1,
|
||||
allow_anonymous TINYINT NOT NULL DEFAULT 1,
|
||||
required_fields JSON,
|
||||
max_length INT NOT NULL DEFAULT 5000,
|
||||
attachment_policy JSON,
|
||||
notice_title VARCHAR(128) NOT NULL DEFAULT '',
|
||||
notice_content TEXT NOT NULL,
|
||||
notice_updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_mailbox_submissions (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
submission_id VARCHAR(64) NOT NULL,
|
||||
sender_id BIGINT NOT NULL,
|
||||
subject VARCHAR(200) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
anonymous TINYINT NOT NULL DEFAULT 0,
|
||||
attachment_file_ids JSON,
|
||||
notice_version VARCHAR(64) NOT NULL DEFAULT '',
|
||||
idempotency_key VARCHAR(128) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'received',
|
||||
submitted_at DATETIME(3) NOT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_mailbox_submissions_submission (submission_id),
|
||||
UNIQUE INDEX uk_im_mailbox_submissions_idempotency (sender_id, idempotency_key),
|
||||
INDEX idx_im_mailbox_submissions_sender (sender_id, id DESC),
|
||||
INDEX idx_im_mailbox_submissions_status (status, id DESC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- Tenant database: chapter 10, profile, privacy, app release, push, youth mode.
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_user_profiles (
|
||||
user_id BIGINT NOT NULL,
|
||||
avatar_file_id VARCHAR(64) NOT NULL DEFAULT '',
|
||||
nickname VARCHAR(64) NOT NULL DEFAULT '',
|
||||
birth_date DATE,
|
||||
signature VARCHAR(256) NOT NULL DEFAULT '',
|
||||
gender VARCHAR(16) NOT NULL DEFAULT '',
|
||||
region VARCHAR(128) NOT NULL DEFAULT '',
|
||||
public_account VARCHAR(64) NOT NULL DEFAULT '',
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_user_privacy_settings (
|
||||
user_id BIGINT NOT NULL,
|
||||
add_me_methods JSON,
|
||||
profile_visibility VARCHAR(32) NOT NULL DEFAULT 'friends',
|
||||
last_seen_visibility VARCHAR(32) NOT NULL DEFAULT 'friends',
|
||||
read_receipts TINYINT NOT NULL DEFAULT 1,
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_app_releases (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
platform VARCHAR(32) NOT NULL,
|
||||
channel VARCHAR(32) NOT NULL DEFAULT 'stable',
|
||||
latest_version VARCHAR(64) NOT NULL,
|
||||
min_supported_version VARCHAR(64) NOT NULL,
|
||||
update_type VARCHAR(16) NOT NULL DEFAULT 'none',
|
||||
release_notes TEXT NOT NULL,
|
||||
store_url VARCHAR(512) NOT NULL DEFAULT '',
|
||||
package_url VARCHAR(512) NOT NULL DEFAULT '',
|
||||
checksum VARCHAR(128) NOT NULL DEFAULT '',
|
||||
remind_after_seconds INT NOT NULL DEFAULT 0,
|
||||
published_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_app_releases_platform_channel (platform, channel)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_device_push_tokens (
|
||||
device_id VARCHAR(128) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
provider VARCHAR(32) NOT NULL,
|
||||
token VARCHAR(512) NOT NULL,
|
||||
app_version VARCHAR(64) NOT NULL DEFAULT '',
|
||||
platform VARCHAR(32) NOT NULL DEFAULT '',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (device_id),
|
||||
UNIQUE INDEX uk_im_device_push_tokens_provider_token (provider, token),
|
||||
INDEX idx_im_device_push_tokens_user (user_id, updated_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- Tenant database: chapter 11, risk challenge, abuse report, feedback.
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_risk_challenges (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
challenge_id VARCHAR(64) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
action VARCHAR(64) NOT NULL,
|
||||
level VARCHAR(16) NOT NULL,
|
||||
notice_code VARCHAR(64) NOT NULL,
|
||||
notice_params JSON,
|
||||
state VARCHAR(16) NOT NULL DEFAULT 'pending',
|
||||
expires_at DATETIME(3) NOT NULL,
|
||||
decision VARCHAR(16) NOT NULL DEFAULT '',
|
||||
action_token_id VARCHAR(64) NOT NULL DEFAULT '',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_risk_challenges_challenge (challenge_id),
|
||||
INDEX idx_im_risk_challenges_user_state (user_id, state, expires_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_abuse_reasons (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
target_type VARCHAR(16) NOT NULL,
|
||||
reason_code VARCHAR(64) NOT NULL,
|
||||
label VARCHAR(128) NOT NULL,
|
||||
requires_description TINYINT NOT NULL DEFAULT 0,
|
||||
allows_evidence TINYINT NOT NULL DEFAULT 1,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
enabled TINYINT NOT NULL DEFAULT 1,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_abuse_reasons_type_code (target_type, reason_code),
|
||||
INDEX idx_im_abuse_reasons_type_order (target_type, enabled, sort_order)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_abuse_reports (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
report_id VARCHAR(64) NOT NULL,
|
||||
reporter_id BIGINT NOT NULL,
|
||||
target_type VARCHAR(16) NOT NULL,
|
||||
target_user_id BIGINT NOT NULL DEFAULT 0,
|
||||
target_topic VARCHAR(32) NOT NULL DEFAULT '',
|
||||
target_seq INT NOT NULL DEFAULT 0,
|
||||
reason_code VARCHAR(64) NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
evidence_file_ids JSON,
|
||||
snapshot JSON,
|
||||
idempotency_key VARCHAR(128) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'received',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_abuse_reports_report (report_id),
|
||||
UNIQUE INDEX uk_im_abuse_reports_idempotency (reporter_id, idempotency_key),
|
||||
INDEX idx_im_abuse_reports_target (target_type, target_topic, target_seq, target_user_id),
|
||||
INDEX idx_im_abuse_reports_reporter (reporter_id, id DESC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_feedback_config (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
categories JSON,
|
||||
max_length INT NOT NULL DEFAULT 1000,
|
||||
attachment_policy JSON,
|
||||
allow_contact TINYINT NOT NULL DEFAULT 1,
|
||||
diagnostics_notice TEXT NOT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_feedback_submissions (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
feedback_id VARCHAR(64) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
category VARCHAR(64) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
contact VARCHAR(128) NOT NULL DEFAULT '',
|
||||
attachment_file_ids JSON,
|
||||
client_context JSON,
|
||||
include_diagnostics TINYINT NOT NULL DEFAULT 0,
|
||||
idempotency_key VARCHAR(128) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'received',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_feedback_submissions_feedback (feedback_id),
|
||||
UNIQUE INDEX uk_im_feedback_submissions_idempotency (user_id, idempotency_key),
|
||||
INDEX idx_im_feedback_submissions_user (user_id, id DESC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- Seed data.
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO hotgo.im_tenant(id, code, name, tenant_desc, db_name, db_dsn, state, created_by, updated_by)
|
||||
VALUES (1, 'default', 'Default Tenant', NULL, 'tinode', '', 1, 0, 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
name = VALUES(name),
|
||||
db_name = VALUES(db_name),
|
||||
state = VALUES(state),
|
||||
updated_at = CURRENT_TIMESTAMP(3);
|
||||
|
||||
INSERT INTO hotgo.im_tenant_config(tenant_id, max_users, max_groups, max_group_members, created_by, updated_by)
|
||||
SELECT id, 0, 0, 0, 0, 0 FROM hotgo.im_tenant WHERE code = 'default'
|
||||
ON DUPLICATE KEY UPDATE updated_at = CURRENT_TIMESTAMP(3);
|
||||
|
||||
INSERT INTO tinode.im_workbench_apps(code, name, route, sort_order, unread_key, enabled, permission)
|
||||
VALUES
|
||||
('organization', '组织架构', '/organization', 10, 'organization', 1, 'enabled'),
|
||||
('meetings', '会议通知', '/meetings', 20, 'meetings', 1, 'enabled'),
|
||||
('daily_reports', '日报', '/reports/daily', 30, 'daily_reports', 1, 'enabled'),
|
||||
('monthly_reports', '月报', '/reports/monthly', 40, 'monthly_reports', 1, 'enabled'),
|
||||
('executive_mailbox', '总经理信箱', '/executive-mailbox', 50, 'executive_mailbox', 1, 'enabled')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
name = VALUES(name),
|
||||
route = VALUES(route),
|
||||
sort_order = VALUES(sort_order),
|
||||
unread_key = VALUES(unread_key),
|
||||
updated_at = CURRENT_TIMESTAMP(3);
|
||||
|
||||
INSERT INTO tinode.im_mailbox_config(id, enabled, allow_anonymous, required_fields, max_length, attachment_policy, notice_title, notice_content)
|
||||
VALUES (1, 1, 1, '["subject","content"]', 5000, '{"max_files":5,"purpose":"executive_mailbox"}', '总经理信箱', '请如实填写诉求,匿名提交将按企业政策保护展示身份。')
|
||||
ON DUPLICATE KEY UPDATE updated_at = updated_at;
|
||||
|
||||
INSERT INTO tinode.im_app_releases(platform, channel, latest_version, min_supported_version, update_type, release_notes, remind_after_seconds)
|
||||
VALUES
|
||||
('ios', 'stable', '1.0.0', '1.0.0', 'none', '当前已是最新版本。', 0),
|
||||
('android', 'stable', '1.0.0', '1.0.0', 'none', '当前已是最新版本。', 0)
|
||||
ON DUPLICATE KEY UPDATE updated_at = CURRENT_TIMESTAMP(3);
|
||||
|
||||
INSERT INTO tinode.im_abuse_reasons(target_type, reason_code, label, requires_description, allows_evidence, sort_order)
|
||||
VALUES
|
||||
('user', 'fraud', '诈骗欺诈', 0, 1, 10),
|
||||
('user', 'harassment', '骚扰辱骂', 0, 1, 20),
|
||||
('user', 'illegal', '违法违规', 1, 1, 30),
|
||||
('group', 'fraud', '诈骗欺诈', 0, 1, 10),
|
||||
('group', 'spam', '垃圾广告', 0, 1, 20),
|
||||
('group', 'illegal', '违法违规', 1, 1, 30),
|
||||
('message', 'fraud', '诈骗欺诈', 0, 1, 10),
|
||||
('message', 'spam', '垃圾广告', 0, 1, 20),
|
||||
('message', 'harassment', '骚扰辱骂', 0, 1, 30),
|
||||
('message', 'illegal', '违法违规', 1, 1, 40)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
label = VALUES(label),
|
||||
requires_description = VALUES(requires_description),
|
||||
allows_evidence = VALUES(allows_evidence),
|
||||
sort_order = VALUES(sort_order),
|
||||
updated_at = CURRENT_TIMESTAMP(3);
|
||||
|
||||
INSERT INTO tinode.im_feedback_config(id, categories, max_length, attachment_policy, allow_contact, diagnostics_notice)
|
||||
VALUES (1, '["bug","suggestion","complaint","other"]', 1000, '{"max_files":5,"purpose":"feedback"}', 1, '仅在你同意时上传脱敏诊断信息。')
|
||||
ON DUPLICATE KEY UPDATE updated_at = updated_at;
|
||||
|
||||
-- Current MySQL adapter schema version expected by server/db/mysql/adapter.go.
|
||||
UPDATE tinode.kvmeta SET `value` = 123 WHERE `key` = 'version';
|
||||
@@ -0,0 +1,932 @@
|
||||
# IM 服务端多租户改造方案
|
||||
|
||||
> **方案变更说明,2026-08-13:当前多租户技术路线已调整为“控制库 + 每租户独立业务库”。本文原主体内容按“共享库共享表 + tenant_id”编写,只作为旧方案参考,不再作为后续开发准绳。新的落地方案见 [IM 独立库多租户改造方案](IM-独立库多租户改造方案.md)。**
|
||||
>
|
||||
> 新方案下,业务库中的 `users`、`auth`、`topics`、`messages`、`fileuploads` 和后续产品业务表不再增加 `tenant_id` 字段;`TenantID` 仍保留在 Session、Token、Hub、缓存、Push、文件授权、插件上下文和 Store 选库流程中,用于定位租户业务库和运行时隔离。
|
||||
|
||||
> 编写日期:2026-08-10
|
||||
> 适用项目:`im_server` 当前服务端源码
|
||||
> 需求背景:[IM 产品原型需求文档](IM-产品原型需求文档.md) 第 1.1 节“企业码登录”及第 9 章“企业工作台与办公协同”
|
||||
> 关联文档:[IM 产品需求服务端改造分析报告](IM-产品需求服务端改造分析报告.md)
|
||||
|
||||
> 实施状态:第一阶段租户识别已落地。MySQL Adapter schema 117 创建 `im_tenant`、`im_tenant_config` 和默认租户;首次 `hi` 使用企业码解析并固定 Session TenantID。核心业务表的 `tenant_id`、租户化认证和路由隔离尚未完成。
|
||||
|
||||
## 1. 方案目标
|
||||
|
||||
当前项目是单租户 Tinode 服务端。用户、认证记录、Topic、Subscription、Message、File、缓存、集群路由和 Push 都没有租户维度。本方案目标是在一套服务集群和一套 MySQL 数据库中安全承载多个企业租户,并满足以下约束:
|
||||
|
||||
1. 一个企业的数据默认只能被本企业用户和管理员访问;
|
||||
2. 同一手机号、用户名可以在不同企业分别注册;
|
||||
3. 单聊、群聊、搜索、文件、Push、系统消息和后台任务均不可跨租户;
|
||||
4. 租户身份由服务端在握手/认证阶段确定,客户端后续消息不能修改;
|
||||
5. 现有 `usr...`、`grp...`、`p2p...` Topic 格式尽量保持兼容;
|
||||
6. 多租户启用后,可以按租户配置功能、配额、认证方式、数据保留和安全策略。
|
||||
|
||||
## 2. 当前实现为什么不支持多租户
|
||||
|
||||
### 2.1 认证和 Session 没有租户
|
||||
|
||||
- `auth.Rec` 只有 `Uid`、`AuthLevel`、有效期、状态等字段,没有 `TenantID`;
|
||||
- `Session` 只保存 `uid` 和 `authLvl`,登录成功后直接把认证记录写入 Session;
|
||||
- `MsgClientHi`、`MsgClientLogin` 和 Token 中均没有租户;
|
||||
- basic 登录名在数据库中全局唯一,手机号/邮箱凭证也全局唯一;
|
||||
- 密码找回按 `method:value` 全局查找账号;
|
||||
- root 用户可以通过 `Extra.AsUser` 代表任意用户操作,没有租户范围。
|
||||
|
||||
这意味着同一手机号无法在两个企业分别注册,也无法证明当前连接属于哪个企业。
|
||||
|
||||
### 2.2 数据库全部按全局键查询
|
||||
|
||||
改造前 MySQL schema 版本为 116;第一阶段建立租户主表和握手解析后为 117。除租户主数据表外,核心表仍没有 `tenant_id`:
|
||||
|
||||
| 表 | 当前关键唯一约束 | 多租户问题 |
|
||||
| --- | --- | --- |
|
||||
| `users` | `id` | 无租户归属 |
|
||||
| `auth` | `uname` 全局唯一 | 不同企业不能使用相同登录名 |
|
||||
| `credentials` | `synthetic` 全局唯一 | 同一手机号不能加入多个企业 |
|
||||
| `usertags` | 用户/Tag | 搜索可能返回其他企业用户 |
|
||||
| `devices` | 设备 Hash 全局唯一 | 同一设备上的多企业账号会冲突 |
|
||||
| `topics` | `name` 全局唯一 | 没有 Topic 租户边界 |
|
||||
| `subscriptions` | Topic/User | 不能证明用户与 Topic 属于同一租户 |
|
||||
| `messages` | Topic/SeqId | 查询只按 Topic,没有租户条件 |
|
||||
| `dellog` | Topic/User | 删除记录没有租户条件 |
|
||||
| `fileuploads` | File ID | 文件查询和清理没有租户范围 |
|
||||
| `filemsglinks` | File/Message/Topic/User | 附件关联不能强制同租户 |
|
||||
|
||||
Store/Adapter 的方法也只接收 UID 或 Topic,例如:
|
||||
|
||||
```go
|
||||
UserGet(uid)
|
||||
AuthGetUniqueRecord(unique)
|
||||
TopicGet(topic)
|
||||
SubscriptionGet(topic, uid, keepDeleted)
|
||||
MessageGetAll(topic, uid, opts)
|
||||
FileGet(fileID)
|
||||
```
|
||||
|
||||
只在 SQL 表加字段但不修改这些接口,后续开发很容易漏写 `WHERE tenant_id = ?`,仍然会发生越权。
|
||||
|
||||
### 2.3 内存、集群和系统 Topic 没有租户
|
||||
|
||||
- Hub 的 `topics` 使用裸 Topic 名作为 `sync.Map` Key;
|
||||
- 集群一致性哈希只对裸 Topic 名计算;
|
||||
- `ClusterSess` 和集群 RPC 没有租户字段;
|
||||
- User Cache 仅以 UID 为 Key;
|
||||
- Push Receipt、FCM Channel 只有 UID/Topic,没有租户;
|
||||
- Hub 启动时只初始化一个字面值为 `sys` 的全局系统 Topic;
|
||||
- 持久化缓存 Key 没有租户命名空间。
|
||||
|
||||
如果两个租户都使用 `sys` 等逻辑 Topic,同一个内存对象会被共享。即使普通 Topic 名当前由全局 UID 生成,也不能依赖“ID 不碰撞”代替租户授权。
|
||||
|
||||
### 2.4 文件接口只获得 UID
|
||||
|
||||
`authFileRequest` 当前只返回 UID。文件下载查询只按 File ID,无法把文件访问限制在当前租户。对象存储 Location 也没有统一的租户目录规则。
|
||||
|
||||
## 3. 关键设计决策
|
||||
|
||||
### 3.1 目标模式
|
||||
|
||||
本方案采用:
|
||||
|
||||
> **共享服务集群 + 共享 MySQL 数据库 + 共享表 + 每行 `tenant_id` 强隔离**
|
||||
|
||||
该模式最符合“多企业共用一套平台”的目标,但也是改造面最大的模式。若实际只需要企业数据物理隔离,采用“一企一实例/一企一库”会更简单;它不属于本文的共享多租户实现。
|
||||
|
||||
### 3.2 用户模型:租户内账号
|
||||
|
||||
推荐同一个自然人在不同企业中对应不同的 Tinode User:
|
||||
|
||||
```text
|
||||
手机号 138****0000
|
||||
├─ 租户 A -> user A1 -> 独立好友、群、消息和设置
|
||||
└─ 租户 B -> user B1 -> 独立好友、群、消息和设置
|
||||
```
|
||||
|
||||
这样可以最大程度复用当前 User/Topic/Subscription 模型,离职、停用、数据导出和删除也能按企业处理。
|
||||
|
||||
如果以后需要一个账号切换多个企业,可新增 `global_identities` 和 `tenant_memberships`,把全局身份映射到各租户的本地 User。不要在第一阶段直接把当前 User 改造成跨租户共享主体,否则所有 P2P、资料、设备、未读数和隐私规则都会变得更复杂。
|
||||
|
||||
### 3.3 ID 策略:保持全局唯一,额外校验租户
|
||||
|
||||
继续使用现有 Snowflake/加密 UID 生成器,User ID、File ID、普通 Group Topic 名保持全局唯一。外部协议继续使用现有 `usr...`、`grp...`、`p2p...` 格式,不把租户编码暴露在这些 ID 中。
|
||||
|
||||
所有数据库查询仍必须带 `tenant_id`,原因是:
|
||||
|
||||
- 全局唯一只防止碰撞,不能证明调用者有权访问;
|
||||
- 已知或泄露的其他租户 ID 仍然可能被直接请求;
|
||||
- 使用复合租户条件可以让越权请求稳定返回不存在。
|
||||
|
||||
### 3.4 跨租户通信默认禁止
|
||||
|
||||
第一阶段规则:
|
||||
|
||||
- 用户搜索只在当前租户;
|
||||
- P2P 双方必须属于同一租户;
|
||||
- Group Topic 及全部成员必须属于同一租户;
|
||||
- 文件、收藏、搜索结果和系统通知不跨租户;
|
||||
- Tenant Admin 只能管理本租户;
|
||||
- Platform Admin 的跨租户操作只能走独立后台 API,禁止使用普通客户端协议随意 `AsUser`。
|
||||
|
||||
未来若需要跨企业协作,应作为“外部联系人/联邦通信”独立设计,使用明确授权、双边租户策略和审计,不能绕过本方案的同租户不变量。
|
||||
|
||||
## 4. 租户领域模型
|
||||
|
||||
### 4.1 TenantID 与租户上下文
|
||||
|
||||
新增强类型,不使用裸字符串在各层传递:
|
||||
|
||||
```go
|
||||
package types
|
||||
|
||||
type TenantID int64
|
||||
|
||||
const ZeroTenantID TenantID = 0
|
||||
|
||||
type TenantContext struct {
|
||||
TenantID TenantID
|
||||
ActorUID Uid
|
||||
}
|
||||
|
||||
type TopicKey struct {
|
||||
TenantID TenantID
|
||||
Topic string
|
||||
}
|
||||
|
||||
type UserKey struct {
|
||||
TenantID TenantID
|
||||
UID Uid
|
||||
}
|
||||
```
|
||||
|
||||
`TenantID = 0` 仅允许出现在“尚未完成租户握手”的连接和平台级任务中。任何租户业务 Store 调用发现 `ZeroTenantID` 应立即返回错误,不能自动降级为全局查询。
|
||||
|
||||
`context.Context` 继续用于超时和取消;租户 ID 应作为显式的强类型参数或租户作用域对象,不能只隐藏在 `context.Value` 中。
|
||||
|
||||
### 4.2 租户表
|
||||
|
||||
建议新增:
|
||||
|
||||
```sql
|
||||
CREATE TABLE im_tenant (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
code VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
tenant_desc VARCHAR(256),
|
||||
state SMALLINT NOT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_by BIGINT NOT NULL,
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
updated_by BIGINT NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY tenants_code (code)
|
||||
);
|
||||
```
|
||||
|
||||
`created_by` 和 `updated_by` 保存操作人用户 ID;系统初始化默认租户时使用 `0` 表示系统操作。
|
||||
|
||||
当前阶段不在租户主表预留 `config_version` 和 `data_region`。真正建设租户配置中心或多地域路由时,再通过独立的 `tenant_settings` 表增加相应版本/地域字段,避免提前维护没有实际使用场景的列。
|
||||
|
||||
租户状态建议包括:
|
||||
|
||||
| 状态 | 登录 | 读消息 | 发消息/写业务 | 管理操作 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `provisioning` | 禁止 | 禁止 | 禁止 | 平台管理员可配置 |
|
||||
| `active` | 允许 | 允许 | 允许 | 按角色允许 |
|
||||
| `suspended` | 禁止新登录 | 可配置只读 | 禁止 | 平台管理员可恢复/导出 |
|
||||
| `deleting` | 禁止 | 禁止 | 禁止 | 仅执行删除任务 |
|
||||
| `deleted` | 禁止 | 禁止 | 禁止 | 只保留必要审计 |
|
||||
|
||||
### 4.3 租户配置与配额
|
||||
|
||||
第一阶段只建立 `im_tenant_config`,保存用户数、群数和单群成员数上限。每个租户最多一条配置;三个上限字段为 `0` 时表示不限制。
|
||||
|
||||
```sql
|
||||
CREATE TABLE im_tenant_config (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
max_users BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
|
||||
max_groups BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
|
||||
max_group_members INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_by BIGINT NOT NULL,
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
|
||||
ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
updated_by BIGINT NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_im_tenant_config_tenant_id (tenant_id),
|
||||
CONSTRAINT fk_im_tenant_config_tenant
|
||||
FOREIGN KEY (tenant_id) REFERENCES im_tenant(id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
```
|
||||
|
||||
`created_by` 和 `updated_by` 保存操作人用户 ID。系统初始化时可以使用约定的系统操作人 ID;在平台管理员账号模型确定前,暂不为这两个字段建立用户外键。
|
||||
|
||||
认证、文件、消息频控、功能开关、保留期、合规策略、第三方服务配置及企业品牌等配置本阶段不建字段,待有明确业务需求后再增加。
|
||||
|
||||
## 5. 租户识别与认证流程
|
||||
|
||||
### 5.1 推荐流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant A as App
|
||||
participant S as Session
|
||||
participant DB as MySQL
|
||||
|
||||
A->>S: 建立 WebSocket
|
||||
A->>S: hi(tenant_code)
|
||||
S->>DB: 按 code 查询租户
|
||||
DB-->>S: tenant
|
||||
S->>S: 校验 active 并固定 TenantID
|
||||
S-->>A: hi success + 公开租户信息
|
||||
A->>S: login(scheme, secret)
|
||||
```
|
||||
|
||||
第一阶段不引入 Tenant Resolve API 和 `tenant_ticket`。企业码不是密码,客户端可直接提交;服务端必须查询数据库并只接受 `active` 租户,客户端不能直接提交数据库 TenantID。
|
||||
|
||||
`tenant_ticket` 的价值在于把一次企业码解析结果变成服务端签名的短期凭证,防止客户端伪造内部 TenantID,并减少 WebSocket 节点的重复查询;它还可以携带目标集群、过期时间和随机数。当前是单集群且刚开始建设,直接查询更简单。以后出现多集群路由或租户解析查询成为性能瓶颈时,可以再增加 ticket,不影响 Session 固定 TenantID 的核心约束。
|
||||
|
||||
### 5.2 协议修改
|
||||
|
||||
在 `MsgClientHi` 增加:
|
||||
|
||||
```go
|
||||
Tenant string `json:"tenant,omitempty"`
|
||||
```
|
||||
|
||||
该字段当前直接传企业码,不传数据库 TenantID。规则如下:
|
||||
|
||||
1. 首次 `hi` 必须设置 Tenant;
|
||||
2. Session 一旦绑定租户,后续 `hi/login/acc/pub/get/set/del/note` 都不能修改;
|
||||
3. 注册、登录和密码找回前必须已绑定租户;
|
||||
4. Tenant 缺失、为空或超过 64 个字符时拒绝握手,不做默认租户兼容;
|
||||
5. 租户不存在时返回 404,租户不是 `active` 状态时返回 403;
|
||||
6. 服务端返回的租户信息仅包含 `code` 和 `name`,不返回内部 ID、状态或密钥。
|
||||
|
||||
当前已实现 `MsgClientHi.Tenant`、MySQL 按企业码查询、Session TenantID 固定、租户公开信息响应,以及未绑定租户时拒绝后续命令。认证器和业务表尚未带 TenantID,因此当前代码只是租户识别基础,不能视为已经完成数据隔离。
|
||||
|
||||
后续 HTTP API 的 TenantID 必须从租户化签名 Token 中获得。禁止只信任 URL、Header 或请求 Body 中客户端自报的 `tenant_id`。
|
||||
|
||||
### 5.3 认证接口修改
|
||||
|
||||
当前认证器接口:
|
||||
|
||||
```go
|
||||
Authenticate(secret []byte, remoteAddr string)
|
||||
```
|
||||
|
||||
建议改为:
|
||||
|
||||
```go
|
||||
type AuthContext struct {
|
||||
TenantID types.TenantID
|
||||
RemoteAddr string
|
||||
}
|
||||
|
||||
Authenticate(ctx AuthContext, secret []byte)
|
||||
```
|
||||
|
||||
`auth.Rec` 增加:
|
||||
|
||||
```go
|
||||
TenantID types.TenantID `json:"tenant_id,omitempty"`
|
||||
```
|
||||
|
||||
basic、REST、code、token 等认证器和所有 Mock 必须同步修改。认证器返回 TenantID 与 Session 不一致时,统一按认证失败处理,并写安全审计。
|
||||
|
||||
### 5.4 登录名和凭证的租户唯一性
|
||||
|
||||
数据库约束修改为:
|
||||
|
||||
```text
|
||||
auth: UNIQUE (tenant_id, uname)
|
||||
auth: UNIQUE (tenant_id, userid, scheme)
|
||||
credentials: UNIQUE (tenant_id, synthetic)
|
||||
devices: UNIQUE (tenant_id, hash)
|
||||
```
|
||||
|
||||
这样同一手机号可以分别加入多个企业。密码找回必须先有 TenantContext,再按 `(tenant_id, method, value)` 查找,不能全库搜索手机号后选择第一个用户。
|
||||
|
||||
### 5.5 Token v2
|
||||
|
||||
当前 HMAC Token 固定包含 UID、过期时间、认证级别、序列号和特性位,没有 TenantID。需要新增带版本的 Token v2:
|
||||
|
||||
```text
|
||||
[version][tenant_id][uid][expires][auth_level][serial][features][signature]
|
||||
```
|
||||
|
||||
Token 规则:
|
||||
|
||||
1. v2 Token 的签名覆盖 TenantID;
|
||||
2. Token 认证返回 `TenantID + UID`;
|
||||
3. Session 已绑定租户时必须与 Token 一致;
|
||||
4. 不包含 TenantID 的 Token 一律拒绝;
|
||||
5. 租户停用时应支持按租户 Token 版本或撤销时间拒绝已签发 Token,不能只能全平台提升 serial。
|
||||
|
||||
### 5.6 管理员模型
|
||||
|
||||
当前 `AuthLevel` 只有 NONE、ANON、AUTH、ROOT,不足以表达租户管理员。建议保留认证级别,同时增加业务角色:
|
||||
|
||||
- `platform_admin`:平台级租户生命周期和运维;
|
||||
- `tenant_owner`:本租户最高管理员;
|
||||
- `tenant_admin`:组织、用户、群和安全管理;
|
||||
- `security_auditor`:实名、举报、审计读取;
|
||||
- 普通用户。
|
||||
|
||||
Tenant Admin 的权限必须绑定 TenantID。普通 WebSocket 的 `Extra.AsUser` 只能在同租户且有明确授权时使用;平台管理员跨租户代操作应走隔离的后台 API、二次验证和完整审计。
|
||||
|
||||
## 6. Session、消息和 Topic 改造
|
||||
|
||||
### 6.1 Session
|
||||
|
||||
`Session`、`ClusterSess` 增加:
|
||||
|
||||
```go
|
||||
tenantID types.TenantID
|
||||
```
|
||||
|
||||
需要建立以下不变量:
|
||||
|
||||
```text
|
||||
已认证 Session:
|
||||
session.tenantID != 0
|
||||
session.uid != 0
|
||||
User(session.uid).tenant_id == session.tenantID
|
||||
```
|
||||
|
||||
所有内部 `ClientComMessage` 和 `ServerComMessage` 应携带服务端写入的 TenantID,序列化给普通客户端时不必暴露。插件、集群和后台任务可以读取,但不得覆盖。
|
||||
|
||||
### 6.2 内部路由键
|
||||
|
||||
外部 Topic 名保持不变,内部统一使用:
|
||||
|
||||
```go
|
||||
func (k TopicKey) RoutingKey() string {
|
||||
return strconv.FormatUint(uint64(k.TenantID), 10) + ":" + k.Topic
|
||||
}
|
||||
```
|
||||
|
||||
需要修改:
|
||||
|
||||
- Hub `topicGet/topicPut/topicDel`;
|
||||
- Session 的订阅 Map;
|
||||
- Topic Proxy 和 Multiplex Session Key;
|
||||
- 集群一致性哈希 `nodeForTopic/isRemoteTopic`;
|
||||
- ClusterReq、ClusterRoute、ClusterResp;
|
||||
- Topic 重哈希和代理失效;
|
||||
- 调试端点中显示的 Topic 标识。
|
||||
|
||||
客户端仍看到原始 Topic 名,只有服务端内部使用 RoutingKey。不要直接把 `tenantID:` 前缀写入数据库 Topic 名或回传客户端,否则会破坏 Topic 类别解析、P2P 解析和现有客户端。
|
||||
|
||||
### 6.3 `sys`、`me`、`fnd`、`slf`
|
||||
|
||||
- `me`、`fnd`、`slf` 虽由全局 UID 派生,仍必须使用 Session TenantID 加载;
|
||||
- 全局字面值 `sys` 必须改为“租户作用域内的逻辑别名”;
|
||||
- Hub 不再启动一个全局 `sys` Topic;应在租户首次使用或租户创建时初始化 `(tenant_id, sys)`;
|
||||
- 系统消息生产者必须指定 TenantID,不能广播到全平台 `sys`;
|
||||
- `fnd` 搜索只读取当前租户 Tags;
|
||||
- `slf` 收藏不能引用其他租户的源消息或文件。
|
||||
|
||||
### 6.4 P2P
|
||||
|
||||
P2P Topic 名仍由两个全局 UID 生成,但创建前必须执行:
|
||||
|
||||
```text
|
||||
initiator.tenant_id == invited.tenant_id == session.tenant_id
|
||||
```
|
||||
|
||||
订阅、发送、读取、删除和通话时都继续带 TenantID 查询,避免通过已知 P2P 名访问其他企业。
|
||||
|
||||
### 6.5 群聊
|
||||
|
||||
创建 Group Topic 时由 Session 注入 TenantID。邀请、审批、管理员变更和群主转让必须验证:
|
||||
|
||||
- Topic 属于当前租户;
|
||||
- 操作者属于当前租户;
|
||||
- 目标成员属于同一租户;
|
||||
- 所有写入的 Subscription 使用同一 TenantID;
|
||||
- Outbox 和系统通知也使用相同 TenantID。
|
||||
|
||||
不要接受客户端在 `set.desc`、Aux 或 Tags 中提交的 tenant 字段作为授权依据。
|
||||
|
||||
## 7. Store 与 Adapter 改造
|
||||
|
||||
### 7.1 不推荐只靠 `context.Value`
|
||||
|
||||
租户条件应进入编译期可见的接口。推荐增加租户作用域 Store:
|
||||
|
||||
```go
|
||||
tenantStore := store.ForTenant(session.tenantID)
|
||||
|
||||
user, err := tenantStore.Users.Get(uid)
|
||||
topic, err := tenantStore.Topics.Get(topicName)
|
||||
msgs, err := tenantStore.Messages.GetAll(topicName, uid, opts)
|
||||
```
|
||||
|
||||
底层 Adapter 接口显式接收 TenantID:
|
||||
|
||||
```go
|
||||
UserGet(tenantID, uid)
|
||||
AuthGetUniqueRecord(tenantID, unique)
|
||||
TopicGet(tenantID, topic)
|
||||
SubscriptionGet(tenantID, topic, uid, keepDeleted)
|
||||
MessageGetAll(tenantID, topic, uid, opts)
|
||||
FileGet(tenantID, fileID)
|
||||
```
|
||||
|
||||
`store.ForTenant(ZeroTenantID)` 必须失败。平台级扫描和租户生命周期操作使用单独的 `PlatformStore` 接口,不能给业务代码一个可省略 TenantID 的全局 Store。
|
||||
|
||||
### 7.2 需要修改的 Store 接口范围
|
||||
|
||||
| 接口域 | 必须租户化的操作 |
|
||||
| --- | --- |
|
||||
| Users | Create/Get/GetAll/Update/Delete/State/Tags/Topics/Subs/Unread |
|
||||
| Auth | Add/Get/Update/Delete/Unique lookup |
|
||||
| Credentials | Upsert/Get/Confirm/Fail/Delete/Reset lookup |
|
||||
| Topics | Create/Get/Share/Delete/Update/Owner/Find |
|
||||
| Subscriptions | Create/Get/Update/Delete/按 User/Topic 查询 |
|
||||
| Messages | Save/Get/Delete/GetDeleted/更新 Topic SeqId |
|
||||
| Devices | Upsert/Get/Delete/Push Channel |
|
||||
| Files | Start/Finish/Get/Link/DeleteUnused/Access check |
|
||||
| Persistent Cache | Get/Upsert/Delete/Expire |
|
||||
|
||||
这会修改 `server/store/store.go`、`server/db/adapter.go`、MySQL Adapter、Mock Store 以及大量调用点。不要为了减少改动只改少数查询;任何保留的无租户业务接口都会成为后续越权入口。
|
||||
|
||||
### 7.3 数据库支持范围
|
||||
|
||||
当前仓库还有 PostgreSQL、MongoDB、RethinkDB Adapter。多租户改造会使公共 Adapter 接口整体变化。
|
||||
|
||||
推荐产品版第一阶段明确只支持 MySQL:
|
||||
|
||||
- MySQL 完成多租户 schema 和全部集成测试;
|
||||
- 其他 Adapter 编译时可使用明确的 `ErrUnsupportedTenant` 或从产品构建中排除;
|
||||
- 不能让未实现多租户过滤的 Adapter 在配置错误时继续启动;
|
||||
- 如果必须继续支持四种数据库,应将工作量视为四套独立实现和隔离验收。
|
||||
|
||||
## 8. MySQL Schema 改造
|
||||
|
||||
### 8.1 统一规则
|
||||
|
||||
1. 所有租户业务表增加 `tenant_id BIGINT NOT NULL`;
|
||||
2. 唯一索引和高频查询索引以 `tenant_id` 作为第一列;
|
||||
3. 子表使用 `(tenant_id, parent_id)` 复合外键,防止跨租户关联;
|
||||
4. `kvmeta` 中数据库 schema 版本属于平台级元数据,不加 TenantID;
|
||||
5. 持久化业务缓存使用 TenantID 列或强制租户 Key 前缀;
|
||||
6. 所有删除、更新、聚合和后台批处理同样带 TenantID;
|
||||
7. MySQL 没有可直接依赖的应用级 RLS,隔离责任主要在 Adapter 和复合约束。
|
||||
|
||||
### 8.2 核心表与索引
|
||||
|
||||
| 表 | 新增列 | 主要索引调整 |
|
||||
| --- | --- | --- |
|
||||
| `users` | `tenant_id` | `UNIQUE(tenant_id,id)`;状态/最近登录索引前加 tenant |
|
||||
| `usertags` | `tenant_id` | `INDEX(tenant_id,tag)`;`UNIQUE(tenant_id,userid,tag)` |
|
||||
| `devices` | `tenant_id` | `UNIQUE(tenant_id,hash)`;复合外键到 users |
|
||||
| `auth` | `tenant_id` | `UNIQUE(tenant_id,uname)`;`UNIQUE(tenant_id,userid,scheme)` |
|
||||
| `topics` | `tenant_id` | `UNIQUE(tenant_id,name)`;Owner/State 索引前加 tenant |
|
||||
| `topictags` | `tenant_id` | `INDEX(tenant_id,tag)`;`UNIQUE(tenant_id,topic,tag)` |
|
||||
| `subscriptions` | `tenant_id` | `UNIQUE(tenant_id,topic,userid)`;User/Topic 查询前加 tenant |
|
||||
| `messages` | `tenant_id` | `UNIQUE(tenant_id,topic,seqid)`;历史查询索引前加 tenant |
|
||||
| `dellog` | `tenant_id` | Topic/User/DelId 索引前加 tenant |
|
||||
| `credentials` | `tenant_id` | `UNIQUE(tenant_id,synthetic)`;User/Method 索引前加 tenant |
|
||||
| `fileuploads` | `tenant_id` | `INDEX(tenant_id,status,updatedat)`;复合用户归属 |
|
||||
| `filemsglinks` | `tenant_id` | File/Message/Topic/User 索引及复合外键全部带 tenant |
|
||||
|
||||
Topic 名当前通常全局唯一,但仍把数据库唯一约束改为 `(tenant_id, name)`,这样各租户可以拥有自己的逻辑 `sys`,也让 schema 明确表达隔离边界。
|
||||
|
||||
### 8.3 业务表
|
||||
|
||||
后续新增的所有 `im_*` 表,包括好友、群审批、组织、会议、日报、举报、Outbox、审计和消息索引,必须从第一版就包含 TenantID。禁止先以全局表上线再补租户。
|
||||
|
||||
推荐所有 Repository 方法使用如下形态:
|
||||
|
||||
```text
|
||||
Get(tenantID, id)
|
||||
List(tenantID, filter, cursor)
|
||||
Create(tenantID, actorID, data)
|
||||
Update(tenantID, id, expectedVersion, data)
|
||||
Delete(tenantID, id)
|
||||
```
|
||||
|
||||
### 8.4 SQL 示例
|
||||
|
||||
认证查询:
|
||||
|
||||
```sql
|
||||
SELECT userid, secret, expires, authlvl
|
||||
FROM auth
|
||||
WHERE tenant_id = ? AND uname = ?;
|
||||
```
|
||||
|
||||
Topic 查询:
|
||||
|
||||
```sql
|
||||
SELECT ...
|
||||
FROM topics
|
||||
WHERE tenant_id = ? AND name = ? AND state <> ?;
|
||||
```
|
||||
|
||||
消息查询:
|
||||
|
||||
```sql
|
||||
SELECT ...
|
||||
FROM messages
|
||||
WHERE tenant_id = ? AND topic = ? AND seqid < ?
|
||||
ORDER BY seqid DESC
|
||||
LIMIT ?;
|
||||
```
|
||||
|
||||
所有 `UPDATE` 和 `DELETE` 必须同时带业务主键和 `tenant_id`。如果受影响行数为 0,统一返回 NotFound/Denied,不能再用无租户查询判断对象是否属于其他企业,以免泄露对象存在性。
|
||||
|
||||
## 9. 缓存与集群改造
|
||||
|
||||
### 9.1 Hub Topic Cache
|
||||
|
||||
当前 Hub 使用裸 Topic 名作为 Key,应改为 `TopicKey.RoutingKey()`。Topic 对象自身增加 TenantID,初始化后不可修改。
|
||||
|
||||
加载 Topic 时同时检查:
|
||||
|
||||
```text
|
||||
requested tenant == loaded topic tenant == database row tenant
|
||||
```
|
||||
|
||||
如果不一致,记录安全告警并拒绝把 Topic 放入缓存。
|
||||
|
||||
### 9.2 User Cache
|
||||
|
||||
现有 UID 在全平台全局唯一,理论上可以继续只按 UID 缓存。但为避免导入数据、测试固定 UID 或代码误用造成污染,建议改为 `UserKey{TenantID, UID}`。
|
||||
|
||||
`UserCacheReq`、未读数 IO、Pending Push、EvictUser、集群 UserCacheUpdate 全部增加 TenantID。设备注销或账号停用只能影响指定租户的 User。
|
||||
|
||||
### 9.3 Session Store
|
||||
|
||||
Session ID 继续全局随机即可,但以下操作必须带租户断言:
|
||||
|
||||
- 通过 SID 完成文件认证;
|
||||
- `EvictUser`;
|
||||
- Cluster Proxy 恢复 Session;
|
||||
- root/管理员代操作;
|
||||
- 调试页面查看 Session。
|
||||
|
||||
建议新增 `EvictUser(tenantID, uid, skipSID)`,防止固定/导入 UID 冲突时误踢其他企业用户。
|
||||
|
||||
### 9.4 集群路由
|
||||
|
||||
一致性哈希由:
|
||||
|
||||
```text
|
||||
hash(topic)
|
||||
```
|
||||
|
||||
改为:
|
||||
|
||||
```text
|
||||
hash(tenant_id + ":" + topic)
|
||||
```
|
||||
|
||||
`ClusterSess`、`ClusterReq`、`ClusterRoute`、`ClusterResp` 和 User Cache RPC 增加 TenantID。接收端验证:
|
||||
|
||||
- RPC 声明租户与消息内部租户一致;
|
||||
- Session 租户与请求租户一致;
|
||||
- RoutingKey 重新计算结果与目标节点一致;
|
||||
- Plugin 返回的替换消息不能修改租户。
|
||||
|
||||
集群节点加入时必须校验协议版本,不理解 Tenant 字段的节点应拒绝加入集群。
|
||||
|
||||
### 9.5 持久化缓存
|
||||
|
||||
缓存 Key 统一通过函数生成:
|
||||
|
||||
```go
|
||||
TenantCacheKey(tenantID, namespace, key)
|
||||
```
|
||||
|
||||
禁止业务代码自行字符串拼接。租户配置变化时只失效该租户 Key。平台级 schema/version Key 与租户业务 Cache 使用不同接口。
|
||||
|
||||
## 10. 文件与对象存储改造
|
||||
|
||||
### 10.1 HTTP 认证结果
|
||||
|
||||
`authFileRequest` 从只返回 UID 改为:
|
||||
|
||||
```go
|
||||
type RequestPrincipal struct {
|
||||
TenantID types.TenantID
|
||||
UID types.Uid
|
||||
Roles []string
|
||||
}
|
||||
```
|
||||
|
||||
Token 认证和 SID 认证都必须得到相同 Principal。API Key 只表示合法客户端应用,不能替代 Tenant 身份和资源授权。
|
||||
|
||||
### 10.2 文件访问
|
||||
|
||||
新增:
|
||||
|
||||
```go
|
||||
FileCanAccess(tenantID, uid, fileID, action)
|
||||
```
|
||||
|
||||
访问流程:
|
||||
|
||||
1. `FileGet(tenantID, fileID)`;
|
||||
2. 上传未完成时只允许同租户上传者访问;
|
||||
3. 已关联消息时,Subscription 必须为同租户且有 Topic Read 权限;
|
||||
4. 群文件删除检查同租户上传者/管理员/群主;
|
||||
5. 转发时源文件、源消息和目标 Topic 全部属于同租户;
|
||||
6. 生成短时签名 URL,签名上下文包含 TenantID/FileID/过期时间;
|
||||
7. 审计日志记录 TenantID、UID、FileID 和结果。
|
||||
|
||||
### 10.3 存储路径
|
||||
|
||||
新文件统一使用:
|
||||
|
||||
```text
|
||||
tenants/{tenant_id}/yyyy/mm/{file_id}
|
||||
```
|
||||
|
||||
Location 不直接返回客户端。垃圾回收任务按 TenantID 分批并设置公平配额,避免大租户占满清理队列。
|
||||
|
||||
## 11. 搜索、Tags 与组织数据
|
||||
|
||||
### 11.1 用户和 Topic 搜索
|
||||
|
||||
`Find`、`FindOne`、Tag 更新和精确账号搜索都必须带 TenantID。相同手机号/用户名在不同租户生成相同 Tag 没有问题,因为索引和唯一约束均以 TenantID 为前缀。
|
||||
|
||||
跨租户查询统一返回空结果或通用 NotFound,不能返回“该用户属于其他企业”。
|
||||
|
||||
### 11.2 消息搜索
|
||||
|
||||
消息索引记录必须包含 TenantID,搜索引擎索引名或文档路由也包含 TenantID:
|
||||
|
||||
```text
|
||||
tenant_id + topic + seq_id
|
||||
```
|
||||
|
||||
查询至少经过两层过滤:
|
||||
|
||||
1. 搜索请求强制加入 TenantID;
|
||||
2. 返回结果按当前用户的同租户 Topic Read ACL 二次校验。
|
||||
|
||||
如果使用 OpenSearch/Elasticsearch,可以共享索引并按 TenantID 路由,也可以按大租户独立索引。无论哪种方式,TenantID 必须来自 Token,而不是客户端 Query 参数。
|
||||
|
||||
### 11.3 组织架构
|
||||
|
||||
`im_org_units`、`im_org_members` 等第 9 章业务表以 TenantID 为根。部门树的 `parent_id`、负责人、成员和数据权限都必须使用复合租户约束,禁止把其他租户 User 设置为部门负责人。
|
||||
|
||||
## 12. Push、插件和外部服务
|
||||
|
||||
### 12.1 Push
|
||||
|
||||
`push.Receipt`、`ChannelReq` 和 `Payload` 内部增加 TenantID。要求:
|
||||
|
||||
- 设备查询使用 `(tenant_id, uid)`;
|
||||
- FCM Topic/Channel 名加入不可碰撞的租户前缀;
|
||||
- 租户级 Push 模板、应用包和凭证由服务端配置选择;
|
||||
- Push Handler 日志不可输出其他租户的设备 Token;
|
||||
- 未读数 Cache 使用 Tenant User Key;
|
||||
- 锁屏正文按租户安全策略生成。
|
||||
|
||||
如果不同租户使用不同品牌 App,Push Handler 需要根据 TenantID 选择 FCM/APNS 凭证,不能继续假定一套全局凭证。
|
||||
|
||||
### 12.2 Plugin Firehose
|
||||
|
||||
Plugin 请求增加 TenantID、租户策略版本和不可伪造的上下文。插件返回 `REPLACE` 时服务端必须保留原 TenantID,禁止插件把消息换到其他租户/Topic。
|
||||
|
||||
插件配置分两类:
|
||||
|
||||
- 平台级统一审核服务;
|
||||
- 租户级专属审核服务。
|
||||
|
||||
无论哪种,调用超时、熔断、失败开放/关闭策略都按租户配置,审计和指标带 TenantID。
|
||||
|
||||
### 12.3 REST 认证、短信、TURN 和对象存储
|
||||
|
||||
外部请求必须携带服务端签名的租户上下文,并对回调结果校验 TenantID。租户自带密钥时,通过密钥管理系统按 TenantID 选择,不把 Secret 缓存在普通 Topic/User JSON 中。
|
||||
|
||||
## 13. API Key、限流和配额
|
||||
|
||||
当前 `X-Tinode-APIKey` 是应用级准入凭证。多租户后建议保持以下职责:
|
||||
|
||||
- API Key:识别客户端应用、平台、版本和是否允许连接;
|
||||
- Tenant Ticket/Token:确定租户;
|
||||
- User Token:确定租户内用户;
|
||||
- Role/Policy:确定具体操作权限。
|
||||
|
||||
不要仅通过 API Key 推导 Tenant,因为同一个 App 可能登录多个企业。若某企业使用专属 App,可额外把 API Key 限定到一个 Tenant,但仍需验证 Token Tenant 一致。
|
||||
|
||||
限流 Key 至少包含:
|
||||
|
||||
```text
|
||||
tenant + endpoint/action
|
||||
tenant + uid + action
|
||||
tenant + ip + action
|
||||
```
|
||||
|
||||
配额计数包括用户、Topic、消息速率、文件大小、总存储、会议/报告等。超限返回稳定业务错误并记录租户审计,不能让一个大租户耗尽全平台队列。
|
||||
|
||||
## 14. 日志、指标与审计
|
||||
|
||||
### 14.1 日志和 Trace
|
||||
|
||||
认证后所有请求日志增加:
|
||||
|
||||
```text
|
||||
request_id, tenant_id, uid, sid, topic, action, result
|
||||
```
|
||||
|
||||
手机号、证件号、Token、企业码密钥和文件签名不写日志。TenantID 可写内部数值或不可逆短标识。
|
||||
|
||||
### 14.2 指标
|
||||
|
||||
租户 ID 直接作为 Prometheus Label 可能造成高基数。建议:
|
||||
|
||||
- 平台指标默认不带 Tenant Label;
|
||||
- 关键配额和 SLA 写入租户用量表或专用统计系统;
|
||||
- 仅对白名单大租户暴露租户级指标;
|
||||
- 错误日志和 Trace 保留 TenantID 供定位。
|
||||
|
||||
### 14.3 审计
|
||||
|
||||
以下操作必须记录 TenantID:
|
||||
|
||||
- 租户创建、停用、恢复和删除;
|
||||
- Tenant Admin 授权和代操作;
|
||||
- 用户停用、离职和数据导出;
|
||||
- 群主/管理员变更;
|
||||
- 实名审核、举报处置、匿名信箱读取;
|
||||
- 文件下载、批量导出和搜索敏感内容;
|
||||
- 平台管理员跨租户操作。
|
||||
|
||||
审计表只允许追加,普通租户管理员不能修改或删除。
|
||||
|
||||
## 15. 测试方案
|
||||
|
||||
### 15.1 单元和接口测试
|
||||
|
||||
- TenantContext 不可为空;
|
||||
- Session 只能绑定一次 Tenant;
|
||||
- Token Tenant 与 Session Tenant 不一致时登录失败;
|
||||
- Store/Adapter 不提供无 Tenant 的业务查询;
|
||||
- TopicKey、UserKey、CacheKey 和 Push Channel 正确命名空间化;
|
||||
- Plugin 不能替换 Tenant;
|
||||
- Platform Admin 与 Tenant Admin 权限边界正确。
|
||||
|
||||
### 15.2 MySQL 集成测试
|
||||
|
||||
创建 Tenant A、Tenant B,并使用相同用户名、手机号、群显示名和逻辑 `sys`。至少覆盖:
|
||||
|
||||
| 场景 | 预期 |
|
||||
| --- | --- |
|
||||
| 两租户注册相同用户名/手机号 | 均成功,各自登录到不同 UID |
|
||||
| A 使用 B 的账号密码登录 | 统一认证失败 |
|
||||
| A 已知 B 的 UID 执行 Get/Set/Delete | NotFound/Denied,不返回存在性 |
|
||||
| A 已知 B 的 Group Topic 订阅或发布 | 拒绝且不加载到 A 的 Hub Cache |
|
||||
| A 邀请 B 用户进入 A 群 | 拒绝,无 Subscription 写入 |
|
||||
| A 已知 B 的 Message Topic/SeqId | 不能读取、搜索、删除或收藏 |
|
||||
| A 已知 B 的 File ID/URL | 不能下载、关联或转发 |
|
||||
| A/B 同时使用 `sys` | 只收到本租户系统消息 |
|
||||
| A 停用用户 | 不影响相同设备上的 B 用户 |
|
||||
| A Tenant Admin `AsUser` B 用户 | 拒绝并审计 |
|
||||
| 同一设备在 A/B 注册 Push | 设备记录和 Push 不串租户 |
|
||||
| A 大量任务积压 | B 的通知和文件 GC 不被长期饿死 |
|
||||
|
||||
### 15.3 集群测试
|
||||
|
||||
- 相同 Topic 别名在不同 Tenant 生成不同 RoutingKey;
|
||||
- Proxy/Master 传输后 Tenant 不丢失;
|
||||
- 节点重启、重哈希和故障转移不串 Topic;
|
||||
- 混入旧节点时启动检查直接失败;
|
||||
- User Cache、未读数和 Push 在远程节点保持租户一致。
|
||||
|
||||
### 15.4 安全测试
|
||||
|
||||
- 篡改 `hi.tenant`、Token Tenant、HTTP Header 和 Body Tenant;
|
||||
- 枚举其他租户 UID、Topic、File ID、企业码;
|
||||
- SQL 查询遗漏 Tenant 的代码扫描与集成测试;
|
||||
- root/OBO 越权;
|
||||
- 搜索索引、缓存、导出、日志和审计的跨租户泄露;
|
||||
- 对象存储签名 URL 重放和跨租户替换;
|
||||
- Plugin/回调伪造 Tenant。
|
||||
|
||||
## 16. 代码修改清单
|
||||
|
||||
| 文件/目录 | 主要修改 |
|
||||
| --- | --- |
|
||||
| `server/store/types/tenant.go`、`types.go` | TenantID、TenantContext、TopicKey、UserKey;核心实体 TenantID |
|
||||
| `server/auth/auth.go` | AuthContext、`auth.Rec.TenantID`、认证接口 |
|
||||
| `server/auth/basic` | 租户内登录名查询和唯一性 |
|
||||
| `server/auth/token` | Token v2、TenantID 和租户撤销版本 |
|
||||
| `server/auth/rest`、`server/auth/code` | 传递并校验租户上下文 |
|
||||
| `server/datamodel.go` | `MsgClientHi.Tenant`;内部消息 TenantID |
|
||||
| `server/session.go` | Tenant 握手、固定 Session Tenant、登录/注册/发布断言 |
|
||||
| `server/user.go` | User Cache Key、租户级未读/踢出/状态更新 |
|
||||
| `server/sessionstore.go` | `EvictUser(tenant, uid)`、SID 认证租户断言 |
|
||||
| `server/hub.go` | TopicKey 缓存、租户 `sys`、租户路由 |
|
||||
| `server/init_topic.go` | Topic Tenant 初始化和同租户检查 |
|
||||
| `server/topic.go` | Topic/成员/消息 Tenant 不变量 |
|
||||
| `server/cluster.go`、`server/topic_proxy.go` | Cluster RPC Tenant、复合 RoutingKey、协议版本检查 |
|
||||
| `server/store/store.go` | `ForTenant` 作用域和所有 Mapper 租户化 |
|
||||
| `server/db/adapter.go` | Adapter 全接口 Tenant 参数 |
|
||||
| `server/db/mysql/adapter.go` | 所有 SQL Tenant 条件和复合约束 |
|
||||
| `server/db/mysql/schema.sql` | im_tenant、tenant_id、索引、外键和新 schema 版本 |
|
||||
| `server/hdl_files.go` | RequestPrincipal、FileCanAccess、Tenant 文件路径 |
|
||||
| `server/push.go`、`server/push` | Receipt/Channel/设备查询 Tenant 化 |
|
||||
| `server/plugins.go`、`pbx` | 插件协议 Tenant 上下文和不可变校验 |
|
||||
| `server/main.go` | 租户配置缓存和启动自检 |
|
||||
| `server/tinode.conf` | 租户服务和配额配置 |
|
||||
| `server/store/mock_store` | 重新生成所有租户化 Mock |
|
||||
| `server/*_test.go`、`server/db/mysql/tests` | 双租户正向、负向和隔离测试 |
|
||||
|
||||
## 17. 实施阶段与交付物
|
||||
|
||||
### 阶段 0:决策和基线
|
||||
|
||||
- 确认共享表模式;
|
||||
- 确认租户内独立 User;
|
||||
- 确认跨租户通信首期禁止;
|
||||
- 确认产品版数据库支持范围;
|
||||
- 冻结 TenantID、TopicKey 和认证协议设计。
|
||||
|
||||
交付物:架构决策记录、数据字典和接口清单。
|
||||
|
||||
### 阶段 1:Schema 与租户基础服务
|
||||
|
||||
- `im_tenant`、`im_tenant_config`;
|
||||
- 核心表 `tenant_id BIGINT NOT NULL`;
|
||||
- 默认租户;
|
||||
- `hi.tenant` 企业码解析和 Session 绑定;
|
||||
- 租户配置缓存。
|
||||
|
||||
交付物:全新数据库初始化 Schema、租户基础服务和 Schema 约束测试。
|
||||
|
||||
### 阶段 2:认证和 Store
|
||||
|
||||
- Session Tenant;
|
||||
- AuthContext 和 Token v2;
|
||||
- `store.ForTenant`;
|
||||
- MySQL 全查询租户化;
|
||||
- Mock 和单元测试更新。
|
||||
|
||||
交付物:租户内注册、登录和 Store 隔离能力。
|
||||
|
||||
### 阶段 3:实时路由与集群
|
||||
|
||||
- Hub TopicKey;
|
||||
- 租户 `sys/me/fnd/slf`;
|
||||
- Cluster RPC 和一致性哈希;
|
||||
- User Cache 和 Session Evict;
|
||||
- Plugin Tenant。
|
||||
|
||||
交付物:双租户集群隔离测试通过,不兼容节点无法加入集群。
|
||||
|
||||
### 阶段 4:文件、Push、搜索和后台任务
|
||||
|
||||
- 文件 Principal、ACL 和租户路径;
|
||||
- Push Receipt/设备/凭证租户化;
|
||||
- 搜索索引和二次 ACL;
|
||||
- Outbox、审计、GC、配额和限流租户化。
|
||||
|
||||
交付物:资源和异步链路跨租户负向测试通过。
|
||||
|
||||
### 阶段 5:上线验收
|
||||
|
||||
- 内部第二租户;
|
||||
- 安全测试、压测和故障演练;
|
||||
- 上线检查和真实第二企业启用。
|
||||
|
||||
交付物:测试报告、应急预案和多租户上线审批。
|
||||
|
||||
## 18. 主要风险
|
||||
|
||||
| 风险 | 影响 | 控制措施 |
|
||||
| --- | --- | --- |
|
||||
| 某个 SQL 漏 Tenant 条件 | 严重数据泄露 | 显式 Store 接口、复合约束、双租户负向测试和代码检查 |
|
||||
| 只改数据库,缓存/集群未改 | 实时消息串租户 | TopicKey/UserKey、Cluster Tenant 不变量和集群测试 |
|
||||
| 全局 `sys` 未拆分 | 系统通知泄露 | 租户逻辑 sys、Hub 复合 Key、通知生产者强制 Tenant |
|
||||
| 文件 URL 泄露 | 跨租户下载 | FileGet Tenant 条件、Topic ACL、短时签名和审计 |
|
||||
| root/OBO 权限过大 | 管理员跨租户操作 | Platform/Tenant Admin 分离、后台 API、二次验证和审计 |
|
||||
| 混跑旧集群节点 | Tenant 字段丢失 | 集群协议版本检查,不兼容节点拒绝加入 |
|
||||
| 指标 Tenant Label 高基数 | 监控系统压力 | 指标聚合,Tenant 主要进入日志/Trace/用量表 |
|
||||
|
||||
## 19. 上线前必须满足的条件
|
||||
|
||||
1. 业务代码不存在无 Tenant 的 User/Topic/Message/File 查询接口;
|
||||
2. 核心表和全部业务表 TenantID 为 NOT NULL;
|
||||
3. Auth、Credential、Device、Topic 和 Subscription 唯一约束已租户化;
|
||||
4. Token v2、Session Tenant 和 HTTP Principal 全链路一致;
|
||||
5. Hub、User Cache、Cluster、Push、Plugin 和 Persistent Cache 均携带 Tenant;
|
||||
6. `sys` 已按租户隔离;
|
||||
7. 文件下载和搜索完成租户过滤及 ACL 二次校验;
|
||||
8. 双租户自动化测试、集群测试和安全测试全部通过;
|
||||
9. 所有运行节点和后台任务均为租户感知版本;
|
||||
10. 内部第二租户稳定运行后,才允许创建真实第二企业。
|
||||
|
||||
## 20. 最终建议
|
||||
|
||||
多租户改造必须被视为服务端基础架构改造,而不是“给 users/topics/messages 加一列”。当前代码的认证、Store 接口、Hub Cache、Cluster、系统 Topic、文件和 Push 都使用全局 UID/Topic 语义,任何一处遗漏都可能造成跨企业数据泄露。
|
||||
|
||||
建议采用租户内独立 User、全局唯一 UID、共享表 `tenant_id` 强隔离的方案。全新数据库从创建时就使用最终多租户 Schema,并在 Store、认证、Token、路由、缓存、文件和 Push 全链路强制 TenantID;所有负向测试通过后,再开启真实租户。
|
||||
|
||||
工程上最重要的控制手段是:让 TenantID 进入类型和方法签名,使遗漏租户条件尽量在编译或测试阶段暴露;平台级全局查询使用隔离接口,普通业务代码永远只能获得 `store.ForTenant(...)`。这样才能让后续好友、群治理、企业工作台、搜索和安全业务从一开始就在正确的隔离边界内建设。
|
||||
@@ -0,0 +1,313 @@
|
||||
# IM 独立库多租户改造方案
|
||||
|
||||
> 编写日期:2026-08-13
|
||||
> 适用项目:`im_server`
|
||||
> 方案状态:替代原“共享库共享表 + tenant_id”方案,后续多租户开发以本文为准。
|
||||
|
||||
## 1. 方案结论
|
||||
|
||||
多租户调整为:
|
||||
|
||||
> 共享服务集群 + 控制库保存租户元数据 + 每个租户独立业务库
|
||||
|
||||
业务库中的 `users`、`auth`、`credentials`、`topics`、`subscriptions`、`messages`、`fileuploads` 以及后续产品业务表都不再增加 `tenant_id` 字段。租户隔离通过运行时选库实现:客户端先用企业码解析租户,服务端得到 `tenant_id` 后,根据控制库中的 `db_name` 或 `db_dsn` 选择对应租户业务库。
|
||||
|
||||
`TenantID` 仍然要保留,但它的职责从“业务表行级隔离字段”调整为“运行时租户上下文”:
|
||||
|
||||
1. 登录前企业码解析;
|
||||
2. Token 签名和校验;
|
||||
3. Session 当前租户固定;
|
||||
4. Hub、缓存、集群路由、Push、文件路径和插件上下文隔离;
|
||||
5. 选择当前请求使用哪个租户业务库;
|
||||
6. 后台平台侧管理租户、配置配额、停启租户。
|
||||
|
||||
## 2. 为什么独立库仍需要 TenantID
|
||||
|
||||
独立库解决的是数据库物理隔离,不代表服务端运行时没有租户。服务进程仍然同时服务多个企业,如果内存 Topic、用户缓存、系统 Topic、Push、文件下载授权、Token 校验不带租户上下文,仍可能出现:
|
||||
|
||||
1. 两个租户都使用逻辑 Topic `sys` 时共用同一个内存 Topic;
|
||||
2. HTTP 文件下载只拿到 `file_id`,无法判断该去哪个租户库查授权;
|
||||
3. Token 只包含 UID 时,无法知道该 UID 属于哪个租户库;
|
||||
4. 定时任务、插件调用、Push 回调无法定位租户库;
|
||||
5. 后台接口可能把平台租户 ID 和业务租户数据混在一起。
|
||||
|
||||
所以新方案的边界是:
|
||||
|
||||
```text
|
||||
TenantID 负责运行时路由和授权上下文
|
||||
tenant_id 字段不进入租户业务库表结构
|
||||
```
|
||||
|
||||
## 3. 数据库拆分
|
||||
|
||||
### 3.1 控制库
|
||||
|
||||
控制库可以使用现有 `hotgo`,只保存平台级租户元数据和配额配置。控制库表需要保留 `tenant_id`,因为这些表本身是在管理所有租户;这里的 `tenant_id` 不是业务库行级隔离字段。
|
||||
|
||||
```sql
|
||||
CREATE TABLE im_tenant (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
code VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
tenant_desc VARCHAR(256),
|
||||
db_name VARCHAR(128) NOT NULL DEFAULT '',
|
||||
db_dsn VARCHAR(1024) NOT NULL DEFAULT '',
|
||||
state SMALLINT NOT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_by BIGINT NOT NULL,
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
updated_by BIGINT NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_im_tenant_code (code),
|
||||
KEY idx_im_tenant_db_name (db_name)
|
||||
);
|
||||
|
||||
CREATE TABLE im_tenant_config (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
max_users BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
|
||||
max_groups BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
|
||||
max_group_members INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_by BIGINT NOT NULL,
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
updated_by BIGINT NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_im_tenant_config_tenant_id (tenant_id),
|
||||
CONSTRAINT fk_im_tenant_config_tenant
|
||||
FOREIGN KEY (tenant_id) REFERENCES im_tenant(id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
```
|
||||
|
||||
`db_name` 适合所有租户共用同一个 MySQL 实例时使用,例如 `im_tenant_1001`。`db_dsn` 适合某些租户需要独立 MySQL 实例时使用。优先级建议为:`db_dsn` 非空时使用完整 DSN,否则使用主 DSN + `db_name`。
|
||||
|
||||
如果要求 `db_name` 不能重复,建议在租户创建服务里校验“非空 db_name 唯一”。MySQL 普通唯一索引不能表达“只对非空字符串唯一”,直接给 `db_name` 建唯一索引会导致多个使用独立 `db_dsn` 的租户因为空 `db_name` 冲突。
|
||||
|
||||
### 3.2 租户业务库
|
||||
|
||||
每个租户一个业务库,例如:
|
||||
|
||||
```text
|
||||
hotgo
|
||||
├─ im_tenant
|
||||
└─ im_tenant_config
|
||||
|
||||
im_tenant_1001
|
||||
├─ users
|
||||
├─ auth
|
||||
├─ credentials
|
||||
├─ topics
|
||||
├─ subscriptions
|
||||
├─ messages
|
||||
├─ fileuploads
|
||||
└─ 后续产品业务表
|
||||
```
|
||||
|
||||
租户业务库表结构恢复为租户内唯一约束,不加 `tenant_id`。同一个手机号、用户名可以在两个租户库中各自存在,不会冲突。
|
||||
|
||||
## 4. 当前代码如何调整
|
||||
|
||||
当前代码已经有一部分独立库路由雏形,可以继续利用:
|
||||
|
||||
1. `server/store/store.go` 已经预留 `TenantRoutedAdapter` 分支;
|
||||
2. `server/db/adapter.go` 已定义 `TenantAdapterFor(tenantID)`;
|
||||
3. `server/db/mysql/adapter.go` 已支持 `tenant_registry_db`、`tenant_db_routing`、`db_name`、`db_dsn`;
|
||||
4. Token、Session、Hub TopicKey、UserKey、插件上下文已经带 `TenantID`。
|
||||
|
||||
需要调整的重点是:
|
||||
|
||||
| 模块 | 旧共享表方案 | 新独立库方案 |
|
||||
| --- | --- | --- |
|
||||
| 控制库 | 与业务表混在一个库 | 单独保存 `im_tenant`、`im_tenant_config` |
|
||||
| 业务库 schema | 所有表加 `tenant_id` | 所有业务表不加 `tenant_id` |
|
||||
| MySQL Adapter | SQL 使用 `WHERE tenant_id=?` | 先根据 TenantID 选库,再执行租户库内 SQL |
|
||||
| Store 接口 | TenantID 作为 SQL 条件 | TenantID 作为选库参数 |
|
||||
| 唯一索引 | `(tenant_id, business_key)` | 租户库内 `business_key` 唯一 |
|
||||
| 外键 | 复合外键带 `tenant_id` | 普通外键 |
|
||||
| 数据迁移 | 共享库内补列回填 | 新租户直接建独立库,无历史迁移 |
|
||||
|
||||
## 5. 代码落地步骤
|
||||
|
||||
### 5.1 拆分 schema
|
||||
|
||||
把 MySQL schema 拆成两个层次:
|
||||
|
||||
1. `registry_schema.sql`:只创建控制库表 `im_tenant`、`im_tenant_config`;
|
||||
2. `tenant_schema.sql`:创建 Tinode 核心表和产品业务表,不包含 `tenant_id`;
|
||||
3. `schema.sql` 可以作为文档索引,说明不能直接混用两套 schema。
|
||||
|
||||
`CreateDb(reset)` 也要拆分:
|
||||
|
||||
1. 初始化控制库;
|
||||
2. 初始化默认租户业务库;
|
||||
3. 根据 `im_tenant.db_name/db_dsn` 初始化指定租户库;
|
||||
4. 禁止在同一个事务中同时创建控制库和多个租户业务库,避免失败后状态难恢复。
|
||||
|
||||
### 5.2 改 MySQL Adapter 路由
|
||||
|
||||
保留 `TenantAdapterFor(tenantID)`,但租户 Adapter 内执行的 SQL 应该是无 `tenant_id` 版本。
|
||||
|
||||
推荐结构:
|
||||
|
||||
```text
|
||||
mysql.adapter
|
||||
├─ registryDB: 连接控制库 hotgo
|
||||
├─ defaultDB: 默认租户库
|
||||
├─ tenantAdapters: map[TenantID]*adapter
|
||||
└─ TenantAdapterFor(tenantID): 从 registryDB 读取 db_name/db_dsn 并返回租户库 adapter
|
||||
```
|
||||
|
||||
租户业务方法仍保留 `TenantUserGet(tenantID, uid)` 这类签名,但实现改为“先选库,后执行租户库内原 SQL”。实际写代码时要避免递归,可以把“无租户 SQL”的方法放在租户库 adapter 上,把“路由包装方法”放在根 adapter 上。
|
||||
|
||||
### 5.3 删除业务表 tenant_id 依赖
|
||||
|
||||
需要回退或重写这些位置:
|
||||
|
||||
1. `server/db/mysql/schema.sql` 中核心业务表的 `tenant_id` 字段;
|
||||
2. `server/db/mysql/tenant_adapter.go` 中所有 `WHERE tenant_id=?`、`INSERT tenant_id`、复合 JOIN;
|
||||
3. `server/db/mysql/tests/mysql_test.go` 中依赖共享表隔离的测试;
|
||||
4. `docs/sql/mysql-multitenant-tenant-id-review.sql` 这份共享表迁移脚本应标记为废弃;
|
||||
5. 后续产品业务表设计也不再带 `tenant_id`。
|
||||
|
||||
不建议删除 Store、Session、Token、Hub、Push 和插件里的 `TenantID`,这些属于运行时隔离边界。
|
||||
|
||||
### 5.4 租户创建流程
|
||||
|
||||
后台创建租户时建议按以下顺序:
|
||||
|
||||
1. 在控制库 `im_tenant` 创建记录,状态为 `provisioning`;
|
||||
2. 生成业务库名,例如 `im_tenant_{id}`;
|
||||
3. 创建业务库并执行 `tenant_schema.sql`;
|
||||
4. 写入默认系统数据,例如 `kvmeta.version`、系统账号、`sys` Topic;
|
||||
5. 回写 `im_tenant.db_name`,状态改为 `active`;
|
||||
6. 创建失败时租户保持不可登录状态,并记录错误供后台重试。
|
||||
|
||||
第一阶段可以不做自动建库接口,先由运维执行建库 SQL,再在控制库写入 `im_tenant`。
|
||||
|
||||
### 5.5 认证与登录
|
||||
|
||||
登录入口仍然是:
|
||||
|
||||
1. 客户端携带 `X-Tinode-APIKey` 请求准入;
|
||||
2. `hi` 或登录前 HTTP 接口提交企业码;
|
||||
3. 服务端在控制库查 `im_tenant.code`;
|
||||
4. 租户状态必须是 `active`;
|
||||
5. Session 固定 `TenantID`;
|
||||
6. 登录认证根据 `TenantID` 选择租户业务库,然后查该库内 `auth`、`credentials`、`users`;
|
||||
7. Token 签名内容继续包含 `TenantID`,HTTP 后续请求可从 Token 反推出该使用哪个租户库。
|
||||
|
||||
客户端不能通过 URL、Header、Body 直接指定业务库名或 `tenant_id`。
|
||||
|
||||
## 6. 产品业务表如何设计
|
||||
|
||||
后续根据《IM 产品原型需求文档》补充的业务表也按“租户库内表”设计,不加 `tenant_id`。所有新业务表仍统一要求:
|
||||
|
||||
```sql
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)
|
||||
```
|
||||
|
||||
例如好友申请:
|
||||
|
||||
```sql
|
||||
CREATE TABLE im_friend_requests (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
request_id CHAR(26) NOT NULL,
|
||||
applicant_id BIGINT NOT NULL,
|
||||
target_id BIGINT NOT NULL,
|
||||
message VARCHAR(256),
|
||||
status SMALLINT NOT NULL,
|
||||
handled_by BIGINT,
|
||||
handled_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_im_friend_requests_request_id (request_id),
|
||||
KEY idx_im_friend_requests_target_status (target_id, status, created_at),
|
||||
KEY idx_im_friend_requests_applicant_status (applicant_id, status, created_at)
|
||||
);
|
||||
```
|
||||
|
||||
## 7. 文件和对象存储
|
||||
|
||||
数据库独立库后,文件元数据在租户库内隔离,但对象存储路径仍建议带租户目录:
|
||||
|
||||
```text
|
||||
tenants/{tenant_id}/yyyy/mm/{file_id}
|
||||
```
|
||||
|
||||
对象存储通常仍是共享 Bucket。带租户目录便于权限策略、容量统计、清理和迁移。
|
||||
|
||||
文件下载流程:
|
||||
|
||||
1. HTTP Token 解析得到 `tenant_id` 和 `uid`;
|
||||
2. 根据 `tenant_id` 选择租户业务库;
|
||||
3. 在该库查 `fileuploads` 和 `filemsglinks`;
|
||||
4. 校验当前用户是否有 Topic 或消息访问权;
|
||||
5. 返回文件或预签名 URL。
|
||||
|
||||
## 8. 测试策略
|
||||
|
||||
需要把旧共享表测试改成独立库测试:
|
||||
|
||||
| 用例 | 准备 | 预期 |
|
||||
| --- | --- | --- |
|
||||
| 企业码解析 | 控制库有 A、B 两个租户 | A、B 返回不同 `tenant_id` 和不同 `db_name` |
|
||||
| 同手机号注册 | A、B 租户库分别注册同手机号 | 两边都成功 |
|
||||
| 登录选库 | A 用户用 B 企业码登录 | B 库查不到该用户,登录失败 |
|
||||
| Token 选库 | A Token 请求文件接口 | 只访问 A 租户库 |
|
||||
| 内存 Topic 隔离 | A、B 都使用 `sys` | Hub 中是两个 TopicKey |
|
||||
| 租户停用 | 控制库把 A 设为 suspended | 新登录失败 |
|
||||
| 租户库不可用 | A 的 DSN 错误或库不存在 | A 请求失败,B 不受影响 |
|
||||
| 连接池复用 | 多次访问 A | 只创建一个 A 租户 adapter,后续复用 |
|
||||
| 配额检查 | A 配置 max_users=1 | A 超额注册失败,B 不受影响 |
|
||||
|
||||
## 9. 对旧方案的处理
|
||||
|
||||
以下内容停止继续开发:
|
||||
|
||||
1. 给核心业务表增加 `tenant_id`;
|
||||
2. 把唯一索引改成 `(tenant_id, xxx)`;
|
||||
3. 所有业务 SQL 加 `WHERE tenant_id=?`;
|
||||
4. 共享库内的数据迁移、回填、复合外键改造;
|
||||
5. 产品业务表中的 `tenant_id` 字段设计。
|
||||
|
||||
以下内容继续保留:
|
||||
|
||||
1. 企业码识别;
|
||||
2. 控制库 `im_tenant`、`im_tenant_config`;
|
||||
3. `TenantID` 类型;
|
||||
4. Session TenantID;
|
||||
5. Token TenantID;
|
||||
6. Hub、缓存、集群、Push、插件上下文中的 TenantID;
|
||||
7. `TenantAdapterFor` 选库机制。
|
||||
|
||||
## 10. 推荐下一步
|
||||
|
||||
下一步代码改造建议从 MySQL 层开始:
|
||||
|
||||
1. 把 `server/db/mysql/schema.sql` 拆为控制库 schema 和租户业务库 schema;
|
||||
2. 把 `tenant_adapter.go` 改成路由包装,不再写 `tenant_id` SQL;
|
||||
3. 恢复或复用原 `adapter.go` 中无租户 SQL 作为租户库真实执行逻辑;
|
||||
4. 修改 MySQL 测试,创建两个独立业务库验证隔离;
|
||||
5. 再继续补产品业务表,所有业务表按租户库内表设计。
|
||||
|
||||
## 11. 当前代码落地状态
|
||||
|
||||
2026-08-13 已开始按独立库方案改造 MySQL 底座:
|
||||
|
||||
1. 默认业务库为 `tinode`;
|
||||
2. 控制库为 `hotgo`;
|
||||
3. `CreateDb` 会在 `hotgo` 创建 `im_tenant`、`im_tenant_config`,默认租户 `id=1/code=default/db_name=tinode`;
|
||||
4. `CreateDb` 会在 `tinode` 创建核心业务表,核心表不再包含 `tenant_id`;
|
||||
5. `TenantAdapterFor` 开启路由后默认租户也会从控制库解析,不再直接退回根连接;
|
||||
6. 租户库 adapter 禁用二次路由,避免递归;
|
||||
7. 用户、认证、凭证、Topic、Subscription、消息、设备、文件等核心 Tenant 方法已改为“先选库,再调用租户库内无 tenant_id 方法”;
|
||||
8. 旧共享表 `tenant_id` 迁移 SQL 已标记废弃。
|
||||
|
||||
剩余项:
|
||||
|
||||
1. 产品扩展表和 CRUD,例如好友、收藏、实名、青少年、系统通知,还需要继续从 `tenant_id` SQL 改成租户库内无 `tenant_id` SQL;
|
||||
2. `server/db/mysql/schema.sql` 仍是参考文件,已标注正在从旧共享表草稿拆分,后续应拆成 `registry_schema.sql` 和 `tenant_schema.sql`;
|
||||
3. 旧版本 `UpgradeDb` 迁移链仍包含共享表历史步骤。因为当前项目不考虑历史数据,优先使用新建库 `CreateDb`;如后续需要升级老库,应单独重写迁移链。
|
||||
@@ -0,0 +1,377 @@
|
||||
# IM 现有实现与新需求差异对照总结
|
||||
|
||||
> 对照日期:2026-08-10
|
||||
> 新需求来源:[IM 产品原型需求文档](IM-产品原型需求文档.md)
|
||||
> 详细改造分析:[IM 产品需求服务端改造分析报告](IM-产品需求服务端改造分析报告.md)
|
||||
> 对照对象:`chat-0.25.3` 当前服务端源码
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
本文从“现有服务端已经实现了什么”出发,对照新产品需求逐项给出处理结论:
|
||||
|
||||
- **保留**:现有实现已基本满足需求,只补配置或测试;
|
||||
- **需修改**:现有能力可复用,但协议、数据模型、权限或流程不完整;
|
||||
- **新增**:现有系统没有对应实现,需要新增模块;
|
||||
- **客户端为主**:主要是移动端交互,服务端只提供状态、配置或资源;
|
||||
- **废弃**:现有产品能力或接入方式在新需求中没有对应章节,不再作为新产品能力继续建设。
|
||||
|
||||
“新需求无直接章节”的数据库、对象存储、集群、监控等运行基础设施不按产品功能废弃,而标记为“保留基础设施”。真正需要废弃的能力单列在第 5 节。
|
||||
|
||||
## 2. 总体结论
|
||||
|
||||
当前系统可以继续作为新产品的即时通讯内核,不需要重写。现有 P2P Topic、群 Topic、Subscription ACL、消息序号、消息广播、回执、文件上传、Push 和 WebRTC 信令均可复用。
|
||||
|
||||
新需求与现状的主要关系如下:
|
||||
|
||||
| 新需求章节 | 功能域 | 当前覆盖程度 | 结论 |
|
||||
| --- | --- | --- | --- |
|
||||
| 第 1 章 | 账号、企业码、短信、实名、独立密码 | 部分覆盖 | 认证内核保留,产品流程需修改,企业码/实名需新增 |
|
||||
| 第 2 章 | 会话首页、网络状态、消息操作、系统消息 | 部分覆盖 | 会话与连接保留,产品状态和系统通知需修改 |
|
||||
| 第 3 章 | 单聊、好友状态、回执、媒体和名片 | 部分覆盖 | P2P/回执保留,好友约束和结构化消息需修改 |
|
||||
| 第 4 章 | 输入、@、语音、文件、收藏、转发 | 少量覆盖 | 文件和通用消息可复用,大部分产品语义需新增 |
|
||||
| 第 5 章 | 通讯录、好友申请、主页、黑名单 | 少量覆盖 | 用户搜索可改造,关系状态机需新增 |
|
||||
| 第 6 章 | 群创建、审批、公告、管理、禁言、群文件 | 部分覆盖 | 群和 ACL 保留,群治理业务需新增 |
|
||||
| 第 7 章 | 收藏、下载、聊天记录搜索 | 少量覆盖 | `slf` 和文件可复用,授权和索引必须修改 |
|
||||
| 第 8 章 | 语音/视频通话 | 部分覆盖 | P2P WebRTC 信令保留,通话业务状态需修改 |
|
||||
| 第 9 章 | 组织、会议、日报、月报、信箱 | 未覆盖 | 全部新增业务模块 |
|
||||
| 第 10 章 | 资料、设置、版本、隐私、青少年模式 | 少量覆盖 | 通用资料可改造,其余多数新增或客户端实现 |
|
||||
| 第 11 章 | 反诈、举报、反馈 | 未形成业务闭环 | 插件机制保留,业务模型和处置流程新增 |
|
||||
|
||||
必须优先处理三个基础问题:
|
||||
|
||||
1. 消息发布缺少客户端消息 ID 幂等校验,断线重试可能产生重复消息;
|
||||
2. 文件下载只校验登录,没有校验用户对文件关联 Topic/消息的读取权限;
|
||||
3. 当前核心表没有 `tenant_id`,共享数据库部署多个企业前必须完成全链路租户隔离。
|
||||
|
||||
## 3. 现有核心实现与新需求关系
|
||||
|
||||
| 现有实现 | 源码位置 | 对应新需求章节 | 结论 | 修改说明 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| basic 用户名/密码认证 | `server/auth/basic` | 1.2、1.3、1.4、1.6 | 需修改 | 保留认证器,增加协议确认、密码策略、错误次数、锁定、找回密码编排和旧 Token 撤销 |
|
||||
| HMAC Token 登录 | `server/auth/token` | 1.2、1.6;2.2 | 保留 | 继续作为登录态;补设备会话、单设备撤销和密码重置后的 Token 失效 |
|
||||
| 电话号码及短信验证器 | `server/validate/tel` | 1.4、1.5、1.6、1.7 | 需修改 | 保留验证码能力,增加用途隔离、发送频控、`retry_after`、一次性验证票据和审计 |
|
||||
| REST 外部认证器 | `server/auth/rest` | 1.1、1.2 | 需修改 | 可连接企业账号中心;企业码解析仍需新增独立入口,认证后绑定企业上下文 |
|
||||
| Session/WebSocket 协议分发 | `server/session.go` | 2.2;第 3、4、8 章 | 保留 | 作为 App 的主实时入口;补幂等、产品策略和结构化错误 |
|
||||
| Topic 消息顺序、保存和广播 | `server/topic.go` | 第 2、3、4、6、7 章 | 需修改 | 保留核心循环;持久化前增加好友、黑名单、禁言、青少年和内容风险校验 |
|
||||
| P2P Topic | `server/topic.go` | 3.1-3.12;5.2-5.14 | 需修改 | 保留单聊容器;是否允许创建、发送和呼叫改由好友/黑名单/隐私策略控制 |
|
||||
| Group Topic | `server/topic.go` | 6.1-6.28;4.2-4.4 | 需修改 | 保留群消息和成员基础;增加审批、公告、二维码、禁言和群文件业务层 |
|
||||
| Subscription ACL | `server/store/types/types.go`、`server/topic.go` | 3.1-3.8;6.4-6.26 | 需修改 | 保留 Join/Read/Write/Admin/Owner;不能用 ACL 代替好友申请、待审批和禁言记录 |
|
||||
| Presence、输入状态 | `server/topic.go`、`server/datamodel.go` | 2.1;3.1-3.4;4.9 | 需修改 | 在线/输入状态可复用;增加隐私控制、节流和过期清理 |
|
||||
| `recv`、`read` 回执 | `server/topic.go` | 3.4、3.7 | 保留 | 定义客户端状态映射;群聊是否显示具体已读人由产品规则控制 |
|
||||
| 通用 JSON `head/content` 与 Drafty | `server/datamodel.go`、`server/drafty` | 3.6、3.10-3.12;4.1-4.20 | 需修改 | 增加消息类型、schema 版本、字段白名单、引用、@、转发和审核状态,不再允许各端自定义语义 |
|
||||
| 消息删除 | `server/session.go`、`server/topic.go` | 2.4;3.8;4.15 | 需修改 | 明确仅自己删除、全员撤回、撤回期限、管理员权限和审计,不直接复用模糊的硬删除语义 |
|
||||
| `slf` 自收藏 Topic | `server/store/types/types.go`、`server/topic.go` | 4.16;7.1-7.8 | 需修改 | 继续保存收藏消息,同时增加收藏索引、来源、去重、类型筛选和源消息删除策略 |
|
||||
| 文件上传与消息附件关联 | `server/hdl_files.go`、`server/store/store.go` | 3.6、3.10-3.12;4.10-4.20;6.27-6.28;7.3、7.7-7.11 | 需修改 | 保留上传;增加附件所有权、病毒扫描、群文件索引、短时下载 URL 和资源级授权 |
|
||||
| Push Handler | `server/push.go`、`server/push` | 2.1、2.5;4.4;8.1-8.11;9.3-9.4;10.18 | 需修改 | 保留 Push;扩展 @、审批、会议、通话和安全通知,增加锁屏内容隐私设置 |
|
||||
| P2P WebRTC 信令 | `server/calls.go` | 8.1-8.11 | 需修改 | 保留 Invite/Offer/Answer/ICE/Hang-up;新增 `call_id`、设备抢答、结束原因、详单和 TURN 指标 |
|
||||
| Plugin Firehose | `server/plugins.go` | 11.1-11.4;2.5 | 需修改 | 用于外部内容审核和反诈;增加超时、熔断、失败策略和审核结果落库 |
|
||||
| User Public/Trusted/Tags | `server/store/types/types.go` | 5.1、5.5-5.8;10.1-10.6 | 需修改 | 可保存基础展示资料;生日、可见性、实名和组织关系应使用明确字段/业务表 |
|
||||
| Topic Public/Trusted/Aux/Tags | `server/store/types/types.go` | 6.10-6.16 | 需修改 | 可保存群基础展示资料;公告、审批、二维码和禁言不能只存任意 JSON |
|
||||
| `fnd` 用户/Topic 查找 | `server/topic.go` | 5.1、5.3-5.5 | 需修改 | 改成租户内精准 ID/受控搜索;执行隐私策略并防止手机号、用户 ID 枚举 |
|
||||
| `sys` 系统 Topic | `server/store/types/types.go`、`server/topic.go` | 2.5;第 9、11 章通知 | 需修改 | 作为可信系统消息通道;定义消息类型、模板版本、跳转参数和去重键 |
|
||||
| MySQL 核心表和 Adapter | `server/db/mysql` | 所有服务端需求的存储基础 | 需修改 | schema 118 已建立 `im_tenant`;仍需为核心表增加 tenant_id,并新增产品表、幂等、搜索索引和文件授权查询 |
|
||||
| 文件系统/S3 媒体后端 | `server/media/fs`、`server/media/s3` | 第 3、4、6、7、11 章附件 | 保留基础设施 | 生产使用 S3/兼容对象存储,本地文件系统仅开发;补扫描和私有签名地址 |
|
||||
|
||||
## 4. 按新需求章节逐项对照
|
||||
|
||||
### 4.1 第 1 章:账号注册、登录与身份安全
|
||||
|
||||
| 新需求章节 | 新功能 | 当前实现 | 是否改 | 如何修改 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1.1 | 企业码登录 | 无企业/租户模型 | 新增 | 增加企业码解析 API、企业状态、服务地址和认证策略;共享库模式需先加 `tenant_id` |
|
||||
| 1.2-1.3 | 账号密码登录及错误提示 | basic + token 已有 | 需修改 | 增加协议确认、统一错误码、失败计数、账号锁定、设备登录记录和安全通知 |
|
||||
| 1.4-1.5 | 短信注册和倒计时 | 电话验证器已有 | 需修改 | 新增注册流程 API;验证码按用途隔离,返回有效期和重发时间,服务端强制限频 |
|
||||
| 1.6 | 忘记密码 | 验证器与密码更新能力分散 | 需修改 | 建立“发码 → 临时票据 → 重置”状态机;成功后撤销旧 Token |
|
||||
| 1.7 | 敏感操作二次验证 | 无统一业务流程 | 新增 | 增加验证挑战、一次性票据、有效期、错误次数和操作绑定 |
|
||||
| 1.8-1.10 | 实名认证及状态提醒 | 无 | 新增 | 独立实名表、字段加密、审核状态、脱敏查询、审核回调和提醒消息 |
|
||||
| 1.11 | 四位独立密码 | 无 | 新增 | 保存强哈希、失败锁定和找回流程;禁止写入普通设置 JSON 或明文日志 |
|
||||
|
||||
### 4.2 第 2 章:消息首页、全局导航与系统通知
|
||||
|
||||
| 新需求章节 | 新功能 | 当前实现 | 是否改 | 如何修改 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 2.1 | 会话列表、摘要、未读数 | Subscription/Topic 已有基础 | 需修改 | 增加置顶、免打扰、隐藏、草稿和结构化消息摘要;文件助手使用固定系统 Topic |
|
||||
| 2.2 | 断网、重连、增量同步 | Session 与序号拉取已有 | 需修改 | 明确同步游标、重连退避、重复消息幂等和连接状态指标 |
|
||||
| 2.3 | 快捷菜单 | 无服务端页面逻辑 | 客户端为主 | 服务端只返回能力/权限开关,菜单和关闭交互由客户端实现 |
|
||||
| 2.4 | 引用、复制、收藏、编辑、转发、撤回、删除、多选、群回执 | 仅删除、通用消息、回执基础 | 需修改 | 增加引用/编辑/转发协议,撤回规则和批量操作校验;复制/多选 UI 在客户端 |
|
||||
| 2.5 | 新设备登录等系统消息 | `sys` Topic 可用,无完整事件生产 | 需修改 | 建立可信系统账号、设备/IP 审计事件、模板、跳转参数和防伪造规则 |
|
||||
|
||||
### 4.3 第 3 章:单聊会话、会话建立与消息状态
|
||||
|
||||
| 新需求章节 | 新功能 | 当前实现 | 是否改 | 如何修改 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 3.1-3.5 | 好友申请前后单聊状态 | P2P Topic 有,无好友状态机 | 新增/修改 | 新增好友申请和关系表;申请通过后再开放 Topic Write 与媒体能力 |
|
||||
| 3.4、3.7 | 发送中、送达、已读、失败 | 消息序号和 read/recv 已有 | 需修改 | 增加客户端消息 ID、稳定状态映射和业务失败原因;拉黑消息不得持久化给对方 |
|
||||
| 3.6 | 图片/语音/视频进度 | 文件上传与消息可用 | 需修改 | 统一 `file_id`、消息类型和上传完成确认;进度主要由客户端维护 |
|
||||
| 3.7-3.8 | 拉黑、删除好友后的发送限制 | 无关系策略 | 新增 | 在建会话、发布、通话、名片和文件入口统一执行关系策略 |
|
||||
| 3.9 | 键盘收起布局 | 无服务端职责 | 客户端为主 | 服务端无需修改 |
|
||||
| 3.10-3.11 | 多类型文件/混合消息 | 通用 JSON + 文件已有 | 需修改 | 定义 text/image/voice/video/file 类型和字段校验,禁止任意 URL |
|
||||
| 3.12 | 名片消息 | 无标准类型 | 新增 | 增加 `contact_card`,只传用户 ID 与安全快照,打开时重新检查资料权限 |
|
||||
|
||||
### 4.4 第 4 章:消息输入、媒体内容与消息操作
|
||||
|
||||
| 新需求章节 | 新功能 | 当前实现 | 是否改 | 如何修改 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 4.1 | 键盘、草稿 | 无专用服务端草稿 | 客户端为主 | 草稿默认本地;如需多端同步,增加每用户每 Topic 的草稿设置 |
|
||||
| 4.2-4.4 | @成员、@所有人 | 无结构化 @语义 | 新增 | 消息保存稳定用户 ID;校验群成员和 @所有人权限,生成定向未读与 Push |
|
||||
| 4.5-4.6 | 表情面板和表情消息 | Drafty 可保存文本 | 保留/客户端为主 | Unicode 表情按文本发送;独立表情包需要资源 ID 和版本,不保存本地路径 |
|
||||
| 4.7-4.12 | 录音、图片/语音/视频上传与展示 | 文件和通用消息基础已有 | 需修改 | 增加媒体类型、时长/尺寸/缩略图、扫描状态;录音交互和播放进度由客户端实现 |
|
||||
| 4.13-4.14 | 文件上传和文件卡片 | 已有上传与附件关联 | 需修改 | 补格式/大小策略、所有权、病毒扫描、可预览信息和安全下载 |
|
||||
| 4.15 | 消息多选批量操作 | 无服务端批量语义 | 需修改 | 客户端选择;服务端逐条鉴权,限制批量数量并返回逐项结果 |
|
||||
| 4.16 | 批量收藏 | `slf` 可承载 | 需修改 | 增加收藏唯一约束、来源索引、类型和批量幂等 |
|
||||
| 4.17-4.19 | 单选/多目标逐条转发 | 无标准转发模型 | 新增 | 校验每个目标发送权和附件权;限制目标数;每条新消息使用独立幂等 ID |
|
||||
| 4.20 | 合并转发 | 无 | 新增 | 创建不可变 `merged_forward` 快照,限制子项和总大小,清理会话 ID 与敏感字段 |
|
||||
|
||||
### 4.5 第 5 章:通讯录、好友关系与黑名单
|
||||
|
||||
| 新需求章节 | 新功能 | 当前实现 | 是否改 | 如何修改 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 5.1 | 好友通讯录和企业成员 | `fnd`/Tags 仅提供基础查找 | 需修改 | 新增好友关系和组织成员查询;备注优先;支持字母/拼音索引 |
|
||||
| 5.2 | 新朋友列表与审批 | 无 | 新增 | 新增好友申请状态机、幂等接受/拒绝、过期和系统通知 |
|
||||
| 5.3-5.5 | ID、扫码添加好友 | 基础查找可部分复用 | 需修改 | 限定租户内精准搜索;二维码使用签名短 Token;执行隐私和反枚举策略 |
|
||||
| 5.6 | 好友验证留言 | 无明确模型 | 新增 | 好友申请表保存验证信息、来源、过期时间和风控结果 |
|
||||
| 5.7-5.8 | 好友/非好友个人主页 | Public/Trusted 可显示基础资料 | 需修改 | 根据关系、企业和隐私设置返回不同字段,不让客户端自行裁剪敏感资料 |
|
||||
| 5.9-5.10 | 拉黑开关 | 无完整黑名单模型 | 新增 | 关系表记录单方 block;立即影响消息、搜索、呼叫和资料访问 |
|
||||
| 5.11 | 删除好友 | 无好友关系真源 | 新增 | 事务更新双方关系;默认保留历史但禁止继续发送,规则需产品确认 |
|
||||
| 5.12-5.14 | 黑名单列表及提示 | 无 | 新增 | 提供黑名单查询、解除接口和统一业务错误,不向被拉黑人泄露过多信息 |
|
||||
|
||||
### 4.6 第 6 章:群聊创建、资料维护与成员治理
|
||||
|
||||
| 新需求章节 | 新功能 | 当前实现 | 是否改 | 如何修改 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 6.1-6.2 | 群列表和空状态 | Group Topic 已有 | 需修改 | 复用订阅列表;增加群业务状态、群头像摘要和已解散处理 |
|
||||
| 6.3-6.6 | 加群、邀请与审批 | ACL 可邀请成员,但无申请状态机 | 新增/修改 | 新增入群申请/邀请表;审批通过后才更新 Subscription,写 Outbox 通知 |
|
||||
| 6.7-6.9 | 群公告和已读 | 无公告模型 | 新增 | 公告主表、版本、发布权限、置顶和成员已读表;变更通过群系统消息通知 |
|
||||
| 6.10-6.16 | 群设置、资料、头像、昵称、简介、标识、二维码 | Public/Aux/Private 可部分承载 | 需修改 | 定义可索引字段和修改权限;二维码新增签名 Token、有效期、使用次数和撤销 |
|
||||
| 6.17-6.22 | 管理员、群主转让 | Admin/Owner ACL 已有 | 需修改 | 复用权限位;补管理员上限、二次确认、操作审计和可靠通知 |
|
||||
| 6.23-6.24 | 成员禁言 | 当前通知静音不等于发言禁用 | 新增 | 新增全员/成员禁言规则,在消息持久化前强制执行,定义管理员豁免 |
|
||||
| 6.25-6.26 | 选择新群主及确认 | 已有 Owner 转移逻辑 | 需修改 | 增加风险校验、幂等、候选人有效性、二次确认和审计 |
|
||||
| 6.27-6.28 | 群文件及上传 | 文件消息可用,无群文件目录 | 新增/修改 | 建群文件索引,支持类型/时间/上传者查询和管理员删除,复用安全上传链路 |
|
||||
|
||||
### 4.7 第 7 章:文件下载、收藏管理与聊天记录检索
|
||||
|
||||
| 新需求章节 | 新功能 | 当前实现 | 是否改 | 如何修改 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 7.1-7.8 | 收藏分类、查看和分享 | `slf` Topic 只有基础容器 | 需修改 | 增加收藏索引、类型、去重和源消息引用;分享时重新执行附件和目标会话权限 |
|
||||
| 7.9-7.11 | 文件下载状态 | 已有 HTTP 下载 | 必须修改 | 下载前检查上传者或关联 Topic Read ACL;使用短时签名 URL;进度/完成状态在客户端 |
|
||||
| 7.12 | 关键词搜索并定位上下文 | 现有查询只按序号范围 | 新增 | 建消息搜索索引,返回 Topic/SeqId 后重新检查 ACL,并支持上下文拉取 |
|
||||
| 7.13-7.16 | 文件、链接、图片、视频分类检索 | 无类型索引 | 新增 | 建 `message_type`、`mime_group`、file/link 字段和游标分页,支持历史回填 |
|
||||
|
||||
### 4.8 第 8 章:实时语音与视频通话
|
||||
|
||||
| 新需求章节 | 新功能 | 当前实现 | 是否改 | 如何修改 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 8.1-8.4 | 邀请、等待、接听、语音通话 | P2P WebRTC 信令已有 | 需修改 | 增加独立 `call_id`、呼叫类型、响铃/接听时间、忙线/拒接/超时原因和通话记录 |
|
||||
| 8.5-8.6 | 麦克风与扬声器状态 | 信令可传临时事件 | 客户端为主 | 本地媒体控制由客户端负责,仅在对端需要展示时同步状态 |
|
||||
| 8.7-8.10 | 视频等待、摄像头关闭、视频中 | WebRTC Offer/Answer/ICE 已有 | 需修改 | 增加视频类型和摄像头状态事件;配置 TURN,监控 ICE 成功率 |
|
||||
| 8.11 | 通话悬浮状态 | 无服务端职责 | 客户端为主 | 服务端保持呼叫会话和重连状态,悬浮窗由客户端实现 |
|
||||
|
||||
当前实现仅适合一对一通话。新需求没有群音视频章节,因此首期不新增 SFU 或群会议通话。
|
||||
|
||||
### 4.9 第 9 章:企业工作台与办公协同
|
||||
|
||||
| 新需求章节 | 新功能 | 当前实现 | 是否改 | 如何修改 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 9.1 | 工作台入口 | 无业务模块 | 新增 | 新增工作台应用配置和用户可见权限;页面布局由客户端实现 |
|
||||
| 9.2 | 部门组织架构 | 无 | 新增 | 新建组织单元和成员表,支持树、搜索、主部门、职位、在职状态和数据范围 |
|
||||
| 9.3-9.4 | 会议通知 | 仅能发送普通消息 | 新增 | 新建会议和参与人模型;创建/变更/取消通过 Outbox 发送系统消息和定时提醒 |
|
||||
| 9.5-9.8 | 日报及普通/管理员视图 | 无 | 新增 | 建报告表和提交状态机;管理员查询按组织数据权限过滤 |
|
||||
| 9.9-9.11 | 月报及详情 | 无 | 新增 | 复用报告模型,以类型/周期区分;增加汇总和附件能力 |
|
||||
| 9.12-9.13 | 总经理信箱 | 无 | 新增 | 建信箱工单、匿名展示、附件、回复和审计;匿名对业务角色隐藏但保留受控追溯 |
|
||||
|
||||
工作台 CRUD 建议走 `/v1/im/*` HTTP API,状态提醒走系统 Topic,不把报告和组织树直接塞进聊天消息协议。
|
||||
|
||||
### 4.10 第 10 章:个人资料、通用设置、隐私与青少年模式
|
||||
|
||||
| 新需求章节 | 新功能 | 当前实现 | 是否改 | 如何修改 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 10.1-10.5 | 我的、个人信息、昵称、生日、签名 | Public/Trusted 有通用字段 | 需修改 | 定义资料字段、格式、可见性、敏感词、变更版本和审计;实名信息单独存储 |
|
||||
| 10.6 | 个人二维码 | 无标准 Token | 新增 | 生成短时签名 Token,扫码后仍执行租户、隐私和账号状态校验 |
|
||||
| 10.7-10.8 | iOS/Android 设置页 | 无服务端页面逻辑 | 客户端为主 | 服务端提供用户设置和能力开关 |
|
||||
| 10.9 | 清空缓存 | 不属于服务端数据删除 | 客户端为主 | 只清理设备缓存,不删除云端消息;无需服务端接口 |
|
||||
| 10.10-10.11 | 版本更新 | 无版本配置服务 | 新增 | 提供平台/渠道最低版本、推荐版本、强更开关、下载地址和文案 API |
|
||||
| 10.12-10.13 | 隐私设置及添加方式 | 无统一隐私模型 | 新增 | 建隐私设置表,并在搜索、加好友、资料和在线状态接口中强制执行 |
|
||||
| 10.14-10.17 | 青少年模式 | 无 | 新增 | 服务端保存启用状态、PIN 强哈希、失败锁定和策略版本;所有关键入口强制检查 |
|
||||
| 10.18 | 推送权限 | Push Token 能力已有 | 需修改/客户端为主 | 系统授权由客户端申请;服务端保存 Token、开关、免打扰和预览策略 |
|
||||
| 10.19-10.21 | 提醒、相机、麦克风说明 | 无服务端职责 | 客户端为主 | 权限说明和系统授权由客户端实现,可由配置服务下发版本化文案 |
|
||||
|
||||
### 4.11 第 11 章:反诈安全、举报与意见反馈
|
||||
|
||||
| 新需求章节 | 新功能 | 当前实现 | 是否改 | 如何修改 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 11.1-11.2 | 反诈一次/二次提示 | Plugin 可拦截消息,无产品策略 | 新增/修改 | 建风险事件和策略版本;同步硬规则本地执行,外部审核设置超时/熔断;客户端按等级弹窗 |
|
||||
| 11.3-11.4 | 举报提交和分类 | 无举报工单 | 新增 | 建举报、证据、处理状态和审计;服务端在有权限时固化消息快照与哈希 |
|
||||
| 11.5 | 意见反馈 | 无 | 新增 | 建反馈工单、分类、附件、联系方式和处理状态;附件复用私有文件授权 |
|
||||
|
||||
## 5. 新需求未覆盖的现有能力:废弃清单
|
||||
|
||||
以下能力在新需求文档中没有对应章节。按新产品范围标注为“废弃”,但应采用“停止新接入 → 观察存量 → 迁移 → 删除”的方式处理,不能在未确认线上使用情况时直接删除源码或数据。
|
||||
|
||||
| 现有能力 | 源码/配置 | 新需求章节 | 处理结论 | 废弃方式 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 匿名登录 | `server/auth/anon` | 无 | **废弃** | 新环境不配置、不向客户端公开;确认无匿名会话后移除注册和文档 |
|
||||
| 邮箱注册/邮箱验证码/邮箱找回 | `server/validate/email` | 无,新需求只定义手机号短信 | **废弃** | 产品 API 不再暴露 email credential;存量账号先绑定手机号再停用 |
|
||||
| Channel 只读广播 Topic | `TopicCatGrp` 的 channel 模式 | 无 | **废弃** | 禁止新建 Channel;如有存量,迁移为普通群或公告业务后再移除入口 |
|
||||
| Long Poll 客户端接入 | `/v0/channels/lp` | 无,新移动端按 WebSocket 设计 | **废弃** | 先记录调用量并公告;无流量后关闭路由,WebSocket 保留 |
|
||||
| 客户端 gRPC 接入 | `grpc_listen`、客户端 gRPC 服务 | 无 | **废弃** | App 不再接入;注意保留插件所需的 gRPC 能力,两者不能一起误删 |
|
||||
| 示例 Python Chat Bot | `tinode.conf` 中 `python_chat_bot` | 无 | **废弃** | 从生产配置删除;插件框架因第 11 章风控继续保留 |
|
||||
| 静态网页托管 | `main.go` 静态文件路由/配置 | 无 Web 产品章节 | **废弃** | 新产品服务端不托管 Web UI;确认运维页面用途后关闭 |
|
||||
| stdout Push Handler | `server/push/stdout` | 无生产需求 | **废弃(生产)** | 仅允许本地测试使用,生产配置删除;FCM/TNPG 等真实 Push 保留 |
|
||||
| PostgreSQL Adapter | `server/db/postgres` | 无数据库兼容要求 | **废弃(产品版候选)** | 若确认生产统一 MySQL,则停止产品功能适配和验收;删除前核查部署 |
|
||||
| MongoDB Adapter | `server/db/mongodb` | 无数据库兼容要求 | **废弃(产品版候选)** | 同上,不继续为新增 `im_*` 业务表实现双写逻辑 |
|
||||
| RethinkDB Adapter | `server/db/rethinkdb` | 无数据库兼容要求 | **废弃(产品版候选)** | 同上;保留上游代码还是物理删除由仓库维护策略决定 |
|
||||
|
||||
以下能力虽然没有独立的新需求章节,但属于满足新需求所需的技术底座,不废弃:
|
||||
|
||||
| 基础能力 | 保留原因 |
|
||||
| --- | --- |
|
||||
| `me` Topic 和设备同步 | 会话列表、未读数、多端状态需要 |
|
||||
| `fnd` 查找 Topic | 第 5 章精准用户搜索可在其基础上收紧 |
|
||||
| `sys` Topic | 第 2、9、11 章的可信系统通知需要 |
|
||||
| `slf` Topic | 第 4、7 章收藏同步可复用 |
|
||||
| gRPC Plugin/Firehose | 第 11 章内容审核和反诈外部服务需要 |
|
||||
| FCM/TNPG Push | 第 2、4、8、9、10 章离线提醒需要,生产只选择实际使用的 Handler |
|
||||
| 文件系统/S3 Media Adapter | 第 3、4、6、7、11 章附件需要,按环境选择 |
|
||||
| 集群、缓存、监控、TLS | 属于可用性和安全基础,不是产品页面功能 |
|
||||
| root/后台管理能力 | 实名审核、举报处置、组织管理和审计需要,但必须收紧权限 |
|
||||
|
||||
## 6. 需要新增的服务端模块
|
||||
|
||||
| 新模块 | 对应新需求章节 | 主要职责 |
|
||||
| --- | --- | --- |
|
||||
| `imapi` | 第 1、5、6、7、9、10、11 章 | HTTP API、认证中间件、输入校验、统一错误和游标分页 |
|
||||
| `impolicy` | 3.7-3.8;5.9-5.14;6.23-6.24;10.12-10.17 | 好友、黑名单、禁言、隐私和青少年同步授权 |
|
||||
| `immessage` | 2.4;3.6、3.10-3.12;第 4 章 | 消息类型、schema 校验、@、引用、编辑、转发和摘要 |
|
||||
| `imbiz/relationship` | 第 5 章 | 好友申请、联系人、备注、拉黑和删除状态机 |
|
||||
| `imbiz/group` | 6.3-6.28 | 群审批、公告、二维码、角色、禁言和群文件 |
|
||||
| `imbiz/identity` | 1.7-1.10 | 二次验证、实名认证、审核和脱敏 |
|
||||
| `imbiz/workbench` | 第 9 章 | 组织、会议、日报、月报和信箱 |
|
||||
| `imbiz/safety` | 第 11 章 | 风险事件、举报证据、处置、反馈和审计 |
|
||||
| `imsearch` | 7.12-7.16 | 消息索引、历史回填、检索和 ACL 二次过滤 |
|
||||
| `imnotify` | 2.5;第 5、6、9、11 章 | Outbox 消费、系统消息和 Push 通知 |
|
||||
| `imjobs` | 1.5;5.2;6.3-6.6;9.3-9.4 | 申请过期、会议提醒、重试、清理和索引回填 |
|
||||
|
||||
## 7. 主要数据修改
|
||||
|
||||
### 7.1 核心表必须修改
|
||||
|
||||
| 对象 | 对应新需求章节 | 修改内容 |
|
||||
| --- | --- | --- |
|
||||
| Message | 2.2、2.4;第 3、4、7 章 | 增加或关联客户端幂等 ID、类型、审核状态、搜索索引;重复发送返回同一 SeqId |
|
||||
| FileDef/File links | 第 3、4、6、7、11 章 | 增加扫描状态、业务用途和访问查询;下载按消息/Topic ACL 授权 |
|
||||
| User | 1.1-1.10;5.1;9.2;第 10 章 | 共享 SaaS 时加 tenant;基础资料字段规范化,敏感身份数据独立存储 |
|
||||
| Topic/Subscription | 第 2、3、5、6 章 | 共享 SaaS 时加 tenant;会话个性设置、群策略和关系状态使用独立业务表 |
|
||||
|
||||
### 7.2 新增业务表
|
||||
|
||||
| 数据域 | 建议表 | 对应新需求章节 |
|
||||
| --- | --- | --- |
|
||||
| 好友与会话 | `im_user_relations`、`im_friend_requests`、`im_conversation_settings`、`im_privacy_settings` | 第 2、3、5 章;10.12-10.13 |
|
||||
| 群治理 | `im_group_settings`、`im_group_join_requests`、`im_group_mutes`、`im_group_announcements`、`im_group_announcement_reads`、`im_group_invite_tokens` | 第 6 章 |
|
||||
| 消息与收藏 | `im_message_idempotency`、`im_message_index`、`im_favorites` | 2.2、2.4;第 4、7 章 |
|
||||
| 身份与设置 | `im_identity_verifications`、`im_user_settings`、`im_teen_mode` | 第 1、10 章 |
|
||||
| 企业办公 | `im_org_units`、`im_org_members`、`im_meetings`、`im_meeting_participants`、`im_work_reports`、`im_mailbox_messages` | 第 9 章 |
|
||||
| 安全治理 | `im_reports`、`im_report_evidence`、`im_feedback`、`im_risk_events`、`im_audit_logs` | 第 11 章 |
|
||||
| 可靠事件 | `im_outbox`、`im_idempotency_keys` | 第 5、6、9、11 章的审批和通知 |
|
||||
|
||||
共享数据库的多企业模式下,核心表和全部 `im_*` 表都必须包含 `tenant_id`,并把租户放入唯一索引、查询条件、缓存键和文件路径。若采用一企一实例/一企一库,则企业码负责服务发现,物理隔离优先。
|
||||
|
||||
## 8. 主要代码修改位置
|
||||
|
||||
| 文件/目录 | 对应新需求章节 | 修改内容 |
|
||||
| --- | --- | --- |
|
||||
| `server/datamodel.go` | 第 2、3、4、7、8 章 | 定义 `x-im-schema`、消息类型、幂等 ID、引用、转发、搜索和通话扩展 |
|
||||
| `server/session.go` | 第 2、3、4、5、8、10、11 章 | 消息幂等、策略调用、租户上下文和统一业务错误 |
|
||||
| `server/topic.go` | 第 2、3、4、6、7 章 | 保存前执行好友/黑名单/禁言/青少年/风险规则,发送结构化系统消息 |
|
||||
| `server/calls.go` | 第 8 章 | call_id、设备抢答、标准结束原因、通话记录和指标 |
|
||||
| `server/hdl_files.go` | 第 3、4、6、7、11 章 | 抽取 HTTP 认证、资源 ACL、签名下载 URL 和扫描状态检查 |
|
||||
| `server/plugins.go` | 第 11 章 | 审核超时、熔断、失败开放/关闭策略和可观测性 |
|
||||
| `server/push.go` | 2.5;4.4;第 8、9、10、11 章 | 产品消息类型、@、会议、审批、通话和隐私 Push |
|
||||
| `server/main.go` | 第 1、5、6、7、9、10、11 章 | 注册 `/v1/im/*` API、业务服务和后台任务;逐步下线废弃路由 |
|
||||
| `server/store` | 所有服务端章节 | 新增核心事务、幂等、文件授权、搜索接口;明确产品版支持的数据库 |
|
||||
| `server/db/mysql` | 所有服务端章节 | 从当前 schema 118 继续迁移核心 tenant_id、业务表、索引和 MySQL 实现 |
|
||||
| `server/tinode.conf` | 所有章节 | 企业、产品模块、搜索、审核、TURN、文件扫描、限流和废弃能力开关 |
|
||||
|
||||
## 9. 修改优先级
|
||||
|
||||
### P0:先保证新系统不会产生安全和数据问题
|
||||
|
||||
| 工作 | 对应新需求章节 |
|
||||
| --- | --- |
|
||||
| 确定一企一实例或共享 SaaS | 1.1、5.1、9.2 |
|
||||
| 确定产品版只支持 MySQL 还是继续支持多数据库 | 所有服务端章节的存储基础 |
|
||||
| 消息客户端 ID 幂等 | 2.2;3.4、3.7;第 4 章 |
|
||||
| 文件下载资源级授权 | 3.10-3.12;4.13-4.20;6.27-6.28;7.3、7.7-7.11 |
|
||||
| 消息类型、错误码和兼容协议 | 2.4;第 3、4、7、8 章 |
|
||||
| 停止新客户端使用匿名、Long Poll、Channel 和客户端 gRPC | 新需求无对应章节,废弃项 |
|
||||
|
||||
### P1:完成即时通讯产品主链路
|
||||
|
||||
| 工作 | 对应新需求章节 |
|
||||
| --- | --- |
|
||||
| 企业码、短信注册/找回、二次验证、实名 | 第 1 章 |
|
||||
| 好友申请、联系人、隐私、黑名单 | 第 5 章;3.1-3.8 |
|
||||
| 结构化消息、@、引用、转发、收藏 | 2.4;第 3、4 章 |
|
||||
| 会话设置和可信系统通知 | 第 2 章 |
|
||||
|
||||
### P2:完成群、文件和搜索
|
||||
|
||||
| 工作 | 对应新需求章节 |
|
||||
| --- | --- |
|
||||
| 群申请、公告、管理员、禁言、二维码、群文件 | 第 6 章 |
|
||||
| 收藏分类和分享 | 7.1-7.8 |
|
||||
| 关键词和媒体类型搜索、历史回填 | 7.12-7.16 |
|
||||
|
||||
### P3:完成通话、设置和安全
|
||||
|
||||
| 工作 | 对应新需求章节 |
|
||||
| --- | --- |
|
||||
| 通话 ID、跨设备状态、TURN 和详单 | 第 8 章 |
|
||||
| 资料、版本、隐私和青少年模式 | 第 10 章 |
|
||||
| 反诈、举报、证据、反馈和后台处置 | 第 11 章 |
|
||||
|
||||
### P4:完成企业办公
|
||||
|
||||
| 工作 | 对应新需求章节 |
|
||||
| --- | --- |
|
||||
| 组织与部门 | 9.2 |
|
||||
| 会议通知 | 9.3-9.4 |
|
||||
| 日报/月报 | 9.5-9.11 |
|
||||
| 总经理信箱 | 9.12-9.13 |
|
||||
|
||||
## 10. 废弃实施规则
|
||||
|
||||
废弃项不要直接删除,应按以下顺序执行:
|
||||
|
||||
1. 在配置和对接文档中标记 deprecated,禁止新客户端接入;
|
||||
2. 为废弃入口增加调用指标,至少观察一个完整客户端版本周期;
|
||||
3. 对匿名账号、邮箱账号、Channel 等有数据的能力先做存量扫描;
|
||||
4. 给存量用户提供迁移方案,例如邮箱账号绑定手机号、Channel 迁移普通群;
|
||||
5. 新版本服务端默认关闭,保留临时回退开关;
|
||||
6. 确认无调用和无未迁移数据后,再删除路由、配置、代码和测试;
|
||||
7. 数据库删除列或表必须单独发布,保留备份和回滚窗口。
|
||||
|
||||
客户端 gRPC 和 Plugin gRPC 必须分开处理:新需求没有客户端 gRPC,因此前者废弃;第 11 章需要外部审核插件,因此 Plugin Firehose 及其 gRPC 通道保留。
|
||||
|
||||
## 11. 验收口径
|
||||
|
||||
每项改造只有同时满足以下条件才算完成:
|
||||
|
||||
1. 文档中的新需求章节有对应 API/协议、数据表和权限规则;
|
||||
2. 旧客户端兼容行为有明确结论,不依赖未定义 JSON 字段;
|
||||
3. 重试、并发、断网恢复不会产生重复业务数据;
|
||||
4. 用户不能通过直接构造协议绕过好友、黑名单、禁言、隐私和青少年策略;
|
||||
5. 文件、搜索、转发和收藏均重新执行当前用户的资源权限;
|
||||
6. 共享 SaaS 模式下通过跨租户负向测试;
|
||||
7. 审批、实名、举报、群管理和匿名信箱敏感操作有审计记录;
|
||||
8. 废弃入口有调用量证据和存量迁移结果,不能仅凭新原型没有页面就直接删除;
|
||||
9. MySQL schema、升级脚本、回滚脚本和代码中的建表定义保持一致;
|
||||
10. 服务端单元测试、MySQL 集成测试、WebSocket/HTTP 端到端测试和真实设备通话测试全部通过。
|
||||
|
||||
## 12. 最终处理建议
|
||||
|
||||
新系统应保留 Tinode 的实时消息内核,重点修改认证流程、消息协议、权限策略、文件授权和通话状态;好友、群治理、搜索、企业工作台、实名、青少年和举报作为新的产品业务层建设。
|
||||
|
||||
新需求未覆盖的匿名登录、邮箱注册验证、Channel、Long Poll、客户端 gRPC、示例聊天机器人和静态网页托管标记为废弃。多数据库 Adapter 是否物理删除取决于产品数据库决策;若产品版确定只支持 MySQL,则 PostgreSQL、MongoDB 和 RethinkDB 不再承担新业务适配与验收。
|
||||
|
||||
改造顺序应先处理租户、消息幂等和文件授权,再完成好友与基础消息,随后完成群治理、搜索、通话与安全,最后建设企业工作台。这样可以最大限度复用现有实现,同时避免继续维护新产品不需要的入口和功能。
|
||||
@@ -0,0 +1,298 @@
|
||||
# IM 第一至十一章后续增强项
|
||||
|
||||
> 日期:2026-08-13
|
||||
> 范围:第一章登录注册与安全、第二章消息首页/导航/系统通知、第三章单聊关系、第四章群消息与 @、第五章通讯录/好友/黑名单、第六章群聊创建/资料/治理、第七章文件/收藏/聊天记录检索、第八章实时语音与视频通话、第九章企业工作台与组织架构、第十章个人设置与客户端能力、第十一章反诈安全/举报/反馈
|
||||
> 用途:记录当前已完成基线之外的增强项,后续统一排期处理。
|
||||
|
||||
## 1. 第一章:登录、注册与安全
|
||||
|
||||
### 1.1 当前已完成基线
|
||||
|
||||
1. 企业码解析、租户状态校验和公开租户配置返回。
|
||||
2. 注册、登录前验证码发送与验证。
|
||||
3. 注册验证码 Token 消费,避免绕过验证码直接注册。
|
||||
4. 忘记密码挑战验证与重置确认。
|
||||
5. Token 绑定 `auth_version`,密码重置后旧 Token 失效。
|
||||
6. 登录后敏感操作二次验证挑战。
|
||||
7. 实名认证状态查询与提交,证件信息脱敏/保护存储。
|
||||
8. 独立密码设置与验证,包含弱密码规则和失败锁定。
|
||||
9. 登录后 bootstrap 返回实名、安全与菜单基础能力。
|
||||
|
||||
### 1.2 后续增强项
|
||||
|
||||
| 优先级 | 增强项 | 说明 | 建议验收点 |
|
||||
| --- | --- | --- | --- |
|
||||
| P0 | HTTP `Idempotency-Key` 统一存储与请求摘要冲突检测 | 当前部分接口要求幂等键,但还没有统一的 `tenant_id + uid + method + route + key + body_hash` 处理层。 | 相同键相同 body 返回同一结果;相同键不同 body 返回冲突。 |
|
||||
| P0 | 验证码真实发送通道 | 当前验证码生成、校验和限频链路已具备,短信/邮件供应商发送仍需接入真实服务。 | 验证码可通过供应商下发;供应商失败有可观测错误和重试策略。 |
|
||||
| P0 | 验证码限频维度补强 | 需要按租户、手机号、IP、设备、场景组合限频,并落库或接入 Redis。 | 高频请求返回 `VERIFICATION_RATE_LIMITED`,不同场景互不误伤。 |
|
||||
| P0 | 安全事件通知联动 | 密码重置、实名提交/审核、独立密码变更、新设备登录等应写入系统通知。 | 操作成功后 `/v1/im/system-notifications` 能看到对应事件。 |
|
||||
| P1 | 实名审核工作流 | 当前支持提交和状态查询,后续需要后台审核、驳回原因、通过后状态变更。 | 审核通过后 `realname.status=verified`;驳回可再次提交。 |
|
||||
| P1 | 独立密码用途扩展 | 当前按 `purpose` 存储,可继续接入青少年模式关闭、敏感设置修改等业务入口。 | 不同 `purpose` 相互隔离;错误次数和锁定互不串扰。 |
|
||||
| P1 | 设备与会话管理 | 需要登录设备列表、主动踢出设备、Token 刷新/撤销审计。 | 修改密码或踢设备后对应 Token 立即不可用。 |
|
||||
| P1 | API Key 轮换策略 | 当前 API Key 校验兼容现有逻辑,尚未实现最低可接受序号或撤销表。 | 旧序号 Key 可按配置禁用,新 Key 不影响正常客户端。 |
|
||||
| P2 | 风控与反诈策略接入 | 第一章安全动作后续应接入风险评分、反诈弹窗和二次确认策略。 | 高风险动作触发二次验证或反诈提示,低风险动作直通。 |
|
||||
|
||||
## 2. 第二章:消息首页、全局导航与系统通知
|
||||
|
||||
### 2.1 当前已完成基线
|
||||
|
||||
1. `GET /v1/im/bootstrap` 返回首页菜单、能力开关、权限和 `message_policy`。
|
||||
2. `GET /v1/im/search/conversations` 支持会话搜索和基础分页。
|
||||
3. WebSocket 发消息支持 `Head["x-im-client-mid"]` 幂等,重连重发不重复落库/广播。
|
||||
4. 消息 Header 基础校验:`x-im-schema`、`type`、`x-im-client-mid`。
|
||||
5. 普通用户禁止发布 `sys` Topic,且禁止伪造 `Head.type=system`。
|
||||
6. `GET /v1/im/topics/{topic}/messages/{seq}/receipts` 返回群消息送达/已读基础列表。
|
||||
7. `POST /v1/im/topics/{topic}/messages/{seq}/recall` 支持版本校验、权限校验和时限内撤回。
|
||||
8. `POST /v1/im/topics/{topic}/messages/{seq}/revisions` 支持文本消息编辑、版本递增和修订记录。
|
||||
9. `GET /v1/im/system-notifications` 和 `POST /v1/im/system-notifications/{id}/read`。
|
||||
|
||||
### 2.2 后续增强项
|
||||
|
||||
| 优先级 | 增强项 | 说明 | 建议验收点 |
|
||||
| --- | --- | --- | --- |
|
||||
| P0 | 编辑/撤回多端实时事件广播 | 当前 HTTP 更新 DB 并返回成功,后续需要向 Topic 在线成员广播编辑/撤回事件。 | A 端撤回/编辑后,B 端无需刷新即可收到事件并更新 UI。 |
|
||||
| P0 | 编辑/撤回事件 Outbox | DB 更新和实时广播应通过事务事件或 Outbox 衔接,避免更新成功但事件丢失。 | 模拟广播失败后,后台任务可补发事件。 |
|
||||
| P0 | 消息变更查询占位策略 | 已撤回消息在普通拉取中应返回撤回占位,而不是仅靠客户端本地事件。 | 新设备登录拉历史时能看到撤回占位和版本。 |
|
||||
| P0 | `Idempotency-Key` 接入编辑/撤回 | 编辑/撤回目前依赖乐观版本控制,后续需要同时支持 HTTP 幂等键。 | 客户端超时重试同一请求不会重复写 revision。 |
|
||||
| P1 | 群回执隐私和角色策略 | 当前返回基础 read/recv 列表,后续需按租户策略、群规模、角色决定是否展示具体成员。 | 普通成员/管理员在不同策略下看到不同粒度回执。 |
|
||||
| P1 | 群回执 cursor 精确分页 | 当前 cursor 是基础位置分页,后续可升级为稳定游标,避免成员变动造成跳页。 | 成员新增/删除时分页不重复、不漏主要数据。 |
|
||||
| P1 | `message_policy` 租户配置化 | 当前 recall/edit 窗口和菜单能力是固定值,后续需要来自租户配置。 | 不同租户可配置不同撤回/编辑时限和菜单能力。 |
|
||||
| P1 | 系统通知可信生产通道 | 当前已提供通知存储和读取接口,后续需要统一服务端生产 API/事件消费者,禁止业务散落直写。 | 安全、群、组织等模块通过统一通道创建通知。 |
|
||||
| P1 | 系统通知 action 白名单 | `action.type` 和参数需要统一注册、校验和版本化。 | 非白名单 action 写入失败;客户端只收到可识别跳转。 |
|
||||
| P1 | 会话搜索排序与类型过滤 | 当前按现有订阅数据过滤,后续需支持会话类型、置顶、免打扰、最近活跃排序等。 | 搜索结果与消息首页排序、筛选规则一致。 |
|
||||
| P2 | 离线重连恢复编排测试 | 需要端到端覆盖 `hi -> token login -> sub me -> 恢复 Topic -> get data`。 | 断网重连后未丢消息、未重复消息、未错乱回执。 |
|
||||
| P2 | 消息 Header schema 版本演进 | 当前做基础格式/枚举校验,后续需要按 `x-im-schema` 版本维护兼容矩阵。 | 老客户端消息可兼容,新客户端非法字段被拒绝。 |
|
||||
|
||||
## 3. 第三章:单聊关系、权限与会话上下文
|
||||
|
||||
### 3.1 当前已完成基线
|
||||
|
||||
1. 单聊会话上下文接口返回双方关系、发送能力、对端公开资料和 P2P Topic。
|
||||
2. P2P 发送路径接入好友关系校验,非好友、拉黑状态下禁止直接发送。
|
||||
3. 好友申请、通过、拒绝、删除好友与单聊关系状态联动。
|
||||
4. 拉黑/解除拉黑后关系上下文即时反映 `blocked_by_me`、`blocked_by_peer` 等状态。
|
||||
5. 会话侧可根据 `can_send` 锁定输入框,并引导重新添加好友。
|
||||
|
||||
### 3.2 后续增强项
|
||||
|
||||
| 优先级 | 增强项 | 说明 | 建议验收点 |
|
||||
| --- | --- | --- | --- |
|
||||
| P0 | P2P 策略统一入口 | 当前核心路径已校验好友/黑名单,后续需要把发消息、通话、名片、文件、历史查看统一收敛到同一策略服务。 | 任一 P2P 业务入口对同一关系状态返回一致权限。 |
|
||||
| P0 | 关系变更实时同步 | 好友通过、删除、拉黑、解除拉黑后,应向双方在线端推送关系和会话能力变化。 | A 拉黑 B 后,B 端输入框无需刷新自动锁定。 |
|
||||
| P0 | 关系事件 Outbox | 关系 DB 状态变化与系统通知、会话事件、Push 需要事务级衔接。 | 模拟推送失败后后台可补发,不产生重复事件。 |
|
||||
| P1 | Pending 会话体验增强 | 待处理好友申请对应的单聊页应携带申请摘要、来源、过期时间和快速处理入口。 | 进入 pending 会话可直接通过/拒绝,不需要跳转新朋友页。 |
|
||||
| P1 | 黑名单历史消息策略 | 当前发送/通话阻断已具备,后续需明确拉黑后历史消息、资料、文件和已读回执可见范围。 | 被拉黑人在不同策略下看到一致的历史与资料裁剪结果。 |
|
||||
| P1 | 非好友资料隐私裁剪 | 单聊上下文中的对端资料需要接入隐私设置、企业可见性和青少年模式。 | 非好友只能看到允许公开字段;好友看到扩展字段。 |
|
||||
| P1 | 好友关系审计 | 删除、拉黑、解除拉黑、申请处理等敏感关系操作写审计日志。 | 后台可按租户、操作者、目标用户查询操作流水。 |
|
||||
| P2 | 关系状态迁移测试矩阵 | 需要覆盖 none、pending_inbound、pending_outbound、friend、blocked_by_me、blocked_by_peer 的全迁移。 | 任意状态迁移后 DB、HTTP 上下文、WS 权限一致。 |
|
||||
|
||||
## 4. 第四章:群消息、群成员与 @ 能力
|
||||
|
||||
### 4.1 当前已完成基线
|
||||
|
||||
1. 群成员列表接口支持基础分页、搜索和 `mentionable` 过滤。
|
||||
2. 返回成员角色、公开资料、ACL 和是否可被 @。
|
||||
3. 群 @ 消息头部字段校验,支持 `mentioned_user_ids`、`mention_all` 等基础结构。
|
||||
4. 群 @ 权限接入成员 ACL,管理员/群主才具备 @ 全体能力。
|
||||
5. 群消息读取链路保留 @ 元数据,客户端可据此展示提醒。
|
||||
|
||||
### 4.2 后续增强项
|
||||
|
||||
| 优先级 | 增强项 | 说明 | 建议验收点 |
|
||||
| --- | --- | --- | --- |
|
||||
| P0 | @ 实时通知与离线 Push | 当前消息元数据已落库,后续需要对被 @ 成员生成高优先级提醒和离线 Push。 | 被 @ 用户在在线、离线、多端场景均收到一次提醒。 |
|
||||
| P0 | @ 全体频控 | @ 全体需要按租户、群、角色、时间窗口限频,避免刷屏。 | 超限返回稳定错误码,管理员可配置限额。 |
|
||||
| P0 | 群成员稳定游标分页 | 当前成员列表以基础 cursor 分页,后续应使用稳定游标避免成员变动跳页。 | 成员入群/退群时翻页不重复、不漏主要数据。 |
|
||||
| P1 | 群成员资料隐私裁剪 | 群内非好友主页需要按群关系、好友关系、隐私设置裁剪资料字段。 | 同一成员从群内和好友主页进入时字段符合策略差异。 |
|
||||
| P1 | 群昵称/角色变更事件 | 群昵称、角色、禁言、移除等变化需要实时推送给群成员并刷新成员列表缓存。 | 群主修改成员角色后在线端成员列表自动更新。 |
|
||||
| P1 | @ 候选排序优化 | @ 面板候选应支持拼音首字母、最近互动、角色优先和本地缓存协同。 | 大群搜索响应稳定,候选排序符合产品规则。 |
|
||||
| P1 | 群治理策略配置化 | 成员私聊、成员邀请、全员禁言、上传权限等应统一由群治理配置控制。 | 不同群可配置不同治理能力,HTTP 与 WS 权限一致。 |
|
||||
| P2 | 大群性能压测 | 需要覆盖 500、2000、10000 成员下的成员列表、@ 搜索和发送性能。 | 达到目标延迟和资源占用阈值。 |
|
||||
|
||||
## 5. 第五章:通讯录、好友关系与黑名单
|
||||
|
||||
### 5.1 当前已完成基线
|
||||
|
||||
1. 好友列表支持查询、首字母、分页,返回备注、头像、显示名来源和会话 Topic。
|
||||
2. 新朋友列表、好友申请、通过、拒绝链路已具备。
|
||||
3. 用户搜索支持账号、精准账号和手机号类型,返回公开资料、关系和 `action`。
|
||||
4. 添加好友二维码支持短期签名 Token 生成与解析。
|
||||
5. 好友个人主页返回关系、资料和操作能力。
|
||||
6. 群内非好友个人主页返回群角色、关系、资料和操作能力。
|
||||
7. 好友备注更新、删除好友、拉黑/解除拉黑已具备。
|
||||
8. 黑名单列表和批量移出黑名单已具备。
|
||||
|
||||
### 5.2 后续增强项
|
||||
|
||||
| 优先级 | 增强项 | 说明 | 建议验收点 |
|
||||
| --- | --- | --- | --- |
|
||||
| P0 | 好友/黑名单操作统一幂等 | 拉黑、解除、删除、批量移出、好友申请等需要统一接入 `Idempotency-Key` 存储和请求摘要。 | 客户端超时重试不会重复创建申请或重复触发事件。 |
|
||||
| P0 | 好友关系实时事件 | 申请、通过、拒绝、删除、备注、拉黑等状态变化需要向相关在线端实时同步。 | 两端关系页、会话页和通讯录无需刷新自动更新。 |
|
||||
| P0 | 用户搜索隐私策略 | 当前搜索基线可用,后续需要接入 `allow_user_search`、添加方式、手机号可搜索等租户和个人隐私设置。 | 禁止手机号添加的用户无法通过手机号被搜索到,且不泄露存在性。 |
|
||||
| P0 | 二维码吊销与一次性策略 | 当前二维码是短期签名 Token,后续如需主动失效、刷新覆盖、扫码审计,需要落库或接入 Redis。 | 用户刷新二维码后旧码按策略失效;后台可查询扫码记录。 |
|
||||
| P1 | 好友资料字段精细裁剪 | 个人主页需按好友、非好友、群内关系、黑名单、实名/青少年等规则返回不同字段。 | 不同入口返回字段符合隐私矩阵。 |
|
||||
| P1 | 通讯录排序与拼音索引 | 当前支持基础 initial,后续需维护昵称/备注拼音、特殊字符分组和本地增量同步。 | 好友列表与客户端通讯录排序一致,中文首字母稳定。 |
|
||||
| P1 | 黑名单占位资料 | 资料删除、离职或不可见时,黑名单列表仍需返回可解除的受控占位用户。 | 黑名单中失效用户可展示占位并成功移出。 |
|
||||
| P1 | 好友申请防骚扰 | 需要按目标用户、申请人、IP、设备、来源和群来源限频,并支持拒收策略。 | 高频申请被限制;群来源申请校验双方仍在群内。 |
|
||||
| P1 | 关系审计和安全通知 | 添加、删除、拉黑、解除、二维码解析等应写审计并按策略生成系统通知。 | 用户可在通知中看到关键关系事件,后台可追溯。 |
|
||||
| P2 | 批量接口部分失败语义增强 | 批量移出黑名单目前返回逐项结果,后续需要统一批量接口错误码、重试建议和幂等结果缓存。 | 部分失败时客户端能只重试失败项。 |
|
||||
|
||||
## 6. 第六章:群聊创建、资料维护与成员治理
|
||||
|
||||
### 6.1 当前已完成基线
|
||||
|
||||
1. 群列表、创建群、群资料查询与更新、群头像和群简介保存接口已具备。
|
||||
2. 群设置、个人群设置、退出群、群昵称接口已具备。
|
||||
3. 群二维码生成与扫码解析支持 `join_group` 场景。
|
||||
4. 群邀请、进群申请、申请列表、同意/拒绝、群 summary 申请计数已具备。
|
||||
5. 群公告最新查询、发布/更新、详情和已读接口已具备。
|
||||
6. 群治理配置 `GET/PATCH /governance` 已具备。
|
||||
7. 管理员候选、管理员上限、批量添加管理员、删除管理员和群主转让接口已具备。
|
||||
8. 禁言候选、批量禁言、禁言列表和解除禁言接口已具备。
|
||||
9. 当前实现优先复用 Topic、Subscription 和 Topic Aux,把产品主链路接口跑通。
|
||||
|
||||
### 6.2 后续增强项
|
||||
|
||||
| 优先级 | 增强项 | 说明 | 建议验收点 |
|
||||
| --- | --- | --- | --- |
|
||||
| P0 | 群业务独立表拆分 | 当前群公告、进群申请、治理配置、禁言记录先存 Topic Aux,后续需要拆为独立表。 | 公告、申请、治理、禁言均有 tenant/topic 维度索引,可稳定分页和审计。 |
|
||||
| P0 | 群治理强事务状态机 | 邀请、申请审批、管理员变更、群主转让、禁言需在事务内同步业务表、订阅 ACL、成员数和事件。 | 并发审批/转让/禁言不会出现 ACL 与业务状态不一致。 |
|
||||
| P0 | 第六章幂等键落库 | 邀请、申请处理、公告发布、治理 PATCH、管理员批量、群主转让、禁言批量都需要统一幂等。 | 超时重试同一请求返回同一结果,不重复加人、升权或发事件。 |
|
||||
| P0 | 系统事件、Push 与 Outbox | 第六章所有成员/公告/治理变化需要生成可信群系统事件,并通过 Outbox 保证推送。 | 在线端实时刷新;模拟推送失败后可补偿且不重复。 |
|
||||
| P1 | 群公告已读独立记录 | 当前公告已读返回基础结果,后续需记录每个成员的公告已读时间。 | 新公告只提醒未读成员,已读状态跨端同步。 |
|
||||
| P1 | 进群申请隐私与限频 | 申请入口需要按群治理、租户策略、用户状态、来源、IP/设备限频。 | 高频申请被限制;普通成员不可看到敏感申请人信息。 |
|
||||
| P1 | 管理员/群主角色层级策略 | 管理员互相操作、管理员禁言、群主转让二次认证等需要完整策略矩阵。 | 群主、管理员、普通成员在每个治理动作上的权限稳定一致。 |
|
||||
| P1 | 禁言发送链路强校验 | 当前禁言接口已具备,后续需要在 WebSocket 发消息路径校验 `GROUP_MUTED`。 | 被禁言成员发群消息被服务端拒绝,过期后自动恢复。 |
|
||||
| P1 | 群文件索引 | 6.27-6.28 需要独立群文件表,绑定上传文件、群成员权限、下载状态和删除权限。 | 群文件列表可搜索/分页,退群后不可下载旧 URL。 |
|
||||
| P1 | 群解散与二维码吊销 | 群解散、二维码撤销、邀请失效需要状态联动和权限回收。 | 解散后 Topic、二维码、邀请、文件新增权限全部失效。 |
|
||||
| P2 | 大群性能与稳定分页 | 成员、申请、公告、禁言、文件列表都需要稳定游标和大群压测。 | 大群场景下分页不重复不漏,接口延迟达标。 |
|
||||
|
||||
## 7. 第七章:文件下载、收藏管理与聊天记录检索
|
||||
|
||||
### 7.1 当前已完成基线
|
||||
|
||||
1. 收藏创建、列表、详情和删除接口已具备,支持文本、文件、语音、链接等类型。
|
||||
2. 收藏批量创建和基础分享前的数据结构已具备。
|
||||
3. Topic 消息关键词搜索、文件/图片/视频等媒体类型检索接口已具备。
|
||||
4. 消息上下文定位接口可围绕指定 seq 返回前后消息。
|
||||
5. 文件访问已接入用户与 Topic 权限校验,下载前可判断资源可见性。
|
||||
|
||||
### 7.2 后续增强项
|
||||
|
||||
| 优先级 | 增强项 | 说明 | 建议验收点 |
|
||||
| --- | --- | --- | --- |
|
||||
| P0 | 收藏幂等与去重策略 | 收藏同一消息、同一文件或同一链接需要统一去重和幂等键处理。 | 重复点击收藏不会产生重复记录;超时重试返回同一收藏 ID。 |
|
||||
| P0 | 收藏分享权限二次校验 | 收藏文件或消息分享给好友/群时,需要重新校验接收方是否可访问原资源或生成受控副本。 | 无权限接收方不能通过收藏分享拿到原会话附件。 |
|
||||
| P0 | 文件下载短时签名 URL | 当前权限判断已具备,后续应生成短时下载 URL,并记录下载审计。 | URL 过期不可访问;退群/拉黑后旧 URL 失效或受策略限制。 |
|
||||
| P0 | 文件安全扫描与 MIME 校验 | 上传后的文件需要扩展名、MIME、内容嗅探、病毒扫描和图片/视频解码安全检查。 | 高风险文件不可下载或转发,客户端收到稳定错误码。 |
|
||||
| P1 | 收藏分类与全文索引 | 收藏列表需要按类型、来源、时间、关键词、发送人等维度检索。 | 大量收藏下搜索响应稳定,类型筛选结果准确。 |
|
||||
| P1 | 聊天记录稳定游标 | 消息搜索和媒体列表需要稳定游标,避免新消息插入导致翻页重复或漏项。 | 搜索期间新增消息不影响当前翻页结果。 |
|
||||
| P1 | 历史消息权限快照策略 | 用户退群、删除好友、拉黑后,历史消息和附件的可见范围需要统一规则。 | 各入口对同一历史资源权限判断一致。 |
|
||||
| P1 | 下载状态与断点续传 | 大文件下载需要断点续传、失败重试、进度状态和下载完成事件。 | 网络中断后可续传;下载完成状态可跨端同步或按端隔离。 |
|
||||
| P2 | 聊天记录高亮与命中片段 | 搜索结果可返回命中摘要、高亮范围和消息类型摘要。 | 客户端无需二次解析即可展示命中片段。 |
|
||||
| P2 | 收藏导出与批量管理 | 后续可支持收藏批量删除、批量转发、导出和云端容量统计。 | 批量操作有部分失败语义和可重试结果。 |
|
||||
|
||||
## 8. 第八章:实时语音与视频通话
|
||||
|
||||
### 8.1 当前已完成基线
|
||||
|
||||
1. 通话 invite、accept、offer、answer、ice-candidate、hang-up 等事件沿用 Tinode call event 链路。
|
||||
2. 通话记录表 `im_call_records` 已具备,记录 `call_id`、Topic、seq、主叫/被叫、类型、状态、时长和结束原因。
|
||||
3. 通话记录创建、接听和结束状态更新已接入 store/adapter。
|
||||
4. 通话记录查询接口已具备:全局 `GET /v1/im/calls` 与 Topic 内 `GET /v1/im/topics/{topic}/calls`。
|
||||
5. 查询结果返回对端用户、方向、通话类型、状态、时长和关键时间点,客户端可先完成通话记录页对接。
|
||||
|
||||
### 8.2 后续增强项
|
||||
|
||||
| 优先级 | 增强项 | 说明 | 建议验收点 |
|
||||
| --- | --- | --- | --- |
|
||||
| P0 | 跨设备抢答状态机 | 同一账号多端收到来电时,需要一个设备接听后自动取消其他端待接状态。 | A 手机接听后,A 平板和 Web 端立即变为已在其他设备接听。 |
|
||||
| P0 | 通话状态强一致与幂等 | invite、accept、hang-up 等事件需要幂等键或事件序号,避免乱序/重复导致状态回退。 | 重复 hang-up 不改变最终状态;迟到 accept 不会覆盖已结束通话。 |
|
||||
| P0 | 通话事件 Outbox 与通知 | 来电、未接、挂断、忙线等需要可信事件和 Push,DB 状态与通知通过 Outbox 衔接。 | 离线用户收到一次来电/未接提醒,失败可补偿。 |
|
||||
| P0 | TURN/STUN 配置与媒体准入 | 服务端应按租户/区域返回可用 ICE 配置,并限制未授权用户创建媒体会话。 | 客户端拉取到有效 TURN;无权限通话被服务端拒绝。 |
|
||||
| P1 | 结束原因标准化 | 需要统一 `missed`、`declined`、`busy`、`timeout`、`network_error` 等原因和客户端展示文案。 | 不同端同一结束原因展示一致,统计口径一致。 |
|
||||
| P1 | 通话质量指标 | 需要采集建立耗时、ICE 成功率、丢包、抖动、断连等指标。 | 后台可按租户和版本查看通话质量趋势。 |
|
||||
| P1 | 群通话/会议房间拆分 | 群音视频不宜复用 P2P call,需要会议房间、参与人状态和 SFU 接入。 | 群通话有独立 room_id、成员列表和退出/重进状态。 |
|
||||
| P1 | 通话权限统一策略 | 通话入口应复用 P2P/群关系策略、黑名单、禁言、青少年模式和企业策略。 | 被拉黑、非好友或受限制用户无法发起通话。 |
|
||||
| P2 | 通话记录聚合与清理 | 通话记录可按会话、日期、未接分类聚合,并支持保留周期清理。 | 记录列表分页稳定,超过保留期按策略清理。 |
|
||||
|
||||
## 9. 第九章:企业工作台与办公协同
|
||||
|
||||
### 9.1 当前已完成基线
|
||||
|
||||
1. 工作台接口 `GET /v1/im/workbench` 已具备,返回模块、布局版本和企业功能开关。
|
||||
2. 工作台模块表 `im_workbench_apps` 已具备,默认包含组织架构、会议通知、日报、月报、总经理信箱入口。
|
||||
3. 服务端对工作台 route 做内部白名单过滤,避免客户端执行任意服务端 URL。
|
||||
4. 组织架构部门表 `im_org_departments` 和成员表 `im_org_members` 已具备。
|
||||
5. 部门列表 `GET /v1/im/organization/departments`、部门成员 `GET /v1/im/organization/departments/{id}/members`、组织搜索 `GET /v1/im/organization/search` 已具备。
|
||||
6. 会议通知 `POST/GET/PATCH/CANCEL/REMIND/MY-RESPONSE` 已具备基线,包含参与人和版本冲突控制。
|
||||
7. 日报/月报草稿、提交、详情、列表、统计与提醒基线已具备。
|
||||
8. 总经理信箱配置与提交接口已具备,包含匿名策略、文案版本和幂等键。
|
||||
|
||||
### 9.2 后续增强项
|
||||
|
||||
| 优先级 | 增强项 | 说明 | 建议验收点 |
|
||||
| --- | --- | --- | --- |
|
||||
| P0 | 工作台权限与功能开关配置化 | 当前模块基线可返回,后续需要按租户、部门、角色、后台配置动态裁剪。 | 不同用户登录同一租户看到不同授权模块。 |
|
||||
| P0 | 工作台未读角标事件 | 文档要求 `workbench.badge.updated`,后续需要模块未读聚合和实时增量事件。 | 会议、日报、信箱未读变化后工作台角标实时更新。 |
|
||||
| P0 | 组织数据后台维护链路 | 当前前台读接口和表已具备,后续需要后台导入、增删改部门、调整成员和离职状态。 | 后台修改组织后前台接口版本变化,客户端可刷新。 |
|
||||
| P0 | 组织权限与可见字段裁剪 | 成员手机号、邮箱、职位等字段需要按组织角色、隐私和企业策略裁剪。 | 普通员工和管理员看到的成员字段不同且不可绕过。 |
|
||||
| P1 | 组织搜索索引优化 | 当前基础 LIKE 搜索可用,后续需要拼音、工号、手机号脱敏匹配和稳定排序。 | 大组织下搜索延迟达标,结果排序符合组织规则。 |
|
||||
| P1 | 组织版本与增量同步 | 部门和成员返回版本,后续需要全局组织版本、变更事件和客户端增量刷新。 | 切换部门或人员变更后客户端无需全量重拉。 |
|
||||
| P1 | 会议通知实时提醒 | 当前接口和表已具备,后续需要会议创建、更新、取消、响应后的在线事件和 Push。 | 参与人在线端无需刷新即可收到会议变化。 |
|
||||
| P1 | 日报/月报后台统计口径 | 当前基础统计可用,后续需要按部门、周期、迟交、补交、未读等维度扩展。 | 管理员看板与业务口径一致。 |
|
||||
| P1 | 总经理信箱后台流转 | 当前前台配置/提交已具备,后续需要后台处理、回复、转派、归档和通知。 | 信件全生命周期可追踪,匿名身份受保护。 |
|
||||
| P1 | 办公协同审计 | 组织查询、会议变更、报告查看、信箱提交等敏感动作需要审计。 | 后台可按租户、用户、对象查询办公操作流水。 |
|
||||
| P2 | 办公模块统一附件策略 | 会议、日报、月报、信箱附件需要复用第七章文件 ACL、用途和病毒扫描。 | 附件上传、查看、下载权限与业务状态一致。 |
|
||||
|
||||
## 10. 个人设置与客户端能力
|
||||
|
||||
### 10.1 当前已完成基线
|
||||
|
||||
1. 个人资料 `GET/PATCH /v1/im/me/profile`、个人页 `GET /v1/im/me` 已具备。
|
||||
2. 隐私设置 `GET/PATCH /v1/im/me/privacy` 已具备,支持版本冲突控制。
|
||||
3. 最新版本检查 `GET /v1/im/app/releases/latest` 已具备,支持未登录按租户解析调用。
|
||||
4. 青年模式查询、开启、关闭接口已具备,复用敏感操作 `action_token` 和独立密码校验链路。
|
||||
5. 设备 Push Token 覆盖写入 `PUT /v1/im/devices/{device_id}/push-token` 已具备。
|
||||
|
||||
### 10.2 后续增强项
|
||||
|
||||
| 优先级 | 增强项 | 说明 | 建议验收点 |
|
||||
| --- | --- | --- | --- |
|
||||
| P0 | 个人资料变更实时同步 | 资料、头像、隐私变更后需要向多端推送 `profile.updated`、`privacy.updated`。 | 同账号多端无需重登即可刷新设置。 |
|
||||
| P0 | 精准登出与会话撤销 | 现有 token 主要靠 `auth_version` 批量失效,后续需要 access/refresh/session 表支持当前设备登出和指定 session 删除。 | 当前设备退出不影响其他设备;删除指定 session 后该设备 token 立即失效。 |
|
||||
| P0 | 青年模式策略配置化 | 当前返回默认 restrictions,后续需要后台按租户配置可用时段、内容限制、消费保护等规则。 | 不同租户返回不同限制策略且能热更新。 |
|
||||
| P0 | Push Token 生命周期 | 当前可覆盖写入,后续需要 token 失效回收、设备解绑、平台厂商错误码处理。 | 推送失败后自动禁用无效 token,重新登录可恢复。 |
|
||||
| P1 | 版本灰度发布 | 当前按 platform/channel 查最新版本,后续需要按租户、用户、灰度比例、最低版本强制升级。 | 灰度用户收到新版本,非灰度用户保持稳定版本。 |
|
||||
| P1 | 资料隐私裁剪落地 | 隐私设置需要接入用户搜索、组织架构、会话资料页等读路径。 | 非授权用户无法通过其他接口绕过隐私配置。 |
|
||||
| P2 | 客户端能力上报 | 后续增加设备能力、通知权限、厂商通道状态和前台活跃状态上报。 | 后台可定位推送不可达和版本兼容问题。 |
|
||||
|
||||
## 11. 反诈安全、举报与意见反馈
|
||||
|
||||
### 11.1 当前已完成基线
|
||||
|
||||
1. URL 安全检查 `POST /v1/im/security/urls/check` 已接入风控 challenge 创建。
|
||||
2. 风控确认 `POST /v1/im/security/risk-challenges/{id}/confirm` 已具备,`continue` 返回短时 `risk_action_token`。
|
||||
3. 举报原因 `GET /v1/im/reports/abuse/reasons` 已具备,默认覆盖用户、群、消息目标。
|
||||
4. 举报提交 `POST /v1/im/reports/abuse` 已具备,支持幂等键、证据文件 ID、目标基础可见性校验和审核快照。
|
||||
5. 意见反馈配置 `GET /v1/im/feedback/config`、提交 `POST /v1/im/feedback` 已具备,支持附件规则、诊断授权和幂等键。
|
||||
|
||||
### 11.2 后续增强项
|
||||
|
||||
| 优先级 | 增强项 | 说明 | 建议验收点 |
|
||||
| --- | --- | --- | --- |
|
||||
| P0 | 风控策略引擎 | 当前 URL 检查仅基础规则,后续需要按动作、关系、内容、设备、频率和租户策略综合判定。 | 高风险发消息、加好友、打开链接等动作统一返回风控 challenge。 |
|
||||
| P0 | 风控 token 消费接入业务接口 | 当前确认可签发 `risk_action_token`,后续需要原业务接口校验并单次消费该 token。 | 二次确认后只允许重试原动作一次,重复使用返回 Token 已消费。 |
|
||||
| P0 | 举报目标完整权限校验 | 当前做基础可见性校验,后续需要结合用户侧删除、撤回、退群、黑名单、文件 ACL 和审核状态。 | 无权限目标无法提交举报,也不泄露目标存在性。 |
|
||||
| P0 | 审核后台流转 | 举报和反馈当前仅 received,后续需要后台分派、处理、关闭、回复和通知。 | 处理状态可追踪,举报人按策略收到处理结果。 |
|
||||
| P1 | 证据文件安全与保留期 | 证据文件需要 `abuse_evidence` 专用 ACL、病毒扫描、不可转发和保留期策略。 | 证据只能审核链路访问,过期按政策清理。 |
|
||||
| P1 | 反诈系统事件可信生产 | 低风险提示需通过可信 `security.risk_notice` 系统事件发送,并限制普通消息伪装。 | 普通用户无法伪造反诈弹窗,客户端只信任系统事件。 |
|
||||
| P1 | 举报合并与限频 | 重复举报需要合并、返回原受理号或限频错误,避免刷单。 | 短时间重复举报同一目标不会生成大量工单。 |
|
||||
| P2 | 反馈诊断包脱敏 | 诊断包需统一脱敏、大小限制、客户端日志授权和服务端下载审计。 | 未同意时没有诊断包,同意后包内不含明文 token/手机号。 |
|
||||
|
||||
## 12. 统一处理建议
|
||||
|
||||
1. 先做 P0:统一幂等、实时事件/Outbox、验证码真实发送与限频、P2P/好友/搜索隐私策略、第六章群业务独立表与事务状态机、文件下载签名、通话强状态机、工作台/组织权限。
|
||||
2. 再做 P1:租户策略配置、系统通知生产通道、实名审核、设备管理、资料隐私裁剪、关系审计、群禁言发送校验、群文件索引、通话质量指标、会议/日报/月报/信箱完整业务表。
|
||||
3. 最后做 P2:端到端重连测试、schema 版本演进、大群压测、批量接口体验优化、收藏导出、通话记录聚合和办公模块附件统一策略。
|
||||
4. 每个增强项落地前同步更新 `IM-系统设计文档.md` 和 `IM-前后端对接文档.md` 中对应章节状态。
|
||||
@@ -0,0 +1,316 @@
|
||||
-- IM 第一至七章数据库增量审核稿
|
||||
-- 仅供审核,不要直接在生产库盲执行。
|
||||
-- 适用目标:已有 Tinode MySQL 库,补齐本项目 1-7 章业务层依赖表与关键字段。
|
||||
-- 注意:本稿使用 MySQL 8 functional index 处理空 idempotency_key 唯一性。
|
||||
-- 第一阶段多库方案:本 SQL 的 im_tenant 放 hotgo 公共库;tinode/各租户业务库表暂时保留 tenant_id 字段。
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- hotgo 公共后台库需要先存在;如果你审核通过,可先执行:
|
||||
-- CREATE DATABASE IF NOT EXISTS hotgo CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
|
||||
-- USE hotgo;
|
||||
--
|
||||
-- 租户业务库 tinode 也需要存在并完成 Tinode 基础表初始化:
|
||||
-- CREATE DATABASE IF NOT EXISTS tinode CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- 1. 租户、认证、安全基础表
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS im_tenant(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
code VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
tenant_desc VARCHAR(256),
|
||||
db_name VARCHAR(128) NOT NULL DEFAULT '' COMMENT 'tenant business database name, e.g. tinode_acme',
|
||||
db_dsn VARCHAR(1024) NOT NULL DEFAULT '' COMMENT 'optional full tenant DB DSN, empty means reuse base MySQL config',
|
||||
state SMALLINT NOT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_by BIGINT NOT NULL,
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
updated_by BIGINT NOT NULL,
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_tenant_code(code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
INSERT INTO im_tenant(code,name,tenant_desc,db_name,db_dsn,state,created_by,updated_by)
|
||||
VALUES('default','Default Tenant',NULL,'tinode','',1,0,0)
|
||||
ON DUPLICATE KEY UPDATE name=VALUES(name), db_name=VALUES(db_name), updated_at=CURRENT_TIMESTAMP(3);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS im_tenant_config(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
max_users BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
|
||||
max_groups BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
|
||||
max_group_members INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_by BIGINT NOT NULL,
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
updated_by BIGINT NOT NULL,
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_tenant_config_tenant_id(tenant_id),
|
||||
CONSTRAINT fk_im_tenant_config_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS im_verification_codes(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
challenge_id VARCHAR(64) NOT NULL,
|
||||
scene VARCHAR(32) NOT NULL,
|
||||
action VARCHAR(64) NOT NULL DEFAULT '',
|
||||
phone_hash CHAR(64) NOT NULL,
|
||||
phone_mask VARCHAR(32) NOT NULL,
|
||||
target_user BIGINT,
|
||||
code_hash CHAR(64) NOT NULL,
|
||||
verification_token_hash CHAR(64),
|
||||
state SMALLINT NOT NULL,
|
||||
attempts INT NOT NULL DEFAULT 0,
|
||||
max_attempts INT NOT NULL,
|
||||
expires_at DATETIME(3) NOT NULL,
|
||||
resend_after DATETIME(3) NOT NULL,
|
||||
verified_at DATETIME(3),
|
||||
consumed_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_verification_tenant_challenge(tenant_id, challenge_id),
|
||||
INDEX idx_im_verification_tenant_scene_phone_state(tenant_id, scene, phone_hash, state, expires_at),
|
||||
CONSTRAINT fk_im_verification_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS im_user_security(
|
||||
tenant_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
auth_version INT NOT NULL DEFAULT 1,
|
||||
password_updated_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(tenant_id,user_id),
|
||||
CONSTRAINT fk_im_user_security_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS im_realname_verifications(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
submission_id VARCHAR(64) NOT NULL,
|
||||
status VARCHAR(16) NOT NULL,
|
||||
name_cipher TEXT NOT NULL,
|
||||
id_type VARCHAR(32) NOT NULL,
|
||||
id_number_cipher TEXT NOT NULL,
|
||||
id_number_hash CHAR(64) NOT NULL,
|
||||
masked_name VARCHAR(64) NOT NULL,
|
||||
masked_id_number VARCHAR(64) NOT NULL,
|
||||
review_reason VARCHAR(256),
|
||||
verified_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_realname_submission(tenant_id, submission_id),
|
||||
INDEX idx_im_realname_user_latest(tenant_id, user_id, created_at),
|
||||
INDEX idx_im_realname_id_hash(tenant_id, id_number_hash),
|
||||
CONSTRAINT fk_im_realname_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS im_action_tokens(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
token_id VARCHAR(64) NOT NULL,
|
||||
action VARCHAR(64) NOT NULL,
|
||||
target VARCHAR(128) NOT NULL DEFAULT '',
|
||||
token_hash CHAR(64) NOT NULL,
|
||||
state SMALLINT NOT NULL,
|
||||
expires_at DATETIME(3) NOT NULL,
|
||||
consumed_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_action_tokens_token(tenant_id, token_id),
|
||||
INDEX idx_im_action_tokens_user_action(tenant_id, user_id, action, target, state, expires_at),
|
||||
CONSTRAINT fk_im_action_tokens_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS im_youth_mode_settings(
|
||||
tenant_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
purpose VARCHAR(64) NOT NULL,
|
||||
password_hash VARBINARY(255) NOT NULL,
|
||||
failed_attempts INT NOT NULL DEFAULT 0,
|
||||
locked_until DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(tenant_id, user_id, purpose),
|
||||
CONSTRAINT fk_im_youth_settings_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS im_system_notifications(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
event_id VARCHAR(64) NOT NULL,
|
||||
event_type VARCHAR(64) NOT NULL,
|
||||
title VARCHAR(128) NOT NULL,
|
||||
summary VARCHAR(512) NOT NULL,
|
||||
action JSON,
|
||||
read_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_system_notifications_event(tenant_id, user_id, event_id),
|
||||
INDEX idx_im_system_notifications_inbox(tenant_id, user_id, id DESC),
|
||||
CONSTRAINT fk_im_system_notifications_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- 2. 好友、黑名单
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS im_friend_requests(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
request_id VARCHAR(64) NOT NULL,
|
||||
applicant_id BIGINT NOT NULL,
|
||||
target_id BIGINT NOT NULL,
|
||||
message VARCHAR(200) NOT NULL DEFAULT '',
|
||||
source VARCHAR(32) NOT NULL DEFAULT '',
|
||||
source_topic VARCHAR(64) NOT NULL DEFAULT '',
|
||||
status VARCHAR(16) NOT NULL,
|
||||
idempotency_key VARCHAR(128) NOT NULL DEFAULT '',
|
||||
expires_at DATETIME(3) NOT NULL,
|
||||
handled_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_friend_requests_request(tenant_id, request_id),
|
||||
INDEX idx_im_friend_requests_pair(tenant_id, applicant_id, target_id, status, created_at),
|
||||
INDEX idx_im_friend_requests_inbox(tenant_id, target_id, status, created_at),
|
||||
CONSTRAINT fk_im_friend_requests_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE UNIQUE INDEX uk_im_friend_requests_idempotency
|
||||
ON im_friend_requests(tenant_id, applicant_id, (NULLIF(idempotency_key, '')));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS im_friendships(
|
||||
tenant_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
peer_id BIGINT NOT NULL,
|
||||
status VARCHAR(16) NOT NULL,
|
||||
remark VARCHAR(64) NOT NULL DEFAULT '',
|
||||
topic CHAR(25) NOT NULL DEFAULT '',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(tenant_id, user_id, peer_id),
|
||||
INDEX idx_im_friendships_peer(tenant_id, peer_id, status),
|
||||
CONSTRAINT fk_im_friendships_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS im_user_blocks(
|
||||
tenant_id BIGINT NOT NULL,
|
||||
blocker_id BIGINT NOT NULL,
|
||||
blocked_id BIGINT NOT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(tenant_id, blocker_id, blocked_id),
|
||||
INDEX idx_im_user_blocks_blocked(tenant_id, blocked_id),
|
||||
CONSTRAINT fk_im_user_blocks_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- 3. 收藏、转发、消息增强
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS im_favorites(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
favorite_id VARCHAR(64) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
item_type VARCHAR(16) NOT NULL,
|
||||
source_topic CHAR(25) NOT NULL,
|
||||
source_seq INT NOT NULL,
|
||||
snapshot JSON,
|
||||
attachment_refs JSON,
|
||||
availability VARCHAR(16) NOT NULL DEFAULT 'available',
|
||||
idempotency_key VARCHAR(128) NOT NULL DEFAULT '',
|
||||
collected_at DATETIME(3) NOT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_favorites_favorite(tenant_id, favorite_id),
|
||||
UNIQUE INDEX uk_im_favorites_source(tenant_id, user_id, source_topic, source_seq),
|
||||
INDEX idx_im_favorites_inbox(tenant_id, user_id, id DESC),
|
||||
INDEX idx_im_favorites_type(tenant_id, user_id, item_type, id DESC),
|
||||
CONSTRAINT fk_im_favorites_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE UNIQUE INDEX uk_im_favorites_idempotency
|
||||
ON im_favorites(tenant_id, user_id, (NULLIF(idempotency_key, '')));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS im_message_revisions(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
createdat DATETIME(3) NOT NULL,
|
||||
updatedat DATETIME(3) NOT NULL,
|
||||
topic CHAR(25) NOT NULL,
|
||||
seqid INT NOT NULL,
|
||||
version INT NOT NULL,
|
||||
actor BIGINT NOT NULL,
|
||||
head JSON,
|
||||
content JSON,
|
||||
reason VARCHAR(256) NOT NULL DEFAULT '',
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_message_revisions_version(tenant_id, topic, seqid, version),
|
||||
CONSTRAINT fk_im_message_revisions_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- 4. Tinode 基础表需补齐的字段/索引
|
||||
-- 如果你的库是用当前 schema.sql 初始化,这些已经存在。
|
||||
-- 如果是旧库,需要逐项核对后再 ALTER。
|
||||
-- ============================================================
|
||||
|
||||
-- users:租户隔离字段和索引。
|
||||
-- ALTER TABLE users ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 1 AFTER id;
|
||||
-- CREATE INDEX users_tenant_id_id ON users(tenant_id, id);
|
||||
|
||||
-- topics:租户隔离、群治理 Aux、租户维度唯一索引。
|
||||
-- 第六章群邀请、入群审批、群公告、群治理配置、成员禁言目前存放在 topics.aux JSON。
|
||||
-- 后续若要强状态机和审计,应拆 im_group_join_requests、im_group_announcements、im_group_mutes 等表。
|
||||
-- ALTER TABLE topics ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 1 AFTER id;
|
||||
-- ALTER TABLE topics ADD COLUMN aux JSON AFTER tags;
|
||||
-- CREATE UNIQUE INDEX topics_tenant_name ON topics(tenant_id, name);
|
||||
-- CREATE INDEX topics_tenant_owner ON topics(tenant_id, owner);
|
||||
|
||||
-- subscriptions:租户字段和按租户查成员索引。
|
||||
-- ALTER TABLE subscriptions ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 1 AFTER id;
|
||||
-- CREATE INDEX subscriptions_tenant_userid ON subscriptions(tenant_id, userid);
|
||||
-- CREATE INDEX subscriptions_tenant_topic ON subscriptions(tenant_id, topic);
|
||||
-- CREATE INDEX subscriptions_userid_topic_deletedat ON subscriptions(userid, topic, deletedat);
|
||||
|
||||
-- messages:租户、客户端消息幂等、编辑撤回字段、搜索/文件授权需要的索引。
|
||||
-- ALTER TABLE messages ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 1 AFTER id;
|
||||
-- ALTER TABLE messages ADD COLUMN client_msg_id VARCHAR(64) AFTER `from`;
|
||||
-- ALTER TABLE messages ADD COLUMN version INT NOT NULL DEFAULT 1 AFTER client_msg_id;
|
||||
-- ALTER TABLE messages ADD COLUMN recalled_at DATETIME(3) AFTER version;
|
||||
-- ALTER TABLE messages ADD COLUMN recalled_by BIGINT AFTER recalled_at;
|
||||
-- CREATE INDEX messages_tenant_topic ON messages(tenant_id, topic);
|
||||
-- CREATE UNIQUE INDEX messages_sender_client_mid ON messages(tenant_id, `from`, client_msg_id);
|
||||
|
||||
-- dellog:用户侧删除过滤需要 tenant_id。
|
||||
-- ALTER TABLE dellog ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 1 AFTER id;
|
||||
-- CREATE INDEX dellog_tenant_topic ON dellog(tenant_id, topic);
|
||||
-- CREATE INDEX dellog_topic_deletedfor_low_hi ON dellog(topic, deletedfor, low, hi);
|
||||
|
||||
-- fileuploads:文件 ACL、租户隔离、ETag。
|
||||
-- ALTER TABLE fileuploads ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 1 AFTER id;
|
||||
-- ALTER TABLE fileuploads ADD COLUMN etag VARCHAR(128) AFTER size;
|
||||
-- CREATE INDEX fileuploads_tenant_status ON fileuploads(tenant_id, status);
|
||||
|
||||
-- filemsglinks:文件资源级 ACL 需要租户、topic 和 userid 引用。
|
||||
-- ALTER TABLE filemsglinks ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 1 AFTER id;
|
||||
-- ALTER TABLE filemsglinks ADD COLUMN topic CHAR(25) AFTER msgid;
|
||||
-- ALTER TABLE filemsglinks ADD COLUMN userid BIGINT AFTER topic;
|
||||
-- CREATE INDEX filemsglinks_tenant_fileid ON filemsglinks(tenant_id, fileid);
|
||||
|
||||
-- usertags/topictags/devices/auth/credentials 也已按租户增加 tenant_id;
|
||||
-- 若旧库缺字段,需要参照当前 server/db/mysql/schema.sql 逐项补齐。
|
||||
@@ -0,0 +1,898 @@
|
||||
# IM 系统设计文档
|
||||
|
||||
> 版本:v1.1<br>
|
||||
> 日期:2026-08-11<br>
|
||||
> 状态:后续开发基线<br>
|
||||
> 需求依据:[IM 产品原型需求文档](IM-产品原型需求文档.md)<br>
|
||||
> 关联文档:[IM 产品需求服务端改造分析报告](IM-产品需求服务端改造分析报告.md)、[IM 现有实现与新需求差异对照总结](IM-现有实现与新需求差异对照总结.md)、[IM 服务端多租户改造方案](IM-服务端多租户改造方案.md)
|
||||
|
||||
| 版本 | 日期 | 变更 |
|
||||
| --- | --- | --- |
|
||||
| v1.0 | 2026-08-11 | 建立总体架构和业务模块设计 |
|
||||
| v1.1 | 2026-08-11 | 冻结 MySQL 范围;补齐认证、配额、消息变更、好友/群真源、审核、事务、Outbox、搜索和测试设计 |
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
本文作为后续 IM 产品开发的系统设计指导文件,用于统一服务端、客户端和测试团队对产品架构、模块边界、数据模型、接口风格、权限规则、消息事件和开发顺序的理解。
|
||||
|
||||
本文不替代详细接口文档和数据库 DDL。后续每个业务模块进入开发前,应在本文定义的边界内补充独立的 API 文档、状态机文档、表结构 SQL 和测试用例。
|
||||
|
||||
本文是产品业务开发的设计基线。关联分析报告用于记录调研过程;表名、唯一约束、状态机或技术选择与本文冲突时,以本文为准。设计发生变化时,必须先修改本文并记录版本,再修改接口和代码。
|
||||
|
||||
## 2. 设计目标
|
||||
|
||||
系统面向企业内部沟通与协作场景,核心目标如下:
|
||||
|
||||
1. 支持企业码进入租户,所有账号、好友、群、消息、文件、组织和办公数据均按 `tenant_id` 强隔离。
|
||||
2. 复用现有 Tinode 实时通信内核,保留 WebSocket 消息、Topic、Subscription、回执、文件、Push 和 WebRTC 信令能力。
|
||||
3. 在实时内核外新增产品业务层,承载好友关系、群治理、收藏、搜索、组织、会议、日报、月报、实名、青少年、举报和反诈等业务。
|
||||
4. 服务端必须成为权限和状态机的唯一可信执行方,客户端只负责交互展示和本地体验优化。
|
||||
5. 所有敏感动作具备幂等、防重、审计和可测试的错误码。
|
||||
6. 支持 iOS、Android 多端一致同步,网络断开后可增量恢复。
|
||||
|
||||
### 2.1 首期技术范围
|
||||
|
||||
1. 产品版首期只支持 MySQL 8,共享服务、共享数据库,通过 `tenant_id` 隔离租户。
|
||||
2. PostgreSQL、MongoDB 和 RethinkDB 不在首期产品业务支持范围内。启用产品模块但 Adapter 未实现完整租户业务接口时,服务必须拒绝启动,不能静默退回无租户接口。
|
||||
3. 实时消息继续使用现有 Tinode WebSocket 协议;事务型产品业务使用 HTTP API。
|
||||
4. 搜索首期使用 MySQL,达到第 11.3 节定义的切换条件后再引入独立搜索引擎。
|
||||
|
||||
## 3. 总体架构
|
||||
|
||||
系统采用“实时通信内核 + 产品业务服务 + 异步事件”的分层架构。
|
||||
|
||||
```mermaid
|
||||
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. 多租户基础规则
|
||||
|
||||
多租户是本系统的底座,后续所有业务模块必须遵守:
|
||||
|
||||
1. 首次 `hi.tenant` 使用企业码解析租户,Session 固定 `TenantID`。
|
||||
2. Token、认证记录、设备、Push、文件和插件调用均携带服务端可信 `TenantID`。
|
||||
3. 普通业务 Store 只能通过租户作用域访问,禁止全局查用户、Topic、消息、文件。
|
||||
4. 新增租户业务表必须包含 `tenant_id BIGINT NOT NULL`。平台级表和租户根表 `im_tenant` 例外;例外表必须在 DDL 评审中明确说明作用域。
|
||||
5. 唯一索引优先使用 `(tenant_id, business_key)`。
|
||||
6. 子表外键优先使用 `(tenant_id, parent_id)` 复合约束。
|
||||
7. 跨租户通信首期禁止;搜索、加好友、入群、转发和文件访问均限制在当前租户。
|
||||
8. 越权访问统一返回 NotFound/Denied,不泄露目标对象是否存在于其他租户。
|
||||
|
||||
### 5.1 租户配置与配额
|
||||
|
||||
`im_tenant_config` 是租户配额的配置真源,首期使用以下字段:
|
||||
|
||||
| 字段 | 含义 | `0` 的含义 |
|
||||
| --- | --- | --- |
|
||||
| `max_users` | 租户有效用户数上限 | 不限制 |
|
||||
| `max_groups` | 租户有效群数上限 | 不限制 |
|
||||
| `max_group_members` | 单群有效成员数上限 | 不限制 |
|
||||
|
||||
配额必须由服务端在事务内强制执行:
|
||||
|
||||
1. 注册或启用用户时锁定租户配置记录,统计有效用户后再创建账号。
|
||||
2. 创建群时锁定租户配置记录,统计有效群后再创建 Topic 和群业务数据。
|
||||
3. 邀请、扫码入群或审批入群时锁定目标群业务记录,统计有效成员并校验 `max_group_members` 后再写成员关系和 Subscription。
|
||||
4. 配额检查和资源创建必须在同一事务中完成,禁止先查询、提交后再创建。
|
||||
5. 超限返回稳定错误码,并记录 `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/*` 前缀,按模块划分:
|
||||
|
||||
```text
|
||||
/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 通用规则:
|
||||
|
||||
1. HTTP 请求使用 `Authorization: Bearer <token>`;统一认证中间件校验 Token 后生成只读 Principal,并从中获取 `tenant_id` 和 `uid`。请求参数中的同名字段不参与授权。
|
||||
2. 写接口必须支持幂等,使用 `Idempotency-Key` 或业务唯一键。HTTP 幂等键作用域固定为 `tenant_id + uid + method + route + key`;相同键但请求体摘要不同返回冲突错误。
|
||||
3. 列表接口统一使用游标分页,不使用不稳定的大 offset。
|
||||
4. 错误响应使用稳定业务错误码,客户端不解析数据库错误。
|
||||
5. 敏感接口记录审计日志。
|
||||
|
||||
### 6.3 文件 API
|
||||
|
||||
继续复用现有文件上传下载入口,但必须满足:
|
||||
|
||||
1. 文件记录带 `tenant_id`。
|
||||
2. 上传路径使用 `tenants/{tenant_id}/yyyy/mm/{file_id}`。
|
||||
3. 下载前校验文件上传者或关联消息/Topic 读取权限。
|
||||
4. 群文件、收藏文件、举报证据和反馈附件复用同一文件基础设施,但业务用途单独记录。
|
||||
5. 客户端不可见内部 `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_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` | 编辑前内容、版本、操作者和变更原因 |
|
||||
|
||||
### 7.3 企业码与登录流程
|
||||
|
||||
1. 未认证客户端先通过 `hi.tenant` 提交企业码;服务端只按 `im_tenant.code` 查找启用租户并把 `TenantID` 固定到 Session。
|
||||
2. 企业不存在、已停用或不允许登录时返回统一企业不可用错误,不能暴露内部状态和数据库信息。
|
||||
3. 后续注册、登录、验证码和找回密码全部使用该服务端租户上下文;不接受客户端在业务请求中重新指定租户。
|
||||
4. HTTP 场景使用 `/v1/im/tenants/resolve` 完成企业码解析,只返回公开品牌信息和服务地址。首期不设计 `tenant_ticket`;注册、登录等未认证请求由服务端根据企业码重新解析租户,认证成功后只信任正式用户 Token。
|
||||
5. 用户 Token 必须包含可信 `tenant_id`、`uid`、`auth_version`、签发时间和过期时间。Token 租户与当前接入租户不一致时拒绝认证。
|
||||
|
||||
### 7.4 验证码、密码重置和二次验证
|
||||
|
||||
验证码按 `register`、`login`、`reset_password`、`sensitive_action` 用途隔离,相同验证码不能跨用途使用。数据库只保存验证码强哈希和手机号规范化哈希,明文只在发送时短暂存在。
|
||||
|
||||
密码重置必须在同一事务中完成:
|
||||
|
||||
1. 消费有效验证码或二次验证挑战。
|
||||
2. 更新密码强哈希和密码更新时间。
|
||||
3. 递增 `im_user_security.auth_version`。
|
||||
4. 撤销该用户现有 `im_login_sessions`。
|
||||
5. 写安全审计和 Outbox 通知。
|
||||
|
||||
二次验证成功后签发一次性操作票据。票据必须绑定 `tenant_id + uid + action + target + expires_at`,只保存票据哈希,成功使用后立即失效。青少年模式独立密码不与登录密码共用,哈希保存在 `im_youth_mode_settings`。
|
||||
|
||||
### 7.5 认证安全约束
|
||||
|
||||
1. 用户名和手机号唯一约束均包含 `tenant_id`。
|
||||
2. 验证码发送和验证按租户、手机号、IP、设备分别限频。
|
||||
3. 登录错误使用统一提示;服务端仍按账号、IP 和设备记录失败次数并执行指数退避或临时锁定。
|
||||
4. 新设备登录、密码修改、会话撤销和异常登录通过 Outbox 发送安全通知。
|
||||
5. 手机号、验证码、密码、票据、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 放入唯一键,避免客户端误把同一个发送请求投递到多个会话:
|
||||
|
||||
```text
|
||||
UNIQUE(tenant_id, from_uid, client_msg_id)
|
||||
```
|
||||
|
||||
断线重试时,如果服务端发现重复 `client_msg_id`,返回原消息的 Topic、`seq_id`、状态和响应摘要,不能再次插入消息。服务端生成的系统消息也必须生成稳定幂等 ID,并以 Outbox 事件 ID 作为去重来源。
|
||||
|
||||
### 8.3 发消息前策略校验
|
||||
|
||||
每条消息持久化前执行统一策略链:
|
||||
|
||||
1. Session 租户和 Topic 租户一致。
|
||||
2. 发送人是同租户有效用户。
|
||||
3. Topic 存在且未删除。
|
||||
4. Subscription 具备写权限。
|
||||
5. 单聊检查好友关系、黑名单、删除状态和隐私策略。
|
||||
6. 群聊检查禁言、全员禁言、群状态和成员身份。
|
||||
7. 文件消息检查文件归属、扫描状态和目标 Topic 权限。
|
||||
8. 青少年模式和反诈策略检查。
|
||||
9. 内容审核通过或进入待审核策略。
|
||||
|
||||
### 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` | 先发布,异步审核;命中后执行系统撤回并审计 | 租户明确允许的低风险内容 |
|
||||
|
||||
状态转换固定为:
|
||||
|
||||
```text
|
||||
pending_review -> approved -> published
|
||||
pending_review -> rejected
|
||||
published -> recalled_by_review
|
||||
```
|
||||
|
||||
客户端提交的审核状态一律忽略。审核 Worker 必须幂等,同一消息只能完成一次终态转换。
|
||||
|
||||
## 9. 好友与黑名单设计
|
||||
|
||||
好友关系是单聊产品状态的真源,不直接用 P2P Topic 是否存在表示好友。
|
||||
|
||||
### 9.1 好友申请与关系状态
|
||||
|
||||
```mermaid
|
||||
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 服务端规则
|
||||
|
||||
1. 申请、同意、拒绝、删除、拉黑必须幂等。
|
||||
2. 好友申请保存验证留言、来源、过期时间和处理人。
|
||||
3. 拉黑是单向行为,唯一约束为 `(tenant_id, blocker_id, blocked_id)`;被拉黑人发消息、通话、名片、查看资料时按策略阻断。
|
||||
4. 删除好友后历史消息默认保留,但双方不能继续发送好友消息。
|
||||
5. 好友资料字段由关系和隐私设置共同决定。
|
||||
6. 接受申请必须在一个事务内更新申请、创建好友关系、创建或激活 P2P Topic/Subscription,并写入 Outbox。
|
||||
7. 单聊策略按“双方账号有效 -> 好友关系有效 -> 任一方向黑名单 -> 隐私/青少年/风控”的顺序判定。
|
||||
|
||||
## 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。
|
||||
|
||||
收藏记录需要保存:
|
||||
|
||||
1. 租户、收藏人、来源 Topic、来源 SeqId、来源消息 ID。
|
||||
2. 消息类型、摘要、文件 ID、发送人、发送时间。
|
||||
3. 收藏时间、去重键和删除状态。
|
||||
4. 原消息删除后的可见策略。
|
||||
|
||||
### 11.2 转发
|
||||
|
||||
逐条转发和合并转发都必须重新执行目标会话权限和文件权限。
|
||||
|
||||
| 转发类型 | 规则 |
|
||||
| --- | --- |
|
||||
| 逐条转发 | 每条源消息生成一条新消息;保持顺序;每条使用新 `client_msg_id` |
|
||||
| 合并转发 | 生成不可变快照;限制条数、总大小和敏感字段;支持点击查看详情 |
|
||||
| 文件转发 | 源文件和目标 Topic 必须同租户;必要时创建新的引用关系 |
|
||||
|
||||
### 11.3 搜索
|
||||
|
||||
搜索分三类:
|
||||
|
||||
1. 用户和群搜索:使用租户内规范化 Tags,并执行隐私策略。
|
||||
2. 聊天记录搜索:`im_message_index` 保存 `tenant_id`、Topic、SeqId、消息类型、发送人、时间、规范化检索文本和资源元数据,返回前二次校验 ACL。
|
||||
3. 文件/图片/视频/链接搜索:依赖消息类型索引和文件元数据索引。
|
||||
|
||||
首期固定使用 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 通话状态
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> ringing: 发起呼叫
|
||||
ringing --> accepted: 接听
|
||||
ringing --> canceled: 发起方取消
|
||||
ringing --> rejected: 接收方拒绝
|
||||
ringing --> timeout: 超时
|
||||
ringing --> busy: 对方忙线
|
||||
accepted --> ended: 正常挂断
|
||||
accepted --> interrupted: 异常断开
|
||||
```
|
||||
|
||||
### 12.2 关键字段
|
||||
|
||||
`im_call_records` 至少包含:
|
||||
|
||||
```text
|
||||
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`,按隐私设置返回 |
|
||||
| 实名姓名、证件号 | 实名表加密存储,不进入 Public |
|
||||
| 群昵称 | `im_group_members`,按需投影到 Subscription Private |
|
||||
|
||||
### 14.2 隐私设置
|
||||
|
||||
隐私设置影响以下入口:
|
||||
|
||||
1. 账号/手机号/二维码/群聊添加好友。
|
||||
2. 个人主页字段展示。
|
||||
3. 在线状态和最后在线时间展示。
|
||||
4. 非好友消息和通话发起。
|
||||
|
||||
客户端可以隐藏入口,但最终阻断必须在服务端执行。
|
||||
|
||||
### 14.3 青少年模式
|
||||
|
||||
青少年模式状态保存在服务端,PIN 使用强哈希,不保存明文。
|
||||
|
||||
受限能力由租户策略决定,首期建议覆盖:
|
||||
|
||||
1. 添加好友。
|
||||
2. 加群和创建群。
|
||||
3. 文件传输。
|
||||
4. 音视频通话。
|
||||
5. 工作台外部链接。
|
||||
|
||||
### 14.4 会话与通知设置
|
||||
|
||||
`im_conversation_settings` 以 `(tenant_id, user_id, topic)` 唯一,保存置顶、免打扰、隐藏等用户侧设置。它不改变其他成员的会话状态,也不能替代 Topic ACL。
|
||||
|
||||
Push 发送前同时检查租户通知策略、用户全局通知设置、会话免打扰、青少年模式和系统权限登记状态。客户端系统通知权限只用于展示引导,服务端保存的是用户偏好和最近一次上报状态,不把客户端上报当作可靠授权依据。
|
||||
|
||||
## 15. 安全、实名、举报与反诈
|
||||
|
||||
### 15.1 实名认证
|
||||
|
||||
实名认证采用独立状态机:
|
||||
|
||||
```text
|
||||
none -> pending -> approved
|
||||
none -> pending -> rejected
|
||||
approved -> appeal_pending -> approved/rejected
|
||||
```
|
||||
|
||||
证件号、姓名等敏感字段加密存储,普通查询只返回脱敏状态。
|
||||
|
||||
### 15.2 反诈策略
|
||||
|
||||
反诈由“本地规则 + 外部风控插件”共同组成:
|
||||
|
||||
1. 本地规则用于稳定阻断和提示,例如陌生人、敏感词、频繁加好友、异常文件。
|
||||
2. 外部插件用于内容审核、风险评分和策略建议。
|
||||
3. 插件调用遵守第 8.5 节的三种审核模式、超时和降级规则,不能在 Topic Actor 中执行无界远程等待。
|
||||
4. 风险提示和二次确认结果写审计。
|
||||
|
||||
### 15.3 举报与反馈
|
||||
|
||||
举报和反馈都按工单处理。
|
||||
|
||||
| 类型 | 特点 |
|
||||
| --- | --- |
|
||||
| 举报 | 绑定被举报对象、会话、消息、原因、证据和处理状态 |
|
||||
| 意见反馈 | 绑定提交人、设备、版本、问题描述、附件和处理状态 |
|
||||
|
||||
举报证据应固化消息快照和文件引用,防止原消息删除后无法处理。
|
||||
|
||||
## 16. 系统消息与通知设计
|
||||
|
||||
需要产生异步副作用的业务状态变化,在同一数据库事务中写入 Outbox,由 Worker 生成系统消息、Push、索引或定时任务。没有异步副作用的更新不要求创建空事件。
|
||||
|
||||
### 16.1 系统消息格式
|
||||
|
||||
系统消息固定使用以下结构;业务模块只能登记 `event_type` 和参数 Schema,不能自行定义另一套消息信封:
|
||||
|
||||
```json
|
||||
{
|
||||
"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 权限判定顺序
|
||||
|
||||
```text
|
||||
租户状态 -> 用户状态 -> 业务角色 -> Topic ACL -> 产品策略 -> 风控策略
|
||||
```
|
||||
|
||||
任何一步失败,写操作不得继续。
|
||||
|
||||
## 18. 数据一致性与异步任务
|
||||
|
||||
### 18.1 事务边界
|
||||
|
||||
必须在同一事务中完成:
|
||||
|
||||
1. 好友申请处理和关系状态更新。
|
||||
2. 群审批通过和 Subscription 写入。
|
||||
3. 群主转让和权限更新。
|
||||
4. 消息保存和附件引用建立。
|
||||
5. 文件记录从 `uploaded` 切换为 `ready`、附件引用和业务索引写入。
|
||||
6. 举报提交和证据引用。
|
||||
|
||||
业务层不能在 Handler 中连续调用多个独立 Store 方法来模拟事务。`imstore` 必须提供按用例划分的事务方法,例如 `AcceptFriendRequest`、`ApproveGroupJoin`、`TransferGroupOwner` 和 `SaveMessageWithAttachments`,方法内部同时更新产品业务表、Tinode 核心表、审计记录和 Outbox。
|
||||
|
||||
对象存储不参与 MySQL 事务,文件采用以下状态机:
|
||||
|
||||
```text
|
||||
pending -> uploaded -> ready
|
||||
pending/uploaded -> failed
|
||||
uploaded(超时且无引用) -> orphaned -> deleted
|
||||
```
|
||||
|
||||
对象上传成功后,再在 MySQL 事务中把文件切换为 `ready` 并建立业务引用。若数据库事务失败,文件保持 `uploaded`,由带宽受控的租户级垃圾回收任务清理;消息只能引用 `ready` 文件。
|
||||
|
||||
### 18.2 Outbox
|
||||
|
||||
需要通知、Push、索引或审计的业务写操作,在事务内写入 `im_outbox_events`。异步 Worker 负责:
|
||||
|
||||
1. 发送系统消息。
|
||||
2. 推送离线通知。
|
||||
3. 更新搜索索引。
|
||||
4. 调用外部审核/反诈。
|
||||
5. 发送会议、日报、月报定时提醒。
|
||||
6. 记录可观测指标。
|
||||
|
||||
`im_outbox_events` 至少包含:
|
||||
|
||||
```text
|
||||
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 处理规则:
|
||||
|
||||
1. `dedupe_key` 在租户内唯一,生产事件和消费副作用都必须幂等。
|
||||
2. Worker 使用 `SELECT ... FOR UPDATE SKIP LOCKED` 分批领取事件,设置最大批量和租户公平调度,避免单个租户占满队列。
|
||||
3. 失败采用有上限的指数退避;超过上限进入 `dead_letter`,触发告警并支持人工重放。
|
||||
4. 系统消息通过集群安全的内部发布接口进入目标 Topic,不能直接假设 Outbox Worker 与目标 Topic 位于同一进程。
|
||||
5. 标记事件成功前必须确认目标副作用已获得稳定幂等结果;进程崩溃后的重复执行不得产生重复消息或 Push。
|
||||
6. 外部审核事件必须按第 8.5 节推进消息审核状态,不能绕过状态机直接广播。
|
||||
|
||||
## 19. API 与错误码规范
|
||||
|
||||
### 19.1 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {},
|
||||
"request_id": "..."
|
||||
}
|
||||
```
|
||||
|
||||
### 19.2 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"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 日志字段
|
||||
|
||||
认证后的请求日志至少包含:
|
||||
|
||||
```text
|
||||
request_id, tenant_id, uid, sid, topic, action, result, latency_ms
|
||||
```
|
||||
|
||||
手机号、证件号、Token、验证码、文件签名和设备 Push Token 不写明文日志。
|
||||
|
||||
### 20.2 审计事件
|
||||
|
||||
必须审计:
|
||||
|
||||
1. 租户、组织和管理员变更。
|
||||
2. 登录、密码修改、实名审核和二次验证。
|
||||
3. 好友删除、拉黑、黑名单解除。
|
||||
4. 群主转让、管理员变更、禁言、解散群。
|
||||
5. 文件下载、批量转发、收藏导出。
|
||||
6. 举报处理、反馈处理和反诈二次确认。
|
||||
7. 平台管理员代操作。
|
||||
|
||||
## 21. 开发阶段规划
|
||||
|
||||
### 阶段 0:基础底座收口
|
||||
|
||||
目标:让后续业务都站在正确地基上。
|
||||
|
||||
交付:
|
||||
|
||||
1. 多租户核心链路完成并通过双租户隔离测试。
|
||||
2. 消息 `client_msg_id` 幂等。
|
||||
3. 文件下载资源级授权。
|
||||
4. 系统消息结构和 Outbox 基础。
|
||||
5. 统一错误码和请求日志。
|
||||
6. 产品版明确只支持 MySQL 8,不支持的 Adapter 在启动时快速失败。
|
||||
7. `im_tenant_config` 三项配额在注册、建群和入群入口强制生效。
|
||||
8. 建立专用业务事务 Store,禁止跨多个 Store 调用拼接关键事务。
|
||||
|
||||
### 阶段 1:账号、资料、好友和黑名单
|
||||
|
||||
交付:
|
||||
|
||||
1. 注册、登录、找回密码、二次验证。
|
||||
2. `auth_version`、登录设备、Token 撤销和安全通知。
|
||||
3. 个人资料、会话设置和隐私设置。
|
||||
4. 好友申请、通讯录、主页、删除好友。
|
||||
5. 独立黑名单和单聊发送/通话策略。
|
||||
|
||||
### 阶段 2:群聊产品化
|
||||
|
||||
交付:
|
||||
|
||||
1. 群列表、创建群、群资料、群头像。
|
||||
2. 入群审批、邀请、群二维码。
|
||||
3. 群公告、管理员、群主转让。
|
||||
4. 全员禁言、成员禁言、群文件。
|
||||
5. 群业务真源与 Topic/Subscription/ACL 投影一致性检查。
|
||||
|
||||
### 阶段 3:消息增强、收藏、搜索和转发
|
||||
|
||||
交付:
|
||||
|
||||
1. 结构化消息类型。
|
||||
2. @成员、引用、编辑、撤回、批量删除及消息版本事件。
|
||||
3. 收藏分类和分享。
|
||||
4. 聊天记录、文件、图片、视频和链接搜索。
|
||||
5. 逐条转发和合并转发。
|
||||
6. `sync_block`、`async_hold` 和 `async_post_audit` 审核状态机。
|
||||
|
||||
### 阶段 4:通话和通知体验
|
||||
|
||||
交付:
|
||||
|
||||
1. `call_id` 和通话记录。
|
||||
2. 通话状态消息。
|
||||
3. 忙线、拒绝、未接、超时和异常断开。
|
||||
4. Push 文案、免打扰、锁屏隐私。
|
||||
|
||||
### 阶段 5:企业工作台
|
||||
|
||||
交付:
|
||||
|
||||
1. 工作台应用配置。
|
||||
2. 组织架构。
|
||||
3. 会议通知和参会确认。
|
||||
4. 日报、月报。
|
||||
5. 总经理信箱。
|
||||
|
||||
### 阶段 6:安全合规
|
||||
|
||||
交付:
|
||||
|
||||
1. 实名认证。
|
||||
2. 青少年模式。
|
||||
3. 反诈策略。
|
||||
4. 举报和意见反馈。
|
||||
5. 审计后台和处置流程。
|
||||
|
||||
## 22. 测试策略
|
||||
|
||||
### 22.1 自动化测试
|
||||
|
||||
每个模块至少包含:
|
||||
|
||||
1. 单元测试:状态机、权限判断、错误码。
|
||||
2. Store 测试:租户隔离、唯一约束、分页、软删除。
|
||||
3. API 测试:参数校验、幂等、越权、并发。
|
||||
4. WebSocket 测试:消息顺序、回执、离线重连、系统消息。
|
||||
5. 安全测试:跨租户、跨用户、跨群、文件越权和枚举。
|
||||
6. 事务测试:在每个数据库写入点注入失败,验证业务真源、通信投影、审计和 Outbox 不出现部分提交。
|
||||
7. Worker 测试:重复消费、锁超时、进程崩溃、指数退避、死信和人工重放。
|
||||
8. Schema 测试:MySQL 初始化 DDL、升级 DDL 和 Adapter SQL 保持一致,唯一约束与外键真实生效。
|
||||
9. 性能测试:大群广播、并发发消息、重连风暴、文件上传下载、中文搜索和 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. 开发约束
|
||||
|
||||
1. 新业务表一律以 `im_` 前缀命名。
|
||||
2. 所有租户业务表必须带 `tenant_id`;平台级表和 `im_tenant` 等根表例外,例外必须经过 DDL 评审。
|
||||
3. 新接口不使用无租户 Store。
|
||||
4. 不在 Topic Actor 中执行慢查询、外部 HTTP 或复杂报表统计。
|
||||
5. 不把敏感字段放入 User Public、Topic Public、普通消息 JSON 或日志。
|
||||
6. 新增消息类型必须先定义 schema,再实现客户端渲染。
|
||||
7. 所有危险操作必须有二次确认、幂等和审计。
|
||||
8. 所有批量操作必须限制数量并返回逐项结果。
|
||||
9. 所有列表必须分页。
|
||||
10. 所有外部服务调用必须有超时、重试上限和降级策略。
|
||||
11. 关键业务状态变化必须通过专用事务 Store 完成,不允许 Handler 直接修改 Topic ACL 绕过业务真源。
|
||||
12. 产品模块只允许在 `TenantBusinessReady()` 为真且数据库版本满足要求时启动。
|
||||
|
||||
## 24. 待产品确认事项
|
||||
|
||||
以下问题会影响接口和表结构,开发前需要产品或业务方确认:
|
||||
|
||||
对应阶段冻结接口和 DDL 前必须确认本节事项。在产品未给出结论时,可以按本文其他章节声明的默认行为开发内部能力,但不得将未确认交互作为正式验收口径。
|
||||
|
||||
1. 删除好友后是否同时删除双方会话,还是仅禁止继续发送。
|
||||
2. 拉黑后是否允许被拉黑人看到历史消息和个人资料。
|
||||
3. 群聊已读回执是否展示具体已读成员,是否只对管理员开放。
|
||||
4. 消息撤回时限和管理员撤回范围。
|
||||
5. 合并转发是否允许包含已撤回、已删除或涉敏消息。
|
||||
6. 群文件是否等同于群内所有文件消息,还是独立上传目录。
|
||||
7. 实名认证是否强制,未实名能否继续登录、聊天、加好友、入群。
|
||||
8. 青少年模式限制范围和默认策略。
|
||||
9. 总经理信箱是否匿名,以及哪些角色可以查看真实提交人。
|
||||
10. 日报/月报是否支持草稿、补交、撤回和修改。
|
||||
|
||||
## 25. 总结
|
||||
|
||||
后续开发应以现有 Tinode 服务端作为实时通信内核,在其外建设清晰的产品业务层。核心原则是:聊天实时链路保持轻,业务状态机走 HTTP 和数据库事务,状态变化通过系统消息和 Push 回到聊天体验中。
|
||||
|
||||
多租户、消息幂等、文件授权、系统消息和审计是所有功能之前的基础工程。好友、群治理、收藏搜索、通话、工作台和安全合规应按阶段推进,每个阶段都必须同时完成数据模型、接口、权限、通知和测试闭环。
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 108 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 138 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 717 B |
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
Reference in New Issue
Block a user