社区话题项目实战 | 青训营笔记

102 阅读3分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天

一、本节课重点内容

1. 需求背景

2. 组件工具

3. 测试运行

二、详细知识点介绍

1. 需求背景

需求描述

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

需求用例

主要包括用户浏览话题内容和回帖列表,想象每个实体的属性有哪些以及它们之间的联系 Snipaste_2023-01-18_13-50-16.png

ER图

用来描述现实世界的概念模型,有了模型实体、属性以及之间的联系,对后续的开发提供比较清晰的思路 Snipaste_2023-01-18_13-53-00.png

分层结构

代码结构采用分层结构设计,包括repository数据层、service逻辑层、controller视图层。数据层关联底层数据模型,封装外部数据的增删改查,对逻辑层屏蔽底层数据差异,即不管底层是文件还是数据库还是微服务,同时对service逻辑层透明,接口模型不变。service逻辑层处理核心业务逻辑,计算打包业务实体entity并上送给视图层。controller视图层处理和外部的交互逻辑,以view视图形式返回给客户端 Snipaste_2023-01-18_13-59-56.png

2. 组件工具

高性能web框架gin v1.3.0版,主要涉及路由分发,使用go module依赖管理,按reposity、service、controller逐步实现

Repository

根据之前的ER图先定义Topic和Post结构体如下: Snipaste_2023-01-18_14-07-10.png 为了实现高效查询,使用map实现内存索引,在服务对外暴露之前,利用文件元数据初始化全局内存索引,可以实现时间复杂度为O(1)的查找操作 Snipaste_2023-01-18_14-10-01.png 迭代遍历数据行,转为结构体存储到map中

func initTopicIndexMap(filePath string) error {
	open, err := os.Open(filePath + "topic")
	if err != nil {
		return err
	}
	scanner := bufio.NewScanner(open)
	topicTmpMap := make(map[int64]*Topic)
	for scanner.Scan() {
		text := scanner.Text()
		var topic Topic
		if err := json.Unmarshal([]byte(text), &topic); err != nil {
			return err
		}
		topicTmpMap[topic.Id] = &topic
	}
	topicIndexMap = topicTmpMap
	return nil
}

查询则直接查询key获得value即可

Service

Service主要包括PageInfo结构体,它包含Topic和PostList

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

Service实现流程包括参数校验、准备数据、组装实体三部分

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
}

prepreinfo方法中,话题和回帖信息的获取都依赖tipicid,这样两者就可并行执行,提高效率,实际开发中要思考流程是否可并发,从而提高并发 Snipaste_2023-01-18_14-13-44.png

Controller

定义PageData结构体作为view对象,通过code和msg打包业务状态信息,data承载业务实体信息

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,
	}

}

Routerr

最后是web服务的引擎配置,path映射到具体controller,通过path变量传递话题id。过程包括初始化数据索引、初始化引擎配置、构建路由、启动服务

func main() {
	if err := Init("./data/"); err != nil {
		os.Exit(-1)
	}
	r := gin.Default()
	r.GET("/community/page/get/:id", func(c *gin.Context) {
		topicId := c.Param("id")
		data := cotroller.QueryPageInfo(topicId)
		c.JSON(200, data)
	})
	err := r.Run()
	if err != nil {
		return
	}
}

三、测试运行

本地go run运行,并使用curl请求服务暴露的接口 Snipaste_2023-01-18_14-16-32.png

四、课后个人总结

在课程中跟着视频从项目拆解,代码落地设计,最后测试运行一步步实现。在现实中,遇到复杂的项目时,也可以通过大拆小的思路,将大需求拆解为小需求的思路来分析问题,遇到问题,各个击破,同时也要做好充分的测试。