【Go + Nuxt.js 搭建一个 BBS 系统】10. 搭建跟帖模块
点击下面链接购买后可获取本课程完整源码,同时提供高大上的在线IDE开发环境,边看教程边动手。
- 购买地址:https://www.shiyanlou.com/courses/1436
- 购买九折优惠邀请码:
ZHwfIjb1
作者简介
大猫猫,互联网公司老码农、不折腾不舒服斯基,多年千万日活服务端研发和架构经验。关注公众号查看更多技术干货:
实验介绍
实验内容
上面我们完成了帖子模块的搭建,接下来我们来完成庚帖功能的搭建,跟帖搭建完成之后就能完成论坛的互动功能了。
知识点
- 跟帖模块服务端接口开发
- 跟帖模块Nuxt.js页面开发
服务端功能开发
同样的Go语言的服务端跟帖模块相关代码我们分为以下三部分:
- 结构体:comment_model
- 业务服务:comment_service
- 控制器:comment_controller
结构体(comment_model)
新建文件server/comment_model.go,在该文件中定义跟帖的结构体,完整代码如下:
package main
import "time"
type Comment struct {
Id int64 `gorm:"PRIMARY_KEY;AUTO_INCREMENT" json:"id"` // 编号
UserId int64 `gorm:"not null" json:"userId"` // 作者编号
TopicId int64 `gorm:"size:128;not null" json:"topicId"` // 帖子编号
Content string `gorm:"type:longtext;not null" json:"content"` // 内容
CreateTime time.Time `gorm:"not null" json:"createTime"` // 创建时间
}
跟帖结构体定义完成之后不要忘记将Comment结构体放入gorm的AutoMigrate中,这样启动服务的时候gorm会自动创建comment表,打开server/main.go,修改gorm配置如下:
db.AutoMigrate(&User{}, &UserToken{}, &Topic{}, &Comment{})
业务服务(comment_service)
新建文件server/comment_service.go,在该文件中我们去实现跟帖相关的业务逻辑,并进行数据库的读写操作,完整代码如下:
package main
import (
"time"
)
var CommentService = &commentService{}
type commentService struct {
}
// 创建评论
func (commentService) Create(userId, topicId int64, content string) (*Comment, error) {
comment := &Comment{
UserId: userId,
TopicId: topicId,
Content: content,
CreateTime: time.Now(),
}
if err := db.Create(comment).Error; err != nil {
return nil, err
}
return comment, nil
}
// 评论列表
func (commentService) List(topicId int64, page int) (comments []Comment, totalCount int) {
if page <= 0 {
page = 1
}
offset := 20 * (page - 1)
db.Where("topic_id = ?", topicId).Order("id desc").Offset(offset).Limit(20).Find(&comments) // 查列表
db.Where("topic_id = ?", topicId).Model(&Comment{}).Count(&totalCount) // 查计数
return
}
控制器(comment_controller)
新建文件server/comment_controller.go,该文件中定义跟帖相关接口,完整代码如下:
package main
import (
"github.com/kataras/iris/context"
"github.com/mlogclub/simple"
)
type CommentController struct {
Ctx context.Context
}
// 发表评论
func (this *CommentController) PostAdd() *simple.JsonResult {
// 获取当前登录用户,发表评论必须要求用户已经登录了
user := UserService.GetCurrent(this.Ctx)
if user == nil {
return simple.JsonErrorMsg("请先登录")
}
var (
topicId = this.Ctx.PostValueInt64Default("topicId", 0)
content = this.Ctx.FormValue("content")
)
// 校验数据
if topicId <= 0 {
return simple.JsonErrorMsg("请传入正确的帖子编号")
}
if len(content) == 0 {
return simple.JsonErrorMsg("请输入评论内容")
}
// 写入数据
comment, err := CommentService.Create(user.Id, topicId, content)
if err != nil {
return simple.JsonErrorMsg(err.Error())
}
return simple.NewRspBuilder(comment).Put("user", user).JsonResult()
}
// 评论列表
func (this *CommentController) GetList() *simple.JsonResult {
page := this.Ctx.URLParamIntDefault("page", 1)
topicId := this.Ctx.URLParamInt64Default("topicId", 0)
if topicId <= 0 {
return simple.JsonErrorMsg("请传入正确的帖子编号")
}
comments, totalCount := CommentService.List(topicId, page)
var commentList []map[string]interface{}
for _, comment := range comments {
// 将评论的作者信息查询出来,并放到评论信息中一起返回
user := UserService.Get(comment.UserId)
commentList = append(commentList, simple.NewRspBuilder(comment).Put("user", user).Build())
}
return simple.NewEmptyRspBuilder().Put("comments", commentList).Put("totalCount", totalCount).JsonResult()
}
完成comment_controller功能开发之后,需要将comment_controller添加到iris路由中,打开server/main.go文件,修改代码如下:
mvc.Configure(app.Party("/api/comment"), func(mvcApp *mvc.Application) {
mvcApp.Handle(new(CommentController))
})
启动服务
综上我们已经完成跟帖模块服务端相关接口开发,下面我们来启动服务,命令如下:
➜ server git:(master) ✗ go run *.go
Now listening on: http://localhost:8081
Application started. Press CMD+C to shut down.
Nuxt.js页面功能开发
跟帖功能是在帖子详情页中,所以我们只需要修改帖子详情页即可,我们同样在上个实验的基础上去修改。打开文件site/pages/topic/_id.vue并修改,下面我们先将修改后的完整代码贴出来,后面再对代码做一个讲解。完整代码如下:
<template>
<section>
<my-nav/>
<section class="section">
<div class="container">
<article>
<div class="title">{{ topic.title }} <span
class="meta">By {{topic.user.nickname}} @ {{topic.createTime}}</span></div>
<pre class="content">{{ topic.content }}</pre>
</article>
<hr/>
<div>
<div>
<div class="field">
<div class="control">
<textarea v-model="commentContent" class="textarea has-fixed-size" rows="3"
placeholder="请输入评论内容"></textarea>
</div>
</div>
<nav class="field">
<div class="control">
<button class="button is-link" @click="createComment">发表</button>
</div>
</nav>
</div>
<ul class="comments">
<li class="comment" v-for="comment in comments" :key="comment.id" :id="'comment-' + comment.id">
<div class="comment-left">
<img class="avatar" src="https://i.loli.net/2019/10/11/Emlz8DiAd6ZoG7U.png"/>
</div>
<div class="comment-right">
<div class="comment-content">
{{ comment.content }}
</div>
<div class="comment-meta">
By {{comment.user.nickname}} @ {{comment.createTime}}
</div>
</div>
</li>
</ul>
<nav class="pagination" role="navigation" aria-label="pagination">
<a class="pagination-previous"
:href="page > 1 ? '/topic/' + topic.id + '?page=' + (page - 1) : 'javascript:void(0)'">上一页</a>
<a class="pagination-next"
:href="page < maxPage ? '/topic/' + topic.id + '?page=' + (page + 1) : 'javascript:void(0)'">下一页</a>
</nav>
</div>
</div>
</section>
<my-footer/>
</section>
</template>
<script>
import MyNav from '~/components/MyNav'
import MyFooter from '~/components/MyFooter'
export default {
components: {
MyNav, MyFooter
},
data () {
return {
commentContent: '',
page: 1, // 当前页码
maxPage: 0 // 最大页码
}
},
async asyncData ({params, query, $axios}) {
const topicId = params.id // 从动态路由参数中获取帖子id
const page = query.page || 1 // 从query参数中获取页码,如果没获取到默认为1
const [topic, commentsResp] = await Promise.all([
$axios.get('http://localhost:8081/topic/' + topicId),
$axios.get('http://localhost:8081/comment/list', {
params: {
topicId: topicId,
page: page
}
})
])
const maxPage = commentsResp.totalCount % 20 > 0 ? parseInt(commentsResp.totalCount / 20) + 1 : commentsResp.totalCount / 20
return {
topic: topic,
comments: commentsResp.comments,
page: parseInt(page),
maxPage: maxPage
}
},
methods: {
async createComment () {
try {
const resp = await this.$axios.post('http://localhost:8081/comment/add', {
topicId: this.topic.id,
content: this.commentContent
})
// 计算跟帖的最大页码
this.maxPage = resp.totalCount % 20 > 0 ? parseInt(resp.totalCount / 20) + 1 : resp.totalCount / 20
// 发表成功后清空输入框内容
this.commentContent = ''
// 将放发表的评论显示在列表中
if (!this.comments) {
this.comments = []
}
this.comments.unshift(resp)
} catch (err) {
alert(err.message || err)
}
}
}
}
</script>
<style scoped>
article .title {
font-weight: bold;
font-size: 15px;
border-bottom: 2px #f7f8fb dashed;
padding-bottom: 10px;
}
article .title .meta {
font-weight: normal;
font-size: 12px;
color: #3b8070;
}
article .content {
margin-top: 10px;
}
.comments {
margin: 20px 0;
}
.comments .comment {
display: flex;
border-bottom: 2px solid #f7f8fb;
}
.comments .comment .avatar {
max-width: 50px;
max-height: 50px;
border-radius: 50%;
}
.comments .comment .comment-left {
margin-right: 10px;
}
.comments .comment .comment-right {
padding-top: 3px;
}
.comments .comment .comment-right .comment-content {
font-weight: bold;
}
.comments .comment .comment-right .comment-meta {
font-weight: bold;
font-size: 12px;
color: #3b8070;
}
</style>
帖子详情页中的跟帖是支持分页的,进去之后默认展示的是第一页的数据,如果要展示其他页码中的数据需要在url的query参数中指定页面,例如:/topic/1?page=2。Nuxt.js中我们是通过query来获取页面的,如下:
async asyncData ({params, query, $axios}) {
const page = query.page || 1 // 从query参数中获取页码,如果没获取到默认为1
...
}
在该页面的asyncData方法中有两个异步操作:获取帖子详情,获取评论列表,当有多个异步操作的时候,我们可以通过Promise.all来并行执行这些异步操作,当他们都执行成功之后统一处理返回结果,例如:
async asyncData ({params, query, $axios}) {
...
const [topic, commentsResp] = await Promise.all([
$axios.get('http://localhost:8081/topic/' + topicId),
$axios.get('http://localhost:8081/comment/list', {
params: {
topicId: topicId,
page: page
}
})
])
...
}
接下来我们执行npm run dev命令来启动site项目,就能看到效果啦。效果图如下:

