feat: support tenant database routing

This commit is contained in:
richjohnrambo
2026-08-13 15:07:06 +08:00
parent 31f82d9c41
commit 92d0066a02
8 changed files with 2253 additions and 1317 deletions
+9
View File
@@ -1,3 +1,12 @@
/.github/
/templ/
/templ/
# macOS metadata.
.DS_Store
# IDE workspace files.
.idea/
# Local Go build output.
/server/server
+845
View File
@@ -0,0 +1,845 @@
# IM 后台需求文档
> 编写日期:2026-08-12
> 依据文档:`IM-产品需求服务端改造分析报告.md`、`IM-服务端多租户改造方案.md`、`IM-多租户第5至12章代码改造与测试方案.md`、`IM-前后端对接文档.md`、`IM-系统设计文档.md`
> 目标读者:后台产品、后端、管理端前端、测试
## 1. 文档目的
本文从现有 IM 产品与服务端设计文档中提取“后台管理端”需求,用于后续后台系统开发。本文只描述后台能力,不重复普通移动端聊天、通讯录、消息收发等客户端功能。
后台系统分为两类:
| 后台 | 使用对象 | 数据范围 |
| --- | --- | --- |
| 平台运营后台 | 平台管理员、运维、安全负责人 | 可管理全部租户,但跨租户操作必须二次验证和审计 |
| 企业管理后台 | 租户负责人、企业管理员、安全审计员、组织管理员 | 只能管理当前租户内的数据 |
所有后台能力必须建立在多租户隔离之上:后台 Token 必须包含可信 `tenant_id`、`uid`、角色、权限版本;平台后台的跨租户操作必须走独立后台 API,禁止通过普通 WebSocket `AsUser` 任意代操作用户。
## 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`。
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` | 企业说明 |
| `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}` | 进入删除流程 |
验收点:
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. 注册、创建群、入群审批必须在事务内校验配额。
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 不写日志。
### 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、二维码、邀请和文件新增权限。
### 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. 外部服务配置修改。
### 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/*`。
成功响应:
```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. 租户业务表必须包含 `tenant_id BIGINT NOT NULL`。
3. 唯一索引优先使用 `(tenant_id, business_key)`。
4. 子表外键优先使用 `(tenant_id, parent_id)`。
5. 更新、删除、聚合、清理任务必须带 `tenant_id`。
6. 平台级表和租户根表例外,但必须在 DDL 评审中说明作用域。
## 11. 开发优先级建议
### P0:后台地基
1. `/admin/v1` 认证中间件、RBAC、租户 Principal。
2. 平台租户管理:创建、停用、恢复、配置配额。
3. 企业管理员与角色权限。
4. 用户管理:停用、重置密码、撤销 Session。
5. 审计日志基础表和查询。
6. Outbox 查询与重试基础能力。
### P1:企业治理
1. 组织架构管理。
2. 群治理后台:群列表、群成员、管理员、禁言、解散、群文件。
3. 文件策略、文件列表、文件审计。
4. 外部服务配置:Push、短信、对象存储、TURN。
5. 功能开关和配额用量面板。
### P2:办公与安全业务
1. 工作台配置。
2. 会议管理。
3. 日报、月报统计和提醒。
4. 总经理信箱。
5. 实名认证审核。
6. 青少年策略。
7. 反诈、内容审核、举报、反馈工单。
## 12. 验收清单
- [ ] 平台后台与企业后台权限边界清晰,跨租户操作必须审计。
- [ ] 所有后台列表、详情、更新、删除都按租户隔离。
- [ ] 租户停用后普通用户登录和 HTTP 产品接口拒绝。
- [ ] 用户停用、重置密码、撤销 Token 不影响其他租户同手机号用户。
- [ ] 创建用户、建群、入群审批配额在事务内强制执行。
- [ ] 群主转让、禁言、管理员变更、解散群写审计并同步系统消息。
- [ ] 实名、举报、反馈、信箱等敏感数据脱敏展示并按权限读取。
- [ ] 文件下载、删除、导出均有资源级授权和审计。
- [ ] 外部服务密钥只保存引用,不写普通日志。
- [ ] Outbox 事件可查询、重试、死信处理,重复执行不产生重复副作用。
- [ ] 审计表只允许追加,普通管理员不可修改或删除。
@@ -1,5 +1,9 @@
# 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 章“企业工作台与办公协同”
+313
View File
@@ -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`;如后续需要升级老库,应单独重写迁移链。
@@ -1,3 +1,14 @@
-- DEPRECATED.
--
-- 2026-08-13: the multi-tenant technical direction changed to
-- "control database + one business database per tenant". Do not execute this
-- shared-table tenant_id migration and do not continue adding tenant_id to
-- tenant business tables.
--
-- New design document:
-- docs/IM-独立库多租户改造方案.md
--
-- Original note:
-- MySQL multi-tenant schema migration draft.
-- REVIEW ONLY: do not execute before the application is fully tenant-aware.
-- Target: MySQL 8.0, final schema version 119.
+180 -606
View File
File diff suppressed because it is too large Load Diff
+17 -508
View File
@@ -1,512 +1,21 @@
# THIS SCHEMA FILE IS FOR REFERENCE/DOCUMENTATION ONLY!
# THIS SCHEMA FILE IS FOR REFERENCE/DOCUMENTATION ONLY.
# DO NOT USE IT TO INITIALIZE THE DATABASE.
# Read installation instructions first.
# The following line will produce an intentional error.
'READ INSTALLATION INSTRUCTIONS!';
# The actual schema is below.
DROP DATABASE IF EXISTS tinode;
CREATE DATABASE tinode CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
USE tinode;
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 INDEX uk_im_tenant_code(code)
);
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 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
);
CREATE TABLE 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
);
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);
CREATE TABLE kvmeta(
`key` VARCHAR(64),
createdat DATETIME(3),
`value` TEXT,
PRIMARY KEY(`key`),
INDEX kvmeta_createdat_key(createdat, `key`)
);
INSERT INTO kvmeta(`key`, `value`) VALUES("version", "123");
CREATE TABLE users(
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL DEFAULT 1,
createdat DATETIME(3) NOT NULL,
updatedat DATETIME(3) NOT NULL,
state SMALLINT NOT NULL DEFAULT 0,
stateat DATETIME(3),
access JSON,
lastseen DATETIME,
useragent VARCHAR(255) DEFAULT '',
public JSON,
trusted JSON,
tags JSON, -- Denormalized array of tags
PRIMARY KEY(id),
INDEX users_tenant_id_id(tenant_id, id),
INDEX users_state_stateat(state, stateat),
INDEX users_lastseen_updatedat(lastseen, updatedat)
);
CREATE TABLE 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
);
CREATE TABLE 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
);
CREATE TABLE 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
);
CREATE TABLE 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
);
CREATE TABLE 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
);
CREATE TABLE 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),
UNIQUE INDEX uk_im_friend_requests_idempotency(tenant_id, applicant_id, idempotency_key),
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
);
CREATE TABLE 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
);
CREATE TABLE 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
);
CREATE TABLE 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),
UNIQUE INDEX uk_im_favorites_idempotency(tenant_id, user_id, idempotency_key),
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
);
# Indexed user tags.
CREATE TABLE usertags(
id INT NOT NULL AUTO_INCREMENT,
tenant_id BIGINT NOT NULL DEFAULT 1,
userid BIGINT NOT NULL,
tag VARCHAR(96) NOT NULL,
PRIMARY KEY(id),
FOREIGN KEY(userid) REFERENCES users(id),
INDEX usertags_tag(tag),
INDEX usertags_tenant_tag(tenant_id, tag),
UNIQUE INDEX usertags_userid_tag(userid, tag)
);
# Indexed devices. Normalized into a separate table.
CREATE TABLE devices(
id INT NOT NULL AUTO_INCREMENT,
tenant_id BIGINT NOT NULL DEFAULT 1,
userid BIGINT NOT NULL,
hash CHAR(16) NOT NULL,
deviceid TEXT NOT NULL,
platform VARCHAR(32),
lastseen DATETIME NOT NULL,
lang VARCHAR(8),
PRIMARY KEY(id),
FOREIGN KEY(userid) REFERENCES users(id),
INDEX devices_tenant_userid(tenant_id, userid),
UNIQUE INDEX devices_hash(hash)
);
# Authentication records for the basic authentication scheme.
CREATE TABLE auth(
id INT NOT NULL AUTO_INCREMENT,
tenant_id BIGINT NOT NULL DEFAULT 1,
uname VARCHAR(32) NOT NULL,
userid BIGINT NOT NULL,
scheme VARCHAR(16) NOT NULL,
authlvl SMALLINT NOT NULL,
secret VARCHAR(255) NOT NULL,
expires DATETIME,
PRIMARY KEY(id),
FOREIGN KEY(userid) REFERENCES users(id),
UNIQUE INDEX auth_tenant_uname(tenant_id, uname),
UNIQUE INDEX auth_userid_scheme(userid, scheme)
);
# Topics
CREATE TABLE topics(
id INT NOT NULL AUTO_INCREMENT,
tenant_id BIGINT NOT NULL DEFAULT 1,
createdat DATETIME(3) NOT NULL,
updatedat DATETIME(3) NOT NULL,
touchedat DATETIME(3),
state SMALLINT NOT NULL DEFAULT 0,
stateat DATETIME(3),
name CHAR(25) NOT NULL,
usebt TINYINT DEFAULT 0,
owner BIGINT NOT NULL DEFAULT 0,
access JSON,
seqid INT NOT NULL DEFAULT 0,
delid INT DEFAULT 0,
subcnt INT DEFAULT 0,
public JSON,
trusted JSON,
tags JSON, -- Denormalized array of tags
aux JSON,
PRIMARY KEY(id),
UNIQUE INDEX topics_tenant_name(tenant_id, name),
UNIQUE INDEX topics_name (name),
INDEX topics_owner(owner),
INDEX topics_tenant_owner(tenant_id, owner),
INDEX topics_state_stateat(state, stateat),
INDEX topics_name_state_seqid(name, state, seqid)
);
# Indexed topic tags.
CREATE TABLE topictags(
id INT NOT NULL AUTO_INCREMENT,
tenant_id BIGINT NOT NULL DEFAULT 1,
topic CHAR(25) NOT NULL,
tag VARCHAR(96) NOT NULL,
PRIMARY KEY(id),
FOREIGN KEY(topic) REFERENCES topics(name),
INDEX topictags_tag (tag),
INDEX topictags_tenant_tag(tenant_id, tag),
UNIQUE INDEX topictags_topic_tag(topic, tag)
);
# Subscriptions
CREATE TABLE subscriptions(
id INT NOT NULL AUTO_INCREMENT,
tenant_id BIGINT NOT NULL DEFAULT 1,
createdat DATETIME(3) NOT NULL,
updatedat DATETIME(3) NOT NULL,
deletedat DATETIME(3),
userid BIGINT NOT NULL,
topic CHAR(25) NOT NULL,
delid INT DEFAULT 0,
recvseqid INT DEFAULT 0,
readseqid INT DEFAULT 0,
modewant CHAR(8),
modegiven CHAR(8),
private JSON,
PRIMARY KEY(id) ,
FOREIGN KEY(userid) REFERENCES users(id),
INDEX subscriptions_tenant_userid(tenant_id, userid),
INDEX subscriptions_tenant_topic(tenant_id, topic),
UNIQUE INDEX subscriptions_topic_userid(topic, userid),
INDEX subscriptions_topic(topic),
INDEX subscriptions_deletedat(deletedat),
INDEX subscriptions_userid_topic_deletedat(userid, topic, deletedat)
);
# Messages
CREATE TABLE messages(
id INT NOT NULL AUTO_INCREMENT,
tenant_id BIGINT NOT NULL DEFAULT 1,
createdat DATETIME(3) NOT NULL,
updatedat DATETIME(3) NOT NULL,
deletedat DATETIME(3),
delid INT DEFAULT 0,
seqid INT NOT NULL,
topic CHAR(25) NOT NULL,
`from` BIGINT NOT NULL,
client_msg_id VARCHAR(64),
version INT NOT NULL DEFAULT 1,
recalled_at DATETIME(3),
recalled_by BIGINT,
head JSON,
content JSON,
PRIMARY KEY(id),
FOREIGN KEY(topic) REFERENCES topics(name),
INDEX messages_tenant_topic(tenant_id, topic),
UNIQUE INDEX messages_sender_client_mid(tenant_id, `from`, client_msg_id),
UNIQUE INDEX messages_topic_seqid (topic, seqid)
);
CREATE TABLE 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
);
# Deletion log
CREATE TABLE dellog(
id INT NOT NULL AUTO_INCREMENT,
tenant_id BIGINT NOT NULL DEFAULT 1,
topic CHAR(25) NOT NULL,
deletedfor BIGINT NOT NULL DEFAULT 0,
delid INT NOT NULL,
low INT NOT NULL,
hi INT NOT NULL,
PRIMARY KEY(id),
FOREIGN KEY(topic) REFERENCES topics(name),
INDEX dellog_tenant_topic(tenant_id, topic),
# For getting the list of deleted message ranges
INDEX dellog_topic_delid_deletedfor(topic,delid,deletedfor),
# Used when getting not-yet-deleted messages(messages LEFT JOIN dellog)
INDEX dellog_topic_deletedfor_low_hi(topic,deletedfor,low,hi),
# Used when deleting a user
INDEX dellog_deletedfor(deletedfor)
);
# User credentials
CREATE TABLE credentials(
id INT NOT NULL AUTO_INCREMENT,
tenant_id BIGINT NOT NULL DEFAULT 1,
createdat DATETIME(3) NOT NULL,
updatedat DATETIME(3) NOT NULL,
deletedat DATETIME(3),
method VARCHAR(16) NOT NULL,
value VARCHAR(128) NOT NULL,
synthetic VARCHAR(192) NOT NULL,
userid BIGINT NOT NULL,
resp VARCHAR(255) NOT NULL,
done TINYINT NOT NULL DEFAULT 0,
retries INT NOT NULL DEFAULT 0,
PRIMARY KEY(id),
FOREIGN KEY(userid) REFERENCES users(id),
UNIQUE credentials_tenant_uniqueness(tenant_id, synthetic)
);
# Records of uploaded files. Files themselves are stored elsewhere.
CREATE TABLE fileuploads(
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL DEFAULT 1,
createdat DATETIME(3) NOT NULL,
updatedat DATETIME(3) NOT NULL,
userid BIGINT,
status INT NOT NULL,
mimetype VARCHAR(255) NOT NULL,
size BIGINT NOT NULL,
location VARCHAR(2048) NOT NULL,
etag VARCHAR(128),
PRIMARY KEY(id),
INDEX fileuploads_tenant_status(tenant_id, status),
INDEX fileuploads_status(status)
);
# Links between uploaded files and messages or topics.
CREATE TABLE filemsglinks(
id INT NOT NULL AUTO_INCREMENT,
tenant_id BIGINT NOT NULL DEFAULT 1,
createdat DATETIME(3) NOT NULL,
fileid BIGINT NOT NULL,
msgid INT,
topic CHAR(25),
userid BIGINT,
PRIMARY KEY(id),
INDEX filemsglinks_tenant_fileid(tenant_id, fileid),
FOREIGN KEY(fileid) REFERENCES fileuploads(id) ON DELETE CASCADE,
FOREIGN KEY(msgid) REFERENCES messages(id) ON DELETE CASCADE,
FOREIGN KEY(topic) REFERENCES topics(name) ON DELETE CASCADE,
FOREIGN KEY(userid) REFERENCES users(id) ON DELETE CASCADE
);
# Current MySQL initialization is generated by server/db/mysql/adapter.go:
#
# - Control database: hotgo
# - im_tenant
# - im_tenant_config
#
# - Default tenant business database: tinode
# - users, topics, subscriptions, messages and extension tables
# - business tables do not store tenant_id in independent-database mode
#
# The previous shared-table tenant_id draft has been removed from this file to
# avoid accidental execution. If standalone SQL initialization scripts are
# needed later, split them explicitly into:
#
# - registry_schema.sql for the hotgo control database.
# - tenant_schema.sql for every tenant business database.
File diff suppressed because it is too large Load Diff