2025-06-19 10:35:26 +08:00

516 lines
13 KiB
Go

package biz
import (
"context"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
"gitlab.guxuan.icu/jinshan_community/internal/config"
"gitlab.guxuan.icu/jinshan_community/internal/mods/rbac/dal"
"gitlab.guxuan.icu/jinshan_community/internal/mods/rbac/schema"
"gitlab.guxuan.icu/jinshan_community/pkg/cachex"
"gitlab.guxuan.icu/jinshan_community/pkg/encoding/json"
"gitlab.guxuan.icu/jinshan_community/pkg/encoding/yaml"
"gitlab.guxuan.icu/jinshan_community/pkg/errors"
"gitlab.guxuan.icu/jinshan_community/pkg/logging"
"gitlab.guxuan.icu/jinshan_community/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, &params); 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()))
}