403 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			403 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package mid
 | ||
| 
 | ||
| //
 | ||
| //import (
 | ||
| //	"context"
 | ||
| //	"log"
 | ||
| //	"net/http"
 | ||
| //	"sync"
 | ||
| //	"time"
 | ||
| //
 | ||
| //	"management/internal/erpserver/model/dto"
 | ||
| //	v1 "management/internal/erpserver/service/v1"
 | ||
| //	"management/internal/pkg/know"
 | ||
| //	"management/internal/pkg/session"
 | ||
| //)
 | ||
| //
 | ||
| //var publicRoutes = map[string]bool{
 | ||
| //	"/home.html":          true,
 | ||
| //	"/dashboard":          true,
 | ||
| //	"/system/menus":       true,
 | ||
| //	"/upload/img":         true,
 | ||
| //	"/upload/file":        true,
 | ||
| //	"/upload/multi_files": true,
 | ||
| //	"/pear.json":          true,
 | ||
| //	"/logout":             true,
 | ||
| //}
 | ||
| //
 | ||
| //// MenuCacheItem 菜单缓存项
 | ||
| //type MenuCacheItem struct {
 | ||
| //	Data     map[string]*dto.OwnerMenuDto
 | ||
| //	ExpireAt time.Time
 | ||
| //	LoadTime time.Time
 | ||
| //	Version  int64
 | ||
| //}
 | ||
| //
 | ||
| //// IsExpired 检查是否过期
 | ||
| //func (item *MenuCacheItem) IsExpired() bool {
 | ||
| //	return time.Now().After(item.ExpireAt)
 | ||
| //}
 | ||
| //
 | ||
| //// MenuCache 内存缓存管理器
 | ||
| //type MenuCache struct {
 | ||
| //	mu          sync.RWMutex
 | ||
| //	cache       map[int32]*MenuCacheItem // roleID -> MenuCacheItem
 | ||
| //	maxSize     int                      // 最大缓存条目数
 | ||
| //	ttl         time.Duration            // 缓存TTL
 | ||
| //	refreshTTL  time.Duration            // 刷新TTL (提前刷新时间)
 | ||
| //	stats       CacheStats               // 缓存统计
 | ||
| //	cleanupTick *time.Ticker             // 清理定时器
 | ||
| //	stopCh      chan struct{}            // 停止信号
 | ||
| //}
 | ||
| //
 | ||
| //// CacheStats 缓存统计信息
 | ||
| //type CacheStats struct {
 | ||
| //	mu           sync.RWMutex
 | ||
| //	Hits         int64
 | ||
| //	Misses       int64
 | ||
| //	Evictions    int64
 | ||
| //	RefreshCount int64
 | ||
| //}
 | ||
| //
 | ||
| //// NewMenuCache 创建新的菜单缓存
 | ||
| //func NewMenuCache(maxSize int, ttl time.Duration) *MenuCache {
 | ||
| //	cache := &MenuCache{
 | ||
| //		cache:       make(map[int32]*MenuCacheItem),
 | ||
| //		maxSize:     maxSize,
 | ||
| //		ttl:         ttl,
 | ||
| //		refreshTTL:  ttl - time.Duration(float64(ttl)*0.1), // 提前10%刷新
 | ||
| //		stopCh:      make(chan struct{}),
 | ||
| //		cleanupTick: time.NewTicker(time.Minute * 5), // 每5分钟清理一次
 | ||
| //	}
 | ||
| //
 | ||
| //	// 启动后台清理协程
 | ||
| //	go cache.cleanup()
 | ||
| //
 | ||
| //	return cache
 | ||
| //}
 | ||
| //
 | ||
| //// Get 获取缓存数据
 | ||
| //func (mc *MenuCache) Get(roleID int32) (map[string]*dto.OwnerMenuDto, bool) {
 | ||
| //	mc.mu.RLock()
 | ||
| //	item, exists := mc.cache[roleID]
 | ||
| //	mc.mu.RUnlock()
 | ||
| //
 | ||
| //	if !exists {
 | ||
| //		mc.recordMiss()
 | ||
| //		return nil, false
 | ||
| //	}
 | ||
| //
 | ||
| //	// 检查是否过期
 | ||
| //	if item.IsExpired() {
 | ||
| //		mc.recordMiss()
 | ||
| //		// 异步删除过期项
 | ||
| //		go func() {
 | ||
| //			mc.mu.Lock()
 | ||
| //			delete(mc.cache, roleID)
 | ||
| //			mc.mu.Unlock()
 | ||
| //		}()
 | ||
| //		return nil, false
 | ||
| //	}
 | ||
| //
 | ||
| //	mc.recordHit()
 | ||
| //	return item.Data, true
 | ||
