# 后端测试执行报告(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 '' -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 '' -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 '' -scenario group -timeout 12s` | 通过 | | `go run ./tools/wsmock -apikey '' -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 '' -scenario call-flow -timeout 12s` | 通过 | | `go run ./tools/wsmock -apikey '' -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 '' -scenario group-admin -timeout 12s` | 通过 | | `go run ./tools/wsmock -apikey '' -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 ` | | 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 '' -scenario upload -timeout 12s` | 通过 | | `mysql -uroot -proot -h127.0.0.1 -P3306 tinode -e ''` | 查到上传文件与消息引用 | | `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 '' -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 '' -scenario pages -timeout 12s` | 通过 | | `go run ./tools/wsmock -apikey '' -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/"` 可从群 `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 '' -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 '' -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 文案与跳转需要移动端补测 |