├── .env.example
├── .gitignore
├── .idea
├── .gitignore
├── modules.xml
├── restdemo.iml
└── vcs.xml
├── .semaphore
└── semaphore.yml
├── Dockerfile
├── GOLANG-RESTFUL-API.pdf
├── LICENSE
├── Makefile
├── README.md
├── auth.go
├── cache.go
├── config.go
├── docker-compose.yml
├── error.go
├── go.mod
├── go.sum
├── main.go
├── main_test.go
├── note.go
├── pagination.go
├── store.go
└── util.go
/.env.example:
--------------------------------------------------------------------------------
1 | CONFIGOR_ENV_PREFIX=-
2 |
3 | APP_DEBUG=true
4 | APP_BASEURL=http://localhost/
5 | APP_FILEURL=http://localhost/
6 |
7 | DB_HOST=mysql
8 | DB_PORT=3306
9 | DB_NAME=demo
10 |
11 | REDIS_HOST=redis
12 | REDIS_PORT=6379
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 | /app
8 | /restdemo
9 |
10 | # Test binary, build with `go test -c`
11 | *.test
12 |
13 | # Output of the go coverage tool, specifically when used with LiteIDE
14 | *.out
15 |
16 | # Editor
17 | .vscode
18 |
19 | # Debug
20 | debug
21 | .env
22 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Datasource local storage ignored files
5 | /dataSources/
6 | /dataSources.local.xml
7 | # Editor-based HTTP Client requests
8 | /httpRequests/
9 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/restdemo.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.semaphore/semaphore.yml:
--------------------------------------------------------------------------------
1 | version: v1.0
2 | name: First pipeline example
3 | agent:
4 | machine:
5 | type: e1-standard-2
6 | os_image: ubuntu1804
7 |
8 | blocks:
9 | - name: "Build"
10 | task:
11 | env_vars:
12 | - name: APP_ENV
13 | value: prod
14 | jobs:
15 | - name: Docker build
16 | commands:
17 | - checkout
18 | - ls -1
19 | - echo $APP_ENV
20 | - echo "Docker build..."
21 | - echo "done"
22 |
23 | - name: "Smoke tests"
24 | task:
25 | jobs:
26 | - name: Smoke
27 | commands:
28 | - checkout
29 | - echo "make smoke"
30 |
31 | - name: "Unit tests"
32 | task:
33 | jobs:
34 | - name: RSpec
35 | commands:
36 | - checkout
37 | - echo "make rspec"
38 |
39 | - name: Lint code
40 | commands:
41 | - checkout
42 | - echo "make lint"
43 |
44 | - name: Check security
45 | commands:
46 | - checkout
47 | - echo "make security"
48 |
49 | - name: "Integration tests"
50 | task:
51 | jobs:
52 | - name: Cucumber
53 | commands:
54 | - checkout
55 | - echo "make cucumber"
56 |
57 | - name: "Push Image"
58 | task:
59 | jobs:
60 | - name: Push
61 | commands:
62 | - checkout
63 | - echo "make docker.push"
64 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # build app
2 | FROM golang AS build-env
3 |
4 | ADD . /restdemo
5 |
6 | WORKDIR /restdemo
7 |
8 | RUN go build
9 |
10 | # safe image
11 | FROM debian
12 |
13 | ENV TZ=Asia/Shanghai
14 |
15 | COPY --from=build-env /restdemo/restdemo /usr/bin/restdemo
16 |
17 | EXPOSE 1324
18 |
19 | CMD ["restdemo"]
20 |
--------------------------------------------------------------------------------
/GOLANG-RESTFUL-API.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hyacinthus/restdemo/2b6f27b8a7a426f30d03881b572e4ac246a484fa/GOLANG-RESTFUL-API.pdf
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 王瑞华
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | up: ## Docker stack deploy
2 | docker pull muninn/restdemo && docker stack deploy -c docker-compose.yml restdemo
3 |
4 | down: ## Docker stack rm
5 | docker stack rm restdemo
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 用 Golang 快速开发 RESTful API
2 |
3 | 这是我 2018 年一个演讲上用的 demo,用 golang 实现一个五脏俱全的 API 项目。
4 |
5 | 计划大概写一篇博客做说明,但是还没抽出空来写,可以看当时的演讲 PPT ,大概只是个
6 | 提纲。项目根目录有个 PDF 文件就是。
7 |
8 | 2021年6月10日:
9 |
10 | 我看到偶尔还有同学通过PPT或者视频来到这个repo,这是三年前的代码,虽然各种原理和写法可以对着演讲参考,
11 | 但是代码本身和它的依赖都已经过时了。大家看个意思就好,这些年来,我也有很多新的经验,它都被反映在了我新的项目中。
12 |
13 | 这里有一个开源项目是我一直维护着的,代码也比较简洁,大家可以去看这个项目学习:
14 |
15 | [https://github.com/hack-fan/skadi](https://github.com/hack-fan/skadi)
16 |
17 | ## 使用方法
18 |
19 | 只要安装了 docker ,并开启了 Swarm 模式,在项目目录执行:
20 |
21 | ```bash
22 | docker stack deploy -c docker-compose.yml demo
23 | ```
24 |
25 | 如果安装了 make , 也可以直接 `make up`,会自动帮你执行以上语句。
26 |
27 | 然后访问 your-host:1328/swagger/index.html 可以看到所有的 api
28 |
29 | 如果没有开放 swarm 模式,也可以自行改改 compose 文件用 docker-compose 启动.
30 |
31 | ## 关于项目组织
32 |
33 | 开发微服务的时候,建议不用建太多文件夹来横向分层。尽量把同一个实体的模型和业务逻
34 | 辑全放一个文件里其实是最利于代码维护的。因为微服务项目每个服务维护的实体其实很少
35 | ,所以并不会很乱。
36 |
37 | 现在文件看起来比较乱是因为有太多的公用模块,业务逻辑只有 `note.go` 一个文件。但
38 | 是实际上,真的写微服务的时候,最好做一个自己的公用 package,剩下的文件大部分就都
39 | 是业务文件了。可以参考我的 [x](https://github.com/hyacinthus/x) 和
40 | [ske](https://github.com/hyacinthus/ske) 这两个 demo
41 |
42 | ## 一些更新
43 |
44 | 还没有体现在这个 demo 里
45 |
46 | - uuid 库我现在已经弃用,更喜欢用 [xid](https://github.com/rs/xid)
47 | - 日志库 zap 比 logrus 更好
48 | - swag 其实可以把展示统一维护在项目之外,在 ci 的时候生成 json 文件,业务代码里
49 | 只有注释,不用提供 swagger 的 endpoint ,这样是侵入最小的。
50 |
--------------------------------------------------------------------------------
/auth.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "net/http"
5 | "strings"
6 | "time"
7 |
8 | "github.com/go-redis/cache"
9 | "github.com/labstack/echo/v4"
10 | "github.com/sirupsen/logrus"
11 | )
12 |
13 | // LoginRequest 登录提供内容
14 | type LoginRequest struct {
15 | // 用户名
16 | Username string `json:"username"`
17 | // 密码
18 | Password string `json:"password"`
19 | }
20 |
21 | // Token 以上第四步返回给客户端的token对象
22 | type Token struct {
23 | Token string `json:"token"`
24 | ExpiresAt time.Time `json:"expires_at"`
25 | UserID int `json:"-"`
26 | }
27 |
28 | // API状态 成功204 失败500
29 | func getStatus(c echo.Context) error {
30 | return c.NoContent(http.StatusNoContent)
31 | }
32 |
33 | // login 登录函数,demo中为了简洁就只有user password可以通过
34 | // 实际应用中这个函数会相当复杂,要用正则判断输入的用户名是什么类型,然后调用相关函数去找用户。
35 | // 还要兼容第三方登录,所以请求结构体也会更加复杂。
36 | // @Tags 用户
37 | // @Summary 登录
38 | // @Description 用户登录
39 | // @Accept json
40 | // @Produce json
41 | // @Param data body main.LoginRequest true "登录凭证"
42 | // @Success 201 {object} main.Token
43 | // @Failure 400 {object} main.httpError
44 | // @Failure 401 {object} main.httpError
45 | // @Failure 500 {object} main.httpError
46 | // @Router /login [post]
47 | func login(c echo.Context) error {
48 | // 判断何种方式登录,小程序为提供code
49 | var req = new(LoginRequest) // 输入请求
50 | if err := c.Bind(req); err != nil {
51 | return err
52 | }
53 | var t *Token
54 | if req.Username == "username" && req.Password == "password" {
55 | // 发行token
56 | t = &Token{
57 | Token: newUUID(),
58 | ExpiresAt: time.Now().Add(time.Hour * 96),
59 | // 这个userid应该是检索出来的,这里为demo写死。
60 | UserID: 1,
61 | }
62 | setcc("token:"+t.Token, t, time.Hour*96)
63 | } else {
64 | return ErrAuthFailed
65 | }
66 | return c.JSON(http.StatusOK, t)
67 | }
68 |
69 | // skipper 这些不需要token
70 | func skipper(c echo.Context) bool {
71 | method := c.Request().Method
72 | path := c.Path()
73 | // 先处理非GET方法,除了登录,现实中还可能有一些 webhooks
74 | switch path {
75 | case
76 | // 登录
77 | "/login":
78 | return true
79 | }
80 | // 从这里开始必须是GET方法
81 | if method != "GET" {
82 | return false
83 | }
84 | if path == "" {
85 | return true
86 | }
87 | resource := strings.Split(path, "/")[1]
88 | switch resource {
89 | case
90 | // 公开信息,把需要公开的资源每个一行写这里
91 | "swagger",
92 | "public":
93 | return true
94 | }
95 | return false
96 | }
97 |
98 | // Validator 校验token是否合法,顺便根据token在 context中赋值 user id
99 | func validator(token string, c echo.Context) (bool, error) {
100 | // 调试后门
101 | logrus.Debug("token:", token)
102 | if config.APP.Debug && token == "debug" {
103 | c.Set("user_id", 1)
104 | return true, nil
105 | }
106 | // 寻找token
107 | var t = new(Token)
108 | err := getcc("token:"+token, t)
109 | if err == cache.ErrCacheMiss {
110 | return false, nil
111 | } else if err != nil {
112 | return false, err
113 | }
114 | // 设置用户
115 | c.Set("user_id", t.UserID)
116 |
117 | return true, nil
118 | }
119 |
120 | // 这个函数还有一种设计风格,就是只是返回userid,
121 | // 以支持可选登录,在业务中判断userid如果是0就没有登录
122 | func parseUser(c echo.Context) (userID int, err error) {
123 | userID, ok := c.Get("user_id").(int)
124 | if !ok || userID == 0 {
125 | return 0, ErrUnauthorized
126 | }
127 | return userID, nil
128 | }
129 |
--------------------------------------------------------------------------------
/cache.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "net/http"
5 | "time"
6 |
7 | "github.com/go-redis/cache"
8 | "github.com/labstack/echo/v4"
9 | "github.com/sirupsen/logrus"
10 | "github.com/vmihailenco/msgpack"
11 | )
12 |
13 | // 初始化缓存
14 | func initCache() {
15 | cc = &cache.Codec{
16 | Redis: rdb,
17 | Marshal: func(v interface{}) ([]byte, error) {
18 | return msgpack.Marshal(v)
19 | },
20 | Unmarshal: func(b []byte, v interface{}) error {
21 | return msgpack.Unmarshal(b, v)
22 | },
23 | }
24 | }
25 |
26 | // setcc 写缓存
27 | func setcc(key string, object interface{}, exp time.Duration) {
28 | cc.Set(&cache.Item{
29 | Key: key,
30 | Object: object,
31 | Expiration: exp,
32 | })
33 | }
34 |
35 | // getcc 读缓存
36 | func getcc(key string, pointer interface{}) error {
37 | return cc.Get(key, pointer)
38 | }
39 |
40 | // delcc 清缓存
41 | func delcc(key string) {
42 | cc.Delete(key)
43 | }
44 |
45 | // cleancc 批量清除一类缓存
46 | func cleancc(cate string) {
47 | if cate == "" {
48 | logrus.Error("someone try to clean all cache keys")
49 | return
50 | }
51 | i := 0
52 | for _, key := range rdb.Keys(cate + "*").Val() {
53 | delcc(key)
54 | i++
55 | }
56 | logrus.Infof("delete %d %s cache", i, cate)
57 | }
58 |
59 | func deleteCache(c echo.Context) error {
60 | cate := c.Param("cate")
61 | switch cate {
62 | case "token":
63 | cleancc("token")
64 | case "all":
65 | cleancc("token")
66 | default:
67 | return newHTTPError(400, "InvalidID", "请在URL中提供合法的缓存类型")
68 | }
69 | return c.NoContent(http.StatusNoContent)
70 | }
71 |
--------------------------------------------------------------------------------
/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/jinzhu/configor"
5 | "github.com/joho/godotenv"
6 | "github.com/sirupsen/logrus"
7 | )
8 |
9 | var config = struct {
10 | APP struct {
11 | Debug bool `default:"false"`
12 | Host string `default:"0.0.0.0"`
13 | Port string `default:"1324"`
14 | PageSize int `default:"10"`
15 | BaseURL string `default:"https://api.example.com/"`
16 | FileURL string `default:"https://static.example.com/"`
17 | }
18 |
19 | DB struct {
20 | Host string `default:"mysql"`
21 | Port string `default:"3306"`
22 | User string `default:"root"`
23 | Password string `default:"root"`
24 | Name string `default:"art"`
25 | }
26 |
27 | Redis struct {
28 | Host string `default:"redis"`
29 | Port string `default:"6379"`
30 | Password string
31 | DB int `default:"0"`
32 | }
33 | }{}
34 |
35 | func init() {
36 | godotenv.Load()
37 | configor.Load(&config)
38 | if config.APP.Debug {
39 | logrus.SetFormatter(&logrus.TextFormatter{
40 | FullTimestamp: true,
41 | TimestampFormat: "06-01-02 15:04:05.00",
42 | })
43 | logrus.SetLevel(logrus.DebugLevel)
44 | } else {
45 | logrus.SetLevel(logrus.InfoLevel)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.7'
2 | services:
3 | api:
4 | image: muninn/restdemo
5 | ports:
6 | - 1328:1324
7 | environment:
8 | - CONFIGOR_ENV_PREFIX=-
9 | - APP_DEBUG=false
10 | - APP_BASEURL=https://demo.crandom.com/
11 | - APP_FILEURL=https://static.crandom.com/
12 | - DB_HOST=mysql
13 | - DB_PORT=3306
14 | - DB_NAME=demo
15 | - REDIS_HOST=redis
16 | - REDIS_PORT=6379
17 | deploy:
18 | replicas: 1
19 | restart_policy:
20 | condition: on-failure
21 |
22 | redis:
23 | image: redis
24 | deploy:
25 | replicas: 1
26 | restart_policy:
27 | condition: on-failure
28 |
29 | mysql:
30 | image: mysql:8
31 | environment:
32 | - TZ=Asia/Shanghai
33 | - MYSQL_ROOT_PASSWORD=root
34 | - MYSQL_DATABASE=demo
35 | volumes:
36 | - db-data:/var/lib/mysql
37 | deploy:
38 | placement:
39 | constraints: [node.role == manager]
40 |
41 | volumes:
42 | db-data:
43 |
--------------------------------------------------------------------------------
/error.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/jinzhu/gorm"
7 | "github.com/labstack/echo/v4"
8 | )
9 |
10 | // 定义错误
11 | var (
12 | ErrNotFound = newHTTPError(404, "NotFound", "没有找到相应记录")
13 | ErrAuthFailed = newHTTPError(401, "AuthFailed", "登录失败")
14 | ErrUnauthorized = newHTTPError(401, "Unauthorized", "本接口只有登录用户才能调用")
15 | ErrForbidden = newHTTPError(403, "Forbidden", "权限不足")
16 | )
17 |
18 | // httpError 对外输出的错误格式
19 | type httpError struct {
20 | code int
21 | // 错误代码,为英文字符串,前端可用此判断大的错误类型。
22 | Key string `json:"error"`
23 | // 错误消息,为详细错误描述,前端可选择性的展示此字段。
24 | Message string `json:"message"`
25 | }
26 |
27 | func newHTTPError(code int, key string, msg string) *httpError {
28 | return &httpError{
29 | code: code,
30 | Key: key,
31 | Message: msg,
32 | }
33 | }
34 |
35 | // Error makes it compatible with `error` interface.
36 | func (e *httpError) Error() string {
37 | return e.Key + ": " + e.Message
38 | }
39 |
40 | // httpErrorHandler customize echo's HTTP error handler.
41 | func httpErrorHandler(err error, c echo.Context) {
42 | var (
43 | code = http.StatusInternalServerError
44 | key = "ServerError"
45 | msg string
46 | )
47 | // 二话不说先打日志
48 | c.Logger().Error(err.Error())
49 |
50 | if he, ok := err.(*httpError); ok {
51 | // 我们自定的错误
52 | code = he.code
53 | key = he.Key
54 | msg = he.Message
55 | } else if ee, ok := err.(*echo.HTTPError); ok {
56 | // echo 框架的错误
57 | code = ee.Code
58 | key = http.StatusText(code)
59 | msg = key
60 | } else if err == gorm.ErrRecordNotFound {
61 | // 我们将 gorm 的没有找到直接返回 404
62 | code = http.StatusNotFound
63 | key = "NotFound"
64 | msg = "没有找到相应记录"
65 | } else if config.APP.Debug {
66 | // 剩下的都是500 开了debug显示详细错误
67 | msg = err.Error()
68 | } else {
69 | // 500 不开debug 用标准错误描述 以防泄漏信息
70 | msg = http.StatusText(code)
71 | }
72 |
73 | // 判断 context 是否已经返回了
74 | if !c.Response().Committed {
75 | if c.Request().Method == echo.HEAD {
76 | err := c.NoContent(code)
77 | if err != nil {
78 | c.Logger().Error(err.Error())
79 | }
80 | } else {
81 | err := c.JSON(code, newHTTPError(code, key, msg))
82 | if err != nil {
83 | c.Logger().Error(err.Error())
84 | }
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/hyacinthus/restdemo
2 |
3 | go 1.16
4 |
5 | require (
6 | github.com/denisenkom/go-mssqldb v0.10.0 // indirect
7 | github.com/go-redis/cache v6.4.0+incompatible
8 | github.com/go-redis/redis v6.15.9+incompatible
9 | github.com/go-sql-driver/mysql v1.6.0 // indirect
10 | github.com/jinzhu/configor v1.2.1
11 | github.com/jinzhu/gorm v1.9.16
12 | github.com/jinzhu/now v1.1.2 // indirect
13 | github.com/joho/godotenv v1.3.0
14 | github.com/kr/pretty v0.2.1 // indirect
15 | github.com/labstack/echo/v4 v4.3.0
16 | github.com/lib/pq v1.10.2 // indirect
17 | github.com/mattn/go-isatty v0.0.13 // indirect
18 | github.com/mattn/go-sqlite3 v1.14.7 // indirect
19 | github.com/onsi/ginkgo v1.16.4 // indirect
20 | github.com/onsi/gomega v1.13.0 // indirect
21 | github.com/satori/go.uuid v1.2.1-0.20180103174451-36e9d2ebbde5
22 | github.com/sirupsen/logrus v1.8.1
23 | github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80
24 | github.com/vmihailenco/msgpack v4.0.4+incompatible
25 | golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect
26 | golang.org/x/net v0.0.0-20210525063256-abc453219eb5 // indirect
27 | golang.org/x/sys v0.0.0-20210608053332-aa57babbf139 // indirect
28 | golang.org/x/time v0.0.0-20210608053304-ed9ce3a009e4 // indirect
29 | google.golang.org/appengine v1.6.7 // indirect
30 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
31 | )
32 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
3 | github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
4 | github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
8 | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
9 | github.com/denisenkom/go-mssqldb v0.10.0 h1:QykgLZBorFE95+gO3u9esLd0BmbvpWp0/waNNZfHBM8=
10 | github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
11 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
12 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
13 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
14 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
15 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
16 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
17 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
18 | github.com/go-redis/cache v6.4.0+incompatible h1:ZaeoZofvBZmMr8ZKxzFDmkoRTSp8sxHdJlB3e3T6GDA=
19 | github.com/go-redis/cache v6.4.0+incompatible/go.mod h1:XNnMdvlNjcZvHjsscEozHAeOeSE5riG9Fj54meG4WT4=
20 | github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
21 | github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
22 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
23 | github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
24 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
25 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
26 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
27 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
28 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
29 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
30 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
31 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
32 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
33 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
34 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
35 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
36 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
37 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
38 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
39 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
40 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
41 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
42 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
43 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
44 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
45 | github.com/jinzhu/configor v1.2.1 h1:OKk9dsR8i6HPOCZR8BcMtcEImAFjIhbJFZNyn5GCZko=
46 | github.com/jinzhu/configor v1.2.1/go.mod h1:nX89/MOmDba7ZX7GCyU/VIaQ2Ar2aizBl2d3JLF/rDc=
47 | github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o=
48 | github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
49 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
50 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
51 | github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
52 | github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI=
53 | github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
54 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
55 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
56 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
57 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
58 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
59 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
60 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
61 | github.com/labstack/echo/v4 v4.3.0 h1:DCP6cbtT+Zu++K6evHOJzSgA2115cPMuCx0xg55q1EQ=
62 | github.com/labstack/echo/v4 v4.3.0/go.mod h1:PvmtTvhVqKDzDQy4d3bWzPjZLzom4iQbAZy2sgZ/qI8=
63 | github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=
64 | github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
65 | github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
66 | github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
67 | github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
68 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
69 | github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
70 | github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
71 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
72 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
73 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
74 | github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA=
75 | github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
76 | github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
77 | github.com/mattn/go-sqlite3 v1.14.7 h1:fxWBnXkxfM6sRiuH3bqJ4CfzZojMOLVc0UTsTglEghA=
78 | github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
79 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
80 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
81 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
82 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
83 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
84 | github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E=
85 | github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
86 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
87 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
88 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
89 | github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak=
90 | github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY=
91 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
92 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
93 | github.com/satori/go.uuid v1.2.1-0.20180103174451-36e9d2ebbde5 h1:Jw7W4WMfQDxsXvfeFSaS2cHlY7bAF4MGrgnbd0+Uo78=
94 | github.com/satori/go.uuid v1.2.1-0.20180103174451-36e9d2ebbde5/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
95 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
96 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
97 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
98 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
99 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
100 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
101 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
102 | github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y=
103 | github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE=
104 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
105 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
106 | github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
107 | github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
108 | github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
109 | github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
110 | github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
111 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
112 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
113 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
114 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
115 | golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
116 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
117 | golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
118 | golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc=
119 | golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
120 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
121 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
122 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
123 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
124 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
125 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
126 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
127 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
128 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
129 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
130 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
131 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
132 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
133 | golang.org/x/net v0.0.0-20210525063256-abc453219eb5 h1:wjuX4b5yYQnEQHzd+CBcrcC6OVR2J1CN6mUy0oSxIPo=
134 | golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
135 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
136 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
137 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
138 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
139 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
140 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
141 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
142 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
143 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
144 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
145 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
146 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
147 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
148 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
149 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
150 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
151 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
152 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
153 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
154 | golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
155 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
156 | golang.org/x/sys v0.0.0-20210608053332-aa57babbf139 h1:C+AwYEtBp/VQwoLntUmQ/yx3MS9vmZaKNdw5eOpoQe8=
157 | golang.org/x/sys v0.0.0-20210608053332-aa57babbf139/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
158 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
159 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
160 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
161 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
162 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
163 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
164 | golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
165 | golang.org/x/time v0.0.0-20210608053304-ed9ce3a009e4 h1:1asO3s7vR+9MvZSNRwUBBTjecxbGtfvmxjy2VWbFR5g=
166 | golang.org/x/time v0.0.0-20210608053304-ed9ce3a009e4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
167 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
168 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
169 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
170 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
171 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
172 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
173 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
174 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
175 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
176 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
177 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
178 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
179 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
180 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
181 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
182 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
183 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
184 | google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
185 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
186 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
187 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
188 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
189 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
190 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
191 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
192 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
193 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
194 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
195 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
196 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
197 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/go-redis/cache"
5 | "github.com/go-redis/redis"
6 | "github.com/jinzhu/gorm"
7 | _ "github.com/jinzhu/gorm/dialects/mysql"
8 | "github.com/labstack/echo/v4"
9 | "github.com/labstack/echo/v4/middleware"
10 | )
11 |
12 | // 定义全区变量 为了保证执行顺序 初始化均在main中执行
13 | var (
14 | // gorm mysql db connection
15 | db *gorm.DB
16 | // redis client
17 | rdb *redis.Client
18 | // global cache
19 | cc *cache.Codec
20 | )
21 |
22 | // @title RESTful API DEMO by Golang & Echo
23 | // @version 1.0
24 | // @description This is a demo server.
25 |
26 | // @contact.name Muninn
27 | // @contact.email hyacinthus@gmail.com
28 |
29 | // @license.name MIT
30 | // @license.url https://github.com/hyacinthus/restdemo/blob/master/LICENSE
31 |
32 | // @host demo.crandom.com
33 | // @BasePath /
34 | func main() {
35 | // init echo
36 | e := echo.New()
37 | e.HTTPErrorHandler = httpErrorHandler
38 | e.Use(middleware.Logger())
39 | e.Use(middleware.Recover())
40 | e.Use(middleware.CORS())
41 | e.Use(middleware.KeyAuthWithConfig(middleware.KeyAuthConfig{
42 | Skipper: skipper, // 跳过验证条件 在 auth.go 定义
43 | Validator: validator, // 处理验证结果 在 auth.go 定义
44 | }))
45 | e.Use(ParsePagination) // 分页参数解析,在 pagination.go 定义
46 |
47 | // Echo debug setting
48 | if config.APP.Debug {
49 | e.Debug = true
50 | }
51 |
52 | // init mysql and redis
53 | initDB()
54 | defer db.Close()
55 | initRedis()
56 | defer rdb.Close()
57 |
58 | // init global cache
59 | initCache()
60 |
61 | // async create tables
62 | go createTables()
63 |
64 | // status
65 | e.GET("/status", getStatus)
66 |
67 | // auth
68 | e.POST("/login", login)
69 |
70 | // note Routes
71 | e.GET("/notes", getNotes)
72 | e.POST("/notes", createNote)
73 | e.GET("/notes/:id", getNote)
74 | e.PUT("/notes/:id", updateNote)
75 | e.DELETE("/notes/:id", deleteNote)
76 | e.GET("/public/notes", getPublicNotes)
77 | e.GET("/public/notes/:id", getPublicNote)
78 |
79 | // Start echo server
80 | e.Logger.Fatal(e.Start(config.APP.Host + ":" + config.APP.Port))
81 | }
82 |
--------------------------------------------------------------------------------
/main_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "os"
5 | "testing"
6 | )
7 |
8 | func TestMain(m *testing.M) {
9 | // init mysql and redis
10 | // initDB()
11 | // defer db.Close()
12 | initRedis()
13 | defer rdb.Close()
14 |
15 | os.Exit(m.Run())
16 | }
17 |
--------------------------------------------------------------------------------
/note.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "net/http"
5 | "strconv"
6 | "time"
7 |
8 | "github.com/labstack/echo/v4"
9 | )
10 |
11 | // Note 纸条
12 | type Note struct {
13 | ID int `json:"id" gorm:"primary_key"`
14 | // 所属用户
15 | UserID int `json:"user_id" gorm:"index:idx_user_update"`
16 | // 标题
17 | Title string `json:"title"`
18 | // 题图
19 | Image FileURL `json:"image"`
20 | // 内容
21 | Content string `json:"content" gorm:"size:2000"`
22 | // 是否公开
23 | IsPublic bool `json:"is_public"`
24 | // 创建时间
25 | CreatedAt time.Time `json:"created_at"`
26 | // 最后更新时间
27 | UpdatedAt time.Time `json:"updated_at" gorm:"index:idx_user_update"`
28 | // 软删除
29 | DeletedAt *time.Time `json:"-"`
30 | }
31 |
32 | // NoteUpdate 更新请求结构体,用指针可以判断是否有请求这个字段
33 | type NoteUpdate struct {
34 | // 标题
35 | Title *string `json:"title"`
36 | // 题图
37 | Image *FileURL `json:"image"`
38 | // 内容
39 | Content *string `json:"content"`
40 | // 是否公开
41 | IsPublic *bool `json:"is_public"`
42 | }
43 |
44 | func findNoteByID(id int) (*Note, error) {
45 | var n = new(Note)
46 | if err := db.First(n, id).Error; err != nil {
47 | return nil, err
48 | }
49 | return n, nil
50 | }
51 |
52 | // createNote 新建笔记
53 | // @Tags 笔记
54 | // @Summary 新建笔记
55 | // @Description 新建一条笔记
56 | // @Accept json
57 | // @Produce json
58 | // @Param data body main.Note true "笔记内容"
59 | // @Success 201 {object} main.Note
60 | // @Failure 400 {object} main.httpError
61 | // @Failure 401 {object} main.httpError
62 | // @Failure 500 {object} main.httpError
63 | // @Security ApiKeyAuth
64 | // @Router /notes [post]
65 | func createNote(c echo.Context) error {
66 | var a = new(Note)
67 | if err := c.Bind(a); err != nil {
68 | return err
69 | }
70 | // 校验
71 | if a.Title == "" {
72 | return newHTTPError(400, "BadRequest", "Empty title")
73 | }
74 | if a.Content == "" {
75 | return newHTTPError(400, "BadRequest", "Empty content")
76 | }
77 | // 用户信息
78 | userID, err := parseUser(c)
79 | if err != nil {
80 | return err
81 | }
82 | a.UserID = userID
83 | // 保存
84 | if err := db.Create(a).Error; err != nil {
85 | return err
86 | }
87 |
88 | return c.JSON(http.StatusCreated, a)
89 | }
90 |
91 | // updateNote 更新笔记
92 | // @Tags 笔记
93 | // @Summary 更新笔记
94 | // @Description 更新指定id的笔记
95 | // @Accept json
96 | // @Produce json
97 | // @Param data body main.NoteUpdate true "更新内容"
98 | // @Success 200 {object} main.Note
99 | // @Failure 400 {object} main.httpError
100 | // @Failure 401 {object} main.httpError
101 | // @Failure 403 {object} main.httpError
102 | // @Failure 404 {object} main.httpError
103 | // @Failure 500 {object} main.httpError
104 | // @Security ApiKeyAuth
105 | // @Router /notes/{id} [put]
106 | func updateNote(c echo.Context) error {
107 | // 获取URL中的ID
108 | id, err := strconv.Atoi(c.Param("id"))
109 | if err != nil {
110 | return newHTTPError(400, "InvalidID", "请在URL中提供合法的ID")
111 | }
112 | var n = new(NoteUpdate)
113 | if err := c.Bind(n); err != nil {
114 | return err
115 | }
116 | old, err := findNoteByID(id)
117 | if err != nil {
118 | return err
119 | }
120 | // 用户权限
121 | userID, err := parseUser(c)
122 | if err != nil {
123 | return err
124 | }
125 | if userID != old.UserID {
126 | return ErrForbidden
127 | }
128 | // 利用指针检查是否有请求这个字段
129 | if n.Title != nil {
130 | if *n.Title == "" {
131 | return newHTTPError(400, "BadRequest", "Empty title")
132 | }
133 | old.Title = *n.Title
134 | }
135 | if n.Image != nil {
136 | old.Image = *n.Image
137 | }
138 | if n.Content != nil {
139 | if *n.Content == "" {
140 | return newHTTPError(400, "BadRequest", "Empty content")
141 | }
142 | old.Content = *n.Content
143 | }
144 | if n.IsPublic != nil {
145 | old.IsPublic = *n.IsPublic
146 | }
147 |
148 | if err := db.Save(old).Error; err != nil {
149 | return err
150 | }
151 |
152 | return c.JSON(http.StatusOK, old)
153 | }
154 |
155 | // deleteNote 删除笔记
156 | // @Tags 笔记
157 | // @Summary 删除笔记
158 | // @Description 删除指定id的笔记
159 | // @Accept json
160 | // @Produce json
161 | // @Param id path int true "笔记编号"
162 | // @Success 204
163 | // @Failure 400 {object} main.httpError
164 | // @Failure 401 {object} main.httpError
165 | // @Failure 403 {object} main.httpError
166 | // @Failure 404 {object} main.httpError
167 | // @Failure 500 {object} main.httpError
168 | // @Security ApiKeyAuth
169 | // @Router /notes/{id} [delete]
170 | func deleteNote(c echo.Context) error {
171 | id, err := strconv.Atoi(c.Param("id"))
172 | if err != nil {
173 | return newHTTPError(400, "InvalidID", "请在URL中提供合法的ID")
174 | }
175 | // 查询对象
176 | n, err := findNoteByID(id)
177 | if err != nil {
178 | return err
179 | }
180 | // 用户权限
181 | userID, err := parseUser(c)
182 | if err != nil {
183 | return err
184 | }
185 | if userID != n.UserID {
186 | return ErrForbidden
187 | }
188 | // 删除数据库对象
189 | if err := db.Delete(&Note{ID: id}).Error; err != nil {
190 | return err
191 | }
192 | return c.NoContent(http.StatusNoContent)
193 | }
194 |
195 | // getNote 获取笔记
196 | // @Tags 笔记
197 | // @Summary 获取笔记
198 | // @Description 获取指定id的笔记
199 | // @Accept json
200 | // @Produce json
201 | // @Param id path int true "笔记编号"
202 | // @Success 200 {object} main.Note
203 | // @Failure 400 {object} main.httpError
204 | // @Failure 401 {object} main.httpError
205 | // @Failure 403 {object} main.httpError
206 | // @Failure 404 {object} main.httpError
207 | // @Failure 500 {object} main.httpError
208 | // @Security ApiKeyAuth
209 | // @Router /notes/{id} [get]
210 | func getNote(c echo.Context) error {
211 | id, err := strconv.Atoi(c.Param("id"))
212 | if err != nil {
213 | return newHTTPError(400, "InvalidID", "请在URL中提供合法的ID")
214 | }
215 | n, err := findNoteByID(id)
216 | if err != nil {
217 | return err
218 | }
219 | // 用户权限
220 | userID, err := parseUser(c)
221 | if err != nil {
222 | return err
223 | }
224 | if userID != n.UserID && !n.IsPublic {
225 | return ErrForbidden
226 | }
227 | return c.JSON(http.StatusOK, n)
228 | }
229 |
230 | // getPublicNote 获取公开笔记
231 | // @Tags 笔记
232 | // @Summary 获取公开笔记
233 | // @Description 获取指定id的公开笔记
234 | // @Accept json
235 | // @Produce json
236 | // @Param id path int true "笔记编号"
237 | // @Success 200 {object} main.Note
238 | // @Failure 400 {object} main.httpError
239 | // @Failure 404 {object} main.httpError
240 | // @Failure 500 {object} main.httpError
241 | // @Router /public/notes/{id} [get]
242 | func getPublicNote(c echo.Context) error {
243 | id, err := strconv.Atoi(c.Param("id"))
244 | if err != nil {
245 | return newHTTPError(400, "InvalidID", "请在URL中提供合法的ID")
246 | }
247 | n, err := findNoteByID(id)
248 | if err != nil {
249 | return err
250 | }
251 | if !n.IsPublic {
252 | return ErrNotFound
253 | }
254 | return c.JSON(http.StatusOK, n)
255 | }
256 |
257 | // getNotes 获取用户笔记列表
258 | // @Tags 笔记
259 | // @Summary 获取用户笔记列表
260 | // @Description 获取用户的全部笔记,有分页,默认一页10条。
261 | // @Accept json
262 | // @Produce json
263 | // @Param page query int false "页码"
264 | // @Param per_page query int false "每页几条"
265 | // @Success 200 {array} main.Note
266 | // @Failure 400 {object} main.httpError
267 | // @Failure 401 {object} main.httpError
268 | // @Failure 500 {object} main.httpError
269 | // @Security ApiKeyAuth
270 | // @Router /notes [get]
271 | func getNotes(c echo.Context) error {
272 | // 提前make可以让查询没有结果的时候返回空列表
273 | var ns = make([]*Note, 0)
274 | // 用户信息
275 | userID, err := parseUser(c)
276 | if err != nil {
277 | return err
278 | }
279 | // 分页信息
280 | limit := c.Get("limit").(int)
281 | offset := c.Get("offset").(int)
282 | err = db.Where("user_id = ?", userID).Order("updated_at desc").
283 | Offset(offset).Limit(limit).Find(&ns).Error
284 | if err != nil {
285 | return err
286 | }
287 | setPaginationHeader(c, limit > len(ns))
288 | return c.JSON(http.StatusOK, ns)
289 | }
290 |
291 | // getPublicNotes 获取公开笔记列表
292 | // @Tags 笔记
293 | // @Summary 获取公开笔记列表
294 | // @Description 获取公开的全部笔记,有分页,默认一页10条。
295 | // @Accept json
296 | // @Produce json
297 | // @Param page query int false "页码"
298 | // @Param per_page query int false "每页几条"
299 | // @Success 200 {array} main.Note
300 | // @Failure 400 {object} main.httpError
301 | // @Failure 500 {object} main.httpError
302 | // @Router /public/notes [get]
303 | func getPublicNotes(c echo.Context) error {
304 | // 提前make可以让查询没有结果的时候返回空列表
305 | var ns = make([]*Note, 0)
306 | // 分页信息
307 | limit := c.Get("limit").(int)
308 | offset := c.Get("offset").(int)
309 | err := db.Where("is_public = true").Order("updated_at desc").
310 | Offset(offset).Limit(limit).Find(&ns).Error
311 | if err != nil {
312 | return err
313 | }
314 | setPaginationHeader(c, limit > len(ns))
315 | return c.JSON(http.StatusOK, ns)
316 | }
317 |
--------------------------------------------------------------------------------
/pagination.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "strconv"
5 |
6 | "github.com/labstack/echo/v4"
7 | "github.com/tomnomnom/linkheader"
8 | )
9 |
10 | // ParsePagination 获得页码,每页条数,Echo中间件。
11 | func ParsePagination(next echo.HandlerFunc) echo.HandlerFunc {
12 | return func(c echo.Context) error {
13 | var err error
14 | var page, pageSize int
15 | // 获得页码
16 | if c.QueryParam("page") == "" {
17 | page = 1
18 | } else {
19 | if page, err = strconv.Atoi(c.QueryParam("page")); err != nil {
20 | return newHTTPError(400, "InvalidPage", "请在URL中提供合法的页码")
21 | }
22 | }
23 | // 获得每页条数
24 | if c.QueryParam("per_page") == "" {
25 | pageSize = config.APP.PageSize
26 | } else {
27 | if pageSize, err = strconv.Atoi(c.QueryParam("per_page")); err != nil {
28 | return newHTTPError(400, "InvalidPage", "请在URL中提供合法的每页条数")
29 | }
30 | }
31 | // 设置查询数据时的 offset 和 limit
32 | c.Set("page", page)
33 | c.Set("offset", (page-1)*pageSize)
34 | c.Set("limit", pageSize)
35 | return next(c)
36 | }
37 | }
38 |
39 | // setPaginationHeader 设置分页相关 resp header
40 | // 如果要显示页码,还需要返回 X-Total-Count 和 Link 的 last 信息,可以多传入一个记录总数参数进行处理。
41 | // 移动应用一般不用知道总条数,传统的web分页器有时会需要。
42 | func setPaginationHeader(c echo.Context, isLast bool) {
43 | page := c.Get("page").(int)
44 | pageSize := c.Get("limit").(int)
45 | c.Response().Header().Set("X-Page-Num", strconv.Itoa(page))
46 | c.Response().Header().Set("X-Page-Size", strconv.Itoa(pageSize))
47 | link := linkheader.Links{
48 | {URL: config.APP.BaseURL + "?page=" + strconv.Itoa(page) + "&per_page=" + strconv.Itoa(pageSize), Rel: "self"},
49 | }
50 | if !isLast {
51 | link = append(link, linkheader.Link{URL: config.APP.BaseURL + "?page=" + strconv.Itoa(page+1) + "&per_page=" + strconv.Itoa(pageSize), Rel: "next"})
52 | }
53 | c.Response().Header().Set("Link", link.String())
54 | return
55 | }
56 |
--------------------------------------------------------------------------------
/store.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/go-redis/redis"
7 | "github.com/jinzhu/gorm"
8 | "github.com/sirupsen/logrus"
9 | )
10 |
11 | func initDB() {
12 | var err error
13 | // mysql conn
14 | for {
15 | db, err = gorm.Open("mysql", config.DB.User+":"+config.DB.Password+
16 | "@tcp("+config.DB.Host+":"+config.DB.Port+")/"+config.DB.Name+
17 | "?charset=utf8mb4&parseTime=True&loc=Local&timeout=90s")
18 | if err != nil {
19 | logrus.Warnf("waiting to connect to db: %s", err.Error())
20 | time.Sleep(time.Second * 2)
21 | continue
22 | }
23 | logrus.Info("Mysql connect successful.")
24 | break
25 | }
26 |
27 | // gorm debug log
28 | if config.APP.Debug {
29 | db.LogMode(true)
30 | }
31 | }
32 |
33 | // createTable gorm auto migrate tables
34 | func createTables() {
35 | db.AutoMigrate(&Note{})
36 | }
37 |
38 | func initRedis() {
39 | // redis conn
40 | rdb = redis.NewClient(&redis.Options{
41 | Addr: config.Redis.Host + ":" + config.Redis.Port,
42 | Password: config.Redis.Password,
43 | DB: config.Redis.DB,
44 | })
45 | logrus.Info("Redis connect successful.")
46 | }
47 |
--------------------------------------------------------------------------------
/util.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "database/sql/driver"
5 | "encoding/json"
6 | "errors"
7 | "strings"
8 |
9 | uuid "github.com/satori/go.uuid"
10 | "github.com/sirupsen/logrus"
11 | )
12 |
13 | // FileURL 图片链接
14 | type FileURL string
15 |
16 | // ToString 转换为string类型
17 | func (f FileURL) ToString() string {
18 | var s = string(f)
19 | var url = s
20 | if !strings.HasPrefix(s, "http") {
21 | url = config.APP.FileURL + s
22 | }
23 | return url
24 | }
25 |
26 | // MarshalJSON 转换为json类型 加域名
27 | func (f FileURL) MarshalJSON() ([]byte, error) {
28 | return json.Marshal(f.ToString())
29 | }
30 |
31 | // UnmarshalJSON 不做处理
32 | func (f *FileURL) UnmarshalJSON(data []byte) error {
33 | var tmp string
34 | if err := json.Unmarshal(data, &tmp); err != nil {
35 | return err
36 | }
37 | tmp = strings.TrimPrefix(tmp, config.APP.FileURL)
38 | *f = FileURL(tmp)
39 | return nil
40 | }
41 |
42 | // Scan implements the Scanner interface.
43 | func (f *FileURL) Scan(src interface{}) error {
44 | if src == nil {
45 | *f = ""
46 | return nil
47 | }
48 | tmp, ok := src.([]byte)
49 | if !ok {
50 | return errors.New("Read file url data from DB failed")
51 | }
52 | *f = FileURL(tmp)
53 | return nil
54 | }
55 |
56 | // Value implements the driver Valuer interface.
57 | func (f FileURL) Value() (driver.Value, error) {
58 | return string(f), nil
59 | }
60 |
61 | func newUUID() string {
62 | id, err := uuid.NewV1()
63 | if err != nil {
64 | logrus.Errorf("Gen uuid Error:%s", err.Error())
65 | return "error"
66 | }
67 | return id.String()
68 | }
69 |
--------------------------------------------------------------------------------