diff --git a/handlers/backend/comment.go b/handlers/backend/comment.go index d7bcb12..dfee1ad 100644 --- a/handlers/backend/comment.go +++ b/handlers/backend/comment.go @@ -2,6 +2,7 @@ package backend import ( "net/http" + "strconv" "goblog/database" "goblog/models" @@ -10,43 +11,72 @@ import ( "gorm.io/gorm" ) -// 创建评论请求 -type CreateCommentRequest struct { - PostID uint `json:"post_id" binding:"required"` - ParentID *uint `json:"parent_id"` - Author string `json:"author" binding:"required"` - Email string `json:"email" binding:"required,email"` - Website string `json:"website"` - Content string `json:"content" binding:"required"` -} - // 获取评论列表 func GetComments(c *gin.Context) { postID := c.Query("post_id") + postSlug := c.Query("post_slug") // 前台使用 slug status := c.Query("status") + pageStr := c.DefaultQuery("page", "1") + pageSizeStr := c.DefaultQuery("page_size", "10") + + // 解析分页参数 + page := 1 + pageSize := 10 + if p, err := strconv.Atoi(pageStr); err == nil && p > 0 { + page = p + } + if ps, err := strconv.Atoi(pageSizeStr); err == nil && ps > 0 && ps <= 100 { + pageSize = ps + } db := database.DB.Model(&models.Comment{}).Preload("User"). Preload("Post", func(db *gorm.DB) *gorm.DB { return db.Select("id", "title", "slug") }) - // 非管理员只能查看自己文章上的评论 + // 非管理员只能查看自己文章上的评论(后台)或已审核的评论(前台) if !isAdmin(c) { + // 前台访问:只显示已审核的评论 + db = db.Where("status = ?", "approved") if userID, exists := c.Get("userID"); exists { - db = db.Where("post_id IN (?)", - database.DB.Model(&models.Post{}).Select("id").Where("author_id = ?", userID)) + // 如果是后台接口调用,限制为自己的文章 + if postID == "" && postSlug == "" { + db = db.Where("post_id IN (?)", + database.DB.Model(&models.Post{}).Select("id").Where("author_id = ?", userID)) + } + } + } else { + // 管理员可以查看所有状态的评论(通过 status 参数过滤) + if status != "" { + db = db.Where("status = ?", status) } } + // 按文章 ID 或 slug 过滤 if postID != "" { db = db.Where("post_id = ?", postID) - } - if status != "" { - db = db.Where("status = ?", status) + } else if postSlug != "" { + var post models.Post + if err := database.DB.Where("slug = ?", postSlug).First(&post).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "文章不存在"}) + return + } + db = db.Where("post_id = ?", post.ID) } + // 统计总数 + var total int64 + db.Count(&total) + + // 只查询顶级评论(parent_id IS NULL),子评论在 Preload 中加载 var comments []models.Comment - db.Order("created_at DESC").Find(&comments) + offset := (page - 1) * pageSize + db.Where("parent_id IS NULL").Order("created_at ASC").Offset(offset).Limit(pageSize).Find(&comments) + + // 预加载子评论 + for i := range comments { + database.DB.Model(&comments[i]).Association("Children").Find(&comments[i].Children) + } // Comment.Post 在 JSON 中被隐藏,额外附加文章标题和别名供前端展示 type commentWithPost struct { @@ -59,48 +89,17 @@ func GetComments(c *gin.Context) { list = append(list, commentWithPost{cm, cm.Post.Title, cm.Post.Slug}) } - c.JSON(http.StatusOK, gin.H{"data": list}) -} - -// 创建评论 -func CreateComment(c *gin.Context) { - var req CreateCommentRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return + totalPages := int((total + int64(pageSize) - 1) / int64(pageSize)) + if totalPages < 1 { + totalPages = 1 } - // 检查文章是否存在 - var post models.Post - if err := database.DB.First(&post, req.PostID).Error; err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "文章不存在"}) - return - } - - comment := models.Comment{ - PostID: req.PostID, - ParentID: req.ParentID, - Author: req.Author, - Email: req.Email, - Website: req.Website, - Content: req.Content, - IP: c.ClientIP(), - Status: "pending", // 默认待审核 - } - - // 如果用户已登录 - if userID, exists := c.Get("userID"); exists { - uid := userID.(uint) - comment.UserID = &uid - comment.Status = "approved" // 登录用户评论自动通过 - } - - if err := database.DB.Create(&comment).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "发表评论失败"}) - return - } - - c.JSON(http.StatusCreated, gin.H{"data": comment}) + c.JSON(http.StatusOK, gin.H{ + "data": list, + "page": page, + "total_pages": totalPages, + "total": total, + }) } // loadAndVerifyComment 加载评论并校验权限,成功返回评论,失败直接写响应并返回 nil diff --git a/handlers/frontend/view.go b/handlers/frontend/view.go index a8b7a62..c419975 100644 --- a/handlers/frontend/view.go +++ b/handlers/frontend/view.go @@ -206,47 +206,6 @@ func PostView(c *gin.Context) { 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, models.OptionShowComments, 0); optErr == nil { - showComments = v != "0" - } - commentPageSize := 10 - if v, optErr := models.GetOptionValue(database.DB, models.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). @@ -261,19 +220,13 @@ func PostView(c *gin.Context) { 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, + Title: post.Title, + SiteName: siteName, + SiteDesc: siteDesc, + Description: post.Summary, + Post: post, + Pages: pages, + Year: time.Now().Year(), // Sidebar 数据 Categories: categories, Tags: tags, @@ -353,36 +306,106 @@ func PageView(c *gin.Context) { c.HTML(http.StatusOK, "page", data) } -// 提交评论(表单提交) +func ListComments(c *gin.Context) { + // 统计评论数 + var commentCount int64 + postID := c.Param("postID") + database.DB.Model(&models.Comment{}).Where("post_id = ? AND status = ?", postID, "approved").Count(&commentCount) + + // 读取评论展示设置 + showComments := true + if v, optErr := models.GetOptionValue(database.DB, models.OptionShowComments, 0); optErr == nil { + showComments = v != "0" + } + commentPageSize := 10 + if v, optErr := models.GetOptionValue(database.DB, models.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", postID, "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", postID, "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) + } + + c.JSON(http.StatusOK, gin.H{ + "data": comments, + "page": commentPage, + "total_pages": commentTotalPages, + "total": commentCount, + }) + +} + +// 创建评论请求 +type CreateCommentRequest struct { + PostID uint `json:"post_id" binding:"required"` + ParentID *uint `json:"parent_id"` + Author string `json:"author" binding:"required"` + Email string `json:"email" binding:"required,email"` + Website string `json:"website"` + Content string `json:"content" binding:"required"` +} + +// 提交评论 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 req CreateCommentRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + // 检查文章是否存在 // 检查文章是否存在 var post models.Post - if err := database.DB.First(&post, postID).Error; err != nil { - c.String(http.StatusBadRequest, "文章不存在") + if err := database.DB.First(&post, req.PostID).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "文章不存在"}) return } comment := models.Comment{ - PostID: uint(postID), - Author: author, - Email: email, - Website: website, - Content: content, - Status: "pending", + PostID: req.PostID, + ParentID: req.ParentID, + Author: req.Author, + Email: req.Email, + Website: req.Website, + Content: req.Content, + IP: c.ClientIP(), + Status: "pending", // 默认待审核 + } + // 如果用户已登录 + if userID, exists := c.Get("userID"); exists { + uid := userID.(uint) + comment.UserID = &uid + comment.Status = "approved" // 登录用户评论自动通过 } if err := database.DB.Create(&comment).Error; err != nil { c.String(http.StatusInternalServerError, "提交失败") return } - - c.Redirect(http.StatusFound, "/post/"+post.Slug) + c.JSON(http.StatusCreated, gin.H{"message": "评论提交成功"}) } // 搜索结果页 diff --git a/routers/router.go b/routers/router.go index c834850..455fd23 100644 --- a/routers/router.go +++ b/routers/router.go @@ -42,7 +42,8 @@ func setupFrontendRoutes(r *gin.Engine) { r.GET("/page/:slug", middleware.OptionalAuth(), frontend.PageView) r.GET("/search", middleware.OptionalAuth(), frontend.SearchView) - r.POST("/comment", frontend.SubmitComment) + r.GET("/posts/:postID/comments", frontend.ListComments) + r.POST("/posts/:postID/comment", frontend.SubmitComment) r.GET("/logout", backend.WebLogout) } @@ -60,9 +61,6 @@ func setupPublicAPIRoutes(api *gin.RouterGroup) { // 登录 api.POST("/auth/login", backend.Login) - // 创建评论(可选认证,登录后关联用户) - api.POST("/comments", middleware.OptionalAuth(), backend.CreateComment) - // 仪表盘公开统计数据(可选认证,登录后返回更多数据) api.GET("/dashboard/stats", middleware.OptionalAuth(), backend.GetDashboardStats) diff --git a/static/js/frontend.js b/static/js/frontend.js new file mode 100644 index 0000000..3e09916 --- /dev/null +++ b/static/js/frontend.js @@ -0,0 +1,182 @@ +/** + * 前台评论 AJAX 功能 + */ + +// 评论分页加载 +async function loadComments(postSlug, page = 1) { + try { + const url = `/posts/${postSlug}/comments?page=${page}`; + const res = await fetch(url); + if (!res.ok) throw new Error('加载评论失败'); + + const data = await res.json(); + renderComments(data.data || [], postSlug); + renderCommentPagination(data.page, data.total_pages, postSlug); + } catch (err) { + console.error('加载评论失败:', err); + document.querySelector('.comments-list').innerHTML = '
评论加载失败
'; + } +} + +// 渲染评论列表 +function renderComments(comments, postSlug) { + const listEl = document.querySelector('.comments-list'); + if (!listEl) return; + + if (!comments || comments.length === 0) { + listEl.innerHTML = '暂无评论,来发表第一条评论吧!
'; + return; + } + + listEl.innerHTML = comments.map(c => buildCommentHTML(c)).join(''); +} + +// 构建单条评论 HTML +function buildCommentHTML(comment) { + let html = ` +