Skip to content

项目设计

目录

目录结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
├── configs
├── docs
├── global
├── go.mod
├── go.sum
├── internal
│   ├── dao
│   ├── middleware
│   ├── model
│   ├── routers
│   └── service
├── main.go
├── pkg
├── scripts
├── storage
└── third_party
  • configs: 配置文件
  • docs: 文档集合
  • global: 全局变量
  • internal: 内部模块
    • dao: 数据访问层(Database Access Object),所有与数据相关的操作都会在 dao 层进行,例如 MySQL、Elasticsearch 等
    • middleware: HTTP 中间层
    • model: 模型层,用于存放 model 对象
    • routers: 路由相关的逻辑
    • service: 项目核心业务逻辑
  • pkg: 项目相关的模块包
  • storage: 项目生成的临时文件
  • scripts: 各类构建、安装、分析等操作的脚本
  • third_party: 第三方的资源工具,如 Swagger UI

数据库

在本次的项目开发中,我们主要实现两个基础业务功能,功能点如下:

  • 标签管理: 文章所归属的分类,也就是标签。通常我们会给文章打好几个标签,用于标识文章内容的要点和要素,以便读者的识别和 SEO 的收录等。
  • 文章管理: 对整个文章内容的管理,并且需要把文章和标签进行关联

要想做业务开发,首先就要设计数据库。
因此我们将根据业务模块进行 MySQL 数据库的创建和表设计

创建数据库

准备一个 MySQL 数据库,5.7 版本即可。在 MySQL 中执行如下 SQL 语句

1
2
3
4
5
mysql> Cysql> CREATE DATABASE
    -> IF
    -> NOT EXISTS blog_service DEFAULT CHARACTER
    -> SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;
Query OK, 1 row affected (0.36 sec)

通过上述 SQL 语句,数据库会创建本项目的数据库 blog_service,并将它的默认编码设置为 utf8mb4。
另外,在每个数据表中,都包含同样的公共字段:

1
2
3
4
5
6
`created on` int(10) unsigned DEFAULT 0 COMMENT '创建时间',
`created_by` varchar(100) DEFAULT '' '创建人',
`modified_on` int(10) unsigned DEFAULT 0 COMMENT '修改时间',
`modified_by` varchar(100) DEFAULT '' COMMENT '修改人',
`deleted_on` int(10) unsigned DEFAULT 0 COMMENT '删除时间',
`is_del` tinyint(3) unsigned DEFAULT 0 COMMENT '是否删除 0 为未删除、1 为已删除',

在创建数据表时,注意将其同时包含写入就可以了。

