手把手教你用gin+gorm+mysql实现多级评论

写在前面

话不多说,先看多级评论的最后效果:

并且评论可以一直嵌套下去,实现了无限评论与回复。有点类似于抖音app的评论区。

仓库地址

关于多级评论demo,所有代码均放到了GitHub仓库:github.com/palp1tate/M...

有需要者可克隆使用。demo主要包括了三个功能:用户发表动态,发表评论,查看评论。

数据库设计

user表:

go 复制代码
type User struct {
	gorm.Model
	Nickname string `gorm:"not null;index;varchar(20)"` // 昵称
	Password string `gorm:"not null"`                   // 密码
	Avatar   string `gorm:"not null;"`                  // 头像
}

moment表:

go 复制代码
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设置了外键,参照UserID,并设置了级联删除与更新。

comment表:

go 复制代码
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"`
}

评论表是比较关键的部分,MomentIdUserIdParentId均设置了外键与级联删除更新,这样做的目的是为了适应一些场景,比如动态被删了,那么评论也会都从表中删掉,父评论删了,那么它对应的子孙评论也会一并删除,这样处理才符合真实的业务逻辑。ParentId顾名思义就是父评论的ID,但由于初始评论没有父评论,所以默认为NULL,这里注意用*int而不用int的原因是:如果用后者,默认空值是0,那么即使创建评论数据时不传入该字段,也会当0处理,这样插入到数据库就会报错,因为ParentId是外键,而最开始的数据库不可能有ID为0的数据,所以就会发生参照外键的错误,导致插入失败,但是不设置外键就没有级联的效果,这样也不符合真实的场景。于是将该字段换位指针类型,这样默认空值为nil,插入到数据库就会显示NULL,这样就能实现插入第一条评论了,可见下图:

关于评论还有一个结构体,用来表示评论树:

go 复制代码
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,发布评论时会用到。

下面附上发布评论的核心代码:

go 复制代码
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",
	})
}

如果是第一条评论,也就是父评论,那么前端在传的时候ParentId0就好,因为它不可能有父评论。我的代码通过判断ParentId是0后,就不需要传入该字段,因为在刚才讲解数据库设计的时候,ParentId在数据库中默认为NULL

下面新建一条评论:

为了之后的显示效果,我又多发布了几条评论。

查看评论是也是比较核心的部分:

go 复制代码
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工程师来说,只要会调用就行😎。

go 复制代码
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仓库中,欢迎查阅。

相关推荐
杨哥带你写代码2 小时前
网上商城系统:Spring Boot框架的实现
java·spring boot·后端
camellias_2 小时前
SpringBoot(二十一)SpringBoot自定义CURL请求类
java·spring boot·后端
背水2 小时前
初识Spring
java·后端·spring
晴天飛 雪2 小时前
Spring Boot MySQL 分库分表
spring boot·后端·mysql
weixin_537590452 小时前
《Spring boot从入门到实战》第七章习题答案
数据库·spring boot·后端
AskHarries3 小时前
Spring Cloud Gateway快速入门Demo
java·后端·spring cloud
Qi妙代码3 小时前
MyBatisPlus(Spring Boot版)的基本使用
java·spring boot·后端
宇宙超级勇猛无敌暴龙战神3 小时前
Springboot整合xxl-job
java·spring boot·后端·xxl-job·定时任务
晚睡早起₍˄·͈༝·͈˄*₎◞ ̑̑3 小时前
SpringBoot(五)
java·spring boot·后端