HTTP应用:写一个完整的博客后端

快速启动

  • 初始化项目 & 安装 gin
1
2
go mod init github.com/go-programming-tour-book/blog-service
go get -u github.com/gin-gonic/gin@v1.6.3
1
2
3
4
5
6
7
8
9
10
11
package main

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

func main(){
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "pong"})
})
r.Run()
}
  • 验证
1
2
$ curl http://127.0.0.1:8080/ping
{"message":"pong"}

后端技术

技术 版本 说明
gin v1.6.3 web 框架
viper v1.7.1 配置文件解析库
gorm v1.9.16 ORM 框架

项目设计

目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.
├── config
├── docs
├── global
├── internal
   ├── dao
   ├── middleware
   ├── model
   ├── routers
   └── service
├── main.go
├── pkg
├── scripts
├── storage
├── third_party
└── go.mod

数据库

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
33
34
35
36
37
38
39
40
41
CREATE TABLE `blog_tag` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(100) DEFAULT '' COMMENT '标签名称',
`state` tinyint(3) unsigned DEFAULT '1' COMMENT '状态 0为禁用、1为启用',
`created_on` int(10) unsigned DEFAULT '0' COMMENT '创建时间',
`created_by` varchar(100) DEFAULT '' COMMENT '创建人',
`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) DEFAULT '0' COMMENT '是否删除,0为未删除,1为已删除',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='文章标签管理'

CREATE TABLE `blog_article` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`title` varchar(100) DEFAULT '' COMMENT '文章标题',
`desc` varchar(255) DEFAULT '' COMMENT '文章简述',
`content` longtext COMMENT '文章内容',
`cover_image_url` varchar(255) DEFAULT '' COMMENT '封面图片地址',
`state` tinyint(3) unsigned DEFAULT '1' COMMENT '状态 0 为禁用、1 为启用',
`created_on` int(11) DEFAULT NULL COMMENT '创建时间',
`created_by` varchar(100) DEFAULT '' COMMENT '创建人',
`modified_on` int(10) unsigned DEFAULT '0' COMMENT '修改时间',
`modified_by` varchar(255) 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 AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='文章管理'

CREATE TABLE `blog_article_tag` (
`id` int(10) NOT NULL,
`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) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建人',
`modified_on` int(10) unsigned DEFAULT '0' COMMENT '修改时间',
`modified_by` varchar(100) COLLATE utf8mb4_unicode_ci 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 COLLATE=utf8mb4_unicode_ci

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"`
}

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

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 uint8 `json:"state"`
}

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

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

接口设计

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
33
34
35
package routers

import (
"github.com/gin-gonic/gin"
"github.com/go-programming-tour-book/blog-service/internal/routers/api/v1"
)

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

article := v1.NewArticle()
tag := v1.NewTag()

v1 := r.Group("/api/v1")
{
//标签管理
v1.POST("/tags", tag.Create)
v1.GET("/tags", tag.List)
v1.PUT("/tags/:id", tag.Update)
v1.DELETE("/tags/:id", tag.Delete)
v1.PATCH("/tags/:id/state", tag.Update)

//文章管理
v1.POST("/articles", article.Create)
v1.GET("/articles", article.List)
v1.GET("/articles/:id", article.Get)
v1.PUT("/articles/:id", article.Update)
v1.DELETE("/articles/:id", article.Delete)
v1.PATCH("/articles/:id/state", article.Update)
}

return r
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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) {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
var (
Success = NewError(0, "成功")
ServerError = NewError(100000, "内部服务器错误")
InvalidParams = NewError(100001, "入参错误")
NotFound = NewError(100002, "找不大")
UnauthorizedAuthNotExist = NewError(100003, "鉴权失败,找不到对应的 AppKey 和 AppSecret")
UnauthorizedTokenError = NewError(100004, "鉴权失败,Token 错误")
UnauthorizedTokenTimeout = NewError(100005, "鉴权失败,Token 超时")
UnauthorizedTokenGenerate = NewError(100006, "鉴权失败,Token 生成失败")
TooManyRequests = NewError(100007, "请求过多")
)
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package errcode

import (
"fmt"
"net/http"
)

// 错误的响应结果
type Error struct {
code int `json:"code"`
msg string `json:"msg"`
details []string `json:"details"`
}

// 全局错误码的存储载体
var codes = map[int]string{}

func NewError(code int, msg string) *Error {
if _, ok := codes[code]; ok {
panic(fmt.Sprintf("错误码 %d 已经存在,请更换一个", code))
}
codes[code] = msg
return &Error{code: code, msg: msg}
}

func (e *Error) Error() string {
return fmt.Sprintf("错误码:%d,错误信息:%s", e.code, e.msg)
}

func (e *Error) Code() int {
return e.code
}

func (e *Error) Msg() string {
return e.msg
}

func (e *Error) Msgf(args []interface{}) string {
return fmt.Sprintf(e.msg, args...)
}

func (e *Error) Details() []string {
return e.details
}

func (e *Error) WithDetails(details ...string) *Error {
newError := *e
newError.details = []string{}
for _, d := range details {
newError.details = append(newError.details, d)
}
return &newError
}

// 针对一些特定错误码进行状态码的转换
func (e *Error) StatusCode() int {
switch e.Code() {
case Success.Code():
return http.StatusOK
case ServerError.Code():
return http.StatusInternalServerError
case InvalidParams.Code():
return http.StatusBadRequest
case UnauthorizedAuthNotExist.Code():
fallthrough
case UnauthorizedTokenError.Code():
fallthrough
case UnauthorizedTokenGenerate.Code():
fallthrough
case UnauthorizedTokenTimeout.Code():
return http.StatusUnauthorized
case TooManyRequests.Code():
return http.StatusTooManyRequests
}
return http.StatusInternalServerError
}
  • 配置管理
1
2
export GOPROXY=https://goproxy.cn
go get -u github.com/spf13/viper@v1.4.0
  • 数据库连接
  • 日志写入
1
2
export GOPROXY=https://goproxy.cn
go get -u gopkg.in/natefinch/lumberjack.v2
  • 响应处理