总结
本实例中我们完成了跟帖功能。下面我们看下本实例完整源码的目录结构:
.
├── server
│ ├── comment_controller.go
│ ├── comment_model.go
│ ├── comment_service.go
│ ├── go.mod
│ ├── go.sum
│ ├── main.go
│ ├── topic_controller.go
│ ├── topic_model.go
│ ├── topic_service.go
│ ├── user_controller.go
│ ├── user_model.go
│ └── user_service.go
└── site
├── README.md
├── assets
│ └── README.md
├── components
│ ├── Logo.vue
│ ├── MyFooter.vue
│ ├── MyNav.vue
│ └── README.md
├── jsconfig.json
├── layouts
│ ├── README.md
│ └── default.vue
├── middleware
│ └── README.md
├── nuxt.config.js
├── package-lock.json
├── package.json
├── pages
│ ├── README.md
│ ├── index.vue
│ ├── topic
│ │ ├── _id.vue
│ │ └── create.vue
│ ├── topics
│ │ └── _page.vue
│ └── user
│ ├── login.vue
│ └── reg.vue
├── plugins
│ ├── README.md
│ └── axios.js
├── static
│ ├── README.md
│ └── favicon.ico
└── store
└── README.md
文章转载请注明出处,原文链接:https://mlog.club/topic/652
