diff --git a/config/config.go b/config/config.go index d03245d..e7e4bdf 100644 --- a/config/config.go +++ b/config/config.go @@ -2,8 +2,6 @@ package config import ( "log/slog" - "os" - "strconv" "github.com/fsnotify/fsnotify" "github.com/spf13/viper" @@ -82,18 +80,13 @@ func setDefaults() { // parseConfig 解析配置到结构体 func parseConfig() *Config { - port := viper.GetInt("server.port") - if port == 0 { - port, _ = strconv.Atoi(getEnv("SERVER_PORT", "8080")) - } - return &Config{ Database: DatabaseConfig{ Driver: viper.GetString("database.driver"), DSN: viper.GetString("database.dsn"), }, Server: ServerConfig{ - Port: port, + Port: viper.GetInt("server.port"), Mode: viper.GetString("server.mode"), }, App: AppConfig{ @@ -124,11 +117,3 @@ func watchConfig() { func GetGlobalConfig() *Config { return globalConfig } - -// getEnv 获取环境变量 -func getEnv(key, defaultValue string) string { - if value := os.Getenv(key); value != "" { - return value - } - return defaultValue -} diff --git a/handlers/admin_view.go b/handlers/admin_view.go index 6427d1f..fabebe1 100644 --- a/handlers/admin_view.go +++ b/handlers/admin_view.go @@ -11,7 +11,6 @@ import ( "goblog/models" "github.com/gin-gonic/gin" - "github.com/golang-jwt/jwt/v5" ) // adminTemplatePath 后台单页模板(含 {{define}} 分块,按权限渲染) @@ -59,25 +58,17 @@ func AdminLoginView(c *gin.Context) { } // AdminView 后台管理页面:校验登录态后按角色用模板渲染菜单与页面框架 +// 依赖 OptionalAuth 中间件注入 userID/role 等上下文 func AdminView(c *gin.Context) { - tokenString, err := c.Cookie("token") - if err != nil || tokenString == "" { - c.Redirect(http.StatusFound, "/admin/login") - return - } - - claims := &Claims{} - token, err := jwt.ParseWithClaims(tokenString, claims, func(t *jwt.Token) (interface{}, error) { - return jwtSecret, nil - }) - if err != nil || !token.Valid { + userID, exists := c.Get("userID") + if !exists { c.Redirect(http.StatusFound, "/admin/login") return } // 校验用户当前状态(禁用账号不允许进入后台) var user models.User - if err := database.DB.First(&user, claims.UserID).Error; err != nil || user.Status == 0 || !user.IsActive { + if err := database.DB.First(&user, userID).Error; err != nil || user.Status == 0 || !user.IsActive { c.Redirect(http.StatusFound, "/admin/login") return } diff --git a/handlers/comment.go b/handlers/comment.go index 5b6981d..79499df 100644 --- a/handlers/comment.go +++ b/handlers/comment.go @@ -103,6 +103,24 @@ func CreateComment(c *gin.Context) { c.JSON(http.StatusCreated, gin.H{"data": comment}) } +// loadAndVerifyComment 加载评论并校验权限,成功返回评论,失败直接写响应并返回 nil +func loadAndVerifyComment(c *gin.Context) *models.Comment { + id := c.Param("id") + + var comment models.Comment + if err := database.DB.First(&comment, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "评论不存在"}) + return nil + } + + if !canManageComment(c, &comment) { + c.JSON(http.StatusForbidden, gin.H{"error": "无权管理该评论"}) + return nil + } + + return &comment +} + // canManageComment 检查当前用户是否有权管理该评论(管理员始终可以,普通用户只能管理自己文章上的评论) func canManageComment(c *gin.Context, comment *models.Comment) bool { if isAdmin(c) { @@ -120,16 +138,8 @@ func canManageComment(c *gin.Context, comment *models.Comment) bool { // 审核评论 func ApproveComment(c *gin.Context) { - id := c.Param("id") - - var comment models.Comment - if err := database.DB.First(&comment, id).Error; err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "评论不存在"}) - return - } - - if !canManageComment(c, &comment) { - c.JSON(http.StatusForbidden, gin.H{"error": "无权管理该评论"}) + comment := loadAndVerifyComment(c) + if comment == nil { return } @@ -144,16 +154,8 @@ func ApproveComment(c *gin.Context) { // 标记为垃圾评论 func MarkSpamComment(c *gin.Context) { - id := c.Param("id") - - var comment models.Comment - if err := database.DB.First(&comment, id).Error; err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "评论不存在"}) - return - } - - if !canManageComment(c, &comment) { - c.JSON(http.StatusForbidden, gin.H{"error": "无权管理该评论"}) + comment := loadAndVerifyComment(c) + if comment == nil { return } @@ -168,16 +170,8 @@ func MarkSpamComment(c *gin.Context) { // 删除评论 func DeleteComment(c *gin.Context) { - id := c.Param("id") - - var comment models.Comment - if err := database.DB.First(&comment, id).Error; err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "评论不存在"}) - return - } - - if !canManageComment(c, &comment) { - c.JSON(http.StatusForbidden, gin.H{"error": "无权管理该评论"}) + comment := loadAndVerifyComment(c) + if comment == nil { return } diff --git a/handlers/post.go b/handlers/post.go index 5d4589d..97c13cb 100644 --- a/handlers/post.go +++ b/handlers/post.go @@ -72,6 +72,20 @@ func generateSlug(title string) string { return slug } +// findOrCreateTags 根据名称列表查找或创建标签 +func findOrCreateTags(tagNames []string) []models.Tag { + var tags []models.Tag + for _, tagName := range tagNames { + var tag models.Tag + database.DB.FirstOrCreate(&tag, models.Tag{ + Name: tagName, + Slug: generateSlug(tagName), + }) + tags = append(tags, tag) + } + return tags +} + // 获取文章列表 func GetPosts(c *gin.Context) { var query PostListQuery @@ -80,6 +94,14 @@ func GetPosts(c *gin.Context) { return } + // 分页参数校验 + if query.Page < 1 { + query.Page = 1 + } + if query.PageSize < 1 || query.PageSize > 100 { + query.PageSize = 10 + } + db := database.DB.Model(&models.Post{}).Preload("Category").Preload("Tags").Preload("Author") // 管理员可查看全部;登录用户可看到自己的全部文章(含草稿);未登录只能看已发布 @@ -191,16 +213,7 @@ func CreatePost(c *gin.Context) { // 处理标签 if len(req.Tags) > 0 { - var tags []models.Tag - for _, tagName := range req.Tags { - var tag models.Tag - database.DB.FirstOrCreate(&tag, models.Tag{ - Name: tagName, - Slug: generateSlug(tagName), - }) - tags = append(tags, tag) - } - post.Tags = tags + post.Tags = findOrCreateTags(req.Tags) } if err := database.DB.Create(&post).Error; err != nil { @@ -272,15 +285,7 @@ func UpdatePost(c *gin.Context) { // 处理标签 if len(req.Tags) > 0 { - var tags []models.Tag - for _, tagName := range req.Tags { - var tag models.Tag - database.DB.FirstOrCreate(&tag, models.Tag{ - Name: tagName, - Slug: generateSlug(tagName), - }) - tags = append(tags, tag) - } + tags := findOrCreateTags(req.Tags) if err := database.DB.Model(&post).Association("Tags").Replace(tags); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "更新文章标签失败"}) return diff --git a/handlers/setting.go b/handlers/setting.go index 6466078..68d06e6 100644 --- a/handlers/setting.go +++ b/handlers/setting.go @@ -4,7 +4,6 @@ import ( "net/http" "strconv" - "goblog/config" "goblog/database" "goblog/models" @@ -21,10 +20,7 @@ const ( // GetSettings 获取站点基础设置(管理员) func GetSettings(c *gin.Context) { - cfg := config.GetGlobalConfig() - if cfg == nil { - cfg = config.Load() - } + cfg := getConfig() opts, err := models.GetOptionsByUser(database.DB, 0) if err != nil { diff --git a/handlers/theme.go b/handlers/theme.go index 0eceb78..94ae3bf 100644 --- a/handlers/theme.go +++ b/handlers/theme.go @@ -237,19 +237,19 @@ func GetThemeFiles(c *gin.Context) { }) } +// resolveThemeFilePath 根据主题名和文件名解析实际文件路径 +func resolveThemeFilePath(themeName, filename string) string { + if filename == themeName+".css" { + return filepath.Join("static", "css", filename) + } + return filepath.Join("templates", themeName, filename) +} + // 获取主题文件内容 func GetThemeFile(c *gin.Context) { themeName := c.Param("theme") filename := c.Param("file") - - var filePath string - - // 判断是模板文件还是 CSS 文件 - if filename == themeName+".css" { - filePath = filepath.Join("static", "css", filename) - } else { - filePath = filepath.Join("templates", themeName, filename) - } + filePath := resolveThemeFilePath(themeName, filename) if _, err := os.Stat(filePath); os.IsNotExist(err) { c.JSON(http.StatusBadRequest, gin.H{ @@ -288,14 +288,7 @@ func SaveThemeFile(c *gin.Context) { return } - var filePath string - - // 判断是模板文件还是 CSS 文件 - if filename == themeName+".css" { - filePath = filepath.Join("static", "css", filename) - } else { - filePath = filepath.Join("templates", themeName, filename) - } + filePath := resolveThemeFilePath(themeName, filename) // 保存文件 if err := os.WriteFile(filePath, []byte(req.Content), 0600); err != nil { diff --git a/handlers/view.go b/handlers/view.go index 30733bb..81b1a80 100644 --- a/handlers/view.go +++ b/handlers/view.go @@ -1,7 +1,6 @@ package handlers import ( - "log/slog" "math" "net/http" "strconv" @@ -15,6 +14,34 @@ import ( "gorm.io/gorm" ) +// getConfig 获取全局配置(启动时已初始化,直接返回) +func getConfig() *config.Config { + if cfg := config.GetGlobalConfig(); cfg != nil { + return cfg + } + return config.Load() +} + +// loadSiteInfo 加载站点名称和描述(优先数据库,回退配置文件) +func loadSiteInfo(cfg *config.Config) (siteName, siteDesc string) { + siteName = cfg.App.Name + siteDesc = cfg.App.Description + if v, err := models.GetOptionValue(database.DB, OptionSiteName, 0); err == nil && v != "" { + siteName = v + } + if v, err := models.GetOptionValue(database.DB, OptionSiteDesc, 0); err == nil && v != "" { + siteDesc = v + } + return +} + +// loadPublishedPages 加载所有已发布页面(按排序) +func loadPublishedPages() []models.Page { + var pages []models.Page + database.DB.Where("status = ?", "published").Order("`order` ASC").Find(&pages) + return pages +} + // loadSidebarData 加载侧边栏数据 func loadSidebarData() (categories []models.Category, tags []models.Tag, postCount, categoryCount, tagCount int64) { database.DB.Order("name ASC").Find(&categories) @@ -58,10 +85,7 @@ type IndexData struct { } func IndexView(c *gin.Context) { - cfg := config.GetGlobalConfig() - if cfg == nil { - cfg = config.Load() - } + cfg := getConfig() page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) if page < 1 { page = 1 @@ -91,39 +115,15 @@ func IndexView(c *gin.Context) { var total int64 db.Count(&total) - // 调试日志 - slog.Info("首页查询", "total", total, "page", page, "categoryID", categoryID, "tagID", tagID) - var posts []models.Post offset := (page - 1) * pageSize db.Order("is_top DESC, published_at DESC, created_at DESC"). Offset(offset).Limit(pageSize).Find(&posts) - // 调试日志 - slog.Info("查询结果", "postCount", len(posts)) - for i, p := range posts { - slog.Info("文章", "index", i, "id", p.ID, "title", p.Title, "status", p.Status, "publishedAt", p.PublishedAt, "views", p.Views, "commentsCount", len(p.Comments)) - } - - // 获取页面 - var pages []models.Page - database.DB.Where("status = ?", "published").Order("`order` ASC").Find(&pages) - totalPages := int(math.Ceil(float64(total) / float64(pageSize))) - - // 加载侧边栏数据 categories, tags, postCount, categoryCount, tagCount := loadSidebarData() - - // 从数据库读取主题设置,如果不存在则使用配置文件中的值 - siteName := cfg.App.Name - siteDesc := cfg.App.Description - - if v, err := models.GetOptionValue(database.DB, "site_name", 0); err == nil { - siteName = v - } - if v, err := models.GetOptionValue(database.DB, "site_desc", 0); err == nil { - siteDesc = v - } + siteName, siteDesc := loadSiteInfo(cfg) + pages := loadPublishedPages() data := IndexData{ Title: "首页", @@ -176,10 +176,7 @@ type PostDetailData struct { } func PostView(c *gin.Context) { - cfg := config.GetGlobalConfig() - if cfg == nil { - cfg = config.Load() - } + cfg := getConfig() slug := c.Param("slug") var post models.Post @@ -207,9 +204,7 @@ func PostView(c *gin.Context) { // 增加浏览量 database.DB.Model(&post).UpdateColumn("views", gorm.Expr("views + 1")) - // 获取页面 - var pages []models.Page - database.DB.Where("status = ?", "published").Order("`order` ASC").Find(&pages) + pages := loadPublishedPages() // 统计评论数 var commentCount int64 @@ -262,18 +257,7 @@ func PostView(c *gin.Context) { nextErr := database.DB.Where("status = ? AND published_at > ?", "published", post.PublishedAt). Order("published_at ASC").First(&nextPost).Error - // 从数据库读取站点设置 - siteName := cfg.App.Name - siteDesc := cfg.App.Description - - if v, err := models.GetOptionValue(database.DB, "site_name", 0); err == nil { - siteName = v - } - if v, err := models.GetOptionValue(database.DB, "site_desc", 0); err == nil { - siteDesc = v - } - - // 加载侧边栏数据 + siteName, siteDesc := loadSiteInfo(cfg) categories, tags, postCount, categoryCount, tagCount := loadSidebarData() data := PostDetailData{ @@ -331,10 +315,7 @@ type PageDetailData struct { } func PageView(c *gin.Context) { - cfg := config.GetGlobalConfig() - if cfg == nil { - cfg = config.Load() - } + cfg := getConfig() slug := c.Param("slug") var page models.Page @@ -348,21 +329,8 @@ func PageView(c *gin.Context) { return } - // 获取所有页面 - var pages []models.Page - database.DB.Where("status = ?", "published").Order("`order` ASC").Find(&pages) - - // 从数据库读取站点设置 - siteName := cfg.App.Name - siteDesc := cfg.App.Description - - if v, err := models.GetOptionValue(database.DB, "site_name", 0); err == nil { - siteName = v - } - if v, err := models.GetOptionValue(database.DB, "site_desc", 0); err == nil { - siteDesc = v - } - // 加载侧边栏数据 + pages := loadPublishedPages() + siteName, siteDesc := loadSiteInfo(cfg) categories, tags, postCount, categoryCount, tagCount := loadSidebarData() data := PageDetailData{ @@ -419,10 +387,7 @@ func SubmitComment(c *gin.Context) { // 搜索结果页 func SearchView(c *gin.Context) { - cfg := config.GetGlobalConfig() - if cfg == nil { - cfg = config.Load() - } + cfg := getConfig() keyword := c.Query("keyword") if keyword == "" { @@ -449,26 +414,10 @@ func SearchView(c *gin.Context) { db.Order("published_at DESC"). Offset(offset).Limit(pageSize).Find(&posts) - // 获取页面 - var pages []models.Page - database.DB.Where("status = ?", "published").Order("`order` ASC").Find(&pages) - + pages := loadPublishedPages() totalPages := int(math.Ceil(float64(total) / float64(pageSize))) - - // 加载侧边栏数据 categories, tags, postCount, categoryCount, tagCount := loadSidebarData() - - // 从数据库读取主题设置 - siteName := cfg.App.Name - siteDesc := cfg.App.Description - - var option models.Option - if err := database.DB.Where("name = ? AND user_id = ?", "site_name", 0).First(&option).Error; err == nil { - siteName = option.Value - } - if err := database.DB.Where("name = ? AND user_id = ?", "site_desc", 0).First(&option).Error; err == nil { - siteDesc = option.Value - } + siteName, siteDesc := loadSiteInfo(cfg) data := IndexData{ Title: "搜索: " + keyword, diff --git a/main.go b/main.go index 32f3273..09a3a7a 100644 --- a/main.go +++ b/main.go @@ -55,7 +55,7 @@ func main() { routers.SetupTemplates(r, theme) // 配置所有路由 - routers.SetupRoutes(r, cfg) + routers.SetupRoutes(r) // 启动服务器 port := routers.GetServerPort(cfg.Server.Port) diff --git a/models/models.go b/models/models.go index 498cff0..26257df 100644 --- a/models/models.go +++ b/models/models.go @@ -201,12 +201,6 @@ func SetOptionValue(db *gorm.DB, name string, userID uint, value string) error { return db.Create(&option).Error } -// DeleteOption 删除配置项 -func DeleteOption(db *gorm.DB, name string, userID uint) error { - result := db.Where("name = ? AND user_id = ?", name, userID).Delete(&Option{}) - return result.Error -} - // GetOptionsByUser 获取用户的所有配置 func GetOptionsByUser(db *gorm.DB, userID uint) (map[string]string, error) { var options []Option diff --git a/routers/router.go b/routers/router.go index 8f6806f..26f8526 100644 --- a/routers/router.go +++ b/routers/router.go @@ -11,7 +11,7 @@ import ( ) // SetupRoutes 配置所有路由 -func SetupRoutes(r *gin.Engine, cfg interface{}) { +func SetupRoutes(r *gin.Engine) { // 静态文件 r.Static("/static", "./static") @@ -19,8 +19,8 @@ func SetupRoutes(r *gin.Engine, cfg interface{}) { r.Static("/admin/static", "./static/admin") // 独立登录页 r.GET("/admin/login", handlers.AdminLoginView) - // 后台首页:服务端按登录角色用模板渲染菜单与页面框架 - r.GET("/admin", handlers.AdminView) + // 后台首页:可选认证中间件注入用户信息,服务端按角色渲染菜单与页面框架 + r.GET("/admin", middleware.OptionalAuth(), handlers.AdminView) // 前端页面路由(可选认证:侧边栏根据登录态显示系统菜单) r.GET("/", middleware.OptionalAuth(), handlers.IndexView)