From 7c550aaf9db44f6e11f124aee6f1e3a58fe6b9be Mon Sep 17 00:00:00 2001 From: Austin Date: Tue, 29 Sep 2026 10:46:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=85=B3=E7=B3=BB?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E5=8F=8A=E6=8C=89=E8=AF=9D=E9=A2=98=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E9=80=9A=E8=AF=9D=E8=AE=B0=E5=BD=95=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 RelationshipSelf 关系类型 - 新增 serveIMCallsByTopic 接口处理按话题查询通话记录 - 实现群成员资料接口返回本人时的 self 关系处理 - 更新接口文档说明本人查询时返回 relationship: self --- docs/IM-前后端对接文档.md | 2 +- docs/对接文档最新/IM-前后端对接文档.md | 2 +- server/im_http.go | 42 +++++++++++++++++++++++--- server/main.go | 1 + server/store/types/relations.go | 1 + 5 files changed, 42 insertions(+), 6 deletions(-) diff --git a/docs/IM-前后端对接文档.md b/docs/IM-前后端对接文档.md index e472ca7..cd933b4 100644 --- a/docs/IM-前后端对接文档.md +++ b/docs/IM-前后端对接文档.md @@ -2204,7 +2204,7 @@ GET /v1/im/topics/{topic}/message-mutations?since_version={localMutationVersion} | `GET /v1/im/groups/{topic}/members?query=&cursor=&mentionable=true` | Query only | `{"topic":"grp...","items":[{"user_id":"usr...","display_name":"张三","group_nickname":"小张","avatar_file_id":"","role":"member","member_status":"active","version":1,"joined_at":"2026-08-20T09:00:00Z","updated_at":"2026-08-20T09:00:00Z","mentionable":true,"acs":{"want":"JRWP","given":"JRWP","mode":"JRWP"}}],"next_cursor":"","sync_mode":"full","permissions":{"can_mention_all":true}}` | | `GET /v1/im/groups/{topic}/members/{user_id}` | Path topic/user | `{"topic":"grp...","user_id":"usr...","display_name":"张三","group_nickname":"小张","role":"member","member_status":"active","version":1,"joined_at":"2026-08-20T09:00:00Z","updated_at":"2026-08-20T09:00:00Z","sync_mode":"single","acs":{"want":"JRWP","given":"JRWP","mode":"JRWP"}}` | | `DELETE /v1/im/groups/{topic}/members/{user_id}` | Path topic/user;管理员可移除普通成员,群主可移除管理员和普通成员;不能移除群主或自己 | `{"topic":"grp...","user_id":"usr...","status":"left"}` | -| `GET /v1/im/groups/{topic}/members/{user_id}/profile` | Path topic/user;`topic` 必须是 `grp...`,不能传 `usr...` | `{"topic":"grp...","user_id":"usr...","display_name":"张三","group_nickname":"小张","role":"member","relationship":"friend","capabilities":{"can_send":true,"can_call":true}}` | +| `GET /v1/im/groups/{topic}/members/{user_id}/profile` | Path topic/user;`topic` 必须是 `grp...`,不能传 `usr...`;`user_id` 可传本人,返回 `relationship:"self"`、`capabilities.can_send=true` | `{"topic":"grp...","user_id":"usr...","display_name":"张三","group_nickname":"小张","role":"member","relationship":"friend","capabilities":{"can_send":true,"can_call":true}}` | | `POST /v1/im/groups/{topic}/qrcodes` | `{"expires_in_seconds":86400}` | `{"qr_token":"token...","qr_payload":"im://qr?token=...&purpose=join_group","expires_at":"2026-08-21T09:00:00Z","group":{"topic":"grp...","name":"项目群","member_count":2},"group_snapshot":{"topic":"grp...","name":"项目群","member_count":2}}` | | `POST /v1/im/groups/{topic}/invitations` | `{"user_ids":["usr..."],"message":"进群一起讨论"}` | `{"topic":"grp...","items":[{"user_id":"usr...","status":"joined","invitation_id":"ginv_1"}]}` | | `POST /v1/im/group-invitations/{invitation_id}/accept` 待实现 | `{}` | `{"invitation_id":"ginv_1","status":"joined","topic":"grp..."}` | diff --git a/docs/对接文档最新/IM-前后端对接文档.md b/docs/对接文档最新/IM-前后端对接文档.md index ca0ec9f..5b5137b 100644 --- a/docs/对接文档最新/IM-前后端对接文档.md +++ b/docs/对接文档最新/IM-前后端对接文档.md @@ -2142,7 +2142,7 @@ GET /v1/im/topics/{topic}/message-mutations?since_version={localMutationVersion} | `PUT /v1/im/groups/{topic}/members/me/profile` | `{"nickname":"我在群里的昵称","version":1}` | `{"nickname":"我在群里的昵称","version":2,"updated_at":"2026-08-20T09:00:00Z"}` | | `POST /v1/im/groups/{topic}/leave` | `{}` | `{"topic":"grp...","status":"left"}` | | `GET /v1/im/groups/{topic}/members?query=&cursor=&mentionable=true` | Query only | `{"topic":"grp...","items":[{"user_id":"usr...","display_name":"张三","avatar_file_id":"","role":"member","mentionable":true,"acs":{"want":"JRWP","given":"JRWP","mode":"JRWP"}}],"next_cursor":"","permissions":{"can_mention_all":true}}` | -| `GET /v1/im/groups/{topic}/members/{user_id}/profile` | Path topic/user;`topic` 必须是 `grp...`,不能传 `usr...` | `{"topic":"grp...","user_id":"usr...","display_name":"张三","group_nickname":"小张","role":"member","relationship":"friend","capabilities":{"can_send":true,"can_call":true}}` | +| `GET /v1/im/groups/{topic}/members/{user_id}/profile` | Path topic/user;`topic` 必须是 `grp...`,不能传 `usr...`;`user_id` 可传本人,返回 `relationship:"self"`、`capabilities.can_send=true` | `{"topic":"grp...","user_id":"usr...","display_name":"张三","group_nickname":"小张","role":"member","relationship":"friend","capabilities":{"can_send":true,"can_call":true}}` | | `POST /v1/im/groups/{topic}/qrcodes` | `{"expires_in_seconds":86400}` | `{"qr_token":"token...","qr_payload":"im://qr?token=...&purpose=join_group","expires_at":"2026-08-21T09:00:00Z","group":{"topic":"grp...","name":"项目群","member_count":2},"group_snapshot":{"topic":"grp...","name":"项目群","member_count":2}}` | | `POST /v1/im/groups/{topic}/invitations` | `{"user_ids":["usr..."],"message":"进群一起讨论"}` | `{"topic":"grp...","items":[{"user_id":"usr...","status":"joined","invitation_id":"ginv_1"}]}` | | `POST /v1/im/group-invitations/{invitation_id}/accept` 待实现 | `{}` | `{"invitation_id":"ginv_1","status":"joined","topic":"grp..."}` | diff --git a/server/im_http.go b/server/im_http.go index eb93166..2b46d09 100644 --- a/server/im_http.go +++ b/server/im_http.go @@ -3133,6 +3133,25 @@ func serveIMTopicCalls(wrt http.ResponseWriter, req *http.Request) { writeIMSuccess(wrt, req, map[string]any{"topic": topic, "items": callRecordPayloads(items, principal.Uid), "next_cursor": nextCursor}) } +// serveIMCallsByTopic serves GET /v1/im/calls/{topic} — 对接文档契约路径,按会话查通话记录。 +func serveIMCallsByTopic(wrt http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil) + return + } + principal, topic, ok := authenticateTopicReader(wrt, req, topicFromCallsByTopicPath(req.URL.Path)) + if !ok { + return + } + limit := parseIMLimit(req.URL.Query().Get("limit"), 50, 100) + items, nextCursor, err := store.Calls.List(principal.TenantID, principal.Uid, topic, strings.TrimSpace(req.URL.Query().Get("cursor")), limit) + if err != nil { + writeIMRelationError(wrt, req, err) + return + } + writeIMSuccess(wrt, req, map[string]any{"topic": topic, "items": callRecordPayloads(items, principal.Uid), "next_cursor": nextCursor}) +} + func serveIMMessageSearch(wrt http.ResponseWriter, req *http.Request) { if req.Method != http.MethodGet { writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil) @@ -5794,10 +5813,16 @@ func serveIMGroupMemberProfile(wrt http.ResponseWriter, req *http.Request, topic writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil) return } - ctx, err := store.Relations.GetRelationshipContext(principal.TenantID, principal.Uid, memberID, principal.Uid.P2PName(memberID)) - if err != nil { - writeIMRelationError(wrt, req, err) - return + // 查询本人时 GetRelationshipContext 不接受 userID==peerID(会报 ErrMalformed), + // 直接构造 self 上下文,群内本人资料返回 200。 + ctx := &types.RelationshipContext{Relationship: types.RelationshipSelf, CanSend: true} + if memberID != principal.Uid { + var err error + ctx, err = store.Relations.GetRelationshipContext(principal.TenantID, principal.Uid, memberID, principal.Uid.P2PName(memberID)) + if err != nil { + writeIMRelationError(wrt, req, err) + return + } } writeIMSuccess(wrt, req, groupMemberProfilePayload(topic, memberSub, user, ctx)) } @@ -11006,6 +11031,15 @@ func topicFromCallsPath(path string) string { return topicFromPathWithSuffix(path, prefix, suffix) } +func topicFromCallsByTopicPath(path string) string { + const prefix = "/v1/im/calls/" + idx := strings.Index(path, prefix) + if idx < 0 { + return "" + } + return strings.Trim(strings.TrimSpace(path[idx+len(prefix):]), "/") +} + func topicFromDraftPath(path string) string { const prefix = "/v1/im/topics/" const suffix = "/draft" diff --git a/server/main.go b/server/main.go index e282b8a..7884bd4 100644 --- a/server/main.go +++ b/server/main.go @@ -934,6 +934,7 @@ func main() { mux.HandleFunc(config.ApiPath+"v1/im/favorites", serveIMFavorites) mux.HandleFunc(config.ApiPath+"v1/im/favorites/", serveIMFavoriteResource) mux.HandleFunc(config.ApiPath+"v1/im/calls/rtc-join", serveIMCallRTCJoin) + mux.HandleFunc(config.ApiPath+"v1/im/calls/", serveIMCallsByTopic) mux.HandleFunc(config.ApiPath+"v1/im/calls", serveIMCalls) mux.HandleFunc(config.ApiPath+"v1/im/topics/", serveIMTopicMessageAction) mux.HandleFunc(config.ApiPath+"v1/im/conversations/", serveIMConversationContext) diff --git a/server/store/types/relations.go b/server/store/types/relations.go index 139d315..1617180 100644 --- a/server/store/types/relations.go +++ b/server/store/types/relations.go @@ -9,6 +9,7 @@ const ( FriendRequestExpired = "expired" RelationshipNone = "none" + RelationshipSelf = "self" RelationshipFriend = "friend" RelationshipPendingInbound = "pending_inbound" RelationshipPendingOutbound = "pending_outbound"