分享
  1. 首页
  2. 文章

vue+golang实现评论系统

aside section._1OhGeD · · 2126 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

数据结构设计:

评论可以被回复, 回复也可以被回复, 以此连接下去就形成了树的结构

哪个用户评论、
评论的对象类型(视频、文章、用户动态...)、
具体哪个评论对象、
评论的内容、
评论的父亲节点(对于顶级评论没有父亲节点)、
根节点(如果想取出某一条评论的所有孩子节点, 也就是取出评论下方的所有回复就可以select * from comments where root_id=comment.id)、
回复了谁(如果不加这个字段需要先关联查询父节点、再从父节点关联查询用户)

本项目基于gin+gorm脚手架 https://github.com/Gourouting/singo

package models
import "time"
type Comment struct {
 ID uint `gorm:"primary_key"`
 CreatedAt time.Time
 UpdatedAt time.Time
 DeletedAt *time.Time `sql:"index"`
 User User `gorm:"foreignkey:UserID;association_foreignkey:ID"`
 UserID uint `gorm:"not null"`
 ReplyTo User `gorm:"foreignkey:ReplyToID;association_foreignkey:ID"`
 ReplyToID uint `sql:"default:null"`
 // 这里没有指定评论对象类型, 直接选择了只对视频评论
 Video Video `gorm:"foreignkey:VideoID;association_foreignkey:ID"`
 VideoID uint `gorm:"not null"`
 Parent *Comment `gorm:"foreignkey:ParentID;association_foreignkey:ID;"`
 ParentID uint `sql:"default:null"`
 Root *Comment `gorm:"foreignkey:RootID;association_foreignkey:ID;"`
 RootID uint `sql:"default:null"`
 Content string `gorm:"not null"`
 Replys []Comment `sql:"default:null"`
}

因为gorm默认不会添加外键约束, 需要手动添加

package models
//执行数据迁移
func migration() {
 // 自动迁移模式
 DB.AutoMigrate(&Video{}, &User{}, &Comment{})
 DB.Model(&Comment{}).AddForeignKey("user_id", "users(id)",
 "CASCADE", "CASCADE")
 DB.Model(&Comment{}).AddForeignKey("reply_to_id", "users(id)",
 "CASCADE", "CASCADE")
 DB.Model(&Comment{}).AddForeignKey("video_id", "videos(id)",
 "CASCADE", "CASCADE")
 DB.Model(&Comment{}).AddForeignKey("parent_id", "comments(id)",
 "CASCADE", "CASCADE")
 DB.Model(&Comment{}).AddForeignKey("root_id", "comments(id)",
 "CASCADE", "CASCADE")
}

添加了几条记录

image.png

获取评论列表

func CommentList(c *gin.Context) {
 var service services.CommentListService
 if err := c.ShouldBind(&service); err != nil {
 c.JSON(400, err.Error())
 } else {
 videoID, _ := strconv.Atoi(c.Param("video_id"))
 res := service.List(uint(videoID))
 c.JSON(200, res)
 }
}

service

package services
import (
 "mygin/e"
 "mygin/models"
 "mygin/serializers"
)
type CommentListService struct {
}
func (service *CommentListService) List(videoID uint) *serializers.Response {
 // 取出顶级评论
 var comments []models.Comment
 if err := models.DB.Where("video_id=? and parent_id is null", videoID).
 Preload("User").Find(&comments).Error; err != nil {
 return &serializers.Response{
 Status: e.SELECT_ERROR,
 Message: e.GetMsg(e.SELECT_ERROR),
 Error: err.Error(),
 }
 }
 // 取出评论下方的回复、回复者、回复了谁
 for i := 0; i < len(comments); i++ {
 if err := models.DB.Where("root_id=?", comments[i].ID).
 Preload("User").Preload("ReplyTo").
 Find(&comments[i].Replys).Error; err != nil {
 return &serializers.Response{
 Status:e.SELECT_ERROR,
 Message:e.GetMsg(e.SELECT_ERROR),
 Error:err.Error(),
 }
 }
 }
 return &serializers.Response{
 Status: e.SUCCESS,
 Message: e.GetMsg(e.SUCCESS),
 Data: serializers.BuildComments(comments),
 }
}

serializer序列化器

