fix: sync contact phone privacy disclosure

This commit is contained in:
iamxvbaba 2026-07-24 11:56:58 +08:00
parent 0e2fcdf9c8
commit e1a95c7318
19 changed files with 789 additions and 50 deletions

View file

@ -81,6 +81,31 @@ func (c *CachedPrivacyStore) SetPrivacyRules(ctx context.Context, rules domain.P
return err
}
c.InvalidateOwners(rules.OwnerUserID)
// 数据写入已提交,预热失败不能伪装成写失败;LISTEN/NOTIFY 也会在每个
// 实例上再次失效并预热,覆盖本实例通知晚于这里到达的时序。
_ = c.WarmOwners(ctx, rules.OwnerUserID)
return nil
}
// WarmOwners 在低频写/变更通知路径一次性装入 owner 的完整规则集。调用方必须先
// InvalidateOwners;epoch 保证预热期间若又发生失效,不会把旧快照写回。
func (c *CachedPrivacyStore) WarmOwners(ctx context.Context, ownerUserIDs ...int64) error {
owners := dedupPrivacyOwnerIDs(ownerUserIDs)
if len(owners) == 0 || c == nil || c.cache == nil {
return nil
}
// 隐私规则变更很少、读取极热。必须重建全部 key,
// 不能只塞本次 key,否则会把 owner 的其它持久规则误当成默认规则。
loadEpoch := c.cache.LoadEpoch()
list, err := c.inner.ListPrivacyRules(ctx, owners, allPrivacyRuleKeys)
if err != nil {
return err
}
snapshots := buildPrivacyRulesByOwner(list, owners)
for _, ownerUserID := range owners {
c.cache.StoreIfEpoch(ownerUserID, snapshots[ownerUserID], loadEpoch)
}
return nil
}

View file

@ -108,7 +108,7 @@ func TestCachedPrivacyStoreUsesOwnerSnapshot(t *testing.T) {
}
}
func TestCachedPrivacyStoreInvalidatesOnSet(t *testing.T) {
func TestCachedPrivacyStoreWarmsCompleteOwnerSnapshotOnSet(t *testing.T) {
ctx := context.Background()
base := memory.NewPrivacyStore()
counting := &countingPrivacyStore{PrivacyStore: base}
@ -121,24 +121,34 @@ func TestCachedPrivacyStoreInvalidatesOnSet(t *testing.T) {
t.Fatalf("set first: %v", err)
}
if _, ok, err := cached.GetPrivacyRules(ctx, 1001, domain.PrivacyKeyPhoneNumber); err != nil || !ok {
t.Fatalf("prime get ok=%v err=%v", ok, err)
t.Fatalf("first memory get ok=%v err=%v", ok, err)
}
if err := cached.SetPrivacyRules(ctx, domain.PrivacyRules{
OwnerUserID: 1001,
Key: domain.PrivacyKeyPhoneNumber,
Rules: []domain.PrivacyRule{{Kind: domain.PrivacyRuleAllowAll}},
Key: domain.PrivacyKeyProfilePhoto,
Rules: []domain.PrivacyRule{{Kind: domain.PrivacyRuleDisallowAll}},
}); err != nil {
t.Fatalf("set second: %v", err)
}
got, ok, err := cached.GetPrivacyRules(ctx, 1001, domain.PrivacyKeyPhoneNumber)
if err != nil || !ok {
t.Fatalf("after invalidation get ok=%v err=%v", ok, err)
t.Fatalf("phone after second set ok=%v err=%v", ok, err)
}
if got.Rules[0].Kind != domain.PrivacyRuleAllowAll {
t.Fatalf("rules after invalidation = %+v, want allow all", got.Rules)
if got.Rules[0].Kind != domain.PrivacyRuleDisallowAll {
t.Fatalf("phone rules after second set = %+v, want disallow all", got.Rules)
}
photo, ok, err := cached.GetPrivacyRules(ctx, 1001, domain.PrivacyKeyProfilePhoto)
if err != nil || !ok {
t.Fatalf("photo after second set ok=%v err=%v", ok, err)
}
if photo.Rules[0].Kind != domain.PrivacyRuleDisallowAll {
t.Fatalf("photo rules after second set = %+v, want disallow all", photo.Rules)
}
if counting.setCalls != 2 {
t.Fatalf("SetPrivacyRules calls = %d, want 2", counting.setCalls)
}
if counting.listCalls != 2 {
t.Fatalf("ListPrivacyRules calls = %d, want 2 after invalidation", counting.listCalls)
t.Fatalf("ListPrivacyRules calls = %d, want exactly one write-path warm per set and no read-path query", counting.listCalls)
}
}