320 lines
7.7 KiB
Go
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
|
|
}
|