156 lines
5.0 KiB
Go
156 lines
5.0 KiB
Go
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)
|
||
}
|
||
}
|
||
}
|