From 89496b3558b7d4d32635a55b22cead37310bb8b5 Mon Sep 17 00:00:00 2001 From: onysd Date: Mon, 20 Jul 2026 12:43:53 +0300 Subject: [PATCH] fix --- internal/app/users/service.go | 22 ++++----- internal/app/users/service_test.go | 76 ++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 11 deletions(-) diff --git a/internal/app/users/service.go b/internal/app/users/service.go index 96b9e27f..1e2f69b1 100644 --- a/internal/app/users/service.go +++ b/internal/app/users/service.go @@ -225,14 +225,14 @@ func (s *Service) UpdateUsername(ctx context.Context, userID int64, username str } } if self.Username == username { - return self, nil + return s.projectOne(ctx, self.ID, self) } u, err := s.users.UpdateUsername(ctx, self.ID, username) if err != nil { return domain.User{}, err } s.refreshCachedUsers(ctx, u) - return u, nil + return s.projectOne(ctx, self.ID, u) } // UpdateProfile 修改当前用户的基础资料。未设置的字段保持原值。 @@ -264,14 +264,14 @@ func (s *Service) UpdateProfile(ctx context.Context, userID int64, update domain return domain.User{}, domain.ErrAboutTooLong } if firstName == self.FirstName && lastName == self.LastName && about == self.About { - return self, nil + return s.projectOne(ctx, self.ID, self) } u, err := s.users.UpdateProfile(ctx, self.ID, firstName, lastName, about) if err != nil { return domain.User{}, err } s.refreshCachedUsers(ctx, u) - return u, nil + return s.projectOne(ctx, self.ID, u) } // UpdateLastSeen records the latest visible account activity time. @@ -333,7 +333,7 @@ func (s *Service) GrantPremium(ctx context.Context, userID int64, months int) (d return domain.User{}, err } s.refreshCachedUsers(ctx, updated) - return updated, nil + return s.projectOne(ctx, userID, updated) } // SetVerified 设置/取消用户认证标记。认证是账号基础事实,所有 user 投影统一消费该字段。 @@ -349,14 +349,14 @@ func (s *Service) SetVerified(ctx context.Context, userID int64, verified bool) return domain.User{}, domain.ErrUserNotFound } if u.Verified == verified { - return u, nil + return s.projectOne(ctx, userID, u) } updated, err := s.users.SetVerified(ctx, userID, verified) if err != nil { return domain.User{}, err } s.refreshCachedUsers(ctx, updated) - return updated, nil + return s.projectOne(ctx, userID, updated) } // SweepExpiredPremium 清理到期会员(store 把过期行清 NULL)并失效用户缓存, @@ -390,7 +390,7 @@ func (s *Service) UpdateEmojiStatus(ctx context.Context, userID int64, documentI return domain.User{}, err } s.refreshCachedUsers(ctx, u) - return u, nil + return s.projectOne(ctx, self.ID, u) } // UpdateBirthday 设置/清除用户生日(account.updateBirthday)。零值 Birthday 表示清除。 @@ -411,7 +411,7 @@ func (s *Service) UpdateBirthday(ctx context.Context, userID int64, birthday dom return domain.User{}, err } s.refreshCachedUsers(ctx, u) - return u, nil + return s.projectOne(ctx, self.ID, u) } // UpdatePersonalChannel 设置/清除资料页个人频道(account.updatePersonalChannel); @@ -426,7 +426,7 @@ func (s *Service) UpdatePersonalChannel(ctx context.Context, userID int64, chann return domain.User{}, err } s.refreshCachedUsers(ctx, u) - return u, nil + return s.projectOne(ctx, self.ID, u) } // UpdateColor updates the user's message accent or profile background color. @@ -440,7 +440,7 @@ func (s *Service) UpdateColor(ctx context.Context, userID int64, forProfile bool return domain.User{}, err } s.refreshCachedUsers(ctx, u) - return u, nil + return s.projectOne(ctx, self.ID, u) } // ResolveUsername 解析 username 到用户;调用方必须已登录。 diff --git a/internal/app/users/service_test.go b/internal/app/users/service_test.go index 432de6ee..4c800202 100644 --- a/internal/app/users/service_test.go +++ b/internal/app/users/service_test.go @@ -122,6 +122,82 @@ func TestServiceUpdateBirthday(t *testing.T) { } } +// fakePhotoProvider always reports the same current photo for every owner, +// regardless of owner id — enough to prove a mutation path preserves the +// photo instead of returning the bare, un-projected store row. +type fakePhotoProvider struct { + ref domain.ProfilePhotoRef +} + +func (f *fakePhotoProvider) CurrentProfilePhotos(ctx context.Context, ownerType domain.PeerType, ownerIDs []int64) (map[int64]domain.ProfilePhotoRef, error) { + out := make(map[int64]domain.ProfilePhotoRef, len(ownerIDs)) + for _, id := range ownerIDs { + out[id] = f.ref + } + return out, nil +} + +// TestServiceMutationsPreservePhoto guards against a class of bug where a +// profile-mutating method returns the bare store row (users table has no +// photo columns at all — the current avatar lives only in the profile_photos +// association, attached via projectOne/Projector.One) instead of the +// photo-enriched projection, causing the client to wipe its own avatar on +// name/username/birthday/verified/color/emoji-status updates. +func TestServiceMutationsPreservePhoto(t *testing.T) { + ctx := context.Background() + store := memory.NewUserStore() + owner, err := store.Create(ctx, domain.User{AccessHash: 1, Phone: "15550000099", FirstName: "Owner"}) + if err != nil { + t.Fatalf("create owner: %v", err) + } + photos := &fakePhotoProvider{ref: domain.ProfilePhotoRef{PhotoID: 555, DCID: 2, Stripped: []byte{1, 2, 3}}} + svc := NewService(store, WithPhotoProvider(photos)) + + assertHasPhoto := func(t *testing.T, label string, u domain.User) { + t.Helper() + if u.PhotoID != 555 || u.PhotoDCID != 2 { + t.Fatalf("%s: photo = {id:%d dc:%d}, want {id:555 dc:2}", label, u.PhotoID, u.PhotoDCID) + } + } + + u, err := svc.UpdateProfile(ctx, owner.ID, domain.UserProfileUpdate{FirstName: "New", HasFirstName: true}) + if err != nil { + t.Fatalf("UpdateProfile: %v", err) + } + assertHasPhoto(t, "UpdateProfile", u) + + // No-op branch (nothing actually changed) must also preserve the photo. + u, err = svc.UpdateProfile(ctx, owner.ID, domain.UserProfileUpdate{FirstName: "New", HasFirstName: true}) + if err != nil { + t.Fatalf("UpdateProfile no-op: %v", err) + } + assertHasPhoto(t, "UpdateProfile no-op", u) + + u, err = svc.UpdateUsername(ctx, owner.ID, "ownerhandle") + if err != nil { + t.Fatalf("UpdateUsername: %v", err) + } + assertHasPhoto(t, "UpdateUsername", u) + + u, err = svc.UpdateBirthday(ctx, owner.ID, domain.Birthday{Day: 1, Month: 1, Year: 2000}) + if err != nil { + t.Fatalf("UpdateBirthday: %v", err) + } + assertHasPhoto(t, "UpdateBirthday", u) + + u, err = svc.SetVerified(ctx, owner.ID, true) + if err != nil { + t.Fatalf("SetVerified: %v", err) + } + assertHasPhoto(t, "SetVerified", u) + + u, err = svc.UpdateColor(ctx, owner.ID, false, domain.PeerColor{HasColor: true, Color: 3}) + if err != nil { + t.Fatalf("UpdateColor: %v", err) + } + assertHasPhoto(t, "UpdateColor", u) +} + func TestServiceUpdatePersonalChannel(t *testing.T) { ctx := context.Background() store := memory.NewUserStore()