写在前面 话不多说,先看多级评论的最后效果:
并且评论可以一直嵌套下去,实现了无限评论与回复。有点类似于抖音app的评论区。
仓库地址 关于多级评论demo,所有代码均放到了GitHub仓库:https://github.com/palp1tate/MultiLevelCommentDemo
有需要者可克隆使用。demo主要包括了三个功能:用户发表动态,发表评论,查看评论。
数据库设计 user
表:
1 2 3 4 5 6 type User struct { gorm.Model Nickname string `gorm:"not null;index;varchar(20)"` Password string `gorm:"not null"` Avatar string `gorm:"not null;"` }
moment
表:
1 2 3 4 5 6 type Moment struct { gorm.Model UserId int `gorm:"not null;index"` User User `gorm:"foreignKey:UserId;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` Content string `gorm:"size:2048"` }
这里给UserId
设置了外键,参照User
的ID
,并设置了级联删除与更新。
comment
表:
1 2 3 4 5 6 7 8 9 10 type Comment struct { gorm.Model MomentId int `gorm:"not null;index"` Moment Moment `gorm:"foreignKey:MomentId;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` UserId int `gorm:"not null;index"` User User `gorm:"foreignKey:UserId;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` ParentId *int `gorm:"index;default:NULL"` Parent *Comment `gorm:"foreignKey:ParentId;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` Content string `gorm:"not null;size:1024"` }
评论表是比较关键的部分,MomentId
,UserId
,ParentId
均设置了外键与级联删除更新,这样做的目的是为了适应一些场景,比如动态被删了,那么评论也会都从表中删掉,父评论删了,那么它对应的子孙评论也会一并删除,这样处理才符合真实的业务逻辑。ParentId
顾名思义就是父评论的ID
,但由于初始评论没有父评论,所以默认为NULL
,这里注意用*int
而不用int
的原因是:如果用后者,默认空值是0,那么即使创建评论数据时不传入该字段,也会当0处理,这样插入到数据库就会报错,因为ParentId
是外键,而最开始的数据库不可能有ID
为0的数据,所以就会发生参照外键的错误,导致插入失败,但是不设置外键就没有级联的效果,这样也不符合真实的场景。于是将该字段换位指针类型,这样默认空值为nil
,插入到数据库就会显示NULL
,这样就能实现插入第一条评论了,可见下图:
关于评论还有一个结构体,用来表示评论树:
1 2 3 4 5 6 7 type CommentTree struct { CommentId int `json:"cid"` Content string `json:"content"` Author map [string ]interface {} `json:"author"` CreatedAt string `json:"createdAt"` Children []*CommentTree `json:"children"` }
通过评论树就可以实现与子孙评论的无限嵌套。
如何使用 关于mysql连接初始化,路由配置,表单验证等均在可以在仓库找到相应的代码,这里不做赘述。
下面讲解真实的业务逻辑。
有动态才能被评论,所以我们先发布一条动态。发布动态的代码可以在仓库里找到。
记住动态ID
为2,发布评论时会用到。
下面附上发布评论的核心代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func AddComment (c *gin.Context) { addCommentForm := form.AddCommentForm{} if err := c.ShouldBind(&addCommentForm); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "code" : 400 , "msg" : err.Error(), }) return } comment := model.Comment{ UserId: addCommentForm.UserId, MomentId: addCommentForm.MomentId, Content: addCommentForm.Content, } if addCommentForm.ParentId != 0 { comment.ParentId = &addCommentForm.ParentId } global.DB.Create(&comment) c.JSON(http.StatusOK, gin.H{ "code" : 200 , "msg" : "success" , }) }
如果是第一条评论,也就是父评论,那么前端在传的时候ParentId
为0
就好,因为它不可能有父评论。我的代码通过判断ParentId
是0后,就不需要传入该字段,因为在刚才讲解数据库设计的时候,ParentId
在数据库中默认为NULL
。
下面新建一条评论:
为了之后的显示效果,我又多发布了几条评论。
查看评论是也是比较核心的部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func GetComments (c *gin.Context) { var commentTrees []model.CommentTree momentId := c.Query("mid" ) if momentId == "" { c.JSON(http.StatusBadRequest, gin.H{ "code" : 400 , "msg" : "mid不能为空" , }) return } mid, _ := strconv.Atoi(momentId) commentTrees = GetMomentComment(mid) c.JSON(http.StatusOK, gin.H{ "code" : 200 , "msg" : "success" , "data" : commentTrees, }) }
GetMomentComment
函数比较核心的函数,你只需要传入动态的ID
进去,就可以获取到该动态的评论树。该函数会查询动态所有的一级评论,并把该一级评论的所有子孙评论放到它的Children
里。同时该函数会调用GetMomentCommentChild
函数,这个函数是根据某条父评论,以获取其所有子孙评论,使用了递归。通过这两个函数,便可以得到一株评论树,两个函数的细节可能会有难理解的地方,但对于我们API
工程师来说,只要会调用就行😎。
1 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 func GetMomentComment (mid int ) []model.CommentTree { var commentTrees []model.CommentTree var comments []model.Comment global.DB.Where("moment_id = ? AND parent_id IS NULL" , mid).Order("id desc" ).Find(&comments) for _, comment := range comments { var user model.User cid := int (comment.ID) uid := comment.UserId global.DB.Where("id = ?" , uid).First(&user) commentTree := model.CommentTree{ CommentId: cid, Content: comment.Content, Author: gin.H{"uid" : uid, "nickname" : user.Nickname, "avatar" : user.Avatar}, CreatedAt: comment.CreatedAt.Format("2006-01-02 15:04" ), Children: []*model.CommentTree{}, } GetMomentCommentChild(cid, &commentTree) commentTrees = append (commentTrees, commentTree) } return commentTrees } func GetMomentCommentChild (pid int , commentTree *model.CommentTree) { var comments []model.Comment global.DB.Where("parent_id = ?" , pid).Find(&comments) for i, _ := range comments { var user model.User cid := int (comments[i].ID) uid := comments[i].UserId global.DB.Where("id = ?" , uid).First(&user) child := model.CommentTree{ CommentId: cid, Content: comments[i].Content, Author: gin.H{"uid" : user.ID, "nickname" : user.Nickname, "avatar" : user.Avatar}, CreatedAt: comments[i].CreatedAt.Format("2006-01-02 15:04" ), Children: []*model.CommentTree{}, } commentTree.Children = append (commentTree.Children, &child) GetMomentCommentChild(cid, &child) } }
以下是查看评论的效果:
到这儿,实现多级评论就全部完成了,完整代码均在Github仓库中,欢迎查阅。