diff --git a/docs/backstage/IM-后台未实现功能清单.md b/docs/backstage/IM-后台未实现功能清单.md
new file mode 100644
index 0000000..6ab02ee
--- /dev/null
+++ b/docs/backstage/IM-后台未实现功能清单.md
@@ -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. 后台前端隐藏按钮只是体验优化,后端接口仍必须做权限和租户边界校验。
diff --git a/docs/backstage/IM-后台需求文档.md b/docs/backstage/IM-后台需求文档.md
new file mode 100644
index 0000000..bd4e598
--- /dev/null
+++ b/docs/backstage/IM-后台需求文档.md
@@ -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 事件可查询、重试、死信处理,重复执行不产生重复副作用。
+- [ ] 审计表只允许追加,普通管理员不可修改或删除。
diff --git a/docs/im/IM-产品原型需求文档.md b/docs/im/IM-产品原型需求文档.md
new file mode 100644
index 0000000..22a7c7c
--- /dev/null
+++ b/docs/im/IM-产品原型需求文档.md
@@ -0,0 +1,1287 @@
+# IM 产品原型需求文档
+
+> 本文档根据 `/Users/superysh/im/第二项目/` 中的 156 张产品原型图整理。功能按业务域归类,每张图以原文件名作为标题,图片下方给出功能说明与界面内容描述。
+
+## 产品总体定位
+
+该产品是一套面向企业内部沟通与协作的即时通讯软件。核心能力包括账号与身份认证、单聊与群聊、通讯录、文件与收藏、音视频通话、企业工作台、日报月报、安全风险提示以及个人设置。系统需要同时兼顾 iOS 和 Android 端的交互差异,并保证消息、权限、文件和通话状态可在多端间一致同步。
+
+## 1. 账号注册、登录与身份安全
+
+本组功能覆盖企业入口、账号密码登录、短信验证码注册、找回密码、身份校验、实名认证与独立密码。需求重点是明确的输入校验、验证码倒计时、错误反馈、协议确认与认证状态展示。
+
+### 1.1 企业码登录.png
+
+
+
+功能说明:
+
+提供企业租户入口。页面展示产品标识、企业码输入框和“进入企业”按钮;提交后应校验企业码是否存在、是否启用,并将用户导向对应企业的登录环境。
+
+### 1.2 登录页面.png
+
+
+
+功能说明:
+
+展示用户名/手机号和密码登录表单,支持密码显隐、忘记密码、注册跳转以及用户协议/隐私政策确认。登录按钮在必填信息和协议条件满足后才可有效提交。
+
+### 1.3 登录页面-密码错误.png
+
+
+
+功能说明:
+
+表现登录失败状态。当账号或密码错误时,在表单上方以 Toast 或明确错误文案告知“账号或密码错误”,保留当前账号并允许用户重新输入密码。
+
+### 1.4 注册页面.png
+
+
+
+功能说明:
+
+提供用户名/手机号、短信验证码、密码和确认密码输入。需支持获取验证码、密码显隐、两次密码一致性校验、已有账号返回登录以及协议勾选。
+
+### 1.5 注册页面-倒计时.png
+
+
+
+功能说明:
+
+展示验证码发送后的倒计时状态。倒计时期间禁止重复发送,归零后恢复“获取验证码”;后端需同时执行发送频控和验证码有效期校验。
+
+### 1.6 忘记密码.png
+
+
+
+功能说明:
+
+通过手机号、验证码、新密码和再次确认完成密码重置。应校验账号归属、验证码正确性及新密码规则,成功后应废弃旧令牌并引导用户重新登录。
+
+### 1.7 身份验证(安卓).png
+
+
+
+功能说明:
+
+用于敏感操作前的二次身份校验。用户输入手机号和短信验证码后确认;应对获取验证码、校验失败、超时和重试状态给出明确反馈。
+
+### 1.8 实名认证.png
+
+
+
+功能说明:
+
+提供姓名与证件号码输入,用于提交实名认证。页面需明确说明信息用途和隐私保护,提交前校验姓名及证件号格式,并防止重复提交。
+
+### 1.9 实名认证-已实名.png
+
+
+
+功能说明:
+
+展示认证通过后的只读状态,姓名和证件号做脱敏处理,提交按钮置灰并显示“已实名”。如支持更正信息,应进入独立的申诉审核流程,不允许在本页直接修改。
+
+### 1.10 实名认证(安卓).png
+
+
+
+功能说明:
+
+安卓端在消息首页上以弹窗提醒用户完成实名认证。弹窗应说明认证原因,提供“去实名认证”主操作,并根据平台策略控制是否允许暂后处理。
+
+### 1.11 设置独立密码(安卓).png
+
+
+
+功能说明:
+
+提供四位独立密码输入框,用于青少年模式或隐私功能的二次保护。应支持连续输入、错误次数限制、忘记密码处理和安全存储,不得以明文保存。
+
+## 2. 消息首页、全局导航与系统通知
+
+本组功能提供即时通讯的全局入口,包括会话列表、网络异常、快捷菜单、长按操作和系统消息。用户应能快速识别未读数、最新消息、联系人在线状态以及当前网络状态。
+
+### 2.1 消息.png
+
+
+
+功能说明:
+
+会话首页包含最近联系人、系统消息、文件助手和普通会话列表。每个会话项展示头像、名称、最新摘要、时间和未读数;页面提供搜索和右上角快捷功能入口。
+
+### 2.2 消息-无网络状态.png
+
+
+
+功能说明:
+
+当网络断开或重连时,顶部显示“加载中”及进度状态,会话缓存内容仍可查看。系统应自动重连,恢复后增量同步消息、未读数和回执,不应造成重复消息。
+
+### 2.3 消息-更多.png
+
+
+
+功能说明:
+
+右上角菜单提供“朋友”、“我的”、“创建群聊”、“添加好友”和“扫一扫”等快捷入口。菜单应与触发图标对齐,点击外部区域关闭,并根据用户权限显示可用项。
+
+### 2.4 消息页面—长按弹出.png
+
+
+
+功能说明:
+
+长按消息后弹出操作菜单,包括引用、复制、收藏、编辑、转发、撤回、删除、多选和群回执等操作。菜单项需根据消息类型、发送人、撤回时限和群权限动态裁剪。
+
+### 2.5 系统消息.png
+
+
+
+功能说明:
+
+系统消息页按时间展示新设备登录等安全通知,包含设备、IP 地址、时间和保障建议。此类消息应由可信系统账号发送,支持跳转安全设置,且不允许普通用户伪造。
+
+## 3. 单聊会话、会话建立与消息状态
+
+本组功能覆盖好友申请前后的单聊页、双方消息显示、已读/未读、删除、拉黑以及文件与名片消息。界面需能清楚区分“我的消息”与“对方消息”,并将发送、失败、已读、撤回等状态准确反映到消息气泡。
+
+### 3.1 个人对话框(对方的状态).png
+
+
+
+功能说明:
+
+展示对方发来的好友申请或首条消息,消息位于左侧并显示对方头像。页面应在尚未建立可聊权限时限制消息输入,并展示申请或通过状态。
+
+### 3.2 个人对话框(对方的状态)(1).png
+
+
+
+功能说明:
+
+展示对方在会话中的状态文案与单条对方消息。会话头部需展示昵称和在线状态,消息时间轴需保留系统提示,用于表达好友关系变化。
+
+### 3.3 个人对话框(我的状态).png
+
+
+
+功能说明:
+
+展示当前用户发出的好友申请或首条消息,气泡位于右侧并配置本人头像。发送成功后应展示业务状态文案,避免用户在等待审批时重复发起。
+
+### 3.4 个人对话框(我的状态)(1).png
+
+
+
+功能说明:
+
+表现我方已发送单条消息及其回执状态。消息气泡应在发送中、已发送、已送达、已读和失败之间平滑更新,且不因状态文字改变气泡主体位置。
+
+### 3.5 开启沟通页面.png
+
+
+
+功能说明:
+
+展示好友关系建立后的沟通开始页,包含系统提示“已成为好友”及首条正常消息。审批通过后应自动解锁输入区和媒体发送能力,同时保留申请上下文。
+
+### 3.6 双方沟通状态.png
+
+
+
+功能说明:
+
+展示双方在会话中发送语音、图片或视频消息时的状态,包含加载/上传进度与时长。媒体消息应使用稳定占位区,上传失败时支持重试,成功后可直接预览或播放。
+
+### 3.7 个人对话—拉黑状态/已读状态.png
+
+
+
+功能说明:
+
+展示消息已读回执以及因拉黑导致的发送失败提示。被拉黑后的消息应标记红色失败图标,不进入对方会话;界面需为用户提供可理解的失败原因。
+
+### 3.8 个人对话—删除状态.png
+
+
+
+功能说明:
+
+表现删除好友或关系解除后的发送失败。系统应阻止继续发送,将已失败消息保留为本地状态,并提示需重新添加好友或发起验证。
+
+### 3.9 个人对话-键盘收起状态.png
+
+
+
+功能说明:
+
+展示输入键盘和附加面板都收起时的标准单聊布局。底部保留语音、文本输入、表情和更多功能入口,且页面高度变化不应影响用户当前阅读位置。
+
+### 3.10 个人对话-发各种文件状态.png
+
+
+
+功能说明:
+
+展示图片、视频、音频和文件附加面板。用户选择类型后进入对应系统选择器或采集界面,应在上传前校验格式、大小和会话发送权限。
+
+### 3.11 个人对话-发各种文件状态(1).png
+
+
+
+功能说明:
+
+展示文本、名片和文件消息混合存在的会话效果。消息列表应根据消息类型使用不同卡片,但统一时间、头像、发送状态和长按操作规则。
+
+### 3.12 个人对话-发送名片状态.png
+
+
+
+功能说明:
+
+展示从通讯录选择用户并发送名片的结果。名片卡需包含头像、昵称和可识别账号信息,点击后进入用户主页;敏感字段不应在名片中直接暴露。
+
+## 4. 消息输入、媒体内容与消息操作
+
+本组功能覆盖键盘、表情、语音录制、图片/视频/文件发送、@ 提醒、多选、收藏和转发。发送链路需具备本地占位、上传进度、失败重试、消息去重和多端一致性。
+
+### 4.1 键盘展开.png
+
+
+
+功能说明:
+
+展示文本输入框获得焦点后的系统键盘状态。键盘弹出时消息列表应上推并保持最新消息可见,输入草稿在切换会话或应用前后台时应保留。
+
+### 4.2 @人员—选择提醒人.png
+
+
+
+功能说明:
+
+在群聊中输入 `@` 后打开成员选择页,提供“所有人”、成员列表、搜索和多选入口。列表需按拼音/字母分组,并根据群规则控制普通成员是否可使用 `@所有人`。
+
+### 4.3 @人员—完成.png
+
+
+
+功能说明:
+
+展示多个提醒人已勾选的状态,右上角“完成”提交选择。完成后应将昵称插入输入框,并在消息结构中同步保存被提醒用户 ID,避免仅依赖昵称匹配。
+
+### 4.4 @人员状态.png
+
+
+
+功能说明:
+
+展示群聊中已发送的 `@`消息,输入框上方可展示已选的提醒人。被提醒用户应获得特殊未读标记和推送文案,退群成员的历史提醒仍需正常渲染。
+
+### 4.5 表情包展开(发送按钮默认状态).png
+
+
+
+功能说明:
+
+表情面板展开但未选中内容时,发送按钮保持禁用状态。面板应支持表情分类、最近使用、删除键和文本输入并存,切换面板不应清空未发送内容。
+
+### 4.6 表情包发送页面.png
+
+
+
+功能说明:
+
+展示表情面板与消息列表联动状态。用户选中表情后“发送”按钮高亮,点击可作为文本表情或独立表情消息发送,消息应支持多端一致显示。
+
+### 4.7 语音—按住说话.png
+
+
+
+功能说明:
+
+切换至语音输入后,底部显示“按住说话”按钮。按下开始录音,松开发送,上滑取消;应处理麦克风权限被拒绝、录音太短、超时和来电打断。
+
+### 4.8 语音按住说话.png
+
+
+
+功能说明:
+
+展示按住录音时的浮层反馈,包含麦克风图标和“手指上滑,取消发送”指引。浮层应实时反映音量幅度,进入取消区时更换颜色和文案。
+
+### 4.9 内容沟通对话3-语音录音中.png
+
+
+
+功能说明:
+
+在群聊背景中展示语音正在录制的中央浮层。录制期间应阻止重复录音或发送其他媒体,并向话题成员发送可选的“正在录音”临时状态。
+
+### 4.10 内容沟通对话3-图片和语音发送中.png
+
+
+
+功能说明:
+
+展示图片与语音消息上传中的加载指示。每条消息应独立维护上传进度,并允许多个上传任务有序执行;离开会话后任务仍应可恢复或给出失败提示。
+
+### 4.11 内容沟通对话3-语音/视频/图片状态.png
+
+
+
+功能说明:
+
+展示群聊中语音时长卡片、视频缩略图和图片预览等多媒体消息。语音支持播放进度与未听标记,视频支持封面和播放按钮,图片支持大图预览。
+
+### 4.12 语音图片发送状态.png
+
+
+
+功能说明:
+
+表现单条图片正在上传、同时存在语音消息的会话状态。本地缩略图应立即显示,上传完成后无闪烁替换为远程资源,失败时提供点击重试。
+
+### 4.13 上传文件.png
+
+
+
+功能说明:
+
+该图为“上传”图标素材。系统在文件助手、群文件和意见反馈等入口应使用统一的上传语义和视觉图标,并配套可访问性文本。
+
+### 4.14 文件发送状态.png
+
+
+
+功能说明:
+
+会话中的文件卡片展示文件名、扩展类型图标、大小和发送状态。多个文件可连续发送,各自维护上传/下载状态;点击卡片进入文件详情或预览。
+
+### 4.15 多选-选中对话框.png
+
+
+
+功能说明:
+
+进入消息多选模式后,每条消息左侧显示选择框,底部显示转发、收藏、删除等批量操作。应支持跨屏滚动选择、显示已选数量,并根据内容类型校验批量操作可用性。
+
+### 4.16 多选-选中对话内容-收藏成功.png
+
+
+
+功能说明:
+
+批量收藏成功后以 Toast 明确反馈,并保留当前多选上下文或按交互约定退出多选。后端需防止同一条消息被重复收藏,并保留原会话、发送人和时间引用。
+
+### 4.17 转发-单选.png
+
+
+
+功能说明:
+
+转发目标页默认支持单选最近会话,包含搜索、文件助手和联系人列表。选中目标后直接进入发送确认,应避免将原消息的私有权限或失效附件错误带入新会话。
+
+### 4.18 转发-多选.png
+
+
+
+功能说明:
+
+切换到多选后可同时选择多个会话目标,页面以复选框显示选中状态。应限制一次转发的最大目标数,并在发送前展示目标汇总与风险确认。
+
+### 4.19 转发—逐条转发.png
+
+
+
+功能说明:
+
+逐条转发将选中的每条消息作为独立消息发送。底部确认面板展示接收人、附加留言和发送按钮;原始顺序、文件引用和消息类型需保持不变。
+
+### 4.20 转发—合并转发—多选状态.png
+
+
+
+功能说明:
+
+合并转发将多条消息封装成一张聊天记录卡片。确认面板展示多个接收人和留言输入;合并内容需保留发送人、时间和内容摘要,同时遵循删除和敏感信息脱敏规则。
+
+## 5. 通讯录、好友关系与黑名单
+
+本组功能覆盖组织通讯录、新好友、精准查找、二维码添加、申请验证、好友主页、备注、拉黑、删除和黑名单管理。好友关系应具备明确的申请、同意、拒绝、删除和拉黑状态机。
+
+### 5.1 通讯录.png
+
+
+
+功能说明:
+
+通讯录页提供搜索、新朋友、群聊、文件助手、系统消息和按字母分组的好友列表。应支持字母索引快速定位、备注名优先显示和企业组织成员同步。
+
+### 5.2 新朋友.png
+
+
+
+功能说明:
+
+新朋友列表展示申请人头像、昵称、验证留言、时间和状态,可执行“拒绝”或“通过验证”。已处理申请应显示已通过/已拒绝,后续刷新不得重复处理。
+
+### 5.3 添加好友.png
+
+
+
+功能说明:
+
+添加好友首页提供账号搜索、扫一扫和分享好友入口,同时可查看本人二维码。需对搜索可见性执行用户隐私设置,不可搜索的用户应返回统一的隐私保护结果。
+
+### 5.4 添加好友(1).png
+
+
+
+功能说明:
+
+展示个人二维码名片,包含头像、昵称、账号 ID、二维码、“分享好友”和“保存到相册”。二维码应包含带签名或有效期的用户标识,防止被篡改或长期滥用。
+
+### 5.5 添加好友-输入精准用户ID.png
+
+
+
+功能说明:
+
+输入精确手机号或用户 ID 后返回匹配用户,列表显示头像、昵称和“添加”按钮。应区分未找到、已是好友、已发送申请和不允许被搜索等结果。
+
+### 5.6 添加好友验证.png
+
+
+
+功能说明:
+
+在目标用户主页上弹出验证留言输入框,支持最多 50 字并提供取消/发送操作。验证消息应同申请状态一起保存,发送成功后按钮转为“已申请”。
+
+### 5.7 好友个人主页.png
+
+
+
+功能说明:
+
+好友主页展示头像、昵称、性别、地区、用户 ID、二维码和个性签名,提供发消息和音视频通话入口。展示字段需受隐私设置控制,备注名优先于原昵称显示。
+
+### 5.8 群聊—非好友个人主页.png
+
+
+
+功能说明:
+
+从群成员列表进入非好友主页,显示可公开的基本资料和“添加好友”按钮。不应展示手机号等私密数据;如群策略或用户隐私禁止通过群添加,则隐藏或禁用该按钮。
+
+### 5.9 好友—聊天设置—未拉黑状态.png
+
+
+
+功能说明:
+
+单聊设置包含查看聊天记录、消息保存方式、置顶、清空记录、备注、拉黑、投诉/举报和删除好友。未拉黑时开关关闭,删除好友以红色危险操作呈现。
+
+### 5.10 好友—聊天设置—拉黑状态.png
+
+
+
+功能说明:
+
+拉黑开关开启后,对方不得继续向当前用户发送消息或发起通话。拉黑状态需在多端同步,取消拉黑时恢复通信权限,但不自动恢复已删除的好友关系。
+
+### 5.11 删除好友提示.png
+
+
+
+功能说明:
+
+执行删除好友前弹出底部确认面板。该操作将解除好友关系并影响后续消息权限,应使用清晰的危险文案、取消和确认按钮;是否保留本地聊天记录需明确约定。
+
+### 5.12 黑名单.png
+
+
+
+功能说明:
+
+黑名单页展示已拉黑用户的头像、昵称和“移出”按钮。支持分页/滚动加载与批量解除;从黑名单移出后应即时同步聊天设置开关。
+
+### 5.13 黑名单提示(1).png
+
+
+
+功能说明:
+
+开启拉黑时弹出确认对话框,说明将不再收到对方消息。确认后更新黑名单和会话 ACL,取消则不改变任何状态;重复操作应具备幂等性。
+
+### 5.14 黑名单提示.png
+
+
+
+功能说明:
+
+原图文案为“清除聊天记录”确认,属于聊天设置的危险操作弹窗。确认后应仅清除当前用户可见的记录,不影响对方历史;文件名与画面语义不一致,实施时应以画面交互为准。
+
+## 6. 群聊创建、资料维护与成员治理
+
+### 6.1 群聊.png
+
+
+
+功能说明:
+
+群聊列表按“我创建的群、我管理的群、我加入的群”分类展示,列表项包含群头像、群名称及成员数量。页面提供搜索与创建群聊入口,切换分类时应保留各自的列表状态。
+
+### 6.2 空白页.png
+
+
+
+功能说明:
+
+当所选群聊分类没有数据时展示统一空状态,避免出现无内容的纯白页面。空状态应与当前分类绑定,后续加入或创建群聊后自动刷新。
+
+### 6.3 加入群聊.png
+
+
+
+功能说明:
+
+用户收到群邀请时,会话内展示包含群头像、群名称和邀请人的群聊卡片,并提供“立即加入”操作。加入前应校验群状态、邀请有效期及是否需要管理员审核,处理完成后更新卡片状态。
+
+### 6.4 内容沟通对话3-开启进群验证与拉人状态.png
+
+
+
+功能说明:
+
+开启进群验证后,群聊以系统消息展示成员邀请、申请通过和加入群聊等事件。系统消息需明确操作者、目标成员与处理结果,并保证所有群成员看到一致的时间线。
+
+### 6.5 邀请进群申请.png
+
+
+
+功能说明:
+
+进群申请列表展示申请人、邀请人、申请时间和当前状态,管理员可执行“拒绝”或“确定”。处理后按钮应切换为已同意/已拒绝等只读状态,并防止重复审批。
+
+### 6.6 新邀请进群申请.png
+
+
+
+功能说明:
+
+群聊顶部展示新的进群申请提醒,便于管理员从会话直接进入审批列表;聊天区同时保留申请处理结果。提醒数量应随审批操作实时更新,全部处理后自动消失。
+
+### 6.7 群公告状态.png
+
+
+
+功能说明:
+
+群聊顶部固定展示最新群公告摘要,点击可查看完整内容。新公告发布时会话中同步生成系统提示,并记录发布人和时间;公告更新后顶部摘要应立即替换。
+
+### 6.8 群公告状态/邀请进群申请同时存在.png
+
+
+
+功能说明:
+
+群公告和进群申请同时存在时,两个通知入口需要在群聊顶部有序展示,且不遮挡聊天内容。进群申请处理后只移除对应提醒,群公告继续保留。
+
+### 6.9 群公告(对方查看状态).png
+
+
+
+功能说明:
+
+普通群成员以只读形式查看群公告详情,页面展示公告正文、发布人和发布时间。成员无编辑入口,返回后可记录已读状态,用于控制会话中的新公告提示。
+
+### 6.10 群聊设置.png
+
+
+
+功能说明:
+
+群聊设置集中提供成员查看与邀请、群资料、群公告、聊天记录、群文件、置顶、消息免打扰、清空记录、举报和退出群聊等功能。菜单权限应依据群主、管理员和普通成员身份动态展示。
+
+### 6.11 群资料.png
+
+
+
+功能说明:
+
+群资料页展示并维护群头像、群名称和群简介。可编辑项需受角色权限控制,修改成功后应同步更新群列表、会话标题和群二维码等关联页面。
+
+### 6.12 设置群头像.png
+
+
+
+功能说明:
+
+修改群头像时从底部弹出“从相册选择”和“拍照”两种来源。选择或拍摄后应支持裁剪、上传进度、失败重试和格式/大小校验,取消操作不保留临时图片。
+
+### 6.13 设置昵称(1).png
+
+
+
+功能说明:
+
+该页面用于设置用户在当前群内的昵称,提供文本输入、字数计数和完成按钮。群昵称只影响当前群的成员列表和消息发送者名称,不应修改账号全局昵称。
+
+### 6.14 群简介.png
+
+
+
+功能说明:
+
+群简介采用多行文本编辑并展示最大字数限制。保存前需过滤全空白内容、校验长度和敏感内容,保存成功后返回群资料页并即时刷新。
+
+### 6.15 群标识.png
+
+
+
+功能说明:
+
+群聊消息发送者昵称旁展示“群主”“管理员”等身份标识,帮助成员识别管理角色。标识应随角色变更实时更新,并与普通消息文字保持清晰区分。
+
+### 6.16 群二维码页面.png
+
+
+
+功能说明:
+
+群二维码页以卡片形式展示群头像、群名称和可扫码加入的二维码,并提供分享或保存入口。二维码应包含有效期与群状态校验,过期或群已解散时不可继续入群。
+
+### 6.17 群组管理.png
+
+
+
+功能说明:
+
+群组管理包含进群验证、全员禁言、群文件上传权限、成员私聊权限、管理员设置、邀请管理、转让群主和解散群聊等配置。危险操作需二次确认,开关变化要记录操作者并同步到服务端。
+
+### 6.18 设置群管理员.png
+
+
+
+功能说明:
+
+页面说明管理员可协助管理群聊,并展示现有管理员列表及添加入口。群主可新增或移除管理员,系统应限制管理员数量且不得将群主本人作为普通管理员处理。
+
+### 6.19 添加管理员.png
+
+
+
+功能说明:
+
+从群成员中搜索并勾选要添加的管理员,底部/顶部操作区显示已选数量并提交。已是管理员、群主或不具备资格的成员应禁选,提交前需再次校验权限和数量上限。
+
+### 6.20 添加管理员-设置成功.png
+
+
+
+功能说明:
+
+管理员添加成功后返回管理员列表并展示成功提示,新管理员立即出现在列表中。角色变更应同步影响群聊身份标识和管理菜单权限。
+
+### 6.21 添加管理员-设置成功(1).png
+
+
+
+功能说明:
+
+该状态展示从成员选择页完成管理员设置后的结果反馈。提交期间应防止重复点击;部分成员设置失败时需给出明确原因,并仅回滚失败对象。
+
+### 6.22 删除管理员-设置成功.png
+
+
+
+功能说明:
+
+群主移除管理员后,页面展示设置成功提示并刷新管理员列表。被移除成员应立即失去管理权限,但仍保留其普通群成员身份。
+
+### 6.23 添加禁言成员.png
+
+
+
+功能说明:
+
+管理员可搜索并选择需要禁言的群成员,已禁言成员应禁选或明确标识。提交后服务端记录禁言状态,目标成员发送消息时需收到清晰的禁言提示。
+
+### 6.24 禁言的群成员.png
+
+
+
+功能说明:
+
+页面集中展示当前被禁言的成员,支持新增和解除禁言。列表需反映最新成员状态,成员退群或群角色变化时应同步清理无效记录。
+
+### 6.25 选择新群主.png
+
+
+
+功能说明:
+
+转让群主时从群成员列表中搜索并单选新群主。当前群主、已退出或受限账号不可选择,确认前需保证目标成员仍在群内且账号状态正常。
+
+### 6.26 选择新群主提示.png
+
+
+
+功能说明:
+
+选定新群主后弹出二次确认,明确转让对象及操作不可随意撤销。确认成功后双方权限即时互换,并在群聊中生成可追溯的系统消息。
+
+### 6.27 群文件.png
+
+
+
+功能说明:
+
+群文件页展示群内共享文件,可按名称搜索,并显示文件类型、名称、大小、上传者、时间及下载状态。文件可见性和删除/上传权限应遵循群组管理配置。
+
+### 6.28 群文件-上传状态.png
+
+
+
+功能说明:
+
+点击上传后提供文件、照片和视频等来源,选择完成后显示上传进度与结果。需限制单文件大小和格式,支持失败重试,并在上传成功后自动刷新群文件列表。
+
+## 7. 文件下载、收藏管理与聊天记录检索
+
+### 7.1 我的收藏.png
+
+
+
+功能说明:
+
+我的收藏汇总用户保存的聊天内容,支持按关键词搜索,并按全部、文本、图片、视频、语音、文件、链接等类型筛选。收藏项需展示来源会话、收藏时间和内容摘要。
+
+### 7.2 我的收藏(1).png
+
+
+
+功能说明:
+
+该状态展示收藏内容列表及多种内容形态,用户可进入详情或对收藏项执行转发、删除等操作。原消息被删除后,已成功收藏的内容仍应按收藏数据策略可访问。
+
+### 7.3 我的收藏-文件.png
+
+
+
+功能说明:
+
+文件分类仅展示已收藏的文件,列表包含文件名、类型、大小、来源和时间。点击后进入文件预览或下载页,失效文件需展示明确状态而非直接报错。
+
+### 7.4 我的收藏-文本.png
+
+
+
+功能说明:
+
+文本分类展示收藏的文字消息及其来源信息,支持长文本折叠、展开和复制。搜索结果应对匹配关键词进行高亮,并可跳转到收藏详情。
+
+### 7.5 我的收藏-语音.png
+
+
+
+功能说明:
+
+语音分类展示语音时长、来源和收藏时间,点击可播放或暂停。列表中同时只能有一条语音播放,离开页面或切换内容时应正确释放播放状态。
+
+### 7.6 我的收藏—链接.png
+
+
+
+功能说明:
+
+链接分类以卡片形式展示标题、摘要、缩略图和来源地址。打开外链前需进行安全校验,无法访问或被拦截时显示风险/失效提示。
+
+### 7.7 收藏—文件(多选).png
+
+
+
+功能说明:
+
+收藏列表进入多选模式后,可勾选多个文件并执行批量转发或删除。操作区需显示选择数量,跨页选择应保持状态,删除操作需要二次确认。
+
+### 7.8 收藏文件—分享给好友页面.png
+
+
+
+功能说明:
+
+分享收藏文件时进入会话选择页,可搜索联系人或群聊并选择接收方。发送前应展示文件和目标会话确认信息,成功后保留收藏原件并生成新的聊天消息。
+
+### 7.9 文件-下载.png
+
+
+
+功能说明:
+
+未下载文件展示名称、类型、大小及下载按钮。点击下载前需检查文件有效性、网络和本地空间,下载目标应使用受控存储路径。
+
+### 7.10 文件-下载中.png
+
+
+
+功能说明:
+
+下载中状态展示实时进度,并允许取消或在失败后重试。应用切换前后台、网络变化或页面重进时,应能恢复准确的下载任务状态。
+
+### 7.11 文件-下载完成.png
+
+
+
+功能说明:
+
+下载完成后提供打开或使用其他应用查看的入口,并显示本地可用状态。若本地文件被清理,页面应恢复为可下载状态,不能继续显示已完成。
+
+### 7.12 查找聊天记录-搜索关键词选中状态(点击后跳出对应聊天内容).png
+
+
+
+功能说明:
+
+聊天记录搜索支持输入关键词并展示命中的消息摘要、发送者和时间,关键词高亮显示。点击结果后返回会话并定位到对应消息,同时加载其上下文内容。
+
+### 7.13 查找聊天记录-文件.png
+
+
+
+功能说明:
+
+文件记录按时间列出会话中发送过的文件,包含文件名、大小、发送者和发送时间。用户可搜索、预览或下载文件,已撤回/失效项需明确标记。
+
+### 7.14 查找聊天记录-链接.png
+
+
+
+功能说明:
+
+链接记录聚合会话中的网页卡片和网址消息,展示标题、摘要与时间。点击时执行外链安全策略,并允许用户定位回原始消息。
+
+### 7.15 查看聊天记录-图片.png
+
+
+
+功能说明:
+
+图片记录以按时间分组的缩略图网格展示,支持多选和进入大图预览。缩略图需懒加载并保留原图加载状态,点击图片可定位其消息来源。
+
+### 7.16 查看聊天记录-视频.png
+
+
+
+功能说明:
+
+视频记录以缩略图展示时长、日期等信息,点击进入播放器。播放前需处理下载/缓冲、文件失效和网络异常,返回后保持原列表位置。
+
+## 8. 实时语音与视频通话
+
+### 8.1 各种通话提示状态.png
+
+
+
+功能说明:
+
+聊天时间线展示语音/视频通话的取消、拒绝、未接听、忙线和通话时长等结果消息。状态文案应由统一通话结果码生成,并在双方会话中保持语义一致。
+
+### 8.2 对方状态.png
+
+
+
+功能说明:
+
+收到视频通话邀请时,全屏展示来电人头像、昵称和通话类型,用户可接听或拒绝。锁屏、后台及系统权限受限时也应通过系统通知提供等价操作。
+
+### 8.3 语音通话-对方待接受状态.png
+
+
+
+功能说明:
+
+发起语音通话后进入等待接听状态,展示对方信息、呼叫提示和取消按钮。超时、对方忙线、拒绝或网络失败时应结束呼叫并生成对应会话记录。
+
+### 8.4 语音通话-双方语音沟通中.png
+
+
+
+功能说明:
+
+接通后展示实时通话时长及静音、扬声器和挂断控制。通话需持续反馈网络/音频状态,异常断开后准确记录实际通话时长。
+
+### 8.5 语音通话-麦克风与扬声器已关.png
+
+
+
+功能说明:
+
+麦克风与扬声器关闭时,控制按钮呈现明确的关闭状态,并即时停止本地采集或切回听筒。权限失效或设备切换时界面状态必须与真实音频路由一致。
+
+### 8.6 语音通话-麦克风与扬声器已开.png
+
+
+
+功能说明:
+
+麦克风与扬声器开启时显示选中状态,用户可在通话过程中独立切换。切换应即时生效,并处理蓝牙耳机、系统音量和音频焦点变化。
+
+### 8.7 视频通话—等待对方接受接受邀请中.png
+
+
+
+功能说明:
+
+视频呼叫等待页展示本地摄像头预览、对方信息及取消、切换摄像头、关闭摄像头等控制。正式接通前不应向对方传输非必要的视频数据。
+
+### 8.8 视频通话—等待对方接受接受邀请-摄像头关闭.png
+
+
+
+功能说明:
+
+等待接听期间关闭摄像头后,本地预览替换为头像/占位画面,摄像头按钮同步显示关闭状态。对方接听后应沿用当前设置,除非用户主动开启。
+
+### 8.9 双方视频通话中.png
+
+
+
+功能说明:
+
+视频通话接通后以对方画面为主、本地画面为小窗,展示时长和静音、摄像头切换、挂断等控制。小窗应支持拖动且不能遮挡关键按钮,横竖屏切换时保持画面比例。
+
+### 8.10 双方视频通话中-对方关闭摄像头状态.png
+
+
+
+功能说明:
+
+对方关闭摄像头时,主画面展示对方头像和状态提示,同时保留语音通话能力。对方重新开启后应自动恢复视频,不需要本端重新接听。
+
+### 8.11 视频与语音通话悬浮状态中.png
+
+
+
+功能说明:
+
+通话最小化后以可拖动悬浮窗继续展示视频画面或语音通话状态,用户可点击返回全屏通话。悬浮窗需遵循系统权限和安全区域,并在挂断后立即关闭。
+
+## 9. 企业工作台与办公协同
+
+### 9.1 工作台.png
+
+
+
+功能说明:
+
+工作台聚合组织内部的常用办公应用和业务入口,包括组织架构、日报/月报、会议通知、总经理信箱等模块。应用入口应根据企业配置和用户权限动态展示,并支持未读数量提示。
+
+### 9.2 部门组织框架.png
+
+
+
+功能说明:
+
+组织架构按部门层级展示企业成员,支持展开下级部门、查看成员和搜索。部门与人员数据应由企业通讯录统一维护,离职、调岗或组织调整后及时同步。
+
+### 9.3 会议通知-发起人状态.png
+
+
+
+功能说明:
+
+会议发起人可查看会议主题、时间、地点、参会人及各成员的确认状态,并执行提醒或管理操作。会议变更或取消后应向所有参会人推送更新,并保留操作记录。
+
+### 9.4 会议通知-接收人状态.png
+
+
+
+功能说明:
+
+会议接收人查看完整会议信息并反馈参加状态。响应结果需实时同步给发起人,会议开始前可按配置发送提醒,过期会议进入只读历史状态。
+
+### 9.5 日报.png
+
+
+
+功能说明:
+
+日报入口展示用户的日报记录,并提供新建或提交日报操作。列表应区分日期、提交状态和查看状态,支持按时间查找历史日报。
+
+### 9.6 日报-普通用户页面.png
+
+
+
+功能说明:
+
+普通用户页面以个人填报为主,展示已提交和待填写的日报,用户可查看自己的历史记录。超过允许修改期限后,已提交日报应转为只读或走补交/修改流程。
+
+### 9.7 日报-管理员页面.png
+
+
+
+功能说明:
+
+管理员页面汇总团队日报提交情况,可查看已提交、未提交及成员明细。统计口径需与组织权限一致,并支持提醒未提交成员和进入日报详情。
+
+### 9.8 日报-查看详情.png
+
+
+
+功能说明:
+
+日报详情展示填报人、日期、工作内容及相关附加信息。查看权限由组织关系决定,内容较长时支持完整滚动,已撤回或被修改的日报需标记最新状态。
+
+### 9.9 月报(1).png
+
+
+
+功能说明:
+
+月报填写页用于录入本月工作总结、下月计划等内容,并提供提交操作。输入项需具备必填和长度校验,可在正式提交前保存草稿,避免页面退出导致内容丢失。
+
+### 9.10 月报.png
+
+
+
+功能说明:
+
+月报列表按月份展示个人或权限范围内的月报记录,区分未提交、已提交等状态。用户可进入填写或详情页,管理员可查看团队提交进度。
+
+### 9.11 月报-详情页.png
+
+
+
+功能说明:
+
+月报详情页展示填报人、所属月份、工作总结和计划等完整信息。已提交内容默认只读,具备修改权限时需在规定周期内开放编辑并记录更新时间。
+
+### 9.12 总经理信箱.png
+
+
+
+功能说明:
+
+总经理信箱为员工提供向管理层提交意见或问题的入口,支持填写主题与正文并提交。内容应按企业隐私策略控制可见范围,提交成功后提供明确反馈,防止重复发送。
+
+### 9.13 总经理信箱-文案提示.png
+
+
+
+功能说明:
+
+页面通过提示文案说明信箱用途、处理方式及隐私边界。提示内容应由企业后台可配置,并在用户提交前清晰告知实名/匿名规则和信息接收方。
+
+## 10. 个人资料、通用设置、隐私与青少年模式
+
+### 10.1 我的.png
+
+
+
+功能说明:
+
+“我的”页面展示头像、昵称、账号信息,并提供个人资料、收藏、设置等个人功能入口。账号数据更新后应在此处即时同步,菜单可根据平台或企业策略动态配置。
+
+### 10.2 个人信息.png
+
+
+
+功能说明:
+
+个人信息页展示并编辑头像、昵称、出生日期、个性签名等资料,同时提供个人二维码入口。不同字段按各自规则校验,修改后同步到联系人资料和聊天界面。
+
+### 10.3 设置昵称.png
+
+
+
+功能说明:
+
+账号昵称编辑页提供单行输入、字数计数和完成按钮。昵称需校验长度、空白字符和敏感内容,保存成功后更新全局个人资料,但不覆盖用户在各群设置的群昵称。
+
+### 10.4 出生日期.png
+
+
+
+功能说明:
+
+出生日期通过日期选择器设置,用户确认后保存到个人资料。日期不得晚于当前日期,展示格式需适配本地化设置,隐私可见性遵循个人资料策略。
+
+### 10.5 个性签名.png
+
+
+
+功能说明:
+
+个性签名采用多行文本编辑并显示长度限制,支持保存和清空。保存前需处理全空白和敏感内容,修改后在个人主页等引用位置同步展示。
+
+### 10.6 二维码.png
+
+
+
+功能说明:
+
+该原图是个人二维码相关的视觉资源/入口标识,用于从个人资料进入二维码名片。实际页面应生成与当前账号绑定的可扫描二维码,并支持刷新失效凭证,避免静态码泄露长期有效信息。
+
+### 10.7 设置(iOS).png
+
+
+
+功能说明:
+
+iOS 设置页提供账号安全、消息通知、隐私、通用、关于与退出登录等入口。页面应遵循 iOS 导航和权限跳转规范,系统级配置需引导用户前往系统设置。
+
+### 10.8 设置(安卓).png
+
+
+
+功能说明:
+
+Android 设置页覆盖账号安全、通知、隐私、通用、关于及退出登录等功能。与 iOS 共享业务能力,但权限说明、系统设置路径和版本更新流程按 Android 平台实现。
+
+### 10.9 清空缓存的提示.png
+
+
+
+功能说明:
+
+清空缓存前弹出确认提示,说明将移除可重新下载的临时数据。操作不应删除账号、聊天记录或收藏等业务数据;完成后刷新缓存占用数值并提示清理结果。
+
+### 10.10 版本更新页面.png
+
+
+
+功能说明:
+
+版本更新页展示当前版本、最新版本和更新说明,并提供检查/立即更新入口。需区分可选更新与强制更新,下载、安装及应用商店跳转方式按操作系统能力处理。
+
+### 10.11 发现新版本.png
+
+
+
+功能说明:
+
+检测到新版本时弹出更新提示,展示版本号、更新内容和稍后/更新操作。强制更新不得提供绕过入口,普通更新关闭后应按策略控制再次提醒频率。
+
+### 10.12 隐私设置.png
+
+
+
+功能说明:
+
+隐私设置集中管理添加好友方式、黑名单及其他账号可见性选项。开关和选项修改应即时保存并在服务端生效,涉及联系人关系的设置需要跨设备同步。
+
+### 10.13 隐私设置-添加我的方式选项.png
+
+
+
+功能说明:
+
+用户可分别控制他人是否能通过账号、手机号、二维码或群聊等渠道添加自己。关闭某渠道后,相关搜索或添加入口应统一阻断,并向发起方展示不泄露隐私的提示。
+
+### 10.14 青少年模式未开启(安卓).png
+
+
+
+功能说明:
+
+未开启页面说明青少年模式的适用范围与限制,并提供开启入口。开启前需由监护人/用户确认并设置独立密码,相关规则由服务端配置并跨设备同步。
+
+### 10.15 青少年模式提示(安卓).png
+
+
+
+功能说明:
+
+开启青少年模式过程中展示确认提示,明确功能限制和密码用途。用户确认后进入密码设置或验证流程,取消则保持原模式不变。
+
+### 10.16 青少年模式已开启(安卓).png
+
+
+
+功能说明:
+
+已开启页面展示当前生效状态及关闭入口,受限功能应按策略隐藏或拦截。应用重启、重新登录或切换设备后仍需保持模式状态,不能仅依赖本地标记。
+
+### 10.17 关闭青少年模式(安卓).png
+
+
+
+功能说明:
+
+关闭青少年模式前要求输入独立密码进行验证。连续输错需触发频率限制或找回流程,验证成功后解除限制并清理敏感页面的输入状态。
+
+### 10.18 获取推送权限(安卓).png
+
+
+
+功能说明:
+
+应用通过说明页解释开启通知权限的用途,并引导用户进入系统授权。拒绝后不应反复打扰,可在消息通知设置中再次触发引导;最终状态以系统权限为准。
+
+### 10.19 提醒(安卓).png
+
+
+
+功能说明:
+
+Android 系统提醒弹窗用于请求必要权限或告知受限能力,提供取消与前往设置操作。提示只在功能确实受权限影响时出现,并根据系统版本显示正确路径。
+
+### 10.20 相机说明(安卓).png
+
+
+
+功能说明:
+
+首次使用拍照、扫码或视频通话前说明相机权限用途,再触发系统授权。拒绝时保留替代入口;永久拒绝后引导至系统设置,不应循环弹出系统权限框。
+
+### 10.21 麦克风说明(安卓).png
+
+
+
+功能说明:
+
+首次发送语音或发起通话前说明麦克风权限用途,并在用户确认后请求系统授权。权限缺失时禁止开始录音,同时提供清晰的授权恢复路径。
+
+## 11. 反诈安全、举报与意见反馈
+
+### 11.1 反诈弹窗(安卓).png
+
+
+
+功能说明:
+
+在疑似风险会话或敏感操作前展示反诈提醒,提示用户警惕陌生人、转账和隐私信息风险。弹窗触发规则应由风控策略控制,并避免泄露具体判定逻辑。
+
+### 11.2 反诈弹窗(二次提示安卓).png
+
+
+
+功能说明:
+
+高风险场景继续操作时展示二次确认,强化风险文案并要求用户明确选择。确认/取消结果应记录用于审计和策略评估,必要时可直接阻断操作而非仅提示。
+
+### 11.3 举报.png
+
+
+
+功能说明:
+
+举报页面提供色情、诈骗、骚扰等举报原因选项,用户选择后进入补充材料流程或直接提交。举报对象、会话上下文和选择原因需随请求传递,并保护举报人身份。
+
+### 11.4 举报(1).png
+
+
+
+功能说明:
+
+举报补充页支持填写问题描述并上传图片等证据,提交前需校验必填项、文件数量、格式和大小。提交成功后返回受理结果,重复举报应按平台规则合并或限流。
+
+### 11.5 意见反馈.png
+
+
+
+功能说明:
+
+意见反馈页支持输入问题或建议、上传截图,并可按产品设计提供联系方式。提交时附带必要的应用版本和设备信息,敏感日志需征得用户同意;成功后展示反馈已受理状态。
diff --git a/docs/im/IM-产品需求服务端改造分析报告.md b/docs/im/IM-产品需求服务端改造分析报告.md
new file mode 100644
index 0000000..53aa63d
--- /dev/null
+++ b/docs/im/IM-产品需求服务端改造分析报告.md
@@ -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 能力,也能避免把大量办公和审批逻辑耦合进消息路由主链路。
diff --git a/docs/im/IM-前后端对接文档.md b/docs/im/IM-前后端对接文档.md
new file mode 100644
index 0000000..b416f68
--- /dev/null
+++ b/docs/im/IM-前后端对接文档.md
@@ -0,0 +1,1965 @@
+# IM 前后端逐图对接协议
+
+> 版本:v2.0
+> 日期:2026-08-11
+> 需求依据:[IM 产品原型需求文档](IM-产品原型需求文档.md)
+> 设计依据:[IM 系统设计文档](IM-系统设计文档.md)
+> 说明:第 1 至第 11 章的章节编号、图片标题和顺序与产品原型完全一致。每张图均给出可直接开发、联调和验收的协议。
+
+## 0. 公共对接约定
+
+### 0.1 实现状态
+
+| 状态 | 含义 |
+| --- | --- |
+| `已实现` | 当前服务端已有能力,可按本文联调 |
+| `需改造` | 已有基础能力,但字段、鉴权或业务规则需要调整 |
+| `新增` | 当前服务端没有该产品接口或数据模型 |
+| `纯客户端` | 界面或系统能力,不调用服务端;关联的提交协议会单独指出 |
+
+文档中所有 `新增`、`需改造` 接口都是目标契约,不代表当前代码已经可用。前后端必须以本文字段为准实施,接口完成前可使用同结构 Mock。
+
+### 0.2 API Key、启动入口与通道
+
+#### 0.2.1 现有系统的 API Key 从哪里来
+
+**API Key 不是客户端启动后向 IM 服务端申请得到的值。现有代码没有“获取 API Key”的 HTTP 或 WebSocket 接口。**
+
+API Key 是应用级接入标识,由部署人员在发布客户端前生成并配置:
+
+1. 新部署同时生成 API Key 和 HMAC Salt:在项目根目录执行 `go run ./keygen`。
+2. 已有服务端继续使用当前 `api_key_salt` 时,执行 `go run ./keygen -sequence <新序号> -salt '<服务端 api_key_salt>'` 生成兼容的普通应用 Key。
+3. 输出中的 `HMAC salt` 配置到服务端 `server/tinode.conf` 的 `api_key_salt`;输出中的 `API key` 配置到 iOS、Android 或 Web 客户端构建配置/受控环境配置。
+4. 客户端启动时从自己的应用配置读取 API Key,不调用 IM 接口获取。禁止把 `-isroot 1` 生成的 Root Key 放进客户端。
+
+生成示意:
+
+```text
+$ go run ./keygen
+API key v1 seq1 [ordinary]:
+HMAC salt:
+```
+
+API Key、企业码和用户 Token 是三个不同概念:
+
+| 值 | 取得时间 | 作用 | 是否代表用户身份 |
+| --- | --- | --- | --- |
+| `api_key` | 应用发布/环境配置时预置 | 允许应用访问 Tinode 入口 | 否 |
+| `tenant_code` | 用户输入企业码 | 将 Session 路由并绑定到企业 | 否 |
+| `token` | 用户登录成功后由服务端返回 | 标识当前租户内的登录用户 | 是 |
+
+API Key 可以被客户端应用提取,因此不能把它当成用户密码或租户授权。现有 `checkAPIKey` 只校验 Key 的格式和 HMAC 签名;最终权限仍必须依赖租户 Session、用户 Token 和 Topic/业务 ACL。
+
+当前 `checkAPIKey` 没有按 `sequence` 吊销旧 Key 的逻辑。生成新序号不会自动让旧 Key 失效;若需要精细轮换,服务端必须增加最低可接受序号或撤销表。直接更换 `api_key_salt` 会让所有旧 Key 同时失效,必须与客户端升级协调。
+
+#### 0.2.2 客户端怎样携带 API Key
+
+| 通道 | 地址 | 认证 | 用途 |
+| --- | --- | --- | --- |
+| WebSocket(浏览器/通用方式) | `wss://{host}/{api_path}v0/channels?apikey={api_key}` | Upgrade 校验 API Key,`hi` 绑定租户,`login` 登录 | Topic、消息、回执、在线状态、通话信令 |
+| WebSocket(支持自定义 Header 的原生客户端) | `wss://{host}/{api_path}v0/channels` | Upgrade Header `X-Tinode-APIKey` | 同上 |
+| 产品 HTTP | `https://{host}/{api_path}v1/im/*` | `Authorization: Bearer ` | 好友、群、收藏、搜索、工作台和设置 |
+| 文件 HTTP | `POST /v0/file/u/`、`GET /v0/file/s/{file_id}` | 当前用 `X-Tinode-Auth: token ` | 文件上传下载;禁止 Query 传 Token |
+
+HTTP 请求统一通过 `X-Tinode-APIKey` 携带。浏览器 WebSocket 无法设置自定义 Upgrade Header 时才使用 `apikey` Query;日志和监控必须对该参数脱敏。产品 HTTP 还必须带 `X-IM-Schema: 1` 和 `X-Request-ID`;写操作带 `Idempotency-Key`。生产环境只允许 HTTPS/WSS。Token、密码、验证码、证件号不得写入 URL、普通日志、埋点或崩溃报告。
+
+WebSocket Upgrade 阶段由 `server/hdl_websock.go` 先调用 `checkAPIKey`。缺少或无效时不会建立 WebSocket,直接返回 HTTP 403:
+
+```json
+{"ctrl":{"code":403,"text":"valid API key required","ts":"2026-08-11T08:30:15.123Z"}}
+```
+
+客户端应把该错误视为应用配置/环境配置错误,停止自动重连并上报脱敏诊断;它不是账号密码错误,也不能通过重新登录解决。
+
+两种连接方式的第一个请求对照如下:
+
+| 连接方式 | 发起第一个网络请求前已有的值 | 第一个网络请求 | 第一个响应得到的值 | 下一步 |
+| --- | --- | --- | --- | --- |
+| WebSocket(推荐) | `host + api_key + tenant_code` | 带 API Key Upgrade `/v0/channels` | HTTP 101 和已建立的 WebSocket;不会返回新 API Key 或 sid | 发送 `{hi tenant}` |
+| Long Polling(兼容) | `host + api_key + tenant_code` | `POST /v0/channels/lp` | HTTP 201 和 `ctrl.params.sid` | 带 `sid` POST `{hi}`,再 GET 等待响应 |
+
+现有实现对应关系:
+
+| 源码 | 作用 |
+| --- | --- |
+| `keygen/keygen.go` | 生成普通/Root API Key 和服务端 HMAC Salt |
+| `server/api_key.go` | `checkAPIKey` 校验 API Key 格式与签名 |
+| `server/http.go` | 按 Header、Query、Form、Cookie 顺序读取 API Key |
+| `server/hdl_websock.go` | WebSocket Upgrade 前校验 API Key,无效直接 403 |
+| `server/hdl_longpoll.go` | 创建 Long Polling Session 并返回 `sid` |
+| `server/session.go` | 处理 `{hi}` 租户绑定、`{login}` 和登录 Token 返回 |
+
+现有 API Key 是应用级、非租户级,同一个普通应用 Key 可以进入不同企业;具体租户只能由后续 `{hi}.tenant` 决定。若将来要求每个租户独立 API Key,需要新增 Key 与租户绑定关系,不能只修改客户端配置。
+
+### 0.3 多租户强制规则
+
+1. 未登录阶段只传公开的 `tenant_code`;客户端永远不接收、不提交数据库 `tenant_id`。
+2. WebSocket 第一条消息必须是 `{hi}`,其中 `tenant` 为企业码。服务端解析后把租户固定到 Session。
+3. Token 内部绑定租户。Token 租户与当前 Session 租户不一致时返回 `TENANT_MISMATCH`,不得自动切换。
+4. 切换企业必须退出登录、关闭 WebSocket、清除该租户的 Token/缓存,再重新解析企业码和握手。
+5. 已登录 HTTP 请求的租户和用户只从 Token Principal 获取。请求体中的 `tenant_id`、`user_id` 不能覆盖身份。
+6. 跨租户对象统一返回 `RESOURCE_NOT_FOUND` 或 `FORBIDDEN`,不得暴露对象是否存在。
+
+### 0.4 HTTP 请求与响应
+
+成功响应统一为:
+
+```json
+{
+ "code": 0,
+ "data": {},
+ "request_id": "01K...",
+ "server_time": "2026-08-11T08:30:15.123Z"
+}
+```
+
+失败响应统一为:
+
+```json
+{
+ "code": 30001,
+ "reason": "GROUP_MUTED",
+ "message": "permission denied",
+ "details": {},
+ "request_id": "01K...",
+ "server_time": "2026-08-11T08:30:15.123Z"
+}
+```
+
+客户端只使用稳定的 `reason` 分支和本地化文案,不解析 `message`。ID、Topic、Seq 游标在客户端均按字符串处理;时间统一为 UTC RFC3339 毫秒。分页统一使用 `limit` 与不透明 `cursor`,返回 `items`、`next_cursor`、`has_more`。
+
+### 0.5 WebSocket 建连、登录和恢复顺序
+
+#### 0.5.1 现有服务端可直接执行的冷启动流程
+
+```text
+读取客户端预置的 host 和普通 API Key
+-> 用户输入/客户端读取 tenant_code
+-> 带 API Key 完成 WebSocket Upgrade
+-> hi(tenant_code)
+-> 收到 hi 的 201 ctrl,确认 tenant 与服务端限制
+-> 有历史 Token 时 token login,否则 basic login/acc
+-> 收到 login 的 200 ctrl,保存 user/token/expires
+-> sub(me) 拉取本人资料和会话订阅
+-> sub(fnd) 建立搜索 Topic
+-> 订阅需要在线的会话并按 seq 增量拉取
+-> 进入可发送状态
+```
+
+当前服务端没有 `/v1/im/tenants/resolve`,因此现阶段企业码是否有效是在第一帧 `{hi}` 中校验的。只有 WebSocket Upgrade 返回 `101 Switching Protocols` 后才能发送下面的 JSON 文本帧。
+
+`hi` 请求:
+
+```json
+{"hi":{"id":"hi-1","ver":"0.25","ua":"android/1.0.0","dev":"device-id","lang":"zh-CN","tenant":"acme"}}
+```
+
+`login` 请求使用 `basic` 或 `token`:
+
+```json
+{"login":{"id":"login-1","scheme":"basic","secret":"base64(username:password)"}}
+```
+
+```json
+{"login":{"id":"login-2","scheme":"token","secret":"<上次登录响应原样返回的 token 字符串>"}}
+```
+
+`basic.secret` 是 `base64(username + ":" + password)`。Token 登录必须把上次 `params.token` 的 JSON 字符串原样填入 `secret`,不能再次 Base64 编码。登录成功 `{ctrl}` 的 `params` 至少包含 `user`、`authlvl`、`token`、`expires`。Token 对客户端是不可解析的 opaque 字符串,只能存入系统安全存储。`id` 只用于匹配本次 Session 的命令响应,不是业务幂等键。
+
+`hi` 成功示例:
+
+```json
+{
+ "ctrl": {
+ "id": "hi-1",
+ "code": 201,
+ "text": "created",
+ "params": {
+ "ver": "0.25",
+ "tenant": {"code": "acme", "name": "示例企业"},
+ "maxFileUploadSize": 104857600,
+ "callTimeout": 30,
+ "iceServers": []
+ }
+ }
+}
+```
+
+客户端只使用实际响应中存在的限制字段;未知字段忽略。`hi` 返回非 2xx 时必须关闭连接,不得继续发送 `login`。
+
+登录成功后发送:
+
+```json
+{"sub":{"id":"sub-me-1","topic":"me","get":{"what":"desc sub tags cred"}}}
+```
+
+客户端按相同 `id` 接收 `{ctrl}` 和一个或多个 `{meta}`,从 `meta.desc` 取得本人资料和读写游标,从 `meta.sub` 取得会话 Topic、访问权限、`seq/read/recv` 与会话摘要。不要依赖 `{ctrl}`、`{meta}` 的固定先后顺序。
+
+随后建立查找 Topic:
+
+```json
+{"sub":{"id":"sub-fnd-1","topic":"fnd"}}
+```
+
+进入具体会话时订阅并增量拉取,`since` 使用客户端已完整持久化的最后 `seq + 1`:
+
+```json
+{"sub":{"id":"sub-topic-1","topic":"grp...","get":{"what":"desc sub data del","data":{"since":125,"limit":50},"del":{"since":1,"limit":50}}}}
+```
+
+订阅成功后才允许 `{pub}`、`note read/recv` 等会话操作。断线恢复必须重新执行 `WebSocket Upgrade -> hi -> token login -> sub me -> 恢复 Topic`,同一连接内不得跳过 `hi` 或切换企业码。
+
+#### 0.5.2 Long Polling 兼容流程:先取得的是 sid
+
+如果客户端无法使用 WebSocket,可以使用现有 `/v0/channels/lp`。该模式确实需要“先请求取得一个值”,但取得的是短期 Session ID `sid`,API Key 仍然必须由客户端预先持有。
+
+第一步,创建 Long Polling Session:
+
+```http
+POST /v0/channels/lp HTTP/1.1
+Host: im.example.com
+X-Tinode-APIKey: <预置的普通 API Key>
+Content-Length: 0
+```
+
+服务端返回 HTTP 201:
+
+```json
+{"ctrl":{"code":201,"text":"created","params":{"sid":"session-id"},"ts":"2026-08-11T08:30:15.123Z"}}
+```
+
+第二步开始,所有 Long Polling 请求都同时携带 API Key 和 `sid`。发送 `hi`:
+
+```http
+POST /v0/channels/lp?sid=session-id HTTP/1.1
+X-Tinode-APIKey: <预置的普通 API Key>
+Content-Type: application/json
+
+{"hi":{"id":"hi-1","ver":"0.25","ua":"android/1.0.0","platf":"android","lang":"zh-CN","tenant":"acme"}}
+```
+
+POST 只负责把命令送入 Session。客户端再保持一个不带 Body 的 GET 来接收下一条 `{ctrl}`、`{meta}`、`{data}`、`{pres}` 或 `{info}`:
+
+```http
+GET /v0/channels/lp?sid=session-id HTTP/1.1
+X-Tinode-APIKey: <预置的普通 API Key>
+```
+
+收到 `hi` 成功后,以相同方式 POST `{login}`,再 GET 登录结果;登录成功后依次 POST `{sub me}`、`{sub fnd}` 和会话订阅,并持续发起下一次 GET 等待下行消息。
+
+`sid` 无效或过期时服务端返回 HTTP 403 `invalid or expired session`,客户端必须重新创建 Long Polling Session 并从 `hi` 开始,不能只重发 `login`。Long Polling 是兼容方案,移动端和 Web 端正常网络条件下优先使用 WebSocket。
+
+#### 0.5.3 目标产品流程
+
+第 1.1 节规划的 `POST /v1/im/tenants/resolve` 属于新增接口。实现后目标流程调整为:
+
+```text
+读取预置 API Key
+-> POST /v1/im/tenants/resolve 校验企业码并取得服务地址/品牌配置
+-> 带同一 API Key 建立返回地址的 WebSocket
+-> hi 再次绑定并校验 tenant_code
+-> login -> sub me -> 恢复会话
+```
+
+企业解析接口只改善入口、服务发现和品牌配置,不能代替 `{hi}` 的租户绑定。即使 HTTP 解析成功,WebSocket 第一帧仍必须带相同 `tenant_code`。
+
+### 0.6 Topic 与产品消息
+
+核心命令:`sub` 订阅、`pub` 发送、`get` 拉取、`set` 更新 Topic、`leave` 离开、`del` 删除、`note` 发送回执/临时状态。发送成功为 `{ctrl code:202, params:{seq}}`,服务端下行消息为 `{data}`。
+
+所有产品消息在 `pub.head` 中携带:
+
+```json
+{
+ "pub": {
+ "id": "pub-1",
+ "topic": "p2p...",
+ "noecho": false,
+ "head": {
+ "x-im-schema": 1,
+ "x-im-type": "text",
+ "x-im-client-mid": "01K...",
+ "mentions": []
+ },
+ "content": {"text":"你好"}
+ }
+}
+```
+
+`x-im-client-mid` 由客户端生成 ULID/UUID,作用域为“租户 + 发送人”,断线重试必须复用;服务端目标实现应返回第一次写入的同一 `seq`。未知 `x-im-type` 显示“暂不支持的消息”,不得崩溃或删除消息。
+
+消息类型首版固定为:`text`、`image`、`video`、`voice`、`file`、`contact_card`、`link`、`forward_bundle`、`call`、`system_event`。自定义表情启用后追加 `sticker`。媒体内容只保存 `file_id`、文件元数据和缩略图 ID,不保存可长期绕过鉴权的公共 URL。
+
+### 0.7 HTTP 幂等、重试和安全
+
+1. 创建、提交、同意、拒绝、删除、转让、收藏、转发等写请求必须携带 `Idempotency-Key`。
+2. GET、断线同步和同幂等键写请求可自动重试;密码、验证码和风控确认只按接口返回的 `retry_after_seconds` 重试。
+3. `401 TOKEN_EXPIRED` 只允许一次静默刷新;刷新失败必须退出登录。`403` 不重试,`429` 按服务端退避。
+4. 服务端必须在最终写入前再次校验好友关系、群角色、禁言、消息归属、文件 ACL、租户配额和风控,不能信任前端按钮状态。
+5. 普通用户禁止向系统 Topic 发布;系统通知必须由服务端受控身份生成。
+
+## 1. 账号注册、登录与身份安全
+
+### 1.1 企业码登录.png
+
+
+
+**状态:新增企业解析接口;WebSocket 租户握手已实现。**
+
+**当前系统的执行方式:**
+
+- 前端动作:输入企业码并点击“进入企业”。只去掉首尾空格并校验非空、最长 64 字符;当前服务端没有自动转小写,客户端不得擅自改变用户输入的大小写。
+- 客户端从构建/环境配置读取 `host` 和普通 API Key,使用 `wss://{host}/{api_path}v0/channels?apikey={api_key}` 建立 WebSocket。
+- Upgrade 成功后第一帧发送 `{"hi":{"id":"hi-1","ver":"0.25","ua":"android/1.0.0","platf":"android","lang":"zh-CN","tenant":"用户输入的企业码"}}`。
+- 服务端通过 `im_tenant.code` 查询企业;成功返回 `{ctrl code:201}` 和 `params.tenant{code,name}`,同时固定 Session 的内部 `tenant_id`。客户端保存服务端返回的标准 `tenant.code/name`,再进入 1.2 登录。
+- 当前失败表现:企业码缺失/超过 64 字符返回 400,企业不存在按存储错误返回,企业停用返回 403。当前还没有统一 `TENANT_UNAVAILABLE` 产品错误码。
+
+**目标产品新增方式:**
+
+- 新增 `POST /v1/im/tenants/resolve`,未登录但必须带 `X-Tinode-APIKey`,Header 传 `X-Tenant-Code: acme`;Body 为 `{"tenant_code":"acme","platform":"android","app_version":"1.0.0"}`。
+- 成功数据:`tenant{code,name,tenant_desc,branding}`、`endpoints{websocket,api,file}`、`auth_methods`、`password_policy`、`app_update`。不得返回 `tenant_id` 或 `tenant_ticket`。
+- 后续:保存公开企业信息,连接响应给出的 WebSocket 地址,仍然发送 0.5 节的 `hi(tenant)` 做最终租户绑定;`hi` 成功后才进入 1.2。
+- 目标失败协议:企业不存在、停用或不允许登录统一返回 `TENANT_UNAVAILABLE`;客户端不得区分不存在和停用。网络失败保留输入并允许重试。
+
+成功响应示例:
+
+```json
+{
+ "code": 0,
+ "data": {
+ "tenant": {"code": "acme", "name": "示例企业", "tenant_desc": null, "branding": {}},
+ "endpoints": {
+ "websocket": "wss://im.example.com/v0/channels",
+ "api": "https://im.example.com/v1/im",
+ "file": "https://im.example.com/v0/file"
+ },
+ "auth_methods": ["password", "sms"],
+ "password_policy": {"min_length": 8, "max_length": 64},
+ "app_update": {"type": "none"}
+ },
+ "request_id": "01K...",
+ "server_time": "2026-08-11T08:30:15.123Z"
+}
+```
+
+### 1.2 登录页面.png
+
+
+
+**状态:基本账号密码登录已实现;协议勾选和安全 Token 生命周期需改造。**
+
+- 前端动作:账号、密码非空且已勾选协议后启用登录;密码仅驻留当前页面内存。
+- 协议:已完成 `hi` 的 WebSocket 发送 `{login scheme:"basic"}`,`secret=base64(username:password)`;不得先登录再传企业码。
+- 成功:读取 `{ctrl params.user/token/expires/authlvl}`,Token 存 Keychain/Keystore;立即 `sub me`,再加载会话。前端不解码 Token。
+- 待改造:登录成功后签发短期 access token 与可轮换 refresh token;Token 绑定 `tenant_id/user_id/auth_version/device_id`。修改密码、封禁账号后旧 Token 失效。
+- 错误:`AUTH_FAILED` 统一显示账号或密码错误;`TENANT_MISMATCH` 清 Token 并回企业码页;`ACCOUNT_DISABLED`、`REALNAME_REQUIRED`、`YOUTH_RESTRICTED` 按专用页面处理。
+
+登录成功示例:
+
+```json
+{
+ "ctrl": {
+ "id": "login-1",
+ "code": 200,
+ "text": "ok",
+ "params": {
+ "user": "usr...",
+ "authlvl": "auth",
+ "token": "base64-opaque-token",
+ "expires": "2026-08-11T10:30:15.123Z"
+ }
+ }
+}
+```
+
+失败仍是同一 `{ctrl}` 结构,使用 `code` 和改造后的稳定 `params.reason` 分支;客户端不得用 `text` 做程序判断。
+
+### 1.3 登录页面-密码错误.png
+
+
+
+**状态:已实现基础错误,错误码和限流需改造。**
+
+- 触发:`login` 返回 `{ctrl code:401}` 或产品错误 `AUTH_FAILED`。
+- 前端:保留账号、清空密码、聚焦密码框;统一显示“账号或密码错误”,不得显示账号是否存在。
+- 服务端:按 `tenant + account + device/IP` 限流,连续失败返回 `AUTH_RATE_LIMITED` 和 `retry_after_seconds`;日志仅记录账号哈希和 Request ID。
+- 验收:同一不存在账号与错误密码在响应码、耗时区间、前端文案上保持一致。
+
+### 1.4 注册页面.png
+
+
+
+**状态:Tinode `{acc}` 基础注册已实现;短信预校验与租户配额需改造。**
+
+- 获取验证码:调用 `POST /v1/im/auth/verification-codes`,Body `{"scene":"register","phone":"138****0000"}`;成功返回 `challenge_id`、`expires_in_seconds`、`resend_after_seconds`。
+- 提交注册:前端校验密码一致和协议后,先调用 `POST /v1/im/auth/verification-codes/{challenge_id}/verify`,Body `{"code":"123456"}`,获得一次性 `verification_token`。
+- WebSocket:把一次性验证结果放在 `{acc}.cred`,不得写入 `desc.private`。服务端必须在当前 Session 租户内消费该 Token。
+
+```json
+{"acc":{"id":"acc-1","user":"new","scheme":"basic","secret":"base64(username:password)","login":true,"tags":["tel:+86138..."],"cred":[{"meth":"tel","val":"+86138...","resp":"verification_token","params":{"challenge_id":"challenge-id"}}],"desc":{"public":{"fn":"用户昵称"}}}}
+```
+- 成功:返回新 `user` 和登录 Token,自动 `sub me`。创建用户前原子校验 `im_tenant_config.max_user_count`,超限返回 `TENANT_USER_QUOTA_EXCEEDED`。
+- 安全:手机号在租户内唯一;验证码只保存摘要、单次消费;密码按服务端策略校验并使用强哈希保存。
+
+### 1.5 注册页面-倒计时.png
+
+
+
+**状态:新增。**
+
+- 倒计时以 1.4 返回的 `resend_after_seconds` 为准,并结合 `server_time` 计算,不能只依赖本地固定 60 秒。
+- 倒计时内前端禁用按钮;即使绕过按钮重复调用,服务端返回 `VERIFICATION_RATE_LIMITED` 和剩余秒数。
+- App 重进页面可使用内存/安全临时存储中的 `challenge_id` 恢复;超过 `expires_in_seconds` 必须重新发送,旧验证码失效。
+
+### 1.6 忘记密码.png
+
+
+
+**状态:新增。**
+
+- 发码:`POST /v1/im/auth/verification-codes`,Body `{"scene":"reset_password","phone":"..."}`。无论账号是否存在都返回相同受理响应。
+- 校验:`POST /v1/im/auth/password-reset/challenges/{challenge_id}/verify`,Body `{"code":"123456"}`,成功返回短时、单次使用的 `reset_token`。
+- 重置:`POST /v1/im/auth/password-reset/confirm`,Body `{"reset_token":"...","new_password":"..."}`,带幂等键。
+- 成功:服务端更新密码、递增 `auth_version`、撤销该用户全部 access/refresh token 和在线 Session;客户端清理本租户凭证并返回登录页。
+- 失败:`PASSWORD_POLICY_VIOLATION` 返回规则明细;验证码错误不返回账号存在性,达到上限后冻结 challenge。
+
+### 1.7 身份验证(安卓).png
+
+
+
+**状态:新增。**
+
+- 敏感操作先调用 `POST /v1/im/security/challenges`,Body `{"scene":"close_youth_mode","channel":"sms"}`;手机号由登录身份读取,客户端不提交目标手机号。
+- 发码/校验:服务端返回 `challenge_id` 和脱敏手机号;提交 `POST /v1/im/security/challenges/{id}/verify`,Body `{"code":"..."}`。
+- 成功返回 `action_token`、`action`、`expires_at`;后续敏感接口以 `X-Action-Token` 携带,Token 只能用于指定动作且单次消费。
+- 错误:`CHALLENGE_EXPIRED`、`CODE_INVALID`、`TOO_MANY_ATTEMPTS`;前端不得把短信验证码或 action token 落普通存储。
+
+### 1.8 实名认证.png
+
+
+
+**状态:新增。**
+
+- 查询:`GET /v1/im/security/realname` 返回 `status=unverified|pending|verified|rejected`、脱敏姓名/证件号和 `can_submit`。
+- 提交:`POST /v1/im/security/realname/submissions`,Body `{"name":"张三","id_type":"cn_id","id_number":"..."}`,必须带幂等键。
+- 成功:返回 `submission_id`、`status:"pending"`;前端禁用重复提交,通过 `GET /v1/im/security/realname` 或 `security.realname.updated` 系统事件更新状态。
+- 安全:证件号传输和存储加密、展示脱敏;普通日志、搜索索引和消息中禁止出现原值;重复身份由租户策略决定并返回稳定错误。
+
+### 1.9 实名认证-已实名.png
+
+
+
+**状态:新增。**
+
+- 页面只调用 `GET /v1/im/security/realname`。`verified` 时返回如 `masked_name:"张*"`、`masked_id_number:"110***********1234"`、`verified_at`。
+- 前端只读展示,不缓存原始证件信息,不提供直接修改接口。
+- 更正信息使用独立申诉 `POST /v1/im/security/realname/appeals`,需二次身份验证;不复用本页保存按钮。
+
+### 1.10 实名认证(安卓).png
+
+
+
+**状态:新增。**
+
+- 登录后 `GET /v1/im/bootstrap` 返回 `security.realname{status,required,allow_skip,remind_after}`;只有 `required=true` 且未认证时显示弹窗。
+- 点击“去认证”进入 1.8;点击稍后仅在 `allow_skip=true` 时本地关闭,并按 `remind_after` 控制再次提示。
+- 服务端对必须实名的受限写操作仍返回 `REALNAME_REQUIRED`,前端弹窗不能作为授权依据。
+
+### 1.11 设置独立密码(安卓).png
+
+
+
+**状态:新增。**
+
+- 设置:`POST /v1/im/settings/independent-password`,Body `{"password":"1234","purpose":"youth_mode","action_token":"..."}`;服务端只存强哈希,不返回密码。
+- 验证:`POST /v1/im/settings/independent-password/verify` 返回短时 `action_token`,用于关闭青少年模式或修改密码。
+- 规则:四位数字、禁止明显连续/重复组合;连续错误返回 `TOO_MANY_ATTEMPTS` 和 `retry_after_seconds`。前端输入框禁截屏/自动填充,离开页面立即清空。
+- 忘记密码:跳转 1.7 的短信身份验证,验证后重置,不提供明文找回。
+
+## 2. 消息首页、全局导航与系统通知
+
+### 2.1 消息.png
+
+
+
+**状态:会话订阅已实现;产品摘要和系统入口需改造。**
+
+- 登录后发送 `sub me`,再通过 `get` 拉取 `sub` 元数据;每项使用 `topic`、`public`、`private`、`updated`、`seq/read/recv` 计算名称、摘要和未读数。
+- 消息增量通过各 Topic 的 `{data}` 更新;未读数为 `max(seq-read,0)`,回执以服务端游标为准,不能只用本地计数。
+- `GET /v1/im/bootstrap` 返回固定入口、功能开关、权限和系统 Topic;搜索调用 `GET /v1/im/search/conversations?q=&cursor=`。
+- 本地缓存按 `tenant_code + user_id` 隔离。切租户、退出或账号变化时不得复用另一身份的会话摘要。
+
+### 2.2 消息-无网络状态.png
+
+
+
+**状态:需改造客户端恢复流程和消息幂等。**
+
+- 纯网络状态由客户端监听;断网时展示缓存并禁止需要在线确认的写操作,未发送消息进入本地 `pending` 队列。
+- 使用指数退避加随机抖动重连;恢复后严格执行 `hi -> token login -> sub me -> 恢复 Topic -> get data`。
+- 每个 Topic 按最后持久化的 `seq/read/recv` 增量拉取。重发消息复用 `x-im-client-mid`,服务端需实现去重并返回原 `seq`。
+- Token 失效转登录页;租户停用返回企业不可用页;禁止无限快速重连。
+
+### 2.3 消息-更多.png
+
+
+
+**状态:菜单为纯客户端;权限数据新增。**
+
+- 菜单打开/关闭不调用服务端。菜单项来自登录后 `GET /v1/im/bootstrap` 的 `features` 与 `permissions`。
+- `create_group=false` 时隐藏“创建群聊”;`user_search=false` 时隐藏“添加好友”;扫码权限只影响实际扫码,不影响服务端授权。
+- 各入口协议分别见 5.3、6.1、10.1;服务端在实际写接口再次鉴权,不能只依赖隐藏按钮。
+
+### 2.4 消息页面—长按弹出.png
+
+
+
+**状态:基础消息操作部分已实现;产品权限矩阵需改造。**
+
+- 长按本身不请求。客户端根据消息的 `type/from/seq/created_at/status`、当前 Topic 权限和服务端下发的 `message_policy` 预裁剪菜单。
+- 引用/复制为本地动作;收藏见 4.16;转发见 4.17~4.20;删除使用 Tinode `{del what:"msg",topic,delseq}`;撤回需新增 `POST /v1/im/topics/{topic}/messages/{seq}/recall`。
+- 编辑使用 `POST /v1/im/topics/{topic}/messages/{seq}/revisions`,Body `{"content":...,"client_mid":"..."}`;仅文本且在时限内允许。
+- 群回执调用 `GET /v1/im/topics/{topic}/messages/{seq}/receipts?cursor=`。所有最终操作由服务端验证消息归属、时限、群角色和租户。
+
+### 2.5 系统消息.png
+
+
+
+**状态:需改造。**
+
+- `GET /v1/im/system-notifications?cursor=` 返回 `id,event_type,title,summary,created_at,read_at,action`;新通知通过服务端专用系统 Topic 的 `system_event` 消息推送。
+- 新设备登录事件内容为 `device_name,platform,ip_masked,login_at,security_action`;IP 只下发必要脱敏值。
+- 已读:`POST /v1/im/system-notifications/{id}/read`,带幂等键;跳转目标只接受服务端白名单 `action.type` 和内部参数。
+- 当前普通认证用户可向 `sys` Topic 发布的路径必须封禁。客户端不得渲染普通用户伪造的系统卡片为可信通知。
+
+## 3. 单聊会话、会话建立与消息状态
+
+### 3.1 个人对话框(对方的状态).png
+
+
+
+**状态:需改造好友关系状态。**
+
+- 进入会话先 `GET /v1/im/conversations/{topic}/context`,返回 `relationship=pending_inbound`、`can_send=false`、对方公开资料和申请 ID。
+- 历史申请提示由 `relationship_event` 系统消息渲染;普通消息仍通过 Topic `{data}` 下行。
+- 输入区锁定;“通过/拒绝”调用 5.2 的好友申请接口。前端不可通过直接 `pub` 绕过,服务端发布路径必须校验好友关系。
+
+### 3.2 个人对话框(对方的状态)(1).png
+
+
+
+**状态:在线状态已具备基础能力;关系文案新增。**
+
+- 订阅 P2P Topic 后用 `{pres}` 更新在线/离线,`last_seen` 仅在对方隐私允许时展示。
+- `GET /v1/im/conversations/{topic}/context` 返回 `display_name,avatar_file_id,online_visibility,relationship`。
+- 好友关系变化由服务端生成 `system_event{event_type:"friend_relationship_changed"}`,客户端不得根据本地动作自行永久插入系统记录。
+
+### 3.3 个人对话框(我的状态).png
+
+
+
+**状态:新增好友申请状态。**
+
+- 发起申请使用 `POST /v1/im/friend-requests`,Body `{"target_user_id":"usr...","message":"我是...","source":"search"}`。
+- 返回 `request_id,status:"pending",created_at`;页面以该服务端状态显示“已发送”,重复点击用同一幂等键返回原申请。
+- 申请待处理时 `context.relationship=pending_outbound`、`can_send=false`,禁止重复创建和普通消息发送。
+
+### 3.4 个人对话框(我的状态)(1).png
+
+
+
+**状态:发送和读回执已实现基础能力;送达状态映射需统一。**
+
+- 本地生成 `client_mid` 后显示 `sending`;`pub` 收到 `202 + seq` 更新为 `sent`;失败保留同一 `client_mid` 重试。
+- 对方 `note what:"recv"` 更新 `delivered`,`note what:"read"` 更新 `read`;多设备取服务端最大游标。
+- 状态只附着在气泡元数据,不修改消息正文。服务端拒绝时映射为 `failed(reason)`,不得伪装成已发送。
+
+### 3.5 开启沟通页面.png
+
+
+
+**状态:新增好友审批与 Topic 联动。**
+
+- 好友申请通过后,服务端在同一事务内建立双方关系和 P2P 订阅,并生成 `friend_relationship_changed:accepted` 系统事件。
+- 客户端收到事件或重新查询 `context.relationship=friend` 后解锁输入区;不得仅因本地“通过”按钮成功动画解锁。
+- 首次进入订阅 P2P Topic,再按 `since` 拉取申请上下文和正常消息。
+
+### 3.6 双方沟通状态.png
+
+
+
+**状态:上传基础已实现;媒体消息 Schema、ACL 和恢复需改造。**
+
+- 选择媒体后本地生成 `client_mid` 和稳定占位;调用文件上传,得到 `file_id` 后再 `pub x-im-type=image|video|voice`。
+- `content` 至少含 `file_id,name,size,mime,width,height,duration_ms,thumbnail_file_id` 中适用字段;上传进度是客户端任务状态,不写成聊天消息。
+- 服务端发布成功后关联文件与 Topic/消息;文件下载必须校验消息可见性。上传失败只重试上传,成功但 `pub` 失败时复用已上传 `file_id` 和 `client_mid`。
+
+### 3.7 个人对话—拉黑状态/已读状态.png
+
+
+
+**状态:黑名单产品规则新增。**
+
+- 拉黑后 `context.relationship=blocked_by_me|blocked_by_peer`、`can_send=false`;服务端 `pub` 返回统一 `MESSAGE_NOT_ALLOWED`,不得向发送者泄露具体是对方拉黑。
+- 失败消息只保存在发送端本地并显示失败,不下发给对方、不写入正常消息表。
+- 已读仍按历史最大 `read` 渲染;拉黑不能回滚既有回执。解除拉黑见 5.10、5.12。
+
+### 3.8 个人对话—删除状态.png
+
+
+
+**状态:新增好友删除后的发送约束。**
+
+- 删除好友使用 `DELETE /v1/im/friends/{user_id}`;成功后双方关系变为 `none`,服务端推送关系变化事件。
+- 后续 `pub` 返回 `FRIEND_RELATION_REQUIRED`;前端保留失败草稿,提供“重新添加好友”入口,不自动发申请。
+- 历史聊天是否保留由 Topic 权限决定,删除好友不等于删除双方历史消息。
+
+### 3.9 个人对话-键盘收起状态.png
+
+
+
+**状态:纯客户端。**
+
+- 键盘、表情和附加面板收起不调用服务端;保持列表锚点和未发送草稿。
+- 草稿默认按 `tenant + user + topic` 本地加密保存;若实现多端草稿,再单独调用 `PUT /v1/im/topics/{topic}/draft`,不得混入聊天消息。
+
+### 3.10 个人对话-发各种文件状态.png
+
+
+
+**状态:面板纯客户端;上传需改造。**
+
+- 打开面板不请求。选择前依据 `context.capabilities` 判断图片、视频、语音、文件是否允许,调用系统选择器/相机。
+- 上传前校验 `mime,size` 和 `bootstrap.limits.max_file_size`;服务端仍必须重新校验租户配额、文件类型和会话权限。
+- 具体上传/发消息使用 3.6 协议;相机和麦克风授权见 10.20、10.21。
+
+### 3.11 个人对话-发各种文件状态(1).png
+
+
+
+**状态:需改造产品消息 Schema。**
+
+- 客户端按 `x-im-type` 选择文本、名片、文件等卡片,公共字段统一使用 `from,topic,seq,ts,head,content`。
+- 每种卡片必须实现未知字段忽略、未知类型占位、发送/失败/回执状态和长按动作。
+- 重新安装或换设备后仅凭服务端消息即可还原卡片,不能依赖发送端本地路径或临时 URL。
+
+### 3.12 个人对话-发送名片状态.png
+
+
+
+**状态:新增产品消息类型。**
+
+- 选人:`GET /v1/im/friends?query=&cursor=`;选择后发送 `x-im-type:"contact_card"`。
+- `content` 为 `{"user_id":"usr...","display_name":"张三","avatar_file_id":"...","public_account":"A1024"}`;服务端按目标用户当前公开资料生成/校正快照,禁止手机号、证件号等敏感字段。
+- 点击卡片调用 `GET /v1/im/users/{user_id}/profile`;用户不可见或已离职时显示失效卡片,不跨租户查询。
+
+## 4. 消息输入、媒体内容与消息操作
+
+### 4.1 键盘展开.png
+
+
+
+**状态:纯客户端。**
+
+- 键盘展开、列表上推和光标变化不调用服务端;草稿按 `tenant + user + topic` 保存。
+- 点击发送时才校验 `context.can_send` 并执行 `pub text`;正文去首尾策略、最大长度从 `bootstrap.limits.max_text_length` 获取,服务端再次校验。
+
+### 4.2 @人员—选择提醒人.png
+
+
+
+**状态:新增群成员查询。**
+
+- 输入 `@` 后调用 `GET /v1/im/groups/{topic}/members?query=&cursor=&mentionable=true`,返回 `user_id,display_name,avatar_file_id,role,mentionable`。
+- `@所有人` 是虚拟项,只有响应 `permissions.can_mention_all=true` 时展示,不以特殊用户 ID 表示。
+- 搜索做 300ms 防抖并取消旧请求;服务端只返回当前租户且仍在群内的成员。
+
+### 4.3 @人员—完成.png
+
+
+
+**状态:纯客户端选择;发送协议需改造。**
+
+- 完成后在编辑器插入带范围的 Mention 节点,内部保存 `user_id`,显示昵称只是快照。
+- 发送时 `pub.head.mentions=[{"user_id":"usr...","offset":0,"length":3}]`;`@所有人` 使用 `mention_all:true`,不得伪造用户 ID。
+- 服务端验证成员身份和 `mention_all` 权限;无效成员可删除提醒语义但保留可读文本,越权 `@所有人` 返回 `MENTION_ALL_FORBIDDEN`。
+
+### 4.4 @人员状态.png
+
+
+
+**状态:需改造提醒和 Push。**
+
+- 消息仍通过 `{data}` 下行,客户端使用结构化 `mentions` 高亮,禁止通过字符串扫描昵称判断。
+- 当前用户被提及时会话摘要标记 `mentioned=true`;读到对应 `seq` 后随 `note read` 清除。
+- Push 服务按结构化目标生成“有人@你”文案;已退群用户不能收到新提醒,但历史消息仍按快照渲染。
+
+### 4.5 表情包展开(发送按钮默认状态).png
+
+
+
+**状态:纯客户端。**
+
+- 面板、分类、最近使用和删除键均本地处理;未选择内容且文本为空时禁用发送。
+- Unicode 表情作为普通 `text` 内容发送。自定义表情资源首版未定义,不得把本地资源路径直接发送。
+
+### 4.6 表情包发送页面.png
+
+
+
+**状态:Unicode 表情使用已实现文本消息;独立表情包新增。**
+
+- 文本表情按 `pub x-im-type:text` 发送。独立自定义表情若启用,使用 `x-im-type:"sticker"`,`content={pack_id,sticker_id,file_id,width,height}`。
+- 自定义表情发送前服务端校验表情包属于当前租户且可用;未知表情显示占位,不使用任意外链 URL。
+
+### 4.7 语音—按住说话.png
+
+
+
+**状态:录音纯客户端;上传和语音消息需改造。**
+
+- 按下前检查麦克风权限和 `context.capabilities.voice`;权限不足进入 10.21,不发请求。
+- 松开发送:编码为服务端允许的 MIME,校验最短/最长时长,上传后 `pub x-im-type:voice`,内容含 `file_id,duration_ms,size,mime,waveform`。
+- 上滑取消、过短、系统来电打断时删除临时文件,不调用上传接口。
+
+### 4.8 语音按住说话.png
+
+
+
+**状态:纯客户端。**
+
+- 音量幅度、上滑取消区域和浮层颜色来自本地录音会话,不上传实时音量。
+- 进入取消区只改变本地状态;最终抬手决定删除或执行 4.7 的上传发送。
+
+### 4.9 内容沟通对话3-语音录音中.png
+
+
+
+**状态:录音纯客户端;临时状态可选改造。**
+
+- 录音期间客户端锁定其他媒体入口。默认不向群成员广播“正在录音”,以减少隐私暴露和状态风暴。
+- 若产品确认需要,可发送不落库的 `{note what:"kpa",topic}`;服务端限频,其他端超时自动消失,不能作为聊天记录。
+
+### 4.10 内容沟通对话3-图片和语音发送中.png
+
+
+
+**状态:客户端任务管理新增;服务端上传需改造。**
+
+- 每个待发送项保存 `client_mid,local_uri,type,upload_id,progress,state`;同一文件可分片/断点续传时使用稳定 `upload_id`。
+- 上传完成后再发布消息;多任务可并发但必须各自保持原排序号。离开页面不取消,退出账号必须暂停并隔离任务。
+- 服务端返回 `FILE_TOO_LARGE`、`FILE_TYPE_NOT_ALLOWED`、`STORAGE_QUOTA_EXCEEDED` 时不可自动无限重试。
+
+### 4.11 内容沟通对话3-语音/视频/图片状态.png
+
+
+
+**状态:需改造媒体 Schema 和受权下载。**
+
+- 语音渲染 `duration_ms,waveform`,未听状态保存在客户端并可选跨端同步;视频渲染 `thumbnail_file_id,duration_ms,width,height`;图片渲染缩略图和原图 ID。
+- 预览前使用认证文件接口获取,服务端校验当前用户仍可访问引用消息;附件撤回或过期返回 `FILE_UNAVAILABLE`。
+- 缩略图和原文件分别授权,客户端缓存键必须包含租户与用户。
+
+### 4.12 语音图片发送状态.png
+
+
+
+**状态:需改造。**
+
+- 图片选择后立即显示本地缩略图,上传成功后只替换资源来源,不改变 `client_mid` 或列表位置。
+- 失败展示重试按钮;重试复用同一上传任务和消息幂等 ID。语音播放与图片上传状态相互独立。
+
+### 4.13 上传文件.png
+
+
+
+**状态:纯客户端视觉资源。**
+
+- 该图片仅定义统一上传图标和无障碍名称“上传文件”,不产生网络请求。
+- 文件助手、群文件、会话附件、反馈证据实际上传时统一走 0.2 文件接口,并额外提交业务 `purpose` 以便服务端执行权限和保留期策略。
+
+### 4.14 文件发送状态.png
+
+
+
+**状态:上传已实现基础能力;消息引用和 ACL 需改造。**
+
+- 上传成功后发送 `x-im-type:file`,`content={file_id,name,size,mime,extension,checksum}`;文件名做长度和控制字符清理。
+- 服务端在消息落库事务中关联文件;未关联的临时上传按 TTL 清理。卡片下载见 7.9~7.11。
+- 多文件逐个生成 `client_mid`,某个失败不能改变其他文件状态。
+
+### 4.15 多选-选中对话框.png
+
+
+
+**状态:选择纯客户端;批量动作新增。**
+
+- 客户端按 `(topic,seq)` 保存已选项并显示数量;默认最多 100 条,超限在本地阻止。
+- 转发、收藏、删除前把选中列表提交对应接口;服务端逐条校验仍可见、类型支持和权限,返回 `succeeded[]` 与 `failed[{seq,reason}]`。
+- 不允许只因第一条可操作就假定整批可操作。
+
+### 4.16 多选-选中对话内容-收藏成功.png
+
+
+
+**状态:新增。**
+
+- `POST /v1/im/favorites/batch`,Body `{"messages":[{"topic":"grp...","seq":"123"}]}`,带幂等键。
+- 返回 `created_count,existing_count,failed[]`;同一用户对同一 `topic+seq` 重复收藏不新增记录。
+- 收藏保存原消息引用、不可变展示快照和附件授权关系;成功 Toast 只在至少一项成功时展示,部分失败需给出汇总。
+
+### 4.17 转发-单选.png
+
+
+
+**状态:新增。**
+
+- 目标列表:`GET /v1/im/forward-targets?query=&cursor=`,只返回 `can_receive_forward=true` 的会话和文件助手。
+- 选中后本地进入确认页,不立即发送;服务端在最终转发接口再次校验目标 Topic、原消息和附件权限。
+
+### 4.18 转发-多选.png
+
+
+
+**状态:新增。**
+
+- 目标查询同 4.17;多选最多取 `bootstrap.limits.max_forward_targets`,首版建议 9。
+- 确认前只保存目标 Topic 字符串;不得缓存或提交 `tenant_id`。跨租户 Topic 在服务端统一拒绝。
+
+### 4.19 转发—逐条转发.png
+
+
+
+**状态:新增。**
+
+- `POST /v1/im/messages/forward`,Body `{"mode":"separate","sources":[{"topic":"p2p...","seq":"12"}],"target_topics":["grp..."],"comment":"..."}`。
+- 返回每个目标和源消息的 `client_mid/seq/status`;服务端保持源顺序,重新生成目标消息,不能复用原 `seq`。
+- 附件必须建立新的授权引用;原消息已撤回、过期或不可见时该项失败。留言作为每个目标的一条独立文本消息发送。
+
+### 4.20 转发—合并转发—多选状态.png
+
+
+
+**状态:新增。**
+
+- 调用 4.19 接口,`mode:"bundle"`;服务端生成 `forward_bundle`,内容含 `bundle_id,title,item_count,preview[]`,完整内容通过 `GET /v1/im/forward-bundles/{bundle_id}?cursor=` 获取。
+- 快照保留显示名、时间和内容摘要,对撤回/敏感字段按策略脱敏;不得允许接收端利用 Bundle 获取原会话额外权限。
+- 多目标共享不可变 Bundle 内容但分别生成目标消息与授权记录。
+
+## 5. 通讯录、好友关系与黑名单
+
+### 5.1 通讯录.png
+
+
+
+**状态:新增产品通讯录接口。**
+
+- 好友列表:`GET /v1/im/friends?query=&initial=&cursor=`,返回 `user_id,display_name,remark,avatar_file_id,initial,online_visibility`;显示名优先级为备注名、昵称、账号。
+- 组织成员:`GET /v1/im/organization/members?query=&department_id=&cursor=`;好友和组织成员是两种关系,前端不得混为一个可发消息权限。
+- 新朋友未处理数、群列表、文件助手和系统消息入口由 `GET /v1/im/bootstrap` 返回。列表缓存以租户和用户隔离。
+
+### 5.2 新朋友.png
+
+
+
+**状态:新增。**
+
+- 列表:`GET /v1/im/friend-requests?direction=inbound&status=all&cursor=`,返回 `request_id,applicant,message,source,status,created_at,handled_at`。
+- 同意:`POST /v1/im/friend-requests/{request_id}/accept`;拒绝:`POST /v1/im/friend-requests/{request_id}/reject`,均带幂等键和可选 `version`。
+- 成功返回最终 `status` 和 P2P `topic`。重复审批返回原结果;并发冲突返回 `REQUEST_ALREADY_HANDLED` 并附当前状态,前端刷新该项。
+
+### 5.3 添加好友.png
+
+
+
+**状态:新增。**
+
+- 账号搜索:`GET /v1/im/users/search?q=&type=account&cursor=`;扫码后调用 `POST /v1/im/qrcodes/resolve`,Body `{"token":"...","purpose":"add_friend"}`。
+- 结果统一返回 `relationship=none|friend|pending_inbound|pending_outbound` 和 `can_add`;隐私不允许或未找到统一为 `USER_NOT_DISCOVERABLE`。
+- 分享/二维码见 5.4;发申请见 5.6。服务端按租户配置 `allow_user_search` 和用户隐私再次拦截。
+
+### 5.4 添加好友(1).png
+
+
+
+**状态:新增。**
+
+- 获取二维码:`POST /v1/im/me/qrcodes`,Body `{"purpose":"add_friend","expires_in_seconds":86400}`,返回 `qr_token,qr_payload,expires_at,display_profile`。
+- 二维码只含不透明、签名、带租户和用途约束的短期 Token,不直接放手机号、用户 ID 或 access token。
+- 分享/保存图片是客户端动作;扫码解析始终调用 5.3,过期返回 `QR_CODE_EXPIRED`,篡改返回 `QR_CODE_INVALID`。
+
+### 5.5 添加好友-输入精准用户ID.png
+
+
+
+**状态:新增。**
+
+- 精准查询仍调用 `GET /v1/im/users/search?q=&type=exact_account`;手机号查询仅当租户和目标隐私同时允许时使用 `type=phone`。
+- 每项返回公开资料和 `action=add|chat|pending|none`。前端不得根据是否返回手机号推断账户存在。
+- 搜索限流返回 `SEARCH_RATE_LIMITED`;禁止客户端批量枚举 ID。
+
+### 5.6 添加好友验证.png
+
+
+
+**状态:新增。**
+
+- `POST /v1/im/friend-requests`,Body `{"target_user_id":"usr...","message":"验证留言","source":"search|qr|group","source_topic":"grp..."}`。
+- 留言最多 50 个 Unicode 字符,服务端做敏感内容审核;`source_topic` 只能是双方当前可见的同租户群。
+- 成功返回 `request_id,status:"pending"`;重复有效申请返回原记录,目标隐私关闭时返回统一 `FRIEND_REQUEST_NOT_ALLOWED`。
+
+### 5.7 好友个人主页.png
+
+
+
+**状态:新增。**
+
+- `GET /v1/im/users/{user_id}/profile` 返回按当前查看人裁剪后的 `display_name,remark,avatar_file_id,gender,region,public_account,signature,relationship,capabilities`。
+- 发消息使用返回的 P2P `topic`;没有 Topic 时调用 `POST /v1/im/conversations/p2p`,Body `{"peer_user_id":"..."}` 并由服务端验证好友关系。
+- 音视频按钮只按 `capabilities.can_call` 展示,真正呼叫时再次鉴权。隐藏字段必须从响应中省略,不能只由前端遮挡。
+
+### 5.8 群聊—非好友个人主页.png
+
+
+
+**状态:新增。**
+
+- `GET /v1/im/groups/{topic}/members/{user_id}/profile` 返回群内昵称、角色和允许公开的账号资料。
+- `can_add_friend` 同时受群 `allow_member_add_friend`、目标隐私和租户策略控制;为 false 时隐藏添加入口。
+- 从该页申请时 5.6 传 `source:"group"` 和当前 Topic;服务端校验双方仍在群内。
+
+### 5.9 好友—聊天设置—未拉黑状态.png
+
+
+
+**状态:部分 Topic 设置已实现;产品设置新增。**
+
+- 查询:`GET /v1/im/conversations/{topic}/settings` 返回 `pinned,mute_notifications,message_retention,blocked,remark,permissions`。
+- 更新置顶/免打扰/保存方式:`PATCH /v1/im/conversations/{topic}/settings`,Body 只提交变更字段并带 `version`;返回最新完整设置。
+- 备注:`PATCH /v1/im/friends/{user_id}`,Body `{"remark":"..."}`;查记录见第 7 章,举报见第 11 章,删除见 5.11。
+
+### 5.10 好友—聊天设置—拉黑状态.png
+
+
+
+**状态:新增。**
+
+- 开启拉黑:`PUT /v1/im/blocks/{user_id}`;解除:`DELETE /v1/im/blocks/{user_id}`,均带幂等键。
+- 成功返回 `blocked,relationship,updated_at` 并向当前用户其他设备推送 `privacy.block.updated`。拉黑后服务端拦截对方消息和呼叫。
+- 解除拉黑只恢复黑名单 ACL;若双方已非好友,`relationship` 仍为 `none`,不能自动恢复好友。
+
+### 5.11 删除好友提示.png
+
+
+
+**状态:确认框纯客户端;删除接口新增。**
+
+- 确认调用 `DELETE /v1/im/friends/{user_id}`,带幂等键。是否清除本地历史由客户端在成功后独立处理,不放入删除好友请求。
+- 服务端解除双向好友关系但不替对方删除历史;返回 `relationship:"none",topic`。当前用户是否删除本地/服务端可见记录是独立选择。
+- 取消不请求;确认后清理页面缓存并等待关系事件校正其他设备。
+
+### 5.12 黑名单.png
+
+
+
+**状态:新增。**
+
+- `GET /v1/im/blocks?cursor=` 返回 `user,blocked_at`;移出调用 5.10 的 DELETE。
+- 批量解除:`POST /v1/im/blocks/batch-delete`,Body `{"user_ids":[...]}`,返回逐项结果;最多 100 人。
+- 用户资料不可见时仍用受控占位显示黑名单记录,不能因资料删除而无法解除。
+
+### 5.13 黑名单提示(1).png
+
+
+
+**状态:确认框纯客户端;写接口新增。**
+
+- 确认后调用 `PUT /v1/im/blocks/{user_id}`,相同幂等键重复提交返回当前 `blocked=true`。
+- 服务端在同一事务内更新黑名单和通信 ACL,并写审计;取消不调用接口。
+- 若对象跨租户或不存在统一返回 `RESOURCE_NOT_FOUND`。
+
+### 5.14 黑名单提示.png
+
+
+
+**状态:图片文件名与画面语义不一致;按“清除聊天记录”实施,需改造。**
+
+- 确认调用 Tinode `{del what:"msg",topic,delseq:[{"low":1,"hi":last_seq+1}],hard:false}`,仅删除当前用户视图;或封装 `DELETE /v1/im/conversations/{topic}/messages`。
+- 不解除好友、不影响对方历史、不删除收藏;附件仅在无任何有效引用且过保留期后回收。
+- 前端需在确认文案明确“仅清除自己的聊天记录”,成功后清本地 Topic 消息缓存并保留会话关系。
+
+## 6. 群聊创建、资料维护与成员治理
+
+### 6.1 群聊.png
+
+
+
+**状态:Tinode 群 Topic 已实现基础能力;产品群列表与配额需改造。**
+
+- 列表:`GET /v1/im/groups?category=owned|managed|joined&query=&cursor=`,返回 `topic,name,avatar_file_id,member_count,my_role,updated_at`。
+- 创建:`POST /v1/im/groups`,Body `{"name":"...","member_user_ids":[],"avatar_file_id":"..."}`;原子校验租户群数上限和群成员上限,返回 `topic`。
+- 分类由服务端 `my_role` 决定;创建成功或角色变化后通过系统事件刷新,不由前端自行移动分组。
+
+### 6.2 空白页.png
+
+
+
+**状态:纯客户端。**
+
+- 对 6.1 任一分类返回 `items=[]` 且 `has_more=false` 时显示对应空状态,不追加空白占位数据。
+- 收到 `group.membership.updated` 或创建成功后重新拉取当前分类。
+
+### 6.3 加入群聊.png
+
+
+
+**状态:新增邀请卡片和加入接口。**
+
+- 邀请卡下行 `system_event{event_type:"group_invitation",invitation_id,group,inviter,status,expires_at}`。
+- 点击加入:`POST /v1/im/group-invitations/{invitation_id}/accept`,带幂等键;返回 `status=joined|pending_approval` 和 `topic`。
+- 服务端校验邀请租户、有效期、群状态、成员资格和成员数上限;重复加入返回当前状态,过期返回 `INVITATION_EXPIRED`。
+
+### 6.4 内容沟通对话3-开启进群验证与拉人状态.png
+
+
+
+**状态:新增群成员事件。**
+
+- 邀请:`POST /v1/im/groups/{topic}/invitations`,Body `{"user_ids":[],"message":""}`;按群策略直接加入或创建待审批申请。
+- 加入、审批、拒绝等结果由服务端生成 `system_event{event_type:"group_membership_changed",operator,target_users,action,result}` 并写入群时间线。
+- 客户端只渲染可信系统事件;服务端同时更新成员表、Topic 订阅和成员数,不允许三者不一致。
+
+### 6.5 邀请进群申请.png
+
+
+
+**状态:新增。**
+
+- `GET /v1/im/groups/{topic}/join-requests?status=pending&cursor=` 返回申请人、邀请人、来源、时间、状态和 `version`。
+- 同意:`POST /v1/im/groups/{topic}/join-requests/{id}/accept`;拒绝:`POST /v1/im/groups/{topic}/join-requests/{id}/reject`,带幂等键与 `version`。
+- 仅群主/管理员可处理;同意时原子校验群成员上限。并发处理返回 `REQUEST_ALREADY_HANDLED` 和最终状态。
+
+### 6.6 新邀请进群申请.png
+
+
+
+**状态:新增。**
+
+- `GET /v1/im/groups/{topic}/summary` 返回 `pending_join_request_count`、最新公告和权限;新申请通过 `group.join_request.created` 推送给有审批权限的成员。
+- 审批完成后服务端广播新数量;前端归零时移除入口。普通成员响应中不得得到申请人敏感信息或数量。
+
+### 6.7 群公告状态.png
+
+
+
+**状态:新增。**
+
+- 查询最新公告:`GET /v1/im/groups/{topic}/announcements/latest`;发布/更新:`PUT /v1/im/groups/{topic}/announcements/current`,Body `{"content":"...","version":1}`。
+- 返回 `announcement_id,content,author,version,published_at`。仅有权限角色可写,使用乐观锁防覆盖。
+- 发布成功后服务端生成群系统事件;顶部摘要使用接口返回数据,不解析聊天文本猜测。
+
+### 6.8 群公告状态/邀请进群申请同时存在.png
+
+
+
+**状态:纯客户端组合;数据接口新增。**
+
+- 页面一次调用 `GET /v1/im/groups/{topic}/summary`,分别读取 `announcement` 和 `pending_join_request_count`,按产品固定顺序渲染两个入口。
+- 处理申请只刷新计数,不清公告;更新公告只替换公告对象,不影响申请入口。
+
+### 6.9 群公告(对方查看状态).png
+
+
+
+**状态:新增。**
+
+- 详情:`GET /v1/im/groups/{topic}/announcements/{announcement_id}`;普通成员响应不含编辑权限。
+- 已读:`POST /v1/im/groups/{topic}/announcements/{id}/read`,带幂等键,返回 `read_at`;用于清除新公告标记。
+- 服务端校验当前群成员资格,退群后不可借历史 ID 读取新公告。
+
+### 6.10 群聊设置.png
+
+
+
+**状态:部分订阅设置已实现;产品菜单新增。**
+
+- `GET /v1/im/groups/{topic}/settings` 返回 `my_role,permissions,profile,announcement,member_count,pinned,mute_notifications` 和群治理开关。
+- 个人置顶/免打扰通过 `PATCH /v1/im/groups/{topic}/member-settings`;群级设置必须使用 6.17,不混在个人设置里。
+- 退出:`POST /v1/im/groups/{topic}/leave`;群主必须先转让或解散。清记录、举报分别见 5.14、11.3。
+
+### 6.11 群资料.png
+
+
+
+**状态:需改造。**
+
+- `GET /v1/im/groups/{topic}` 返回 `name,avatar_file_id,description,owner,member_count,version,permissions`。
+- 更新:`PATCH /v1/im/groups/{topic}`,Body 可含 `name,avatar_file_id,description,version`;成功返回最新对象并推送 `group.profile.updated`。
+- 服务端做角色、长度、敏感内容、文件归属校验;前端只展示 `permissions.can_edit_profile` 允许的入口。
+
+### 6.12 设置群头像.png
+
+
+
+**状态:上传基础已实现;用途绑定需改造。**
+
+- 来源选择、裁剪是客户端动作。上传使用 `purpose=group_avatar`,服务端校验图片 MIME、尺寸、大小并返回 `file_id`。
+- 上传后调用 6.11 PATCH `{"avatar_file_id":"...","version":n}`;只有 PATCH 成功才替换正式头像。
+- 取消或 PATCH 失败的临时文件按 TTL 回收,不能污染群文件列表。
+
+### 6.13 设置昵称(1).png
+
+
+
+**状态:新增。**
+
+- `PUT /v1/im/groups/{topic}/members/me/profile`,Body `{"nickname":"...","version":n}`。
+- 服务端校验成员资格、长度和敏感词,返回 `nickname,version,updated_at` 并推送成员资料变化。
+- 该昵称仅作用当前群,不能修改全局用户昵称或其他群资料。
+
+### 6.14 群简介.png
+
+
+
+**状态:同 6.11,需改造。**
+
+- 保存调用 `PATCH /v1/im/groups/{topic}`,Body `{"description":"...","version":n}`;空字符串表示清空,省略表示不修改。
+- 服务端返回 `VALIDATION_FAILED` 的 `details.field/max_length` 或 `CONTENT_REJECTED`;前端保留草稿供修正。
+
+### 6.15 群标识.png
+
+
+
+**状态:角色已有基础数据;产品渲染需统一。**
+
+- 群消息下行增加服务端确认的 `sender_snapshot{display_name,role}`,角色枚举固定 `owner|admin|member`。
+- 成员角色变化事件到达后刷新成员缓存;历史消息可以保留发送时角色快照,详情以当前角色接口为准。
+- 客户端不得根据本地管理员列表给消息自行加可信标识。
+
+### 6.16 群二维码页面.png
+
+
+
+**状态:新增。**
+
+- `POST /v1/im/groups/{topic}/qrcodes`,Body `{"expires_in_seconds":86400}`,返回签名 `qr_token,qr_payload,expires_at,group_snapshot`。
+- 扫码调用 `POST /v1/im/qrcodes/resolve`,`purpose:"join_group"`;随后 `POST /v1/im/groups/{topic}/join-requests` 并携带 `qr_token`。
+- 群解散、二维码过期或管理员撤销后必须失效;二维码不得直接承载内部群权限。
+
+### 6.17 群组管理.png
+
+
+
+**状态:新增。**
+
+- 查询/更新:`GET/PATCH /v1/im/groups/{topic}/governance`,字段为 `join_approval,mute_all,member_upload_files,member_private_chat,member_invite` 和 `version`。
+- 每个字段返回 `editable`;PATCH 仅群主或被授权管理员可用,记录操作者、旧值、新值和 Request ID。
+- 解散:`DELETE /v1/im/groups/{topic}`,需二次 `X-Action-Token` 和幂等键;成功后撤销 Topic、二维码、邀请和文件新增权限。
+
+### 6.18 设置群管理员.png
+
+
+
+**状态:新增。**
+
+- `GET /v1/im/groups/{topic}/members?role=admin&cursor=` 返回管理员列表;`GET /v1/im/groups/{topic}/role-limits` 返回 `max_admin_count,current_admin_count`。
+- 只有群主可进入。群主本人以 `owner` 单独展示,不计入普通管理员列表和名额。
+
+### 6.19 添加管理员.png
+
+
+
+**状态:新增。**
+
+- 候选:`GET /v1/im/groups/{topic}/members?eligible_role=admin&query=&cursor=`,返回 `eligible` 和不可选原因。
+- 提交:`POST /v1/im/groups/{topic}/admins/batch`,Body `{"user_ids":[],"version":n}`;服务端原子校验群主身份、成员资格和管理员上限。
+- 超限返回 `GROUP_ADMIN_LIMIT_EXCEEDED`,并附 `max/current/requested`。
+
+### 6.20 添加管理员-设置成功.png
+
+
+
+**状态:新增。**
+
+- 6.19 成功返回 `admins,version,updated_at`;前端用响应替换列表并显示成功,不在请求完成前自行修改角色。
+- 服务端推送 `group.role.updated`,新管理员收到后刷新菜单权限和群标识。
+
+### 6.21 添加管理员-设置成功(1).png
+
+
+
+**状态:新增。**
+
+- 提交期间按幂等键禁重复点击。批量接口返回 `succeeded[]`、`failed[{user_id,reason}]` 和最新 `admins`。
+- 部分失败时保留成功角色,不做整批客户端回滚;前端展示失败原因并以服务端列表为准。
+
+### 6.22 删除管理员-设置成功.png
+
+
+
+**状态:新增。**
+
+- `DELETE /v1/im/groups/{topic}/admins/{user_id}`,带幂等键和 `If-Match`/`version`。
+- 成功后目标角色变为 `member`,保留群成员关系;服务端立即撤销管理权限并广播角色事件。
+- 删除群主、非成员或最后状态冲突分别返回稳定错误,不允许前端拼接通用成功。
+
+### 6.23 添加禁言成员.png
+
+
+
+**状态:新增。**
+
+- 候选:`GET /v1/im/groups/{topic}/members?mute_eligible=true&query=&cursor=`。
+- 提交:`POST /v1/im/groups/{topic}/mutes/batch`,Body `{"members":[{"user_id":"usr...","expires_at":null}],"reason":""}`。
+- 服务端校验操作者角色层级;管理员不能禁言群主,普通管理员互相操作规则由治理策略确定。
+
+### 6.24 禁言的群成员.png
+
+
+
+**状态:新增。**
+
+- `GET /v1/im/groups/{topic}/mutes?cursor=` 返回 `user,muted_by,muted_at,expires_at`;解除 `DELETE /v1/im/groups/{topic}/mutes/{user_id}`。
+- 禁言成员发送消息时服务端返回 `GROUP_MUTED` 和可选 `expires_at`;不能只禁用前端输入框。
+- 退群时清理禁言记录;过期由服务端自动失效并推送更新。
+
+### 6.25 选择新群主.png
+
+
+
+**状态:新增。**
+
+- `GET /v1/im/groups/{topic}/members?eligible_role=owner&query=&cursor=`,只返回状态正常的当前成员。
+- 前端单选并保存 `target_user_id`;当前群主、已退出、停用账号返回 `eligible=false`。
+- 此步骤不修改权限,真正转让仅在 6.26 确认后执行。
+
+### 6.26 选择新群主提示.png
+
+
+
+**状态:新增。**
+
+- `POST /v1/im/groups/{topic}/owner-transfer`,Body `{"target_user_id":"usr...","version":n}`,必须带幂等键和二次认证 `X-Action-Token`。
+- 服务端事务内把旧群主降为管理员或成员、目标升为群主、更新权限和审计,再生成系统事件。
+- 成功返回 `owner,previous_owner,roles,version`;并发成员变化返回 `GROUP_VERSION_CONFLICT`,前端重新确认,不自动重试。
+
+### 6.27 群文件.png
+
+
+
+**状态:新增群文件索引;底层文件存储已实现。**
+
+- `GET /v1/im/groups/{topic}/files?query=&type=&cursor=` 返回 `group_file_id,file_id,name,size,mime,uploader,created_at,download_state,permissions`。
+- 下载使用认证文件接口,服务端校验当前群成员和文件状态;删除 `DELETE /v1/im/groups/{topic}/files/{group_file_id}` 需上传者或管理员权限。
+- 退群后不可继续使用旧 URL 下载;缓存文件是否保留由客户端隐私策略处理。
+
+### 6.28 群文件-上传状态.png
+
+
+
+**状态:需改造。**
+
+- 先用 `purpose=group_file,topic=...` 上传,再 `POST /v1/im/groups/{topic}/files`,Body `{"file_id":"..."}` 建立群文件记录。
+- 服务端校验 `member_upload_files`、单文件上限、租户总存储和群成员身份;成功返回完整群文件项。
+- 上传失败可复用 `upload_id`;登记失败不得在列表显示,孤立文件按 TTL 回收。
+
+## 7. 文件下载、收藏管理与聊天记录检索
+
+### 7.1 我的收藏.png
+
+
+
+**状态:新增。**
+
+- `GET /v1/im/favorites?type=all|text|image|video|voice|file|link&q=&cursor=`,返回 `favorite_id,type,snapshot,source,collected_at,availability`。
+- 类型与关键词由服务端过滤,客户端不下载全部收藏再筛选。`source` 仅下发用户仍可知晓的会话摘要。
+- 收藏数据属于当前租户当前用户;跨租户登录不得合并列表。
+
+### 7.2 我的收藏(1).png
+
+
+
+**状态:新增。**
+
+- 详情:`GET /v1/im/favorites/{favorite_id}`;删除:`DELETE /v1/im/favorites/{favorite_id}`;转发使用 7.8。
+- 收藏创建时保存受控快照和附件引用,因此原消息对当前用户删除后仍可按保留策略展示;原资源因合规删除时返回 `availability:"removed"`。
+- 删除收藏不删除原消息或已转发消息。
+
+### 7.3 我的收藏-文件.png
+
+
+
+**状态:新增。**
+
+- 调用 7.1,固定 `type=file`;项中使用 `snapshot.file{name,size,mime,file_id}` 和 `availability`。
+- 点击前调用收藏详情获得当前授权,下载仍走 7.9 文件协议;失效显示原因,不反复请求公共 URL。
+
+### 7.4 我的收藏-文本.png
+
+
+
+**状态:新增。**
+
+- 调用 7.1,固定 `type=text`;服务端返回纯文本/结构化 Drafty 快照和安全高亮区间 `matches[]`。
+- 展开、折叠、复制为本地动作;复制前遵守企业 DLP 策略 `permissions.can_copy`。
+
+### 7.5 我的收藏-语音.png
+
+
+
+**状态:新增。**
+
+- 调用 7.1,固定 `type=voice`;播放前获取认证文件,返回 `duration_ms,waveform,availability`。
+- 播放/暂停和单实例播放器是客户端状态;服务端不接收逐秒播放进度,除非后续定义跨端收听状态。
+
+### 7.6 我的收藏—链接.png
+
+
+
+**状态:新增。**
+
+- 调用 7.1,固定 `type=link`;服务端快照返回 `url,title,summary,thumbnail_file_id,risk_level`。
+- 点击前 `POST /v1/im/security/urls/check`,Body `{"url":"...","source":"favorite"}`;返回 `allow|warn|block` 和 `risk_challenge_id`。
+- 客户端不得在未确认时自动打开或预加载高风险外链。
+
+### 7.7 收藏—文件(多选).png
+
+
+
+**状态:新增。**
+
+- 选择状态本地维护;批量删除 `POST /v1/im/favorites/batch-delete`,Body `{"favorite_ids":[]}`;批量转发见 7.8。
+- 返回逐项成功/失败;删除必须二次确认并带幂等键。跨页选择只保存 ID,不保留失效的临时下载地址。
+
+### 7.8 收藏文件—分享给好友页面.png
+
+
+
+**状态:新增。**
+
+- 目标列表使用 4.17;提交 `POST /v1/im/favorites/forward`,Body `{"favorite_ids":[],"target_topics":[],"comment":""}`。
+- 服务端从收藏的受控附件引用生成新消息和新 ACL,返回每个目标的消息结果;收藏原件不变。
+- 文件失效、目标禁言或无发送权限按目标逐项失败,不允许客户端直接复用旧下载 URL 发送。
+
+### 7.9 文件-下载.png
+
+
+
+**状态:底层下载已实现租户校验;资源 ACL 需改造。**
+
+- 客户端使用 `GET /v0/file/s/{file_id}`,Header 传 API Key 与 `X-Tinode-Auth: token `;禁止 `?auth=&secret=`。
+- 服务端除 Token 租户外,还必须校验该用户通过消息、群文件或收藏拥有有效引用,并支持 `Range`、`ETag`、内容长度和安全文件名。
+- 下载前客户端校验本地空间和网络策略;`FILE_FORBIDDEN` 不自动重试,`FILE_UNAVAILABLE` 显示失效。
+
+### 7.10 文件-下载中.png
+
+
+
+**状态:客户端任务管理;服务端需支持 Range。**
+
+- 下载任务保存 `file_id,etag,total_bytes,downloaded_bytes,temp_path,state`;恢复时携带 `Range` 与 `If-Range: `。
+- ETag 改变或服务端返回完整 200 时丢弃旧分片重新下载;取消时关闭连接并清理临时文件。
+- Token 过期先完成一次刷新后续传,不能把 Token 写入任务文件或日志。
+
+### 7.11 文件-下载完成.png
+
+
+
+**状态:纯客户端完成态。**
+
+- 完成后校验长度和服务端提供的 checksum,再原子移动到受控目录;校验失败删除并标记重试。
+- 打开/分享遵守系统沙箱和企业 DLP。每次展示页面检查本地文件存在性,不存在则恢复“可下载”。
+
+### 7.12 查找聊天记录-搜索关键词选中状态(点击后跳出对应聊天内容).png
+
+
+
+**状态:新增服务端搜索。**
+
+- `GET /v1/im/topics/{topic}/messages/search?q=&type=text&cursor=` 返回 `seq,from,ts,snippet,matches[]`。
+- 点击结果后 `GET /v1/im/topics/{topic}/messages/{seq}/context?before=20&after=20`,返回锚点及上下文;客户端定位 `seq` 并高亮。
+- 服务端只搜索当前用户可见且未硬删除的消息,关键词做长度/频率限制,搜索日志不得记录原文。
+
+### 7.13 查找聊天记录-文件.png
+
+
+
+**状态:新增。**
+
+- 调用 `GET /v1/im/topics/{topic}/messages/search?type=file&q=&cursor=`,返回消息 `seq` 与文件快照、发送者、时间、`availability`。
+- 预览/下载使用 7.9;撤回或无授权时显示失效。点击来源用 7.12 上下文接口定位。
+
+### 7.14 查找聊天记录-链接.png
+
+
+
+**状态:新增。**
+
+- 调用搜索接口 `type=link`,返回 `url,title,summary,seq,ts,risk_level`;打开前执行 7.6 URL 安全检查。
+- 链接预览由服务端受控抓取和缓存,客户端不直接抓取内网地址,防止 SSRF 和隐私泄露。
+
+### 7.15 查看聊天记录-图片.png
+
+
+
+**状态:新增。**
+
+- `GET /v1/im/topics/{topic}/media?type=image&cursor=` 返回 `seq,thumbnail_file_id,file_id,width,height,ts,availability`。
+- 缩略图懒加载使用认证文件请求;大图前再次授权。多选仅保存 `(topic,seq)`,操作使用第 4 章批量接口。
+
+### 7.16 查看聊天记录-视频.png
+
+
+
+**状态:新增。**
+
+- 调用 7.15 接口,`type=video`,额外返回 `duration_ms,mime,size`。
+- 播放器使用支持 Range 的认证下载/流式读取;文件失效、Token 过期和网络中断分别映射可理解状态,返回列表保持游标与滚动位置。
+
+## 8. 实时语音与视频通话
+
+### 8.1 各种通话提示状态.png
+
+
+
+**状态:已实现基础通话记录替换;产品文案映射需统一。**
+
+- 发起呼叫时的持久消息就是通话记录。服务端以相同 `seq` 写 replacement `{data}`,`head.replace=":{seq}"`,并把 `head.webrtc` 更新为 `accepted|finished|disconnected|missed|declined`。
+- 已接通结束时 `head.webrtc-duration` 为服务端计算的毫秒数。客户端映射:`finished=已结束`、`missed=未接听/已取消`、`declined=已拒绝`、`disconnected=异常结束`。
+- 双方使用原消息 `content.callId/callType` 和同一 `seq`;客户端不得另发一条“通话结果”消息或相信本地传入的 duration。
+
+### 8.2 对方状态.png
+
+
+
+**状态:在线邀请已实现;Push 与产品鉴权需改造。**
+
+- 来电是 Topic 下行的初始 `{data}`:`head.webrtc="started"`、`head.x-im-type="call"`,`content` 至少含 `callId,callType,audio,video`。客户端保存该消息 `seq` 作为后续所有 call note 的关联 ID。
+- 来电端开始响铃后发送 `{note:{topic,what:"call",event:"ringing",seq,payload:{"callId":"..."}}}`;接听发送 `event:"accept"`;拒绝发送 `event:"hang-up"`。
+- 后台 Push 只携带 `callId,topic,seq,caller_display,callType,expires_at`,不含 SDP、ICE 或 Token;用户操作后必须回 WebSocket 鉴权。
+
+### 8.3 语音通话-对方待接受状态.png
+
+
+
+**状态:发起与超时已实现基础;好友/拉黑/租户开关校验需改造。**
+
+- 发起前查询 `capabilities.can_call`,再发布持久邀请消息:
+
+```json
+{"pub":{"id":"call-1","topic":"p2p...","head":{"mime":"application/json","webrtc":"started","x-im-type":"call"},"content":{"callId":"01K...","callType":"audio","audio":true,"video":false}}}
+```
+
+- `{ctrl code:202,params.seq}` 的 `seq` 是后续 `ringing/accept/offer/answer/ice-candidate/hang-up` 必填关联值。服务端只允许 P2P 双方参与,并校验好友、拉黑、租户通话开关和当前 Topic 无并发呼叫。
+- 发起方取消发送 `{note event:"hang-up",seq,payload:{"callId":"...","reason":"cancelled"}}`;等待超时由服务端结束并把原消息替换为 `missed`。
+
+### 8.4 语音通话-双方语音沟通中.png
+
+
+
+**状态:WebRTC 客户端能力;信令和服务端计时已实现。**
+
+- 被叫发送 `accept` 后,服务端把原消息替换为 `webrtc:"accepted"` 并向主叫发送 `{info what:"call",event:"accept",seq}`。
+- 双方随后用 `{note what:"call",event:"offer|answer|ice-candidate",seq,payload:{"callId":"...",...}}` 交换 SDP/ICE。ICE 配置使用 `hi.ctrl.params.iceServers`,不得在 App 中硬编码 TURN 长期凭证。
+- 任一端发送 `hang-up`,服务端按接受时间计算 duration 并替换原通话消息。异常断线后按原 `topic+seq+callId` 恢复或结束,不新建第二通呼叫。
+
+### 8.5 语音通话-麦克风与扬声器已关.png
+
+
+
+**状态:纯客户端设备控制。**
+
+- 麦克风关闭通过禁用本地音轨,扬声器关闭/听筒切换通过系统音频路由完成,不调用业务 HTTP。
+- UI 必须读取实际音轨和路由状态;权限撤销时强制更新。是否告知对方静音可发送临时 `media-state`,不落聊天记录。
+
+### 8.6 语音通话-麦克风与扬声器已开.png
+
+
+
+**状态:纯客户端设备控制。**
+
+- 开启麦克风恢复本地音轨,开启扬声器选择实际输出设备;蓝牙/耳机变化以操作系统回调为准。
+- 不把设备名称、系统音量上传服务端。切换失败恢复真实按钮状态并给本地提示。
+
+### 8.7 视频通话—等待对方接受接受邀请中.png
+
+
+
+**状态:基础通话信令已实现;隐私约束需落实。**
+
+- 发起协议同 8.3,`media_type:"video"`。本地预览只在设备显示,接听前媒体轨默认不发给对方,SDP 只声明能力。
+- 切换摄像头纯客户端;取消发送 `hang-up:cancelled`。相机权限不足时改为音频呼叫或取消,不伪装视频已开启。
+
+### 8.8 视频通话—等待对方接受接受邀请-摄像头关闭.png
+
+
+
+**状态:纯客户端状态;媒体状态事件需改造。**
+
+- 关闭本地视频轨并显示头像;接听后沿用 `video_enabled=false`。
+- 当前服务端只允许 `ringing/accept/offer/answer/ice-candidate/hang-up`;首版应直接通过 WebRTC 轨道 mute 状态呈现。若后续新增 `media-state`,必须先修改 `server/calls.go` 事件白名单、参与者校验和限频,客户端不能提前发送未知事件。
+
+### 8.9 双方视频通话中.png
+
+
+
+**状态:WebRTC 客户端能力;服务端信令已具备基础。**
+
+- 主/小窗布局、拖动和横竖屏纯客户端;WebRTC offer/answer/ICE 与 8.2~8.4 相同。
+- 服务端只做身份鉴权、信令路由、TURN 配置和通话审计,不接收前端伪造的通话时长。
+- 网络恶化可本地降码率或关闭视频轨;最终结束必须发 `hang-up`。服务端未支持 `media-state` 前不得发送该事件。
+
+### 8.10 双方视频通话中-对方关闭摄像头状态.png
+
+
+
+**状态:现阶段纯客户端;可选媒体状态事件需改造。**
+
+- 以对端 WebRTC 视频轨的 mute/unmute 作为真实信号;关闭时显示资料缓存中的对方头像并保留音频,轨道恢复后自动显示视频。
+- 不需要业务 HTTP。可选 `media-state` 必须等服务端扩展事件白名单后另行启用,且不持久化、不改变通话结果。
+
+### 8.11 视频与语音通话悬浮状态中.png
+
+
+
+**状态:纯客户端。**
+
+- 最小化不结束 WebRTC 或 WebSocket,会话仍以同一 `call_id` 运行;点击恢复全屏不发送新 invite。
+- 系统悬浮窗权限、画中画和安全区域按平台实现。收到 `hang-up`、Token 失效或媒体会话结束后立即关闭悬浮窗并落正确结果态。
+
+## 9. 企业工作台与办公协同
+
+### 9.1 工作台.png
+
+
+
+**状态:新增。**
+
+- `GET /v1/im/workbench` 返回 `modules[{code,name,icon_file_id,route,unread_count,enabled,permission}]`、布局版本和企业配置。
+- 服务端按当前 Token 的租户、组织角色和功能开关裁剪;前端只接受白名单内部 `route`,禁止直接执行服务端任意 URL。
+- 未读数通过 `workbench.badge.updated` 系统事件增量更新;切租户必须重新加载,不复用另一企业工作台。
+
+### 9.2 部门组织框架.png
+
+
+
+**状态:新增。**
+
+- 根节点:`GET /v1/im/organization/departments?parent_id=root&cursor=`;子部门同接口传 `parent_id`;成员 `GET /v1/im/organization/departments/{id}/members?cursor=`。
+- 部门返回 `department_id,name,parent_id,child_count,member_count,version`;成员只返回当前查看人可见字段。
+- 全局搜索:`GET /v1/im/organization/search?q=&types=department,user&cursor=`。服务端执行组织范围授权,离职用户标记不可联系或从结果移除。
+
+### 9.3 会议通知-发起人状态.png
+
+
+
+**状态:新增。**
+
+- 创建:`POST /v1/im/meetings`,Body `{"title":"...","start_at":"...","end_at":"...","location":"...","attendee_user_ids":[],"reminder_minutes":[]}`。
+- 详情:`GET /v1/im/meetings/{meeting_id}` 返回会议、参会人的 `response=pending|accepted|declined|tentative`、权限和 `version`。
+- 更新/取消:`PATCH /v1/im/meetings/{id}`、`POST /v1/im/meetings/{id}/cancel`;提醒未响应者 `POST /v1/im/meetings/{id}/reminders`。所有写操作带幂等键并生成通知和审计。
+
+### 9.4 会议通知-接收人状态.png
+
+
+
+**状态:新增。**
+
+- 详情同 9.3;响应:`PUT /v1/im/meetings/{meeting_id}/my-response`,Body `{"response":"accepted|declined|tentative","version":n}`。
+- 返回我的最终响应和会议版本,并实时通知发起人;重复提交同状态幂等。
+- 会议过期返回 `MEETING_CLOSED` 并只读展示;客户端本地日历写入是可选系统动作,不替代服务端响应。
+
+### 9.5 日报.png
+
+
+
+**状态:新增。**
+
+- `GET /v1/im/reports/daily?scope=mine&from=&to=&status=&cursor=` 返回 `report_id,report_date,status,draft_updated_at,submitted_at,read_state`。
+- 新建草稿 `POST /v1/im/reports/daily`,Body `{"report_date":"2026-08-11"}`;同一用户同一天唯一,重复创建返回原草稿。
+- `permissions.can_create` 和可补交日期由服务端返回,前端不自行判断工作日规则。
+
+### 9.6 日报-普通用户页面.png
+
+
+
+**状态:新增。**
+
+- 编辑:`PUT /v1/im/reports/daily/{id}/draft`,Body `{"content":"...","attachments":[],"version":n}`;提交 `POST /v1/im/reports/daily/{id}/submit`。
+- 服务端返回自动保存版本,客户端用 2~5 秒防抖且串行保存;版本冲突返回 `REPORT_VERSION_CONFLICT` 并给最新版本。
+- 提交后按 `editable_until` 决定是否可修改;附件使用 `purpose=daily_report` 上传并做权限绑定。
+
+### 9.7 日报-管理员页面.png
+
+
+
+**状态:新增。**
+
+- `GET /v1/im/reports/daily/statistics?date=&department_id=` 返回 `expected_count,submitted_count,missing_count` 和组织权限范围。
+- 明细:`GET /v1/im/reports/daily?scope=managed&date=&status=submitted|missing&department_id=&cursor=`。
+- 提醒:`POST /v1/im/reports/daily/reminders`,Body `{"date":"...","user_ids":[]}`;服务端过滤无管理权限对象并限流,返回逐项结果。
+
+### 9.8 日报-查看详情.png
+
+
+
+**状态:新增。**
+
+- `GET /v1/im/reports/daily/{report_id}` 返回填报人、日期、内容、附件、状态、版本、修改时间和 `permissions`。
+- 管理员查看时服务端记录必要的已读状态;附件下载复用第 7 章 ACL。
+- 撤回或修订使用专用状态和版本,客户端必须显示最新版本与 `revised_at`,不能混合旧缓存。
+
+### 9.9 月报(1).png
+
+
+
+**状态:新增。**
+
+- 创建/取得当月草稿:`POST /v1/im/reports/monthly`,Body `{"month":"2026-08"}`。
+- 保存:`PUT /v1/im/reports/monthly/{id}/draft`,Body `{"work_summary":"...","next_plan":"...","attachments":[],"version":n}`;提交 `POST /v1/im/reports/monthly/{id}/submit`。
+- 字段必填、最大长度、提交截止时间由响应 `validation` 提供;自动保存和冲突处理同 9.6。
+
+### 9.10 月报.png
+
+
+
+**状态:新增。**
+
+- `GET /v1/im/reports/monthly?scope=mine|managed&from_month=&to_month=&status=&cursor=` 返回月份、填报人、状态和时间。
+- 管理员统计 `GET /v1/im/reports/monthly/statistics?month=&department_id=`;普通用户响应中不下发团队统计入口权限。
+- 未提交项进入 9.9,已提交项进入 9.11,路由由返回 `action=edit|view` 决定。
+
+### 9.11 月报-详情页.png
+
+
+
+**状态:新增。**
+
+- `GET /v1/im/reports/monthly/{report_id}` 返回 `month,author,work_summary,next_plan,attachments,status,submitted_at,updated_at,version,permissions`。
+- `permissions.can_edit=true` 时进入草稿更新;否则只读。修改已提交月报需专用 `POST /v1/im/reports/monthly/{id}/reopen` 或审核流程,不直接覆盖。
+
+### 9.12 总经理信箱.png
+
+
+
+**状态:新增。**
+
+- 页面配置:`GET /v1/im/executive-mailbox/config` 返回 `enabled,allow_anonymous,required_fields,max_length,attachment_policy,notice`。
+- 提交:`POST /v1/im/executive-mailbox/submissions`,Body `{"subject":"...","content":"...","anonymous":false,"attachment_file_ids":[]}`,带幂等键。
+- 成功仅返回 `submission_id,status:"received",submitted_at`;匿名模式下业务接收方不见身份,但安全审计是否保留内部追踪按企业政策执行并提前告知。
+
+### 9.13 总经理信箱-文案提示.png
+
+
+
+**状态:新增配置接口。**
+
+- 文案使用 9.12 配置中的 `notice{title,content,updated_at}`,由租户后台配置;客户端不得硬编码实名/匿名承诺。
+- 提交按钮前必须展示当前版本文案;请求可携带 `notice_version`,服务端发现关键政策已变更时返回 `NOTICE_RECONFIRM_REQUIRED`。
+
+## 10. 个人资料、通用设置、隐私与青少年模式
+
+### 10.1 我的.png
+
+
+
+**状态:基础用户资料已有;产品菜单新增。**
+
+- `GET /v1/im/me` 返回公开账号资料、实名/青少年模式摘要和资料版本;`GET /v1/im/bootstrap` 返回个人菜单与权限。
+- 头像、昵称更新事件到达后刷新头部;收藏和设置入口分别进入第 7、10 章。
+- 账号 ID 按字符串展示,客户端不得展示内部数据库 ID 或 tenant_id。
+
+### 10.2 个人信息.png
+
+
+
+**状态:Tinode `me` 资料更新已有基础;目标产品接口需改造。**
+
+- 查询 `GET /v1/im/me/profile` 返回 `avatar_file_id,nickname,birth_date,signature,gender,region,public_account,version,field_rules`。
+- 更新统一使用 `PATCH /v1/im/me/profile`,仅提交变更字段并带 `version`;服务端更新后同步 Tinode `me.public` 并推送资料事件。
+- 头像先以 `purpose=user_avatar` 上传再 PATCH;任何字段的可见性由隐私策略裁剪。
+
+### 10.3 设置昵称.png
+
+
+
+**状态:需改造。**
+
+- `PATCH /v1/im/me/profile`,Body `{"nickname":"新昵称","version":n}`。
+- 服务端校验 Unicode 长度、全空白、控制字符和敏感内容;返回更新后的 `nickname,version,updated_at`。
+- 成功刷新全局资料和联系人快照,但不得覆盖各群昵称。版本冲突先拉最新资料再由用户确认。
+
+### 10.4 出生日期.png
+
+
+
+**状态:新增产品字段。**
+
+- 保存同 10.3,Body `{"birth_date":"2000-01-02","version":n}`;仅传 ISO 日期,不传本地化文本或时区时间戳。
+- 服务端校验不晚于当前租户日期及合理范围,返回 `PROFILE_FIELD_INVALID`。展示格式由客户端本地化。
+
+### 10.5 个性签名.png
+
+
+
+**状态:需改造。**
+
+- 保存同 10.3,Body `{"signature":"...","version":n}`;空字符串表示清空。
+- 服务端按 `field_rules.signature.max_length` 和内容审核处理;失败保留本地草稿,成功刷新引用页面。
+
+### 10.6 二维码.png
+
+
+
+**状态:图标纯客户端;二维码协议新增。**
+
+- 点击入口进入 5.4 并创建短期个人二维码。每次主动刷新调用 `POST /v1/im/me/qrcodes` 获取新 Token。
+- 二维码不能使用静态 `user_id`、手机号或登录 Token;过期、撤销和切租户后必须重新生成。
+
+### 10.7 设置(iOS).png
+
+
+
+**状态:菜单纯客户端;数据接口部分新增。**
+
+- 菜单来自 `bootstrap.settings_menu`,安全、通知、隐私、通用、关于按权限展示。系统通知权限状态由 iOS API 读取,不由服务端伪造。
+- 退出:`POST /v1/im/auth/logout` 撤销当前 refresh/access token 和设备会话,成功或 Token 已失效后关闭 WS、清安全存储和本租户用户缓存。
+- 多设备退出使用 `DELETE /v1/im/security/sessions/{session_id}`,需二次验证。
+
+### 10.8 设置(安卓).png
+
+
+
+**状态:菜单纯客户端;业务协议同 10.7。**
+
+- 使用相同 `bootstrap.settings_menu` 与退出协议;Android 系统权限、通知渠道和应用更新使用平台实现。
+- 服务端只保存应用内通知偏好与设备 Push Token,不把系统设置页面是否打开当作已授权。
+
+### 10.9 清空缓存的提示.png
+
+
+
+**状态:纯客户端。**
+
+- 确认后只删除缩略图、可重下文件、临时上传下载和搜索缓存;不调用服务端删除消息、收藏或账号接口。
+- 不删除安全存储中的登录凭证,除非用户选择退出登录。完成后重新计算本地占用并显示结果。
+
+### 10.10 版本更新页面.png
+
+
+
+**状态:新增 App 配置接口。**
+
+- `GET /v1/im/app/releases/latest?platform=ios|android¤t_version=1.0.0&channel=stable`,未登录可在企业解析后调用。
+- 返回 `latest_version,min_supported_version,update_type=none|optional|required,release_notes,store_url,package_url,checksum,published_at`。
+- 客户端按平台只打开受信任商店/签名下载地址;Android 包必须校验签名和 checksum,服务端 URL 使用允许域名。
+
+### 10.11 发现新版本.png
+
+
+
+**状态:纯客户端弹窗;数据来自 10.10。**
+
+- `optional` 显示稍后/更新,记录本地已忽略版本并按服务端 `remind_after` 再提示;`required` 不显示绕过入口。
+- 下载和安装状态由平台管理;接口异常时不得误判为强制更新,除非本地已有有效签名的最低版本策略缓存。
+
+### 10.12 隐私设置.png
+
+
+
+**状态:新增。**
+
+- `GET /v1/im/me/privacy` 返回 `add_me_methods,profile_visibility,last_seen_visibility,read_receipts,version` 和各字段 `editable`。
+- `PATCH /v1/im/me/privacy` 只提交变更字段和 `version`;成功返回完整最新配置并向其他设备推送 `privacy.updated`。
+- 黑名单入口使用 5.12。服务端在搜索、主页、回执等实际读写路径执行隐私规则,不能只保存开关。
+
+### 10.13 隐私设置-添加我的方式选项.png
+
+
+
+**状态:新增。**
+
+- `add_me_methods` 固定字段 `account,phone,qr,group`,每项为布尔值;更新用 10.12 PATCH,如 `{"add_me_methods":{"phone":false},"version":n}`。
+- 关闭后对应搜索/二维码/群来源申请由服务端统一返回 `FRIEND_REQUEST_NOT_ALLOWED` 或 `USER_NOT_DISCOVERABLE`,不泄露具体关闭项。
+- 企业强制策略可令字段 `editable=false`,前端禁用并显示服务端提供的通用说明。
+
+### 10.14 青少年模式未开启(安卓).png
+
+
+
+**状态:新增。**
+
+- `GET /v1/im/settings/youth-mode` 返回 `enabled,policy_version,restrictions,password_set,can_enable`。
+- 点击开启先展示 10.15,再按 1.7 完成身份验证和 1.11 设置独立密码;最后调用 `POST /v1/im/settings/youth-mode/enable`,Body `{"policy_version":n,"action_token":"..."}`。
+- 服务端持久化到用户租户范围并推送所有设备,不能仅设置本地标志。
+
+### 10.15 青少年模式提示(安卓).png
+
+
+
+**状态:弹窗纯客户端;文案由服务端配置。**
+
+- 使用 10.14 返回的 `restrictions` 与 `policy_version` 渲染;取消不请求,确认进入密码设置/验证。
+- 最终 enable 请求携带用户确认的 `policy_version`;若策略更新返回 `POLICY_RECONFIRM_REQUIRED`,重新展示新内容。
+
+### 10.16 青少年模式已开启(安卓).png
+
+
+
+**状态:新增。**
+
+- 页面调用 10.14,`enabled=true` 时展示 `enabled_at,restrictions`。`bootstrap` 同时返回受限模块和动作。
+- 受限接口服务端返回 `YOUTH_MODE_RESTRICTED`;前端隐藏只是体验优化,不能作为安全控制。
+- 登录、App 重启和另一设备均从服务端恢复状态;收到 `youth_mode.updated` 即刷新。
+
+### 10.17 关闭青少年模式(安卓).png
+
+
+
+**状态:新增。**
+
+- 先调用 1.11 的独立密码验证,获得 `action_token`;再 `POST /v1/im/settings/youth-mode/disable`,Header `X-Action-Token`,带幂等键。
+- 成功返回 `enabled:false,updated_at` 并广播;连续密码错误按服务端 `retry_after_seconds` 锁定。
+- 忘记密码走 1.7 身份验证后重置,不能绕过密码直接修改本地状态。
+
+### 10.18 获取推送权限(安卓).png
+
+
+
+**状态:权限引导纯客户端;设备注册需改造。**
+
+- 用户确认后调用 Android 系统权限;拒绝状态本地记录,最终以系统回调为准。
+- 获取 Push Token 后 `PUT /v1/im/devices/{device_id}/push-token`,Body `{"provider":"fcm","token":"...","app_version":"..."}`;Token 轮换时覆盖。
+- 退出登录删除/解绑该用户设备 Token。Push Token 属敏感凭证,不写日志。
+
+### 10.19 提醒(安卓).png
+
+
+
+**状态:纯客户端。**
+
+- 仅在功能因系统权限不可用时显示;取消和“前往设置”不调用业务服务端。
+- 回到应用后重新读取真实权限,不能因用户打开过设置页就假定授权成功。
+
+### 10.20 相机说明(安卓).png
+
+
+
+**状态:纯客户端。**
+
+- 首次拍照、扫码或视频通话前展示用途说明,确认后再触发系统权限;拒绝不发媒体/扫码请求。
+- 永久拒绝时提供系统设置入口;允许从相册选择等替代能力。客户端不得上传系统权限状态作为服务端授权依据。
+
+### 10.21 麦克风说明(安卓).png
+
+
+
+**状态:纯客户端。**
+
+- 首次录音或通话前说明并申请系统权限;未授权时不得创建录音文件或发送 call invite。
+- 永久拒绝后进入系统设置引导;恢复授权后重新执行 4.7 或第 8 章动作,不复用过期媒体会话。
+
+## 11. 反诈安全、举报与意见反馈
+
+### 11.1 反诈弹窗(安卓).png
+
+
+
+**状态:新增风控决策协议。**
+
+- 高风险动作在原业务接口返回 `409 RISK_CONFIRMATION_REQUIRED`,`details={risk_challenge_id,level,notice_code,notice_params,expires_at,action}`。
+- 客户端用 `notice_code` 映射经过审核的本地文案,不显示服务端规则、分数或命中特征;取消不重试原操作。
+- 低风险提示也可由可信 `security.risk_notice` 系统事件触发。普通消息不得伪装成系统反诈弹窗。
+
+### 11.2 反诈弹窗(二次提示安卓).png
+
+
+
+**状态:新增。**
+
+- 确认:`POST /v1/im/security/risk-challenges/{risk_challenge_id}/confirm`,Body `{"decision":"continue|cancel"}`,带幂等键。
+- `continue` 成功返回单次、短期 `risk_action_token`;客户端在重试原业务接口时以 `X-Risk-Action-Token` 携带,原请求幂等键保持不变。
+- 服务端可返回 `RISK_ACTION_BLOCKED` 直接阻断;确认、取消、过期均写审计,客户端不得无限创建 challenge 绕过。
+
+### 11.3 举报.png
+
+
+
+**状态:新增。**
+
+- 原因:`GET /v1/im/reports/abuse/reasons?target_type=user|group|message` 返回 `reason_code,label,requires_description,allows_evidence`。
+- 进入页面携带受控目标:用户 `user_id`、群 `topic` 或消息 `(topic,seq)`;客户端不提交 tenant_id 或被举报者任意文本身份。
+- 服务端先验证举报人能够看到目标,再返回适用原因;举报人身份不下发给被举报方。
+
+### 11.4 举报(1).png
+
+
+
+**状态:新增。**
+
+- 证据使用 `purpose=abuse_evidence` 上传;提交 `POST /v1/im/reports/abuse`,Body `{"target":{"type":"message","topic":"...","seq":"12"},"reason_code":"fraud","description":"...","evidence_file_ids":[]}`。
+- 服务端从目标消息保存必要、受控的审核快照,校验证据数量/MIME/大小,返回 `report_id,status:"received"`。
+- 重复举报按策略合并并返回原受理号或 `REPORT_RATE_LIMITED`;证据使用独立严格 ACL 和保留期。
+
+### 11.5 意见反馈.png
+
+
+
+**状态:新增。**
+
+- 配置:`GET /v1/im/feedback/config` 返回类别、最大正文、附件规则、是否允许联系方式和日志授权说明。
+- 提交:`POST /v1/im/feedback`,Body `{"category":"bug","content":"...","contact":"...","attachment_file_ids":[],"client_context":{"platform":"android","app_version":"1.0.0","os_version":"..."},"include_diagnostics":false}`。
+- 附件用 `purpose=feedback`;只有用户明确同意 `include_diagnostics=true` 才上传经过脱敏的诊断包。成功返回 `feedback_id,status:"received"`,重复提交由幂等键去重。
+
+## 12. 联调实施清单
+
+### 12.1 当前可直接联调的基础能力
+
+1. WebSocket `/v0/channels`、Wire Version `0.25` 和最低版本 `0.20`。
+2. 第一条 `hi` 携带企业码并固定 Session 租户;响应包含公开租户信息、限制、ICE 配置和呼叫超时。
+3. `basic`/`token` 登录,`sub/get/set/pub/leave/del/note` 基础 Tinode 协议。
+4. `pub` 成功返回 `202 + seq`,Topic 消息与基础回执同步。
+5. `/v0/file/u/` 和 `/v0/file/s/{file_id}` 的基础上传下载及租户存储隔离。
+
+### 12.2 必须优先完成的服务端阻塞项
+
+| 优先级 | 改造项 | 阻塞图片章节 |
+| --- | --- | --- |
+| P0 | `/v1/im` 统一认证中间件、响应封装、幂等键、租户 Principal | 全部 HTTP 产品能力 |
+| P0 | 企业码解析、验证码、密码重置、二次认证、Token `auth_version` 和刷新/撤销 | 1.1~1.11、10.7~10.8 |
+| P0 | 普通用户禁止发布系统 Topic;系统事件可信生产通道 | 2.5、3.1~3.5、6.4、11.1 |
+| P0 | 消息 `x-im-schema/type/client-mid` 校验和服务端幂等 | 2~4 章 |
+| P0 | 文件资源级 ACL、Range、ETag、用途绑定和租户存储配额 | 3.6、4.10~4.14、6.27~7.16、11.4~11.5 |
+| P1 | 好友申请、好友关系、黑名单状态机 | 第 3、5 章 |
+| P1 | 群成员、角色、禁言、公告、邀请审批和配额 | 第 6 章 |
+| P1 | 收藏、转发和聊天记录搜索 | 第 4、7 章 |
+| P1 | 通话媒体状态、结果消息、Push 来电 | 第 8 章 |
+| P2 | 工作台、组织、会议、日报、月报、信箱 | 第 9 章 |
+| P2 | 隐私、青少年模式、反诈、举报、反馈 | 第 10、11 章 |
+
+### 12.3 前后端联调最低验收用例
+
+| 编号 | 场景 | 预期结果 |
+| --- | --- | --- |
+| T-01 | A 企业 Token 连接 B 企业 Session | 登录失败 `TENANT_MISMATCH`,不产生用户/Topic 数据 |
+| T-02 | 企业停用后使用旧 Token 登录和请求 HTTP | WS/HTTP 均拒绝,客户端清除凭证并回企业入口 |
+| T-03 | 同 `client_mid` 断线重发同一消息 | 只落一条消息并返回同一 `seq` |
+| T-04 | 相同 HTTP 幂等键、相同请求体重复提交 | 返回第一次结果;不同请求体返回 `IDEMPOTENCY_CONFLICT` |
+| T-05 | 被拉黑或删除好友后绕过按钮直接 `pub` | 服务端拒绝,对方不收到消息 |
+| T-06 | 普通用户构造 `system_event` 或向系统 Topic 发布 | 服务端拒绝,系统消息页不展示可信样式 |
+| T-07 | 普通群成员调用管理员、禁言、转让群主接口 | 返回 `FORBIDDEN`,群状态不变且写安全审计 |
+| T-08 | 群成员数已达租户配置上限时同意两个并发申请 | 至多一个成功,成员数不超限 |
+| T-09 | 非消息接收者持同租户 Token 下载文件 | 返回 `FILE_FORBIDDEN`,不能只通过 file_id 下载 |
+| T-10 | 文件下载中 Token 过期后刷新并 Range 续传 | 文件完整、checksum 正确、不重复下载已完成分片 |
+| T-11 | 风控二次确认后重复重放 risk token | 只有首次有效,其余返回 Token 已消费 |
+| T-12 | 切换企业后查看缓存、上传任务、收藏和搜索 | 不出现上一租户数据或任务 |
+
+### 12.4 统一错误 reason
+
+首版客户端至少实现:`TENANT_UNAVAILABLE`、`TENANT_MISMATCH`、`AUTH_FAILED`、`TOKEN_EXPIRED`、`ACCOUNT_DISABLED`、`RATE_LIMITED`、`VALIDATION_FAILED`、`IDEMPOTENCY_CONFLICT`、`RESOURCE_NOT_FOUND`、`FORBIDDEN`、`FRIEND_RELATION_REQUIRED`、`MESSAGE_NOT_ALLOWED`、`GROUP_MUTED`、`GROUP_MEMBER_LIMIT_EXCEEDED`、`TENANT_USER_QUOTA_EXCEEDED`、`TENANT_GROUP_QUOTA_EXCEEDED`、`FILE_TOO_LARGE`、`FILE_FORBIDDEN`、`FILE_UNAVAILABLE`、`STORAGE_QUOTA_EXCEEDED`、`REALNAME_REQUIRED`、`YOUTH_MODE_RESTRICTED`、`RISK_CONFIRMATION_REQUIRED`、`RISK_ACTION_BLOCKED`。
+
+未知 `reason` 使用通用失败文案并记录 `request_id`,不得把服务端 `message` 原样展示给用户。
+
+### 12.5 完成定义
+
+每个图片小节进入联调前,服务端须提供 OpenAPI/JSON Schema、错误码、权限规则和可用测试租户;前端须提供请求日志脱敏、断线/重试行为和四态页面(加载、空、成功、失败)。联调完成必须逐图勾选对应协议、至少覆盖成功、权限拒绝、重复提交、断网恢复和跨租户五类场景。
diff --git a/docs/im/IM-多租户第5至12章代码改造与测试方案.md b/docs/im/IM-多租户第5至12章代码改造与测试方案.md
new file mode 100644
index 0000000..860920e
--- /dev/null
+++ b/docs/im/IM-多租户第5至12章代码改造与测试方案.md
@@ -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。
diff --git a/docs/im/IM-数据库变更汇总.sql b/docs/im/IM-数据库变更汇总.sql
new file mode 100644
index 0000000..e05dfec
--- /dev/null
+++ b/docs/im/IM-数据库变更汇总.sql
@@ -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';
diff --git a/docs/im/IM-服务端多租户改造方案.md b/docs/im/IM-服务端多租户改造方案.md
new file mode 100644
index 0000000..eabd8ac
--- /dev/null
+++ b/docs/im/IM-服务端多租户改造方案.md
@@ -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(...)`。这样才能让后续好友、群治理、企业工作台、搜索和安全业务从一开始就在正确的隔离边界内建设。
diff --git a/docs/im/IM-独立库多租户改造方案.md b/docs/im/IM-独立库多租户改造方案.md
new file mode 100644
index 0000000..e4f1ebc
--- /dev/null
+++ b/docs/im/IM-独立库多租户改造方案.md
@@ -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`;如后续需要升级老库,应单独重写迁移链。
diff --git a/docs/im/IM-现有实现与新需求差异对照总结.md b/docs/im/IM-现有实现与新需求差异对照总结.md
new file mode 100644
index 0000000..efcdf27
--- /dev/null
+++ b/docs/im/IM-现有实现与新需求差异对照总结.md
@@ -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 不再承担新业务适配与验收。
+
+改造顺序应先处理租户、消息幂等和文件授权,再完成好友与基础消息,随后完成群治理、搜索、通话与安全,最后建设企业工作台。这样可以最大限度复用现有实现,同时避免继续维护新产品不需要的入口和功能。
diff --git a/docs/im/IM-第一二章后续增强项.md b/docs/im/IM-第一二章后续增强项.md
new file mode 100644
index 0000000..b58999b
--- /dev/null
+++ b/docs/im/IM-第一二章后续增强项.md
@@ -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` 中对应章节状态。
diff --git a/docs/im/IM-第一至七章数据库增量审核.sql b/docs/im/IM-第一至七章数据库增量审核.sql
new file mode 100644
index 0000000..bb6c62b
--- /dev/null
+++ b/docs/im/IM-第一至七章数据库增量审核.sql
@@ -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 逐项补齐。
diff --git a/docs/im/IM-系统设计文档.md b/docs/im/IM-系统设计文档.md
new file mode 100644
index 0000000..7c53433
--- /dev/null
+++ b/docs/im/IM-系统设计文档.md
@@ -0,0 +1,898 @@
+# IM 系统设计文档
+
+> 版本:v1.1
+> 日期:2026-08-11
+> 状态:后续开发基线
+> 需求依据:[IM 产品原型需求文档](IM-产品原型需求文档.md)
+> 关联文档:[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 后生成只读 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 回到聊天体验中。
+
+多租户、消息幂等、文件授权、系统消息和审计是所有功能之前的基础工程。好友、群治理、收藏搜索、通话、工作台和安全合规应按阶段推进,每个阶段都必须同时完成数据模型、接口、权限、通知和测试闭环。
diff --git a/docs/im/images/@人员—完成.png b/docs/im/images/@人员—完成.png
new file mode 100644
index 0000000..ff0cf07
Binary files /dev/null and b/docs/im/images/@人员—完成.png differ
diff --git a/docs/im/images/@人员—选择提醒人.png b/docs/im/images/@人员—选择提醒人.png
new file mode 100644
index 0000000..e15a9e2
Binary files /dev/null and b/docs/im/images/@人员—选择提醒人.png differ
diff --git a/docs/im/images/@人员状态.png b/docs/im/images/@人员状态.png
new file mode 100644
index 0000000..d0491d4
Binary files /dev/null and b/docs/im/images/@人员状态.png differ
diff --git a/docs/im/images/上传文件.png b/docs/im/images/上传文件.png
new file mode 100644
index 0000000..81ccb2d
Binary files /dev/null and b/docs/im/images/上传文件.png differ
diff --git a/docs/im/images/个人信息.png b/docs/im/images/个人信息.png
new file mode 100644
index 0000000..e8a9b96
Binary files /dev/null and b/docs/im/images/个人信息.png differ
diff --git a/docs/im/images/个人对话-发各种文件状态(1).png b/docs/im/images/个人对话-发各种文件状态(1).png
new file mode 100644
index 0000000..0314844
Binary files /dev/null and b/docs/im/images/个人对话-发各种文件状态(1).png differ