package privacy import ( "context" "time" "telesrv/internal/domain" "telesrv/internal/readmodelcache" "telesrv/internal/store" ) const ( DefaultPrivacyRulesCacheTTL = 24 * time.Hour privacySnapshotMaxOwners = 8192 ) var allPrivacyRuleKeys = []domain.PrivacyKey{ domain.PrivacyKeyStatusTimestamp, domain.PrivacyKeyChatInvite, domain.PrivacyKeyPhoneCall, domain.PrivacyKeyPhoneP2P, domain.PrivacyKeyForwards, domain.PrivacyKeyProfilePhoto, domain.PrivacyKeyPhoneNumber, domain.PrivacyKeyAddedByPhone, domain.PrivacyKeyVoiceMessages, domain.PrivacyKeyAbout, domain.PrivacyKeyBirthday, domain.PrivacyKeyStarGiftsAutoSave, domain.PrivacyKeyNoPaidMessages, domain.PrivacyKeySavedMusic, } // privacyRulesMap 是单个 owner 的全部隐私规则(空 map = 查过且无规则,即负缓存)。 type privacyRulesMap map[domain.PrivacyKey]domain.PrivacyRules // CachedPrivacyStore 是 account privacy rules 的 owner 级 read-model 缓存,由统一缓存原语承载 // (LRU 单条驱逐 / epoch 守卫 / clone)。owner 级、变更稀少:一次性装入某 owner 全部 key,让 // projectBatch/CanSeeMatrix 在内存里判 phone/status/photo 可见性,免去反复规划 account_privacy_rules。 // 单 owner 走 GetOrLoad,多 owner 走 GetOrLoadBatch(一次 LoadEpoch + 合批 ListPrivacyRules + 写回)。 type CachedPrivacyStore struct { inner store.PrivacyStore cache *readmodelcache.Cache[int64, privacyRulesMap] } func NewCachedPrivacyStore(inner store.PrivacyStore, ttl time.Duration) *CachedPrivacyStore { if inner == nil { return nil } if ttl <= 0 { ttl = DefaultPrivacyRulesCacheTTL } return &CachedPrivacyStore{ inner: inner, cache: readmodelcache.New[int64, privacyRulesMap](readmodelcache.Config[int64, privacyRulesMap]{ MaxEntries: privacySnapshotMaxOwners, TTL: ttl, Clone: clonePrivacyRulesMap, }), } } func (c *CachedPrivacyStore) GetPrivacyRules(ctx context.Context, ownerUserID int64, key domain.PrivacyKey) (domain.PrivacyRules, bool, error) { if ownerUserID == 0 { return domain.PrivacyRules{}, false, nil } rules, err := c.ownerRules(ctx, ownerUserID) if err != nil { return domain.PrivacyRules{}, false, err } r, ok := rules[key] if !ok { return domain.PrivacyRules{}, false, nil } return cloneRules(r), true, nil } func (c *CachedPrivacyStore) SetPrivacyRules(ctx context.Context, rules domain.PrivacyRules) error { if err := c.inner.SetPrivacyRules(ctx, rules); err != nil { return err } c.InvalidateOwners(rules.OwnerUserID) return nil } func (c *CachedPrivacyStore) ListPrivacyRules(ctx context.Context, ownerUserIDs []int64, keys []domain.PrivacyKey) ([]domain.PrivacyRules, error) { owners := dedupPrivacyOwnerIDs(ownerUserIDs) if len(owners) == 0 || len(keys) == 0 { return nil, nil } byOwner, err := c.ownerRulesBatch(ctx, owners) if err != nil { return nil, err } keySet := make(map[domain.PrivacyKey]struct{}, len(keys)) for _, key := range keys { keySet[key] = struct{}{} } out := make([]domain.PrivacyRules, 0, len(owners)*len(keys)) for _, owner := range owners { for key, rules := range byOwner[owner] { if _, want := keySet[key]; !want { continue } out = append(out, cloneRules(rules)) } } return out, nil } func (c *CachedPrivacyStore) ownerRules(ctx context.Context, ownerUserID int64) (privacyRulesMap, error) { load := func() (privacyRulesMap, error) { list, err := c.inner.ListPrivacyRules(ctx, []int64{ownerUserID}, allPrivacyRuleKeys) if err != nil { return nil, err } return buildPrivacyRulesByOwner(list, []int64{ownerUserID})[ownerUserID], nil } if c == nil || c.cache == nil { return load() } return c.cache.GetOrLoad(ctx, ownerUserID, load) } func (c *CachedPrivacyStore) ownerRulesBatch(ctx context.Context, owners []int64) (map[int64]privacyRulesMap, error) { loadMissing := func(ctx context.Context, missing []int64) (map[int64]privacyRulesMap, error) { list, err := c.inner.ListPrivacyRules(ctx, missing, allPrivacyRuleKeys) if err != nil { return nil, err } return buildPrivacyRulesByOwner(list, missing), nil } if c == nil || c.cache == nil { return loadMissing(ctx, owners) } return c.cache.GetOrLoadBatch(ctx, owners, func(int64) (int64, bool) { return 0, true }, // 纯 TTL,无版本闸门 loadMissing, ) } func (c *CachedPrivacyStore) InvalidateOwners(ids ...int64) { if c == nil || c.cache == nil || len(ids) == 0 { return } nonZero := make([]int64, 0, len(ids)) for _, id := range ids { if id != 0 { nonZero = append(nonZero, id) } } c.cache.Invalidate(nonZero...) } func (c *CachedPrivacyStore) FlushReadModelCache() { if c == nil || c.cache == nil { return } c.cache.Flush() } // buildPrivacyRulesByOwner 把扁平规则按 owner 归组;每个 owner 都建一个条目(无规则即空 map), // 这样「查过且无规则」的 owner 也被负缓存,不会反复打后端。 func buildPrivacyRulesByOwner(list []domain.PrivacyRules, owners []int64) map[int64]privacyRulesMap { out := make(map[int64]privacyRulesMap, len(owners)) for _, owner := range owners { out[owner] = make(privacyRulesMap) } for _, item := range list { if item.OwnerUserID == 0 || item.Key == "" { continue } m, ok := out[item.OwnerUserID] if !ok { m = make(privacyRulesMap) out[item.OwnerUserID] = m } m[item.Key] = cloneRules(item) } return out } func clonePrivacyRulesMap(in privacyRulesMap) privacyRulesMap { if in == nil { return nil } out := make(privacyRulesMap, len(in)) for key, rules := range in { out[key] = cloneRules(rules) } return out } func dedupPrivacyOwnerIDs(ids []int64) []int64 { seen := make(map[int64]struct{}, len(ids)) out := make([]int64, 0, len(ids)) for _, id := range ids { if id == 0 { continue } if _, ok := seen[id]; ok { continue } seen[id] = struct{}{} out = append(out, id) } return out }