fix group member display names

This commit is contained in:
richjohnrambo
2026-09-10 21:57:25 +08:00
parent 928a45ecfb
commit 75d1aabcb0
3 changed files with 52 additions and 21 deletions
+2 -1
View File
@@ -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"}]}` |
+27 -14
View File
@@ -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
+23 -6
View File
@@ -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) {