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 }