516 lines
12 KiB
Go
516 lines
12 KiB
Go
package biz
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/guxuan/hailin_service/internal/config"
|
|
"github.com/guxuan/hailin_service/internal/mods/rbac/dal"
|
|
"github.com/guxuan/hailin_service/internal/mods/rbac/schema"
|
|
"github.com/guxuan/hailin_service/pkg/cachex"
|
|
"github.com/guxuan/hailin_service/pkg/encoding/json"
|
|
"github.com/guxuan/hailin_service/pkg/encoding/yaml"
|
|
"github.com/guxuan/hailin_service/pkg/errors"
|
|
"github.com/guxuan/hailin_service/pkg/logging"
|
|
"github.com/guxuan/hailin_service/pkg/util"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// Menu management for RBAC
|
|
type Menu struct {
|
|
Cache cachex.Cacher
|
|
Trans *util.Trans
|
|
MenuDAL *dal.Menu
|
|
MenuResourceDAL *dal.MenuResource
|
|
RoleMenuDAL *dal.RoleMenu
|
|
}
|
|
|
|
func (a *Menu) InitFromFile(ctx context.Context, menuFile string) error {
|
|
f, err := os.ReadFile(menuFile)
|
|
if err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
logging.Context(ctx).Warn("Menu data file not found, skip init menu data from file", zap.String("file", menuFile))
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
var menus schema.Menus
|
|
if ext := filepath.Ext(menuFile); ext == ".json" {
|
|
if err := json.Unmarshal(f, &menus); err != nil {
|
|
return errors.Wrapf(err, "Unmarshal JSON file '%s' failed", menuFile)
|
|
}
|
|
} else if ext == ".yaml" || ext == ".yml" {
|
|
if err := yaml.Unmarshal(f, &menus); err != nil {
|
|
return errors.Wrapf(err, "Unmarshal YAML file '%s' failed", menuFile)
|
|
}
|
|
} else {
|
|
return errors.Errorf("Unsupported file type '%s'", ext)
|
|
}
|
|
|
|
return a.Trans.Exec(ctx, func(ctx context.Context) error {
|
|
return a.createInBatchByParent(ctx, menus, nil)
|
|
})
|
|
}
|
|
|
|
func (a *Menu) createInBatchByParent(ctx context.Context, items schema.Menus, parent *schema.Menu) error {
|
|
total := len(items)
|
|
|
|
for i, item := range items {
|
|
var parentID string
|
|
if parent != nil {
|
|
parentID = parent.ID
|
|
}
|
|
|
|
var (
|
|
menuItem *schema.Menu
|
|
err error
|
|
)
|
|
|
|
if item.ID != "" {
|
|
menuItem, err = a.MenuDAL.Get(ctx, item.ID)
|
|
} else if item.Code != "" {
|
|
menuItem, err = a.MenuDAL.GetByCodeAndParentID(ctx, item.Code, parentID)
|
|
} else if item.Name != "" {
|
|
menuItem, err = a.MenuDAL.GetByNameAndParentID(ctx, item.Name, parentID)
|
|
}
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if item.Status == "" {
|
|
item.Status = schema.MenuStatusEnabled
|
|
}
|
|
|
|
if menuItem != nil {
|
|
changed := false
|
|
if menuItem.Name != item.Name {
|
|
menuItem.Name = item.Name
|
|
changed = true
|
|
}
|
|
if menuItem.Description != item.Description {
|
|
menuItem.Description = item.Description
|
|
changed = true
|
|
}
|
|
if menuItem.Path != item.Path {
|
|
menuItem.Path = item.Path
|
|
changed = true
|
|
}
|
|
if menuItem.Type != item.Type {
|
|
menuItem.Type = item.Type
|
|
changed = true
|
|
}
|
|
if menuItem.Sequence != item.Sequence {
|
|
menuItem.Sequence = item.Sequence
|
|
changed = true
|
|
}
|
|
if menuItem.Status != item.Status {
|
|
menuItem.Status = item.Status
|
|
changed = true
|
|
}
|
|
if changed {
|
|
menuItem.UpdatedAt = time.Now()
|
|
if err := a.MenuDAL.Update(ctx, menuItem); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
} else {
|
|
if item.ID == "" {
|
|
item.ID = util.NewXID()
|
|
}
|
|
if item.Sequence == 0 {
|
|
item.Sequence = total - i
|
|
}
|
|
item.ParentID = parentID
|
|
if parent != nil {
|
|
item.ParentPath = parent.ParentPath + parentID + util.TreePathDelimiter
|
|
}
|
|
menuItem = item
|
|
if err := a.MenuDAL.Create(ctx, item); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for _, res := range item.Resources {
|
|
if res.ID != "" {
|
|
exists, err := a.MenuResourceDAL.Exists(ctx, res.ID)
|
|
if err != nil {
|
|
return err
|
|
} else if exists {
|
|
continue
|
|
}
|
|
}
|
|
|
|
if res.Path != "" {
|
|
exists, err := a.MenuResourceDAL.ExistsMethodPathByMenuID(ctx, res.Method, res.Path, menuItem.ID)
|
|
if err != nil {
|
|
return err
|
|
} else if exists {
|
|
continue
|
|
}
|
|
}
|
|
if res.ID == "" {
|
|
res.ID = util.NewXID()
|
|
}
|
|
res.MenuID = menuItem.ID
|
|
if err := a.MenuResourceDAL.Create(ctx, res); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if item.Children != nil {
|
|
if err := a.createInBatchByParent(ctx, *item.Children, menuItem); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Query menus from the data access object based on the provided parameters and options.
|
|
func (a *Menu) Query(ctx context.Context, params schema.MenuQueryParam) (*schema.MenuQueryResult, error) {
|
|
params.Pagination = false
|
|
|
|
if err := a.fillQueryParam(ctx, ¶ms); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result, err := a.MenuDAL.Query(ctx, params, schema.MenuQueryOptions{
|
|
QueryOptions: util.QueryOptions{
|
|
OrderFields: schema.MenusOrderParams,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if params.LikeName != "" || params.CodePath != "" {
|
|
result.Data, err = a.appendChildren(ctx, result.Data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if params.IncludeResources {
|
|
for i, item := range result.Data {
|
|
resResult, err := a.MenuResourceDAL.Query(ctx, schema.MenuResourceQueryParam{
|
|
MenuID: item.ID,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result.Data[i].Resources = resResult.Data
|
|
}
|
|
}
|
|
|
|
result.Data = result.Data.ToTree()
|
|
return result, nil
|
|
}
|
|
|
|
func (a *Menu) fillQueryParam(ctx context.Context, params *schema.MenuQueryParam) error {
|
|
if params.CodePath != "" {
|
|
var (
|
|
codes []string
|
|
lastMenu schema.Menu
|
|
)
|
|
for _, code := range strings.Split(params.CodePath, util.TreePathDelimiter) {
|
|
if code == "" {
|
|
continue
|
|
}
|
|
codes = append(codes, code)
|
|
menu, err := a.MenuDAL.GetByCodeAndParentID(ctx, code, lastMenu.ParentID, schema.MenuQueryOptions{
|
|
QueryOptions: util.QueryOptions{
|
|
SelectFields: []string{"id", "parent_id", "parent_path"},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
} else if menu == nil {
|
|
return errors.NotFound("", "Menu not found by code '%s'", strings.Join(codes, util.TreePathDelimiter))
|
|
}
|
|
lastMenu = *menu
|
|
}
|
|
params.ParentPathPrefix = lastMenu.ParentPath + lastMenu.ID + util.TreePathDelimiter
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *Menu) appendChildren(ctx context.Context, data schema.Menus) (schema.Menus, error) {
|
|
if len(data) == 0 {
|
|
return data, nil
|
|
}
|
|
|
|
existsInData := func(id string) bool {
|
|
for _, item := range data {
|
|
if item.ID == id {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
for _, item := range data {
|
|
childResult, err := a.MenuDAL.Query(ctx, schema.MenuQueryParam{
|
|
ParentPathPrefix: item.ParentPath + item.ID + util.TreePathDelimiter,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, child := range childResult.Data {
|
|
if existsInData(child.ID) {
|
|
continue
|
|
}
|
|
data = append(data, child)
|
|
}
|
|
}
|
|
|
|
if parentIDs := data.SplitParentIDs(); len(parentIDs) > 0 {
|
|
parentResult, err := a.MenuDAL.Query(ctx, schema.MenuQueryParam{
|
|
InIDs: parentIDs,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, p := range parentResult.Data {
|
|
if existsInData(p.ID) {
|
|
continue
|
|
}
|
|
data = append(data, p)
|
|
}
|
|
}
|
|
sort.Sort(data)
|
|
|
|
return data, nil
|
|
}
|
|
|
|
// Get the specified menu from the data access object.
|
|
func (a *Menu) Get(ctx context.Context, id string) (*schema.Menu, error) {
|
|
menu, err := a.MenuDAL.Get(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
} else if menu == nil {
|
|
return nil, errors.NotFound("", "Menu not found")
|
|
}
|
|
|
|
menuResResult, err := a.MenuResourceDAL.Query(ctx, schema.MenuResourceQueryParam{
|
|
MenuID: menu.ID,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
menu.Resources = menuResResult.Data
|
|
|
|
return menu, nil
|
|
}
|
|
|
|
// Create a new menu in the data access object.
|
|
func (a *Menu) Create(ctx context.Context, formItem *schema.MenuForm) (*schema.Menu, error) {
|
|
if config.C.General.DenyOperateMenu {
|
|
return nil, errors.BadRequest("", "Menu creation is not allowed")
|
|
}
|
|
|
|
menu := &schema.Menu{
|
|
ID: util.NewXID(),
|
|
CreatedAt: time.Now(),
|
|
}
|
|
|
|
if parentID := formItem.ParentID; parentID != "" {
|
|
parent, err := a.MenuDAL.Get(ctx, parentID)
|
|
if err != nil {
|
|
return nil, err
|
|
} else if parent == nil {
|
|
return nil, errors.NotFound("", "Parent not found")
|
|
}
|
|
menu.ParentPath = parent.ParentPath + parent.ID + util.TreePathDelimiter
|
|
}
|
|
|
|
if exists, err := a.MenuDAL.ExistsCodeByParentID(ctx, formItem.Code, formItem.ParentID); err != nil {
|
|
return nil, err
|
|
} else if exists {
|
|
return nil, errors.BadRequest("", "Menu code already exists at the same level")
|
|
}
|
|
|
|
if err := formItem.FillTo(menu); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err := a.Trans.Exec(ctx, func(ctx context.Context) error {
|
|
if err := a.MenuDAL.Create(ctx, menu); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, res := range formItem.Resources {
|
|
res.ID = util.NewXID()
|
|
res.MenuID = menu.ID
|
|
res.CreatedAt = time.Now()
|
|
if err := a.MenuResourceDAL.Create(ctx, res); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return menu, nil
|
|
}
|
|
|
|
// Update the specified menu in the data access object.
|
|
func (a *Menu) Update(ctx context.Context, id string, formItem *schema.MenuForm) error {
|
|
if config.C.General.DenyOperateMenu {
|
|
return errors.BadRequest("", "Menu update is not allowed")
|
|
}
|
|
|
|
menu, err := a.MenuDAL.Get(ctx, id)
|
|
if err != nil {
|
|
return err
|
|
} else if menu == nil {
|
|
return errors.NotFound("", "Menu not found")
|
|
}
|
|
|
|
oldParentPath := menu.ParentPath
|
|
oldStatus := menu.Status
|
|
var childData schema.Menus
|
|
if menu.ParentID != formItem.ParentID {
|
|
if parentID := formItem.ParentID; parentID != "" {
|
|
parent, err := a.MenuDAL.Get(ctx, parentID)
|
|
if err != nil {
|
|
return err
|
|
} else if parent == nil {
|
|
return errors.NotFound("", "Parent not found")
|
|
}
|
|
menu.ParentPath = parent.ParentPath + parent.ID + util.TreePathDelimiter
|
|
} else {
|
|
menu.ParentPath = ""
|
|
}
|
|
|
|
childResult, err := a.MenuDAL.Query(ctx, schema.MenuQueryParam{
|
|
ParentPathPrefix: oldParentPath + menu.ID + util.TreePathDelimiter,
|
|
}, schema.MenuQueryOptions{
|
|
QueryOptions: util.QueryOptions{
|
|
SelectFields: []string{"id", "parent_path"},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
childData = childResult.Data
|
|
}
|
|
|
|
if menu.Code != formItem.Code {
|
|
if exists, err := a.MenuDAL.ExistsCodeByParentID(ctx, formItem.Code, formItem.ParentID); err != nil {
|
|
return err
|
|
} else if exists {
|
|
return errors.BadRequest("", "Menu code already exists at the same level")
|
|
}
|
|
}
|
|
|
|
if err := formItem.FillTo(menu); err != nil {
|
|
return err
|
|
}
|
|
|
|
return a.Trans.Exec(ctx, func(ctx context.Context) error {
|
|
if oldStatus != formItem.Status {
|
|
oldPath := oldParentPath + menu.ID + util.TreePathDelimiter
|
|
if err := a.MenuDAL.UpdateStatusByParentPath(ctx, oldPath, formItem.Status); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for _, child := range childData {
|
|
oldPath := oldParentPath + menu.ID + util.TreePathDelimiter
|
|
newPath := menu.ParentPath + menu.ID + util.TreePathDelimiter
|
|
err := a.MenuDAL.UpdateParentPath(ctx, child.ID, strings.Replace(child.ParentPath, oldPath, newPath, 1))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := a.MenuDAL.Update(ctx, menu); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := a.MenuResourceDAL.DeleteByMenuID(ctx, id); err != nil {
|
|
return err
|
|
}
|
|
for _, res := range formItem.Resources {
|
|
if res.ID == "" {
|
|
res.ID = util.NewXID()
|
|
}
|
|
res.MenuID = id
|
|
if res.CreatedAt.IsZero() {
|
|
res.CreatedAt = time.Now()
|
|
}
|
|
res.UpdatedAt = time.Now()
|
|
if err := a.MenuResourceDAL.Create(ctx, res); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return a.syncToCasbin(ctx)
|
|
})
|
|
}
|
|
|
|
// Delete the specified menu from the data access object.
|
|
func (a *Menu) Delete(ctx context.Context, id string) error {
|
|
if config.C.General.DenyOperateMenu {
|
|
return errors.BadRequest("", "Menu deletion is not allowed")
|
|
}
|
|
|
|
menu, err := a.MenuDAL.Get(ctx, id)
|
|
if err != nil {
|
|
return err
|
|
} else if menu == nil {
|
|
return errors.NotFound("", "Menu not found")
|
|
}
|
|
|
|
childResult, err := a.MenuDAL.Query(ctx, schema.MenuQueryParam{
|
|
ParentPathPrefix: menu.ParentPath + menu.ID + util.TreePathDelimiter,
|
|
}, schema.MenuQueryOptions{
|
|
QueryOptions: util.QueryOptions{
|
|
SelectFields: []string{"id"},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return a.Trans.Exec(ctx, func(ctx context.Context) error {
|
|
if err := a.delete(ctx, id); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, child := range childResult.Data {
|
|
if err := a.delete(ctx, child.ID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return a.syncToCasbin(ctx)
|
|
})
|
|
}
|
|
|
|
func (a *Menu) delete(ctx context.Context, id string) error {
|
|
if err := a.MenuDAL.Delete(ctx, id); err != nil {
|
|
return err
|
|
}
|
|
if err := a.MenuResourceDAL.DeleteByMenuID(ctx, id); err != nil {
|
|
return err
|
|
}
|
|
if err := a.RoleMenuDAL.DeleteByMenuID(ctx, id); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *Menu) syncToCasbin(ctx context.Context) error {
|
|
return a.Cache.Set(ctx, config.CacheNSForRole, config.CacheKeyForSyncToCasbin, fmt.Sprintf("%d", time.Now().Unix()))
|
|
}
|