| //}
 | ||
| //
 | ||
| //// Set 设置缓存数据
 | ||
| //func (mc *MenuCache) Set(roleID int32, data map[string]*dto.OwnerMenuDto, version int64) {
 | ||
| //	mc.mu.Lock()
 | ||
| //	defer mc.mu.Unlock()
 | ||
| //
 | ||
| //	// 如果缓存已满,执行LRU淘汰
 | ||
| //	if len(mc.cache) >= mc.maxSize {
 | ||
| //		mc.evictLRU()
 | ||
| //	}
 | ||
| //
 | ||
| //	item := &MenuCacheItem{
 | ||
| //		Data:     data,
 | ||
| //		ExpireAt: time.Now().Add(mc.ttl),
 | ||
| //		LoadTime: time.Now(),
 | ||
| //		Version:  version,
 | ||
| //	}
 | ||
| //
 | ||
| //	mc.cache[roleID] = item
 | ||
| //}
 | ||
| //
 | ||
| //// NeedRefresh 检查是否需要提前刷新
 | ||
| //func (mc *MenuCache) NeedRefresh(roleID int32) bool {
 | ||
| //	mc.mu.RLock()
 | ||
| //	item, exists := mc.cache[roleID]
 | ||
| //	mc.mu.RUnlock()
 | ||
| //
 | ||
| //	if !exists {
 | ||
| //		return true
 | ||
| //	}
 | ||
| //
 | ||
| //	// 提前刷新策略:在过期前10%时间开始刷新
 | ||
| //	refreshTime := item.LoadTime.Add(mc.refreshTTL)
 | ||
| //	return time.Now().After(refreshTime)
 | ||
| //}
 | ||
| //
 | ||
| //// evictLRU 淘汰最久未使用的缓存项
 | ||
| //func (mc *MenuCache) evictLRU() {
 | ||
| //	var oldestRoleID int32
 | ||
| //	var oldestTime time.Time = time.Now()
 | ||
| //
 | ||
| //	for roleID, item := range mc.cache {
 | ||
| //		if item.LoadTime.Before(oldestTime) {
 | ||
| //			oldestTime = item.LoadTime
 | ||
| //			oldestRoleID = roleID
 | ||
| //		}
 | ||
| //	}
 | ||
| //
 | ||
| //	if oldestRoleID != 0 {
 | ||
| //		delete(mc.cache, oldestRoleID)
 | ||
| //		mc.stats.mu.Lock()
 | ||
| //		mc.stats.Evictions++
 | ||
| //		mc.stats.mu.Unlock()
 | ||
| //	}
 | ||
| //}
 | ||
| //
 | ||
| //// cleanup 后台清理过期缓存
 | ||
| //func (mc *MenuCache) cleanup() {
 | ||
| //	for {
 | ||
| //		select {
 | ||
| //		case <-mc.cleanupTick.C:
 | ||
| //			mc.cleanupExpired()
 | ||
| //		case <-mc.stopCh:
 | ||
| //			mc.cleanupTick.Stop()
 | ||
| //			return
 | ||
| //		}
 | ||
| //	}
 | ||
| //}
 | ||
| //
 | ||
| //// cleanupExpired 清理过期缓存
 | ||
| //func (mc *MenuCache) cleanupExpired() {
 | ||
| //	mc.mu.Lock()
 | ||
| //	defer mc.mu.Unlock()
 | ||
| //
 | ||
| //	now := time.Now()
 | ||
| //	for roleID, item := range mc.cache {
 | ||
| //		if now.After(item.ExpireAt) {
 | ||
| //			delete(mc.cache, roleID)
 | ||
| //		}
 | ||
| //	}
 | ||
| //}
 | ||
| //
 | ||
| //// GetStats 获取缓存统计信息
 | ||
| //func (mc *MenuCache) GetStats() CacheStats {
 | ||
| //	mc.stats.mu.RLock()
 | ||
| //	defer mc.stats.mu.RUnlock()
 | ||
| //	return mc.stats
 | ||
| //}
 | ||
| //
 | ||
| //// recordHit 记录缓存命中
 | ||
| //func (mc *MenuCache) recordHit() {
 | ||
| //	mc.stats.mu.Lock()
 | ||
| //	mc.stats.Hits++
 | ||
| //	mc.stats.mu.Unlock()
 | ||
| //}
 | ||
| //
 | ||
| //// recordMiss 记录缓存未命中
 | ||
| //func (mc *MenuCache) recordMiss() {
 | ||
| //	mc.stats.mu.Lock()
 | ||
| //	mc.stats.Misses++
 | ||
| //	mc.stats.mu.Unlock()
 | ||
| //}
 | ||
| //
 | ||
