feat(goauto): add configurable module menus (#137)
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
package version_local
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"go-admin/app/goauto/access"
|
||||
"go-admin/cmd/migrate/migration"
|
||||
migrationmodels "go-admin/cmd/migrate/migration/models"
|
||||
common "go-admin/common/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func init() {
|
||||
_, fileName, _, _ := runtime.Caller(0)
|
||||
migration.Migrate.SetVersion(migration.GetFilename(fileName), migrateGoAutoMenus)
|
||||
}
|
||||
|
||||
func migrateGoAutoMenus(db *gorm.DB, version string) error {
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := ensureGoAutoMenusAndPurchaserBindings(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Create(&common.Migration{Version: version}).Error
|
||||
})
|
||||
}
|
||||
|
||||
func ensureGoAutoMenusAndPurchaserBindings(db *gorm.DB) error {
|
||||
var existingRoleCount int64
|
||||
if err := db.Model(&migrationmodels.SysRole{}).
|
||||
Where("role_key = ?", access.RolePurchaser).Count(&existingRoleCount).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ensurePurchaserRoleAndPolicies(db); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var purchaser migrationmodels.SysRole
|
||||
if err := db.Where("role_key = ?", access.RolePurchaser).First(&purchaser).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, module := range access.GoAutoModules() {
|
||||
parent, _, err := upsertGoAutoMenu(db, migrationmodels.SysMenu{
|
||||
MenuName: module.RouteName + "Root",
|
||||
Title: module.Title,
|
||||
Icon: module.Icon,
|
||||
Path: module.Path,
|
||||
MenuType: "M",
|
||||
Action: "无",
|
||||
ParentId: 0,
|
||||
Component: "Layout",
|
||||
Sort: module.Sort,
|
||||
Visible: "0",
|
||||
IsFrame: "1",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := db.Model(&parent).Update("paths", fmt.Sprintf("/0/%d", parent.MenuId)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
child, childCreated, err := upsertGoAutoMenu(db, migrationmodels.SysMenu{
|
||||
MenuName: module.RouteName,
|
||||
Title: module.Title,
|
||||
Icon: module.Icon,
|
||||
Path: module.Path + "/index",
|
||||
MenuType: "C",
|
||||
Action: "无",
|
||||
ParentId: parent.MenuId,
|
||||
Component: module.Component,
|
||||
Sort: 1,
|
||||
Visible: "0",
|
||||
IsFrame: "1",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := db.Model(&child).Update("paths", fmt.Sprintf("/0/%d/%d", parent.MenuId, child.MenuId)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apis := make([]migrationmodels.SysApi, 0, len(module.APIs))
|
||||
for _, permission := range module.APIs {
|
||||
api := migrationmodels.SysApi{}
|
||||
if err := db.Where("path = ? AND action = ?", permission.Path, permission.Method).
|
||||
Assign(migrationmodels.SysApi{Title: permission.Title, Type: "BUS"}).
|
||||
FirstOrCreate(&api).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
apis = append(apis, api)
|
||||
}
|
||||
if err := db.Model(&child).Association("SysApi").Replace(apis); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if module.PurchaserHardHidden {
|
||||
if err := db.Model(&purchaser).Association("SysMenu").Delete(&parent, &child); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if module.PurchaserDefault && (existingRoleCount == 0 || childCreated) {
|
||||
if err := db.Model(&purchaser).Association("SysMenu").Append(&child); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func upsertGoAutoMenu(db *gorm.DB, desired migrationmodels.SysMenu) (migrationmodels.SysMenu, bool, error) {
|
||||
var menu migrationmodels.SysMenu
|
||||
err := db.Unscoped().Where("menu_name = ?", desired.MenuName).First(&menu).Error
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return menu, false, err
|
||||
}
|
||||
created := err == gorm.ErrRecordNotFound
|
||||
if created {
|
||||
menu = desired
|
||||
return menu, true, db.Create(&menu).Error
|
||||
}
|
||||
|
||||
desired.MenuId = menu.MenuId
|
||||
desired.CreatedAt = menu.CreatedAt
|
||||
return desired, false, db.Unscoped().Save(&desired).Error
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package version_local
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"go-admin/app/goauto/access"
|
||||
migrationmodels "go-admin/cmd/migrate/migration/models"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestEnsureGoAutoMenusAndPurchaserBindings(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = db.AutoMigrate(
|
||||
&migrationmodels.SysRole{}, &migrationmodels.SysMenu{}, &migrationmodels.SysApi{},
|
||||
&purchaserCasbinRule{},
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
custom := migrationmodels.SysMenu{MenuName: "UserCustomMenu", Title: "用户自定义菜单", Path: "/custom"}
|
||||
if err = db.Create(&custom).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
staleAIGrant := purchaserCasbinRule{
|
||||
Ptype: "p", V0: access.RolePurchaser,
|
||||
V1: "/api/admin/v1/ai-matching-settings", V2: "GET",
|
||||
}
|
||||
if err = db.Create(&staleAIGrant).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = ensureGoAutoMenusAndPurchaserBindings(db); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertGoAutoMenuState(t, db)
|
||||
var customAfter migrationmodels.SysMenu
|
||||
if err = db.First(&customAfter, custom.MenuId).Error; err != nil {
|
||||
t.Fatal("migration must not remove a user menu:", err)
|
||||
}
|
||||
if customAfter.Title != custom.Title || customAfter.Path != custom.Path {
|
||||
t.Fatalf("migration changed a user menu: %#v", customAfter)
|
||||
}
|
||||
var readGrantCount int64
|
||||
if err = db.Model(&purchaserCasbinRule{}).
|
||||
Where("v0 = ? AND v1 = ? AND v2 = ?", access.RolePurchaser, "/api/admin/v1/ai-matching-settings", "GET").
|
||||
Count(&readGrantCount).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if readGrantCount != 1 {
|
||||
t.Fatal("purchaser AI enabled-status read policy must remain unchanged")
|
||||
}
|
||||
var forbiddenAIGrantCount int64
|
||||
if err = db.Model(&purchaserCasbinRule{}).
|
||||
Where("v0 = ? AND v1 LIKE ? AND v2 <> ?", access.RolePurchaser, "/api/admin/v1/ai-matching-settings%", "GET").
|
||||
Count(&forbiddenAIGrantCount).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if forbiddenAIGrantCount != 0 {
|
||||
t.Fatal("purchaser must not gain AI settings write/test policies")
|
||||
}
|
||||
|
||||
var purchaser migrationmodels.SysRole
|
||||
if err = db.Where("role_key = ?", access.RolePurchaser).First(&purchaser).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var devices migrationmodels.SysMenu
|
||||
if err = db.Where("menu_name = ?", "GoAutoDeviceList").First(&devices).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = db.Model(&purchaser).Association("SysMenu").Delete(&devices); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var aiParent, aiChild migrationmodels.SysMenu
|
||||
if err = db.Where("menu_name = ?", "GoAutoAiMatchingSettingsRoot").First(&aiParent).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = db.Where("menu_name = ?", "GoAutoAiMatchingSettings").First(&aiChild).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = db.Model(&purchaser).Association("SysMenu").Append(&aiParent, &aiChild); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err = ensureGoAutoMenusAndPurchaserBindings(db); err != nil {
|
||||
t.Fatalf("reconcile must be repeatable: %v", err)
|
||||
}
|
||||
assertRoleHasMenu(t, db, purchaser.RoleId, devices.MenuId, false)
|
||||
assertRoleHasMenu(t, db, purchaser.RoleId, aiParent.MenuId, false)
|
||||
assertRoleHasMenu(t, db, purchaser.RoleId, aiChild.MenuId, false)
|
||||
assertGoAutoMenuState(t, db)
|
||||
|
||||
// A genuinely new default module is added once. Deleting one code-owned
|
||||
// menu simulates introducing that module into an existing deployment.
|
||||
if err = db.Unscoped().Delete(&devices).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = ensureGoAutoMenusAndPurchaserBindings(db); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
devices = migrationmodels.SysMenu{}
|
||||
if err = db.Where("menu_name = ?", "GoAutoDeviceList").First(&devices).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertRoleHasMenu(t, db, purchaser.RoleId, devices.MenuId, true)
|
||||
}
|
||||
|
||||
func assertGoAutoMenuState(t *testing.T, db *gorm.DB) {
|
||||
t.Helper()
|
||||
var count int64
|
||||
if err := db.Model(&migrationmodels.SysMenu{}).Where("menu_name LIKE ?", "GoAuto%").Count(&count).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if count != int64(len(access.GoAutoModules())*2) {
|
||||
t.Fatalf("got %d GoAuto menu rows, want %d", count, len(access.GoAutoModules())*2)
|
||||
}
|
||||
for _, module := range access.GoAutoModules() {
|
||||
var child migrationmodels.SysMenu
|
||||
if err := db.Preload("SysApi").Where("menu_name = ?", module.RouteName).First(&child).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if child.Path != module.Path+"/index" || child.Component != module.Component || child.ParentId == 0 {
|
||||
t.Fatalf("unexpected menu for %s: %#v", module.Key, child)
|
||||
}
|
||||
if len(child.SysApi) != len(module.APIs) {
|
||||
t.Fatalf("%s has %d APIs, want %d", module.Key, len(child.SysApi), len(module.APIs))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertRoleHasMenu(t *testing.T, db *gorm.DB, roleID, menuID int, want bool) {
|
||||
t.Helper()
|
||||
var count int64
|
||||
if err := db.Table("sys_role_menu").Where("role_id = ? AND menu_id = ?", roleID, menuID).Count(&count).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if (count > 0) != want {
|
||||
t.Fatalf("role %d menu %d present=%v, want %v", roleID, menuID, count > 0, want)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user