摘要
今天来记录一下青训营第二天课程中的一个项目。这个项目是一个小的后端项目,用来帮助初学者了解后端项目从项目需求->架构设计->编码测试的全过程。
项目需求
- 展示话题(标题,文字描述)和回帖列表;
- 暂不考虑前端页面实现,仅仅实现一个本地 web 服务;
- 话题和回帖数据用文件存储。
分层结构
后端项目在设计时往往采用分层结构来进行项目代码的设计,这样做逻辑清晰,职责明确,通过各层之间暴露的接口来进行合作,而不需要知道其他层的代码实现细节,有利于提高代码编写的效率。
- 主要分为以下几层:
- 数据层 (repository):数据 Model,外部数据的增删改查;
- 逻辑层 (service):业务 Entity,处理核心业务逻辑输出;
- 视图层 (controller):视图 view,处理和外部的交互逻辑。
组件工具
这个项目中会使用到的组件有
- gorm:用于操作数据库的CRUD
- gin:负责网络部分的路由,网络信息的发送和接收
项目搭建
项目整体的一个文件结构
- 在项目构建之初,我们用 go mod init 进行初始化,然后通过 go get gopkg.in/gin-gonic/g… 来远程拉取代码包及其依赖包,并自动完成编译和安装。
- 根据之前的需求分析对数据层 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 }
- 根据话题 Id 查询话题:QueryTopicById
- 在数据库中查询到数据后,将其传输到逻辑层 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 }
- publish_post
- 然后在视图层 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 } - 最后,利用 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 } - 初始化数据索引
- 初始化引擎配置
- 构建路由
- 启动服务