From 6d6b1c00c411d8839ef0af19e407a730a74fc582 Mon Sep 17 00:00:00 2001 From: skyle1995 Date: Fri, 17 Apr 2026 04:54:54 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20=E5=AF=BC=E8=88=AA?= =?UTF-8?q?=E6=A0=8F=E9=87=8D=E5=A4=8D=E5=88=9D=E5=A7=8B=E5=8C=96=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.sh | 4 +- cmd/server.go | 24 +++++++++--- controllers/admin/portal_navigation.go | 53 +++++++++++++++++++++++--- database/portal_navigation.go | 50 ++++++++++++++++++++++++ models/portal_navigation.go | 2 + services/portal_navigation.go | 31 ++++++++++++++- 6 files changed, 150 insertions(+), 14 deletions(-) diff --git a/build.sh b/build.sh index 4601a9b..cace253 100755 --- a/build.sh +++ b/build.sh @@ -54,9 +54,9 @@ build_backend() { local desc=$4 # 确定可执行文件名称 - local exe_name="NetworkAuth" + local exe_name="ApiServe" if [ "$os" = "windows" ]; then - exe_name="NetworkAuth.exe" + exe_name="ApiServe.exe" fi # 创建对应架构的输出目录 diff --git a/cmd/server.go b/cmd/server.go index cf38953..f7203d3 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -11,6 +11,7 @@ import ( "NetworkAuth/database" "NetworkAuth/middleware" + "NetworkAuth/models" "NetworkAuth/server" "NetworkAuth/services" "NetworkAuth/utils" @@ -72,8 +73,19 @@ func runServer(cmd *cobra.Command, args []string) { if db != nil { // 检查系统是否已安装 - isInstalled := services.GetSettingsService().GetString("is_installed", "0") - if isInstalled == "1" { + isInstalled := false + if db.Migrator().HasTable(&models.Settings{}) { + var setting models.Settings + if err := db.Where("name = ?", "is_installed").First(&setting).Error; err == nil { + isInstalled = setting.Value == "1" + } + } + if isInstalled { + needSeedPortalNavigation, err := database.NeedSeedDefaultPortalNavigation() + if err != nil { + logrus.WithError(err).Fatal("检测默认门户导航状态失败") + } + // 执行自动迁移(确保表结构存在) if err := database.AutoMigrate(); err != nil { logrus.WithError(err).Fatal("数据库自动迁移失败") @@ -82,8 +94,10 @@ func runServer(cmd *cobra.Command, args []string) { if err := database.SeedDefaultSettings(); err != nil { logrus.WithError(err).Fatal("默认系统设置初始化失败") } - if err := database.SeedDefaultPortalNavigation(); err != nil { - logrus.WithError(err).Fatal("默认门户导航初始化失败") + if needSeedPortalNavigation { + if err := database.SeedDefaultPortalNavigation(); err != nil { + logrus.WithError(err).Fatal("默认门户导航初始化失败") + } } // 初始化加密管理器 @@ -96,7 +110,7 @@ func runServer(cmd *cobra.Command, args []string) { // 启动日志清理定时任务 services.StartLogCleanupTask() } else { - logrus.Info("系统尚未安装 (is_installed=0),跳过核心组件初始化") + logrus.Info("系统处于未安装状态,跳过数据库自动迁移和核心组件初始化") } } else { logrus.Info("系统处于未初始化状态,跳过数据库自动迁移和设置加载") diff --git a/controllers/admin/portal_navigation.go b/controllers/admin/portal_navigation.go index cfcf596..d12cef7 100644 --- a/controllers/admin/portal_navigation.go +++ b/controllers/admin/portal_navigation.go @@ -26,7 +26,7 @@ func PortalNavigationListHandler(c *gin.Context) { } // PortalNavigationPublicListHandler 查询公开门户导航列表 -// 返回门户首页展示使用的可见导航数据 +// 返回门户首页展示使用的导航数据 func PortalNavigationPublicListHandler(c *gin.Context) { db, ok := authBaseController.GetDB(c) if !ok { @@ -141,6 +141,18 @@ func PortalNavigationDeleteHandler(c *gin.Context) { authBaseController.HandleValidationError(c, "管理员登录导航为系统保留项,不允许删除") return } + switch services.IsPortalNavigationGroup(item) { + case true: + var childCount int64 + if err := db.Model(&models.PortalNavigation{}).Where("parent_id = ?", item.ID).Count(&childCount).Error; err != nil { + authBaseController.HandleInternalError(c, "查询分组子导航失败", err) + return + } + if childCount > 0 { + authBaseController.HandleValidationError(c, "请先移除分组下的导航链接") + return + } + } if err := db.Delete(&item).Error; err != nil { authBaseController.HandleInternalError(c, "删除门户导航失败", err) @@ -155,6 +167,8 @@ func PortalNavigationDeleteHandler(c *gin.Context) { type portalNavigationPayload struct { ID uint `json:"id"` Name string `json:"name"` + Type string `json:"type"` + ParentID uint `json:"parent_id"` Path string `json:"path"` Sort int `json:"sort"` IsHome bool `json:"is_home"` @@ -166,7 +180,10 @@ type portalNavigationPayload struct { // 负责统一做字段校验和数据转换 func buildPortalNavigationFromPayload(c *gin.Context, body portalNavigationPayload) (models.PortalNavigation, bool) { item := models.PortalNavigation{ + ID: body.ID, Name: body.Name, + Type: body.Type, + ParentID: body.ParentID, Path: body.Path, Sort: body.Sort, IsHome: body.IsHome, @@ -175,7 +192,7 @@ func buildPortalNavigationFromPayload(c *gin.Context, body portalNavigationPaylo } services.NormalizePortalNavigation(&item) - if err := validatePortalNavigationInput(item); err != nil { + if err := validatePortalNavigationInput(c, item); err != nil { authBaseController.HandleValidationError(c, err.Error()) return models.PortalNavigation{}, false } @@ -185,21 +202,47 @@ func buildPortalNavigationFromPayload(c *gin.Context, body portalNavigationPaylo // validatePortalNavigationInput 校验门户导航字段 // 保证名称和地址满足基础格式要求 -func validatePortalNavigationInput(item models.PortalNavigation) error { +func validatePortalNavigationInput(c *gin.Context, item models.PortalNavigation) error { switch { case item.Name == "": return fmt.Errorf("名称不能为空") case len(item.Name) > 64: return fmt.Errorf("名称长度不能超过64个字符") - case item.Path == "": + case item.Type != "link" && item.Type != "group": + return fmt.Errorf("导航类型不合法") + case item.Type == "link" && item.Path == "": return fmt.Errorf("地址不能为空") - case len(item.Path) > 255: + case item.Type == "link" && len(item.Path) > 255: return fmt.Errorf("地址长度不能超过255个字符") case item.Sort < 0: return fmt.Errorf("排序不能小于0") case item.IsHome && item.IsHidden: return fmt.Errorf("设为首页后禁止隐藏") + case item.Type == "group" && item.ParentID > 0: + return fmt.Errorf("分组不允许设置所属分组") + case item.Type == "group" && item.IsHome: + return fmt.Errorf("分组不允许设为首页") + case item.Type == "group" && item.IsExternal: + return fmt.Errorf("分组不支持外部打开") + case item.ParentID > 0 && item.ParentID == item.ID: + return fmt.Errorf("所属分组不能选择自身") + case item.ParentID > 0 && item.IsHome: + return fmt.Errorf("分组内链接不允许设为首页") default: + db, ok := authBaseController.GetDB(c) + if !ok { + return fmt.Errorf("数据库连接失败") + } + if item.ParentID == 0 { + return nil + } + var parent models.PortalNavigation + if err := db.Where("id = ?", item.ParentID).First(&parent).Error; err != nil { + return fmt.Errorf("所属分组不存在") + } + if !services.IsPortalNavigationGroup(parent) { + return fmt.Errorf("所属导航不是分组") + } return nil } } diff --git a/database/portal_navigation.go b/database/portal_navigation.go index 381c691..b0a1c37 100644 --- a/database/portal_navigation.go +++ b/database/portal_navigation.go @@ -8,6 +8,43 @@ import ( "gorm.io/gorm" ) +// NeedSeedDefaultPortalNavigation 判断是否需要修复默认门户导航。 +// 仅在门户导航表缺失、关键字段缺失、没有任何数据或存在旧版脏数据时返回 true。 +func NeedSeedDefaultPortalNavigation() (bool, error) { + db, err := GetDB() + if err != nil { + return false, err + } + + if !db.Migrator().HasTable(&models.PortalNavigation{}) { + return true, nil + } + + requiredColumns := []string{"type", "parent_id", "is_home", "is_hidden", "is_external"} + for _, column := range requiredColumns { + if !db.Migrator().HasColumn(&models.PortalNavigation{}, column) { + return true, nil + } + } + + var count int64 + if err := db.Model(&models.PortalNavigation{}).Count(&count).Error; err != nil { + return false, err + } + if count == 0 { + return true, nil + } + + if err := db.Model(&models.PortalNavigation{}).Where("type = '' OR type IS NULL").Count(&count).Error; err != nil { + return false, err + } + if count > 0 { + return true, nil + } + + return false, nil +} + // SeedDefaultPortalNavigation 初始化默认门户导航 // 当系统首次安装或升级后缺少默认入口时,自动补充首页和管理员登录入口 func SeedDefaultPortalNavigation() error { @@ -19,6 +56,8 @@ func SeedDefaultPortalNavigation() error { defaultItems := []models.PortalNavigation{ { Name: "首页", + Type: "link", + ParentID: 0, Path: "/home/index", Sort: 0, IsHome: true, @@ -27,6 +66,8 @@ func SeedDefaultPortalNavigation() error { }, { Name: "管理员登录", + Type: "link", + ParentID: 0, Path: "admin", Sort: 999, IsHome: false, @@ -52,6 +93,8 @@ func SeedDefaultPortalNavigation() error { case true: if err := db.Model(&models.PortalNavigation{}).Where("id = ?", exists.ID).Updates(map[string]interface{}{ "name": "管理员登录", + "type": "link", + "parent_id": 0, "path": "admin", "sort": 999, "is_home": false, @@ -65,6 +108,13 @@ func SeedDefaultPortalNavigation() error { } } + if err := db.Model(&models.PortalNavigation{}).Where("type = '' OR type IS NULL").Updates(map[string]interface{}{ + "type": "link", + "parent_id": 0, + }).Error; err != nil { + return err + } + logrus.Info("默认门户导航初始化完成") return nil } diff --git a/models/portal_navigation.go b/models/portal_navigation.go index 6328802..5b2e325 100644 --- a/models/portal_navigation.go +++ b/models/portal_navigation.go @@ -7,6 +7,8 @@ import "time" type PortalNavigation struct { ID uint `json:"id" gorm:"primaryKey;comment:导航ID,自增主键"` Name string `json:"name" gorm:"size:64;not null;comment:导航名称"` + Type string `json:"type" gorm:"size:16;not null;default:link;comment:导航类型,link=链接,group=分组"` + ParentID uint `json:"parent_id" gorm:"default:0;not null;comment:所属分组ID,0表示顶级导航"` Path string `json:"path" gorm:"size:255;not null;comment:导航地址或路由路径"` Sort int `json:"sort" gorm:"default:0;not null;comment:排序值,越小越靠前,0最优先"` IsHome bool `json:"is_home" gorm:"default:false;comment:是否为门户首页"` diff --git a/services/portal_navigation.go b/services/portal_navigation.go index 34cc648..93880d2 100644 --- a/services/portal_navigation.go +++ b/services/portal_navigation.go @@ -9,20 +9,43 @@ import ( const portalNavigationAdminPath = "admin" const portalNavigationAdminSort = 999 +const portalNavigationTypeLink = "link" +const portalNavigationTypeGroup = "group" // NormalizePortalNavigation 规范化门户导航数据 -// 统一清理首尾空白,并处理首页与排序约束 +// 统一清理首尾空白,避免保存脏数据 func NormalizePortalNavigation(item *models.PortalNavigation) { item.Name = strings.TrimSpace(item.Name) + item.Type = strings.ToLower(strings.TrimSpace(item.Type)) + if item.Type == "" { + item.Type = portalNavigationTypeLink + } item.Path = strings.TrimSpace(item.Path) if item.Sort < 0 { item.Sort = 0 } + if item.Type == portalNavigationTypeGroup { + item.ParentID = 0 + item.Path = "" + item.IsExternal = false + item.IsHome = false + } if item.IsHome { item.IsHidden = false + item.ParentID = 0 } } +// IsPortalNavigationGroup 判断是否为分组导航 +func IsPortalNavigationGroup(item models.PortalNavigation) bool { + return strings.EqualFold(strings.TrimSpace(item.Type), portalNavigationTypeGroup) +} + +// IsPortalNavigationLink 判断是否为链接导航 +func IsPortalNavigationLink(item models.PortalNavigation) bool { + return !IsPortalNavigationGroup(item) +} + // IsPortalNavigationAdminEntry 判断是否为管理员入口 // 管理员入口属于系统保留导航项,不允许修改基础信息 func IsPortalNavigationAdminEntry(item models.PortalNavigation) bool { @@ -30,11 +53,13 @@ func IsPortalNavigationAdminEntry(item models.PortalNavigation) bool { } // LockPortalNavigationProtectedFields 锁定系统保留导航字段 -// 管理员入口仅允许调整隐藏状态,其余字段保持系统固定值 +// 管理员入口仅允许调整隐藏状态,其余字段保持数据库原值 func LockPortalNavigationProtectedFields(item *models.PortalNavigation, exists models.PortalNavigation) { switch IsPortalNavigationAdminEntry(exists) { case true: item.Name = "管理员登录" + item.Type = portalNavigationTypeLink + item.ParentID = 0 item.Path = portalNavigationAdminPath item.Sort = portalNavigationAdminSort item.IsHome = false @@ -69,6 +94,8 @@ func SavePortalNavigation(db *gorm.DB, item *models.PortalNavigation, exists ... default: return tx.Model(&models.PortalNavigation{}).Where("id = ?", item.ID).Updates(map[string]interface{}{ "name": item.Name, + "type": item.Type, + "parent_id": item.ParentID, "path": item.Path, "sort": item.Sort, "is_home": item.IsHome,