Files
im_server/docs/problems/IM后端问题报告-好友通过后系统提示投递到旧命名话题.md
T

240 lines
13 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.
# IM 后端问题报告:好友申请通过后 `friend_added` 系统提示被投递到旧命名话题 `usr<uid>`,同一会话存在两套 Topic
> 报告日期:2026-09-13
> 客户端:iOS(MeetingIM Swift SDK)
> 涉及账号:`usrv97xq86jw3c`(echarles5,**通过人本人**,本报告取证端)、`usrHnkwn8VSfKc`(charles1,**申请人**)
> 涉及话题:`usrHnkwn8VSfKc`(旧标准命名)与 `p2pv97xq86jw3ceeTCfxVJ8pw`(后端内部命名)
> 优先级建议:**高**(会话列表消息与未读不更新,用户可见)
---
## 1. 问题概述
好友申请通过后,服务端把一条**会话内系统提示** `system_notice / friend_added`("你们已成为好友,现在可以开始聊天了。")下发到了
- ❌ `usrHnkwn8VSfKc`(MeetingIM 的 `usr<对端uid>` 旧标准命名,**本端并未订阅该话题**)
而不是
- ✅ `p2pv97xq86jw3ceeTCfxVJ8pw`(服务端自己在 `me` 的 `meta sub` 里下发、并在 `friend.request.accepted` 的 `payload.topic` 里给出的**真正会话**)。
同一时刻服务端还把**同一条逻辑消息**(同 `ts`、同 `seq=17`)写进了 `p2p...` 话题,形成**双写**。
结果:客户端本地同一好友出现**两条单聊会话**,导致用户可见的两个问题:
1. 会话行的 `usr<uid>` 那条没有服务端下发的 `acs` → `isJoiner == false` → 客户端在会话行右下角显示出 `nosign`(🚫)禁止图标;
2. 客户端会话列表去重逻辑保留 `usr<uid>`、隐藏 `p2p...` → **对方的后续消息与未读数都不更新**(未读还被从总数里扣掉,"消息"Tab 角标偏小)。
---
## 2. 时间线(取证端 = 通过人本人 B,2026-09-13)
| 时间(UTC) | 事件 |
| --- | --- |
| 06:13:39 | B(echarles5)删除好友 A(charles1),双方关系解除(A 端收到 `friend.deleted`) |
| 06:27:09.610 | A 申请、B 通过;服务端下发 `friend_added` 系统提示,**同时写入 `usrHnkwn8VSfKc`(seq 17)与 `p2pv97xq86jw3ceeTCfxVJ8pw`(seq 17)** |
| 06:27:09.594 | `GET /v1/im/friends` 返回 A 的会话 `topic = p2pv97xq86jw3ceeTCfxVJ8pw` |
| 06:27:10.367 | `POST /v1/im/conversations/p2p {"peer_user_id":"usrHnkwn8VSfKc"}` 返回 `topic = p2pv97xq86jw3ceeTCfxVJ8pw` |
| 06:27:11.070 | `me` 的 `meta sub` 只下发 `p2pv97xq86jw3ceeTCfxVJ8pw`(订阅 `acs=JRWPA`、`priv.peer=usrHnkwn8VSfKc`),**没有 `usrHnkwn8VSfKc`** |
| 06:27:32.089 | A 发送消息 `a`,落在 `p2pv97xq86jw3ceeTCfxVJ8pw` seq 18 |
---
## 3. 服务端下行报文(原文)
### 3.1 系统提示被实时投递到旧命名话题(本端未订阅)
```json
{
"data": {
"topic": "usrHnkwn8VSfKc",
"ts": "2026-09-13T06:27:09.61Z",
"seq": 17,
"head": {
"x-im-from": "system",
"x-im-schema": 1,
"x-im-template": "friend_added",
"x-im-type": "system_notice"
},
"content": {
"actors": [
{ "user_id": "usrv97xq86jw3c", "click": { "target_id": "usrv97xq86jw3c", "type": "user_profile" } },
{ "user_id": "usrHnkwn8VSfKc", "click": { "target_id": "usrHnkwn8VSfKc", "type": "user_profile" } }
],
"extra": { "request_id": "frq_OYo72xKkkgU" },
"level": "normal",
"template": "friend_added",
"text": "你们已成为好友,现在可以开始聊天了。"
}
}
}
```
客户端侧随即落库并回执(说明客户端把它当作**合法会话消息**处理):
```text
SqlStore - msgReceived:来自话题 payload usrHnkwn8VSfKc,topicId=9,userId=3,seq=17
出站[回执(note)] {"note":{"what":"recv","topic":"usrHnkwn8VSfKc","seq":17}}
入站[在线状态(pres)] {"pres":{"topic":"me","src":"usrHnkwn8VSfKc","what":"msg","seq":17}}
```
### 3.2 同一条消息也被写进真会话(双写证据)
随后客户端订阅真会话拉历史,拿到**同 `ts`、同 `seq`** 的同一条提示:
```json
{
"data": {
"topic": "p2pv97xq86jw3ceeTCfxVJ8pw",
"ts": "2026-09-13T06:27:09.61Z",
"seq": 17,
"head": { "x-im-from": "system", "x-im-schema": 1, "x-im-template": "friend_added", "x-im-type": "system_notice" },
"content": { "template": "friend_added", "text": "你们已成为好友,现在可以开始聊天了。", "...": "与上一条 content 完全一致" }
}
}
```
```text
SqlStore - msgReceived:来自话题 payload p2pv97xq86jw3ceeTCfxVJ8pw,topicId=11,userId=3,seq=17
```
> 同一 `ts` + 同一 `seq=17` 出现在两个 topic ⇒ 服务端对该提示**写了两份**。
> 本地 `topicId`:`usrHnkwn8VSfKc = 9`、`p2pv97xq86jw3ceeTCfxVJ8pw = 11` —— 旧命名话题**更早存在**,本轮关系重建后并未被撤销或合并。
### 3.3 服务端自己只承认 `p2p...`(`me` 的 `meta sub`)
```json
{
"meta": {
"id": "115975",
"topic": "me",
"ts": "2026-09-13T06:27:32.444Z",
"sub": [
{
"updated": "2026-09-13T06:27:11.07Z",
"online": true,
"acs": { "want": "JRWPA", "given": "JRWPA", "mode": "JRWPA" },
"public": {
"avatar_file_id": "ZrI4Fut4heE.jpg",
"display_name": "charles1",
"fn": "charles1",
"name": "charles1",
"nickname": "charles1",
"public_account": "usrHnkwn8VSfKc"
},
"private": { "created_by": "http", "peer": "usrHnkwn8VSfKc" },
"topic": "p2pv97xq86jw3ceeTCfxVJ8pw",
"touched": "2026-09-13T06:27:32.089Z",
"seq": 18
}
]
}
}
```
> `sub` 数组里**只有** `p2pv97xq86jw3ceeTCfxVJ8pw`。服务端却把 `{data}` 投到了 `usrHnkwn8VSfKc` —— 这是一个**本端从未订阅**的话题。
### 3.4 业务事件与 HTTP 接口都指向 `p2p...`
```json
{"info":{"topic":"usrv97xq86jw3c","from":"usrHnkwn8VSfKc","what":"im","event":"friend.request.accepted",
"payload":{"topic":"p2pv97xq86jw3ceeTCfxVJ8pw","user_id":"usrHnkwn8VSfKc","request_id":"frq_OYo72xKkkgU"}}}
```
```json
{"user_id":"usrHnkwn8VSfKc","display_name":"charles1","relationship":"friend",
"topic":"p2pv97xq86jw3ceeTCfxVJ8pw","avatar_file_id":"ZrI4Fut4heE.jpg"}
```
> `friend.request.accepted` 的 `payload.topic`、`GET /v1/im/friends` 的 `topic`、`POST /v1/im/conversations/p2p` 的响应都明确是 `p2pv97xq86jw3ceeTCfxVJ8pw`。
> 说明服务端**知道**该会话的真实 topic,但系统提示仍写到了 `usr<uid>`。
---
## 4. 与接口文档契约的冲突
| 文档位置 | 约定 | 实际 |
| --- | --- | --- |
| 《IM-前后端对接文档》0.5.4.3.1(约 330 行) | "会话内系统消息**必须进入对应 P2P 或群 Topic 的消息流**,占用 `seq`" | `friend_added` 同时进入 `usr<uid>`(非对应会话) |
| 同章 `system_notice` 下行示例(约 337 行) | `{"data":{"topic":"p2p...", ... "x-im-template":"friend_added" ...}}` | 实际 `"topic":"usrHnkwn8VSfKc"` |
| 同文档 3.5(约 1108 行) | 好友申请通过后服务端"在同一事务内建立双方关系和 **P2P 订阅**" | 建立了 `p2p...` 订阅,但旧 `usr<uid>` topic/订阅仍在,且系统提示写到旧 topic |
| 同文档 `GET /v1/im/friends`(约 1564 行) | 好友的会话 `topic` 为 `p2p...` | 一致 ✅(正因如此客户端更无法理解 `usr<uid>` 上的消息) |
---
## 5. 对客户端的影响(为什么客户端只能兜、不能治)
1. **会话列表出现两条同一好友的会话**,去重只能靠启发式规则(按对端 uid 分组打分)择一保留;
2. **保留错的那条就丢消息/未读**:旧命名话题 `acs` 为空(`isJoiner == false`)但本地已落库消息,客户端历史上"标准命名优先"的规则会保留它、隐藏真会话 → 用户看到会话不更新;
3. **误显示禁止图标**:`acs` 为空时客户端无法区分"未同步"与"已不是成员",会在会话行右下角画 `nosign`(🚫);
4. **历史数据永久分裂**:已有一批用户的本地历史落在 `usr<uid>`,服务端新会话在 `p2p...`,客户端无法自动合并;
5. 客户端对**未订阅**话题收到的 `{data}` 只能被动落库(已回执 `recv`),无法判断该不该收。
---
## 6. 需要后端确认 / 处理
1. **`friend_added` 等会话内系统提示为什么写到 `usr<uid>`?**
请改为只写**会话 topic**(当前为 `p2p...`),与 `payload.topic` / `GET /friends` 的 `topic` 保持一致。
2. **同一对好友为什么存在两套 Topic + 两份订阅?**
重装/删好友/重新加好友后,旧的 `usr<uid>` topic 与订阅应当被撤销或合并;
当前 `usrHnkwn8VSfKc`(本端 uid 维度)与 `p2pv97xq86jw3ceeTCfxVJ8pw` 同时存在,且系统提示双写。
3. **`{data}` 投递到未在 `me` 的 `meta sub` 里的话题是否预期?**
若不是,请修投递;若是(设计如此),请在文档里明确 `usr<uid>` 仍是合法会话名,并同步在 `meta sub` 里下发它的 `acs`/`seq`/`pub`,否则客户端无法正确渲染。
4. **是否可以把单聊统一收敛到 `p2p...`**,并提供旧 `usr<uid>` 历史消息的迁移/合并方案(客户端愿意配合按话题重定向)。
5. **(附带,需确认口径)** `friend.request.accepted` 的 `payload.user_id` 与文档不一致:
文档写 `payload:{"user_id":"usr_actor"}`(通过人),实测该帧在**通过人本人**设备上 `from` 和 `payload.user_id` 都是**申请人** `usrHnkwn8VSfKc`。客户端目前已按"`payload.user_id` = 对方"消费(用于刷新会话 context、反查会话),若后端要改回文档口径请提前同步。
---
## 7. 复现步骤
### 前置
- 账号 A:`usrHnkwn8VSfKc`(charles1),账号 B:`usrv97xq86jw3c`(echarles5),两台设备/两个模拟器分别登录;
- 两端都开启 Debug 日志(会打印 `入站[消息数据(data)]` 与 `SqlStore - msgReceived`)。
### 步骤
1. **先制造"旧命名话题"残留**:B 删除 A 好友(B 端:聊天 → A 的会话 → 右上角设置 → 「删除好友」,确认删除)。
- 预期:A 端收到 `{"info":{"...","event":"friend.deleted","payload":{"user_id":"usrv97xq86jw3c"}}}`。
- 此时 B 端本地已存在旧命名话题 `usr<A的uid>`(本次取证中为 `usrHnkwn8VSfKc`)。
2. A 向 B 发起好友申请(A 端:添加好友 → 搜索 `echarles5` → 发送申请)。
3. B 通过好友申请(B 端:通讯录 → 新的朋友 → 通过)。
4. **观察 B 端日志**,应能复现:
```text
入站[消息数据(data)] {"data":{"topic":"usrHnkwn8VSfKc","seq":17,"head":{"x-im-template":"friend_added","x-im-type":"system_notice"},...}}
SqlStore - msgReceived:来自话题 payload usrHnkwn8VSfKc,topicId=9,userId=3,seq=17
入站[在线状态(pres)] {"pres":{"topic":"me","src":"usrHnkwn8VSfKc","what":"msg","seq":17}}
```
随后拉真会话历史时,同 `ts`/`seq` 的提示又出现在 `p2pv97xq86jw3ceeTCfxVJ8pw`;
并且 `me` 的 `{meta sub}` 里**只有** `p2pv97xq86jw3ceeTCfxVJ8pw`。
5. **观察 B 端界面**:会话列表里 A 的那一行副标题是"你们已成为好友,现在可以开始聊天了。",右下角带 🚫 图标。
6. A 再发一条消息(如 `a`)→ B 端该会话行**副标题与未读数都不更新**(消息实际落在 `p2pv97xq86jw3ceeTCfxVJ8pw`,被去重隐藏)。
7. **服务端核对**:`topics` 表应能查到同一对好友的两行
(`usrHnkwn8VSfKc` 与 `p2pv97xq86jw3ceeTCfxVJ8pw`),以及双方的订阅记录;`messages` 表里 `friend_added` 在两个话题下各有一条(同 `ts`、同 `seq`)。
### 最小验证(不依赖"先删好友"这段历史)
让 A 申请、B 通过,直接核对服务端把 `friend_added` 写到了哪个 topic:若仍是 `usr<A的uid>`,即复现本问题(第 1 步只是让残留话题必然存在,便于稳定复现)。
---
## 8. 验收标准
1. 好友申请通过后,`friend_added` **只**下发到会话 topic(当前为 `p2p...`),且该 topic 出现在 `me` 的 `meta sub` 中;`usr<uid>` 上不再出现任何消息;
2. `me` 的 `meta sub` 中同一对好友**只有一个** P2P topic、一份订阅;历史遗留的 `usr<uid>` topic/订阅被撤销或合并;
3. 对不存在的会话不再有"投递到未订阅话题"的 `{data}`(或按文档补齐该话题的 `meta sub`);
4. 客户端表现(不依赖本轮客户端兜底):B 端会话列表只出现**一行** A 的会话,副标题为最新消息、未读角标正确、**无 🚫 图标**;A 端同理。
---
## 9. 客户端已做的兜底(不依赖后端修复,但不能替代后端修)
1. `Utils.duplicateP2PTopicNames` 改为**按对端 uid 分组 + 打分择一保留**
(本地有消息 8 > 仍是成员 `isJoiner` 4 > 标准命名 2 > 有 `priv.peer` 1),
让"有消息且在关系内"的 `p2p...` 真会话优先,同时保留"有历史优先"以免去重丢消息;
2. `ChatListViewCell` 的禁止图标改为**仅在确认服务端下发过 `acs.mode` 且确非成员**时显示
(`acs` 未同步不再误画 🚫);
3. 会话列表过滤对"本地已有消息但 `acs` 未下发"的单聊放宽(`isLocalKnownP2PConversation`),
保证"有消息就一定有会话行"。
> 以上兜底只解决"显示哪一条、图标对不对"。**系统提示被写进隐藏话题、历史分裂在两个 topic** 这两点必须后端收敛后才能根治。