feat: add message mutation sync

This commit is contained in:
richjohnrambo
2026-09-15 15:54:52 +08:00
parent 55afd976c9
commit bc27a5c574
12 changed files with 949 additions and 13 deletions
+220
View File
@@ -1420,6 +1420,7 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
| `PUT /v1/im/topics/{topic}/draft` 待实现 | `{"content":"未发送草稿","version":1}` | `{"topic":"grp...","content":"未发送草稿","version":2,"updated_at":"2026-08-20T09:00:00Z"}` |
| `POST /v1/im/topics/{topic}/messages/{seq}/recall` | 单条撤回;`topic` 可传 `p2p...` 或 `grp...`;Body `{}`,可选 `{"version":1,"reason":"mistake"}` | `{"topic":"grp...","seq":12,"status":"recalled","version":2,"updated_at":"2026-08-20T09:00:00Z"}` |
| `POST /v1/im/topics/{topic}/messages/recall` | 多选批量撤回;`{"items":[{"seq":12,"version":1},{"seq":13}],"reason":"mistake"}`;`items` 1~50 条,`version` 可选 | `{"topic":"grp...","items":[{"topic":"grp...","seq":12,"status":"recalled","version":2,"updated_at":"2026-08-20T09:00:00Z"},{"seq":13,"status":"failed","http":409,"code":30006,"reason":"VERSION_CONFLICT","message":"message version conflict"}]}` |
| `GET /v1/im/topics/{topic}/message-mutations?since_version=&limit=` | 拉取撤回/编辑等原地消息变更;`since_version` 取本地已应用的会话级 `mutation_version` | `{"topic":"grp...","items":[{"seq":12,"version":2,"event":"message.recalled","operator":"usr...","mutated_at":"2026-08-20T09:00:00Z","mutation_version":3}],"next_cursor":"3","has_more":false,"mutation_version":3,"expired":false}` |
| `POST /v1/im/topics/{topic}/messages/{seq}/revisions` | `{"content":{"text":"修改后的文本"},"client_mid":"mid_1","version":1}` | `{"topic":"grp...","seq":12,"status":"edited","version":2,"updated_at":"2026-08-20T09:00:00Z"}` |
| `GET /v1/im/topics/{topic}/messages/{seq}/receipts?cursor=` | Query only | `{"topic":"grp...","seq":12,"items":[{"user_id":"usr...","read_at":"2026-08-20T09:00:00Z","recv_at":"2026-08-20T08:59:00Z"}],"next_cursor":""}` |
| `GET /v1/im/topics/{topic}/messages/search?q=&type=text&cursor=` | Query only | `{"topic":"grp...","items":[{"seq":12,"from":"usr...","ts":"2026-08-20T09:00:00Z","snippet":"命中的文本","matches":[{"start":0,"end":2}]}],"next_cursor":""}` |
@@ -1429,6 +1430,225 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
消息撤回和编辑成功后,Topic 在线成员会收到 0.5.4.6 定义的 `info.what="im"` 业务事件:`message.recalled` 或 `message.edited`。批量撤回按成功项逐条产生 `message.recalled` 事件;失败项不会产生事件。前端收到后按 `topic + seq + version` 刷新本地消息,不要等待用户重新进入会话才更新。
#### 4.21.2 群消息撤回与批量撤回
群聊撤回使用 Topic 消息操作接口,`topic` 必须传真实群 Topic(`grp...`),不能传用户 ID、群 ID 昵称或本地会话别名。单条撤回和多选批量撤回都要求登录态,Header 至少包含 `Authorization: Bearer <token>`、`X-MeetingIM-APIKey`、`X-IM-Schema: 1`、`X-Request-ID`。
单条撤回:
```http
POST /v1/im/topics/{topic}/messages/{seq}/recall
```
Path 参数:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `topic` | String | 是 | 群 Topic,例如 `grp6PIoZgdpc2g`。 |
| `seq` | Int64 | 是 | 要撤回的消息序号。 |
Body:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `version` | Int | 否 | 期望消息版本。不传时后端使用当前消息版本;传旧版本可能返回版本冲突。 |
| `reason` | String | 否 | 撤回原因,当前主要用于客户端/审计扩展,可传空。 |
单条请求示例:
```json
{
"version": 1,
"reason": "mistake"
}
```
单条成功响应 `data`:
```json
{
"topic": "grp6PIoZgdpc2g",
"seq": 102,
"status": "recalled",
"version": 2,
"updated_at": "2026-09-15T04:30:00Z",
"recalled_at": "2026-09-15T04:30:00Z",
"recalled_by": "usr_actor"
}
```
多选批量撤回:
```http
POST /v1/im/topics/{topic}/messages/recall
```
Path 参数:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `topic` | String | 是 | 群 Topic,例如 `grp6PIoZgdpc2g`。 |
Body:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `items` | Array | 是 | 要撤回的消息列表,长度 1~50。 |
| `items[].seq` | Int64 | 是 | 要撤回的消息序号。 |
| `items[].version` | Int | 否 | 该消息期望版本。不传时后端使用当前消息版本。 |
| `items[].reason` | String | 否 | 单条撤回原因;可不传。 |
| `reason` | String | 否 | 批量默认撤回原因;单条 `items[].reason` 可覆盖。 |
批量请求示例:
```json
{
"items": [
{
"seq": 102,
"version": 1
},
{
"seq": 103,
"version": 1
}
],
"reason": "mistake"
}
```
批量响应始终按条目返回处理结果。一条失败不影响其他条目;成功条目会包含最新 `version`,失败条目会包含该条失败原因。
```json
{
"topic": "grp6PIoZgdpc2g",
"items": [
{
"topic": "grp6PIoZgdpc2g",
"seq": 102,
"status": "recalled",
"version": 2,
"updated_at": "2026-09-15T04:30:00Z",
"recalled_at": "2026-09-15T04:30:00Z",
"recalled_by": "usr_actor"
},
{
"seq": 103,
"status": "failed",
"http": 409,
"code": 30006,
"reason": "VERSION_CONFLICT",
"message": "message version conflict"
}
]
}
```
常见失败项:
| `http` | `code` | `reason` | 说明 |
| --- | --- | --- | --- |
| `400` | `30000` | `MALFORMED_REQUEST` | `seq` 非法、`items` 为空或超过 50 条。 |
| `403` | `30005` | `PERMISSION_DENIED` | 当前用户无权撤回该消息,或消息已超过撤回窗口。超过窗口时 `message` 为 `message recall window expired`。 |
| `404` | `30004` | `MALFORMED_REQUEST` | 消息不存在。 |
| `409` | `30006` | `VERSION_CONFLICT` | 请求版本与服务端当前消息版本不一致,或消息已撤回/删除。 |
权限与时限:
- 发送人可撤回自己的消息。
- 群主/管理员可撤回群成员消息。
- 当前后端撤回窗口为 120 秒;超过后返回 `message recall window expired`。
- 被撤回消息服务端会清空普通正文,历史拉取应展示撤回占位,不继续展示旧内容。
实时事件:
撤回成功后,Topic 在线成员会收到 `message.recalled`。批量撤回按成功项逐条发送事件,失败项不会发送。
```json
{
"info": {
"topic": "grp6PIoZgdpc2g",
"from": "usr_actor",
"what": "im",
"event": "message.recalled",
"payload": {
"topic": "grp6PIoZgdpc2g",
"seq": 102,
"version": 2
}
}
}
```
客户端处理规则:
- 多选撤回优先调用批量接口,不要并发打很多单条接口。
- 批量接口 HTTP `200` 只表示请求被服务端处理,仍需逐条读取 `items[].status`。
- `status="recalled"` 时,用 `topic + seq + version` 更新本地消息为撤回状态。
- `status="failed"` 时,只对该条提示失败原因,其他成功项正常生效。
- 收到实时事件后也按 `topic + seq + version` 刷新本地消息,避免只依赖发起方本地状态。
#### 4.21.3 消息变更同步水位
撤回、编辑属于 `seq` 不变的原地变更。在线且已订阅 Topic 时,客户端会收到 `message.recalled` / `message.edited` 实时事件;离线或未订阅时,需要靠会话级水位补偿。
会话列表同步时,`me.meta.sub[].private` 会带:
```json
{
"mutation_version": 3
}
```
客户端按账号 + topic 保存本地已应用水位。若服务端 `mutation_version` 大于本地值,调用:
```http
GET /v1/im/topics/{topic}/message-mutations?since_version={localMutationVersion}&limit=50
```
响应 `data`:
```json
{
"topic": "grp6PIoZgdpc2g",
"items": [
{
"seq": 102,
"version": 2,
"event": "message.recalled",
"operator": "usr_actor",
"mutated_at": "2026-09-15T04:30:00Z",
"mutation_version": 3
}
],
"next_cursor": "3",
"has_more": false,
"mutation_version": 3,
"expired": false
}
```
字段说明:
| 字段 | 说明 |
| --- | --- |
| `seq` | 被变更消息的序号。 |
| `version` | 变更后的消息版本,用于同一 `seq` 的幂等与乱序保护。 |
| `event` | `message.recalled` / `message.edited`,与实时 `info.event` 保持一致。 |
| `operator` | 操作人用户 ID。 |
| `mutated_at` | 变更发生时间。 |
| `mutation_version` | 会话级最新水位。客户端成功应用返回项后推进本地水位。 |
| `next_cursor` | 当前实现等同最新 `mutation_version`,客户端原样保存即可。 |
| `expired` | 当前轻量实现固定为 `false`;后续若服务端裁剪变更记录,该字段会用于提示客户端降级重拉。 |
客户端处理规则:
- `mutation_version` 是会话级水位,不是单条消息的 `version`。
- 本地只做 `服务端 mutation_version > 本地 mutation_version` 比较,不要假设连续递增。
- 拉到 `message.recalled` 后,将本地 `topic + seq` 标记为撤回,并只在 `version` 更高时覆盖。
- 拉到 `message.edited` 后,按 `seq + version` 拉取或覆盖消息内容。
- 实时事件和补偿接口可能重复到达,客户端必须按 `topic + seq + version` 幂等处理。
## 5. 通讯录、好友关系与黑名单
### 5.1 通讯录.png
@@ -0,0 +1,5 @@
-- IM 消息变更同步水位
-- 用于撤回/编辑等 seq 不变的原地消息变更离线补偿。
ALTER TABLE im_topics
ADD COLUMN mutation_version BIGINT NOT NULL DEFAULT 0 AFTER subcnt;
@@ -1379,6 +1379,7 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
| `PUT /v1/im/topics/{topic}/draft` 待实现 | `{"content":"未发送草稿","version":1}` | `{"topic":"grp...","content":"未发送草稿","version":2,"updated_at":"2026-08-20T09:00:00Z"}` |
| `POST /v1/im/topics/{topic}/messages/{seq}/recall` | 单条撤回;`topic` 可传 `p2p...` 或 `grp...`;Body `{}`,可选 `{"version":1,"reason":"mistake"}` | `{"topic":"grp...","seq":12,"status":"recalled","version":2,"updated_at":"2026-08-20T09:00:00Z"}` |
| `POST /v1/im/topics/{topic}/messages/recall` | 多选批量撤回;`{"items":[{"seq":12,"version":1},{"seq":13}],"reason":"mistake"}`;`items` 1~50 条,`version` 可选 | `{"topic":"grp...","items":[{"topic":"grp...","seq":12,"status":"recalled","version":2,"updated_at":"2026-08-20T09:00:00Z"},{"seq":13,"status":"failed","http":409,"code":30006,"reason":"VERSION_CONFLICT","message":"message version conflict"}]}` |
| `GET /v1/im/topics/{topic}/message-mutations?since_version=&limit=` | 拉取撤回/编辑等原地消息变更;`since_version` 取本地已应用的会话级 `mutation_version` | `{"topic":"grp...","items":[{"seq":12,"version":2,"event":"message.recalled","operator":"usr...","mutated_at":"2026-08-20T09:00:00Z","mutation_version":3}],"next_cursor":"3","has_more":false,"mutation_version":3,"expired":false}` |
| `POST /v1/im/topics/{topic}/messages/{seq}/revisions` | `{"content":{"text":"修改后的文本"},"client_mid":"mid_1","version":1}` | `{"topic":"grp...","seq":12,"status":"edited","version":2,"updated_at":"2026-08-20T09:00:00Z"}` |
| `GET /v1/im/topics/{topic}/messages/{seq}/receipts?cursor=` | Query only | `{"topic":"grp...","seq":12,"items":[{"user_id":"usr...","read_at":"2026-08-20T09:00:00Z","recv_at":"2026-08-20T08:59:00Z"}],"next_cursor":""}` |
| `GET /v1/im/topics/{topic}/messages/search?q=&type=text&cursor=` | Query only | `{"topic":"grp...","items":[{"seq":12,"from":"usr...","ts":"2026-08-20T09:00:00Z","snippet":"命中的文本","matches":[{"start":0,"end":2}]}],"next_cursor":""}` |
@@ -1388,6 +1389,225 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
消息撤回和编辑成功后,Topic 在线成员会收到 0.5.4.6 定义的 `info.what="im"` 业务事件:`message.recalled` 或 `message.edited`。批量撤回按成功项逐条产生 `message.recalled` 事件;失败项不会产生事件。前端收到后按 `topic + seq + version` 刷新本地消息,不要等待用户重新进入会话才更新。
#### 4.21.2 群消息撤回与批量撤回
群聊撤回使用 Topic 消息操作接口,`topic` 必须传真实群 Topic(`grp...`),不能传用户 ID、群 ID 昵称或本地会话别名。单条撤回和多选批量撤回都要求登录态,Header 至少包含 `Authorization: Bearer <token>`、`X-MeetingIM-APIKey`、`X-IM-Schema: 1`、`X-Request-ID`。
单条撤回:
```http
POST /v1/im/topics/{topic}/messages/{seq}/recall
```
Path 参数:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `topic` | String | 是 | 群 Topic,例如 `grp6PIoZgdpc2g`。 |
| `seq` | Int64 | 是 | 要撤回的消息序号。 |
Body:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `version` | Int | 否 | 期望消息版本。不传时后端使用当前消息版本;传旧版本可能返回版本冲突。 |
| `reason` | String | 否 | 撤回原因,当前主要用于客户端/审计扩展,可传空。 |
单条请求示例:
```json
{
"version": 1,
"reason": "mistake"
}
```
单条成功响应 `data`:
```json
{
"topic": "grp6PIoZgdpc2g",
"seq": 102,
"status": "recalled",
"version": 2,
"updated_at": "2026-09-15T04:30:00Z",
"recalled_at": "2026-09-15T04:30:00Z",
"recalled_by": "usr_actor"
}
```
多选批量撤回:
```http
POST /v1/im/topics/{topic}/messages/recall
```
Path 参数:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `topic` | String | 是 | 群 Topic,例如 `grp6PIoZgdpc2g`。 |
Body:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `items` | Array | 是 | 要撤回的消息列表,长度 1~50。 |
| `items[].seq` | Int64 | 是 | 要撤回的消息序号。 |
| `items[].version` | Int | 否 | 该消息期望版本。不传时后端使用当前消息版本。 |
| `items[].reason` | String | 否 | 单条撤回原因;可不传。 |
| `reason` | String | 否 | 批量默认撤回原因;单条 `items[].reason` 可覆盖。 |
批量请求示例:
```json
{
"items": [
{
"seq": 102,
"version": 1
},
{
"seq": 103,
"version": 1
}
],
"reason": "mistake"
}
```
批量响应始终按条目返回处理结果。一条失败不影响其他条目;成功条目会包含最新 `version`,失败条目会包含该条失败原因。
```json
{
"topic": "grp6PIoZgdpc2g",
"items": [
{
"topic": "grp6PIoZgdpc2g",
"seq": 102,
"status": "recalled",
"version": 2,
"updated_at": "2026-09-15T04:30:00Z",
"recalled_at": "2026-09-15T04:30:00Z",
"recalled_by": "usr_actor"
},
{
"seq": 103,
"status": "failed",
"http": 409,
"code": 30006,
"reason": "VERSION_CONFLICT",
"message": "message version conflict"
}
]
}
```
常见失败项:
| `http` | `code` | `reason` | 说明 |
| --- | --- | --- | --- |
| `400` | `30000` | `MALFORMED_REQUEST` | `seq` 非法、`items` 为空或超过 50 条。 |
| `403` | `30005` | `PERMISSION_DENIED` | 当前用户无权撤回该消息,或消息已超过撤回窗口。超过窗口时 `message` 为 `message recall window expired`。 |
| `404` | `30004` | `MALFORMED_REQUEST` | 消息不存在。 |
| `409` | `30006` | `VERSION_CONFLICT` | 请求版本与服务端当前消息版本不一致,或消息已撤回/删除。 |
权限与时限:
- 发送人可撤回自己的消息。
- 群主/管理员可撤回群成员消息。
- 当前后端撤回窗口为 120 秒;超过后返回 `message recall window expired`。
- 被撤回消息服务端会清空普通正文,历史拉取应展示撤回占位,不继续展示旧内容。
实时事件:
撤回成功后,Topic 在线成员会收到 `message.recalled`。批量撤回按成功项逐条发送事件,失败项不会发送。
```json
{
"info": {
"topic": "grp6PIoZgdpc2g",
"from": "usr_actor",
"what": "im",
"event": "message.recalled",
"payload": {
"topic": "grp6PIoZgdpc2g",
"seq": 102,
"version": 2
}
}
}
```
客户端处理规则:
- 多选撤回优先调用批量接口,不要并发打很多单条接口。
- 批量接口 HTTP `200` 只表示请求被服务端处理,仍需逐条读取 `items[].status`。
- `status="recalled"` 时,用 `topic + seq + version` 更新本地消息为撤回状态。
- `status="failed"` 时,只对该条提示失败原因,其他成功项正常生效。
- 收到实时事件后也按 `topic + seq + version` 刷新本地消息,避免只依赖发起方本地状态。
#### 4.21.3 消息变更同步水位
撤回、编辑属于 `seq` 不变的原地变更。在线且已订阅 Topic 时,客户端会收到 `message.recalled` / `message.edited` 实时事件;离线或未订阅时,需要靠会话级水位补偿。
会话列表同步时,`me.meta.sub[].private` 会带:
```json
{
"mutation_version": 3
}
```
客户端按账号 + topic 保存本地已应用水位。若服务端 `mutation_version` 大于本地值,调用:
```http
GET /v1/im/topics/{topic}/message-mutations?since_version={localMutationVersion}&limit=50
```
响应 `data`:
```json
{
"topic": "grp6PIoZgdpc2g",
"items": [
{
"seq": 102,
"version": 2,
"event": "message.recalled",
"operator": "usr_actor",
"mutated_at": "2026-09-15T04:30:00Z",
"mutation_version": 3
}
],
"next_cursor": "3",
"has_more": false,
"mutation_version": 3,
"expired": false
}
```
字段说明:
| 字段 | 说明 |
| --- | --- |
| `seq` | 被变更消息的序号。 |
| `version` | 变更后的消息版本,用于同一 `seq` 的幂等与乱序保护。 |
| `event` | `message.recalled` / `message.edited`,与实时 `info.event` 保持一致。 |
| `operator` | 操作人用户 ID。 |
| `mutated_at` | 变更发生时间。 |
| `mutation_version` | 会话级最新水位。客户端成功应用返回项后推进本地水位。 |
| `next_cursor` | 当前实现等同最新 `mutation_version`,客户端原样保存即可。 |
| `expired` | 当前轻量实现固定为 `false`;后续若服务端裁剪变更记录,该字段会用于提示客户端降级重拉。 |
客户端处理规则:
- `mutation_version` 是会话级水位,不是单条消息的 `version`。
- 本地只做 `服务端 mutation_version > 本地 mutation_version` 比较,不要假设连续递增。
- 拉到 `message.recalled` 后,将本地 `topic + seq` 标记为撤回,并只在 `version` 更高时覆盖。
- 拉到 `message.edited` 后,按 `seq + version` 拉取或覆盖消息内容。
- 实时事件和补偿接口可能重复到达,客户端必须按 `topic + seq + version` 幂等处理。
## 5. 通讯录、好友关系与黑名单
### 5.1 通讯录.png