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 + +个人对话框(对方的状态)(1) + +功能说明: + +展示对方在会话中的状态文案与单条对方消息。会话头部需展示昵称和在线状态,消息时间轴需保留系统提示,用于表达好友关系变化。 + +### 3.3 个人对话框(我的状态).png + +个人对话框(我的状态) + +功能说明: + +展示当前用户发出的好友申请或首条消息,气泡位于右侧并配置本人头像。发送成功后应展示业务状态文案,避免用户在等待审批时重复发起。 + +### 3.4 个人对话框(我的状态)(1).png + +个人对话框(我的状态)(1) + +功能说明: + +表现我方已发送单条消息及其回执状态。消息气泡应在发送中、已发送、已送达、已读和失败之间平滑更新,且不因状态文字改变气泡主体位置。 + +### 3.5 开启沟通页面.png + +开启沟通页面 + +功能说明: + +展示好友关系建立后的沟通开始页,包含系统提示“已成为好友”及首条正常消息。审批通过后应自动解锁输入区和媒体发送能力,同时保留申请上下文。 + +### 3.6 双方沟通状态.png + +双方沟通状态 + +功能说明: + +展示双方在会话中发送语音、图片或视频消息时的状态,包含加载/上传进度与时长。媒体消息应使用稳定占位区,上传失败时支持重试,成功后可直接预览或播放。 + +### 3.7 个人对话—拉黑状态/已读状态.png + +个人对话—拉黑状态/已读状态 + +功能说明: + +展示消息已读回执以及因拉黑导致的发送失败提示。被拉黑后的消息应标记红色失败图标,不进入对方会话;界面需为用户提供可理解的失败原因。 + +### 3.8 个人对话—删除状态.png + +个人对话—删除状态 + +功能说明: + +表现删除好友或关系解除后的发送失败。系统应阻止继续发送,将已失败消息保留为本地状态,并提示需重新添加好友或发起验证。 + +### 3.9 个人对话-键盘收起状态.png + +个人对话-键盘收起状态 + +功能说明: + +展示输入键盘和附加面板都收起时的标准单聊布局。底部保留语音、文本输入、表情和更多功能入口,且页面高度变化不应影响用户当前阅读位置。 + +### 3.10 个人对话-发各种文件状态.png + +个人对话-发各种文件状态 + +功能说明: + +展示图片、视频、音频和文件附加面板。用户选择类型后进入对应系统选择器或采集界面,应在上传前校验格式、大小和会话发送权限。 + +### 3.11 个人对话-发各种文件状态(1).png + +个人对话-发各种文件状态(1) + +功能说明: + +展示文本、名片和文件消息混合存在的会话效果。消息列表应根据消息类型使用不同卡片,但统一时间、头像、发送状态和长按操作规则。 + +### 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 + +内容沟通对话3-语音录音中 + +功能说明: + +在群聊背景中展示语音正在录制的中央浮层。录制期间应阻止重复录音或发送其他媒体,并向话题成员发送可选的“正在录音”临时状态。 + +### 4.10 内容沟通对话3-图片和语音发送中.png + +内容沟通对话3-图片和语音发送中 + +功能说明: + +展示图片与语音消息上传中的加载指示。每条消息应独立维护上传进度,并允许多个上传任务有序执行;离开会话后任务仍应可恢复或给出失败提示。 + +### 4.11 内容沟通对话3-语音/视频/图片状态.png + +内容沟通对话3-语音/视频/图片状态 + +功能说明: + +展示群聊中语音时长卡片、视频缩略图和图片预览等多媒体消息。语音支持播放进度与未听标记,视频支持封面和播放按钮,图片支持大图预览。 + +### 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 + +添加好友(1) + +功能说明: + +展示个人二维码名片,包含头像、昵称、账号 ID、二维码、“分享好友”和“保存到相册”。二维码应包含带签名或有效期的用户标识,防止被篡改或长期滥用。 + +### 5.5 添加好友-输入精准用户ID.png + +添加好友-输入精准用户ID + +功能说明: + +输入精确手机号或用户 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 + +黑名单提示(1) + +功能说明: + +开启拉黑时弹出确认对话框,说明将不再收到对方消息。确认后更新黑名单和会话 ACL,取消则不改变任何状态;重复操作应具备幂等性。 + +### 5.14 黑名单提示.png + +黑名单提示 + +功能说明: + +原图文案为“清除聊天记录”确认,属于聊天设置的危险操作弹窗。确认后应仅清除当前用户可见的记录,不影响对方历史;文件名与画面语义不一致,实施时应以画面交互为准。 + +## 6. 群聊创建、资料维护与成员治理 + +### 6.1 群聊.png + +群聊 + +功能说明: + +群聊列表按“我创建的群、我管理的群、我加入的群”分类展示,列表项包含群头像、群名称及成员数量。页面提供搜索与创建群聊入口,切换分类时应保留各自的列表状态。 + +### 6.2 空白页.png + +空白页 + +功能说明: + +当所选群聊分类没有数据时展示统一空状态,避免出现无内容的纯白页面。空状态应与当前分类绑定,后续加入或创建群聊后自动刷新。 + +### 6.3 加入群聊.png + +加入群聊 + +功能说明: + +用户收到群邀请时,会话内展示包含群头像、群名称和邀请人的群聊卡片,并提供“立即加入”操作。加入前应校验群状态、邀请有效期及是否需要管理员审核,处理完成后更新卡片状态。 + +### 6.4 内容沟通对话3-开启进群验证与拉人状态.png + +内容沟通对话3-开启进群验证与拉人状态 + +功能说明: + +开启进群验证后,群聊以系统消息展示成员邀请、申请通过和加入群聊等事件。系统消息需明确操作者、目标成员与处理结果,并保证所有群成员看到一致的时间线。 + +### 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 + +设置昵称(1) + +功能说明: + +该页面用于设置用户在当前群内的昵称,提供文本输入、字数计数和完成按钮。群昵称只影响当前群的成员列表和消息发送者名称,不应修改账号全局昵称。 + +### 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 + +添加管理员-设置成功(1) + +功能说明: + +该状态展示从成员选择页完成管理员设置后的结果反馈。提交期间应防止重复点击;部分成员设置失败时需给出明确原因,并仅回滚失败对象。 + +### 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 + +我的收藏(1) + +功能说明: + +该状态展示收藏内容列表及多种内容形态,用户可进入详情或对收藏项执行转发、删除等操作。原消息被删除后,已成功收藏的内容仍应按收藏数据策略可访问。 + +### 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 + +月报(1) + +功能说明: + +月报填写页用于录入本月工作总结、下月计划等内容,并提供提交操作。输入项需具备必填和长度校验,可在正式提交前保存草稿,避免页面退出导致内容丢失。 + +### 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 设置页提供账号安全、消息通知、隐私、通用、关于与退出登录等入口。页面应遵循 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 + +举报(1) + +功能说明: + +举报补充页支持填写问题描述并上传图片等证据,提交前需校验必填项、文件数量、格式和大小。提交成功后返回受理结果,重复举报应按平台规则合并或限流。 + +### 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 + +个人对话框(对方的状态)(1) + +**状态:在线状态已具备基础能力;关系文案新增。** + +- 订阅 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 + +个人对话框(我的状态)(1) + +**状态:发送和读回执已实现基础能力;送达状态映射需统一。** + +- 本地生成 `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 + +个人对话-发各种文件状态(1) + +**状态:需改造产品消息 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 + +内容沟通对话3-语音录音中 + +**状态:录音纯客户端;临时状态可选改造。** + +- 录音期间客户端锁定其他媒体入口。默认不向群成员广播“正在录音”,以减少隐私暴露和状态风暴。 +- 若产品确认需要,可发送不落库的 `{note what:"kpa",topic}`;服务端限频,其他端超时自动消失,不能作为聊天记录。 + +### 4.10 内容沟通对话3-图片和语音发送中.png + +内容沟通对话3-图片和语音发送中 + +**状态:客户端任务管理新增;服务端上传需改造。** + +- 每个待发送项保存 `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 + +内容沟通对话3-语音/视频/图片状态 + +**状态:需改造媒体 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 + +添加好友(1) + +**状态:新增。** + +- 获取二维码:`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 + +添加好友-输入精准用户ID + +**状态:新增。** + +- 精准查询仍调用 `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 + +黑名单提示(1) + +**状态:确认框纯客户端;写接口新增。** + +- 确认后调用 `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 + +内容沟通对话3-开启进群验证与拉人状态 + +**状态:新增群成员事件。** + +- 邀请:`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 + +设置昵称(1) + +**状态:新增。** + +- `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 + +添加管理员-设置成功(1) + +**状态:新增。** + +- 提交期间按幂等键禁重复点击。批量接口返回 `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 + +我的收藏(1) + +**状态:新增。** + +- 详情:`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 + +月报(1) + +**状态:新增。** + +- 创建/取得当月草稿:`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 + +设置(iOS) + +**状态:菜单纯客户端;数据接口部分新增。** + +- 菜单来自 `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 + +举报(1) + +**状态:新增。** + +- 证据使用 `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-服务端多租户改造方案.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 diff --git a/docs/im/images/个人对话-发各种文件状态.png b/docs/im/images/个人对话-发各种文件状态.png new file mode 100644 index 0000000..113156d 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..240a068 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..b97492a 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..ae1a644 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..7305031 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..989c8c0 Binary files /dev/null and b/docs/im/images/个人对话框(对方的状态)(1).png differ diff --git a/docs/im/images/个人对话框(对方的状态).png b/docs/im/images/个人对话框(对方的状态).png new file mode 100644 index 0000000..e63317e 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..544c09c Binary files /dev/null and b/docs/im/images/个人对话框(我的状态)(1).png differ diff --git a/docs/im/images/个人对话框(我的状态).png b/docs/im/images/个人对话框(我的状态).png new file mode 100644 index 0000000..ece3a36 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..6001349 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..6e02403 Binary files /dev/null and b/docs/im/images/举报(1).png differ diff --git a/docs/im/images/举报.png b/docs/im/images/举报.png new file mode 100644 index 0000000..8a81202 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..5575573 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..ff61c9c 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..5ca94a2 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..9ba0166 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..f2d1f1d Binary files /dev/null and b/docs/im/images/关闭青少年模式(安卓).png differ diff --git a/docs/im/images/内容沟通对话3-图片和语音发送中.png b/docs/im/images/内容沟通对话3-图片和语音发送中.png new file mode 100644 index 0000000..443258f Binary files /dev/null and b/docs/im/images/内容沟通对话3-图片和语音发送中.png differ diff --git a/docs/im/images/内容沟通对话3-开启进群验证与拉人状态.png b/docs/im/images/内容沟通对话3-开启进群验证与拉人状态.png new file mode 100644 index 0000000..dc99738 Binary files /dev/null and b/docs/im/images/内容沟通对话3-开启进群验证与拉人状态.png differ diff --git a/docs/im/images/内容沟通对话3-语音录音中.png b/docs/im/images/内容沟通对话3-语音录音中.png new file mode 100644 index 0000000..45a25c8 Binary files /dev/null and b/docs/im/images/内容沟通对话3-语音录音中.png differ diff --git a/docs/im/images/内容沟通对话3-语音/视频/图片状态.png b/docs/im/images/内容沟通对话3-语音/视频/图片状态.png new file mode 100644 index 0000000..36c9544 Binary files /dev/null and b/docs/im/images/内容沟通对话3-语音/视频/图片状态.png differ diff --git a/docs/im/images/出生日期.png b/docs/im/images/出生日期.png new file mode 100644 index 0000000..23f27a2 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..dfe594a 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..3510b5a 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..b2023cf 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..cc6726a 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..22af72a 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..b782809 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..08b698e 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..151b3ef 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..8cb8280 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..3dace39 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..36bbd79 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..955431f 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..6253363 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..26255a7 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..c0e4a75 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..2b4a170 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..61a06bd 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..b60db3c 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..de2713e 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..117abed 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..67e928e 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..9723c0e 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..edd2039 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..b5e1cb2 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..93c9516 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..028458d 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..2a2f224 Binary files /dev/null and b/docs/im/images/我的收藏(1).png differ diff --git a/docs/im/images/我的收藏-文件.png b/docs/im/images/我的收藏-文件.png new file mode 100644 index 0000000..c37ef78 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..a56dd94 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..03d4da7 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..065e26a 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..534d26a 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..d6211ba 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..0e3578a 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..c509808 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..d89fdc5 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..80331da 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..ded18bc 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..20f083b 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..0e6a27c 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..ccee33e 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..9f44fa6 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..ac9fb29 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..dcd573e 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..682dc57 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..354b821 Binary files /dev/null and b/docs/im/images/月报(1).png differ diff --git a/docs/im/images/月报-详情页.png b/docs/im/images/月报-详情页.png new file mode 100644 index 0000000..8f90197 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..8a97e3e 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..eb8042d 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..e1f3d61 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..715c78a 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..0b5ca2e 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..55c4762 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..d622985 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..025a6a0 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..d03c948 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..db5e6b4 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..24675b2 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..84e3e72 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..ce60f2b Binary files /dev/null and b/docs/im/images/添加好友(1).png differ diff --git a/docs/im/images/添加好友-输入精准用户ID.png b/docs/im/images/添加好友-输入精准用户ID.png new file mode 100644 index 0000000..e367d8a Binary files /dev/null and b/docs/im/images/添加好友-输入精准用户ID.png differ diff --git a/docs/im/images/添加好友.png b/docs/im/images/添加好友.png new file mode 100644 index 0000000..be2c4bd 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..ca22198 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..bcab63f 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..effc341 Binary files /dev/null and b/docs/im/images/添加管理员-设置成功(1).png differ diff --git a/docs/im/images/添加管理员-设置成功.png b/docs/im/images/添加管理员-设置成功.png new file mode 100644 index 0000000..4efc2d9 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..69614b8 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..a38f82d 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..e3402cb 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..61e14c7 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..c0e0a1a 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..cae8942 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..21415ba 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..f466fd7 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..7ad46f4 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..24cc5d2 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..4fae17a 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..5aaea9c 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..78d1dff 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..ff9ce39 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..2cef984 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..472a5a4 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..89a5871 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..9c1246e 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..cf2c9ad 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..345c7a1 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..d4fa914 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..9409bd6 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..c342b5d 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..c0206e8 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..d77f5dc 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..d15ab1d 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..1018148 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..42f307f 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..2729912 Binary files /dev/null and b/docs/im/images/设置昵称(1).png differ diff --git a/docs/im/images/设置昵称.png b/docs/im/images/设置昵称.png new file mode 100644 index 0000000..0e2322d 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..1e6c668 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..49137b1 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..2f3aae5 Binary files /dev/null and b/docs/im/images/设置群管理员.png differ diff --git a/docs/im/images/设置(iOS).png b/docs/im/images/设置(iOS).png new file mode 100644 index 0000000..f8d7536 Binary files /dev/null and b/docs/im/images/设置(iOS).png differ diff --git a/docs/im/images/设置(安卓).png b/docs/im/images/设置(安卓).png new file mode 100644 index 0000000..3aff453 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..1e7304e 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..193b4c7 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..3715035 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..b8b061d 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..b638fdf 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..fcc5fc7 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..a8efae5 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..01b2216 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..f2bc973 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..271692b 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..1832fa7 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..0d125f0 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..8e096fc 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..53303c6 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..bdbf5ca 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..05812d0 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..94f08e3 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..a1173fb 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..b2dbfe4 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..d97a7e1 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..e4b2c09 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..913652d 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..c12c878 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..162d588 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..d452875 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..865510f Binary files /dev/null and b/docs/im/images/黑名单提示(1).png differ diff --git a/docs/im/images/黑名单提示.png b/docs/im/images/黑名单提示.png new file mode 100644 index 0000000..0d0d184 Binary files /dev/null and b/docs/im/images/黑名单提示.png differ diff --git a/server/hack/config.yaml b/server/hack/config.yaml index f779bf5..0453049 100644 --- a/server/hack/config.yaml +++ b/server/hack/config.yaml @@ -22,7 +22,7 @@ gfcli: gen: dao: - - link: "mysql:hotgo:hg123456.@tcp(127.0.0.1:3306)/hotgo?loc=Local&parseTime=true&charset=utf8mb4" + - link: "mysql:root:oWxz*2=sb),ouW%p@tcp(64.118.137.182:53306)/hotgo?loc=Local&parseTime=true&charset=utf8mb4" # - link: "pgsql:postgres:hg123456@tcp(127.0.0.1:5432)/hotgo" group: "default" # 分组 使用hotgo代码生成功能时必须填 # tables: "" # 指定当前数据库中需要执行代码生成的数据表。如果为空,表示数据库的所有表都会生成。