社区话题页面项目 | 青训营笔记

182 阅读4分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记。 本篇笔记记录了课程中社区话题页面项目实现中的一些问题。

1.项目需求

  • 展示话题(标题, 文字描述)和帖子列表(一个话题可以有多个列表)
  • 仅仅实现一个本地web服务,不考虑前端页面实现
  • 话题和帖子数据存在本地文件中

原始数据

话题和帖子的原始数据存储在本地文件中,每一行就是一条数据,以JSON的格式保存。

Topic数据 topic.png

Post数据 post.png Topic和Post的ER实体图

2022-05-09_111846.png

2.项目拆解

整个项目分为三个层面,数据层,逻辑层和业务层。数据层主要从文件中读取数据并封装到结构体中方便后续使用,然后把数据访问接口暴露给逻辑层。逻辑层主要实现核心业务,在本项目中就是根据话题id来查询相应的话题和帖子列表。视图层就是把查询到的数据渲染出来。

cengji.png 根据该分层结构,我们可以搭建项目目录如下

mulu.png

3.项目实现

3.1 Repository

首先原始数据保存在本地文件中,我们的目标是通过一个话题id来获取话题信息和相关的帖子信息。最简单的方法就是一行一行的扫描本地文件,对比id,把符合要求的话题数据和帖子数据找出来,封装到结构体中然后返回。但是该方法的性能不好,如果数据文件很大,那么查询就很耗时。所以我们把话题和帖子数据直接保存到内存中,这样就可以加快查询速度。当然,因为本项目的测试数据很少,所以可以直接全部加载进来,但是如果数据量很大,就只能加载一部分。

type Post struct {
   Id         int64  `json:"id"`
   ParentId   int64  `json:"parent_id"`
   Content    string `json:"content"`
   CreateTime int64  `json:"create_time"`
}
type PostDao struct{}


type Topic struct {
   Id         int64  `json:"id"`
   Title      string `json:"title"`
   Content    string `json:"content"`
   CreateTime int64  `json:"create_time"`
}
type TopicDao struct{}

将原始数据封装到结构体方便操作。每一个字段后面有一个标签,作用是为了把json数据对应key的值映射到结构体的字段中。因为本项目的需求比较简单,数据也比较简单,所以数据访问对象(DAO)是用一个空结构体实现的。

var topicOnce sync.Once
func NewTopicDaoInstance() *TopicDao {
   topicOnce.Do(
      func() {
         topicDao = &TopicDao{}
      })
   return topicDao
}

为了节省资源,这里使用了单例模式,即只会实例化一次数据访问对象。

repository另一个比较重要的部分就是如何把文件中的数据存储到内存中的map对象中。

func initPostListIndexMap(filePath string) error {
    // 打开文件
   file, err := os.Open(filePath + "post")
   //关闭文件
   defer file.Close()
   if err != nil {
      return err
   }
   scanner := bufio.NewScanner(file)
   postTmpMap := make(map[int64][]*Post)
   //逐行扫描数据,并添加到map中
   for scanner.Scan() {
      text := scanner.Text()
      var post Post
      //把json数据封装到post对象中
      if err := json.Unmarshal([]byte(text), &post); err != nil {
         return err
      }
      // 每一个话题id对应多个帖子,把同话题id的帖子放到一个列表中
      postTmpMap[post.ParentId] = append(postTmpMap[post.ParentId], &post)
   }
   postListIndexMap = postTmpMap
   return nil
}

3.2 Service

Service的流程分为三步:参数校验 ==> 准备数据 ==> 组装实体

type PageInfo struct {
   Topic    *repository.Topic
   PostList []*repository.Post
}

type QueryPageInfoFlow struct {
   topicId  int64
   pageInfo *PageInfo

   topic *repository.Topic
   posts []*repository.Post
}

// QueryPageInfoFlow的几个方法
func (f *QueryPageInfoFlow) Do() (*PageInfo, error)
func (f *QueryPageInfoFlow) checkParam() error //检查参数
func (f *QueryPageInfoFlow) prepareInfo() error //准备数据
func (f *QueryPageInfoFlow) packPageInfo() error // 组装实体

PageInfo结构体用于保存页面的话题信息和帖子信息,QueryPageInfoFlow结构体用于实现整个服务流程。

3.3 Controller

// 该结构体用于返回数据给调用者,包括状态码,信息,和数据
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(),
		}
	}
	pageInfo, err := service.QueryPageInfo(topicId)
	if err != nil {
		return &PageData{
			Code: -1,
			Msg:  err.Error(),
		}
	}

	return &PageData{
		Code: 0,
		Msg:  "success",
		Data: pageInfo,
	}
}

先获取话题id,然后根据id获取话题数据和帖子数据。

3.4 主函数

func main() {
    // 初始化,把文件中的数据加载到内存中的map来
   if err := Init("./data/"); err != nil {
      os.Exit(-1)
   }
   // 获取gin默认运行实体
   r := gin.Default()
   // 绑定路由和处理方法
   // GET()方法第一个参数为路由,第二个参数为处理方法,当你访问该路由就会调用该处理方法。
   r.GET("/community/page/get/:id", func(c *gin.Context) {
      topicId := c.Param("id")
      data := controller.QueryPageInfo(topicId)
      c.JSON(200, data)
   })
   err := r.Run()
   if err != nil {
      return
   }
}

func Init(filePath string) error {
	if err := repository.Init(filePath); err != nil {
		return err
	}
	return nil
}

Windows环境下运行程序时,把地址改为 http://127.0.0.1:8080/community/page/get/2