package serializers
import (
 "mygin/config"
 "mygin/models"
)
type CommentSerializer struct {
 ID uint `json:"id"`
 User UserSerializer `json:"user"`
 VideoID uint `json:"video_id"`
 Content string `json:"content"`
 CreatedAt string `json:"created_at"`
 Replys []ReplySerializer `json:"replys"`
}
type ReplySerializer struct {
 ID uint `json:"id"`
 User UserSerializer `json:"user"`
 ReplyTo UserSerializer `json:"reply_to"`
 ParentID uint `json:"parent_id"`
 RootID uint `json:"root_id"`
 Content string `json:"content"`
 CreatedAt string `json:"created_at"`
}
// 序列化回复
func BuildReply(item models.Comment) ReplySerializer {
 return ReplySerializer{
 ID: item.ID,
 User: BuildUser(item.User),
 ReplyTo: BuildUser(item.ReplyTo),
 ParentID: item.ParentID,
 RootID: item.RootID,
 Content: item.Content,
 CreatedAt: item.CreatedAt.Format(config.CurrentTime),
 }
}
// 序列化评论
func BuildComment(item models.Comment) CommentSerializer {
 var replys []ReplySerializer
 if len(item.Replys) != 0 {
 for _, reply := range item.Replys {
 replys = append(replys, BuildReply(reply))
 }
 }
 return CommentSerializer{
 ID: item.ID,
 User: BuildUser(item.User),
 VideoID: item.VideoID,
 Content: item.Content,
 CreatedAt: item.CreatedAt.Format(config.CurrentTime),
 Replys: replys,
 }
}
func BuildComments(items []models.Comment) []CommentSerializer {
 var comments []CommentSerializer
 for _, item := range items {
 comments = append(comments, BuildComment(item))
 }
 return comments
}

执行结果

image.png

接下来是前端的编写

初始化定义

 comments: [
 {
 id: 0,
 user: {
 id: 0,
 username: '',
 head_img: ''
 },
 video_id: 0,
 content: '',
 created_at: '',
 replys: [ //回复,或子评论
 {
 id: 0,
 user: {
 id: 0,
 username: '',
 head_img: ''
 },
 reply_to: {
 id: 0,
 username: '',
 head_img: ''
 },
 parent_id: 0,
 root_id: 0,
 content: '',
 created_at: ''
 }
 ]
 }
 ]

评论表单

<el-form :model="ruleForm" :rules="rules" ref="ruleForm">
 <el-form-item prop="content">
 <el-input v-model="ruleForm.content" type="textarea"
 placeholder="写下你的评论"></el-input>
 </el-form-item>
 <el-form-item>
 <el-button type="primary" @click="submitComment" 
 style="float: right">提交评论</el-button>
 </el-form-item>
</el-form>
<script>
export default {
 name: 'comment',
 props: [
 'video_id'
 ],
 data() {
 return {
 ruleForm: {
 content: ''
 },
 rules: {
 content: [
 { required: true, message: '请输入评论内容', trigger: 'submit' },
 { min: 3, message: '至少输入3个字符', trigger: 'submit' },
 ]
 },
 }
 },
 created() {
 axios.get("http://localhost:8888/api/v1/comment/"+this.video_id).
 then(rep => {
 console.log(rep);
 this.comments = rep.data.data;
 })
 },
 methods: {
 submitComment() {
 this.$refs.ruleForm.validate((valid) => {
 if (valid) {
 axios.post("/comment", {
 video_id: this.video_id,
 content: this.ruleForm.content,
 }, {
 headers: {
 token: this.$store.state.token // 附带用户token
 }
 }).then(rep => {
 console.log(rep);
 this.comments = this.comments||[];
 this.comments.unshift(rep.data.data); // 添加到要展示的评论数组中
 this.ruleForm.content = '' // 清除评论框内容
 }).catch(err => {
 console.log(err.response);
 })
 } else {
 console.log('error submit!!');
 return false;
 }
 });
 },
 }
}
</script>

对于提交评论来说很简单, 把当前用户、评论的对象、评论内容提交过去就行了, 但对于提交回复来说还要把回复了谁、父亲节点、根节点也提交过去

先来看一下评论列表的展示和提交回复的逻辑(具体的css这里没给出)

 <div class="comment" v-for="item in comments">
 <hr>
<!-- 展示当前评论用户 -->
 <img :src="item.user.head_img">
 <div>{{item.user.username}}</div>
 <span class="date">{{item.created_at}}</span>
 <p>{{item.content}}</p>
<!-- 展示回复input框 -->
 <el-tag @click="showCommentInput(item)">回复</el-tag>
 <!-- 展示评论下方的回复 -->
 <div v-for="reply in item.replys">
 <img :src="reply.user.head_img">
 <span>{{reply.user.username}}</span><span>: </span>
 <span>@{{reply.reply_to.username}}</span>
 <div class="date">{{reply.created_at}}</div>
 <p>{{reply.content}}</p>
 <el-tag @click="showCommentInput(item, reply)">回复</el-tag>
 </div>
 <!-- 展示评论框 -->
 <div v-if="current_root_id===item.id">
 <el-form :model="ruleForm2" :rules="rules2" ref="ruleForm2">
 <el-form-item prop="content">
 <el-input v-model="ruleForm2.content"
 type="textarea"
 placeholder="写下你的评论">
 </el-input>
 </el-form-item>
 <el-form-item>
 <el-button style="float: right;" 
 @click="submitReply">确定</el-button>
 </el-form-item>
 </el-form>
 </div>
 </div>
