Merge remote-tracking branch 'upstream/main' into merge-gramsrv-2965f5d

This commit is contained in:
onysd 2026-07-20 23:43:51 +03:00
commit ebb0be38d9
355 changed files with 44640 additions and 2320 deletions

View file

@ -107,7 +107,7 @@ func TestUpdateEmojiStatusPremiumGate(t *testing.T) {
svc := NewService(store)
// 非会员设置被拒PREMIUM_ACCOUNT_REQUIRED
if _, err := svc.UpdateEmojiStatus(ctx, u.ID, 42, 0); !errors.Is(err, domain.ErrPremiumRequired) {
if _, err := svc.UpdateEmojiStatus(ctx, u.ID, domain.UserEmojiStatus{DocumentID: 42}); !errors.Is(err, domain.ErrPremiumRequired) {
t.Fatalf("non-premium set err = %v, want ErrPremiumRequired", err)
}
@ -115,14 +115,14 @@ func TestUpdateEmojiStatusPremiumGate(t *testing.T) {
if _, err := store.SetPremiumUntil(ctx, u.ID, int(time.Now().Add(time.Hour).Unix())); err != nil {
t.Fatalf("grant: %v", err)
}
set, err := svc.UpdateEmojiStatus(ctx, u.ID, 42, 0)
set, err := svc.UpdateEmojiStatus(ctx, u.ID, domain.UserEmojiStatus{DocumentID: 42})
if err != nil || set.EmojiStatusDocumentID != 42 {
t.Fatalf("premium set = %+v err %v, want document 42", set, err)
}
if _, err := store.SetPremiumUntil(ctx, u.ID, 0); err != nil {
t.Fatalf("downgrade: %v", err)
}
cleared, err := svc.UpdateEmojiStatus(ctx, u.ID, 0, 0)
cleared, err := svc.UpdateEmojiStatus(ctx, u.ID, domain.UserEmojiStatus{})
if err != nil || cleared.EmojiStatusDocumentID != 0 {
t.Fatalf("clear after downgrade = %+v err %v, want cleared", cleared, err)
}

View file

@ -375,17 +375,14 @@ func (s *Service) SweepExpiredPremium(ctx context.Context, now int64, limit int)
return users, nil
}
// UpdateEmojiStatus 更新当前用户 emoji statuspremium 专属;documentID=0 清除)。
// UpdateEmojiStatus 更新当前用户 emoji statuspremium 专属;零值清除)。
// 清除不要求会员(到期降级后客户端仍可显式清掉残留状态)。
func (s *Service) UpdateEmojiStatus(ctx context.Context, userID int64, documentID int64, until int) (domain.User, error) {
self, err := s.loadSelf(ctx, userID)
func (s *Service) UpdateEmojiStatus(ctx context.Context, userID int64, status domain.UserEmojiStatus) (domain.User, error) {
self, err := s.validateEmojiStatusUpdate(ctx, userID, status)
if err != nil {
return domain.User{}, err
}
if documentID != 0 && !self.PremiumActiveAt(time.Now().Unix()) {
return domain.User{}, domain.ErrPremiumRequired
}
u, err := s.users.UpdateEmojiStatus(ctx, self.ID, documentID, until)
u, err := s.users.UpdateEmojiStatus(ctx, self.ID, status)
if err != nil {
return domain.User{}, err
}
@ -393,6 +390,52 @@ func (s *Service) UpdateEmojiStatus(ctx context.Context, userID int64, documentI
return s.projectOne(ctx, self.ID, u)
}
// UpdateEmojiStatusWithEvent uses the store's aggregate transaction when it
// is available. The bool reports whether the returned event was durably
// appended with dispatch; lightweight memory/test wiring falls back to the
// ordinary state write and lets the RPC's Updates service append the event.
func (s *Service) UpdateEmojiStatusWithEvent(ctx context.Context, userID int64, status domain.UserEmojiStatus, date int, excludeAuthKeyID [8]byte, excludeSessionID int64) (domain.User, domain.UpdateEvent, bool, error) {
self, err := s.validateEmojiStatusUpdate(ctx, userID, status)
if err != nil {
return domain.User{}, domain.UpdateEvent{}, false, err
}
writer, ok := s.users.(store.UserEmojiStatusEventStore)
if !ok {
u, err := s.users.UpdateEmojiStatus(ctx, self.ID, status)
if err == nil {
s.refreshCachedUsers(ctx, u)
}
return u, domain.UpdateEvent{}, false, err
}
event := domain.UpdateEvent{
Type: domain.UpdateEventUserEmojiStatus,
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: self.ID},
EmojiStatus: status,
Date: date,
PtsCount: 1,
}
u, event, err := writer.UpdateEmojiStatusWithEvent(ctx, self.ID, status, event, excludeAuthKeyID, excludeSessionID)
if err != nil {
return domain.User{}, domain.UpdateEvent{}, false, err
}
s.refreshCachedUsers(ctx, u)
return u, event, true, nil
}
func (s *Service) validateEmojiStatusUpdate(ctx context.Context, userID int64, status domain.UserEmojiStatus) (domain.User, error) {
self, err := s.loadSelf(ctx, userID)
if err != nil {
return domain.User{}, err
}
if !status.Valid() {
return domain.User{}, domain.ErrStarGiftCollectibleInvalid
}
if !status.Empty() && !self.PremiumActiveAt(time.Now().Unix()) {
return domain.User{}, domain.ErrPremiumRequired
}
return self, nil
}
// UpdateBirthday 设置/清除用户生日account.updateBirthday。零值 Birthday 表示清除。
func (s *Service) UpdateBirthday(ctx context.Context, userID int64, birthday domain.Birthday) (domain.User, error) {
self, err := s.loadSelf(ctx, userID)
@ -513,7 +556,7 @@ func (s *Service) loadBaseUsersByIDs(ctx context.Context, userIDs []int64) ([]do
if s.cache != nil {
if cached, err := s.cache.GetByIDs(ctx, ids); err == nil && len(cached) > 0 {
for id, u := range cached {
if u.ID != 0 {
if u.ID != 0 && u.EmojiStatusCollectible.Empty() {
loaded[id] = u
}
}
@ -561,7 +604,18 @@ func (s *Service) putCachedUsers(ctx context.Context, users ...domain.User) {
if s.cache == nil || len(users) == 0 {
return
}
_ = s.cache.PutMany(ctx, users)
cacheable := make([]domain.User, 0, len(users))
for _, user := range users {
// Collectible ownership may change inside the star-gift aggregate. Keep
// these uncommon users on the authoritative store path so the database
// lifecycle trigger can never be masked by a stale base-user cache entry.
if user.ID != 0 && user.EmojiStatusCollectible.Empty() {
cacheable = append(cacheable, user)
}
}
if len(cacheable) > 0 {
_ = s.cache.PutMany(ctx, cacheable)
}
}
func (s *Service) dropCachedUsers(ctx context.Context, userIDs ...int64) {