// Package rbac 提供后台 RBAC 的权限聚合、菜单树组装、缓存与种子初始化, // 以及管理员 / 角色 / 权限(接口、菜单)的 CRUD 业务。 // // 依赖方向:handler → 本包 service → repository/admin → model/admin。 package rbac import ( "context" "sort" "sync" "time" "portal-mini-go/internal/model/admin" "portal-mini-go/internal/pkg/auth" "portal-mini-go/internal/pkg/errors" adminrepo "portal-mini-go/internal/repository/admin" ) // UserRepository 是 Provider 与管理员业务所需的用户仓储接口(由 repository/admin 实现)。 type UserRepository interface { GetByUsername(ctx context.Context, username string) (*admin.User, error) GetByID(ctx context.Context, id uint64) (*admin.User, error) Create(ctx context.Context, u *admin.User) error Count(ctx context.Context) (int64, error) List(ctx context.Context, f adminrepo.UserListFilter) ([]admin.User, int64, error) Update(ctx context.Context, id uint64, cols map[string]interface{}) error Delete(ctx context.Context, id uint64) error SetRoles(ctx context.Context, adminID uint64, roleIDs []uint64) error RoleCodesOf(ctx context.Context, adminID uint64) ([]string, error) PermissionCodesOf(ctx context.Context, adminID uint64) ([]string, error) } // RoleRepository 是 Provider 与角色业务所需的角色仓储接口。 type RoleRepository interface { GetByID(ctx context.Context, id uint64) (*admin.Role, error) GetByCode(ctx context.Context, code string) (*admin.Role, error) Create(ctx context.Context, m *admin.Role) error List(ctx context.Context, f adminrepo.RoleListFilter) ([]admin.Role, int64, error) Update(ctx context.Context, id uint64, cols map[string]interface{}) error Delete(ctx context.Context, id uint64) error SetPermissions(ctx context.Context, roleID uint64, permIDs []uint64) error PermissionCodesOf(ctx context.Context, roleID uint64) ([]string, error) } // PermissionRepository 是 Provider 与权限业务所需的权限仓储接口。 type PermissionRepository interface { GetByID(ctx context.Context, id uint64) (*admin.Permission, error) Create(ctx context.Context, m *admin.Permission) error List(ctx context.Context, f adminrepo.PermListFilter) ([]admin.Permission, int64, error) Update(ctx context.Context, id uint64, cols map[string]interface{}) error Delete(ctx context.Context, id uint64) error AllMenuPermissions(ctx context.Context) ([]admin.Permission, error) AllCodes(ctx context.Context) ([]string, error) } // CacheInvalidator 用于写操作后失效权限缓存,由 Provider 实现并被各业务服务依赖。 type CacheInvalidator interface { Invalidate(ctx context.Context, adminID uint64) InvalidateAll(ctx context.Context) } // PermissionCache 是管理员权限集的缓存抽象。默认实现为内存版; // 生产环境若接入 Redis,可实现该接口替换(见 NewRedisCache 预留说明)。 type PermissionCache interface { Get(ctx context.Context, adminID uint64) (roles, perms []string, ok bool) Set(ctx context.Context, adminID uint64, roles, perms []string) Invalidate(ctx context.Context, adminID uint64) InvalidateAll(ctx context.Context) } // Provider 聚合管理员权限数据,实现 middleware.PermissionProvider。 type Provider struct { userRepo UserRepository roleRepo RoleRepository permRepo PermissionRepository cache PermissionCache } // NewProvider 构造权限 Provider。 func NewProvider( userRepo UserRepository, roleRepo RoleRepository, permRepo PermissionRepository, cache PermissionCache, ) *Provider { return &Provider{userRepo: userRepo, roleRepo: roleRepo, permRepo: permRepo, cache: cache} } // Load 返回管理员的角色与权限 code 集合,优先读缓存。 // 若含超级管理员角色,则权限集为全部权限 code(短路放行)。 func (p *Provider) Load(ctx context.Context, adminID uint64) (roles, perms []string, err error) { if r, pp, ok := p.cache.Get(ctx, adminID); ok { return r, pp, nil } roles, err = p.userRepo.RoleCodesOf(ctx, adminID) if err != nil { return nil, nil, err } if contains(roles, admin.SuperAdminRoleCode) { all, e := p.permRepo.AllCodes(ctx) if e != nil { return nil, nil, e } perms = all } else { perms, err = p.userRepo.PermissionCodesOf(ctx, adminID) if err != nil { return nil, nil, err } } p.cache.Set(ctx, adminID, roles, perms) return roles, perms, nil } // RolesOf 返回管理员角色 code 列表。 func (p *Provider) RolesOf(ctx context.Context, adminID uint64) ([]string, error) { roles, _, err := p.Load(ctx, adminID) return roles, err } // PermissionsOf 返回管理员权限 code 列表。 func (p *Provider) PermissionsOf(ctx context.Context, adminID uint64) ([]string, error) { _, perms, err := p.Load(ctx, adminID) return perms, err } // MenuTree 返回该管理员可见的菜单树(按权限过滤,超级管理员返回完整树)。 func (p *Provider) MenuTree(ctx context.Context, adminID uint64) ([]admin.MenuNode, error) { roles, perms, err := p.Load(ctx, adminID) if err != nil { return nil, err } menus, err := p.permRepo.AllMenuPermissions(ctx) if err != nil { return nil, err } allowed := make(map[string]struct{}, len(perms)) for _, c := range perms { allowed[c] = struct{}{} } return buildMenuTree(menus, contains(roles, admin.SuperAdminRoleCode), allowed), nil } // Invalidate 失效指定管理员的权限缓存。 func (p *Provider) Invalidate(ctx context.Context, adminID uint64) { p.cache.Invalidate(ctx, adminID) } // InvalidateAll 失效全部管理员的权限缓存(角色 / 权限变更后调用)。 func (p *Provider) InvalidateAll(ctx context.Context) { p.cache.InvalidateAll(ctx) } // Seed 初始化超级管理员角色与首个管理员账号(若表为空)。 // defaultUser / defaultPwd 来自环境变量,缺失时使用安全默认值。 func (p *Provider) Seed(ctx context.Context, defaultUser, defaultPwd string) error { if _, err := p.roleRepo.GetByCode(ctx, admin.SuperAdminRoleCode); err != nil { if !errors.Is(err, errors.ErrNotFound) { return err } if err := p.roleRepo.Create(ctx, &admin.Role{ Code: admin.SuperAdminRoleCode, Name: "超级管理员", Status: admin.UserStatusEnabled, }); err != nil { return err } } n, err := p.userRepo.Count(ctx) if err != nil { return err } if n > 0 { return nil } hash, err := auth.HashPassword(defaultPwd) if err != nil { return errors.Wrap(50000, "hash seed password", err) } u := &admin.User{ Username: defaultUser, PasswordHash: hash, Nickname: "超级管理员", Status: admin.UserStatusEnabled, } if err := p.userRepo.Create(ctx, u); err != nil { return err } role, err := p.roleRepo.GetByCode(ctx, admin.SuperAdminRoleCode) if err != nil { return err } return p.userRepo.SetRoles(ctx, u.ID, []uint64{role.ID}) } // buildMenuTree 依据菜单权限列表与管理员可见集合组装树。 // 节点被保留的条件:自身 code 在 allowed 中(超级管理员恒为真),或存在被保留的后代。 func buildMenuTree(menus []admin.Permission, isSuper bool, allowed map[string]struct{}) []admin.MenuNode { byID := make(map[uint64]admin.Permission, len(menus)) childrenOf := make(map[uint64][]uint64) for _, m := range menus { byID[m.ID] = m childrenOf[m.ParentID] = append(childrenOf[m.ParentID], m.ID) } kept := make(map[uint64]bool, len(menus)) var mark func(id uint64) bool mark = func(id uint64) bool { if v, ok := kept[id]; ok { return v } p := byID[id] self := isSuper if !isSuper { if _, ok := allowed[p.Code]; ok { self = true } } childKept := false for _, cid := range childrenOf[id] { if mark(cid) { childKept = true } } keep := self || childKept kept[id] = keep return keep } for id := range byID { mark(id) } var build func(parentID uint64) []admin.MenuNode build = func(parentID uint64) []admin.MenuNode { ids := childrenOf[parentID] out := make([]admin.MenuNode, 0, len(ids)) for _, id := range ids { if !kept[id] { continue } node := admin.MenuNode{Permission: byID[id]} node.Children = build(id) out = append(out, node) } sort.Slice(out, func(i, j int) bool { return out[i].Sort < out[j].Sort }) return out } return build(0) } func contains(s []string, target string) bool { for _, v := range s { if v == target { return true } } return false } // MemoryCache 进程内权限缓存,适用于无 Redis 的本地 / 测试环境。 // 多实例部署时各进程不共享,仅供单实例使用;生产可替换为 RedisCache。 type MemoryCache struct { mu sync.RWMutex data map[uint64]memItem ttl time.Duration now func() time.Time } type memItem struct { roles []string perms []string expireAt time.Time } // NewMemoryCache 创建内存权限缓存,ttl 为兜底过期时间(0 表示永不过期,仅靠失效)。 func NewMemoryCache(ttl time.Duration) *MemoryCache { return &MemoryCache{data: make(map[uint64]memItem), ttl: ttl, now: time.Now} } // Get 读取缓存的权限集。 func (m *MemoryCache) Get(_ context.Context, adminID uint64) (roles, perms []string, ok bool) { m.mu.RLock() e, found := m.data[adminID] m.mu.RUnlock() if !found { return nil, nil, false } if m.ttl > 0 && m.now().After(e.expireAt) { m.mu.Lock() delete(m.data, adminID) m.mu.Unlock() return nil, nil, false } return e.roles, e.perms, true } // Set 写入权限集。 func (m *MemoryCache) Set(_ context.Context, adminID uint64, roles, perms []string) { m.mu.Lock() defer m.mu.Unlock() e := memItem{roles: roles, perms: perms} if m.ttl > 0 { e.expireAt = m.now().Add(m.ttl) } m.data[adminID] = e } // Invalidate 失效单个管理员缓存。 func (m *MemoryCache) Invalidate(_ context.Context, adminID uint64) { m.mu.Lock() delete(m.data, adminID) m.mu.Unlock() } // InvalidateAll 清空全部缓存。 func (m *MemoryCache) InvalidateAll(_ context.Context) { m.mu.Lock() m.data = make(map[uint64]memItem) m.mu.Unlock() } // NewRedisCache 预留:生产环境接入 Redis 时实现此构造, // 满足 PermissionCache 接口即可在 NewProvider 处替换 MemoryCache。 // 当前未启用(项目暂未引入 go-redis 依赖),保留接口以明确扩展点。 func NewRedisCache(_ interface{}) PermissionCache { // TODO: 接入 go-redis 后实现 Get/Set/Invalidate/InvalidateAll。 return nil }