| //// Close 关闭缓存
 | ||
| //func (mc *MenuCache) Close() {
 | ||
| //	close(mc.stopCh)
 | ||
| //}
 | ||
| //
 | ||
| //// CachedMenuService 带缓存的菜单服务包装器
 | ||
| //type CachedMenuService struct {
 | ||
| //	menuService v1.MenuService
 | ||
| //	cache       *MenuCache
 | ||
| //	mu          sync.RWMutex
 | ||
| //	refreshing  map[int32]bool // 正在刷新的roleID
 | ||
| //}
 | ||
| //
 | ||
| //// NewCachedMenuService 创建带缓存的菜单服务
 | ||
| //func NewCachedMenuService(menuService v1.MenuService, cache *MenuCache) *CachedMenuService {
 | ||
| //	return &CachedMenuService{
 | ||
| //		menuService: menuService,
 | ||
| //		cache:       cache,
 | ||
| //		refreshing:  make(map[int32]bool),
 | ||
| //	}
 | ||
| //}
 | ||
| //
 | ||
| //// ListByRoleIDToMap 获取菜单数据(带缓存)
 | ||
| //func (cms *CachedMenuService) ListByRoleIDToMap(ctx context.Context, roleID int32) (map[string]*dto.OwnerMenuDto, error) {
 | ||
| //	// 先尝试从缓存获取
 | ||
| //	if data, hit := cms.cache.Get(roleID); hit {
 | ||
| //		// 检查是否需要异步刷新
 | ||
| //		if cms.cache.NeedRefresh(roleID) {
 | ||
| //			go cms.asyncRefresh(ctx, roleID)
 | ||
| //		}
 | ||
| //		return data, nil
 | ||
| //	}
 | ||
| //
 | ||
| //	// 缓存未命中,同步获取数据
 | ||
| //	return cms.loadAndCache(ctx, roleID)
 | ||
| //}
 | ||
| //
 | ||
| //// loadAndCache 加载数据并缓存
 | ||
| //func (cms *CachedMenuService) loadAndCache(ctx context.Context, roleID int32) (map[string]*dto.OwnerMenuDto, error) {
 | ||
| //	// 防止并发重复加载
 | ||
| //	cms.mu.Lock()
 | ||
| //	if cms.refreshing[roleID] {
 | ||
| //		cms.mu.Unlock()
 | ||
| //		// 如果正在加载,等待一小段时间后重试缓存
 | ||
| //		time.Sleep(time.Millisecond * 10)
 | ||
| //		if data, hit := cms.cache.Get(roleID); hit {
 | ||
| //			return data, nil
 | ||
| //		}
 | ||
| //		// 重试失败,继续执行加载逻辑
 | ||
| //		cms.mu.Lock()
 | ||
| //	}
 | ||
| //	cms.refreshing[roleID] = true
 | ||
| //	cms.mu.Unlock()
 | ||
| //
 | ||
| //	defer func() {
 | ||
| //		cms.mu.Lock()
 | ||
| //		delete(cms.refreshing, roleID)
 | ||
| //		cms.mu.Unlock()
 | ||
| //	}()
 | ||
| //
 | ||
| //	// 从原始服务获取数据
 | ||
| //	data, err := cms.menuService.ListByRoleIDToMap(ctx, roleID)
 | ||
| //	if err != nil {
 | ||
| //		return nil, err
 | ||
| //	}
 | ||
| //
 | ||
| //	// 缓存数据
 | ||
| //	version := time.Now().UnixNano()
 | ||
| //	cms.cache.Set(roleID, data, version)
 | ||
| //
 | ||
| //	return data, nil
 | ||
| //}
 | ||
| //
 | ||
| //// asyncRefresh 异步刷新缓存
 | ||
| //func (cms *CachedMenuService) asyncRefresh(ctx context.Context, roleID int32) {
 | ||
| //	// 使用背景上下文,避免原请求取消影响刷新
 | ||
| //	bgCtx := context.Background()
 | ||
| //
 | ||
| //	cms.mu.RLock()
 | ||
| //	if cms.refreshing[roleID] {
 | ||
| //		cms.mu.RUnlock()
 | ||
| //		return
 | ||
| //	}
 | ||
| //	cms.mu.RUnlock()
 | ||
| //
 | ||
| //	_, err := cms.loadAndCache(bgCtx, roleID)
 | ||
| //	if err != nil {
 | ||
| //		log.Printf("async refresh menu cache failed for roleID %d: %v", roleID, err)
 | ||
| //	}
 | ||
| //
 | ||
| //	cms.cache.stats.mu.Lock()
 | ||
| //	cms.cache.stats.RefreshCount++
 | ||
| //	cms.cache.stats.mu.Unlock()
 | ||
