diff --git a/docs/IM-前后端对接文档.md b/docs/IM-前后端对接文档.md index 4e51bae..2c2e724 100644 --- a/docs/IM-前后端对接文档.md +++ b/docs/IM-前后端对接文档.md @@ -1140,7 +1140,9 @@ X-MeetingIM-APIKey: <预置的普通 API Key> - 选择媒体后本地生成 `client_mid` 和稳定占位;调用文件上传,得到 `file_id` 后再 `pub x-im-type=image|video|voice`。 - `content` 至少含 `file_id,name,size,mime,width,height,duration_ms,thumbnail_file_id` 中适用字段;上传进度是客户端任务状态,不写成聊天消息。 -- 服务端发布成功后关联文件与 Topic/消息;文件下载必须校验消息可见性。上传失败只重试上传,成功但 `pub` 失败时复用已上传 `file_id` 和 `client_mid`。 +- 服务端发布成功后关联文件与 Topic/消息;文件下载必须校验消息可见性。 +- 上传失败(网络中断、超时、5xx)按**整文件重传**处理:重新调用 `POST /v0/file/u/` 上传完整文件,不做分片/断点续传;每次重传得到新 `file_id`,旧 `file_id` 直接放弃、由服务端 TTL 回收,客户端不得再引用。 +- 上传成功但 `pub` 失败时不重新上传,复用已上传 `file_id` 和原 `client_mid` 重发消息,服务端按 `client_mid` 幂等去重。 ### 3.7 个人对话—拉黑状态/已读状态.png @@ -1294,7 +1296,9 @@ X-MeetingIM-APIKey: <预置的普通 API Key> **状态:客户端任务管理新增;服务端上传需改造。** -- 每个待发送项保存 `client_mid,local_uri,type,upload_id,progress,state`;同一文件可分片/断点续传时使用稳定 `upload_id`。 +- 每个待发送项保存 `client_mid,local_uri,type,upload_id,progress,state`;`upload_id` 仅在单次上传任务内有效(进度上报与本地去重用)。 +- 重试即新建上传任务:`progress` 归零、生成新 `upload_id`,**整文件重传** `POST /v0/file/u/`,不做分片/断点续传;重传成功后消息体 `file_id` 等字段全部取最新一次上传响应。 +- 自动重试采用指数退避且不超过 3 次,之后转人工点按重试;App 冷启动扫描本地 `failed` 任务队列重新上传。 - 上传完成后再发布消息;多任务可并发但必须各自保持原排序号。离开页面不取消,退出账号必须暂停并隔离任务。 - 服务端返回 `FILE_TOO_LARGE`、`FILE_TYPE_NOT_ALLOWED`、`STORAGE_QUOTA_EXCEEDED` 时不可自动无限重试。 @@ -1667,6 +1671,7 @@ GET /v1/im/topics/{topic}/message-mutations?since_version={localMutationVersion} - `mutation_version` 是会话级水位,不是单条消息的 `version`。 - 本地只做 `服务端 mutation_version > 本地 mutation_version` 比较,不要假设连续递增。 +- 补偿接口返回的是最新 `limit` 条(默认 50、上限 100)变更的**状态快照**,非严格增量;变更条数超过 `limit` 时更早的变更不再返回。撤回窗口 120 秒、编辑窗口 300 秒,超窗变更无需客户端处理。 - 拉到 `message.recalled` 后,将本地 `topic + seq` 标记为撤回,并只在 `version` 更高时覆盖。 - 拉到 `message.edited` 后,按 `seq + version` 拉取或覆盖消息内容。 - 实时事件和补偿接口可能重复到达,客户端必须按 `topic + seq + version` 幂等处理。 @@ -2162,7 +2167,7 @@ GET /v1/im/topics/{topic}/message-mutations?since_version={localMutationVersion} - 先用 `purpose=group_file,topic=...` 上传,再 `POST /v1/im/groups/{topic}/files`,Body `{"file_id":"...","file_name":"方案.pdf","mime_type":"application/pdf","file_size":1024}` 建立群文件记录。 - 服务端校验 `member_upload_files`、文件是否已上传完成、`file_name` 长度不超过 255、`mime_type` 长度不超过 128、`file_size` 非负且与上传记录一致、单文件上限、租户总存储和群成员身份;成功返回完整群文件项。 -- 上传失败可复用 `upload_id`;登记失败不得在列表显示,孤立文件按 TTL 回收。 +- 上传失败按整文件重传重新上传并取得新 `file_id`;登记失败不得在列表显示,孤立文件按 TTL 回收。 ### 6.29 接口示例 diff --git a/docs/对接文档最新/IM-前后端对接文档.md b/docs/对接文档最新/IM-前后端对接文档.md index f85d404..27b2b26 100644 --- a/docs/对接文档最新/IM-前后端对接文档.md +++ b/docs/对接文档最新/IM-前后端对接文档.md @@ -1105,7 +1105,9 @@ X-MeetingIM-APIKey: <预置的普通 API Key> - 选择媒体后本地生成 `client_mid` 和稳定占位;调用文件上传,得到 `file_id` 后再 `pub x-im-type=image|video|voice`。 - `content` 至少含 `file_id,name,size,mime,width,height,duration_ms,thumbnail_file_id` 中适用字段;上传进度是客户端任务状态,不写成聊天消息。 -- 服务端发布成功后关联文件与 Topic/消息;文件下载必须校验消息可见性。上传失败只重试上传,成功但 `pub` 失败时复用已上传 `file_id` 和 `client_mid`。 +- 服务端发布成功后关联文件与 Topic/消息;文件下载必须校验消息可见性。 +- 上传失败(网络中断、超时、5xx)按**整文件重传**处理:重新调用 `POST /v0/file/u/` 上传完整文件,不做分片/断点续传;每次重传得到新 `file_id`,旧 `file_id` 直接放弃、由服务端 TTL 回收,客户端不得再引用。 +- 上传成功但 `pub` 失败时不重新上传,复用已上传 `file_id` 和原 `client_mid` 重发消息,服务端按 `client_mid` 幂等去重。 ### 3.7 个人对话—拉黑状态/已读状态.png @@ -1259,7 +1261,9 @@ X-MeetingIM-APIKey: <预置的普通 API Key> **状态:客户端任务管理新增;服务端上传需改造。** -- 每个待发送项保存 `client_mid,local_uri,type,upload_id,progress,state`;同一文件可分片/断点续传时使用稳定 `upload_id`。 +- 每个待发送项保存 `client_mid,local_uri,type,upload_id,progress,state`;`upload_id` 仅在单次上传任务内有效(进度上报与本地去重用)。 +- 重试即新建上传任务:`progress` 归零、生成新 `upload_id`,**整文件重传** `POST /v0/file/u/`,不做分片/断点续传;重传成功后消息体 `file_id` 等字段全部取最新一次上传响应。 +- 自动重试采用指数退避且不超过 3 次,之后转人工点按重试;App 冷启动扫描本地 `failed` 任务队列重新上传。 - 上传完成后再发布消息;多任务可并发但必须各自保持原排序号。离开页面不取消,退出账号必须暂停并隔离任务。 - 服务端返回 `FILE_TOO_LARGE`、`FILE_TYPE_NOT_ALLOWED`、`STORAGE_QUOTA_EXCEEDED` 时不可自动无限重试。 @@ -1553,6 +1557,8 @@ Body: 撤回、编辑属于 `seq` 不变的原地变更。在线且已订阅 Topic 时,客户端会收到 `message.recalled` / `message.edited` 实时事件;离线或未订阅时,需要靠会话级水位补偿。 +单聊场景中,客户端在线但未订阅该 P2P 时,服务端还会向双方用户通道下发 `message.mutation` 脏标记事件(`what:"im"`、`event:"message.mutation"`,payload 含 `topic/seq/version/event`),客户端收到后同样按下方水位补偿流程拉取;群聊不下发脏标记,仅靠会话列表水位 + 拉取补偿。 + 会话列表同步时,`me.meta.sub[].private` 会带: ```json @@ -1606,6 +1612,7 @@ GET /v1/im/topics/{topic}/message-mutations?since_version={localMutationVersion} - `mutation_version` 是会话级水位,不是单条消息的 `version`。 - 本地只做 `服务端 mutation_version > 本地 mutation_version` 比较,不要假设连续递增。 +- 补偿接口返回的是最新 `limit` 条(默认 50、上限 100)变更的**状态快照**,非严格增量;变更条数超过 `limit` 时更早的变更不再返回。撤回窗口 120 秒、编辑窗口 300 秒,超窗变更无需客户端处理。 - 拉到 `message.recalled` 后,将本地 `topic + seq` 标记为撤回,并只在 `version` 更高时覆盖。 - 拉到 `message.edited` 后,按 `seq + version` 拉取或覆盖消息内容。 - 实时事件和补偿接口可能重复到达,客户端必须按 `topic + seq + version` 幂等处理。 @@ -2100,7 +2107,7 @@ GET /v1/im/topics/{topic}/message-mutations?since_version={localMutationVersion} - 先用 `purpose=group_file,topic=...` 上传,再 `POST /v1/im/groups/{topic}/files`,Body `{"file_id":"..."}` 建立群文件记录。 - 服务端校验 `member_upload_files`、单文件上限、租户总存储和群成员身份;成功返回完整群文件项。 -- 上传失败可复用 `upload_id`;登记失败不得在列表显示,孤立文件按 TTL 回收。 +- 上传失败按整文件重传重新上传并取得新 `file_id`;登记失败不得在列表显示,孤立文件按 TTL 回收。 ### 6.29 接口示例 diff --git a/server/db/mysql/tenant_adapter.go b/server/db/mysql/tenant_adapter.go index ab4fe07..7a80374 100644 --- a/server/db/mysql/tenant_adapter.go +++ b/server/db/mysql/tenant_adapter.go @@ -3305,7 +3305,7 @@ func (a *adapter) tenantP2PMessageMutations(ctx context.Context, id t.TenantID, fmt.Sprintf("FROM %s AS m ", tenantMessagesTable(id))+ "LEFT JOIN im_dellog AS d ON d.tenant_id=m.tenant_id AND d.topic=m.topic AND m.seqid BETWEEN d.low AND d.hi-1 AND d.deletedfor=? "+ "LEFT JOIN im_message_revisions AS rev ON rev.tenant_id=m.tenant_id AND rev.topic=m.topic AND rev.seqid=m.seqid AND rev.version=m.version-1 "+ - "WHERE m.tenant_id=? AND m.topic=? AND d.deletedfor IS NULL AND (m.version>1 OR m.recalled_at IS NOT NULL OR m.deletedat IS NOT NULL) "+ + "WHERE m.tenant_id=? AND m.topic=? AND d.deletedfor IS NULL AND (m.version>1 OR m.recalled_at IS NOT NULL) "+ "ORDER BY m.updatedat DESC,m.seqid DESC LIMIT ?", decodeUID(uid), id, topic, limit+1) if err != nil { @@ -3327,7 +3327,7 @@ func (a *adapter) tenantGroupMessageMutations(ctx context.Context, id t.TenantID fmt.Sprintf("FROM %s AS m ", table)+ "LEFT JOIN im_dellog AS d ON d.tenant_id=m.tenant_id AND d.topic COLLATE utf8mb4_unicode_ci=m.topic AND m.seqid BETWEEN d.low AND d.hi-1 AND d.deletedfor=? "+ "LEFT JOIN im_message_revisions AS rev ON rev.tenant_id=m.tenant_id AND rev.topic=m.topic AND rev.seqid=m.seqid AND rev.version=m.version-1 "+ - "WHERE m.tenant_id=? AND m.topic=? AND d.deletedfor IS NULL AND (m.version>1 OR m.recalled_at IS NOT NULL OR m.deleted_at IS NOT NULL OR m.status='recalled')") + "WHERE m.tenant_id=? AND m.topic=? AND d.deletedfor IS NULL AND (m.version>1 OR m.recalled_at IS NOT NULL OR m.status='recalled')") args = append(args, decodeUID(uid), id, topic) } args = append(args, limit+1)