这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记。 本篇笔记记录了课程中社区话题页面项目实现中的一些问题。
1.项目需求
- 展示话题(标题, 文字描述)和帖子列表(一个话题可以有多个列表)
- 仅仅实现一个本地web服务,不考虑前端页面实现
- 话题和帖子数据存在本地文件中
原始数据
话题和帖子的原始数据存储在本地文件中,每一行就是一条数据,以JSON的格式保存。
Topic数据
Post数据
Topic和Post的ER实体图
2.项目拆解
整个项目分为三个层面,数据层,逻辑层和业务层。数据层主要从文件中读取数据并封装到结构体中方便后续使用,然后把数据访问接口暴露给逻辑层。逻辑层主要实现核心业务,在本项目中就是根据话题id来查询相应的话题和帖子列表。视图层就是把查询到的数据渲染出来。
根据该分层结构,我们可以搭建项目目录如下
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