Files
smart-go/internal/iam/service/menu_service.go
T
2026-04-23 18:58:13 +08:00

320 lines
7.7 KiB
Go

package service
import (
"context"
"errors"
"fmt"
"sort"
"giter.top/smart/internal/iam/entity"
"giter.top/smart/internal/iam/repository"
"giter.top/smart/pkg/utils/id"
)
// MenuService 菜单(全局资源)
type MenuService interface {
Create(ctx context.Context, req *CreateMenuRequest, isPlatform bool) (*entity.Menu, error)
Update(ctx context.Context, mid string, req *UpdateMenuRequest, isPlatform bool) (*entity.Menu, error)
Delete(ctx context.Context, ids []string, isPlatform bool) error
Get(ctx context.Context, mid string) (*entity.Menu, error)
Tree(ctx context.Context, menuType *int16) ([]MenuNode, error)
NavForUser(ctx context.Context, userID string) ([]MenuNode, error)
PermsForUser(ctx context.Context, userID string) ([]string, error)
}
type CreateMenuRequest struct {
ParentID string `json:"parent_id"`
MenuName string `json:"menu_name" binding:"required,max=128"`
MenuType int16 `json:"menu_type" binding:"required"`
Perms string `json:"perms"`
Path string `json:"path"`
Component string `json:"component"`
Icon string `json:"icon"`
SortOrder int `json:"sort_order"`
IsVisible bool `json:"is_visible"`
IsBuiltin bool `json:"is_builtin"`
ExternalLink string `json:"external_link"`
}
type UpdateMenuRequest struct {
ParentID *string `json:"parent_id"`
MenuName *string `json:"menu_name"`
SortOrder *int `json:"sort_order"`
IsVisible *bool `json:"is_visible"`
Path *string `json:"path"`
Component *string `json:"component"`
Icon *string `json:"icon"`
ExternalLink *string `json:"external_link"`
Status *int16 `json:"status"`
}
// MenuNode 菜单树节点
type MenuNode struct {
entity.Menu
Children []MenuNode `json:"children,omitempty"`
}
type menuService struct {
menus repository.MenuRepository
roles repository.RoleRepository
users repository.UserRepository
}
func NewMenuService(menus repository.MenuRepository, roles repository.RoleRepository, users repository.UserRepository) MenuService {
return &menuService{menus: menus, roles: roles, users: users}
}
func normalizeMenuParent(pid string) string {
if pid == "0" {
return ""
}
return pid
}
func (s *menuService) Create(ctx context.Context, req *CreateMenuRequest, isPlatform bool) (*entity.Menu, error) {
if !isPlatform {
return nil, repository.ErrForbidden
}
if req.Perms != "" {
ok, err := s.menus.ExistsPerms(ctx, req.Perms, "")
if err != nil {
return nil, err
}
if ok {
return nil, fmt.Errorf("权限标识已存在")
}
}
m := &entity.Menu{
ID: id.New(),
ParentID: normalizeMenuParent(req.ParentID),
MenuName: req.MenuName,
MenuType: req.MenuType,
Perms: req.Perms,
Path: req.Path,
Component: req.Component,
Icon: req.Icon,
SortOrder: req.SortOrder,
IsVisible: req.IsVisible,
IsBuiltin: req.IsBuiltin,
ExternalLink: req.ExternalLink,
Status: 1,
}
if err := s.menus.Create(ctx, m); err != nil {
return nil, err
}
return m, nil
}
func (s *menuService) Update(ctx context.Context, mid string, req *UpdateMenuRequest, isPlatform bool) (*entity.Menu, error) {
if !isPlatform {
return nil, repository.ErrForbidden
}
m, err := s.menus.GetByID(ctx, mid)
if err != nil {
if errors.Is(err, repository.ErrNotFound) {
return nil, fmt.Errorf("菜单不存在")
}
return nil, err
}
if m.IsBuiltin {
return nil, fmt.Errorf("系统内置菜单禁止修改")
}
if req.MenuName != nil {
m.MenuName = *req.MenuName
}
if req.SortOrder != nil {
m.SortOrder = *req.SortOrder
}
if req.IsVisible != nil {
m.IsVisible = *req.IsVisible
}
if req.Path != nil {
m.Path = *req.Path
}
if req.Component != nil {
m.Component = *req.Component
}
if req.Icon != nil {
m.Icon = *req.Icon
}
if req.ExternalLink != nil {
m.ExternalLink = *req.ExternalLink
}
if req.Status != nil {
m.Status = *req.Status
}
if err := s.menus.Update(ctx, m); err != nil {
return nil, err
}
return m, nil
}
func (s *menuService) Delete(ctx context.Context, ids []string, isPlatform bool) error {
if !isPlatform {
return repository.ErrForbidden
}
for _, mid := range ids {
m, err := s.menus.GetByID(ctx, mid)
if err != nil {
return err
}
if m.IsBuiltin {
return fmt.Errorf("系统内置菜单禁止删除")
}
n, err := s.menus.CountChildren(ctx, mid)
if err != nil {
return err
}
if n > 0 {
return fmt.Errorf("存在子菜单,无法删除")
}
rn, err := s.menus.CountRoleRefs(ctx, mid)
if err != nil {
return err
}
if rn > 0 {
return fmt.Errorf("菜单仍被角色引用")
}
if err := s.menus.Delete(ctx, mid); err != nil {
return err
}
}
return nil
}
func (s *menuService) Get(ctx context.Context, mid string) (*entity.Menu, error) {
return s.menus.GetByID(ctx, mid)
}
func (s *menuService) Tree(ctx context.Context, menuType *int16) ([]MenuNode, error) {
rows, err := s.menus.ListByType(ctx, menuType)
if err != nil {
return nil, err
}
return buildMenuTreeRows(rows), nil
}
func buildMenuTreeRows(rows []entity.Menu) []MenuNode {
byParent := map[string][]entity.Menu{}
for _, m := range rows {
pid := normalizeMenuParent(m.ParentID)
byParent[pid] = append(byParent[pid], m)
}
for k := range byParent {
sort.Slice(byParent[k], func(i, j int) bool {
if byParent[k][i].SortOrder != byParent[k][j].SortOrder {
return byParent[k][i].SortOrder < byParent[k][j].SortOrder
}
return byParent[k][i].ID < byParent[k][j].ID
})
}
var walk func(pid string) []MenuNode
walk = func(pid string) []MenuNode {
list := byParent[pid]
out := make([]MenuNode, 0, len(list))
for _, m := range list {
out = append(out, MenuNode{Menu: m, Children: walk(m.ID)})
}
return out
}
return walk("")
}
func (s *menuService) NavForUser(ctx context.Context, userID string) ([]MenuNode, error) {
rids, err := s.users.ListRoleIDs(ctx, userID)
if err != nil {
return nil, err
}
menuIDs, err := s.roles.ListMenuIDsByRoles(ctx, rids)
if err != nil {
return nil, err
}
allowed := map[string]struct{}{}
for _, mid := range menuIDs {
allowed[mid] = struct{}{}
}
pub, err := s.menus.ListByPerms(ctx, entity.PublicOverviewPerms)
if err != nil {
return nil, err
}
for _, m := range pub {
allowed[m.ID] = struct{}{}
}
all, err := s.menus.ListAll(ctx)
if err != nil {
return nil, err
}
byID := map[string]entity.Menu{}
for _, m := range all {
byID[m.ID] = m
}
for _, m := range all {
if _, ok := allowed[m.ID]; !ok {
continue
}
cur := m
for {
pid := normalizeMenuParent(cur.ParentID)
if pid == "" {
break
}
p, ok := byID[pid]
if !ok {
break
}
allowed[p.ID] = struct{}{}
cur = p
}
}
filtered := make([]entity.Menu, 0)
for _, m := range all {
if _, ok := allowed[m.ID]; ok && m.Status == 1 && m.IsVisible {
filtered = append(filtered, m)
}
}
tree := buildMenuTreeRows(filtered)
return pruneEmptyDirs(tree), nil
}
func pruneEmptyDirs(nodes []MenuNode) []MenuNode {
out := make([]MenuNode, 0, len(nodes))
for _, n := range nodes {
ch := pruneEmptyDirs(n.Children)
if n.MenuType == 1 && len(ch) == 0 {
continue
}
n.Children = ch
out = append(out, n)
}
return out
}
func (s *menuService) PermsForUser(ctx context.Context, userID string) ([]string, error) {
rids, err := s.users.ListRoleIDs(ctx, userID)
if err != nil {
return nil, err
}
mids, err := s.roles.ListMenuIDsByRoles(ctx, rids)
if err != nil {
return nil, err
}
all, err := s.menus.ListAll(ctx)
if err != nil {
return nil, err
}
idset := map[string]struct{}{}
for _, mid := range mids {
idset[mid] = struct{}{}
}
var perms []string
for _, m := range all {
if _, ok := idset[m.ID]; !ok {
continue
}
if m.Perms != "" {
perms = append(perms, m.Perms)
}
}
return perms, nil
}