├── .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 | --------------------------------------------------------------------------------