diff --git a/docs/IM-前后端对接文档.md b/docs/IM-前后端对接文档.md index 35251cb..785e5d4 100644 --- a/docs/IM-前后端对接文档.md +++ b/docs/IM-前后端对接文档.md @@ -644,8 +644,9 @@ X-MeetingIM-APIKey: <预置的普通 API Key> **文件上传/下载鉴权与回收规则(已实现,客户端必须遵守):** 1. 上传聊天附件(图片/视频/语音/文件)必须携带 `topic=<消息将要发送到的会话 topic>`。服务端校验上传者是该 `p2p`/`grp` 会话的订阅成员(`modewant`/`modegiven` 含读权限),不是成员返回 `403 permission denied`;校验通过则在服务端建立「文件-会话」引用,该会话全体成员凭有效 Token 即可下载。 -2. 不带 `topic` 的上传只有上传者本人可下载(可用于头像等场景)。任何文件若一直未被任何会话/消息引用,服务端在创建 1 小时后由后台任务物理回收;上传后必须及时发送消息或携带 `topic`,否则对端将拿到 404。 -3. 下载放行范围:上传者本人、文件所属会话成员、消息级引用可见者(未撤回未删除且未被执行删除)、群成员、好友关系、收藏引用。非成员返回 `403 permission denied`(即 `FILE_FORBIDDEN`),客户端不自动重试。 +2. 不带 `topic` 的上传只有上传者本人可下载。任何文件若一直未被任何会话/消息引用,服务端在创建 1 小时后由后台任务物理回收;上传后必须及时发送消息或携带 `topic`,否则对端将拿到 404。用户头像/资料类文件不受此限:客户端设置头像(PATCH `avatar_file_id`)时服务端自动写入文件引用,该文件不会被回收。 +3. 下载放行范围(2026-10-14 更新):上传者本人、文件所属会话成员、消息级引用可见者(未撤回未删除且未被执行删除)、群成员、好友关系、收藏引用、**用户头像/资料文件(对任何登录用户放行,2026-10-14 修复:此前仅上传者可读,群成员拉取他人头像 403)**。非授权返回 `403 permission denied`(即 `FILE_FORBIDDEN`),客户端不自动重试。 +4. `avatar_url` 统一回填规则(2026-10-14 起,所有返回用户/群摘要的接口一致):`avatar_file_id` 为完整 URL 时 `avatar_url` 原样返回;为裸 file_id 时返回 `"/v0/file/s/{file_id}"`(可直接下载,无需客户端拼接);头像为空时回落默认头像 URL(男/女按性别,`avatar_file_id` 与 `avatar_url` 同值)。涉及接口:`/v1/im/groups/{topic}/members`、`/members/{userId}/profile`、`/v1/im/users/{id}/profile`、`/v1/im/me/profile`、转发目标列表等。 ### 0.6 Topic 与产品消息 @@ -2340,10 +2341,11 @@ GET /v1/im/topics/{topic}/message-mutations?since_version={localMutationVersion} 文件-下载 -**状态:下载授权已实现(按上传者/会话成员/消息引用/群成员/好友/收藏放行)。** +**状态:下载授权已实现(按上传者/会话成员/消息引用/群成员/好友/收藏/头像放行)。** - 客户端使用 `GET /v0/file/s/{file_id}`,Header 传 API Key 与 `X-MeetingIM-Auth: token `;禁止 `?auth=&secret=`。URL 可带扩展名,服务端自动剥离。 -- 服务端按文件 ACL 放行:上传者本人、文件所属 `p2p`/`grp` 会话成员、消息级引用可见者(未撤回未删除且未被执行删除)、群成员、好友关系、收藏引用;非成员返回 `403 permission denied`(`FILE_FORBIDDEN`)。对象存储模式下鉴权通过返回 302 短期签名 URL。 +- 服务端按文件 ACL 放行:上传者本人、文件所属 `p2p`/`grp` 会话成员、消息级引用可见者(未撤回未删除且未被执行删除)、群成员、好友关系、收藏引用、用户头像/资料文件(对任何登录用户放行,2026-10-14 更新);非授权返回 `403 permission denied`(`FILE_FORBIDDEN`)。对象存储模式下鉴权通过返回 302 短期签名 URL。 +- 头像排障口径:群成员/资料接口返回的 `avatar_url` 可直接下载;若对端头像显示占位图且下载 `404`,说明该头像文件从未被引用或已被回收,让对方重新上传并设置头像即可;`403` 为非登录态(Token 失效),重新登录。 - 前提:发送方上传附件时带了 `topic`(见 0.5.5.1)。若对端下载一直 404,说明该文件从未关联会话/消息且已被 1 小时回收任务删除,应让发送方重新上传并携带 `topic`。 - 下载前客户端校验本地空间和网络策略;`FILE_FORBIDDEN` 不自动重试,`FILE_UNAVAILABLE` 显示失效。 diff --git a/docs/对接文档最新/IM-前后端对接文档.md b/docs/对接文档最新/IM-前后端对接文档.md index 35251cb..785e5d4 100644 --- a/docs/对接文档最新/IM-前后端对接文档.md +++ b/docs/对接文档最新/IM-前后端对接文档.md @@ -644,8 +644,9 @@ X-MeetingIM-APIKey: <预置的普通 API Key> **文件上传/下载鉴权与回收规则(已实现,客户端必须遵守):** 1. 上传聊天附件(图片/视频/语音/文件)必须携带 `topic=<消息将要发送到的会话 topic>`。服务端校验上传者是该 `p2p`/`grp` 会话的订阅成员(`modewant`/`modegiven` 含读权限),不是成员返回 `403 permission denied`;校验通过则在服务端建立「文件-会话」引用,该会话全体成员凭有效 Token 即可下载。 -2. 不带 `topic` 的上传只有上传者本人可下载(可用于头像等场景)。任何文件若一直未被任何会话/消息引用,服务端在创建 1 小时后由后台任务物理回收;上传后必须及时发送消息或携带 `topic`,否则对端将拿到 404。 -3. 下载放行范围:上传者本人、文件所属会话成员、消息级引用可见者(未撤回未删除且未被执行删除)、群成员、好友关系、收藏引用。非成员返回 `403 permission denied`(即 `FILE_FORBIDDEN`),客户端不自动重试。 +2. 不带 `topic` 的上传只有上传者本人可下载。任何文件若一直未被任何会话/消息引用,服务端在创建 1 小时后由后台任务物理回收;上传后必须及时发送消息或携带 `topic`,否则对端将拿到 404。用户头像/资料类文件不受此限:客户端设置头像(PATCH `avatar_file_id`)时服务端自动写入文件引用,该文件不会被回收。 +3. 下载放行范围(2026-10-14 更新):上传者本人、文件所属会话成员、消息级引用可见者(未撤回未删除且未被执行删除)、群成员、好友关系、收藏引用、**用户头像/资料文件(对任何登录用户放行,2026-10-14 修复:此前仅上传者可读,群成员拉取他人头像 403)**。非授权返回 `403 permission denied`(即 `FILE_FORBIDDEN`),客户端不自动重试。 +4. `avatar_url` 统一回填规则(2026-10-14 起,所有返回用户/群摘要的接口一致):`avatar_file_id` 为完整 URL 时 `avatar_url` 原样返回;为裸 file_id 时返回 `"/v0/file/s/{file_id}"`(可直接下载,无需客户端拼接);头像为空时回落默认头像 URL(男/女按性别,`avatar_file_id` 与 `avatar_url` 同值)。涉及接口:`/v1/im/groups/{topic}/members`、`/members/{userId}/profile`、`/v1/im/users/{id}/profile`、`/v1/im/me/profile`、转发目标列表等。 ### 0.6 Topic 与产品消息 @@ -2340,10 +2341,11 @@ GET /v1/im/topics/{topic}/message-mutations?since_version={localMutationVersion} 文件-下载 -**状态:下载授权已实现(按上传者/会话成员/消息引用/群成员/好友/收藏放行)。** +**状态:下载授权已实现(按上传者/会话成员/消息引用/群成员/好友/收藏/头像放行)。** - 客户端使用 `GET /v0/file/s/{file_id}`,Header 传 API Key 与 `X-MeetingIM-Auth: token `;禁止 `?auth=&secret=`。URL 可带扩展名,服务端自动剥离。 -- 服务端按文件 ACL 放行:上传者本人、文件所属 `p2p`/`grp` 会话成员、消息级引用可见者(未撤回未删除且未被执行删除)、群成员、好友关系、收藏引用;非成员返回 `403 permission denied`(`FILE_FORBIDDEN`)。对象存储模式下鉴权通过返回 302 短期签名 URL。 +- 服务端按文件 ACL 放行:上传者本人、文件所属 `p2p`/`grp` 会话成员、消息级引用可见者(未撤回未删除且未被执行删除)、群成员、好友关系、收藏引用、用户头像/资料文件(对任何登录用户放行,2026-10-14 更新);非授权返回 `403 permission denied`(`FILE_FORBIDDEN`)。对象存储模式下鉴权通过返回 302 短期签名 URL。 +- 头像排障口径:群成员/资料接口返回的 `avatar_url` 可直接下载;若对端头像显示占位图且下载 `404`,说明该头像文件从未被引用或已被回收,让对方重新上传并设置头像即可;`403` 为非登录态(Token 失效),重新登录。 - 前提:发送方上传附件时带了 `topic`(见 0.5.5.1)。若对端下载一直 404,说明该文件从未关联会话/消息且已被 1 小时回收任务删除,应让发送方重新上传并携带 `topic`。 - 下载前客户端校验本地空间和网络策略;`FILE_FORBIDDEN` 不自动重试,`FILE_UNAVAILABLE` 显示失效。 diff --git a/server/db/mysql/adapter.go b/server/db/mysql/adapter.go index deae444..d2ddaf9 100644 --- a/server/db/mysql/adapter.go +++ b/server/db/mysql/adapter.go @@ -4729,6 +4729,17 @@ func (a *adapter) FileUserHasAccess(fid string, uid t.Uid) (bool, error) { WHERE (gf.file_id=? OR gf.file_id LIKE CONCAT(?,'.%')) AND gf.status='active' AND s.deletedat IS NULL AND INSTR(s.modewant,'R')>0 AND INSTR(s.modegiven,'R')>0 ) + OR EXISTS ( + -- 用户头像/资料文件:设置头像时写入的链接行有两种形态—— + -- userid 形态(topic/msgid 为 NULL,userid=头像主人,旧版写入)与 + -- 纯数字 uid topic 形态(现行写入;合法业务 topic 均带字母前缀,不会误伤)。 + -- 头像属于公开资料,对任何登录用户放行。 + SELECT 1 FROM im_filemsglinks AS fml + WHERE fml.fileid=fu.id AND fml.msgid IS NULL AND ( + (fml.topic IS NULL AND fml.userid IS NOT NULL) + OR fml.topic REGEXP '^[0-9]+$' + ) + ) ) LIMIT 1`, args } run := func() error { diff --git a/server/default_avatar.go b/server/default_avatar.go index 997407e..94c8f2b 100644 --- a/server/default_avatar.go +++ b/server/default_avatar.go @@ -36,6 +36,12 @@ func applyAvatarURL(data map[string]any) { avatar := strings.TrimSpace(stringFromAny(data["avatar_file_id"])) if isAvatarURLValue(avatar) { data["avatar_url"] = avatar + return + } + // 裸文件 ID:统一补一个可直接下载的 avatar_url。 + // 下载走 /v0/file/s/{file_id},自定义头像的 ACL 对登录用户放行(见 adapter.FileUserHasAccess)。 + if avatar != "" && !types.ParseUid(avatar).IsZero() { + data["avatar_url"] = "/v0/file/s/" + avatar } } diff --git a/server/im_http.go b/server/im_http.go index d310f09..2c157e0 100644 --- a/server/im_http.go +++ b/server/im_http.go @@ -7439,13 +7439,15 @@ func forwardTargetPayload(sub types.Subscription) map[string]any { default: return nil } - return map[string]any{ + target := map[string]any{ "topic": sub.Topic, "type": topicType, "display_name": imDisplayNameFromPublic(sub.GetPublic(), sub.Topic), "avatar_file_id": imAvatarFileIDFromPublic(sub.GetPublic()), "can_receive_forward": true, } + applyAvatarURL(target) + return target } func forwardTargetMatches(topic, displayName, query string) bool { @@ -7643,7 +7645,7 @@ func groupBusinessMemberPayload(member types.GroupMember, remark string) map[str } func maskIMGroupMemberProfileEntrypoint(data map[string]any) { - for _, key := range []string{"avatar_file_id", "public_account", "signature", "gender", "region"} { + for _, key := range []string{"avatar_file_id", "avatar_url", "public_account", "signature", "gender", "region"} { delete(data, key) } data["can_add"] = false