Files
im_server/docs/TEST-RUN-2026-07-31-MYSQL-CN.md
T
2026-08-11 15:59:57 +08:00

951 lines
48 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 后端测试执行报告(MySQL)
> 执行时间:2026-07-31 10:50:28 CST
> 项目路径:`/Users/superysh/im/chat-0.25.3`
> Go 版本:`go1.26.0 darwin/arm64`
> MySQL:`8.0.42`
> 数据库连接:`127.0.0.1:3306`,用户 `root/root`
## 结论
本次后端测试通过。按 MySQL build tag 编译并执行后,核心后端包、公共组件、MySQL 数据库适配器集成测试均通过;`server` 与 `tinode-db` 的 MySQL 目标构建也通过。
需要注意:不能直接把 `go test ./...` 作为本项目的标准全量测试命令,因为数据库适配器使用 Go build tags。未指定 tag 时,MySQL/PostgreSQL/MongoDB/RethinkDB 测试包会引用不到真实 adapter 的 `GetTestAdapter`,导致编译失败。
## 测试环境检查
已验证 MySQL 服务可连接,并确认存在业务库 `tinode` 与测试库 `tinode_test`。
```bash
mysql -uroot -proot -h127.0.0.1 -P3306 -e "SELECT VERSION() AS mysql_version; SHOW DATABASES LIKE 'tinode'; SHOW DATABASES LIKE 'tinode_test';"
```
结果摘要:
| 项 | 结果 |
|---|---|
| MySQL 版本 | `8.0.42` |
| `tinode` | 存在 |
| `tinode_test` | 存在 |
说明:仓库自带 MySQL 测试配置 [server/db/mysql/tests/test.conf](/Users/superysh/im/chat-0.25.3/server/db/mysql/tests/test.conf:1) 使用 `tinode_test`,并设置 `reset_db_data=true`。这组测试会重建测试库数据。为避免影响你已经建好的业务库 `tinode`,本次 MySQL adapter 测试使用 `tinode_test`。
## 执行命令与结果
### 1. 后端非数据库实例依赖测试
```bash
go test -count=1 $(go list ./server/... | grep -v '/server/db/.*/tests$')
```
结果:通过。
覆盖包摘要:
| 包 | 结果 |
|---|---|
| `github.com/tinode/chat/server` | `ok` |
| `github.com/tinode/chat/server/db/common` | `ok` |
| `github.com/tinode/chat/server/drafty` | `ok` |
| `github.com/tinode/chat/server/media` | `ok` |
| `github.com/tinode/chat/server/ringhash` | `ok` |
| `github.com/tinode/chat/server/store/types` | `ok` |
### 2. MySQL adapter 集成测试
```bash
go test -count=1 -tags mysql ./server/db/mysql/tests
```
结果:通过。
```text
ok github.com/tinode/chat/server/db/mysql/tests 0.687s
```
覆盖的主要持久化能力:
| 能力 | 典型测试 |
|---|---|
| 建库与 schema 初始化 | `TestCreateDb` |
| 用户创建、查询、更新、删除 | `TestUserCreate`、`TestUserGet`、`TestUserUpdate`、`TestUserDelete` |
| 登录认证记录 | `TestAuthAddRecord`、`TestAuthGetRecord`、`TestAuthUpdRecord` |
| 凭证验证 | `TestCredUpsert`、`TestCredGetActive`、`TestCredConfirm`、`TestCredDel` |
| Topic 创建与更新 | `TestTopicCreate`、`TestTopicCreateP2P`、`TestTopicUpdate`、`TestTopicDelete` |
| 订阅关系 | `TestTopicShare`、`TestSubscriptionGet`、`TestSubsUpdate`、`TestSubsDelete` |
| 消息存储、查询、删除 | `TestMessageSave`、`TestMessageGetAll`、`TestMessageDeleteList`、`TestMessageGetDeleted` |
| 文件与附件 | `TestFileStartUpload`、`TestFileFinishUpload`、`TestMessageAttachments` |
| 设备与推送记录 | `TestDeviceUpsert`、`TestDeviceGetAll`、`TestDeviceDelete` |
| 未读数 | `TestUserUnreadCount` |
| 持久缓存 | `TestPCacheUpsert`、`TestPCacheGet`、`TestPCacheDelete`、`TestPCacheExpire` |
### 3. MySQL tag 下的 Go 包测试
```bash
go test -count=1 -tags mysql $(go list ./... | grep -v '/server/db/mongodb/tests$' | grep -v '/server/db/postgres/tests$' | grep -v '/server/db/rethinkdb/tests$')
```
结果:通过。
结果摘要:
| 范围 | 结果 |
|---|---|
| 根模块 Go 包 | 通过 |
| `server` 核心测试 | 通过 |
| `server/db/common` | 通过 |
| `server/db/mysql/tests` | 通过 |
| `drafty`、`media`、`ringhash`、`store/types` | 通过 |
| `tinode-db` | 无测试文件,编译检查通过 |
本次实际执行的测试函数约 `185` 个,不含 PostgreSQL/MongoDB/RethinkDB adapter 测试。
### 4. MySQL 目标构建
```bash
go build -tags mysql ./server ./tinode-db
```
结果:通过。
## 直接全量测试的已知失败
执行以下命令:
```bash
go test -count=1 ./...
```
结果:失败,原因是未指定数据库 build tag,导致各数据库测试包无法引用真实 adapter。
失败摘要:
```text
server/db/mysql/tests/mysql_test.go:1319:16: undefined: backend.GetTestAdapter
server/db/postgres/tests/postgres_test.go:1385:16: undefined: backend.GetTestAdapter
server/db/rethinkdb/tests/rethink_test.go:1552:16: undefined: backend.GetTestAdapter
server/db/mongodb/tests/mongo_test.go:1256:16: undefined: backend.GetTestAdapter
```
这是项目构建方式导致的预期问题,不代表 MySQL 后端测试失败。标准命令应按目标数据库指定 build tag。
## 建议后续标准测试命令
当前以 MySQL 为后端开发目标时,建议使用:
```bash
go test -count=1 -tags mysql $(go list ./... | grep -v '/server/db/mongodb/tests$' | grep -v '/server/db/postgres/tests$' | grep -v '/server/db/rethinkdb/tests$')
```
如果只改核心消息、会话、Topic 逻辑,可先跑:
```bash
go test -count=1 ./server ./server/db/common ./server/drafty ./server/media ./server/ringhash ./server/store/types
```
如果改 MySQL 持久化层,必须补跑:
```bash
go test -count=1 -tags mysql ./server/db/mysql/tests
```
如果改启动、配置或数据库初始化工具,补跑:
```bash
go build -tags mysql ./server ./tinode-db
```
## 开发后回归记录
> 执行时间:2026-07-31 11:46:05 CST
本轮新增 `x-im-*` 协议扩展实现与测试后,已执行以下回归:
| 命令 | 结果 |
|---|---|
| `go test -count=1 ./server` | 通过 |
| `go test -count=1 ./server/drafty ./server/store/types ./server/db/common` | 通过 |
| `go test -count=1 -tags mysql $(go list ./... \| grep -v '/server/db/mongodb/tests$' \| grep -v '/server/db/postgres/tests$' \| grep -v '/server/db/rethinkdb/tests$')` | 通过 |
| `go build -tags mysql ./server ./tinode-db` | 通过 |
| 校验 [docs/IM-WS-API-CN.md](/Users/superysh/im/chat-0.25.3/docs/IM-WS-API-CN.md:1) 内全部 `json` 代码块 | `94/94` 通过 |
新增覆盖点:
| 覆盖点 | 说明 |
|---|---|
| `x-im-type` 白名单 | 拒绝未知或非字符串业务消息类型 |
| 通话记录类型 | 支持 `head.x-im-type="call"` 并保持原样广播 |
| 聊天记录筛选 | 覆盖按 `x-im-filter.types` 与 `x-im-search.keyword` 过滤 |
| 群全员禁言 | 覆盖普通成员拒绝、管理员允许 |
## 开发后回归记录(二)
> 执行时间:2026-07-31 11:57:58 CST
本轮新增好友/入群申请资料校验、群业务配置校验、创建群初始 `aux` 保存后,已执行以下回归:
| 命令 | 结果 |
|---|---|
| `go test -count=1 ./server` | 通过 |
| `go test -count=1 ./server/drafty ./server/store/types ./server/db/common` | 通过 |
| `go test -count=1 -tags mysql $(go list ./... \| grep -v '/server/db/mongodb/tests$' \| grep -v '/server/db/postgres/tests$' \| grep -v '/server/db/rethinkdb/tests$')` | 通过 |
| `go build -tags mysql ./server ./tinode-db` | 通过 |
| 校验 [docs/IM-WS-API-CN.md](/Users/superysh/im/chat-0.25.3/docs/IM-WS-API-CN.md:1) 内全部 `json` 代码块 | `94/94` 通过 |
新增覆盖点:
| 覆盖点 | 说明 |
|---|---|
| 好友申请备注 | `private.x-im-applyText` 支持保存,非法类型或超长返回 400 |
| 私有业务开关 | `x-im-blocked`、`x-im-muted`、`x-im-pinned` 必须为布尔值 |
| 群业务配置 | `aux.x-im-group` 的核心布尔字段、禁言成员数组、公告文本类型受校验 |
| 创建群初始配置 | `{sub topic="new" set.aux.x-im-group}` 会写入 `topics.aux` |
| 群配置更新 | 管理员 `{set aux}` 可更新 `x-im-group`,非法配置返回 400 |
## 开发后回归记录(三)
> 执行时间:2026-07-31 12:02:05 CST
本轮新增群级业务策略后,已执行以下回归:
| 命令 | 结果 |
|---|---|
| `go test -count=1 ./server ./server/drafty ./server/store/types ./server/db/common` | 通过 |
| `go test -count=1 -tags mysql $(go list ./... \| grep -v '/server/db/mongodb/tests$' \| grep -v '/server/db/postgres/tests$' \| grep -v '/server/db/rethinkdb/tests$')` | 通过 |
| `go build -tags mysql ./server ./tinode-db` | 通过 |
| 校验 [docs/IM-WS-API-CN.md](/Users/superysh/im/chat-0.25.3/docs/IM-WS-API-CN.md:1) 内全部 `json` 代码块 | `95/95` 通过 |
新增覆盖点:
| 覆盖点 | 说明 |
|---|---|
| 单成员禁言 | `aux.x-im-group.mutedUsers` 中的普通成员发消息返回 403 |
| 成员文件上传限制 | `allowMemberFileUpload=false` 时普通成员发送附件类消息返回 403 |
| 仅管理员邀请 | `onlyAdminInvite=true` 时非管理员邀请成员返回 403 |
| 管理员豁免 | 群全员禁言下管理员仍可发消息,沿用前序测试覆盖 |
## 开发后回归记录(四)
> 执行时间:2026-07-31 12:10:16 CST
本轮启动 Phase 2,新增 MySQL 消息业务索引 `im_message_index`,并把 MySQL adapter 版本升级到 `117`。测试库检查结果:
| 项 | 结果 |
|---|---|
| `tinode_test.kvmeta.version` | `117` |
| `tinode_test.im_message_index` | 存在 |
已执行以下回归:
| 命令 | 结果 |
|---|---|
| `go test -count=1 ./server ./server/drafty ./server/store/types ./server/db/common` | 通过 |
| `go test -count=1 -tags mysql ./server/db/mysql/tests` | 通过 |
| `go test -count=1 -tags mysql $(go list ./... \| grep -v '/server/db/mongodb/tests$' \| grep -v '/server/db/postgres/tests$' \| grep -v '/server/db/rethinkdb/tests$')` | 通过 |
| `go build -tags mysql ./server ./tinode-db` | 通过 |
新增覆盖点:
| 覆盖点 | 说明 |
|---|---|
| 数据库迁移 | MySQL adapter `116 -> 117`,`CreateDb` 与 `UpgradeDb` 均支持 `im_message_index` |
| 历史消息回填 | 旧库升级时扫描 `messages` 与 `filemsglinks` 回填索引 |
| 索引写入 | `MessageIndexSave` 保存消息类型、检索文本、附件数量 |
| 索引查询 | `MessageIndexSearch` 支持 topic、seq 范围、类型、关键词、limit |
| 软删除过滤 | 索引查询继续按 Tinode `dellog` 排除当前用户已删除消息 |
| 服务层接入 | `{get data}` 携带 `x-im-search` 或 `x-im-filter` 时优先使用索引,未实现索引的 adapter 回退内存过滤 |
注意:业务库 `tinode` 如仍是旧版本,需要使用 `tinode-db --upgrade` 升级到 `117` 后,当前 MySQL server 构建才能通过数据库版本检查。
## 业务库升级记录
> 执行时间:2026-07-31 12:28:43 CST
已按 Phase 2 迁移要求升级业务库 `127.0.0.1:3306/tinode`。
升级前检查:
| 项 | 结果 |
|---|---|
| `tinode.kvmeta.version` | `116` |
| `tinode.im_message_index` | 不存在 |
升级前已备份:
```bash
mysqldump -uroot -proot -h127.0.0.1 -P3306 --single-transaction --routines --triggers tinode > /Users/superysh/im/chat-0.25.3/db_backups/tinode-before-117-20260731-122808.sql
```
执行升级:
```bash
go run -tags mysql ./tinode-db --upgrade --config=server/tinode.conf
```
升级日志摘要:
```text
Database adapter: 'mysql'; version: 117
Wrong DB version: expected 117, got 116. Upgrading the database.
Database successfully upgraded.
All done.
```
升级后验证:
| 项 | 结果 |
|---|---|
| `tinode.kvmeta.version` | `117` |
| `tinode.im_message_index` | 存在 |
| `tinode.messages` | `100` 条 |
| `tinode.im_message_index` | `100` 条 |
| `go run -tags mysql ./tinode-db --config=server/tinode.conf` | `Database exists, version is correct.` |
| `go build -tags mysql ./server ./tinode-db` | 通过 |
| `go test -count=1 -tags mysql ./server/db/mysql/tests` | 通过 |
## 开发后回归记录(五)
> 执行时间:2026-07-31 12:37:59 CST
本轮继续 Phase 2,新增 MySQL 积分与每日事件持久化能力,并把 MySQL adapter 版本升级到 `118`。
代码变更摘要:
| 覆盖点 | 说明 |
|---|---|
| 数据库迁移 | MySQL adapter `117 -> 118`,`CreateDb` 与 `UpgradeDb` 均支持 `im_user_points`、`im_user_daily_events` |
| 积分查询 | `IMUserPointsGet` 支持读取用户积分余额 |
| 积分增加 | `IMUserPointsAdd` 支持首次创建与余额累加 |
| 积分扣减 | `IMUserPointsConsume` 使用条件更新保证余额不足时不扣减 |
| 每日事件幂等 | `IMDailyEventCreate` 依赖唯一索引保证同一用户、日期、事件类型只记录一次 |
| 每日事件读取 | `IMDailyEventGet` 支持读取事件 payload,供签到、每日任务等业务复用 |
已执行以下回归:
| 命令 | 结果 |
|---|---|
| `go test -count=1 ./server ./server/drafty ./server/store/types ./server/db/common` | 通过 |
| `go test -count=1 -tags mysql ./server/db/mysql/tests` | 通过 |
| `go test -count=1 -tags mysql $(go list ./... \| grep -v '/server/db/mongodb/tests$' \| grep -v '/server/db/postgres/tests$' \| grep -v '/server/db/rethinkdb/tests$')` | 通过 |
| `go build -tags mysql ./server ./tinode-db` | 通过 |
新增测试用例:
| 用例 | 目的 |
|---|---|
| `TestIMUserPointsAddAndConsume` | 验证积分首次创建、增加、扣减后的余额 |
| `TestIMUserPointsInsufficient` | 验证余额不足时返回 `types.ErrPolicy` |
| `TestIMDailyEventIdempotent` | 验证每日事件重复写入只成功一次,并可读取 payload |
## 业务库升级记录(二)
> 执行时间:2026-07-31 12:37:59 CST
已按 Phase 2 二期迁移要求升级业务库 `127.0.0.1:3306/tinode`。
升级前检查:
| 项 | 结果 |
|---|---|
| `tinode.kvmeta.version` | `117` |
| `tinode.im_message_index` | 存在 |
| `tinode.im_user_points` | 不存在 |
| `tinode.im_user_daily_events` | 不存在 |
升级前已备份:
```bash
mysqldump -h127.0.0.1 -P3306 -uroot -proot tinode > /Users/superysh/im/chat-0.25.3/db_backups/tinode-before-118-20260731-123747.sql
```
执行升级:
```bash
go run -tags mysql ./tinode-db --upgrade --config=server/tinode.conf
```
升级日志摘要:
```text
Database adapter: 'mysql'; version: 118
Wrong DB version: expected 118, got 117. Upgrading the database.
Database successfully upgraded.
All done.
```
升级后验证:
| 项 | 结果 |
|---|---|
| `tinode.kvmeta.version` | `118` |
| `tinode.im_message_index` | 存在 |
| `tinode.im_user_points` | 存在 |
| `tinode.im_user_daily_events` | 存在 |
| `tinode.messages` | `100` 条 |
| `tinode.im_message_index` | `100` 条 |
## 开发后回归记录(六)
> 执行时间:2026-07-31 12:50:00 CST
本轮将 Phase 2 签到能力正式接入 WebSocket `{pub}` 流程。客户端向 `slf` 发送 `head.x-im-type="checkin"` 时,服务端会先执行每日事件幂等与积分发放,再决定是否保存签到消息。
代码变更摘要:
| 覆盖点 | 说明 |
|---|---|
| WS 签到接入 | `Topic.handlePubBroadcast` 在消息落库前处理 `checkin` |
| 幂等控制 | 同一用户、日期、事件类型重复签到返回 `304 no action` |
| 消息去重 | 重复签到不保存第二条 `slf` 消息 |
| 固定奖励 | 服务端固定每日签到奖励为 `5`,拒绝客户端伪造其它积分值 |
| 原子发放 | `IMDailyEventCreateWithPoints` 在同一事务内创建每日事件并增加积分 |
已执行以下回归:
| 命令 | 结果 |
|---|---|
| `go test -count=1 ./server ./server/drafty ./server/store/types ./server/db/common` | 通过 |
| `go test -count=1 -tags mysql ./server/db/mysql/tests` | 通过 |
| `go test -count=1 -tags mysql $(go list ./... \| grep -v '/server/db/mongodb/tests$' \| grep -v '/server/db/postgres/tests$' \| grep -v '/server/db/rethinkdb/tests$')` | 通过 |
| `go build -tags mysql ./server ./tinode-db` | 通过 |
新增测试用例:
| 用例 | 目的 |
|---|---|
| `TestIMParseCheckinPayload` | 验证签到日期与默认积分解析 |
| `TestIMParseCheckinPayloadRejectsClientControlledReward` | 验证客户端不能伪造非默认签到奖励 |
| `TestIMDailyEventCreateWithPoints` | 验证每日事件与积分发放的事务一致性和重复不加分 |
## 开发后回归记录(七)
> 执行时间:2026-07-31 13:05:00 CST
本轮将 Phase 2 话题发布积分扣减接入 `{sub topic="nch"}` 频道创建流程。客户端在 `public.x-im-topic.pointsCost` 中声明扣费金额时,MySQL 后端会在同一事务中创建话题、创建作者订阅并扣减积分。
代码变更摘要:
| 覆盖点 | 说明 |
|---|---|
| 话题扣费解析 | 从 `public.x-im-topic.pointsCost` 读取非负整数扣费金额 |
| 普通群聊隔离 | 仅 `nch` 频道创建触发扣费,`new` 群聊创建不受影响 |
| 原子创建 | `IMTopicCreateWithPoints` 在一个事务内完成 topic 创建、owner subscription 创建和积分扣减 |
| 余额不足保护 | 余额不足返回 `types.ErrPolicy`,事务回滚,话题不会创建 |
| 可选 adapter | 不修改 Tinode 全局 adapter 接口,非 MySQL 后端没有实现时返回 unsupported |
已执行以下回归:
| 命令 | 结果 |
|---|---|
| `go test -count=1 ./server ./server/drafty ./server/store/types ./server/db/common` | 通过 |
| `go test -count=1 -tags mysql ./server/db/mysql/tests` | 通过 |
| `go test -count=1 -tags mysql $(go list ./... \| grep -v '/server/db/mongodb/tests$' \| grep -v '/server/db/postgres/tests$' \| grep -v '/server/db/rethinkdb/tests$')` | 通过 |
| `go build -tags mysql ./server ./tinode-db` | 通过 |
新增测试用例:
| 用例 | 目的 |
|---|---|
| `TestIMTopicPointsCost` | 验证话题发布扣费字段解析 |
| `TestIMTopicPointsCostRejectsMalformedCost` | 验证非法扣费金额返回 malformed |
| `TestIMTopicCreateWithPoints` | 验证话题创建成功后积分被扣减,并创建作者订阅 |
| `TestIMTopicCreateWithPointsInsufficient` | 验证余额不足时返回 policy,且不会创建 topic |
## 开发后回归记录(八)
> 执行时间:2026-07-31 13:58:00 CST
本轮接入产品通话 payload 校验,同时保持 Tinode 原生 WebRTC 信令兼容。
代码变更摘要:
| 覆盖点 | 说明 |
|---|---|
| 通话邀请校验 | `head.x-im-type="call"` 且 `head.webrtc="started"` 时,`content.callId` 必填 |
| 控制事件校验 | `ringing`、`accept`、`hang-up` 的 `payload.callId` 必填,非法 payload 静默丢弃 |
| 原生信令兼容 | `offer`、`answer`、`ice-candidate` 不强制产品字段,继续透传 SDP/ICE payload |
| 通话记录兼容 | 普通 `x-im-type="call"` 持久消息不强制 `callId`,避免影响历史通话记录展示 |
已执行以下回归:
| 命令 | 结果 |
|---|---|
| `go test -count=1 ./server` | 通过 |
| `go test -count=1 ./server/drafty ./server/store/types ./server/db/common` | 通过 |
| `go test -count=1 -tags mysql ./server/db/mysql/tests` | 通过 |
| `go test -count=1 -tags mysql $(go list ./... \| grep -v '/server/db/mongodb/tests$' \| grep -v '/server/db/postgres/tests$' \| grep -v '/server/db/rethinkdb/tests$')` | 通过 |
| `go build -tags mysql ./server ./tinode-db` | 通过 |
新增测试用例:
| 用例 | 目的 |
|---|---|
| `TestIMValidateCallContent` | 验证产品通话内容中 `callId` 合法 |
| `TestIMValidateCallContentMalformed` | 验证产品通话内容缺少 `callId` 被拒绝 |
| `TestHandleBroadcastProductCallRejectsMissingCallID` | 验证产品通话邀请缺少 `callId` 返回 `400 malformed`,且不进入通话状态 |
## 开发后回归记录(九)
> 执行时间:2026-07-31 14:17:42 CST
本轮新增 WebSocket mock 联调程序 `tools/wsmock`,用于按前后端对接文档直接模拟前端通过 `/v0/channels` 与后端通信。程序不新增第三方依赖,复用项目已有 `github.com/gorilla/websocket`。
代码变更摘要:
| 覆盖点 | 说明 |
|---|---|
| WS 握手 | 通过 `X-Tinode-APIKey` 连接 `/v0/channels` 并发送 `{hi}` |
| 注册登录 | 支持 basic 注册;本地配置要求 email credential 时,自动走 `acc -> 300 validate credentials -> login cred.resp` |
| 自存消息 | 订阅 `slf` 并发布 `head.x-im-type="text"` 消息,验证 `202 accepted` |
| 签到幂等 | 首次签到验证 `202 accepted`,重复同日签到验证 `304 no action` |
| 频道积分 | 多日签到累计积分后创建 `nch` 频道并扣减积分,再用超高 `pointsCost` 验证 `422 policy violation` |
| 通话校验 | 产品通话邀请缺少 `content.callId` 时验证返回 `400 malformed` |
已执行以下联调:
| 命令 | 结果 |
|---|---|
| `go build ./tools/wsmock` | 通过 |
| `go run -tags mysql ./server -config=tinode.conf -static_data=-` | 服务启动成功,监听 `:6060` |
| `go run ./keygen -salt 'T713/rYYgW7g4m3vG6zGRh7+FM1t0T8j13koXScOAj4='` | 生成本地联调用 API key |
| `go run ./tools/wsmock -apikey '<generated>' -scenario all -timeout 12s` | 通过 |
联调观察:
| 协议点 | 实际返回 |
|---|---|
| `{hi}` 成功 | `201 created` |
| `{pub}` 成功 | `202 accepted`,随后下发 `{data}` |
| 重复签到 | `304 no action`,`params.x-im-checkin.already=true` |
| 频道余额不足 | `422 policy violation` |
| 产品通话缺少 `callId` | `400 malformed` |
| 本地 SMTP | `tinode.conf` 启用 dummy email response,但 SMTP 服务未启动;联调可通过,服务端会异步记录 SMTP connection refused |
## 开发后回归记录(十)
> 执行时间:2026-07-31 14:27:57 CST
本轮增强 `tools/wsmock`,在原有 Phase 2 smoke/checkin/topic/call 场景基础上,新增双端 P2P、聊天记录搜索/分类筛选、举报到 `sys` 的端到端联调。
代码变更摘要:
| 覆盖点 | 说明 |
|---|---|
| 双端客户端 | 自动创建 Alice 和 Bob 两个 WebSocket 连接,各自完成 `{hi}`、注册、email credential 验证、登录和订阅 `me` |
| P2P 建链 | Alice 订阅 Bob 的 `usr...`,Bob 订阅 Alice 的 `usr...`,验证 P2P topic 可创建和进入 |
| P2P 消息 | Alice 向 Bob 发送 Drafty 文本消息,Bob 收到 `{data}` |
| 已收已读 | Bob 发送 `recv/read` `{note}`,Alice 收到 `read` `{info}` |
| 文件分类 | Alice 发送 `head.x-im-type="file"` 文件卡片,Bob 收到 `{data}` |
| 索引搜索 | Alice 使用 `{get what="data" x-im-search}` 搜索关键词,返回 `208 delivered` 且 `params.count=1` |
| 类型筛选 | Alice 使用 `{get what="data" x-im-filter.types=["file"]}` 查询文件消息,返回 `208 delivered` 且 `params.count=1` |
| 举报 | Alice 向 `sys` 发布 `head.x-im-type="report"`,验证 `202 accepted` |
已执行以下联调:
| 命令 | 结果 |
|---|---|
| `go build ./tools/wsmock` | 通过 |
| `go run -tags mysql ./server -config=tinode.conf -static_data=-` | 服务启动成功,监听 `:6060` |
| `go run ./tools/wsmock -apikey '<generated>' -scenario all -timeout 12s` | 通过 |
联调观察:
| 协议点 | 实际返回 |
|---|---|
| P2P 建链 | `{sub topic="usrPeer"}` 返回 `200 ok`,双方订阅列表包含两个用户 |
| P2P 文本发布 | 发布方 `{ctrl code=202}`,双方收到对应 `{data}` |
| 已读回执 | 接收方 `{note what="read"}` 后,发送方收到 `{info what="read"}` |
| 搜索命中 | `{get x-im-search}` 返回 `208 delivered`,`params.count=1` |
| 文件筛选命中 | `{get x-im-filter}` 返回 `208 delivered`,`params.count=1` |
| 举报到 `sys` | 普通用户不订阅 `sys` 也可 `{pub}`,返回 `202 accepted` |
## 开发后回归记录(十一)
> 执行时间:2026-07-31 14:31:44 CST
本轮继续增强 `tools/wsmock`,新增群聊多角色与业务权限策略联调,并纳入 `all` 全量场景。
代码变更摘要:
| 覆盖点 | 说明 |
|---|---|
| 建群 | Alice 使用 `{sub topic="new"}` 创建群,初始化 `aux.x-im-group` |
| 邀请成员 | Alice 使用 `{set sub.user mode="JRWP"}` 邀请 Bob |
| 成员入群 | Bob 订阅 `grp...` 并读取 `desc sub data aux` |
| 成员发言 | Bob 发布 Drafty 文本,Alice 收到 `{data}` |
| 全员禁言 | Alice 设置 `aux.x-im-group.muteAll=true` 后,Bob 发言返回 `403 permission denied` |
| 群主豁免 | 全员禁言下 Alice 作为群主发言仍返回 `202 accepted` |
| 文件权限 | Alice 设置 `allowMemberFileUpload=false` 后,Bob 发送 `x-im-type="file"` 返回 `403 permission denied` |
| 管理员/群主文件豁免 | 文件上传受限时 Alice 发送文件仍返回 `202 accepted` |
已执行以下联调:
| 命令 | 结果 |
|---|---|
| `go build ./tools/wsmock` | 通过 |
| `go run -tags mysql ./server -config=tinode.conf -static_data=-` | 服务启动成功,监听 `:6060` |
| `go run ./tools/wsmock -apikey '<generated>' -scenario group -timeout 12s` | 通过 |
| `go run ./tools/wsmock -apikey '<generated>' -scenario all -timeout 12s` | 通过 |
联调观察:
| 协议点 | 实际返回 |
|---|---|
| 群创建 | `{sub topic="new"}` 返回 `200 ok`,`ctrl.topic=grp...` |
| 邀请成员 | `{set sub.user}` 返回 `200 ok`,`params.user=usr...` |
| 成员正常发言 | `{pub}` 返回 `202 accepted`,群主收到 `{data}` |
| 全员禁言拦截 | 普通成员 `{pub text}` 返回 `403 permission denied` |
| 群主禁言豁免 | 群主 `{pub text}` 返回 `202 accepted` |
| 文件上传限制 | 普通成员 `{pub file}` 返回 `403 permission denied`,群主 `{pub file}` 返回 `202 accepted` |
## 开发后回归记录(十二)
> 执行时间:2026-07-31 14:48:06 CST
本轮继续增强 `tools/wsmock`,新增完整 WebRTC 通话信令专项联调 `call-flow`,并修正 mock 客户端等待逻辑:当服务端异步下发的 `{meta}`、`{pres}`、`{data}`、`{info}` 与当前等待条件不匹配时,先缓存后续步骤可消费,避免真实 WS 多帧交错导致误判超时。
代码变更摘要:
| 覆盖点 | 说明 |
|---|---|
| 通话邀请 | Alice 向 Bob 的 P2P topic 发布 `head.webrtc="started"`、`head.x-im-type="call"`、`content.callId` |
| 来电展示 | Bob 收到 `webrtc=started` 的 `{data}` |
| 振铃 | Bob 发送 `{note what="call" event="ringing"}`,Alice 收到 `{info event="ringing"}` |
| 接听 | Bob 发送 `{note event="accept"}`,Alice 收到 `{info event="accept"}`,双方收到 `webrtc=accepted` 替换消息 |
| SDP 交换 | Alice 发送 `offer`,Bob 收到;Bob 发送 `answer`,Alice 收到 |
| ICE 交换 | Alice/Bob 双向发送 `ice-candidate`,对端均收到对应 `{info}` |
| 挂断 | Alice 发送 `hang-up`,双方收到 `{info event="hang-up"}` 和 `webrtc=finished` 替换消息 |
已执行以下联调:
| 命令 | 结果 |
|---|---|
| `go build ./tools/wsmock` | 通过 |
| `go run -tags mysql ./server -config=/tmp/tinode-webrtc.conf -static_data=-` | 服务启动成功,监听 `:6060`,日志显示 `Video calls enabled with 2 ICE servers` |
| `go run ./tools/wsmock -apikey '<generated>' -scenario call-flow -timeout 12s` | 通过 |
| `go run ./tools/wsmock -apikey '<generated>' -scenario all -timeout 12s` | 通过 |
联调观察:
| 协议点 | 实际返回 |
|---|---|
| `{hi}` WebRTC 参数 | `params.callTimeout=30`,并返回配置的 `iceServers` |
| 通话邀请 | `{pub}` 返回 `202 accepted`,接收方收到 `webrtc=started` |
| 接听行为 | 发起方收到 `{info event="accept"}`;接听方不额外收到 `accept info`,而是直接收到 `webrtc=accepted` 替换消息 |
| 通话媒体协商 | `offer`、`answer`、双向 `ice-candidate` 均通过 `{note}` 发送,并由服务端以 `{info}` 转发给对端 |
| 挂断结束 | 双方均收到 `event="hang-up"` 与 `webrtc=finished` |
## 开发后回归记录(十三)
> 执行时间:2026-07-31 15:02:40 CST
本轮新增更完整的群管理 WebSocket 专项联调 `group-admin`,覆盖 Alice 群主、Bob 管理员候选、Carol 普通成员、Dave 被邀请/被踢成员四个角色,并继续复用真实 `/v0/channels` 协议。
代码变更摘要:
| 覆盖点 | 说明 |
|---|---|
| 群配置读取 | Alice 设置 `aux.x-im-group.joinApproval/onlyAdminInvite/announcement` 后,通过 `{get what="aux"}` 读取并断言字段 |
| 仅管理员邀请 | Bob 先具备 `S` 分享权限但不具备 `A` 管理权限,在 `onlyAdminInvite=true` 下邀请 Dave 返回 `403 permission denied` |
| 管理员授权 | Alice 使用绝对权限 `JRWPASD` 授予 Bob 管理权限,Bob 再用自己的 `{set.sub}` 将 `want` 调整为 `JRWPASD`,最终有效权限变为管理员 |
| 管理员邀请 | Bob 成为管理员后邀请 Dave 返回 `200 ok`,Dave 可订阅入群 |
| 单成员禁言 | Alice 将 Carol 写入 `aux.x-im-group.mutedUsers` 后,Carol 发言返回 `403`;清空后 Carol 发言返回 `202` |
| 全员禁言管理员豁免 | Bob 设置 `muteAll=true` 后,Dave 发言返回 `403`,Bob 作为管理员发言返回 `202` |
| 踢人 | Bob 使用 `{del what="sub" user=dave}` 踢出 Dave 返回 `200`;Dave 当前会话收到 `205 evicted`,后续发言返回 `409 must attach first` |
已执行以下联调:
| 命令 | 结果 |
|---|---|
| `gofmt -w tools/wsmock/main.go && go build ./tools/wsmock` | 通过 |
| `go test -count=1 ./tools/wsmock` | 通过,无测试文件 |
| `go run -tags mysql ./server -config=tinode.conf -static_data=-` | 服务启动成功,监听 `:6060`,日志显示 `Video calls disabled` |
| `go run ./tools/wsmock -apikey '<generated>' -scenario group-admin -timeout 12s` | 通过 |
| `go run ./tools/wsmock -apikey '<generated>' -scenario all -timeout 12s` | 通过 |
联调观察:
| 协议点 | 实际返回 |
|---|---|
| `{set.sub.mode}` 权限写法 | 当前后端使用绝对权限字符串,不接受 `+A`、`-W` 这类增量字符串;错误写法返回 `400 malformed` |
| 有效权限计算 | 用户有效权限为 `want & given`,管理员授权需要服务端授予 `given`,目标用户也要把自己的 `want` 提升到包含 `A` |
| 踢人后状态 | 被踢用户会收到无 id 的 `{ctrl code=205 text="evicted" params.unsub=true}`,同一 WS 会话再发群消息返回 `409 must attach first` |
## 开发后回归记录(十四)
> 执行时间:2026-07-31 15:12:24 CST
本轮补齐真实文件上传链路,不再只用“文件卡片消息”模拟附件。新增 `tools/wsmock -scenario upload`,通过 HTTP `/v0/file/u/` 上传小文件,再通过 WebSocket 发布 Drafty 文件消息,并显式写入 `extra.attachments`,验证服务端保存消息时把上传文件绑定到消息,避免后续 GC 清理。
代码变更摘要:
| 覆盖点 | 说明 |
|---|---|
| 上传入口 | 从 `ws://127.0.0.1:6060/v0/channels` 自动推导 `http://127.0.0.1:6060/v0/file/u/` |
| 上传鉴权 | HTTP 请求带 `X-Tinode-APIKey` 与 `Authorization: token <login-token>` |
| Multipart 上传 | 使用字段 `file` 上传真实二进制内容,服务端返回 `{ctrl code=200 params.url}` |
| 附件消息 | WebSocket `{pub}` 使用 `mime=text/x-drafty`、`x-im-type=file`、Drafty `EX` entity,并写 `extra.attachments=[url]` |
| 接收验证 | Bob 收到 `{data}`,其 Drafty content 中的 `ref` 与上传返回 URL 一致 |
| 查询验证 | Alice 使用 `x-im-filter.types=["file"]` 查询到该文件消息 |
| 防 GC 引用 | MySQL `fileuploads` 记录成功完成,`filemsglinks` 已绑定到对应 `messages.id` |
已执行以下联调:
| 命令 | 结果 |
|---|---|
| `gofmt -w tools/wsmock/main.go && go build ./tools/wsmock` | 通过 |
| `go test -count=1 ./tools/wsmock` | 通过,无测试文件 |
| `go run -tags mysql ./server -config=tinode.conf -static_data=-` | 服务启动成功,监听 `:6060`,日志显示 `Large media handling enabled fs` |
| `go run ./tools/wsmock -apikey '<generated>' -scenario upload -timeout 12s` | 通过 |
| `mysql -uroot -proot -h127.0.0.1 -P3306 tinode -e '<fileuploads/filemsglinks join query>'` | 查到上传文件与消息引用 |
| `curl -H 'X-Tinode-APIKey: ...' 'http://127.0.0.1:6060/v0/file/s/KX2Un74V9eI.conf?...'` | `200 OK`,内容与上传文本一致 |
联调观察:
| 协议点 | 实际返回 |
|---|---|
| `/v0/file/u/` | `200 ok`,`ctrl.params.url="/v0/file/s/KX2Un74V9eI.conf"`,`ctrl.params.expires` 返回未绑定前临时保留时间 |
| 文件落库 | `fileuploads.id=1557212408072638464`,`status=1`,`mimetype=text/plain; charset=utf-8`,`size=50`,`location=uploads/ff6zjh56cx26e` |
| 消息绑定 | `filemsglinks.fileid=1557212408072638464` 关联 `messages.id=170`,消息 topic 为 P2P topic,`seqid=1` |
| 文件下载 | `/v0/file/s/KX2Un74V9eI.conf` 返回 `200 OK`,`Content-Length=50`,内容与上传文本一致 |
| 场景归属 | `upload` 会写真实文件和业务库记录,暂不纳入默认 `all`,避免普通回归持续堆积附件 |
## 开发后回归记录(十五)
> 执行时间:2026-07-31 15:19:10 CST
本轮补齐好友/联系人业务状态专项联调。新增 `tools/wsmock -scenario contacts`,通过真实 WebSocket 验证 P2P 建链时的好友申请备注、联系人私有状态读写、拉黑 ACL 阻断、取消拉黑恢复发言,以及非法 private 字段校验。
代码变更摘要:
| 覆盖点 | 说明 |
|---|---|
| 申请备注 | Alice 使用 `{sub topic=bobUserId set.desc.private.x-im-applyText}` 建立 P2P 关系 |
| 详情读回 | Alice 使用 `{get topic=bobUserId what="desc"}` 读取 `desc.private` 并断言申请备注 |
| 通讯录读回 | Alice 使用 `{get topic="me" what="sub"}` 读取联系人列表,断言目标联系人 `sub.private` |
| 私有状态 | 写入并读回 `comment`、`x-im-remarkUpdatedAt`、`x-im-muted`、`x-im-pinned`、`x-im-blocked` |
| 拉黑联动 | Alice 设置 `x-im-blocked=true` 并把 Bob 的 P2P `given` 调整为 `JRPA`,Bob 再发言返回 `403 permission denied` |
| 取消拉黑 | Alice 设置 `x-im-blocked=false` 并把 Bob 的 P2P `given` 调整为 `JRWPA`,Bob 发言返回 `202 accepted` 且 Alice 收到 `{data}` |
| 参数校验 | `x-im-blocked` 非布尔值、`x-im-applyText` 超长均返回 `400 malformed` |
已执行以下联调:
| 命令 | 结果 |
|---|---|
| `gofmt -w tools/wsmock/main.go` | 通过 |
| `go test -count=1 ./tools/wsmock` | 通过,无测试文件 |
| `go run -tags mysql ./server -config=tinode.conf -static_data=-` | 服务启动成功,监听 `:6060`,MySQL adapter schema `118` |
| `go run ./tools/wsmock -apikey '<generated>' -scenario contacts -timeout 12s` | 通过 |
联调观察:
| 协议点 | 实际返回 |
|---|---|
| 好友申请备注 | `private.x-im-applyText="好友申请备注联调 151910"` 在 topic desc 和 `me.sub` 中均可读回 |
| 私有状态 | `comment`、`x-im-muted`、`x-im-pinned`、`x-im-blocked`、`x-im-remarkUpdatedAt` 在 `desc.private` 与联系人列表中保持一致 |
| 拉黑 ACL | `{set.desc.private + set.sub.mode="JRPA"}` 返回 `200`;服务端向 Bob 推送 `pres what="acs" dacs.given="+P-W"` |
| 被拉黑发言 | Bob 发布 P2P 文本返回 `403 permission denied` |
| 取消拉黑 ACL | `{set.desc.private + set.sub.mode="JRWPA"}` 返回 `200`;服务端向 Bob 推送 `pres what="acs" dacs.given="+W"` |
| 恢复发言 | Bob 发布 P2P 文本返回 `202 accepted`,Alice 收到对应 `{data}` |
| 非法 private | `x-im-blocked="true"` 与 257 字符 `x-im-applyText` 均返回 `400 malformed` |
| 场景归属 | `contacts` 会写真实联系人私有状态并调整 P2P ACL,暂不纳入默认 `all`,便于专项定位 |
## 开发后回归记录(十六)
> 执行时间:2026-07-31 15:37:26 CST
本轮补齐“页面型能力”中可复用 Tinode 原生协议低改动落地的专项联调。新增 `tools/wsmock -scenario pages` 与 `tools/wsmock -scenario group-owner`,覆盖收藏/文件助手/个人设置/黑名单视图/删除好友/群二维码/群主转让。
代码变更摘要:
| 覆盖点 | 说明 |
|---|---|
| 个人设置 | Alice 写入 `me.desc.private.x-im-settings`,再通过 `{get topic="me" what="desc"}` 读回 |
| 收藏列表 | Alice 向 `slf` 发布 `head.x-im-type="favorite"`,再用 `x-im-search.types=["favorite"]` 搜索收藏 |
| 文件助手 | Alice 向 `slf` 发布文件卡片消息,再用 `x-im-filter.types=["file"]` 读取文件助手列表 |
| 黑名单视图 | Alice 拉黑 Bob 后,通过 `{get topic="me" what="sub"}` 读取联系人列表并过滤 `sub.private.x-im-blocked=true` |
| 删除好友 | Alice 对 P2P topic 执行 `{leave unsub=true}`,再次读取 `me.sub` 时目标联系人已消失 |
| 群二维码 | 群主写入群 `public.x-im-qrcode`,再通过 `{get what="desc"}` 读回 |
| 群主转让 | Alice 先给 Bob 的 `given` 授予 `JRWPASDO`,Bob 再设置自己的 `want=JRWPASDO` 接受转让 |
| 群主唯一性 | 转让后 Bob 的 `acs.mode` 包含 `O`,Alice 的 `acs.mode` 不再包含 `O` |
已执行以下联调:
| 命令 | 结果 |
|---|---|
| `gofmt -w tools/wsmock/main.go` | 通过 |
| `go test -count=1 ./tools/wsmock` | 通过,无测试文件 |
| `go build ./tools/wsmock` | 通过 |
| `go run -tags mysql ./server -config=tinode.conf -static_data=-` | 服务启动成功,监听 `:6060`,MySQL adapter schema `118` |
| `go run ./tools/wsmock -apikey '<generated>' -scenario pages -timeout 12s` | 通过 |
| `go run ./tools/wsmock -apikey '<generated>' -scenario group-owner -timeout 12s` | 通过 |
联调观察:
| 协议点 | 实际返回 |
|---|---|
| `x-im-settings` | `allowSearchByCard/allowSearchByQr/allowSearchByGroup/allowSearchByPhone/joinGroupNeedsApproval/teenMode` 均可通过 `me.private` 保存并读回 |
| 收藏消息 | `slf` 收藏消息返回 `202 accepted`,搜索返回 `208 delivered` 且 `params.count=1` |
| 文件助手 | `slf` 文件卡片返回 `202 accepted`,文件类型筛选返回 `208 delivered` 且 `params.count=1` |
| 黑名单列表 | 拉黑后 `me.sub` 中对应联系人 `private.x-im-blocked=true` |
| 删除好友 | `{leave topic=usrPeer unsub=true}` 返回 `200 ok`,后续 `me.sub` 不再包含该联系人 |
| 群二维码 | `public.x-im-qrcode="im://group/<groupTopic>"` 可从群 `desc.public` 读回 |
| 群主转让 | Bob 接受后服务端向 Bob 推送 `given="+O"`,向 Alice 推送 `want="-O", given="-O"` |
| 新群主操作 | Bob 成为群主后可继续更新 `aux.x-im-group.announcement`,返回 `200 ok` |
| 场景归属 | `pages` 会写真实用户 private、`slf` 消息和 P2P 订阅;`group-owner` 会改变群 owner,均暂不纳入默认 `all` |
后续仍需前端或运营侧承接的页面:
| 页面/能力 | 当前状态 |
|---|---|
| 新朋友审批队列 | 后续记录(十九)已补齐 `private.x-im-applyStatus` 状态字段和 WS 联调 |
| 进群审批队列 | 后续记录(十九)已补齐 `want/given` 待审批订阅、`x-im-joinStatus` 自动流转和 WS 联调 |
| 系统消息列表 | 当前已覆盖普通用户向 `sys` 举报;平台主动系统通知需要系统账号、运营后台或服务端投递入口 |
| 版本更新/权限弹窗 | 多数为客户端本地或应用商店/配置中心能力,后端只需在后续配置接口中承载版本策略 |
## 开发后回归记录(十七)
> 执行时间:2026-07-31 15:40 CST
本轮按本地 MySQL 环境重新整理并验证测试命令。普通 `go test ./server/...` 会同时编译 PostgreSQL、MongoDB、RethinkDB 的 adapter 测试包;这些测试包没有对应 build tag 时会使用 blank adapter,因此 `backend.GetTestAdapter` 未定义。MySQL CI 应显式使用 `-tags mysql`,并排除其它数据库 adapter 的测试包。
测试环境:
| 项 | 值 |
|---|---|
| MySQL 地址 | `127.0.0.1:3306` |
| 业务库 | `tinode`,未用于 adapter reset 测试 |
| 测试库 | `tinode_test` |
| 测试配置 | `server/db/mysql/tests/test.conf`,`reset_db_data=true` |
| 数据库存在性 | `tinode` 与 `tinode_test` 均存在 |
已执行以下测试:
| 命令 | 结果 |
|---|---|
| `mysql -uroot -proot -h127.0.0.1 -P3306 -e 'SHOW DATABASES LIKE "tinode%";'` | 查到 `tinode`、`tinode_test` |
| `go test -count=1 -tags mysql ./server/db/mysql/tests` | 通过,`ok github.com/tinode/chat/server/db/mysql/tests 0.617s` |
| `go test -count=1 -tags mysql $(go list ./... \| grep -v '/server/db/mongodb/tests$' \| grep -v '/server/db/postgres/tests$' \| grep -v '/server/db/rethinkdb/tests$')` | 通过,包含核心 `server`、MySQL adapter tests、`tools/wsmock` 等包 |
后续 CI 建议:
```bash
go test -count=1 -tags mysql ./server/db/mysql/tests
go test -count=1 -tags mysql $(go list ./... | grep -v '/server/db/mongodb/tests$' | grep -v '/server/db/postgres/tests$' | grep -v '/server/db/rethinkdb/tests$')
```
## 风险与注意事项
1. `server/db/mysql/tests/test.conf` 当前连接的是 `tinode_test`,并且会重置测试数据。不要把该配置直接改成生产/业务库 `tinode` 后执行测试。
2. PostgreSQL、MongoDB、RethinkDB 测试本次未执行;如后续需要支持这些后端,应分别准备对应数据库实例并使用对应 build tag 单独测试。
3. WebSocket 端到端联调已覆盖核心 Phase 2 场景、基础 P2P/搜索/举报、好友/联系人私有状态、拉黑 ACL、收藏、文件助手、删除好友、黑名单视图、群二维码、群主转让、真实文件上传与附件 GC 绑定、群业务权限、完整群管理流程和完整 WebRTC 信令;真实浏览器音视频采集、设备权限、NAT 穿透质量仍需在前端或真机环境补测。
4. 当前目录不是 git 仓库,无法通过 `git status` 生成变更基线。
## 开发后回归记录(十八)
> 执行时间:2026-07-31 16:00 CST
本轮实现并验证 Phase 3 MVP:反诈风控、内容审核标记、话题推荐排序、MySQL 全文搜索优化。设计原则仍保持小改动:不新增 HTTP 业务接口,不修改 Tinode 原生协议含义,前后端继续通过 WebSocket `x-im-*` 扩展字段对接。
代码与数据库变更:
| 项 | 说明 |
|---|---|
| 反诈风控 | `{pub}` 入库和广播前调用 `imReviewPubMessage`,命中转账、验证码、外链、违禁词等规则时写入 `head.x-im-anti-fraud=true`、`head.x-im-risk-level`、`head.x-im-moderation` |
| 内容审核 | 当前为内置规则标记 MVP,只标记 `flagged` 与 `reasons`,不阻断消息,避免误伤主聊天链路 |
| 推荐排序 | `fnd` 的 `{get what="sub"}` 支持 `x-im-recommend`,MySQL adapter 通过 `FindRecommendedTopics` 按关键词、类型标签、业务标签、订阅数和活跃时间排序 |
| 全文搜索优化 | `im_message_index.search_text` 增加 `FULLTEXT INDEX im_message_index_search_text(search_text)`;查询使用 `MATCH ... AGAINST` + `LIKE` fallback |
| 数据库版本 | MySQL adapter/schema 从 `118` 升级到 `119` |
| 顺手修复 | MySQL `UserDelete` 硬删除拥有 topic 的用户时,`tx.Exec` 参数切片已改为 `args...` 展开 |
数据库执行与确认:
| 命令 | 结果 |
|---|---|
| `go run -tags mysql ./tinode-db -config=server/tinode.conf -upgrade` | 业务库从 `118` 升级到 `119`,输出 `Database successfully upgraded.` |
| `SELECT value FROM kvmeta WHERE key='version'` | 返回 `119` |
| `SHOW INDEX FROM im_message_index WHERE Key_name='im_message_index_search_text'` | 返回 `Index_type=FULLTEXT` |
已执行以下测试:
| 命令 | 结果 |
|---|---|
| `gofmt -w server/datamodel.go server/imext.go server/topic.go server/store/types/types.go server/db/mysql/adapter.go server/db/mysql/tests/mysql_test.go server/imext_test.go tools/wsmock/main.go` | 通过 |
| `go test -count=1 ./server` | 通过,`ok github.com/tinode/chat/server 0.032s` |
| `go test -count=1 ./tools/wsmock` | 通过,无测试文件 |
| `go test -count=1 -tags mysql ./server/db/mysql/tests` | 通过,`ok github.com/tinode/chat/server/db/mysql/tests 0.688s` |
| `go test -count=1 -tags mysql $(go list ./... \| grep -v '/server/db/mongodb/tests$' \| grep -v '/server/db/postgres/tests$' \| grep -v '/server/db/rethinkdb/tests$')` | 通过,包含核心 server、MySQL adapter tests、`tools/wsmock` 等包 |
| `go run -tags mysql . -config=tinode.conf -static_data=-` | 服务启动成功,监听 `:6060`,MySQL adapter schema `119` |
| `go run ./tools/wsmock -apikey '<generated>' -scenario phase3 -timeout 12s` | 通过,输出 `wsmock scenario "phase3" passed` |
WebSocket 联调观察:
| 协议点 | 实际返回 |
|---|---|
| 风险消息 | Alice 发送 `请点击链接并输入验证码后转账` 后,Bob 收到 `{data}`,`head.x-im-anti-fraud=true` |
| 风险等级 | 返回 `head.x-im-risk-level="high"` |
| 审核标记 | 返回 `head.x-im-moderation.status="flagged"`,`reasons=["payment-scam","credential-risk","external-link"]` |
| 推荐查询 | Alice 通过 `fnd` 发送 `{get what="sub" x-im-recommend={keyword:"发布会",types:["news"],labels:["美食"]}}` |
| 推荐返回 | 返回 `meta.sub`,频道话题按 Tinode 规范显示为 `chn...`,并带 `public.x-im-topic` 与标签列表 |
后续可演进项:
| 能力 | 当前状态 | 后续建议 |
|---|---|---|
| 风控 | 内置关键词规则标记 | 接入独立风控服务、用户画像、设备/IP 规则 |
| 内容审核 | 规则命中后 `flagged` | 建后台审核队列、人工复核和处置闭环 |
| 推荐 | MySQL 轻量排序 | 引入异步行为统计、热度衰减、个性化召回 |
| 全文搜索 | MySQL FULLTEXT + LIKE fallback | 如果搜索量上升,再接 OpenSearch/Elastic/Manticore 等独立搜索服务 |
## 开发后回归记录(十九)
> 执行时间:2026-07-31 16:31 CST
本轮补齐好友审批状态与进群审批队列,继续保持 WebSocket + Tinode ACL 的实现方式,不新增数据库表、不新增 HTTP 业务接口。
代码变更:
| 项 | 说明 |
|---|---|
| 好友审批状态 | P2P 订阅 `private` 增加 `x-im-applyStatus`,支持 `pending/accepted/rejected/expired` 枚举校验 |
| 进群申请备注 | 群订阅 `private` 增加 `x-im-joinApplyText` 与 `x-im-joinStatus`,备注最长 256 字符 |
| 待审批订阅 | 群 `joinApproval=true` 且默认 `defacs.auth=N` 时,申请人 `{sub set.sub.mode="JRWPS"}` 会保存 `want=JRWPS/given=N`,但不会 attach 到群 |
| 管理员队列 | 管理员 `{get topic=grp what=sub}` 可看到待审批用户的 `acs` 与 `private.x-im-join*` |
| 审批流转 | 管理员 `{set sub user=... mode="JRWPS"}` 自动把申请人 `x-im-joinStatus` 更新为 `accepted`;`mode="N"` 自动更新为 `rejected` |
| 审批留痕 | 新增 `x-im-type="group-join-approve/group-join-reject"` 消息类型,管理员可按需发布审批结果消息 |
| Mock 联调 | 新增 `tools/wsmock -scenario approvals`,覆盖好友状态、非法状态校验、进群 pending/accepted/rejected |
已执行以下测试:
| 命令 | 结果 |
|---|---|
| `gofmt -w server/imext.go server/topic.go server/imext_test.go server/topic_test.go tools/wsmock/main.go` | 通过 |
| `go test -count=1 ./server` | 通过,`ok github.com/tinode/chat/server 0.027s` |
| `go test -count=1 ./tools/wsmock` | 通过,无测试文件 |
| `go test -count=1 -tags mysql ./server/db/mysql/tests` | 通过,`ok github.com/tinode/chat/server/db/mysql/tests 0.936s` |
| `go test -count=1 -tags mysql $(go list ./... \| grep -v '/server/db/mongodb/tests$' \| grep -v '/server/db/postgres/tests$' \| grep -v '/server/db/rethinkdb/tests$')` | 通过,包含核心 server、MySQL adapter tests、`tools/wsmock` 等包 |
| `go run -tags mysql . -config=tinode.conf -static_data=-` | 服务启动成功,监听 `:6060`,MySQL adapter schema `119` |
| `go run ./tools/wsmock -apikey '<generated>' -scenario approvals -timeout 12s` | 通过,输出 `wsmock scenario "approvals" passed` |
WebSocket 联调观察:
| 协议点 | 实际返回 |
|---|---|
| 好友申请 | Alice 创建 P2P 时写入 `private.x-im-applyText` 与 `x-im-applyStatus=pending`,`me.sub.private` 可读回 |
| 好友同意 | Bob 订阅 P2P 并写入 `x-im-applyStatus=accepted`;Alice 更新后从联系人列表读回 `accepted` |
| 好友拒绝 | Alice 写入 `x-im-applyStatus=rejected` 后从 `me.sub.private` 读回 |
| 非法状态 | `x-im-applyStatus="done"` 返回 `400 malformed` |
| 进群申请 | Carol 对审批群 `{sub set.sub.mode="JRWPS"}` 返回 `200 ok`,`acs.want=JRWPS/given=N/mode=N` |
| 待审批发言 | Carol 未 attach 到群,发言返回 `409 must attach first` |
| 管理员队列 | Alice `{get what=sub}` 可看到 Carol 的 `private.x-im-joinApplyText` 与 `x-im-joinStatus=pending` |
| 通过入群 | Alice 授权 Carol `mode=JRWPS` 后,Carol 订阅群并读回 `x-im-joinStatus=accepted`,随后可发言 |
| 拒绝入群 | Dave 申请后 Alice 设置 `mode=N`,管理员列表读回 `x-im-joinStatus=rejected` |
仍需后续承接:
| 能力 | 说明 |
|---|---|
| 平台主动系统通知 | 仍需要系统账号、运营后台或服务端投递入口 |
| 审批后台页面 | 后端状态已可查可改,管理页面需要前端按 `me.sub` 和 `grp.sub` 渲染 |
| 真机推送 | 当前 WS 在线链路已通,离线 push 文案与跳转需要移动端补测 |