├── .gitignore ├── LICENSE ├── README.md ├── cmd ├── Makefile └── main.go ├── config.yaml ├── docs ├── docs.go ├── swagger.json └── swagger.yaml ├── go.mod ├── go.sum ├── internal ├── app │ ├── build.go │ ├── community │ │ ├── build.go │ │ ├── controller │ │ │ ├── community.go │ │ │ └── controller.go │ │ ├── model │ │ │ ├── dto.go │ │ │ └── model.go │ │ ├── repository │ │ │ ├── community.go │ │ │ └── repo.go │ │ └── service │ │ │ ├── community.go │ │ │ └── service.go │ ├── post │ │ ├── build.go │ │ ├── controller │ │ │ ├── controller.go │ │ │ └── post.go │ │ ├── model │ │ │ ├── dto.go │ │ │ ├── entity.go │ │ │ └── http.go │ │ ├── repository │ │ │ ├── post.go │ │ │ ├── redis.go │ │ │ └── repo.go │ │ └── service │ │ │ ├── post.go │ │ │ └── service.go │ └── user │ │ ├── build.go │ │ ├── controller │ │ ├── controller.go │ │ └── user.go │ │ ├── model │ │ ├── dto.go │ │ ├── entity.go │ │ └── http.go │ │ ├── repository │ │ ├── repo.go │ │ └── user.go │ │ └── service │ │ ├── service.go │ │ └── user.go └── pkg │ ├── constant │ ├── e │ │ ├── code.go │ │ ├── errors.go │ │ └── msg.go │ └── key.go │ ├── ginx │ ├── get_current_user.go │ ├── parse.go │ ├── response.go │ └── validator.go │ ├── middleware │ ├── cors.go │ ├── jwt.go │ └── log_recovery.go │ └── util │ ├── page.go │ └── time.go ├── pkg ├── cache │ └── redis.go ├── conf │ ├── conf.go │ └── init.go ├── database │ └── gorm.go ├── logger │ └── zap.go ├── servers │ └── http.go └── utils │ ├── hash │ └── hash.go │ ├── jwt │ └── jwt.go │ └── snowflake │ └── snowflake.go └── script └── my_app.sql /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | *.logXS-bbs 8 | 9 | 10 | # Test binary, built with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Dependency directories (remove the comment below to include it) 17 | # vendor/ 18 | 19 | 20 | # other 21 | 22 | .idea 23 | .vscode 24 | logs 25 | vendor 26 | tmp -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 hblock 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XS-bbs 2 | Xiang Shou 论坛,XS-bbs 基于gin + gorm + go-redis 实战,面向接口开发。 3 | 4 | 项目早期使用过 wire 来解决依赖注入,主要是学习完 wire 后进行实践。 5 | 6 | 后来个人觉得该项目很小,引入 wire 反而增加了初学者理解该项目的难度,就全部改为 Go 原生实现依赖注入了~ 7 | 8 | 另外个人在该项目中不仅使用了gin, 还使用了 GoFrame, 一个开箱即用的框架,不仅仅是web开发能用, 主要是学习完后使用了GoFame的一些模块。 9 | 10 | 这里是个人使用GoFame validate模块时写的文章:https://goframe.org/pages/viewpage.action?pageId=3673259 11 | 12 | 13 | 14 | ## 目录结构 15 | ```sh 16 | ├── cmd # 程序入口 17 | │ ├── main.go 18 | │ ├── wire_gen.go # 已删除 19 | │ └── wire.go # 已删除 20 | ├── docs # swagger接口文档 21 | │ ├── docs.go 22 | │ ├── swagger.json 23 | │ └── swagger.yaml 24 | ├── internal # 私有模块,业务代码和业务严重依赖的库 25 | │ ├── app # app 项目,按功能模块划分,方便后续扩展微服务 26 | │ └── pkg # 业务严重依赖的公共库 27 | ├── pkg # 公共模块,和业务无关,可以对外使用的库 28 | │ ├── cache # 缓存初始化封装 29 | │ ├── conf # 配置定义及初始化封装 30 | │ ├── database # 数据库初始化封装 31 | │ ├── logger # 日志库初始化封装 32 | │ ├── servers # http 路由初识化、注册相关,后续可以支持 grpc server 33 | │ └── utils # 一些工具封装 34 | ├── config.yaml # 配置文件 35 | ├── go.mod 36 | ├── go.sum 37 | ├── LICENSE 38 | ├── README.md 39 | └── script # 脚本文件 40 | └── my_app.sql 41 | ``` 42 | 43 | ## 技术选型 44 | 45 | | 技术方向 | 框架名称 | 46 | | -------------- | --------- | 47 | | 请求、路由处理 | gin | 48 | | 参数校验 | gf/gvalid | 49 | | 数据库(mysql) | gorm | 50 | | 缓存 | go-redis | 51 | | 配置读写 | viper | 52 | | 日志 | zap | 53 | | API文档 | swagger | 54 | | 依赖注入 | wire | 55 | 56 | 面向接口开发,`repo` 层和 `service` 层都封装了业务接口。 57 | 58 | > `wire.go`定义了项目初始化的过程,通过`wire`工具生成具体的初始化,在`wire_gen.go`中。 已去掉 `wire` 的使用 59 | 60 | ## 项目运行 61 | 62 | ```sh 63 | cd cmd/ 64 | 65 | go build 66 | 67 | ./cmd 68 | ``` 69 | -------------------------------------------------------------------------------- /cmd/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all build run gotool clean help 2 | 3 | BINARY="XS-bbs" 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 ../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 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | "os/signal" 10 | "syscall" 11 | "time" 12 | 13 | "github.com/gin-gonic/gin" 14 | 15 | "go.uber.org/zap" 16 | 17 | "xs.bbs/internal/app" 18 | "xs.bbs/pkg/cache" 19 | "xs.bbs/pkg/conf" 20 | "xs.bbs/pkg/database" 21 | logger "xs.bbs/pkg/logger" 22 | "xs.bbs/pkg/utils/snowflake" 23 | ) 24 | 25 | // @title 项目标题 26 | // @version 0.0.1 27 | // @description 项目描述 28 | // @termsOfService http://swagger.io/terms/ 29 | 30 | // @contact.name 这里写联系人信息 31 | // @contact.url http://www.swagger.io/support 32 | // @contact.email support@swagger.io 33 | 34 | // @license.name Apache 2.0 35 | // @license.url http://www.apache.org/licenses/LICENSE-2.0.html 36 | 37 | // @host 127.0.0.1:8090 38 | // @BasePath /api/ 39 | func main() { 40 | // 1. init config 41 | config, err := conf.Init() 42 | if err != nil { 43 | log.Fatalf("conf.Build failed, err: %+v", err) 44 | } 45 | 46 | // 2. init logger 47 | if err = logger.Init(config); err != nil { 48 | log.Fatalf("log.Build failed, err: %+v", err) 49 | } 50 | 51 | // 3. init snowflake 52 | if err = snowflake.Init(config); err != nil { 53 | zap.L().Error("snowflake.Build failed", zap.Error(err)) 54 | return 55 | } 56 | 57 | // 4. init gorm client 58 | db, err := database.Init(config) 59 | if err != nil { 60 | zap.L().Error("database.Build failed", zap.Error(err)) 61 | return 62 | } 63 | 64 | // 5. init gorm client 65 | rbd, err := cache.Init(config) 66 | if err != nil { 67 | zap.L().Error("cache.Build failed", zap.Error(err)) 68 | return 69 | } 70 | 71 | if config.Mode == gin.ReleaseMode { 72 | gin.SetMode(gin.ReleaseMode) // gin设置成发布模式 73 | } 74 | 75 | // 6. init app business 76 | router := app.Build(db, rbd) 77 | gracefulShutDown(router, config.System.Port) 78 | 79 | } 80 | 81 | func gracefulShutDown(router *gin.Engine, port int) { 82 | 83 | srv := &http.Server{ 84 | Addr: fmt.Sprintf(":%d", port), 85 | Handler: router, 86 | } 87 | 88 | // 7. start http server and gracefully shutdown 89 | go func() { 90 | // service connections 91 | if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { 92 | log.Fatalf("listen: %+v\n", err) 93 | } 94 | }() 95 | 96 | // Wait for interrupt signal to gracefully shutdown the server with 97 | // a timeout of 5 seconds. 98 | quit := make(chan os.Signal) 99 | // kill (no param) default send syscanll.SIGTERM 100 | // kill -2 is syscall.SIGINT 101 | // kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it 102 | signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) 103 | <-quit 104 | log.Println("Shutdown Server ...") 105 | 106 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 107 | defer cancel() 108 | if err := srv.Shutdown(ctx); err != nil { 109 | log.Fatal("Server Shutdown:", err) 110 | } 111 | // catching ctx.Done(). timeout of 5 seconds. 112 | select { 113 | case <-ctx.Done(): 114 | log.Println("timeout of 5 seconds.") 115 | } 116 | log.Println("Server exiting") 117 | } 118 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | system: 2 | app_name: "myApp" 3 | mode: "prod" 4 | port: 8090 5 | version: "v0.0.1" 6 | start_time: "2020-10-01" 7 | machine_id: 1 8 | 9 | log: 10 | prefix: "[xs-bbs]" 11 | level: "error" 12 | filename: "../logs/xs-bbs.log" 13 | max_size: 200 14 | max_age: 30 15 | max_backups: 7 16 | 17 | mysql: 18 | addr: "127.0.0.1:4000" 19 | dsn: "root:123456@tcp(127.0.0.1:3306)/bbs?timeout=5s&readTimeout=5s&writeTimeout=5s&parseTime=true&loc=Local&charset=utf8,utf8mb4" 20 | log_mode: false 21 | max_open_cons: 10 22 | max_idle_cons: 10 23 | 24 | redis: 25 | host: "127.0.0.1" 26 | port: 6379 27 | password: "" 28 | db: 0 29 | pool_size: 100 30 | min_idle_cons: 5 -------------------------------------------------------------------------------- /docs/docs.go: -------------------------------------------------------------------------------- 1 | // GENERATED BY THE COMMAND ABOVE; DO NOT EDIT 2 | // This file was generated by swaggo/swag 3 | 4 | package docs 5 | 6 | import ( 7 | "bytes" 8 | "encoding/json" 9 | "strings" 10 | 11 | "github.com/alecthomas/template" 12 | "github.com/swaggo/swag" 13 | ) 14 | 15 | var doc = `{ 16 | "schemes": {{ marshal .Schemes }}, 17 | "swagger": "2.0", 18 | "info": { 19 | "description": "{{.Description}}", 20 | "title": "{{.Title}}", 21 | "termsOfService": "http://swagger.io/terms/", 22 | "contact": { 23 | "name": "这里写联系人信息", 24 | "url": "http://www.swagger.io/support", 25 | "email": "support@swagger.io" 26 | }, 27 | "license": { 28 | "name": "Apache 2.0", 29 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 30 | }, 31 | "version": "{{.Version}}" 32 | }, 33 | "host": "{{.Host}}", 34 | "basePath": "{{.BasePath}}", 35 | "paths": { 36 | "/user/Get": { 37 | "get": { 38 | "description": "根据id获取用户", 39 | "consumes": [ 40 | "application/json" 41 | ], 42 | "produces": [ 43 | "application/json" 44 | ], 45 | "tags": [ 46 | "用户接口" 47 | ], 48 | "summary": "根据id获取用户", 49 | "operationId": "/user/Get", 50 | "parameters": [ 51 | { 52 | "type": "string", 53 | "description": "id", 54 | "name": "id", 55 | "in": "query", 56 | "required": true 57 | } 58 | ], 59 | "responses": { 60 | "200": { 61 | "description": "success", 62 | "schema": { 63 | "allOf": [ 64 | { 65 | "$ref": "#/definitions/ginx.Response" 66 | }, 67 | { 68 | "type": "object", 69 | "properties": { 70 | "data": { 71 | "$ref": "#/definitions/model.UserDto" 72 | } 73 | } 74 | } 75 | ] 76 | } 77 | } 78 | } 79 | } 80 | }, 81 | "/user/delete": { 82 | "get": { 83 | "description": "根据id删除用户", 84 | "consumes": [ 85 | "application/json" 86 | ], 87 | "produces": [ 88 | "application/json" 89 | ], 90 | "tags": [ 91 | "用户接口" 92 | ], 93 | "summary": "根据id删除用户", 94 | "operationId": "/user/delete", 95 | "parameters": [ 96 | { 97 | "type": "string", 98 | "description": "id", 99 | "name": "id", 100 | "in": "query", 101 | "required": true 102 | } 103 | ], 104 | "responses": { 105 | "200": { 106 | "description": "success", 107 | "schema": { 108 | "allOf": [ 109 | { 110 | "$ref": "#/definitions/ginx.Response" 111 | }, 112 | { 113 | "type": "object", 114 | "properties": { 115 | "data": { 116 | "type": "string" 117 | } 118 | } 119 | } 120 | ] 121 | } 122 | } 123 | } 124 | } 125 | }, 126 | "/user/signup": { 127 | "post": { 128 | "description": "用户注册", 129 | "consumes": [ 130 | "application/json" 131 | ], 132 | "produces": [ 133 | "application/json" 134 | ], 135 | "tags": [ 136 | "用户接口" 137 | ], 138 | "summary": "用户注册账号", 139 | "operationId": "/user/signup", 140 | "parameters": [ 141 | { 142 | "description": "body", 143 | "name": "body", 144 | "in": "body", 145 | "required": true, 146 | "schema": { 147 | "$ref": "#/definitions/model.SignUpParam" 148 | } 149 | } 150 | ], 151 | "responses": { 152 | "200": { 153 | "description": "success", 154 | "schema": { 155 | "allOf": [ 156 | { 157 | "$ref": "#/definitions/ginx.Response" 158 | }, 159 | { 160 | "type": "object", 161 | "properties": { 162 | "data": { 163 | "$ref": "#/definitions/model.UserDto" 164 | } 165 | } 166 | } 167 | ] 168 | } 169 | } 170 | } 171 | } 172 | } 173 | }, 174 | "definitions": { 175 | "ginx.Response": { 176 | "type": "object", 177 | "properties": { 178 | "code": { 179 | "type": "integer" 180 | }, 181 | "data": { 182 | "type": "object" 183 | }, 184 | "msg": { 185 | "type": "string" 186 | } 187 | } 188 | }, 189 | "model.SignUpParam": { 190 | "type": "object", 191 | "required": [ 192 | "password", 193 | "username" 194 | ], 195 | "properties": { 196 | "nickName": { 197 | "description": "s", 198 | "type": "string" 199 | }, 200 | "password": { 201 | "description": "s", 202 | "type": "string" 203 | }, 204 | "rePassword": { 205 | "description": "s", 206 | "type": "string" 207 | }, 208 | "username": { 209 | "description": "s", 210 | "type": "string" 211 | } 212 | } 213 | }, 214 | "model.UserDto": { 215 | "type": "object", 216 | "properties": { 217 | "email": { 218 | "description": "邮箱", 219 | "type": "string" 220 | }, 221 | "nickname": { 222 | "description": "昵称", 223 | "type": "string" 224 | }, 225 | "password": { 226 | "description": "密码", 227 | "type": "string" 228 | }, 229 | "username": { 230 | "description": "用户名", 231 | "type": "string" 232 | } 233 | } 234 | } 235 | } 236 | }` 237 | 238 | type swaggerInfo struct { 239 | Version string 240 | Host string 241 | BasePath string 242 | Schemes []string 243 | Title string 244 | Description string 245 | } 246 | 247 | // SwaggerInfo holds exported Swagger Info so clients can modify it 248 | var SwaggerInfo = swaggerInfo{ 249 | Version: "0.0.1", 250 | Host: "127.0.0.1:8090", 251 | BasePath: "/api/", 252 | Schemes: []string{}, 253 | Title: "项目标题", 254 | Description: "这是一个gin web开发脚手架", 255 | } 256 | 257 | type s struct{} 258 | 259 | func (s *s) ReadDoc() string { 260 | sInfo := SwaggerInfo 261 | sInfo.Description = strings.Replace(sInfo.Description, "\n", "\\n", -1) 262 | 263 | t, err := template.New("swagger_info").Funcs(template.FuncMap{ 264 | "marshal": func(v interface{}) string { 265 | a, _ := json.Marshal(v) 266 | return string(a) 267 | }, 268 | }).Parse(doc) 269 | if err != nil { 270 | return doc 271 | } 272 | 273 | var tpl bytes.Buffer 274 | if err := t.Execute(&tpl, sInfo); err != nil { 275 | return doc 276 | } 277 | 278 | return tpl.String() 279 | } 280 | 281 | func init() { 282 | swag.Register(swag.Name, &s{}) 283 | } 284 | -------------------------------------------------------------------------------- /docs/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "description": "这是一个gin web开发脚手架", 5 | "title": "项目标题", 6 | "termsOfService": "http://swagger.io/terms/", 7 | "contact": { 8 | "name": "这里写联系人信息", 9 | "url": "http://www.swagger.io/support", 10 | "email": "support@swagger.io" 11 | }, 12 | "license": { 13 | "name": "Apache 2.0", 14 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 15 | }, 16 | "version": "0.0.1" 17 | }, 18 | "host": "127.0.0.1:8090", 19 | "basePath": "/api/", 20 | "paths": { 21 | "/user/Get": { 22 | "get": { 23 | "description": "根据id获取用户", 24 | "consumes": [ 25 | "application/json" 26 | ], 27 | "produces": [ 28 | "application/json" 29 | ], 30 | "tags": [ 31 | "用户接口" 32 | ], 33 | "summary": "根据id获取用户", 34 | "operationId": "/user/Get", 35 | "parameters": [ 36 | { 37 | "type": "string", 38 | "description": "id", 39 | "name": "id", 40 | "in": "query", 41 | "required": true 42 | } 43 | ], 44 | "responses": { 45 | "200": { 46 | "description": "success", 47 | "schema": { 48 | "allOf": [ 49 | { 50 | "$ref": "#/definitions/ginx.Response" 51 | }, 52 | { 53 | "type": "object", 54 | "properties": { 55 | "data": { 56 | "$ref": "#/definitions/model.UserDto" 57 | } 58 | } 59 | } 60 | ] 61 | } 62 | } 63 | } 64 | } 65 | }, 66 | "/user/delete": { 67 | "get": { 68 | "description": "根据id删除用户", 69 | "consumes": [ 70 | "application/json" 71 | ], 72 | "produces": [ 73 | "application/json" 74 | ], 75 | "tags": [ 76 | "用户接口" 77 | ], 78 | "summary": "根据id删除用户", 79 | "operationId": "/user/delete", 80 | "parameters": [ 81 | { 82 | "type": "string", 83 | "description": "id", 84 | "name": "id", 85 | "in": "query", 86 | "required": true 87 | } 88 | ], 89 | "responses": { 90 | "200": { 91 | "description": "success", 92 | "schema": { 93 | "allOf": [ 94 | { 95 | "$ref": "#/definitions/ginx.Response" 96 | }, 97 | { 98 | "type": "object", 99 | "properties": { 100 | "data": { 101 | "type": "string" 102 | } 103 | } 104 | } 105 | ] 106 | } 107 | } 108 | } 109 | } 110 | }, 111 | "/user/signup": { 112 | "post": { 113 | "description": "用户注册", 114 | "consumes": [ 115 | "application/json" 116 | ], 117 | "produces": [ 118 | "application/json" 119 | ], 120 | "tags": [ 121 | "用户接口" 122 | ], 123 | "summary": "用户注册账号", 124 | "operationId": "/user/signup", 125 | "parameters": [ 126 | { 127 | "description": "body", 128 | "name": "body", 129 | "in": "body", 130 | "required": true, 131 | "schema": { 132 | "$ref": "#/definitions/model.SignUpParam" 133 | } 134 | } 135 | ], 136 | "responses": { 137 | "200": { 138 | "description": "success", 139 | "schema": { 140 | "allOf": [ 141 | { 142 | "$ref": "#/definitions/ginx.Response" 143 | }, 144 | { 145 | "type": "object", 146 | "properties": { 147 | "data": { 148 | "$ref": "#/definitions/model.UserDto" 149 | } 150 | } 151 | } 152 | ] 153 | } 154 | } 155 | } 156 | } 157 | } 158 | }, 159 | "definitions": { 160 | "ginx.Response": { 161 | "type": "object", 162 | "properties": { 163 | "code": { 164 | "type": "integer" 165 | }, 166 | "data": { 167 | "type": "object" 168 | }, 169 | "msg": { 170 | "type": "string" 171 | } 172 | } 173 | }, 174 | "model.SignUpParam": { 175 | "type": "object", 176 | "required": [ 177 | "password", 178 | "username" 179 | ], 180 | "properties": { 181 | "nickName": { 182 | "description": "s", 183 | "type": "string" 184 | }, 185 | "password": { 186 | "description": "s", 187 | "type": "string" 188 | }, 189 | "rePassword": { 190 | "description": "s", 191 | "type": "string" 192 | }, 193 | "username": { 194 | "description": "s", 195 | "type": "string" 196 | } 197 | } 198 | }, 199 | "model.UserDto": { 200 | "type": "object", 201 | "properties": { 202 | "email": { 203 | "description": "邮箱", 204 | "type": "string" 205 | }, 206 | "nickname": { 207 | "description": "昵称", 208 | "type": "string" 209 | }, 210 | "password": { 211 | "description": "密码", 212 | "type": "string" 213 | }, 214 | "username": { 215 | "description": "用户名", 216 | "type": "string" 217 | } 218 | } 219 | } 220 | } 221 | } -------------------------------------------------------------------------------- /docs/swagger.yaml: -------------------------------------------------------------------------------- 1 | basePath: /api/ 2 | definitions: 3 | ginx.Response: 4 | properties: 5 | code: 6 | type: integer 7 | data: 8 | type: object 9 | msg: 10 | type: string 11 | type: object 12 | model.SignUpParam: 13 | properties: 14 | nickName: 15 | description: s 16 | type: string 17 | password: 18 | description: s 19 | type: string 20 | rePassword: 21 | description: s 22 | type: string 23 | username: 24 | description: s 25 | type: string 26 | required: 27 | - password 28 | - username 29 | type: object 30 | model.UserDto: 31 | properties: 32 | email: 33 | description: 邮箱 34 | type: string 35 | nickname: 36 | description: 昵称 37 | type: string 38 | password: 39 | description: 密码 40 | type: string 41 | username: 42 | description: 用户名 43 | type: string 44 | type: object 45 | host: 127.0.0.1:8090 46 | info: 47 | contact: 48 | email: support@swagger.io 49 | name: 这里写联系人信息 50 | url: http://www.swagger.io/support 51 | description: 这是一个gin web开发脚手架 52 | license: 53 | name: Apache 2.0 54 | url: http://www.apache.org/licenses/LICENSE-2.0.html 55 | termsOfService: http://swagger.io/terms/ 56 | title: 项目标题 57 | version: 0.0.1 58 | paths: 59 | /user/Get: 60 | get: 61 | consumes: 62 | - application/json 63 | description: 根据id获取用户 64 | operationId: /user/Get 65 | parameters: 66 | - description: id 67 | in: query 68 | name: id 69 | required: true 70 | type: string 71 | produces: 72 | - application/json 73 | responses: 74 | "200": 75 | description: success 76 | schema: 77 | allOf: 78 | - $ref: '#/definitions/ginx.Response' 79 | - properties: 80 | data: 81 | $ref: '#/definitions/model.UserDto' 82 | type: object 83 | summary: 根据id获取用户 84 | tags: 85 | - 用户接口 86 | /user/delete: 87 | get: 88 | consumes: 89 | - application/json 90 | description: 根据id删除用户 91 | operationId: /user/delete 92 | parameters: 93 | - description: id 94 | in: query 95 | name: id 96 | required: true 97 | type: string 98 | produces: 99 | - application/json 100 | responses: 101 | "200": 102 | description: success 103 | schema: 104 | allOf: 105 | - $ref: '#/definitions/ginx.Response' 106 | - properties: 107 | data: 108 | type: string 109 | type: object 110 | summary: 根据id删除用户 111 | tags: 112 | - 用户接口 113 | /user/signup: 114 | post: 115 | consumes: 116 | - application/json 117 | description: 用户注册 118 | operationId: /user/signup 119 | parameters: 120 | - description: body 121 | in: body 122 | name: body 123 | required: true 124 | schema: 125 | $ref: '#/definitions/model.SignUpParam' 126 | produces: 127 | - application/json 128 | responses: 129 | "200": 130 | description: success 131 | schema: 132 | allOf: 133 | - $ref: '#/definitions/ginx.Response' 134 | - properties: 135 | data: 136 | $ref: '#/definitions/model.UserDto' 137 | type: object 138 | summary: 用户注册账号 139 | tags: 140 | - 用户接口 141 | swagger: "2.0" 142 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module xs.bbs 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 7 | github.com/bwmarrin/snowflake v0.3.0 8 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 9 | github.com/fsnotify/fsnotify v1.4.9 10 | github.com/gin-gonic/gin v1.7.6 11 | github.com/go-sql-driver/mysql v1.5.0 12 | github.com/gogf/gf v1.14.2 13 | github.com/natefinch/lumberjack v2.0.0+incompatible 14 | github.com/pkg/errors v0.8.1 15 | github.com/spf13/pflag v1.0.5 16 | github.com/spf13/viper v1.7.1 17 | github.com/swaggo/gin-swagger v1.3.0 18 | github.com/swaggo/swag v1.6.9 19 | go.uber.org/zap v1.16.0 20 | gorm.io/driver/mysql v1.0.3 21 | gorm.io/gorm v1.20.5 22 | ) 23 | 24 | require ( 25 | github.com/go-redis/redis v6.15.9+incompatible 26 | github.com/go-redis/redis/v9 v9.0.0-beta.1 27 | ) 28 | 29 | require ( 30 | github.com/BurntSushi/toml v0.3.1 // indirect 31 | github.com/KyleBanks/depth v1.2.1 // indirect 32 | github.com/PuerkitoBio/purell v1.1.1 // indirect 33 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect 34 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 35 | github.com/clbanning/mxj v1.8.5-0.20200714211355-ff02cfb8ea28 // indirect 36 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 37 | github.com/gin-contrib/sse v0.1.0 // indirect 38 | github.com/go-openapi/jsonpointer v0.19.3 // indirect 39 | github.com/go-openapi/jsonreference v0.19.4 // indirect 40 | github.com/go-openapi/spec v0.19.9 // indirect 41 | github.com/go-openapi/swag v0.19.5 // indirect 42 | github.com/go-playground/locales v0.14.0 // indirect 43 | github.com/go-playground/universal-translator v0.18.0 // indirect 44 | github.com/go-playground/validator/v10 v10.9.0 // indirect 45 | github.com/golang/protobuf v1.5.2 // indirect 46 | github.com/gqcn/structs v1.1.1 // indirect 47 | github.com/hashicorp/hcl v1.0.0 // indirect 48 | github.com/jinzhu/inflection v1.0.0 // indirect 49 | github.com/jinzhu/now v1.1.1 // indirect 50 | github.com/json-iterator/go v1.1.12 // indirect 51 | github.com/leodido/go-urn v1.2.1 // indirect 52 | github.com/magiconair/properties v1.8.1 // indirect 53 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e // indirect 54 | github.com/mattn/go-isatty v0.0.14 // indirect 55 | github.com/mitchellh/mapstructure v1.1.2 // indirect 56 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 57 | github.com/modern-go/reflect2 v1.0.2 // indirect 58 | github.com/pelletier/go-toml v1.2.0 // indirect 59 | github.com/spf13/afero v1.1.2 // indirect 60 | github.com/spf13/cast v1.3.0 // indirect 61 | github.com/spf13/jwalterweatherman v1.0.0 // indirect 62 | github.com/subosito/gotenv v1.2.0 // indirect 63 | github.com/ugorji/go/codec v1.2.6 // indirect 64 | go.uber.org/atomic v1.6.0 // indirect 65 | go.uber.org/multierr v1.5.0 // indirect 66 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect 67 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect 68 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect 69 | golang.org/x/text v0.3.7 // indirect 70 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e // indirect 71 | google.golang.org/protobuf v1.27.1 // indirect 72 | gopkg.in/ini.v1 v1.51.0 // indirect 73 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect 74 | gopkg.in/yaml.v2 v2.4.0 // indirect 75 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 76 | ) 77 | -------------------------------------------------------------------------------- /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/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= 18 | github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= 19 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 20 | github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 21 | github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= 22 | github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 23 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= 24 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 25 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 26 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= 27 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 28 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 29 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 30 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 31 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 32 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 33 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 34 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 35 | github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= 36 | github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0= 37 | github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE= 38 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 39 | github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= 40 | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 41 | github.com/clbanning/mxj v1.8.5-0.20200714211355-ff02cfb8ea28 h1:LdXxtjzvZYhhUaonAaAKArG3pyC67kGL3YY+6hGG8G4= 42 | github.com/clbanning/mxj v1.8.5-0.20200714211355-ff02cfb8ea28/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= 43 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 44 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 45 | github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 46 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 47 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 48 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 49 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 50 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 51 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 52 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 53 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 54 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 55 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 56 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 57 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 58 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 59 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 60 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 61 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 62 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 63 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 64 | github.com/gin-contrib/gzip v0.0.1 h1:ezvKOL6jH+jlzdHNE4h9h8q8uMpDQjyl0NN0Jd7jozc= 65 | github.com/gin-contrib/gzip v0.0.1/go.mod h1:fGBJBCdt6qCZuCAOwWuFhBB4OOq9EFqlo5dEaFhhu5w= 66 | github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= 67 | github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= 68 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 69 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 70 | github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y= 71 | github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= 72 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 73 | github.com/gin-gonic/gin v1.7.6 h1:Ma2JlolDP9KCHuHTrW58EIIxVUQKxSxzuCKguCYyFas= 74 | github.com/gin-gonic/gin v1.7.6/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= 75 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 76 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 77 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 78 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 79 | github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= 80 | github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w= 81 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 82 | github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= 83 | github.com/go-openapi/jsonreference v0.19.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= 84 | github.com/go-openapi/jsonreference v0.19.4 h1:3Vw+rh13uq2JFNxgnMTGE1rnoieU9FmyE1gvnyylsYg= 85 | github.com/go-openapi/jsonreference v0.19.4/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= 86 | github.com/go-openapi/spec v0.19.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= 87 | github.com/go-openapi/spec v0.19.9 h1:9z9cbFuZJ7AcvOHKIY+f6Aevb4vObNDkTEyoMfO7rAc= 88 | github.com/go-openapi/spec v0.19.9/go.mod h1:vqK/dIdLGCosfvYsQV3WfC7N3TiZSnGY2RZKoFK7X28= 89 | github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= 90 | github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= 91 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 92 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 93 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 94 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 95 | github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= 96 | github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= 97 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 98 | github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= 99 | github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= 100 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 101 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= 102 | github.com/go-playground/validator/v10 v10.9.0 h1:NgTtmN58D0m8+UuxtYmGztBJB7VnPgjj221I1QHci2A= 103 | github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= 104 | github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= 105 | github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= 106 | github.com/go-redis/redis/v9 v9.0.0-beta.1 h1:oW3jlPic5HhGUbYMH0lidnP+72BgsT+lCwlVud6o2Mc= 107 | github.com/go-redis/redis/v9 v9.0.0-beta.1/go.mod h1:6gNX1bXdwkpEG0M/hEBNK/Fp8zdyCkjwwKc6vBbfCDI= 108 | github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= 109 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 110 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 111 | github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 112 | github.com/gogf/gf v1.14.2 h1:xIgfq2YcMYAm1ZcbVD/u0y5qZDDdARY4u7261w8QlHA= 113 | github.com/gogf/gf v1.14.2/go.mod h1:7b21qQKDyIwJO4PkBCxVci5C62tm89MANGV2wJgAf50= 114 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 115 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 116 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 117 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 118 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 119 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 120 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 121 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 122 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 123 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 124 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 125 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 126 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 127 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 128 | github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= 129 | github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= 130 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 131 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 132 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 133 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 134 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 135 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 136 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 137 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 138 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 139 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 140 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 141 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 142 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 143 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 144 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 145 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 146 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 147 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 148 | github.com/gqcn/structs v1.1.1 h1:cyzGRwfmn3d1d54fwW3KUNyG9QxR0ldIeqwFGeBt638= 149 | github.com/gqcn/structs v1.1.1/go.mod h1:/aBhTBSsKQ2Ec9pbnYdGphtdWXHFn4KrCL0fXM/Adok= 150 | github.com/grokify/html-strip-tags-go v0.0.0-20190921062105-daaa06bf1aaf h1:wIOAyJMMen0ELGiFzlmqxdcV1yGbkyHBAB6PolcNbLA= 151 | github.com/grokify/html-strip-tags-go v0.0.0-20190921062105-daaa06bf1aaf/go.mod h1:2Su6romC5/1VXOQMaWL2yb618ARB8iVo6/DR99A6d78= 152 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 153 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 154 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 155 | github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= 156 | github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= 157 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 158 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 159 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 160 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 161 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 162 | github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= 163 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 164 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 165 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 166 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 167 | github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= 168 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 169 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 170 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 171 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 172 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 173 | github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= 174 | github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= 175 | github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= 176 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 177 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 178 | github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E= 179 | github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 180 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 181 | github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 182 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 183 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 184 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 185 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 186 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 187 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 188 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 189 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 190 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 191 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 192 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 193 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 194 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 195 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 196 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 197 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 198 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 199 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 200 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 201 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 202 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 203 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 204 | github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= 205 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= 206 | github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= 207 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 208 | github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 209 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 210 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8= 211 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 212 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 213 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 214 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 215 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 216 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 217 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 218 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 219 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 220 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= 221 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 222 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 223 | github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 224 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 225 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 226 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 227 | github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= 228 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= 229 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 230 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 231 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 232 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 233 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 234 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 235 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 236 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 237 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 238 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 239 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 240 | github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM= 241 | github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= 242 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 243 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 244 | github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88= 245 | github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= 246 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 247 | github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= 248 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 249 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 250 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 251 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 252 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 253 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 254 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 255 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 256 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 257 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 258 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 259 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 260 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 261 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 262 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 263 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 264 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 265 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 266 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 267 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 268 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 269 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 270 | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= 271 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 272 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 273 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 274 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 275 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 276 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 277 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 278 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 279 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 280 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 281 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 282 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 283 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= 284 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 285 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 286 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 287 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= 288 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 289 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 290 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 291 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 292 | github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= 293 | github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= 294 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 295 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 296 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 297 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 298 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 299 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 300 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 301 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 302 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= 303 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 304 | github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E= 305 | github.com/swaggo/gin-swagger v1.2.0/go.mod h1:qlH2+W7zXGZkczuL+r2nEBR2JTT+/lX05Nn6vPhc7OI= 306 | github.com/swaggo/gin-swagger v1.3.0 h1:eOmp7r57oUgZPw2dJOjcGNMse9cvXcI4tTqBcnZtPsI= 307 | github.com/swaggo/gin-swagger v1.3.0/go.mod h1:oy1BRA6WvgtCp848lhxce7BnWH4C8Bxa0m5SkWx+cS0= 308 | github.com/swaggo/swag v1.5.1/go.mod h1:1Bl9F/ZBpVWh22nY0zmYyASPO1lI/zIwRDrpZU+tv8Y= 309 | github.com/swaggo/swag v1.6.9 h1:BukKRwZjnEcUxQt7Xgfrt9fpav0hiWw9YimdNO9wssw= 310 | github.com/swaggo/swag v1.6.9/go.mod h1:a0IpNeMfGidNOcm2TsqODUh9JHdHu3kxDA0UlGbBKjI= 311 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 312 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 313 | github.com/ugorji/go v1.1.5-pre/go.mod h1:FwP/aQVg39TXzItUBMwnWp9T9gPQnXw4Poh4/oBQZ/0= 314 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 315 | github.com/ugorji/go v1.1.13/go.mod h1:jxau1n+/wyTGLQoCkjok9r5zFa/FxT6eI5HiHKQszjc= 316 | github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0= 317 | github.com/ugorji/go/codec v0.0.0-20181022190402-e5e69e061d4f/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 318 | github.com/ugorji/go/codec v1.1.5-pre/go.mod h1:tULtS6Gy1AE1yCENaw4Vb//HLH5njI2tfCQDUqRd8fI= 319 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 320 | github.com/ugorji/go/codec v1.1.13/go.mod h1:oNVt3Dq+FO91WNQ/9JnHKQP2QJxTzoN7wCBFCq1OeuU= 321 | github.com/ugorji/go/codec v1.2.6 h1:7kbGefxLoDBuYXOms4yD7223OpNMMPNPZxXk5TvFcyQ= 322 | github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw= 323 | github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= 324 | github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= 325 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 326 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 327 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 328 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 329 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 330 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 331 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 332 | go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= 333 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 334 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 335 | go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= 336 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 337 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= 338 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 339 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 340 | go.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM= 341 | go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= 342 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 343 | golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 344 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 345 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 346 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 347 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 348 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 349 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= 350 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 351 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 352 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 353 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 354 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 355 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 356 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 357 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 358 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 359 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 360 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 361 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 362 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 363 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 364 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= 365 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 366 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 367 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 368 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 369 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 370 | golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= 371 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 372 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 373 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 374 | golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 375 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 376 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 377 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 378 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 379 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 380 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 381 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 382 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 383 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 384 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 385 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 386 | golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 387 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 388 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 389 | golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 390 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 391 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 392 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 393 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= 394 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 395 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 396 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 397 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 398 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 399 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 400 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 401 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 402 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 403 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 404 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 405 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 406 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 407 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 408 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 409 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 410 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 411 | golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 412 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 413 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 414 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 415 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 416 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 417 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 418 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 419 | golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 420 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 421 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 422 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 423 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 424 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 425 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 426 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 427 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 428 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 429 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= 430 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 431 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 432 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 433 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 434 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 435 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 436 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 437 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 438 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 439 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 440 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 441 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 442 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 443 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 444 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 445 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 446 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 447 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 448 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 449 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 450 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 451 | golang.org/x/tools v0.0.0-20190606050223-4d9ae51c2468/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 452 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 453 | golang.org/x/tools v0.0.0-20190611222205-d73e1c7e250b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 454 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 455 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 456 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 457 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 458 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 459 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 460 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 461 | golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 462 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 463 | golang.org/x/tools v0.0.0-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 464 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e h1:4nW4NLDYnU28ojHaHO8OVxFHk/aQ33U01a9cjED+pzE= 465 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 466 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 467 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 468 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 469 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 470 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 471 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 472 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 473 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 474 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 475 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 476 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 477 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 478 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 479 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 480 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 481 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 482 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 483 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 484 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 485 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 486 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 487 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 488 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 489 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 490 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 491 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 492 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 493 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 494 | google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= 495 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 496 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 497 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 498 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 499 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 500 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 501 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 502 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 503 | gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= 504 | gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= 505 | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 506 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= 507 | gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= 508 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 509 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 510 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 511 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 512 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 513 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 514 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 515 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 516 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 517 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 518 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 519 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 520 | gorm.io/driver/mysql v1.0.3 h1:+JKBYPfn1tygR1/of/Fh2T8iwuVwzt+PEJmKaXzMQXg= 521 | gorm.io/driver/mysql v1.0.3/go.mod h1:twGxftLBlFgNVNakL7F+P/x9oYqoymG3YYT8cAfI9oI= 522 | gorm.io/gorm v1.20.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= 523 | gorm.io/gorm v1.20.5 h1:g3tpSF9kggASzReK+Z3dYei1IJODLqNUbOjSuCczY8g= 524 | gorm.io/gorm v1.20.5/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= 525 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 526 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 527 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 528 | honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= 529 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 530 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 531 | -------------------------------------------------------------------------------- /internal/app/build.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | "gorm.io/gorm" 6 | "xs.bbs/pkg/servers" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/go-redis/redis/v9" 10 | _ "xs.bbs/docs" 11 | "xs.bbs/internal/app/community" 12 | "xs.bbs/internal/app/post" 13 | "xs.bbs/internal/app/user" 14 | ) 15 | 16 | var Entities = []interface{}{user.Entity, community.Entity, post.Entity} 17 | 18 | func Build(db *gorm.DB, rdb *redis.Client) *gin.Engine { 19 | if err := db.AutoMigrate(Entities...); err != nil { 20 | zap.L().Error("auto migrate tables error", zap.Error(err)) 21 | } 22 | 23 | return servers.NewHttpServer( 24 | user.Build(db), 25 | community.Build(db), 26 | post.Build(db, rdb), 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /internal/app/community/build.go: -------------------------------------------------------------------------------- 1 | package community 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | "xs.bbs/internal/app/community/controller" 6 | "xs.bbs/internal/app/community/model" 7 | "xs.bbs/internal/app/community/repository" 8 | "xs.bbs/internal/app/community/service" 9 | ) 10 | 11 | var ( 12 | Entity = model.Community{} 13 | //Set = wire.NewSet( 14 | // repo.CommunityDaoSet, 15 | // service.CommunityServiceSet, 16 | // controller.CommunityControllerSet, 17 | //) 18 | ) 19 | 20 | func Build(db *gorm.DB) *controller.CommunityController { 21 | repo := repository.NewCommunityRepo(db) 22 | cs := service.NewCommunityService(repo) 23 | return controller.NewCommunityController(cs) 24 | } 25 | -------------------------------------------------------------------------------- /internal/app/community/controller/community.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "xs.bbs/internal/app/community/model" 6 | "xs.bbs/internal/pkg/constant/e" 7 | "xs.bbs/internal/pkg/ginx" 8 | ) 9 | 10 | // GetCommunityList 获取所有文章标签 11 | func (cc *CommunityController) GetCommunityList(c *gin.Context) { 12 | resList, err := cc.communityService.GetCommunityList(c.Request.Context()) 13 | if err != nil { 14 | 15 | ginx.RespError(c, e.CodeError) 16 | return 17 | } 18 | ginx.RespSuccess(c, resList) 19 | } 20 | 21 | // GetCommunityDetail 获取所有文章标签 22 | func (cc *CommunityController) GetCommunityDetail(c *gin.Context) { 23 | var ( 24 | id int64 25 | err error 26 | resDto *model.CommunityDto 27 | ) 28 | if id, err = ginx.QueryInt("communityID", c); err != nil { 29 | ginx.RespError(c, e.CodeInvalidParams) 30 | return 31 | } 32 | 33 | resDto, err = cc.communityService.GetCommunityDetailByID(c.Request.Context(), id) 34 | 35 | switch err { 36 | case nil: 37 | ginx.RespSuccess(c, resDto) 38 | case e.ErrInvalidID: 39 | ginx.RespError(c, e.CodeInvalidID) 40 | case e.ErrConvDataErr: 41 | ginx.RespError(c, e.CodeConvDataErr) 42 | default: 43 | ginx.RespError(c, e.CodeError) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /internal/app/community/controller/controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "xs.bbs/internal/app/community/service" 6 | "xs.bbs/internal/pkg/middleware" 7 | ) 8 | 9 | //var CommunityControllerSet = wire.NewSet(NewCommunityController) 10 | 11 | type CommunityController struct { 12 | engine *gin.Engine 13 | communityService service.CommunityService 14 | } 15 | 16 | func NewCommunityController(service service.CommunityService) *CommunityController { 17 | return &CommunityController{ 18 | communityService: service, 19 | } 20 | } 21 | 22 | func (cc *CommunityController) RegisterHTTPRouter(r *gin.Engine) { 23 | r.Use(middleware.JWTAuth()) 24 | g := r.Group("/api/community") 25 | 26 | { 27 | g.GET("/list", cc.GetCommunityList) 28 | g.GET("/info", cc.GetCommunityDetail) 29 | } 30 | 31 | return 32 | } 33 | -------------------------------------------------------------------------------- /internal/app/community/model/dto.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // CommunityDto 社区标签 4 | type CommunityDto struct { 5 | CommunityID int64 `json:"communityID"` // 社区编号 6 | CommunityName string `json:"communityName"` // 社区名称 7 | Introduction string `json:"introduction"` // 社区介绍 8 | } 9 | -------------------------------------------------------------------------------- /internal/app/community/model/model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "gorm.io/gorm" 4 | 5 | // Community 社区标签 6 | type Community struct { 7 | gorm.Model 8 | CommunityID int64 `gorm:"not null;index:idx_community_id;"` // 社区编号 9 | CommunityName string `gorm:"size:32"` // 社区名称 10 | Introduction string `gorm:"type:text"` // 社区介绍 11 | } 12 | -------------------------------------------------------------------------------- /internal/app/community/repository/community.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "xs.bbs/internal/app/community/model" 8 | "xs.bbs/internal/pkg/constant/e" 9 | 10 | "go.uber.org/zap" 11 | "gorm.io/gorm" 12 | ) 13 | 14 | func (c *communityRepo) GetCommunityList(ctx context.Context) (resList []model.Community, err error) { 15 | if err = c.db.WithContext(ctx).Find(&resList).Error; err != nil { 16 | if errors.Is(err, gorm.ErrRecordNotFound) { 17 | zap.L().Error("repo.GetCommunityList no data", zap.Error(err)) 18 | err = gorm.ErrRecordNotFound 19 | } 20 | zap.L().Error("repo.GetCommunityList failed", zap.Error(err)) 21 | } 22 | return 23 | } 24 | 25 | func (c *communityRepo) GetCommunityDetailByID(ctx context.Context, ID int64) (res *model.Community, err error) { 26 | res = new(model.Community) 27 | if err = c.db.WithContext(ctx).Where("community_id", ID).First(&res).Error; err != nil { 28 | if errors.Is(err, gorm.ErrRecordNotFound) { 29 | zap.L().Error("repo.GetCommunityList no data", zap.Error(err)) 30 | err = e.ErrInvalidID 31 | } 32 | zap.L().Error("repo.GetCommunityList failed", zap.Error(err)) 33 | } 34 | return 35 | } 36 | -------------------------------------------------------------------------------- /internal/app/community/repository/repo.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | 6 | "gorm.io/gorm" 7 | "xs.bbs/internal/app/community/model" 8 | ) 9 | 10 | var _ CommunityRepo = (*communityRepo)(nil) 11 | 12 | // CommunityDaoSet CommunityDao依赖注入 13 | //var CommunityDaoSet = wire.NewSet( 14 | // wire.Struct(new(communityRepo), "*"), 15 | // wire.Bind(new(CommunityRepo), new(*communityRepo))) 16 | 17 | type ( 18 | communityRepo struct { 19 | db *gorm.DB 20 | } 21 | 22 | // CommunityRepo 文章标签接口 23 | CommunityRepo interface { 24 | GetCommunityList(context.Context) ([]model.Community, error) 25 | GetCommunityDetailByID(context.Context, int64) (*model.Community, error) 26 | } 27 | ) 28 | 29 | func NewCommunityRepo(db *gorm.DB) CommunityRepo { 30 | return &communityRepo{db: db} 31 | } 32 | -------------------------------------------------------------------------------- /internal/app/community/service/community.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gogf/gf/util/gconv" 7 | "github.com/pkg/errors" 8 | "xs.bbs/internal/app/community/model" 9 | "xs.bbs/internal/pkg/constant/e" 10 | ) 11 | 12 | // GetCommunityList 获取所有文章标签 13 | func (s *communityService) GetCommunityList(ctx context.Context) (resList []*CommunityDto, err error) { 14 | var communityList []model.Community 15 | communityList, err = s.repo.GetCommunityList(ctx) 16 | for _, c := range communityList { 17 | dto := new(CommunityDto) 18 | dto.CommunityID = c.CommunityID 19 | dto.CommunityName = c.CommunityName 20 | dto.Introduction = c.Introduction 21 | 22 | resList = append(resList, dto) 23 | } 24 | return 25 | } 26 | 27 | // GetCommunityDetailByID 根据社区id获取社区详情 28 | func (s *communityService) GetCommunityDetailByID(ctx context.Context, ID int64) (commDto *CommunityDto, err error) { 29 | var commuModel *model.Community 30 | 31 | if commuModel, err = s.repo.GetCommunityDetailByID(ctx, ID); err != nil { 32 | return nil, err 33 | } 34 | if err = gconv.Struct(commuModel, &commDto); err != nil { 35 | err = errors.Wrap(e.ErrConvDataErr, err.Error()) 36 | return 37 | } 38 | return 39 | } 40 | -------------------------------------------------------------------------------- /internal/app/community/service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | 6 | "xs.bbs/internal/app/community/model" 7 | "xs.bbs/internal/app/community/repository" 8 | ) 9 | 10 | var _ CommunityService = (*communityService)(nil) 11 | 12 | // CommunityServiceSet CommunityServiceSet依赖注入 13 | //var CommunityServiceSet = wire.NewSet( 14 | // wire.Struct(new(CommunityService), "*"), 15 | // wire.Bind(new(CommunityService), new(*CommunityService)), 16 | //) 17 | 18 | type ( 19 | CommunityDto = model.CommunityDto 20 | communityService struct { 21 | repo repository.CommunityRepo 22 | } 23 | 24 | // CommunityService 文章标签接口 25 | CommunityService interface { 26 | // GetCommunityList 获取所有文章标签 27 | GetCommunityList(context.Context) ([]*CommunityDto, error) 28 | // GetCommunityDetailByID 根据社区id获取社区详情 29 | GetCommunityDetailByID(context.Context, int64) (*CommunityDto, error) 30 | } 31 | ) 32 | 33 | func NewCommunityService(repo repository.CommunityRepo) CommunityService { 34 | return &communityService{ 35 | repo: repo, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /internal/app/post/build.go: -------------------------------------------------------------------------------- 1 | package post 2 | 3 | import ( 4 | "github.com/go-redis/redis/v9" 5 | "gorm.io/gorm" 6 | 7 | "xs.bbs/internal/app/post/controller" 8 | 9 | commuRepo "xs.bbs/internal/app/community/repository" 10 | "xs.bbs/internal/app/post/model" 11 | postRepo "xs.bbs/internal/app/post/repository" 12 | "xs.bbs/internal/app/post/service" 13 | userRepo "xs.bbs/internal/app/user/repository" 14 | ) 15 | 16 | var ( 17 | Entity = model.Post{} 18 | //Set = wire.NewSet( 19 | // repo.PostRepoSet, 20 | // service.PostServiceSet, 21 | // controller.PostControllerSet, 22 | //) 23 | ) 24 | 25 | /* 26 | 笔记: 27 | 曾经想 service.NewPostService()修改为service.NewService(store) 28 | 29 | 其中store是一个接口,嵌套了全部数据访问接口: PostRepo + userRepo + communityRepo 30 | 31 | 然后service层可以链式调用: 32 | 33 | c.repository.Post().Find(args1).... 34 | c.repository.User().Create(args2).... 35 | c.repository.Community().Update(args3).... 36 | 37 | 但是后来否决了这个想法,原因如下: 38 | 39 | 1.我们应该给函数传入它关心的最小集合作为参数,而不是,我有一个 struct,当某个函数需要这个 struct 的成员的时候, 40 | 我们把整个 struct 都作为参数传递进去。应该仅仅传递函数关心的最小集合。 41 | 2.不要链式调用方法,传进去的一整条调用链对函数来说,都是无关的耦合,只会让代码更 hard to change,让工程师惧怕去修改 42 | 3.只管命令不要询问,直接做具体的事,不要去找是哪个接口,链式调用就出现了先找到Post接口,然后再调用其实现 43 | */ 44 | func Build(db *gorm.DB, rdb *redis.Client) *controller.PostController { 45 | post := postRepo.NewPostRepo(db, rdb) 46 | user := userRepo.NewUserRepo(db) 47 | community := commuRepo.NewCommunityRepo(db) 48 | postService := service.NewPostService(post, user, community) 49 | return controller.NewPostController(postService) 50 | } 51 | -------------------------------------------------------------------------------- /internal/app/post/controller/controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "xs.bbs/internal/app/post/service" 6 | ) 7 | 8 | //var PostControllerSet = wire.NewSet( 9 | // NewPostController) 10 | 11 | type PostController struct { 12 | engine *gin.Engine 13 | postService service.PostService 14 | } 15 | 16 | func NewPostController(us service.PostService) *PostController { 17 | return &PostController{ 18 | postService: us, 19 | } 20 | } 21 | 22 | func (p *PostController) RegisterHTTPRouter(r *gin.Engine) { 23 | g := r.Group("/api/post") 24 | 25 | { 26 | g.POST("/", p.CreatePostHandle) 27 | g.GET("/info", p.GetPostDetailHandle) 28 | g.GET("/list", p.GetPostListHandle) 29 | g.POST("/vote", p.VoteForPost) 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /internal/app/post/controller/post.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "xs.bbs/internal/app/post/model" 6 | "xs.bbs/internal/pkg/constant/e" 7 | "xs.bbs/internal/pkg/ginx" 8 | "xs.bbs/internal/pkg/util" 9 | ) 10 | 11 | func (p *PostController) CreatePostHandle(c *gin.Context) { 12 | var ( 13 | err error 14 | userID int64 15 | postParam model.PostParam 16 | ) 17 | 18 | if errStr := ginx.BindAndValid(c, &postParam); errStr != "" { 19 | ginx.RespErrorWithMsg(c, e.CodeInvalidParams, errStr) 20 | return 21 | } 22 | 23 | if userID, err = ginx.GetCurrentUserID(c); err != nil { 24 | ginx.RespError(c, e.CodeNeedLogin) 25 | return 26 | } 27 | 28 | postParam.AuthorID = userID 29 | if err = p.postService.Create(c.Request.Context(), &postParam); err != nil { 30 | ginx.RespError(c, e.CodeError) 31 | return 32 | } 33 | 34 | ginx.RespSuccess(c, nil) 35 | } 36 | 37 | func (p *PostController) GetPostDetailHandle(c *gin.Context) { 38 | var ( 39 | pID int64 40 | err error 41 | dto *model.PostDetailDto 42 | ) 43 | 44 | if pID, err = ginx.QueryInt("postID", c); err != nil { 45 | ginx.RespError(c, e.CodeInvalidParams) 46 | return 47 | } 48 | 49 | if dto, err = p.postService.GetPostByID(c.Request.Context(), pID); err != nil { 50 | ginx.RespErrorWithMsg(c, e.CodeError, err.Error()) 51 | return 52 | } 53 | 54 | ginx.RespSuccess(c, dto) 55 | } 56 | 57 | func (p *PostController) GetPostListHandle(c *gin.Context) { 58 | var ( 59 | err error 60 | total int64 61 | posts []*model.PostDetailDto 62 | ) 63 | 64 | pageInfo := util.PageInfo{ 65 | Page: 1, 66 | PageSize: 5, 67 | } 68 | 69 | if errStr := ginx.BindAndValid(c, &pageInfo); errStr != "" { 70 | ginx.RespErrorWithMsg(c, e.CodeInvalidParams, errStr) 71 | return 72 | } 73 | 74 | if posts, total, err = p.postService.GetPostListByIDs(c.Request.Context(), &pageInfo); err != nil { 75 | ginx.RespError(c, e.CodeError) 76 | return 77 | } 78 | 79 | pageRes := &util.PageResult{ 80 | List: posts, 81 | Total: total, 82 | Page: pageInfo.Page, 83 | PageSize: pageInfo.PageSize, 84 | } 85 | 86 | ginx.RespSuccess(c, pageRes) 87 | } 88 | 89 | func (p *PostController) VoteForPost(c *gin.Context) { 90 | var ( 91 | err error 92 | userID int64 93 | voteParam model.PostVoteParam 94 | ) 95 | 96 | if errStr := ginx.BindAndValid(c, &voteParam); errStr != "" { 97 | ginx.RespErrorWithMsg(c, e.CodeInvalidParams, errStr) 98 | return 99 | } 100 | 101 | if userID, err = ginx.GetCurrentUserID(c); err != nil { 102 | ginx.RespError(c, e.CodeError) 103 | return 104 | } 105 | 106 | if err = p.postService.Vote(c.Request.Context(), userID, &voteParam); err != nil { 107 | ginx.RespError(c, e.CodeError) 108 | return 109 | } 110 | 111 | ginx.RespSuccess(c, nil) 112 | } 113 | -------------------------------------------------------------------------------- /internal/app/post/model/dto.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "xs.bbs/internal/app/community/model" 4 | 5 | // PostDto 帖子dto 6 | type PostDto struct { 7 | PostID int64 `json:"postID,string" form:"postID"` // 帖子id,增加“,string”解决前端ID失真问题 8 | AuthorID int64 `json:"authorID,string" form:"authorID"` // 作者的用户id 9 | CommunityID int64 `json:"communityID,string" form:"communityID"` // 所属社区 10 | Status int8 `json:"status" form:"status"` // 帖子状态 1:有效,0:无效 11 | Title string `json:"title" form:"title"` // 标题 12 | Content string `json:"content" form:"content"` // 内容 13 | CreatedAt string `json:"createdAt" form:"createdAt"` // 创建时间 14 | } 15 | 16 | // PostDetailDto 帖子详情dto 17 | type PostDetailDto struct { 18 | UserName string `json:"userName"` 19 | *PostDto 20 | *model.CommunityDto `json:"communityDto"` 21 | } 22 | -------------------------------------------------------------------------------- /internal/app/post/model/entity.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "gorm.io/gorm" 4 | 5 | // Post 帖子 6 | type Post struct { 7 | gorm.Model 8 | PostID int64 `gorm:"index:idx_post_id,unique"` // 帖子id 9 | AuthorID int64 `gorm:"index:idx_author_id"` // 作者的用户id 10 | CommunityID int64 `gorm:"index:idx_community_id"` // 所属社区 11 | Status int8 `gorm:"default:1"` // 帖子状态 1:有效,0:无效 12 | Title string `gorm:"size:256"` // 标题 13 | Content string `gorm:"type:text"` // 内容 14 | } 15 | -------------------------------------------------------------------------------- /internal/app/post/model/http.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // PostParam 帖子dto 4 | type PostParam struct { 5 | AuthorID int64 `json:"authorID" form:"authorID"` // 作者的用户id 6 | Title string `json:"title" form:"title"` // 标题 7 | Content string `json:"content" form:"content"` // 内容 8 | } 9 | 10 | // PostVoteParam 帖子投票dto 11 | type PostVoteParam struct { 12 | PostID int64 `json:"postID" form:"postID" v:"postID@required|min:1 #请输入帖子ID|请输入帖子ID"` // 帖子ID;min:1用来保证postID必须传入 // 13 | Direction int8 `json:"direction" form:"direction" v:"direction@in:1,-1,0#direction参数值应该在(1,-1,0)中"` // 赞成票:1;反对票:-1;取消投票:0 14 | } 15 | -------------------------------------------------------------------------------- /internal/app/post/repository/post.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "xs.bbs/internal/pkg/constant" 8 | 9 | "github.com/go-redis/redis/v9" 10 | "xs.bbs/internal/app/post/model" 11 | "xs.bbs/pkg/utils/snowflake" 12 | ) 13 | 14 | func (p *postRepo) Create(ctx context.Context, post *model.Post) (err error) { 15 | post.PostID = snowflake.GenID() 16 | // 1.存到MySQL中 17 | if err = p.db.Create(post).Error; err != nil { 18 | return 19 | } 20 | // 2.存到redis中 21 | pipeline := p.rdb.Pipeline() 22 | // 帖子时间 23 | pipeline.ZAdd(ctx, constant.RedisKey(constant.KeyPostTimeZSet), redis.Z{ 24 | Score: float64(time.Now().Unix()), 25 | Member: post.PostID, 26 | }) 27 | 28 | // 帖子分数 29 | pipeline.ZAdd(ctx, constant.RedisKey(constant.KeyPostScoreZSet), redis.Z{ 30 | Score: float64(time.Now().Unix()), 31 | Member: post.PostID, 32 | }) 33 | _, err = pipeline.Exec(ctx) 34 | return 35 | } 36 | 37 | func (p *postRepo) GetPostByID(ctx context.Context, pID int64) (post *model.Post, err error) { 38 | post = new(model.Post) 39 | err = p.db.WithContext(ctx).Where("post_id", pID).First(&post).Error 40 | return 41 | } 42 | 43 | func (p *postRepo) GetPostList(ctx context.Context, limit, offset int) (posts []*model.Post, total int64, err error) { 44 | posts = make([]*model.Post, 0, limit) // 默认取limit条 45 | db := p.db.WithContext(ctx).Model(&model.Post{}) 46 | err = db.Count(&total).Error 47 | err = db.Limit(limit).Offset(offset).Find(&posts).Error 48 | return 49 | } 50 | 51 | // GetPostListByIDs 根据post_id切片获取post列表,并按照给定的post_id顺序返回 52 | func (p *postRepo) GetPostListByIDs(ctx context.Context, pIDs []string) ([]*model.Post, int64, error) { 53 | panic("implement me") 54 | } 55 | -------------------------------------------------------------------------------- /internal/app/post/repository/redis.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "math" 6 | "time" 7 | 8 | "xs.bbs/internal/pkg/constant" 9 | 10 | "github.com/go-redis/redis/v9" 11 | "xs.bbs/internal/pkg/constant/e" 12 | ) 13 | 14 | // 推荐阅读 15 | // 基于用户投票的相关算法:http://www.ruanyifeng.com/blog/algorithm/ 16 | 17 | // 本项目使用简化版的投票分数 18 | // 投一票就加432分 86400/200 --> 200张赞成票可以给你的帖子续一天 19 | 20 | /* 投票的几种情况: 21 | direction=1时,有两种情况: 22 | 1. 之前没有投过票,现在投赞成票 --> 更新分数和投票记录 差值的绝对值:1 +432 23 | 2. 之前投反对票,现在改投赞成票 --> 更新分数和投票记录 差值的绝对值:2 +432*2 24 | direction=0时,有两种情况: 25 | 1. 之前投过反对票,现在要取消投票 --> 更新分数和投票记录 差值的绝对值:1 +432 26 | 2. 之前投过赞成票,现在要取消投票 --> 更新分数和投票记录 差值的绝对值:1 -432 27 | direction=-1时,有两种情况: 28 | 1. 之前没有投过票,现在投反对票 --> 更新分数和投票记录 差值的绝对值:1 -432 29 | 2. 之前投赞成票,现在改投反对票 --> 更新分数和投票记录 差值的绝对值:2 -432*2 30 | 31 | 投票的限制: 32 | 每个贴子自发表之日起一个星期之内允许用户投票,超过一个星期就不允许再投票了。 33 | 1. 到期之后将redis中保存的赞成票数及反对票数存储到mysql表中 34 | 2. 到期之后删除那个 KeyPostVotedZSetPF 35 | */ 36 | const ( 37 | oneWeekInSeconds = 7 * 24 * 3600 38 | scorePerVote = 432 // 每一票值多少分 39 | ) 40 | 41 | func (p *postRepo) Vote(ctx context.Context, userID, postID string, value float64) (err error) { 42 | // 1. 判断投票限制 43 | // 去redis取帖子发布时间 44 | postTime := p.rdb.ZScore(ctx, constant.RedisKey(constant.KeyPostTimeZSet), postID).Val() 45 | if float64(time.Now().Unix())-postTime > oneWeekInSeconds { // 发帖超过一周不允许参与投票 46 | return e.ErrVoteTimeExpire 47 | } 48 | // 2和3需要放到一个pipeline事务中操作 49 | 50 | // 2. 更新贴子的分数 51 | // 先查当前用户给当前帖子的投票记录 52 | oldVal := p.rdb.ZScore(ctx, constant.RedisKey(constant.KeyPostVotedZSetPre+postID), userID).Val() 53 | var op float64 54 | // 如果当前投票值大于查询出oldVal 55 | if value > oldVal { 56 | op = 1 57 | } else { 58 | op = -1 59 | } 60 | diff := math.Abs(oldVal - value) // 计算两次投票的差值 61 | pipeline := p.rdb.Pipeline() 62 | // 更新贴子的分数 63 | pipeline.ZIncrBy(ctx, constant.RedisKey(constant.KeyPostScoreZSet), op*diff*scorePerVote, postID) 64 | // 3. 记录用户为该贴子投票的数据 65 | if value == 0 { // 如果未投票,删除 66 | pipeline.ZRem(ctx, constant.RedisKey(constant.KeyPostVotedZSetPre+postID), userID) 67 | } else { 68 | pipeline.ZAdd(ctx, constant.RedisKey(constant.KeyPostVotedZSetPre+postID), redis.Z{ 69 | Score: value, // 赞成票还是反对票 70 | Member: userID, 71 | }) 72 | } 73 | _, err = pipeline.Exec(ctx) 74 | return 75 | } 76 | -------------------------------------------------------------------------------- /internal/app/post/repository/repo.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-redis/redis/v9" 7 | "gorm.io/gorm" 8 | "xs.bbs/internal/app/post/model" 9 | ) 10 | 11 | var _ PostRepo = (*postRepo)(nil) 12 | 13 | //var PostDaoSet = wire.NewSet( 14 | // new(postRepo), "*", 15 | // wire.Bind(new(PostRepo), new(*postRepo)), 16 | //) 17 | 18 | type ( 19 | postRepo struct { 20 | db *gorm.DB 21 | rdb *redis.Client 22 | } 23 | 24 | PostRepo interface { 25 | Create(ctx context.Context, post *model.Post) error 26 | GetPostByID(ctx context.Context, pID int64) (*model.Post, error) 27 | GetPostListByIDs(ctx context.Context, ids []string) ([]*model.Post, int64, error) 28 | GetPostList(ctx context.Context, page, pageSize int) ([]*model.Post, int64, error) 29 | // Vote 投票,数据存储于redis中 30 | Vote(ctx context.Context, userID, postID string, value float64) error 31 | } 32 | ) 33 | 34 | func NewPostRepo(db *gorm.DB, rdb *redis.Client) PostRepo { 35 | return &postRepo{db: db, rdb: rdb} 36 | } 37 | -------------------------------------------------------------------------------- /internal/app/post/service/post.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | 7 | "github.com/pkg/errors" 8 | 9 | "github.com/gogf/gf/util/gconv" 10 | community "xs.bbs/internal/app/community/model" 11 | "xs.bbs/internal/app/post/model" 12 | post "xs.bbs/internal/app/post/model" 13 | user "xs.bbs/internal/app/user/model" 14 | "xs.bbs/internal/pkg/constant/e" 15 | "xs.bbs/internal/pkg/util" 16 | "xs.bbs/pkg/utils/snowflake" 17 | ) 18 | 19 | func (p *postService) Create(ctx context.Context, parm *post.PostParam) (err error) { 20 | var postModel post.Post 21 | if err = gconv.Struct(parm, &postModel); err != nil { 22 | err = errors.Wrap(e.ErrConvDataErr, err.Error()) 23 | return 24 | } 25 | postModel.PostID = snowflake.GenID() 26 | return p.postRepo.Create(ctx, &postModel) 27 | } 28 | 29 | func (p *postService) GetPostByID(ctx context.Context, postID int64) (dto *post.PostDetailDto, err error) { 30 | var ( 31 | postModel *post.Post 32 | userModel *user.User 33 | communityModel *community.Community 34 | ) 35 | // 1.获取帖子 36 | if postModel, err = p.postRepo.GetPostByID(ctx, postID); err != nil { 37 | return 38 | } 39 | // 2.获取作者信息 40 | if userModel, err = p.userRepo.GetUserByID(ctx, postModel.AuthorID); err != nil { 41 | return 42 | } 43 | // 3.获取社区信息 44 | if communityModel, err = p.communityRepo.GetCommunityDetailByID(ctx, postModel.CommunityID); err != nil { 45 | return 46 | } 47 | if dto, err = ConvertToPostDetailDto(userModel, communityModel, postModel); err != nil { 48 | return 49 | } 50 | return 51 | } 52 | 53 | // GetPostListByIDs 根据post_id切片获取post列表,并按照给定的post_id顺序返回 54 | 55 | func (p *postService) GetPostListByIDs(ctx context.Context, paging *util.PageInfo) (resList []*model.PostDetailDto, total int64, err error) { 56 | var ( 57 | postListM []*post.Post 58 | userM *user.User 59 | communityM *community.Community 60 | ) 61 | // 1.获取帖子列表 62 | if postListM, total, err = p.postRepo.GetPostList(ctx, paging.PageSize, paging.Offset()); err != nil { 63 | err = errors.Wrap(err, "service: GetPostList failed") 64 | return 65 | } 66 | resList = make([]*model.PostDetailDto, 0, len(postListM)) 67 | for _, item := range postListM { 68 | // 2.获取作者信息 69 | if userM, err = p.userRepo.GetUserByID(ctx, item.AuthorID); err != nil { 70 | continue 71 | } 72 | // 3.获取社区信息 73 | if communityM, err = p.communityRepo.GetCommunityDetailByID(ctx, item.CommunityID); err != nil { 74 | continue 75 | } 76 | resDto := new(model.PostDetailDto) 77 | if resDto, err = ConvertToPostDetailDto(userM, communityM, item); err != nil { 78 | continue 79 | } 80 | resList = append(resList, resDto) 81 | } 82 | return 83 | } 84 | 85 | func ConvertToPostDetailDto( 86 | userM *user.User, 87 | communityM *community.Community, 88 | postM *post.Post) (dto *post.PostDetailDto, err error) { 89 | var ( 90 | postDto post.PostDto 91 | communityDto community.CommunityDto 92 | ) 93 | if err = gconv.Struct(postM, &postDto); err != nil { 94 | err = errors.Wrap(e.ErrConvDataErr, err.Error()) 95 | return 96 | } 97 | if err = gconv.Struct(communityM, &communityDto); err != nil { 98 | err = errors.Wrap(e.ErrConvDataErr, err.Error()) 99 | return 100 | } 101 | dto = &post.PostDetailDto{ 102 | UserName: userM.Username, 103 | PostDto: &postDto, 104 | CommunityDto: &communityDto, 105 | } 106 | dto.CreatedAt = util.TimeFormat(postM.CreatedAt, util.FMT_DATE_TIME) 107 | return 108 | } 109 | 110 | func (p *postService) Vote(ctx context.Context, userID int64, vote *model.PostVoteParam) (err error) { 111 | return p.postRepo.Vote(ctx, strconv.Itoa(int(userID)), strconv.Itoa(int(vote.PostID)), float64(vote.Direction)) 112 | } 113 | -------------------------------------------------------------------------------- /internal/app/post/service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | 6 | community "xs.bbs/internal/app/community/repository" 7 | "xs.bbs/internal/app/post/model" 8 | post "xs.bbs/internal/app/post/repository" 9 | user "xs.bbs/internal/app/user/repository" 10 | "xs.bbs/internal/pkg/util" 11 | ) 12 | 13 | var _ PostService = (*postService)(nil) 14 | 15 | //var PostServiceSet = wire.NewSet( 16 | // new(postService), "*", 17 | // wire.Bind(new(PostService), new(*postService)), 18 | //) 19 | 20 | type ( 21 | postService struct { 22 | postRepo post.PostRepo 23 | userRepo user.UserRepo 24 | communityRepo community.CommunityRepo 25 | } 26 | PostService interface { 27 | Create(ctx context.Context, post *model.PostParam) error 28 | GetPostByID(ctx context.Context, id int64) (*model.PostDetailDto, error) 29 | GetPostListByIDs(ctx context.Context, paging *util.PageInfo) ([]*model.PostDetailDto, int64, error) 30 | Vote(ctx context.Context, userID int64, voteP *model.PostVoteParam) error 31 | } 32 | ) 33 | 34 | func NewPostService(post post.PostRepo, user user.UserRepo, 35 | commu community.CommunityRepo) PostService { 36 | return &postService{ 37 | postRepo: post, 38 | userRepo: user, 39 | communityRepo: commu, 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /internal/app/user/build.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | "xs.bbs/internal/app/user/controller" 6 | "xs.bbs/internal/app/user/model" 7 | "xs.bbs/internal/app/user/repository" 8 | "xs.bbs/internal/app/user/service" 9 | ) 10 | 11 | var ( 12 | Entity = model.User{} 13 | // 不使用wire依赖注入 14 | //Set = wire.NewSet( 15 | // repo.UserDaoSet, 16 | // service.UserServiceSet, 17 | // controller.UserControllerSet, 18 | //) 19 | ) 20 | 21 | func Build(db *gorm.DB) *controller.UserController { 22 | userRepo := repository.NewUserRepo(db) 23 | userService := service.NewUserService(userRepo) 24 | return controller.NewUserController(userService) 25 | } 26 | -------------------------------------------------------------------------------- /internal/app/user/controller/controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "xs.bbs/internal/app/user/service" 6 | ) 7 | 8 | //var UserControllerSet = wire.NewSet( 9 | // NewUserController) 10 | 11 | type UserController struct { 12 | userService service.UserService 13 | } 14 | 15 | func NewUserController(us service.UserService) *UserController { 16 | return &UserController{ 17 | userService: us, 18 | } 19 | } 20 | 21 | func (u *UserController) RegisterHTTPRouter(r *gin.Engine) { 22 | g := r.Group("/api/user") 23 | 24 | { 25 | g.POST("/signup", u.Register) 26 | g.POST("/signin", u.Login) 27 | g.GET("/:userID", u.Get) 28 | g.DELETE("/:userID", u.Delete) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /internal/app/user/controller/user.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "errors" 5 | 6 | "xs.bbs/internal/app/user/model" 7 | "xs.bbs/internal/pkg/constant/e" 8 | "xs.bbs/internal/pkg/ginx" 9 | 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | // Register godoc 14 | // @Summary 用户注册账号 15 | // @Description 用户注册 16 | // @Tags 用户接口 17 | // @ID /user/signup 18 | // @Accept json 19 | // @Produce json 20 | // @Param body body model.RegisterReq true "body" 21 | // @Success 200 {object} ginx.Resp{data=model.UserDto} "success" 22 | // @Router /user/signup [post] 23 | func (u *UserController) Register(c *gin.Context) { 24 | var ( 25 | err error 26 | uParam model.RegisterReq 27 | uDto *model.UserDto 28 | ) 29 | if errStr := ginx.BindAndValid(c, &uParam); errStr != "" { 30 | ginx.RespErrorWithMsg(c, e.CodeError, errStr) 31 | return 32 | } 33 | 34 | uDto, err = u.userService.Register(c.Request.Context(), &uParam) 35 | 36 | switch err { 37 | case nil: 38 | ginx.RespSuccess(c, uDto) 39 | case e.ErrEmailExist: 40 | ginx.RespError(c, e.CodeEmailExist) 41 | case e.ErrConvDataErr: 42 | ginx.RespError(c, e.CodeConvDataErr) 43 | default: 44 | ginx.RespError(c, e.CodeError) 45 | } 46 | } 47 | 48 | // Login godoc 49 | // @Summary 登录 50 | // @Description 登录 51 | // @Tags 用户接口 52 | // @ID /user/signin 53 | // @Accept json 54 | // @Produce json 55 | // @Param body body model.LoginReq true "body参数" 56 | // @Success 200 {string} string "ok" "登陆成功" 57 | // @Router /user/signin [post] 58 | func (u *UserController) Login(c *gin.Context) { 59 | var ( 60 | err error 61 | signParam model.LoginReq 62 | token string 63 | ) 64 | 65 | if errStr := ginx.BindAndValid(c, &signParam); errStr != "" { 66 | ginx.RespErrorWithMsg(c, e.CodeError, errStr) 67 | return 68 | } 69 | if token, err = u.userService.Login(c.Request.Context(), &signParam); err != nil { 70 | if errors.Is(err, e.ErrUserNotExist) { 71 | ginx.RespError(c, e.CodeUserNotExist) 72 | return 73 | } 74 | ginx.RespError(c, e.CodeWrongUserNameOrPassword) 75 | return 76 | } 77 | token, err = u.userService.Login(c.Request.Context(), &signParam) 78 | 79 | switch err { 80 | case nil: 81 | ginx.RespSuccess(c, token) 82 | case e.ErrUserNotExist: 83 | ginx.RespError(c, e.CodeUserNotExist) 84 | default: 85 | ginx.RespError(c, e.CodeWrongUserNameOrPassword) 86 | } 87 | } 88 | 89 | // Get godoc 90 | // @Summary 根据id获取用户 91 | // @Description 根据id获取用户 92 | // @Tags 用户接口 93 | // @ID /user/Get 94 | // @Accept json 95 | // @Produce json 96 | // @Param id query string true "id" 97 | // @Success 200 {object} ginx.Resp{data=model.UserDto} "success" 98 | // @Router /user/Get [get] 99 | func (u *UserController) Get(c *gin.Context) { 100 | var ( 101 | userID int64 102 | err error 103 | uDto *model.UserDto 104 | ) 105 | 106 | if userID, err = ginx.QueryInt("userID", c); err != nil { 107 | ginx.RespError(c, e.CodeInvalidParams) 108 | return 109 | } 110 | 111 | uDto, err = u.userService.SelectByID(c.Request.Context(), userID) 112 | 113 | switch err { 114 | case nil: 115 | ginx.RespSuccess(c, uDto) 116 | case e.ErrUserNotExist: 117 | ginx.RespError(c, e.CodeUserNotExist) 118 | case e.ErrConvDataErr: 119 | ginx.RespError(c, e.CodeConvDataErr) 120 | default: 121 | ginx.RespError(c, e.CodeError) 122 | } 123 | } 124 | 125 | // Delete godoc 126 | // @Summary 根据id删除用户 127 | // @Description 根据id删除用户 128 | // @Tags 用户接口 129 | // @ID /user/delete 130 | // @Accept json 131 | // @Produce json 132 | // @Param id query string true "id" 133 | // @Success 200 {object} ginx.Resp{data=string} "success" 134 | // @Router /user/delete [get] 135 | func (u *UserController) Delete(c *gin.Context) { 136 | var ( 137 | userID int64 138 | err error 139 | ) 140 | 141 | if userID, err = ginx.QueryInt("userID", c); err != nil { 142 | ginx.RespError(c, e.CodeInvalidParams) 143 | return 144 | } 145 | 146 | if !u.userService.Delete(c.Request.Context(), userID) { 147 | ginx.RespError(c, e.CodeError) 148 | return 149 | } 150 | ginx.RespSuccess(c, nil) 151 | } 152 | -------------------------------------------------------------------------------- /internal/app/user/model/dto.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // UserDto . 4 | type UserDto struct { 5 | ID uint `json:"id"` // ID 6 | UserID int64 `json:"userID"` // 用户ID 7 | Username string `json:"username" form:"username"` // 用户名 8 | Email string `json:"email" form:"email"` // 邮箱 9 | Nickname string `json:"nickname" form:"nickname"` // 昵称 10 | } 11 | -------------------------------------------------------------------------------- /internal/app/user/model/entity.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | ) 6 | 7 | // User 用户结构体 8 | type User struct { 9 | gorm.Model 10 | UserID int64 `gorm:"not null;index:idx_user_id;"` // 用户ID 11 | Username string `gorm:"not null;size:32;unique;"` // 用户名 12 | Email string `gorm:"not null;size:128;unique;"` // 邮箱 13 | Nickname string `gorm:"not null;size:16;"` // 昵称 14 | Password string `gorm:"not null;size:512"` // 密码 15 | } 16 | -------------------------------------------------------------------------------- /internal/app/user/model/http.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // RegisterReq 用户注册参数 4 | type RegisterReq struct { 5 | Username string `json:"username" form:"username" v:"username@required|length:6,30#请输入用户名|用户名长度应当在:min到:max之间"` // 用户名 6 | Password string `json:"password" form:"password" v:"password@required|length:6,16#请输入密码|密码长度应当在:min到:max之间"` // 密码 7 | RePassword string `json:"rePassword" form:"rePassword" v:"rePassword@required|same:password#请输入密码|两次密码不一致,请重新输入"` // 重复密码 8 | Nickname string `json:"nickname" form:"nickname" v:"nickname@required#请输入中文名"` // 中文名 9 | Email string `json:"email" form:"email" v:"email@required|email#请输入邮箱|邮箱不合法"` // 邮箱 10 | } 11 | 12 | // LoginReq 用户注册参数 13 | type LoginReq struct { 14 | Username string `json:"username" form:"username" v:"username@required#请输入用户名"` // 用户名 15 | Password string `json:"password" form:"password" v:"password@required#请输入密码"` // 密码 16 | } 17 | -------------------------------------------------------------------------------- /internal/app/user/repository/repo.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | 6 | "xs.bbs/internal/app/user/model" 7 | 8 | "gorm.io/gorm" 9 | ) 10 | 11 | // 验证接口是否实现 12 | var _ UserRepo = (*userRepo)(nil) 13 | 14 | // UserDaoSet 使用wire 依赖注入,相当于下面的 NewUserRepo 函数 15 | //var UserDaoSet = wire.NewSet( 16 | // wire.Struct(new(userRepo), "*"), 17 | // wire.Bind(new(UserRepo), new(*userRepo))) 18 | 19 | type ( 20 | userRepo struct { 21 | db *gorm.DB 22 | } 23 | 24 | UserRepo interface { 25 | Insert(ctx context.Context, user *model.User) error 26 | Delete(ctx context.Context, id int64) bool 27 | Update(ctx context.Context, user *model.User) error 28 | GetUserByID(ctx context.Context, id int64) (*model.User, error) 29 | GetUserByName(ctx context.Context, userName string) (*model.User, error) 30 | // CheckUserByUserName 根据userName检查用户是否存在 31 | CheckUserByUserName(ctx context.Context, userName string) error 32 | // CheckUserByEmail 通过用户email检查用户 33 | CheckUserByEmail(ctx context.Context, email string) error 34 | } 35 | ) 36 | 37 | func NewUserRepo(db *gorm.DB) UserRepo { 38 | return &userRepo{db: db} 39 | } 40 | -------------------------------------------------------------------------------- /internal/app/user/repository/user.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "gorm.io/gorm" 8 | "xs.bbs/internal/app/user/model" 9 | "xs.bbs/internal/pkg/constant/e" 10 | ) 11 | 12 | // Insert 新增用户 13 | func (u *userRepo) Insert(ctx context.Context, user *model.User) (err error) { 14 | return u.db.WithContext(ctx).Create(&user).Error 15 | } 16 | 17 | // Delete 根据用户ID删除用户,软删除 18 | func (u *userRepo) Delete(ctx context.Context, userID int64) bool { 19 | return u.db.WithContext(ctx).Where("user_id = ?", userID).Delete(&model.User{}).RowsAffected > 0 20 | } 21 | 22 | // Update 根据用户ID修改用户 23 | func (u *userRepo) Update(ctx context.Context, user *model.User) error { 24 | return u.db.WithContext(ctx).Where("user_id = ?").Updates(&user).Error 25 | } 26 | 27 | // GetUserByName 根据用户名查询用户 28 | func (u *userRepo) GetUserByName(ctx context.Context, userName string) (user *model.User, err error) { 29 | user = new(model.User) 30 | if err = u.db.WithContext(ctx).Where("username = ?", userName).Find(user).Error; err != nil { 31 | return 32 | } 33 | return 34 | } 35 | 36 | // GetUserByID 根据用户ID查询用户 37 | func (u *userRepo) GetUserByID(ctx context.Context, userID int64) (user *model.User, err error) { 38 | user = new(model.User) 39 | if err = u.db.WithContext(ctx).Where("user_id = ?", userID).First(user).Error; err != nil { 40 | if errors.Is(err, gorm.ErrRecordNotFound) { 41 | err = e.ErrUserNotExist 42 | return 43 | } 44 | return 45 | } 46 | return 47 | } 48 | 49 | // CheckUserByUserName 根据userName检查用户是否存在 50 | func (u *userRepo) CheckUserByUserName(ctx context.Context, userName string) error { 51 | var count int64 52 | if err := u.db.WithContext(ctx).Model(&model.User{}).Where("username = ?", userName).Count(&count).Error; err != nil { 53 | return err 54 | } 55 | if count > 0 { 56 | return e.ErrUserExist 57 | } 58 | return nil 59 | } 60 | 61 | // CheckUserByEmail 通过email检查用户 62 | func (u *userRepo) CheckUserByEmail(ctx context.Context, email string) error { 63 | var count int64 64 | if err := u.db.WithContext(ctx).Model(&model.User{}).Where("email = ?", email).Count(&count).Error; err != nil { 65 | return err 66 | } 67 | if count > 0 { 68 | return e.ErrEmailExist 69 | } 70 | return nil 71 | } 72 | -------------------------------------------------------------------------------- /internal/app/user/service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | 6 | "xs.bbs/internal/app/user/model" 7 | "xs.bbs/internal/app/user/repository" 8 | ) 9 | 10 | // 验证接口是否实现 11 | var _ UserService = (*userService)(nil) 12 | 13 | // UserServiceSet 使用 wire 依赖注入,相当于下面的 NewUserService 函数 14 | //var UserServiceSet = wire.NewSet( 15 | // wire.Struct(new(userService), "*"), 16 | // wire.Bind(new(UserService), new(*userService))) 17 | 18 | type ( 19 | UserDto = model.UserDto 20 | userService struct { 21 | repo repository.UserRepo 22 | } 23 | 24 | UserService interface { 25 | // Register 注册 26 | Register(ctx context.Context, signUp *model.RegisterReq) (*UserDto, error) 27 | // Login 登陆 28 | Login(ctx context.Context, signIn *model.LoginReq) (string, error) 29 | Delete(ctx context.Context, id int64) bool 30 | Update(ctx context.Context, user *model.UserDto) error 31 | SelectByID(ctx context.Context, id int64) (*UserDto, error) 32 | SelectByName(ctx context.Context, userName string) (*UserDto, error) 33 | } 34 | ) 35 | 36 | func NewUserService(repo repository.UserRepo) UserService { 37 | return &userService{ 38 | repo: repo, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /internal/app/user/service/user.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/pkg/errors" 7 | "xs.bbs/internal/app/user/model" 8 | "xs.bbs/internal/pkg/constant/e" 9 | "xs.bbs/pkg/utils/hash" 10 | "xs.bbs/pkg/utils/jwt" 11 | "xs.bbs/pkg/utils/snowflake" 12 | 13 | "github.com/gogf/gf/util/gconv" 14 | ) 15 | 16 | // Register . 17 | func (u *userService) Register(ctx context.Context, param *model.RegisterReq) (resDto *UserDto, err error) { 18 | var uModel model.User 19 | 20 | if err = u.repo.CheckUserByUserName(ctx, param.Username); err != nil { 21 | return 22 | } 23 | 24 | if err = u.repo.CheckUserByEmail(ctx, param.Email); err != nil { 25 | return 26 | } 27 | 28 | if err = gconv.Struct(param, &uModel); err != nil { 29 | err = errors.Wrap(e.ErrConvDataErr, err.Error()) 30 | return 31 | } 32 | 33 | uModel.UserID = snowflake.GenID() 34 | // 密码加密 35 | uModel.Password = hash.MD5String(param.Password) 36 | 37 | if err = u.repo.Insert(ctx, &uModel); err != nil { 38 | return 39 | } 40 | 41 | if err = gconv.Struct(uModel, &resDto); err != nil { 42 | err = errors.Wrap(e.ErrConvDataErr, err.Error()) 43 | return 44 | } 45 | 46 | return 47 | } 48 | 49 | // Login 登陆 50 | func (u *userService) Login(ctx context.Context, signIn *model.LoginReq) (token string, err error) { 51 | var user *model.User 52 | // 获取用户信息 53 | if user, err = u.repo.GetUserByName(ctx, signIn.Username); err != nil { 54 | return 55 | } 56 | 57 | // 验证密码 58 | if user.Password != hash.MD5String(signIn.Password) { 59 | return 60 | } 61 | 62 | // 生成token 63 | return jwt.GenToken(user.UserID) 64 | } 65 | 66 | // Delete 根据用户ID删除用户 67 | func (u *userService) Delete(ctx context.Context, userID int64) bool { 68 | return u.repo.Delete(ctx, userID) 69 | } 70 | 71 | // Update 根据用户ID修改用户 72 | func (u *userService) Update(ctx context.Context, user *UserDto) error { 73 | var uModel model.User 74 | if err := gconv.Struct(user, &uModel); err != nil { 75 | return err 76 | } 77 | return u.repo.Update(ctx, &uModel) 78 | } 79 | 80 | // SelectByName 根据用户名查询用户 81 | func (u *userService) SelectByName(ctx context.Context, userName string) (resDto *UserDto, err error) { 82 | var uModel *model.User 83 | 84 | if uModel, err = u.repo.GetUserByName(ctx, userName); err != nil { 85 | return 86 | } 87 | 88 | if err = gconv.Struct(uModel, &resDto); err != nil { 89 | err = errors.Wrap(e.ErrConvDataErr, err.Error()) 90 | return 91 | } 92 | 93 | return 94 | } 95 | 96 | // SelectByID 根据用户ID查询用户 97 | func (u *userService) SelectByID(ctx context.Context, userID int64) (resDto *UserDto, err error) { 98 | var uModel *model.User 99 | 100 | if uModel, err = u.repo.GetUserByID(ctx, userID); err != nil { 101 | return 102 | } 103 | 104 | if err = gconv.Struct(uModel, &resDto); err != nil { 105 | err = errors.Wrap(e.ErrConvDataErr, err.Error()) 106 | return 107 | } 108 | 109 | return 110 | } 111 | -------------------------------------------------------------------------------- /internal/pkg/constant/e/code.go: -------------------------------------------------------------------------------- 1 | package e 2 | 3 | // ResCode . 4 | type ResCode int 5 | 6 | const ( 7 | CodeSuccess ResCode = 200 8 | CodeInvalidParams ResCode = 400 9 | CodeError ResCode = 500 10 | 11 | CodeConvDataErr ResCode = 50000 12 | CodeValidateParamsErr ResCode = 50001 13 | CodeInvalidToken ResCode = 50002 14 | CodeNeedLogin ResCode = 50003 15 | CodeInvalidID ResCode = 50004 16 | 17 | CodeWrongPassword ResCode = 40301 18 | CodeWrongUserNameOrPassword ResCode = 40302 19 | CodeUserNotExist ResCode = 40401 20 | CodeUserExist ResCode = 40902 21 | CodeEmailExist ResCode = 40903 22 | ) 23 | -------------------------------------------------------------------------------- /internal/pkg/constant/e/errors.go: -------------------------------------------------------------------------------- 1 | package e 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrUserNotLogin = errors.New(CodeNeedLogin.Msg()) 7 | ErrUserExist = errors.New(CodeUserExist.Msg()) 8 | ErrUserNotExist = errors.New(CodeUserNotExist.Msg()) 9 | ErrEmailExist = errors.New(CodeEmailExist.Msg()) 10 | ErrInvalidID = errors.New(CodeInvalidID.Msg()) 11 | ErrConvDataErr = errors.New(CodeConvDataErr.Msg()) 12 | ErrVoteTimeExpire = errors.New("投票时间已过") 13 | ) 14 | -------------------------------------------------------------------------------- /internal/pkg/constant/e/msg.go: -------------------------------------------------------------------------------- 1 | package e 2 | 3 | var codeMsgMap = map[ResCode]string{ 4 | CodeSuccess: "操作成功", // 200 5 | CodeInvalidParams: "请求参数错误", // 400 6 | CodeError: "服务器繁忙", // 500 7 | 8 | CodeConvDataErr: "数据转换错误", // 50000 9 | CodeValidateParamsErr: "参数校验错误", // 50001 10 | CodeInvalidToken: "无效的token", // 50002 11 | CodeNeedLogin: "请先登陆", // 50003 12 | CodeInvalidID: "无效的ID", // 50004 13 | 14 | CodeWrongPassword: "密码错误", // 40301 15 | CodeWrongUserNameOrPassword: "用户名或密码错误", // 40302 16 | CodeUserNotExist: "该用户不存在", // 40401 17 | CodeUserExist: "该用户已存在", // 40901 18 | CodeEmailExist: "该邮箱已存在", // 40902 19 | 20 | } 21 | 22 | // Msg . 23 | func (code ResCode) Msg() string { 24 | msg, ok := codeMsgMap[code] 25 | if !ok { 26 | msg = codeMsgMap[CodeError] 27 | } 28 | return msg 29 | } 30 | -------------------------------------------------------------------------------- /internal/pkg/constant/key.go: -------------------------------------------------------------------------------- 1 | package constant 2 | 3 | const ( 4 | KeyMySecret = "xiangshouduan.xs.bbs" 5 | KeyCtxUserID = "userID" 6 | ) 7 | const ( 8 | KeyPrefix = "xs.bbs:" 9 | KeyPostTimeZSet = "post:time" // zset;帖子及发帖时间 10 | KeyPostScoreZSet = "post:score" // zset;帖子及投票的分数 11 | KeyPostVotedZSetPre = "post:voted:" // zset;记录用户及投票类型;参数是post_id 12 | ) 13 | 14 | // RedisKey redis key 15 | func RedisKey(key string) string { 16 | return KeyPrefix + key 17 | } 18 | -------------------------------------------------------------------------------- /internal/pkg/ginx/get_current_user.go: -------------------------------------------------------------------------------- 1 | package ginx 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "xs.bbs/internal/pkg/constant" 6 | "xs.bbs/internal/pkg/constant/e" 7 | ) 8 | 9 | // GetCurrentUserID 获取当前登录的用户ID 10 | func GetCurrentUserID(c *gin.Context) (userID int64, err error) { 11 | uid, ok := c.Get(constant.KeyCtxUserID) 12 | if !ok { 13 | err = e.ErrUserNotLogin 14 | return 15 | } 16 | userID, ok = uid.(int64) 17 | if !ok { 18 | err = e.ErrUserNotLogin 19 | return 20 | } 21 | return 22 | } 23 | -------------------------------------------------------------------------------- /internal/pkg/ginx/parse.go: -------------------------------------------------------------------------------- 1 | package ginx 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/gin-gonic/gin" 7 | "go.uber.org/zap" 8 | ) 9 | 10 | func QueryInt(param string, c *gin.Context) (intVar int64, err error) { 11 | var intStr string 12 | 13 | if intStr = c.Param(param); intStr == "" { 14 | intStr = c.Query(param) 15 | } 16 | 17 | if intVar, err = strconv.ParseInt(intStr, 10, 64); err != nil { 18 | zap.L().Error("strconv.Atoi(intStr) 异常", zap.Error(err)) 19 | return 20 | } 21 | 22 | return 23 | } 24 | -------------------------------------------------------------------------------- /internal/pkg/ginx/response.go: -------------------------------------------------------------------------------- 1 | package ginx 2 | 3 | import ( 4 | "net/http" 5 | 6 | "xs.bbs/internal/pkg/constant/e" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | // Response . 12 | type Response struct { 13 | Code e.ResCode `json:"code"` 14 | Msg string `json:"msg"` 15 | Data interface{} `json:"data,omitempty"` 16 | } 17 | 18 | // RespSuccess 响应成功 19 | func RespSuccess(c *gin.Context, data interface{}) { 20 | c.JSON(http.StatusOK, &Response{ 21 | Code: e.CodeSuccess, 22 | Msg: e.CodeSuccess.Msg(), 23 | Data: data, 24 | }) 25 | } 26 | 27 | // RespError 响应失败,携带状态及对应信息 28 | func RespError(c *gin.Context, code e.ResCode) { 29 | c.JSON(http.StatusOK, &Response{ 30 | Code: code, 31 | Msg: code.Msg(), 32 | Data: nil, 33 | }) 34 | } 35 | 36 | // RespErrorWithMsg 响应失败,携带状态+其他自定义信息 37 | func RespErrorWithMsg(c *gin.Context, code e.ResCode, msg string) { 38 | c.JSON(http.StatusOK, &Response{ 39 | Code: code, 40 | Msg: msg, 41 | Data: nil, 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /internal/pkg/ginx/validator.go: -------------------------------------------------------------------------------- 1 | package ginx 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/gogf/gf/util/gvalid" 6 | "go.uber.org/zap" 7 | "xs.bbs/internal/pkg/constant/e" 8 | ) 9 | 10 | // BindAndValid 参数绑定及校验 11 | func BindAndValid(c *gin.Context, params interface{}) string { 12 | if err := c.ShouldBind(params); err != nil { 13 | zap.L().Error(e.CodeInvalidParams.Msg(), zap.Error(err)) 14 | return e.CodeInvalidParams.Msg() 15 | } 16 | if err := gvalid.CheckStruct(params, nil); err != nil { 17 | zap.L().Error(e.CodeValidateParamsErr.Msg(), zap.Error(err)) 18 | return err.FirstString() 19 | } 20 | return "" 21 | } 22 | -------------------------------------------------------------------------------- /internal/pkg/middleware/cors.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | -------------------------------------------------------------------------------- /internal/pkg/middleware/jwt.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "strings" 5 | 6 | "xs.bbs/internal/pkg/constant" 7 | 8 | "github.com/gin-gonic/gin" 9 | "xs.bbs/internal/pkg/constant/e" 10 | "xs.bbs/internal/pkg/ginx" 11 | "xs.bbs/pkg/utils/jwt" 12 | ) 13 | 14 | // JWTAuth jwt中间件 15 | func JWTAuth() gin.HandlerFunc { 16 | return func(c *gin.Context) { 17 | // 客户端携带Token有三种方式 1.放在请求头 2.放在请求体 3.放在URI 18 | // 这里假设Token放在Header的Authorization中,并使用Bearer开头 19 | // Authorization: Bearer xxxxxxx.xxx.xxx / X-TOKEN: xxx.xxx.xx 20 | // 这里的具体实现方式要依据你的实际业务情况决定 21 | authHeader := c.Request.Header.Get("Authorization") 22 | if authHeader == "" { 23 | ginx.RespError(c, e.CodeNeedLogin) 24 | c.Abort() 25 | return 26 | } 27 | // 按空格分割 28 | parts := strings.SplitN(authHeader, " ", 2) 29 | if !(len(parts) == 2 && parts[0] == "Bearer") { 30 | ginx.RespError(c, e.CodeInvalidToken) 31 | c.Abort() 32 | return 33 | } 34 | // parts[1]是获取到的tokenString,我们使用之前定义好的解析JWT的函数来解析它 35 | mc, err := jwt.ParseToken(parts[1]) 36 | if err != nil { 37 | ginx.RespError(c, e.CodeInvalidToken) 38 | c.Abort() 39 | return 40 | } 41 | // 将当前请求的userID信息保存到请求的上下文c上 42 | c.Set(constant.KeyCtxUserID, mc.UserID) 43 | 44 | c.Next() // 后续的处理请求的函数中 可以用过c.Get(KeyCtxUserID) 来获取当前请求的用户信息 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /internal/pkg/middleware/log_recovery.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | "net/http/httputil" 7 | "os" 8 | "runtime/debug" 9 | "strings" 10 | "time" 11 | 12 | "github.com/gin-gonic/gin" 13 | "go.uber.org/zap" 14 | ) 15 | 16 | // GinLogger 接收gin框架默认的日志 17 | func GinLogger() gin.HandlerFunc { 18 | return func(c *gin.Context) { 19 | start := time.Now() 20 | path := c.Request.URL.Path 21 | query := c.Request.URL.RawQuery 22 | c.Next() 23 | 24 | cost := time.Since(start) 25 | // 自定义日志输出内容 26 | zap.L().Info(path, 27 | zap.Int("status", c.Writer.Status()), // 响应状态 28 | zap.String("method", c.Request.Method), // 请求方式,eg:GFT/POST... 29 | zap.String("path", path), // 请求路径 30 | zap.String("query", query), // 请求参数 31 | // zap.String("ip", c.ClientIP()), // 客户端IP 32 | // zap.String("user-agent", c.Request.UserAgent()),// user-agent 内容 33 | zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()), 34 | zap.Duration("cost", cost), 35 | ) 36 | } 37 | } 38 | 39 | // GinRecovery recover掉项目可能出现的panic,并使用zap记录相关日志 40 | func GinRecovery(stack bool) gin.HandlerFunc { 41 | return func(c *gin.Context) { 42 | defer func() { 43 | if err := recover(); err != nil { 44 | // Check for a broken connection, as it is not really a 45 | // condition that warrants a panic stack trace. 46 | var brokenPipe bool 47 | if ne, ok := err.(*net.OpError); ok { 48 | if se, ok := ne.Err.(*os.SyscallError); ok { 49 | if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") { 50 | brokenPipe = true 51 | } 52 | } 53 | } 54 | 55 | httpRequest, _ := httputil.DumpRequest(c.Request, false) 56 | if brokenPipe { 57 | zap.L().Error(c.Request.URL.Path, 58 | zap.Any("error", err), 59 | zap.String("request", string(httpRequest)), 60 | ) 61 | // If the connection is dead, we can't write a status to it. 62 | c.Error(err.(error)) // nolint: errcheck 63 | c.Abort() 64 | return 65 | } 66 | 67 | if stack { 68 | zap.L().Error("[Recovery from panic]", 69 | zap.Any("error", err), 70 | zap.String("request", string(httpRequest)), 71 | zap.String("stack", string(debug.Stack())), 72 | ) 73 | } else { 74 | zap.L().Error("[Recovery from panic]", 75 | zap.Any("error", err), 76 | zap.String("request", string(httpRequest)), 77 | ) 78 | } 79 | c.AbortWithStatus(http.StatusInternalServerError) 80 | } 81 | }() 82 | c.Next() 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /internal/pkg/util/page.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | // PageInfo 分页请求数据 4 | type PageInfo struct { 5 | Page int `json:"page" form:"page"` // 页码 6 | PageSize int `json:"pageSize" form:"pageSize"` // 每页条数 7 | Order string `json:"order" form:"order"` // 排序方式 8 | } 9 | 10 | func (p *PageInfo) Offset() int { 11 | offset := 0 12 | if p.Page > 0 { 13 | offset = (p.Page - 1) * p.PageSize 14 | } 15 | return offset 16 | } 17 | 18 | // PageResult 分页返回数据 19 | type PageResult struct { 20 | List interface{} `json:"list"` 21 | Total int64 `json:"total"` 22 | Page int `json:"page"` 23 | PageSize int `json:"pageSize"` 24 | } 25 | -------------------------------------------------------------------------------- /internal/pkg/util/time.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | ) 7 | 8 | const ( 9 | FMT_DATE_TIME = "2006-01-02 15:04:05" 10 | FMT_DATE = "2006-01-02" 11 | FMT_TIME = "15:04:05" 12 | FMT_DATE_TIME_CN = "2006年01月02日 15时04分05秒" 13 | FMT_DATE_CN = "2006年01月02日" 14 | FMT_TIME_CN = "15时04分05秒" 15 | ) 16 | 17 | // NowUnix 秒时间戳 18 | func NowUnix() int64 { 19 | return time.Now().Unix() 20 | } 21 | 22 | // NowTimestamp 毫秒时间戳 23 | func NowTimestamp() int64 { 24 | return time.Now().UnixNano() / 1e6 25 | } 26 | 27 | // Timestamp 毫秒时间戳 28 | func Timestamp(t time.Time) int64 { 29 | return t.UnixNano() / 1e6 30 | } 31 | 32 | // TimeFromUnix 秒时间戳转时间 33 | func TimeFromUnix(unix int64) time.Time { 34 | return time.Unix(unix, 0) 35 | } 36 | 37 | // TimeFromTimestamp 毫秒时间戳转时间 38 | func TimeFromTimestamp(timestamp int64) time.Time { 39 | return time.Unix(0, timestamp*int64(time.Millisecond)) 40 | } 41 | 42 | // TimeFormat 时间格式化 43 | func TimeFormat(time time.Time, layout string) string { 44 | return time.Format(layout) 45 | } 46 | 47 | // TimeParse 字符串时间转时间类型 48 | func TimeParse(timeStr, layout string) (time.Time, error) { 49 | return time.Parse(layout, timeStr) 50 | } 51 | 52 | // GetDay return yyyyMMdd 53 | func GetDay(time time.Time) int { 54 | ret, _ := strconv.Atoi(time.Format("20060102")) 55 | return ret 56 | } 57 | 58 | // WithTimeAsStartOfDay 返回指定时间当天的开始时间 59 | func WithTimeAsStartOfDay(t time.Time) time.Time { 60 | return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()) 61 | } 62 | -------------------------------------------------------------------------------- /pkg/cache/redis.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/go-redis/redis/v9" 8 | 9 | "go.uber.org/zap" 10 | "xs.bbs/pkg/conf" 11 | ) 12 | 13 | var rdb *redis.Client 14 | 15 | // Init 初始化redis连接 16 | func Init(cfg *conf.Config) (*redis.Client, error) { 17 | rdb = redis.NewClient(&redis.Options{ 18 | Addr: fmt.Sprintf("%s:%d", cfg.RedisConfig.Host, cfg.RedisConfig.Port), 19 | Password: cfg.Password, // no password set 20 | DB: cfg.DB, // use default db 21 | PoolSize: cfg.PoolSize, 22 | MinIdleConns: cfg.MinIdleCons, 23 | }) 24 | if _, err := rdb.Ping(context.Background()).Result(); err != nil { 25 | zap.L().Error("redis ping failed", zap.Error(err)) 26 | return nil, err 27 | } 28 | return rdb, nil 29 | } 30 | 31 | // Close 关闭redis client连接资源 32 | func Close() { 33 | _ = rdb.Close() 34 | } 35 | -------------------------------------------------------------------------------- /pkg/conf/conf.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | // Config 应用配置 4 | type Config struct { 5 | System `mapstructure:"system"` 6 | LogConfig `mapstructure:"log"` 7 | MySQLConfig `mapstructure:"mysql"` 8 | RedisConfig `mapstructure:"redis"` 9 | } 10 | 11 | type System struct { 12 | AppName string `mapstructure:"app_name"` 13 | Mode string `mapstructure:"mode"` 14 | Version string `mapstructure:"version"` 15 | StartTime string `mapstructure:"start_time"` 16 | MachineID int64 `mapstructure:"machine_id"` 17 | Port int `mapstructure:"port"` 18 | } 19 | 20 | // LogConfig zap log配置 21 | type LogConfig struct { 22 | Prefix string `mapstructure:"prefix"` 23 | Level string `mapstructure:"level"` 24 | Filename string `mapstructure:"filename"` 25 | MaxSize int `mapstructure:"max_size"` 26 | MaxAge int `mapstructure:"max_age"` 27 | MaxBackups int `mapstructure:"max_backups"` 28 | } 29 | 30 | // MySQLConfig mysql配置 31 | type MySQLConfig struct { 32 | DSN string `mapstructure:"dsn"` // write data source name. 33 | LogMode bool `mapstructure:"log_mode"` // whether to open the log 34 | MaxOpenCons int `mapstructure:"max_open_cons"` // max open cons 35 | MaxIdleCons int `mapstructure:"max_idle_cons"` // max idle cons 36 | } 37 | 38 | // RedisConfig redis配置 39 | type RedisConfig struct { 40 | Host string `mapstructure:"host"` 41 | Password string `mapstructure:"password"` 42 | Port int `mapstructure:"port"` 43 | DB int `mapstructure:"db"` 44 | PoolSize int `mapstructure:"pool_size"` 45 | MinIdleCons int `mapstructure:"min_idle_cons"` 46 | } 47 | -------------------------------------------------------------------------------- /pkg/conf/init.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/fsnotify/fsnotify" 8 | 9 | "github.com/spf13/pflag" 10 | "github.com/spf13/viper" 11 | ) 12 | 13 | const defaultConfigFile = "../config.yaml" 14 | 15 | // Init init config 16 | func Init() (conf *Config, err error) { 17 | 18 | pflag.StringP("conf", "c", "", "choose conf file.") 19 | pflag.Parse() 20 | 21 | // 优先级: 命令行 > 环境变量 > 默认值 22 | v := viper.New() 23 | v.BindPFlags(pflag.CommandLine) 24 | 25 | configFile := v.GetString("conf") 26 | if configFile == "" { 27 | configFile = defaultConfigFile 28 | } 29 | 30 | v.SetConfigFile(configFile) 31 | if err = v.ReadInConfig(); err != nil { 32 | panic(fmt.Sprintf("Fatal error conf file: %s", err)) 33 | } 34 | // 监控config改变 35 | watchConfig(conf, v) 36 | 37 | if err = v.Unmarshal(&conf); err != nil { 38 | log.Fatalf("config unmarshal, err: %+v", err) 39 | } 40 | 41 | return 42 | } 43 | 44 | func watchConfig(conf *Config, v *viper.Viper) { 45 | v.WatchConfig() 46 | 47 | v.OnConfigChange(func(e fsnotify.Event) { 48 | fmt.Println("conf file changed:", e.Name) 49 | if err := v.Unmarshal(conf); err != nil { 50 | fmt.Println("v.OnConfigChange -> v.Unmarshal(&conf) failed,err:", err) 51 | return 52 | } 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /pkg/database/gorm.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "database/sql" 5 | 6 | "go.uber.org/zap" 7 | "xs.bbs/pkg/conf" 8 | 9 | "gorm.io/gorm/schema" 10 | 11 | "gorm.io/gorm/logger" 12 | 13 | _ "github.com/go-sql-driver/mysql" 14 | "gorm.io/driver/mysql" 15 | "gorm.io/gorm" 16 | ) 17 | 18 | var ( 19 | sqlDB *sql.DB 20 | ) 21 | 22 | // Init 初始化MySQL 23 | func Init(cfg *conf.Config) (db *gorm.DB, err error) { 24 | mysqlConfig := mysql.Config{ 25 | DSN: cfg.DSN, // DSN data source name 26 | DefaultStringSize: 191, // string 类型字段的默认长度 27 | DisableDatetimePrecision: true, // 禁用 datetime 精度,MySQL 5.6 之前的数据库不支持 28 | DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式,MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引 29 | DontSupportRenameColumn: true, // 用 `change` 重命名列,MySQL 8 之前的数据库和 MariaDB 不支持重命名列 30 | SkipInitializeWithVersion: false, // 根据版本自动配置 31 | } 32 | gormConfig := config(cfg.LogMode) 33 | if db, err = gorm.Open(mysql.New(mysqlConfig), gormConfig); err != nil { 34 | zap.L().Error("opens database failed", zap.Error(err)) 35 | return 36 | } 37 | 38 | if sqlDB, err = db.DB(); err != nil { 39 | zap.L().Error("db.db() failed", zap.Error(err)) 40 | return 41 | } 42 | sqlDB.SetMaxIdleConns(cfg.MaxIdleCons) 43 | sqlDB.SetMaxOpenConns(cfg.MaxOpenCons) 44 | return 45 | } 46 | 47 | // config 根据配置决定是否开启日志 48 | func config(mod bool) (c *gorm.Config) { 49 | if mod { 50 | c = &gorm.Config{ 51 | Logger: logger.Default.LogMode(logger.Info), 52 | DisableForeignKeyConstraintWhenMigrating: true, 53 | NamingStrategy: schema.NamingStrategy{ 54 | SingularTable: true, // 表名不加复数形式,false默认加 55 | }, 56 | } 57 | } else { 58 | c = &gorm.Config{ 59 | Logger: logger.Default.LogMode(logger.Silent), 60 | DisableForeignKeyConstraintWhenMigrating: true, 61 | NamingStrategy: schema.NamingStrategy{ 62 | SingularTable: true, // 表名不加复数形式,false默认加 63 | }, 64 | } 65 | } 66 | return 67 | } 68 | -------------------------------------------------------------------------------- /pkg/logger/zap.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/http" 7 | "net/http/httputil" 8 | "os" 9 | "runtime/debug" 10 | "strings" 11 | "time" 12 | 13 | "xs.bbs/pkg/conf" 14 | 15 | "github.com/gin-gonic/gin" 16 | "github.com/natefinch/lumberjack" 17 | "go.uber.org/zap" 18 | "go.uber.org/zap/zapcore" 19 | ) 20 | 21 | var lg *zap.Logger 22 | 23 | // Init 初始化Logger 24 | func Init(cfg *conf.Config) (err error) { 25 | var ( 26 | l zapcore.Level 27 | core zapcore.Core 28 | ) 29 | writeSyncer := getLogWriter( 30 | cfg.Filename, // 日志文件的位置 31 | cfg.MaxSize, // 在进行切割之前,日志文件的最大大小(以MB为单位) 32 | cfg.MaxBackups, // 保留旧文件的最大个数 33 | cfg.MaxAge, // 保留旧文件的最大天数 34 | ) 35 | encoder := getEncoder() 36 | if err = l.UnmarshalText([]byte(cfg.Level)); err != nil { 37 | fmt.Println("zap init failed:", err) 38 | return 39 | } 40 | if cfg.Mode == "dev" { 41 | // 进入开发模式,日志输出到终端 42 | consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()) 43 | core = zapcore.NewTee( 44 | zapcore.NewCore(encoder, writeSyncer, l), 45 | zapcore.NewCore(consoleEncoder, zapcore.Lock(os.Stdout), zapcore.DebugLevel), 46 | ) 47 | } else { 48 | core = zapcore.NewCore(encoder, writeSyncer, l) 49 | } 50 | lg = zap.New(core, zap.AddCaller()) 51 | zap.ReplaceGlobals(lg) 52 | zap.L().Info("init logger success") 53 | return 54 | } 55 | 56 | // getEncoder 设置zap编码器 57 | func getEncoder() zapcore.Encoder { 58 | encoderConfig := zap.NewProductionEncoderConfig() 59 | encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder 60 | encoderConfig.TimeKey = "time" 61 | encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder 62 | encoderConfig.EncodeDuration = zapcore.SecondsDurationEncoder 63 | encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder 64 | return zapcore.NewJSONEncoder(encoderConfig) 65 | } 66 | 67 | // getLogWriter 指定日志将写到哪里去,并使用Lumberjack进行日志切割归档 68 | func getLogWriter(filename string, maxSize, maxBackup, maxAge int) zapcore.WriteSyncer { 69 | lumberJackLogger := &lumberjack.Logger{ 70 | Filename: filename, 71 | MaxSize: maxSize, 72 | MaxBackups: maxBackup, 73 | MaxAge: maxAge, 74 | } 75 | return zapcore.AddSync(lumberJackLogger) 76 | } 77 | 78 | // GinLogger 接收gin框架默认的日志 79 | func GinLogger() gin.HandlerFunc { 80 | return func(c *gin.Context) { 81 | start := time.Now() 82 | path := c.Request.URL.Path 83 | query := c.Request.URL.RawQuery 84 | c.Next() 85 | 86 | cost := time.Since(start) 87 | // 自定义日志输出内容 88 | lg.Info(path, 89 | zap.Int("status", c.Writer.Status()), // 响应状态 90 | zap.String("method", c.Request.Method), // 请求方式,eg:GFT/POST... 91 | zap.String("path", path), // 请求路径 92 | zap.String("query", query), // 请求参数 93 | // zap.String("ip", c.ClientIP()), // 客户端IP 94 | // zap.String("user-agent", c.Request.UserAgent()),// user-agent 内容 95 | zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()), 96 | zap.Duration("cost", cost), 97 | ) 98 | } 99 | } 100 | 101 | // GinRecovery recover掉项目可能出现的panic,并使用zap记录相关日志 102 | func GinRecovery(stack bool) gin.HandlerFunc { 103 | return func(c *gin.Context) { 104 | defer func() { 105 | if err := recover(); err != nil { 106 | // Check for a broken connection, as it is not really a 107 | // condition that warrants a panic stack trace. 108 | var brokenPipe bool 109 | if ne, ok := err.(*net.OpError); ok { 110 | if se, ok := ne.Err.(*os.SyscallError); ok { 111 | if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") { 112 | brokenPipe = true 113 | } 114 | } 115 | } 116 | 117 | httpRequest, _ := httputil.DumpRequest(c.Request, false) 118 | if brokenPipe { 119 | lg.Error(c.Request.URL.Path, 120 | zap.Any("error", err), 121 | zap.String("request", string(httpRequest)), 122 | ) 123 | // If the connection is dead, we can't write a status to it. 124 | c.Error(err.(error)) // nolint: errcheck 125 | c.Abort() 126 | return 127 | } 128 | 129 | if stack { 130 | lg.Error("[Recovery from panic]", 131 | zap.Any("error", err), 132 | zap.String("request", string(httpRequest)), 133 | zap.String("stack", string(debug.Stack())), 134 | ) 135 | } else { 136 | lg.Error("[Recovery from panic]", 137 | zap.Any("error", err), 138 | zap.String("request", string(httpRequest)), 139 | ) 140 | } 141 | c.AbortWithStatus(http.StatusInternalServerError) 142 | } 143 | }() 144 | c.Next() 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /pkg/servers/http.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | ginSwagger "github.com/swaggo/gin-swagger" 6 | "github.com/swaggo/gin-swagger/swaggerFiles" 7 | "xs.bbs/internal/pkg/middleware" 8 | ) 9 | 10 | // Server http server 11 | type Server interface { 12 | // RegisterHTTPRouter register http router 13 | RegisterHTTPRouter(r *gin.Engine) 14 | } 15 | 16 | func NewHttpServer(servers ...Server) *gin.Engine { 17 | 18 | r := gin.New() 19 | // 设置swagger 20 | r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) 21 | 22 | // 设置公共中间件 23 | r.Use( 24 | middleware.GinLogger(), // zap logger中间件 25 | middleware.GinRecovery(true), // zap recovery中间件 26 | ) 27 | 28 | for _, s := range servers { 29 | s.RegisterHTTPRouter(r) 30 | } 31 | 32 | return r 33 | 34 | } 35 | -------------------------------------------------------------------------------- /pkg/utils/hash/hash.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | 7 | "xs.bbs/internal/pkg/constant" 8 | ) 9 | 10 | // MD5String 密码加密 11 | func MD5String(oPassword string) string { 12 | h := md5.New() 13 | h.Write([]byte(constant.KeyMySecret)) 14 | return hex.EncodeToString(h.Sum([]byte(oPassword))) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/utils/jwt/jwt.go: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "xs.bbs/internal/pkg/constant" 8 | 9 | "github.com/dgrijalva/jwt-go" 10 | ) 11 | 12 | type MyClaims struct { 13 | UserID int64 `json:"userID"` 14 | jwt.StandardClaims 15 | } 16 | 17 | const TokenExpireDuration = time.Hour * 24 18 | 19 | // GenToken 生成token 20 | func GenToken(userID int64) (string, error) { 21 | 22 | // 创建一个我们自己的声明 23 | c := &MyClaims{ 24 | userID, 25 | jwt.StandardClaims{ 26 | ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(), // 过期时间 27 | Issuer: "XS-bbs", // 签发人 28 | }, 29 | } 30 | // 使用指定的签名方法创建签名对象 31 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, c) 32 | // 使用指定的secret签名,注意转换为字节切片,并获得完整的编码后的字符串token 33 | return token.SignedString([]byte(constant.KeyMySecret)) 34 | } 35 | 36 | // ParseToken 解析JWT 37 | func ParseToken(tokenString string) (claims *MyClaims, err error) { 38 | // 解析token 39 | var ( 40 | token *jwt.Token 41 | ) 42 | // 这行分配内存地址的代码一定要写,否则在赋值时会提示invalid vlaue 43 | claims = new(MyClaims) 44 | token, err = jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (i interface{}, err error) { 45 | return []byte(constant.KeyMySecret), nil 46 | }) 47 | if err != nil { 48 | return 49 | } 50 | if token.Valid { // 校验token 51 | return 52 | } 53 | return nil, errors.New("invalid token") 54 | } 55 | -------------------------------------------------------------------------------- /pkg/utils/snowflake/snowflake.go: -------------------------------------------------------------------------------- 1 | package snowflake 2 | 3 | import ( 4 | "time" 5 | 6 | sf "github.com/bwmarrin/snowflake" 7 | "xs.bbs/pkg/conf" 8 | ) 9 | 10 | var node *sf.Node 11 | 12 | func Init(c *conf.Config) (err error) { 13 | var st time.Time 14 | st, err = time.Parse("2006-01-02", c.StartTime) 15 | if err != nil { 16 | return 17 | } 18 | sf.Epoch = st.UnixNano() / 1000000 19 | node, err = sf.NewNode(c.MachineID) 20 | if err != nil { 21 | return 22 | } 23 | return 24 | } 25 | 26 | func GenID() int64 { 27 | return node.Generate().Int64() 28 | } 29 | -------------------------------------------------------------------------------- /script/my_app.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `user` ( 2 | `id` bigint(20) NOT NULL AUTO_INCREMENT, 3 | `user_id` bigint(20) NOT NULL, 4 | `username` varchar(64) COLLATE utf8mb4_general_ci NOT NULL, 5 | `password` varchar(64) COLLATE utf8mb4_general_ci NOT NULL, 6 | `email` varchar(64) COLLATE utf8mb4_general_ci, 7 | `gender` tinyint(4) NOT NULL DEFAULT '0', 8 | `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP, 9 | `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 10 | PRIMARY KEY (`id`), 11 | UNIQUE KEY `idx_username` (`username`) USING BTREE, 12 | UNIQUE KEY `idx_user_id` (`user_id`) USING BTREE 13 | ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci; --------------------------------------------------------------------------------