<script>
export default {
 data() {
 return {
 // 当点击展示回复框的时候记录下回复了谁、父亲节点、根节点, 方便提交
 current_root_id: 0, 
 current_parent_id: 0,
 current_reply_to_user_id: 0,
 ruleForm2: {
 content: ''
 },
 rules2: {
 content: [
 { required: true, message: '请输入评论内容', trigger: 'submit' },
 { min: 3, message: '至少输入3个字符', trigger: 'submit' },
 ]
 },
 }
 },
 methods: {
 // 展示回复框
 showCommentInput(item, reply) {
 // 如果回复了回复
 if (reply) {
 this.ruleForm2.content = "@" + reply.user.username + " ";
 this.current_reply_to_user_id = reply.user.id;
 this.current_parent_id = reply.id;
 } else {
 // 如果回复了评论
 this.ruleForm2.content = '';
 this.current_parent_id = item.id;
 this.current_reply_to_user_id = item.user.id;
 }
 this.current_root_id = item.id;
 },
 // 提交回复
 submitReply() {
 this.$refs.ruleForm2[0].validate((valid) => {
 if (valid) {
 axios.post("/comment", {
 reply_to_id: this.current_reply_to_user_id,
 video_id: this.video_id,
 parent_id: this.current_parent_id,
 root_id: this.current_root_id,
 content: this.ruleForm2.content,
 }, {
 headers: {
 token: this.$store.state.token
 }
 }).then(rep => {
 // 获取根节点
 const comment = this.comments.find(item => item.id===this.current_root_id);
 comment.replys = comment.replys||[];
 // 插入到根节点的下方
 comment.replys.push(rep.data.data);
 this.ruleForm2.content = ''
 }).catch(err => {
 console.log(err);
 })
 } else {
 console.log('error submit!!');
 return false;
 }
 });
 },
 }
}
</script>

后台创建评论接口

func CreateComment(c *gin.Context) {
 var service services.CreateCommentService
 if err := c.ShouldBind(&service); err != nil {
 c.JSON(400, err.Error())
 } else {
 // 从前端传过来的headers的token解析出当前用户
 user := models.CurrentUser(c)
 res := service.Create(user)
 c.JSON(200, res)
 }
}

service

package services
import (
 "mygin/e"
 "mygin/models"
 "mygin/serializers"
)
// 表单验证
type CreateCommentService struct {
 ReplyToID uint `json:"reply_to_id" form:"reply_to_id"`
 VideoID uint `json:"video_id" form:"video_id" binding:"required"`
 ParentID uint `json:"parent_id" form:"parent_id"`
 RootID uint `json:"root_id" form:"root_id"`
 Content string `json:"content" form:"content" binding:"required,min=3"`
}
func (service *CreateCommentService) Create(user *models.User) *serializers.Response {
 comment := models.Comment{
 UserID: user.ID,
 ReplyToID: service.ReplyToID,
 VideoID: service.VideoID,
 ParentID: service.ParentID,
 RootID: service.RootID,
 Content: service.Content,
 }
 if err := models.DB.Create(&comment).Error; err != nil {
 return &serializers.Response{
 Status: e.CREATE_ERROR,
 Message: e.GetMsg(e.CREATE_ERROR),
 Error: err.Error(),
 }
 }
 // 返回当前评论的用户和 replyTo用户(如果是回复的话)
 comment.User = *user
 if comment.ReplyToID != 0 {
 var replyToUser models.User
 if err := models.DB.Find(&replyToUser, service.ReplyToID).Error; err != nil {
 return &serializers.Response{
 Status:e.SELECT_ERROR,
 Message:e.GetMsg(e.SELECT_ERROR),
 Error:err.Error(),
 }
 }
 comment.ReplyTo = replyToUser
 return &serializers.Response{
 Status: e.SUCCESS,
 Message: e.GetMsg(e.SUCCESS),
 Data: serializers.BuildReply(comment),
 }
 }
 return &serializers.Response{
 Status: e.SUCCESS,
 Message: e.GetMsg(e.SUCCESS),
 Data: serializers.BuildComment(comment),
 }
}

效果:

深度录屏_google-chrome_20191024194933.gif

有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:aside section._1OhGeD

查看原文:vue+golang实现评论系统

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
2126 次点击
2 回复 | 直到 2019年11月08日 14:51:39
暂无回复
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