后端项目搭建流程 | 青训营笔记

41 阅读5分钟

摘要

今天来记录一下青训营第二天课程中的一个项目。这个项目是一个小的后端项目,用来帮助初学者了解后端项目从项目需求->架构设计->编码测试的全过程。

项目需求

  1. 展示话题(标题,文字描述)和回帖列表;
  2. 暂不考虑前端页面实现,仅仅实现一个本地 web 服务;
  3. 话题和回帖数据用文件存储。

分层结构

后端项目在设计时往往采用分层结构来进行项目代码的设计,这样做逻辑清晰,职责明确,通过各层之间暴露的接口来进行合作,而不需要知道其他层的代码实现细节,有利于提高代码编写的效率。

  • 主要分为以下几层:
    • 数据层 (repository):数据 Model,外部数据的增删改查;
    • 逻辑层 (service):业务 Entity,处理核心业务逻辑输出;
    • 视图层 (controller):视图 view,处理和外部的交互逻辑。

组件工具

这个项目中会使用到的组件有

  • gorm:用于操作数据库的CRUD
  • gin:负责网络部分的路由,网络信息的发送和接收

项目搭建

项目整体的一个文件结构 image.png

  1. 在项目构建之初,我们用 go mod init 进行初始化,然后通过 go get gopkg.in/gin-gonic/g… 来远程拉取代码包及其依赖包,并自动完成编译和安装。
  2. 根据之前的需求分析对数据层 Repository 的数据库部分进行代码编写,应该实现两个基本的查询操作:
    • 根据话题 Id 查询话题:QueryTopicById
      
      type Topic struct {
         Id         int64     `gorm:"column:id"`
         UserId     int64     `gorm:"column:user_id"`
         Title      string    `gorm:"column:title"`
         Content    string    `gorm:"column:content"`
         CreateTime time.Time `gorm:"column:create_time"`
      }
      
      func (Topic) TableName() string {
         return "topic"
      }
      
      type TopicDao struct {
      }
      
      var topicDao *TopicDao
      var topicOnce sync.Once
      
      // 单例模式
      func NewTopicDaoInstance() *TopicDao {
         topicOnce.Do(
            func() {
               topicDao = &TopicDao{}
            })
         return topicDao
      }
      
      func (*TopicDao) QueryTopicById(id int64) (*Topic, error) {
         var topic Topic
         err := db.Where("id = ?", id).Find(&topic).Error
         if err != nil {
            util.Logger.Error("find topic by id err:" + err.Error())
            return nil, err
         }
         return &topic, nil
      }
      
    • 根据话题 Id 查询所有帖子数据:QueryPostByParentId
      
      type Post struct {
         Id         int64     `gorm:"column:id"`
         ParentId   int64     `gorm:"parent_id"`
         UserId     int64     `gorm:"column:user_id"`
         Content    string    `gorm:"column:content"`
         DiggCount  int32     `gorm:"column:digg_count"`
         CreateTime time.Time `gorm:"column:create_time"`
      }
      
      func (Post) TableName() string {
         return "post"
      }
      
      type PostDao struct {
      }
      
      var postDao *PostDao
      var postOnce sync.Once
      
      func NewPostDaoInstance() *PostDao {
         postOnce.Do(
            func() {
               postDao = &PostDao{}
            })
         return postDao
      }
      
      func (*PostDao) QueryPostById(id int64) (*Post, error) {
         var post Post
         err := db.Where("id = ?", id).Find(&post).Error
         if err == gorm.ErrRecordNotFound {
            return nil, nil
         }
         if err != nil {
            util.Logger.Error("find post by id err:" + err.Error())
            return nil, err
         }
         return &post, nil
      }
      
      func (*PostDao) QueryPostByParentId(parentId int64) ([]*Post, error) {
         var posts []*Post
         err := db.Where("parent_id = ?", parentId).Find(&posts).Error
         if err != nil {
            util.Logger.Error("find posts by parent_id err:" + err.Error())
            return nil, err
         }
         return posts, nil
      }
      
      func (*PostDao) CreatePost(post *Post) error {
         if err := db.Create(post).Error; err != nil {
            util.Logger.Error("insert post err:" + err.Error())
            return err
         }
         return nil
      }
      
  3. 在数据库中查询到数据后,将其传输到逻辑层 Service 进行处理:参数校验 checkParam -> 准备数据 prepareInfo -> 组装实体 packPageInfo ;
    • publish_post
      
      func PublishPost(topicId, userId int64, content string) (int64, error) {
         return NewPublishPostFlow(topicId, userId, content).Do()
      }
      
      func NewPublishPostFlow(topicId, userId int64, content string) *PublishPostFlow {
         return &PublishPostFlow{
            userId:  userId,
            content: content,
            topicId: topicId,
         }
      }
      
      type PublishPostFlow struct {
         userId  int64
         content string
         topicId int64
      
         postId int64
      }
      
      func (f *PublishPostFlow) Do() (int64, error) {
         if err := f.checkParam(); err != nil {
            return 0, err
         }
         if err := f.publish(); err != nil {
            return 0, err
         }
         return f.postId, nil
      }
      
      func (f *PublishPostFlow) checkParam() error {
         if f.userId <= 0 {
            return errors.New("userId id must be larger than 0")
         }
         if utf8.RuneCountInString(f.content) >= 500 {
            return errors.New("content length must be less than 500")
         }
         return nil
      }
      
      func (f *PublishPostFlow) publish() error {
         post := &repository.Post{
            ParentId:   f.topicId,
            UserId:     f.userId,
            Content:    f.content,
            CreateTime: time.Now(),
         }
         if err := repository.NewPostDaoInstance().CreatePost(post); err != nil {
            return err
         }
         f.postId = post.Id
         return nil
      }
      
    • query_page
      
      type TopicInfo struct {
         Topic *repository.Topic
         User  *repository.User
      }
      
      type PostInfo struct {
         Post *repository.Post
         User *repository.User
      }
      
      type PageInfo struct {
         TopicInfo *TopicInfo
         PostList  []*PostInfo
      }
      
      func QueryPageInfo(topicId int64) (*PageInfo, error) {
         return NewQueryPageInfoFlow(topicId).Do()
      }
      
      func NewQueryPageInfoFlow(topId int64) *QueryPageInfoFlow {
         return &QueryPageInfoFlow{
            topicId: topId,
         }
      }
      
      type QueryPageInfoFlow struct {
         topicId  int64
         pageInfo *PageInfo
      
         topic   *repository.Topic
         posts   []*repository.Post
         userMap map[int64]*repository.User
      }
      
      func (f *QueryPageInfoFlow) Do() (*PageInfo, error) {
         if err := f.checkParam(); err != nil {
            return nil, err
         }
         if err := f.prepareInfo(); err != nil {
            return nil, err
         }
         if err := f.packPageInfo(); err != nil {
            return nil, err
         }
         return f.pageInfo, nil
      }
      
      func (f *QueryPageInfoFlow) checkParam() error {
         if f.topicId <= 0 {
            return errors.New("topic id must be larger than 0")
         }
         return nil
      }
      
      func (f *QueryPageInfoFlow) prepareInfo() error {
         //获取topic信息
         var wg sync.WaitGroup
         wg.Add(2)
         var topicErr, postErr error
         go func() {
            defer wg.Done()
            topic, err := repository.NewTopicDaoInstance().QueryTopicById(f.topicId)
            if err != nil {
               topicErr = err
               return
            }
            f.topic = topic
         }()
         //获取post列表
         go func() {
            defer wg.Done()
            posts, err := repository.NewPostDaoInstance().QueryPostByParentId(f.topicId)
            if err != nil {
               postErr = err
               return
            }
            f.posts = posts
         }()
         wg.Wait()
         if topicErr != nil {
            return topicErr
         }
         if postErr != nil {
            return postErr
         }
         //获取用户信息
         uids := []int64{f.topic.Id}
         for _, post := range f.posts {
            uids = append(uids, post.Id)
         }
         userMap, err := repository.NewUserDaoInstance().MQueryUserById(uids)
         if err != nil {
            return err
         }
         f.userMap = userMap
         return nil
      }
      
      func (f *QueryPageInfoFlow) packPageInfo() error {
         //topic info
         userMap := f.userMap
         topicUser, ok := userMap[f.topic.UserId]
         if !ok {
            return errors.New("has no topic user info")
         }
         //post list
         postList := make([]*PostInfo, 0)
         for _, post := range f.posts {
            postUser, ok := userMap[post.UserId]
            if !ok {
               return errors.New("has no post user info for " + fmt.Sprint(post.UserId))
            }
            postList = append(postList, &PostInfo{
               Post: post,
               User: postUser,
            })
         }
         f.pageInfo = &PageInfo{
            TopicInfo: &TopicInfo{
               Topic: f.topic,
               User:  topicUser,
            },
            PostList: postList,
         }
         return nil
      }
      
  4. 然后在视图层 Controller 将从逻辑层得到的数据显示为话题页面。
    • publish_post
    
    type PageData struct {
       Code int64       `json:"code"`
       Msg  string      `json:"msg"`
       Data interface{} `json:"data"`
    }
    
    func QueryPageInfo(topicIdStr string) *PageData {
       //参数转换
       topicId, err := strconv.ParseInt(topicIdStr, 10, 64)
       if err != nil {
          return &PageData{
             Code: -1,
             Msg:  err.Error(),
          }
       }
       //获取service层结果
       pageInfo, err := service.QueryPageInfo(topicId)
       if err != nil {
          return &PageData{
             Code: -1,
             Msg:  err.Error(),
          }
       }
       return &PageData{
          Code: 0,
          Msg:  "success",
          Data: pageInfo,
       }
    
    }
    
    • query_page
    
    type TopicInfo struct {
       Topic *repository.Topic
       User  *repository.User
    }
    
    type PostInfo struct {
       Post *repository.Post
       User *repository.User
    }
    
    type PageInfo struct {
       TopicInfo *TopicInfo
       PostList  []*PostInfo
    }
    
    func QueryPageInfo(topicId int64) (*PageInfo, error) {
       return NewQueryPageInfoFlow(topicId).Do()
    }
    
    func NewQueryPageInfoFlow(topId int64) *QueryPageInfoFlow {
       return &QueryPageInfoFlow{
          topicId: topId,
       }
    }
    
    type QueryPageInfoFlow struct {
       topicId  int64
       pageInfo *PageInfo
    
       topic   *repository.Topic
       posts   []*repository.Post
       userMap map[int64]*repository.User
    }
    
    func (f *QueryPageInfoFlow) checkParam() error {
       if f.topicId <= 0 {
          return errors.New("topic id must be larger than 0")
       }
       return nil
    }
    
    func (f *QueryPageInfoFlow) prepareInfo() error {
       //获取topic信息
       var wg sync.WaitGroup
       wg.Add(2)
       var topicErr, postErr error
       go func() {
          defer wg.Done()
          topic, err := repository.NewTopicDaoInstance().QueryTopicById(f.topicId)
          if err != nil {
             topicErr = err
             return
          }
          f.topic = topic
       }()
       //获取post列表
       go func() {
          defer wg.Done()
          posts, err := repository.NewPostDaoInstance().QueryPostByParentId(f.topicId)
          if err != nil {
             postErr = err
             return
          }
          f.posts = posts
       }()
       wg.Wait()
       if topicErr != nil {
          return topicErr
       }
       if postErr != nil {
          return postErr
       }
       //获取用户信息
       uids := []int64{f.topic.Id}
       for _, post := range f.posts {
          uids = append(uids, post.Id)
       }
       userMap, err := repository.NewUserDaoInstance().MQueryUserById(uids)
       if err != nil {
          return err
       }
       f.userMap = userMap
       return nil
    }
    
    func (f *QueryPageInfoFlow) packPageInfo() error {
       //topic info
       userMap := f.userMap
       topicUser, ok := userMap[f.topic.UserId]
       if !ok {
          return errors.New("has no topic user info")
       }
       //post list
       postList := make([]*PostInfo, 0)
       for _, post := range f.posts {
          postUser, ok := userMap[post.UserId]
          if !ok {
             return errors.New("has no post user info for " + fmt.Sprint(post.UserId))
          }
          postList = append(postList, &PostInfo{
             Post: post,
             User: postUser,
          })
       }
       f.pageInfo = &PageInfo{
          TopicInfo: &TopicInfo{
             Topic: f.topic,
             User:  topicUser,
          },
          PostList: postList,
       }
       return nil
    }
    
  5. 最后,利用 gin 这个框架来完成整个项目的实现。包括:
    • server代码
    
    func main() {
       if err := Init(); err != nil {
          os.Exit(-1)
       }
       r := gin.Default()
    
       r.Use(gin.Logger())
    
       r.GET("/ping", func(c *gin.Context) {
          c.JSON(200, gin.H{
             "message": "pong",
          })
       })
    
       r.GET("/community/page/get/:id", func(c *gin.Context) {
          topicId := c.Param("id")
          data := handler.QueryPageInfo(topicId)
          c.JSON(200, data)
       })
    
       r.POST("/community/post/do", func(c *gin.Context) {
          uid, _ := c.GetPostForm("uid")
          topicId, _ := c.GetPostForm("topic_id")
          content, _ := c.GetPostForm("content")
          data := handler.PublishPost(uid, topicId, content)
          c.JSON(200, data)
       })
       err := r.Run()
       if err != nil {
          return
       }
    }
    
    func Init() error {
       if err := repository.Init(); err != nil {
          return err
       }
       if err := util.InitLogger(); err != nil {
          return err
       }
       return nil
    }
    
  6. 初始化数据索引
  7. 初始化引擎配置
  8. 构建路由
  9. 启动服务