From 75d1aabcb015064c03f90b0d776d81dbeff520b8 Mon Sep 17 00:00:00 2001 From: richjohnrambo Date: Thu, 10 Sep 2026 21:57:25 +0800 Subject: [PATCH] fix group member display names --- docs/IM-前后端对接文档.md | 3 ++- server/im_http.go | 41 ++++++++++++++++++++++++++------------- server/im_http_test.go | 29 +++++++++++++++++++++------ 3 files changed, 52 insertions(+), 21 deletions(-) diff --git a/docs/IM-前后端对接文档.md b/docs/IM-前后端对接文档.md index e2df47b..ae07a34 100644 --- a/docs/IM-前后端对接文档.md +++ b/docs/IM-前后端对接文档.md @@ -1708,6 +1708,7 @@ X-MeetingIM-APIKey: <预置的普通 API Key> - `PUT /v1/im/groups/{topic}/members/me/profile`,Body `{"nickname":"...","version":n}`。 - 服务端校验成员资格、长度和敏感词,返回 `nickname,version,updated_at` 并推送成员资料变化。 - 该昵称仅作用当前群,不能修改全局用户昵称或其他群资料。 +- 群成员列表和群成员资料中的 `display_name` 仍由服务端按当前登录人计算,好友备注优先;群内昵称单独返回为 `group_nickname`,前端不要用 `nickname/group_nickname` 覆盖 `display_name`。 ### 6.14 群简介.png @@ -1879,7 +1880,7 @@ X-MeetingIM-APIKey: <预置的普通 API Key> | `PATCH /v1/im/groups/{topic}/member-settings` | `{"pinned":true,"mute_notifications":false,"version":1}` | `{"topic":"grp...","pinned":true,"mute_notifications":false,"version":2,"updated_at":"2026-08-20T09:00:00Z"}` | | `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?query=&cursor=&mentionable=true` | Query only | `{"topic":"grp...","items":[{"user_id":"usr...","display_name":"张三","group_nickname":"小张","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}}` | | `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_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"}]}` | diff --git a/server/im_http.go b/server/im_http.go index d662874..eb37fbb 100644 --- a/server/im_http.go +++ b/server/im_http.go @@ -4828,11 +4828,6 @@ func serveIMGroupMembers(wrt http.ResponseWriter, req *http.Request, topic strin roleFilter := strings.TrimSpace(req.URL.Query().Get("role")) eligibleRole := strings.TrimSpace(req.URL.Query().Get("eligible_role")) muteEligible := parseIMBool(req.URL.Query().Get("mute_eligible")) - cacheKey := groupMemberPageCacheKey(topic, principal.Uid, selfAccess, query, roleFilter, eligibleRole, limit, cursor, mentionableOnly, muteEligible) - if cached, ok := store.GetCachedGroupMemberPage(principal.TenantID, cacheKey); ok { - writeIMSuccess(wrt, req, cached) - return - } members, nextCursor, err := store.GroupBusiness.ListMembers(principal.TenantID, topic, types.GroupMemberListOptions{ Role: roleFilter, EligibleRole: eligibleRole, @@ -4845,10 +4840,11 @@ func serveIMGroupMembers(wrt http.ResponseWriter, req *http.Request, topic strin writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil) return } + remarks := groupMemberRemarkMap(principal.TenantID, principal.Uid, members) items := make([]map[string]any, 0, limit) for _, member := range members { access := groupMemberAccess(member) - item := groupBusinessMemberPayload(member) + item := groupBusinessMemberPayload(member, remarks[member.UserID]) if eligibleRole != "" { eligible, reason := groupRoleEligibility(selfAccess, access, eligibleRole) item["eligible"] = eligible @@ -4874,7 +4870,6 @@ func serveIMGroupMembers(wrt http.ResponseWriter, req *http.Request, topic strin "next_cursor": nextCursor, "permissions": permissions, } - store.CacheGroupMemberPage(principal.TenantID, cacheKey, payload) writeIMSuccess(wrt, req, payload) } @@ -7386,10 +7381,10 @@ func groupMemberSettingsPayload(sub *types.Subscription) map[string]any { } } -func groupMemberPayload(sub types.Subscription, mentionable bool) map[string]any { +func groupMemberPayload(sub types.Subscription, mentionable bool, remark string) map[string]any { access := sub.ModeGiven & sub.ModeWant user := &types.User{ObjHeader: types.ObjHeader{Id: sub.User}, Public: sub.GetPublic(), Trusted: sub.GetTrusted()} - data := userSummaryPayload(user, "") + data := userSummaryPayload(user, remark) if private := groupPrivateMap(sub.Private); stringFromAny(private["nickname"]) != "" { data["group_nickname"] = stringFromAny(private["nickname"]) } @@ -7403,10 +7398,10 @@ func groupMemberPayload(sub types.Subscription, mentionable bool) map[string]any return data } -func groupBusinessMemberPayload(member types.GroupMember) map[string]any { +func groupBusinessMemberPayload(member types.GroupMember, remark string) map[string]any { access := groupMemberAccess(member) user := &types.User{ObjHeader: types.ObjHeader{Id: member.UserID.String()}, Public: member.Public, Trusted: member.Trusted} - data := userSummaryPayload(user, "") + data := userSummaryPayload(user, remark) if nickname := strings.TrimSpace(member.Nickname); nickname != "" { data["group_nickname"] = nickname } @@ -7420,6 +7415,24 @@ func groupBusinessMemberPayload(member types.GroupMember) map[string]any { return data } +func groupMemberRemarkMap(tenantID types.TenantID, viewer types.Uid, members []types.GroupMember) map[types.Uid]string { + remarks := make(map[types.Uid]string) + for _, member := range members { + if member.UserID.IsZero() || member.UserID == viewer { + continue + } + ctx, err := store.Relations.GetRelationshipContext(tenantID, viewer, member.UserID, viewer.P2PName(member.UserID)) + if err != nil { + logs.Warn.Printf("group member[%s:%s]: failed to resolve friend remark for viewer %s: %v", member.Topic, member.UserID.UserId(), viewer.UserId(), err) + continue + } + if ctx != nil && ctx.Remark != "" { + remarks[member.UserID] = ctx.Remark + } + } + return remarks +} + func groupMemberAccess(member types.GroupMember) types.AccessMode { access := member.ModeGiven & member.ModeWant if access != types.ModeNone { @@ -7436,7 +7449,7 @@ func groupMemberAccess(member types.GroupMember) types.AccessMode { } func groupMemberProfilePayload(topic string, sub *types.Subscription, user *types.User, ctx *types.RelationshipContext) map[string]any { - data := groupMemberPayload(*sub, true) + data := groupMemberPayload(*sub, true, ctx.Remark) data["topic"] = topic data["relationship"] = ctx.Relationship data["capabilities"] = map[string]any{ @@ -7448,7 +7461,7 @@ func groupMemberProfilePayload(topic string, sub *types.Subscription, user *type data["request_id"] = ctx.RequestID } if user != nil { - for key, value := range userSummaryPayload(user, "") { + for key, value := range userSummaryPayload(user, ctx.Remark) { if key == "avatar_file_id" || key == "display_name" { data[key] = value } @@ -7829,7 +7842,7 @@ func groupAdminItems(members []types.Subscription) []map[string]any { for _, member := range members { access := member.ModeGiven & member.ModeWant if access.IsAdmin() && !access.IsOwner() { - items = append(items, groupMemberPayload(member, true)) + items = append(items, groupMemberPayload(member, true, "")) } } return items diff --git a/server/im_http_test.go b/server/im_http_test.go index 9d1fea9..9913a69 100644 --- a/server/im_http_test.go +++ b/server/im_http_test.go @@ -3895,13 +3895,24 @@ func TestServeIMGroupMembers(t *testing.T) { expectIMBearer(t, ctrl, "token-1", 7, 42) oldSubs := store.Subs oldGroupBusiness := store.GroupBusiness + oldRelations := store.Relations subs := mock_store.NewMockSubsPersistenceInterface(ctrl) store.Subs = subs groupStore := &testGroupBusinessStore{listMembersNext: "101"} store.GroupBusiness = groupStore + store.Relations = &testRelationsStore{context: &types.RelationshipContext{ + TenantID: 7, + UserID: 42, + PeerID: types.Uid(101), + Topic: types.Uid(42).P2PName(types.Uid(101)), + Relationship: types.RelationshipFriend, + CanSend: true, + Remark: "老王", + }} defer func() { store.Subs = oldSubs store.GroupBusiness = oldGroupBusiness + store.Relations = oldRelations }() subs.EXPECT().Get(types.TenantID(7), "grpMention", types.Uid(42), false).Return(&types.Subscription{ @@ -3918,6 +3929,7 @@ func TestServeIMGroupMembers(t *testing.T) { Role: types.GroupMemberRoleMember, ModeWant: types.ModeCPublic, ModeGiven: types.ModeCPublic, + Nickname: "群里小王", Public: map[string]any{"display_name": "Alice", "avatar_file_id": "alice.jpg"}, MemberStatus: types.GroupMemberStatusActive, }} @@ -3933,7 +3945,8 @@ func TestServeIMGroupMembers(t *testing.T) { } data := decodeIMHTTPResponse(t, rec).Data.(map[string]any) items := data["items"].([]any) - if len(items) != 1 || items[0].(map[string]any)["display_name"] != "Alice" || + if len(items) != 1 || items[0].(map[string]any)["display_name"] != "老王" || + items[0].(map[string]any)["group_nickname"] != "群里小王" || items[0].(map[string]any)["avatar_file_id"] != "alice.jpg" || items[0].(map[string]any)["mentionable"] != true { t.Fatalf("unexpected members response: %#v", data) @@ -3956,7 +3969,7 @@ func TestGroupMemberPayloadUserIDFormats(t *testing.T) { User: raw.String(), ModeWant: types.ModeJoin, ModeGiven: types.ModeJoin, - }, true) + }, true, "") if fromRaw["user_id"] != raw.UserId() { t.Fatalf("raw uid user_id: got %#v, want %s", fromRaw["user_id"], raw.UserId()) } @@ -3966,7 +3979,7 @@ func TestGroupMemberPayloadUserIDFormats(t *testing.T) { User: prefixed.UserId(), ModeWant: types.ModeJoin, ModeGiven: types.ModeJoin, - }, true) + }, true, "") if fromPrefixed["user_id"] != prefixed.UserId() { t.Fatalf("prefixed uid user_id: got %#v, want %s", fromPrefixed["user_id"], prefixed.UserId()) } @@ -3989,8 +4002,9 @@ func TestServeIMGroupMemberProfile(t *testing.T) { UserID: 42, PeerID: peer, Topic: types.Uid(42).P2PName(peer), - Relationship: types.RelationshipNone, - CanSend: false, + Relationship: types.RelationshipFriend, + CanSend: true, + Remark: "老王", }} defer func() { store.Subs = oldSubs @@ -4024,9 +4038,12 @@ func TestServeIMGroupMemberProfile(t *testing.T) { t.Fatalf("status: got %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String()) } data := decodeIMHTTPResponse(t, rec).Data.(map[string]any) - if data["relationship"] != types.RelationshipNone || data["signature"] != "hi" || data["role"] != "member" { + if data["relationship"] != types.RelationshipFriend || data["signature"] != "hi" || data["role"] != "member" { t.Fatalf("unexpected group member profile response: %#v", data) } + if data["display_name"] != "老王" { + t.Fatalf("group member profile display name should use friend remark: %#v", data) + } } func TestServeIMGroupMemberProfileRejectsUserTopic(t *testing.T) {