394 lines
14 KiB
Go
394 lines
14 KiB
Go
package system
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"kra/internal/biz/system"
|
||
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
type menuRepo struct{ data Provider }
|
||
|
||
func NewMenuRepo(data Provider) system.MenuRepo { return &menuRepo{data: data} }
|
||
|
||
func menuFromPO(po menuPO) *system.Menu {
|
||
return &system.Menu{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: deletedAtPointer(po.DeletedAt), ID: po.ID, ParentID: po.ParentID, Path: po.Path, Name: po.Name, Hidden: po.Hidden, Component: po.Component, Sort: po.Sort, ActiveName: po.ActiveName, KeepAlive: po.KeepAlive, DefaultMenu: po.DefaultMenu, Title: po.Title, Icon: po.Icon, CloseTab: po.CloseTab, TransitionType: po.TransitionType}
|
||
}
|
||
|
||
func menuToPO(v *system.Menu) menuPO {
|
||
return menuPO{ID: v.ID, ParentID: v.ParentID, Path: v.Path, Name: v.Name, Hidden: v.Hidden, Component: v.Component, Sort: v.Sort, ActiveName: v.ActiveName, KeepAlive: v.KeepAlive, DefaultMenu: v.DefaultMenu, Title: v.Title, Icon: v.Icon, CloseTab: v.CloseTab, TransitionType: v.TransitionType}
|
||
}
|
||
|
||
func replaceMenuRelations(tx *gorm.DB, menu *system.Menu) error {
|
||
if err := tx.Unscoped().Where("sys_base_menu_id = ?", menu.ID).Delete(&menuParameterPO{}).Error; err != nil {
|
||
return err
|
||
}
|
||
if err := tx.Unscoped().Where("sys_base_menu_id = ?", menu.ID).Delete(&menuButtonPO{}).Error; err != nil {
|
||
return err
|
||
}
|
||
return createMenuRelations(tx, menu)
|
||
}
|
||
|
||
func createMenuRelations(tx *gorm.DB, menu *system.Menu) error {
|
||
parameters := make([]menuParameterPO, 0, len(menu.Parameters))
|
||
for _, parameter := range menu.Parameters {
|
||
parameters = append(parameters, menuParameterPO{ID: parameter.ID, MenuID: menu.ID, Type: parameter.Type, Key: parameter.Key, Value: parameter.Value})
|
||
}
|
||
if len(parameters) > 0 {
|
||
if err := tx.Create(¶meters).Error; err != nil {
|
||
return err
|
||
}
|
||
}
|
||
buttons := make([]menuButtonPO, 0, len(menu.Buttons))
|
||
for _, button := range menu.Buttons {
|
||
buttons = append(buttons, menuButtonPO{ID: button.ID, Name: button.Name, Description: button.Description, MenuID: menu.ID})
|
||
}
|
||
if len(buttons) > 0 {
|
||
return tx.Create(&buttons).Error
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (r *menuRepo) Create(ctx context.Context, v *system.Menu) error {
|
||
return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||
var count int64
|
||
if err := tx.Model(&menuPO{}).Where("name = ?", v.Name).Count(&count).Error; err != nil {
|
||
return err
|
||
}
|
||
if count > 0 {
|
||
return errors.New("存在重复name,请修改name")
|
||
}
|
||
if v.ParentID != 0 {
|
||
var parent menuPO
|
||
if err := tx.First(&parent, v.ParentID).Error; err != nil {
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return errors.New("父菜单不存在")
|
||
}
|
||
return err
|
||
}
|
||
if err := tx.Model(&menuPO{}).Where("parent_id = ?", v.ParentID).Count(&count).Error; err != nil {
|
||
return err
|
||
}
|
||
if count == 0 {
|
||
if err := tx.Model(&authorityPO{}).Where("default_router = ?", parent.Name).Count(&count).Error; err != nil {
|
||
return err
|
||
}
|
||
if count > 0 {
|
||
return errors.New("父菜单已被其他角色的首页占用,请先释放父菜单的首页权限")
|
||
}
|
||
if err := tx.Where("sys_base_menu_id = ?", v.ParentID).Delete(&authorityMenuPO{}).Error; err != nil {
|
||
return err
|
||
}
|
||
}
|
||
}
|
||
po := menuToPO(v)
|
||
po.ID = 0
|
||
if err := tx.Create(&po).Error; err != nil {
|
||
return err
|
||
}
|
||
v.ID = po.ID
|
||
return createMenuRelations(tx, v)
|
||
})
|
||
}
|
||
|
||
func (r *menuRepo) Update(ctx context.Context, v *system.Menu) error {
|
||
return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||
var old menuPO
|
||
if err := tx.First(&old, v.ID).Error; err != nil {
|
||
return err
|
||
}
|
||
var count int64
|
||
if old.Name != v.Name {
|
||
if err := tx.Model(&menuPO{}).Where("id <> ? AND name = ?", v.ID, v.Name).Count(&count).Error; err != nil {
|
||
return err
|
||
}
|
||
if count > 0 {
|
||
return errors.New("存在相同name修改失败")
|
||
}
|
||
}
|
||
if err := replaceMenuRelations(tx, v); err != nil {
|
||
return err
|
||
}
|
||
values := map[string]any{"parent_id": v.ParentID, "path": v.Path, "name": v.Name, "hidden": v.Hidden, "component": v.Component, "sort": v.Sort, "active_name": v.ActiveName, "keep_alive": v.KeepAlive, "default_menu": v.DefaultMenu, "title": v.Title, "icon": v.Icon, "close_tab": v.CloseTab, "transition_type": v.TransitionType}
|
||
return tx.Model(&old).Updates(values).Error
|
||
})
|
||
}
|
||
|
||
func (r *menuRepo) Delete(ctx context.Context, id uint) error {
|
||
return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||
var count int64
|
||
if err := tx.Model(&menuPO{}).Where("parent_id = ?", id).Count(&count).Error; err != nil {
|
||
return err
|
||
}
|
||
if count > 0 {
|
||
return errors.New("此菜单存在子菜单不可删除")
|
||
}
|
||
var menu menuPO
|
||
if err := tx.First(&menu, id).Error; err != nil {
|
||
return errors.New("记录不存在")
|
||
}
|
||
if err := tx.Model(&authorityPO{}).Where("default_router = ?", menu.Name).Count(&count).Error; err != nil {
|
||
return err
|
||
}
|
||
if count > 0 {
|
||
return errors.New("此菜单有角色正在作为首页,不可删除")
|
||
}
|
||
if err := tx.Delete(&menuPO{}, id).Error; err != nil {
|
||
return err
|
||
}
|
||
if err := tx.Where("sys_base_menu_id = ?", id).Delete(&menuParameterPO{}).Error; err != nil {
|
||
return err
|
||
}
|
||
if err := tx.Where("sys_base_menu_id = ?", id).Delete(&menuButtonPO{}).Error; err != nil {
|
||
return err
|
||
}
|
||
if err := tx.Where("sys_menu_id = ?", id).Delete(&authorityButtonPO{}).Error; err != nil {
|
||
return err
|
||
}
|
||
return tx.Where("sys_base_menu_id = ?", id).Delete(&authorityMenuPO{}).Error
|
||
})
|
||
}
|
||
|
||
func (r *menuRepo) loadRelations(ctx context.Context, menu *system.Menu) error {
|
||
var parameters []menuParameterPO
|
||
if err := r.data.DB().WithContext(ctx).Where("sys_base_menu_id = ?", menu.ID).Find(¶meters).Error; err != nil {
|
||
return err
|
||
}
|
||
menu.Parameters = make([]*system.MenuParameter, 0, len(parameters))
|
||
for _, parameter := range parameters {
|
||
menu.Parameters = append(menu.Parameters, menuParameterFromPO(parameter))
|
||
}
|
||
var buttons []menuButtonPO
|
||
if err := r.data.DB().WithContext(ctx).Where("sys_base_menu_id = ?", menu.ID).Find(&buttons).Error; err != nil {
|
||
return err
|
||
}
|
||
menu.Buttons = make([]*system.MenuButton, 0, len(buttons))
|
||
for _, button := range buttons {
|
||
menu.Buttons = append(menu.Buttons, menuButtonFromPO(button))
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (r *menuRepo) Find(ctx context.Context, id uint) (*system.Menu, error) {
|
||
var po menuPO
|
||
if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
menu := menuFromPO(po)
|
||
if err := r.loadRelations(ctx, menu); err != nil {
|
||
return nil, err
|
||
}
|
||
return menu, nil
|
||
}
|
||
|
||
func (r *menuRepo) List(ctx context.Context) ([]*system.Menu, error) {
|
||
var pos []menuPO
|
||
db := r.data.DB().WithContext(ctx).Model(&menuPO{}).Order("sort")
|
||
config := r.data.Runtime().Admin()
|
||
if actor, ok := system.ActorFromContext(ctx); ok && config != nil && config.System != nil && config.System.UseStrictAuth {
|
||
var authority authorityPO
|
||
if err := r.data.DB().WithContext(ctx).Where("authority_id = ?", actor.AuthorityID).First(&authority).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
if authority.ParentID != nil && *authority.ParentID != 0 {
|
||
db = db.Where("id IN (?)", r.data.DB().WithContext(ctx).Model(&authorityMenuPO{}).Select("sys_base_menu_id").Where("sys_authority_authority_id = ?", actor.AuthorityID))
|
||
}
|
||
}
|
||
if err := db.Find(&pos).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
out := make([]*system.Menu, 0, len(pos))
|
||
for _, po := range pos {
|
||
out = append(out, menuFromPO(po))
|
||
}
|
||
if len(out) == 0 {
|
||
return out, nil
|
||
}
|
||
byID := make(map[uint]*system.Menu, len(out))
|
||
ids := make([]uint, 0, len(out))
|
||
for _, menu := range out {
|
||
byID[menu.ID] = menu
|
||
ids = append(ids, menu.ID)
|
||
menu.Parameters = []*system.MenuParameter{}
|
||
menu.Buttons = []*system.MenuButton{}
|
||
}
|
||
var parameters []menuParameterPO
|
||
if err := r.data.DB().WithContext(ctx).Where("sys_base_menu_id IN ?", ids).Find(¶meters).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
for _, parameter := range parameters {
|
||
menu := byID[parameter.MenuID]
|
||
menu.Parameters = append(menu.Parameters, menuParameterFromPO(parameter))
|
||
}
|
||
var buttons []menuButtonPO
|
||
if err := r.data.DB().WithContext(ctx).Where("sys_base_menu_id IN ?", ids).Find(&buttons).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
for _, button := range buttons {
|
||
menu := byID[button.MenuID]
|
||
menu.Buttons = append(menu.Buttons, menuButtonFromPO(button))
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
func menuParameterFromPO(po menuParameterPO) *system.MenuParameter {
|
||
return &system.MenuParameter{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: deletedAtPointer(po.DeletedAt), ID: po.ID, MenuID: po.MenuID, Type: po.Type, Key: po.Key, Value: po.Value}
|
||
}
|
||
|
||
func menuButtonFromPO(po menuButtonPO) *system.MenuButton {
|
||
return &system.MenuButton{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: deletedAtPointer(po.DeletedAt), ID: po.ID, Name: po.Name, Description: po.Description, MenuID: po.MenuID}
|
||
}
|
||
|
||
func (r *menuRepo) SetAuthorityMenus(ctx context.Context, id uint, ids []uint) error {
|
||
if err := (&authorityAccessRepo{data: r.data}).checkAuthorityIDAuth(ctx, id); err != nil {
|
||
return err
|
||
}
|
||
if err := r.data.DB().WithContext(ctx).Where("authority_id = ?", id).First(&authorityPO{}).Error; err != nil {
|
||
return err
|
||
}
|
||
if err := r.checkMenuAssignmentAuth(ctx, ids); err != nil {
|
||
return err
|
||
}
|
||
return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||
if err := tx.Where("sys_authority_authority_id = ?", id).Delete(&authorityMenuPO{}).Error; err != nil {
|
||
return err
|
||
}
|
||
links := make([]authorityMenuPO, 0, len(ids))
|
||
for _, menuID := range ids {
|
||
links = append(links, authorityMenuPO{SysAuthorityAuthorityID: id, SysBaseMenuID: menuID})
|
||
}
|
||
if len(links) > 0 {
|
||
return tx.Create(&links).Error
|
||
}
|
||
return nil
|
||
})
|
||
}
|
||
|
||
// ListAuthorityMenus reads the authority-menu query. It reads the
|
||
// target authority's relation rows first and then fetches only those base
|
||
// menus. In particular, it deliberately does not call List: List applies the
|
||
// current request actor's strict-auth filter, which would incorrectly hide
|
||
// menus when an administrator inspects a different (child) authority.
|
||
func (r *menuRepo) ListAuthorityMenus(ctx context.Context, authorityID uint) ([]*system.Menu, error) {
|
||
var pos []menuPO
|
||
db := r.data.DB().WithContext(ctx).Model(&menuPO{}).
|
||
Where("id IN (?)", r.data.DB().WithContext(ctx).Model(&authorityMenuPO{}).
|
||
Select("sys_base_menu_id").Where("sys_authority_authority_id = ?", authorityID)).
|
||
Order("sort")
|
||
if err := db.Find(&pos).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
out := make([]*system.Menu, 0, len(pos))
|
||
for _, po := range pos {
|
||
// This endpoint does not preload Parameters or MenuBtn. Keep
|
||
// both relations nil so the response shape remains compatible.
|
||
out = append(out, menuFromPO(po))
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
func (r *menuRepo) MenuRoleIDs(ctx context.Context, id uint) ([]uint, error) {
|
||
var ids []uint
|
||
err := r.data.DB().WithContext(ctx).Model(&authorityMenuPO{}).Where("sys_base_menu_id = ?", id).Pluck("sys_authority_authority_id", &ids).Error
|
||
return ids, err
|
||
}
|
||
|
||
func (r *menuRepo) DefaultRouterRoleIDs(ctx context.Context, id uint) ([]uint, error) {
|
||
var menu menuPO
|
||
if err := r.data.DB().WithContext(ctx).First(&menu, id).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
var ids []uint
|
||
err := r.data.DB().WithContext(ctx).Model(&authorityPO{}).Where("default_router = ?", menu.Name).Pluck("authority_id", &ids).Error
|
||
return ids, err
|
||
}
|
||
|
||
func (r *menuRepo) SetMenuRoles(ctx context.Context, id uint, ids []uint) error {
|
||
access := &authorityAccessRepo{data: r.data}
|
||
_, allowedAuthorities, strict, err := access.strictAuthorityAccess(ctx)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if err := r.checkMenuAssignmentAuth(ctx, []uint{id}); err != nil {
|
||
return err
|
||
}
|
||
if strict {
|
||
for _, authorityID := range ids {
|
||
if !allowedAuthorities[authorityID] {
|
||
return errors.New("您提交的角色ID不合法")
|
||
}
|
||
}
|
||
}
|
||
return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||
if strict {
|
||
if len(allowedAuthorities) > 0 {
|
||
if err := tx.Where("sys_base_menu_id = ? AND sys_authority_authority_id IN ?", id, authorityIDs(allowedAuthorities)).Delete(&authorityMenuPO{}).Error; err != nil {
|
||
return err
|
||
}
|
||
}
|
||
} else if err := tx.Where("sys_base_menu_id = ?", id).Delete(&authorityMenuPO{}).Error; err != nil {
|
||
return err
|
||
}
|
||
links := make([]authorityMenuPO, 0, len(ids))
|
||
for _, authorityID := range ids {
|
||
links = append(links, authorityMenuPO{SysAuthorityAuthorityID: authorityID, SysBaseMenuID: id})
|
||
}
|
||
if len(links) > 0 {
|
||
return tx.Create(&links).Error
|
||
}
|
||
return nil
|
||
})
|
||
}
|
||
|
||
func authorityIDs(allowed map[uint]bool) []uint {
|
||
ids := make([]uint, 0, len(allowed))
|
||
for id := range allowed {
|
||
ids = append(ids, id)
|
||
}
|
||
return ids
|
||
}
|
||
|
||
func (r *menuRepo) checkMenuAssignmentAuth(ctx context.Context, menuIDs []uint) error {
|
||
actor, allowedAuthorities, strict, err := (&authorityAccessRepo{data: r.data}).strictAuthorityAccess(ctx)
|
||
if err != nil || !strict {
|
||
return err
|
||
}
|
||
return checkMenuAssignment(r.data.DB().WithContext(ctx), actor.AuthorityID, allowedAuthorities[actor.AuthorityID], menuIDs)
|
||
}
|
||
|
||
func checkMenuAssignment(db *gorm.DB, actorID uint, root bool, menuIDs []uint) error {
|
||
if len(menuIDs) == 0 {
|
||
return nil
|
||
}
|
||
unique := make(map[uint]struct{}, len(menuIDs))
|
||
for _, menuID := range menuIDs {
|
||
unique[menuID] = struct{}{}
|
||
}
|
||
if root {
|
||
var count int64
|
||
if err := db.Model(&menuPO{}).Where("id IN ?", menuIDs).Count(&count).Error; err != nil {
|
||
return err
|
||
}
|
||
if count != int64(len(unique)) {
|
||
return errors.New("添加失败,菜单不存在")
|
||
}
|
||
return nil
|
||
}
|
||
var allowedIDs []uint
|
||
if err := db.Model(&authorityMenuPO{}).
|
||
Where("sys_authority_authority_id = ?", actorID).Pluck("sys_base_menu_id", &allowedIDs).Error; err != nil {
|
||
return err
|
||
}
|
||
allowed := make(map[uint]bool, len(allowedIDs))
|
||
for _, menuID := range allowedIDs {
|
||
allowed[menuID] = true
|
||
}
|
||
for menuID := range unique {
|
||
if !allowed[menuID] {
|
||
return errors.New("添加失败,请勿跨级操作")
|
||
}
|
||
}
|
||
return nil
|
||
}
|