diff --git a/docs/IM-前后端对接文档.md b/docs/IM-前后端对接文档.md index 03067ba..7832aef 100644 --- a/docs/IM-前后端对接文档.md +++ b/docs/IM-前后端对接文档.md @@ -1627,7 +1627,7 @@ X-MeetingIM-APIKey: <预置的普通 API Key> **状态:新增。** -- `GET /v1/im/groups/{topic}/join-requests?status=pending&cursor=` 返回申请人、邀请人、来源、时间、状态和 `version`。 +- `GET /v1/im/groups/{topic}/join-requests?status=pending&cursor=` 返回申请人、邀请人、来源、时间、状态和 `version`;申请人展示字段直接带 `display_name/avatar_file_id/user`,邀请人展示字段带 `inviter_display_name/inviter_avatar_file_id/inviter`。 - 同意:`POST /v1/im/groups/{topic}/join-requests/{id}/accept`;拒绝:`POST /v1/im/groups/{topic}/join-requests/{id}/reject`,带幂等键与 `version`。 - 仅群主/管理员可处理;同意时原子校验群成员上限。并发处理返回 `REQUEST_ALREADY_HANDLED` 和最终状态。 @@ -1886,7 +1886,7 @@ X-MeetingIM-APIKey: <预置的普通 API Key> | `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..."}` | | `POST /v1/im/groups/{topic}/join-requests` | `{"message":"申请入群","source":"qr","qr_token":"token..."}` | `{"request_id":"gjr_1","topic":"grp...","status":"pending","created_at":"2026-08-20T09:00:00Z","version":1}` | -| `GET /v1/im/groups/{topic}/join-requests?status=pending&cursor=` | Query only | `{"topic":"grp...","items":[{"request_id":"gjr_1","applicant":{"user_id":"usr...","display_name":"张三"},"status":"pending","source":"qr","created_at":"2026-08-20T09:00:00Z","version":1}],"next_cursor":""}` | +| `GET /v1/im/groups/{topic}/join-requests?status=pending&cursor=` | Query only | `{"topic":"grp...","items":[{"request_id":"gjr_1","user_id":"usr...","display_name":"张三","avatar_file_id":"avatar.jpg","user":{"user_id":"usr...","display_name":"张三","avatar_file_id":"avatar.jpg"},"inviter_id":"usr_inviter","inviter_display_name":"李四","inviter_avatar_file_id":"inviter.jpg","inviter":{"user_id":"usr_inviter","display_name":"李四","avatar_file_id":"inviter.jpg"},"status":"pending","source":"invitation","created_at":"2026-08-20T09:00:00Z","version":1}],"next_cursor":""}` | | `POST /v1/im/groups/{topic}/join-requests/{id}/accept` | `{"version":1}` | `{"request_id":"gjr_1","topic":"grp...","status":"accepted","handled_at":"2026-08-20T09:00:00Z","version":2}` | | `POST /v1/im/groups/{topic}/join-requests/{id}/reject` | `{"version":1,"reason":"人数已满"}` | `{"request_id":"gjr_1","topic":"grp...","status":"rejected","handled_at":"2026-08-20T09:00:00Z","version":2}` | | `GET /v1/im/groups/{topic}/summary` | Path topic | `{"topic":"grp...","pending_join_request_count":0,"announcement":{"announcement_id":"ann_1","content":"公告"},"permissions":{"can_manage_members":true}}` | diff --git a/server/im_http.go b/server/im_http.go index fd4021d..7c67c33 100644 --- a/server/im_http.go +++ b/server/im_http.go @@ -5641,7 +5641,12 @@ func serveIMGroupJoinRequests(wrt http.ResponseWriter, req *http.Request, topic } items = nil } - writeIMSuccess(wrt, req, map[string]any{"topic": topic, "items": groupJoinRequestPayloads(items), "next_cursor": nextCursor}) + payloads, err := groupJoinRequestPayloadsWithUsers(principal.TenantID, items) + if err != nil { + writeIMRelationError(wrt, req, err) + return + } + writeIMSuccess(wrt, req, map[string]any{"topic": topic, "items": payloads, "next_cursor": nextCursor}) } } @@ -7782,6 +7787,68 @@ func groupJoinRequestPayloads(items []types.GroupJoinRequest) []map[string]any { return out } +func groupJoinRequestPayloadsWithUsers(tenantID types.TenantID, items []types.GroupJoinRequest) ([]map[string]any, error) { + payloads := groupJoinRequestPayloads(items) + if len(items) == 0 || store.Users == nil { + return payloads, nil + } + seen := make(map[types.Uid]struct{}, len(items)*2) + uids := make([]types.Uid, 0, len(items)*2) + for i := range items { + for _, uid := range []types.Uid{items[i].ApplicantUserID, items[i].InviterUserID} { + if uid.IsZero() { + continue + } + if _, ok := seen[uid]; ok { + continue + } + seen[uid] = struct{}{} + uids = append(uids, uid) + } + } + if len(uids) == 0 { + return payloads, nil + } + users, err := store.Users.GetAll(tenantID, uids...) + if err != nil { + return nil, err + } + byID := make(map[types.Uid]*types.User, len(users)) + for i := range users { + uid := types.ParseUid(users[i].Id) + if uid.IsZero() { + uid = types.ParseUserId(users[i].Id) + } + if uid.IsZero() { + continue + } + byID[uid] = &users[i] + } + for i := range items { + enrichGroupJoinRequestUserPayload(payloads[i], "user", "", items[i].ApplicantUserID, byID) + enrichGroupJoinRequestUserPayload(payloads[i], "inviter", "inviter_", items[i].InviterUserID, byID) + } + return payloads, nil +} + +func enrichGroupJoinRequestUserPayload(data map[string]any, objectKey, fieldPrefix string, uid types.Uid, users map[types.Uid]*types.User) { + if data == nil || uid.IsZero() { + return + } + user := users[uid] + summary := map[string]any{"user_id": uid.UserId(), "display_name": uid.UserId()} + if user != nil { + summary = userSummaryPayload(user, "") + } + data[objectKey] = summary + if displayName, ok := summary["display_name"]; ok { + data[fieldPrefix+"display_name"] = displayName + } + if avatarFileID, ok := summary["avatar_file_id"]; ok { + data[fieldPrefix+"avatar_file_id"] = avatarFileID + } +} + func groupAnnouncementPayload(ann *types.GroupAnnouncement) map[string]any { if ann == nil { return map[string]any{ diff --git a/server/im_http_test.go b/server/im_http_test.go index ee350b7..0e41699 100644 --- a/server/im_http_test.go +++ b/server/im_http_test.go @@ -4765,16 +4765,20 @@ func TestServeIMGroupInvitationCreatesApprovalRequest(t *testing.T) { mockStore.EXPECT().GetUidString().Return("request") oldSubs := store.Subs oldGroupBusiness := store.GroupBusiness + oldUsers := store.Users subs := mock_store.NewMockSubsPersistenceInterface(ctrl) + users := mock_store.NewMockUsersPersistenceInterface(ctrl) groupStore := &testGroupBusinessStore{settings: &types.GroupSettings{ Topic: "grpAlpha", OwnerUserID: types.Uid(42), Name: "Alpha", GroupStatus: types.GroupStatusActive, JoinApproval: true, MemberInvite: true, MaxAdminCount: groupMaxAdminCount, Version: 1, }} store.Subs = subs + store.Users = users store.GroupBusiness = groupStore defer func() { store.Subs = oldSubs store.GroupBusiness = oldGroupBusiness + store.Users = oldUsers }() owner := types.Uid(42) target := types.Uid(43) @@ -4817,6 +4821,10 @@ func TestServeIMGroupInvitationCreatesApprovalRequest(t *testing.T) { t.Fatalf("unexpected summary response: %#v", summary) } + users.EXPECT().GetAll(types.TenantID(7), target, owner).Return([]types.User{ + {ObjHeader: types.ObjHeader{Id: target.String()}, Public: map[string]any{"display_name": "申请人", "avatar_file_id": "avatar-target.jpg"}}, + {ObjHeader: types.ObjHeader{Id: owner.String()}, Public: map[string]any{"display_name": "邀请人", "avatar_file_id": "avatar-owner.jpg"}}, + }, nil) req = httptest.NewRequest(http.MethodGet, "/v1/im/groups/grpAlpha/join-requests?status=pending", nil) req.Header.Set("Authorization", "Bearer token-1") rec = httptest.NewRecorder() @@ -4829,6 +4837,16 @@ func TestServeIMGroupInvitationCreatesApprovalRequest(t *testing.T) { if len(requests) != 1 || requests[0].(map[string]any)["request_id"] != "gjr_request" { t.Fatalf("unexpected join requests response: %#v", list) } + requestItem := requests[0].(map[string]any) + if requestItem["display_name"] != "申请人" || requestItem["avatar_file_id"] != "avatar-target.jpg" { + t.Fatalf("join request applicant profile was not populated: %#v", requestItem) + } + if requestItem["inviter_display_name"] != "邀请人" || requestItem["inviter_avatar_file_id"] != "avatar-owner.jpg" { + t.Fatalf("join request inviter profile was not populated: %#v", requestItem) + } + if user := requestItem["user"].(map[string]any); user["display_name"] != "申请人" || user["avatar_file_id"] != "avatar-target.jpg" { + t.Fatalf("join request user object was not populated: %#v", user) + } } func TestServeIMGroupInvitationAccept(t *testing.T) {