From bcb7aadf15d15b9e6ab8ba5432d74d8ef076c990 Mon Sep 17 00:00:00 2001 From: Code Migration Date: Sun, 18 Oct 2026 13:27:23 +0800 Subject: [PATCH] chore: seed migration objects batch 1 --- docs/backstage/IM-后台未实现功能清单.md | 240 ++ docs/backstage/IM-后台需求文档.md | 883 ++++++++ docs/im/IM-产品原型需求文档.md | 1287 +++++++++++ docs/im/IM-产品需求服务端改造分析报告.md | 852 +++++++ docs/im/IM-前后端对接文档.md | 1965 +++++++++++++++++ .../IM-多租户第5至12章代码改造与测试方案.md | 659 ++++++ docs/im/IM-数据库变更汇总.sql | 713 ++++++ docs/im/IM-服务端多租户改造方案.md | 932 ++++++++ docs/im/IM-独立库多租户改造方案.md | 313 +++ docs/im/IM-现有实现与新需求差异对照总结.md | 377 ++++ docs/im/IM-第一二章后续增强项.md | 298 +++ docs/im/IM-第一至七章数据库增量审核.sql | 316 +++ docs/im/IM-系统设计文档.md | 898 ++++++++ docs/im/images/@人员—完成.png | Bin 0 -> 110215 bytes docs/im/images/@人员—选择提醒人.png | Bin 0 -> 94128 bytes docs/im/images/@人员状态.png | Bin 0 -> 140998 bytes docs/im/images/上传文件.png | Bin 0 -> 717 bytes docs/im/images/个人信息.png | Bin 0 -> 67125 bytes docs/im/images/个人对话-发各种文件状态(1).png | Bin 0 -> 86467 bytes 19 files changed, 9733 insertions(+) create mode 100644 docs/backstage/IM-后台未实现功能清单.md create mode 100644 docs/backstage/IM-后台需求文档.md create mode 100644 docs/im/IM-产品原型需求文档.md create mode 100644 docs/im/IM-产品需求服务端改造分析报告.md create mode 100644 docs/im/IM-前后端对接文档.md create mode 100644 docs/im/IM-多租户第5至12章代码改造与测试方案.md create mode 100644 docs/im/IM-数据库变更汇总.sql create mode 100644 docs/im/IM-服务端多租户改造方案.md create mode 100644 docs/im/IM-独立库多租户改造方案.md create mode 100644 docs/im/IM-现有实现与新需求差异对照总结.md create mode 100644 docs/im/IM-第一二章后续增强项.md create mode 100644 docs/im/IM-第一至七章数据库增量审核.sql create mode 100644 docs/im/IM-系统设计文档.md create mode 100644 docs/im/images/@人员—完成.png create mode 100644 docs/im/images/@人员—选择提醒人.png create mode 100644 docs/im/images/@人员状态.png create mode 100644 docs/im/images/上传文件.png create mode 100644 docs/im/images/个人信息.png create mode 100644 docs/im/images/个人对话-发各种文件状态(1).png diff --git a/docs/backstage/IM-后台未实现功能清单.md b/docs/backstage/IM-后台未实现功能清单.md new file mode 100644 index 0000000..6ab02ee --- /dev/null +++ b/docs/backstage/IM-后台未实现功能清单.md @@ -0,0 +1,240 @@ +# IM 后台未实现功能清单 + +> 编写日期:2026-08-16 +> 目的:记录当前 HotGo IM 后台已经完成的能力、尚未实现的后台功能,以及后续应优先通过 `im_server` 管理 API 完成的写操作。 + +## 1. 当前已实现 + +### 1.1 平台控制库 + +已实现: + +| 模块 | 当前能力 | +| --- | --- | +| IM 租户管理 | 租户列表、详情、新增/编辑、状态切换 | +| IM 租户配额配置 | `im_tenant_config` 随租户编辑维护 | +| 租户业务库路由 | 后台按 `im_tenant.db_dsn` 或 `db_name` 动态连接租户库 | +| 后台菜单 | 已导入 IM 管理根菜单及各模块权限点 | + +未完成: + +| 功能 | 说明 | +| --- | --- | +| 租户 provisioning 流程 | 创建租户业务库、执行 schema、初始化系统账号和默认 Topic 尚未实现 | +| 租户删除流程 | 删除前检查、异步归档、业务库删除/保留策略未实现 | +| 平台级审计写入 | 租户创建、停用、恢复、删除等平台操作还未写平台审计 | +| 租户用量统计 | 用户数、群数、文件容量、消息量统计接口未实现 | + +### 1.2 租户业务库只读后台 + +已实现: + +| 模块 | 当前能力 | +| --- | --- | +| IM 用户管理 | 按租户查看用户列表、用户详情 | +| IM 群组管理 | 群列表、群详情、群成员列表 | +| 群治理只读视图 | 查看 `im_group_settings`、入群申请、群邀请、群公告、禁言记录、群管理日志 | +| 实名审核 | 实名提交列表、详情、通过、拒绝 | +| 举报反馈工单 | 举报/反馈列表、详情、状态更新 | +| 租户审计日志 | `im_audit_logs` 列表、详情,只读 | + +注意: + +1. 群成员列表优先读取 `im_group_members`,如果租户库尚未回填业务真源,会回退读取 `im_subscriptions`。 +2. 群治理写操作暂未在 HotGo 后台直接实现,避免绕过 `im_server` 的 Topic、Subscription、ACL、审计和 Outbox 状态机。 +3. 举报反馈当前只更新工单状态,尚未实现“警告、禁言、封禁、撤回消息、通知用户”等处置动作。 + +## 2. P0 未实现 + +### 2.1 后台认证、租户 Principal 与数据权限 + +未实现: + +| 功能 | 说明 | +| --- | --- | +| 租户级 Principal | 后台 Token 中可信 `tenant_id`、角色、权限版本尚未完整建模 | +| 企业后台权限边界 | 普通租户管理员只能访问本租户数据的服务端强校验还未系统化 | +| 跨租户操作二次验证 | 平台管理员切换租户、导出、代操作缺少二次确认 | +| 数据范围权限 | 部门级、群级、安全审计员的数据范围过滤未实现 | + +### 2.2 审计写入 + +未实现: + +| 功能 | 说明 | +| --- | --- | +| 平台级审计表 | 控制库中的平台审计表和查询页面尚未实现 | +| 租户级审计写入 | 用户、群、实名、举报、反馈、文件等敏感操作尚未统一写 `im_audit_logs` | +| 审计导出 | 审计导出、导出审计、脱敏策略未实现 | +| 审计不可篡改策略 | 追加写、防删除、防修改、保留周期尚未实现 | + +### 2.3 Outbox 与后台任务 + +未实现: + +| 功能 | 说明 | +| --- | --- | +| 租户级 Outbox 查询 | `im_outbox_events`、`im_group_events_outbox` 后台查看未实现 | +| Outbox 重试/死信 | 人工重放、死信处理、幂等检查未实现 | +| 任务状态监控 | 文件 GC、索引重建、Push、外部审核任务状态未实现 | + +## 3. P1 未实现 + +### 3.1 群治理写操作 + +这些操作必须优先通过 `im_server` 管理 API 实现,HotGo 后台只调用 API,不直接改业务表: + +| 功能 | 涉及表 | 说明 | +| --- | --- | --- | +| 修改群资料 | `im_group_settings`、`im_topics` | 群名、头像、简介,需同步通信投影 | +| 修改治理配置 | `im_group_settings` | 入群审批、全员禁言、上传文件、成员私聊、成员邀请 | +| 入群审批 | `im_group_join_requests`、`im_group_members`、`im_subscriptions` | 同意/拒绝,需版本号、幂等、配额校验 | +| 邀请处理 | `im_group_invitations`、`im_group_join_requests` | 接受邀请后可能直接入群或进入审批 | +| 群公告发布/更新/删除 | `im_group_announcements`、`im_group_announcement_reads` | 需要乐观锁、系统消息、Outbox | +| 成员禁言/解除 | `im_group_mutes` | 需写管理日志、审计和群系统事件 | +| 管理员管理 | `im_group_members`、`im_subscriptions` | 添加/移除管理员并同步 ACL | +| 群主转让 | `im_group_settings`、`im_group_members`、`im_topics` | 二次确认,同步 Owner 和 ACL | +| 解散群 | `im_group_settings`、`im_topics`、邀请、二维码、文件入口 | 需撤销入口、写审计、发系统事件 | +| 群二维码 | `im_group_qrcodes` | 生成、撤销、过期、扫码解析管理 | + +当前 HotGo 后台只读查看这些表,不做上述写操作。 + +### 3.2 文件与存储治理 + +未实现: + +| 功能 | 说明 | +| --- | --- | +| 文件列表 | 按租户、用途、状态、MIME、上传人查询 | +| 文件详情 | 文件元数据、引用消息、引用群、ACL、扫描状态 | +| 文件下载审计 | 后台下载必须写审计 | +| 隔离/删除文件 | 必须通过 `im_server` API 处理引用、消息、收藏、举报证据 | +| 文件策略 | 单文件大小、总容量、允许 MIME、保留期、病毒扫描策略 | +| 群文件治理 | `im_group_files` 列表、删除、下载权限未实现 | + +### 3.3 组织架构与企业通讯录 + +未实现: + +| 功能 | 说明 | +| --- | --- | +| 部门管理 | 部门树、移动部门、防环、删除校验 | +| 成员归属 | 用户与部门、岗位、负责人关系 | +| 组织数据权限 | 按部门/下级部门限制日报、会议、通讯录可见范围 | + +### 3.4 用户与账号安全写操作 + +未实现: + +| 功能 | 说明 | +| --- | --- | +| 停用/恢复用户 | 需同步登录、Token、WS、审计 | +| 重置密码/安全项 | 必须二次确认并写审计 | +| 设备管理 | 查看、踢下线、撤销设备 Token | +| 用户导出 | 需要导出审计和字段脱敏 | +| 黑名单/好友关系后台 | 查询、处置、修复关系未实现 | + +## 4. P2 未实现 + +### 4.1 反诈与内容审核 + +未实现: + +| 功能 | 说明 | +| --- | --- | +| 风控策略配置 | 高风险动作、提示文案、二次确认、阻断策略 | +| 内容审核策略 | `sync_block`、`async_hold`、`async_post_audit` 配置 | +| 审核插件配置 | 平台统一审核服务、租户专属审核服务 | +| 风险事件列表 | 确认、取消、过期、阻断记录 | +| 审核任务列表 | 待审核消息/文件/图片/视频任务 | +| 审核通过/拒绝 | 必须通过 `im_server` 推进状态机 | +| 审核撤回消息 | 已发布违规消息系统撤回、审计、Outbox 未实现 | + +### 4.2 举报反馈完整处置 + +当前已实现列表、详情、状态更新。未实现: + +| 功能 | 说明 | +| --- | --- | +| 举报原因配置 | 按用户、群、消息目标类型配置原因 | +| 工单分派 | 分派给处理人或安全团队 | +| 工单回复 | 处理结果回复举报人/反馈人 | +| 标记重复 | 合并重复举报或反馈 | +| 处置动作 | 警告、禁言、封禁用户、解散群、撤回消息 | +| 结果通知 | 通过系统通知或客户端查询展示处理结果 | +| 证据文件管理 | `im_evidence_files` 或附件引用的 ACL、保留期、下载审计 | + +### 4.3 实名审核完善 + +当前已实现列表、详情、通过、拒绝。未实现: + +| 功能 | 说明 | +| --- | --- | +| 实名策略配置 | 是否强制实名、允许跳过、提醒周期、重复身份策略 | +| 要求补充材料 | 当前只有通过/拒绝,没有补充材料状态 | +| 敏感原文查看流程 | 需要权限、二次验证、审计,普通管理员不可查看 | +| 审核结果通知 | 审核后通过系统事件通知用户 | +| 审核操作审计 | 当前未统一写租户审计 | + +### 4.4 会议、日报、月报 + +未实现: + +| 功能 | 说明 | +| --- | --- | +| 会议管理 | 会议列表、详情、参与人响应、提醒 | +| 日报/月报列表 | 按部门、成员、状态、时间查询 | +| 统计 | 日报/月报提交率、阅读率、部门统计 | +| 提醒 | 未提交、未读提醒,需限流和逐项结果 | + +### 4.5 总经理信箱 + +未实现: + +| 功能 | 说明 | +| --- | --- | +| 信箱配置 | 是否启用、匿名、必填字段、附件策略、提示文案 | +| 提交列表/详情 | 匿名模式下处理人不可见提交人 | +| 分派/回复/关闭 | 需要审计和通知 | + +### 4.6 工作台应用配置 + +未实现: + +| 功能 | 说明 | +| --- | --- | +| 应用模板 | 平台应用模板、默认权限、分类 | +| 租户应用配置 | 启用/停用、排序、权限、外链安全 | +| 外部链接审计 | 工作台外链访问和安全策略 | + +### 4.7 Push、短信、TURN、对象存储配置 + +未实现: + +| 功能 | 说明 | +| --- | --- | +| Push 配置 | 租户级 Push 通道和测试 | +| 短信配置 | 租户短信模板、签名、发送日志 | +| TURN 配置 | 音视频 TURN 服务配置和连通性检查 | +| 对象存储配置 | 租户桶、路径、回源、签名、上传策略 | +| 外部服务变更审计 | 修改配置必须二次确认和审计 | + +## 5. 后续开发建议顺序 + +建议按以下顺序继续: + +1. **审计写入基础设施**:先实现统一 `im_audit_logs` 写入工具和平台审计表。 +2. **im_server 管理 API 对接层**:HotGo 后台新增统一 client,所有群治理、文件治理、用户处置写操作都走它。 +3. **群治理写操作**:入群审批、治理配置、公告、禁言、管理员、群主转让、解散。 +4. **文件治理**:文件列表、群文件、下载审计、隔离/删除、文件策略。 +5. **Outbox 与任务后台**:事件查看、重试、死信、任务监控。 +6. **内容审核与风险处置**:审核任务、风险事件、违规消息撤回。 +7. **组织/会议/日报/月报/工作台**:企业协同类功能。 + +## 6. 开发边界提醒 + +1. 租户业务库是“一个租户一个库”,业务表不带 `tenant_id`。 +2. HotGo 后台直连租户库适合只读查询、诊断和报表。 +3. 会影响用户、群、消息、文件、`im_topics`、`im_subscriptions`、ACL、Outbox 的写操作,应优先通过 `im_server` 管理 API。 +4. 敏感操作必须具备:权限校验、二次确认、幂等键或版本号、审计、Outbox 通知。 +5. 后台前端隐藏按钮只是体验优化,后端接口仍必须做权限和租户边界校验。 diff --git a/docs/backstage/IM-后台需求文档.md b/docs/backstage/IM-后台需求文档.md new file mode 100644 index 0000000..bd4e598 --- /dev/null +++ b/docs/backstage/IM-后台需求文档.md @@ -0,0 +1,883 @@ +# IM 后台需求文档 + +> 编写日期:2026-08-12 +> 依据文档:`IM-产品需求服务端改造分析报告.md`、`IM-独立库多租户改造方案.md`、`IM-多租户第5至12章代码改造与测试方案.md`、`IM-前后端对接文档.md`、`IM-系统设计文档.md` +> 目标读者:后台产品、后端、管理端前端、测试 + +## 1. 文档目的 + +本文从现有 IM 产品与服务端设计文档中提取“后台管理端”需求,用于后续后台系统开发。本文只描述后台能力,不重复普通移动端聊天、通讯录、消息收发等客户端功能。 + +后台系统分为两类: + +| 后台 | 使用对象 | 数据范围 | +| --- | --- | --- | +| 平台运营后台 | 平台管理员、运维、安全负责人 | 可管理全部租户,但跨租户操作必须二次验证和审计 | +| 企业管理后台 | 租户负责人、企业管理员、安全审计员、组织管理员 | 只能管理当前租户内的数据 | + +所有后台能力必须建立在多租户隔离之上:当前多租户采用“控制库 + 每租户独立业务库”方案。后台 Token 必须包含可信 `tenant_id`、`uid`、角色、权限版本;其中 `tenant_id` 只作为运行时租户上下文和选库依据,不要求租户业务库内业务表保存 `tenant_id` 字段。平台后台的跨租户操作必须走独立后台 API,禁止通过普通 WebSocket `AsUser` 任意代操作用户。 + +后台系统与 `im_server` 的职责边界如下: + +1. HotGo 后台负责管理平台控制库、后台页面、菜单权限、平台运营流程和必要的数据展示。 +2. 租户内用户、群、文件、实名、举报、反馈、Outbox 等业务动作优先通过 `im_server` 管理 API 执行,避免绕过 `im_server` 的状态机、审计、Outbox、权限校验和租户选库逻辑。 +3. HotGo 后台如需直连租户业务库,只能用于只读报表、诊断或经过评审的维护任务;写操作必须明确说明为什么不能调用 `im_server` API。 + +## 2. 后台角色与权限 + +### 2.1 角色 + +| 角色 | 作用域 | 说明 | +| --- | --- | --- | +| `platform_admin` | 平台级 | 租户生命周期、平台配置、运维诊断、跨租户审计 | +| `tenant_owner` | 租户级 | 企业最高管理员,拥有本租户全部管理权限 | +| `tenant_admin` | 租户级 | 组织、用户、群、安全策略和工作台配置 | +| `security_auditor` | 租户级 | 实名、举报、反馈、审计读取和安全处置 | +| `org_admin` | 租户级或部门级 | 部门、成员、岗位、组织数据范围 | +| `report_admin` | 部门级 | 日报、月报、会议统计与提醒 | +| `group_admin` | 群级 | 群成员、公告、禁言、群文件等群治理 | + +### 2.2 权限原则 + +1. 平台管理员可以选择租户上下文进行运维,但所有跨租户读取、导出、代操作必须记录审计。 +2. 企业管理员只能访问自己租户的数据,不能通过 URL、Header、Body 指定其他 `tenant_id`、`db_name` 或 `db_dsn`。 +3. 后台菜单和按钮由服务端返回权限裁剪结果,前端隐藏只是体验优化,服务端仍必须二次鉴权。 +4. 敏感操作必须支持二次认证、幂等键、版本号或 `If-Match`,并写入审计。 +5. 普通租户管理员不能修改或删除审计日志。 + +## 3. 后台功能总览 + +| 模块 | 平台后台 | 企业后台 | 优先级 | +| --- | --- | --- | --- | +| 租户管理 | 是 | 否 | P0 | +| 租户配置与配额 | 是 | 部分只读或申请变更 | P0 | +| 管理员与角色权限 | 是 | 是 | P0 | +| 用户与账号安全 | 支持跨租户运维 | 是 | P0 | +| 组织架构 | 否 | 是 | P1 | +| 群治理 | 运维只读/处置 | 是 | P1 | +| 工作台应用配置 | 平台模板 | 租户配置 | P2 | +| 会议、日报、月报 | 否 | 是 | P2 | +| 总经理信箱 | 否 | 是 | P2 | +| 实名认证审核 | 平台可审计 | 是 | P2 | +| 反诈与内容审核 | 平台策略/插件 | 租户策略/处置 | P2 | +| 举报与反馈工单 | 平台可审计 | 是 | P2 | +| 文件与存储治理 | 平台运维 | 租户范围 | P1 | +| Push、短信、TURN、对象存储配置 | 是 | 租户专属配置 | P1 | +| 审计日志 | 是 | 只读本租户 | P0 | +| Outbox 与后台任务 | 是 | 租户任务可见 | P1 | + +## 4. 平台后台需求 + +### 4.1 租户管理 + +后台需要支持租户的创建、查看、编辑、停用、恢复、删除前置检查和删除归档。 + +租户状态: + +| 状态 | 登录 | 读消息 | 写业务 | 管理操作 | +| --- | --- | --- | --- | --- | +| `provisioning` | 禁止 | 禁止 | 禁止 | 平台管理员可配置 | +| `active` | 允许 | 允许 | 允许 | 按角色允许 | +| `suspended` | 禁止新登录 | 可配置只读 | 禁止 | 平台管理员可恢复或导出 | +| `deleting` | 禁止 | 禁止 | 禁止 | 只允许删除任务 | +| `deleted` | 禁止 | 禁止 | 禁止 | 只保留审计 | + +核心字段: + +| 字段 | 说明 | +| --- | --- | +| `id` | 内部租户 ID,普通客户端不返回 | +| `code` | 企业码,全局唯一,最长 64 字符 | +| `name` | 企业名称 | +| `tenant_desc` | 企业说明 | +| `db_name` | 租户业务库名称;适合同 MySQL 实例下按库隔离,如 `im_tenant_1001` | +| `db_dsn` | 租户业务库完整 DSN;适合独立 MySQL 实例或特殊部署,优先级高于 `db_name` | +| `state` | 租户状态 | +| `created_by/updated_by` | 操作人 | +| `created_at/updated_at` | 时间 | + +建议接口: + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| `GET` | `/admin/v1/tenants` | 租户列表,支持状态、企业码、名称过滤 | +| `POST` | `/admin/v1/tenants` | 创建租户 | +| `GET` | `/admin/v1/tenants/{tenant_id}` | 租户详情 | +| `PATCH` | `/admin/v1/tenants/{tenant_id}` | 修改名称、说明等基础资料 | +| `POST` | `/admin/v1/tenants/{tenant_id}/suspend` | 停用租户 | +| `POST` | `/admin/v1/tenants/{tenant_id}/restore` | 恢复租户 | +| `POST` | `/admin/v1/tenants/{tenant_id}/deletion-check` | 删除前检查 | +| `DELETE` | `/admin/v1/tenants/{tenant_id}` | 进入删除流程 | + +独立库模式下,创建租户必须按 provisioning 流程执行: + +1. 在控制库创建 `im_tenant` 记录,初始状态为 `provisioning`。 +2. 生成或填写租户业务库路由信息,优先使用 `db_dsn`,否则使用主 DSN + `db_name`。 +3. 创建租户业务库并执行 `tenant_schema.sql`。 +4. 初始化默认系统数据,例如 `kvmeta.version`、系统账号、`sys` Topic 和必要的默认配置。 +5. 回写 `im_tenant.db_name/db_dsn`,状态改为 `active`。 +6. 任一步失败时保留 `provisioning` 或进入明确失败状态,后台提供错误详情和重试入口;禁止创建失败的租户进入登录流程。 + +验收点: + +1. 租户 `code` 全局唯一。 +2. 停用后旧 Token、WS 登录、HTTP 产品接口均拒绝。 +3. 删除必须进入异步任务,不允许直接硬删租户业务库。 +4. 所有状态变更写平台审计。 + +### 4.2 租户配置与配额 + +首期配置真源是 `im_tenant_config`。 + +| 配置 | 说明 | 默认 | +| --- | --- | --- | +| `max_users` | 有效用户数上限 | `0` 表示不限 | +| `max_groups` | 有效群数量上限 | `0` 表示不限 | +| `max_group_members` | 单群有效成员上限 | `0` 表示不限 | + +后续可扩展配置: + +| 配置域 | 示例 | +| --- | --- | +| 功能开关 | 加好友、建群、用户搜索、音视频、群文件、日报月报 | +| 安全策略 | 实名要求、青少年限制、风控模式、数据保留期 | +| 消息策略 | 文本长度、媒体类型、消息频控、撤回时限、编辑时限 | +| 文件策略 | 单文件大小、总存储配额、允许 MIME、病毒扫描 | +| Push 配置 | APNS/FCM 凭证、模板、锁屏隐私 | +| 外部服务 | 短信、REST 认证、TURN、对象存储、审核插件 | +| 企业品牌 | Logo、主题色、企业描述、客户端下载地址 | + +建议接口: + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| `GET` | `/admin/v1/tenants/{tenant_id}/config` | 查询租户配置 | +| `PATCH` | `/admin/v1/tenants/{tenant_id}/config` | 修改配额和策略 | +| `GET` | `/admin/v1/tenants/{tenant_id}/usage` | 查询用户数、群数、文件容量、消息量 | +| `GET` | `/admin/v1/tenants/{tenant_id}/features` | 查询功能开关 | +| `PATCH` | `/admin/v1/tenants/{tenant_id}/features` | 修改功能开关 | + +验收点: + +1. 注册、创建群、入群审批必须在 `im_server` 对应租户业务库事务内校验配额。 +2. 超限返回稳定错误码,并记录当前用量、限制值、租户和操作。 +3. 修改配置只影响当前租户,不能污染全平台缓存。 + +### 4.3 平台管理员管理 + +需求: + +1. 创建、停用、恢复平台管理员。 +2. 分配平台角色和权限。 +3. 平台管理员登录、二次认证、Token 撤销。 +4. 查看平台管理员操作审计。 + +敏感操作: + +| 操作 | 要求 | +| --- | --- | +| 创建平台管理员 | 二次确认和审计 | +| 授权 `platform_admin` | 二次认证 | +| 跨租户代操作 | 必须填写原因,记录目标租户和目标对象 | +| 导出数据 | 审批或二次认证,记录导出范围 | + +### 4.4 平台运维与任务 + +需求: + +1. 查看运行节点、数据库版本、Adapter 类型和多租户能力。 +2. 非 MySQL 或未完成独立库租户路由能力的 Adapter 在产品模式下必须拒绝启动。 +3. 查看 Outbox 事件积压、失败、死信和重试。 +4. 查看文件垃圾回收、搜索索引任务、Push 投递、外部审核调用状态。 +5. 支持按租户维度查看任务,但避免 Prometheus 高基数标签。 + +建议接口: + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| `GET` | `/admin/v1/system/health` | 系统健康 | +| `GET` | `/admin/v1/system/nodes` | 节点列表 | +| `GET` | `/admin/v1/system/outbox-events` | Outbox 查询 | +| `POST` | `/admin/v1/system/outbox-events/{id}/retry` | 重试事件 | +| `POST` | `/admin/v1/system/outbox-events/{id}/dead-letter` | 标记死信 | +| `GET` | `/admin/v1/system/audit-logs` | 平台审计 | + +## 5. 企业后台需求 + +### 5.1 企业管理员与角色权限 + +企业后台需要支持本租户内管理员管理。 + +角色: + +| 角色 | 权限 | +| --- | --- | +| `tenant_owner` | 全部租户管理权限 | +| `tenant_admin` | 用户、组织、群、安全、工作台配置 | +| `security_auditor` | 实名、举报、反馈、审计读取 | +| `org_admin` | 组织架构和成员维护 | +| `report_admin` | 日报、月报、会议统计 | + +建议接口: + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| `GET` | `/admin/v1/tenant/admins` | 企业管理员列表 | +| `POST` | `/admin/v1/tenant/admins` | 添加管理员 | +| `PATCH` | `/admin/v1/tenant/admins/{user_id}` | 修改角色 | +| `DELETE` | `/admin/v1/tenant/admins/{user_id}` | 移除管理员 | +| `GET` | `/admin/v1/tenant/roles` | 可用角色和权限点 | + +验收点: + +1. Tenant Admin 只能管理当前租户。 +2. 授权、移除、代操作必须审计。 +3. 不允许最后一个 `tenant_owner` 被删除或停用。 + +### 5.2 用户与账号安全管理 + +需求: + +1. 用户列表、搜索、详情。 +2. 创建、启用、停用、离职、恢复用户。 +3. 重置密码、撤销 Token、踢下线。 +4. 查看登录设备、登录 Session、安全事件。 +5. 查看用户实名状态、青少年状态、隐私设置摘要。 +6. 支持同手机号在不同租户独立存在,本租户后台只看到本租户用户。 + +建议接口: + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| `GET` | `/admin/v1/tenant/users` | 用户列表 | +| `POST` | `/admin/v1/tenant/users` | 创建用户 | +| `GET` | `/admin/v1/tenant/users/{user_id}` | 用户详情 | +| `PATCH` | `/admin/v1/tenant/users/{user_id}` | 修改资料或状态 | +| `POST` | `/admin/v1/tenant/users/{user_id}/reset-password` | 重置密码 | +| `POST` | `/admin/v1/tenant/users/{user_id}/revoke-sessions` | 撤销会话和 Token | +| `GET` | `/admin/v1/tenant/users/{user_id}/devices` | 登录设备 | +| `GET` | `/admin/v1/tenant/users/{user_id}/security-events` | 安全事件 | + +验收点: + +1. 停用用户不影响其他租户相同手机号或相同设备用户。 +2. 重置密码必须递增 `auth_version` 并撤销旧 Token。 +3. 查看敏感信息必须脱敏,证件号、手机号、Token 不写日志。 +4. 用户管理接口必须通过 `tenant_id` 选择对应租户业务库;不得在控制库中查找租户内用户主数据。 + +### 5.3 组织架构管理 + +需求: + +1. 部门树管理:新增、编辑、移动、删除部门。 +2. 部门成员管理:添加、移除、调岗、设置负责人。 +3. 组织搜索:按部门、用户、岗位搜索。 +4. 离职用户处理:不可联系或从结果移除。 +5. 组织数据权限:管理员只能管理权限范围内部门。 + +建议数据表: + +| 表 | 说明 | +| --- | --- | +| `im_org_units` | 部门树 | +| `im_org_members` | 部门成员、岗位、负责人 | + +建议接口: + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| `GET` | `/admin/v1/tenant/org/departments` | 部门树 | +| `POST` | `/admin/v1/tenant/org/departments` | 创建部门 | +| `PATCH` | `/admin/v1/tenant/org/departments/{id}` | 修改或移动部门 | +| `DELETE` | `/admin/v1/tenant/org/departments/{id}` | 删除部门 | +| `GET` | `/admin/v1/tenant/org/departments/{id}/members` | 部门成员 | +| `POST` | `/admin/v1/tenant/org/departments/{id}/members` | 添加成员 | +| `DELETE` | `/admin/v1/tenant/org/departments/{id}/members/{user_id}` | 移除成员 | +| `GET` | `/admin/v1/tenant/org/search` | 组织搜索 | + +验收点: + +1. 部门 `parent_id`、负责人、成员必须来自当前租户业务库。 +2. 移动部门不能形成环。 +3. 删除部门需要校验子部门和成员处理策略。 + +### 5.4 群治理管理 + +企业后台和群主/群管理员都需要群治理能力。后台侧侧重全租户范围查询、策略配置、违规处置和审计。 + +需求: + +1. 群列表、群详情、群成员列表。 +2. 群资料编辑:名称、头像、简介。 +3. 群治理配置:入群审批、全员禁言、成员上传文件、成员私聊、成员邀请。 +4. 群管理员管理:添加、删除管理员,管理员数量限制。 +5. 禁言管理:添加、解除、过期自动失效。 +6. 群主转让、解散群聊。 +7. 群二维码管理:生成、撤销、过期。 +8. 群文件管理:列表、搜索、删除、下载权限。 +9. 群操作日志:角色变化、禁言、公告、解散。 + +建议数据表: + +| 表 | 说明 | +| --- | --- | +| `im_group_settings` | 群治理配置 | +| `im_group_members` | 群成员和角色真源 | +| `im_group_join_requests` | 入群申请 | +| `im_group_announcements` | 群公告 | +| `im_group_mutes` | 禁言记录 | +| `im_group_admin_logs` | 群管理日志 | +| `im_group_files` | 群文件索引 | + +建议接口: + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| `GET` | `/admin/v1/tenant/groups` | 群列表 | +| `GET` | `/admin/v1/tenant/groups/{topic}` | 群详情 | +| `PATCH` | `/admin/v1/tenant/groups/{topic}` | 修改群资料 | +| `GET/PATCH` | `/admin/v1/tenant/groups/{topic}/governance` | 群治理配置 | +| `GET` | `/admin/v1/tenant/groups/{topic}/members` | 成员列表 | +| `POST` | `/admin/v1/tenant/groups/{topic}/admins/batch` | 添加管理员 | +| `DELETE` | `/admin/v1/tenant/groups/{topic}/admins/{user_id}` | 删除管理员 | +| `POST` | `/admin/v1/tenant/groups/{topic}/mutes/batch` | 添加禁言 | +| `DELETE` | `/admin/v1/tenant/groups/{topic}/mutes/{user_id}` | 解除禁言 | +| `POST` | `/admin/v1/tenant/groups/{topic}/owner-transfer` | 群主转让 | +| `DELETE` | `/admin/v1/tenant/groups/{topic}` | 解散群 | +| `GET` | `/admin/v1/tenant/groups/{topic}/files` | 群文件 | +| `DELETE` | `/admin/v1/tenant/groups/{topic}/files/{group_file_id}` | 删除群文件 | + +验收点: + +1. 群业务真源与 Topic/Subscription/ACL 投影必须在同一事务内更新。 +2. 管理员不能越级操作群主,普通管理员之间操作规则由治理策略决定。 +3. 解散群必须撤销 Topic、二维码、邀请和文件新增权限。 +4. 群治理写操作必须由 `im_server` 在目标租户业务库内完成,HotGo 后台不能直接改 Topic、Subscription 或 ACL 绕过业务真源。 + +### 5.5 工作台应用配置 + +需求: + +1. 工作台模块配置:启用、禁用、排序、图标、名称、路由。 +2. 根据组织角色、权限和功能开关裁剪用户可见模块。 +3. 维护会议、日报、月报、总经理信箱等入口。 +4. 工作台未读角标由系统事件更新。 + +建议数据表: + +| 表 | 说明 | +| --- | --- | +| `im_workbench_apps` | 租户工作台应用配置 | +| `im_workbench_user_apps` | 用户或角色级工作台配置 | + +建议接口: + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| `GET` | `/admin/v1/tenant/workbench/apps` | 应用列表 | +| `POST` | `/admin/v1/tenant/workbench/apps` | 新增应用 | +| `PATCH` | `/admin/v1/tenant/workbench/apps/{code}` | 修改应用 | +| `DELETE` | `/admin/v1/tenant/workbench/apps/{code}` | 删除或禁用应用 | +| `POST` | `/admin/v1/tenant/workbench/apps/reorder` | 排序 | + +验收点: + +1. 客户端只接受白名单内部 `route`。 +2. 禁用模块后普通用户不可通过直接 URL 访问。 + +### 5.6 会议管理 + +需求: + +1. 创建、编辑、取消会议。 +2. 管理参会人、提醒未响应者。 +3. 查看参会响应:`pending`、`accepted`、`declined`、`tentative`。 +4. 会议过期后只读展示。 +5. 生成系统通知和 Push。 + +建议数据表: + +| 表 | 说明 | +| --- | --- | +| `im_meetings` | 会议主表 | +| `im_meeting_participants` | 参会人响应 | + +建议接口: + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| `GET` | `/admin/v1/tenant/meetings` | 会议列表 | +| `POST` | `/admin/v1/tenant/meetings` | 创建会议 | +| `GET` | `/admin/v1/tenant/meetings/{id}` | 会议详情 | +| `PATCH` | `/admin/v1/tenant/meetings/{id}` | 更新会议 | +| `POST` | `/admin/v1/tenant/meetings/{id}/cancel` | 取消会议 | +| `POST` | `/admin/v1/tenant/meetings/{id}/reminders` | 提醒未响应者 | + +### 5.7 日报与月报管理 + +需求: + +1. 日报/月报模板和校验规则配置。 +2. 普通用户草稿、提交、修订。 +3. 管理员按部门查看统计和明细。 +4. 提醒未提交人员。 +5. 查看详情时记录已读状态。 +6. 附件使用文件 ACL,不能通过旧 URL 越权下载。 + +建议数据表: + +| 表 | 说明 | +| --- | --- | +| `im_reports` | 日报/月报统一表 | +| `im_report_read_states` | 阅读状态 | + +建议接口: + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| `GET` | `/admin/v1/tenant/reports/daily/statistics` | 日报统计 | +| `GET` | `/admin/v1/tenant/reports/daily` | 日报列表 | +| `GET` | `/admin/v1/tenant/reports/daily/{id}` | 日报详情 | +| `POST` | `/admin/v1/tenant/reports/daily/reminders` | 日报提醒 | +| `GET` | `/admin/v1/tenant/reports/monthly/statistics` | 月报统计 | +| `GET` | `/admin/v1/tenant/reports/monthly` | 月报列表 | +| `GET` | `/admin/v1/tenant/reports/monthly/{id}` | 月报详情 | +| `POST` | `/admin/v1/tenant/reports/monthly/reminders` | 月报提醒 | + +验收点: + +1. 管理员只能看组织权限范围内成员。 +2. 并发保存必须使用版本号,冲突返回当前最新版本。 +3. 提醒接口需要限流并返回逐项结果。 + +### 5.8 总经理信箱 + +需求: + +1. 配置是否启用、是否允许匿名、必填字段、附件策略、提示文案。 +2. 用户提交意见或建议。 +3. 后台查看、分派、处理、回复和关闭。 +4. 匿名模式下业务接收方不可见提交人;安全审计是否保留内部追踪由企业政策决定。 +5. 文案变更后,客户端提交时可要求重新确认。 + +建议数据表: + +| 表 | 说明 | +| --- | --- | +| `im_manager_mailbox` | 信箱提交和处理记录 | + +建议接口: + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| `GET` | `/admin/v1/tenant/executive-mailbox/config` | 查询配置 | +| `PATCH` | `/admin/v1/tenant/executive-mailbox/config` | 修改配置 | +| `GET` | `/admin/v1/tenant/executive-mailbox/submissions` | 提交列表 | +| `GET` | `/admin/v1/tenant/executive-mailbox/submissions/{id}` | 提交详情 | +| `POST` | `/admin/v1/tenant/executive-mailbox/submissions/{id}/assign` | 分派 | +| `POST` | `/admin/v1/tenant/executive-mailbox/submissions/{id}/reply` | 回复 | +| `POST` | `/admin/v1/tenant/executive-mailbox/submissions/{id}/close` | 关闭 | + +## 6. 安全、审核与工单后台 + +### 6.1 实名认证审核 + +需求: + +1. 查看实名认证列表:未认证、待审核、已通过、已拒绝、申诉中。 +2. 查看脱敏信息,具备权限时按流程查看敏感原文。 +3. 审核通过、拒绝、要求补充材料。 +4. 实名策略配置:是否强制实名、允许跳过、提醒周期、重复身份策略。 +5. 审核结果通过系统事件通知用户。 + +建议数据表: + +| 表 | 说明 | +| --- | --- | +| `im_realname_verifications` | 实名信息、审核状态、脱敏展示 | + +建议接口: + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| `GET` | `/admin/v1/tenant/security/realname/submissions` | 实名审核列表 | +| `GET` | `/admin/v1/tenant/security/realname/submissions/{id}` | 审核详情 | +| `POST` | `/admin/v1/tenant/security/realname/submissions/{id}/approve` | 通过 | +| `POST` | `/admin/v1/tenant/security/realname/submissions/{id}/reject` | 拒绝 | +| `GET/PATCH` | `/admin/v1/tenant/security/realname/policy` | 实名策略 | + +验收点: + +1. 姓名、证件号加密存储,普通日志不出现原文。 +2. 审核操作必须审计。 +3. 普通管理员无权读取敏感原文。 + +### 6.2 青少年模式策略 + +需求: + +1. 配置青少年模式默认策略和限制范围。 +2. 查看用户青少年模式状态。 +3. 处理用户忘记独立密码后的重置流程。 + +限制范围首期建议包括: + +1. 添加好友。 +2. 加群和创建群。 +3. 文件传输。 +4. 音视频通话。 +5. 工作台外部链接。 + +建议接口: + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| `GET/PATCH` | `/admin/v1/tenant/security/youth-mode/policy` | 青少年策略 | +| `GET` | `/admin/v1/tenant/security/youth-mode/users` | 用户状态列表 | +| `POST` | `/admin/v1/tenant/security/youth-mode/users/{user_id}/reset-pin` | 重置独立密码 | + +### 6.3 反诈与内容审核 + +需求: + +1. 风控策略配置:高风险动作、提示文案、二次确认、阻断策略。 +2. 审核插件配置:平台统一审核服务或租户专属审核服务。 +3. 审核模式配置:`sync_block`、`async_hold`、`async_post_audit`。 +4. 查看风险事件、确认、取消、过期、阻断记录。 +5. 对已发布违规消息执行系统撤回。 + +建议接口: + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| `GET/PATCH` | `/admin/v1/tenant/security/risk-policy` | 风控策略 | +| `GET/PATCH` | `/admin/v1/tenant/security/moderation-policy` | 内容审核策略 | +| `GET` | `/admin/v1/tenant/security/risk-events` | 风险事件 | +| `GET` | `/admin/v1/tenant/security/moderation-tasks` | 审核任务 | +| `POST` | `/admin/v1/tenant/security/moderation-tasks/{id}/approve` | 审核通过 | +| `POST` | `/admin/v1/tenant/security/moderation-tasks/{id}/reject` | 审核拒绝 | +| `POST` | `/admin/v1/tenant/messages/{message_id}/recall-by-review` | 审核撤回 | + +验收点: + +1. 客户端提交的审核状态一律忽略。 +2. 插件返回 `REPLACE` 或 `RESPOND` 时不能修改 TenantID。 +3. 风控确认 Token 单次、短期、绑定原动作。 + +### 6.4 举报工单 + +需求: + +1. 配置举报原因,按目标类型区分用户、群、消息。 +2. 举报列表、详情、证据、目标快照。 +3. 工单状态:`received`、`processing`、`resolved`、`rejected`、`closed`。 +4. 处置动作:警告、禁言、封禁用户、解散群、撤回消息、标记无效。 +5. 重复举报合并策略。 +6. 处理结果通知举报人,必要时通知被处置对象。 + +建议数据表: + +| 表 | 说明 | +| --- | --- | +| `im_abuse_reports` | 举报工单 | +| `im_evidence_files` | 证据文件 | + +建议接口: + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| `GET/PATCH` | `/admin/v1/tenant/reports-abuse/reasons` | 举报原因配置 | +| `GET` | `/admin/v1/tenant/reports-abuse` | 举报列表 | +| `GET` | `/admin/v1/tenant/reports-abuse/{id}` | 举报详情 | +| `POST` | `/admin/v1/tenant/reports-abuse/{id}/assign` | 分派 | +| `POST` | `/admin/v1/tenant/reports-abuse/{id}/resolve` | 处置 | +| `POST` | `/admin/v1/tenant/reports-abuse/{id}/close` | 关闭 | + +验收点: + +1. 举报人必须能看到被举报目标,后台详情使用提交时固化快照。 +2. 证据文件使用独立 ACL 和保留期。 +3. 原消息删除或撤回后,举报证据仍可处理。 + +### 6.5 意见反馈工单 + +需求: + +1. 配置反馈类别、最大正文、附件规则、是否允许联系方式、日志授权说明。 +2. 反馈列表、详情、附件、设备和版本信息。 +3. 分派、回复、关闭、标记重复。 +4. 处理结果通过系统通知或客户端查询展示。 + +建议数据表: + +| 表 | 说明 | +| --- | --- | +| `im_feedbacks` | 反馈工单 | +| `im_evidence_files` | 反馈附件 | + +建议接口: + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| `GET/PATCH` | `/admin/v1/tenant/feedback/config` | 反馈配置 | +| `GET` | `/admin/v1/tenant/feedbacks` | 反馈列表 | +| `GET` | `/admin/v1/tenant/feedbacks/{id}` | 反馈详情 | +| `POST` | `/admin/v1/tenant/feedbacks/{id}/assign` | 分派 | +| `POST` | `/admin/v1/tenant/feedbacks/{id}/reply` | 回复 | +| `POST` | `/admin/v1/tenant/feedbacks/{id}/close` | 关闭 | + +## 7. 文件、搜索与数据治理后台 + +### 7.1 文件治理 + +需求: + +1. 按租户查看文件用量、文件列表、状态和业务用途。 +2. 支持群文件、收藏文件、举报证据、反馈附件、日报/月报附件分类管理。 +3. 文件下载审计。 +4. 清理未关联文件、过期文件和违规文件。 +5. 配置文件大小、MIME、总存储配额、保留期。 + +文件状态建议: + +```text +pending -> uploaded -> ready +pending/uploaded -> failed +uploaded(超时且无引用) -> orphaned -> deleted +``` + +建议接口: + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| `GET` | `/admin/v1/tenant/files` | 文件列表 | +| `GET` | `/admin/v1/tenant/files/{file_id}` | 文件详情 | +| `POST` | `/admin/v1/tenant/files/{file_id}/quarantine` | 隔离文件 | +| `DELETE` | `/admin/v1/tenant/files/{file_id}` | 删除文件 | +| `GET/PATCH` | `/admin/v1/tenant/files/policy` | 文件策略 | + +验收点: + +1. 文件下载不能只靠 FileID,必须校验上传者或关联消息/Topic 读取权限。 +2. 后台删除文件必须审计,并处理消息、收藏、举报等引用。 + +### 7.2 搜索治理 + +需求: + +1. 消息索引状态查看。 +2. 重建指定租户、Topic 或时间段索引。 +3. 搜索敏感内容审计。 +4. 查看搜索性能和失败任务。 + +建议接口: + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| `GET` | `/admin/v1/tenant/search/index-status` | 索引状态 | +| `POST` | `/admin/v1/tenant/search/reindex` | 重建索引 | +| `GET` | `/admin/v1/tenant/search/audit-logs` | 搜索审计 | + +验收点: + +1. 搜索索引不是授权来源,返回前仍需 Topic Read ACL 二次校验。 +2. 用户退群或权限变化后必须立即阻断搜索结果。 + +## 8. 通知、Push 与外部服务后台 + +### 8.1 系统消息模板 + +需求: + +1. 管理系统事件类型和模板版本。 +2. 配置账号安全、好友、群、文件、通话、工作台、安全等通知模板。 +3. Outbox 事件驱动系统消息、Push、索引和定时任务。 + +首期事件: + +| 业务 | 事件 | +| --- | --- | +| 账号安全 | 新设备登录、密码修改、实名提醒 | +| 好友 | 新申请、通过、拒绝、删除 | +| 群 | 邀请、申请、审批、公告、禁言、管理员变更、群主转让、解散 | +| 文件 | 群文件上传、文件失效 | +| 通话 | 未接、拒绝、取消、忙线、通话结束 | +| 工作台 | 会议通知、会议变更、日报提醒、月报提醒、信箱处理 | +| 安全 | 反诈提示、举报受理、反馈受理 | + +### 8.2 Push、短信、TURN、对象存储 + +需求: + +1. 租户级 Push 模板、App 包名、APNS/FCM 凭证引用。 +2. 短信服务配置和模板。 +3. TURN 配置。 +4. 对象存储 Bucket、Prefix、签名策略。 +5. 外部 REST 认证服务配置。 +6. 外部审核和风控插件配置。 + +验收点: + +1. 租户没有配置时按明确策略失败,不得静默使用其他租户配置。 +2. 密钥只保存引用,不把明文 Secret 放入普通业务 JSON。 +3. 外部回调必须校验服务端签名的 TenantContext 和 nonce。 + +## 9. 审计与日志后台 + +### 9.1 审计范围 + +以下操作必须写审计: + +1. 租户创建、停用、恢复、删除。 +2. 平台管理员和租户管理员授权、移除、代操作。 +3. 用户停用、离职、密码重置、数据导出。 +4. 群主转让、管理员变更、禁言、解散群。 +5. 实名审核、举报处置、反馈处理、匿名信箱读取。 +6. 文件下载、删除、批量导出、搜索敏感内容。 +7. 反诈二次确认、内容审核处置。 +8. 外部服务配置修改。 + +独立库模式下审计存储分为两类: + +1. 平台级审计存控制库,覆盖租户生命周期、平台管理员、跨租户运维、租户业务库创建/删除/导出等操作。 +2. 租户级业务审计优先存对应租户业务库,覆盖用户、群、文件、实名、举报、反馈、办公协同等操作;后台查询时通过 `tenant_id` 选择租户业务库。 + +平台后台可汇总查询租户级审计,但跨租户聚合、导出和代操作必须记录平台级审计。 + +### 9.2 审计字段 + +| 字段 | 说明 | +| --- | --- | +| `tenant_id` | 租户 | +| `actor_uid` | 操作人 | +| `actor_role` | 操作角色 | +| `action` | 操作类型 | +| `target_type` | 目标类型 | +| `target_id` | 目标 ID | +| `before_snapshot` | 变更前摘要 | +| `after_snapshot` | 变更后摘要 | +| `reason` | 操作原因 | +| `request_id` | 请求 ID | +| `ip` | 操作 IP,必要时脱敏 | +| `user_agent` | 操作客户端 | +| `created_at` | 操作时间 | + +审计表只允许追加,不允许普通管理员修改或删除。 + +建议接口: + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| `GET` | `/admin/v1/tenant/audit-logs` | 租户审计 | +| `GET` | `/admin/v1/audit-logs` | 平台审计 | +| `POST` | `/admin/v1/audit-logs/export` | 审计导出 | + +## 10. 后台公共能力 + +### 10.1 认证与会话 + +1. 后台登录必须使用独立后台入口,不与普通 App 登录混用权限。 +2. 后台 Token 包含 `tenant_id`、`uid`、角色、权限版本、`auth_version`。 +3. 支持刷新、撤销、踢下线、登录设备管理。 +4. 高危操作需要二次认证或 `X-Action-Token`。 + +### 10.2 API 规范 + +后台 API 建议统一使用 `/admin/v1/*`。该路径是 `im_server` 管理 API 的目标契约;在 HotGo 中落地页面和接口时,可以按 HotGo 现有 `/admin/...` 路由规范做适配,但必须保持权限语义、请求字段、幂等、审计和错误码一致。 + +成功响应: + +```json +{ + "code": 0, + "data": {}, + "request_id": "01K...", + "server_time": "2026-08-12T05:30:00.000Z" +} +``` + +失败响应: + +```json +{ + "code": 30001, + "reason": "FORBIDDEN", + "message": "permission denied", + "details": {}, + "request_id": "01K...", + "server_time": "2026-08-12T05:30:00.000Z" +} +``` + +公共要求: + +1. 写操作必须带 `Idempotency-Key`。 +2. 更新操作必须带版本号或 `If-Match`。 +3. 列表统一使用 `limit + cursor`。 +4. 请求必须带 `X-Request-ID`。 +5. 不能从请求 Body、URL Query 或普通 Header 信任客户端自报 `tenant_id`。 +6. 手机号、证件号、验证码、Token、设备 Token、文件签名不写日志。 + +### 10.3 数据表公共规则 + +1. 新业务表统一使用 `im_` 前缀。 +2. 控制库只保存平台级租户元数据、平台级审计、平台任务和全局配置,例如 `im_tenant`、`im_tenant_config`。 +3. 租户业务库保存租户内业务表,例如用户安全、好友、群治理、收藏、实名、举报、反馈、组织、会议、日报月报等;这些表不包含 `tenant_id` 字段。 +4. 租户业务表唯一索引按租户库内业务键设计,例如 `business_key`、`user_id + peer_id`、`topic + seqid`,不使用 `(tenant_id, business_key)`。 +5. 子表外键按租户库内父子关系设计,不使用 `(tenant_id, parent_id)` 复合外键。 +6. 更新、删除、聚合、清理任务必须先从可信 Principal 或平台运维上下文取得 `tenant_id`,再路由到对应租户业务库执行。 +7. 禁止通过 URL、Header、Body 传入的 `db_name`、`db_dsn` 或 `tenant_id` 决定业务库;业务库路由只能来自控制库和服务端可信上下文。 + +### 10.4 Outbox 与后台任务存储 + +Outbox 和后台任务同样按作用域拆分: + +1. 平台级 Outbox 存控制库,用于租户创建、租户停用、建库初始化、跨租户导出、平台通知等。 +2. 租户级 Outbox 存租户业务库,用于好友、群、文件、实名、举报、反馈、会议、日报月报等业务事件。 +3. 后台查询 Outbox 时,平台后台可以先按租户筛选,再路由到对应业务库;企业后台只能查询当前租户业务库内事件。 +4. 重试、死信和人工重放必须幂等,并写入对应作用域的审计。 + +## 11. 开发优先级建议 + +### P0:后台地基 + +1. `/admin/v1` 认证中间件、RBAC、租户 Principal。 +2. 平台租户管理:创建、停用、恢复、配置配额。 +3. 租户业务库 provisioning:建库、执行 schema、初始化系统数据、回写 `db_name/db_dsn`、失败重试。 +4. `im_server` 管理 API 对接和租户选库校验。 +5. 企业管理员与角色权限。 +6. 用户管理:停用、重置密码、撤销 Session。 +7. 审计日志基础表和查询。 +8. Outbox 查询与重试基础能力。 + +### P1:企业治理 + +1. 组织架构管理。 +2. 群治理后台:群列表、群成员、管理员、禁言、解散、群文件。 +3. 文件策略、文件列表、文件审计。 +4. 外部服务配置:Push、短信、对象存储、TURN。 +5. 功能开关和配额用量面板。 + +### P2:办公与安全业务 + +1. 工作台配置。 +2. 会议管理。 +3. 日报、月报统计和提醒。 +4. 总经理信箱。 +5. 实名认证审核。 +6. 青少年策略。 +7. 反诈、内容审核、举报、反馈工单。 + +## 12. 验收清单 + +- [ ] 平台后台与企业后台权限边界清晰,跨租户操作必须审计。 +- [ ] 所有后台列表、详情、更新、删除都按租户隔离。 +- [ ] 租户停用后普通用户登录和 HTTP 产品接口拒绝。 +- [ ] 用户停用、重置密码、撤销 Token 不影响其他租户同手机号用户。 +- [ ] 创建用户、建群、入群审批配额在事务内强制执行。 +- [ ] 群主转让、禁言、管理员变更、解散群写审计并同步系统消息。 +- [ ] 实名、举报、反馈、信箱等敏感数据脱敏展示并按权限读取。 +- [ ] 文件下载、删除、导出均有资源级授权和审计。 +- [ ] 外部服务密钥只保存引用,不写普通日志。 +- [ ] Outbox 事件可查询、重试、死信处理,重复执行不产生重复副作用。 +- [ ] 审计表只允许追加,普通管理员不可修改或删除。 diff --git a/docs/im/IM-产品原型需求文档.md b/docs/im/IM-产品原型需求文档.md new file mode 100644 index 0000000..22a7c7c --- /dev/null +++ b/docs/im/IM-产品原型需求文档.md @@ -0,0 +1,1287 @@ +# IM 产品原型需求文档 + +> 本文档根据 `/Users/superysh/im/第二项目/` 中的 156 张产品原型图整理。功能按业务域归类,每张图以原文件名作为标题,图片下方给出功能说明与界面内容描述。 + +## 产品总体定位 + +该产品是一套面向企业内部沟通与协作的即时通讯软件。核心能力包括账号与身份认证、单聊与群聊、通讯录、文件与收藏、音视频通话、企业工作台、日报月报、安全风险提示以及个人设置。系统需要同时兼顾 iOS 和 Android 端的交互差异,并保证消息、权限、文件和通话状态可在多端间一致同步。 + +## 1. 账号注册、登录与身份安全 + +本组功能覆盖企业入口、账号密码登录、短信验证码注册、找回密码、身份校验、实名认证与独立密码。需求重点是明确的输入校验、验证码倒计时、错误反馈、协议确认与认证状态展示。 + +### 1.1 企业码登录.png + +企业码登录 + +功能说明: + +提供企业租户入口。页面展示产品标识、企业码输入框和“进入企业”按钮;提交后应校验企业码是否存在、是否启用,并将用户导向对应企业的登录环境。 + +### 1.2 登录页面.png + +登录页面 + +功能说明: + +展示用户名/手机号和密码登录表单,支持密码显隐、忘记密码、注册跳转以及用户协议/隐私政策确认。登录按钮在必填信息和协议条件满足后才可有效提交。 + +### 1.3 登录页面-密码错误.png + +登录页面-密码错误 + +功能说明: + +表现登录失败状态。当账号或密码错误时,在表单上方以 Toast 或明确错误文案告知“账号或密码错误”,保留当前账号并允许用户重新输入密码。 + +### 1.4 注册页面.png + +注册页面 + +功能说明: + +提供用户名/手机号、短信验证码、密码和确认密码输入。需支持获取验证码、密码显隐、两次密码一致性校验、已有账号返回登录以及协议勾选。 + +### 1.5 注册页面-倒计时.png + +注册页面-倒计时 + +功能说明: + +展示验证码发送后的倒计时状态。倒计时期间禁止重复发送,归零后恢复“获取验证码”;后端需同时执行发送频控和验证码有效期校验。 + +### 1.6 忘记密码.png + +忘记密码 + +功能说明: + +通过手机号、验证码、新密码和再次确认完成密码重置。应校验账号归属、验证码正确性及新密码规则,成功后应废弃旧令牌并引导用户重新登录。 + +### 1.7 身份验证(安卓).png + +身份验证(安卓) + +功能说明: + +用于敏感操作前的二次身份校验。用户输入手机号和短信验证码后确认;应对获取验证码、校验失败、超时和重试状态给出明确反馈。 + +### 1.8 实名认证.png + +实名认证 + +功能说明: + +提供姓名与证件号码输入,用于提交实名认证。页面需明确说明信息用途和隐私保护,提交前校验姓名及证件号格式,并防止重复提交。 + +### 1.9 实名认证-已实名.png + +实名认证-已实名 + +功能说明: + +展示认证通过后的只读状态,姓名和证件号做脱敏处理,提交按钮置灰并显示“已实名”。如支持更正信息,应进入独立的申诉审核流程,不允许在本页直接修改。 + +### 1.10 实名认证(安卓).png + +实名认证(安卓) + +功能说明: + +安卓端在消息首页上以弹窗提醒用户完成实名认证。弹窗应说明认证原因,提供“去实名认证”主操作,并根据平台策略控制是否允许暂后处理。 + +### 1.11 设置独立密码(安卓).png + +设置独立密码(安卓) + +功能说明: + +提供四位独立密码输入框,用于青少年模式或隐私功能的二次保护。应支持连续输入、错误次数限制、忘记密码处理和安全存储,不得以明文保存。 + +## 2. 消息首页、全局导航与系统通知 + +本组功能提供即时通讯的全局入口,包括会话列表、网络异常、快捷菜单、长按操作和系统消息。用户应能快速识别未读数、最新消息、联系人在线状态以及当前网络状态。 + +### 2.1 消息.png + +消息 + +功能说明: + +会话首页包含最近联系人、系统消息、文件助手和普通会话列表。每个会话项展示头像、名称、最新摘要、时间和未读数;页面提供搜索和右上角快捷功能入口。 + +### 2.2 消息-无网络状态.png + +消息-无网络状态 + +功能说明: + +当网络断开或重连时,顶部显示“加载中”及进度状态,会话缓存内容仍可查看。系统应自动重连,恢复后增量同步消息、未读数和回执,不应造成重复消息。 + +### 2.3 消息-更多.png + +消息-更多 + +功能说明: + +右上角菜单提供“朋友”、“我的”、“创建群聊”、“添加好友”和“扫一扫”等快捷入口。菜单应与触发图标对齐,点击外部区域关闭,并根据用户权限显示可用项。 + +### 2.4 消息页面—长按弹出.png + +消息页面—长按弹出 + +功能说明: + +长按消息后弹出操作菜单,包括引用、复制、收藏、编辑、转发、撤回、删除、多选和群回执等操作。菜单项需根据消息类型、发送人、撤回时限和群权限动态裁剪。 + +### 2.5 系统消息.png + +系统消息 + +功能说明: + +系统消息页按时间展示新设备登录等安全通知,包含设备、IP 地址、时间和保障建议。此类消息应由可信系统账号发送,支持跳转安全设置,且不允许普通用户伪造。 + +## 3. 单聊会话、会话建立与消息状态 + +本组功能覆盖好友申请前后的单聊页、双方消息显示、已读/未读、删除、拉黑以及文件与名片消息。界面需能清楚区分“我的消息”与“对方消息”,并将发送、失败、已读、撤回等状态准确反映到消息气泡。 + +### 3.1 个人对话框(对方的状态).png + +个人对话框(对方的状态) + +功能说明: + +展示对方发来的好友申请或首条消息,消息位于左侧并显示对方头像。页面应在尚未建立可聊权限时限制消息输入,并展示申请或通过状态。 + +### 3.2 个人对话框(对方的状态)(1).png + +个人对话框(对方的状态)(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-数据库变更汇总.sql b/docs/im/IM-数据库变更汇总.sql new file mode 100644 index 0000000..e05dfec --- /dev/null +++ b/docs/im/IM-数据库变更汇总.sql @@ -0,0 +1,713 @@ +-- IM product database changes summary, chapters 1-11. +-- Target: +-- public backend database: hotgo +-- default tenant business database: tinode +-- Notes: +-- 1. Public tenant registry tables live in hotgo. +-- 2. Tenant-scoped business tables live in each tenant database and do not include tenant_id. +-- 3. This file is for review and manual execution. Existing legacy columns/indexes should be checked before ALTER/DROP. +-- 4. Table definitions are aligned with server/db/mysql/adapter.go plus im_verification_codes used by chapter 1 verification APIs. + +SET NAMES utf8mb4; + +CREATE DATABASE IF NOT EXISTS hotgo CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; +CREATE DATABASE IF NOT EXISTS tinode CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; + +-- ============================================================ +-- Public database: tenant registry. +-- ============================================================ + +CREATE TABLE IF NOT EXISTS hotgo.im_tenant ( + id BIGINT NOT NULL AUTO_INCREMENT, + code VARCHAR(64) NOT NULL, + name VARCHAR(128) NOT NULL, + tenant_desc VARCHAR(256), + db_name VARCHAR(128) NOT NULL DEFAULT '', + db_dsn VARCHAR(1024) NOT NULL DEFAULT '', + state SMALLINT NOT NULL, + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + created_by BIGINT NOT NULL, + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + updated_by BIGINT NOT NULL, + PRIMARY KEY (id), + UNIQUE INDEX uk_im_tenant_code (code), + INDEX idx_im_tenant_db_name (db_name) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS hotgo.im_tenant_config ( + id BIGINT NOT NULL AUTO_INCREMENT, + tenant_id BIGINT NOT NULL, + max_users BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited', + max_groups BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited', + max_group_members INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + created_by BIGINT NOT NULL, + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + updated_by BIGINT NOT NULL, + PRIMARY KEY (id), + UNIQUE INDEX uk_im_tenant_config_tenant_id (tenant_id), + CONSTRAINT fk_im_tenant_config_tenant FOREIGN KEY (tenant_id) REFERENCES hotgo.im_tenant(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ============================================================ +-- Tenant database: chapter 1, tenant-aware login, verification, security. +-- ============================================================ + +CREATE TABLE IF NOT EXISTS tinode.im_user_security ( + user_id BIGINT NOT NULL, + auth_version INT NOT NULL DEFAULT 1, + password_updated_at DATETIME(3), + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (user_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_verification_codes ( + id BIGINT NOT NULL AUTO_INCREMENT, + challenge_id VARCHAR(64) NOT NULL, + scene VARCHAR(32) NOT NULL, + action VARCHAR(64) NOT NULL DEFAULT '', + phone_hash CHAR(64) NOT NULL, + phone_mask VARCHAR(32) NOT NULL, + target_user BIGINT, + code_hash CHAR(64) NOT NULL, + verification_token_hash CHAR(64), + state SMALLINT NOT NULL, + attempts INT NOT NULL DEFAULT 0, + max_attempts INT NOT NULL, + expires_at DATETIME(3) NOT NULL, + resend_after DATETIME(3) NOT NULL, + verified_at DATETIME(3), + consumed_at DATETIME(3), + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (id), + UNIQUE INDEX uk_im_verification_challenge (challenge_id), + INDEX idx_im_verification_scene_phone_state (scene, phone_hash, state, expires_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_realname_verifications ( + id BIGINT NOT NULL AUTO_INCREMENT, + user_id BIGINT NOT NULL, + submission_id VARCHAR(64) NOT NULL, + status VARCHAR(16) NOT NULL, + name_cipher TEXT NOT NULL, + id_type VARCHAR(32) NOT NULL, + id_number_cipher TEXT NOT NULL, + id_number_hash CHAR(64) NOT NULL, + masked_name VARCHAR(64) NOT NULL, + masked_id_number VARCHAR(64) NOT NULL, + review_reason VARCHAR(256), + verified_at DATETIME(3), + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (id), + UNIQUE INDEX uk_im_realname_submission (submission_id), + INDEX idx_im_realname_user_latest (user_id, created_at), + INDEX idx_im_realname_id_hash (id_number_hash) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_action_tokens ( + id BIGINT NOT NULL AUTO_INCREMENT, + user_id BIGINT NOT NULL, + token_id VARCHAR(64) NOT NULL, + action VARCHAR(64) NOT NULL, + target VARCHAR(128) NOT NULL DEFAULT '', + token_hash CHAR(64) NOT NULL, + state SMALLINT NOT NULL, + expires_at DATETIME(3) NOT NULL, + consumed_at DATETIME(3), + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (id), + UNIQUE INDEX uk_im_action_tokens_token (token_id), + INDEX idx_im_action_tokens_user_action (user_id, action, target, state, expires_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_youth_mode_settings ( + user_id BIGINT NOT NULL, + purpose VARCHAR(64) NOT NULL, + password_hash VARBINARY(255) NOT NULL, + enabled TINYINT NOT NULL DEFAULT 0, + policy_version INT NOT NULL DEFAULT 1, + restrictions JSON, + enabled_at DATETIME(3), + failed_attempts INT NOT NULL DEFAULT 0, + locked_until DATETIME(3), + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (user_id, purpose) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ============================================================ +-- Tenant database: chapters 2-7, notifications, messages, friends, files, favorites. +-- ============================================================ + +CREATE TABLE IF NOT EXISTS tinode.im_system_notifications ( + id BIGINT NOT NULL AUTO_INCREMENT, + user_id BIGINT NOT NULL, + event_id VARCHAR(64) NOT NULL, + event_type VARCHAR(64) NOT NULL, + title VARCHAR(128) NOT NULL, + summary VARCHAR(512) NOT NULL, + action JSON, + read_at DATETIME(3), + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (id), + UNIQUE INDEX uk_im_system_notifications_event (user_id, event_id), + INDEX idx_im_system_notifications_inbox (user_id, id DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_message_revisions ( + id BIGINT NOT NULL AUTO_INCREMENT, + createdat DATETIME(3) NOT NULL, + updatedat DATETIME(3) NOT NULL, + topic CHAR(25) NOT NULL, + seqid INT NOT NULL, + version INT NOT NULL, + actor BIGINT NOT NULL, + head JSON, + content JSON, + reason VARCHAR(256) NOT NULL DEFAULT '', + PRIMARY KEY (id), + UNIQUE INDEX uk_im_message_revisions_version (topic, seqid, version) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_friend_requests ( + id BIGINT NOT NULL AUTO_INCREMENT, + request_id VARCHAR(64) NOT NULL, + applicant_id BIGINT NOT NULL, + target_id BIGINT NOT NULL, + message VARCHAR(200) NOT NULL DEFAULT '', + source VARCHAR(32) NOT NULL DEFAULT '', + source_topic VARCHAR(64) NOT NULL DEFAULT '', + status VARCHAR(16) NOT NULL, + idempotency_key VARCHAR(128) NOT NULL DEFAULT '', + expires_at DATETIME(3) NOT NULL, + handled_at DATETIME(3), + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (id), + UNIQUE INDEX uk_im_friend_requests_request (request_id), + UNIQUE INDEX uk_im_friend_requests_idempotency (applicant_id, idempotency_key), + INDEX idx_im_friend_requests_pair (applicant_id, target_id, status, created_at), + INDEX idx_im_friend_requests_inbox (target_id, status, created_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_friendships ( + user_id BIGINT NOT NULL, + peer_id BIGINT NOT NULL, + status VARCHAR(16) NOT NULL, + remark VARCHAR(64) NOT NULL DEFAULT '', + topic CHAR(25) NOT NULL DEFAULT '', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (user_id, peer_id), + INDEX idx_im_friendships_peer (peer_id, status) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_user_blocks ( + blocker_id BIGINT NOT NULL, + blocked_id BIGINT NOT NULL, + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (blocker_id, blocked_id), + INDEX idx_im_user_blocks_blocked (blocked_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_favorites ( + id BIGINT NOT NULL AUTO_INCREMENT, + favorite_id VARCHAR(64) NOT NULL, + user_id BIGINT NOT NULL, + item_type VARCHAR(16) NOT NULL, + source_topic CHAR(25) NOT NULL, + source_seq INT NOT NULL, + snapshot JSON, + attachment_refs JSON, + availability VARCHAR(16) NOT NULL DEFAULT 'available', + idempotency_key VARCHAR(128) NOT NULL DEFAULT '', + collected_at DATETIME(3) NOT NULL, + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (id), + UNIQUE INDEX uk_im_favorites_favorite (favorite_id), + UNIQUE INDEX uk_im_favorites_source (user_id, source_topic, source_seq), + UNIQUE INDEX uk_im_favorites_idempotency (user_id, ((NULLIF(idempotency_key, '')))), + INDEX idx_im_favorites_inbox (user_id, id DESC), + INDEX idx_im_favorites_type (user_id, item_type, id DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- Existing Tinode tables must match the current schema for chapters 2-7: +-- topics.aux, fileuploads.etag, and filemsglinks.topic/userid are required by current code. + +SET @im_sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'client_msg_id') = 0, + 'ALTER TABLE tinode.messages ADD COLUMN client_msg_id VARCHAR(64) AFTER `from`', + 'SELECT 1' +); +PREPARE im_stmt FROM @im_sql; +EXECUTE im_stmt; +DEALLOCATE PREPARE im_stmt; + +SET @im_sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'version') = 0, + 'ALTER TABLE tinode.messages ADD COLUMN version INT NOT NULL DEFAULT 1 AFTER client_msg_id', + 'SELECT 1' +); +PREPARE im_stmt FROM @im_sql; +EXECUTE im_stmt; +DEALLOCATE PREPARE im_stmt; + +SET @im_sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'recalled_at') = 0, + 'ALTER TABLE tinode.messages ADD COLUMN recalled_at DATETIME(3) AFTER version', + 'SELECT 1' +); +PREPARE im_stmt FROM @im_sql; +EXECUTE im_stmt; +DEALLOCATE PREPARE im_stmt; + +SET @im_sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'recalled_by') = 0, + 'ALTER TABLE tinode.messages ADD COLUMN recalled_by BIGINT AFTER recalled_at', + 'SELECT 1' +); +PREPARE im_stmt FROM @im_sql; +EXECUTE im_stmt; +DEALLOCATE PREPARE im_stmt; + +SET @im_sql = IF( + (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'messages' AND INDEX_NAME = 'messages_sender_client_mid') = 0, + 'CREATE UNIQUE INDEX messages_sender_client_mid ON tinode.messages(`from`, client_msg_id)', + 'SELECT 1' +); +PREPARE im_stmt FROM @im_sql; +EXECUTE im_stmt; +DEALLOCATE PREPARE im_stmt; + +-- ============================================================ +-- Tenant database: chapter 8, realtime call records. +-- ============================================================ + +CREATE TABLE IF NOT EXISTS tinode.im_call_records ( + id BIGINT NOT NULL AUTO_INCREMENT, + call_id VARCHAR(64) NOT NULL, + topic CHAR(25) NOT NULL, + seq_id INT NOT NULL, + caller_id BIGINT NOT NULL, + callee_id BIGINT NOT NULL, + call_type VARCHAR(16) NOT NULL, + status VARCHAR(32) NOT NULL, + end_reason VARCHAR(64) NOT NULL DEFAULT '', + duration_ms BIGINT NOT NULL DEFAULT 0, + started_at DATETIME(3) NOT NULL, + accepted_at DATETIME(3), + finished_at DATETIME(3), + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (id), + UNIQUE INDEX uk_im_call_records_call (call_id), + UNIQUE INDEX uk_im_call_records_topic_seq (topic, seq_id), + INDEX idx_im_call_records_caller (caller_id, id DESC), + INDEX idx_im_call_records_callee (callee_id, id DESC), + INDEX idx_im_call_records_status (status, updated_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ============================================================ +-- Tenant database: chapter 9, workbench and office collaboration. +-- ============================================================ + +CREATE TABLE IF NOT EXISTS tinode.im_workbench_apps ( + id BIGINT NOT NULL AUTO_INCREMENT, + code VARCHAR(64) NOT NULL, + name VARCHAR(128) NOT NULL, + icon_file_id VARCHAR(64) NOT NULL DEFAULT '', + route VARCHAR(128) NOT NULL, + sort_order INT NOT NULL DEFAULT 0, + unread_key VARCHAR(64) NOT NULL DEFAULT '', + enabled TINYINT NOT NULL DEFAULT 1, + permission VARCHAR(64) NOT NULL DEFAULT 'enabled', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (id), + UNIQUE INDEX uk_im_workbench_apps_code (code), + INDEX idx_im_workbench_apps_enabled_order (enabled, sort_order) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_org_departments ( + id BIGINT NOT NULL AUTO_INCREMENT, + department_id VARCHAR(64) NOT NULL, + name VARCHAR(128) NOT NULL, + parent_id VARCHAR(64) NOT NULL DEFAULT 'root', + sort_order INT NOT NULL DEFAULT 0, + version INT NOT NULL DEFAULT 1, + state VARCHAR(16) NOT NULL DEFAULT 'active', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (id), + UNIQUE INDEX uk_im_org_departments_department (department_id), + INDEX idx_im_org_departments_parent (parent_id, state, sort_order, id), + INDEX idx_im_org_departments_name (name) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_org_members ( + id BIGINT NOT NULL AUTO_INCREMENT, + department_id VARCHAR(64) NOT NULL, + user_id BIGINT NOT NULL, + display_name VARCHAR(128) NOT NULL DEFAULT '', + position VARCHAR(128) NOT NULL DEFAULT '', + avatar_file_id VARCHAR(64) NOT NULL DEFAULT '', + mobile VARCHAR(32) NOT NULL DEFAULT '', + email VARCHAR(128) NOT NULL DEFAULT '', + employment_state VARCHAR(16) NOT NULL DEFAULT 'active', + contactable TINYINT NOT NULL DEFAULT 1, + sort_order INT NOT NULL DEFAULT 0, + version INT NOT NULL DEFAULT 1, + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (id), + UNIQUE INDEX uk_im_org_members_department_user (department_id, user_id), + INDEX idx_im_org_members_department (department_id, employment_state, sort_order, id), + INDEX idx_im_org_members_user (user_id), + INDEX idx_im_org_members_display_name (display_name), + CONSTRAINT fk_im_org_members_department FOREIGN KEY (department_id) REFERENCES tinode.im_org_departments(department_id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_meetings ( + id BIGINT NOT NULL AUTO_INCREMENT, + meeting_id VARCHAR(64) NOT NULL, + organizer_id BIGINT NOT NULL, + title VARCHAR(200) NOT NULL, + start_at DATETIME(3) NOT NULL, + end_at DATETIME(3) NOT NULL, + location VARCHAR(256) NOT NULL DEFAULT '', + state VARCHAR(16) NOT NULL DEFAULT 'scheduled', + reminder_minutes JSON, + idempotency_key VARCHAR(128), + version INT NOT NULL DEFAULT 1, + canceled_at DATETIME(3), + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (id), + UNIQUE INDEX uk_im_meetings_meeting (meeting_id), + UNIQUE INDEX uk_im_meetings_idempotency (organizer_id, idempotency_key), + INDEX idx_im_meetings_organizer (organizer_id, start_at), + INDEX idx_im_meetings_state_time (state, start_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_meeting_participants ( + id BIGINT NOT NULL AUTO_INCREMENT, + meeting_id VARCHAR(64) NOT NULL, + user_id BIGINT NOT NULL, + response VARCHAR(16) NOT NULL DEFAULT 'pending', + response_at DATETIME(3), + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (id), + UNIQUE INDEX uk_im_meeting_participants_user (meeting_id, user_id), + INDEX idx_im_meeting_participants_user_time (user_id, meeting_id), + INDEX idx_im_meeting_participants_response (meeting_id, response), + CONSTRAINT fk_im_meeting_participants_meeting FOREIGN KEY (meeting_id) REFERENCES tinode.im_meetings(meeting_id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_daily_reports ( + id BIGINT NOT NULL AUTO_INCREMENT, + report_id VARCHAR(64) NOT NULL, + author_id BIGINT NOT NULL, + report_date DATE NOT NULL, + content TEXT NOT NULL, + attachments JSON, + status VARCHAR(16) NOT NULL DEFAULT 'draft', + read_state VARCHAR(16) NOT NULL DEFAULT 'unread', + version INT NOT NULL DEFAULT 1, + submitted_at DATETIME(3), + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (id), + UNIQUE INDEX uk_im_daily_reports_report (report_id), + UNIQUE INDEX uk_im_daily_reports_author_date (author_id, report_date), + INDEX idx_im_daily_reports_author_id (author_id, id DESC), + INDEX idx_im_daily_reports_date_status (report_date, status, id DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_monthly_reports ( + id BIGINT NOT NULL AUTO_INCREMENT, + report_id VARCHAR(64) NOT NULL, + author_id BIGINT NOT NULL, + report_month CHAR(7) NOT NULL, + work_summary TEXT NOT NULL, + next_plan TEXT NOT NULL, + attachments JSON, + status VARCHAR(16) NOT NULL DEFAULT 'draft', + read_state VARCHAR(16) NOT NULL DEFAULT 'unread', + version INT NOT NULL DEFAULT 1, + submitted_at DATETIME(3), + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (id), + UNIQUE INDEX uk_im_monthly_reports_report (report_id), + UNIQUE INDEX uk_im_monthly_reports_author_month (author_id, report_month), + INDEX idx_im_monthly_reports_author_id (author_id, id DESC), + INDEX idx_im_monthly_reports_month_status (report_month, status, id DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_mailbox_config ( + id BIGINT NOT NULL AUTO_INCREMENT, + enabled TINYINT NOT NULL DEFAULT 1, + allow_anonymous TINYINT NOT NULL DEFAULT 1, + required_fields JSON, + max_length INT NOT NULL DEFAULT 5000, + attachment_policy JSON, + notice_title VARCHAR(128) NOT NULL DEFAULT '', + notice_content TEXT NOT NULL, + notice_updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_mailbox_submissions ( + id BIGINT NOT NULL AUTO_INCREMENT, + submission_id VARCHAR(64) NOT NULL, + sender_id BIGINT NOT NULL, + subject VARCHAR(200) NOT NULL, + content TEXT NOT NULL, + anonymous TINYINT NOT NULL DEFAULT 0, + attachment_file_ids JSON, + notice_version VARCHAR(64) NOT NULL DEFAULT '', + idempotency_key VARCHAR(128) NOT NULL, + status VARCHAR(32) NOT NULL DEFAULT 'received', + submitted_at DATETIME(3) NOT NULL, + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (id), + UNIQUE INDEX uk_im_mailbox_submissions_submission (submission_id), + UNIQUE INDEX uk_im_mailbox_submissions_idempotency (sender_id, idempotency_key), + INDEX idx_im_mailbox_submissions_sender (sender_id, id DESC), + INDEX idx_im_mailbox_submissions_status (status, id DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ============================================================ +-- Tenant database: chapter 10, profile, privacy, app release, push, youth mode. +-- ============================================================ + +CREATE TABLE IF NOT EXISTS tinode.im_user_profiles ( + user_id BIGINT NOT NULL, + avatar_file_id VARCHAR(64) NOT NULL DEFAULT '', + nickname VARCHAR(64) NOT NULL DEFAULT '', + birth_date DATE, + signature VARCHAR(256) NOT NULL DEFAULT '', + gender VARCHAR(16) NOT NULL DEFAULT '', + region VARCHAR(128) NOT NULL DEFAULT '', + public_account VARCHAR(64) NOT NULL DEFAULT '', + version INT NOT NULL DEFAULT 1, + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (user_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_user_privacy_settings ( + user_id BIGINT NOT NULL, + add_me_methods JSON, + profile_visibility VARCHAR(32) NOT NULL DEFAULT 'friends', + last_seen_visibility VARCHAR(32) NOT NULL DEFAULT 'friends', + read_receipts TINYINT NOT NULL DEFAULT 1, + version INT NOT NULL DEFAULT 1, + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (user_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_app_releases ( + id BIGINT NOT NULL AUTO_INCREMENT, + platform VARCHAR(32) NOT NULL, + channel VARCHAR(32) NOT NULL DEFAULT 'stable', + latest_version VARCHAR(64) NOT NULL, + min_supported_version VARCHAR(64) NOT NULL, + update_type VARCHAR(16) NOT NULL DEFAULT 'none', + release_notes TEXT NOT NULL, + store_url VARCHAR(512) NOT NULL DEFAULT '', + package_url VARCHAR(512) NOT NULL DEFAULT '', + checksum VARCHAR(128) NOT NULL DEFAULT '', + remind_after_seconds INT NOT NULL DEFAULT 0, + published_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (id), + UNIQUE INDEX uk_im_app_releases_platform_channel (platform, channel) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_device_push_tokens ( + device_id VARCHAR(128) NOT NULL, + user_id BIGINT NOT NULL, + provider VARCHAR(32) NOT NULL, + token VARCHAR(512) NOT NULL, + app_version VARCHAR(64) NOT NULL DEFAULT '', + platform VARCHAR(32) NOT NULL DEFAULT '', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (device_id), + UNIQUE INDEX uk_im_device_push_tokens_provider_token (provider, token), + INDEX idx_im_device_push_tokens_user (user_id, updated_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ============================================================ +-- Tenant database: chapter 11, risk challenge, abuse report, feedback. +-- ============================================================ + +CREATE TABLE IF NOT EXISTS tinode.im_risk_challenges ( + id BIGINT NOT NULL AUTO_INCREMENT, + challenge_id VARCHAR(64) NOT NULL, + user_id BIGINT NOT NULL, + action VARCHAR(64) NOT NULL, + level VARCHAR(16) NOT NULL, + notice_code VARCHAR(64) NOT NULL, + notice_params JSON, + state VARCHAR(16) NOT NULL DEFAULT 'pending', + expires_at DATETIME(3) NOT NULL, + decision VARCHAR(16) NOT NULL DEFAULT '', + action_token_id VARCHAR(64) NOT NULL DEFAULT '', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (id), + UNIQUE INDEX uk_im_risk_challenges_challenge (challenge_id), + INDEX idx_im_risk_challenges_user_state (user_id, state, expires_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_abuse_reasons ( + id BIGINT NOT NULL AUTO_INCREMENT, + target_type VARCHAR(16) NOT NULL, + reason_code VARCHAR(64) NOT NULL, + label VARCHAR(128) NOT NULL, + requires_description TINYINT NOT NULL DEFAULT 0, + allows_evidence TINYINT NOT NULL DEFAULT 1, + sort_order INT NOT NULL DEFAULT 0, + enabled TINYINT NOT NULL DEFAULT 1, + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (id), + UNIQUE INDEX uk_im_abuse_reasons_type_code (target_type, reason_code), + INDEX idx_im_abuse_reasons_type_order (target_type, enabled, sort_order) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_abuse_reports ( + id BIGINT NOT NULL AUTO_INCREMENT, + report_id VARCHAR(64) NOT NULL, + reporter_id BIGINT NOT NULL, + target_type VARCHAR(16) NOT NULL, + target_user_id BIGINT NOT NULL DEFAULT 0, + target_topic VARCHAR(32) NOT NULL DEFAULT '', + target_seq INT NOT NULL DEFAULT 0, + reason_code VARCHAR(64) NOT NULL, + description TEXT NOT NULL, + evidence_file_ids JSON, + snapshot JSON, + idempotency_key VARCHAR(128) NOT NULL, + status VARCHAR(32) NOT NULL DEFAULT 'received', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (id), + UNIQUE INDEX uk_im_abuse_reports_report (report_id), + UNIQUE INDEX uk_im_abuse_reports_idempotency (reporter_id, idempotency_key), + INDEX idx_im_abuse_reports_target (target_type, target_topic, target_seq, target_user_id), + INDEX idx_im_abuse_reports_reporter (reporter_id, id DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_feedback_config ( + id BIGINT NOT NULL AUTO_INCREMENT, + categories JSON, + max_length INT NOT NULL DEFAULT 1000, + attachment_policy JSON, + allow_contact TINYINT NOT NULL DEFAULT 1, + diagnostics_notice TEXT NOT NULL, + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_feedback_submissions ( + id BIGINT NOT NULL AUTO_INCREMENT, + feedback_id VARCHAR(64) NOT NULL, + user_id BIGINT NOT NULL, + category VARCHAR(64) NOT NULL, + content TEXT NOT NULL, + contact VARCHAR(128) NOT NULL DEFAULT '', + attachment_file_ids JSON, + client_context JSON, + include_diagnostics TINYINT NOT NULL DEFAULT 0, + idempotency_key VARCHAR(128) NOT NULL, + status VARCHAR(32) NOT NULL DEFAULT 'received', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (id), + UNIQUE INDEX uk_im_feedback_submissions_feedback (feedback_id), + UNIQUE INDEX uk_im_feedback_submissions_idempotency (user_id, idempotency_key), + INDEX idx_im_feedback_submissions_user (user_id, id DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ============================================================ +-- Seed data. +-- ============================================================ + +INSERT INTO hotgo.im_tenant(id, code, name, tenant_desc, db_name, db_dsn, state, created_by, updated_by) +VALUES (1, 'default', 'Default Tenant', NULL, 'tinode', '', 1, 0, 0) +ON DUPLICATE KEY UPDATE + name = VALUES(name), + db_name = VALUES(db_name), + state = VALUES(state), + updated_at = CURRENT_TIMESTAMP(3); + +INSERT INTO hotgo.im_tenant_config(tenant_id, max_users, max_groups, max_group_members, created_by, updated_by) +SELECT id, 0, 0, 0, 0, 0 FROM hotgo.im_tenant WHERE code = 'default' +ON DUPLICATE KEY UPDATE updated_at = CURRENT_TIMESTAMP(3); + +INSERT INTO tinode.im_workbench_apps(code, name, route, sort_order, unread_key, enabled, permission) +VALUES + ('organization', '组织架构', '/organization', 10, 'organization', 1, 'enabled'), + ('meetings', '会议通知', '/meetings', 20, 'meetings', 1, 'enabled'), + ('daily_reports', '日报', '/reports/daily', 30, 'daily_reports', 1, 'enabled'), + ('monthly_reports', '月报', '/reports/monthly', 40, 'monthly_reports', 1, 'enabled'), + ('executive_mailbox', '总经理信箱', '/executive-mailbox', 50, 'executive_mailbox', 1, 'enabled') +ON DUPLICATE KEY UPDATE + name = VALUES(name), + route = VALUES(route), + sort_order = VALUES(sort_order), + unread_key = VALUES(unread_key), + updated_at = CURRENT_TIMESTAMP(3); + +INSERT INTO tinode.im_mailbox_config(id, enabled, allow_anonymous, required_fields, max_length, attachment_policy, notice_title, notice_content) +VALUES (1, 1, 1, '["subject","content"]', 5000, '{"max_files":5,"purpose":"executive_mailbox"}', '总经理信箱', '请如实填写诉求,匿名提交将按企业政策保护展示身份。') +ON DUPLICATE KEY UPDATE updated_at = updated_at; + +INSERT INTO tinode.im_app_releases(platform, channel, latest_version, min_supported_version, update_type, release_notes, remind_after_seconds) +VALUES + ('ios', 'stable', '1.0.0', '1.0.0', 'none', '当前已是最新版本。', 0), + ('android', 'stable', '1.0.0', '1.0.0', 'none', '当前已是最新版本。', 0) +ON DUPLICATE KEY UPDATE updated_at = CURRENT_TIMESTAMP(3); + +INSERT INTO tinode.im_abuse_reasons(target_type, reason_code, label, requires_description, allows_evidence, sort_order) +VALUES + ('user', 'fraud', '诈骗欺诈', 0, 1, 10), + ('user', 'harassment', '骚扰辱骂', 0, 1, 20), + ('user', 'illegal', '违法违规', 1, 1, 30), + ('group', 'fraud', '诈骗欺诈', 0, 1, 10), + ('group', 'spam', '垃圾广告', 0, 1, 20), + ('group', 'illegal', '违法违规', 1, 1, 30), + ('message', 'fraud', '诈骗欺诈', 0, 1, 10), + ('message', 'spam', '垃圾广告', 0, 1, 20), + ('message', 'harassment', '骚扰辱骂', 0, 1, 30), + ('message', 'illegal', '违法违规', 1, 1, 40) +ON DUPLICATE KEY UPDATE + label = VALUES(label), + requires_description = VALUES(requires_description), + allows_evidence = VALUES(allows_evidence), + sort_order = VALUES(sort_order), + updated_at = CURRENT_TIMESTAMP(3); + +INSERT INTO tinode.im_feedback_config(id, categories, max_length, attachment_policy, allow_contact, diagnostics_notice) +VALUES (1, '["bug","suggestion","complaint","other"]', 1000, '{"max_files":5,"purpose":"feedback"}', 1, '仅在你同意时上传脱敏诊断信息。') +ON DUPLICATE KEY UPDATE updated_at = updated_at; + +-- Current MySQL adapter schema version expected by server/db/mysql/adapter.go. +UPDATE tinode.kvmeta SET `value` = 123 WHERE `key` = 'version'; diff --git a/docs/im/IM-服务端多租户改造方案.md b/docs/im/IM-服务端多租户改造方案.md new file mode 100644 index 0000000..eabd8ac --- /dev/null +++ b/docs/im/IM-服务端多租户改造方案.md @@ -0,0 +1,932 @@ +# IM 服务端多租户改造方案 + +> **方案变更说明,2026-08-13:当前多租户技术路线已调整为“控制库 + 每租户独立业务库”。本文原主体内容按“共享库共享表 + tenant_id”编写,只作为旧方案参考,不再作为后续开发准绳。新的落地方案见 [IM 独立库多租户改造方案](IM-独立库多租户改造方案.md)。** +> +> 新方案下,业务库中的 `users`、`auth`、`topics`、`messages`、`fileuploads` 和后续产品业务表不再增加 `tenant_id` 字段;`TenantID` 仍保留在 Session、Token、Hub、缓存、Push、文件授权、插件上下文和 Store 选库流程中,用于定位租户业务库和运行时隔离。 + +> 编写日期:2026-08-10 +> 适用项目:`im_server` 当前服务端源码 +> 需求背景:[IM 产品原型需求文档](IM-产品原型需求文档.md) 第 1.1 节“企业码登录”及第 9 章“企业工作台与办公协同” +> 关联文档:[IM 产品需求服务端改造分析报告](IM-产品需求服务端改造分析报告.md) + +> 实施状态:第一阶段租户识别已落地。MySQL Adapter schema 117 创建 `im_tenant`、`im_tenant_config` 和默认租户;首次 `hi` 使用企业码解析并固定 Session TenantID。核心业务表的 `tenant_id`、租户化认证和路由隔离尚未完成。 + +## 1. 方案目标 + +当前项目是单租户 Tinode 服务端。用户、认证记录、Topic、Subscription、Message、File、缓存、集群路由和 Push 都没有租户维度。本方案目标是在一套服务集群和一套 MySQL 数据库中安全承载多个企业租户,并满足以下约束: + +1. 一个企业的数据默认只能被本企业用户和管理员访问; +2. 同一手机号、用户名可以在不同企业分别注册; +3. 单聊、群聊、搜索、文件、Push、系统消息和后台任务均不可跨租户; +4. 租户身份由服务端在握手/认证阶段确定,客户端后续消息不能修改; +5. 现有 `usr...`、`grp...`、`p2p...` Topic 格式尽量保持兼容; +6. 多租户启用后,可以按租户配置功能、配额、认证方式、数据保留和安全策略。 + +## 2. 当前实现为什么不支持多租户 + +### 2.1 认证和 Session 没有租户 + +- `auth.Rec` 只有 `Uid`、`AuthLevel`、有效期、状态等字段,没有 `TenantID`; +- `Session` 只保存 `uid` 和 `authLvl`,登录成功后直接把认证记录写入 Session; +- `MsgClientHi`、`MsgClientLogin` 和 Token 中均没有租户; +- basic 登录名在数据库中全局唯一,手机号/邮箱凭证也全局唯一; +- 密码找回按 `method:value` 全局查找账号; +- root 用户可以通过 `Extra.AsUser` 代表任意用户操作,没有租户范围。 + +这意味着同一手机号无法在两个企业分别注册,也无法证明当前连接属于哪个企业。 + +### 2.2 数据库全部按全局键查询 + +改造前 MySQL schema 版本为 116;第一阶段建立租户主表和握手解析后为 117。除租户主数据表外,核心表仍没有 `tenant_id`: + +| 表 | 当前关键唯一约束 | 多租户问题 | +| --- | --- | --- | +| `users` | `id` | 无租户归属 | +| `auth` | `uname` 全局唯一 | 不同企业不能使用相同登录名 | +| `credentials` | `synthetic` 全局唯一 | 同一手机号不能加入多个企业 | +| `usertags` | 用户/Tag | 搜索可能返回其他企业用户 | +| `devices` | 设备 Hash 全局唯一 | 同一设备上的多企业账号会冲突 | +| `topics` | `name` 全局唯一 | 没有 Topic 租户边界 | +| `subscriptions` | Topic/User | 不能证明用户与 Topic 属于同一租户 | +| `messages` | Topic/SeqId | 查询只按 Topic,没有租户条件 | +| `dellog` | Topic/User | 删除记录没有租户条件 | +| `fileuploads` | File ID | 文件查询和清理没有租户范围 | +| `filemsglinks` | File/Message/Topic/User | 附件关联不能强制同租户 | + +Store/Adapter 的方法也只接收 UID 或 Topic,例如: + +```go +UserGet(uid) +AuthGetUniqueRecord(unique) +TopicGet(topic) +SubscriptionGet(topic, uid, keepDeleted) +MessageGetAll(topic, uid, opts) +FileGet(fileID) +``` + +只在 SQL 表加字段但不修改这些接口,后续开发很容易漏写 `WHERE tenant_id = ?`,仍然会发生越权。 + +### 2.3 内存、集群和系统 Topic 没有租户 + +- Hub 的 `topics` 使用裸 Topic 名作为 `sync.Map` Key; +- 集群一致性哈希只对裸 Topic 名计算; +- `ClusterSess` 和集群 RPC 没有租户字段; +- User Cache 仅以 UID 为 Key; +- Push Receipt、FCM Channel 只有 UID/Topic,没有租户; +- Hub 启动时只初始化一个字面值为 `sys` 的全局系统 Topic; +- 持久化缓存 Key 没有租户命名空间。 + +如果两个租户都使用 `sys` 等逻辑 Topic,同一个内存对象会被共享。即使普通 Topic 名当前由全局 UID 生成,也不能依赖“ID 不碰撞”代替租户授权。 + +### 2.4 文件接口只获得 UID + +`authFileRequest` 当前只返回 UID。文件下载查询只按 File ID,无法把文件访问限制在当前租户。对象存储 Location 也没有统一的租户目录规则。 + +## 3. 关键设计决策 + +### 3.1 目标模式 + +本方案采用: + +> **共享服务集群 + 共享 MySQL 数据库 + 共享表 + 每行 `tenant_id` 强隔离** + +该模式最符合“多企业共用一套平台”的目标,但也是改造面最大的模式。若实际只需要企业数据物理隔离,采用“一企一实例/一企一库”会更简单;它不属于本文的共享多租户实现。 + +### 3.2 用户模型:租户内账号 + +推荐同一个自然人在不同企业中对应不同的 Tinode User: + +```text +手机号 138****0000 + ├─ 租户 A -> user A1 -> 独立好友、群、消息和设置 + └─ 租户 B -> user B1 -> 独立好友、群、消息和设置 +``` + +这样可以最大程度复用当前 User/Topic/Subscription 模型,离职、停用、数据导出和删除也能按企业处理。 + +如果以后需要一个账号切换多个企业,可新增 `global_identities` 和 `tenant_memberships`,把全局身份映射到各租户的本地 User。不要在第一阶段直接把当前 User 改造成跨租户共享主体,否则所有 P2P、资料、设备、未读数和隐私规则都会变得更复杂。 + +### 3.3 ID 策略:保持全局唯一,额外校验租户 + +继续使用现有 Snowflake/加密 UID 生成器,User ID、File ID、普通 Group Topic 名保持全局唯一。外部协议继续使用现有 `usr...`、`grp...`、`p2p...` 格式,不把租户编码暴露在这些 ID 中。 + +所有数据库查询仍必须带 `tenant_id`,原因是: + +- 全局唯一只防止碰撞,不能证明调用者有权访问; +- 已知或泄露的其他租户 ID 仍然可能被直接请求; +- 使用复合租户条件可以让越权请求稳定返回不存在。 + +### 3.4 跨租户通信默认禁止 + +第一阶段规则: + +- 用户搜索只在当前租户; +- P2P 双方必须属于同一租户; +- Group Topic 及全部成员必须属于同一租户; +- 文件、收藏、搜索结果和系统通知不跨租户; +- Tenant Admin 只能管理本租户; +- Platform Admin 的跨租户操作只能走独立后台 API,禁止使用普通客户端协议随意 `AsUser`。 + +未来若需要跨企业协作,应作为“外部联系人/联邦通信”独立设计,使用明确授权、双边租户策略和审计,不能绕过本方案的同租户不变量。 + +## 4. 租户领域模型 + +### 4.1 TenantID 与租户上下文 + +新增强类型,不使用裸字符串在各层传递: + +```go +package types + +type TenantID int64 + +const ZeroTenantID TenantID = 0 + +type TenantContext struct { + TenantID TenantID + ActorUID Uid +} + +type TopicKey struct { + TenantID TenantID + Topic string +} + +type UserKey struct { + TenantID TenantID + UID Uid +} +``` + +`TenantID = 0` 仅允许出现在“尚未完成租户握手”的连接和平台级任务中。任何租户业务 Store 调用发现 `ZeroTenantID` 应立即返回错误,不能自动降级为全局查询。 + +`context.Context` 继续用于超时和取消;租户 ID 应作为显式的强类型参数或租户作用域对象,不能只隐藏在 `context.Value` 中。 + +### 4.2 租户表 + +建议新增: + +```sql +CREATE TABLE im_tenant ( + id BIGINT NOT NULL AUTO_INCREMENT, + code VARCHAR(64) NOT NULL, + name VARCHAR(128) NOT NULL, + tenant_desc VARCHAR(256), + state SMALLINT NOT NULL, + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + created_by BIGINT NOT NULL, + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + updated_by BIGINT NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY tenants_code (code) +); +``` + +`created_by` 和 `updated_by` 保存操作人用户 ID;系统初始化默认租户时使用 `0` 表示系统操作。 + +当前阶段不在租户主表预留 `config_version` 和 `data_region`。真正建设租户配置中心或多地域路由时,再通过独立的 `tenant_settings` 表增加相应版本/地域字段,避免提前维护没有实际使用场景的列。 + +租户状态建议包括: + +| 状态 | 登录 | 读消息 | 发消息/写业务 | 管理操作 | +| --- | --- | --- | --- | --- | +| `provisioning` | 禁止 | 禁止 | 禁止 | 平台管理员可配置 | +| `active` | 允许 | 允许 | 允许 | 按角色允许 | +| `suspended` | 禁止新登录 | 可配置只读 | 禁止 | 平台管理员可恢复/导出 | +| `deleting` | 禁止 | 禁止 | 禁止 | 仅执行删除任务 | +| `deleted` | 禁止 | 禁止 | 禁止 | 只保留必要审计 | + +### 4.3 租户配置与配额 + +第一阶段只建立 `im_tenant_config`,保存用户数、群数和单群成员数上限。每个租户最多一条配置;三个上限字段为 `0` 时表示不限制。 + +```sql +CREATE TABLE im_tenant_config ( + id BIGINT NOT NULL AUTO_INCREMENT, + tenant_id BIGINT NOT NULL, + max_users BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited', + max_groups BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited', + max_group_members INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + created_by BIGINT NOT NULL, + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) + ON UPDATE CURRENT_TIMESTAMP(3), + updated_by BIGINT NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY uk_im_tenant_config_tenant_id (tenant_id), + CONSTRAINT fk_im_tenant_config_tenant + FOREIGN KEY (tenant_id) REFERENCES im_tenant(id) + ON DELETE CASCADE +); +``` + +`created_by` 和 `updated_by` 保存操作人用户 ID。系统初始化时可以使用约定的系统操作人 ID;在平台管理员账号模型确定前,暂不为这两个字段建立用户外键。 + +认证、文件、消息频控、功能开关、保留期、合规策略、第三方服务配置及企业品牌等配置本阶段不建字段,待有明确业务需求后再增加。 + +## 5. 租户识别与认证流程 + +### 5.1 推荐流程 + +```mermaid +sequenceDiagram + participant A as App + participant S as Session + participant DB as MySQL + + A->>S: 建立 WebSocket + A->>S: hi(tenant_code) + S->>DB: 按 code 查询租户 + DB-->>S: tenant + S->>S: 校验 active 并固定 TenantID + S-->>A: hi success + 公开租户信息 + A->>S: login(scheme, secret) +``` + +第一阶段不引入 Tenant Resolve API 和 `tenant_ticket`。企业码不是密码,客户端可直接提交;服务端必须查询数据库并只接受 `active` 租户,客户端不能直接提交数据库 TenantID。 + +`tenant_ticket` 的价值在于把一次企业码解析结果变成服务端签名的短期凭证,防止客户端伪造内部 TenantID,并减少 WebSocket 节点的重复查询;它还可以携带目标集群、过期时间和随机数。当前是单集群且刚开始建设,直接查询更简单。以后出现多集群路由或租户解析查询成为性能瓶颈时,可以再增加 ticket,不影响 Session 固定 TenantID 的核心约束。 + +### 5.2 协议修改 + +在 `MsgClientHi` 增加: + +```go +Tenant string `json:"tenant,omitempty"` +``` + +该字段当前直接传企业码,不传数据库 TenantID。规则如下: + +1. 首次 `hi` 必须设置 Tenant; +2. Session 一旦绑定租户,后续 `hi/login/acc/pub/get/set/del/note` 都不能修改; +3. 注册、登录和密码找回前必须已绑定租户; +4. Tenant 缺失、为空或超过 64 个字符时拒绝握手,不做默认租户兼容; +5. 租户不存在时返回 404,租户不是 `active` 状态时返回 403; +6. 服务端返回的租户信息仅包含 `code` 和 `name`,不返回内部 ID、状态或密钥。 + +当前已实现 `MsgClientHi.Tenant`、MySQL 按企业码查询、Session TenantID 固定、租户公开信息响应,以及未绑定租户时拒绝后续命令。认证器和业务表尚未带 TenantID,因此当前代码只是租户识别基础,不能视为已经完成数据隔离。 + +后续 HTTP API 的 TenantID 必须从租户化签名 Token 中获得。禁止只信任 URL、Header 或请求 Body 中客户端自报的 `tenant_id`。 + +### 5.3 认证接口修改 + +当前认证器接口: + +```go +Authenticate(secret []byte, remoteAddr string) +``` + +建议改为: + +```go +type AuthContext struct { + TenantID types.TenantID + RemoteAddr string +} + +Authenticate(ctx AuthContext, secret []byte) +``` + +`auth.Rec` 增加: + +```go +TenantID types.TenantID `json:"tenant_id,omitempty"` +``` + +basic、REST、code、token 等认证器和所有 Mock 必须同步修改。认证器返回 TenantID 与 Session 不一致时,统一按认证失败处理,并写安全审计。 + +### 5.4 登录名和凭证的租户唯一性 + +数据库约束修改为: + +```text +auth: UNIQUE (tenant_id, uname) +auth: UNIQUE (tenant_id, userid, scheme) +credentials: UNIQUE (tenant_id, synthetic) +devices: UNIQUE (tenant_id, hash) +``` + +这样同一手机号可以分别加入多个企业。密码找回必须先有 TenantContext,再按 `(tenant_id, method, value)` 查找,不能全库搜索手机号后选择第一个用户。 + +### 5.5 Token v2 + +当前 HMAC Token 固定包含 UID、过期时间、认证级别、序列号和特性位,没有 TenantID。需要新增带版本的 Token v2: + +```text +[version][tenant_id][uid][expires][auth_level][serial][features][signature] +``` + +Token 规则: + +1. v2 Token 的签名覆盖 TenantID; +2. Token 认证返回 `TenantID + UID`; +3. Session 已绑定租户时必须与 Token 一致; +4. 不包含 TenantID 的 Token 一律拒绝; +5. 租户停用时应支持按租户 Token 版本或撤销时间拒绝已签发 Token,不能只能全平台提升 serial。 + +### 5.6 管理员模型 + +当前 `AuthLevel` 只有 NONE、ANON、AUTH、ROOT,不足以表达租户管理员。建议保留认证级别,同时增加业务角色: + +- `platform_admin`:平台级租户生命周期和运维; +- `tenant_owner`:本租户最高管理员; +- `tenant_admin`:组织、用户、群和安全管理; +- `security_auditor`:实名、举报、审计读取; +- 普通用户。 + +Tenant Admin 的权限必须绑定 TenantID。普通 WebSocket 的 `Extra.AsUser` 只能在同租户且有明确授权时使用;平台管理员跨租户代操作应走隔离的后台 API、二次验证和完整审计。 + +## 6. Session、消息和 Topic 改造 + +### 6.1 Session + +`Session`、`ClusterSess` 增加: + +```go +tenantID types.TenantID +``` + +需要建立以下不变量: + +```text +已认证 Session: +session.tenantID != 0 +session.uid != 0 +User(session.uid).tenant_id == session.tenantID +``` + +所有内部 `ClientComMessage` 和 `ServerComMessage` 应携带服务端写入的 TenantID,序列化给普通客户端时不必暴露。插件、集群和后台任务可以读取,但不得覆盖。 + +### 6.2 内部路由键 + +外部 Topic 名保持不变,内部统一使用: + +```go +func (k TopicKey) RoutingKey() string { + return strconv.FormatUint(uint64(k.TenantID), 10) + ":" + k.Topic +} +``` + +需要修改: + +- Hub `topicGet/topicPut/topicDel`; +- Session 的订阅 Map; +- Topic Proxy 和 Multiplex Session Key; +- 集群一致性哈希 `nodeForTopic/isRemoteTopic`; +- ClusterReq、ClusterRoute、ClusterResp; +- Topic 重哈希和代理失效; +- 调试端点中显示的 Topic 标识。 + +客户端仍看到原始 Topic 名,只有服务端内部使用 RoutingKey。不要直接把 `tenantID:` 前缀写入数据库 Topic 名或回传客户端,否则会破坏 Topic 类别解析、P2P 解析和现有客户端。 + +### 6.3 `sys`、`me`、`fnd`、`slf` + +- `me`、`fnd`、`slf` 虽由全局 UID 派生,仍必须使用 Session TenantID 加载; +- 全局字面值 `sys` 必须改为“租户作用域内的逻辑别名”; +- Hub 不再启动一个全局 `sys` Topic;应在租户首次使用或租户创建时初始化 `(tenant_id, sys)`; +- 系统消息生产者必须指定 TenantID,不能广播到全平台 `sys`; +- `fnd` 搜索只读取当前租户 Tags; +- `slf` 收藏不能引用其他租户的源消息或文件。 + +### 6.4 P2P + +P2P Topic 名仍由两个全局 UID 生成,但创建前必须执行: + +```text +initiator.tenant_id == invited.tenant_id == session.tenant_id +``` + +订阅、发送、读取、删除和通话时都继续带 TenantID 查询,避免通过已知 P2P 名访问其他企业。 + +### 6.5 群聊 + +创建 Group Topic 时由 Session 注入 TenantID。邀请、审批、管理员变更和群主转让必须验证: + +- Topic 属于当前租户; +- 操作者属于当前租户; +- 目标成员属于同一租户; +- 所有写入的 Subscription 使用同一 TenantID; +- Outbox 和系统通知也使用相同 TenantID。 + +不要接受客户端在 `set.desc`、Aux 或 Tags 中提交的 tenant 字段作为授权依据。 + +## 7. Store 与 Adapter 改造 + +### 7.1 不推荐只靠 `context.Value` + +租户条件应进入编译期可见的接口。推荐增加租户作用域 Store: + +```go +tenantStore := store.ForTenant(session.tenantID) + +user, err := tenantStore.Users.Get(uid) +topic, err := tenantStore.Topics.Get(topicName) +msgs, err := tenantStore.Messages.GetAll(topicName, uid, opts) +``` + +底层 Adapter 接口显式接收 TenantID: + +```go +UserGet(tenantID, uid) +AuthGetUniqueRecord(tenantID, unique) +TopicGet(tenantID, topic) +SubscriptionGet(tenantID, topic, uid, keepDeleted) +MessageGetAll(tenantID, topic, uid, opts) +FileGet(tenantID, fileID) +``` + +`store.ForTenant(ZeroTenantID)` 必须失败。平台级扫描和租户生命周期操作使用单独的 `PlatformStore` 接口,不能给业务代码一个可省略 TenantID 的全局 Store。 + +### 7.2 需要修改的 Store 接口范围 + +| 接口域 | 必须租户化的操作 | +| --- | --- | +| Users | Create/Get/GetAll/Update/Delete/State/Tags/Topics/Subs/Unread | +| Auth | Add/Get/Update/Delete/Unique lookup | +| Credentials | Upsert/Get/Confirm/Fail/Delete/Reset lookup | +| Topics | Create/Get/Share/Delete/Update/Owner/Find | +| Subscriptions | Create/Get/Update/Delete/按 User/Topic 查询 | +| Messages | Save/Get/Delete/GetDeleted/更新 Topic SeqId | +| Devices | Upsert/Get/Delete/Push Channel | +| Files | Start/Finish/Get/Link/DeleteUnused/Access check | +| Persistent Cache | Get/Upsert/Delete/Expire | + +这会修改 `server/store/store.go`、`server/db/adapter.go`、MySQL Adapter、Mock Store 以及大量调用点。不要为了减少改动只改少数查询;任何保留的无租户业务接口都会成为后续越权入口。 + +### 7.3 数据库支持范围 + +当前仓库还有 PostgreSQL、MongoDB、RethinkDB Adapter。多租户改造会使公共 Adapter 接口整体变化。 + +推荐产品版第一阶段明确只支持 MySQL: + +- MySQL 完成多租户 schema 和全部集成测试; +- 其他 Adapter 编译时可使用明确的 `ErrUnsupportedTenant` 或从产品构建中排除; +- 不能让未实现多租户过滤的 Adapter 在配置错误时继续启动; +- 如果必须继续支持四种数据库,应将工作量视为四套独立实现和隔离验收。 + +## 8. MySQL Schema 改造 + +### 8.1 统一规则 + +1. 所有租户业务表增加 `tenant_id BIGINT NOT NULL`; +2. 唯一索引和高频查询索引以 `tenant_id` 作为第一列; +3. 子表使用 `(tenant_id, parent_id)` 复合外键,防止跨租户关联; +4. `kvmeta` 中数据库 schema 版本属于平台级元数据,不加 TenantID; +5. 持久化业务缓存使用 TenantID 列或强制租户 Key 前缀; +6. 所有删除、更新、聚合和后台批处理同样带 TenantID; +7. MySQL 没有可直接依赖的应用级 RLS,隔离责任主要在 Adapter 和复合约束。 + +### 8.2 核心表与索引 + +| 表 | 新增列 | 主要索引调整 | +| --- | --- | --- | +| `users` | `tenant_id` | `UNIQUE(tenant_id,id)`;状态/最近登录索引前加 tenant | +| `usertags` | `tenant_id` | `INDEX(tenant_id,tag)`;`UNIQUE(tenant_id,userid,tag)` | +| `devices` | `tenant_id` | `UNIQUE(tenant_id,hash)`;复合外键到 users | +| `auth` | `tenant_id` | `UNIQUE(tenant_id,uname)`;`UNIQUE(tenant_id,userid,scheme)` | +| `topics` | `tenant_id` | `UNIQUE(tenant_id,name)`;Owner/State 索引前加 tenant | +| `topictags` | `tenant_id` | `INDEX(tenant_id,tag)`;`UNIQUE(tenant_id,topic,tag)` | +| `subscriptions` | `tenant_id` | `UNIQUE(tenant_id,topic,userid)`;User/Topic 查询前加 tenant | +| `messages` | `tenant_id` | `UNIQUE(tenant_id,topic,seqid)`;历史查询索引前加 tenant | +| `dellog` | `tenant_id` | Topic/User/DelId 索引前加 tenant | +| `credentials` | `tenant_id` | `UNIQUE(tenant_id,synthetic)`;User/Method 索引前加 tenant | +| `fileuploads` | `tenant_id` | `INDEX(tenant_id,status,updatedat)`;复合用户归属 | +| `filemsglinks` | `tenant_id` | File/Message/Topic/User 索引及复合外键全部带 tenant | + +Topic 名当前通常全局唯一,但仍把数据库唯一约束改为 `(tenant_id, name)`,这样各租户可以拥有自己的逻辑 `sys`,也让 schema 明确表达隔离边界。 + +### 8.3 业务表 + +后续新增的所有 `im_*` 表,包括好友、群审批、组织、会议、日报、举报、Outbox、审计和消息索引,必须从第一版就包含 TenantID。禁止先以全局表上线再补租户。 + +推荐所有 Repository 方法使用如下形态: + +```text +Get(tenantID, id) +List(tenantID, filter, cursor) +Create(tenantID, actorID, data) +Update(tenantID, id, expectedVersion, data) +Delete(tenantID, id) +``` + +### 8.4 SQL 示例 + +认证查询: + +```sql +SELECT userid, secret, expires, authlvl +FROM auth +WHERE tenant_id = ? AND uname = ?; +``` + +Topic 查询: + +```sql +SELECT ... +FROM topics +WHERE tenant_id = ? AND name = ? AND state <> ?; +``` + +消息查询: + +```sql +SELECT ... +FROM messages +WHERE tenant_id = ? AND topic = ? AND seqid < ? +ORDER BY seqid DESC +LIMIT ?; +``` + +所有 `UPDATE` 和 `DELETE` 必须同时带业务主键和 `tenant_id`。如果受影响行数为 0,统一返回 NotFound/Denied,不能再用无租户查询判断对象是否属于其他企业,以免泄露对象存在性。 + +## 9. 缓存与集群改造 + +### 9.1 Hub Topic Cache + +当前 Hub 使用裸 Topic 名作为 Key,应改为 `TopicKey.RoutingKey()`。Topic 对象自身增加 TenantID,初始化后不可修改。 + +加载 Topic 时同时检查: + +```text +requested tenant == loaded topic tenant == database row tenant +``` + +如果不一致,记录安全告警并拒绝把 Topic 放入缓存。 + +### 9.2 User Cache + +现有 UID 在全平台全局唯一,理论上可以继续只按 UID 缓存。但为避免导入数据、测试固定 UID 或代码误用造成污染,建议改为 `UserKey{TenantID, UID}`。 + +`UserCacheReq`、未读数 IO、Pending Push、EvictUser、集群 UserCacheUpdate 全部增加 TenantID。设备注销或账号停用只能影响指定租户的 User。 + +### 9.3 Session Store + +Session ID 继续全局随机即可,但以下操作必须带租户断言: + +- 通过 SID 完成文件认证; +- `EvictUser`; +- Cluster Proxy 恢复 Session; +- root/管理员代操作; +- 调试页面查看 Session。 + +建议新增 `EvictUser(tenantID, uid, skipSID)`,防止固定/导入 UID 冲突时误踢其他企业用户。 + +### 9.4 集群路由 + +一致性哈希由: + +```text +hash(topic) +``` + +改为: + +```text +hash(tenant_id + ":" + topic) +``` + +`ClusterSess`、`ClusterReq`、`ClusterRoute`、`ClusterResp` 和 User Cache RPC 增加 TenantID。接收端验证: + +- RPC 声明租户与消息内部租户一致; +- Session 租户与请求租户一致; +- RoutingKey 重新计算结果与目标节点一致; +- Plugin 返回的替换消息不能修改租户。 + +集群节点加入时必须校验协议版本,不理解 Tenant 字段的节点应拒绝加入集群。 + +### 9.5 持久化缓存 + +缓存 Key 统一通过函数生成: + +```go +TenantCacheKey(tenantID, namespace, key) +``` + +禁止业务代码自行字符串拼接。租户配置变化时只失效该租户 Key。平台级 schema/version Key 与租户业务 Cache 使用不同接口。 + +## 10. 文件与对象存储改造 + +### 10.1 HTTP 认证结果 + +`authFileRequest` 从只返回 UID 改为: + +```go +type RequestPrincipal struct { + TenantID types.TenantID + UID types.Uid + Roles []string +} +``` + +Token 认证和 SID 认证都必须得到相同 Principal。API Key 只表示合法客户端应用,不能替代 Tenant 身份和资源授权。 + +### 10.2 文件访问 + +新增: + +```go +FileCanAccess(tenantID, uid, fileID, action) +``` + +访问流程: + +1. `FileGet(tenantID, fileID)`; +2. 上传未完成时只允许同租户上传者访问; +3. 已关联消息时,Subscription 必须为同租户且有 Topic Read 权限; +4. 群文件删除检查同租户上传者/管理员/群主; +5. 转发时源文件、源消息和目标 Topic 全部属于同租户; +6. 生成短时签名 URL,签名上下文包含 TenantID/FileID/过期时间; +7. 审计日志记录 TenantID、UID、FileID 和结果。 + +### 10.3 存储路径 + +新文件统一使用: + +```text +tenants/{tenant_id}/yyyy/mm/{file_id} +``` + +Location 不直接返回客户端。垃圾回收任务按 TenantID 分批并设置公平配额,避免大租户占满清理队列。 + +## 11. 搜索、Tags 与组织数据 + +### 11.1 用户和 Topic 搜索 + +`Find`、`FindOne`、Tag 更新和精确账号搜索都必须带 TenantID。相同手机号/用户名在不同租户生成相同 Tag 没有问题,因为索引和唯一约束均以 TenantID 为前缀。 + +跨租户查询统一返回空结果或通用 NotFound,不能返回“该用户属于其他企业”。 + +### 11.2 消息搜索 + +消息索引记录必须包含 TenantID,搜索引擎索引名或文档路由也包含 TenantID: + +```text +tenant_id + topic + seq_id +``` + +查询至少经过两层过滤: + +1. 搜索请求强制加入 TenantID; +2. 返回结果按当前用户的同租户 Topic Read ACL 二次校验。 + +如果使用 OpenSearch/Elasticsearch,可以共享索引并按 TenantID 路由,也可以按大租户独立索引。无论哪种方式,TenantID 必须来自 Token,而不是客户端 Query 参数。 + +### 11.3 组织架构 + +`im_org_units`、`im_org_members` 等第 9 章业务表以 TenantID 为根。部门树的 `parent_id`、负责人、成员和数据权限都必须使用复合租户约束,禁止把其他租户 User 设置为部门负责人。 + +## 12. Push、插件和外部服务 + +### 12.1 Push + +`push.Receipt`、`ChannelReq` 和 `Payload` 内部增加 TenantID。要求: + +- 设备查询使用 `(tenant_id, uid)`; +- FCM Topic/Channel 名加入不可碰撞的租户前缀; +- 租户级 Push 模板、应用包和凭证由服务端配置选择; +- Push Handler 日志不可输出其他租户的设备 Token; +- 未读数 Cache 使用 Tenant User Key; +- 锁屏正文按租户安全策略生成。 + +如果不同租户使用不同品牌 App,Push Handler 需要根据 TenantID 选择 FCM/APNS 凭证,不能继续假定一套全局凭证。 + +### 12.2 Plugin Firehose + +Plugin 请求增加 TenantID、租户策略版本和不可伪造的上下文。插件返回 `REPLACE` 时服务端必须保留原 TenantID,禁止插件把消息换到其他租户/Topic。 + +插件配置分两类: + +- 平台级统一审核服务; +- 租户级专属审核服务。 + +无论哪种,调用超时、熔断、失败开放/关闭策略都按租户配置,审计和指标带 TenantID。 + +### 12.3 REST 认证、短信、TURN 和对象存储 + +外部请求必须携带服务端签名的租户上下文,并对回调结果校验 TenantID。租户自带密钥时,通过密钥管理系统按 TenantID 选择,不把 Secret 缓存在普通 Topic/User JSON 中。 + +## 13. API Key、限流和配额 + +当前 `X-Tinode-APIKey` 是应用级准入凭证。多租户后建议保持以下职责: + +- API Key:识别客户端应用、平台、版本和是否允许连接; +- Tenant Ticket/Token:确定租户; +- User Token:确定租户内用户; +- Role/Policy:确定具体操作权限。 + +不要仅通过 API Key 推导 Tenant,因为同一个 App 可能登录多个企业。若某企业使用专属 App,可额外把 API Key 限定到一个 Tenant,但仍需验证 Token Tenant 一致。 + +限流 Key 至少包含: + +```text +tenant + endpoint/action +tenant + uid + action +tenant + ip + action +``` + +配额计数包括用户、Topic、消息速率、文件大小、总存储、会议/报告等。超限返回稳定业务错误并记录租户审计,不能让一个大租户耗尽全平台队列。 + +## 14. 日志、指标与审计 + +### 14.1 日志和 Trace + +认证后所有请求日志增加: + +```text +request_id, tenant_id, uid, sid, topic, action, result +``` + +手机号、证件号、Token、企业码密钥和文件签名不写日志。TenantID 可写内部数值或不可逆短标识。 + +### 14.2 指标 + +租户 ID 直接作为 Prometheus Label 可能造成高基数。建议: + +- 平台指标默认不带 Tenant Label; +- 关键配额和 SLA 写入租户用量表或专用统计系统; +- 仅对白名单大租户暴露租户级指标; +- 错误日志和 Trace 保留 TenantID 供定位。 + +### 14.3 审计 + +以下操作必须记录 TenantID: + +- 租户创建、停用、恢复和删除; +- Tenant Admin 授权和代操作; +- 用户停用、离职和数据导出; +- 群主/管理员变更; +- 实名审核、举报处置、匿名信箱读取; +- 文件下载、批量导出和搜索敏感内容; +- 平台管理员跨租户操作。 + +审计表只允许追加,普通租户管理员不能修改或删除。 + +## 15. 测试方案 + +### 15.1 单元和接口测试 + +- TenantContext 不可为空; +- Session 只能绑定一次 Tenant; +- Token Tenant 与 Session Tenant 不一致时登录失败; +- Store/Adapter 不提供无 Tenant 的业务查询; +- TopicKey、UserKey、CacheKey 和 Push Channel 正确命名空间化; +- Plugin 不能替换 Tenant; +- Platform Admin 与 Tenant Admin 权限边界正确。 + +### 15.2 MySQL 集成测试 + +创建 Tenant A、Tenant B,并使用相同用户名、手机号、群显示名和逻辑 `sys`。至少覆盖: + +| 场景 | 预期 | +| --- | --- | +| 两租户注册相同用户名/手机号 | 均成功,各自登录到不同 UID | +| A 使用 B 的账号密码登录 | 统一认证失败 | +| A 已知 B 的 UID 执行 Get/Set/Delete | NotFound/Denied,不返回存在性 | +| A 已知 B 的 Group Topic 订阅或发布 | 拒绝且不加载到 A 的 Hub Cache | +| A 邀请 B 用户进入 A 群 | 拒绝,无 Subscription 写入 | +| A 已知 B 的 Message Topic/SeqId | 不能读取、搜索、删除或收藏 | +| A 已知 B 的 File ID/URL | 不能下载、关联或转发 | +| A/B 同时使用 `sys` | 只收到本租户系统消息 | +| A 停用用户 | 不影响相同设备上的 B 用户 | +| A Tenant Admin `AsUser` B 用户 | 拒绝并审计 | +| 同一设备在 A/B 注册 Push | 设备记录和 Push 不串租户 | +| A 大量任务积压 | B 的通知和文件 GC 不被长期饿死 | + +### 15.3 集群测试 + +- 相同 Topic 别名在不同 Tenant 生成不同 RoutingKey; +- Proxy/Master 传输后 Tenant 不丢失; +- 节点重启、重哈希和故障转移不串 Topic; +- 混入旧节点时启动检查直接失败; +- User Cache、未读数和 Push 在远程节点保持租户一致。 + +### 15.4 安全测试 + +- 篡改 `hi.tenant`、Token Tenant、HTTP Header 和 Body Tenant; +- 枚举其他租户 UID、Topic、File ID、企业码; +- SQL 查询遗漏 Tenant 的代码扫描与集成测试; +- root/OBO 越权; +- 搜索索引、缓存、导出、日志和审计的跨租户泄露; +- 对象存储签名 URL 重放和跨租户替换; +- Plugin/回调伪造 Tenant。 + +## 16. 代码修改清单 + +| 文件/目录 | 主要修改 | +| --- | --- | +| `server/store/types/tenant.go`、`types.go` | TenantID、TenantContext、TopicKey、UserKey;核心实体 TenantID | +| `server/auth/auth.go` | AuthContext、`auth.Rec.TenantID`、认证接口 | +| `server/auth/basic` | 租户内登录名查询和唯一性 | +| `server/auth/token` | Token v2、TenantID 和租户撤销版本 | +| `server/auth/rest`、`server/auth/code` | 传递并校验租户上下文 | +| `server/datamodel.go` | `MsgClientHi.Tenant`;内部消息 TenantID | +| `server/session.go` | Tenant 握手、固定 Session Tenant、登录/注册/发布断言 | +| `server/user.go` | User Cache Key、租户级未读/踢出/状态更新 | +| `server/sessionstore.go` | `EvictUser(tenant, uid)`、SID 认证租户断言 | +| `server/hub.go` | TopicKey 缓存、租户 `sys`、租户路由 | +| `server/init_topic.go` | Topic Tenant 初始化和同租户检查 | +| `server/topic.go` | Topic/成员/消息 Tenant 不变量 | +| `server/cluster.go`、`server/topic_proxy.go` | Cluster RPC Tenant、复合 RoutingKey、协议版本检查 | +| `server/store/store.go` | `ForTenant` 作用域和所有 Mapper 租户化 | +| `server/db/adapter.go` | Adapter 全接口 Tenant 参数 | +| `server/db/mysql/adapter.go` | 所有 SQL Tenant 条件和复合约束 | +| `server/db/mysql/schema.sql` | im_tenant、tenant_id、索引、外键和新 schema 版本 | +| `server/hdl_files.go` | RequestPrincipal、FileCanAccess、Tenant 文件路径 | +| `server/push.go`、`server/push` | Receipt/Channel/设备查询 Tenant 化 | +| `server/plugins.go`、`pbx` | 插件协议 Tenant 上下文和不可变校验 | +| `server/main.go` | 租户配置缓存和启动自检 | +| `server/tinode.conf` | 租户服务和配额配置 | +| `server/store/mock_store` | 重新生成所有租户化 Mock | +| `server/*_test.go`、`server/db/mysql/tests` | 双租户正向、负向和隔离测试 | + +## 17. 实施阶段与交付物 + +### 阶段 0:决策和基线 + +- 确认共享表模式; +- 确认租户内独立 User; +- 确认跨租户通信首期禁止; +- 确认产品版数据库支持范围; +- 冻结 TenantID、TopicKey 和认证协议设计。 + +交付物:架构决策记录、数据字典和接口清单。 + +### 阶段 1:Schema 与租户基础服务 + +- `im_tenant`、`im_tenant_config`; +- 核心表 `tenant_id BIGINT NOT NULL`; +- 默认租户; +- `hi.tenant` 企业码解析和 Session 绑定; +- 租户配置缓存。 + +交付物:全新数据库初始化 Schema、租户基础服务和 Schema 约束测试。 + +### 阶段 2:认证和 Store + +- Session Tenant; +- AuthContext 和 Token v2; +- `store.ForTenant`; +- MySQL 全查询租户化; +- Mock 和单元测试更新。 + +交付物:租户内注册、登录和 Store 隔离能力。 + +### 阶段 3:实时路由与集群 + +- Hub TopicKey; +- 租户 `sys/me/fnd/slf`; +- Cluster RPC 和一致性哈希; +- User Cache 和 Session Evict; +- Plugin Tenant。 + +交付物:双租户集群隔离测试通过,不兼容节点无法加入集群。 + +### 阶段 4:文件、Push、搜索和后台任务 + +- 文件 Principal、ACL 和租户路径; +- Push Receipt/设备/凭证租户化; +- 搜索索引和二次 ACL; +- Outbox、审计、GC、配额和限流租户化。 + +交付物:资源和异步链路跨租户负向测试通过。 + +### 阶段 5:上线验收 + +- 内部第二租户; +- 安全测试、压测和故障演练; +- 上线检查和真实第二企业启用。 + +交付物:测试报告、应急预案和多租户上线审批。 + +## 18. 主要风险 + +| 风险 | 影响 | 控制措施 | +| --- | --- | --- | +| 某个 SQL 漏 Tenant 条件 | 严重数据泄露 | 显式 Store 接口、复合约束、双租户负向测试和代码检查 | +| 只改数据库,缓存/集群未改 | 实时消息串租户 | TopicKey/UserKey、Cluster Tenant 不变量和集群测试 | +| 全局 `sys` 未拆分 | 系统通知泄露 | 租户逻辑 sys、Hub 复合 Key、通知生产者强制 Tenant | +| 文件 URL 泄露 | 跨租户下载 | FileGet Tenant 条件、Topic ACL、短时签名和审计 | +| root/OBO 权限过大 | 管理员跨租户操作 | Platform/Tenant Admin 分离、后台 API、二次验证和审计 | +| 混跑旧集群节点 | Tenant 字段丢失 | 集群协议版本检查,不兼容节点拒绝加入 | +| 指标 Tenant Label 高基数 | 监控系统压力 | 指标聚合,Tenant 主要进入日志/Trace/用量表 | + +## 19. 上线前必须满足的条件 + +1. 业务代码不存在无 Tenant 的 User/Topic/Message/File 查询接口; +2. 核心表和全部业务表 TenantID 为 NOT NULL; +3. Auth、Credential、Device、Topic 和 Subscription 唯一约束已租户化; +4. Token v2、Session Tenant 和 HTTP Principal 全链路一致; +5. Hub、User Cache、Cluster、Push、Plugin 和 Persistent Cache 均携带 Tenant; +6. `sys` 已按租户隔离; +7. 文件下载和搜索完成租户过滤及 ACL 二次校验; +8. 双租户自动化测试、集群测试和安全测试全部通过; +9. 所有运行节点和后台任务均为租户感知版本; +10. 内部第二租户稳定运行后,才允许创建真实第二企业。 + +## 20. 最终建议 + +多租户改造必须被视为服务端基础架构改造,而不是“给 users/topics/messages 加一列”。当前代码的认证、Store 接口、Hub Cache、Cluster、系统 Topic、文件和 Push 都使用全局 UID/Topic 语义,任何一处遗漏都可能造成跨企业数据泄露。 + +建议采用租户内独立 User、全局唯一 UID、共享表 `tenant_id` 强隔离的方案。全新数据库从创建时就使用最终多租户 Schema,并在 Store、认证、Token、路由、缓存、文件和 Push 全链路强制 TenantID;所有负向测试通过后,再开启真实租户。 + +工程上最重要的控制手段是:让 TenantID 进入类型和方法签名,使遗漏租户条件尽量在编译或测试阶段暴露;平台级全局查询使用隔离接口,普通业务代码永远只能获得 `store.ForTenant(...)`。这样才能让后续好友、群治理、企业工作台、搜索和安全业务从一开始就在正确的隔离边界内建设。 diff --git a/docs/im/IM-独立库多租户改造方案.md b/docs/im/IM-独立库多租户改造方案.md new file mode 100644 index 0000000..e4f1ebc --- /dev/null +++ b/docs/im/IM-独立库多租户改造方案.md @@ -0,0 +1,313 @@ +# IM 独立库多租户改造方案 + +> 编写日期:2026-08-13 +> 适用项目:`im_server` +> 方案状态:替代原“共享库共享表 + tenant_id”方案,后续多租户开发以本文为准。 + +## 1. 方案结论 + +多租户调整为: + +> 共享服务集群 + 控制库保存租户元数据 + 每个租户独立业务库 + +业务库中的 `users`、`auth`、`credentials`、`topics`、`subscriptions`、`messages`、`fileuploads` 以及后续产品业务表都不再增加 `tenant_id` 字段。租户隔离通过运行时选库实现:客户端先用企业码解析租户,服务端得到 `tenant_id` 后,根据控制库中的 `db_name` 或 `db_dsn` 选择对应租户业务库。 + +`TenantID` 仍然要保留,但它的职责从“业务表行级隔离字段”调整为“运行时租户上下文”: + +1. 登录前企业码解析; +2. Token 签名和校验; +3. Session 当前租户固定; +4. Hub、缓存、集群路由、Push、文件路径和插件上下文隔离; +5. 选择当前请求使用哪个租户业务库; +6. 后台平台侧管理租户、配置配额、停启租户。 + +## 2. 为什么独立库仍需要 TenantID + +独立库解决的是数据库物理隔离,不代表服务端运行时没有租户。服务进程仍然同时服务多个企业,如果内存 Topic、用户缓存、系统 Topic、Push、文件下载授权、Token 校验不带租户上下文,仍可能出现: + +1. 两个租户都使用逻辑 Topic `sys` 时共用同一个内存 Topic; +2. HTTP 文件下载只拿到 `file_id`,无法判断该去哪个租户库查授权; +3. Token 只包含 UID 时,无法知道该 UID 属于哪个租户库; +4. 定时任务、插件调用、Push 回调无法定位租户库; +5. 后台接口可能把平台租户 ID 和业务租户数据混在一起。 + +所以新方案的边界是: + +```text +TenantID 负责运行时路由和授权上下文 +tenant_id 字段不进入租户业务库表结构 +``` + +## 3. 数据库拆分 + +### 3.1 控制库 + +控制库可以使用现有 `hotgo`,只保存平台级租户元数据和配额配置。控制库表需要保留 `tenant_id`,因为这些表本身是在管理所有租户;这里的 `tenant_id` 不是业务库行级隔离字段。 + +```sql +CREATE TABLE im_tenant ( + id BIGINT NOT NULL AUTO_INCREMENT, + code VARCHAR(64) NOT NULL, + name VARCHAR(128) NOT NULL, + tenant_desc VARCHAR(256), + db_name VARCHAR(128) NOT NULL DEFAULT '', + db_dsn VARCHAR(1024) NOT NULL DEFAULT '', + state SMALLINT NOT NULL, + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + created_by BIGINT NOT NULL, + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + updated_by BIGINT NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY uk_im_tenant_code (code), + KEY idx_im_tenant_db_name (db_name) +); + +CREATE TABLE im_tenant_config ( + id BIGINT NOT NULL AUTO_INCREMENT, + tenant_id BIGINT NOT NULL, + max_users BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited', + max_groups BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited', + max_group_members INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + created_by BIGINT NOT NULL, + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + updated_by BIGINT NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY uk_im_tenant_config_tenant_id (tenant_id), + CONSTRAINT fk_im_tenant_config_tenant + FOREIGN KEY (tenant_id) REFERENCES im_tenant(id) + ON DELETE CASCADE +); +``` + +`db_name` 适合所有租户共用同一个 MySQL 实例时使用,例如 `im_tenant_1001`。`db_dsn` 适合某些租户需要独立 MySQL 实例时使用。优先级建议为:`db_dsn` 非空时使用完整 DSN,否则使用主 DSN + `db_name`。 + +如果要求 `db_name` 不能重复,建议在租户创建服务里校验“非空 db_name 唯一”。MySQL 普通唯一索引不能表达“只对非空字符串唯一”,直接给 `db_name` 建唯一索引会导致多个使用独立 `db_dsn` 的租户因为空 `db_name` 冲突。 + +### 3.2 租户业务库 + +每个租户一个业务库,例如: + +```text +hotgo + ├─ im_tenant + └─ im_tenant_config + +im_tenant_1001 + ├─ users + ├─ auth + ├─ credentials + ├─ topics + ├─ subscriptions + ├─ messages + ├─ fileuploads + └─ 后续产品业务表 +``` + +租户业务库表结构恢复为租户内唯一约束,不加 `tenant_id`。同一个手机号、用户名可以在两个租户库中各自存在,不会冲突。 + +## 4. 当前代码如何调整 + +当前代码已经有一部分独立库路由雏形,可以继续利用: + +1. `server/store/store.go` 已经预留 `TenantRoutedAdapter` 分支; +2. `server/db/adapter.go` 已定义 `TenantAdapterFor(tenantID)`; +3. `server/db/mysql/adapter.go` 已支持 `tenant_registry_db`、`tenant_db_routing`、`db_name`、`db_dsn`; +4. Token、Session、Hub TopicKey、UserKey、插件上下文已经带 `TenantID`。 + +需要调整的重点是: + +| 模块 | 旧共享表方案 | 新独立库方案 | +| --- | --- | --- | +| 控制库 | 与业务表混在一个库 | 单独保存 `im_tenant`、`im_tenant_config` | +| 业务库 schema | 所有表加 `tenant_id` | 所有业务表不加 `tenant_id` | +| MySQL Adapter | SQL 使用 `WHERE tenant_id=?` | 先根据 TenantID 选库,再执行租户库内 SQL | +| Store 接口 | TenantID 作为 SQL 条件 | TenantID 作为选库参数 | +| 唯一索引 | `(tenant_id, business_key)` | 租户库内 `business_key` 唯一 | +| 外键 | 复合外键带 `tenant_id` | 普通外键 | +| 数据迁移 | 共享库内补列回填 | 新租户直接建独立库,无历史迁移 | + +## 5. 代码落地步骤 + +### 5.1 拆分 schema + +把 MySQL schema 拆成两个层次: + +1. `registry_schema.sql`:只创建控制库表 `im_tenant`、`im_tenant_config`; +2. `tenant_schema.sql`:创建 Tinode 核心表和产品业务表,不包含 `tenant_id`; +3. `schema.sql` 可以作为文档索引,说明不能直接混用两套 schema。 + +`CreateDb(reset)` 也要拆分: + +1. 初始化控制库; +2. 初始化默认租户业务库; +3. 根据 `im_tenant.db_name/db_dsn` 初始化指定租户库; +4. 禁止在同一个事务中同时创建控制库和多个租户业务库,避免失败后状态难恢复。 + +### 5.2 改 MySQL Adapter 路由 + +保留 `TenantAdapterFor(tenantID)`,但租户 Adapter 内执行的 SQL 应该是无 `tenant_id` 版本。 + +推荐结构: + +```text +mysql.adapter + ├─ registryDB: 连接控制库 hotgo + ├─ defaultDB: 默认租户库 + ├─ tenantAdapters: map[TenantID]*adapter + └─ TenantAdapterFor(tenantID): 从 registryDB 读取 db_name/db_dsn 并返回租户库 adapter +``` + +租户业务方法仍保留 `TenantUserGet(tenantID, uid)` 这类签名,但实现改为“先选库,后执行租户库内原 SQL”。实际写代码时要避免递归,可以把“无租户 SQL”的方法放在租户库 adapter 上,把“路由包装方法”放在根 adapter 上。 + +### 5.3 删除业务表 tenant_id 依赖 + +需要回退或重写这些位置: + +1. `server/db/mysql/schema.sql` 中核心业务表的 `tenant_id` 字段; +2. `server/db/mysql/tenant_adapter.go` 中所有 `WHERE tenant_id=?`、`INSERT tenant_id`、复合 JOIN; +3. `server/db/mysql/tests/mysql_test.go` 中依赖共享表隔离的测试; +4. `docs/sql/mysql-multitenant-tenant-id-review.sql` 这份共享表迁移脚本应标记为废弃; +5. 后续产品业务表设计也不再带 `tenant_id`。 + +不建议删除 Store、Session、Token、Hub、Push 和插件里的 `TenantID`,这些属于运行时隔离边界。 + +### 5.4 租户创建流程 + +后台创建租户时建议按以下顺序: + +1. 在控制库 `im_tenant` 创建记录,状态为 `provisioning`; +2. 生成业务库名,例如 `im_tenant_{id}`; +3. 创建业务库并执行 `tenant_schema.sql`; +4. 写入默认系统数据,例如 `kvmeta.version`、系统账号、`sys` Topic; +5. 回写 `im_tenant.db_name`,状态改为 `active`; +6. 创建失败时租户保持不可登录状态,并记录错误供后台重试。 + +第一阶段可以不做自动建库接口,先由运维执行建库 SQL,再在控制库写入 `im_tenant`。 + +### 5.5 认证与登录 + +登录入口仍然是: + +1. 客户端携带 `X-Tinode-APIKey` 请求准入; +2. `hi` 或登录前 HTTP 接口提交企业码; +3. 服务端在控制库查 `im_tenant.code`; +4. 租户状态必须是 `active`; +5. Session 固定 `TenantID`; +6. 登录认证根据 `TenantID` 选择租户业务库,然后查该库内 `auth`、`credentials`、`users`; +7. Token 签名内容继续包含 `TenantID`,HTTP 后续请求可从 Token 反推出该使用哪个租户库。 + +客户端不能通过 URL、Header、Body 直接指定业务库名或 `tenant_id`。 + +## 6. 产品业务表如何设计 + +后续根据《IM 产品原型需求文档》补充的业务表也按“租户库内表”设计,不加 `tenant_id`。所有新业务表仍统一要求: + +```sql +created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), +updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) +``` + +例如好友申请: + +```sql +CREATE TABLE im_friend_requests ( + id BIGINT NOT NULL AUTO_INCREMENT, + request_id CHAR(26) NOT NULL, + applicant_id BIGINT NOT NULL, + target_id BIGINT NOT NULL, + message VARCHAR(256), + status SMALLINT NOT NULL, + handled_by BIGINT, + handled_at DATETIME(3), + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (id), + UNIQUE KEY uk_im_friend_requests_request_id (request_id), + KEY idx_im_friend_requests_target_status (target_id, status, created_at), + KEY idx_im_friend_requests_applicant_status (applicant_id, status, created_at) +); +``` + +## 7. 文件和对象存储 + +数据库独立库后,文件元数据在租户库内隔离,但对象存储路径仍建议带租户目录: + +```text +tenants/{tenant_id}/yyyy/mm/{file_id} +``` + +对象存储通常仍是共享 Bucket。带租户目录便于权限策略、容量统计、清理和迁移。 + +文件下载流程: + +1. HTTP Token 解析得到 `tenant_id` 和 `uid`; +2. 根据 `tenant_id` 选择租户业务库; +3. 在该库查 `fileuploads` 和 `filemsglinks`; +4. 校验当前用户是否有 Topic 或消息访问权; +5. 返回文件或预签名 URL。 + +## 8. 测试策略 + +需要把旧共享表测试改成独立库测试: + +| 用例 | 准备 | 预期 | +| --- | --- | --- | +| 企业码解析 | 控制库有 A、B 两个租户 | A、B 返回不同 `tenant_id` 和不同 `db_name` | +| 同手机号注册 | A、B 租户库分别注册同手机号 | 两边都成功 | +| 登录选库 | A 用户用 B 企业码登录 | B 库查不到该用户,登录失败 | +| Token 选库 | A Token 请求文件接口 | 只访问 A 租户库 | +| 内存 Topic 隔离 | A、B 都使用 `sys` | Hub 中是两个 TopicKey | +| 租户停用 | 控制库把 A 设为 suspended | 新登录失败 | +| 租户库不可用 | A 的 DSN 错误或库不存在 | A 请求失败,B 不受影响 | +| 连接池复用 | 多次访问 A | 只创建一个 A 租户 adapter,后续复用 | +| 配额检查 | A 配置 max_users=1 | A 超额注册失败,B 不受影响 | + +## 9. 对旧方案的处理 + +以下内容停止继续开发: + +1. 给核心业务表增加 `tenant_id`; +2. 把唯一索引改成 `(tenant_id, xxx)`; +3. 所有业务 SQL 加 `WHERE tenant_id=?`; +4. 共享库内的数据迁移、回填、复合外键改造; +5. 产品业务表中的 `tenant_id` 字段设计。 + +以下内容继续保留: + +1. 企业码识别; +2. 控制库 `im_tenant`、`im_tenant_config`; +3. `TenantID` 类型; +4. Session TenantID; +5. Token TenantID; +6. Hub、缓存、集群、Push、插件上下文中的 TenantID; +7. `TenantAdapterFor` 选库机制。 + +## 10. 推荐下一步 + +下一步代码改造建议从 MySQL 层开始: + +1. 把 `server/db/mysql/schema.sql` 拆为控制库 schema 和租户业务库 schema; +2. 把 `tenant_adapter.go` 改成路由包装,不再写 `tenant_id` SQL; +3. 恢复或复用原 `adapter.go` 中无租户 SQL 作为租户库真实执行逻辑; +4. 修改 MySQL 测试,创建两个独立业务库验证隔离; +5. 再继续补产品业务表,所有业务表按租户库内表设计。 + +## 11. 当前代码落地状态 + +2026-08-13 已开始按独立库方案改造 MySQL 底座: + +1. 默认业务库为 `tinode`; +2. 控制库为 `hotgo`; +3. `CreateDb` 会在 `hotgo` 创建 `im_tenant`、`im_tenant_config`,默认租户 `id=1/code=default/db_name=tinode`; +4. `CreateDb` 会在 `tinode` 创建核心业务表,核心表不再包含 `tenant_id`; +5. `TenantAdapterFor` 开启路由后默认租户也会从控制库解析,不再直接退回根连接; +6. 租户库 adapter 禁用二次路由,避免递归; +7. 用户、认证、凭证、Topic、Subscription、消息、设备、文件等核心 Tenant 方法已改为“先选库,再调用租户库内无 tenant_id 方法”; +8. 旧共享表 `tenant_id` 迁移 SQL 已标记废弃。 + +剩余项: + +1. 产品扩展表和 CRUD,例如好友、收藏、实名、青少年、系统通知,还需要继续从 `tenant_id` SQL 改成租户库内无 `tenant_id` SQL; +2. `server/db/mysql/schema.sql` 仍是参考文件,已标注正在从旧共享表草稿拆分,后续应拆成 `registry_schema.sql` 和 `tenant_schema.sql`; +3. 旧版本 `UpgradeDb` 迁移链仍包含共享表历史步骤。因为当前项目不考虑历史数据,优先使用新建库 `CreateDb`;如后续需要升级老库,应单独重写迁移链。 diff --git a/docs/im/IM-现有实现与新需求差异对照总结.md b/docs/im/IM-现有实现与新需求差异对照总结.md new file mode 100644 index 0000000..efcdf27 --- /dev/null +++ b/docs/im/IM-现有实现与新需求差异对照总结.md @@ -0,0 +1,377 @@ +# IM 现有实现与新需求差异对照总结 + +> 对照日期:2026-08-10 +> 新需求来源:[IM 产品原型需求文档](IM-产品原型需求文档.md) +> 详细改造分析:[IM 产品需求服务端改造分析报告](IM-产品需求服务端改造分析报告.md) +> 对照对象:`chat-0.25.3` 当前服务端源码 + +## 1. 文档目的 + +本文从“现有服务端已经实现了什么”出发,对照新产品需求逐项给出处理结论: + +- **保留**:现有实现已基本满足需求,只补配置或测试; +- **需修改**:现有能力可复用,但协议、数据模型、权限或流程不完整; +- **新增**:现有系统没有对应实现,需要新增模块; +- **客户端为主**:主要是移动端交互,服务端只提供状态、配置或资源; +- **废弃**:现有产品能力或接入方式在新需求中没有对应章节,不再作为新产品能力继续建设。 + +“新需求无直接章节”的数据库、对象存储、集群、监控等运行基础设施不按产品功能废弃,而标记为“保留基础设施”。真正需要废弃的能力单列在第 5 节。 + +## 2. 总体结论 + +当前系统可以继续作为新产品的即时通讯内核,不需要重写。现有 P2P Topic、群 Topic、Subscription ACL、消息序号、消息广播、回执、文件上传、Push 和 WebRTC 信令均可复用。 + +新需求与现状的主要关系如下: + +| 新需求章节 | 功能域 | 当前覆盖程度 | 结论 | +| --- | --- | --- | --- | +| 第 1 章 | 账号、企业码、短信、实名、独立密码 | 部分覆盖 | 认证内核保留,产品流程需修改,企业码/实名需新增 | +| 第 2 章 | 会话首页、网络状态、消息操作、系统消息 | 部分覆盖 | 会话与连接保留,产品状态和系统通知需修改 | +| 第 3 章 | 单聊、好友状态、回执、媒体和名片 | 部分覆盖 | P2P/回执保留,好友约束和结构化消息需修改 | +| 第 4 章 | 输入、@、语音、文件、收藏、转发 | 少量覆盖 | 文件和通用消息可复用,大部分产品语义需新增 | +| 第 5 章 | 通讯录、好友申请、主页、黑名单 | 少量覆盖 | 用户搜索可改造,关系状态机需新增 | +| 第 6 章 | 群创建、审批、公告、管理、禁言、群文件 | 部分覆盖 | 群和 ACL 保留,群治理业务需新增 | +| 第 7 章 | 收藏、下载、聊天记录搜索 | 少量覆盖 | `slf` 和文件可复用,授权和索引必须修改 | +| 第 8 章 | 语音/视频通话 | 部分覆盖 | P2P WebRTC 信令保留,通话业务状态需修改 | +| 第 9 章 | 组织、会议、日报、月报、信箱 | 未覆盖 | 全部新增业务模块 | +| 第 10 章 | 资料、设置、版本、隐私、青少年模式 | 少量覆盖 | 通用资料可改造,其余多数新增或客户端实现 | +| 第 11 章 | 反诈、举报、反馈 | 未形成业务闭环 | 插件机制保留,业务模型和处置流程新增 | + +必须优先处理三个基础问题: + +1. 消息发布缺少客户端消息 ID 幂等校验,断线重试可能产生重复消息; +2. 文件下载只校验登录,没有校验用户对文件关联 Topic/消息的读取权限; +3. 当前核心表没有 `tenant_id`,共享数据库部署多个企业前必须完成全链路租户隔离。 + +## 3. 现有核心实现与新需求关系 + +| 现有实现 | 源码位置 | 对应新需求章节 | 结论 | 修改说明 | +| --- | --- | --- | --- | --- | +| basic 用户名/密码认证 | `server/auth/basic` | 1.2、1.3、1.4、1.6 | 需修改 | 保留认证器,增加协议确认、密码策略、错误次数、锁定、找回密码编排和旧 Token 撤销 | +| HMAC Token 登录 | `server/auth/token` | 1.2、1.6;2.2 | 保留 | 继续作为登录态;补设备会话、单设备撤销和密码重置后的 Token 失效 | +| 电话号码及短信验证器 | `server/validate/tel` | 1.4、1.5、1.6、1.7 | 需修改 | 保留验证码能力,增加用途隔离、发送频控、`retry_after`、一次性验证票据和审计 | +| REST 外部认证器 | `server/auth/rest` | 1.1、1.2 | 需修改 | 可连接企业账号中心;企业码解析仍需新增独立入口,认证后绑定企业上下文 | +| Session/WebSocket 协议分发 | `server/session.go` | 2.2;第 3、4、8 章 | 保留 | 作为 App 的主实时入口;补幂等、产品策略和结构化错误 | +| Topic 消息顺序、保存和广播 | `server/topic.go` | 第 2、3、4、6、7 章 | 需修改 | 保留核心循环;持久化前增加好友、黑名单、禁言、青少年和内容风险校验 | +| P2P Topic | `server/topic.go` | 3.1-3.12;5.2-5.14 | 需修改 | 保留单聊容器;是否允许创建、发送和呼叫改由好友/黑名单/隐私策略控制 | +| Group Topic | `server/topic.go` | 6.1-6.28;4.2-4.4 | 需修改 | 保留群消息和成员基础;增加审批、公告、二维码、禁言和群文件业务层 | +| Subscription ACL | `server/store/types/types.go`、`server/topic.go` | 3.1-3.8;6.4-6.26 | 需修改 | 保留 Join/Read/Write/Admin/Owner;不能用 ACL 代替好友申请、待审批和禁言记录 | +| Presence、输入状态 | `server/topic.go`、`server/datamodel.go` | 2.1;3.1-3.4;4.9 | 需修改 | 在线/输入状态可复用;增加隐私控制、节流和过期清理 | +| `recv`、`read` 回执 | `server/topic.go` | 3.4、3.7 | 保留 | 定义客户端状态映射;群聊是否显示具体已读人由产品规则控制 | +| 通用 JSON `head/content` 与 Drafty | `server/datamodel.go`、`server/drafty` | 3.6、3.10-3.12;4.1-4.20 | 需修改 | 增加消息类型、schema 版本、字段白名单、引用、@、转发和审核状态,不再允许各端自定义语义 | +| 消息删除 | `server/session.go`、`server/topic.go` | 2.4;3.8;4.15 | 需修改 | 明确仅自己删除、全员撤回、撤回期限、管理员权限和审计,不直接复用模糊的硬删除语义 | +| `slf` 自收藏 Topic | `server/store/types/types.go`、`server/topic.go` | 4.16;7.1-7.8 | 需修改 | 继续保存收藏消息,同时增加收藏索引、来源、去重、类型筛选和源消息删除策略 | +| 文件上传与消息附件关联 | `server/hdl_files.go`、`server/store/store.go` | 3.6、3.10-3.12;4.10-4.20;6.27-6.28;7.3、7.7-7.11 | 需修改 | 保留上传;增加附件所有权、病毒扫描、群文件索引、短时下载 URL 和资源级授权 | +| Push Handler | `server/push.go`、`server/push` | 2.1、2.5;4.4;8.1-8.11;9.3-9.4;10.18 | 需修改 | 保留 Push;扩展 @、审批、会议、通话和安全通知,增加锁屏内容隐私设置 | +| P2P WebRTC 信令 | `server/calls.go` | 8.1-8.11 | 需修改 | 保留 Invite/Offer/Answer/ICE/Hang-up;新增 `call_id`、设备抢答、结束原因、详单和 TURN 指标 | +| Plugin Firehose | `server/plugins.go` | 11.1-11.4;2.5 | 需修改 | 用于外部内容审核和反诈;增加超时、熔断、失败策略和审核结果落库 | +| User Public/Trusted/Tags | `server/store/types/types.go` | 5.1、5.5-5.8;10.1-10.6 | 需修改 | 可保存基础展示资料;生日、可见性、实名和组织关系应使用明确字段/业务表 | +| Topic Public/Trusted/Aux/Tags | `server/store/types/types.go` | 6.10-6.16 | 需修改 | 可保存群基础展示资料;公告、审批、二维码和禁言不能只存任意 JSON | +| `fnd` 用户/Topic 查找 | `server/topic.go` | 5.1、5.3-5.5 | 需修改 | 改成租户内精准 ID/受控搜索;执行隐私策略并防止手机号、用户 ID 枚举 | +| `sys` 系统 Topic | `server/store/types/types.go`、`server/topic.go` | 2.5;第 9、11 章通知 | 需修改 | 作为可信系统消息通道;定义消息类型、模板版本、跳转参数和去重键 | +| MySQL 核心表和 Adapter | `server/db/mysql` | 所有服务端需求的存储基础 | 需修改 | schema 118 已建立 `im_tenant`;仍需为核心表增加 tenant_id,并新增产品表、幂等、搜索索引和文件授权查询 | +| 文件系统/S3 媒体后端 | `server/media/fs`、`server/media/s3` | 第 3、4、6、7、11 章附件 | 保留基础设施 | 生产使用 S3/兼容对象存储,本地文件系统仅开发;补扫描和私有签名地址 | + +## 4. 按新需求章节逐项对照 + +### 4.1 第 1 章:账号注册、登录与身份安全 + +| 新需求章节 | 新功能 | 当前实现 | 是否改 | 如何修改 | +| --- | --- | --- | --- | --- | +| 1.1 | 企业码登录 | 无企业/租户模型 | 新增 | 增加企业码解析 API、企业状态、服务地址和认证策略;共享库模式需先加 `tenant_id` | +| 1.2-1.3 | 账号密码登录及错误提示 | basic + token 已有 | 需修改 | 增加协议确认、统一错误码、失败计数、账号锁定、设备登录记录和安全通知 | +| 1.4-1.5 | 短信注册和倒计时 | 电话验证器已有 | 需修改 | 新增注册流程 API;验证码按用途隔离,返回有效期和重发时间,服务端强制限频 | +| 1.6 | 忘记密码 | 验证器与密码更新能力分散 | 需修改 | 建立“发码 → 临时票据 → 重置”状态机;成功后撤销旧 Token | +| 1.7 | 敏感操作二次验证 | 无统一业务流程 | 新增 | 增加验证挑战、一次性票据、有效期、错误次数和操作绑定 | +| 1.8-1.10 | 实名认证及状态提醒 | 无 | 新增 | 独立实名表、字段加密、审核状态、脱敏查询、审核回调和提醒消息 | +| 1.11 | 四位独立密码 | 无 | 新增 | 保存强哈希、失败锁定和找回流程;禁止写入普通设置 JSON 或明文日志 | + +### 4.2 第 2 章:消息首页、全局导航与系统通知 + +| 新需求章节 | 新功能 | 当前实现 | 是否改 | 如何修改 | +| --- | --- | --- | --- | --- | +| 2.1 | 会话列表、摘要、未读数 | Subscription/Topic 已有基础 | 需修改 | 增加置顶、免打扰、隐藏、草稿和结构化消息摘要;文件助手使用固定系统 Topic | +| 2.2 | 断网、重连、增量同步 | Session 与序号拉取已有 | 需修改 | 明确同步游标、重连退避、重复消息幂等和连接状态指标 | +| 2.3 | 快捷菜单 | 无服务端页面逻辑 | 客户端为主 | 服务端只返回能力/权限开关,菜单和关闭交互由客户端实现 | +| 2.4 | 引用、复制、收藏、编辑、转发、撤回、删除、多选、群回执 | 仅删除、通用消息、回执基础 | 需修改 | 增加引用/编辑/转发协议,撤回规则和批量操作校验;复制/多选 UI 在客户端 | +| 2.5 | 新设备登录等系统消息 | `sys` Topic 可用,无完整事件生产 | 需修改 | 建立可信系统账号、设备/IP 审计事件、模板、跳转参数和防伪造规则 | + +### 4.3 第 3 章:单聊会话、会话建立与消息状态 + +| 新需求章节 | 新功能 | 当前实现 | 是否改 | 如何修改 | +| --- | --- | --- | --- | --- | +| 3.1-3.5 | 好友申请前后单聊状态 | P2P Topic 有,无好友状态机 | 新增/修改 | 新增好友申请和关系表;申请通过后再开放 Topic Write 与媒体能力 | +| 3.4、3.7 | 发送中、送达、已读、失败 | 消息序号和 read/recv 已有 | 需修改 | 增加客户端消息 ID、稳定状态映射和业务失败原因;拉黑消息不得持久化给对方 | +| 3.6 | 图片/语音/视频进度 | 文件上传与消息可用 | 需修改 | 统一 `file_id`、消息类型和上传完成确认;进度主要由客户端维护 | +| 3.7-3.8 | 拉黑、删除好友后的发送限制 | 无关系策略 | 新增 | 在建会话、发布、通话、名片和文件入口统一执行关系策略 | +| 3.9 | 键盘收起布局 | 无服务端职责 | 客户端为主 | 服务端无需修改 | +| 3.10-3.11 | 多类型文件/混合消息 | 通用 JSON + 文件已有 | 需修改 | 定义 text/image/voice/video/file 类型和字段校验,禁止任意 URL | +| 3.12 | 名片消息 | 无标准类型 | 新增 | 增加 `contact_card`,只传用户 ID 与安全快照,打开时重新检查资料权限 | + +### 4.4 第 4 章:消息输入、媒体内容与消息操作 + +| 新需求章节 | 新功能 | 当前实现 | 是否改 | 如何修改 | +| --- | --- | --- | --- | --- | +| 4.1 | 键盘、草稿 | 无专用服务端草稿 | 客户端为主 | 草稿默认本地;如需多端同步,增加每用户每 Topic 的草稿设置 | +| 4.2-4.4 | @成员、@所有人 | 无结构化 @语义 | 新增 | 消息保存稳定用户 ID;校验群成员和 @所有人权限,生成定向未读与 Push | +| 4.5-4.6 | 表情面板和表情消息 | Drafty 可保存文本 | 保留/客户端为主 | Unicode 表情按文本发送;独立表情包需要资源 ID 和版本,不保存本地路径 | +| 4.7-4.12 | 录音、图片/语音/视频上传与展示 | 文件和通用消息基础已有 | 需修改 | 增加媒体类型、时长/尺寸/缩略图、扫描状态;录音交互和播放进度由客户端实现 | +| 4.13-4.14 | 文件上传和文件卡片 | 已有上传与附件关联 | 需修改 | 补格式/大小策略、所有权、病毒扫描、可预览信息和安全下载 | +| 4.15 | 消息多选批量操作 | 无服务端批量语义 | 需修改 | 客户端选择;服务端逐条鉴权,限制批量数量并返回逐项结果 | +| 4.16 | 批量收藏 | `slf` 可承载 | 需修改 | 增加收藏唯一约束、来源索引、类型和批量幂等 | +| 4.17-4.19 | 单选/多目标逐条转发 | 无标准转发模型 | 新增 | 校验每个目标发送权和附件权;限制目标数;每条新消息使用独立幂等 ID | +| 4.20 | 合并转发 | 无 | 新增 | 创建不可变 `merged_forward` 快照,限制子项和总大小,清理会话 ID 与敏感字段 | + +### 4.5 第 5 章:通讯录、好友关系与黑名单 + +| 新需求章节 | 新功能 | 当前实现 | 是否改 | 如何修改 | +| --- | --- | --- | --- | --- | +| 5.1 | 好友通讯录和企业成员 | `fnd`/Tags 仅提供基础查找 | 需修改 | 新增好友关系和组织成员查询;备注优先;支持字母/拼音索引 | +| 5.2 | 新朋友列表与审批 | 无 | 新增 | 新增好友申请状态机、幂等接受/拒绝、过期和系统通知 | +| 5.3-5.5 | ID、扫码添加好友 | 基础查找可部分复用 | 需修改 | 限定租户内精准搜索;二维码使用签名短 Token;执行隐私和反枚举策略 | +| 5.6 | 好友验证留言 | 无明确模型 | 新增 | 好友申请表保存验证信息、来源、过期时间和风控结果 | +| 5.7-5.8 | 好友/非好友个人主页 | Public/Trusted 可显示基础资料 | 需修改 | 根据关系、企业和隐私设置返回不同字段,不让客户端自行裁剪敏感资料 | +| 5.9-5.10 | 拉黑开关 | 无完整黑名单模型 | 新增 | 关系表记录单方 block;立即影响消息、搜索、呼叫和资料访问 | +| 5.11 | 删除好友 | 无好友关系真源 | 新增 | 事务更新双方关系;默认保留历史但禁止继续发送,规则需产品确认 | +| 5.12-5.14 | 黑名单列表及提示 | 无 | 新增 | 提供黑名单查询、解除接口和统一业务错误,不向被拉黑人泄露过多信息 | + +### 4.6 第 6 章:群聊创建、资料维护与成员治理 + +| 新需求章节 | 新功能 | 当前实现 | 是否改 | 如何修改 | +| --- | --- | --- | --- | --- | +| 6.1-6.2 | 群列表和空状态 | Group Topic 已有 | 需修改 | 复用订阅列表;增加群业务状态、群头像摘要和已解散处理 | +| 6.3-6.6 | 加群、邀请与审批 | ACL 可邀请成员,但无申请状态机 | 新增/修改 | 新增入群申请/邀请表;审批通过后才更新 Subscription,写 Outbox 通知 | +| 6.7-6.9 | 群公告和已读 | 无公告模型 | 新增 | 公告主表、版本、发布权限、置顶和成员已读表;变更通过群系统消息通知 | +| 6.10-6.16 | 群设置、资料、头像、昵称、简介、标识、二维码 | Public/Aux/Private 可部分承载 | 需修改 | 定义可索引字段和修改权限;二维码新增签名 Token、有效期、使用次数和撤销 | +| 6.17-6.22 | 管理员、群主转让 | Admin/Owner ACL 已有 | 需修改 | 复用权限位;补管理员上限、二次确认、操作审计和可靠通知 | +| 6.23-6.24 | 成员禁言 | 当前通知静音不等于发言禁用 | 新增 | 新增全员/成员禁言规则,在消息持久化前强制执行,定义管理员豁免 | +| 6.25-6.26 | 选择新群主及确认 | 已有 Owner 转移逻辑 | 需修改 | 增加风险校验、幂等、候选人有效性、二次确认和审计 | +| 6.27-6.28 | 群文件及上传 | 文件消息可用,无群文件目录 | 新增/修改 | 建群文件索引,支持类型/时间/上传者查询和管理员删除,复用安全上传链路 | + +### 4.7 第 7 章:文件下载、收藏管理与聊天记录检索 + +| 新需求章节 | 新功能 | 当前实现 | 是否改 | 如何修改 | +| --- | --- | --- | --- | --- | +| 7.1-7.8 | 收藏分类、查看和分享 | `slf` Topic 只有基础容器 | 需修改 | 增加收藏索引、类型、去重和源消息引用;分享时重新执行附件和目标会话权限 | +| 7.9-7.11 | 文件下载状态 | 已有 HTTP 下载 | 必须修改 | 下载前检查上传者或关联 Topic Read ACL;使用短时签名 URL;进度/完成状态在客户端 | +| 7.12 | 关键词搜索并定位上下文 | 现有查询只按序号范围 | 新增 | 建消息搜索索引,返回 Topic/SeqId 后重新检查 ACL,并支持上下文拉取 | +| 7.13-7.16 | 文件、链接、图片、视频分类检索 | 无类型索引 | 新增 | 建 `message_type`、`mime_group`、file/link 字段和游标分页,支持历史回填 | + +### 4.8 第 8 章:实时语音与视频通话 + +| 新需求章节 | 新功能 | 当前实现 | 是否改 | 如何修改 | +| --- | --- | --- | --- | --- | +| 8.1-8.4 | 邀请、等待、接听、语音通话 | P2P WebRTC 信令已有 | 需修改 | 增加独立 `call_id`、呼叫类型、响铃/接听时间、忙线/拒接/超时原因和通话记录 | +| 8.5-8.6 | 麦克风与扬声器状态 | 信令可传临时事件 | 客户端为主 | 本地媒体控制由客户端负责,仅在对端需要展示时同步状态 | +| 8.7-8.10 | 视频等待、摄像头关闭、视频中 | WebRTC Offer/Answer/ICE 已有 | 需修改 | 增加视频类型和摄像头状态事件;配置 TURN,监控 ICE 成功率 | +| 8.11 | 通话悬浮状态 | 无服务端职责 | 客户端为主 | 服务端保持呼叫会话和重连状态,悬浮窗由客户端实现 | + +当前实现仅适合一对一通话。新需求没有群音视频章节,因此首期不新增 SFU 或群会议通话。 + +### 4.9 第 9 章:企业工作台与办公协同 + +| 新需求章节 | 新功能 | 当前实现 | 是否改 | 如何修改 | +| --- | --- | --- | --- | --- | +| 9.1 | 工作台入口 | 无业务模块 | 新增 | 新增工作台应用配置和用户可见权限;页面布局由客户端实现 | +| 9.2 | 部门组织架构 | 无 | 新增 | 新建组织单元和成员表,支持树、搜索、主部门、职位、在职状态和数据范围 | +| 9.3-9.4 | 会议通知 | 仅能发送普通消息 | 新增 | 新建会议和参与人模型;创建/变更/取消通过 Outbox 发送系统消息和定时提醒 | +| 9.5-9.8 | 日报及普通/管理员视图 | 无 | 新增 | 建报告表和提交状态机;管理员查询按组织数据权限过滤 | +| 9.9-9.11 | 月报及详情 | 无 | 新增 | 复用报告模型,以类型/周期区分;增加汇总和附件能力 | +| 9.12-9.13 | 总经理信箱 | 无 | 新增 | 建信箱工单、匿名展示、附件、回复和审计;匿名对业务角色隐藏但保留受控追溯 | + +工作台 CRUD 建议走 `/v1/im/*` HTTP API,状态提醒走系统 Topic,不把报告和组织树直接塞进聊天消息协议。 + +### 4.10 第 10 章:个人资料、通用设置、隐私与青少年模式 + +| 新需求章节 | 新功能 | 当前实现 | 是否改 | 如何修改 | +| --- | --- | --- | --- | --- | +| 10.1-10.5 | 我的、个人信息、昵称、生日、签名 | Public/Trusted 有通用字段 | 需修改 | 定义资料字段、格式、可见性、敏感词、变更版本和审计;实名信息单独存储 | +| 10.6 | 个人二维码 | 无标准 Token | 新增 | 生成短时签名 Token,扫码后仍执行租户、隐私和账号状态校验 | +| 10.7-10.8 | iOS/Android 设置页 | 无服务端页面逻辑 | 客户端为主 | 服务端提供用户设置和能力开关 | +| 10.9 | 清空缓存 | 不属于服务端数据删除 | 客户端为主 | 只清理设备缓存,不删除云端消息;无需服务端接口 | +| 10.10-10.11 | 版本更新 | 无版本配置服务 | 新增 | 提供平台/渠道最低版本、推荐版本、强更开关、下载地址和文案 API | +| 10.12-10.13 | 隐私设置及添加方式 | 无统一隐私模型 | 新增 | 建隐私设置表,并在搜索、加好友、资料和在线状态接口中强制执行 | +| 10.14-10.17 | 青少年模式 | 无 | 新增 | 服务端保存启用状态、PIN 强哈希、失败锁定和策略版本;所有关键入口强制检查 | +| 10.18 | 推送权限 | Push Token 能力已有 | 需修改/客户端为主 | 系统授权由客户端申请;服务端保存 Token、开关、免打扰和预览策略 | +| 10.19-10.21 | 提醒、相机、麦克风说明 | 无服务端职责 | 客户端为主 | 权限说明和系统授权由客户端实现,可由配置服务下发版本化文案 | + +### 4.11 第 11 章:反诈安全、举报与意见反馈 + +| 新需求章节 | 新功能 | 当前实现 | 是否改 | 如何修改 | +| --- | --- | --- | --- | --- | +| 11.1-11.2 | 反诈一次/二次提示 | Plugin 可拦截消息,无产品策略 | 新增/修改 | 建风险事件和策略版本;同步硬规则本地执行,外部审核设置超时/熔断;客户端按等级弹窗 | +| 11.3-11.4 | 举报提交和分类 | 无举报工单 | 新增 | 建举报、证据、处理状态和审计;服务端在有权限时固化消息快照与哈希 | +| 11.5 | 意见反馈 | 无 | 新增 | 建反馈工单、分类、附件、联系方式和处理状态;附件复用私有文件授权 | + +## 5. 新需求未覆盖的现有能力:废弃清单 + +以下能力在新需求文档中没有对应章节。按新产品范围标注为“废弃”,但应采用“停止新接入 → 观察存量 → 迁移 → 删除”的方式处理,不能在未确认线上使用情况时直接删除源码或数据。 + +| 现有能力 | 源码/配置 | 新需求章节 | 处理结论 | 废弃方式 | +| --- | --- | --- | --- | --- | +| 匿名登录 | `server/auth/anon` | 无 | **废弃** | 新环境不配置、不向客户端公开;确认无匿名会话后移除注册和文档 | +| 邮箱注册/邮箱验证码/邮箱找回 | `server/validate/email` | 无,新需求只定义手机号短信 | **废弃** | 产品 API 不再暴露 email credential;存量账号先绑定手机号再停用 | +| Channel 只读广播 Topic | `TopicCatGrp` 的 channel 模式 | 无 | **废弃** | 禁止新建 Channel;如有存量,迁移为普通群或公告业务后再移除入口 | +| Long Poll 客户端接入 | `/v0/channels/lp` | 无,新移动端按 WebSocket 设计 | **废弃** | 先记录调用量并公告;无流量后关闭路由,WebSocket 保留 | +| 客户端 gRPC 接入 | `grpc_listen`、客户端 gRPC 服务 | 无 | **废弃** | App 不再接入;注意保留插件所需的 gRPC 能力,两者不能一起误删 | +| 示例 Python Chat Bot | `tinode.conf` 中 `python_chat_bot` | 无 | **废弃** | 从生产配置删除;插件框架因第 11 章风控继续保留 | +| 静态网页托管 | `main.go` 静态文件路由/配置 | 无 Web 产品章节 | **废弃** | 新产品服务端不托管 Web UI;确认运维页面用途后关闭 | +| stdout Push Handler | `server/push/stdout` | 无生产需求 | **废弃(生产)** | 仅允许本地测试使用,生产配置删除;FCM/TNPG 等真实 Push 保留 | +| PostgreSQL Adapter | `server/db/postgres` | 无数据库兼容要求 | **废弃(产品版候选)** | 若确认生产统一 MySQL,则停止产品功能适配和验收;删除前核查部署 | +| MongoDB Adapter | `server/db/mongodb` | 无数据库兼容要求 | **废弃(产品版候选)** | 同上,不继续为新增 `im_*` 业务表实现双写逻辑 | +| RethinkDB Adapter | `server/db/rethinkdb` | 无数据库兼容要求 | **废弃(产品版候选)** | 同上;保留上游代码还是物理删除由仓库维护策略决定 | + +以下能力虽然没有独立的新需求章节,但属于满足新需求所需的技术底座,不废弃: + +| 基础能力 | 保留原因 | +| --- | --- | +| `me` Topic 和设备同步 | 会话列表、未读数、多端状态需要 | +| `fnd` 查找 Topic | 第 5 章精准用户搜索可在其基础上收紧 | +| `sys` Topic | 第 2、9、11 章的可信系统通知需要 | +| `slf` Topic | 第 4、7 章收藏同步可复用 | +| gRPC Plugin/Firehose | 第 11 章内容审核和反诈外部服务需要 | +| FCM/TNPG Push | 第 2、4、8、9、10 章离线提醒需要,生产只选择实际使用的 Handler | +| 文件系统/S3 Media Adapter | 第 3、4、6、7、11 章附件需要,按环境选择 | +| 集群、缓存、监控、TLS | 属于可用性和安全基础,不是产品页面功能 | +| root/后台管理能力 | 实名审核、举报处置、组织管理和审计需要,但必须收紧权限 | + +## 6. 需要新增的服务端模块 + +| 新模块 | 对应新需求章节 | 主要职责 | +| --- | --- | --- | +| `imapi` | 第 1、5、6、7、9、10、11 章 | HTTP API、认证中间件、输入校验、统一错误和游标分页 | +| `impolicy` | 3.7-3.8;5.9-5.14;6.23-6.24;10.12-10.17 | 好友、黑名单、禁言、隐私和青少年同步授权 | +| `immessage` | 2.4;3.6、3.10-3.12;第 4 章 | 消息类型、schema 校验、@、引用、编辑、转发和摘要 | +| `imbiz/relationship` | 第 5 章 | 好友申请、联系人、备注、拉黑和删除状态机 | +| `imbiz/group` | 6.3-6.28 | 群审批、公告、二维码、角色、禁言和群文件 | +| `imbiz/identity` | 1.7-1.10 | 二次验证、实名认证、审核和脱敏 | +| `imbiz/workbench` | 第 9 章 | 组织、会议、日报、月报和信箱 | +| `imbiz/safety` | 第 11 章 | 风险事件、举报证据、处置、反馈和审计 | +| `imsearch` | 7.12-7.16 | 消息索引、历史回填、检索和 ACL 二次过滤 | +| `imnotify` | 2.5;第 5、6、9、11 章 | Outbox 消费、系统消息和 Push 通知 | +| `imjobs` | 1.5;5.2;6.3-6.6;9.3-9.4 | 申请过期、会议提醒、重试、清理和索引回填 | + +## 7. 主要数据修改 + +### 7.1 核心表必须修改 + +| 对象 | 对应新需求章节 | 修改内容 | +| --- | --- | --- | +| Message | 2.2、2.4;第 3、4、7 章 | 增加或关联客户端幂等 ID、类型、审核状态、搜索索引;重复发送返回同一 SeqId | +| FileDef/File links | 第 3、4、6、7、11 章 | 增加扫描状态、业务用途和访问查询;下载按消息/Topic ACL 授权 | +| User | 1.1-1.10;5.1;9.2;第 10 章 | 共享 SaaS 时加 tenant;基础资料字段规范化,敏感身份数据独立存储 | +| Topic/Subscription | 第 2、3、5、6 章 | 共享 SaaS 时加 tenant;会话个性设置、群策略和关系状态使用独立业务表 | + +### 7.2 新增业务表 + +| 数据域 | 建议表 | 对应新需求章节 | +| --- | --- | --- | +| 好友与会话 | `im_user_relations`、`im_friend_requests`、`im_conversation_settings`、`im_privacy_settings` | 第 2、3、5 章;10.12-10.13 | +| 群治理 | `im_group_settings`、`im_group_join_requests`、`im_group_mutes`、`im_group_announcements`、`im_group_announcement_reads`、`im_group_invite_tokens` | 第 6 章 | +| 消息与收藏 | `im_message_idempotency`、`im_message_index`、`im_favorites` | 2.2、2.4;第 4、7 章 | +| 身份与设置 | `im_identity_verifications`、`im_user_settings`、`im_teen_mode` | 第 1、10 章 | +| 企业办公 | `im_org_units`、`im_org_members`、`im_meetings`、`im_meeting_participants`、`im_work_reports`、`im_mailbox_messages` | 第 9 章 | +| 安全治理 | `im_reports`、`im_report_evidence`、`im_feedback`、`im_risk_events`、`im_audit_logs` | 第 11 章 | +| 可靠事件 | `im_outbox`、`im_idempotency_keys` | 第 5、6、9、11 章的审批和通知 | + +共享数据库的多企业模式下,核心表和全部 `im_*` 表都必须包含 `tenant_id`,并把租户放入唯一索引、查询条件、缓存键和文件路径。若采用一企一实例/一企一库,则企业码负责服务发现,物理隔离优先。 + +## 8. 主要代码修改位置 + +| 文件/目录 | 对应新需求章节 | 修改内容 | +| --- | --- | --- | +| `server/datamodel.go` | 第 2、3、4、7、8 章 | 定义 `x-im-schema`、消息类型、幂等 ID、引用、转发、搜索和通话扩展 | +| `server/session.go` | 第 2、3、4、5、8、10、11 章 | 消息幂等、策略调用、租户上下文和统一业务错误 | +| `server/topic.go` | 第 2、3、4、6、7 章 | 保存前执行好友/黑名单/禁言/青少年/风险规则,发送结构化系统消息 | +| `server/calls.go` | 第 8 章 | call_id、设备抢答、标准结束原因、通话记录和指标 | +| `server/hdl_files.go` | 第 3、4、6、7、11 章 | 抽取 HTTP 认证、资源 ACL、签名下载 URL 和扫描状态检查 | +| `server/plugins.go` | 第 11 章 | 审核超时、熔断、失败开放/关闭策略和可观测性 | +| `server/push.go` | 2.5;4.4;第 8、9、10、11 章 | 产品消息类型、@、会议、审批、通话和隐私 Push | +| `server/main.go` | 第 1、5、6、7、9、10、11 章 | 注册 `/v1/im/*` API、业务服务和后台任务;逐步下线废弃路由 | +| `server/store` | 所有服务端章节 | 新增核心事务、幂等、文件授权、搜索接口;明确产品版支持的数据库 | +| `server/db/mysql` | 所有服务端章节 | 从当前 schema 118 继续迁移核心 tenant_id、业务表、索引和 MySQL 实现 | +| `server/tinode.conf` | 所有章节 | 企业、产品模块、搜索、审核、TURN、文件扫描、限流和废弃能力开关 | + +## 9. 修改优先级 + +### P0:先保证新系统不会产生安全和数据问题 + +| 工作 | 对应新需求章节 | +| --- | --- | +| 确定一企一实例或共享 SaaS | 1.1、5.1、9.2 | +| 确定产品版只支持 MySQL 还是继续支持多数据库 | 所有服务端章节的存储基础 | +| 消息客户端 ID 幂等 | 2.2;3.4、3.7;第 4 章 | +| 文件下载资源级授权 | 3.10-3.12;4.13-4.20;6.27-6.28;7.3、7.7-7.11 | +| 消息类型、错误码和兼容协议 | 2.4;第 3、4、7、8 章 | +| 停止新客户端使用匿名、Long Poll、Channel 和客户端 gRPC | 新需求无对应章节,废弃项 | + +### P1:完成即时通讯产品主链路 + +| 工作 | 对应新需求章节 | +| --- | --- | +| 企业码、短信注册/找回、二次验证、实名 | 第 1 章 | +| 好友申请、联系人、隐私、黑名单 | 第 5 章;3.1-3.8 | +| 结构化消息、@、引用、转发、收藏 | 2.4;第 3、4 章 | +| 会话设置和可信系统通知 | 第 2 章 | + +### P2:完成群、文件和搜索 + +| 工作 | 对应新需求章节 | +| --- | --- | +| 群申请、公告、管理员、禁言、二维码、群文件 | 第 6 章 | +| 收藏分类和分享 | 7.1-7.8 | +| 关键词和媒体类型搜索、历史回填 | 7.12-7.16 | + +### P3:完成通话、设置和安全 + +| 工作 | 对应新需求章节 | +| --- | --- | +| 通话 ID、跨设备状态、TURN 和详单 | 第 8 章 | +| 资料、版本、隐私和青少年模式 | 第 10 章 | +| 反诈、举报、证据、反馈和后台处置 | 第 11 章 | + +### P4:完成企业办公 + +| 工作 | 对应新需求章节 | +| --- | --- | +| 组织与部门 | 9.2 | +| 会议通知 | 9.3-9.4 | +| 日报/月报 | 9.5-9.11 | +| 总经理信箱 | 9.12-9.13 | + +## 10. 废弃实施规则 + +废弃项不要直接删除,应按以下顺序执行: + +1. 在配置和对接文档中标记 deprecated,禁止新客户端接入; +2. 为废弃入口增加调用指标,至少观察一个完整客户端版本周期; +3. 对匿名账号、邮箱账号、Channel 等有数据的能力先做存量扫描; +4. 给存量用户提供迁移方案,例如邮箱账号绑定手机号、Channel 迁移普通群; +5. 新版本服务端默认关闭,保留临时回退开关; +6. 确认无调用和无未迁移数据后,再删除路由、配置、代码和测试; +7. 数据库删除列或表必须单独发布,保留备份和回滚窗口。 + +客户端 gRPC 和 Plugin gRPC 必须分开处理:新需求没有客户端 gRPC,因此前者废弃;第 11 章需要外部审核插件,因此 Plugin Firehose 及其 gRPC 通道保留。 + +## 11. 验收口径 + +每项改造只有同时满足以下条件才算完成: + +1. 文档中的新需求章节有对应 API/协议、数据表和权限规则; +2. 旧客户端兼容行为有明确结论,不依赖未定义 JSON 字段; +3. 重试、并发、断网恢复不会产生重复业务数据; +4. 用户不能通过直接构造协议绕过好友、黑名单、禁言、隐私和青少年策略; +5. 文件、搜索、转发和收藏均重新执行当前用户的资源权限; +6. 共享 SaaS 模式下通过跨租户负向测试; +7. 审批、实名、举报、群管理和匿名信箱敏感操作有审计记录; +8. 废弃入口有调用量证据和存量迁移结果,不能仅凭新原型没有页面就直接删除; +9. MySQL schema、升级脚本、回滚脚本和代码中的建表定义保持一致; +10. 服务端单元测试、MySQL 集成测试、WebSocket/HTTP 端到端测试和真实设备通话测试全部通过。 + +## 12. 最终处理建议 + +新系统应保留 Tinode 的实时消息内核,重点修改认证流程、消息协议、权限策略、文件授权和通话状态;好友、群治理、搜索、企业工作台、实名、青少年和举报作为新的产品业务层建设。 + +新需求未覆盖的匿名登录、邮箱注册验证、Channel、Long Poll、客户端 gRPC、示例聊天机器人和静态网页托管标记为废弃。多数据库 Adapter 是否物理删除取决于产品数据库决策;若产品版确定只支持 MySQL,则 PostgreSQL、MongoDB 和 RethinkDB 不再承担新业务适配与验收。 + +改造顺序应先处理租户、消息幂等和文件授权,再完成好友与基础消息,随后完成群治理、搜索、通话与安全,最后建设企业工作台。这样可以最大限度复用现有实现,同时避免继续维护新产品不需要的入口和功能。 diff --git a/docs/im/IM-第一二章后续增强项.md b/docs/im/IM-第一二章后续增强项.md new file mode 100644 index 0000000..b58999b --- /dev/null +++ b/docs/im/IM-第一二章后续增强项.md @@ -0,0 +1,298 @@ +# IM 第一至十一章后续增强项 + +> 日期:2026-08-13 +> 范围:第一章登录注册与安全、第二章消息首页/导航/系统通知、第三章单聊关系、第四章群消息与 @、第五章通讯录/好友/黑名单、第六章群聊创建/资料/治理、第七章文件/收藏/聊天记录检索、第八章实时语音与视频通话、第九章企业工作台与组织架构、第十章个人设置与客户端能力、第十一章反诈安全/举报/反馈 +> 用途:记录当前已完成基线之外的增强项,后续统一排期处理。 + +## 1. 第一章:登录、注册与安全 + +### 1.1 当前已完成基线 + +1. 企业码解析、租户状态校验和公开租户配置返回。 +2. 注册、登录前验证码发送与验证。 +3. 注册验证码 Token 消费,避免绕过验证码直接注册。 +4. 忘记密码挑战验证与重置确认。 +5. Token 绑定 `auth_version`,密码重置后旧 Token 失效。 +6. 登录后敏感操作二次验证挑战。 +7. 实名认证状态查询与提交,证件信息脱敏/保护存储。 +8. 独立密码设置与验证,包含弱密码规则和失败锁定。 +9. 登录后 bootstrap 返回实名、安全与菜单基础能力。 + +### 1.2 后续增强项 + +| 优先级 | 增强项 | 说明 | 建议验收点 | +| --- | --- | --- | --- | +| P0 | HTTP `Idempotency-Key` 统一存储与请求摘要冲突检测 | 当前部分接口要求幂等键,但还没有统一的 `tenant_id + uid + method + route + key + body_hash` 处理层。 | 相同键相同 body 返回同一结果;相同键不同 body 返回冲突。 | +| P0 | 验证码真实发送通道 | 当前验证码生成、校验和限频链路已具备,短信/邮件供应商发送仍需接入真实服务。 | 验证码可通过供应商下发;供应商失败有可观测错误和重试策略。 | +| P0 | 验证码限频维度补强 | 需要按租户、手机号、IP、设备、场景组合限频,并落库或接入 Redis。 | 高频请求返回 `VERIFICATION_RATE_LIMITED`,不同场景互不误伤。 | +| P0 | 安全事件通知联动 | 密码重置、实名提交/审核、独立密码变更、新设备登录等应写入系统通知。 | 操作成功后 `/v1/im/system-notifications` 能看到对应事件。 | +| P1 | 实名审核工作流 | 当前支持提交和状态查询,后续需要后台审核、驳回原因、通过后状态变更。 | 审核通过后 `realname.status=verified`;驳回可再次提交。 | +| P1 | 独立密码用途扩展 | 当前按 `purpose` 存储,可继续接入青少年模式关闭、敏感设置修改等业务入口。 | 不同 `purpose` 相互隔离;错误次数和锁定互不串扰。 | +| P1 | 设备与会话管理 | 需要登录设备列表、主动踢出设备、Token 刷新/撤销审计。 | 修改密码或踢设备后对应 Token 立即不可用。 | +| P1 | API Key 轮换策略 | 当前 API Key 校验兼容现有逻辑,尚未实现最低可接受序号或撤销表。 | 旧序号 Key 可按配置禁用,新 Key 不影响正常客户端。 | +| P2 | 风控与反诈策略接入 | 第一章安全动作后续应接入风险评分、反诈弹窗和二次确认策略。 | 高风险动作触发二次验证或反诈提示,低风险动作直通。 | + +## 2. 第二章:消息首页、全局导航与系统通知 + +### 2.1 当前已完成基线 + +1. `GET /v1/im/bootstrap` 返回首页菜单、能力开关、权限和 `message_policy`。 +2. `GET /v1/im/search/conversations` 支持会话搜索和基础分页。 +3. WebSocket 发消息支持 `Head["x-im-client-mid"]` 幂等,重连重发不重复落库/广播。 +4. 消息 Header 基础校验:`x-im-schema`、`type`、`x-im-client-mid`。 +5. 普通用户禁止发布 `sys` Topic,且禁止伪造 `Head.type=system`。 +6. `GET /v1/im/topics/{topic}/messages/{seq}/receipts` 返回群消息送达/已读基础列表。 +7. `POST /v1/im/topics/{topic}/messages/{seq}/recall` 支持版本校验、权限校验和时限内撤回。 +8. `POST /v1/im/topics/{topic}/messages/{seq}/revisions` 支持文本消息编辑、版本递增和修订记录。 +9. `GET /v1/im/system-notifications` 和 `POST /v1/im/system-notifications/{id}/read`。 + +### 2.2 后续增强项 + +| 优先级 | 增强项 | 说明 | 建议验收点 | +| --- | --- | --- | --- | +| P0 | 编辑/撤回多端实时事件广播 | 当前 HTTP 更新 DB 并返回成功,后续需要向 Topic 在线成员广播编辑/撤回事件。 | A 端撤回/编辑后,B 端无需刷新即可收到事件并更新 UI。 | +| P0 | 编辑/撤回事件 Outbox | DB 更新和实时广播应通过事务事件或 Outbox 衔接,避免更新成功但事件丢失。 | 模拟广播失败后,后台任务可补发事件。 | +| P0 | 消息变更查询占位策略 | 已撤回消息在普通拉取中应返回撤回占位,而不是仅靠客户端本地事件。 | 新设备登录拉历史时能看到撤回占位和版本。 | +| P0 | `Idempotency-Key` 接入编辑/撤回 | 编辑/撤回目前依赖乐观版本控制,后续需要同时支持 HTTP 幂等键。 | 客户端超时重试同一请求不会重复写 revision。 | +| P1 | 群回执隐私和角色策略 | 当前返回基础 read/recv 列表,后续需按租户策略、群规模、角色决定是否展示具体成员。 | 普通成员/管理员在不同策略下看到不同粒度回执。 | +| P1 | 群回执 cursor 精确分页 | 当前 cursor 是基础位置分页,后续可升级为稳定游标,避免成员变动造成跳页。 | 成员新增/删除时分页不重复、不漏主要数据。 | +| P1 | `message_policy` 租户配置化 | 当前 recall/edit 窗口和菜单能力是固定值,后续需要来自租户配置。 | 不同租户可配置不同撤回/编辑时限和菜单能力。 | +| P1 | 系统通知可信生产通道 | 当前已提供通知存储和读取接口,后续需要统一服务端生产 API/事件消费者,禁止业务散落直写。 | 安全、群、组织等模块通过统一通道创建通知。 | +| P1 | 系统通知 action 白名单 | `action.type` 和参数需要统一注册、校验和版本化。 | 非白名单 action 写入失败;客户端只收到可识别跳转。 | +| P1 | 会话搜索排序与类型过滤 | 当前按现有订阅数据过滤,后续需支持会话类型、置顶、免打扰、最近活跃排序等。 | 搜索结果与消息首页排序、筛选规则一致。 | +| P2 | 离线重连恢复编排测试 | 需要端到端覆盖 `hi -> token login -> sub me -> 恢复 Topic -> get data`。 | 断网重连后未丢消息、未重复消息、未错乱回执。 | +| P2 | 消息 Header schema 版本演进 | 当前做基础格式/枚举校验,后续需要按 `x-im-schema` 版本维护兼容矩阵。 | 老客户端消息可兼容,新客户端非法字段被拒绝。 | + +## 3. 第三章:单聊关系、权限与会话上下文 + +### 3.1 当前已完成基线 + +1. 单聊会话上下文接口返回双方关系、发送能力、对端公开资料和 P2P Topic。 +2. P2P 发送路径接入好友关系校验,非好友、拉黑状态下禁止直接发送。 +3. 好友申请、通过、拒绝、删除好友与单聊关系状态联动。 +4. 拉黑/解除拉黑后关系上下文即时反映 `blocked_by_me`、`blocked_by_peer` 等状态。 +5. 会话侧可根据 `can_send` 锁定输入框,并引导重新添加好友。 + +### 3.2 后续增强项 + +| 优先级 | 增强项 | 说明 | 建议验收点 | +| --- | --- | --- | --- | +| P0 | P2P 策略统一入口 | 当前核心路径已校验好友/黑名单,后续需要把发消息、通话、名片、文件、历史查看统一收敛到同一策略服务。 | 任一 P2P 业务入口对同一关系状态返回一致权限。 | +| P0 | 关系变更实时同步 | 好友通过、删除、拉黑、解除拉黑后,应向双方在线端推送关系和会话能力变化。 | A 拉黑 B 后,B 端输入框无需刷新自动锁定。 | +| P0 | 关系事件 Outbox | 关系 DB 状态变化与系统通知、会话事件、Push 需要事务级衔接。 | 模拟推送失败后后台可补发,不产生重复事件。 | +| P1 | Pending 会话体验增强 | 待处理好友申请对应的单聊页应携带申请摘要、来源、过期时间和快速处理入口。 | 进入 pending 会话可直接通过/拒绝,不需要跳转新朋友页。 | +| P1 | 黑名单历史消息策略 | 当前发送/通话阻断已具备,后续需明确拉黑后历史消息、资料、文件和已读回执可见范围。 | 被拉黑人在不同策略下看到一致的历史与资料裁剪结果。 | +| P1 | 非好友资料隐私裁剪 | 单聊上下文中的对端资料需要接入隐私设置、企业可见性和青少年模式。 | 非好友只能看到允许公开字段;好友看到扩展字段。 | +| P1 | 好友关系审计 | 删除、拉黑、解除拉黑、申请处理等敏感关系操作写审计日志。 | 后台可按租户、操作者、目标用户查询操作流水。 | +| P2 | 关系状态迁移测试矩阵 | 需要覆盖 none、pending_inbound、pending_outbound、friend、blocked_by_me、blocked_by_peer 的全迁移。 | 任意状态迁移后 DB、HTTP 上下文、WS 权限一致。 | + +## 4. 第四章:群消息、群成员与 @ 能力 + +### 4.1 当前已完成基线 + +1. 群成员列表接口支持基础分页、搜索和 `mentionable` 过滤。 +2. 返回成员角色、公开资料、ACL 和是否可被 @。 +3. 群 @ 消息头部字段校验,支持 `mentioned_user_ids`、`mention_all` 等基础结构。 +4. 群 @ 权限接入成员 ACL,管理员/群主才具备 @ 全体能力。 +5. 群消息读取链路保留 @ 元数据,客户端可据此展示提醒。 + +### 4.2 后续增强项 + +| 优先级 | 增强项 | 说明 | 建议验收点 | +| --- | --- | --- | --- | +| P0 | @ 实时通知与离线 Push | 当前消息元数据已落库,后续需要对被 @ 成员生成高优先级提醒和离线 Push。 | 被 @ 用户在在线、离线、多端场景均收到一次提醒。 | +| P0 | @ 全体频控 | @ 全体需要按租户、群、角色、时间窗口限频,避免刷屏。 | 超限返回稳定错误码,管理员可配置限额。 | +| P0 | 群成员稳定游标分页 | 当前成员列表以基础 cursor 分页,后续应使用稳定游标避免成员变动跳页。 | 成员入群/退群时翻页不重复、不漏主要数据。 | +| P1 | 群成员资料隐私裁剪 | 群内非好友主页需要按群关系、好友关系、隐私设置裁剪资料字段。 | 同一成员从群内和好友主页进入时字段符合策略差异。 | +| P1 | 群昵称/角色变更事件 | 群昵称、角色、禁言、移除等变化需要实时推送给群成员并刷新成员列表缓存。 | 群主修改成员角色后在线端成员列表自动更新。 | +| P1 | @ 候选排序优化 | @ 面板候选应支持拼音首字母、最近互动、角色优先和本地缓存协同。 | 大群搜索响应稳定,候选排序符合产品规则。 | +| P1 | 群治理策略配置化 | 成员私聊、成员邀请、全员禁言、上传权限等应统一由群治理配置控制。 | 不同群可配置不同治理能力,HTTP 与 WS 权限一致。 | +| P2 | 大群性能压测 | 需要覆盖 500、2000、10000 成员下的成员列表、@ 搜索和发送性能。 | 达到目标延迟和资源占用阈值。 | + +## 5. 第五章:通讯录、好友关系与黑名单 + +### 5.1 当前已完成基线 + +1. 好友列表支持查询、首字母、分页,返回备注、头像、显示名来源和会话 Topic。 +2. 新朋友列表、好友申请、通过、拒绝链路已具备。 +3. 用户搜索支持账号、精准账号和手机号类型,返回公开资料、关系和 `action`。 +4. 添加好友二维码支持短期签名 Token 生成与解析。 +5. 好友个人主页返回关系、资料和操作能力。 +6. 群内非好友个人主页返回群角色、关系、资料和操作能力。 +7. 好友备注更新、删除好友、拉黑/解除拉黑已具备。 +8. 黑名单列表和批量移出黑名单已具备。 + +### 5.2 后续增强项 + +| 优先级 | 增强项 | 说明 | 建议验收点 | +| --- | --- | --- | --- | +| P0 | 好友/黑名单操作统一幂等 | 拉黑、解除、删除、批量移出、好友申请等需要统一接入 `Idempotency-Key` 存储和请求摘要。 | 客户端超时重试不会重复创建申请或重复触发事件。 | +| P0 | 好友关系实时事件 | 申请、通过、拒绝、删除、备注、拉黑等状态变化需要向相关在线端实时同步。 | 两端关系页、会话页和通讯录无需刷新自动更新。 | +| P0 | 用户搜索隐私策略 | 当前搜索基线可用,后续需要接入 `allow_user_search`、添加方式、手机号可搜索等租户和个人隐私设置。 | 禁止手机号添加的用户无法通过手机号被搜索到,且不泄露存在性。 | +| P0 | 二维码吊销与一次性策略 | 当前二维码是短期签名 Token,后续如需主动失效、刷新覆盖、扫码审计,需要落库或接入 Redis。 | 用户刷新二维码后旧码按策略失效;后台可查询扫码记录。 | +| P1 | 好友资料字段精细裁剪 | 个人主页需按好友、非好友、群内关系、黑名单、实名/青少年等规则返回不同字段。 | 不同入口返回字段符合隐私矩阵。 | +| P1 | 通讯录排序与拼音索引 | 当前支持基础 initial,后续需维护昵称/备注拼音、特殊字符分组和本地增量同步。 | 好友列表与客户端通讯录排序一致,中文首字母稳定。 | +| P1 | 黑名单占位资料 | 资料删除、离职或不可见时,黑名单列表仍需返回可解除的受控占位用户。 | 黑名单中失效用户可展示占位并成功移出。 | +| P1 | 好友申请防骚扰 | 需要按目标用户、申请人、IP、设备、来源和群来源限频,并支持拒收策略。 | 高频申请被限制;群来源申请校验双方仍在群内。 | +| P1 | 关系审计和安全通知 | 添加、删除、拉黑、解除、二维码解析等应写审计并按策略生成系统通知。 | 用户可在通知中看到关键关系事件,后台可追溯。 | +| P2 | 批量接口部分失败语义增强 | 批量移出黑名单目前返回逐项结果,后续需要统一批量接口错误码、重试建议和幂等结果缓存。 | 部分失败时客户端能只重试失败项。 | + +## 6. 第六章:群聊创建、资料维护与成员治理 + +### 6.1 当前已完成基线 + +1. 群列表、创建群、群资料查询与更新、群头像和群简介保存接口已具备。 +2. 群设置、个人群设置、退出群、群昵称接口已具备。 +3. 群二维码生成与扫码解析支持 `join_group` 场景。 +4. 群邀请、进群申请、申请列表、同意/拒绝、群 summary 申请计数已具备。 +5. 群公告最新查询、发布/更新、详情和已读接口已具备。 +6. 群治理配置 `GET/PATCH /governance` 已具备。 +7. 管理员候选、管理员上限、批量添加管理员、删除管理员和群主转让接口已具备。 +8. 禁言候选、批量禁言、禁言列表和解除禁言接口已具备。 +9. 当前实现优先复用 Topic、Subscription 和 Topic Aux,把产品主链路接口跑通。 + +### 6.2 后续增强项 + +| 优先级 | 增强项 | 说明 | 建议验收点 | +| --- | --- | --- | --- | +| P0 | 群业务独立表拆分 | 当前群公告、进群申请、治理配置、禁言记录先存 Topic Aux,后续需要拆为独立表。 | 公告、申请、治理、禁言均有 tenant/topic 维度索引,可稳定分页和审计。 | +| P0 | 群治理强事务状态机 | 邀请、申请审批、管理员变更、群主转让、禁言需在事务内同步业务表、订阅 ACL、成员数和事件。 | 并发审批/转让/禁言不会出现 ACL 与业务状态不一致。 | +| P0 | 第六章幂等键落库 | 邀请、申请处理、公告发布、治理 PATCH、管理员批量、群主转让、禁言批量都需要统一幂等。 | 超时重试同一请求返回同一结果,不重复加人、升权或发事件。 | +| P0 | 系统事件、Push 与 Outbox | 第六章所有成员/公告/治理变化需要生成可信群系统事件,并通过 Outbox 保证推送。 | 在线端实时刷新;模拟推送失败后可补偿且不重复。 | +| P1 | 群公告已读独立记录 | 当前公告已读返回基础结果,后续需记录每个成员的公告已读时间。 | 新公告只提醒未读成员,已读状态跨端同步。 | +| P1 | 进群申请隐私与限频 | 申请入口需要按群治理、租户策略、用户状态、来源、IP/设备限频。 | 高频申请被限制;普通成员不可看到敏感申请人信息。 | +| P1 | 管理员/群主角色层级策略 | 管理员互相操作、管理员禁言、群主转让二次认证等需要完整策略矩阵。 | 群主、管理员、普通成员在每个治理动作上的权限稳定一致。 | +| P1 | 禁言发送链路强校验 | 当前禁言接口已具备,后续需要在 WebSocket 发消息路径校验 `GROUP_MUTED`。 | 被禁言成员发群消息被服务端拒绝,过期后自动恢复。 | +| P1 | 群文件索引 | 6.27-6.28 需要独立群文件表,绑定上传文件、群成员权限、下载状态和删除权限。 | 群文件列表可搜索/分页,退群后不可下载旧 URL。 | +| P1 | 群解散与二维码吊销 | 群解散、二维码撤销、邀请失效需要状态联动和权限回收。 | 解散后 Topic、二维码、邀请、文件新增权限全部失效。 | +| P2 | 大群性能与稳定分页 | 成员、申请、公告、禁言、文件列表都需要稳定游标和大群压测。 | 大群场景下分页不重复不漏,接口延迟达标。 | + +## 7. 第七章:文件下载、收藏管理与聊天记录检索 + +### 7.1 当前已完成基线 + +1. 收藏创建、列表、详情和删除接口已具备,支持文本、文件、语音、链接等类型。 +2. 收藏批量创建和基础分享前的数据结构已具备。 +3. Topic 消息关键词搜索、文件/图片/视频等媒体类型检索接口已具备。 +4. 消息上下文定位接口可围绕指定 seq 返回前后消息。 +5. 文件访问已接入用户与 Topic 权限校验,下载前可判断资源可见性。 + +### 7.2 后续增强项 + +| 优先级 | 增强项 | 说明 | 建议验收点 | +| --- | --- | --- | --- | +| P0 | 收藏幂等与去重策略 | 收藏同一消息、同一文件或同一链接需要统一去重和幂等键处理。 | 重复点击收藏不会产生重复记录;超时重试返回同一收藏 ID。 | +| P0 | 收藏分享权限二次校验 | 收藏文件或消息分享给好友/群时,需要重新校验接收方是否可访问原资源或生成受控副本。 | 无权限接收方不能通过收藏分享拿到原会话附件。 | +| P0 | 文件下载短时签名 URL | 当前权限判断已具备,后续应生成短时下载 URL,并记录下载审计。 | URL 过期不可访问;退群/拉黑后旧 URL 失效或受策略限制。 | +| P0 | 文件安全扫描与 MIME 校验 | 上传后的文件需要扩展名、MIME、内容嗅探、病毒扫描和图片/视频解码安全检查。 | 高风险文件不可下载或转发,客户端收到稳定错误码。 | +| P1 | 收藏分类与全文索引 | 收藏列表需要按类型、来源、时间、关键词、发送人等维度检索。 | 大量收藏下搜索响应稳定,类型筛选结果准确。 | +| P1 | 聊天记录稳定游标 | 消息搜索和媒体列表需要稳定游标,避免新消息插入导致翻页重复或漏项。 | 搜索期间新增消息不影响当前翻页结果。 | +| P1 | 历史消息权限快照策略 | 用户退群、删除好友、拉黑后,历史消息和附件的可见范围需要统一规则。 | 各入口对同一历史资源权限判断一致。 | +| P1 | 下载状态与断点续传 | 大文件下载需要断点续传、失败重试、进度状态和下载完成事件。 | 网络中断后可续传;下载完成状态可跨端同步或按端隔离。 | +| P2 | 聊天记录高亮与命中片段 | 搜索结果可返回命中摘要、高亮范围和消息类型摘要。 | 客户端无需二次解析即可展示命中片段。 | +| P2 | 收藏导出与批量管理 | 后续可支持收藏批量删除、批量转发、导出和云端容量统计。 | 批量操作有部分失败语义和可重试结果。 | + +## 8. 第八章:实时语音与视频通话 + +### 8.1 当前已完成基线 + +1. 通话 invite、accept、offer、answer、ice-candidate、hang-up 等事件沿用 Tinode call event 链路。 +2. 通话记录表 `im_call_records` 已具备,记录 `call_id`、Topic、seq、主叫/被叫、类型、状态、时长和结束原因。 +3. 通话记录创建、接听和结束状态更新已接入 store/adapter。 +4. 通话记录查询接口已具备:全局 `GET /v1/im/calls` 与 Topic 内 `GET /v1/im/topics/{topic}/calls`。 +5. 查询结果返回对端用户、方向、通话类型、状态、时长和关键时间点,客户端可先完成通话记录页对接。 + +### 8.2 后续增强项 + +| 优先级 | 增强项 | 说明 | 建议验收点 | +| --- | --- | --- | --- | +| P0 | 跨设备抢答状态机 | 同一账号多端收到来电时,需要一个设备接听后自动取消其他端待接状态。 | A 手机接听后,A 平板和 Web 端立即变为已在其他设备接听。 | +| P0 | 通话状态强一致与幂等 | invite、accept、hang-up 等事件需要幂等键或事件序号,避免乱序/重复导致状态回退。 | 重复 hang-up 不改变最终状态;迟到 accept 不会覆盖已结束通话。 | +| P0 | 通话事件 Outbox 与通知 | 来电、未接、挂断、忙线等需要可信事件和 Push,DB 状态与通知通过 Outbox 衔接。 | 离线用户收到一次来电/未接提醒,失败可补偿。 | +| P0 | TURN/STUN 配置与媒体准入 | 服务端应按租户/区域返回可用 ICE 配置,并限制未授权用户创建媒体会话。 | 客户端拉取到有效 TURN;无权限通话被服务端拒绝。 | +| P1 | 结束原因标准化 | 需要统一 `missed`、`declined`、`busy`、`timeout`、`network_error` 等原因和客户端展示文案。 | 不同端同一结束原因展示一致,统计口径一致。 | +| P1 | 通话质量指标 | 需要采集建立耗时、ICE 成功率、丢包、抖动、断连等指标。 | 后台可按租户和版本查看通话质量趋势。 | +| P1 | 群通话/会议房间拆分 | 群音视频不宜复用 P2P call,需要会议房间、参与人状态和 SFU 接入。 | 群通话有独立 room_id、成员列表和退出/重进状态。 | +| P1 | 通话权限统一策略 | 通话入口应复用 P2P/群关系策略、黑名单、禁言、青少年模式和企业策略。 | 被拉黑、非好友或受限制用户无法发起通话。 | +| P2 | 通话记录聚合与清理 | 通话记录可按会话、日期、未接分类聚合,并支持保留周期清理。 | 记录列表分页稳定,超过保留期按策略清理。 | + +## 9. 第九章:企业工作台与办公协同 + +### 9.1 当前已完成基线 + +1. 工作台接口 `GET /v1/im/workbench` 已具备,返回模块、布局版本和企业功能开关。 +2. 工作台模块表 `im_workbench_apps` 已具备,默认包含组织架构、会议通知、日报、月报、总经理信箱入口。 +3. 服务端对工作台 route 做内部白名单过滤,避免客户端执行任意服务端 URL。 +4. 组织架构部门表 `im_org_departments` 和成员表 `im_org_members` 已具备。 +5. 部门列表 `GET /v1/im/organization/departments`、部门成员 `GET /v1/im/organization/departments/{id}/members`、组织搜索 `GET /v1/im/organization/search` 已具备。 +6. 会议通知 `POST/GET/PATCH/CANCEL/REMIND/MY-RESPONSE` 已具备基线,包含参与人和版本冲突控制。 +7. 日报/月报草稿、提交、详情、列表、统计与提醒基线已具备。 +8. 总经理信箱配置与提交接口已具备,包含匿名策略、文案版本和幂等键。 + +### 9.2 后续增强项 + +| 优先级 | 增强项 | 说明 | 建议验收点 | +| --- | --- | --- | --- | +| P0 | 工作台权限与功能开关配置化 | 当前模块基线可返回,后续需要按租户、部门、角色、后台配置动态裁剪。 | 不同用户登录同一租户看到不同授权模块。 | +| P0 | 工作台未读角标事件 | 文档要求 `workbench.badge.updated`,后续需要模块未读聚合和实时增量事件。 | 会议、日报、信箱未读变化后工作台角标实时更新。 | +| P0 | 组织数据后台维护链路 | 当前前台读接口和表已具备,后续需要后台导入、增删改部门、调整成员和离职状态。 | 后台修改组织后前台接口版本变化,客户端可刷新。 | +| P0 | 组织权限与可见字段裁剪 | 成员手机号、邮箱、职位等字段需要按组织角色、隐私和企业策略裁剪。 | 普通员工和管理员看到的成员字段不同且不可绕过。 | +| P1 | 组织搜索索引优化 | 当前基础 LIKE 搜索可用,后续需要拼音、工号、手机号脱敏匹配和稳定排序。 | 大组织下搜索延迟达标,结果排序符合组织规则。 | +| P1 | 组织版本与增量同步 | 部门和成员返回版本,后续需要全局组织版本、变更事件和客户端增量刷新。 | 切换部门或人员变更后客户端无需全量重拉。 | +| P1 | 会议通知实时提醒 | 当前接口和表已具备,后续需要会议创建、更新、取消、响应后的在线事件和 Push。 | 参与人在线端无需刷新即可收到会议变化。 | +| P1 | 日报/月报后台统计口径 | 当前基础统计可用,后续需要按部门、周期、迟交、补交、未读等维度扩展。 | 管理员看板与业务口径一致。 | +| P1 | 总经理信箱后台流转 | 当前前台配置/提交已具备,后续需要后台处理、回复、转派、归档和通知。 | 信件全生命周期可追踪,匿名身份受保护。 | +| P1 | 办公协同审计 | 组织查询、会议变更、报告查看、信箱提交等敏感动作需要审计。 | 后台可按租户、用户、对象查询办公操作流水。 | +| P2 | 办公模块统一附件策略 | 会议、日报、月报、信箱附件需要复用第七章文件 ACL、用途和病毒扫描。 | 附件上传、查看、下载权限与业务状态一致。 | + +## 10. 个人设置与客户端能力 + +### 10.1 当前已完成基线 + +1. 个人资料 `GET/PATCH /v1/im/me/profile`、个人页 `GET /v1/im/me` 已具备。 +2. 隐私设置 `GET/PATCH /v1/im/me/privacy` 已具备,支持版本冲突控制。 +3. 最新版本检查 `GET /v1/im/app/releases/latest` 已具备,支持未登录按租户解析调用。 +4. 青年模式查询、开启、关闭接口已具备,复用敏感操作 `action_token` 和独立密码校验链路。 +5. 设备 Push Token 覆盖写入 `PUT /v1/im/devices/{device_id}/push-token` 已具备。 + +### 10.2 后续增强项 + +| 优先级 | 增强项 | 说明 | 建议验收点 | +| --- | --- | --- | --- | +| P0 | 个人资料变更实时同步 | 资料、头像、隐私变更后需要向多端推送 `profile.updated`、`privacy.updated`。 | 同账号多端无需重登即可刷新设置。 | +| P0 | 精准登出与会话撤销 | 现有 token 主要靠 `auth_version` 批量失效,后续需要 access/refresh/session 表支持当前设备登出和指定 session 删除。 | 当前设备退出不影响其他设备;删除指定 session 后该设备 token 立即失效。 | +| P0 | 青年模式策略配置化 | 当前返回默认 restrictions,后续需要后台按租户配置可用时段、内容限制、消费保护等规则。 | 不同租户返回不同限制策略且能热更新。 | +| P0 | Push Token 生命周期 | 当前可覆盖写入,后续需要 token 失效回收、设备解绑、平台厂商错误码处理。 | 推送失败后自动禁用无效 token,重新登录可恢复。 | +| P1 | 版本灰度发布 | 当前按 platform/channel 查最新版本,后续需要按租户、用户、灰度比例、最低版本强制升级。 | 灰度用户收到新版本,非灰度用户保持稳定版本。 | +| P1 | 资料隐私裁剪落地 | 隐私设置需要接入用户搜索、组织架构、会话资料页等读路径。 | 非授权用户无法通过其他接口绕过隐私配置。 | +| P2 | 客户端能力上报 | 后续增加设备能力、通知权限、厂商通道状态和前台活跃状态上报。 | 后台可定位推送不可达和版本兼容问题。 | + +## 11. 反诈安全、举报与意见反馈 + +### 11.1 当前已完成基线 + +1. URL 安全检查 `POST /v1/im/security/urls/check` 已接入风控 challenge 创建。 +2. 风控确认 `POST /v1/im/security/risk-challenges/{id}/confirm` 已具备,`continue` 返回短时 `risk_action_token`。 +3. 举报原因 `GET /v1/im/reports/abuse/reasons` 已具备,默认覆盖用户、群、消息目标。 +4. 举报提交 `POST /v1/im/reports/abuse` 已具备,支持幂等键、证据文件 ID、目标基础可见性校验和审核快照。 +5. 意见反馈配置 `GET /v1/im/feedback/config`、提交 `POST /v1/im/feedback` 已具备,支持附件规则、诊断授权和幂等键。 + +### 11.2 后续增强项 + +| 优先级 | 增强项 | 说明 | 建议验收点 | +| --- | --- | --- | --- | +| P0 | 风控策略引擎 | 当前 URL 检查仅基础规则,后续需要按动作、关系、内容、设备、频率和租户策略综合判定。 | 高风险发消息、加好友、打开链接等动作统一返回风控 challenge。 | +| P0 | 风控 token 消费接入业务接口 | 当前确认可签发 `risk_action_token`,后续需要原业务接口校验并单次消费该 token。 | 二次确认后只允许重试原动作一次,重复使用返回 Token 已消费。 | +| P0 | 举报目标完整权限校验 | 当前做基础可见性校验,后续需要结合用户侧删除、撤回、退群、黑名单、文件 ACL 和审核状态。 | 无权限目标无法提交举报,也不泄露目标存在性。 | +| P0 | 审核后台流转 | 举报和反馈当前仅 received,后续需要后台分派、处理、关闭、回复和通知。 | 处理状态可追踪,举报人按策略收到处理结果。 | +| P1 | 证据文件安全与保留期 | 证据文件需要 `abuse_evidence` 专用 ACL、病毒扫描、不可转发和保留期策略。 | 证据只能审核链路访问,过期按政策清理。 | +| P1 | 反诈系统事件可信生产 | 低风险提示需通过可信 `security.risk_notice` 系统事件发送,并限制普通消息伪装。 | 普通用户无法伪造反诈弹窗,客户端只信任系统事件。 | +| P1 | 举报合并与限频 | 重复举报需要合并、返回原受理号或限频错误,避免刷单。 | 短时间重复举报同一目标不会生成大量工单。 | +| P2 | 反馈诊断包脱敏 | 诊断包需统一脱敏、大小限制、客户端日志授权和服务端下载审计。 | 未同意时没有诊断包,同意后包内不含明文 token/手机号。 | + +## 12. 统一处理建议 + +1. 先做 P0:统一幂等、实时事件/Outbox、验证码真实发送与限频、P2P/好友/搜索隐私策略、第六章群业务独立表与事务状态机、文件下载签名、通话强状态机、工作台/组织权限。 +2. 再做 P1:租户策略配置、系统通知生产通道、实名审核、设备管理、资料隐私裁剪、关系审计、群禁言发送校验、群文件索引、通话质量指标、会议/日报/月报/信箱完整业务表。 +3. 最后做 P2:端到端重连测试、schema 版本演进、大群压测、批量接口体验优化、收藏导出、通话记录聚合和办公模块附件统一策略。 +4. 每个增强项落地前同步更新 `IM-系统设计文档.md` 和 `IM-前后端对接文档.md` 中对应章节状态。 diff --git a/docs/im/IM-第一至七章数据库增量审核.sql b/docs/im/IM-第一至七章数据库增量审核.sql new file mode 100644 index 0000000..bb6c62b --- /dev/null +++ b/docs/im/IM-第一至七章数据库增量审核.sql @@ -0,0 +1,316 @@ +-- IM 第一至七章数据库增量审核稿 +-- 仅供审核,不要直接在生产库盲执行。 +-- 适用目标:已有 Tinode MySQL 库,补齐本项目 1-7 章业务层依赖表与关键字段。 +-- 注意:本稿使用 MySQL 8 functional index 处理空 idempotency_key 唯一性。 +-- 第一阶段多库方案:本 SQL 的 im_tenant 放 hotgo 公共库;tinode/各租户业务库表暂时保留 tenant_id 字段。 + +SET NAMES utf8mb4; + +-- hotgo 公共后台库需要先存在;如果你审核通过,可先执行: +-- CREATE DATABASE IF NOT EXISTS hotgo CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; +-- USE hotgo; +-- +-- 租户业务库 tinode 也需要存在并完成 Tinode 基础表初始化: +-- CREATE DATABASE IF NOT EXISTS tinode CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; + +-- ============================================================ +-- 1. 租户、认证、安全基础表 +-- ============================================================ + +CREATE TABLE IF NOT EXISTS im_tenant( + id BIGINT NOT NULL AUTO_INCREMENT, + code VARCHAR(64) NOT NULL, + name VARCHAR(128) NOT NULL, + tenant_desc VARCHAR(256), + db_name VARCHAR(128) NOT NULL DEFAULT '' COMMENT 'tenant business database name, e.g. tinode_acme', + db_dsn VARCHAR(1024) NOT NULL DEFAULT '' COMMENT 'optional full tenant DB DSN, empty means reuse base MySQL config', + state SMALLINT NOT NULL, + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + created_by BIGINT NOT NULL, + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + updated_by BIGINT NOT NULL, + PRIMARY KEY(id), + UNIQUE INDEX uk_im_tenant_code(code) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +INSERT INTO im_tenant(code,name,tenant_desc,db_name,db_dsn,state,created_by,updated_by) +VALUES('default','Default Tenant',NULL,'tinode','',1,0,0) +ON DUPLICATE KEY UPDATE name=VALUES(name), db_name=VALUES(db_name), updated_at=CURRENT_TIMESTAMP(3); + +CREATE TABLE IF NOT EXISTS im_tenant_config( + id BIGINT NOT NULL AUTO_INCREMENT, + tenant_id BIGINT NOT NULL, + max_users BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited', + max_groups BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited', + max_group_members INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + created_by BIGINT NOT NULL, + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + updated_by BIGINT NOT NULL, + PRIMARY KEY(id), + UNIQUE INDEX uk_im_tenant_config_tenant_id(tenant_id), + CONSTRAINT fk_im_tenant_config_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS im_verification_codes( + id BIGINT NOT NULL AUTO_INCREMENT, + tenant_id BIGINT NOT NULL, + challenge_id VARCHAR(64) NOT NULL, + scene VARCHAR(32) NOT NULL, + action VARCHAR(64) NOT NULL DEFAULT '', + phone_hash CHAR(64) NOT NULL, + phone_mask VARCHAR(32) NOT NULL, + target_user BIGINT, + code_hash CHAR(64) NOT NULL, + verification_token_hash CHAR(64), + state SMALLINT NOT NULL, + attempts INT NOT NULL DEFAULT 0, + max_attempts INT NOT NULL, + expires_at DATETIME(3) NOT NULL, + resend_after DATETIME(3) NOT NULL, + verified_at DATETIME(3), + consumed_at DATETIME(3), + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY(id), + UNIQUE INDEX uk_im_verification_tenant_challenge(tenant_id, challenge_id), + INDEX idx_im_verification_tenant_scene_phone_state(tenant_id, scene, phone_hash, state, expires_at), + CONSTRAINT fk_im_verification_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS im_user_security( + tenant_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + auth_version INT NOT NULL DEFAULT 1, + password_updated_at DATETIME(3), + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY(tenant_id,user_id), + CONSTRAINT fk_im_user_security_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS im_realname_verifications( + id BIGINT NOT NULL AUTO_INCREMENT, + tenant_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + submission_id VARCHAR(64) NOT NULL, + status VARCHAR(16) NOT NULL, + name_cipher TEXT NOT NULL, + id_type VARCHAR(32) NOT NULL, + id_number_cipher TEXT NOT NULL, + id_number_hash CHAR(64) NOT NULL, + masked_name VARCHAR(64) NOT NULL, + masked_id_number VARCHAR(64) NOT NULL, + review_reason VARCHAR(256), + verified_at DATETIME(3), + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY(id), + UNIQUE INDEX uk_im_realname_submission(tenant_id, submission_id), + INDEX idx_im_realname_user_latest(tenant_id, user_id, created_at), + INDEX idx_im_realname_id_hash(tenant_id, id_number_hash), + CONSTRAINT fk_im_realname_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS im_action_tokens( + id BIGINT NOT NULL AUTO_INCREMENT, + tenant_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + token_id VARCHAR(64) NOT NULL, + action VARCHAR(64) NOT NULL, + target VARCHAR(128) NOT NULL DEFAULT '', + token_hash CHAR(64) NOT NULL, + state SMALLINT NOT NULL, + expires_at DATETIME(3) NOT NULL, + consumed_at DATETIME(3), + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY(id), + UNIQUE INDEX uk_im_action_tokens_token(tenant_id, token_id), + INDEX idx_im_action_tokens_user_action(tenant_id, user_id, action, target, state, expires_at), + CONSTRAINT fk_im_action_tokens_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS im_youth_mode_settings( + tenant_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + purpose VARCHAR(64) NOT NULL, + password_hash VARBINARY(255) NOT NULL, + failed_attempts INT NOT NULL DEFAULT 0, + locked_until DATETIME(3), + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY(tenant_id, user_id, purpose), + CONSTRAINT fk_im_youth_settings_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS im_system_notifications( + id BIGINT NOT NULL AUTO_INCREMENT, + tenant_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + event_id VARCHAR(64) NOT NULL, + event_type VARCHAR(64) NOT NULL, + title VARCHAR(128) NOT NULL, + summary VARCHAR(512) NOT NULL, + action JSON, + read_at DATETIME(3), + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY(id), + UNIQUE INDEX uk_im_system_notifications_event(tenant_id, user_id, event_id), + INDEX idx_im_system_notifications_inbox(tenant_id, user_id, id DESC), + CONSTRAINT fk_im_system_notifications_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ============================================================ +-- 2. 好友、黑名单 +-- ============================================================ + +CREATE TABLE IF NOT EXISTS im_friend_requests( + id BIGINT NOT NULL AUTO_INCREMENT, + tenant_id BIGINT NOT NULL, + request_id VARCHAR(64) NOT NULL, + applicant_id BIGINT NOT NULL, + target_id BIGINT NOT NULL, + message VARCHAR(200) NOT NULL DEFAULT '', + source VARCHAR(32) NOT NULL DEFAULT '', + source_topic VARCHAR(64) NOT NULL DEFAULT '', + status VARCHAR(16) NOT NULL, + idempotency_key VARCHAR(128) NOT NULL DEFAULT '', + expires_at DATETIME(3) NOT NULL, + handled_at DATETIME(3), + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY(id), + UNIQUE INDEX uk_im_friend_requests_request(tenant_id, request_id), + INDEX idx_im_friend_requests_pair(tenant_id, applicant_id, target_id, status, created_at), + INDEX idx_im_friend_requests_inbox(tenant_id, target_id, status, created_at), + CONSTRAINT fk_im_friend_requests_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE UNIQUE INDEX uk_im_friend_requests_idempotency + ON im_friend_requests(tenant_id, applicant_id, (NULLIF(idempotency_key, ''))); + +CREATE TABLE IF NOT EXISTS im_friendships( + tenant_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + peer_id BIGINT NOT NULL, + status VARCHAR(16) NOT NULL, + remark VARCHAR(64) NOT NULL DEFAULT '', + topic CHAR(25) NOT NULL DEFAULT '', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY(tenant_id, user_id, peer_id), + INDEX idx_im_friendships_peer(tenant_id, peer_id, status), + CONSTRAINT fk_im_friendships_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS im_user_blocks( + tenant_id BIGINT NOT NULL, + blocker_id BIGINT NOT NULL, + blocked_id BIGINT NOT NULL, + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY(tenant_id, blocker_id, blocked_id), + INDEX idx_im_user_blocks_blocked(tenant_id, blocked_id), + CONSTRAINT fk_im_user_blocks_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ============================================================ +-- 3. 收藏、转发、消息增强 +-- ============================================================ + +CREATE TABLE IF NOT EXISTS im_favorites( + id BIGINT NOT NULL AUTO_INCREMENT, + tenant_id BIGINT NOT NULL, + favorite_id VARCHAR(64) NOT NULL, + user_id BIGINT NOT NULL, + item_type VARCHAR(16) NOT NULL, + source_topic CHAR(25) NOT NULL, + source_seq INT NOT NULL, + snapshot JSON, + attachment_refs JSON, + availability VARCHAR(16) NOT NULL DEFAULT 'available', + idempotency_key VARCHAR(128) NOT NULL DEFAULT '', + collected_at DATETIME(3) NOT NULL, + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY(id), + UNIQUE INDEX uk_im_favorites_favorite(tenant_id, favorite_id), + UNIQUE INDEX uk_im_favorites_source(tenant_id, user_id, source_topic, source_seq), + INDEX idx_im_favorites_inbox(tenant_id, user_id, id DESC), + INDEX idx_im_favorites_type(tenant_id, user_id, item_type, id DESC), + CONSTRAINT fk_im_favorites_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE UNIQUE INDEX uk_im_favorites_idempotency + ON im_favorites(tenant_id, user_id, (NULLIF(idempotency_key, ''))); + +CREATE TABLE IF NOT EXISTS im_message_revisions( + id BIGINT NOT NULL AUTO_INCREMENT, + tenant_id BIGINT NOT NULL, + createdat DATETIME(3) NOT NULL, + updatedat DATETIME(3) NOT NULL, + topic CHAR(25) NOT NULL, + seqid INT NOT NULL, + version INT NOT NULL, + actor BIGINT NOT NULL, + head JSON, + content JSON, + reason VARCHAR(256) NOT NULL DEFAULT '', + PRIMARY KEY(id), + UNIQUE INDEX uk_im_message_revisions_version(tenant_id, topic, seqid, version), + CONSTRAINT fk_im_message_revisions_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ============================================================ +-- 4. Tinode 基础表需补齐的字段/索引 +-- 如果你的库是用当前 schema.sql 初始化,这些已经存在。 +-- 如果是旧库,需要逐项核对后再 ALTER。 +-- ============================================================ + +-- users:租户隔离字段和索引。 +-- ALTER TABLE users ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 1 AFTER id; +-- CREATE INDEX users_tenant_id_id ON users(tenant_id, id); + +-- topics:租户隔离、群治理 Aux、租户维度唯一索引。 +-- 第六章群邀请、入群审批、群公告、群治理配置、成员禁言目前存放在 topics.aux JSON。 +-- 后续若要强状态机和审计,应拆 im_group_join_requests、im_group_announcements、im_group_mutes 等表。 +-- ALTER TABLE topics ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 1 AFTER id; +-- ALTER TABLE topics ADD COLUMN aux JSON AFTER tags; +-- CREATE UNIQUE INDEX topics_tenant_name ON topics(tenant_id, name); +-- CREATE INDEX topics_tenant_owner ON topics(tenant_id, owner); + +-- subscriptions:租户字段和按租户查成员索引。 +-- ALTER TABLE subscriptions ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 1 AFTER id; +-- CREATE INDEX subscriptions_tenant_userid ON subscriptions(tenant_id, userid); +-- CREATE INDEX subscriptions_tenant_topic ON subscriptions(tenant_id, topic); +-- CREATE INDEX subscriptions_userid_topic_deletedat ON subscriptions(userid, topic, deletedat); + +-- messages:租户、客户端消息幂等、编辑撤回字段、搜索/文件授权需要的索引。 +-- ALTER TABLE messages ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 1 AFTER id; +-- ALTER TABLE messages ADD COLUMN client_msg_id VARCHAR(64) AFTER `from`; +-- ALTER TABLE messages ADD COLUMN version INT NOT NULL DEFAULT 1 AFTER client_msg_id; +-- ALTER TABLE messages ADD COLUMN recalled_at DATETIME(3) AFTER version; +-- ALTER TABLE messages ADD COLUMN recalled_by BIGINT AFTER recalled_at; +-- CREATE INDEX messages_tenant_topic ON messages(tenant_id, topic); +-- CREATE UNIQUE INDEX messages_sender_client_mid ON messages(tenant_id, `from`, client_msg_id); + +-- dellog:用户侧删除过滤需要 tenant_id。 +-- ALTER TABLE dellog ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 1 AFTER id; +-- CREATE INDEX dellog_tenant_topic ON dellog(tenant_id, topic); +-- CREATE INDEX dellog_topic_deletedfor_low_hi ON dellog(topic, deletedfor, low, hi); + +-- fileuploads:文件 ACL、租户隔离、ETag。 +-- ALTER TABLE fileuploads ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 1 AFTER id; +-- ALTER TABLE fileuploads ADD COLUMN etag VARCHAR(128) AFTER size; +-- CREATE INDEX fileuploads_tenant_status ON fileuploads(tenant_id, status); + +-- filemsglinks:文件资源级 ACL 需要租户、topic 和 userid 引用。 +-- ALTER TABLE filemsglinks ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 1 AFTER id; +-- ALTER TABLE filemsglinks ADD COLUMN topic CHAR(25) AFTER msgid; +-- ALTER TABLE filemsglinks ADD COLUMN userid BIGINT AFTER topic; +-- CREATE INDEX filemsglinks_tenant_fileid ON filemsglinks(tenant_id, fileid); + +-- usertags/topictags/devices/auth/credentials 也已按租户增加 tenant_id; +-- 若旧库缺字段,需要参照当前 server/db/mysql/schema.sql 逐项补齐。 diff --git a/docs/im/IM-系统设计文档.md b/docs/im/IM-系统设计文档.md new file mode 100644 index 0000000..7c53433 --- /dev/null +++ b/docs/im/IM-系统设计文档.md @@ -0,0 +1,898 @@ +# IM 系统设计文档 + +> 版本:v1.1
+> 日期:2026-08-11
+> 状态:后续开发基线
+> 需求依据:[IM 产品原型需求文档](IM-产品原型需求文档.md)
+> 关联文档:[IM 产品需求服务端改造分析报告](IM-产品需求服务端改造分析报告.md)、[IM 现有实现与新需求差异对照总结](IM-现有实现与新需求差异对照总结.md)、[IM 服务端多租户改造方案](IM-服务端多租户改造方案.md) + +| 版本 | 日期 | 变更 | +| --- | --- | --- | +| v1.0 | 2026-08-11 | 建立总体架构和业务模块设计 | +| v1.1 | 2026-08-11 | 冻结 MySQL 范围;补齐认证、配额、消息变更、好友/群真源、审核、事务、Outbox、搜索和测试设计 | + +## 1. 文档目的 + +本文作为后续 IM 产品开发的系统设计指导文件,用于统一服务端、客户端和测试团队对产品架构、模块边界、数据模型、接口风格、权限规则、消息事件和开发顺序的理解。 + +本文不替代详细接口文档和数据库 DDL。后续每个业务模块进入开发前,应在本文定义的边界内补充独立的 API 文档、状态机文档、表结构 SQL 和测试用例。 + +本文是产品业务开发的设计基线。关联分析报告用于记录调研过程;表名、唯一约束、状态机或技术选择与本文冲突时,以本文为准。设计发生变化时,必须先修改本文并记录版本,再修改接口和代码。 + +## 2. 设计目标 + +系统面向企业内部沟通与协作场景,核心目标如下: + +1. 支持企业码进入租户,所有账号、好友、群、消息、文件、组织和办公数据均按 `tenant_id` 强隔离。 +2. 复用现有 Tinode 实时通信内核,保留 WebSocket 消息、Topic、Subscription、回执、文件、Push 和 WebRTC 信令能力。 +3. 在实时内核外新增产品业务层,承载好友关系、群治理、收藏、搜索、组织、会议、日报、月报、实名、青少年、举报和反诈等业务。 +4. 服务端必须成为权限和状态机的唯一可信执行方,客户端只负责交互展示和本地体验优化。 +5. 所有敏感动作具备幂等、防重、审计和可测试的错误码。 +6. 支持 iOS、Android 多端一致同步,网络断开后可增量恢复。 + +### 2.1 首期技术范围 + +1. 产品版首期只支持 MySQL 8,共享服务、共享数据库,通过 `tenant_id` 隔离租户。 +2. PostgreSQL、MongoDB 和 RethinkDB 不在首期产品业务支持范围内。启用产品模块但 Adapter 未实现完整租户业务接口时,服务必须拒绝启动,不能静默退回无租户接口。 +3. 实时消息继续使用现有 Tinode WebSocket 协议;事务型产品业务使用 HTTP API。 +4. 搜索首期使用 MySQL,达到第 11.3 节定义的切换条件后再引入独立搜索引擎。 + +## 3. 总体架构 + +系统采用“实时通信内核 + 产品业务服务 + 异步事件”的分层架构。 + +```mermaid +flowchart TB + APP["iOS / Android 客户端"] + + subgraph Gateway["接入层"] + WS["WebSocket 实时协议"] + API["HTTP /v1/im/* 业务 API"] + FILE["HTTP 文件上传下载"] + end + + subgraph Core["通信内核"] + SESSION["Session"] + HUB["Hub / Topic Actor"] + STORE["Tenant Store"] + CALL["WebRTC Call Signaling"] + end + + subgraph Product["产品业务层"] + POLICY["策略引擎"] + REL["好友与黑名单"] + GROUP["群治理"] + FAV["收藏与转发"] + SEARCH["搜索索引"] + ORG["组织与工作台"] + SAFE["实名 / 青少年 / 举报 / 反诈"] + end + + subgraph Infra["基础设施"] + DB["MySQL"] + CACHE["内存缓存 / 持久化缓存"] + OUTBOX["Outbox 异步事件"] + PUSH["Push / 短信"] + MEDIA["对象存储"] + PLUGIN["审核 / 风控插件"] + end + + APP --> WS + APP --> API + APP --> FILE + WS --> SESSION --> HUB --> STORE --> DB + API --> Product --> DB + FILE --> MEDIA + HUB --> CALL + HUB --> POLICY + POLICY --> REL + POLICY --> GROUP + POLICY --> SAFE + Product --> OUTBOX --> PUSH + OUTBOX --> HUB + POLICY --> PLUGIN +``` + +## 4. 核心分层职责 + +| 层 | 职责 | 禁止事项 | +| --- | --- | --- | +| 接入层 | 解析 Token、Session、文件认证和租户上下文;做基础参数校验 | 不从客户端自报字段信任内部 `tenant_id` | +| 通信内核 | 维护连接、Topic 路由、消息顺序、广播、回执、Push 触发和通话信令 | 不承载复杂审批、报表、组织树等事务业务 | +| 产品策略层 | 发消息前校验好友、黑名单、群禁言、青少年、文件权限和内容风险 | 不把策略只放客户端 | +| 业务服务层 | 处理好友申请、群审批、公告、收藏、组织、会议、日报、举报等 CRUD 和状态机 | 不把主数据塞进 Topic Public/Aux 任意 JSON | +| 存储层 | 所有业务表带 `tenant_id`,更新删除必须带租户条件 | 不保留无租户业务查询入口 | +| 异步层 | 系统消息、Push、搜索索引、审核任务、定时提醒和审计导出 | 不在 Topic 事件循环中执行耗时远程调用;核心审计记录仍在业务事务内落库 | + +## 5. 多租户基础规则 + +多租户是本系统的底座,后续所有业务模块必须遵守: + +1. 首次 `hi.tenant` 使用企业码解析租户,Session 固定 `TenantID`。 +2. Token、认证记录、设备、Push、文件和插件调用均携带服务端可信 `TenantID`。 +3. 普通业务 Store 只能通过租户作用域访问,禁止全局查用户、Topic、消息、文件。 +4. 新增租户业务表必须包含 `tenant_id BIGINT NOT NULL`。平台级表和租户根表 `im_tenant` 例外;例外表必须在 DDL 评审中明确说明作用域。 +5. 唯一索引优先使用 `(tenant_id, business_key)`。 +6. 子表外键优先使用 `(tenant_id, parent_id)` 复合约束。 +7. 跨租户通信首期禁止;搜索、加好友、入群、转发和文件访问均限制在当前租户。 +8. 越权访问统一返回 NotFound/Denied,不泄露目标对象是否存在于其他租户。 + +### 5.1 租户配置与配额 + +`im_tenant_config` 是租户配额的配置真源,首期使用以下字段: + +| 字段 | 含义 | `0` 的含义 | +| --- | --- | --- | +| `max_users` | 租户有效用户数上限 | 不限制 | +| `max_groups` | 租户有效群数上限 | 不限制 | +| `max_group_members` | 单群有效成员数上限 | 不限制 | + +配额必须由服务端在事务内强制执行: + +1. 注册或启用用户时锁定租户配置记录,统计有效用户后再创建账号。 +2. 创建群时锁定租户配置记录,统计有效群后再创建 Topic 和群业务数据。 +3. 邀请、扫码入群或审批入群时锁定目标群业务记录,统计有效成员并校验 `max_group_members` 后再写成员关系和 Subscription。 +4. 配额检查和资源创建必须在同一事务中完成,禁止先查询、提交后再创建。 +5. 超限返回稳定错误码,并记录 `tenant_id`、当前用量、限制值和操作类型;响应不得泄露其他租户信息。 + +## 6. 通道与接口风格 + +### 6.1 WebSocket 实时协议 + +继续使用现有 Tinode 协议承载以下实时能力: + +| 能力 | 协议方向 | 说明 | +| --- | --- | --- | +| 登录、注册、Token 登录 | client -> server | 已租户化,继续复用 | +| 发消息 | `pub` | 增加产品策略校验、消息类型和客户端幂等 ID | +| 拉消息、拉元数据 | `get` | 保留原序号查询;复杂搜索走 HTTP | +| 已读、送达、输入中 | `note` | 保留并增加隐私和节流规则 | +| 订阅/退订 Topic | `sub` / `leave` | 由好友、群审批等业务控制是否允许 | +| 通话信令 | `note` 或现有 call 信令 | 增加 `call_id`、通话类型和结束原因 | +| 系统消息 | 服务端发布的 `system` 类型消息 | 个人事件进入服务端管理的系统通知 Topic;群内事件进入对应群 Topic;普通用户不能发布该类型 | + +### 6.2 HTTP 业务 API + +新增业务 API 使用 `/v1/im/*` 前缀,按模块划分: + +```text +/v1/im/friends/* +/v1/im/groups/* +/v1/im/favorites/* +/v1/im/search/* +/v1/im/org/* +/v1/im/meetings/* +/v1/im/reports/* +/v1/im/profile/* +/v1/im/settings/* +/v1/im/security/* +/v1/im/reports-abuse/* +/v1/im/feedback/* +``` + +HTTP API 通用规则: + +1. HTTP 请求使用 `Authorization: Bearer `;统一认证中间件校验 Token 后生成只读 Principal,并从中获取 `tenant_id` 和 `uid`。请求参数中的同名字段不参与授权。 +2. 写接口必须支持幂等,使用 `Idempotency-Key` 或业务唯一键。HTTP 幂等键作用域固定为 `tenant_id + uid + method + route + key`;相同键但请求体摘要不同返回冲突错误。 +3. 列表接口统一使用游标分页,不使用不稳定的大 offset。 +4. 错误响应使用稳定业务错误码,客户端不解析数据库错误。 +5. 敏感接口记录审计日志。 + +### 6.3 文件 API + +继续复用现有文件上传下载入口,但必须满足: + +1. 文件记录带 `tenant_id`。 +2. 上传路径使用 `tenants/{tenant_id}/yyyy/mm/{file_id}`。 +3. 下载前校验文件上传者或关联消息/Topic 读取权限。 +4. 群文件、收藏文件、举报证据和反馈附件复用同一文件基础设施,但业务用途单独记录。 +5. 客户端不可见内部 `location`。 + +## 7. 核心领域与认证设计 + +### 7.1 已有通信模型 + +| 模型 | 继续复用 | 新增约束 | +| --- | --- | --- | +| User | 用户账号和基础资料 | 租户归属、实名状态、隐私设置、青少年状态 | +| Topic | 单聊、群聊、系统 Topic、收藏 Topic | 租户归属、产品业务类型、群治理策略 | +| Subscription | 用户与 Topic 的关系和 ACL | 好友/群状态不能只靠 ACL 表达 | +| Message | 话题内顺序消息 | 消息类型、幂等 ID、审核状态、搜索索引 | +| FileDef | 上传文件元数据 | 租户归属、业务用途、访问授权、扫描状态 | + +### 7.2 新增产品模型 + +| 模块 | 目标表 | 主要职责 | +| --- | --- | --- | +| 好友关系 | `im_friendships`、`im_friend_requests`、`im_user_blocks` | 好友申请、同意、拒绝、删除、拉黑 | +| 用户资料 | `im_user_profiles`、`im_user_privacy_settings` | 昵称、生日、签名、可见性和添加方式 | +| 群治理 | `im_group_settings`、`im_group_join_requests`、`im_group_announcements`、`im_group_mutes`、`im_group_admin_logs` | 入群审批、公告、禁言、管理员、转让群主 | +| 群文件 | `im_group_files` | 群文件目录、搜索、删除权限 | +| 收藏 | `im_favorites` | 收藏去重、分类、来源、搜索和分享 | +| 搜索 | `im_message_index` 或外部搜索索引 | 关键词、类型、附件、链接、图片、视频检索 | +| 通话 | `im_call_records`、`im_call_participants` | 通话记录、状态、时长、结束原因 | +| 组织 | `im_org_units`、`im_org_members` | 部门树、成员、岗位、数据范围 | +| 工作台 | `im_workbench_apps`、`im_workbench_user_apps` | 企业应用入口和权限 | +| 会议 | `im_meetings`、`im_meeting_participants` | 会议通知、确认状态、提醒和取消 | +| 日报月报 | `im_reports`、`im_report_read_states` | 日报/月报填写、提交、查看和统计 | +| 总经理信箱 | `im_manager_mailbox` | 意见提交、匿名规则、处理状态 | +| 实名认证 | `im_realname_verifications` | 实名信息加密、审核状态、脱敏展示 | +| 青少年模式 | `im_youth_mode_settings` | 启用状态、PIN 哈希、策略版本 | +| 举报反馈 | `im_abuse_reports`、`im_feedbacks`、`im_evidence_files` | 举报、证据、反馈和处理状态 | +| 审计 | `im_audit_logs` | 敏感操作追加日志 | +| 异步事件 | `im_outbox_events` | 系统消息、Push、搜索索引、定时任务 | + +新增以下基础模型,避免各业务模块重复实现认证、会话设置和消息变更: + +| 模块 | 表 | 主要职责 | +| --- | --- | --- | +| 账号安全 | `im_user_security` | `auth_version`、密码更新时间、登录失败和锁定状态 | +| 短信验证 | `im_verification_codes` | 验证码用途、目标号码哈希、有效期、错误次数和发送频控 | +| 二次验证 | `im_auth_challenges` | 敏感操作挑战、一次性票据哈希、绑定操作和过期时间 | +| 登录设备 | `im_login_sessions` | 设备会话、Token 标识、最近活动和撤销状态 | +| 会话设置 | `im_conversation_settings` | 置顶、免打扰、隐藏和用户侧会话状态 | +| 群成员 | `im_group_members` | 群成员业务状态、角色、入群来源和操作者 | +| 消息版本 | `im_message_revisions` | 编辑前内容、版本、操作者和变更原因 | + +### 7.3 企业码与登录流程 + +1. 未认证客户端先通过 `hi.tenant` 提交企业码;服务端只按 `im_tenant.code` 查找启用租户并把 `TenantID` 固定到 Session。 +2. 企业不存在、已停用或不允许登录时返回统一企业不可用错误,不能暴露内部状态和数据库信息。 +3. 后续注册、登录、验证码和找回密码全部使用该服务端租户上下文;不接受客户端在业务请求中重新指定租户。 +4. HTTP 场景使用 `/v1/im/tenants/resolve` 完成企业码解析,只返回公开品牌信息和服务地址。首期不设计 `tenant_ticket`;注册、登录等未认证请求由服务端根据企业码重新解析租户,认证成功后只信任正式用户 Token。 +5. 用户 Token 必须包含可信 `tenant_id`、`uid`、`auth_version`、签发时间和过期时间。Token 租户与当前接入租户不一致时拒绝认证。 + +### 7.4 验证码、密码重置和二次验证 + +验证码按 `register`、`login`、`reset_password`、`sensitive_action` 用途隔离,相同验证码不能跨用途使用。数据库只保存验证码强哈希和手机号规范化哈希,明文只在发送时短暂存在。 + +密码重置必须在同一事务中完成: + +1. 消费有效验证码或二次验证挑战。 +2. 更新密码强哈希和密码更新时间。 +3. 递增 `im_user_security.auth_version`。 +4. 撤销该用户现有 `im_login_sessions`。 +5. 写安全审计和 Outbox 通知。 + +二次验证成功后签发一次性操作票据。票据必须绑定 `tenant_id + uid + action + target + expires_at`,只保存票据哈希,成功使用后立即失效。青少年模式独立密码不与登录密码共用,哈希保存在 `im_youth_mode_settings`。 + +### 7.5 认证安全约束 + +1. 用户名和手机号唯一约束均包含 `tenant_id`。 +2. 验证码发送和验证按租户、手机号、IP、设备分别限频。 +3. 登录错误使用统一提示;服务端仍按账号、IP 和设备记录失败次数并执行指数退避或临时锁定。 +4. 新设备登录、密码修改、会话撤销和异常登录通过 Outbox 发送安全通知。 +5. 手机号、验证码、密码、票据、Token 和证件信息不得写入普通日志或消息内容。 + +## 8. 消息体系设计 + +### 8.1 消息类型 + +服务端需要定义稳定的消息类型,不允许各端随意约定 JSON 字段。 + +产品消息协议 v1 使用现有 `pub`,固定字段放在 `head`: + +| 字段 | 规则 | +| --- | --- | +| `x-im-schema` | 产品消息必填,首版固定为 `1` | +| `x-im-type` | 产品消息必填,值必须在下表白名单中 | +| `x-im-client-mid` | 普通客户端消息必填,对应服务端 `client_msg_id` | +| `x-im-reply-to` | 引用消息时填写同 Topic 的 SeqId | +| `x-im-forward-mode` | 转发时为 `single` 或 `merged` | +| `x-im-risk-state` | 仅服务端生成;客户端提交时删除并记录安全日志 | + +`content` 只保存对应消息类型的业务内容。每种类型进入开发前必须冻结字段、长度、必填条件和兼容规则;客户端未知字段应忽略,未知消息类型显示安全占位而不能崩溃。 + +| 类型 | 用途 | 关键字段 | +| --- | --- | --- | +| `text` | 普通文本和 Unicode 表情 | `text`、`mentions` | +| `image` | 图片消息 | `file_id`、`width`、`height`、`thumb_file_id` | +| `voice` | 语音消息 | `file_id`、`duration_ms`、`waveform` | +| `video` | 视频消息 | `file_id`、`duration_ms`、`thumb_file_id` | +| `file` | 普通文件 | `file_id`、`name`、`size`、`mime` | +| `contact_card` | 名片 | `user_id`、`display_name_snapshot`、`avatar_snapshot` | +| `call` | 通话结果消息 | `call_id`、`call_type`、`result`、`duration_sec` | +| `system` | 系统提示 | `event_type`、`actor_id`、`target_ids`、`params` | +| `forward_single` | 单条转发 | `source_message_ref`、`snapshot` | +| `forward_merged` | 合并转发 | `summary`、`items`、`source_refs` | + +### 8.2 消息幂等 + +客户端发消息必须带不超过 64 字符的 `client_msg_id`,推荐使用 UUID/ULID。服务端在当前租户和发送人范围内保证唯一,不把 Topic 放入唯一键,避免客户端误把同一个发送请求投递到多个会话: + +```text +UNIQUE(tenant_id, from_uid, client_msg_id) +``` + +断线重试时,如果服务端发现重复 `client_msg_id`,返回原消息的 Topic、`seq_id`、状态和响应摘要,不能再次插入消息。服务端生成的系统消息也必须生成稳定幂等 ID,并以 Outbox 事件 ID 作为去重来源。 + +### 8.3 发消息前策略校验 + +每条消息持久化前执行统一策略链: + +1. Session 租户和 Topic 租户一致。 +2. 发送人是同租户有效用户。 +3. Topic 存在且未删除。 +4. Subscription 具备写权限。 +5. 单聊检查好友关系、黑名单、删除状态和隐私策略。 +6. 群聊检查禁言、全员禁言、群状态和成员身份。 +7. 文件消息检查文件归属、扫描状态和目标 Topic 权限。 +8. 青少年模式和反诈策略检查。 +9. 内容审核通过或进入待审核策略。 + +### 8.4 消息编辑、引用、撤回与删除 + +消息在 Topic 中的 `seq_id` 和原发送人不可变。产品消息增加 `message_type`、`client_msg_id`、`version`、`moderation_state`、`recalled_at` 和 `recalled_by` 等明确字段,不能依赖客户端任意 JSON 决定服务端状态。 + +| 操作 | 服务端规则 | 多端同步 | +| --- | --- | --- | +| 引用 | 保存 `topic + seq_id` 引用和发送时的安全摘要;禁止跨租户引用 | 新消息正常广播;原消息删除后仍显示受控摘要 | +| 编辑 | 仅原发送人、允许类型、规定时限内可编辑;事务内递增 `version` 并写 `im_message_revisions` | 广播带版本号的编辑事件,旧版本事件不得覆盖新版本 | +| 全员撤回 | 校验发送人或群管理员权限及撤回时限;保留受保护审计内容,普通查询只返回撤回占位 | 广播撤回事件,收藏、搜索和引用按保留策略更新 | +| 仅自己删除 | 使用现有用户侧删除日志,不改变其他成员可见性 | 只同步当前用户的其他设备 | +| 批量删除/撤回 | 限制单次数量,逐项授权,返回逐项结果 | 每个成功项产生独立事件和审计记录 | + +编辑、撤回与索引更新必须使用乐观版本控制。请求携带期望 `version`,版本不一致返回冲突错误,客户端重新拉取当前消息状态。 + +### 8.5 内容审核时序 + +发送链路先执行本地权限、限频和硬规则,再按租户及消息类型选择以下一种审核模式: + +| 模式 | 处理方式 | 适用场景 | +| --- | --- | --- | +| `sync_block` | 在短超时内同步审核,通过后持久化和广播;超时按租户配置拒绝或降级 | 高风险文本、小请求 | +| `async_hold` | 以 `pending_review` 持久化但不广播;Outbox 审核通过后发布,拒绝后标记 `rejected` | 文件、图片和高风险内容 | +| `async_post_audit` | 先发布,异步审核;命中后执行系统撤回并审计 | 租户明确允许的低风险内容 | + +状态转换固定为: + +```text +pending_review -> approved -> published +pending_review -> rejected +published -> recalled_by_review +``` + +客户端提交的审核状态一律忽略。审核 Worker 必须幂等,同一消息只能完成一次终态转换。 + +## 9. 好友与黑名单设计 + +好友关系是单聊产品状态的真源,不直接用 P2P Topic 是否存在表示好友。 + +### 9.1 好友申请与关系状态 + +```mermaid +stateDiagram-v2 + [*] --> none + none --> requested: 发起申请 + requested --> accepted: 通过 + requested --> rejected: 拒绝 + requested --> expired: 过期 + accepted --> friends: 事务创建好友关系 + friends --> deleted: 删除好友关系 + deleted --> requested: 重新申请 + rejected --> requested: 重新申请 +``` + +`im_friend_requests` 记录申请状态,`im_friendships` 记录双方好友关系,`im_user_blocks` 记录单向拉黑。拉黑不修改好友关系状态;取消拉黑只删除当前用户的黑名单记录,不自动恢复已经删除的好友关系。 + +### 9.2 服务端规则 + +1. 申请、同意、拒绝、删除、拉黑必须幂等。 +2. 好友申请保存验证留言、来源、过期时间和处理人。 +3. 拉黑是单向行为,唯一约束为 `(tenant_id, blocker_id, blocked_id)`;被拉黑人发消息、通话、名片、查看资料时按策略阻断。 +4. 删除好友后历史消息默认保留,但双方不能继续发送好友消息。 +5. 好友资料字段由关系和隐私设置共同决定。 +6. 接受申请必须在一个事务内更新申请、创建好友关系、创建或激活 P2P Topic/Subscription,并写入 Outbox。 +7. 单聊策略按“双方账号有效 -> 好友关系有效 -> 任一方向黑名单 -> 隐私/青少年/风控”的顺序判定。 + +## 10. 群治理设计 + +现有 Group Topic 继续作为群消息容器。`im_group_settings` 与 `im_group_members` 是群业务状态真源;Topic、Subscription 和 ACL 是通信内核投影。所有成员和角色变化必须通过产品业务接口完成,并在同一 MySQL 事务内同步业务真源与通信投影。 + +### 10.1 群角色 + +| 角色 | 业务真源 | 通信投影 | 权限 | +| --- | --- | --- | --- | +| 群主 | `im_group_settings.owner_user_id` + `im_group_members.role=owner` | Topic Owner + Owner ACL | 全部群管理、转让、解散 | +| 管理员 | `im_group_members.role=admin` | Admin ACL | 审批、公告、禁言、成员管理 | +| 普通成员 | `im_group_members.role=member` | 有效 Subscription | 收发消息、查看公告和群文件 | +| 游客/待审批 | `im_group_join_requests` | 无 Subscription | 不具备群消息读写权限 | + +禁止客户端直接修改群 Topic Owner 或管理员 ACL。服务启动或定时一致性任务发现业务真源与通信投影不一致时,记录告警并以业务真源修复投影。 + +### 10.2 群能力 + +| 能力 | 设计 | +| --- | --- | +| 创建群 | 同一事务创建 Topic、群设置、群主成员记录、Subscription、ACL、审计和 Outbox | +| 入群邀请 | 写邀请/申请记录,审批通过后在同一事务写成员记录和 Subscription | +| 入群验证 | 群设置控制是否需要审批 | +| 群公告 | 公告表保存版本,群内发送系统消息 | +| 群二维码 | 签名短 Token,包含群 ID、租户、过期时间和用途 | +| 管理员 | 更新 `im_group_members.role`,同一事务同步 ACL | +| 禁言 | 全员禁言和成员禁言都在发消息前强制检查 | +| 群文件 | 文件消息成功后进入群文件索引,按群权限查询 | +| 转让群主 | 二次确认;同一事务更新 owner、双方角色、Topic Owner/ACL、审计和 Outbox | +| 解散群聊 | 标记群状态、踢出成员、停用二维码和审批入口 | + +## 11. 收藏、转发与搜索设计 + +### 11.1 收藏 + +收藏使用独立 `im_favorites` 表作为查询和去重真源,不仅依赖 `slf` Topic。 + +收藏记录需要保存: + +1. 租户、收藏人、来源 Topic、来源 SeqId、来源消息 ID。 +2. 消息类型、摘要、文件 ID、发送人、发送时间。 +3. 收藏时间、去重键和删除状态。 +4. 原消息删除后的可见策略。 + +### 11.2 转发 + +逐条转发和合并转发都必须重新执行目标会话权限和文件权限。 + +| 转发类型 | 规则 | +| --- | --- | +| 逐条转发 | 每条源消息生成一条新消息;保持顺序;每条使用新 `client_msg_id` | +| 合并转发 | 生成不可变快照;限制条数、总大小和敏感字段;支持点击查看详情 | +| 文件转发 | 源文件和目标 Topic 必须同租户;必要时创建新的引用关系 | + +### 11.3 搜索 + +搜索分三类: + +1. 用户和群搜索:使用租户内规范化 Tags,并执行隐私策略。 +2. 聊天记录搜索:`im_message_index` 保存 `tenant_id`、Topic、SeqId、消息类型、发送人、时间、规范化检索文本和资源元数据,返回前二次校验 ACL。 +3. 文件/图片/视频/链接搜索:依赖消息类型索引和文件元数据索引。 + +首期固定使用 MySQL 8:结构化条件使用 B-Tree 复合索引,消息正文使用独立 `im_message_index.search_text` 和适合中文的 FULLTEXT/ngram 索引。分页游标使用稳定的 `created_at + message_id`,不能使用大 Offset。 + +搜索索引不是授权来源。查询必须先限定 `tenant_id` 和当前用户可读 Topic 集合,返回前再次校验 Topic Read ACL、成员状态、用户侧删除、全员撤回和审核状态。退群、拉黑或权限变更后必须立即阻断查询,即使异步索引尚未删除。 + +出现以下任一情况时重新评估 OpenSearch/Elasticsearch:单租户可检索消息达到千万级、MySQL 搜索 P95 超过 500ms、需要复杂中文相关性/高亮,或索引写入明显影响消息主库。切换时保持 HTTP 搜索契约不变。 + +## 12. 通话设计 + +现有 P2P WebRTC 信令继续复用,新增业务通话记录。 + +### 12.1 通话状态 + +```mermaid +stateDiagram-v2 + [*] --> ringing: 发起呼叫 + ringing --> accepted: 接听 + ringing --> canceled: 发起方取消 + ringing --> rejected: 接收方拒绝 + ringing --> timeout: 超时 + ringing --> busy: 对方忙线 + accepted --> ended: 正常挂断 + accepted --> interrupted: 异常断开 +``` + +### 12.2 关键字段 + +`im_call_records` 至少包含: + +```text +tenant_id, call_id, topic, caller_id, callee_id, call_type, +state, start_at, accepted_at, ended_at, duration_sec, +end_reason, created_at, updated_at +``` + +通话结束后生成 `call` 类型消息,双方看到的文案由统一结果码生成。 + +## 13. 企业工作台设计 + +工作台是独立业务域,不能依赖聊天 Topic 保存主数据。 + +| 模块 | 入口 | 通知方式 | +| --- | --- | --- | +| 组织架构 | `/v1/im/org/*` | 组织变更一般不发聊天消息,必要时发系统通知 | +| 会议通知 | `/v1/im/meetings/*` | 创建、变更、取消和提醒通过系统消息 + Push | +| 日报/月报 | `/v1/im/reports/*` | 未提交提醒、提交通知和管理员提醒通过 Outbox | +| 总经理信箱 | `/v1/im/manager-mailbox/*` | 提交成功给本人通知;处理结果按匿名规则通知 | +| 工作台应用 | `/v1/im/workbench/apps` | 返回租户配置和用户权限范围内的入口 | + +组织数据权限用于日报、月报、会议统计和通讯录查询。管理员只能查看权限范围内成员数据。 + +## 14. 个人资料、隐私与青少年模式 + +### 14.1 个人资料 + +基础展示资料可以同步到 User Public,但可索引、可审计或敏感字段应进入独立表。 + +| 字段 | 存储建议 | +| --- | --- | +| 头像、昵称、签名 | User Public + `im_user_profiles` | +| 生日、地区、性别 | `im_user_profiles`,按隐私设置返回 | +| 实名姓名、证件号 | 实名表加密存储,不进入 Public | +| 群昵称 | `im_group_members`,按需投影到 Subscription Private | + +### 14.2 隐私设置 + +隐私设置影响以下入口: + +1. 账号/手机号/二维码/群聊添加好友。 +2. 个人主页字段展示。 +3. 在线状态和最后在线时间展示。 +4. 非好友消息和通话发起。 + +客户端可以隐藏入口,但最终阻断必须在服务端执行。 + +### 14.3 青少年模式 + +青少年模式状态保存在服务端,PIN 使用强哈希,不保存明文。 + +受限能力由租户策略决定,首期建议覆盖: + +1. 添加好友。 +2. 加群和创建群。 +3. 文件传输。 +4. 音视频通话。 +5. 工作台外部链接。 + +### 14.4 会话与通知设置 + +`im_conversation_settings` 以 `(tenant_id, user_id, topic)` 唯一,保存置顶、免打扰、隐藏等用户侧设置。它不改变其他成员的会话状态,也不能替代 Topic ACL。 + +Push 发送前同时检查租户通知策略、用户全局通知设置、会话免打扰、青少年模式和系统权限登记状态。客户端系统通知权限只用于展示引导,服务端保存的是用户偏好和最近一次上报状态,不把客户端上报当作可靠授权依据。 + +## 15. 安全、实名、举报与反诈 + +### 15.1 实名认证 + +实名认证采用独立状态机: + +```text +none -> pending -> approved +none -> pending -> rejected +approved -> appeal_pending -> approved/rejected +``` + +证件号、姓名等敏感字段加密存储,普通查询只返回脱敏状态。 + +### 15.2 反诈策略 + +反诈由“本地规则 + 外部风控插件”共同组成: + +1. 本地规则用于稳定阻断和提示,例如陌生人、敏感词、频繁加好友、异常文件。 +2. 外部插件用于内容审核、风险评分和策略建议。 +3. 插件调用遵守第 8.5 节的三种审核模式、超时和降级规则,不能在 Topic Actor 中执行无界远程等待。 +4. 风险提示和二次确认结果写审计。 + +### 15.3 举报与反馈 + +举报和反馈都按工单处理。 + +| 类型 | 特点 | +| --- | --- | +| 举报 | 绑定被举报对象、会话、消息、原因、证据和处理状态 | +| 意见反馈 | 绑定提交人、设备、版本、问题描述、附件和处理状态 | + +举报证据应固化消息快照和文件引用,防止原消息删除后无法处理。 + +## 16. 系统消息与通知设计 + +需要产生异步副作用的业务状态变化,在同一数据库事务中写入 Outbox,由 Worker 生成系统消息、Push、索引或定时任务。没有异步副作用的更新不要求创建空事件。 + +### 16.1 系统消息格式 + +系统消息固定使用以下结构;业务模块只能登记 `event_type` 和参数 Schema,不能自行定义另一套消息信封: + +```json +{ + "type": "system", + "event_type": "group_join_approved", + "template_version": 1, + "actor_id": "usr...", + "target_ids": ["usr..."], + "params": { + "group_id": "grp...", + "group_name": "项目群" + }, + "dedupe_key": "tenant:1:group:grpxxx:join:request123" +} +``` + +### 16.2 事件类型 + +首期至少定义: + +| 业务 | 事件 | +| --- | --- | +| 账号安全 | 新设备登录、密码修改、实名提醒 | +| 好友 | 新申请、通过、拒绝、删除 | +| 群 | 邀请、申请、审批、公告、禁言、管理员变更、群主转让、解散 | +| 文件 | 群文件上传、文件失效 | +| 通话 | 未接、拒绝、取消、忙线、通话结束 | +| 工作台 | 会议通知、会议变更、日报提醒、月报提醒、信箱处理 | +| 安全 | 反诈提示、举报受理、反馈受理 | + +## 17. 权限模型 + +### 17.1 角色层级 + +| 层级 | 角色 | +| --- | --- | +| 平台 | `platform_admin` | +| 租户 | `tenant_owner`、`tenant_admin` | +| 组织 | 部门负责人、报表管理员 | +| 群 | 群主、管理员、成员 | +| 关系 | 好友、非好友、黑名单 | +| 账号 | 普通用户、实名用户、青少年模式用户 | + +### 17.2 权限判定顺序 + +```text +租户状态 -> 用户状态 -> 业务角色 -> Topic ACL -> 产品策略 -> 风控策略 +``` + +任何一步失败,写操作不得继续。 + +## 18. 数据一致性与异步任务 + +### 18.1 事务边界 + +必须在同一事务中完成: + +1. 好友申请处理和关系状态更新。 +2. 群审批通过和 Subscription 写入。 +3. 群主转让和权限更新。 +4. 消息保存和附件引用建立。 +5. 文件记录从 `uploaded` 切换为 `ready`、附件引用和业务索引写入。 +6. 举报提交和证据引用。 + +业务层不能在 Handler 中连续调用多个独立 Store 方法来模拟事务。`imstore` 必须提供按用例划分的事务方法,例如 `AcceptFriendRequest`、`ApproveGroupJoin`、`TransferGroupOwner` 和 `SaveMessageWithAttachments`,方法内部同时更新产品业务表、Tinode 核心表、审计记录和 Outbox。 + +对象存储不参与 MySQL 事务,文件采用以下状态机: + +```text +pending -> uploaded -> ready +pending/uploaded -> failed +uploaded(超时且无引用) -> orphaned -> deleted +``` + +对象上传成功后,再在 MySQL 事务中把文件切换为 `ready` 并建立业务引用。若数据库事务失败,文件保持 `uploaded`,由带宽受控的租户级垃圾回收任务清理;消息只能引用 `ready` 文件。 + +### 18.2 Outbox + +需要通知、Push、索引或审计的业务写操作,在事务内写入 `im_outbox_events`。异步 Worker 负责: + +1. 发送系统消息。 +2. 推送离线通知。 +3. 更新搜索索引。 +4. 调用外部审核/反诈。 +5. 发送会议、日报、月报定时提醒。 +6. 记录可观测指标。 + +`im_outbox_events` 至少包含: + +```text +id, tenant_id, aggregate_type, aggregate_id, event_type, +payload, dedupe_key, state, available_at, attempts, +locked_by, locked_at, last_error, created_at, processed_at +``` + +Outbox 处理规则: + +1. `dedupe_key` 在租户内唯一,生产事件和消费副作用都必须幂等。 +2. Worker 使用 `SELECT ... FOR UPDATE SKIP LOCKED` 分批领取事件,设置最大批量和租户公平调度,避免单个租户占满队列。 +3. 失败采用有上限的指数退避;超过上限进入 `dead_letter`,触发告警并支持人工重放。 +4. 系统消息通过集群安全的内部发布接口进入目标 Topic,不能直接假设 Outbox Worker 与目标 Topic 位于同一进程。 +5. 标记事件成功前必须确认目标副作用已获得稳定幂等结果;进程崩溃后的重复执行不得产生重复消息或 Push。 +6. 外部审核事件必须按第 8.5 节推进消息审核状态,不能绕过状态机直接广播。 + +## 19. API 与错误码规范 + +### 19.1 成功响应 + +```json +{ + "code": 0, + "data": {}, + "request_id": "..." +} +``` + +### 19.2 错误响应 + +```json +{ + "code": 30001, + "message": "permission denied", + "reason": "GROUP_MUTED", + "request_id": "..." +} +``` + +错误码按模块分段: + +| 范围 | 模块 | +| --- | --- | +| 10000-19999 | 通用、认证、租户 | +| 20000-29999 | 好友、黑名单、隐私 | +| 30000-39999 | 群、公告、群文件 | +| 40000-49999 | 消息、收藏、搜索、转发 | +| 50000-59999 | 文件、媒体、通话 | +| 60000-69999 | 组织、会议、日报、月报 | +| 70000-79999 | 实名、青少年、安全、举报、反馈 | + +HTTP 状态码表达协议层结果,业务 `code` 表达稳定失败原因:参数错误使用 400,未认证使用 401,无权限使用 403,不可见资源使用 404,版本或幂等冲突使用 409,限频使用 429,服务端故障使用 5xx。客户端不得只判断 HTTP 状态而忽略业务 `code`。 + +## 20. 可观测性与审计 + +### 20.1 日志字段 + +认证后的请求日志至少包含: + +```text +request_id, tenant_id, uid, sid, topic, action, result, latency_ms +``` + +手机号、证件号、Token、验证码、文件签名和设备 Push Token 不写明文日志。 + +### 20.2 审计事件 + +必须审计: + +1. 租户、组织和管理员变更。 +2. 登录、密码修改、实名审核和二次验证。 +3. 好友删除、拉黑、黑名单解除。 +4. 群主转让、管理员变更、禁言、解散群。 +5. 文件下载、批量转发、收藏导出。 +6. 举报处理、反馈处理和反诈二次确认。 +7. 平台管理员代操作。 + +## 21. 开发阶段规划 + +### 阶段 0:基础底座收口 + +目标:让后续业务都站在正确地基上。 + +交付: + +1. 多租户核心链路完成并通过双租户隔离测试。 +2. 消息 `client_msg_id` 幂等。 +3. 文件下载资源级授权。 +4. 系统消息结构和 Outbox 基础。 +5. 统一错误码和请求日志。 +6. 产品版明确只支持 MySQL 8,不支持的 Adapter 在启动时快速失败。 +7. `im_tenant_config` 三项配额在注册、建群和入群入口强制生效。 +8. 建立专用业务事务 Store,禁止跨多个 Store 调用拼接关键事务。 + +### 阶段 1:账号、资料、好友和黑名单 + +交付: + +1. 注册、登录、找回密码、二次验证。 +2. `auth_version`、登录设备、Token 撤销和安全通知。 +3. 个人资料、会话设置和隐私设置。 +4. 好友申请、通讯录、主页、删除好友。 +5. 独立黑名单和单聊发送/通话策略。 + +### 阶段 2:群聊产品化 + +交付: + +1. 群列表、创建群、群资料、群头像。 +2. 入群审批、邀请、群二维码。 +3. 群公告、管理员、群主转让。 +4. 全员禁言、成员禁言、群文件。 +5. 群业务真源与 Topic/Subscription/ACL 投影一致性检查。 + +### 阶段 3:消息增强、收藏、搜索和转发 + +交付: + +1. 结构化消息类型。 +2. @成员、引用、编辑、撤回、批量删除及消息版本事件。 +3. 收藏分类和分享。 +4. 聊天记录、文件、图片、视频和链接搜索。 +5. 逐条转发和合并转发。 +6. `sync_block`、`async_hold` 和 `async_post_audit` 审核状态机。 + +### 阶段 4:通话和通知体验 + +交付: + +1. `call_id` 和通话记录。 +2. 通话状态消息。 +3. 忙线、拒绝、未接、超时和异常断开。 +4. Push 文案、免打扰、锁屏隐私。 + +### 阶段 5:企业工作台 + +交付: + +1. 工作台应用配置。 +2. 组织架构。 +3. 会议通知和参会确认。 +4. 日报、月报。 +5. 总经理信箱。 + +### 阶段 6:安全合规 + +交付: + +1. 实名认证。 +2. 青少年模式。 +3. 反诈策略。 +4. 举报和意见反馈。 +5. 审计后台和处置流程。 + +## 22. 测试策略 + +### 22.1 自动化测试 + +每个模块至少包含: + +1. 单元测试:状态机、权限判断、错误码。 +2. Store 测试:租户隔离、唯一约束、分页、软删除。 +3. API 测试:参数校验、幂等、越权、并发。 +4. WebSocket 测试:消息顺序、回执、离线重连、系统消息。 +5. 安全测试:跨租户、跨用户、跨群、文件越权和枚举。 +6. 事务测试:在每个数据库写入点注入失败,验证业务真源、通信投影、审计和 Outbox 不出现部分提交。 +7. Worker 测试:重复消费、锁超时、进程崩溃、指数退避、死信和人工重放。 +8. Schema 测试:MySQL 初始化 DDL、升级 DDL 和 Adapter SQL 保持一致,唯一约束与外键真实生效。 +9. 性能测试:大群广播、并发发消息、重连风暴、文件上传下载、中文搜索和 Outbox 积压恢复。 + +性能测试不能只记录平均值。每次发布前根据部署规模给出 P95/P99 延迟、错误率、吞吐、数据库连接和队列积压报告;未确定正式 SLO 前,以当前稳定版本基线为回归门槛。 + +### 22.2 重点验收场景 + +| 场景 | 预期 | +| --- | --- | +| 两个租户使用相同手机号注册 | 均成功,互不影响 | +| A 租户尝试搜索 B 租户用户 | 返回空或通用 NotFound | +| 被拉黑用户发送消息 | 不进入对方会话,返回稳定失败原因 | +| 禁言成员发群消息 | 服务端拒绝,其他成员不可见 | +| 文件 URL 被其他用户重放 | 无权限返回 403/404 | +| 断线重试同一消息 | 不重复入库,返回原消息状态 | +| 同一发送人把相同 `client_msg_id` 投递到不同 Topic | 第二次返回首次投递结果,不写入另一 Topic | +| 并发注册超过 `max_users` | 成功数不超过配额,失败请求返回同一超限错误 | +| 并发审批入群超过 `max_group_members` | 最终有效成员数不超过配额 | +| 群主转让并发提交 | 最终只有一个成功,审计完整 | +| 群主转让在任一写入点失败 | owner、成员角色和 Topic ACL 全部回滚 | +| `async_hold` 消息等待审核 | 审核前其他成员不可见,通过后只广播一次 | +| Outbox 发送成功但标记前进程崩溃 | 重启重放不产生重复系统消息或 Push | +| 用户退群后搜索旧群消息 | 即使索引尚在也不返回结果 | +| 举报消息后原消息被撤回 | 举报证据仍可处理 | + +## 23. 开发约束 + +1. 新业务表一律以 `im_` 前缀命名。 +2. 所有租户业务表必须带 `tenant_id`;平台级表和 `im_tenant` 等根表例外,例外必须经过 DDL 评审。 +3. 新接口不使用无租户 Store。 +4. 不在 Topic Actor 中执行慢查询、外部 HTTP 或复杂报表统计。 +5. 不把敏感字段放入 User Public、Topic Public、普通消息 JSON 或日志。 +6. 新增消息类型必须先定义 schema,再实现客户端渲染。 +7. 所有危险操作必须有二次确认、幂等和审计。 +8. 所有批量操作必须限制数量并返回逐项结果。 +9. 所有列表必须分页。 +10. 所有外部服务调用必须有超时、重试上限和降级策略。 +11. 关键业务状态变化必须通过专用事务 Store 完成,不允许 Handler 直接修改 Topic ACL 绕过业务真源。 +12. 产品模块只允许在 `TenantBusinessReady()` 为真且数据库版本满足要求时启动。 + +## 24. 待产品确认事项 + +以下问题会影响接口和表结构,开发前需要产品或业务方确认: + +对应阶段冻结接口和 DDL 前必须确认本节事项。在产品未给出结论时,可以按本文其他章节声明的默认行为开发内部能力,但不得将未确认交互作为正式验收口径。 + +1. 删除好友后是否同时删除双方会话,还是仅禁止继续发送。 +2. 拉黑后是否允许被拉黑人看到历史消息和个人资料。 +3. 群聊已读回执是否展示具体已读成员,是否只对管理员开放。 +4. 消息撤回时限和管理员撤回范围。 +5. 合并转发是否允许包含已撤回、已删除或涉敏消息。 +6. 群文件是否等同于群内所有文件消息,还是独立上传目录。 +7. 实名认证是否强制,未实名能否继续登录、聊天、加好友、入群。 +8. 青少年模式限制范围和默认策略。 +9. 总经理信箱是否匿名,以及哪些角色可以查看真实提交人。 +10. 日报/月报是否支持草稿、补交、撤回和修改。 + +## 25. 总结 + +后续开发应以现有 Tinode 服务端作为实时通信内核,在其外建设清晰的产品业务层。核心原则是:聊天实时链路保持轻,业务状态机走 HTTP 和数据库事务,状态变化通过系统消息和 Push 回到聊天体验中。 + +多租户、消息幂等、文件授权、系统消息和审计是所有功能之前的基础工程。好友、群治理、收藏搜索、通话、工作台和安全合规应按阶段推进,每个阶段都必须同时完成数据模型、接口、权限、通知和测试闭环。 diff --git a/docs/im/images/@人员—完成.png b/docs/im/images/@人员—完成.png new file mode 100644 index 0000000000000000000000000000000000000000..ff0cf07928e8d30be0459e0f060e235604222ee3 GIT binary patch literal 110215 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 literal 0 HcmV?d00001 diff --git a/docs/im/images/@人员—选择提醒人.png b/docs/im/images/@人员—选择提醒人.png new file mode 100644 index 0000000000000000000000000000000000000000..e15a9e2feb16618a604f90be301ffb585ba223a0 GIT binary patch literal 94128 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⋘d--y{p5neQZ7!|vDR{x0ZKAY|&U3T>Dy4V1TU!q5W zFQ;_=q)@QLg+k(2PxekKsB+8uV9!Hm2WE3bqf};>Cms{aK^2VEow9m8PM+sCR5V1C zdB$X;)Jh5sk~O4(=lV6ZCo<+bAOLPe;|Jo8@14mn($4|y5WL<0-1slYYk}UygM)ND>f2jaJ~pd zr; z2D-le$b#_{pyK=eHMp5R$d+<#hin=M2+NpBtEez-7z^*YpMY3Fq|n7I^GBOg{!efD z4!ysTWyBfg>1pR&@Rp;YZ!+UjzO*zkCzkhd-MoHrs&|J#a*iFFV)PTJua#J5XEF_mf#g*}Q8bolN1$%rC)Y=<|Q0 z^F~!ImgCB9nK+;r)AQlazK!$|$1~x?L);;@)xpqN#5M;P1Ix)~%E-Tb3HcS?dls8D zH_={I7CJlX71z`5-I4t2dYQ9E-mJ*5sZ)VAy#Du(&XvW7oXESb)i0#e)mx+!EXr6I z_BEQFDxVvQjt&KZTd%QQd)|;KXfCXOV6xG75BEpR{P_w+UN0S#X1~JSax*>1<}OJ zn_dr#zdHo}`$30^*Z&b#n^41ZutFcY_*Edbeo~={wMzsvCFSZHBiR0X)`B%ZH+FaF zLmcqfaVXsAQ6lIv-S^pdMx@W&Y5pzda2ce{k<3F;+oA7SBDU-pm5x8KQ?lRu$g1u$ zzjI8>tVn{E$*VRMM=w&{WZZj-x>Oxg7BF;hPfa{{IJDYx$HDC~Ca6TSMALSS(;`j3 zei!MPBC_f`%zpk<>OzF!@l|=?-hQB4cM%NVy?**Uv%P$|)tT`?k%^Q?wppEyH&OJi1#Z$f1;y zwj9?s*ft0KFO4HL@pmq-Me;a4cteej8?XT-+1W#lEyh0*i=Kqj8$VOy^T-=^QLEs= zL?68Ov{z_wZ7)c`LBpJjo zPiStdi)*B3y-rt|j&UKf-LHO@JP>E_Q;=OcD{1kZYV(yOXX&|^ja^KbXBf>DdEr`w z==YHq%6iG`5x*|c6)b>z4W>)Yaja@>DrZX*9nNv(i1gRykxWzMXU;Xsvf zG=H?~m4q)fVz36@x-Zw6<{T0gC)}|63a(=if)k?|Jcofdz>3kSUjQ;>-%sxkzW6Z% zJk^&E5yXZMeNz|B`efHmo{?=}-i+^m;+Mqxdh&K1!=vq5eMoo$J8{Jx!dHs!r~ATf z^X#Sb0qX;hq$~OMoA(^0e`OCX=kM+Y1;Thf3GWkzbd(hBQtrI{UM0{!D3s;6vhSq` zU*+eIvsHRCL#G|ak(FM)uU-u;cb> zqSJTk6fs6{Z0D=0u!%ZwwBs9;0Cpa;S5GU}umoT}!k!KNYbrz1EbE*8y`aSCo@ZNP zkuLBpE8LAYdTEi<@I)FD-Kl_F&kuJ)bL|}oXEoMg)ZJq5u-Q24akk0YdpH*t`q{vi z_J0~!2lTrB`7Gm{@Y3UhOIJ{fI9J?c+E8{uyg!kszrCnW=vKcck&*p#^$CQS4{ah^ zkl)|?oDWs+n2S9!Hr&-no^{V^f8sGByCT)}`c8L{pyflpqI~1r>ho8Ymkt&6GfNWA zNpU6YFHRUMT-E4tSY|V&PpX%JYwI_Afx5*x79yOTSvq6XVqx@JLfG`D|3LYjE2kpE z{}zgVep!x>94b6HWjx9O!V__n`EM_owUTOl*0)pK3-EIb4Nlu4CnAG~1D(O=`LD3} z>!_u(HE|XLv%T5)#*vjwRN9Q)|MGrSyGNebs*|)O?tDQa(o@jjYhqw$H02VPSr$vB zUO2-AaZB=dCPqS3rprPRDt?Veu0hfc%#CQE%XQhT>+R(@F#fscg5jg1e6su{VrE$6 zD0Ah(h|^zsW%23^a#O6P0h<9-(Z4p^rtIc|7LR7=%C~PLcu0NU`_nCx z3^XOc61m^}_;L4AeDBsB`)kI|I-F8f;&}ABeDw0ny;}Tc?UA294S;RKDtG3}JeC`- z*rhN?Z>PVBEbCtP{??ImK&-SYIh&j@?*MOVpTjO~Rw-;2#Q#KoAh0)$wJ&nvrQZ*( zgcpWxtfI3V>G(V~>Dq-uA;6fktq z^*|{8quRaL?ylsl&$&2t=^6V_k&KFofu-MXpE{GAsz}RWdx7^j4GZ#ODx}*sc^c~V zD+?7@tDAyg3pq30CaqV6xab`k>$>Q6A}wCD=CVxmf;e{ly%Qe#F0dn#tu?;wRB4ea zF}+Y8maVz=dy`LWL#sD8*v9jIz;n+EXI-o9mGtnP*Xz~^t{?3pJ#3)l;X5DS$B`VL zm4a`OoQKWL5(EYA+}qD%AO38TK=rVI3t+htVl6Fe#sGRjd8>jx=#*cz$%}-HfeOTr zQakzymhgJD5xTYSkUEvwOIL*F^RM{NInAyf?6yww8hmHWpo(Tvc3@jC>%}B|NCM4o zQBPa-E;=(}eT*S_d3V z?BWtN6DAbwd;hCS4$rTB?qw_Uq2*-D{fgoomJRUa%z(K(CxIW%*@PvsC*6B zUy_`%;ZQh$qwB7QE~CPADmIMyEbqD};hnMkYN#z<9Ck;xl`)UneRp0JvP_#k)=3ZS%3k-X}{KnT{4GA+HgL^Gyda5?95neNLfX z*`=9OwIF&o_pSm9UNu1kP`0v9ELTM+Jgyyn!f-* z@PW)BtLN3L)3k}lcd>UqaY)t+oAoNElOE63lX7xBWk$As#qW6Ye}V1^2pfTGJk)Xz+fM!+G}gA^mD zoj9@sWQZDt@i(pzG~xul7DX8n4ZT zpLMe;%+FA2!kZ@#pkuW_c$Q%YL#GuH^k}WJQgz6$K82vk?K(&)h@4Rzif5JOc=lGZ z7ae^8Isy(B0!<;rFVnlxzo<*f!!1qYAZ6#0%fu->qPpmH*nVr%t8+Xd=~c?c=q7_- zpu%JSh)o{^axthm57*u%bjo%FpKm*(%f}gooj*$>xcg$Sl1(h2icedPN!=O3`c*|Y zvV>i`AoK>G-}S!LTuLZbjE*X`yE{YN>v_leqt!&JL-vbnHB+j$7&AFQ-Ri!X(U{Kf z_$Pty?&rEeAZW!H!LR&2Skh~)qR(F>Qfn&G+?C9*Lh<$w_BnY?V5QIN6mudub)P2i zi@%T_pX^303x49qSE`58Z9Y$ZTOP9BdfYr7iYap9#!@y zFW|=i{Uia~DM|fR8puDvJGrZx*qmE$Ogk4WmaD0nxi70i=p92m3U2fKa9e1aPC&v< z#eLq%U^o_SrO->{V&uJN+_kJjy5I6XHJJZLGyFl&hz(`jQS@_IuBJGH{F+CD~gM@!_@Rdcx;35I@ir(7Nm+~ zX_STDC0y{l;P=jhk-3Afwn6YqbiCjfAFYq0<3`TsuVA`UjWzQ*x?UhZhkR<0D4y~W z$*^Xz$sP587k`?Lsfdx+kb$I{7q#)V9p!|D8_;zV0R3EdE+9WZzD`L532E^TTwV^% z2^Tw1IX9x*cXx*lp!b42mB|Uj`{(avS0$}+Sz>RmPP129-JP@* zDhwSv0y+oK83XC6LIS3r;-eR$v^1Je8n=KrXL`-(7{+T)$1%d>4h=URPrBw6|4t-xvf zx)5-4-8_=={U#%6;aLs|nv{2+g}AESPY*NJS7X*1uGlGpZkr1)2OvROw?-bdqv`UDn`QEpVAKRlbM3dLHX_$n!8SCu&1PTN(PNK$ zii>uzyb^jzL5(&--y3=NSNz!eu+8R?so_^+cXOJ($|a4<^2a43ylu|EZk5uwToP`* zrk>GPMeIRJx+XU)9^QX_Ho6Ges^^dnRP*w^w%E9`;19qw7bNsN>|NZrs)QF!Zl;TH zRw#M?3K>(+2$t~&8b*YZ}?#$-eAB+pC8O^mWb(seTgj%H$+P$|3 zvms;QG8J7&0_147he!8jt_(UXr;UZBUculUl+jEt45X}E*EaSwspYU_2+POG(HqXF z{0d(raYc7re60U(wJwTR+tn#Q(AU)wcD#(zFPi_xdeu%9h}BN1lcycyB=cdHpEIJ# zXe04Y$wPZnG4Gew{>f6iMU2n5&k_~?h!QPK zY0Wr8Rh(&={o2tw`}uld*q}ui_gHMh8?~|JvFy@DkIolTTqM<;jh4DR*YEG!wWk4s`i;X%p11KDj7lzB_#I^f9E>HFbyv=I{i@1a z*7bTs=}awtKNlNo(z0-8QD1FU0%Mh#_>WA`bb`P0sP;5w>MqEQ#h~al>z*yhgcn{m zs#tgECy(C)suB7m>C7961CzY)?2x1{oVG&MYWJjke`nfK7E+(nm8U)>4y8TDUApUD z*OXh4%sV68shUCph# ztD#`_gvOtppQDCr`krj6GTWeBl6FX*Dlg)+gm31qOD2XUk=*MP$xh$i_OZMjc$Zh4 zfpHvpF$1&5!9amFvn@Zs_E`^}MXtF6!R8&- z^8XHY#8pjWcSFIxZ;h7^>e{FvTBoS(zP8gwNUb8FQU+gGH7->r&R(YS^p(di3Bp6$ z`QFzGv4hp!P0m}U{-p|GyYLmxR*zzFaM0M1osI9&fqVDzlIT)p!uRIA zdtS*dXD@gWR~b@%NB?c9GPmaE-#1{L(7I;p@cPb|B~GU=mk%;s?CwC9e+`yWd8J9N zpK34n^f*gMpXLm){cx9Bzy`7;Y&V;Jow3#KcbY2N%XG%D|JD<}!ol9cnDG0Ogj$z6 z!XbYQ{G2B9=~A-e?T-yg-O3k&zq|IE9@Q<3JwH#_l6+%*$DzX6eL6JM#{23i&}bK5 z*Sz#U)S%tX_|>4UYk8LKzg8ZEo_uU;Z*2+M?%u>k2IXY^+tO{M#U&kg<)Pk^owoZ+ zyu;|r;<9D;{g&a+9C)h)V-wYC(-%MW3Fbmd*{c=&Sg}e6WRO_NlOw}^ddd&lp#AQ` zgaPYvU2{FOk$H%+^STH}T#>QRkhEhM-HqY-S8-xS$@`Vp*Y+U(!Pl~Dy*lgKD2myH zKZ~~D;``AXt`Iz@VrvOYJ*@1>ZdN>!Cd=VOklL-pm=; zpCKGRcUp6+(X8e)l2mY+&bT!9FjuW-;Qc#Y!UOQF#AaeqoQyt5XP#4m+(F(X|4%fej53q9t3;d_QX0JDJFehRF*Fn1+fU+pLR zGe%Ce=82(~{EqpOt&mfh&B<4o;NM{jj%{1|e|&L;Y2!3Rcjp_Aa#y<2VcQP_x#&If z+WEHNZE(DinRMR5v?KN(L$FefXaOe;DrEhhz*mE!_>iQ;;5(@j3s_n>p&GcV_-5S9 z?g{)b-;OZKU(F06ZjVh@D+*pa@SWQg_Lp+Eo(MMz4?kEvpY(V4Ii)SHP@1-5uP}A7 zraw2UCzuNlc2Z-s-9#yOd}pRC^e9v5b9hCl`fTn*_NLGxt zmVtuiYkOHtWDY{*#G`>6F$LAYDx<35^uQ3dDT}`ku{T_6o`Y^mMP9tfz=5o?{VUzw zGNFC;9J+1Mr$7c3XVQ9zy0-D((OQU*D~)K%mk^Kj3L#1dFPw#ZaBGHa;vFkcm|knL8vnm07= zwTi)KX`wsP{K8#~MKh512uog}Agklru!P_Wnm@iqfgP-c&skJgZa!~azZ!QiJ>l8j z)m0FJ>2`E)Ys{@pkhrX7uWvEfS4XXvFLoBHy=YutILY9Z$S`@z`%r*p4%T%ZH2#ag znw|?lg2Mnugc^FA9$G4e3o8 zktXt*J|fR(7MFhY6p(5yg_Ly13F}R&<2G+SICt95fawmJ-7fd;jB25fUSj)ole7lB~Ed@Qb=6+&QU-s%**crogS%nvt)x%({} z61$Cj>6W1!weB-f*7f^lOXL4RO1nhx@vMJhlM0x4pf5O4Wzy6>6_3|N@ zIctc`X~6N%ir`mPMfg!q@xK2i{;}q(mM_^k|7<{n>F{^RA7_-u@YehPonIP{72+x? z#3O87WGVyqT>rPXNMuFFF(3fb-5ySpj3I6VeA_dnb#?5iPrE&n&2>>TWxJmN@ zt1w&KWR)``umAkt1^OaC*Zrs*nBBJnOB*Lc*)jr%zW2|BO_?y1(bb2QQ+K1HqQ1gN zW}=JK|KFUUW(@GgWx$~_LI4sd8-SVJ*wDaVhVyb$((CMUMn0u@=bV6xHD*Jx&{%k^ z53$+{TR+UJezVPW><$$PR#XiVVmSo3dtp@BKQ@;3dQ`ujk&%%AFg~?Y+D=16J4C(# zRQ2v9kqa$mrDph6w+XQgToe1FtES01!#a8xWjgE8N6x`fTs*K?KP944&IBs^bq;Y} z01~E8e*(xyOyrpSx17<$ju*GFaz(!YVYoZ30E6Q5UZh=inXDyL2)p`uS2mWiy81$N z=TOK-Au1C_?@dq2$>F7bhi=JT0PzKg#rnm_3W`dScgCzdSYEe(-3-xSxGE-mUXe29H4mi{0c47 zMb|ST*N@rk(D|dkmvr`cH<7SUnt=eC-P6?nz>!xM{8s|;Xr~O&W-~LDvf10!^-oW4 zZ>j&M`cFST%{x^`p350#PuD86DSQtrlU3HjgI^6{o>`3A!WZUZV6jloh>7`JObe_7 zl?48}E0u&7pt;n{UOW2i8^eDq4%{}z^#<3K0Zov}252E`mw|yn1`=6)4r$#N<4(*m z2yP0NpDW}7Z=;90&HTZhVol%*unh2MtOQTeOh@Iy7jSqx8J11*Fe;bQ6OOC}q)jQ{spwFx@PW-t+Sork9eEEReG zgYYLCqJSlw-kz9c@U7svzy;2|=LG4xA7Y{xn z@Oc4w3cmzOgb%Rr>LwUF@8j*9_}Z&=%7@QG(R5)-aAqw;O;VpR2hC=$ssqvn-dleV z-^4!QMDW=Z%jqOBSRcF&IR*Gq{{sWujL@iDh@=)OAHNXAT6!XrSei74$!4}lE;JF0 zh0~K920vDX;bNc@V`z1&(HIDE5k$nwk@}#G(AxxwTXLFGhZzy&%G4>$G^TCca`R6b ze>IU61!+6ppV@u`hyd+3xCP^H8JZlfJb~;4==+1-U|TR4A=Nf!w8A(KB7UacUuP${ zD+VS58AIhuaw1svJZgemJnQ)iQ;qEmqM#x424X^e)Sa7ot%{k4Rn-Pb>dDs>hLJ!35=+nk>i;qt7 zI7xB;`k&>!0erJbz`$xE%0rD$sK{itzPDPG_Q<(J;rQ&4HK3Z*wi5y=% zZW*1v0fAFfwmIvzc`X!r#vYFx!zWN)H@pc6GSiP@|I`LV_3Kaja*twINt(;WJ^-i; zY#{_CXxfswNsH^~=*z2vB|?;BbaOM;-TL+0LV1nQdbbAabJ(X}j(E`|U3!4fW}l3U zQ$7e+e)+#Ig6k#u34-}41ulXtZSLusJ^IKMzv3K0c1<5WcAUYMMr< zD6Wl8JcQzMeG2(*C|B9)JF)|0A$B3o6Z2JKxqbePIKSPpV(mLYys(X`aD)vr+=9`&ry_8%?+?@s@8l?-q zsFUOMVO^-(*C+Y5&^XG*IZd(QM+!5k%g9KuSQHmv+10bOimP#Ub|x(t{NL|()56sp zGCXug8opioka3SKjx+&&0N?)#vL!zW8o3GVOE&X5SV5?m6Bi2CEz56_yh3=Q40%p< zVCmg!65?`6IUnIb36h9?d_vwlxkBA1h5reUC7+9T;2KD`R^NeADpGTDas(m{f-g&Y zR@_?P-Yj^W%QDnw5#VfO@9cX!fDd(93N`cq{9T9-WsXUG9@Dgme)QP+_HFCaq|>19y zzj-Ttpyon32rV~`vRn}}Csz{3T*q2XiVQa*beDS%v3_5gFoNv9b6Kb|iCP+tCjdr@ttBB^I8$pYB@Lk;;&B)Jw#mH-+K94& zA=E|2LDW%8KL;|aZOh+pmb z%iWIuxvjLsO%CWUfJMGLYHxkpylpQVGkw~C3E{6H|`A)7taI>TcNy#`Yy7k#D9KJjy{r?5h5QxSF^R?j^e`Y0ddp2 z0mu0G_<2BEs9NB5E6lUWob-?*9Wvq9EBi>vx>#HPtB~jo#Ccjcvh%p(UnKLU^#2<0 zv<3hTBS$_)ZiR!pCMZDhzE7p>H`@Oc&>wn5jBZe z3}z?9vkZSbGsHRcXbay7EBN(YAZaq|DG)J)3VlW7VaMbJP3-;7&BX-Q&4n=5-FVkj z4o=TW>T8bzNgp2U=ALr@x7~KD^;@sy+DT6u!|jy%I@LsBWVqej<-gLh04zjbM!bFN zo$rfK`e}!{0HTU23xQ=B%+i#0or27TkHweWjN05yPUZm6WE+4u2p^0zJysT@J+2_Q zDJ8eE5|7!EJPIZr%L8hT3opO52u2BlL{dD}_g8B<&xW%zRN@6p^^^Ezxf;C(7m@CyA^wO)0Y+<@gwfqlhm z410ji99D@}!MorsyjN1K^Yc)j&2vdIK-T|01h`BzZ9&#j1?IsbnT&A$bTCYR5+s9@ z+dzqake@)lR+OVlwaez(^de2y>?4of_(C!l?^MQ6=mYHnpR9D(zY9{-YXH%}n!a6p?04}sgXD0)bKyvn|^Q5AkVZ7TX zlH2RRv^tjO`2v0o2T%YwwuGs4FDfqsT`M5cfj}rCs4>iV&0(a4l+;u>+1OA=ezPyW z!7U5Iv)MNhT;TGw%&;FSSApV|m3>8GGxMQjxhJf;zgSk;(w3t6=iZ+AR@s4A+fYRqJFc%T}CfPzs=P@rr@MCkpma*s?&k zgdDG1w+U0mW7f+g-hM?sM;(fafRA@!UqMf#;?X2AJcqw@e32!|Glwxc| z-n)dJQs=z7?+dW=9nI6Q?==tsy+cxTY+R`?VUGuhpfW-X7oPx&DO&}Rg9!~<-d!%K zs#Q;_5ItQHNaBTXyWK#nU%n?bkOtB1anO(ml$fKElNY2K^j&vk*#jVA{`#`J$pSZH zJAph|bRJ~`gzs|jiA$ZBdO^-~Io%!~;T_aVT_+;SUl-OdM00~YYZ2D$R(rE=bMYbr z**`r19uOLu`_VfS<+3b5xOs+jPl}Gg!e%`7y{fMczTT}GM2R<2wbq5O9r5Jg3Rhb7b%5)F(11^4XFlDv2coT)lg^vLX!RaWk)HRh_T}BMyuT z?6!FipbdMA1%@VCt7f*`nkKW|jww8wz@HTxSHSO^t|w+UuLyT|q~uNs$ps z*VK3s&LQ{iI>mU*%L6pMdj%W>b@RbODGQra}O}lGbIS{;3u8Xy>AVOjGUc}JGKK^e)#?GbNS-Fg$-?zX$zG*o*g2pzrGj&NSs5W`Eb3*jC6Lf?tT%bj{nQL zDSViN-gnldvI+;tFx3am$&t!Lm8J|kPkSMGsPoM1rJB+!NLpW8l*=b(6w8oUPRDXu zzLCT=aB~4|8@X|P3sAbdj`KMN)HHUYp-{WKm`>`7d$nyu)VX@YJ0HyF6?fSq37Ot! zIbF0I@1kT1Qn!{43NyOO<)e(PVIhB!e!K8ejub?E7*!~$lk-WXS_@LV0I zc|^!=nf+`?ZoxnfcSe1dCfsvEw$u75_#L1~HV*D_Pzw6sPfy`1POC64$G8cW55z0j z=Ib~YbdlDhVl_@q6?kA1KOP^*I83yh1<7ofs))ch51b97E*85X8W#FRy(@vJIi&Tw zcI5;Xvhw3*lX%a9g)6iw(P$KZlV!Hp%*{tdl+oJ%5DIqDw7VLy>iiQ7+Ryw5Tsn1fU$TTQ2h!`T# zPnZwc*{u8F`d{v3$}JPRHD``*eb!BI3tLU!+F38}EwWRkr8vw6w@#p_p*x6No4vI$ z=|XLBEja2mD%&j?B91^C86!kf^s7f53tfnZ*ONZ@6R4i4BM!HE_9{T^3Jr54XkPY& z4}*Mz@%1`aQOJ~^iOedSwNElHWm+dqM@)JNOI>`AZ&={8ie?TiOj&AHlH7CnFrK=E zjuE1+@$BkMr`nv=R@2y=7jsrlK02L&$?utO*jMH&X7cE^!g}R|O;67%Wll-=TMz6+ z#~ZGRUT9vo(`_8@^N%ZrFY+}M7WS&y31clq>E_^hsCpM2A67&7iNli~RhWe|uBIQ5 zeFVP*iEmc!d#ajJaJWNDHl@|(rbl!%oLUz|uOckh3bU4)TcK5Q?SL-wsE@j1Mw4>d z^+FrvvG99m=`7L=0C`t=Cf@>K5!|GjxWS`eh05J6lvy`(o9J~2yf_yY8udDZ10s*` z9TUMeh!3Et5R$kUhHSA}kO96ThTJ8~FXG(bjACml7Txv|_FM?FA2z`ofOHK1xH{Fy`Q{aNAd@4@f%GD28O)%@%yS;$*!-oxxN*^_$> zAb^uhw1NK1K*O#qWi*y~Ni2|1)=9MuRSviZ=grAw_RVErDJe1V%?tQo-wJfp^7$!$ z5DlqL)gl5=7V7V=5T{mIdI-TxC7AejHZ=8!SsLzOx>y^Dpnzpid6X*lgGcjb%%#rt+s7LHt15oabh0Zp^)3YeHO zi$?5(5X)}`8Pz89>gfN&+*kiK`Mq(gNC+q*APp)?Dk)upFw_qr($d}CEz&UtBGMos zA>ADcY~)7g$N?iq2?ItvH~M{E&%f~ew%>N2bFLHDb-e?-#0nFu*H4hTPVOroXitds zT>C?x{HAVm>hdL7ol8(n4vp1acRgI{5)*D(lK7_VT_ppR*Z6W*kaE?oe5ukeuNp3& z*|6_DVyz-};C3YJIB&-oAiR+{>4G~&3JyN)mT(TfgYI#?qSBJ6A8$+_ZPlaT2BPF` z!<%71+ke5}d?SqBp(2=A3f+-CyG13+?u_^BdcoXy00E5hwzEykJJ{1EWI}w(3a&Ty zIP^`FC-#kSI>i%?0FD|31%=s*UG6s7{lQ7VgYkG@aL&EWW#y3mZ zC_r&1#kUCA3p%ma#dEm&H;smNz>ln(P--s-b*4Dt?+N!RsLm`=Oebr*3T8gI2)K1UmW$yuG|<$qGh(~TgK9MnCf;g*V-)e*>zzqPjgbc zbLUL0;oW4)=jhcv$E|~1x8m~F>UlOw#I@!{ml#MJBFh24N&9l> zCc&M7akY@7uFn*w+@RO{h^ax>-umcJV?9<;>qHDre1894nycX*hgw^N z@wR#8Ftt{a*uuZNVx0+?j0D58ABMky0*Wb4LT9T%TZ~#xY{I$M6%rFOW$Sul=~zv@ zS(8?4cYX6oqiRiacOrY?9elx1(QdI&v0~OaOOexYc|D}be_eN6m|#Ic#i?%CMR-<8 z&dTEFG92J-$RF_|JST%b`#m~Dt3GGTuTSj&8 zaj0wLTRIZ}cn$;%f}^erUs?6g_yIr_Bd#HfhcN40@4$~cN*0!WE3<($W#!2mKc&hR z;2pNr)@*ebUm{dAR5;C~&T+wkS#)FkMbC=ZOhz;urLQ?RuFOhp<+1=S^Kp5<$eP!L z;3#PM^Hg3u!;WoeninW-BUP|s(*`KkYHlhCW-Z$v?>+Hu)f0UUuFU9eLialOYJ9w2 zz-X9;e?m}D5IzKje7pQkMVYmq8@b^zD&tXx^Lr|=^EAcQ&`KuXv=tLG_O#mvZAs&8 zED@TMQYOqSY(HbRU^&QgxOL9NaQpp~XndyKj}5oE3>r^wFY1iPei+tDvB|kPit=YS z>uP{cljoatVDR%FRT8e%^3wm=a7SJgk)$X1Ek6BJ&qr+aTo}!BBg5MlgR*2F5}$QE zxdgoI?%?#m*RnZX`!d6q)K`ar)39R01Cz7|8!NHmm=5WMgS)tM;Q-W}NIy*@TY@$wluQdrH)C#>~XvSHbD*caM{a+ET%Qg{qie!&@}P|wRm!{B20;tGaHy@?EX zYjl$caHN^CuSDt^^(yi2xXmVbds8PomSEdOzi{*>QKnKJV7i->mH*nW1O;JB^5-m5 zF~$LPjhfay3Vc4sWB8jphJhH@tZJBhS;eN50CW0OpFNO^ZuGv1E_PgjqDi{-8X!3t zWfHqQ-g^A3xrv6r$rgbMrh|l1)gxU<9NJNA4U3;iz(Gx|ty#)AtV8Rd6xx|hwz_l&_miK#M$6N50GrwAhf@z!G zxgQR>bvn=H2HWtcj-HFOBKFj}T2HlPZ{rveJ#%sJsrmtJ+~Vd$RqZ$|b1-|u&9s_b zx95DX+hNX}IyOniUMx~mhT0=Jd=tfX(<^*wJT{-rg*XZWoZkuZ^b#nX#|+ULDczLW_QrKqlLyBHuT7 zTFgNvcwKAW7Al~$Wu?3s*nR-I{+Ibwh_XA-afK0`f*S=!%K>G1o69dLXDAQc*5{z) zscA-=O|uq(_*QfYBmQCn@$6AvVe)$?HFoXA(tt1Y5 za!>Ra0{utn7mQqY#_TEg-BwkoxZX8LpDYFov3#YR$e_hitjb9v-jD{~UCiw-Rx?v` zYkotDFBB?)U#A-v`6LV8l4d-BcDTtJ5C$nD_;R&(9z(Ik&HY*x;V;3Hrc|^3GDHK!}F~5cq-AVYK zv2ZI}sTqyB7j&$Ben+`40bNDTRfa$)_Z)zSta$d2r$(HEv~XrHt$C!!^~D&midqFl zH~NV)8{%dRe-~K*Jr7w=->r$#WA6`kp2WMS2A-O7OvR!3YTJm($13Qw1PdUE%i$Q| zfZmZ8#~0T!HqRXZl5GDKU&`Gw-|M71RO~8yE<}TEB(lGWjjghYAnn=+;(ACS_xlM^ z{AH6F5QxM9k+L9{azi!QUz?TfLsQXRlA{JTthjQQvuA>;=gy3|zUL*2YQv~^K{cl9 z41gNggM97mN_P61#omC-8X9AKU4B1b$aV6}*eMelsqJ&}1q?;4X3c@&s8s*zk}x>PoBqY`kGD)%IVz zG*k$gm_n$D52_JT^nf>| zgM#VQt4^fd)N#+glYiLiQC8#DkEo6h0f1^_GGn_TmaeiuVSaqfdDwguXr zz(^0s8c6|7Vut`E}XrU6BdAVy=_=p0#sW zQ&F_S?85XTfdyx`32|!Yce2@1l9J%8%RdG7T0n}QK)(*HPP(k>=kD~KKV2v;;3c=TQ)AB|3$qvxwN2Q z=#M9&H0xVio!!)iF_VCm@U=Iph2j5@D(dOvza@Kj>K7dFC77B0_GHA3-K4~}o``1` zD`yAvK1I8qzc#?tm8?YfF8tME0BZWjb zd@uIBCcOhg7E$~E2^9AKzU2+-W!|)%ah=}(X6`V)e>DzaKCJZr&}@6Mf9uo0=(*@b z|1XN~=Kq8VW!?V2`ejY@U6oIlwlF5u{?^`ruMCOt4T(xi8dCu!QoE@rKNX${M`Va! z21>0INXR67?gt)B@0uD;D~uz)*N+8Xnvl0K=d{-o>I#=D zI6e;lzyYyqi=b{j`C;|)R>`~%6M;}xxLpzRzpeZHSwXE6ndc?Lug>}oW^aa`_l5L9 znNh-0*ip?ER}!!jcEHdCG4Fhi$qKwH4wF%m;_bH<8WuR##=&na?W9S;X7@NI?`D%@GE@V2qF~yz zj>rQXlXOJ*v2xBlsj84J)bDB9@vvk15#!bbUAQEnbb@dJ|7T<_FCr~Sh62BxImytC z9MiB%E}(rwi1e!+7Z`D;{PEoRkjSp!SYJXi12dEYdqt|&iF4{bx_Yotg8YZ$trThU zxS7A{fHJxo_vJgR4y?#QMjbu!A^rU50Y-gf-*@fppI>!LOlV{yf@o*=@{g*zZpA(C zA<{U+53m!E+d+@C#fQRUXi+DXuN|d3A@uv0Ri6rrInef06}~u@S=QWxV9;lr$reVw z#`LpVEtx?wqG zu7*p9pHJD>o6OfQ(`Ku#8jHEiXL!})(yYc(8@BfG3TtoqN>p!2Hj;jTIyW9TzrJajsM>JV49enQ#j^~Y}Soz&=+WMvWF3jRn{F)S^ z9u>J8fMVWnzdCQv(aZdIMcpSE7W|#zeH?6fLY92eON|myqy*n#pD%R*hbPcho|O+CfsG$woxaDW`SGB*J`|yJJJ}0qxtYSf zEa^So%gt}P`<*!Ey3;caTVc6qQGDe*oX60R&BWkD&E%>thqOH*z5zocpXoj1(P7qN zZu^x?Obo;Sy_1HierM$PFD#tX4@jH^H%f>jZ;uTTV;;d9)EeqbtO`Cke%xmuFxG8Q zxaYO~85Vm1Z>50xLL|JD-wav`c)a2@C|h3adJf`Ll4!CO?gNXpu$HeHo7hZ5oHO{~ z&YZm5X#6ExwX1DDu?g;mA0s`SI<}_XR%BiD%J$J<@i$APJr7m%xmA@7Wx|~& z!@JX$v>~PUA-nVfCd@rQF1*agD8tU)c@4``8$HUZr%(#ly`Zty9D1G<&$F^$CfrJH zYMQIv>~Yse*ghz!*R-PEOyAx0p;u8&tHy%JcAGcc)u*kIP#S=tgDQkKHC}>OGvOBY zQh{S47-^v7=yh5=Z)Ga@?;sb^Y;j_4Ty4+wS-gXj__FCh{-dyO7^=HXFT=nFiOrU9 zNVVMf2B{frgwcAUn+S(*eAjU_@pE~&@m182Gx#g{WNAgDduyxL_!Ok_;Q2iU zsSumepgE|w^w#G?-U^?)?}CdsF2 zi#9bVKA#6a4`)^|+TJXbQD2m$ZXJI&9a&sQ_aNR2^7ApCxawCEKev1b2!b~_^T2*S zBQW`6ZIqpl;0Wjyz^Uw}%dIFVuqY`u$jAhBHtdGF57dQQjrSD_+OpeE1px#I681mk zLX%~=IxkH7r_rWXgFa@yXL0Z&lRB|bP;OYdcfbypL2#4m@o{tV;W3SHyK1?y$)6jV zrPHMU9u+-NR2Vhvr2y0cSKK2FNz++2Yxc*`=^`^b+NiZ}I^?b#XVkd2WO!i**UZ;k zFy~`-jW)<#0+$U+3vdL&g4l>zv2^-XBfmX^+f;9XpvA~wBj+ITJ?TTzD1ZN{!;{xj z>F-hyd3@pmp+2=1X%767&wRaNdZS?8WZ)X8%YTmFE8npK?yWlS{_ziAfN$8c$)w4O9u zVcO}Xy`1r?Bmlfqm1)WLdo9(OE7m(x`Ns}wZ_i-mK+=Ni-}k0Z179rVa|j5hC_jNo zJ{YT;A-_X7PqG%$Y?~T6DH`R1VH#0lmicKO+TbWuo*0(&t{Y9nr)&Rx#!g$6-~PIW_e?5Eli*p*^iQlFw|OxGtv<(OS~91W>Lz`cVs5Bl+5{@J-2X0j>mUA- z>57}CWKyv+Wdv}sK!FcG4E)e+p1JMna^@B@_a3ykxh*&tvsa|@*9Zy>Q-isllMCS;Fz_{F5&H##_`!n}QVsz>I}@m&u7 zYjp^~RstZs`yL&)%Ug9dPsRcg2uYG}xLZO*xG8vC#%y+@m)W6a3Y>X%Snw~#sWbVt z{*8e8P~ZFJNU|8jXOkvtDFi5`hJ3W>Wtw>^O$sNfjV+76%=e*|p0iYDy}!6n$?PXV z-8vH^l+TH`vQ_CK;&RQkCwXf~5i1f|`vH_cWVz#d+*dzv7at6&xv%~$@Sw+A#e2B? zmd8HZtoYltD5V_fe$Cr;7Z=fy?;eV{8Z>P~wrMNfV`7S(9=$S5|I$8h zwX_#Pek6`H(NH0$)^~YGS4-#fPW}(+-N!vjgZLKTkPmTzw?@N7FO)vg9C;;iKa z>_usc2H=a;fv40bL(J#Ye;;lU0%XiwpPbI_r$YR25!hsHN7119hkNnG$_42rEyhgr zm1*$O;YvZ;c!M_ZzJ|@! zkBmyvp%)1h$qwUuqd!f2)*c1;O(L9N7b=eopMNip>rTzSIY6h-3PLm76_=@_qC+o` z5{*bcGYg-1B3;8IrDZv|HXt}Eq?^Tk^mtO*3Pg%eN&IZZ6;uQJJa_T4W0JuY>5oP@ zUrBoY%TCk1ONf@{&!!iiCCO*>*RXiZ=w~&%PrTkLQ){TkrZ2Y`XHK44;rm*3itL$mOasA|&^;5U(ML8q zWcBCg#$F)1$t{gPiNx$wE<#}f5`y#@Jc(8Au?E#q2ftdAXKG0VaM+o*l^oJ|_mnoP z%^3nL*zQT4KDAI#7&OZMLu0~PnHYz3E)y<+H{jnYtsYV|7XwJ$du#@P7^tVO7_T~!- zSF2mn3!=ue7Mn3W$+l}FZG&Bw95aL72h%0wf2_rumOj7BCf~<}!S=yfQX!!H1){@F zA$mj3wK(B2-|VsAcb%OmajYq9wVEh(Ej5v!^;j~vlaMSMNxJi{@R<6xD`MP*Hu2y& z-BCXI;hMw`6XeqTB})g`YU*XDhEAXh8o?_m89F~N^}m?9wlwL4wc#l;LXlBxzl0g_j&JvA3k`*=LAZNrkpZhnZWt_+WVV2wmYwA zl`A+~2{8HYF{!iO>!`N{{TL!fIe1G)b?ni#IIyO@uGE%;h^lkGuURbqHwA5HI*NF*jPPgbf@ajE%kO6sQzm$qa)@3G2Ci`d$CG;VthXXxL)@N~Qo`Wr`GDcuJk^UZ1^kITFa zM>38vK4A_4xgmH%vmUS?Nc_ClGZ zgCnEJFS2#PRCT5qc%c@f58pm{DJyNu;Mul@E7fGI{RR=Vos{oS%l}YUlwYpi)wS|+ zqMYfKRAFba()SeZ>~A{3@x5WE1i4clzkW;w?5`%LCU^C$8?VH?!h+WeP7s!UW9Ous zi4s+MMv-U-3b=gOz*SWDmudaZv6b8R00v~O?d1R7M6>`RfXu#*G4iV-%~e*huiQ!< zjH^%#ongyfyZpAWPCuVYODZh8G+1#u=#z6jZ9Lzu68*2sos66Jlo5*^J?tK_iYeG! z7h0~4&Tp+^2t(VKC56Wyvv+*ZM&BLgxwJUh>BBE0Va(mr)CMN_9ztpIa}6h@APTi= zK?bG_nSMQq3@#Hdy9cJ7xqAbLj}jGBtnY74l!q^j1_z_=rq}-bq|O=*h>S9vNYKeM zr|A7k7c$q!Y8G1)a=|akC+R)FQ*Ka^KW~*#k4v#-C@#iy!{}5`1a6uYdOpt+Iq=hUK7bzwIgyr zjBi(v;Y8v1wb6wKfora{K67h;IvwffX^ZVWLd{R*hQcA!@2Dh0bM)`lmX}i}piGbF z6$&`l8EHDSyJ3HvsDh-yZ5&45+sS@>;Nh$t64@J_YC-7@q~dg1E3J1c3z7xe30B@0 z$8|!>AXGRP%+YPz}t*c#PIWM}%Gj?_gNj0Rjxa|v$!M674Tl=Zu;+dH;jVaX> zZwhP$w|?V)s#^8 z9?okc2HauBzu{3F!xbV&Q_7?(h}|LJ6=HL2ixv^wUT9~&Hk#i)g!rwnc+G7Y5MC!h z5o&`6cd%#AdC$HM)+VgCJD6<7gZbk}nO28r<0Bs$L}R4g=%3&gN{+wDcxLs)rk@Dw#>pspT*o6>5zW3@1z43)<&Czk#U#+seZ(X7<0=F5 zI?{wPTlg%RAH4b*vp+4CUwqeWuqT_=V$gfw#;=Gg*`E=hCPI~MT2N)+-HF>8U#t9q z{Z%UvoMutc@kY51=_q7*TSDv80mqO(?ne zsNT2C0}|LGs+j=$00oc{D3!miwp?B-t_1|FCTcCK^GQn1G4VdX9eA?=0Q&D70{mZp z5+<189jU+g?%lFxjM%1S76|~877%hk{taviPXGiL&Z6SpCV-`ZzxYjA`kij4mfJ7R zE;*9H)4{bk<~Ok+WctrHNI02tZcvN+>_(YdJsSMpk^!pX?Hv6dYcOizu_a2Z6tTF|UN*eKN^LfRW$8*jOZ%&);^@{b~POo1*lG zX^GG3H+_#Grq$%Q(98$#x&!QH{ceslwp`#F9YWnbZnCb;atz^lyd~hME{NRGtv4R$ zyvLNhW1Yj61sp##6D4yY6sv%}Pt9B`)RhX!b$S-vJ%@6i)UVHb^7V7dtl!?pA0MoL zU65orK7M*#V2Z$4(lTPJ{GCeqj7^fLHe_Bt9v2!68K`2xn33f~7DXyJoRjJwzpUOg zczO)8xyedLi%W-1>^Nuc*r@z4YF^BZi?z(KUx*HoFj)dDOn3z}UJvRumj@1Aat^6I z&k+*{ldU9s!60<2g>0~ovI>-}qNr@Luue)4%^T|rFLUeb%^$!~y6CvJz09}d0!lc1 zaQ}P3X+kFbbvr$D@mX(w?9}e~C)*Or)Y#bepML;~Q{*pWnlnwX;;vYW`yWb;eN6s{ z-oc`RVs~V3o$bSzVKC`SXPjTQX{If=Ut^gHpJIL1dqtjS5?EP0YT})1cr(ZqAjO+P zB*PGamxqS|-?;081L>QN8E!uhTd@>h;vvJFor^n8RT$5s?lFisjWGCK9}J2X061|k zLim2#PTA%Comh8W7!Amorg`s!h_Fm8ZS(HLGi;8(D)Uob18b8B?d`UOnq@%xR(N?* zWVhSoVeNFPKaZP$8F&#vT&sCQZc)Ww37|I-pP;_I5l1%W1jS)O zH1+8wVlG`A6aAo)`t6kuuFQT}f?!&*U}(^S>mqNytp+LWew+39#~3Zk;mKLc!>wlQ zcIRPv3vwPeV29VJ_)s%+)-2Yt-IdFVA%52qDw)tf>!o5#JQ`=Xn-N%AF{_8&lid zN5SDKPCMST%{@5}uP59owHBYtO;n$Im&+8|!W@a+pDx59r}y<5Uq&2mZ~GwV_~wZ> zCx+`%JXyXsp2wpVJmV`d_2frxNN~rRi6VotS7R zO`~`)o+Bnk$)t1Uv7+$LscHa<+;;Loy`ReL5V~JJzJockMT{@>v}5Vsq8nTv{L=(ZTcyA-@JbX1xGcxGI_(P8%(WSGlf!D_b z@DYC;#`P1#l}zvgA+p1f2S#NIp}$wC046g!P=j|?Gc#`xWot9M)D|d3z#b7ED3v9U zr8FA_wP}s{>b#93oP3n_A{k-H$5+QSv0_)pk;dkKjBGJoASJGHo4Y9dUS;0AL4Ta| ziT858EvpLeKH^-m#cgF^c8!6$h@7o!n19QyHDUj&L+K~C-~N@cI0fmmVSMZ})7RG$ zr4Jb*o~voe+JDeW1JRdftqSVcs$8&moH^T3fGsIhdfbT;)aRzZG*+t z{FPGrwR|*C4I1aAfQ7<~8uEdd!Iw5`-cck1-aWMBaQf^x42Ri}+9p`f`?z^5buFD~ z^M&Sr6D51xlu<$!0^(|~JBiLdWRu}t1W%|Us9YLsC)vxIoJjJIC4&8y@C%*liYH%H zPK7w?U45Nq9}vAWdHB1|h-`L&Gir#J&QuE`xSUC$pXP#8p(_j9w{j52w;35Wuu;fB z?+#Qp$Fw$Mh8OuHVEYX_0MKaip*sV0-d|Qjk-1K&cy={10P_Ho>54QBmvk#G0F?*J zF`Cf~^y|aF?PYxE{$_1hX>fXG zN*5sU^K}07Tc9~yO(DA-aqkr!w7c9%eoPD^fJa)_Kh;|)lLM2gKzsS1m19(tjciQl z!U0>Uegh9df-{0g-M!BQw>X(Uoc%yn6E?zxF(hA*KP6V~a@g=<+ z0oO=|p{wt{fI6!<^K%;MZ)r}|RMJE_DuaHI7(RbEfTl0Rmm=`-t*~+G37Dvxcu7yg zNH0H4gq?3QaOJVy3K0+!;1#(cog`CEzE%P*Q3rw3Fs8fnlAx8Z?`>~`><-&pK`2C1V9D#}MM-$P3+p#H|J=n{X;Ne) z2aQBc0tatDU3@mLhwRK;g0GTl!1GC*9BV$4Qp2I&TYv#lOnMdex7pt{QtkI!G8V=# zy(opQVKe3Tux->#u{s*#4G44?9B&AE_GejvWvubNYMI;?($9k6hu%Y((%3iqVLfz& zt>(7_qV|(Z$T2@|ajsU8&sx(Q(u1Pue`U%92DVcTw-`)s7rcfehD$Aln1o^z1Ymip zJ?QO=@lAJu8|%NaJHxm4lk{9T^11Gk`ULswmuU=7+NH>Je0ASrV`JO#Z`ckL=cX8P zc3kXOGLDIfHFYuRJAV?V@NM!iX)OzK&lD_19paqvuxTS}kBeAjhI z*f4%snW%I8Rdw;Gqne!FVk&JyR7IZRy#Ul%!ARc=L1d#WSE?a7%ariq%*uNk>^R+R zOYIz=K--?gkoit?9kP<^rzo;1l>Oh}8ZihK|z3d)O+ma&7-BWDU49en~ zLi!Go+I8nzc8MQuNJ`?@SZqQ!gk+_5vJ133)EjSolX^%ZIVCL=l=4n8Rl)`?E+%C` zt-8GOeu2oIcys;(IXEoiqs`p0iApcg$>-@TQ0>R=GJSEd)j|{=j?BoS&MxeZ4$CDQ zpJ_<5Wm#IU*}!Rxk??|W;=$~Z*s&vd$gwXUwAy$SG&b1>$~W$DtBcdCZX%;K0A zTzg+#D?_X%(w%TBX;yIbO`GW}mHR zKXdaIy$KYgpo%_?>44LCW(vl+^-k-VF8o)Z%8TwXb6xxBemG4W79Q@r-<`CW9nRc! z$lZEN(*Hul$*Fy?Z}uUra@3&3U^$*U?!Q-y?`C&`>633XraMon-Tmq1YfpTZ-U$Ej zD*Fr|1fE1-Gq*Ui=iDyfw$2*wVY1xPIwf(dpNu({InP(oUJb`HwmY>PP}(b@q3({@l&?La~a zwlbVcf@R-frebLlC9g$V0mVyssO=PSPmZ^;hJ@|yZW7=81;7?x%lYLcf!Op83B_kUp{_PeaC)=AFz9b&1N&L#2wIsUsP>&d`sd9I1`SlB^SB#OR8(R|BvbwDE97n~S z&}<4nAnRsRe#iz?bLbvDhpw_IP*kNeG-<=cBa740d5Hf^nL-#34=Fd#S3k8s<)uK} z`9Z>1pUi%D7v^5W<#XLuNcRD9Hk>UylOj(>B*{~OhQz5x|M**UZ-{Fe#W}finWTZF zU<~f5fAM1Dh}Ec2_0$AyH48$BmL=8xUh9Nhk+xj9b03>!F_9Yn);H-oH1EBb82W-% z<8oepiO7kCu739-8egOlwqH&v?xc-?2mELrfbP+&ECuBVm`azkX zx2gBkfQNvaUw#QLdYj~q=OtYS`*7u(dvQ{yy(;fouEB5x$-s)^CSZ&hSNAyM`*p5c zNa`;Aow8fA}_Q!Mmzxj+K- z#WLn_UN#v{uhkOvll-{W!m8W5kD=Pfib&jZSB|CRaTY@NrqVBE;?U1?QIg$O?rJPm z04CMSD|w=w{G3Y9Yxr4V>LH-NJsuYvjOQV1S5!|3-UscwVK`WaMPXek~63AjXF31$?2bqHO!`g;@p#Cf%XU!#ODr=l@bq&XSKr=$grE zVqR^hv}jOfrjPYdWQ};j=lXfvy#WzFYS=S8E+>OXm!y zQdP5V7TYlU5$e;?1PvqKJYuJIyzMp7FtvBMHuZU$ng%9eYWp;P`FnO~i}P4Ci2d+# zeSQ?Mv+0}gFNi~^$sOiE@1VPMxXWI;tl#*#s-sKqeTh$vO0o2yBW#b+C?**6UGuS; zZjL1&w4SA6*2UBj@Sn}r4Dklvw%G2c68O%p6+f5JsS#LpLSopI}`j{?}s6(%F;I-+Ze#XDsoSdk?!cK@L?2kWo*hF zV#+UbJ`;^wv=?Jty)994GRO~^x1rs!&jb(?*?FM;PN2cE5k|fXy~#r>BPGgb3pAd7 zXz3(Le!MH#mwu>lbK>>EElL?cF^*M-o3?w9Mz-jb3~s4+t%LL&y5r-b;7=R_nzD>Qqn)B z<3nN*6ycTR%ChE{ennam`Znphndyt0$m4EB?NzK>f}J=ad(6?Y;~LR$v6W*s+k3*P^hN2fztX!Xnnk9=LMNdYE;W%3nJI=}?A6%}7Ums3la}F^zvrrzn=CWcTC`%mmygkjNQeauk(Ew?a^a+8 z60Qtv6*I;`rBaCr z)~#>+hfn`bEKNgMZoqx(D2ZNd34`-k1H^?c2Y>_+s@>S0B*jV8>!Fwl7<2vfaN&_Zz zc%cDYza=%LTps^83vq`Fq_ntCcn5$?k|WW1V>$lMXPQ9e%Er6XZCFWP57V6{_hb&l zj0mY~*Z2Xc!NHf}VbfTi6_Ph8GC|>D4L5B1{G`%iuSA!39dR3A6hcD}Pwd=U?C3FI z@OR_CJNViKym=%6KfC^1k8}yE!hI%(w6*AcYgqzoTp@|H1nGQf|LA10^$u}xecvpv z12jFAa&hXObDXI2kM&cK@?P;IRo3|ibS{B1xFenR?v2NK&49@J+9nx-7tR+)~gBWlmmfd_T}U0qZ567g(Z_?_&- zyLL#q;ZPIybie;#7w3nFdbAc)4>*+oGiv{ z3G-Q@l^5G2yn>R^sm?Q^r?qXBpyYS$twPz!$)2Zp*~c;~k)j*Sl-PWZwcKL1`=Zi=76w{ug|TY&uGN z5`Ojcb+!WE2E$x#E#&Des6iGjtLF>w+CBV%!FP2l#nwrmo@?vuqXvtbk?qC7>AN!| zq8PVzh~Vk5*Vf~N!07I^pP@lnJhKGgO<>eb#nkorx8VI9lvy!1F>lKY2 zBx(s16aKq=W#_T3@LJ5-R1c9)j29Q>yS-6buQi6Y(~@D_aZ(s>VI94PNSv|Fyu}55 zcI%Z!@q}s7cdq|bMY59foRpht{#JXQ{gFC|hdgh9T%dYw0VNSgcevD_Z24$iCg0~` z4#pIU@vBjAR!;+Fy-$I^@4IN&?3njS-M`z&|9l)XKK;d9&_!j&xm7!HU!jatdN( zLvIGH7b*;ATyx9XkRv?Ehm2nmZ;@ZIQSTu|f|?L_XVjamF||jyOez#!@g$wGVaNJ< zq5e{hIfHNA*Z^Ev2P+=cJ+_lK_Uo5SvyZhAemlgb)<(|1JIX`Zh+8C(tZ@LE`~ZOdsQ^tSrAxa$)v^%+^)MHnv%vV7=Nq*_TF198n2dVZT~ zS-)MX7$z-U`#!ixsK!J5w@r_=2-x7KI!%X^dkov}_Ylz zSN>!RGsJHdfo9DoFm7)5cgWdH{{;EQ_C#pWfz3rM9`T?%zL&V=k4;`ResdHjquqq? z#Lo>86^O_!6zyZFkY5_Mb}Rz}+cAyz=A5G35fMjGv~Rk+T@RT;DfE4%6DwOWwHBRH zGx(`7Lu>DT`bmn4Ixu>PU8+HA7b;No6PVvsj0buIUc>>Xzr%l%PY>;a*{+*uGl1jguyH3t@H}tl1&iE85k6i^I4Ti zi1@VMMe|;d^p)Udb%JJkGiOiXB*Tu+8@BmejzZ^5Mu#864Gbd^N}+nsi(=hcqcYn9 zNbi=)I2tk^(~!fvidzzh59zs2P=b^HY z9W$QeJUbLAN+MHKp;}d7($*L;uMhm6d>Ush-Pyneu!>VO@O-dtrNf*JYDIWJQjxx( z%|^cc!aWEt#45z3oo!=x=a+8Mu)VaJ$Z*TaJDCLO>%J7=Hp>;^R!K(Pc7&bfOW-A% zB~LjHQQ!LAoT$o@BuUhHEIMN^uy(jbPz7BQ`YmbS)k5B-w_k%G!bbBn-rf?jlZ$HI zFfcmCB?~@jD=3&eu0mId(nl}hBts?6hay~2O^2E5L$0^D1oi+4++oh2gRH6U+~3u^ zvtNgA6aUonD=HWZ*DH(oP#Kb~@mjmpM{uG>*)a_$Di@bE5f@LE#AY0K zTt4Wq^RK2Ay*%eHKkF7wmVte|e)0kmnn_g8kiEyYmsDZ!z~Sm#zKARsL^DlX)v+xs ztwKy8A~R-q>nN3K-2cY|ehvh&J;*}H4r#{nf^yj|cFCrYa+z{F)a2K8|I`dL(+}RX zcw{qPHLUv08v7I$rG^AfBmI@-Cxwzq5FLcxR=Kl@M7Lt z%Sq%d#r>$Cr4xFWb7e{^gQ4cKh_ti;8w1Pp8#)WGCrJ4CJfRS;bZS zOu>cZ;jn!s?)J@pm;|EAWd_eLTN|CWmpfS>gXWF!6dAykU;)sy5URxY=_0wjBGT7w z3B`|91Pa--%mMEe;1Oq_+L+C4(&eLAr};)SOG5ulQ?FkctgK|FLRPFI`tfQEi=ps6 z|6fEW`<1~S#(Cg0q1nU_WQ<}F!;#MFcJgMlEV-sMLOK3ECeKq_QU#aiB8~tM5vs~j zF3^zqo#ZoAlcgO_Q4HmBT9W3dGr7RcN=3D-UFRUZ`}KL<)4#K>DFvv(CfFzGgD20| zt|$cir(PH&_tp;99PY+@kx}S)HPb#$Km53k5LlS6;Y#LbRV&sUE*yW)zQV`LYo+z- zN19518%$(Li6v*cI#cj-=CoJSA_ohSt!5r0%ot!H&$tLRP)1Y+vpF|js7#x!Ng zaDJgJXw~IYG}*8v|K|w%5!PC-E~`>R*qtf6(ninMZog$Ae&1@ww^*UL4cgA_G(HYa z^Gx1({x_xp^B6OAsPj?ULU^#R=aP8VRaU&0WxMb58T&v*nxlr<7k>t~ct6?;#`ZZK zjc9~Y9#8ts?)^^pZZ(!`p6(Cuus{-O-8HuOKkU8fUsCHEK3ci0c2?FdB{g-Im8Gef zspUjjTIQ5FGoAs<`-Rpj?d%CU=8MiV;8}9dgO{=cYP4}x@%eU`-=KuV!zV5#QwU{RYsnhX& znRU;&EnyuFz^7ZQEWx$TmH8hJ(phJ$Yh>Kg9CV&{s~*tm?%}lnV)YAq&*Zv!A!1yW zPJA&1sVu-4&!{akD$;pg>ZH&=>z;oy#FGzN#j{TqUh3S4M9!SndH(v}z_DA!qll~{ zVje|4{jd`UU>Ue~bN)xzllxdQ`-h=V?Z^=v=&x9Q_4Lo@Gs4zt8MvF9{ zSIt`QGdEs`bsT0{{klH#fO+f&QTg-3$OlEf9ZtYk9%z>VWhJHfW2CXEqoFF!SpkdP zf@ybdcf1R%9S?e!wVmR_Muu`4CYal&f_7KK=x$}kkFG$rr%j6!q6{Iv)1OV3=E^5;MJdQt=Bvh ziF#(n4&pJzJK)jHDkDY}pyK{Klgy4xPCN3v$Cw9ox9itY-=w4t5m;LdW?jZ0nY2Qc zyaAgQGQTwjpQv=L@|iwV5%}Yv6y@&DvV!`pTTV)p($wCJ_{bQ4mB{o-8F=!gj*g;I zN0r7H5A#xFgaV8Q{G@lzE|u!r-Wf&M6Drb942G zWNHmBYwSH4n0`7@m5`p1QA<%1_d4?Haq%0Pn2KnsMzm{H;w@U03bM1KrsO64mUDhM z){UnN)WQ>&@oQDZ+*=oLZyFZ z+d2GszT%BZtBy}c@(fV%O`X|Sb52s@hRtOBXH8ww`HJG)a}YiYuCwoUg?UmK0gdAb zP$J)<<4*%$h>j%X@3dPs9h%fWo?Fpp6Txc+^6%A;QlpbYj~-KS&vYCsvv*WqP%X9b zmag)>Hh2wb>WVKbBZ>gRv2b+IW{ETDVmd;>QgouyQz;}?IRAL^=wn@j zROiAE-97=M%DPVhLzcw$AC7|g_L(gQ7pr06{l`b+$8xfB+6)R_@M|36=Zh@4rfAyH z-zj<9^{L$E@>%v>_qDkeeP&ns{yRAY5>;tjk5`tyQ#DkLTeo$s>8xkbLVDD=@wy#j z29%qGXC9Lwe_Hz9+!VDtm8`e<7R)`)S#SroWEk3z?_m!*Dz1UOgURoXDs3d|y8Pw) z>I=4QyY1^qn%u7={9>+uXC51k6E_~`f#DN(Mhgm)>H0AN<#MvNXPFG=2ps7L9TB z^6EOg7u0?^vqa?no{7S=1>j(Ud1N>}7 zEj6gxP0V<1tJ)6lQ0Do7s|OXHa~)FsT&VIhJGGXJ<-yP2&cq5|tidMKT)HN7`c<{h zWc)YzV`ab5+x5vTyQ{9a-FTJ8&B6Rc$Gvs?(^Xe8&7G<0(+gLcoTZBtmDC+c;;_&c zfDnE56JC|zpd~_AC2*RxkfArQ^}B%r=-eR~SV>$o)`;+&u65{H?yH}#LAEW&ll}mO zwvCt8W@&?AT$H5NTko({?+q@yjte-%SgWK?XEiF0W|qbo1Rsvq_Esj|be`62+9m>? zby`hdyHP*2w>G!v&7JQFT0be-++OO_H!^moC=!HJy=Irz`m61)!tScBufr>SybkI~ zos8XwI&X?Kn#_2L61=G(9n^<^Y#c@Vgy~e^KKTZJwz5J;3CKtMkDt7G(nz4QImvqu zhG_>u_ka!q{n=?MaLr!&nL`?}=$1RS>;=N2lR|yB8VNCJ^((%YGe^8WN>T7E<6nA$*@r8iKlBhD zT|FJYZhS67a=Nu0VE+yjO6&1-Zu*U+-kgx`cD*tq4#soBLYV^L(a3OGNmpvf#&BxW z3HO666Ij;0T(RW)JTp;?TgJgch_0awIwZnYp|asYNQJ|p!htM*5=189l7SZ zT}6YA_fUY^wHQP@`i{Y05(oR64@dGejUKPHe7v3mW)=)RGyKmk&pE$&Nqx>ZmH3Ri zDCjWG2bQuc`J$#3;}#&CdvYPlR#3%bD$b%g1_udr*y=D0{Kq{-F-=0ME53d}Sbt@@ zUfiC#4?cN*%nQFCSRiElzv;5JpNny)DO)D815ft`?>uL=Zk30Rk8~A|JV7}~4Koj1 z=a--RtkYQZhag-#%k9E{ll&_OcvG&I{zI}C{@;K9&(+F-|G(_P@XhyYGYunN2_#Qkw z*gUF`t%E%Q(<8)^yje+mOtw{sYzY$t>O7NP|Yce_a*NP zj)eSo+bjMvR{PWAy2^Klut2q3Bk7!k|2*Wv1EchpR9kT@;_JhqoHZ}1t@-aB58fmC z#JpkQ_xkWWd8{uk#umWJ^P6_m5K9&fg=bUf=l}CQ?@T}V6!rXm&60irW=8^+@i?E; zTOgjM%b#1nxyhfv(mt7Pq$Z`5RSu7N@aOOIM(~$K>n{vChD1KD{%RzZsXsF}x45VF zA4N}Fdaqt=_qE+my>JjOmum4&VcG8|J-r=j^uM*e8%Z~v`X7-U@8?ZLBW8E;9~*hM zc>n)q7m>gHf3E&Nxd;EoE2Rgs143>)JeAe{tLxXn z58^Ws*&|}}4DZgAzD%E%s6RXuy#D$Kz}wHo&IeBur4qSHmjY)a-rE7St5lThl(t)d zQO+E|c}~)sI}jLCf8JR=c84UH$!KZ{R*K?qBy9aRE(S4sKoNkZIT_oR5pVu{BR6kl z(Oa{hvFF&JYt#5?0*|*LQBvIDLcd&282O&Y-~j|9GFf78!;+NdDm=$r4jb@TPBT#3 z9XjtRq3SjB4$!o&^P5oT(#ik$r#|!OcZ}C|6$3eQ#@8BH2??4G(|3TIWT{0CWnf|Q zOI%_m)o9V35#l0p686&xvCS~ms!YH7`rqUfa5xY!^_cyTqv-7twUiM+Iy}xLRyAOy#wv)4<1v zMS~u-9|nXU31)OT4L>Lvc1dSEc9l$iC}dOX`NNw>F}KKL9Q|*cpNFP+35`H}LJ^uK zspz*7(T2)BNys+Zt>S0BJ_F9~OcfK^Lt!#>KdkDUOTAm-q!FZWyW=qJVY6#n$nnY0 z{uhhP&fYYE(?@Rge(vdbuj+lK0t5YVaF23mHa3;JAoicP+8CxGu6u+B6V=1t4RvDn zH_qAHBujTn`B8&aa?lT!`mIG2?$!K!XYkIWTWJ>Z>e;#2QmO;{>EDY5 z7k>P|e60)y`p@|AL|tuvTskf}6V0XH$3$~}fZEzs_Y^nzZ2pVko~^pgLsvKoXNEY3 zTr7&7DQdy~O`J`vZj4@JMx5K`3yO$djq!eiN<6AWcE!6e_i0X^RT0{(4{|xt5Graj z&~Sn@fI7p_SU*7**Le4{`0px1hrrr6-_o#QpKc=p~8}D~8KEh<` z>{ZPNpWmP?zW))dv2GwLqEIfAQwEt@qyBU%y%`;hyS5PKyM65vovKTnJ8?6h#ynle zJ@mAF*pbNM8->^9zWV0&b)fFFcP;YDwDZv~C)K$o3;*3!NYd_JfUh8hJCpfH1UhkZ z{O483PU(yqNE+{}J-alA>(~miKLhJjU)Z|yp#vOl-7!0VwSTf!eA53&Si@Bd#xKQ3 zSM$>Jtj@uBl6hlr^GBK45zcQyt2CmD-``D>A3u@dr<`eMTR!wKBnWzDiExtEi>F~g z@88?i-TiZZM6HO<_t&POxSLDg3@M%%!lewgp`6pdw&sIZcqeQ+gQ=x`2R1k*>IT%#=&$NG&&8U=MFZUQQIhw$S`6EBK27=(e#| zq{X9_lyi`=Z-`49sm{7wG=^keXt$ter<3%W^21*sQ+)|{h6zev_-_@|Se7M+4@ml)T(eIB*d^&dh zq&QVv#MwKw`_13unm4#4*)hdxc#LoU+mNb=N#fNb%&V`ja{?3L1YV83H#I=d;GUs>wGBJ$^NPV{oWnu`yELFh>WX+rBwwLbAxZ8= zAF9Us*d3MljWE{BzNOSJ#DZ(4#rdJ@*X-+^g!=InB77y$5FA-(;R7I3VLB)ya1n-{^V?9Y0&PxNauo3cTGgd(A%RRm9`{ zob3xCk-{pc^}9T6BK=go%VEaOl{cNqC+wN6vgOV%4(9*JKO^I`F$U^72Ut_ah`t#m z`yn#)n@!0*g7FjRx?EJ*`lOo;`2Sw(rWb@woWmC51 z15S6;yPyOH+T71g?i0wre&>bI!Ll9mP$tZ~Q@jrN^!S;3D(0OY!J?{?uJvpS7Na|w2A=+SJMrMbu_54s z1i@a%D75K85))H|W=7YqjidjBD?ZDWdDkiViTG!Z*Pkoq;lu&rTWjVffL()6*FHuF z_HJ3g50OB7_S>FeCyyU9mU40W(C74#R^@Udo;2%{H|&--(||Mb4cX^N-kbx6Vwf38uX4Bkj} zhW#NVy6IVnk>pyzrSZR$E1hB%o9U-95fCzC-3_X zBj$nUJ=Dy#n}zf0PMmk*Jif98n{*R5UAjKJdNe0^!SyubSiVH<;HJ`wn6)E6v;V!7 z)Eanx{nuaplV^+M+G zg^y=rhbMSB!GE%UUQH=@?)KuDl`{S7r>7eYEc`CSb?sj^W}=F&+P1#XTj`}C;ekRjcbIkQ2(KHJQ)v>24!gTAd}ms;-yho2)bKe49%mRn*7&mQWmA;@A9 zv8H!x_`HWlb0Nx&RIh-_^W_Ac#&I#ce~}jZC-+Mm&eE^b>o;OXQMJ34C(b<&B4YxCy=7OVn@GKkH;nc%*Qe+IR`FEt^|WzldbB$ z)Q)FtHB+LYD^dsc?fYJ{6mys&owIw4i>=Fczt~b1Y%oM^Hc{^_fy~Et-iM}y2-_S; zV6&|hOm762&@oJ;tN(6bKuB5REJ?;Nrc+)OU{-AQc_|$)@iSzK+Qd=`qX}~E_23@- za4r3r8?*VsxJrC=r?)cif&~_r!x@%z_3$u@w>J0d6Jbwi23R^7he8Y_oY-FJ6Ls?* zUyrW2W5NTz1L+0#|EBO>?D!W?s!v@5xoUu$EB`eQ-N0Ga3$BRz=YU0kGAnx4PEH6KcssZ*#-Wp$ePSWR~c|cCZnhA((IABRgPfdJM*W=RQYO>M9 ziSrK!mD&rlV36eO42{ijqAp-U+B+;2Id#9Gq_R^}GERwR>ojwz=a<`2LHFR`ABEI(}MoRuPWb9g!SG+^1rTw3x^M`T(X(_iTv-3LX~rz@&h9Ab=vX zQvoRxpi_eHj*AZ+qwaA7_I`$!(0f2M#7&bMK^&(oNSR;k!ChQzXAX0Wx_d?G>SO+W z`|1PhHPT@Ux9nB=(h0PJM9C{g&kJ^dkKQMY8G+s(th+^)-diYpwsisGKcheD!#Hlo zl_v*pflO!5m}51|GEQRy@CO%=_w%?&tiB_N)LnelX2_;(B;gUg}&1yj}Qv=U7{*2mn0PGbch3{sn z(VQ{!vqlzX(+GkV(e33B`RO%a>L#~LU2+dB3fvanzi;2%R5?P3kymHOPA4uk+ob<{ z3K4o`+C3DAs;U1P5Rc;|W$-3coRVYQYmE*$7=g^S&LAynk3?Y@DKY|eE{Fs!5s2Ho|`0IKH~XF>;?bq)KL ztR$Pj264qcb1ITk?%21Vs&knQfYauF;5|5ST!Z*c%sikcaW4I}_V+Q`UXNGpiG~qCI0%imPf$cfUPHyo^ovLu`+~#S?twbD>7(|D$`dK^1JOQH+ z`lNHf_KFENtEX5)b?diqcV$yJ&nx$NxW5G5+@M8#(0bwuLOBjUp=9^=p0~-Xl<4Pus z8y@cU5E}$u4P_r1^+8$|8?G#a*`93XVz}k%S{+NFeh38U77#`-vuT5=fNS)CjW(FK zKJ0%Y&6=E@MXk%g&8`CcPa03?oz@w?`BQoT5-LE(Y|3I5c+8IkAX4gng2$CLj74Xn ztK4R|pzJ2z_)50?dOtUhp>0qa1N=)%PvC(|iDMt(GpU=_a7kf;xULnY8 zhF4!7Yd6_biPH1m9hK)a;?-&?An#U#FC@M)9EJe z4!V8{nh3iQma@jw;AX3yG9kfrepEi55}p`Bu2sVoqHexS-m5OE&!}Ilw!6qF9ey5d zztc@EA-(3xQTKqhSYSY>zxhtb8!z@eXjBQd`)y$@jzXRS$g~>B>nD@QHvG@{MOP_Slw>H*i_Zf7bmTifKr+pIwZ<^fM|x+`3AR;0t@W$Ok7GTz^vqW@hW__ zLZHj%@z6r6tDzCaH~3aS1ey$|mUM0b_FFAOQ{7#C?C;iO1cJhgHgB3tS)2JTSOui? zyUnj{xviy{-0+=X3=qnEy_xE8YaqlX9|bS_2RaqSJ|_;L)T?bdn~CAbs^O2<P_6S6QKF;%@ueK&@=g>Qw<25fXJe#EDMc9M612wFIl;&BL z^a5vfu?Fqn#qT!BmhSTJq`U2ImzkAhWZ%lyM3KsNvxUvU;K3US?AOD@!FteFH-u6! z#)&5yUc(Rf(K)5gXdqzsNCBuG_@wp-<&LE?Xsm7UvoHZuR527u8Sy*UoXQf29Mz<* zO(wzHlpex`$c;=-HD&zh@MaTS%2d9(lk!Bay(|xehotJ3VT{QHEli7a6|;4w1!r#P zN7u2FD_v^WE)Cw9k5&!KiV7$xopwaXE&q_@lq1Wh{bsow2GR^!Cpt?GkX!eTY7Sh@ zjg}a;NG8;sbicJb31eH?J+x?70P3R-^AL|^w5MnC6>S&vKM#5yZp|@t06*ul=%F;l zvd6em%HU4G{k^Yg@ThJ30dq8p*gyoMs(vLT?NYa{S`rf)>Tl+E4}SBG-~828vbzgkkN?-q6!4}H|$d6>Zi$%MVj_i@>f^?Q~?!wA_=~S^?T9FsP_jbI^Qb@9p9=@hn~t+> zuUtY;JOKlRF3~}Y#ByDRJ<<|Q%E8`lRzT0WFa`j-FDp@>XsVpxtnQr*mZ(7OnZ?KN9av*MA+vn@U3}N-W zslx%&m#|@Yzm}#ESAX|V@Ri_ z!d8xFhfuE6Sc@M^#4SpIG%h6!xZQ+w*quGf`r@4_mrN}hmwSz3dmtw_G8Hb0PWcrr z8;2pdqV0LpTLa|^4U4tQ{ue7>HYW=o6PR#vsT*vz>Uq$xUR`>DJItfLFEn>kf6Gs- z-1KN(55&LF+#)5K*8cD~-Ca~vWcixKX5caN{1JwA6$JZknO)|SCv^iwXGdHC%18o< zZ{Mr&`=y?bYoSk^rUBhmEjFWq4J4T9?9~PW(VWpZ2OX?6-gJ~OOj*);HGR12B;Z+p ze->TH3wXV9+#Kf`>BN=Km*}|C zvEeuraxvT)Iq6m#!?XHe#`*;ENiycdI<>vSc&ekAqI{m~XY;#-8ElR^vN2!_xBc_QIvJw7lJHM(2%XzD3b-t$_vFL|?d-k7uj-Gy>d&+7uI@qV3c;bWlj04U&O3%?Gyt&j+5EYdIPYzX4S6Q1DCHyoxlgyY<75A z+5rnaR^2HynTPPZM7id?5V?&cujgV~liK55V_$_4D(DPvYA5wHb_;OaHDg!cb(iH_ znE!Z`i`{~SNQtnGOU~T^8*00mhF%#&j^eZ;@vpr;X0BRY|2fwT~E_vGm zY)Avrreoy`DuKJtE`5CRs#>O8yvHlkJiO--1}zAA$#+R$)}o>AgsuDyO%OKpLsVEv zQ)<;P`Pp13!3&^3pQ201`$raUB9Eqqw5#nkwZ~H6oC>tgrG&6dgMlzuM==r(o-#~rFa=p9 zdTttcLFCL?WXe+OBLRZ^S3!eDMJui|UTskoLS()I>A80k@8jx(B)^c<8K|v_akmod zlCnM8(}1eWVCdMwC$MEdqUr{yb_S;U2FVRwBM>K`b}|;O@{z1|vsk$gHrUw&=R1K_ zw_VSLhPg=2#C7cs%YC*cbv{*K?q2i6`Uq4Ird+!EU1hP{%>V@LS13S&WY@K90&sk} zA07sEG6wP@r(I8*M2LTc=eY)N&nv>8=%2sUGAsbCI6=L<3zdC|d-=NpMqyI*-r*IW zz2xM3s*13#0iT~dtxG4%&#^2-@X->ihiD{(AxqUs3`0K-PynP9x)yMY_A`c@0+xQJ z39LzV4>wbu_-tehfA_QKv=bIrv%&~sO2+n97pmT=?D)$E++u4S>=d&%paow0VDn|x z4YN0A^i4Rl=Swqmzl&9$ytk8!9N^9IS^L-)g8c&|ie2y}lw+arkNjIZTf463V*B=; zK=a;K%?4lNHJ!vbL(os6x*3jn(EL1L&TeVy5~>aQ(RG}k)OKhyu%y_npfCnyWz3ZA z1Z{fCV%>1;^m7*2DB5VCS&h{iWyQi3SRH!+R zKFP^25UQTwI2R{}5^jc2^CsG-!#Zg1hM2n1*B9=3eN0PxnuH}FhmPNx^B#|4PEABX z-)71}M&vGo)(SP9+RDfRRa^pU4O^Tkq8Ea8F zB8;XK9I({95{-Wd>9erJUd93NZ2C1MY+)_~#?m*b8Lp}{!c60$JS^>C zYNcfY6XuqlbxdOLn92CXN$cj^f`VY>N(V!uwXxNtfh(li8;UAr&UPzmyS2?aOVg4v z^d!4ckb#>zlHQ0r?*dqS`io%}u)lc$zAiiyyYZ1dNQGL8YY`#tZb1SPf zV$n!F;erqtnhck*gA~m0`vXW)0FL|JZX114D;PbGnti9~?J_`aJl*YvYB2MZa6$%6 zs$(YkTjs7Z;*WYYR)4G%>a_o%d&{dsxUP(f!=zOtim@*`gbhkQ&&~NAP04rA`57DM z>j5NHme@7gc#iPF&f9eGi5d>QDK9r$m5F=cj!mX4r{GnLV<;Qv$NwC9PRaxwVWgS_LX_@;>i2-nFe}7D6J$Bi6m}O zs4~|0+^Mc={4MMOl-$7@J^3;9Sr3K!M_DD0#ps?&HM<|+H5q8y_PCkhv*x|lx*EnH z0)4U5IQo4fbkDKQ2_Y8%F0~KO`Wj1@ub(C{*D1y==7W%QB;~%m|NZwR4V`wDt93`h zh?1EB5Ke=M+11;IBXFZoQppsg(+J$+5c|S&tB7--((krv5U>sqPtSCk!d)G)dm;RdG z=MA9-FnVNTqt&U^UmTz%TSeerSk z;)ZWtW4`B_ij7o965hw8q%Mb2A^FzWil|$fv|o}d6QEE_a1#@HQGV1W8TX==vG*%2 zL2%nAM>v_rd7h3P6P>&k2#y%^6QY-e+d6N=%6~xRUq~YMW4_Fmm$VWfu>yi(a;yCn z4MEnEJjrp|m2jVv4KZPg!Iy@2w#EGE#S;ppxSLf-dUGn_@vQgTKu~B&?^E~Ak5c#j zuOyq(eQ*4FTJk{TtK7b6b|&pD%sbbE+2jlFEqDP-w3~Pm0OiS^EkRkOrL@ACYT{$i zHt%~bt@AW>>TRQWlLkx%#^Q`^8=QhMjkeW4Cm2Ei3w2?r+=p>7YkT|cW*#e^DVo}Z zTm3e|i;shUDjc>;gPHpUPcq6Ajd^VZ6sIscm=l$=hchOMh3|=Ia%s}E1*P!9kxq`b zA%Ma|c!v2EZVi3bz_10KY%OM(J;l)v>@X6|5)oayzzAnpOB4&im_fVP|*><*ze*V>%sji&qPp7x?h4699A%jZrQJ`io=n%VHdRfPc26j-7+c-&a+kRj96A-yjE z61<&A?XESS>1%7LBl0p77Pl_8e-0zNvl^H5skr)Oz7kloyt__u@xlRrRLzR^UfLyT zFnq*nc(TfQ)FrO*(8UC=f ztSEjba*CUj)KnNQ0G?(+d zu@IwM=sh$9PFNM>Zc&RLv%IG&oMG_%#S8;Nz_J+31Uv9iLHCKS*j1yxLlhaC=Xyfc5|A>Mso-IYe#x#2qL3FVpAEGRM8bh$?^*EzYgG@eWwK2h=Hp)EgE7E^Yh-J@dDnLN@itcsG~LFS=mr_(Xqm znKm@?I1XCmr4f>Nd^RNYt#pyyj`xnMl0}rad>z)dca%)u^D6NPmnwW1#+&ArhMsjw zNmg&V^f2j3a`rf>pF1Buit`RHI^Mh?!4Qr1r>k>yieZl(0hg`9Czi&@^{aM`XPgeJ zI3+TduAOSJSNW>e*5A#XfspqQe@<|9n3-_)D;uwh70i6%c(n->^N@$N zzU%Tbr-^ILBi?OhwH=?kscfTsb~h@qOGT$W%3q9OU;HEy67KjlhwLKCLJF5Jep`@6 zPqz+Rpz`UPp9E)2%&x%KeZsGNgJc^)^l(N!;?%3NBdM8#9Y-EmkP@@`N znHDO7DC-talODmpTg9q>OJT&Htv60>U6L@pIRW8VYx@`Pk-l5(PLSCXi=_*k!MlR1 z+7n+hD?_|;!(@dZs7IG2od+;!ib*_^&zu~a^D3l5Unb%2LDgcTNS9-=0S*D7T)}T9 zo;&Jb_;zrqwvC0UL${bCY6028#K?rYHXRE9P^ZD+Qj-K zP^2-drr~#sj5_V^P3#3B>^ydf*{0)R=h+~GdN)=(z0PK^Ix2VUpl<_szWS*{rOI#$H*;7b+q~9A!(fLgG@taa0WrD+g+yONH>rWMz&GZJ#v*UJ`w>M8>J7y`I zXX}=yl_@3~Jacx?%x7qT2*NrF~UTR5sZ;>BbWKD?7~d@VAWfve-b zk&mjp9@b!m>UBpDZFb~-REs;7$Z68i-6`pQ=D40&CNCNI(#Zm4J_FMCZpwzynx+-k zn&g^H!+2*H&vzXHllLOd<4knnk#)tVb7*eLHz&ObEi zhOc_?hXrLBC0WjzfC-$ZtIr)s5u^S~&dwKD{XWlRdRfg_J?rW_Re*EM`mAd)&W&kj z7LV*F=!of`E#RH!3Uy`M@Jg4~?939hG)gFOw%{vzToe5p&A_(q=xddtnwLb1c|{qmL9W_MDg+Y#fQ4+PO_^r zN$lBDvbN8V+Z|_N!N9Q!kcfhIsD;NrPVSGZc-Z(8%TV7U9%<&rsrFnq7>Y6NO;@n? zwdSBk*=5V}w;zLjD(H*&scT)$_rrCk+gm*wP^(K`NJWeN;d>jMY(}Qs7tQggH=58Y zoC!}7yE407{>VW-M*C%jecb}bAo0IxnB(GjHF$uNP3N5y`Yan7Mq)rsaARw?_1~G7 zl397<&5&iAT^fj;R#_oN!!&U_13EJ){z6*`$QLZSuzPy3nVpUngS}oXFx2BG4rl{} z3GoJIyBHZZ?Kt6zWxX+KWU6=k`#c8>UkQIuNyqpnw_sroOzYkKbmO(!m73|jLN$Gm znL+>|^@f?NLGdueQX9oKE~M9&wG*AtjZu&}69MSUn=_o$=gOOy8y zN4$|f=%0$nsO&^q6FzjSPnLY0fe+`V20dyhRXqSNh}QXW%2B=zASQBA z?^oyD#y6!o0@ThIFTzkUHHH4xv2a#=#&vv^S26@*^diY+^9fe>2PUtk?SdZBxv35z zgGc`A)I+4zQeJ~qr4R)SbVv9+my1@2}Thki~`1E70UiGAqVK*Hr6Zs%l7H@=G;S2;{Xd z;9S)&kN|4#GOz@}NzMzMnai7Mp^}xJ+H3GIY0hO3@IjaudkENQ$}5&y7B-yD{2(l# zlsWlTkt_3FsGgtbBmZc`{ZbB*SixAU=6v*;|G%E4=Tsd6w^?myWRL znu#>ulfq0>OWsPMj&rMg^`+fYF7FIRdl#s#EuSp0!M+t7;>dd^WLU_HK*J0p;4GsaePS}iZ9m=nK6!r z*K-@|_T<26v;pLCk~>75+5JBK35OqvS} zjlEtt0LlLZUug9sC5wcuj=-=44ttS!@#Aq^wW)VKQ&*F_5G$XA%BU}C9T*sJ@$KB7 z8m)vM@QO7~dT8!bWE~gV8)KO53xIDyqfsDj;(g;~G~$xadkxTDqxy?+#<+BVCA+#W zeM)+UY8Qo8Tek7pTLZiI6q}j#O=a9-9y`W*|G|p;jX?5IfUnnVu-^p!?s=IJQZ#=t z=n35QvD}a#Q46PJLuPSYd#i8tYiB2GAE5gRmf>SU0z**2!kOq$fgVjp*3GhAMdBgv zo8Rt63%Un+nDqr33t8&HH2t)3luFo={ku?ZgZYSDsB>))_D~>z$R;_<5}qP%;mTNr zs4w*_&wv-`uO1r%qZ#bVys@h86qtqc>erC6*Dyjmh;LWPquPJL6`Nn{o!{^Q9q9K1 zrFDt7)wa`R-(pb3#%D*5QFo?y>CVs=%jqRYe{*NRKeP}P^iXSccA%NZ*I(CPqMRKx zAB!>r1C&v61wnG<5l1Y|7NebibiAyD&p^)&wd?by zUV=I@@iy5j&%|yphY~1IyrZ|2bX8s_PPOhA`^xp^D(ft6Oh`|aTTRA8 z*}D&C_#r}F4UZ7RhPtM9V0XXhcw|kCJD^mwM*0>Pl0$^4aPoSlGjG^atZZJ zmQtwY^WeU1kaxpivxk6|`F5NQ6=gn*a4N*D3L~xu!b(2XWoVLtL0!wXa77Jb5&B*- zT=#x-!1A`N{O6+fF(a)H0Hjj=YEs3VmwG|Bf+Rew;n`|=W48+fN2hWki6Vbe{d|#=L&LOZ;B|{#^ zFy%gxN-h+{U^B?P4GKKX><&^KQ=~@!T7ImrM1WfBV49O~MZ8g;xt@7Ck@G5VUbV!gL0`1d z2EImv5s7Da&u>ao-eW&z>%0R^3h_L`o$=-+LN#M$;CGYL8UOY}JPbhLS^*hgPHv+L z`J&M%uP|Zaemei0DW=Cnv2`f%jlp&&cgZ-}LWW|29qg+2POYd-Uux%nTQBNPLFdgbq@J$f8cc2`bz2YApTM8}_6uo2<8^NTvI+Gi1%p%{Eq6#V&3{+F z_NfRNyi`@i`({Gfby|pdarIFT<**NB_i8WK9G=VeGWG=9*n7DKyS6B(#4pCcYZ_(W zTH0cp3z_Lk?6;fPTSO=KI6)Gh5fr;ANSP5_JUY9u*f(x!m&9QLa5GiJ6S#RMV zxl_ctGZo;s!n@5^V*66NCBQ=Q8t?>il3P;yaRghbT&KQ{GwSU+P6l4#E$gY0<(gS~ zp8=>2BnaF^Zg&Cq4()CNpaXTy7iz_dJza>}sQ9>Q;dD1QKb#bBwqp4;WKCu*Irr@g z=K(m*+LxjHyrq$#>RtH?R$Bcv_0Fv2FVC|DvPm`txYwZ`nwzUe<{g41&(slg-}nTT zFnz=8H}W>3_@F}`|Ii@ITb!jkzo5Q^N0}1}*;-^4ftSrJ%nV-}Hx~o`%Uj-@?Q3{_ zDxEToOym%OL&l+VT#F(Py^7(WaOu%(RH2!^qhz6Hi~+(L?4U&xFi%ZQjme>1C3!cQ znT<+en2!|=JL-l{|2?bluEue<;)DEG0YUFoJJ<4Ycr?*D_Iu7f)Nu zC!_;BRoi)@lpdx9kx=^Hwk9wi#i`@obK%7_-&6h)nb8NbRXHhIoZGymNYUIs?R{v-le1@6qDhLcQ?`m@j~t} zpSl}{{`9pghLSs4mhX|@rtR!r$>2{Z;8h&$R;^N4@UV=+3{aXMfUiPV*ER^Wo6^{W z+_|EJ3mgIc^;KWVO<7jHp8BXxsW1rMboc|Xmeye$M;+O@vs}gNU7Ks=mz>dO0M~)c z=_2SBZ1pWkHiUwb1Iz-d9o-U<)i$U&T1^o)QO3jh^uZ*M*6oQ`bz8FU&dcPq`18y^LwPmSl_nYwo)Lzg*UP68`7K35#`^ zId2xYxqtt?O%{|qw(fW}=}C3{rW<=Qiq22As{12tRvEjI`$kydcGp#A%>Sm&ygbXY zI7T`7vDc+r4*LVPeTsb&of31^_WiCuu^Z<}iXU73Ze!=8SEXIrhmRWH34W9nIMe^q zyr}f}p!|pWIfwQfQ~$pELS4rJ;5RXrg0GIy> zg$K^Be7a-xG5_qGr0w1>JvRmh)h`cuy!_M;!O61rwrblqzN>uHRI_x3!{ygIlecvV zPOLQiW7xY}YNpiXD-X-Fv))SsyOstJHlpv*uHXZdB*j5$yJq)G*`~f`u3*q z>|EQ03m+OTz1qhA=IPlRR-wQaPyHU|4Dk{Z(16^YV4tOU&=CT^qOjsoZ6q zXNTN3uDYry{=q$TeaPMS(NdE4pF91QJDM2_blZWaWoOxd%c>a^R29gPfVR=#C(eZqSxW%>&X3 z^m85Ef??F6(MTOl*P~f`v~(LS;OSfgk5)ayqUu@65Mv5F8T~IW!x`YIzWW(>SP9%| zTzvqvl>;;jc~X782m?>m`RaR%`D-39-`bu(pP@zflq@61waw@4R)ZElhumV!IkoCt zzs;u(;BBco3=frdFf(rU`VBmtS@FLp!?~kc@A_@ONvt(?I1n+1i{;Jf_ zbrB4Qj>(ii&`M0cCs+TcP}aUKh9P0u^5uWetqi%@@>=w<)_ literal 0 HcmV?d00001 diff --git a/docs/im/images/@人员状态.png b/docs/im/images/@人员状态.png new file mode 100644 index 0000000000000000000000000000000000000000..d0491d4847cb3569ca66317c0c1f0a5159815940 GIT binary patch literal 140998 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%0D>o~3B0k$B+TX>X&+ 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 UKNhwDH-cWOYCo$`c^mS-0Q##PBLDyZ literal 0 HcmV?d00001

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%0A0wC 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 F{ts4OyX61? literal 0 HcmV?d00001 diff --git a/docs/im/images/个人对话-发各种文件状态(1).png b/docs/im/images/个人对话-发各种文件状态(1).png new file mode 100644 index 0000000000000000000000000000000000000000..031484478a7748ffc0d6ab5b247a13c50f114a1c GIT binary patch literal 86467 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