| //}
 | ||
| //
 | ||
| //// 全局缓存实例
 | ||
| //var (
 | ||
| //	menuCache     *MenuCache
 | ||
| //	cachedMenuSvc *CachedMenuService
 | ||
| //	cacheInitOnce sync.Once
 | ||
| //)
 | ||
| //
 | ||
| //// InitMenuCache 初始化菜单缓存(在应用启动时调用)
 | ||
| //func InitMenuCache(menuService v1.MenuService) {
 | ||
| //	cacheInitOnce.Do(func() {
 | ||
| //		// 配置参数:最大1000个角色的缓存,TTL 10分钟
 | ||
| //		menuCache = NewMenuCache(1000, time.Minute*10)
 | ||
| //		cachedMenuSvc = NewCachedMenuService(menuService, menuCache)
 | ||
| //	})
 | ||
| //}
 | ||
| //
 | ||
| //// GetCachedMenuService 获取缓存菜单服务实例
 | ||
| //func GetCachedMenuService() *CachedMenuService {
 | ||
| //	return cachedMenuSvc
 | ||
| //}
 | ||
| //
 | ||
| //// Authorize 修改后的授权中间件
 | ||
| //func Authorize(
 | ||
| //	sess session.Manager,
 | ||
| //	menuService v1.MenuService,
 | ||
| //) func(http.Handler) http.Handler {
 | ||
| //	// 初始化缓存
 | ||
| //	InitMenuCache(menuService)
 | ||
| //
 | ||
| //	return func(next http.Handler) http.Handler {
 | ||
| //		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | ||
| //			ctx := r.Context()
 | ||
| //			path := r.URL.Path
 | ||
| //
 | ||
| //			// 登陆检查
 | ||
| //			n := time.Now()
 | ||
| //			user, err := sess.GetUser(ctx, know.StoreName)
 | ||
| //			if err != nil || user.ID == 0 {
 | ||
| //				http.Redirect(w, r, "/", http.StatusFound)
 | ||
| //				return
 | ||
| //			}
 | ||
| //
 | ||
| //			log.Printf("scs get user: %s", time.Since(n).String())
 | ||
| //
 | ||
| //			// 公共路由放行
 | ||
| //			if publicRoutes[path] {
 | ||
| //				ctx = setUser(ctx, user)
 | ||
| //				next.ServeHTTP(w, r.WithContext(ctx))
 | ||
| //				return
 | ||
| //			}
 | ||
| //
 | ||
| //			n1 := time.Now()
 | ||
| //			// 权限检查 - 使用缓存服务
 | ||
| //			menus, err := GetCachedMenuService().ListByRoleIDToMap(ctx, user.RoleID)
 | ||
| //			if err != nil || !hasPermission(menus, path) {
 | ||
| //				http.Error(w, "Forbidden", http.StatusForbidden)
 | ||
| //				return
 | ||
| //			}
 | ||
| //
 | ||
| //			log.Printf("listByRoleIDToMap (cached): %s", time.Since(n1).String())
 | ||
| //
 | ||
| //			n2 := time.Now()
 | ||
| //			cur := getCurrentMenus(menus, path)
 | ||
| //			log.Printf("getCurrentMenus: %s", time.Since(n2).String())
 | ||
| //
 | ||
| //			ctx = setUser(ctx, user)
 | ||
| //			ctx = setCurMenus(ctx, cur)
 | ||
| //
 | ||
| //			next.ServeHTTP(w, r.WithContext(ctx))
 | ||
| //		})
 | ||
| //	}
 | ||
| //}
 | ||
| //
 | ||
| //func hasPermission(menus map[string]*dto.OwnerMenuDto, path string) bool {
 | ||
| //	_, ok := menus[path]
 | ||
| //	return ok
 | ||
| //}
 | ||
| //
 | ||
| //func getCurrentMenus(data map[string]*dto.OwnerMenuDto, path string) []dto.OwnerMenuDto {
 | ||
| //	var res []dto.OwnerMenuDto
 | ||
| //
 | ||
| //	menu, ok := data[path]
 | ||
| //	if !ok {
 | ||
| //		return res
 | ||
| //	}
 | ||
| //
 | ||
| //	for _, item := range data {
 | ||
| //		if menu.IsList {
 | ||
| //			if item.ParentID == menu.ID || item.ID == menu.ID {
 | ||
| //				res = append(res, *item)
 | ||
| //			}
 | ||
| //		} else {
 | ||
| //			if item.ParentID == menu.ParentID {
 | ||
| //				res = append(res, *item)
 | ||
| //			}
 | ||
| //		}
 | ||
| //	}
 | ||
| //
 | ||
| //	return res
 | ||
| //}
 |