From 73ddb2d9fa3cbc4f7e9e7ef8bf77239f65bf5349 Mon Sep 17 00:00:00 2001 From: richjohnrambo Date: Wed, 19 Aug 2026 17:23:36 +0800 Subject: [PATCH] fix: improve IM user account search --- server/db/mysql/tenant_adapter.go | 8 +++- server/db/mysql/tests/mysql_test.go | 73 +++++++++++++++++++++++++++++ server/im_http.go | 5 ++ server/im_http_test.go | 5 +- 4 files changed, 87 insertions(+), 4 deletions(-) diff --git a/server/db/mysql/tenant_adapter.go b/server/db/mysql/tenant_adapter.go index 21dc234..06a4b34 100644 --- a/server/db/mysql/tenant_adapter.go +++ b/server/db/mysql/tenant_adapter.go @@ -7132,8 +7132,12 @@ func (a *adapter) TenantUserSearch(id t.TenantID, viewer t.Uid, queryText, searc switch searchType { case "", "account": like := "%" + queryText + "%" - where = append(where, "(profile.nickname LIKE ? OR profile.public_account LIKE ? OR CAST(u.public AS CHAR) LIKE ? OR u.id=?)") - args = append(args, like, like, like, userIDNumericArg(queryText)) + phoneCandidate := queryText + if phone, err := normalizeUserPhone(queryText); err == nil { + phoneCandidate = phone + } + where = append(where, "(profile.nickname LIKE ? OR profile.public_account LIKE ? OR u.username LIKE ? OR u.phone=? OR u.phone=? OR CAST(u.public AS CHAR) LIKE ? OR u.id=?)") + args = append(args, like, like, like, queryText, phoneCandidate, like, userIDNumericArg(queryText)) case "exact_account": where = append(where, "(profile.public_account=? OR u.username=? OR u.id=?)") args = append(args, queryText, strings.TrimPrefix(queryText, "basic:"), userIDNumericArg(queryText)) diff --git a/server/db/mysql/tests/mysql_test.go b/server/db/mysql/tests/mysql_test.go index 3c6005e..8499b0b 100644 --- a/server/db/mysql/tests/mysql_test.go +++ b/server/db/mysql/tests/mysql_test.go @@ -326,6 +326,79 @@ func TestTenantAuthIsolation(t *testing.T) { } } +func TestTenantUserSearchMatchesUsernamePhoneAndPublicAccount(t *testing.T) { + businessAdp, ok := adp.(adapter.TenantBusinessAdapter) + if !ok { + t.Fatal("MySQL adapter does not implement TenantBusinessAdapter") + } + var tenantID types.TenantID + if err := db.Get(&tenantID, "SELECT id FROM im_tenant WHERE code='default'"); err != nil { + t.Fatal(err) + } + + now := types.TimeNow() + viewerID := now.UnixNano() & 0x3fffffffffffffff + targetID := viewerID + 1 + viewer := store.EncodeUid(viewerID) + target := store.EncodeUid(targetID) + t.Cleanup(func() { + db.Exec("DELETE FROM im_friendships WHERE user_id IN (?,?) OR peer_id IN (?,?)", viewerID, targetID, viewerID, targetID) + db.Exec("DELETE FROM im_user_privacy_settings WHERE user_id IN (?,?)", viewerID, targetID) + db.Exec("DELETE FROM im_user_profiles WHERE user_id IN (?,?)", viewerID, targetID) + db.Exec("DELETE FROM im_credentials WHERE userid IN (?,?)", viewerID, targetID) + db.Exec("DELETE FROM im_auth WHERE userid IN (?,?)", viewerID, targetID) + db.Exec("DELETE FROM im_users WHERE id IN (?,?)", viewerID, targetID) + }) + + for _, user := range []struct { + id int64 + username string + phone any + public string + }{ + {viewerID, "viewerx", nil, `{"fn":"Viewer"}`}, + {targetID, "potter", "+8615500000002", `{"fn":"Potter"}`}, + } { + if _, err := db.Exec( + `INSERT INTO im_users(id,username,createdat,updatedat,state,access,phone,public,trusted) + VALUES(?,?,?,?,?,?,?,?,?)`, + user.id, user.username, now, now, types.StateOK, `{"Auth":"JRWPAS","Anon":"N"}`, user.phone, user.public, `{}`); err != nil { + t.Fatal(err) + } + if _, err := db.Exec( + `INSERT INTO im_user_privacy_settings(id,user_id,add_me_methods,created_at,updated_at) + VALUES(?,?,?,?,?)`, + user.id+1000, user.id, `{"account":true,"phone":true,"qr":true,"group":true}`, now, now); err != nil { + t.Fatal(err) + } + } + if _, err := db.Exec( + `INSERT INTO im_user_profiles(id,user_id,nickname,public_account,avatar_file_id,signature,gender,region,created_at,updated_at) + VALUES(?,?,?,?,?,?,?,?,?,?)`, + targetID+1000, targetID, "后端-potter", "15500000002", "avatar-1", "hi", "other", "CN", now, now); err != nil { + t.Fatal(err) + } + + for _, tc := range []struct { + name string + query string + searchType string + }{ + {"username", "potter", "account"}, + {"public_account", "15500000002", "account"}, + {"phone_raw", "15500000002", "account"}, + {"phone_e164", "+8615500000002", "phone"}, + } { + users, _, err := businessAdp.TenantUserSearch(tenantID, viewer, tc.query, tc.searchType, "", 20) + if err != nil { + t.Fatalf("%s search failed: %v", tc.name, err) + } + if len(users) != 1 || users[0].Uid() != target { + t.Fatalf("%s search returned %+v, want %s", tc.name, users, target.UserId()) + } + } +} + // ================== Create tests ================================ func TestUserCreate(t *testing.T) { for _, user := range testData.Users { diff --git a/server/im_http.go b/server/im_http.go index 93a85ab..de576ae 100644 --- a/server/im_http.go +++ b/server/im_http.go @@ -6460,6 +6460,11 @@ func friendListItemPayload(item types.FriendListItem) map[string]any { func userSearchItemPayload(item types.UserSearchItem) map[string]any { data := userSummaryPayload(item.User, "") + if public, ok := item.User.Public.(map[string]any); ok { + copyOptionalProfileFields(data, public) + } else if public, ok := item.User.Public.(types.KVMap); ok { + copyOptionalProfileFields(data, map[string]any(public)) + } data["relationship"] = item.Relationship data["can_add"] = item.CanAdd data["action"] = item.Action diff --git a/server/im_http_test.go b/server/im_http_test.go index 03cc864..879d627 100644 --- a/server/im_http_test.go +++ b/server/im_http_test.go @@ -4077,7 +4077,7 @@ func TestServeIMUserSearch(t *testing.T) { oldRelations := store.Relations peer := types.Uid(2) store.Relations = &testRelationsStore{searchResults: []types.UserSearchItem{{ - User: &types.User{ObjHeader: types.ObjHeader{Id: peer.String()}, TenantID: 7, Public: map[string]any{"display_name": "Alice", "avatar_file_id": "file-1"}}, + User: &types.User{ObjHeader: types.ObjHeader{Id: peer.String()}, TenantID: 7, Public: map[string]any{"display_name": "Alice", "avatar_file_id": "file-1", "public_account": "alice001"}}, Relationship: types.RelationshipNone, CanAdd: true, Action: "add", @@ -4095,7 +4095,8 @@ func TestServeIMUserSearch(t *testing.T) { } data := decodeIMHTTPResponse(t, rec).Data.(map[string]any) items := data["items"].([]any) - if len(items) != 1 || items[0].(map[string]any)["relationship"] != types.RelationshipNone || items[0].(map[string]any)["can_add"] != true { + if len(items) != 1 || items[0].(map[string]any)["relationship"] != types.RelationshipNone || items[0].(map[string]any)["can_add"] != true || + items[0].(map[string]any)["public_account"] != "alice001" { t.Fatalf("unexpected search response: %#v", data) } }