2026-07-22 13:55:55 +08:00

156 lines
5.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}
}
}