前言
本文内容是博客项目的后端代码逻辑的编写与实现,不了解的小伙伴可以去看前期的项目准备部分——(go + react) 博客项目(一)。之前提到我们已经通过gf框架的代码生成工具生成了接口,现在我们可以正式开始后端逻辑的编写。
关于框架说明:按照框架的设计思想,接口的具体实现是在/internal/controller
包中,service层的接口声明在/internal/service
包,service的具体实现代码存放在/internal/logic
包中。
service层代码
文章 article
在/internal/controller/article
包中,gf框架已经生成好了接口,只需要我们去实现了,可以注意到我们之前在api层定义的文件,也都在这里面有了对应的文件,具体的代码就在这些文件中编写
但是我们现在不关注controller中的内容,我们的工作从service层开始:在/internal/logic
包中创建article
文件夹,并创建"article.go"文件,声明出我们的articleService的结构体,后面文章相关的service函数都要声明为这个结构体的属性。
type sArticleService struct{}
创建文章
创建文章的数据校验比较简单,作者、标题、内容不为空就可以写入到数据库了,然后再把id拿出来返回。主要处理逻辑还是要放在logic层,在/internal/logic/article
包中创建"create_article.go"文件,进行代码编写。
func (s *sArticleService) CreateArticle(ctx context.Context, title string, content string, authorId int) (id int, err error) {
// 判断标题和内容是否为空
if err = s.checkTitleAndContent(title, content); err != nil {
return -1, err
}
// 判断作者是否存在
if err = s.checkUserExist(ctx, authorId); err != nil {
return -1, err
}
// 创建文章, 返回文章ID
aid, err := dao.LandcBlogArticle.Ctx(ctx).InsertAndGetId(&entity.LandcBlogArticle{
Title: title,
Content: content,
AuthorId: authorId,
})
if err != nil {
return -1, gerror.New("create article failed")
}
return int(aid), nil
}
func (s *sArticleService) checkTitleAndContent(title string, content string) error {
if gstr.Trim(title) == "" {
return gerror.New("title is empty")
}
if gstr.Trim(content) == "" {
return gerror.New("content is empty")
}
return nil
}
func (s *sArticleService) checkUserExist(ctx context.Context, userId int) error {
count, err := dao.LandcBlogUser.Ctx(ctx).Where("id = ?", userId).Count()
if err != nil {
return err
}
if count == 0 {
return gerror.New("user not exist")
}
return nil
}
注意:其中形参中ctx的类型是Contenxt,可能存在多个包,我们选context包:
import context
删除文章
删除文章需要校验文章是否存在,用户是否存在,以及文章是否为用户所编写的内容,成功则返回true,失败返回false并说明原因。在/internal/logic/article
包中创建"delete_article.go"文件,进行代码编写。
func (s *sArticleService) DeleteArticle(ctx context.Context, id int, authorId int) (success bool, info string, err error) {
// 判断文章是否存在
if info, err = s.checkArticleExist(ctx, id); err != nil || info != "" {
return false, "", err
}
// 判断作者是否存在
if err = s.checkUserExist(ctx, authorId); err != nil {
return false, "", err
}
// 删除文章
result, err := dao.LandcBlogArticle.Ctx(ctx).Where("id = ?", id).Where("author_id = ?", authorId).Delete()
if err != nil {
return false, "", err
}
// 根据影响行数判断是否成功
affected, err := result.RowsAffected()
if err != nil {
return false, "", err
}
// 如果影响行数是0,说明数据库中没有对应的数据
if affected == 0 {
return false, "无权限删除文章", nil
}
return true, "", nil
}
func (s *sArticleService) checkArticleExist(ctx context.Context, id int) (info string, err error) {
count, err := dao.LandcBlogArticle.Ctx(ctx).Where("id = ?", id).Count()
if err != nil {
return "", err
}
if count == 0 {
return "article not exist", nil
}
return "", nil
}
修改文章
修改文章的校验内容比较多,文章是否存在,作者是否存在,文章是否为作者所写,文章的标题和内容是否为空。在/internal/logic/article
包中创建"modify_article.go"文件,进行代码编写。
func (s *sArticleService) ModifyArticle(ctx context.Context, id int, title string, content string, authorId int) (success bool, info string, err error) {
// 判断标题和内容是否为空
if err = s.checkTitleAndContent(title, content); err != nil {
return false, "", err
}
// 判断作者是否存在
if err = s.checkUserExist(ctx, authorId); err != nil {
return false, "", err
}
// 判断文章是否存在
if info, err = s.checkArticleExist(ctx, authorId); err != nil || info != "" {
return false, info, err
}
// 修改文章,根据影响行数判断文章和作者是否匹配
affected, err := dao.LandcBlogArticle.Ctx(ctx).UpdateAndGetAffected(&entity.LandcBlogArticle{
Id: id,
Title: title,
Content: content,
AuthorId: authorId,
}, "id = ?", id)
if err != nil {
return false, "", err
}
if affected <= 0 {
return false, "无权限修改文章", nil
}
return true, "", nil
}
查询文章内容
查询文章内容简单一些,只需要校验文章是否存在即可,由于我们还需要向前端返回用户信息,因此service层除了查询文章,还需要查询用户信息。在/internal/logic/article
包中创建"describe_article.go"文件,进行代码编写。
func (s *sArticleService) DescribeArticle(ctx context.Context, id int) (article *entity.LandcBlogArticle, author *entity.LandcBlogUser, err error) {
// 判断文章是否存在
if info, err := s.checkArticleExist(ctx, id); err != nil || info != "" {
if err != nil {
return nil, nil, err
}
return nil, nil, gerror.New(info)
}
err = dao.LandcBlogArticle.Ctx(ctx).Where("id = ?", id).Scan(&article)
if err != nil {
return nil, nil, err
}
err = dao.LandcBlogUser.Ctx(ctx).Where("id = ?", article.AuthorId).Scan(&author)
if err != nil {
return nil, nil, err
}
return article, author, nil
}
查询文章列表
由于查询文章列表的复用,我们需要判断用户id的合法性,以及对searchKey做模糊查询,在/internal/logic/article
包中创建"describe_article_list.go"文件,进行代码编写。
func (s *sArticleService) DescribeArticleList(ctx context.Context, id int, searchKey string) (articleAndAuthor *gmap.AnyAnyMap, err error) {
articleOrm := dao.LandcBlogArticle.Ctx(ctx)
var author *entity.LandcBlogUser
// 判断用户id的合法性
if id > 0 {
record, err := dao.LandcBlogUser.Ctx(ctx).Where("id = ?", id).One()
if err != nil {
return nil, err
}
if record == nil {
return nil, gerror.New("用户不存在")
}
err = record.Struct(author)
if err != nil {
return nil, err
}
articleOrm = articleOrm.Where("author_id = ?", id)
}
if searchKey != "" {
articleOrm = articleOrm.Where("title like ?", "%"+searchKey+"%")
articleOrm = articleOrm.WhereOr("content like ?", "%"+searchKey+"%")
}
articleOrm = articleOrm.Order("id desc")
articleResult, err := articleOrm.All()
if err != nil {
return nil, err
}
articleAndAuthor = gmap.NewAnyAnyMap()
for _, v := range articleResult {
var article *entity.LandcBlogArticle
err := v.Struct(article)
if err != nil {
return nil, err
}
if id > 0 {
articleAndAuthor.Set(&article, &author)
} else {
err = dao.LandcBlogUser.Ctx(ctx).Where("id = ?", article.AuthorId).Scan(author)
if err != nil {
return nil, err
}
articleAndAuthor.Set(&article, &author)
}
}
return articleAndAuthor, nil
}
用户 user
像之前一样,在/internal/logic
包下面创建"user"包,再在"user"下面创建"user.go",然后声明好user的service结构体。再次声明,用户模块的实现极为简单,仅做练习使用。
type sUserService struct{}
创建用户
创建用户的检查和创建文章的检查大致相同,检查用户名和密码是否为空,用户还要多加一条用户名唯一性的检验,在/internal/logic/user
文件夹下创建"create_user.go"文件。
func (s *sUserService) CreateUser(ctx context.Context, name string, password string) (id int, err error) {
if err := s.checkNameAndPassword(name, password); err != nil {
return -1, err
}
if err := s.checkNameExist(ctx, name); err != nil {
return -1, err
}
uid, err := dao.LandcBlogUser.Ctx(ctx).InsertAndGetId(&entity.LandcBlogUser{
Name: name,
Password: password,
})
if err != nil {
return -1, err
}
return int(uid), nil
}
func (s *sUserService) checkNameAndPassword(name string, password string) error {
if gstr.Trim(name) == "" {
return gerror.New("name is empty")
}
if gstr.Trim(password) == "" {
return gerror.New("password is empty")
}
return nil
}
func (s *sUserService) checkNameExist(ctx context.Context, name string) error {
if count, err := dao.LandcBlogUser.Ctx(ctx).Where("name = ?", name).Count(); err != nil || count > 0 {
if err != nil {
return err
}
return gerror.New("name already exist")
}
return nil
}
删除用户
删除用户简单实现就可以了,判断用户id是否存在,存在即可删除。在/internal/logic/user
文件夹下创建"delete_user.go"文件。
func (s *sUserService) DeleteUser(ctx context.Context, id int) (success bool,info string, err error) {
result, err := dao.LandcBlogUser.Ctx(ctx).Where("id = ?", id).Delete()
if err != nil {
return false, "", err
}
affected, err := result.RowsAffected()
if err != nil {
return false, "", err
}
if affected == 0 {
return false, "用户不存在", nil
}
return true, "", nil
}
修改用户
修改用户需要校验用户id是否存在,以及用户名是是否重复。在/internal/logic/user
文件夹下创建"modify_user.go"文件。
func (s *sUserService) ModifyUser(ctx context.Context, id int, name string, password string) (success bool, info string, err error) {
if err := s.checkNameAndPassword(name, password); err != nil {
return false, "", err
}
if err := s.checkNameExist(ctx, name); err != nil {
var tmpId int32
one, tempErr := dao.LandcBlogUser.Ctx(ctx).Where("name = ?", name).Fields("id").One()
if tempErr != nil {
return false, "", tempErr
}
tmpId = one.GMap().Get("id").(int32)
if int(tmpId) != id {
return false, "", err
}
}
affected, err := dao.LandcBlogUser.Ctx(ctx).UpdateAndGetAffected(&entity.LandcBlogUser{Name: name, Password: password}, "id = ?", id)
if err != nil {
return false, "", err
}
if affected == 0 {
return false, "用户不存在", nil
}
return true, "", nil
}
查询/登录
使用用户名和密码查询出用户id,返回回去,如果查询不到就是用户名或密码出现错误。在/internal/logic/user
文件夹下创建"describe_user.go"文件。
func (s *sUserService) DescribeUser(ctx context.Context, name string, password string) (id int, err error) {
user := &entity.LandcBlogUser{}
err = dao.LandcBlogUser.Ctx(ctx).Where("name = ?", name).Where("password = ?", password).Scan(user)
if err != nil {
return 0, gerror.New("用户名或密码错误")
}
return user.Id, nil
}
评论 comment
在/internal/logic
包下面创建"comment"包,再在"comment"下面创建"comment.go",然后声明好comment的service结构体。
type sCommentService struct {}
创建评论
创建评论需要检验用户以及文章的合法性,除此之外还要注意parentId的合法性,如果parentId不存在或者两个文章id不同,是不允许创建的,再就是comment的内容不可以为空。在/internal/logic/comment
文件夹下创建"create_comment.go"文件。
func (s *sCommentService) CreateComment(ctx context.Context, articleId int, authorId int, content string, parentId int) (id int, err error) {
count, err := dao.LandcBlogArticle.Ctx(ctx).Where("id = ?", articleId).Count()
if err != nil {
return 0, err
}
if count == 0 {
return 0, gerror.New("article not found")
}
count, err = dao.LandcBlogUser.Ctx(ctx).Where("id = ?", authorId).Count()
if err != nil {
return 0, err
}
if count == 0 {
return 0, gerror.New("user not found")
}
if parentId > 0 {
var parent = &entity.LandcBlogComment{}
err := dao.LandcBlogComment.Ctx(ctx).Where("id = ?", parentId).Scan(parent)
if err != nil {
return 0, err
}
if parent == nil {
return 0, gerror.New("parent comment not found")
}
if parent.ArticleId != articleId {
return 0, gerror.New("parent comment not belong to article")
}
}
if gstr.Trim(content) == "" {
return 0, gerror.New("content is empty")
}
insertId, err := dao.LandcBlogComment.Ctx(ctx).InsertAndGetId(entity.LandcBlogComment{
ArticleId: articleId,
AuthorId: authorId,
Content: content,
ParentId: parentId,
})
if err != nil {
return 0, err
}
return int(insertId), nil
}
删除评论
删除评论没有什么好说的,和之前的删除逻辑基本一致。在/internal/logic/comment
文件夹下创建"delete_comment.go"文件。
func (s *sCommentService) DeleteComment(ctx context.Context, id int, authorId int) (success bool, info string, err error) {
result, err := dao.LandcBlogComment.Ctx(ctx).Where("id = ? and author_id = ?", id, authorId).Delete()
if err != nil {
return false, "", err
}
affected, err := result.RowsAffected()
if err != nil {
return false, "", err
}
if affected == 0 {
return false, "无权限删除", nil
}
return true, "", nil
}
获取评论列表
在获取评论列表的时候就比较复杂了,检查文章存不存在,然后就是关系的组装,创建一个list用来存放文章的评论,再便利文章下所有的评论,根据parentId找到父评论添加到children列表中,为了方便寻找添加了一个map。在/internal/logic/comment
文件夹下创建"describe_comment_list.go"文件。
func (s *sCommentService) DescribeCommentList(ctx context.Context, aid int) (commentList []v1.CommentItem, err error) {
count, err := dao.LandcBlogArticle.Ctx(ctx).Where("id=?", aid).Count()
if err != nil {
return nil, err
}
if count == 0 {
return nil, gerror.New("文章不存在")
}
array, err := dao.LandcBlogComment.Ctx(ctx).Where("article_id=?", aid).OrderAsc("id").All()
if err != nil {
return nil, err
}
commentList = make([]v1.CommentItem, 0)
commentMap := gmap.NewIntAnyMap()
for _, v := range array {
var (
ormItem = &entity.LandcBlogComment{}
author = &entity.LandcBlogUser{}
item v1.CommentItem
itemPtr *v1.CommentItem
)
err := v.Struct(ormItem)
if err != nil {
return nil, err
}
err = dao.LandcBlogUser.Ctx(ctx).Where("id=?", ormItem.AuthorId).Scan(&author)
if err != nil || author == nil {
continue
}
item.Id = ormItem.Id
item.AuthorName = author.Name
item.Content = ormItem.Content
item.AuthorId = author.Id
item.ChildrenList = make([]v1.CommentItem, 0)
if ormItem.ParentId <= 0 {
commentList = append(commentList, item)
itemPtr = &(commentList[len(commentList)-1])
} else {
value := commentMap.Get(ormItem.ParentId)
if value == nil {
continue
}
commentItem := value.(*v1.CommentItem)
commentItem.ChildrenList = append(commentItem.ChildrenList, item)
item = commentList[len(commentItem.ChildrenList)-1]
}
commentMap.Set(ormItem.Id, itemPtr)
}
return commentList, nil
}
生成service接口
到此,所有的logic层代码都变写完了,现在需要声明接口,可以通过gf的代码生成工具实现,在项目文件夹中输入以下命令,就可以在service包中生成接口。
gf gen service
生成好service层接口之后,需要将logic层的实现注册进去,这样才能在controller层调用,在"/internal/logic/article/article.go"、"/internal/logic/user/user.go"、"/internal/logic/comment/comment.go"
这三个文件中分别加入以下代码:
// article.go
func init() {
service.RegisterArticleService(&sArticleService{})
}
// user.go
func init() {
service.RegisterUserService(&sUserService{})
}
//comment.go
func init() {
service.RegisterCommentService(&sCommentService{})
}
这样,所有service层的代码算是全部结束了,可以开始controller层的代码了
controller层代码
controller的代码其实比较简单了,就只是调用service层代码然后将返回值封装成res就可以了,进入/internal/controller
包,完成每个函数的实现,在这里不进行具体解释了,只贴出代码。
文章
article_v1_create_article.go
func (c *ControllerV1) CreateArticle(ctx context.Context, req *v1.CreateArticleReq) (res *v1.CreateArticleRes, err error) {
id, err := service.ArticleService().CreateArticle(ctx, req.Title, req.Content, req.AuthorId)
if err != nil {
return nil, err
}
return &v1.CreateArticleRes{Id: id}, nil
}
article_v1_delete_article.go
func (c *ControllerV1) DeleteArticle(ctx context.Context, req *v1.DeleteArticleReq) (res *v1.DeleteArticleRes, err error) {
success, info, err := service.ArticleService().DeleteArticle(ctx, req.Id, req.AuthorId)
if err != nil {
info = err.Error()
}
res = &v1.DeleteArticleRes{
Success: success,
Info: info,
}
return
}
article_v1_describe_article.go
func (c *ControllerV1) DescribeArticle(ctx context.Context, req *v1.DescribeArticleReq) (res *v1.DescribeArticleRes, err error) {
article, author, err := service.ArticleService().DescribeArticle(ctx, req.Id)
if err != nil {
return nil, err
}
res = &v1.DescribeArticleRes{
Id: article.Id,
Title: article.Title,
Content: article.Content,
AuthorId: author.Id,
AuthorName: author.Name,
}
return res, nil
}
article_v1_describe_article_list.go
func (c *ControllerV1) DescribeArticleList(ctx context.Context, req *v1.DescribeArticleListReq) (res *v1.DescribeArticleListRes, err error) {
articleAndAuthor, err := service.ArticleService().DescribeArticleList(ctx, req.Id, req.SearchKey)
if err != nil {
return nil, err
}
res = &v1.DescribeArticleListRes{}
res.ArticleList = make([]v1.ArticleListItem, articleAndAuthor.Size())
for article, author := range articleAndAuthor.Map() {
blogArticle := *(article.(**entity.LandcBlogArticle))
user := *(author.(**entity.LandcBlogUser))
res.ArticleList = append(res.ArticleList, v1.ArticleListItem{
Id: blogArticle.Id,
Title: blogArticle.Title,
AuthorName: user.Name,
})
}
return
}
article_v1_modify_article.go
func (c *ControllerV1) ModifyArticle(ctx context.Context, req *v1.ModifyArticleReq) (res *v1.ModifyArticleRes, err error) {
success, info, err := service.ArticleService().ModifyArticle(ctx, req.Id, req.Title, req.Content, req.AuthorId)
if err != nil {
info = err.Error()
}
res = &v1.ModifyArticleRes{
Success: success,
Info: info,
}
return
}
用户
user_v1_create_user.go
func (c *ControllerV1) CreateUser(ctx context.Context, req *v1.CreateUserReq) (res *v1.CreateUserRes, err error) {
id, err := service.UserService().CreateUser(ctx, req.Name, req.Password)
if err != nil {
return nil, err
}
return &v1.CreateUserRes{Id: id}, nil
}
user_v1_delete_user.go
func (c *ControllerV1) DeleteUser(ctx context.Context, req *v1.DeleteUserReq) (res *v1.DeleteUserRes, err error) {
success, info, err := service.UserService().DeleteUser(ctx, req.Id)
if err != nil {
info = err.Error()
}
res = &v1.DeleteUserRes{
Success: success,
Info: info,
}
return
}
user_v1_describe_user.go
func (c *ControllerV1) DescribeUser(ctx context.Context, req *v1.DescribeUserReq) (res *v1.DescribeUserRes, err error) {
id, err := service.UserService().DescribeUser(ctx, req.Name, req.Password)
if err != nil {
return nil, err
}
res = &v1.DescribeUserRes{
Id: id,
}
return res, nil
}
user_v1_modify_user.go
func (c *ControllerV1) ModifyUser(ctx context.Context, req *v1.ModifyUserReq) (res *v1.ModifyUserRes, err error) {
success, info, err := service.UserService().ModifyUser(ctx, req.Id, req.Name, req.Password)
if err != nil {
info = err.Error()
}
res = &v1.ModifyUserRes{
Success: success,
Info: info,
}
return
}
评论
comment_v1_create_comment.go
func (c *ControllerV1) CreateComment(ctx context.Context, req *v1.CreateCommentReq) (res *v1.CreateCommentRes, err error) {
id, err := service.CommentService().CreateComment(ctx, req.ArticleId, req.AuthorId, req.Content, req.ParentId)
if err != nil {
return nil, err
}
return &v1.CreateCommentRes{Id: id}, nil
}
comment_v1_delete_comment.go
func (c *ControllerV1) DeleteComment(ctx context.Context, req *v1.DeleteCommentReq) (res *v1.DeleteCommentRes, err error) {
success, info, err := service.CommentService().DeleteComment(ctx, req.Id, req.AuthorId)
if err != nil {
info = err.Error()
}
res = &v1.DeleteCommentRes{
Success: success,
Info: info,
}
return
}
comment_v1_describe_comment_list.go
func (c *ControllerV1) DescribeCommentList(ctx context.Context, req *v1.DescribeCommentListReq) (res *v1.DescribeCommentListRes, err error) {
list, err := service.CommentService().DescribeCommentList(ctx, req.Aid)
if err != nil {
return nil, err
}
res = &v1.DescribeCommentListRes{
CommentList: list,
}
return res, nil
}
环境准备
代码写了这么久,都还没有进行配置,打开/manifest/config/config.yaml
文件,在里面配置好数据库连接,最终效果大概如下所示:
server:
address: ":8000"
openapiPath: "/api.json"
swaggerPath: "/swagger"
logger:
level : "all"
stdout: true
database:
link: "mysql:root:12345678@tcp(localhost:3306)/landc_new_blog?parseTime=true"
再打开/internal/cmd/cmd.go
文件,进行路由配置:
var (
Main = gcmd.Command{
Name: "main",
Usage: "main",
Brief: "start http server",
Func: func(ctx context.Context, parser *gcmd.Parser) (err error) {
s := g.Server()
s.Use(ghttp.MiddlewareHandlerResponse)
s.Group("/article", func(group *ghttp.RouterGroup) {
group.Bind(article.NewV1())
})
s.Group("/user", func(group *ghttp.RouterGroup) {
group.Bind(user.NewV1())
})
s.Group("/comment", func(group *ghttp.RouterGroup) {
group.Bind(comment.NewV1())
})
s.Run()
return nil
},
}
)
配置好之后运行,在控制台看到打印接口,就可以进行访问了
结语
本来以为很快就能写出这篇,但是有很多事情耽搁了,跟大家说一声对不起。本期内容并没有太多的东西,大部分只是代码的展示,下一篇将会开始前端的搭建,希望能够很快搭建完。后续代码会上传到github和gitee上,上传之后我会将连接更新到本篇中,大家可以关注一下。