├── .air.conf
├── .idea
├── .gitignore
├── bluebell.iml
├── misc.xml
└── modules.xml
├── Dockerfile
├── Makefile
├── README.md
├── bluebell_frontend.zip
├── config.yaml
├── controller
├── code.go
├── community.go
├── doc_response_models.go
├── post.go
├── post_test.go
├── request.go
├── response.go
├── user.go
├── validator.go
└── vote.go
├── dao
├── mysql
│ ├── community.go
│ ├── error_code.go
│ ├── mysql.go
│ ├── post.go
│ ├── post_test.go
│ └── user.go
└── redis
│ ├── keys.go
│ ├── post.go
│ ├── redis.go
│ └── vote.go
├── docker-compose.yml
├── go.mod
├── go.sum
├── logger
└── logger.go
├── logic
├── community.go
├── post.go
├── user.go
└── vote.go
├── main.go
├── middlewares
└── auth.go
├── models
├── community.go
├── create_table.sql
├── params.go
├── post.go
└── user.go
├── ossdemo
├── ossCreateBucket.go
├── ossGolang.go
├── ossReadFiles.go
└── ossUploadFile.go
├── pkg
├── jwt
│ └── jwt.go
└── snowflake
│ └── snowflake.go
├── router
└── route.go
├── setting
└── setting.go
├── tmp
└── main.exe
├── web_app
└── web_app.log
/.air.conf:
--------------------------------------------------------------------------------
1 | # [Air](https://github.com/cosmtrek/air) TOML 格式的配置文件
2 |
3 | # 工作目录
4 | # 使用 . 或绝对路径,请注意 `tmp_dir` 目录必须在 `root` 目录下
5 | root = "."
6 | tmp_dir = "tmp"
7 |
8 | [build]
9 | # 只需要写你平常编译使用的shell命令。你也可以使用 `make` 编译当前项目
10 | cmd = "go build -o ./tmp/main"
11 | # 由`cmd`命令得到的二进制文件名
12 | bin = "tmp/main"
13 | # 自定义的二进制,可以添加额外的编译标识例如添加 GIN_MODE=release
14 | full_bin = "./tmp/main ./conf/config.yaml"
15 | # 监听以下文件扩展名的文件.
16 | include_ext = ["go", "tpl", "tmpl", "html", "yaml"]
17 | # 忽略这些文件扩展名或目录
18 | exclude_dir = ["assets", "tmp", "vendor", "frontend/node_modules"]
19 | # 监听以下指定目录的文件
20 | include_dir = []
21 | # 排除以下文件
22 | exclude_file = []
23 | # 如果文件更改过于频繁,则没有必要在每次更改时都触发构建。可以设置触发构建的延迟时间
24 | delay = 1000 # ms
25 | # 发生构建错误时,停止运行旧的二进制文件。
26 | stop_on_error = true
27 | # air的日志文件名,该日志文件放置在你的`tmp_dir`中
28 | log = "air_errors.log"
29 |
30 | [log]
31 | # 显示日志时间
32 | time = true
33 |
34 | [color]
35 | # 自定义每个部分显示的颜色。如果找不到颜色,使用原始的应用程序日志。
36 | main = "magenta"
37 | watcher = "cyan"
38 | build = "yellow"
39 | runner = "green"
40 |
41 | [misc]
42 | # 退出时删除tmp目录
43 | clean_on_exit = true
--------------------------------------------------------------------------------
/.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/bluebell.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:alpine AS builder
2 |
3 | # 为我们的镜像设置必要的环境变量
4 | ENV GO111MODULE=on \
5 | CGO_ENABLED=0 \
6 | GOOS=linux \
7 | GOARCH=amd64
8 |
9 | # 移动到工作目录:/build
10 | WORKDIR /build
11 |
12 | # 复制项目中的 go.mod 和 go.sum文件并下载依赖信息
13 | COPY go.mod .
14 | COPY go.sum .
15 | RUN go mod download
16 |
17 | # 将代码复制到容器中
18 | COPY . .
19 |
20 | # 将我们的代码编译成二进制可执行文件 bluebell_app
21 | RUN go build -o bluebell_app .
22 |
23 | ###################
24 | # 接下来创建一个小镜像
25 | ###################
26 | FROM debian:stretch-slim
27 |
28 | COPY ./wait-for.sh /
29 | COPY ./templates /templates
30 | COPY ./static /static
31 | COPY ./conf /conf
32 |
33 | # 从builder镜像中把/dist/app 拷贝到当前目录
34 | COPY --from=builder /build/bluebell_app /
35 |
36 | RUN set -eux; \
37 | apt-get update; \
38 | apt-get install -y \
39 | --no-install-recommends \
40 | netcat; \
41 | chmod 755 wait-for.sh
42 |
43 | # 声明服务端口
44 | EXPOSE 8084
45 |
46 | # 需要运行的命令
47 | #ENTRYPOINT ["/bluebell_app", "conf/config.yaml"]
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: all build run gotool clean help
2 |
3 | BINARY="bluebell"
4 |
5 | all: gotool build
6 |
7 | build:
8 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ${BINARY}
9 |
10 | run:
11 | @go run ./main.go conf/config.yaml
12 |
13 | gotool:
14 | go fmt ./
15 | go vet ./
16 |
17 | clean:
18 | @if [ -f ${BINARY} ] ; then rm ${BINARY} ; fi
19 |
20 | help:
21 | @echo "make - 格式化 Go 代码, 并编译生成二进制文件"
22 | @echo "make build - 编译 Go 代码, 生成二进制文件"
23 | @echo "make run - 直接运行 Go 代码"
24 | @echo "make clean - 移除二进制文件和 vim swap files"
25 | @echo "make gotool - 运行 Go 工具 'fmt' and 'vet'"
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | :octocat:
2 |
3 | # bulebell
4 |
5 |
6 |
7 | ## update on 2020.10.20:前端文件好像无法上传到GitHub
8 |
9 | ## 有需要请加QQ 1099462011 :D
10 |
11 |
12 |
13 | ~~一个可以发帖看热度的小小网站~~ 最重要的是还可以~~摸鱼~~
14 |
15 | backend-technology-stack:gin
16 |
17 | frontend-technology-stack:vue
18 |
19 |
20 |
21 | **本项目同时提供了dockerfile以及docker-compose两种写好的文件,可以直接在**
22 |
23 | **docker上部署.也可以go build main.go生成二进制文件 使用nohop命令部署.**
24 |
25 | ## 大致页面:
26 |
27 | ### 主页面:
28 |
29 | 
30 |
31 | ### 本地运行页面:
32 |
33 | 
34 |
35 | ### 登录:
36 |
37 | 
38 |
39 |
40 | ### 注册:
41 |
42 | 
43 |
44 |
45 |
46 | **前端代码放在 liqiqi-front目录下**
47 |
48 | ```
49 | #记得先执行以下命令下载依赖
50 |
51 | npm install
52 |
53 | #打包部署到nginx
54 |
55 | npm run build
56 |
57 | ```
58 |
59 |
60 | **How to support this project:**
61 |
62 | 如果你觉得项目有用,可以请baijianruoliorz喝一杯咖啡 :D
63 |
64 | 
65 |
66 |
--------------------------------------------------------------------------------
/bluebell_frontend.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Programming-With-Love/bluebell/5cf4d11f91b6ad07c84605ad43348413fe27f9aa/bluebell_frontend.zip
--------------------------------------------------------------------------------
/config.yaml:
--------------------------------------------------------------------------------
1 | app:
2 | name: "bulebell"
3 | mode: "dev"
4 | port: 8080
5 |
6 | auth:
7 | jwt_expire8760
8 | log:
9 | level: "debug"
10 | filename: "web_app.log"
11 | max_size: 200
12 | max_age: 30
13 | max_backups: 7
14 | mysql:
15 | host: "127.0.0.1"
16 | port: 3306
17 | user: "root"
18 | password: "123456"
19 | dbname: "sql_demo"
20 | max_open_conns: 200
21 | max_idle_conns: 50
22 | redis:
23 | host: "127.0.0.1"
24 | port: 6379
25 | password: ""
26 | db: 0
27 | pool_size: 100
--------------------------------------------------------------------------------
/controller/code.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | /*
4 | * @author liqiqiorz
5 | * @data 2020/9/19 9:22
6 | */
7 | type ResCode int64
8 |
9 | const (
10 | CodeSuccess ResCode = 1000 + iota
11 | CodeInvalidParam
12 | CodeUserExist
13 | CodeUserNotExist
14 | CodeInvalidPassword
15 | CodeServerBusy
16 | CodeNeedLogin
17 | CoderInvalAuth
18 | )
19 |
20 | var codeMsgMap = map[ResCode]string{
21 | CodeSuccess: "success",
22 | CodeInvalidParam: "请求参数错误",
23 | CodeUserExist: "用户已存在",
24 | CodeUserNotExist: "用户不存在",
25 | CodeInvalidPassword: "用户名或密码错误",
26 | CodeServerBusy: "服务繁忙",
27 | CodeNeedLogin: "需要登录",
28 | CoderInvalAuth: "无效的token",
29 | }
30 |
31 | func (c ResCode) Msg() string {
32 | msg, ok := codeMsgMap[c]
33 | if !ok {
34 | msg = codeMsgMap[CodeServerBusy]
35 | }
36 | return msg
37 | }
38 |
--------------------------------------------------------------------------------
/controller/community.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "bluebell/logic"
5 | "github.com/gin-gonic/gin"
6 | "go.uber.org/zap"
7 | "strconv"
8 | )
9 |
10 | /*
11 | * @author liqiqiorz
12 | * @data 2020/9/19 21:24
13 | */
14 | //社区相关的
15 | func CommunityHandler(c *gin.Context) {
16 | // 查询到所有的社区 community_id community_name
17 | data, err := logic.GetCommunityList()
18 | if err != nil {
19 | zap.L().Error("logic.GetCommunityList() failed", zap.Error(err))
20 | //bu'qing'yi把服务端报错报给用户
21 | ResponseError(c, CodeServerBusy)
22 | return
23 | }
24 | ResponseSuccess(c, data)
25 | }
26 |
27 | func CommunityDetailHandler(c *gin.Context) {
28 | //获取社区ID,与冒号后面对应上
29 | communityID := c.Param("id")
30 | id, err := strconv.ParseInt(communityID, 10, 64)
31 | //如果不是字符类型,则报类型错误
32 | if err != nil {
33 | ResponseError(c, CodeInvalidParam)
34 | }
35 | // 查询到所有的社区 community_id community_name
36 | data, err := logic.GetCommunityDetail(id)
37 | if err != nil {
38 | zap.L().Error("logic.GetCommunityList() failed", zap.Error(err))
39 | //bu'qing'yi把服务端报错报给用户
40 | ResponseError(c, CodeServerBusy)
41 | return
42 | }
43 | ResponseSuccess(c, data)
44 | }
45 |
--------------------------------------------------------------------------------
/controller/doc_response_models.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import "bluebell/models"
4 |
5 | // 专门用来放接口文档用到的model
6 | // 因为我们的接口文档返回的数据格式是一致的,但是具体的data类型不一致
7 |
8 | // _ResponsePostList 帖子列表接口响应数据
9 | type _ResponsePostList struct {
10 | Code ResCode `json:"code"` // 业务响应状态码
11 | Message string `json:"message"` // 提示信息
12 | Data []*models.ApiPostDetail `json:"data"` // 数据
13 | }
14 |
--------------------------------------------------------------------------------
/controller/post.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "bluebell/logic"
5 | "bluebell/models"
6 | "github.com/gin-gonic/gin"
7 | "go.uber.org/zap"
8 | "strconv"
9 | )
10 |
11 | /*
12 | * @author liqiqiorz
13 | * @data 2020/9/20 0:12
14 | */
15 | func CreatePostHandler(c *gin.Context) {
16 | //1.获取参数以及参数的校验
17 | p := new(models.Post)
18 | if err := c.ShouldBindJSON(p); err != nil {
19 | zap.L().Debug("error", zap.Any("err", err))
20 | zap.L().Error("create post with invalid param")
21 | ResponseError(c, CodeInvalidParam)
22 | return
23 | }
24 | //从c.content里面取到当前用户的ID值
25 | userID, err := getCurrentUser(c)
26 | if err != nil {
27 | ResponseError(c, CodeNeedLogin)
28 | return
29 | }
30 | p.AuthorID = userID
31 | // 2.创建帖子
32 | if err := logic.CreatePost(p); err != nil {
33 | zap.L().Error("logic.CreatePost(p) failed", zap.Error(err))
34 | ResponseError(c, CodeInvalidParam)
35 | return
36 | }
37 | // 3.返回相应
38 | //返回成功的相应
39 | ResponseSuccess(c, nil)
40 | }
41 |
42 | //获取帖子详情
43 | func GetPostDetailHandler(c *gin.Context) {
44 | // 获取参数(帖子的ID
45 | pidStr := c.Param("id")
46 | //这个方法是解析字符串,1表示字符串,二表示十进制,3表示64位
47 | pid, err := strconv.ParseInt(pidStr, 10, 64)
48 | if err != nil {
49 | zap.L().Error("get post detail with invalid param", zap.Error(err))
50 | ResponseError(c, CodeInvalidParam)
51 | return
52 | }
53 | // 根据ID取出帖子数据层
54 | data, err := logic.GetPostById(pid)
55 | if err != nil {
56 | zap.L().Error("logic.GetPostById(pid) failed", zap.Error(err))
57 | ResponseError(c, CodeServerBusy)
58 | return
59 | }
60 | // 返回相应
61 | ResponseSuccess(c, data)
62 | }
63 |
64 | //GetPostListHandler 获取帖子列表的接口
65 | func GetPostListHandler(c *gin.Context) {
66 | // 获取分页参数
67 | page, size := getPageInfo(c)
68 | // 获取数据
69 | data, err := logic.GetPostList(page, size)
70 | if err != nil {
71 | zap.L().Error("logic.GetPostList() failed", zap.Error(err))
72 | ResponseError(c, CodeServerBusy)
73 | return
74 | }
75 | ResponseSuccess(c, data)
76 | // 返回响应
77 | }
78 |
--------------------------------------------------------------------------------
/controller/post_test.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "net/http"
7 | "net/http/httptest"
8 | "testing"
9 |
10 | "github.com/gin-gonic/gin"
11 | "github.com/stretchr/testify/assert"
12 | )
13 |
14 | func TestCreatePostHandler(t *testing.T) {
15 |
16 | gin.SetMode(gin.TestMode)
17 | r := gin.Default()
18 | url := "/api/v1/post"
19 | r.POST(url, CreatePostHandler)
20 |
21 | body := `{
22 | "community_id": 1,
23 | "title": "test",
24 | "content": "just a test"
25 | }`
26 |
27 | req, _ := http.NewRequest(http.MethodPost, url, bytes.NewReader([]byte(body)))
28 |
29 | w := httptest.NewRecorder()
30 | r.ServeHTTP(w, req)
31 |
32 | assert.Equal(t, 200, w.Code)
33 |
34 | // 判断响应的内容是不是按预期返回了需要登录的错误
35 |
36 | // 方法1:判断响应内容中是不是包含指定的字符串
37 | //assert.Contains(t, w.Body.String(), "需要登录")
38 |
39 | // 方法2:将响应的内容反序列化到ResponseData 然后判断字段与预期是否一致
40 | res := new(ResponseData)
41 | if err := json.Unmarshal(w.Body.Bytes(), res); err != nil {
42 | t.Fatalf("json.Unmarshal w.Body failed, err:%v\n", err)
43 | }
44 | assert.Equal(t, res.Code, CodeNeedLogin)
45 | }
46 |
--------------------------------------------------------------------------------
/controller/request.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "errors"
5 | "strconv"
6 |
7 | "github.com/gin-gonic/gin"
8 | )
9 |
10 | const CtxUserIDKey = "userID"
11 |
12 | var ErrorUserNotLogin = errors.New("用户未登录")
13 |
14 | // getCurrentUserID 获取当前登录的用户ID
15 | func getCurrentUserID(c *gin.Context) (userID int64, err error) {
16 | uid, ok := c.Get(CtxUserIDKey)
17 | if !ok {
18 | err = ErrorUserNotLogin
19 | return
20 | }
21 | userID, ok = uid.(int64)
22 | if !ok {
23 | err = ErrorUserNotLogin
24 | return
25 | }
26 | return
27 | }
28 |
29 | // getCurrentUser 获取当前登录的用户ID
30 | func getCurrentUser(c *gin.Context) (userID int64, err error) {
31 | uid, ok := c.Get(CtxUserIDKey)
32 | if !ok {
33 | err = ErrorUserNotLogin
34 | return
35 | }
36 | userID, ok = uid.(int64)
37 | if !ok {
38 | err = ErrorUserNotLogin
39 | return
40 | }
41 | return
42 | }
43 |
44 | func getPageInfo(c *gin.Context) (int64, int64) {
45 | pageStr := c.Query("page")
46 | sizeStr := c.Query("size")
47 |
48 | var (
49 | page int64
50 | size int64
51 | err error
52 | )
53 |
54 | page, err = strconv.ParseInt(pageStr, 10, 64)
55 | if err != nil {
56 | page = 1
57 | }
58 | size, err = strconv.ParseInt(sizeStr, 10, 64)
59 | if err != nil {
60 | size = 10
61 | }
62 | return page, size
63 | }
64 |
--------------------------------------------------------------------------------
/controller/response.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "net/http"
6 | )
7 |
8 | /*
9 | * @author liqiqiorz
10 | * @data 2020/9/19 9:19
11 | */
12 | //{
13 | // "code": 10001//错误码
14 | // "msg": xx,
15 | // "data":{
16 | //
17 | //}
18 | //}
19 |
20 | type ResponseData struct {
21 | Code ResCode `json:"code"`
22 | Msg interface{} `json:"msg"`
23 | Data interface{} `json:"data"`
24 | }
25 |
26 | func ResponseError(c *gin.Context, code ResCode) {
27 | re := &ResponseData{
28 | Code: code,
29 | Msg: code.Msg(),
30 | Data: nil,
31 | }
32 | c.JSON(http.StatusOK, re)
33 | }
34 | func ResponseErrorWithMsg(c *gin.Context, code ResCode, msg interface{}) {
35 | c.JSON(http.StatusOK, &ResponseData{
36 | Code: code,
37 | Msg: msg,
38 | Data: nil,
39 | })
40 | }
41 |
42 | func ResponseSuccess(c *gin.Context, data interface{}) {
43 | re := &ResponseData{
44 | Code: CodeSuccess,
45 | Msg: CodeSuccess.Msg(),
46 | Data: data,
47 | }
48 | c.JSON(http.StatusOK, re)
49 | }
50 |
51 | //返回token后 在authorized里面选择 bearer token 然后带上token即可访问
52 |
--------------------------------------------------------------------------------
/controller/user.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "bluebell/dao/mysql"
5 | "bluebell/logic"
6 | "bluebell/models"
7 | "errors"
8 |
9 | "github.com/go-playground/validator/v10"
10 |
11 | "go.uber.org/zap"
12 |
13 | "github.com/gin-gonic/gin"
14 | )
15 |
16 | // SignUpHandler 处理注册请求的函数
17 | func SignUpHandler(c *gin.Context) {
18 | // 1. 获取参数和参数校验
19 | p := new(models.ParamSignUp)
20 | if err := c.ShouldBindJSON(p); err != nil {
21 | // 请求参数有误,直接返回响应
22 | zap.L().Error("SignUp with invalid param", zap.Error(err))
23 | // 判断err是不是validator.ValidationErrors 类型
24 | errs, ok := err.(validator.ValidationErrors)
25 | if !ok {
26 | ResponseError(c, CodeInvalidParam)
27 | return
28 | }
29 | ResponseErrorWithMsg(c, CodeInvalidParam, removeTopStruct(errs.Translate(trans)))
30 | return
31 | }
32 | // 2. 业务处理
33 | if err := logic.SignUp(p); err != nil {
34 | zap.L().Error("logic.SignUp failed", zap.Error(err))
35 | if errors.Is(err, mysql.ErrorUserExist) {
36 | ResponseError(c, CodeUserExist)
37 | return
38 | }
39 | ResponseError(c, CodeServerBusy)
40 | return
41 | }
42 | // 3. 返回响应
43 | ResponseSuccess(c, nil)
44 | }
45 |
46 | func LoginHandler(c *gin.Context) {
47 | // 1.获取请求参数及参数校验
48 | p := new(models.ParamLogin)
49 | if err := c.ShouldBindJSON(p); err != nil {
50 | // 请求参数有误,直接返回响应
51 | zap.L().Error("Login with invalid param", zap.Error(err))
52 | // 判断err是不是validator.ValidationErrors 类型
53 | errs, ok := err.(validator.ValidationErrors)
54 | if !ok {
55 | ResponseError(c, CodeInvalidParam)
56 | return
57 | }
58 | ResponseErrorWithMsg(c, CodeInvalidParam, removeTopStruct(errs.Translate(trans)))
59 | return
60 | }
61 | // 2.业务逻辑处理
62 | token, err := logic.Login(p)
63 | if err != nil {
64 | zap.L().Error("logic.Login failed", zap.String("username", p.Username), zap.Error(err))
65 | if errors.Is(err, mysql.ErrorUserNotExist) {
66 | ResponseError(c, CodeUserNotExist)
67 | return
68 | }
69 | ResponseError(c, CodeInvalidPassword)
70 | return
71 | }
72 |
73 | // 3.返回响应
74 | ResponseSuccess(c, token)
75 | }
76 |
--------------------------------------------------------------------------------
/controller/validator.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | /*
4 | * @author liqiqiorz
5 | * @data 2020/9/18 22:20
6 | */
7 |
8 | import (
9 | "bluebell/models"
10 | "fmt"
11 | "reflect"
12 | "strings"
13 |
14 | "github.com/gin-gonic/gin/binding"
15 | "github.com/go-playground/locales/en"
16 | "github.com/go-playground/locales/zh"
17 | ut "github.com/go-playground/universal-translator"
18 | "github.com/go-playground/validator/v10"
19 | enTranslations "github.com/go-playground/validator/v10/translations/en"
20 | zhTranslations "github.com/go-playground/validator/v10/translations/zh"
21 | )
22 |
23 | // 定义一个全局翻译器T
24 | var trans ut.Translator
25 |
26 | // InitTrans 初始化翻译器
27 | func InitTrans(locale string) (err error) {
28 | // 修改gin框架中的Validator引擎属性,实现自定制
29 | if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
30 |
31 | // 注册一个获取json tag的自定义方法
32 | //作为返回信息提示的字段,不然错误容易看不懂~
33 | v.RegisterTagNameFunc(func(fld reflect.StructField) string {
34 | name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
35 | if name == "-" {
36 | return ""
37 | }
38 | return name
39 | })
40 |
41 | // 为SignUpParam注册自定义校验方法
42 | v.RegisterStructValidation(SignUpParamStructLevelValidation, models.ParamSignUp{})
43 |
44 | zhT := zh.New() // 中文翻译器
45 | enT := en.New() // 英文翻译器
46 |
47 | // 第一个参数是备用(fallback)的语言环境
48 | // 后面的参数是应该支持的语言环境(支持多个)
49 | // uni := ut.New(zhT, zhT) 也是可以的
50 | uni := ut.New(enT, zhT, enT)
51 |
52 | // locale 通常取决于 http 请求头的 'Accept-Language'
53 | var ok bool
54 | // 也可以使用 uni.FindTranslator(...) 传入多个locale进行查找
55 | trans, ok = uni.GetTranslator(locale)
56 | if !ok {
57 | return fmt.Errorf("uni.GetTranslator(%s) failed", locale)
58 | }
59 |
60 | // 注册翻译器
61 | switch locale {
62 | case "en":
63 | err = enTranslations.RegisterDefaultTranslations(v, trans)
64 | case "zh":
65 | err = zhTranslations.RegisterDefaultTranslations(v, trans)
66 | default:
67 | err = enTranslations.RegisterDefaultTranslations(v, trans)
68 | }
69 | return
70 | }
71 | return
72 | }
73 |
74 | // removeTopStruct 去除提示信息中的结构体名称,让错误更加清晰
75 | func removeTopStruct(fields map[string]string) map[string]string {
76 | res := map[string]string{}
77 | for field, err := range fields {
78 | res[field[strings.Index(field, ".")+1:]] = err
79 | }
80 | return res
81 | }
82 |
83 | // SignUpParamStructLevelValidation 自定义SignUpParam结构体校验函数
84 | func SignUpParamStructLevelValidation(sl validator.StructLevel) {
85 | su := sl.Current().Interface().(models.ParamSignUp)
86 |
87 | if su.Password != su.RePassword {
88 | // 输出错误提示信息,最后一个参数就是传递的param
89 | sl.ReportError(su.RePassword, "re_password", "RePassword", "eqfield", "password")
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/controller/vote.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "bluebell/logic"
5 | "bluebell/models"
6 |
7 | "go.uber.org/zap"
8 |
9 | "github.com/go-playground/validator/v10"
10 |
11 | "github.com/gin-gonic/gin"
12 | )
13 |
14 | // 投票
15 |
16 | //type VoteData struct {
17 | // // UserID 从请求中获取当前的用户
18 | // PostID int64 `json:"post_id,string"` // 贴子id
19 | // Direction int `json:"direction,string"` // 赞成票(1)还是反对票(-1)
20 | //}
21 |
22 | func PostVoteController(c *gin.Context) {
23 | // 参数校验
24 | p := new(models.ParamVoteData)
25 | if err := c.ShouldBindJSON(p); err != nil {
26 | errs, ok := err.(validator.ValidationErrors) // 类型断言
27 | if !ok {
28 | ResponseError(c, CodeInvalidParam)
29 | return
30 | }
31 | errData := removeTopStruct(errs.Translate(trans)) // 翻译并去除掉错误提示中的结构体标识
32 | ResponseErrorWithMsg(c, CodeInvalidParam, errData)
33 | return
34 | }
35 | // 获取当前请求的用户的id
36 | userID, err := getCurrentUserID(c)
37 | if err != nil {
38 | ResponseError(c, CodeNeedLogin)
39 | return
40 | }
41 | // 具体投票的业务逻辑
42 | if err := logic.VoteForPost(userID, p); err != nil {
43 | zap.L().Error("logic.VoteForPost() failed", zap.Error(err))
44 | ResponseError(c, CodeServerBusy)
45 | return
46 | }
47 |
48 | ResponseSuccess(c, nil)
49 | }
50 |
--------------------------------------------------------------------------------
/dao/mysql/community.go:
--------------------------------------------------------------------------------
1 | package mysql
2 |
3 | import (
4 | "bluebell/models"
5 | "database/sql"
6 | "go.uber.org/zap"
7 | )
8 |
9 | /*
10 | * @author liqiqiorz
11 | * @data 2020/9/19 21:32
12 | */
13 | //
14 | func GetCommunityList() (data []*models.Community, err error) {
15 | // 查询数据
16 | sqlStr := "select community_id,community_name from community"
17 | if err := db.Select(&data, sqlStr); err != nil {
18 | //查询为空
19 | if err == sql.ErrNoRows {
20 | zap.L().Warn("there is no community in db")
21 | err = nil
22 | }
23 | }
24 | return
25 | }
26 |
27 | //p51这里解决很经典,把参数实际话
28 | func GetCommunityDetailByID(id int64) (community *models.CommunityDetail, err error) {
29 | community = new(models.CommunityDetail)
30 | sqlStr := "select community_id, community_name,introduction,create_time from community where community_id=?"
31 | if err := db.Get(community, sqlStr, id); err != nil {
32 | if err == sql.ErrNoRows {
33 | //自定义的错误
34 | err = ErrorInvalidID
35 | }
36 | }
37 | return community, err
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/dao/mysql/error_code.go:
--------------------------------------------------------------------------------
1 | package mysql
2 |
3 | import "errors"
4 |
5 | var (
6 | ErrorUserExist = errors.New("用户已存在")
7 | ErrorUserNotExist = errors.New("用户不存在")
8 | ErrorInvalidPassword = errors.New("用户名或密码错误")
9 | ErrorInvalidID = errors.New("无效的ID")
10 | )
11 |
--------------------------------------------------------------------------------
/dao/mysql/mysql.go:
--------------------------------------------------------------------------------
1 | package mysql
2 |
3 | import (
4 | "bluebell/setting"
5 | "fmt"
6 | _ "github.com/go-sql-driver/mysql"
7 | "github.com/jmoiron/sqlx"
8 | )
9 |
10 | var db *sqlx.DB
11 |
12 | // Init 初始化MySQL连接
13 | func Init(cfg *setting.MySQLConfig) (err error) {
14 | // "user:password@tcp(host:port)/dbname"
15 | dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true&loc=Local", cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.DB)
16 | db, err = sqlx.Connect("mysql", dsn)
17 | if err != nil {
18 | return
19 | }
20 | db.SetMaxOpenConns(cfg.MaxOpenConns)
21 | db.SetMaxIdleConns(cfg.MaxIdleConns)
22 | return
23 | }
24 |
25 | // Close 关闭MySQL连接
26 | func Close() {
27 | _ = db.Close()
28 | }
29 |
--------------------------------------------------------------------------------
/dao/mysql/post.go:
--------------------------------------------------------------------------------
1 | package mysql
2 |
3 | import (
4 | "bluebell/models"
5 | )
6 |
7 | /*
8 | * @author liqiqiorz
9 | * @data 2020/9/20 8:54
10 | */
11 | func CreatePost(p *models.Post) (err error) {
12 | //这里注意sql 语句只能用`
13 | sqlStr := `insert into post(post_id,title,content,author_id,community_id) values(?,?,?,?,?)`
14 | _, err = db.Exec(sqlStr, p.ID, p.Title, p.Content, p.AuthorID, p.CommunityID)
15 | return
16 | }
17 |
18 | func GetPostById(pid int64) (post *models.Post, err error) {
19 | post = new(models.Post)
20 | sqlStr := `select post_id,title,content,author_id,community_id,create_time from post where post_id=?`
21 | err = db.Get(post, sqlStr, pid)
22 | return
23 |
24 | }
25 |
26 | // GetPostList 查询帖子列表函数
27 | func GetPostList(page, size int64) (posts []*models.Post, err error) {
28 | sqlStr := `select
29 | post_id, title, content, author_id, community_id, create_time
30 | from post
31 | ORDER BY create_time
32 | DESC
33 | limit ?,?
34 | `
35 | posts = make([]*models.Post, 0, 2) // 不要写成make([]*models.Post, 2)
36 | err = db.Select(&posts, sqlStr, (page-1)*size, size)
37 | return
38 | }
39 |
--------------------------------------------------------------------------------
/dao/mysql/post_test.go:
--------------------------------------------------------------------------------
1 | package mysql
2 |
3 | import (
4 | "bluebell/models"
5 | "bluebell/setting"
6 | "testing"
7 | )
8 |
9 | func init() {
10 | dbCfg := setting.MySQLConfig{
11 | Host: "127.0.0.1",
12 | User: "root",
13 | Password: "root1234",
14 | DB: "bluebell",
15 | Port: 13306,
16 | MaxOpenConns: 10,
17 | MaxIdleConns: 10,
18 | }
19 | err := Init(&dbCfg)
20 | if err != nil {
21 | panic(err)
22 | }
23 | }
24 |
25 | func TestCreatePost(t *testing.T) {
26 | post := models.Post{
27 | ID: 10,
28 | AuthorID: 123,
29 | CommunityID: 1,
30 | Title: "test",
31 | Content: "just a test",
32 | }
33 | err := CreatePost(&post)
34 | if err != nil {
35 | t.Fatalf("CreatePost insert record into mysql failed, err:%v\n", err)
36 | }
37 | t.Logf("CreatePost insert record into mysql success")
38 | }
39 |
--------------------------------------------------------------------------------
/dao/mysql/user.go:
--------------------------------------------------------------------------------
1 | package mysql
2 |
3 | import (
4 | "bluebell/models"
5 | "crypto/md5"
6 | "database/sql"
7 | "encoding/hex"
8 | )
9 |
10 | // 把每一步数据库操作封装成函数
11 | // 待logic层根据业务需求调用
12 |
13 | const secret = "yangxiangrui.site"
14 |
15 | // CheckUserExist 检查指定用户名的用户是否存在
16 | func CheckUserExist(username string) (err error) {
17 | sqlStr := `select count(user_id) from user where username = ?`
18 | var count int64
19 | if err := db.Get(&count, sqlStr, username); err != nil {
20 | return err
21 | }
22 | if count > 0 {
23 | return ErrorUserExist
24 | }
25 | return
26 | }
27 |
28 | // InsertUser 想数据库中插入一条新的用户记录,这个返回错误信息
29 | func InsertUser(user *models.User) (err error) {
30 | // 对密码进行加密
31 | user.Password = encryptPassword(user.Password)
32 | // 执行SQL语句入库
33 | sqlStr := `insert into user(user_id, username, password) values(?,?,?)`
34 | //传入sql加三个参数
35 | _, err = db.Exec(sqlStr, user.UserID, user.Username, user.Password)
36 | return
37 | }
38 |
39 | // encryptPassword 密码加密,加密算法,以后直接抄得了
40 | func encryptPassword(oPassword string) string {
41 | h := md5.New()
42 | //加盐的字符串
43 | h.Write([]byte(secret))
44 | return hex.EncodeToString(h.Sum([]byte(oPassword)))
45 | }
46 |
47 | func Login(user *models.User) (err error) {
48 | oPassword := user.Password // 用户登录的密码
49 | sqlStr := `select user_id, username, password from user where username=?`
50 | err = db.Get(user, sqlStr, user.Username)
51 | if err == sql.ErrNoRows {
52 | return ErrorUserNotExist
53 | }
54 | if err != nil {
55 | // 查询数据库失败
56 | return err
57 | }
58 | // 判断密码是否正确
59 | password := encryptPassword(oPassword)
60 | if password != user.Password {
61 | return ErrorInvalidPassword
62 | }
63 | return
64 | }
65 | func GetUserById(uid int64) (user *models.User, err error) {
66 | user = new(models.User)
67 | sqlStr := `select user_id, username from user where user_id = ?`
68 | err = db.Get(user, sqlStr, uid)
69 | return
70 | }
71 |
--------------------------------------------------------------------------------
/dao/redis/keys.go:
--------------------------------------------------------------------------------
1 | package redis
2 |
3 | // redis key
4 |
5 | // redis key注意使用命名空间的方式,方便查询和拆分
6 |
7 | const (
8 | Prefix = "bluebell:" // 项目key前缀
9 | KeyPostTimeZSet = "post:time" // zset;贴子及发帖时间
10 | KeyPostScoreZSet = "post:score" // zset;贴子及投票的分数
11 | KeyPostVotedZSetPF = "post:voted:" // zset;记录用户及投票类型;参数是post id
12 |
13 | KeyCommunitySetPF = "community:" // set;保存每个分区下帖子的id
14 | )
15 |
16 | // 给redis key加上前缀
17 | func getRedisKey(key string) string {
18 | return Prefix + key
19 | }
20 |
--------------------------------------------------------------------------------
/dao/redis/post.go:
--------------------------------------------------------------------------------
1 | package redis
2 |
3 | import (
4 | "bluebell/models"
5 | "strconv"
6 | "time"
7 |
8 | "github.com/go-redis/redis"
9 | )
10 |
11 | func getIDsFormKey(key string, page, size int64) ([]string, error) {
12 | start := (page - 1) * size
13 | end := start + size - 1
14 | // 3. ZREVRANGE 按分数从大到小的顺序查询指定数量的元素
15 | return client.ZRevRange(key, start, end).Result()
16 | }
17 |
18 | func GetPostIDsInOrder(p *models.ParamPostList) ([]string, error) {
19 | // 从redis获取id
20 | // 1. 根据用户请求中携带的order参数确定要查询的redis key
21 | key := getRedisKey(KeyPostTimeZSet)
22 | if p.Order == models.OrderScore {
23 | key = getRedisKey(KeyPostScoreZSet)
24 | }
25 | // 2. 确定查询的索引起始点
26 | return getIDsFormKey(key, p.Page, p.Size)
27 | }
28 |
29 | // GetPostVoteData 根据ids查询每篇帖子的投赞成票的数据
30 | func GetPostVoteData(ids []string) (data []int64, err error) {
31 | //data = make([]int64, 0, len(ids))
32 | //for _, id := range ids {
33 | // key := getRedisKey(KeyPostVotedZSetPF + id)
34 | // // 查找key中分数是1的元素的数量->统计每篇帖子的赞成票的数量
35 | // v := client.ZCount(key, "1", "1").Val()
36 | // data = append(data, v)
37 | //}
38 | // 使用pipeline一次发送多条命令,减少RTT
39 | pipeline := client.Pipeline()
40 | for _, id := range ids {
41 | key := getRedisKey(KeyPostVotedZSetPF + id)
42 | pipeline.ZCount(key, "1", "1")
43 | }
44 | cmders, err := pipeline.Exec()
45 | if err != nil {
46 | return nil, err
47 | }
48 | data = make([]int64, 0, len(cmders))
49 | for _, cmder := range cmders {
50 | v := cmder.(*redis.IntCmd).Val()
51 | data = append(data, v)
52 | }
53 | return
54 | }
55 |
56 | // GetCommunityPostIDsInOrder 按社区查询ids
57 | func GetCommunityPostIDsInOrder(p *models.ParamPostList) ([]string, error) {
58 |
59 | orderKey := getRedisKey(KeyPostTimeZSet)
60 | if p.Order == models.OrderScore {
61 | orderKey = getRedisKey(KeyPostScoreZSet)
62 | }
63 |
64 | // 使用 zinterstore 把分区的帖子set与帖子分数的 zset 生成一个新的zset
65 | // 针对新的zset 按之前的逻辑取数据
66 |
67 | // 社区的key
68 | cKey := getRedisKey(KeyCommunitySetPF + strconv.Itoa(int(p.CommunityID)))
69 |
70 | // 利用缓存key减少zinterstore执行的次数
71 | key := orderKey + strconv.Itoa(int(p.CommunityID))
72 | if client.Exists(key).Val() < 1 {
73 | // 不存在,需要计算
74 | pipeline := client.Pipeline()
75 | pipeline.ZInterStore(key, redis.ZStore{
76 | Aggregate: "MAX",
77 | }, cKey, orderKey) // zinterstore 计算
78 | pipeline.Expire(key, 60*time.Second) // 设置超时时间
79 | _, err := pipeline.Exec()
80 | if err != nil {
81 | return nil, err
82 | }
83 | }
84 | // 存在的话就直接根据key查询ids
85 | return getIDsFormKey(key, p.Page, p.Size)
86 | }
87 |
--------------------------------------------------------------------------------
/dao/redis/redis.go:
--------------------------------------------------------------------------------
1 | package redis
2 |
3 | import (
4 | "bluebell/setting"
5 | "fmt"
6 |
7 | "github.com/go-redis/redis"
8 | )
9 |
10 | var (
11 | client *redis.Client
12 | Nil = redis.Nil
13 | )
14 |
15 | // Init 初始化连接
16 | func Init(cfg *setting.RedisConfig) (err error) {
17 | client = redis.NewClient(&redis.Options{
18 | Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
19 | Password: cfg.Password, // no password set
20 | DB: cfg.DB, // use default DB
21 | PoolSize: cfg.PoolSize,
22 | MinIdleConns: cfg.MinIdleConns,
23 | })
24 |
25 | _, err = client.Ping().Result()
26 | if err != nil {
27 | return err
28 | }
29 | return nil
30 | }
31 |
32 | func Close() {
33 | _ = client.Close()
34 | }
35 |
--------------------------------------------------------------------------------
/dao/redis/vote.go:
--------------------------------------------------------------------------------
1 | package redis
2 |
3 | import (
4 | "errors"
5 | "math"
6 | "strconv"
7 | "time"
8 |
9 | "github.com/go-redis/redis"
10 | )
11 |
12 | // 推荐阅读
13 | // 基于用户投票的相关算法:http://www.ruanyifeng.com/blog/algorithm/
14 |
15 | // 本项目使用简化版的投票分数
16 | // 投一票就加432分 86400/200 --> 200张赞成票可以给你的帖子续一天
17 |
18 | /* 投票的几种情况:
19 | direction=1时,有两种情况:
20 | 1. 之前没有投过票,现在投赞成票 --> 更新分数和投票记录 差值的绝对值:1 +432
21 | 2. 之前投反对票,现在改投赞成票 --> 更新分数和投票记录 差值的绝对值:2 +432*2
22 | direction=0时,有两种情况:
23 | 1. 之前投过反对票,现在要取消投票 --> 更新分数和投票记录 差值的绝对值:1 +432
24 | 2. 之前投过赞成票,现在要取消投票 --> 更新分数和投票记录 差值的绝对值:1 -432
25 | direction=-1时,有两种情况:
26 | 1. 之前没有投过票,现在投反对票 --> 更新分数和投票记录 差值的绝对值:1 -432
27 | 2. 之前投赞成票,现在改投反对票 --> 更新分数和投票记录 差值的绝对值:2 -432*2
28 |
29 | 投票的限制:
30 | 每个贴子自发表之日起一个星期之内允许用户投票,超过一个星期就不允许再投票了。
31 | 1. 到期之后将redis中保存的赞成票数及反对票数存储到mysql表中
32 | 2. 到期之后删除那个 KeyPostVotedZSetPF
33 | */
34 |
35 | const (
36 | oneWeekInSeconds = 7 * 24 * 3600
37 | scorePerVote = 432 // 每一票值多少分
38 | )
39 |
40 | var (
41 | ErrVoteTimeExpire = errors.New("投票时间已过")
42 | ErrVoteRepeated = errors.New("不允许重复投票")
43 | )
44 |
45 | func CreatePost(postID, communityID int64) error {
46 | pipeline := client.TxPipeline()
47 | // 帖子时间
48 | pipeline.ZAdd(getRedisKey(KeyPostTimeZSet), redis.Z{
49 | Score: float64(time.Now().Unix()),
50 | Member: postID,
51 | })
52 |
53 | // 帖子分数
54 | pipeline.ZAdd(getRedisKey(KeyPostScoreZSet), redis.Z{
55 | Score: float64(time.Now().Unix()),
56 | Member: postID,
57 | })
58 | // 更新:把帖子id加到社区的set
59 | cKey := getRedisKey(KeyCommunitySetPF + strconv.Itoa(int(communityID)))
60 | pipeline.SAdd(cKey, postID)
61 | _, err := pipeline.Exec()
62 | return err
63 | }
64 |
65 | func VoteForPost(userID, postID string, value float64) error {
66 | // 1. 判断投票限制
67 | // 去redis取帖子发布时间
68 | postTime := client.ZScore(getRedisKey(KeyPostTimeZSet), postID).Val()
69 | if float64(time.Now().Unix())-postTime > oneWeekInSeconds {
70 | return ErrVoteTimeExpire
71 | }
72 | // 2和3需要放到一个pipeline事务中操作
73 |
74 | // 2. 更新贴子的分数
75 | // 先查当前用户给当前帖子的投票记录
76 | ov := client.ZScore(getRedisKey(KeyPostVotedZSetPF+postID), userID).Val()
77 |
78 | // 更新:如果这一次投票的值和之前保存的值一致,就提示不允许重复投票
79 | if value == ov {
80 | return ErrVoteRepeated
81 | }
82 | var op float64
83 | if value > ov {
84 | op = 1
85 | } else {
86 | op = -1
87 | }
88 | diff := math.Abs(ov - value) // 计算两次投票的差值
89 | pipeline := client.TxPipeline()
90 | pipeline.ZIncrBy(getRedisKey(KeyPostScoreZSet), op*diff*scorePerVote, postID)
91 |
92 | // 3. 记录用户为该贴子投票的数据
93 | if value == 0 {
94 | pipeline.ZRem(getRedisKey(KeyPostVotedZSetPF+postID), userID)
95 | } else {
96 | pipeline.ZAdd(getRedisKey(KeyPostVotedZSetPF+postID), redis.Z{
97 | Score: value, // 赞成票还是反对票
98 | Member: userID,
99 | })
100 | }
101 | _, err := pipeline.Exec()
102 | return err
103 | }
104 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | # yaml 配置
2 | version: "3.7"
3 | services:
4 | mysql8019:
5 | image: "mysql:8.0.19"
6 | ports:
7 | - "33061:3306"
8 | command: "--default-authentication-plugin=mysql_native_password --init-file /data/application/init.sql"
9 | environment:
10 | MYSQL_ROOT_PASSWORD: "root"
11 | MYSQL_DATABASE: "bluebell"
12 | MYSQL_PASSWORD: "admin"
13 | volumes:
14 | - ./init.sql:/data/application/init.sql
15 | redis507:
16 | image: "redis:5.0.7"
17 | ports:
18 | - "26379:6379"
19 | bluebell_app:
20 | build: .
21 | command: sh -c "./wait-for.sh mysql8019:3306 redis507:6379 -- ./bluebell_app ./conf/config.yaml"
22 | depends_on:
23 | - mysql8019
24 | - redis507
25 | ports:
26 | - "8888:8084"
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module bluebell
2 |
3 | go 1.14
4 |
5 | require (
6 | github.com/bwmarrin/snowflake v0.3.0
7 | github.com/cosmtrek/air v1.21.2 // indirect
8 | github.com/creack/pty v1.1.11 // indirect
9 | github.com/dgrijalva/jwt-go v3.2.0+incompatible
10 | github.com/fatih/color v1.9.0 // indirect
11 | github.com/fsnotify/fsnotify v1.4.9
12 | github.com/gin-gonic/gin v1.6.3
13 | github.com/go-playground/locales v0.13.0
14 | github.com/go-playground/universal-translator v0.17.0
15 | github.com/go-playground/validator/v10 v10.2.0
16 | github.com/go-redis/redis v6.15.9+incompatible
17 | github.com/go-sql-driver/mysql v1.5.0
18 | github.com/imdario/mergo v0.3.11 // indirect
19 | github.com/jmoiron/sqlx v1.2.0
20 | github.com/mattn/go-colorable v0.1.7 // indirect
21 | github.com/natefinch/lumberjack v2.0.0+incompatible
22 | github.com/onsi/ginkgo v1.14.1 // indirect
23 | github.com/onsi/gomega v1.10.2 // indirect
24 | github.com/pelletier/go-toml v1.8.1 // indirect
25 | github.com/spf13/viper v1.7.1
26 | go.uber.org/zap v1.10.0
27 | golang.org/x/sys v0.0.0-20200918174421-af09f7315aff // indirect
28 | gopkg.in/errgo.v2 v2.1.0
29 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
30 | )
31 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
8 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
9 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
10 | cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
11 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
12 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
13 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
14 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
15 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
16 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
17 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
18 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
19 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
20 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
21 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
22 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
23 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
24 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
25 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
26 | github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
27 | github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
28 | github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
29 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
30 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
31 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
32 | github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
33 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
34 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
35 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
36 | github.com/cosmtrek/air v1.21.2 h1:PwChdKs3qlSkKucKwwC04daw5eoy4SVgiEBQiHX5L9A=
37 | github.com/cosmtrek/air v1.21.2/go.mod h1:5EsgUqrBIHlW2ghNoevwPBEG1FQvF5XNulikjPte538=
38 | github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
39 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
40 | github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
41 | github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
42 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
43 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
44 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
45 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
46 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
47 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
48 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
49 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
50 | github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
51 | github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
52 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
53 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
54 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
55 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
56 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
57 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
58 | github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
59 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
60 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
61 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
62 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
63 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
64 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
65 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
66 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
67 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
68 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
69 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
70 | github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
71 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
72 | github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
73 | github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
74 | github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
75 | github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
76 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
77 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
78 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
79 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
80 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
81 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
82 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
83 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
84 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
85 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
86 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
87 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
88 | github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
89 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
90 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
91 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
92 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
93 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
94 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
95 | github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
96 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
97 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
98 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
99 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
100 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
101 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
102 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
103 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
104 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
105 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
106 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
107 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
108 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
109 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
110 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
111 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
112 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
113 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
114 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
115 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
116 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
117 | github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
118 | github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
119 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
120 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
121 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
122 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
123 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
124 | github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
125 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
126 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
127 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
128 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
129 | github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
130 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
131 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
132 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
133 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
134 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
135 | github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
136 | github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
137 | github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
138 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
139 | github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
140 | github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
141 | github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
142 | github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
143 | github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
144 | github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
145 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
146 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
147 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
148 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
149 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
150 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
151 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
152 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
153 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
154 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
155 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
156 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
157 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
158 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
159 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
160 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
161 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
162 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
163 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
164 | github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
165 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
166 | github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
167 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
168 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
169 | github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
170 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
171 | github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw=
172 | github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
173 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
174 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
175 | github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
176 | github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
177 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
178 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
179 | github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4=
180 | github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
181 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
182 | github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
183 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
184 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
185 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
186 | github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
187 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
188 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
189 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
190 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
191 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
192 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
193 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
194 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
195 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
196 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
197 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
198 | github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM=
199 | github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
200 | github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
201 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
202 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
203 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
204 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
205 | github.com/onsi/ginkgo v1.14.1 h1:jMU0WaQrP0a/YAEq8eJmJKjBoMs+pClEr1vDMlM/Do4=
206 | github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
207 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
208 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
209 | github.com/onsi/gomega v1.10.2 h1:aY/nuoWlKJud2J6U0E3NWsjlg+0GtwXxgEqthRdzlcs=
210 | github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
211 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
212 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
213 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
214 | github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4=
215 | github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
216 | github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM=
217 | github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
218 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
219 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
220 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
221 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
222 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
223 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
224 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
225 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
226 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
227 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
228 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
229 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
230 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
231 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
232 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
233 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
234 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
235 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
236 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
237 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
238 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
239 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
240 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
241 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
242 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
243 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
244 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
245 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
246 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
247 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
248 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
249 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
250 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
251 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
252 | github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk=
253 | github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
254 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
255 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
256 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
257 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
258 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
259 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
260 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
261 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
262 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
263 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
264 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
265 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
266 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
267 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
268 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
269 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
270 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
271 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
272 | go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=
273 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
274 | go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
275 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
276 | go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=
277 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
278 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
279 | golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
280 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
281 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
282 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
283 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
284 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
285 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
286 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
287 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
288 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
289 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
290 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
291 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
292 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
293 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
294 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
295 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
296 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
297 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
298 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
299 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
300 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
301 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
302 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
303 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
304 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
305 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
306 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
307 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
308 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
309 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
310 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
311 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
312 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
313 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
314 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
315 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
316 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 h1:AeiKBIuRw3UomYXSbLy0Mc2dDLfdtbT/IVn4keq83P0=
317 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
318 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
319 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
320 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
321 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
322 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
323 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
324 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
325 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
326 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
327 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
328 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
329 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
330 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
331 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
332 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
333 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
334 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
335 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
336 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
337 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
338 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
339 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
340 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
341 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
342 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9 h1:L2auWcuQIvxz9xSEqzESnV/QN/gNRXNApHi3fYwl2w0=
343 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
344 | golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
345 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
346 | golang.org/x/sys v0.0.0-20191110163157-d32e6e3b99c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
347 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
348 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
349 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
350 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
351 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
352 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4=
353 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
354 | golang.org/x/sys v0.0.0-20200918174421-af09f7315aff h1:1CPUrky56AcgSpxz/KfgzQWzfG09u5YOL8MvPYBlrL8=
355 | golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
356 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
357 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
358 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
359 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
360 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
361 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
362 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
363 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
364 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
365 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
366 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
367 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
368 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
369 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
370 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
371 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
372 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
373 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
374 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
375 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
376 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
377 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
378 | golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
379 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
380 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
381 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
382 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
383 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
384 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
385 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
386 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
387 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
388 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
389 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
390 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
391 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
392 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
393 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
394 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
395 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
396 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
397 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
398 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
399 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
400 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
401 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
402 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
403 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
404 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
405 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
406 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
407 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
408 | google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
409 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
410 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
411 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
412 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
413 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
414 | gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=
415 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
416 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
417 | gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
418 | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
419 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
420 | gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
421 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
422 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
423 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
424 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
425 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
426 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
427 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
428 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
429 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
430 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
431 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
432 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
433 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
434 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
435 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
436 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
437 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
438 |
--------------------------------------------------------------------------------
/logger/logger.go:
--------------------------------------------------------------------------------
1 | package logger
2 |
3 | import (
4 | "bluebell/setting"
5 | "net"
6 | "net/http"
7 | "net/http/httputil"
8 | "os"
9 | "runtime/debug"
10 | "strings"
11 | "time"
12 |
13 | "github.com/gin-gonic/gin"
14 | "github.com/natefinch/lumberjack"
15 | "go.uber.org/zap"
16 | "go.uber.org/zap/zapcore"
17 | )
18 |
19 | var lg *zap.Logger
20 |
21 | // Init 初始化lg
22 | //mode用来判断日志输出位置
23 | func Init(cfg *setting.LogConfig, mode string) (err error) {
24 | writeSyncer := getLogWriter(cfg.Filename, cfg.MaxSize, cfg.MaxBackups, cfg.MaxAge)
25 | encoder := getEncoder()
26 | var l = new(zapcore.Level)
27 | err = l.UnmarshalText([]byte(cfg.Level))
28 | if err != nil {
29 | return
30 | }
31 | var core zapcore.Core
32 | if mode == "dev" {
33 | // 进入开发模式,日志输出到终端
34 | consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
35 | //根据日志性质不同而输出不同位置
36 | core = zapcore.NewTee(
37 | //往文件输出
38 | zapcore.NewCore(encoder, writeSyncer, l),
39 | //往终端输出
40 | zapcore.NewCore(consoleEncoder, zapcore.Lock(os.Stdout), zapcore.DebugLevel),
41 | )
42 | } else {
43 | core = zapcore.NewCore(encoder, writeSyncer, l)
44 | }
45 |
46 | lg = zap.New(core, zap.AddCaller())
47 |
48 | zap.ReplaceGlobals(lg)
49 | zap.L().Info("init logger success")
50 | return
51 | }
52 |
53 | func getEncoder() zapcore.Encoder {
54 | encoderConfig := zap.NewProductionEncoderConfig()
55 | encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
56 | encoderConfig.TimeKey = "time"
57 | encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
58 | encoderConfig.EncodeDuration = zapcore.SecondsDurationEncoder
59 | encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder
60 | return zapcore.NewJSONEncoder(encoderConfig)
61 | }
62 |
63 | func getLogWriter(filename string, maxSize, maxBackup, maxAge int) zapcore.WriteSyncer {
64 | lumberJackLogger := &lumberjack.Logger{
65 | Filename: filename,
66 | MaxSize: maxSize,
67 | MaxBackups: maxBackup,
68 | MaxAge: maxAge,
69 | }
70 | return zapcore.AddSync(lumberJackLogger)
71 | }
72 |
73 | // GinLogger 接收gin框架默认的日志
74 | func GinLogger() gin.HandlerFunc {
75 | return func(c *gin.Context) {
76 | start := time.Now()
77 | path := c.Request.URL.Path
78 | query := c.Request.URL.RawQuery
79 | c.Next()
80 |
81 | cost := time.Since(start)
82 | lg.Info(path,
83 | zap.Int("status", c.Writer.Status()),
84 | zap.String("method", c.Request.Method),
85 | zap.String("path", path),
86 | zap.String("query", query),
87 | zap.String("ip", c.ClientIP()),
88 | zap.String("user-agent", c.Request.UserAgent()),
89 | zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),
90 | zap.Duration("cost", cost),
91 | )
92 | }
93 | }
94 |
95 | // GinRecovery recover掉项目可能出现的panic,并使用zap记录相关日志
96 | func GinRecovery(stack bool) gin.HandlerFunc {
97 | return func(c *gin.Context) {
98 | defer func() {
99 | if err := recover(); err != nil {
100 | // Check for a broken connection, as it is not really a
101 | // condition that warrants a panic stack trace.
102 | var brokenPipe bool
103 | if ne, ok := err.(*net.OpError); ok {
104 | if se, ok := ne.Err.(*os.SyscallError); ok {
105 | if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
106 | brokenPipe = true
107 | }
108 | }
109 | }
110 |
111 | httpRequest, _ := httputil.DumpRequest(c.Request, false)
112 | if brokenPipe {
113 | lg.Error(c.Request.URL.Path,
114 | zap.Any("error", err),
115 | zap.String("request", string(httpRequest)),
116 | )
117 | // If the connection is dead, we can't write a status to it.
118 | c.Error(err.(error)) // nolint: errcheck
119 | c.Abort()
120 | return
121 | }
122 |
123 | if stack {
124 | lg.Error("[Recovery from panic]",
125 | zap.Any("error", err),
126 | zap.String("request", string(httpRequest)),
127 | zap.String("stack", string(debug.Stack())),
128 | )
129 | } else {
130 | lg.Error("[Recovery from panic]",
131 | zap.Any("error", err),
132 | zap.String("request", string(httpRequest)),
133 | )
134 | }
135 | c.AbortWithStatus(http.StatusInternalServerError)
136 | }
137 | }()
138 | c.Next()
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/logic/community.go:
--------------------------------------------------------------------------------
1 | package logic
2 |
3 | import (
4 | "bluebell/dao/mysql"
5 | "bluebell/models"
6 | )
7 |
8 | /*
9 | * @author liqiqiorz
10 | * @data 2020/9/19 21:30
11 | */
12 | //这里就是查数据啦
13 | func GetCommunityList() ([]*models.Community, error) {
14 | // 查所有数据并返回
15 | return mysql.GetCommunityList()
16 |
17 | }
18 | func GetCommunityDetail(id int64) (*models.CommunityDetail, error) {
19 | return mysql.GetCommunityDetailByID(id)
20 | }
21 |
--------------------------------------------------------------------------------
/logic/post.go:
--------------------------------------------------------------------------------
1 | package logic
2 |
3 | import (
4 | "bluebell/dao/mysql"
5 | "bluebell/models"
6 | "bluebell/pkg/snowflake"
7 | "go.uber.org/zap"
8 | )
9 |
10 | /*
11 | * @author liqiqiorz
12 | * @data 2020/9/20 0:35
13 | */
14 | func CreatePost(p *models.Post) (err error) {
15 |
16 | //生成postID
17 | p.ID = int64(snowflake.GenID())
18 |
19 | //保存到数据库
20 | return mysql.CreatePost(p)
21 |
22 | }
23 |
24 | func GetPostById(pid int64) (data *models.ApiPostDetail, err error) {
25 |
26 | //查询数据并组合我们的接口相拥的数据
27 | post, err := mysql.GetPostById(pid)
28 | if err != nil {
29 | zap.L().Error("mysql.GetPostById(pid) failed", zap.Int64("pid", pid), zap.Error(err))
30 | return
31 | }
32 |
33 | user, err := mysql.GetUserById(post.AuthorID)
34 | if err != nil {
35 | zap.L().Error("mysql.GetUserById(post.AuthorID) failed", zap.Int64("author_id", post.AuthorID),
36 | zap.Error(err))
37 | return
38 | }
39 | //根据社区ID查询社区详情信息
40 | community, err := mysql.GetCommunityDetailByID(post.CommunityID)
41 | if err != nil {
42 | zap.L().Error("mysql.GetCommunityDetailByID(post.AuthorID) failed", zap.Int64("community_id", post.CommunityID),
43 | zap.Error(err))
44 | return
45 | }
46 | data = &models.ApiPostDetail{
47 | AuthorName: user.Username,
48 | Post: post,
49 | CommunityDetail: community,
50 | }
51 | return
52 | }
53 |
54 | // GetPostList 获取帖子列表
55 | func GetPostList(page, size int64) (data []*models.ApiPostDetail, err error) {
56 |
57 | posts, err := mysql.GetPostList(page, size)
58 | if err != nil {
59 | return nil, err
60 | }
61 | data = make([]*models.ApiPostDetail, 0, len(posts))
62 |
63 | for _, post := range posts {
64 | // 根据作者id查询作者信息
65 | user, err := mysql.GetUserById(post.AuthorID)
66 | if err != nil {
67 | zap.L().Error("mysql.GetUserById(post.AuthorID) failed",
68 | zap.Int64("author_id", post.AuthorID),
69 | zap.Error(err))
70 | continue
71 | }
72 | // 根据社区id查询社区详细信息
73 | community, err := mysql.GetCommunityDetailByID(post.CommunityID)
74 | if err != nil {
75 | zap.L().Error("mysql.GetUserById(post.AuthorID) failed",
76 | zap.Int64("community_id", post.CommunityID),
77 | zap.Error(err))
78 | continue
79 | }
80 | postDetail := &models.ApiPostDetail{
81 | AuthorName: user.Username,
82 | Post: post,
83 | CommunityDetail: community,
84 | }
85 | data = append(data, postDetail)
86 | }
87 | return
88 | }
89 |
--------------------------------------------------------------------------------
/logic/user.go:
--------------------------------------------------------------------------------
1 | package logic
2 |
3 | import (
4 | "bluebell/dao/mysql"
5 | "bluebell/models"
6 | "bluebell/pkg/jwt"
7 | "bluebell/pkg/snowflake"
8 | )
9 |
10 | /*
11 | * @author liqiqiorz
12 | * @data 2020/9/18 21:17
13 | */
14 |
15 | //存放业务逻辑的代码
16 |
17 | func SignUp(p *models.ParamSignUp) (err error) {
18 | // 判断用户存不存在
19 | if err := mysql.CheckUserExist(p.Username); err != nil {
20 | return err
21 | }
22 | //生成UID
23 | userID := snowflake.GenID()
24 | user := &models.User{
25 | UserID: userID,
26 | Username: p.Username,
27 | Password: p.Password,
28 | }
29 | // 用户密码加密
30 | //保存数据仓库
31 | return mysql.InsertUser(user)
32 | }
33 | func Login(p *models.ParamLogin) (token string, err error) {
34 |
35 | user := &models.User{
36 | Username: p.Username,
37 | Password: p.Password,
38 | }
39 | //传递的是指针,可以拿到user.userID
40 | if err := mysql.Login(user); err != nil {
41 | // 登录失败
42 | return "", err
43 | }
44 | // 生成JWT
45 | return jwt.GenToken(user.UserID, user.Username)
46 | }
47 |
--------------------------------------------------------------------------------
/logic/vote.go:
--------------------------------------------------------------------------------
1 | package logic
2 |
3 | import (
4 | "bluebell/dao/redis"
5 | "bluebell/models"
6 | "strconv"
7 |
8 | "go.uber.org/zap"
9 | )
10 |
11 | // 推荐阅读
12 | // 基于用户投票的相关算法:http://www.ruanyifeng.com/blog/algorithm/
13 |
14 | // 本项目使用简化版的投票分数
15 | // 投一票就加432分 86400/200 --> 200张赞成票可以给你的帖子续一天
16 |
17 | /* 投票的几种情况:
18 | direction=1时,有两种情况:
19 | 1. 之前没有投过票,现在投赞成票 --> 更新分数和投票记录
20 | 2. 之前投反对票,现在改投赞成票 --> 更新分数和投票记录
21 | direction=0时,有两种情况:
22 | 1. 之前投过赞成票,现在要取消投票 --> 更新分数和投票记录
23 | 2. 之前投过反对票,现在要取消投票 --> 更新分数和投票记录
24 | direction=-1时,有两种情况:
25 | 1. 之前没有投过票,现在投反对票 --> 更新分数和投票记录
26 | 2. 之前投赞成票,现在改投反对票 --> 更新分数和投票记录
27 |
28 | 投票的限制:
29 | 每个贴子自发表之日起一个星期之内允许用户投票,超过一个星期就不允许再投票了。
30 | 1. 到期之后将redis中保存的赞成票数及反对票数存储到mysql表中
31 | 2. 到期之后删除那个 KeyPostVotedZSetPF
32 | */
33 |
34 | // VoteForPost 为帖子投票的函数
35 | func VoteForPost(userID int64, p *models.ParamVoteData) error {
36 | zap.L().Debug("VoteForPost",
37 | zap.Int64("userID", userID),
38 | zap.String("postID", p.PostID),
39 | zap.Int8("direction", p.Direction))
40 | return redis.VoteForPost(strconv.Itoa(int(userID)), p.PostID, float64(p.Direction))
41 | }
42 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bluebell/controller"
5 | "bluebell/dao/mysql"
6 | "bluebell/dao/redis"
7 | "bluebell/logger"
8 | "bluebell/pkg/snowflake"
9 | "bluebell/router"
10 | "bluebell/setting"
11 | "fmt"
12 | "os"
13 | )
14 |
15 | func main() {
16 | if len(os.Args) < 2 {
17 | fmt.Println("need config file.eg: bluebell config.yaml")
18 | return
19 | }
20 | // 加载配置
21 | if err := setting.Init(os.Args[1]); err != nil {
22 | fmt.Printf("load config failed, err:%v\n", err)
23 | return
24 | }
25 | //必须要传mode,这样就行了,会输出到终端
26 | if err := logger.Init(setting.Conf.LogConfig, setting.Conf.Mode); err != nil {
27 | fmt.Printf("init logger failed, err:%v\n", err)
28 | return
29 | }
30 | if err := mysql.Init(setting.Conf.MySQLConfig); err != nil {
31 | fmt.Printf("init mysql failed, err:%v\n", err)
32 | return
33 | }
34 | defer mysql.Close() // 程序退出关闭数据库连接
35 | if err := redis.Init(setting.Conf.RedisConfig); err != nil {
36 | fmt.Printf("init redis failed, err:%v\n", err)
37 | return
38 | }
39 | defer redis.Close()
40 |
41 | if err := snowflake.Init(setting.Conf.StartTime, setting.Conf.MachineID); err != nil {
42 | fmt.Printf("init snowflake failed, err:%v\n", err)
43 | return
44 | }
45 | // 初始化gin框架内置的校验器使用的翻译器
46 | if err := controller.InitTrans("zh"); err != nil {
47 | fmt.Printf("init validator trans failed, err:%v\n", err)
48 | return
49 | }
50 | // 注册路由
51 | r := router.SetupRouter(setting.Conf.Mode)
52 | err := r.Run(fmt.Sprintf(":%d", setting.Conf.Port))
53 | if err != nil {
54 | fmt.Printf("run server failed, err:%v\n", err)
55 | return
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/middlewares/auth.go:
--------------------------------------------------------------------------------
1 | package middlewares
2 |
3 | import (
4 | "bluebell/controller"
5 | "bluebell/pkg/jwt"
6 | "strings"
7 |
8 | "github.com/gin-gonic/gin"
9 | )
10 |
11 | // JWTAuthMiddleware 基于JWT的认证中间件
12 | func JWTAuthMiddleware() func(c *gin.Context) {
13 | return func(c *gin.Context) {
14 | // 客户端携带Token有三种方式 1.放在请求头 2.放在请求体 3.放在URI
15 | // 这里假设Token放在Header的Authorization中,并使用Bearer开头
16 | // Authorization: Bearer xxxxxxx.xxx.xxx / X-TOKEN: xxx.xxx.xx
17 | // 这里的具体实现方式要依据你的实际业务情况决定
18 | authHeader := c.Request.Header.Get("Authorization")
19 | if authHeader == "" {
20 | controller.ResponseError(c, controller.CodeNeedLogin)
21 | c.Abort()
22 | return
23 | }
24 | // 按空格分割
25 | parts := strings.SplitN(authHeader, " ", 2)
26 | if !(len(parts) == 2 && parts[0] == "Bearer") {
27 | //没有登录
28 | controller.ResponseError(c, controller.CodeNeedLogin)
29 | c.Abort()
30 | return
31 | }
32 | // parts[1]是获取到的tokenString,我们使用之前定义好的解析JWT的函数来解析它
33 | mc, err := jwt.ParseToken(parts[1])
34 | if err != nil {
35 | controller.ResponseError(c, controller.CoderInvalAuth)
36 | c.Abort()
37 | return
38 | }
39 | // 将当前请求的userID信息保存到请求的上下文c上
40 | c.Set("userID", mc.UserID)
41 |
42 | c.Next() // 后续的处理请求的函数中 可以用过c.Get(CtxUserIDKey) 来获取当前请求的用户信息
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/models/community.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import "time"
4 |
5 | /*
6 | * @author liqiqiorz
7 | * @data 2020/9/19 21:33
8 | */
9 | type Community struct {
10 | ID int64 `json:"id" db:"community_id"`
11 | Name string `json:"name" db:"community_name"`
12 | }
13 | type CommunityDetail struct {
14 | ID int64 `json:"id" db:"community_id"`
15 | Name string `json:"name" db:"community_name"`
16 | //omitempty表示如果是空的话就不展示了
17 | Introduction string `json:"introduction,omitempty",db:"introduction"`
18 | //使用time.time类型的时候,数据库用的是时间戳,所以连接mysql要加上parseTime=true
19 | CreateTime time.Time `json:"create_time" db:"create_time"`
20 | }
21 |
--------------------------------------------------------------------------------
/models/create_table.sql:
--------------------------------------------------------------------------------
1 |
2 | CREATE TABLE `user` (
3 | `id` bigint(20) NOT NULL AUTO_INCREMENT,
4 | `user_id` bigint(20) NOT NULL,
5 | `username` varchar(64) COLLATE utf8mb4_general_ci NOT NULL,
6 | `password` varchar(64) COLLATE utf8mb4_general_ci NOT NULL,
7 | `email` varchar(64) COLLATE utf8mb4_general_ci,
8 | `gender` tinyint(4) NOT NULL DEFAULT '0',
9 | `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
10 | `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
11 | PRIMARY KEY (`id`),
12 | UNIQUE KEY `idx_username` (`username`) USING BTREE,
13 | UNIQUE KEY `idx_user_id` (`user_id`) USING BTREE
14 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
15 |
16 | DROP TABLE IF EXISTS `community`;
17 | CREATE TABLE `community` (
18 | `id` int(11) NOT NULL AUTO_INCREMENT,
19 | `community_id` int(10) unsigned NOT NULL,
20 | `community_name` varchar(128) COLLATE utf8mb4_general_ci NOT NULL,
21 | `introduction` varchar(256) COLLATE utf8mb4_general_ci NOT NULL,
22 | `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
23 | `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
24 | PRIMARY KEY (`id`),
25 | UNIQUE KEY `idx_community_id` (`community_id`),
26 | UNIQUE KEY `idx_community_name` (`community_name`)
27 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
28 |
29 |
30 | INSERT INTO `community` VALUES ('1', '1', 'Go', 'Golang', '2016-11-01 08:10:10', '2016-11-01 08:10:10');
31 | INSERT INTO `community` VALUES ('2', '2', 'leetcode', '刷题刷题刷题', '2020-01-01 08:00:00', '2020-01-01 08:00:00');
32 | INSERT INTO `community` VALUES ('3', '3', 'CS:GO', 'Rush B。。。', '2018-08-07 08:30:00', '2018-08-07 08:30:00');
33 | INSERT INTO `community` VALUES ('4', '4', 'LOL', '欢迎来到英雄联盟!', '2016-01-01 08:00:00', '2016-01-01 08:00:00');
34 |
35 | DROP TABLE IF EXISTS `post`;
36 | CREATE TABLE `post` (
37 | `id` bigint(20) NOT NULL AUTO_INCREMENT,
38 | `post_id` bigint(20) NOT NULL COMMENT '帖子id',
39 | `title` varchar(128) COLLATE utf8mb4_general_ci NOT NULL COMMENT '标题',
40 | `content` varchar(8192) COLLATE utf8mb4_general_ci NOT NULL COMMENT '内容',
41 | `author_id` bigint(20) NOT NULL COMMENT '作者的用户id',
42 | `community_id` bigint(20) NOT NULL COMMENT '所属社区',
43 | `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '帖子状态',
44 | `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
45 | `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
46 | PRIMARY KEY (`id`),
47 | UNIQUE KEY `idx_post_id` (`post_id`),
48 | KEY `idx_author_id` (`author_id`),
49 | KEY `idx_community_id` (`community_id`)
50 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
--------------------------------------------------------------------------------
/models/params.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | // 定义请求的参数结构体
4 | const (
5 | OrderTime = "time"
6 | OrderScore = "score"
7 | )
8 |
9 | // ParamSignUp 注册请求参数
10 | type ParamSignUp struct {
11 | //这里必须要指定tag哦,否则会对应不上的...
12 | //binding,防止别的包,require就是需要这个,必须要有值,否则会报错
13 | Username string `json:"username" binding:"required"`
14 | Password string `json:"password" binding:"required"`
15 | RePassword string `json:"re_password" binding:"required,eqfield=Password"`
16 | }
17 |
18 | // ParamLogin 登录请求参数
19 | type ParamLogin struct {
20 | Username string `json:"username" binding:"required"`
21 | Password string `json:"password" binding:"required"`
22 | }
23 |
24 | // ParamVoteData 投票数据
25 | type ParamVoteData struct {
26 | // UserID 从请求中获取当前的用户
27 | PostID string `json:"post_id" binding:"required"` // 贴子id
28 | Direction int8 `json:"direction,string" binding:"oneof=1 0 -1" ` // 赞成票(1)还是反对票(-1)取消投票(0)
29 | }
30 |
31 | // ParamPostList 获取帖子列表query string参数
32 | type ParamPostList struct {
33 | CommunityID int64 `json:"community_id" form:"community_id"` // 可以为空
34 | Page int64 `json:"page" form:"page" example:"1"` // 页码
35 | Size int64 `json:"size" form:"size" example:"10"` // 每页数据量
36 | Order string `json:"order" form:"order" example:"score"` // 排序依据
37 | }
38 |
--------------------------------------------------------------------------------
/models/post.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import "time"
4 |
5 | /*
6 | * @author liqiqiorz
7 | * @data 2020/9/20 0:15
8 | */
9 | //内存对齐概念 尽量和数据库在同一位置
10 | //数据库对应字段创建到这里main
11 | type Post struct {
12 | ID int64 `json:"id" db:"post_id"`
13 | AuthorID int64 `json:"author_id" db:"author_id""`
14 | CommunityID int64 `json:"community_id" db:"community_id" binding:"required"`
15 | Status int32 `json:"status" db:"status"`
16 | Title string `json:"title" db:"title" binding:"required"`
17 | Content string `json:"content" db:"content" binding:"required"`
18 | CreateTime time.Time `json:"create_time" db:"create_time"`
19 | }
20 |
21 | //帖子详情内容的结构体
22 | type ApiPostDetail struct {
23 | AuthorName string `json:"author_name"`
24 | *Post // 嵌入帖子结构体
25 | *CommunityDetail `json:"community"` // 嵌入社区信息
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/models/user.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | //与数据库的user对应,但是简化了一下
4 | type User struct {
5 | UserID int64 `db:"user_id"`
6 | Username string `db:"username"`
7 | Password string `db:"password"`
8 | }
9 |
--------------------------------------------------------------------------------
/ossdemo/ossCreateBucket.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "github.com/aliyun/aliyun-oss-go-sdk/oss"
6 | "os"
7 | )
8 |
9 | /*
10 | * @author liqiqiorz
11 | * @data 2020/11/6 20:08
12 | */
13 |
14 | const (
15 | ENDPOINT="http://oss-cn-beijing.aliyuncs.com"
16 | ACCESSKEYID="LTAI4FyG9N8ejRFEzGKmWR5V"
17 | ACCESSKEYSECRENT="kmmlYGgVyVtduS5mpvkmxWYKdAeDHa"
18 | OSSBUCKETNAME="edu-1014"
19 |
20 | )
21 | func handleError(err error){
22 | fmt.Println("Error",err)
23 | os.Exit(-1)
24 | }
25 | //以下程序可以创建一个bucket示例
26 | func main() {
27 | // Endpoint以杭州为例,其它Region请按实际情况填写。
28 | endpoint := ENDPOINT
29 | // 阿里云主账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM账号进行API访问或日常运维,请登录 https://ram.console.aliyun.com 创建RAM账号。
30 | accessKeyId := ACCESSKEYID
31 | accessKeySecret := ACCESSKEYSECRENT
32 |
33 | bucketName := "oss-go14"
34 | // 创建OSSClient实例。
35 | client, err := oss.New(endpoint, accessKeyId, accessKeySecret)
36 | if err != nil {
37 | handleError(err)
38 | }
39 | // 创建存储空间。
40 | err = client.CreateBucket(bucketName)
41 | if err != nil {
42 | handleError(err)
43 | }
44 | }
--------------------------------------------------------------------------------
/ossdemo/ossGolang.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "github.com/aliyun/aliyun-oss-go-sdk/oss"
6 | "github.com/spf13/viper"
7 | )
8 |
9 | /*
10 | * @author liqiqiorz
11 | * @data 2020/11/6 20:01
12 | */
13 |
14 | func main(){
15 | getString := viper.GetInt("redis.port")
16 |
17 | fmt.Println("OSS end point is :")
18 | fmt.Println("OSS end point is :",getString)
19 |
20 | fmt.Println("OSS GO SDK version: ",oss.Version)
21 |
22 | }
--------------------------------------------------------------------------------
/ossdemo/ossReadFiles.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "github.com/aliyun/aliyun-oss-go-sdk/oss"
6 | "os"
7 | )
8 |
9 | /*
10 | * @author liqiqiorz
11 | * @data 2020/11/6 20:57
12 | */
13 |
14 | const (
15 | ENDPOINTSS="http://oss-cn-beijing.aliyuncs.com"
16 | ACCESSKEYIDSS="LTAI4FyG9N8ejRFEzGKmWR5V"
17 | ACCESSKEYSECRENTSS="kmmlYGgVyVtduS5mpvkmxWYKdAeDHa"
18 | OSSBUCKETNAMESS="edu-1014"
19 |
20 | )
21 | func HandleError(err error) {
22 | fmt.Println("Error:", err)
23 | os.Exit(-1)
24 | }
25 | func main() {
26 | // 创建OSSClient实例。
27 | client, err := oss.New(ENDPOINTSS, ACCESSKEYIDSS, ACCESSKEYSECRENTSS)
28 | if err != nil {
29 | HandleError(err)
30 | }
31 | // 获取存储空间。
32 | bucketName := "oss-go14"
33 | bucket, err := client.Bucket(bucketName)
34 | if err != nil {
35 | HandleError(err)
36 | }
37 | // 列举文件。
38 | marker := ""
39 | for {
40 | lsRes, err := bucket.ListObjects(oss.Marker(marker))
41 | if err != nil {
42 | HandleError(err)
43 | }
44 | // 打印列举文件,默认情况下一次返回100条记录。
45 | for _, object := range lsRes.Objects {
46 | // 这个输出的并不是图片的url
47 | //Bucket: baijianruoliorz/github/haha.jpg
48 |
49 | fmt.Println("Bucket: ", object.Key)
50 | }
51 | if lsRes.IsTruncated {
52 | marker = lsRes.NextMarker
53 | } else {
54 | break
55 | }
56 | }
57 | }
--------------------------------------------------------------------------------
/ossdemo/ossUploadFile.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "github.com/aliyun/aliyun-oss-go-sdk/oss"
6 | "os"
7 | )
8 |
9 | //因为多个main 所以必须重新定义这些东西和函数,但是保持原名会报错
10 | const (
11 | ENDPOINTS="http://oss-cn-beijing.aliyuncs.com"
12 | ACCESSKEYIDS="LTAI4FyG9N8ejRFEzGKmWR5V"
13 | ACCESSKEYSECRENTS="kmmlYGgVyVtduS5mpvkmxWYKdAeDHa"
14 | OSSBUCKETNAMES="edu-1014"
15 |
16 | )
17 | /*
18 | * @author liqiqiorz
19 | * @data 2020/11/6 20:49
20 | */
21 | func handleError1(err error){
22 | fmt.Println("Error",err)
23 | os.Exit(-1)
24 | }
25 | func main() {
26 | // Endpoint以杭州为例,其它Region请按实际情况填写。
27 | endpoint := ENDPOINTS
28 | // 阿里云主账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM账号进行API访问或日常运维,请登录 https://ram.console.aliyun.com 创建RAM账号。
29 | accessKeyId := ACCESSKEYIDS
30 | accessKeySecret := ACCESSKEYSECRENTS
31 | bucketName := "oss-go14"
32 | // 上传文件到OSS时需要指定包含文件后缀在内的完整路径,例如abc/efg/123.jpg。
33 |
34 | //这里是上传的jpg的名称 会自动创建问价加
35 | objectName := "baijianruoliorz/github/haha.jpg"
36 | // 由本地文件路径加文件名包括后缀组成,例如/users/local/myfile.txt。
37 | //实体名称
38 | localFileName := "D:\\JPG\\Ekc7xAgU0AAWzKz.jpg"
39 | // 创建OSSClient实例。
40 | client, err := oss.New(endpoint, accessKeyId, accessKeySecret)
41 | if err != nil {
42 | handleError1(err)
43 | }
44 | // 获取存储空间。
45 | bucket, err := client.Bucket(bucketName)
46 | if err != nil {
47 | handleError1(err)
48 | }
49 | // 上传文件。
50 | err = bucket.PutObjectFromFile(objectName, localFileName)
51 | if err != nil {
52 | handleError1(err)
53 | }
54 | }
--------------------------------------------------------------------------------
/pkg/jwt/jwt.go:
--------------------------------------------------------------------------------
1 | package jwt
2 |
3 | import (
4 | "errors"
5 | "github.com/spf13/viper"
6 | "time"
7 |
8 | "github.com/dgrijalva/jwt-go"
9 | )
10 |
11 | var mySecret = []byte("我最爱的小茜酱!")
12 |
13 | // MyClaims 自定义声明结构体并内嵌jwt.StandardClaims
14 | // jwt包自带的jwt.StandardClaims只包含了官方字段
15 | // 我们这里需要额外记录一个username字段,所以要自定义结构体
16 | // 如果想要保存更多信息,都可以添加到这个结构体中
17 | type MyClaims struct {
18 | UserID int64 `json:"user_id"`
19 | Username string `json:"username"`
20 | jwt.StandardClaims
21 | }
22 |
23 | // GenToken 生成JWT
24 | func GenToken(userID int64, username string) (string, error) {
25 | // 创建一个我们自己的声明的数据
26 | c := MyClaims{
27 | userID,
28 | "username", // 自定义字段
29 | jwt.StandardClaims{
30 | ExpiresAt: time.Now().Add( //这里注意INT不能直接与time.hour相乘,所以用time.duration转换一下
31 | time.Duration(viper.GetInt("auth.jet_expire")) * time.Hour).Unix(), // 过期时间
32 | Issuer: "bluebell", // 签发人
33 | },
34 | }
35 | // 使用指定的签名方法创建签名对象
36 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
37 | // 使用指定的secret签名并获得完整的编码后的字符串token
38 | return token.SignedString(mySecret)
39 | }
40 |
41 | // ParseToken 解析JWT
42 | func ParseToken(tokenString string) (*MyClaims, error) {
43 | // 解析token
44 | var mc = new(MyClaims)
45 | //解析token
46 | token, err := jwt.ParseWithClaims(tokenString, mc, func(token *jwt.Token) (i interface{}, err error) {
47 | return mySecret, nil
48 | })
49 | if err != nil {
50 | return nil, err
51 | }
52 | if token.Valid { // 校验token
53 | return mc, nil
54 | }
55 | return nil, errors.New("invalid token")
56 | }
57 |
--------------------------------------------------------------------------------
/pkg/snowflake/snowflake.go:
--------------------------------------------------------------------------------
1 | package snowflake
2 |
3 | import (
4 | "time"
5 |
6 | sf "github.com/bwmarrin/snowflake"
7 | )
8 |
9 | //调用直接生成ID值
10 | var node *sf.Node
11 |
12 | //传过来上线时间 ,整个分布系统有关表示
13 | func Init(startTime string, machineID int64) (err error) {
14 | var st time.Time
15 | st, err = time.Parse("2006-01-02", startTime)
16 | if err != nil {
17 | return
18 | }
19 | sf.Epoch = st.UnixNano() / 1000000
20 | node, err = sf.NewNode(machineID)
21 | return
22 | }
23 | func GenID() int64 {
24 | return node.Generate().Int64()
25 | }
26 |
--------------------------------------------------------------------------------
/router/route.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "bluebell/controller"
5 | "bluebell/logger"
6 | "bluebell/middlewares"
7 | "net/http"
8 |
9 | "github.com/gin-gonic/gin"
10 | )
11 |
12 | func SetupRouter(mode string) *gin.Engine {
13 | if mode == gin.ReleaseMode {
14 | gin.SetMode(gin.ReleaseMode) // gin设置成发布模式,就不会输出控制台内容
15 | }
16 | r := gin.New()
17 | r.Use(logger.GinLogger(), logger.GinRecovery(true))
18 | v1 := r.Group("api/vi")
19 | // 注册
20 | v1.POST("/signup", controller.SignUpHandler)
21 | // 登录
22 | v1.POST("/login", controller.LoginHandler)
23 | //需要token的位置加一个中间件就行了
24 |
25 | //v1使用中间件,使用中间件的方法的集合...
26 | v1.Use(middlewares.JWTAuthMiddleware())
27 | {
28 | v1.GET("/community", controller.CommunityHandler)
29 | //路径参数
30 | v1.GET("/community/:id", controller.CommunityDetailHandler)
31 | v1.POST("/post", controller.CreatePostHandler)
32 | v1.GET("/post/:id", controller.GetPostDetailHandler)
33 | v1.GET("/posts/", controller.GetPostListHandler)
34 | }
35 |
36 | //v1.GET("/ping", middlewares.JWTAuthMiddleware(), func(c *gin.Context) {
37 | // // 如果是登录的用户,判断请求头中是否有 有效的JWT ?
38 | // //Authorization: Bearer xxxxxx.xxx.xxx
39 | //
40 | // c.Request.Header.Get("Authorization")
41 | // c.JSON(http.StatusOK, gin.H{
42 | // "msg": "ok",
43 | // })
44 | //})
45 |
46 | r.NoRoute(func(c *gin.Context) {
47 | c.JSON(http.StatusOK, gin.H{
48 | "msg": "404",
49 | })
50 | })
51 | return r
52 | }
53 |
--------------------------------------------------------------------------------
/setting/setting.go:
--------------------------------------------------------------------------------
1 | package setting
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/fsnotify/fsnotify"
7 | "github.com/spf13/viper"
8 | )
9 |
10 | var Conf = new(AppConfig)
11 |
12 | type AppConfig struct {
13 | Name string `mapstructure:"name"`
14 | Mode string `mapstructure:"mode"`
15 | Version string `mapstructure:"version"`
16 | StartTime string `mapstructure:"start_time"`
17 | MachineID int64 `mapstructure:"machine_id"`
18 | Port int `mapstructure:"port"`
19 |
20 | *LogConfig `mapstructure:"log"`
21 | *MySQLConfig `mapstructure:"mysql"`
22 | *RedisConfig `mapstructure:"redis"`
23 | }
24 |
25 | type MySQLConfig struct {
26 | Host string `mapstructure:"host"`
27 | User string `mapstructure:"user"`
28 | Password string `mapstructure:"password"`
29 | DB string `mapstructure:"dbname"`
30 | Port int `mapstructure:"port"`
31 | MaxOpenConns int `mapstructure:"max_open_conns"`
32 | MaxIdleConns int `mapstructure:"max_idle_conns"`
33 | }
34 |
35 | type RedisConfig struct {
36 | Host string `mapstructure:"host"`
37 | Password string `mapstructure:"password"`
38 | Port int `mapstructure:"port"`
39 | DB int `mapstructure:"db"`
40 | PoolSize int `mapstructure:"pool_size"`
41 | MinIdleConns int `mapstructure:"min_idle_conns"`
42 | }
43 |
44 | type LogConfig struct {
45 | Level string `mapstructure:"level"`
46 | Filename string `mapstructure:"filename"`
47 | MaxSize int `mapstructure:"max_size"`
48 | MaxAge int `mapstructure:"max_age"`
49 | MaxBackups int `mapstructure:"max_backups"`
50 | }
51 |
52 | func Init(filePath string) (err error) {
53 | // 方式1:直接指定配置文件路径(相对路径或者绝对路径)
54 | // 相对路径:相对执行的可执行文件的相对路径
55 | //viper.SetConfigFile("./conf/config.yaml")
56 | // 绝对路径:系统中实际的文件路径
57 | //viper.SetConfigFile("/Users/liwenzhou/Desktop/bluebell/conf/config.yaml")
58 |
59 | // 方式2:指定配置文件名和配置文件的位置,viper自行查找可用的配置文件
60 | // 配置文件名不需要带后缀
61 | // 配置文件位置可配置多个
62 | //viper.SetConfigName("config") // 指定配置文件名(不带后缀)
63 | //viper.AddConfigPath(".") // 指定查找配置文件的路径(这里使用相对路径)
64 | //viper.AddConfigPath("./conf") // 指定查找配置文件的路径(这里使用相对路径)
65 |
66 | // 基本上是配合远程配置中心使用的,告诉viper当前的数据使用什么格式去解析
67 | //viper.SetConfigType("json")
68 |
69 | viper.SetConfigFile(filePath)
70 |
71 | err = viper.ReadInConfig() // 读取配置信息
72 | if err != nil {
73 | // 读取配置信息失败
74 | fmt.Printf("viper.ReadInConfig failed, err:%v\n", err)
75 | return
76 | }
77 |
78 | // 把读取到的配置信息反序列化到 Conf 变量中
79 | if err := viper.Unmarshal(Conf); err != nil {
80 | fmt.Printf("viper.Unmarshal failed, err:%v\n", err)
81 | }
82 |
83 | viper.WatchConfig()
84 | viper.OnConfigChange(func(in fsnotify.Event) {
85 | fmt.Println("配置文件修改了...")
86 | if err := viper.Unmarshal(Conf); err != nil {
87 | fmt.Printf("viper.Unmarshal failed, err:%v\n", err)
88 | }
89 | })
90 | return
91 | }
92 |
--------------------------------------------------------------------------------
/tmp/main.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Programming-With-Love/bluebell/5cf4d11f91b6ad07c84605ad43348413fe27f9aa/tmp/main.exe
--------------------------------------------------------------------------------
/web_app:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Programming-With-Love/bluebell/5cf4d11f91b6ad07c84605ad43348413fe27f9aa/web_app
--------------------------------------------------------------------------------
/web_app.log:
--------------------------------------------------------------------------------
1 | {"level":"DEBUG","time":"2020-06-21T23:10:31.392+0800","caller":"web_app/main.go:37","msg":"logger init success..."}
2 | {"level":"INFO","time":"2020-06-21T23:10:34.656+0800","caller":"web_app/main.go:73","msg":"Shutdown Server ..."}
3 | {"level":"INFO","time":"2020-06-21T23:10:34.656+0800","caller":"web_app/main.go:82","msg":"Server exiting"}
4 |
--------------------------------------------------------------------------------