├── .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 | 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 | ![image](https://edu-1014.oss-cn-beijing.aliyuncs.com/Y3%7B4_F%7BH%5B%60%7B%29%25%40%40Q%7B%25T%402%5DC.png) 30 | 31 | ### 本地运行页面: 32 | 33 | ![image](https://edu-1014.oss-cn-beijing.aliyuncs.com/qwq.jpg) 34 | 35 | ### 登录: 36 | 37 | ![image](https://edu-1014.oss-cn-beijing.aliyuncs.com/TIM%E5%9B%BE%E7%89%8720200920184704.png) 38 | 39 | 40 | ### 注册: 41 | 42 | ![image](https://edu-1014.oss-cn-beijing.aliyuncs.com/TIM%E5%9B%BE%E7%89%8720200920184700.png) 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 | ![image](https://edu-1014.oss-cn-beijing.aliyuncs.com/TIM%E5%9B%BE%E7%89%8720200920185309.jpg) 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 | --------------------------------------------------------------------------------