创建标签表

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
CREATE TABLE `blog_tag` (
    `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
    `name` varchar(100) DEFAULT '' COMMENT '标签名称',
    `created on` int(10) unsigned DEFAULT 0 COMMENT '创建时间',
    `created_by` varchar(100) DEFAULT '' '创建人',
    `modified_on` int(10) unsigned DEFAULT 0 COMMENT '修改时间',
    `modified_by` varchar(100) DEFAULT '' COMMENT '修改人',
    `deleted_on` int(10) unsigned DEFAULT 0 COMMENT '删除时间',
    `is_del` tinyint(3) unsigned DEFAULT 0 COMMENT '是否删除 0 为未删除、1 为已删除',
    `state` tinyint(3) unsigned DEFAULT 1 COMMENT '状态 0 为禁用、1 为启用',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='标签管理';

创建标签表,表字段主要为标签的名称、状态和公共字段

创建文章表

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
CREATE TABLE `blog_article` (
    `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
    `title` varchar(100) DEFAULT '' COMMENT '文章标题',
    `desc` varchar(255) DEFAULT '' COMMENT '文章简述',
    `cover_image_url` varchar(255) DEFAULT '' COMMENT '封面图片地址',
    `content` longtext COMMENT '文章内容',
    `created on` int(10) unsigned DEFAULT 0 COMMENT '创建时间',
    `created_by` varchar(100) DEFAULT '' '创建人',
    `modified_on` int(10) unsigned DEFAULT 0 COMMENT '修改时间',
    `modified_by` varchar(100) DEFAULT '' COMMENT '修改人',
    `deleted_on` int(10) unsigned DEFAULT 0 COMMENT '删除时间',
    `is_del` tinyint(3) unsigned DEFAULT 0 COMMENT '是否删除 0 为未删除、1 为已删除',
    `state` tinyint(3) unsigned DEFAULT 1 COMMENT '状态 0 为禁用、1 为启用',
    PRIMARY KEY(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章管理';

创建文章表,表字段主要为文章的标题、封面图、内容概述和公共字段

创建文章标签关联表

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
CREATE TABLE `blog_article_tag` (
    `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
    `article_id` int(11) NOT NULL COMMENT '文章ID',
    `tag_id` int(10) unsigned NOT NULL DEFAULT 0 COMMENT '标签ID',
    `created on` int(10) unsigned DEFAULT 0 COMMENT '创建时间',
    `created_by` varchar(100) DEFAULT '' '创建人',
    `modified_on` int(10) unsigned DEFAULT 0 COMMENT '修改时间',
    `modified_by` varchar(100) DEFAULT '' COMMENT '修改人',
    `deleted_on` int(10) unsigned DEFAULT 0 COMMENT '删除时间',
    `is_del` tinyint(3) unsigned DEFAULT 0 COMMENT '是否删除 0 为未删除、1 为已删除',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章标签关联';

创建文章标签关联表,这个表主要用于记录文章和标签之间的 1:N 的关联关系

创建 model

在创建完成数据库的表之后,到 internal/model 目录中创建对应的 model 对象,以便后续使用应用程序。

创建公共 model

internal/model 目录下创建 model.go 文件,写入如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package model

type Model struct {
    ID         uint32 `gorm:"primary_key" json:"id"`
    CreatedBy  string `json:"created_by"`
    ModifiedBy string `json:"modified_by"`
    CreatedOn  uint32 `json:"created_on"`
    ModifiedOn uint32 `json:"modified_on"`
    DeletedOn  uint32 `json:"deleted_on"`
    IsDel      uint8  `json:"is_del"`
}

创建标签 model

internal/model目录下创建 tag.go 文件,写入如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package model

type Tag struct {
    *Model
    Name  string `json:"name"`
    State uint8  `json:"state"`
}

func (t Tag) TableName() string {
    return "blog_tag"
}

创建文章 model

internal/model目录下创建 article.go 文件,写入如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package model

type Article struct {
    *Model
    Title         string `json:"title"`
    Desc          string `json:"desc"`
    Content       string `json:"content"`
    CoverImageUrl string `json:"cover_image_url"`
    State         string `json:"state"`
}

func (a Article) TableName() string {
    return "blog_article"
}

创建文章标签 model

在 internal/model 目录下创建 article_tag.go 文件,写入如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package model

type ArticleTag struct {
    *Model
    TagId     uint32 `json:"tag_id"`
    ArticleID uint32 `json:"article_id"`
}

func (a ArticleTag) TableName() string {
    return "blog_article_tag"
}

路由

在完成数据库的设计之后,我们需要对业务模块的管理接口进行设计。
在这部分内容中,最核心的就是设计和编写增删改查的 RESTful API。
在 RESTful API 中,HTTP 方法对应的行为和动作如下:

  • GET: 读取和检索动作
  • POST: 新增和新建动作
  • PUT: 更新动作,用于更新一个完整的资源,要求为幂等
  • PATCH: 更新动作,用于更新某一个资源的一个组成部分。也就是说,当只需更新该资源的某一项时,应该使用 PATCH 而不是 PUT,可以不幂等
  • DELETE: 删除动作

下面就我们根据 RESTful API 的基本规范,对业务模块设计路由规则,从业务角度划分多个管理接口。

标签管理

功能 HTTP方法 路径
新增标签 POST /tags
删除指定标签 DELETE /tags/:id
功能 HTTP方法 路径
更新指定标签 PUT /tags/:id
获取标签列表 GET /tags

文章管理

功能 HTTP方法 路径
新增文章 POST /articles
删除指定文章 DELETE /articles/:id
更新指定文章 PUT /articles/:id
获取指定文章 GET /articles/:id
获取文章列表 GET /articles

路由管理

在确定了业务接口设计后,需要对业务接口进行基础编码,以确定其方法原型。
把当前工作区切换到项目目录的 internal/routers 下,新建 router.go 文件,写入如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package routers

import "github.com/gin-gonic/gin"

func NewRouter() *gin.Engine {
    r := gin.New()
    r.Use(gin.Logger())
    r.Use(gin.Recovery())

    apiv1 := r.Group("/api/v1")
    {
        apiv1.POST("/tags")
        apiv1.DELETE("/tags/:id")
        apiv1.PUT("/tags/:id")
        apiv1.PATCH("/tags/:id/state")
        apiv1.GET("/tags")

        apiv1.POST("/articles")
        apiv1.DELETE("/articles/:id")
        apiv1.PUT("/articles/:id")
        apiv1.PATCH("/articles/:id/state")
        apiv1.GET("/articles/:id")
        apiv1.GET("/articles")
    }

    return r
}

处理程序

下面编写对应路由的处理方法。
在目录 internal/routers/api 下新建 v1 文件夹,在 v1 文件夹中新建 tag.go(标签)和 article.go(文章)文件,写入如下代码.

标签方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package v1

import "github.com/gin-gonic/gin"

type Tag struct{}

func NewTag() Tag {
    return Tag{}
}

func (t Tag) Get(c *gin.Context)    {}
func (t Tag) List(c *gin.Context)   {}
func (t Tag) Create(c *gin.Context) {}
func (t Tag) Update(c *gin.Context) {}
func (t Tag) Delete(c *gin.Context) {}

文章方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package v1

import "github.com/gin-gonic/gin"

type Article struct{}

func NewArticle() Article {
    return Article{}
}

func (a Article) Get(c *gin.Context)    {}
func (a Article) List(c *gin.Context)   {}
func (a Article) Create(c *gin.Context) {}
func (a Article) Update(c *gin.Context) {}
func (a Article) Delete(c *gin.Context) {}

路由管理

在编写好路由的 Handler 方法后,只需将其注册到对应的路由规则上即可。
打开项目目录 internal/routers 下的 router.go 文件,修改代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package routers

import (
    v1 "example.com/blog-service/internal/routers/v1"
    "github.com/gin-gonic/gin"
)

func NewRouter() *gin.Engine {
    r := gin.New()
    r.Use(gin.Logger())
    r.Use(gin.Recovery())

    article := v1.NewArticle()
    tag := v1.NewTag()
    apiv1 := r.Group("/api/v1")
    {
        apiv1.POST("/tags", tag.Create)
        apiv1.DELETE("/tags/:id", tag.Update)
        apiv1.PUT("/tags/:id", tag.Update)
        apiv1.PATCH("/tags/:id/state", tag.Update)
        apiv1.GET("/tags", tag.List)

        apiv1.POST("/articles", article.Create)
        apiv1.DELETE("/articles/:id", article.Delete)
        apiv1.PUT("/articles/:id", article.Update)
        apiv1.PATCH("/articles/:id/state", article.Update)
        apiv1.GET("/articles/:id", article.Get)
        apiv1.GET("/articles", article.List)
    }

    return r
}

启动接入

在编写完模型、路由的代码后,即可修改前面的 main.go 文件,把它改造为这个项目的启动文件,修改代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
    "net/http"
    "time"

    "example.com/blog-service/internal/routers"
)

func main() {
    router := routers.NewRouter()
    s := &http.Server{
        Addr:           ":8080",
        Handler:        router,
        ReadTimeout:    10 * time.Second,
        WriteTimeout:   10 * time.Second,
        MaxHeaderBytes: 1 << 20,
    }
    s.ListenAndServe()
}

通过自定义 http.Server,可以设置监听的 TCP Endpoint、处理的程序、允许读取/写入的最大时间、请求头的最大字节数等基础参数,最后调用 ListenAndServe 方法即可开始监听。

验证

在项目根目录下,执行 go run main.go 将服务运行起来,然后查看服务是否可以正常运行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
$ go run main.go
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] POST   /api/v1/tags              --> example.com/blog-service/internal/routers/v1.Tag.Create-fm (3 handlers)
[GIN-debug] DELETE /api/v1/tags/:id          --> example.com/blog-service/internal/routers/v1.Tag.Update-fm (3 handlers)
[GIN-debug] PUT    /api/v1/tags/:id          --> example.com/blog-service/internal/routers/v1.Tag.Update-fm (3 handlers)
[GIN-debug] PATCH  /api/v1/tags/:id/state    --> example.com/blog-service/internal/routers/v1.Tag.Update-fm (3 handlers)
[GIN-debug] GET    /api/v1/tags              --> example.com/blog-service/internal/routers/v1.Tag.List-fm (3 handlers)
[GIN-debug] POST   /api/v1/articles          --> example.com/blog-service/internal/routers/v1.Article.Create-fm (3 handlers)
[GIN-debug] DELETE /api/v1/articles/:id      --> example.com/blog-service/internal/routers/v1.Article.Delete-fm (3 handlers)
[GIN-debug] PUT    /api/v1/articles/:id      --> example.com/blog-service/internal/routers/v1.Article.Update-fm (3 handlers)
[GIN-debug] PATCH  /api/v1/articles/:id/state --> example.com/blog-service/internal/routers/v1.Article.Update-fm (3 handlers)
[GIN-debug] GET    /api/v1/articles/:id      --> example.com/blog-service/internal/routers/v1.Article.Get-fm (3 handlers)
[GIN-debug] GET    /api/v1/articles          --> example.com/blog-service/internal/routers/v1.Article.List-fm (3 handlers)

启动信息表示路由注册正常,再实际调用一下接口,如果能够正常返回,就大功告成了。