第一个项目实战 | 青训营笔记

155 阅读4分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第一篇笔记

1、需求背景

社区话题页面

image.png

2、需求描述

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

3、需求用例

3.1、浏览消费者

每一个访问浏览的人都可以查看话题和话题对应的评论列表

image.png

4、ER图-Entity Relationship Diagram

通过ER图能够更清晰的看出两者之间的关联

image.png

5、分层结构

结构分层能够使你的项目更加整洁,方便后期维护(问题排查、新增函数、实现新功能等等)

image.png

数据层:数据Model,外部数据的增删改查

逻辑层:业务Entity,处理核心业务逻辑输出

视图层:视图view,处理和外部的交互逻辑

6、组件工具

6.1、Gin高性能go web框架

github.com/gin-gonic/g…

6.2、Go Mod

go mod init

go get gopkg.in/gin-gonic/gin.v1@v1.3.0

7、Repository

7.1、数据格式

json数据格式

7.2、Repository查询

两个基本的查询操作:topic:通过id查询 post:通过topicid查询

image.png

image.png

7.3、实现方式:map索引

image.png

7.4、初始化话题数据索引
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
}
7.5、初始化回复数据索引
func InitPostIndexMap(filePath string) error {
    open, err := os.Open(filePath + "post")
    if err != nil {
        return err
    }
    scanner := bufio.NewScanner(open)
    postTmpMap := make(map[int64][]*Post)
    for scanner.Scan() {
        text := scanner.Text()
        var post Post
        if err := json.Unmarshal([]byte(text), &post); err != nil {
            return err
        }
        postTmpMap[post.TopicId] = append(postTmpMap[post.TopicId], &post)
    }
    PostIndexMap = postTmpMap
    return nil
}

8、Service

8.1、实体
//文章详情结构体
type PageInfo struct {
	Topic    *repository.Topic
	PostList []*repository.Post
}
8.2、流程

image.png

package service

import (
	"errors"
	"myproject/repository"
	"sync"
)
//操作流程对象
type QueryPageInfoFlow struct {
	PageInfo PageInfo
}
//执行操作
func (f *QueryPageInfoFlow) Do(topicId int64) (*PageInfo, error) {
    //参数校验
	if err := f.checkParam(topicId); err != nil {
		return nil, err
	}
    //准备数据
	if err := f.prepareInfo(topicId); err != nil {
		return nil, err
	}
    //组装实体
	if err := f.packPageInfo(); err != nil {
		return nil, err
	}
	return &f.PageInfo, nil
}
//参数校验的方法
func (f *QueryPageInfoFlow) checkParam(topicId int64) error {
	if _, ok := repository.TopicIndexMap[topicId]; !ok {
		return errors.New("topic is not exist")
	}
	return nil
}
//准备数据的方法
func (f *QueryPageInfoFlow) prepareInfo(topicId int64) error {
	var wg sync.WaitGroup
	wg.Add(2)
	//获取topic信息
	go func() {
		topicDao := repository.NewTopicDaoInstance()
		f.PageInfo.Topic = topicDao.QueryTopicById(topicId)
		defer wg.Done()
	}()
	//获取post列表
	go func() {
		defer wg.Done()
		postDao := repository.NewPostDaoInstance()
		f.PageInfo.PostList = postDao.QueryPostByTopicId(topicId)
	}()
	wg.Wait()
	return nil
}
//组装数据的方法
func (f *QueryPageInfoFlow) packPageInfo() error {
	return nil
}

tips:个人觉得准备数据和组装数据可以合并在一起

8.3、可用性

采用并行处理

image.png

9、Controller

9.1、构建业务对象
type PageData struct {
	Code int64       `json:"Code,omitempty"` //业务错误码:-1错误,0正确
	Msg  string      `json:"Msg,omitempty"`
	Data interface{} `json:"Data,omitempty"`
}
9.2、定义相关函数
func QueryPageInfo(topicIdstr string) *PageData {
	f = new(service.QueryPageInfoFlow)
	topicId, err := strconv.ParseInt(topicIdstr, 10, 64)
	if err != nil {
		return &PageData{-1, "failed", nil}
	}
	pageInfo, err := f.Do(topicId)
	if err != nil {
		return &PageData{-1, "failed", nil}
	}
	return &PageData{0, "success", pageInfo}
}
func PublishPost(topicIdstr string, content string) *PageData {
	f = new(service.QueryPageInfoFlow)
	topicId, err := strconv.ParseInt(topicIdstr, 10, 64)
	if err != nil {
		return &PageData{-1, "failed", nil}
	}
	post := repository.Post{
		Id:         util.CreateId(),
		TopicId:    topicId,
		Content:    content,
		CreateTime: time.Now(),
	}
	err = repository.SavePost("./data/", post)
	if err != nil {
		return &PageData{-1, "failed", nil}
	}
	pageInfo, err := f.Do(topicId)
	if err != nil {
		return &PageData{-1, "failed", nil}
	}
	return &PageData{0, "success", pageInfo}
}

10、Router

10.1、初始化数据索引
10.2、初始化引擎配置
10.3、构建路由
10.4、启动服务

由于此项目业务逻辑较简单,路由较少,所以均放在了main函数里

package main

import (
	"github.com/gin-gonic/gin"
	"myproject/controller"
	"myproject/repository"
	"os"
)

func main() {
    //初始化数据索引
	if err := Init("./data/"); err != nil {
		os.Exit(-1)
	}
    
	//insertdata()插入测试数据
    
    //初始化引擎配置
	r := gin.Default()
    //构建路由
	r.GET("/community/page/get/:id", func(c *gin.Context) {
		topicId := c.Param("id")
		data := controller.QueryPageInfo(topicId)
		c.JSON(200, data)
	})
	r.POST("/community/post/do", func(c *gin.Context) {
		topicId, _ := c.GetPostForm("topic_id")
		content, _ := c.GetPostForm("content")
		data := controller.PublishPost(topicId, content)
		c.JSON(200, data)
	})
    //启动服务默认8080端口
	err := r.Run()
	if err != nil {
		return
	}
}
//初始化数据索引
func Init(filePath string) error {
	if err := repository.InitTopicIndexMap(filePath); err != nil {
		return err
	}
	if err := repository.InitPostIndexMap(filePath); err != nil {
		return err
	}
	return nil
}

11、效果演示

11.1、查询topic

image.png

11.2、发布post

image.png

12、总结

根据老师给出的框架,对相关逻辑和函数的完善和实现,最终实现了基本的话题查询和话题评论两个功能。通过本项目,我们可以学到一般项目的开发流程和框架设计。本项目的难点在于各结构体方法的设计,需要注意的是map的并发安全,可以用sync的锁来解决这个问题。

最后奉上源码:github.com/zzyzzyzzyzz…

如有不足,欢迎指正!