kra-oa/app/system/internal/data/repository/menu.go

416 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package system
import (
"context"
"errors"
"time"
"kra/app/system/internal/biz"
"gorm.io/gorm"
)
type menuRepo struct{ data Provider }
func NewMenuRepo(data Provider) biz.MenuRepo { return &menuRepo{data: data} }
func menuFromPO(po menuPO) *biz.Menu {
var deletedAt *time.Time
if po.DeletedAt.Valid {
value := po.DeletedAt.Time
deletedAt = &value
}
return &biz.Menu{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: 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 *biz.Menu) menuPO {
return menuPO{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, 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 *biz.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 *biz.Menu) error {
parameters := make([]menuParameterPO, 0, len(menu.Parameters))
for _, parameter := range menu.Parameters {
parameters = append(parameters, menuParameterPO{ID: parameter.ID, CreatedAt: parameter.CreatedAt, UpdatedAt: parameter.UpdatedAt, MenuID: menu.ID, Type: parameter.Type, Key: parameter.Key, Value: parameter.Value})
}
if len(parameters) > 0 {
if err := tx.Create(&parameters).Error; err != nil {
return err
}
}
buttons := make([]menuButtonPO, 0, len(menu.Buttons))
for _, button := range menu.Buttons {
buttons = append(buttons, menuButtonPO{ID: button.ID, CreatedAt: button.CreatedAt, UpdatedAt: button.UpdatedAt, 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 *biz.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)
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 *biz.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 *biz.Menu) error {
var parameters []menuParameterPO
if err := r.data.DB().WithContext(ctx).Where("sys_base_menu_id = ?", menu.ID).Find(&parameters).Error; err != nil {
return err
}
menu.Parameters = make([]*biz.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([]*biz.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) (*biz.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) ([]*biz.Menu, error) {
var pos []menuPO
db := r.data.DB().WithContext(ctx).Model(&menuPO{}).Order("sort")
config := r.data.Runtime().Admin()
if actor, ok := biz.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([]*biz.Menu, 0, len(pos))
for _, po := range pos {
out = append(out, menuFromPO(po))
}
if len(out) == 0 {
return out, nil
}
byID := make(map[uint]*biz.Menu, len(out))
ids := make([]uint, 0, len(out))
for _, menu := range out {
byID[menu.ID] = menu
ids = append(ids, menu.ID)
menu.Parameters = []*biz.MenuParameter{}
menu.Buttons = []*biz.MenuButton{}
}
var parameters []menuParameterPO
if err := r.data.DB().WithContext(ctx).Where("sys_base_menu_id IN ?", ids).Find(&parameters).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) *biz.MenuParameter {
var deletedAt *time.Time
if po.DeletedAt.Valid {
value := po.DeletedAt.Time
deletedAt = &value
}
return &biz.MenuParameter{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: deletedAt, ID: po.ID, MenuID: po.MenuID, Type: po.Type, Key: po.Key, Value: po.Value}
}
func menuButtonFromPO(po menuButtonPO) *biz.MenuButton {
var deletedAt *time.Time
if po.DeletedAt.Valid {
value := po.DeletedAt.Time
deletedAt = &value
}
return &biz.MenuButton{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: 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
})
}
func (r *menuRepo) AuthorityMenuIDs(ctx context.Context, id uint) ([]uint, error) {
var ids []uint
err := r.data.DB().WithContext(ctx).Model(&authorityMenuPO{}).Where("sys_authority_authority_id = ?", id).Pluck("sys_base_menu_id", &ids).Error
return ids, err
}
// 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) ([]*biz.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([]*biz.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
}