外观
本章节将实现文章的评论系统,包括评论发表、回复功能、评论审核和评论树形结构展示。
完成本章节后,你将能够:
创建 internal/service/comment.go:
internal/service/comment.go
package service import ( "blog-system/internal/model" "blog-system/internal/repository" "errors" ) type CommentService interface { Create(comment *model.Comment) error GetByArticleID(articleID uint, page, pageSize int) ([]model.Comment, int64, error) GetByID(id uint) (*model.Comment, error) Approve(id uint) error Reject(id uint) error Delete(id uint, userID uint) error } type CommentServiceImpl struct { commentRepo repository.CommentRepository articleRepo repository.ArticleRepository } func NewCommentService(commentRepo repository.CommentRepository, articleRepo repository.ArticleRepository) CommentService { return &CommentServiceImpl{ commentRepo: commentRepo, articleRepo: articleRepo, } } func (s *CommentServiceImpl) Create(comment *model.Comment) error { // 检查文章是否存在 article, err := s.articleRepo.GetByID(comment.ArticleID) if err != nil { return errors.New("文章不存在") } // 如果文章不允许评论 if article.Status != "published" { return errors.New("文章未发布,无法评论") } // 创建评论 if err := s.commentRepo.Create(comment); err != nil { return err } // 更新文章评论数 go s.updateArticleCommentCount(comment.ArticleID) return nil } func (s *CommentServiceImpl) GetByArticleID(articleID uint, page, pageSize int) ([]model.Comment, int64, error) { // 只获取已审核的评论 comments, err := s.commentRepo.GetByArticleID(articleID, "approved", page, pageSize) if err != nil { return nil, 0, err } // 构建评论树 tree := s.buildCommentTree(comments) total, _ := s.commentRepo.CountByArticleID(articleID, "approved") return tree, total, nil } func (s *CommentServiceImpl) buildCommentTree(comments []model.Comment) []model.Comment { // 构建评论树结构 commentMap := make(map[uint]*model.Comment) var roots []model.Comment // 第一遍:创建映射 for i := range comments { commentMap[comments[i].ID] = &comments[i] comments[i].Replies = []model.Comment{} } // 第二遍:构建树 for i := range comments { if comments[i].ParentID == nil { roots = append(roots, comments[i]) } else { if parent, ok := commentMap[*comments[i].ParentID]; ok { parent.Replies = append(parent.Replies, comments[i]) } } } return roots } func (s *CommentServiceImpl) updateArticleCommentCount(articleID uint) { count, _ := s.commentRepo.CountByArticleID(articleID, "approved") s.articleRepo.UpdateCommentCount(articleID, count) }
创建 internal/handler/comment.go:
internal/handler/comment.go
package handler import ( "net/http" "strconv" "blog-system/internal/model" "blog-system/internal/service" "github.com/gin-gonic/gin" ) type CommentHandler struct { commentService service.CommentService } func NewCommentHandler(commentService service.CommentService) *CommentHandler { return &CommentHandler{commentService: commentService} } func (h *CommentHandler) Create(c *gin.Context) { articleID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "message": "无效的文章ID", }) return } var req CommentCreateRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "message": "请求参数无效", }) return } userID := c.GetUint("user_id") comment := &model.Comment{ ArticleID: uint(articleID), UserID: userID, Content: req.Content, ParentID: req.ParentID, Status: "pending", // 默认待审核 } // 获取客户端IP和User-Agent comment.IP = c.ClientIP() comment.UserAgent = c.GetHeader("User-Agent") if err := h.commentService.Create(comment); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "message": err.Error(), }) return } c.JSON(http.StatusCreated, gin.H{ "success": true, "message": "评论发表成功,等待审核", "data": comment, }) } func (h *CommentHandler) List(c *gin.Context) { articleID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "message": "无效的文章ID", }) return } page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10")) comments, total, err := h.commentService.GetByArticleID(uint(articleID), page, pageSize) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "message": "获取评论列表失败", }) return } c.JSON(http.StatusOK, gin.H{ "success": true, "data": gin.H{ "items": comments, "total": total, "page": page, "page_size": pageSize, }, }) }
func (s *CommentServiceImpl) Approve(id uint) error { comment, err := s.commentRepo.GetByID(id) if err != nil { return errors.New("评论不存在") } comment.Status = "approved" if err := s.commentRepo.Update(comment); err != nil { return err } // 更新文章评论数 go s.updateArticleCommentCount(comment.ArticleID) return nil } func (s *CommentServiceImpl) Reject(id uint) error { comment, err := s.commentRepo.GetByID(id) if err != nil { return errors.New("评论不存在") } comment.Status = "rejected" return s.commentRepo.Update(comment) }
func (h *CommentHandler) Statistics(c *gin.Context) { articleID, _ := strconv.ParseUint(c.Param("id"), 10, 32) stats, err := h.commentService.GetStatistics(uint(articleID)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "message": "获取统计失败", }) return } c.JSON(http.StatusOK, gin.H{ "success": true, "data": stats, }) }
func setupCommentRoutes(r *gin.RouterGroup, commentHandler *handler.CommentHandler) { comments := r.Group("/articles/:id/comments") { comments.GET("", commentHandler.List) comments.POST("", auth.AuthMiddleware(), commentHandler.Create) comments.DELETE("/:comment_id", auth.AuthMiddleware(), commentHandler.Delete) } // 管理员路由 admin := r.Group("/admin/comments") admin.Use(auth.AuthMiddleware(), auth.AdminOnly()) { admin.GET("", commentHandler.AdminList) admin.PATCH("/:id/approve", commentHandler.Approve) admin.PATCH("/:id/reject", commentHandler.Reject) } }
curl -X POST http://localhost:8080/api/articles/1/comments \ -H "Content-Type: application/json" \ -H "Authorization: Bearer <token>" \ -d '{ "content": "这是一条评论", "parent_id": null }'
curl -X POST http://localhost:8080/api/articles/1/comments \ -H "Content-Type: application/json" \ -H "Authorization: Bearer <token>" \ -d '{ "content": "这是回复", "parent_id": 1 }'
curl http://localhost:8080/api/articles/1/comments?page=1&page_size=10
评论系统完成后,下一步是:
🎉 评论系统完成! 现在你可以开始实现文件上传功能了。
评论系统
本章节将实现文章的评论系统,包括评论发表、回复功能、评论审核和评论树形结构展示。
📋 学习目标
完成本章节后,你将能够:
💬 评论服务
创建
internal/service/comment.go:2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
📝 评论处理器
创建
internal/handler/comment.go:2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
🔍 评论审核
审核服务方法
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
📊 评论统计
获取评论统计
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
🔧 路由设置
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
📝 API 使用示例
发表评论
2
3
4
5
6
7
回复评论
2
3
4
5
6
7
获取评论列表
💡 最佳实践
1. 评论审核策略
2. 性能优化
3. 安全考虑
⏭️ 下一步
评论系统完成后,下一步是:
🎉 评论系统完成! 现在你可以开始实现文件上传功能了。