package host import ( "context" "database/sql" "strings" "sync" "time" "hyapp/pkg/appcode" "hyapp/pkg/xerr" hostservice "hyapp/services/user-service/internal/service/host" ) type roleScopePolicyCacheEntry struct { policy hostservice.RoleScopePolicy found bool expiresAt time.Time } // GetRoleScopePolicy 通过 (app_code, scene) 主键读取 owner 配置。过期后数据库失败直接返回错误,不使用旧权限放宽结果。 func (r *Repository) GetRoleScopePolicy(ctx context.Context, scene string) (hostservice.RoleScopePolicy, bool, error) { if r == nil || r.db == nil { return hostservice.RoleScopePolicy{}, false, xerr.New(xerr.Unavailable, "host repository is not configured") } app := appcode.FromContext(ctx) cacheKey := app + "\x00" + scene now := time.Now() if cached, ok := r.cachedRoleScopePolicy(cacheKey, now); ok { return cached.policy, cached.found, nil } // 同一 App+scene 的并发首次读取只能落一次 MySQL,防止缓存过期瞬间形成配置表尖峰。 keyLock := r.roleScopePolicyLock(cacheKey) keyLock.Lock() defer keyLock.Unlock() if cached, ok := r.cachedRoleScopePolicy(cacheKey, time.Now()); ok { return cached.policy, cached.found, nil } var policy hostservice.RoleScopePolicy err := r.db.QueryRowContext(ctx, ` SELECT base_scope, expanded_scope, region_expansion_configurable, updated_at_ms FROM role_scope_policies WHERE app_code = ? AND scene = ? LIMIT 1 `, app, scene).Scan(&policy.BaseScope, &policy.ExpandedScope, &policy.RegionExpansionConfigurable, &policy.UpdatedAtMS) if err != nil && err != sql.ErrNoRows { return hostservice.RoleScopePolicy{}, false, err } policy.Scene = scene entry := roleScopePolicyCacheEntry{ policy: policy, found: err == nil, expiresAt: time.Now().Add(r.roleScopePolicyTTL()), } r.storeRoleScopePolicyCache(cacheKey, entry) return entry.policy, entry.found, nil } // UpsertRoleScopePolicies 在 user DB 单事务保存同一 App 的完整策略快照,提交后使当前实例缓存立即失效。 func (r *Repository) UpsertRoleScopePolicies(ctx context.Context, policies []hostservice.RoleScopePolicy, updatedAtMS int64) ([]hostservice.RoleScopePolicy, error) { if r == nil || r.db == nil { return nil, xerr.New(xerr.Unavailable, "host repository is not configured") } app := appcode.FromContext(ctx) tx, err := r.db.BeginTx(ctx, nil) if err != nil { return nil, err } defer tx.Rollback() stored := make([]hostservice.RoleScopePolicy, 0, len(policies)) for _, policy := range policies { if _, err := tx.ExecContext(ctx, ` INSERT INTO role_scope_policies ( app_code, scene, base_scope, expanded_scope, region_expansion_configurable, created_at_ms, updated_at_ms ) VALUES (?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE base_scope = VALUES(base_scope), expanded_scope = VALUES(expanded_scope), region_expansion_configurable = VALUES(region_expansion_configurable), updated_at_ms = VALUES(updated_at_ms) `, app, policy.Scene, policy.BaseScope, policy.ExpandedScope, policy.RegionExpansionConfigurable, updatedAtMS, updatedAtMS); err != nil { return nil, err } policy.UpdatedAtMS = updatedAtMS stored = append(stored, policy) } if err := tx.Commit(); err != nil { return nil, err } r.invalidateRoleScopePolicies(app) return stored, nil } // roleRegionMatchRequired 把组织关系写入统一解释为 owner 策略;读取失败时调用方必须终止事务而不是放宽区域。 func (r *Repository) roleRegionMatchRequired(ctx context.Context) (bool, error) { policy, err := hostservice.ResolveRoleScopePolicy(ctx, r, hostservice.RoleScopeSceneOrganizationExpansion) if err != nil { return true, err } return policy.BaseScope != hostservice.RoleScopeGlobal, nil } func (r *Repository) cachedRoleScopePolicy(key string, now time.Time) (roleScopePolicyCacheEntry, bool) { r.roleScopeMu.Lock() defer r.roleScopeMu.Unlock() entry, ok := r.roleScopeCache[key] return entry, ok && now.Before(entry.expiresAt) } func (r *Repository) storeRoleScopePolicyCache(key string, entry roleScopePolicyCacheEntry) { r.roleScopeMu.Lock() defer r.roleScopeMu.Unlock() if r.roleScopeCache == nil { r.roleScopeCache = make(map[string]roleScopePolicyCacheEntry) } r.roleScopeCache[key] = entry } func (r *Repository) roleScopePolicyLock(key string) *sync.Mutex { r.roleScopeMu.Lock() defer r.roleScopeMu.Unlock() if r.roleScopeLocks == nil { r.roleScopeLocks = make(map[string]*sync.Mutex) } if lock := r.roleScopeLocks[key]; lock != nil { return lock } lock := &sync.Mutex{} r.roleScopeLocks[key] = lock return lock } func (r *Repository) roleScopePolicyTTL() time.Duration { r.roleScopeMu.Lock() defer r.roleScopeMu.Unlock() if r.roleScopeTTL <= 0 { return 30 * time.Second } return r.roleScopeTTL } func (r *Repository) invalidateRoleScopePolicies(app string) { prefix := app + "\x00" r.roleScopeMu.Lock() defer r.roleScopeMu.Unlock() for key := range r.roleScopeCache { if strings.HasPrefix(key, prefix) { delete(r.roleScopeCache, key) } } }