# vblog **Repository Path**: hong_golang/code ## Basic Information - **Project Name**: vblog - **Description**: The Gopher way - **Primary Language**: Go - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2023-05-19 - **Last Updated**: 2023-06-25 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 微博客 前后端分离的博客系统,是一个单体应用 # 产品 用户定位 + 访客:浏览博客的人 + 作者:写博客的人 [产品原型](./docs/prd.drawio) # 架构 [技术架构](./docs/design.drawio) # 业务逻辑代码组织方式: 业务代码组织风格 + MVC 分层架构:比较传统的代码组织风格 + 微服务渐进架构:微服务的孵化,在业务开展之前,尽量少的产生划分服务(2~5个服务) + DDD分区架构(Domain Driven Develop):域(邻域):一个业务单元(商品管理/订单管理),简单理解为一个业务分区 基于DDD和TDD的功能分区架构:[Blog业务实现流程](./apps/blog/docs/object.drawio) 代码开发的2种方式: + 从上往下 进行设计,顶层设计 + 从下往上 做业务的具体实现 # RESTful接口设计 (representation state transfer) //文章管理 API, 同时设计业务接口(需要暴露成HTTP 的 RESTful接口) [业务接口定义](./apps/blog/interface.go) HTTP 接口 只是把业务接口的能力,通过HTTP协议对外进行暴露 GET /vblog/api/v1/blogs 获取博客列表 POST /vblog/api/v1/blogs 创建博客 参数通过Body传入 + [创建请求](./docs/api/create_blog.json) GET /vblog/api/v1/blogs/:id 获取单个文章 PUT /vblog/api/v1/blogs/:id 更新单个文章 + [修改请求](./docs/api/create_blog.json) Delete /vblog/api/v1/blogs/:id 删除单个文章 需要定义一个Blog的数据结构 # 数据库的设计 文章属性:Blog 文章的元数据 + 文章Id + 创建时间 + 发布时间 + 修改时间 用户传入的数据 + 标题 + 作者 + 内容(Markdown) + 标签(map) # 项目开发 项目骨架做定义(流程从上到下) + 业务处理模块:每个业务一个模块:[apps](./apps/README.md) + 项目的配置管理:是一个pkg conf ,是程序逻辑,项目的运行参数,通过配置传递给程序:[conf](./conf/README.md) + 异常处理... + 项目的文档:docs + 项目的接口:protocl 协议服务器,监听对应的端口(http server/grpc server),[处理用户的连接](./protocol/README.md) + 项目CLI: cmd 项目的cli工具(cobra),[./vblog start/init](./cmd/README.md) + 项目入口文件: main.go (放在最外层),项目所有的包,类,在main.go中进行程序组装 + 程序日志处理:Logurs/zap/zerolog,该项目里使用zerolog,程序里负责统一打印日志的组件:logger + 程序的配置文件:[加载配置文件目录](./etc/README.md) + 文件格式的配置:config.toml + 环境变量的配置:config.env + 程序的样例配置:config.example.toml + 单元测试配置:unit_test.env(vscode 使用的) + .gitignore: 哪些文件忽略不提交, config.toml 项目开发详细流程 + 首先写业务模块(业务单元),并放在apps下变成一个个包;针对每一个业务分三层 + 先定义内部使用的接口(blog/impl/interface.go) + 定义接口时,有两个关键点: 1:数据结构(blog/model.go),而在定义数据结构时,必然关系到数据如何存数据库问题,可以在同一纬度考虑 ;定义好数据结构(也就是数据库表字段) 2:接口定义好后如何实现? 通过实例类去实现(blog/impl/impl.go) + 接口实现后,对外暴露成API(blog/api/blog.go), 通过web框架 gin 来暴露,只处理不同的协议请求,然后调用后面具体的实例类进行业务逻辑的处理 + 不要习惯于调用第三方库去处理问题 + 自己先思考 --> 进行解决方案的设计 --> 寻找符合工具(克制,容易对程序失去控制) + 以下常见的可以用一用 + web框架 + 配置加载 + orm怎么与与数据库交互 # v1.0版本 问题 业务开发流程: + 写业务模块,业务模块并没有被加载到框架中去 + 加载业务逻辑,手动添加业务逻辑的初始化 ```go // 加载业务逻辑实现 r := gin.Default() // 首先将 blog 业务模块 初始化 blogService := &impl.Impl{} // 再将业务对象服务做一下初始化 err := blogService.Init() cobra.CheckErr(err) // 初始化后,把业务对象传给接口 apiHeadler := api.NewHeadler(blogService) //把vblog的接口注册到gin的路由上 apiHeadler.Registry(r) // 20多个业务模块 ... ... ``` 1. 注意里面不能集中在写业务功能上,不希望在apps模块之外还要做业务代码的添加 2. 对象的依赖问题,一些其他的封装的业务,可能会用很多业务对象来开发上层业务 ... # v2.0版本 核心采用Ioc来进行重构 + 1. 先写业务实现 + 2. 再写接口实现 + 3. 实例注册到ioc + 4. 程序启动时,注册所有的实例:apps/registry.go, ```sh # 先实现impl里的业务对象 gitee.com/hong_golang/code/vblog/apps/blog/impl # 再实现API gitee.com/hong_golang/code/vblog/apps/blog/api # 最后import 注册业务对象 ```go import ( // 注册api handler _ "gitee.com/hong_golang/code/vblog/apps/blog/api" // 注册 blog 业务具体实现 _ "gitee.com/hong_golang/code/vblog/apps/blog/impl" ) ``` ``` # 接口测试 请求 ```sh curl --location 'http://127.0.0.1:8010/vblog/api/v1/blogs' \ --header 'Content-Type: application/json' \ --data '{ "title": "api test", "author": "hcc", "content": "go_vblog_project" }' ``` 请求返回 ```json { "id": 23, "created_at": 1686045708, "updated_at": 1686045708, "publishd_at": 0, "title": "api test", "author": "hcc", "content": "go_vblog_project", "tags": {}, "status": 0 } ``` 如何控制枚举类型的序列化 :"status":0 + "status": "草稿/Drafa", 0 --> 草稿 object(内存中的数据结构) --> json(字符串),对于枚举类型,如何自定义JSON序列化 ```go // Marshaler is the interface implemented by types that // can marshal themselves into valid JSON. // type Marshaler interface { // MarshalJSON() ([]byte, error) // } // 自定义序列化;自己定义当前类型的JSON输出,一定要是一个合法的json // 注意:合法的json格式为 status: ""; []byte("草稿")则输出为 --> status: 草稿; // 所以要加上反引号 []byte(`"草稿"``) func (s STATUS) MarshalJSON() ([]byte, error) { switch s { case STATUS_DRAFT: return []byte(`"草稿"`), nil case STATUS_PUBLISHED: return []byte(`"已发布"`), nil } return []byte(fmt.Sprintf("%d", s)), nil } ``` ```sh curl --location 'http://127.0.0.1:8010/vblog/api/v1/blogs?keywords=test' ``` ```json { "total": 1, "items": [ { "id": 22, "created_at": 1685684471, "updated_at": 1685684471, "publishd_at": 0, "title": "test", "author": "test", "content": "test", "tags": {}, "status": "草稿" } ] } ``` # 认证功能开发 验证认证接口: ```sh curl --location --request POST 'http://localhost:8010/vblog/api/v1/tokens' \ --header 'Content-Type: application/json' \ --header 'Cookie: token=ci0nq5ivr9i60o0cfteg' \ --data '{ "username": "admin", "password": "123456" }' ``` # 程序的debug 1. 单元测试:调试某个函数 2. 程序Debug:程序以Debug启动 # 大型工程开发的一种实践(不是啥标准,也没有标准) 什么时候用接口?什么时候用init初始化函数? 用接口只是为了统一规范;