471 lines
24 KiB
Markdown
471 lines
24 KiB
Markdown
# IM 服务端架构与新功能开发指南
|
||
|
||
> 文档状态:按 2026-09-08 代码实现整理
|
||
> 适用项目:`im_server`
|
||
> 文档目标:让新接手的开发者能够快速判断一个功能应放在哪里、如何调用、如何存储、如何做多节点一致性,以及如何验收。
|
||
|
||
## 1. 系统定位
|
||
|
||
`im_server` 是基于 Tinode 扩展的多租户 IM 业务服务,同时承担两类职责:
|
||
|
||
1. IM 实时核心:WebSocket 连接、登录会话、Topic 订阅、消息路由、在线状态、音视频信令。
|
||
2. C 端产品业务:注册、安全、好友、群治理、收藏、文件、工作台、会议、日报、举报等 HTTP API。
|
||
|
||
系统不是传统的 `controller -> service -> repository` 三层单体。它的核心是“Topic Actor 实时模型 + HTTP 业务扩展层 + Store 统一持久化边界”。开发新功能时必须先判断它属于实时 Topic 链路,还是 HTTP 业务链路,或者同时涉及两者。
|
||
|
||
## 2. 系统边界
|
||
|
||
### 2.1 服务与数据库归属
|
||
|
||
| 服务 | 直连数据库 | 负责数据 |
|
||
| --- | --- | --- |
|
||
| IM Server | 租户业务库,默认库为 `tinode` | 用户、认证、Topic、订阅、私聊/群聊消息、群业务、好友关系、文件元数据、C 端业务数据 |
|
||
| Admin / HotGo | `hotgo` | 租户注册表、系统配置、字典、后台账号、菜单权限、后台命令 Outbox、管理端摘要数据 |
|
||
|
||
原则:服务与库一一对应,Admin 不直连租户库,IM 不直连 `hotgo`。跨系统写操作通过 Kafka 命令/事件,必须同步返回的少量查询通过受控的内部 HTTP API。
|
||
|
||
### 2.2 租户隔离
|
||
|
||
目标模型是“一个租户一个业务库”:
|
||
|
||
- `tenant_id` 是请求上下文和路由键,不要因为租户库内没有 `tenant_id` 列就在代码中丢掉租户上下文。
|
||
- 所有业务 Store 调用必须显式传递 `types.TenantID`。
|
||
- `store.ForTenant` 和 `tenantBusinessAdapter` 负责验证租户与选择租户适配器。
|
||
- MySQL 的 `TenantRoutedAdapter.TenantAdapterFor` 负责将租户路由到对应连接池。
|
||
- 任何无租户上下文的业务查询都应失败关闭,不允许回退到旧的无租户查询。
|
||
|
||
## 3. 技术栈
|
||
|
||
| 领域 | 当前实现 | 用途 |
|
||
| --- | --- | --- |
|
||
| 语言 | Go 1.26 | 服务端主体 |
|
||
| 实时协议 | Tinode JSON Protocol、WebSocket、Long Polling、gRPC | 长连接、Topic 订阅、消息和集群通信 |
|
||
| HTTP | `net/http` + `http.ServeMux` | C 端 REST API、文件 API、内部 API |
|
||
| 数据库 | MySQL + `database/sql` + `sqlx` | 租户业务最终状态 |
|
||
| 热状态/缓存 | Redis 7,`go-redis/v9` | 在线状态、Seq、幂等、群热消息、对象缓存、关系 Set、限流、跨节点 fanout |
|
||
| 异步事件 | Kafka,`segmentio/kafka-go` | 跨系统命令、消息异步持久化、审计、缓存失效、失败重试与死信 |
|
||
| 文件 | 阿里云 OSS,保留 FS/S3 适配器 | 文件对象存储,业务库只存元数据和引用 |
|
||
| 文件安全 | 阿里云内容安全 | 上传完成后的异步审核 |
|
||
| 离线推送 | JPush,同时保留 FCM/TNPG 插件能力 | 离线提醒;客户端重连后按 Topic seq 补偿 |
|
||
| 音视频 | Tinode 通话信令 + 声网 RTC Token | 呼叫邀请/接听/挂断与 RTC 加入 |
|
||
| 认证 | basic/token/code/rest 可插拔认证器 | 密码、Token、验证码和外部认证 |
|
||
| 可观测性 | Go `log`、expvar、Prometheus exporter、pprof | 日志、运行指标和性能诊断 |
|
||
|
||
## 4. 运行时架构
|
||
|
||
```text
|
||
Client
|
||
|-- WebSocket /v0/channels
|
||
| -> Session -> Hub -> Topic actor -> Store/Redis
|
||
| |-> local sessions
|
||
| `-> Redis fanout -> other IM nodes -> WebSocket
|
||
|
|
||
|-- HTTP /v1/im/*
|
||
| -> protocol middleware -> bearer/API key/tenant
|
||
| -> handler -> Store mapper -> tenant adapter -> tenant MySQL
|
||
| |-> Redis cache/short state
|
||
| `-> business event -> online WebSocket/Kafka
|
||
|
|
||
`-- Upload/Download
|
||
-> media handler -> OSS
|
||
-> file metadata/link -> tenant MySQL
|
||
`-> file security task -> Kafka -> Aliyun security
|
||
|
||
Admin / HotGo
|
||
|-- Kafka admin command/config event -> IM consumer -> tenant business mutation
|
||
`-- signed internal HTTP <-------> IM internal read API
|
||
```
|
||
|
||
### 4.1 启动顺序
|
||
|
||
`server/main.go` 是服务入口,主要顺序为:
|
||
|
||
1. 读取 JSON with comments 格式的 `meetingim.conf`/`tinode.conf`。
|
||
2. 初始化日志、expvar、pprof 和集群 worker ID。
|
||
3. 打开 Store,校验数据库版本与租户适配能力,同时初始化 Redis runtime。
|
||
4. 初始化 Kafka、IM 内部 API 和 Admin 内部配置 API 客户端。
|
||
5. 初始化认证器、验证器、文件处理器、Push 和音视频。
|
||
6. 创建 `SessionStore` 和 `Hub`,启动 Redis 群消息 fanout 订阅。
|
||
7. 启动 Kafka Admin Command、Config Event、Cache Invalidate、Message Persist 和 Dead Letter 消费者。
|
||
8. 启动集群、gRPC、HTTP/WebSocket 路由和优雅退出。
|
||
|
||
任何新增的基础组件都应明确放在这个顺序中,初始化失败时应快速终止启动,不要让必需依赖在不可用状态下继续运行。
|
||
|
||
## 5. 核心业务模型
|
||
|
||
### 5.1 Session
|
||
|
||
`Session` 表示一条客户端连接,保存租户、用户、设备、认证等级、已订阅 Topic 和输出队列。一个用户可以同时有多个 Session。
|
||
|
||
登录和断线时需同步 Redis 在线状态;心跳用于刷新 presence TTL。不应仅依赖本进程 Session 判断全局在线状态。
|
||
|
||
### 5.2 Hub
|
||
|
||
`Hub` 是 Topic 路由中心,核心通道包括:
|
||
|
||
| 通道 | 用途 |
|
||
| --- | --- |
|
||
| `join` | 加载/创建 Topic,将 Session 附加到 Topic |
|
||
| `routeCli` | 客户端消息路由到 Topic |
|
||
| `routeSrv` | 服务端业务消息路由到 Topic/Session |
|
||
| `meta` | 未订阅 Topic 的元数据查询 |
|
||
| `unreg` | 卸载或删除 Topic |
|
||
| `userStatus` | 用户停用/恢复等状态变更 |
|
||
|
||
Topic 索引不是纯字符串,而是 `types.TopicKey{TenantID, Topic}`,以防止不同租户的同名 Topic 串数据。
|
||
|
||
### 5.3 Topic Actor
|
||
|
||
`Topic` 是会话和消息处理单元,通过 channel 串行处理加入、退出、客户端消息、服务端消息和元数据操作。主要分类包括:
|
||
|
||
| Topic | 含义 |
|
||
| --- | --- |
|
||
| `me` | 用户主 Topic,会话列表和用户级 presence |
|
||
| `fnd` | 搜索/发现 Topic |
|
||
| `usr...` | 客户端可见的 P2P 对端标识,入库前需归一化为真实 P2P Topic |
|
||
| `grp...` | 群聊 Topic |
|
||
| `chn...` | 频道 Topic |
|
||
| `slf...` | 个人收藏/保存消息 Topic |
|
||
| `sys...` | 系统通知 Topic |
|
||
|
||
涉及消息发送、已读、撤回、编辑、通话信令、在线投递时,优先复用 Topic/Session 链路,不要另造一套无状态 HTTP 投递逻辑。
|
||
|
||
## 6. 四类标准调用链路
|
||
|
||
### 6.1 WebSocket 实时消息
|
||
|
||
```text
|
||
Client packet
|
||
-> hdl_websock.go 解码
|
||
-> Session 认证/租户上下文
|
||
-> Hub.join / Hub.routeCli
|
||
-> Topic 权限和业务校验
|
||
-> Redis 原子 Seq + client_mid 幂等
|
||
-> 本地 Session 实时投递
|
||
-> Redis Pub/Sub 跨节点 fanout
|
||
-> Kafka persist 异步落 MySQL
|
||
```
|
||
|
||
群消息使用 `im_group_messages`,私聊仍使用私聊消息表。`SeqId` 全链路必须使用 `int64/BIGINT`,群 Topic seq 由 Redis 原子递增,数据库只作基线与最终持久化。
|
||
|
||
群热消息可在 Redis 保留 24 小时左右,历史补偿最终以 MySQL 为准。离线用户只接收 Push 提醒,重连后按 Topic seq 拉取缺口。
|
||
|
||
### 6.2 C 端 HTTP 业务请求
|
||
|
||
```text
|
||
/v1/im/*
|
||
-> withIMHTTPProtocol
|
||
-> API key + X-IM-Schema + X-Request-ID
|
||
-> Bearer token -> principal{tenant_id, uid}
|
||
-> 写请求 Idempotency-Key 中间件
|
||
-> serveIM... handler
|
||
-> store.<Domain> mapper
|
||
-> TenantBusinessAdapter
|
||
-> tenant MySQL / Redis
|
||
-> writeIMSuccess / writeIMError
|
||
```
|
||
|
||
所有已认证的 `POST/PUT/PATCH/DELETE` 默认要求 `Idempotency-Key`,幂等结果保留 24 小时。同一 key 使用不同请求体必须返回冲突,已完成请求应重放原响应。
|
||
|
||
HTTP 统一响应包含 `code/data/reason/message/details/request_id/server_time`。业务错误应返回稳定 `reason`,不应将 SQL 或内部异常直接暴露给客户端。
|
||
|
||
### 6.3 Admin 后台写操作
|
||
|
||
```text
|
||
Admin API
|
||
-> hotgo 本地权限校验
|
||
-> 同事务写 command + outbox
|
||
-> Kafka im.admin.command
|
||
-> IM consumer 校验 tenant/event/actor
|
||
-> event_id + consumer_group 幂等抢占
|
||
-> 租户库业务变更
|
||
-> 缓存失效 + Topic/Session 同步
|
||
-> Kafka im.admin.command.result
|
||
-> Admin 更新命令结果
|
||
```
|
||
|
||
不可重试的参数、权限、版本冲突应直接返回稳定失败原因。临时故障按退避策略重试,超过上限写 `im.dead.letter` 和租户失败表。人工重放必须生成新 `event_id`,并使用 `parent_event_id` 串起审计链。
|
||
|
||
### 6.4 文件链路
|
||
|
||
```text
|
||
Client request upload
|
||
-> IM 创建文件元数据
|
||
-> OSS 上传/回调完成
|
||
-> 租户库更新文件状态
|
||
-> 绑定消息/群文件/业务引用
|
||
-> Kafka im.file.security
|
||
-> 阿里云安全检测
|
||
-> 更新审核状态并通知用户
|
||
```
|
||
|
||
文件字节流不经 Kafka,Kafka 只传递文件 ID、OSS 对象信息和审核任务。任何业务文件都必须建立引用记录,否则垃圾回收可能将它当作孤儿文件删除。
|
||
|
||
## 7. 数据分层与一致性
|
||
|
||
### 7.1 进程内缓存
|
||
|
||
适合低频变更、全局或租户级、可短时间最终一致的数据:
|
||
|
||
- 租户基本配置和状态。
|
||
- 注册短信开关。
|
||
- 字典和第三方配置(OSS、文件安全、短信、JPush、Agora)。
|
||
|
||
更新来源为定时刷新和 Kafka `im.config.event`/`im.cache.invalidate`。进程内缓存不应与 Redis 重复缓存同一份高频业务对象。
|
||
|
||
### 7.2 Redis
|
||
|
||
Redis 用于跨节点共享且高频的数据:
|
||
|
||
- 用户在线状态、Session 和群成员节点索引。
|
||
- 群消息原子 `SeqId`、`client_mid` 唯一占位和状态。
|
||
- 24 小时左右的群热消息。
|
||
- 用户、Topic、Subscription、群设置、群成员、成员分页快照和未读数缓存。
|
||
- 好友/黑名单 Set。
|
||
- 验证码、Action Token、临时占位、限流等 TTL 状态。
|
||
- 群消息 Redis Pub/Sub 跨节点实时 fanout。
|
||
|
||
Redis key 必须包含统一前缀和租户维度,不允许跨租户共用业务 key。写 MySQL 成功后必须失效或刷新对应 Redis 数据,多节点通过 Kafka 失效事件收敛。
|
||
|
||
### 7.3 MySQL
|
||
|
||
MySQL 是最终业务状态、历史查询和审计依据。以下数据不得仅存 Redis:
|
||
|
||
- 用户、凭据、好友、黑名单、Topic、订阅和群治理最终状态。
|
||
- 私聊/群聊历史消息、撤回和编辑版本。
|
||
- 文件元数据、业务引用和安全审核结果。
|
||
- HTTP 幂等结果、Kafka 消费幂等、死信和审计数据。
|
||
|
||
### 7.4 一致性顺序
|
||
|
||
普通业务写的推荐顺序:
|
||
|
||
1. 校验租户、身份、权限、幂等键和业务版本。
|
||
2. 在同一事务中更新 MySQL 业务状态及 outbox(需要可靠事件时)。
|
||
3. 提交事务。
|
||
4. 失效本节点 Redis/进程内缓存。
|
||
5. 投递 Kafka 失效或业务事件,通知其他节点和客户端。
|
||
|
||
如果业务要求“先实时到达,再异步落库”,则必须先完成 Redis 幂等占位和 seq 分配,投递 Kafka 持久化事件,并为失败准备重试和死信补偿。
|
||
|
||
## 8. Kafka 责任边界
|
||
|
||
| Topic | 主要用途 |
|
||
| --- | --- |
|
||
| `im.admin.command` | Admin 到 IM 的业务写命令 |
|
||
| `im.admin.command.result` | IM 回传命令结果 |
|
||
| `im.config.event` | 租户、字典、第三方配置变更 |
|
||
| `im.business.event` | 好友、群、会议、工作台等 C 端业务事件 |
|
||
| `im.message.persist` | 私聊消息异步落库 |
|
||
| `im.group.message.persist` | 群聊消息异步落库 |
|
||
| `im.message.push` | 离线 Push 任务,属增强链路 |
|
||
| `im.file.security` | 文件安全检测 |
|
||
| `im.cache.invalidate` | 多节点缓存失效 |
|
||
| `im.audit.event` | 跨系统审计 |
|
||
| `im.dead.letter` | 超过重试上限的事件 |
|
||
|
||
Kafka 不用于 WebSocket 建连、验证码、在线状态、实时群消息 fanout、OSS 字节流或必须当场返回结果的查询。
|
||
|
||
统一 Envelope 至少包含:`event_id`、`trace_id`、`tenant_id/tenant_code`、`source_service`、`target_service`、`event_type`、`biz_type`、`biz_id`、`idempotency_key`、`version`、`occurred_at`、`actor`、`payload`。
|
||
|
||
## 9. 代码组织
|
||
|
||
| 路径 | 职责 | 新功能常见修改 |
|
||
| --- | --- | --- |
|
||
| `server/main.go` | 进程入口、组件初始化、HTTP 路由注册 | 新增路由或新基础组件启动 |
|
||
| `server/session.go` | 连接、登录、请求分发、设备和 Session 状态 | 账号登录或连接级功能 |
|
||
| `server/hub.go` | Topic 加载与全局消息路由 | 路由、Topic 生命周期 |
|
||
| `server/topic.go` / `init_topic.go` | Topic actor、权限、订阅和消息处理 | 消息、群权限、Topic 内实时行为 |
|
||
| `server/datamodel.go` | WebSocket 客户端/服务端协议结构 | 协议字段或服务端推送类型 |
|
||
| `server/im_http.go` | C 端产品 HTTP API 的路由处理与通用约定 | 大部分 C 端业务入口 |
|
||
| `server/internal_api.go` | IM 向 Admin 提供的签名内部查询 API | 后台需要同步读取租户数据 |
|
||
| `server/admin_internal_config.go` | IM 调用 Admin 内部配置 API | IM 需要 hotgo 配置兜底查询 |
|
||
| `server/kafka_*.go` | Kafka runtime、生产、消费、重试、死信和各业务 handler | 跨系统或异步事件 |
|
||
| `server/im_business_events.go` | 业务事件持久化与在线投递 | 需主动推给 C 端的状态变更 |
|
||
| `server/redis_fanout.go` | 群消息跨节点实时投递 | 群大并发/未加载 Topic 投递 |
|
||
| `server/store/types/` | 业务实体、值对象、查询参数和稳定错误 | 先定义领域数据结构 |
|
||
| `server/store/store.go` | Store mapper、参数验证、租户边界、缓存编排 | 对 handler 提供统一持久化 API |
|
||
| `server/store/infrastructure.go` | HTTP 幂等、业务事件、Kafka 幂等/死信 Store | 可靠基础链路 |
|
||
| `server/store/group_message_runtime.go` | Redis seq、唯一 key、热消息、presence、缓存和限流 | Redis 新 key 或新的 TTL 状态 |
|
||
| `server/db/adapter.go` | 持久化能力接口 | 新增 Store 数据能力 |
|
||
| `server/db/mysql/tenant_adapter.go` | MySQL 租户业务 SQL 实现 | 业务 CRUD/事务 |
|
||
| `server/db/mysql/*_schema.go` | 扩展业务表建表/升级逻辑 | 新表、列、索引 |
|
||
| `server/media/` | FS/OSS/S3 文件处理插件 | 文件存储能力 |
|
||
| `server/filesafety/` | 文件安全适配器 | 文件检测能力 |
|
||
| `server/push/` | 离线 Push 插件 | 厂商 Push 接入 |
|
||
| `server/auth/` / `server/validate/` | 认证和凭据验证插件 | 登录或验证方式 |
|
||
| `server/*_test.go` | 单元与链路测试 | handler、Topic、Kafka、Redis 逻辑测试 |
|
||
| `server/db/mysql/*_test.go` | MySQL schema 和 SQL 回归 | 数据库变更测试 |
|
||
|
||
## 10. 新功能放置决策
|
||
|
||
| 问题 | 选择 |
|
||
| --- | --- |
|
||
| 是否是客户端建连、订阅、发消息或通话实时信令? | WebSocket + Session/Hub/Topic |
|
||
| 是否是 C 端业务 CRUD、列表、搜索、设置或审批? | `/v1/im/*` HTTP + Store |
|
||
| 是否是 Admin 对租户业务的写操作? | Admin 本地 command/outbox + Kafka + IM consumer |
|
||
| Admin 是否必须当场读取租户详情? | IM 受控内部 GET API |
|
||
| IM 是否需要读取 hotgo 的低频配置? | 进程内缓存 + Kafka 刷新 + Admin 内部 API 兜底 |
|
||
| 是否需要跨节点高频共享、TTL 或原子计数? | Redis |
|
||
| 是否需要最终可追溯的业务状态? | 租户 MySQL |
|
||
| 是否跨系统、可异步、需重试/削峰/审计? | Kafka |
|
||
| 是否为在线群消息实时扩散? | Redis Pub/Sub + WS,不用 Kafka 做主链路 |
|
||
| 是否为文件本体? | OSS;MySQL 只存元数据/引用 |
|
||
|
||
## 11. 新功能标准开发流程
|
||
|
||
### 11.1 需求分析
|
||
|
||
1. 从前后端对接文档提取请求、响应、权限、状态机、幂等、并发和通知要求。
|
||
2. 标出数据归属:`hotgo` 公共数据或租户库业务数据。
|
||
3. 选择入口:HTTP、WebSocket、Kafka Admin Command 或内部 API。
|
||
4. 列出需要同步的状态:MySQL、Redis、Topic 内存、进程内缓存、C 端推送。
|
||
5. 明确失败类型:可重试、不可重试、并发冲突、部分成功和补偿。
|
||
|
||
### 11.2 领域与数据库
|
||
|
||
1. 在 `server/store/types/` 按业务域定义实体、状态常量、查询参数和结果。
|
||
2. 在 `server/db/adapter.go` 增加最小必要的租户持久化接口,方法必须携带 `TenantID`。
|
||
3. 在 MySQL 适配器实现 SQL;复杂写操作使用事务,状态迁移在 SQL `WHERE` 中校验旧状态/版本。
|
||
4. 在对应 `*_schema.go` 添加幂等建表/升级 SQL,为高频查询、唯一幂等键和状态扫描建索引。
|
||
5. 在 `server/store/store.go` 增加 mapper,负责参数验证、调用 adapter 和缓存编排;handler 不应直接拼 SQL。
|
||
|
||
表命名遵循当前租户库风格,类型必须与 Go 全链路一致,尤其是 ID、seq、version 使用 `BIGINT/int64`。面向 JavaScript 客户端的 64 位 ID 优先以字符串输出,避免精度丢失。
|
||
|
||
### 11.3 API 或协议层
|
||
|
||
HTTP 功能:
|
||
|
||
1. 在 `server/im_http.go` 定义独立 request/response 结构,不要直接暴露数据库 row。
|
||
2. 使用 `decodeIMJSONBody`、`authenticateIMHTTPBearer`、`requireIMGroupMember` 等公共函数做解码、认证和权限检查。
|
||
3. 在 `server/main.go` 注册具体路由,路由顺序要避免通配前缀吞掉更具体的路径。
|
||
4. 使用 `writeIMSuccess`/`writeIMError` 统一输出,新增稳定业务 `reason`。
|
||
5. 资源更新需并发保护时使用 version/`If-Match`,不允许用 `version=0` 绕过乐观锁。
|
||
|
||
WebSocket 功能:
|
||
|
||
1. 在 `datamodel.go` 增加最小协议字段,考虑 JSON 和 protobuf 转换兼容。
|
||
2. 在 Session 做连接级验证,在 Topic 做资源权限与业务状态验证。
|
||
3. 所有可重发消息必须有 `client_mid`/业务幂等键。
|
||
4. 成功后同时考虑本节点 Session、跨节点 fanout、离线 Push 与历史补偿。
|
||
|
||
### 11.4 缓存、事件与审计
|
||
|
||
1. 对高频读对象选择 cache-aside:先 Redis,miss 后读 MySQL 并回填。
|
||
2. 所有写方法列出需失效的 key,包括对象 key、关系 Set、分页快照和未读数。
|
||
3. 需可靠异步处理时同事务写 outbox,消费端使用 `event_id + consumer_group` 幂等。
|
||
4. 影响管理权限、账号安全、消息内容、文件处置或人工重放的操作要写审计。
|
||
5. 业务状态变更需立即告知 C 端时,创建业务事件并通过已认证服务端链路投递,不信任客户端伪造的系统消息。
|
||
|
||
### 11.5 测试与文档
|
||
|
||
每个功能至少覆盖:
|
||
|
||
1. 正常路径、无认证、跨租户、无权限、资源不存在和方法错误。
|
||
2. 重复 `Idempotency-Key`/`client_mid` 不产生重复数据。
|
||
3. 同一幂等键配不同 payload 返回冲突。
|
||
4. version 冲突和并发状态迁移。
|
||
5. MySQL 成功后 Redis/Topic 缓存及在线客户端状态正确刷新。
|
||
6. Kafka 重复消费、临时失败重试、不可重试失败和死信重放。
|
||
7. 64 位 ID/seq 在 JSON 编解码时不丢失精度。
|
||
8. 更新前后端对接文档、SQL 变更集和未实现增强项。
|
||
|
||
## 12. 关键约定
|
||
|
||
### 12.1 ID 与时间
|
||
|
||
- 用户 ID 由 Snowflake UID 体系生成,对外使用 `usr...` 编码形式或协议规定的字符串。
|
||
- 业务 ID 必须全局或租户内唯一,不能用秒级时间戳直接代替唯一 ID。
|
||
- `SeqId`、数据库主键和 version 使用 `int64/BIGINT`。
|
||
- 持久化与对外时间使用 UTC,HTTP 输出 RFC3339/RFC3339Nano。
|
||
|
||
### 12.2 Topic 归一化
|
||
|
||
客户端传入的 `usr...`、`slf` 等可见 Topic 不一定是数据库真实 Topic。历史查询、上下文、收藏、转发、媒体、回执等接口在查库前必须统一归一化,并在鉴权、查询和响应之间保持明确的 Topic 语义。
|
||
|
||
### 12.3 权限
|
||
|
||
- HTTP handler 负责验证主体和请求参数,Store/adapter 仍应使用租户和业务条件限制写入。
|
||
- Topic 管理员、群主、普通成员权限以服务端为准,不接受客户端声明的角色。
|
||
- 群主转让、设管理员、禁言、踢人等变更后,必须刷新 Topic 内 `perUser` 权限快照和 Redis 订阅/成员缓存,不能等待重连后生效。
|
||
- 封禁、冻结、踢设备、全端退出和注销等账号治理必须同步处理已存在 Session。
|
||
|
||
### 12.4 可观测性
|
||
|
||
- 日志使用 `server/logs` 的 `Info/Warn/Err`,默认输出 stderr,由容器日志系统收集。
|
||
- 日志行前缀为 `I/W/E` 加 Go log flags 生成的日期时间;敏感值、Token、密码、验证码、证件号和第三方密钥不得记录。
|
||
- HTTP 日志和异步事件应携带 `request_id/trace_id`、租户、业务类型和业务 ID。
|
||
- 新增核心链路应增加请求数、失败数、延迟、队列深度/积压和死信指标。
|
||
|
||
## 13. 禁止的实现方式
|
||
|
||
1. 在 HTTP handler 内直接打开 MySQL 或拼接 SQL。
|
||
2. 因租户库表没有 `tenant_id` 列,就不向 Store 传递 `TenantID`。
|
||
3. Admin 直连 `tinode` 或 IM 直连 `hotgo`。
|
||
4. 将 Kafka 当作 HTTP RPC 或实时 WebSocket fanout 使用。
|
||
5. 将业务最终状态只存在 Redis。
|
||
6. 为同一份高频数据同时增加进程内缓存和 Redis 缓存。
|
||
7. 业务写成功后不失效 Redis、Topic 权限快照和成员分页快照。
|
||
8. 重试请求没有 `Idempotency-Key/client_mid/event_id`。
|
||
9. 使用 `int` 或 JSON number 承载可能超过 JavaScript 安全范围的 ID/seq。
|
||
10. 直接信任客户端构造的系统通知、管理员角色或审计信息。
|
||
11. 文件上传成功后不创建业务引用和安全审核任务。
|
||
12. 新增数据表只在开发者本机手工执行,不写幂等 schema SQL、升级逻辑和回归测试。
|
||
|
||
## 14. 开发与验收命令
|
||
|
||
MySQL 构建使用 build tag:
|
||
|
||
```bash
|
||
go build -tags mysql -o bin/tinode ./server
|
||
go build -tags mysql -o bin/init-db ./tinode-db
|
||
```
|
||
|
||
建议测试顺序:
|
||
|
||
```bash
|
||
# 先跑修改模块的定向测试
|
||
go test ./server -run 'TestName'
|
||
|
||
# Store 与 MySQL schema/adapter
|
||
go test -tags mysql ./server/store/... ./server/db/mysql/...
|
||
|
||
# 完整服务端回归
|
||
go test -tags mysql ./server/...
|
||
```
|
||
|
||
发版前至少检查:
|
||
|
||
- 配置文件可解析,MySQL、Redis、Kafka、OSS 等必需依赖可连通。
|
||
- 数据库版本与代码一致,所有新表/列/索引已在目标环境幂等执行。
|
||
- Kafka Topic、消费组、重试和死信链路可用。
|
||
- 单节点和多节点下的在线状态、群消息、权限变更均一致。
|
||
- 新增接口与前后端对接文档一致,无密钥、密码或测试 Token 进入 Git。
|
||
|
||
## 15. 新功能提交清单
|
||
|
||
开发者在提交 PR/代码前应逐项确认:
|
||
|
||
- [ ] 已确定功能入口是 HTTP、WebSocket、Kafka 还是内部 API。
|
||
- [ ] 已确定数据归属 `hotgo` 还是租户库。
|
||
- [ ] 所有 Store/adapter 调用都显式携带 `TenantID`。
|
||
- [ ] 已设计幂等键、唯一索引、业务版本和状态机。
|
||
- [ ] 已明确 MySQL、Redis、进程内缓存的使用和失效顺序。
|
||
- [ ] 需跨系统可靠投递时使用 outbox + Kafka,消费端已幂等。
|
||
- [ ] 已处理 Topic 内存权限快照和多节点同步。
|
||
- [ ] 文件已存 OSS,业务引用和安全审核已建立。
|
||
- [ ] `SeqId`/ID/version 已在 Go、JSON、Kafka 和 MySQL 全链路使用 64 位安全类型。
|
||
- [ ] 已增加正常、权限、跨租户、幂等、并发、缓存和重试测试。
|
||
- [ ] 已补充 API 对接文档、SQL 变更和未完成增强项。
|
||
|
||
## 16. 相关文档
|
||
|
||
- `docs/IM-前后端对接文档.md`:C 端业务 API 和交互协议。
|
||
- `docs/IM-系统设计文档.md`:原始产品系统设计。
|
||
- `docs/IM-Redis三层数据分层改造方案.md`:进程内、Redis、MySQL 分层细节。
|
||
- `docs/IM-Kafka接入方案.md`:Admin/IM 边界、Topic、幂等、重试和死信。
|
||
- `docs/IM-Kafka接入范围识别.md`:按业务场景判断 Kafka 接入范围。
|
||
- `docs/IM-Kafka接入联调测试文档.md`:Admin 与 IM 联调用例。
|
||
- `docs/third/IM-文件安全第三方接入记录.md`:OSS 和文件安全链路。
|
||
- `docs/IM-未实现后续增强项.md`:已知未完成能力和深度优化项。
|