From 62af7722603ff2d62841f41b1e8730248f9ae07a Mon Sep 17 00:00:00 2001 From: richjohnrambo Date: Fri, 4 Sep 2026 10:49:10 +0800 Subject: [PATCH] Read user profile display from profile table --- server/db/mysql/adapter.go | 17 ++-- server/db/mysql/tenant_adapter.go | 121 ++++++++++++++++++++++---- server/db/mysql/user_identity_test.go | 36 +++++++- 3 files changed, 146 insertions(+), 28 deletions(-) diff --git a/server/db/mysql/adapter.go b/server/db/mysql/adapter.go index a6776f7..6a9eb65 100644 --- a/server/db/mysql/adapter.go +++ b/server/db/mysql/adapter.go @@ -3084,12 +3084,13 @@ func (a *adapter) TopicsForUser(uid t.Uid, keepDeleted bool, opts *t.QueryOpt) ( // Fetch p2p users and join to p2p subscriptions. if len(usrq) > 0 { - q = "SELECT id,updatedat,state,access,lastseen,useragent,public,trusted " + - "FROM im_users WHERE id IN (?)" + q = `SELECT u.id,u.updatedat,u.state,u.access,u.lastseen,u.useragent,u.public,u.trusted, + p.nickname AS profile_nickname,p.avatar_file_id AS profile_avatar_file_id,p.updated_at AS profile_updated_at + FROM im_users AS u LEFT JOIN im_user_profiles AS p ON p.user_id=u.id WHERE u.id IN (?)` q, args, _ = sqlx.In(q, usrq) if !keepDeleted { // Optionally skip deleted users. - q += " AND state!=?" + q += " AND u.state!=?" args = append(args, t.StateDeleted) } @@ -3105,19 +3106,19 @@ func (a *adapter) TopicsForUser(uid t.Uid, keepDeleted bool, opts *t.QueryOpt) ( } for rows.Next() { - var usr2 t.User + var usr2 profileUserRow if err = rows.StructScan(&usr2); err != nil { break } - joinOn := uid.P2PName(common.EncodeUidString(usr2.Id)) + joinOn := uid.P2PName(common.EncodeUidString(usr2.ID)) if sub, ok := join[joinOn]; ok { - sub.UpdatedAt = common.SelectLatestTime(sub.UpdatedAt, usr2.UpdatedAt) + sub.UpdatedAt = common.SelectLatestTime(sub.UpdatedAt, usr2.effectiveUpdatedAt()) sub.SetState(usr2.State) - sub.SetPublic(common.FromJSON(usr2.Public)) + sub.SetPublic(usr2.profilePublic()) sub.SetTrusted(common.FromJSON(usr2.Trusted)) sub.SetDefaultAccess(usr2.Access.Auth, usr2.Access.Anon) - sub.SetLastSeenAndUA(usr2.LastSeen, usr2.UserAgent) + sub.SetLastSeenAndUA(usr2.lastSeen(), usr2.UserAgent) join[joinOn] = sub } } diff --git a/server/db/mysql/tenant_adapter.go b/server/db/mysql/tenant_adapter.go index 2e27e93..7e9a172 100644 --- a/server/db/mysql/tenant_adapter.go +++ b/server/db/mysql/tenant_adapter.go @@ -47,6 +47,83 @@ func stampTenantUser(id t.TenantID, user *t.User) *t.User { return user } +type profileUserRow struct { + TenantID t.TenantID `db:"tenant_id"` + ID string `db:"id"` + UpdatedAt time.Time `db:"updatedat"` + State t.ObjState `db:"state"` + Access t.DefaultAccess + LastSeen sql.NullTime `db:"lastseen"` + UserAgent string `db:"useragent"` + Public any `db:"public"` + Trusted any `db:"trusted"` + ProfileNickname sql.NullString `db:"profile_nickname"` + ProfileAvatarFileID sql.NullString `db:"profile_avatar_file_id"` + ProfileUpdatedAt sql.NullTime `db:"profile_updated_at"` +} + +func (row profileUserRow) effectiveUpdatedAt() time.Time { + if row.ProfileUpdatedAt.Valid { + return common.SelectLatestTime(row.UpdatedAt, row.ProfileUpdatedAt.Time) + } + return row.UpdatedAt +} + +func (row profileUserRow) lastSeen() *time.Time { + if row.LastSeen.Valid { + return &row.LastSeen.Time + } + return nil +} + +func (row profileUserRow) profilePublic() any { + return profilePublicFromFields(common.FromJSON(row.Public), row.ProfileNickname, row.ProfileAvatarFileID) +} + +func profilePublicFromFields(base any, nickname, avatarFileID sql.NullString) any { + public := publicMap(base) + if nickname.Valid { + value := strings.TrimSpace(nickname.String) + for _, key := range []string{"display_name", "nickname", "fn", "name"} { + if value == "" { + delete(public, key) + } else { + public[key] = value + } + } + } + if avatarFileID.Valid { + value := strings.TrimSpace(avatarFileID.String) + for _, key := range []string{"avatar_file_id", "photo", "avatar"} { + if value == "" { + delete(public, key) + } else if key == "avatar_file_id" { + public[key] = value + } + } + } + return public +} + +func publicMap(value any) map[string]any { + switch public := value.(type) { + case map[string]any: + out := make(map[string]any, len(public)) + for key, val := range public { + out[key] = val + } + return out + case map[string]string: + out := make(map[string]any, len(public)) + for key, val := range public { + out[key] = val + } + return out + default: + return map[string]any{} + } +} + func stampTenantUsers(id t.TenantID, users []t.User) []t.User { for i := range users { users[i].TenantID = id @@ -870,10 +947,12 @@ func (a *adapter) TenantTopicsForUser(id t.TenantID, uid t.Uid, keepDeleted bool } } if len(usrq) > 0 { - q = "SELECT tenant_id,id,updatedat,state,access,lastseen,useragent,public,trusted FROM im_users WHERE tenant_id=? AND id IN (?)" + q = `SELECT u.tenant_id,u.id,u.updatedat,u.state,u.access,u.lastseen,u.useragent,u.public,u.trusted, + p.nickname AS profile_nickname,p.avatar_file_id AS profile_avatar_file_id,p.updated_at AS profile_updated_at + FROM im_users AS u LEFT JOIN im_user_profiles AS p ON p.user_id=u.id WHERE u.tenant_id=? AND u.id IN (?)` q, args, _ = sqlx.In(q, id, usrq) if !keepDeleted { - q += " AND state!=?" + q += " AND u.state!=?" args = append(args, t.StateDeleted) } rows, err = a.db.QueryxContext(ctx, a.db.Rebind(q), args...) @@ -881,19 +960,19 @@ func (a *adapter) TenantTopicsForUser(id t.TenantID, uid t.Uid, keepDeleted bool return nil, err } for rows.Next() { - var user t.User + var user profileUserRow if err = rows.StructScan(&user); err != nil { rows.Close() return nil, err } - joinOn := uid.P2PName(common.EncodeUidString(user.Id)) + joinOn := uid.P2PName(common.EncodeUidString(user.ID)) if sub, ok := join[joinOn]; ok { - sub.UpdatedAt = common.SelectLatestTime(sub.UpdatedAt, user.UpdatedAt) + sub.UpdatedAt = common.SelectLatestTime(sub.UpdatedAt, user.effectiveUpdatedAt()) sub.SetState(user.State) - sub.SetPublic(common.FromJSON(user.Public)) + sub.SetPublic(user.profilePublic()) sub.SetTrusted(common.FromJSON(user.Trusted)) sub.SetDefaultAccess(user.Access.Auth, user.Access.Anon) - sub.SetLastSeenAndUA(user.LastSeen, user.UserAgent) + sub.SetLastSeenAndUA(user.lastSeen(), user.UserAgent) join[joinOn] = sub } } @@ -8791,7 +8870,8 @@ func (a *adapter) TenantFriendshipList(id t.TenantID, uid t.Uid, queryText, init if cancel != nil { defer cancel() } - rows, err := a.db.QueryxContext(ctx, `SELECT fs.*,u.public,u.trusted + rows, err := a.db.QueryxContext(ctx, `SELECT fs.*,u.public,u.trusted, + p.nickname AS profile_nickname,p.avatar_file_id AS profile_avatar_file_id,p.updated_at AS profile_updated_at FROM im_friendships AS fs JOIN im_users AS u ON u.id=fs.peer_id LEFT JOIN im_user_profiles AS p ON p.user_id=u.id @@ -8805,16 +8885,19 @@ func (a *adapter) TenantFriendshipList(id t.TenantID, uid t.Uid, queryText, init nextCursor := "" for rows.Next() { row := struct { - ID int64 - UserID int64 `db:"user_id"` - PeerID int64 `db:"peer_id"` - Status string - Remark string - Topic string - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` - Public any - Trusted any + ID int64 + UserID int64 `db:"user_id"` + PeerID int64 `db:"peer_id"` + Status string + Remark string + Topic string + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` + Public any + Trusted any + ProfileNickname sql.NullString `db:"profile_nickname"` + ProfileAvatarFileID sql.NullString `db:"profile_avatar_file_id"` + ProfileUpdatedAt sql.NullTime `db:"profile_updated_at"` }{} if err = rows.StructScan(&row); err != nil { return nil, "", err @@ -8838,7 +8921,7 @@ func (a *adapter) TenantFriendshipList(id t.TenantID, uid t.Uid, queryText, init Peer: &t.User{ ObjHeader: t.ObjHeader{Id: peerID.String()}, TenantID: id, - Public: common.FromJSON(row.Public), + Public: profilePublicFromFields(common.FromJSON(row.Public), row.ProfileNickname, row.ProfileAvatarFileID), Trusted: common.FromJSON(row.Trusted), }, }) diff --git a/server/db/mysql/user_identity_test.go b/server/db/mysql/user_identity_test.go index a207842..1416c43 100644 --- a/server/db/mysql/user_identity_test.go +++ b/server/db/mysql/user_identity_test.go @@ -3,7 +3,10 @@ package mysql -import "testing" +import ( + "database/sql" + "testing" +) func TestNormalizeUserPhone(t *testing.T) { for input, want := range map[string]string{ @@ -23,3 +26,34 @@ func TestNormalizeUserPhone(t *testing.T) { t.Fatal("invalid phone unexpectedly accepted") } } + +func TestProfilePublicFromFields(t *testing.T) { + public := profilePublicFromFields(map[string]any{ + "fn": "旧昵称", + "name": "旧昵称", + "nickname": "旧昵称", + "display_name": "旧昵称", + "avatar_file_id": "old_avatar", + "photo": "old_photo", + "public_account": "usrAccount", + }, sql.NullString{String: "新昵称", Valid: true}, sql.NullString{String: "file_avatar_1", Valid: true}).(map[string]any) + + for _, key := range []string{"fn", "name", "nickname", "display_name"} { + if public[key] != "新昵称" { + t.Fatalf("nickname field %s not overlaid: %#v", key, public) + } + } + if public["avatar_file_id"] != "file_avatar_1" || public["public_account"] != "usrAccount" { + t.Fatalf("profile public not overlaid: %#v", public) + } + + public = profilePublicFromFields(public, sql.NullString{Valid: true}, sql.NullString{Valid: true}).(map[string]any) + for _, key := range []string{"fn", "name", "nickname", "display_name", "avatar_file_id", "photo", "avatar"} { + if _, ok := public[key]; ok { + t.Fatalf("field %s should be hidden by empty profile value: %#v", key, public) + } + } + if public["public_account"] != "usrAccount" { + t.Fatalf("unrelated public field changed: %#v", public) + } +}