Read user profile display from profile table

This commit is contained in:
richjohnrambo
2026-09-04 10:49:33 +08:00
parent 3173313ab9
commit 62af772260
3 changed files with 146 additions and 28 deletions
+9 -8
View File
@@ -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
}
}
+102 -19
View File
@@ -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),
},
})
+35 -1
View File
@@ -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)
}
}