package handlers import ( "math" "net/http" "strconv" "time" "goblog/config" "goblog/database" "goblog/models" "github.com/gin-gonic/gin" "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) database.DB.Order("name ASC").Find(&tags) database.DB.Model(&models.Post{}).Where("status = ?", "published").Count(&postCount) database.DB.Model(&models.Category{}).Count(&categoryCount) database.DB.Model(&models.Tag{}).Count(&tagCount) return } // loginInfo 从上下文提取登录态信息(供侧边栏系统菜单区分已登录/未登录) func loginInfo(c *gin.Context) (isLoggedIn bool, loginName string) { if _, ok := c.Get("userID"); !ok { return false, "" } name, _ := c.Get("username") loginName, _ = name.(string) return true, loginName } // 首页 type IndexData struct { Title string SiteName string SiteDesc string Description string Posts []models.Post Pages []models.Page CurrentPage int TotalPages int Year int // Sidebar 数据 Categories []models.Category Tags []models.Tag PostCount int64 CategoryCount int64 TagCount int64 // 登录态(侧边栏系统菜单用) IsLoggedIn bool LoginName string } func IndexView(c *gin.Context) { cfg := getConfig() page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) if page < 1 { page = 1 } pageSize := 10 categoryID, _ := strconv.Atoi(c.Query("category")) tagID, _ := strconv.Atoi(c.Query("tag")) db := database.DB.Model(&models.Post{}).Preload("Category").Preload("Tags").Preload("Author"). Where("status = ?", "published") userId, exists := c.Get("userID") if exists { db = db.Or("author_id = ?", userId) } if categoryID > 0 { db = db.Where("category_id = ?", categoryID) } if tagID > 0 { db = db.Joins("JOIN post_tags ON post_tags.post_id = posts.id"). Where("post_tags.tag_id = ?", tagID) } var total int64 db.Count(&total) 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) totalPages := int(math.Ceil(float64(total) / float64(pageSize))) categories, tags, postCount, categoryCount, tagCount := loadSidebarData() siteName, siteDesc := loadSiteInfo(cfg) pages := loadPublishedPages() data := IndexData{ Title: "首页", SiteName: siteName, SiteDesc: siteDesc, Description: siteDesc, Posts: posts, Pages: pages, CurrentPage: page, TotalPages: totalPages, Year: time.Now().Year(), // Sidebar 数据 Categories: categories, Tags: tags, PostCount: postCount, CategoryCount: categoryCount, TagCount: tagCount, } data.IsLoggedIn, data.LoginName = loginInfo(c) c.HTML(http.StatusOK, "index", data) } // 文章详情页 type PostDetailData struct { Title string SiteName string SiteDesc string Description string Post models.Post Pages []models.Page Comments []models.Comment CommentCount int64 Year int PrevPost *models.Post // 上一篇 NextPost *models.Post // 下一篇 // 评论展示设置与分页 ShowComments bool CommentPage int CommentTotalPages int // Sidebar 数据 Categories []models.Category Tags []models.Tag PostCount int64 CategoryCount int64 TagCount int64 // 登录态(侧边栏系统菜单用) IsLoggedIn bool LoginName string } func PostView(c *gin.Context) { cfg := getConfig() slug := c.Param("slug") var post models.Post query := database.DB.Preload("Category").Preload("Tags").Preload("Author"). Where("status = ?", "published"). Session(&gorm.Session{}) // 创建可安全复用的查询会话,避免两次查询条件叠加 err := query.Where("slug = ?", slug).First(&post).Error // slug 未命中时,若参数为纯数字则回退按文章 ID 查询(兼容 /post/17 形式的链接) if err != nil { if id, convErr := strconv.Atoi(slug); convErr == nil { err = query.Where("id = ?", id).First(&post).Error } } if err != nil { c.HTML(http.StatusNotFound, "index", gin.H{ "Title": "404 - 页面不存在", "SiteName": cfg.App.Name, "Year": time.Now().Year(), }) return } // 增加浏览量 database.DB.Model(&post).UpdateColumn("views", gorm.Expr("views + 1")) pages := loadPublishedPages() // 统计评论数 var commentCount int64 database.DB.Model(&models.Comment{}).Where("post_id = ? AND status = ?", post.ID, "approved").Count(&commentCount) // 读取评论展示设置 showComments := true if v, optErr := models.GetOptionValue(database.DB, OptionShowComments, 0); optErr == nil { showComments = v != "0" } commentPageSize := 10 if v, optErr := models.GetOptionValue(database.DB, OptionCommentPageSize, 0); optErr == nil { if n, convErr := strconv.Atoi(v); convErr == nil && n > 0 { commentPageSize = n } } // 顶级评论分页查询(回复随父评论一起展示,不单独分页) commentPage, _ := strconv.Atoi(c.DefaultQuery("cpage", "1")) if commentPage < 1 { commentPage = 1 } var comments []models.Comment commentTotalPages := 0 if showComments { var topCount int64 database.DB.Model(&models.Comment{}). Where("post_id = ? AND status = ? AND parent_id IS NULL", post.ID, "approved"). Count(&topCount) commentTotalPages = int(math.Ceil(float64(topCount) / float64(commentPageSize))) if commentTotalPages > 0 && commentPage > commentTotalPages { commentPage = commentTotalPages } database.DB.Where("post_id = ? AND status = ? AND parent_id IS NULL", post.ID, "approved"). Preload("Children", func(db *gorm.DB) *gorm.DB { return db.Where("status = ?", "approved") }). Order("created_at ASC"). Offset((commentPage - 1) * commentPageSize).Limit(commentPageSize). Find(&comments) } // 获取上一篇(发布时间更早的文章) var prevPost models.Post prevErr := database.DB.Where("status = ? AND published_at < ?", "published", post.PublishedAt). Order("published_at DESC").First(&prevPost).Error // 获取下一篇(发布时间更晚的文章) var nextPost models.Post nextErr := database.DB.Where("status = ? AND published_at > ?", "published", post.PublishedAt). Order("published_at ASC").First(&nextPost).Error siteName, siteDesc := loadSiteInfo(cfg) categories, tags, postCount, categoryCount, tagCount := loadSidebarData() data := PostDetailData{ Title: post.Title, SiteName: siteName, SiteDesc: siteDesc, Description: post.Summary, Post: post, Pages: pages, Comments: comments, CommentCount: commentCount, Year: time.Now().Year(), // 评论展示设置与分页 ShowComments: showComments, CommentPage: commentPage, CommentTotalPages: commentTotalPages, // Sidebar 数据 Categories: categories, Tags: tags, PostCount: postCount, CategoryCount: categoryCount, TagCount: tagCount, } data.IsLoggedIn, data.LoginName = loginInfo(c) // 只有当查询成功时才赋值 if prevErr == nil { data.PrevPost = &prevPost } if nextErr == nil { data.NextPost = &nextPost } c.HTML(http.StatusOK, "post", data) } // 独立页面 type PageDetailData struct { Title string SiteName string SiteDesc string Description string Page models.Page Pages []models.Page Year int // Sidebar 数据 Categories []models.Category Tags []models.Tag PostCount int64 CategoryCount int64 TagCount int64 // 登录态(侧边栏系统菜单用) IsLoggedIn bool LoginName string } func PageView(c *gin.Context) { cfg := getConfig() slug := c.Param("slug") var page models.Page err := database.DB.Where("slug = ? AND status = ?", slug, "published").First(&page).Error if err != nil { c.HTML(http.StatusNotFound, "index", gin.H{ "Title": "404 - 页面不存在", "SiteName": cfg.App.Name, "Year": time.Now().Year(), }) return } pages := loadPublishedPages() siteName, siteDesc := loadSiteInfo(cfg) categories, tags, postCount, categoryCount, tagCount := loadSidebarData() data := PageDetailData{ Title: page.Title, SiteName: siteName, SiteDesc: siteDesc, Description: "", Page: page, Pages: pages, Year: time.Now().Year(), // Sidebar 数据 Categories: categories, Tags: tags, PostCount: postCount, CategoryCount: categoryCount, TagCount: tagCount, } data.IsLoggedIn, data.LoginName = loginInfo(c) c.HTML(http.StatusOK, "page", data) } // 提交评论(表单提交) func SubmitComment(c *gin.Context) { postID, _ := strconv.Atoi(c.PostForm("post_id")) author := c.PostForm("author") email := c.PostForm("email") website := c.PostForm("website") content := c.PostForm("content") // 检查文章是否存在 var post models.Post if err := database.DB.First(&post, postID).Error; err != nil { c.String(http.StatusBadRequest, "文章不存在") return } comment := models.Comment{ PostID: uint(postID), Author: author, Email: email, Website: website, Content: content, Status: "pending", } if err := database.DB.Create(&comment).Error; err != nil { c.String(http.StatusInternalServerError, "提交失败") return } c.Redirect(http.StatusFound, "/post/"+post.Slug) } // 搜索结果页 func SearchView(c *gin.Context) { cfg := getConfig() keyword := c.Query("keyword") if keyword == "" { c.Redirect(http.StatusFound, "/") return } page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) if page < 1 { page = 1 } pageSize := 10 db := database.DB.Model(&models.Post{}).Preload("Category").Preload("Tags").Preload("Author"). Where("status = ?", "published"). Where("title LIKE ? OR content LIKE ? OR summary LIKE ?", "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%") var total int64 db.Count(&total) var posts []models.Post offset := (page - 1) * pageSize db.Order("published_at DESC"). Offset(offset).Limit(pageSize).Find(&posts) pages := loadPublishedPages() totalPages := int(math.Ceil(float64(total) / float64(pageSize))) categories, tags, postCount, categoryCount, tagCount := loadSidebarData() siteName, siteDesc := loadSiteInfo(cfg) data := IndexData{ Title: "搜索: " + keyword, SiteName: siteName, SiteDesc: siteDesc, Description: "搜索关键词: " + keyword, Posts: posts, Pages: pages, CurrentPage: page, TotalPages: totalPages, Year: time.Now().Year(), Categories: categories, Tags: tags, PostCount: postCount, CategoryCount: categoryCount, TagCount: tagCount, } data.IsLoggedIn, data.LoginName = loginInfo(c) c.HTML(http.StatusOK, "index", data) }