feat: sync privacy read-model rules

This commit is contained in:
iamxvbaba 2026-07-24 11:56:59 +08:00
parent 9f467f4be7
commit 5d5883a3d1
20 changed files with 1011 additions and 20 deletions

View file

@ -207,6 +207,45 @@ func TestReadModelChangeListenerInvalidatesPrivateMediaCountCache(t *testing.T)
}
}
func TestReadModelChangeListenerInvalidatesAccountSettingsCache(t *testing.T) {
settings := &fakeAccountSettingsReadModelCache{}
listener := NewReadModelChangeListener("", ReadModelCacheSet{
AccountSettings: settings,
}, nil)
listener.handlePayload(`{"model":"account_settings","owner_user_id":100,"peer_type":"user","peer_id":100,"version":2}`)
if len(settings.invalidated) != 1 || settings.invalidated[0] != 100 {
t.Fatalf("account_settings invalidations = %v, want [100]", settings.invalidated)
}
if len(settings.warmed) != 1 || settings.warmed[0] != 100 {
t.Fatalf("account_settings warmups = %v, want [100]", settings.warmed)
}
listener.flush("test")
if settings.flushes != 1 {
t.Fatalf("account_settings flushes = %d, want 1", settings.flushes)
}
}
type fakeAccountSettingsReadModelCache struct {
invalidated []int64
warmed []int64
flushes int
}
func (f *fakeAccountSettingsReadModelCache) InvalidateAccountSettingsReadModel(userID int64) {
f.invalidated = append(f.invalidated, userID)
}
func (f *fakeAccountSettingsReadModelCache) FlushAccountSettingsReadModel() {
f.flushes++
}
func (f *fakeAccountSettingsReadModelCache) WarmAccountSettingsReadModel(_ context.Context, userID int64) error {
f.warmed = append(f.warmed, userID)
return nil
}
// TestReadModelChangeListenerBotFullFlushesChannelFullBots 回归: bot 改资料(bot_full 事件,
// 迁移 0013)须 flush channelFullBotInfoCache(否则群信息页 bot 简介/命令跨实例陈旧至 TTL);
// 普通用户的 user_base 事件不得 flush(否则该缓存形同虚设)。

View file

@ -37,6 +37,7 @@ type ReadModelCacheSet struct {
BaseUsers BaseUserCache
BotProfiles BotProfileReadModelCache
StarGifts StarGiftCatalogCache
AccountSettings AccountSettingsReadModelCache
}
type StarGiftCatalogCache interface {
@ -44,6 +45,15 @@ type StarGiftCatalogCache interface {
FlushStarGiftCatalog()
}
type AccountSettingsReadModelCache interface {
InvalidateAccountSettingsReadModel(userID int64)
FlushAccountSettingsReadModel()
}
type AccountSettingsReadModelWarmer interface {
WarmAccountSettingsReadModel(context.Context, int64) error
}
// BaseUserCache 是跨进程共享的 user:base 缓存(Redis)。user_base read-model 事件必须删除
// 对应 user 键,否则 RPC 投影失效后会从陈旧的 user:base 重建(失效被未失效的源自我抵消)。
// 不参与重连 flushRedis 是跨实例共享的,整库清空是错的,漏掉的通知靠其自身 TTL 兜底。
@ -241,7 +251,8 @@ func (l *ReadModelChangeListener) empty() bool {
l.caches.RPCProjections == nil &&
l.caches.BaseUsers == nil &&
l.caches.BotProfiles == nil &&
l.caches.StarGifts == nil
l.caches.StarGifts == nil &&
l.caches.AccountSettings == nil
}
func (l *ReadModelChangeListener) flush(reasons ...string) {
@ -318,6 +329,10 @@ func (l *ReadModelChangeListener) flush(reasons ...string) {
l.caches.StarGifts.FlushStarGiftCatalog()
flushed = append(flushed, "star_gifts")
}
if l.caches.AccountSettings != nil {
l.caches.AccountSettings.FlushAccountSettingsReadModel()
flushed = append(flushed, "account_settings")
}
// 注意BaseUsers(Redis) 刻意不在重连时 flush——它是跨实例共享缓存整库清空会误伤
// 其它实例;漏掉的通知由其 5min TTL 兜底。
l.log.Info("read model caches flushed",
@ -346,6 +361,19 @@ func (l *ReadModelChangeListener) handlePayload(payload string) {
}
}
switch evt.Model {
case "account_settings":
if evt.OwnerUserID != 0 && l.caches.AccountSettings != nil {
l.caches.AccountSettings.InvalidateAccountSettingsReadModel(evt.OwnerUserID)
if warmer, ok := l.caches.AccountSettings.(AccountSettingsReadModelWarmer); ok {
ctx, cancel := context.WithTimeout(context.Background(), privacyReadModelWarmTimeout)
err := warmer.WarmAccountSettingsReadModel(ctx, evt.OwnerUserID)
cancel()
if err != nil {
l.log.Warn("warm account settings read model after change",
zap.Int64("owner_user_id", evt.OwnerUserID), zap.Error(err))
}
}
}
case "star_gift_catalog":
if l.caches.StarGifts != nil {
l.caches.StarGifts.InvalidateStarGiftCatalog()