修复 导航栏重复初始化的问题

This commit is contained in:
2026-04-17 04:54:54 +08:00
parent ec508e6a32
commit 6d6b1c00c4
6 changed files with 150 additions and 14 deletions

View File

@@ -54,9 +54,9 @@ build_backend() {
local desc=$4 local desc=$4
# 确定可执行文件名称 # 确定可执行文件名称
local exe_name="NetworkAuth" local exe_name="ApiServe"
if [ "$os" = "windows" ]; then if [ "$os" = "windows" ]; then
exe_name="NetworkAuth.exe" exe_name="ApiServe.exe"
fi fi
# 创建对应架构的输出目录 # 创建对应架构的输出目录

View File

@@ -11,6 +11,7 @@ import (
"NetworkAuth/database" "NetworkAuth/database"
"NetworkAuth/middleware" "NetworkAuth/middleware"
"NetworkAuth/models"
"NetworkAuth/server" "NetworkAuth/server"
"NetworkAuth/services" "NetworkAuth/services"
"NetworkAuth/utils" "NetworkAuth/utils"
@@ -72,8 +73,19 @@ func runServer(cmd *cobra.Command, args []string) {
if db != nil { if db != nil {
// 检查系统是否已安装 // 检查系统是否已安装
isInstalled := services.GetSettingsService().GetString("is_installed", "0") isInstalled := false
if isInstalled == "1" { 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 { if err := database.AutoMigrate(); err != nil {
logrus.WithError(err).Fatal("数据库自动迁移失败") logrus.WithError(err).Fatal("数据库自动迁移失败")
@@ -82,8 +94,10 @@ func runServer(cmd *cobra.Command, args []string) {
if err := database.SeedDefaultSettings(); err != nil { if err := database.SeedDefaultSettings(); err != nil {
logrus.WithError(err).Fatal("默认系统设置初始化失败") logrus.WithError(err).Fatal("默认系统设置初始化失败")
} }
if err := database.SeedDefaultPortalNavigation(); err != nil { if needSeedPortalNavigation {
logrus.WithError(err).Fatal("默认门户导航初始化失败") if err := database.SeedDefaultPortalNavigation(); err != nil {
logrus.WithError(err).Fatal("默认门户导航初始化失败")
}
} }
// 初始化加密管理器 // 初始化加密管理器
@@ -96,7 +110,7 @@ func runServer(cmd *cobra.Command, args []string) {
// 启动日志清理定时任务 // 启动日志清理定时任务
services.StartLogCleanupTask() services.StartLogCleanupTask()
} else { } else {
logrus.Info("系统未安装 (is_installed=0),跳过核心组件初始化") logrus.Info("系统处于未安装状态,跳过数据库自动迁移和核心组件初始化")
} }
} else { } else {
logrus.Info("系统处于未初始化状态,跳过数据库自动迁移和设置加载") logrus.Info("系统处于未初始化状态,跳过数据库自动迁移和设置加载")

View File

@@ -26,7 +26,7 @@ func PortalNavigationListHandler(c *gin.Context) {
} }
// PortalNavigationPublicListHandler 查询公开门户导航列表 // PortalNavigationPublicListHandler 查询公开门户导航列表
// 返回门户首页展示使用的可见导航数据 // 返回门户首页展示使用的导航数据
func PortalNavigationPublicListHandler(c *gin.Context) { func PortalNavigationPublicListHandler(c *gin.Context) {
db, ok := authBaseController.GetDB(c) db, ok := authBaseController.GetDB(c)
if !ok { if !ok {
@@ -141,6 +141,18 @@ func PortalNavigationDeleteHandler(c *gin.Context) {
authBaseController.HandleValidationError(c, "管理员登录导航为系统保留项,不允许删除") authBaseController.HandleValidationError(c, "管理员登录导航为系统保留项,不允许删除")
return 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 { if err := db.Delete(&item).Error; err != nil {
authBaseController.HandleInternalError(c, "删除门户导航失败", err) authBaseController.HandleInternalError(c, "删除门户导航失败", err)
@@ -155,6 +167,8 @@ func PortalNavigationDeleteHandler(c *gin.Context) {
type portalNavigationPayload struct { type portalNavigationPayload struct {
ID uint `json:"id"` ID uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Type string `json:"type"`
ParentID uint `json:"parent_id"`
Path string `json:"path"` Path string `json:"path"`
Sort int `json:"sort"` Sort int `json:"sort"`
IsHome bool `json:"is_home"` IsHome bool `json:"is_home"`
@@ -166,7 +180,10 @@ type portalNavigationPayload struct {
// 负责统一做字段校验和数据转换 // 负责统一做字段校验和数据转换
func buildPortalNavigationFromPayload(c *gin.Context, body portalNavigationPayload) (models.PortalNavigation, bool) { func buildPortalNavigationFromPayload(c *gin.Context, body portalNavigationPayload) (models.PortalNavigation, bool) {
item := models.PortalNavigation{ item := models.PortalNavigation{
ID: body.ID,
Name: body.Name, Name: body.Name,
Type: body.Type,
ParentID: body.ParentID,
Path: body.Path, Path: body.Path,
Sort: body.Sort, Sort: body.Sort,
IsHome: body.IsHome, IsHome: body.IsHome,
@@ -175,7 +192,7 @@ func buildPortalNavigationFromPayload(c *gin.Context, body portalNavigationPaylo
} }
services.NormalizePortalNavigation(&item) services.NormalizePortalNavigation(&item)
if err := validatePortalNavigationInput(item); err != nil { if err := validatePortalNavigationInput(c, item); err != nil {
authBaseController.HandleValidationError(c, err.Error()) authBaseController.HandleValidationError(c, err.Error())
return models.PortalNavigation{}, false return models.PortalNavigation{}, false
} }
@@ -185,21 +202,47 @@ func buildPortalNavigationFromPayload(c *gin.Context, body portalNavigationPaylo
// validatePortalNavigationInput 校验门户导航字段 // validatePortalNavigationInput 校验门户导航字段
// 保证名称和地址满足基础格式要求 // 保证名称和地址满足基础格式要求
func validatePortalNavigationInput(item models.PortalNavigation) error { func validatePortalNavigationInput(c *gin.Context, item models.PortalNavigation) error {
switch { switch {
case item.Name == "": case item.Name == "":
return fmt.Errorf("名称不能为空") return fmt.Errorf("名称不能为空")
case len(item.Name) > 64: case len(item.Name) > 64:
return fmt.Errorf("名称长度不能超过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("地址不能为空") return fmt.Errorf("地址不能为空")
case len(item.Path) > 255: case item.Type == "link" && len(item.Path) > 255:
return fmt.Errorf("地址长度不能超过255个字符") return fmt.Errorf("地址长度不能超过255个字符")
case item.Sort < 0: case item.Sort < 0:
return fmt.Errorf("排序不能小于0") return fmt.Errorf("排序不能小于0")
case item.IsHome && item.IsHidden: case item.IsHome && item.IsHidden:
return fmt.Errorf("设为首页后禁止隐藏") 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: 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 return nil
} }
} }

View File

@@ -8,6 +8,43 @@ import (
"gorm.io/gorm" "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 初始化默认门户导航 // SeedDefaultPortalNavigation 初始化默认门户导航
// 当系统首次安装或升级后缺少默认入口时,自动补充首页和管理员登录入口 // 当系统首次安装或升级后缺少默认入口时,自动补充首页和管理员登录入口
func SeedDefaultPortalNavigation() error { func SeedDefaultPortalNavigation() error {
@@ -19,6 +56,8 @@ func SeedDefaultPortalNavigation() error {
defaultItems := []models.PortalNavigation{ defaultItems := []models.PortalNavigation{
{ {
Name: "首页", Name: "首页",
Type: "link",
ParentID: 0,
Path: "/home/index", Path: "/home/index",
Sort: 0, Sort: 0,
IsHome: true, IsHome: true,
@@ -27,6 +66,8 @@ func SeedDefaultPortalNavigation() error {
}, },
{ {
Name: "管理员登录", Name: "管理员登录",
Type: "link",
ParentID: 0,
Path: "admin", Path: "admin",
Sort: 999, Sort: 999,
IsHome: false, IsHome: false,
@@ -52,6 +93,8 @@ func SeedDefaultPortalNavigation() error {
case true: case true:
if err := db.Model(&models.PortalNavigation{}).Where("id = ?", exists.ID).Updates(map[string]interface{}{ if err := db.Model(&models.PortalNavigation{}).Where("id = ?", exists.ID).Updates(map[string]interface{}{
"name": "管理员登录", "name": "管理员登录",
"type": "link",
"parent_id": 0,
"path": "admin", "path": "admin",
"sort": 999, "sort": 999,
"is_home": false, "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("默认门户导航初始化完成") logrus.Info("默认门户导航初始化完成")
return nil return nil
} }

View File

@@ -7,6 +7,8 @@ import "time"
type PortalNavigation struct { type PortalNavigation struct {
ID uint `json:"id" gorm:"primaryKey;comment:导航ID自增主键"` ID uint `json:"id" gorm:"primaryKey;comment:导航ID自增主键"`
Name string `json:"name" gorm:"size:64;not null;comment:导航名称"` 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:所属分组ID0表示顶级导航"`
Path string `json:"path" gorm:"size:255;not null;comment:导航地址或路由路径"` Path string `json:"path" gorm:"size:255;not null;comment:导航地址或路由路径"`
Sort int `json:"sort" gorm:"default:0;not null;comment:排序值越小越靠前0最优先"` Sort int `json:"sort" gorm:"default:0;not null;comment:排序值越小越靠前0最优先"`
IsHome bool `json:"is_home" gorm:"default:false;comment:是否为门户首页"` IsHome bool `json:"is_home" gorm:"default:false;comment:是否为门户首页"`

View File

@@ -9,20 +9,43 @@ import (
const portalNavigationAdminPath = "admin" const portalNavigationAdminPath = "admin"
const portalNavigationAdminSort = 999 const portalNavigationAdminSort = 999
const portalNavigationTypeLink = "link"
const portalNavigationTypeGroup = "group"
// NormalizePortalNavigation 规范化门户导航数据 // NormalizePortalNavigation 规范化门户导航数据
// 统一清理首尾空白,并处理首页与排序约束 // 统一清理首尾空白,避免保存脏数据
func NormalizePortalNavigation(item *models.PortalNavigation) { func NormalizePortalNavigation(item *models.PortalNavigation) {
item.Name = strings.TrimSpace(item.Name) 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) item.Path = strings.TrimSpace(item.Path)
if item.Sort < 0 { if item.Sort < 0 {
item.Sort = 0 item.Sort = 0
} }
if item.Type == portalNavigationTypeGroup {
item.ParentID = 0
item.Path = ""
item.IsExternal = false
item.IsHome = false
}
if item.IsHome { if item.IsHome {
item.IsHidden = false 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 判断是否为管理员入口 // IsPortalNavigationAdminEntry 判断是否为管理员入口
// 管理员入口属于系统保留导航项,不允许修改基础信息 // 管理员入口属于系统保留导航项,不允许修改基础信息
func IsPortalNavigationAdminEntry(item models.PortalNavigation) bool { func IsPortalNavigationAdminEntry(item models.PortalNavigation) bool {
@@ -30,11 +53,13 @@ func IsPortalNavigationAdminEntry(item models.PortalNavigation) bool {
} }
// LockPortalNavigationProtectedFields 锁定系统保留导航字段 // LockPortalNavigationProtectedFields 锁定系统保留导航字段
// 管理员入口仅允许调整隐藏状态,其余字段保持系统固定 // 管理员入口仅允许调整隐藏状态,其余字段保持数据库原
func LockPortalNavigationProtectedFields(item *models.PortalNavigation, exists models.PortalNavigation) { func LockPortalNavigationProtectedFields(item *models.PortalNavigation, exists models.PortalNavigation) {
switch IsPortalNavigationAdminEntry(exists) { switch IsPortalNavigationAdminEntry(exists) {
case true: case true:
item.Name = "管理员登录" item.Name = "管理员登录"
item.Type = portalNavigationTypeLink
item.ParentID = 0
item.Path = portalNavigationAdminPath item.Path = portalNavigationAdminPath
item.Sort = portalNavigationAdminSort item.Sort = portalNavigationAdminSort
item.IsHome = false item.IsHome = false
@@ -69,6 +94,8 @@ func SavePortalNavigation(db *gorm.DB, item *models.PortalNavigation, exists ...
default: default:
return tx.Model(&models.PortalNavigation{}).Where("id = ?", item.ID).Updates(map[string]interface{}{ return tx.Model(&models.PortalNavigation{}).Where("id = ?", item.ID).Updates(map[string]interface{}{
"name": item.Name, "name": item.Name,
"type": item.Type,
"parent_id": item.ParentID,
"path": item.Path, "path": item.Path,
"sort": item.Sort, "sort": item.Sort,
"is_home": item.IsHome, "is_home": item.IsHome,