commit 174410ffa0b295ae259a2fcde6b9035e9b9e58e9 Author: 李寻欢 Date: Fri Aug 20 14:59:13 2021 +0800 :tada: 初始化项目,基础功能已完成 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..20d3308 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea +vendor +logs \ No newline at end of file diff --git a/config/alisms.go b/config/alisms.go new file mode 100644 index 0000000..2c00e75 --- /dev/null +++ b/config/alisms.go @@ -0,0 +1,15 @@ +package config + +// 阿里云通讯配置信息 +type aliSmsConfig struct { + AccessKey string + AccessSecret string +} + +// InitAliSmsConfig 初始化阿里云通信配置 +func InitAliSmsConfig() { + key := getEnvVal("ALI_SMS_KEY", "") + secret := getEnvVal("ALI_SMS_SECRET", "") + + AliSmsConfig = aliSmsConfig{AccessKey: key, AccessSecret: secret} +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..205ab61 --- /dev/null +++ b/config/config.go @@ -0,0 +1,46 @@ +package config + +import ( + "os" + "strconv" +) + +var ( + AliSmsConfig aliSmsConfig + RedisConfig redisConfig +) + +// 从环境变量获取字符串类型值 +func getEnvVal(key, defaultVal string) string { + val, exist := os.LookupEnv(key) + if !exist { + return defaultVal + } + return val +} + +// 从环境变量获取数字类型值 +func getEnvIntVal(key string, defaultVal int) int { + valStr, exist := os.LookupEnv(key) + if !exist { + return defaultVal + } + val, err := strconv.Atoi(valStr) + if err != nil { + return defaultVal + } + return val +} + +// 从环境变量获取数字类型值 +func getEnvBoolVal(key string, defaultVal bool) bool { + valStr, exist := os.LookupEnv(key) + if !exist { + return defaultVal + } + val, err := strconv.ParseBool(valStr) + if err != nil { + return defaultVal + } + return val +} diff --git a/config/mysql.go b/config/mysql.go new file mode 100644 index 0000000..d912156 --- /dev/null +++ b/config/mysql.go @@ -0,0 +1 @@ +package config diff --git a/config/redis.go b/config/redis.go new file mode 100644 index 0000000..96ed34b --- /dev/null +++ b/config/redis.go @@ -0,0 +1,28 @@ +package config + +// Redis配置 +type redisConfig struct { + Host string // Redis主机 + Port string // Redis端口 + Password string // Redis密码 + Db int // Redis库 +} + +// InitRedisConfig 初始化Redis配置 +func InitRedisConfig() { + // RedisHost Redis主机 + host := getEnvVal("REDIS_HOST", "redis") + // RedisPort Redis端口 + port := getEnvVal("REDIS_PORT", "6379") + // RedisPassword Redis密码 + password := getEnvVal("REDIS_PWD", "") + // Redis库 + db := getEnvIntVal("REDIS_DB", 0) + + RedisConfig = redisConfig{ + Host: host, + Port: port, + Password: password, + Db: db, + } +} diff --git a/controller/login.go b/controller/login.go new file mode 100644 index 0000000..9068973 --- /dev/null +++ b/controller/login.go @@ -0,0 +1,94 @@ +package controller + +import ( + "fmt" + "github.com/gin-gonic/gin" + . "go_api_tmpl/core" + . "go_api_tmpl/global" + "go_api_tmpl/repository" + "go_api_tmpl/utils" + "math/rand" + "net/http" + "time" +) + +// GetSmsCodeController 获取登录验证码 +func GetSmsCodeController(ctx *gin.Context) { + type requestBody struct { + Phone string `json:"phone"` + } + // 取出并判断设备指纹是否存在 + deviceId := ctx.GetHeader("di") + // 取出手机号 + var params requestBody + err := ctx.ShouldBindJSON(¶ms) + if err != nil { + Log.Errorf("[发送短信验证码]读取参数错误: %v", err.Error()) + R(ctx).FailWithMessage("参数错误") + return + } + phone := params.Phone + Log.Debugf("设备指纹: %v -----> 手机号: %v", deviceId, phone) + // 判断设备是否发送过 + phoneKey := fmt.Sprintf("auth:sms:phone:%v", phone) + flag := RedisConn.ExistKey(phoneKey) + if flag { + R(ctx).FailWithMessage("发送间隔过短,请稍后重试") + return + } + // 开始发送短信 + rnd := rand.New(rand.NewSource(time.Now().UnixNano())) + code := fmt.Sprintf("%06v", rnd.Int31n(1000000)) + // 发送 + flag = AliSmsBot.SentSms(phone, code) + if !flag { + R(ctx).FailWithMessage("验证码发送失败,请稍后重试") + return + } + err = RedisConn.SetWithTimeout(phoneKey, code, "600") + if err != nil { + R(ctx).FailWithMessage("验证码发送失败,请稍后重试") + return + } + R(ctx).OkWithMessage("验证码已发送,有效期十分钟,请尽快使用") +} + +// LoginController 登录 +func LoginController(ctx *gin.Context) { + type loginParams struct { + Phone string + Code string + } + var params loginParams + if err := ctx.ShouldBindJSON(¶ms); err != nil { + R(ctx).FailWithMessage("参数错误") + return + } + phoneKey := fmt.Sprintf("auth:sms:phone:%v", params.Phone) + if !RedisConn.ExistKey(phoneKey) { + R(ctx).FailWithMessageAndCode("未发送验证码或已过期", http.StatusForbidden) + return + } + cacheCode, err := RedisConn.GetData(phoneKey) + if err != nil { + R(ctx).FailWithMessage("系统异常") + return + } + if cacheCode != params.Code { + R(ctx).FailWithMessageAndCode("验证码错误", http.StatusForbidden) + return + } + // 验证通过,查询数据库数据 + ur := repository.NewUserRepository() + dbUser, err := ur.GetUserByPhone(params.Phone) + if err != nil { + Log.Errorf("用户数据获取失败: %v", err.Error()) + R(ctx).FailWithMessage("系统错误") + return + } + token := utils.GenerateToken(dbUser.ID) + result := map[string]interface{}{} + result["token"] = token + + R(ctx).OkDetailed(result, "登录成功") +} diff --git a/core/error_handle.go b/core/error_handle.go new file mode 100644 index 0000000..90884d0 --- /dev/null +++ b/core/error_handle.go @@ -0,0 +1,22 @@ +package core + +import ( + "github.com/gin-gonic/gin" + "net/http" +) + +// CustomHTTPErrorHandler 默认全局异常处理 +func CustomHTTPErrorHandler() gin.HandlerFunc { + return func(ctx *gin.Context) { + R(ctx).FailWithMessage("不知道出了啥异常") + ctx.Abort() + } +} + +// NotFoundErrorHandler 404异常处理 +func NotFoundErrorHandler() gin.HandlerFunc { + return func(ctx *gin.Context) { + R(ctx).FailWithMessageAndCode("请求接口不存在", http.StatusNotFound) + ctx.Abort() + } +} diff --git a/core/response.go b/core/response.go new file mode 100644 index 0000000..13efbd8 --- /dev/null +++ b/core/response.go @@ -0,0 +1,74 @@ +package core + +import ( + "github.com/gin-gonic/gin" + "net/http" +) + +// 返回数据包装 +type response struct { + Code int `json:"code"` + Data interface{} `json:"data"` + Msg string `json:"msg"` +} + +type rs struct { + ctx *gin.Context +} + +// 定义状态码 +const ( + ERROR = http.StatusInternalServerError + SUCCESS = http.StatusOK +) + +func R(ctx *gin.Context) *rs { + return &rs{ctx: ctx} +} + +// Result 手动组装返回结果 +func (r rs) Result(code int, data interface{}, msg string) { + if data == nil { + data = map[string]interface{}{} + } + r.ctx.JSON(code, response{ + code, + data, + msg, + }) +} + +// Ok 返回无数据的成功 +func (r rs) Ok() { + r.Result(SUCCESS, nil, "操作成功") +} + +// OkWithMessage 返回自定义成功的消息 +func (r rs) OkWithMessage(message string) { + r.Result(SUCCESS, nil, message) +} + +// OkWithData 自定义内容的成功返回 +func (r rs) OkWithData(data interface{}) { + r.Result(SUCCESS, data, "操作成功") +} + +// OkDetailed 自定义消息和内容的成功返回 +func (r rs) OkDetailed(data interface{}, message string) { + r.Result(SUCCESS, data, message) +} + +// Fail 返回默认失败 +func (r rs) Fail() { + r.Result(ERROR, nil, "操作失败") +} + +// FailWithMessage 返回默认状态码自定义消息的失败 +func (r rs) FailWithMessage(message string) { + r.Result(ERROR, nil, message) +} + +// FailWithMessageAndCode 返回自定义消息和状态码的失败 +func (r rs) FailWithMessageAndCode(message string, code int) { + r.Result(code, nil, message) +} diff --git a/global/alisms.go b/global/alisms.go new file mode 100644 index 0000000..2ebc8da --- /dev/null +++ b/global/alisms.go @@ -0,0 +1,28 @@ +package global + +import ( + "fmt" + "github.com/aliyun/alibaba-cloud-sdk-go/services/dysmsapi" + "go_api_tmpl/config" +) + +type aliSmsBot struct{} + +// SentSms 发送短信验证码 +func (b aliSmsBot) SentSms(phone string, code string) bool { + client, err := dysmsapi.NewClientWithAccessKey("ap-northeast-1", config.AliSmsConfig.AccessKey, config.AliSmsConfig.AccessSecret) + request := dysmsapi.CreateSendSmsRequest() + request.Scheme = "https" + + request.PhoneNumbers = phone + request.SignName = "悟空单车" + request.TemplateCode = "SMS_34310120" + request.TemplateParam = fmt.Sprintf("{\"name\":\"%v\",\"code\":\"%v\"}", phone, code) + + response, err := client.SendSms(request) + if err != nil { + Log.Errorf("验证码发送失败: %v", err.Error()) + return false + } + return response.IsSuccess() +} diff --git a/global/error.go b/global/error.go new file mode 100644 index 0000000..52b689f --- /dev/null +++ b/global/error.go @@ -0,0 +1,8 @@ +package global + +// CheckError 处理错误 +func CheckError(err error, msgTmpl string) { + if err != nil { + Log.Panicf(msgTmpl, err) + } +} diff --git a/global/global.go b/global/global.go new file mode 100644 index 0000000..282c896 --- /dev/null +++ b/global/global.go @@ -0,0 +1,15 @@ +package global + +import ( + "github.com/casbin/casbin/v2" + "go.uber.org/zap" + "gorm.io/gorm" +) + +var ( + MySQLConn *gorm.DB + RedisConn RedisClient + AliSmsBot aliSmsBot + Enforcer *casbin.Enforcer + Log *zap.SugaredLogger +) diff --git a/global/logger.go b/global/logger.go new file mode 100644 index 0000000..c56d9e6 --- /dev/null +++ b/global/logger.go @@ -0,0 +1,32 @@ +package global + +import ( + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "os" + "time" +) + +// InitLogger 初始化日志工具 +func InitLogger() { + // 配置 sugaredLogger + writer := zapcore.AddSync(os.Stdout) + + // 自定义时间输出格式 + customTimeEncoder := func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { + enc.AppendString("[" + t.Format("2006-01-02 15:04:05.000") + "]") + } + + // 格式相关的配置 + encoderConfig := zap.NewProductionEncoderConfig() + // 修改时间戳的格式 + encoderConfig.EncodeTime = customTimeEncoder + // 日志级别使用大写带颜色显示 + encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder + encoder := zapcore.NewConsoleEncoder(encoderConfig) + // 将日志级别设置为 DEBUG + core := zapcore.NewCore(encoder, writer, zapcore.DebugLevel) + // 增加 caller 信息 + logger := zap.New(core, zap.AddCaller()) + Log = logger.Sugar() +} diff --git a/global/mysql.go b/global/mysql.go new file mode 100644 index 0000000..4e4984b --- /dev/null +++ b/global/mysql.go @@ -0,0 +1,36 @@ +package global + +import ( + "fmt" + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/logger" + "log" + "os" + "time" +) + +// InitMySQLClient 初始化MySQL连接 +func InitMySQLClient() { + USER := "casbin" + PASS := "casbin123" + HOST := "192.168.3.9" + PORT := "3306" + DBNAME := "casbin_demo" + + newLogger := logger.New( + log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer + logger.Config{ + SlowThreshold: time.Second, // Slow SQL threshold + LogLevel: logger.Info, // Log level + Colorful: true, // Disable color + }, + ) + + url := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", USER, PASS, HOST, PORT, DBNAME) + conn, err := gorm.Open(mysql.Open(url), &gorm.Config{Logger: newLogger}) + if err != nil { + Log.Panicf("初始化MySQL连接失败, 错误信息: %v", err) + } + MySQLConn = conn +} diff --git a/global/redis.go b/global/redis.go new file mode 100644 index 0000000..869e7ac --- /dev/null +++ b/global/redis.go @@ -0,0 +1,57 @@ +package global + +import ( + "errors" + "github.com/garyburd/redigo/redis" +) + +// RedisClient Redis连接对象 +type RedisClient struct { + Client redis.Conn +} + +// GetData 获取数据 +func (r *RedisClient) GetData(key string) (string, error) { + return redis.String(r.Client.Do("get", key)) +} + +// GetKeys 获取key列表 +func (r *RedisClient) GetKeys(key string) ([]string, error) { + return redis.Strings(r.Client.Do("keys", key)) +} + +// ExistKey 判断是否存在key +func (r *RedisClient) ExistKey(key string) bool { + keys, err := redis.Strings(r.Client.Do("keys", key)) + if err != nil { + return false + } + return len(keys) > 0 +} + +// Set 保存数据 +func (r *RedisClient) Set(key string, value string) error { + _, err := r.Client.Do("set", key, value) + if err != nil { + return errors.New("Redis保存数据失败") + } + return nil +} + +// SetWithTimeout 保存带过期时间的数据(单位:秒) +func (r *RedisClient) SetWithTimeout(key string, value string, timeout string) error { + _, err := r.Client.Do("set", key, value, "EX", timeout) + if err != nil { + return errors.New("Redis保存数据失败") + } + return nil +} + +// Del 根据key删除Redis数据 +func (r *RedisClient) Del(key string) error { + _, err := r.Client.Do("DEL", key) + if err != nil { + return err + } + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..468b5ae --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module go_api_tmpl + +go 1.16 + +require ( + github.com/aliyun/alibaba-cloud-sdk-go v1.61.1238 + github.com/casbin/casbin/v2 v2.28.3 + github.com/casbin/gorm-adapter/v3 v3.3.3 + github.com/garyburd/redigo v1.6.2 + github.com/gin-gonic/gin v1.7.4 + github.com/golang-jwt/jwt v3.2.2+incompatible + go.uber.org/zap v1.19.0 + golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 + gorm.io/driver/mysql v1.1.2 + gorm.io/gorm v1.21.13 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..11ed3e9 --- /dev/null +++ b/go.sum @@ -0,0 +1,277 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/aliyun/alibaba-cloud-sdk-go v1.61.1238 h1:K0l7Nb8xi2nb/j6evXwGofebgW+6XT/AuVOEuWGI344= +github.com/aliyun/alibaba-cloud-sdk-go v1.61.1238/go.mod h1:pUKYbK5JQ+1Dfxk80P0qxGqe5dkxDoabbZS7zOcouyA= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/casbin/casbin/v2 v2.28.3 h1:iHxxEsNHwSciRoYh+54etVUA8AXKS9OKzNy6/39UWvY= +github.com/casbin/casbin/v2 v2.28.3/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg= +github.com/casbin/gorm-adapter/v3 v3.3.3 h1:aO3ixIs+FsGh/in+iXXY8rLpdmeucs+s50T/RI2Rsdc= +github.com/casbin/gorm-adapter/v3 v3.3.3/go.mod h1:z0J/CpAznL6MyXMPnzltbLBI6lykprGc1rusy+vMJps= +github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc h1:VRRKCwnzqk8QCaRC4os14xoKDdbHqqlJtJA0oc1ZAjg= +github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/garyburd/redigo v1.6.2 h1:yE/pwKCrbLpLpQICzYTeZ7JsTA/C53wFTJHaEtRqniM= +github.com/garyburd/redigo v1.6.2/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.7.4 h1:QmUZXrvJ9qZ3GfWvQ+2wnW/1ePrTEJqPKMYEU3lD/DM= +github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= +github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= +github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk= +github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= +github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= +github.com/jackc/pgconn v1.8.0 h1:FmjZ0rOyXTr1wfWs45i4a9vjnjWUAGpMuQLD9OSs+lw= +github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2 h1:JVX6jT/XfzNqIjye4717ITLaNwV9mWbJx0dLCpcRzdA= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.0.6 h1:b1105ZGEMFe7aCvrT1Cca3VoVb4ZFMaFJLJcg/3zD+8= +github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0= +github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po= +github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ= +github.com/jackc/pgtype v1.6.2 h1:b3pDeuhbbzBYcg5kwNmNDun4pFUD/0AAr1kLXZLeNt8= +github.com/jackc/pgtype v1.6.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA= +github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o= +github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg= +github.com/jackc/pgx/v4 v4.10.1 h1:/6Q3ye4myIj6AaplUm+eRcz4OhK9HAvFf4ePsG40LJY= +github.com/jackc/pgx/v4 v4.10.1/go.mod h1:QlrWebbs3kqEZPHCTGyxecvzG6tvIsYu+A5b1raylkA= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI= +github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg= +github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc h1:jUIKcSPO9MoMJBbEoyE/RJoE8vz7Mb8AjvifMMwSyvY= +github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a h1:pa8hGb/2YqsZKovtsgrwcDH1RZhVbTKCjLp47XpqCDs= +github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.19.0 h1:mZQZefskPPCMIBCSEH0v2/iUqqLrYtaeqwD6FUGUnFE= +go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11 h1:Yq9t9jnGoR+dBuitxdo9l6Q7xh/zOyNnYUtDKaQ3x0E= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= +gopkg.in/ini.v1 v1.42.0 h1:7N3gPTt50s8GuLortA00n8AqRTk75qOP98+mTPpgzRk= +gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.0.3/go.mod h1:twGxftLBlFgNVNakL7F+P/x9oYqoymG3YYT8cAfI9oI= +gorm.io/driver/mysql v1.1.2 h1:OofcyE2lga734MxwcCW9uB4mWNXMr50uaGRVwQL2B0M= +gorm.io/driver/mysql v1.1.2/go.mod h1:4P/X9vSc3WTrhTLZ259cpFd6xKNYiSSdSZngkSBGIMM= +gorm.io/driver/postgres v1.0.8 h1:PAgM+PaHOSAeroTjHkCHCBIHHoBIf9RgPWGo8dF2DA8= +gorm.io/driver/postgres v1.0.8/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg= +gorm.io/driver/sqlserver v1.0.4 h1:V15fszi0XAo7fbx3/cF50ngshDSN4QT0MXpWTylyPTY= +gorm.io/driver/sqlserver v1.0.4/go.mod h1:ciEo5btfITTBCj9BkoUVDvgQbUdLWQNqdFY5OGuGnRg= +gorm.io/gorm v1.20.0/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= +gorm.io/gorm v1.20.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= +gorm.io/gorm v1.20.11/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= +gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= +gorm.io/gorm v1.21.9/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= +gorm.io/gorm v1.21.12/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= +gorm.io/gorm v1.21.13 h1:JU5A4yVemRjdMndJ0oZU7VX+Nr2ICE3C60U5bgR6mHE= +gorm.io/gorm v1.21.13/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= +gorm.io/plugin/dbresolver v1.1.0 h1:cegr4DeprR6SkLIQlKhJLYxH8muFbJ4SmnojXvoeb00= +gorm.io/plugin/dbresolver v1.1.0/go.mod h1:tpImigFAEejCALOttyhWqsy4vfa2Uh/vAUVnL5IRF7Y= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= diff --git a/handle/apis.go b/handle/apis.go new file mode 100644 index 0000000..d47bb2c --- /dev/null +++ b/handle/apis.go @@ -0,0 +1,14 @@ +package handle + +import ( + "github.com/gin-gonic/gin" + "go_api_tmpl/global" + "go_api_tmpl/repository" +) + +// SaveAllRoute 保存入库所有接口路由信息 +func SaveAllRoute(routes gin.RoutesInfo) { + global.Log.Debugf("待入库路由数量: %v", len(routes)) + ar := repository.NewApisRepository() + ar.SaveAllApi(routes) +} diff --git a/handle/request_log.go b/handle/request_log.go new file mode 100644 index 0000000..944bf07 --- /dev/null +++ b/handle/request_log.go @@ -0,0 +1 @@ +package handle diff --git a/initialization/casbin.go b/initialization/casbin.go new file mode 100644 index 0000000..71039a1 --- /dev/null +++ b/initialization/casbin.go @@ -0,0 +1,33 @@ +package initialization + +import ( + "fmt" + "github.com/casbin/casbin/v2" + "github.com/casbin/casbin/v2/model" + gormAdapter "github.com/casbin/gorm-adapter/v3" + "go_api_tmpl/global" +) + +// Casbin 初始化Casbin的RABC模型 +func Casbin() { + // Initialize casbin adapter + adapter, err := gormAdapter.NewAdapterByDBUseTableName(global.MySQLConn, "", "t_role_rule") + if err != nil { + panic(fmt.Sprintf("failed to initialize casbin adapter: %v", err)) + } + + // 初始化模型 + model := model.NewModel() + model.AddDef("r", "r", "sub, obj, act") + model.AddDef("p", "p", "sub, obj, act") + model.AddDef("g", "g", "_, _") + model.AddDef("e", "e", "some(where (p.eft == allow))") + // 匹配规则(下面的规则适用与根据Gin的Route的Path和Method做校验,最后那个r.sub == 1是用来判断超级管理员的) + model.AddDef("m", "m", "g(r.sub, p.sub) && keyMatch2(r.obj, p.obj) && regexMatch(r.act, p.act) || r.sub == \"1\"") + + enf, err := casbin.NewEnforcer(model, adapter) + if err != nil { + panic(fmt.Sprintf("failed to create casbin enforcer: %v", err)) + } + global.Enforcer = enf +} diff --git a/initialization/db.go b/initialization/db.go new file mode 100644 index 0000000..ff28fb2 --- /dev/null +++ b/initialization/db.go @@ -0,0 +1,48 @@ +package initialization + +import ( + . "go_api_tmpl/global" + "go_api_tmpl/repository" +) + +// DatabaseTable 初始化数据库表结构 +func DatabaseTable() { + // 初始化菜单表 + Log.Debug("开始初始化接口路由表") + ar := repository.NewApisRepository() + err := ar.Migrate() + CheckError(err, "接口路由表初始化失败: %v") + + Log.Debug("开始初始化用户表") + ur := repository.NewUserRepository() + err = ur.Migrate() + CheckError(err, "用户表初始化失败: %v") + + Log.Debug("开始初始化角色表") + rr := repository.NewRoleRepository() + err = rr.Migrate() + CheckError(err, "角色表初始化失败: %v") + + Log.Debug("开始初始化菜单表") + mr := repository.NewMenuRepository() + err = mr.Migrate() + CheckError(err, "菜单表初始化失败: %v") + + Log.Debug("开始初始化用户角色表") + urr := repository.NewUserRoleRepository() + err = urr.Migrate() + CheckError(err, "用户角色表初始化失败: %v") + + Log.Debug("开始初始化角色菜单表") + rmr := repository.NewRoleMenuRepository() + err = rmr.Migrate() + CheckError(err, "角色菜单表初始化失败: %v") + + Log.Debug("开始初始化接口访问记录表") + rlr := repository.NewRequestLogRepository() + err = rlr.Migrate() + CheckError(err, "接口访问记录表初始化失败: %v") + + // TODO 初始化其他表 + Log.Debug("所有数据库表初始化完成") +} diff --git a/initialization/init.go b/initialization/init.go new file mode 100644 index 0000000..0612217 --- /dev/null +++ b/initialization/init.go @@ -0,0 +1,14 @@ +package initialization + +import "go_api_tmpl/config" + +func Init() { + // 初始化数据库表 + DatabaseTable() + // 初始化Casbin + Casbin() + // 初始化Redis + InitRedisConn() + // 阿里云短信配置 + config.InitAliSmsConfig() +} diff --git a/initialization/redis.go b/initialization/redis.go new file mode 100644 index 0000000..b832f05 --- /dev/null +++ b/initialization/redis.go @@ -0,0 +1,35 @@ +package initialization + +import ( + "fmt" + "github.com/garyburd/redigo/redis" + "go_api_tmpl/config" + "go_api_tmpl/global" + "time" +) + +// InitRedisConn 初始化Redis连接对象 +func InitRedisConn() { + // 读取配置 + config.InitRedisConfig() + // 初始化连接 + conn, err := redis.Dial("tcp", + // Redis连接信息 + fmt.Sprintf("%s:%s", config.RedisConfig.Host, config.RedisConfig.Port), + // 密码 + redis.DialPassword(config.RedisConfig.Password), + // 默认使用数据库 + redis.DialDatabase(config.RedisConfig.Db), + redis.DialKeepAlive(1*time.Second), + redis.DialConnectTimeout(5*time.Second), + redis.DialReadTimeout(1*time.Second), + redis.DialWriteTimeout(1*time.Second)) + if err != nil { + global.Log.Panicf("Redis初始化连接失败: %v", err.Error()) + } else { + global.Log.Info("Redis连接初始化成功") + global.RedisConn = global.RedisClient{ + Client: conn, + } + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..bec627f --- /dev/null +++ b/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "github.com/gin-gonic/gin" + "go_api_tmpl/core" + "go_api_tmpl/global" + "go_api_tmpl/handle" + "go_api_tmpl/initialization" + "go_api_tmpl/middleware" + "go_api_tmpl/route" +) + +func main() { + // 初始化日志工具 + global.InitLogger() + // 初始化数据库连接 + global.InitMySQLClient() + // 初始化相关组件 + initialization.Init() + + // 初始化Gin + //app := gin.New() + //app.Use(middleware.ZapLogger(), gin.Recovery()) + app := gin.Default() + // 定义全局异常处理 + app.NoRoute(core.NotFoundErrorHandler()) + // 保存接口访问记录 + app.Use(middleware.SaveRequestLog(), middleware.CheckDeviceId()) + // 初始化路由 + route.InitRoute(app) + // 路由初始化完毕,入库所有接口 + handle.SaveAllRoute(app.Routes()) + + // 启动项目 + _ = app.Run(":8888") +} diff --git a/middleware/casbin.go b/middleware/casbin.go new file mode 100644 index 0000000..0e7083b --- /dev/null +++ b/middleware/casbin.go @@ -0,0 +1,47 @@ +package middleware + +import ( + "fmt" + "github.com/gin-gonic/gin" + "go_api_tmpl/core" + . "go_api_tmpl/global" + "net/http" +) + +// AuthorityVerify 权限验证中间件 +func AuthorityVerify() gin.HandlerFunc { + return func(c *gin.Context) { + // 取出用户ID + userId, existed := c.Get("userId") + if !existed { + core.R(c).FailWithMessageAndCode("请先登录", http.StatusUnauthorized) + c.Abort() + return + } + Log.Info(userId) + // 从数据库加载权限规则数据 + err := Enforcer.LoadPolicy() + if err != nil { + core.R(c).FailWithMessage("权限加载失败") + c.Abort() + return + } + + // 取出Path和Method + p := c.Request.URL.Path + m := c.Request.Method + // 验证权限 + ok, err := Enforcer.Enforce(fmt.Sprint(userId), p, m) + if err != nil { + core.R(c).FailWithMessage("权限验证失败") + c.Abort() + return + } + if !ok { + core.R(c).FailWithMessage("权限不足") + c.Abort() + return + } + c.Next() + } +} diff --git a/middleware/jwt.go b/middleware/jwt.go new file mode 100644 index 0000000..9ee1ec5 --- /dev/null +++ b/middleware/jwt.go @@ -0,0 +1,41 @@ +package middleware + +import ( + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt" + "go_api_tmpl/core" + "go_api_tmpl/global" + "go_api_tmpl/utils" + "net/http" +) + +// AuthorizeJWT 验证JWT +func AuthorizeJWT() gin.HandlerFunc { + return func(ctx *gin.Context) { + const BearerSchema string = "Bearer " + authHeader := ctx.GetHeader("Authorization") + if authHeader == "" { + core.R(ctx).FailWithMessageAndCode("请先登录", http.StatusUnauthorized) + ctx.Abort() + return + } + tokenString := authHeader[len(BearerSchema):] + + if token, err := utils.ValidateToken(tokenString); err != nil { + global.Log.Errorf("Token校验失败: %v ------> %v", tokenString, err.Error()) + core.R(ctx).FailWithMessageAndCode("Token校验失败", http.StatusUnauthorized) + return + } else { + if claims, ok := token.Claims.(jwt.MapClaims); !ok { + ctx.AbortWithStatus(http.StatusUnauthorized) + } else { + if token.Valid { + ctx.Set("userId", claims["userId"]) + } else { + ctx.AbortWithStatus(http.StatusUnauthorized) + } + } + } + + } +} diff --git a/middleware/logger.go b/middleware/logger.go new file mode 100644 index 0000000..20c9ded --- /dev/null +++ b/middleware/logger.go @@ -0,0 +1,39 @@ +package middleware + +import ( + "github.com/gin-gonic/gin" + "go_api_tmpl/global" + "time" +) + +// ZapLogger 接收gin框架的路由日志 +func ZapLogger() gin.HandlerFunc { + return func(c *gin.Context) { + start := time.Now() + path := c.Request.URL.Path + query := c.Request.URL.RawQuery + c.Next() + + cost := time.Since(start) + //global.Log.Info(path, + // zap.Int("status", c.Writer.Status()), + // zap.String("method", c.Request.Method), + // zap.String("path", path), + // zap.String("query", query), + // zap.String("ip", c.ClientIP()), + // zap.String("user-agent", c.Request.UserAgent()), + // zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()), + // zap.Duration("cost", cost), + //) + logTmpl := ` +================================================================================= +Path: %v +Param: %v +Method: %v +IP: %v +Cost: %v +================================================================================= +` + global.Log.Debugf(logTmpl, path, query, c.Request.Method, c.ClientIP(), cost) + } +} diff --git a/middleware/request.go b/middleware/request.go new file mode 100644 index 0000000..8480d7f --- /dev/null +++ b/middleware/request.go @@ -0,0 +1,76 @@ +package middleware + +import ( + "bytes" + "github.com/gin-gonic/gin" + "go_api_tmpl/core" + "go_api_tmpl/model" + "go_api_tmpl/repository" + "io/ioutil" + "net/http" + "time" +) + +// CheckDeviceId 检查是否存在设备指纹 +func CheckDeviceId() gin.HandlerFunc { + return func(ctx *gin.Context) { + // 取出设备指纹 + deviceId := ctx.Request.Header.Get("di") + if deviceId == "" { + // 设备指纹不存在 + core.R(ctx).FailWithMessageAndCode("请求不合法", http.StatusBadRequest) + ctx.Abort() + return + } + ctx.Next() + } +} + +// SaveRequestLog 保存所有请求接口日志 +func SaveRequestLog() gin.HandlerFunc { + return func(ctx *gin.Context) { + // 开始时间 + start := time.Now() + // 取出接口 + path := ctx.Request.URL.Path + // 取出参数 + query := ctx.Request.URL.RawQuery + // body参数 + body, err := ctx.GetRawData() + bodyStr := "" + if err == nil { + bodyStr = string(body) + ctx.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body)) + } + // form参数 + form := ctx.Request.Form.Encode() + // 取出请求方式 + method := ctx.Request.Method + // 取出设备指纹 + deviceId := ctx.Request.Header.Get("di") + // 取出IP + ip := ctx.ClientIP() + // UA + ua := ctx.Request.UserAgent() + ctx.Next() + // 计算耗时 + cost := time.Since(start).Milliseconds() + // 组装实体 + rlog := model.RequestLog{ + Method: method, + Path: path, + Param: query, + Body: bodyStr, + Form: form, + Cost: cost, + StatusCode: ctx.Writer.Status(), + DeviceId: deviceId, + Ip: ip, + UserAgent: ua, + } + + //global.Log.Debugf("接口请求结果: %v", rlog) + rlr := repository.NewRequestLogRepository() + rlr.SaveLog(&rlog) + } +} diff --git a/model/apis.go b/model/apis.go new file mode 100644 index 0000000..538308f --- /dev/null +++ b/model/apis.go @@ -0,0 +1,11 @@ +package model + +type Apis struct { + ID uint `json:"id" gorm:"primarykey"` + Path string `json:"path" ` + Method string `json:"method" ` +} + +func (Apis) TableName() string { + return "t_api_route" +} diff --git a/model/menu.go b/model/menu.go new file mode 100644 index 0000000..9f625a2 --- /dev/null +++ b/model/menu.go @@ -0,0 +1,11 @@ +package model + +import "gorm.io/gorm" + +type Menu struct { + gorm.Model +} + +func (Menu) TableName() string { + return "t_menu" +} diff --git a/model/request_log.go b/model/request_log.go new file mode 100644 index 0000000..30cd14b --- /dev/null +++ b/model/request_log.go @@ -0,0 +1,25 @@ +package model + +import ( + "time" +) + +// RequestLog 请求日志实体 +type RequestLog struct { + ID uint `gorm:"primarykey"` + CreatedAt time.Time + Method string `json:"method" gorm:"type:varchar(10) not null comment '请求方式';"` // 请求方式 + Path string `json:"path" gorm:"type:varchar(100) not null comment '请求接口';"` // 请求接口 + Param string `json:"param" gorm:"type:text comment 'Query参数';"` // Query参数 + Body string `json:"body" gorm:"type:text comment 'Body参数';"` // Body参数 + Form string `json:"form" gorm:"type:text comment 'Form参数';"` // Form参数 + Cost int64 `json:"cost" gorm:"type:int comment 'Query参数';"` // 耗时(微秒) + StatusCode int `json:"status_code" gorm:"type:int comment '返回状态码';"` // 返回状态码 + DeviceId string `json:"device_id" gorm:"type:varchar(50) not null comment '设备指纹';"` // 设备指纹 + Ip string `json:"ip" gorm:"type:varchar(50) comment 'IP地址';"` // IP地址 + UserAgent string `json:"user_agent" gorm:"type:varchar(500) comment 'UserAgent';"` // UA +} + +func (RequestLog) TableName() string { + return "t_request_log" +} diff --git a/model/role.go b/model/role.go new file mode 100644 index 0000000..433cc83 --- /dev/null +++ b/model/role.go @@ -0,0 +1,11 @@ +package model + +import "gorm.io/gorm" + +type Role struct { + gorm.Model +} + +func (Role) TableName() string { + return "t_role" +} diff --git a/model/role_menu.go b/model/role_menu.go new file mode 100644 index 0000000..ef97f6f --- /dev/null +++ b/model/role_menu.go @@ -0,0 +1,11 @@ +package model + +import "gorm.io/gorm" + +type RoleMenu struct { + gorm.Model +} + +func (RoleMenu) TableName() string { + return "t_role_menu" +} diff --git a/model/user.go b/model/user.go new file mode 100644 index 0000000..506c901 --- /dev/null +++ b/model/user.go @@ -0,0 +1,12 @@ +package model + +import "gorm.io/gorm" + +type User struct { + gorm.Model + Phone string +} + +func (User) TableName() string { + return "t_user" +} diff --git a/model/user_role.go b/model/user_role.go new file mode 100644 index 0000000..7e85fb9 --- /dev/null +++ b/model/user_role.go @@ -0,0 +1,11 @@ +package model + +import "gorm.io/gorm" + +type UserRole struct { + gorm.Model +} + +func (UserRole) TableName() string { + return "t_user_role" +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..d131c88 --- /dev/null +++ b/readme.md @@ -0,0 +1,5 @@ +### go_api_tmpl + +### 我是谁 + +基于`Gin`、`Casbin`的权限框架的模板 diff --git a/repository/apis_repository.go b/repository/apis_repository.go new file mode 100644 index 0000000..58686b6 --- /dev/null +++ b/repository/apis_repository.go @@ -0,0 +1,35 @@ +package repository + +import ( + "github.com/gin-gonic/gin" + . "go_api_tmpl/global" + "go_api_tmpl/model" +) + +type apisRepository struct{} + +type ApisRepository interface { + SaveAllApi(info gin.RoutesInfo) + Migrate() error +} + +// NewApisRepository 新建实例 +func NewApisRepository() ApisRepository { + return apisRepository{} +} + +// SaveAllApi 保存所有gin的路由 +func (ar apisRepository) SaveAllApi(routes gin.RoutesInfo) { + for _, route := range routes { + m := model.Apis{Path: route.Path, Method: route.Method} + err := MySQLConn.FirstOrCreate(&m, &m).Error + if err != nil { + Log.Infof("保存菜单错误: %v", err) + } + } +} + +// Migrate 初始化数据库结构 +func (ar apisRepository) Migrate() error { + return MySQLConn.AutoMigrate(&model.Apis{}) +} diff --git a/repository/menu_repository.go b/repository/menu_repository.go new file mode 100644 index 0000000..a6d59c2 --- /dev/null +++ b/repository/menu_repository.go @@ -0,0 +1,22 @@ +package repository + +import ( + "go_api_tmpl/global" + "go_api_tmpl/model" +) + +type menuRepository struct{} + +type MenuRepository interface { + Migrate() error +} + +// NewMenuRepository 新建实例 +func NewMenuRepository() MenuRepository { + return menuRepository{} +} + +// Migrate 初始化数据库结构 +func (r menuRepository) Migrate() error { + return global.MySQLConn.AutoMigrate(&model.Menu{}) +} diff --git a/repository/request_log_repository.go b/repository/request_log_repository.go new file mode 100644 index 0000000..e4fbdab --- /dev/null +++ b/repository/request_log_repository.go @@ -0,0 +1,30 @@ +package repository + +import ( + "go_api_tmpl/global" + "go_api_tmpl/model" +) + +type requestLogRepository struct{} + +type RequestLogRepository interface { + SaveLog(log *model.RequestLog) + Migrate() error +} + +// NewRequestLogRepository 新建实例 +func NewRequestLogRepository() RequestLogRepository { + return requestLogRepository{} +} + +// SaveLog 保存接口访问记录 +func (r requestLogRepository) SaveLog(log *model.RequestLog) { + if err := global.MySQLConn.Create(log).Error; err != nil { + global.Log.Errorf("接口访问记录保存错误: %v", err) + } +} + +// Migrate 初始化数据库结构 +func (r requestLogRepository) Migrate() error { + return global.MySQLConn.AutoMigrate(&model.RequestLog{}) +} diff --git a/repository/role_menu_repository.go b/repository/role_menu_repository.go new file mode 100644 index 0000000..1a5d9e8 --- /dev/null +++ b/repository/role_menu_repository.go @@ -0,0 +1,22 @@ +package repository + +import ( + "go_api_tmpl/global" + "go_api_tmpl/model" +) + +type roleMenuRepository struct{} + +type RoleMenuRepository interface { + Migrate() error +} + +// NewRoleMenuRepository 新建实例 +func NewRoleMenuRepository() RoleMenuRepository { + return roleMenuRepository{} +} + +// Migrate 初始化数据库结构 +func (r roleMenuRepository) Migrate() error { + return global.MySQLConn.AutoMigrate(&model.RoleMenu{}) +} diff --git a/repository/role_repository.go b/repository/role_repository.go new file mode 100644 index 0000000..b8f86eb --- /dev/null +++ b/repository/role_repository.go @@ -0,0 +1,22 @@ +package repository + +import ( + "go_api_tmpl/global" + "go_api_tmpl/model" +) + +type roleRepository struct{} + +type RoleRepository interface { + Migrate() error +} + +// NewRoleRepository 新建实例 +func NewRoleRepository() RoleRepository { + return roleRepository{} +} + +// Migrate 初始化数据库结构 +func (r roleRepository) Migrate() error { + return global.MySQLConn.AutoMigrate(&model.Role{}) +} diff --git a/repository/user_repository.go b/repository/user_repository.go new file mode 100644 index 0000000..c96ffab --- /dev/null +++ b/repository/user_repository.go @@ -0,0 +1,33 @@ +package repository + +import ( + "go_api_tmpl/global" + "go_api_tmpl/model" +) + +type userRepository struct{} + +type UserRepository interface { + Migrate() error + GetUserByPhone(string) (*model.User, error) +} + +// NewUserRepository 新建实例 +func NewUserRepository() UserRepository { + return userRepository{} +} + +// Migrate 初始化数据库结构 +func (r userRepository) Migrate() error { + return global.MySQLConn.AutoMigrate(&model.User{}) +} + +// GetUserByPhone 根据手机号查询用户信息 +func (r userRepository) GetUserByPhone(phone string) (*model.User, error) { + u := model.User{Phone: phone} + err := global.MySQLConn.FirstOrCreate(&u, &u).Error + if err != nil { + return nil, err + } + return &u, nil +} diff --git a/repository/user_role_repository.go b/repository/user_role_repository.go new file mode 100644 index 0000000..423132f --- /dev/null +++ b/repository/user_role_repository.go @@ -0,0 +1,22 @@ +package repository + +import ( + "go_api_tmpl/global" + "go_api_tmpl/model" +) + +type userRoleRepository struct{} + +type UserRoleRepository interface { + Migrate() error +} + +// NewUserRoleRepository 新建实例 +func NewUserRoleRepository() UserRoleRepository { + return userRoleRepository{} +} + +// Migrate 初始化数据库结构 +func (r userRoleRepository) Migrate() error { + return global.MySQLConn.AutoMigrate(&model.UserRole{}) +} diff --git a/route/login.go b/route/login.go new file mode 100644 index 0000000..4c956ac --- /dev/null +++ b/route/login.go @@ -0,0 +1,14 @@ +package route + +import ( + "github.com/gin-gonic/gin" + "go_api_tmpl/controller" +) + +// 初始化登录相关接口路由 +func initLoginRoute(r *gin.RouterGroup) { + // 获取短信验证码 + r.POST("/sms", controller.GetSmsCodeController) + // 登录 + r.POST("/login", controller.LoginController) +} diff --git a/route/route.go b/route/route.go new file mode 100644 index 0000000..c2debb2 --- /dev/null +++ b/route/route.go @@ -0,0 +1,17 @@ +package route + +import ( + "github.com/gin-gonic/gin" + "go_api_tmpl/core" +) + +func InitRoute(app *gin.Engine) { + app.GET("/", func(context *gin.Context) { + core.R(context).OkWithMessage("瞅啥呢?") + }) + group := app.Group("/api") + // 登录相关接口路由 + initLoginRoute(group) + // 测试用的 + initTestRoute(group) +} diff --git a/route/test.go b/route/test.go new file mode 100644 index 0000000..73b6f45 --- /dev/null +++ b/route/test.go @@ -0,0 +1,19 @@ +package route + +import ( + "github.com/gin-gonic/gin" + "go_api_tmpl/core" + "go_api_tmpl/middleware" +) + +func initTestRoute(r *gin.RouterGroup) { + group := r.Group("/test", middleware.AuthorizeJWT()) + + group.GET("/pub", func(context *gin.Context) { + core.R(context).OkWithMessage("已登录且无需权限可访问") + }) + + group.GET("/hello", middleware.AuthorityVerify(), func(context *gin.Context) { + core.R(context).OkWithMessage("已登录且有权限") + }) +} diff --git a/utils/bcrypt.go b/utils/bcrypt.go new file mode 100644 index 0000000..d5d05b2 --- /dev/null +++ b/utils/bcrypt.go @@ -0,0 +1,49 @@ +package utils + +import ( + "fmt" + "github.com/golang-jwt/jwt" + "golang.org/x/crypto/bcrypt" + "os" + "time" +) + +func HashPassword(pass *string) { + bytePass := []byte(*pass) + hPass, _ := bcrypt.GenerateFromPassword(bytePass, bcrypt.DefaultCost) + *pass = string(hPass) +} + +func ComparePassword(dbPass, pass string) bool { + return bcrypt.CompareHashAndPassword([]byte(dbPass), []byte(pass)) == nil +} + +//GenerateToken -> generates token +func GenerateToken(userId uint) string { + claims := jwt.MapClaims{ + "exp": time.Now().Add(time.Hour * 3).Unix(), + "iat": time.Now().Unix(), + "userId": userId, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + t, _ := token.SignedString([]byte(os.Getenv("JWT_SECRET"))) + // 保存Token到Redis + //redisKey := fmt.Sprintf("auth:token:%v", userId) + //_ = global.RedisConn.SetWithTimeout(redisKey, t, "10800") + return t + +} + +//ValidateToken --> validate the given token +func ValidateToken(token string) (*jwt.Token, error) { + + //2nd arg function return secret key after checking if the signing method is HMAC and returned key is used by 'Parse' to decode the token) + return jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + //nil secret key + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(os.Getenv("JWT_SECRET")), nil + }) +}