This commit is contained in:
新亮
2021-01-09 20:10:13 +08:00
parent f4072dc5fb
commit f2cd04993f
56 changed files with 2239 additions and 567 deletions

View File

@@ -6,6 +6,7 @@
╚██████╔╝╚██████╔╝ ╚██████╔╝██║██║ ╚████║ ██║ ██║██║ ██║
╚═════╝ ╚═════╝ ╚═════╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═╝╚═╝ ╚═╝
* [register -env fat]
* [register cors]
* [register rate]
* [register panic notify]
@@ -18,12 +19,41 @@
基于 [Gin](https://github.com/gin-gonic/gin) 进行模块化设计的 API 框架,封装了常用的功能,使用简单,致力于进行快速的业务研发,同时增加了更多限制,以约束项目组开发成员、规避混乱无序和自由随意。
亮点功能:
- :star: [trace] 开发调试的辅助工具。
可记录如下信息:
- trace_id当前请求的唯一ID
- request当前请求的请求信息
- response当前请求的返回信息
- third_party_requests当前请求涉及到调用第三方的信息
- debugs当前请求的调试信息
- sqls当前请求执行的 sql 信息
- success当前请求结果
- cost_seconds执行时长单位秒
供参考学习,线上使用请谨慎!
**查看 Jaeger 链路追踪代码,请查看 [v1.0版](https://github.com/xinliangnote/go-gin-api/releases/tag/v1.0),文档 [jaeger.md](https://github.com/xinliangnote/go-gin-api/blob/master/docs/jaeger.md) 点这里**
持续更新...
## Catalogue
```cassandraql
├── cmd # 项目入口文件api/main.go 为启动 HTTP API 服务
├── configs # 配置文件统一存放目录
├── docs # Swagger 文档,执行 swag init 生成的
├── init # 项目初始脚本,比如初始化 SQL 语句
├── internal # 业务目录
│ ├── api # 业务代码
│ ├── core # 脚本代码
│ ├── pkg # 内部使用的 package
├── logs # 存放日志的目录
└── pkg # 一些封装好的 package
```
## Features
- [x] 包管理工具 [Go Modules](https://github.com/golang/go/wiki/Modules)
@@ -31,19 +61,20 @@
- [x] 配置文件解析库 [Viper](https://github.com/spf13/viper)
- [x] 文档使用 [Swagger](https://swagger.io/) 生成
- [x] 性能分析使用 [pprof](github.com/gin-contrib/pprof)
- [x] 集成
- [x] [JWT](https://jwt.io/) 身份认证
- [x] [zap](go.uber.org/zap) 日志记录
- [x] [rate](golang.org/x/time/rate) 限流
- [x] 异常捕获并邮件告警
- [x] 每个请求具备链路ID
- [x] 统一定义错误码
- [x] 支持 FAT、UAT、PRO 环境
- [x] MD5、AES 对称加密、RSA 非对称加密
- [ ] 存储
- [ ] MySQL
- [ ] Redis
- [ ] MongoDB
- [x] [zap](go.uber.org/zap) 日志记录
- [x] [rate](golang.org/x/time/rate) 限流
- [x] [token] 基于[JWT](github.com/dgrijalva/jwt-go) 身份认证
- [x] [notify] 异常捕获并进行邮件告警
- [x] [trace] 开发调试的辅助工具
- [x] [errno] 统一定义错误码
- [x] [env] 支持 FAT、UAT、PRO 环境
- [x] [aes] AES 对称加密
- [x] [rsa] RSA 非对称加密
- [x] 数据库组件使用 [GORM V2](gorm.io/gorm)
- [x] Redis 组件使用 [go-redis](https://github.com/go-redis/redis)
- [ ] MongoDB
- [ ] Prometheus
- [ ] 任务调度
- [ ] gRPC
- [ ] ...
@@ -51,7 +82,7 @@
#### Requirements
- Go version >= 1.12
- Go version >= 1.15
- Global environment configure (Linux/Mac)
```
@@ -59,12 +90,41 @@ export GO111MODULE=on
export GOPROXY=https://goproxy.io
```
#### Environment
```
-env fat
// dev 开发环境
// fat 测试环境[默认]
// uat 预发布环境
// pro 正式环境
```
#### Configs
配置文件目录:`./configs`,根据不同的环境变量使用不同的配置文件。
项目启动时,需配置如下配置:
- MySQL 配置,主、从和基础项;
- Redis 配置;
- Mail 配置;
#### Init
项目初始化目录:`./init`
- db/tables.sql初始化 MySQL 表结构;
#### Build & Run
```
cd go-gin-api
go run main.go
// 启动成功后可访问http://127.0.0.1:9999/h/info
```
#### swagger

1
cmd/api/README.md Normal file
View File

@@ -0,0 +1 @@
## HTTP API 入口文件

View File

@@ -6,8 +6,10 @@ import (
"net/http"
"time"
"github.com/xinliangnote/go-gin-api/internal/pkg/configs"
"github.com/xinliangnote/go-gin-api/internal/router"
"github.com/xinliangnote/go-gin-api/configs"
"github.com/xinliangnote/go-gin-api/internal/api/repository/cache_repo"
"github.com/xinliangnote/go-gin-api/internal/api/repository/db_repo"
"github.com/xinliangnote/go-gin-api/internal/api/router"
"github.com/xinliangnote/go-gin-api/pkg/logger"
"github.com/xinliangnote/go-gin-api/pkg/shutdown"
@@ -25,6 +27,7 @@ import (
// @host 127.0.0.1:9999
// @BasePath
func main() {
// 初始化日志
loggers, err := logger.NewJSONLogger(
logger.WithField("domain", configs.ProjectName()),
logger.WithTimeLayout("2006-01-02 15:04:05"),
@@ -35,7 +38,20 @@ func main() {
}
defer loggers.Sync()
mux, err := router.NewHTTPMux(loggers)
// 初始化数据库
dbRepo, err := db_repo.New()
if err != nil {
loggers.Fatal("new db err", zap.Error(err))
}
// 初始化缓存
cacheRepo, err := cache_repo.New()
if err != nil {
loggers.Fatal("new cache err", zap.Error(err))
}
// 初始化 HTTP 服务
mux, err := router.NewHTTPMux(loggers, dbRepo, cacheRepo)
if err != nil {
panic(err)
}
@@ -51,6 +67,7 @@ func main() {
}
}()
// 优雅关闭
shutdown.NewHook().Close(func() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()

1
cmd/cron/README.md Normal file
View File

@@ -0,0 +1 @@
## 定时任务入口文件

11
cmd/cron/main.go Normal file
View File

@@ -0,0 +1,11 @@
package main
import (
"fmt"
"github.com/xinliangnote/go-gin-api/pkg/env"
)
func main() {
fmt.Println(env.Active().Value())
}

91
configs/configs.go Normal file
View File

@@ -0,0 +1,91 @@
package configs
import (
"time"
"github.com/xinliangnote/go-gin-api/pkg/env"
"github.com/spf13/viper"
)
var config = new(Config)
type Config struct {
MySQL struct {
Read struct {
Addr string `toml:"addr"`
User string `toml:"user"`
Pass string `toml:"pass"`
Name string `toml:"name"`
} `toml:"read"`
Write struct {
Addr string `toml:"addr"`
User string `toml:"user"`
Pass string `toml:"pass"`
Name string `toml:"name"`
} `toml:"write"`
Base struct {
MaxOpenConn int `toml:"maxOpenConn"`
MaxIdleConn int `toml:"maxIdleConn"`
ConnMaxLifeTime time.Duration `toml:"connMaxLifeTime"`
} `toml:"base"`
} `toml:"mysql"`
Redis struct {
Addr string `toml:"addr"`
Pass string `toml:"pass"`
Db int `toml:"db"`
MaxRetries int `toml:"maxRetries"`
PoolSize int `toml:"poolSize"`
MinIdleConns int `toml:"minIdleConns"`
} `toml:"redis"`
Mail struct {
Host string `toml:"host"`
Port int `toml:"port"`
User string `toml:"user"`
Pass string `toml:"pass"`
To string `toml:"to"`
} `toml:"mail"`
JWT struct {
Secret string `toml:"secret"`
} `toml:"jwt"`
Aes struct {
Key string `toml:"key"`
Iv string `toml:"iv"`
} `toml:"aes"`
Rsa struct {
Private string `toml:"private"`
Public string `toml:"public"`
} `toml:"rsa"`
}
func init() {
viper.SetConfigName(env.Active().Value() + "_configs")
viper.SetConfigType("toml")
viper.AddConfigPath("./configs")
viper.AddConfigPath("../../configs") // 兼容 cmd/cron/main.go 引用配置文件
if err := viper.ReadInConfig(); err != nil {
panic(err)
}
if err := viper.Unmarshal(config); err != nil {
panic(err)
}
}
func Get() Config {
return *config
}
func ProjectName() string {
return "go-gin-api"
}
func ProjectVersion() string {
return "v2.0"
}

View File

@@ -1,3 +1,28 @@
[mysql]
[mysql.read]
addr = ''
user = ''
pass = ''
name = ''
[mysql.write]
addr = ''
user = ''
pass = ''
name = ''
[mysql.base]
maxOpenConn = 10
maxIdleConn = 60
connMaxLifeTime = 60
[redis]
addr = ''
pass = ''
db = 0
maxRetries = 3
poolSize = 10
minIdleConns = 5
[mail]
host = 'smtp.163.com'
port = 465

View File

@@ -76,9 +76,9 @@ var doc = `{
}
}
},
"/user/login": {
"/user/create": {
"post": {
"description": "登录获取 Authorization 码",
"description": "创建用户",
"consumes": [
"application/json"
],
@@ -88,23 +88,123 @@ var doc = `{
"tags": [
"Demo"
],
"summary": "登录获取 Authorization 码",
"summary": "创建用户",
"parameters": [
{
"description": "请求信息",
"name": "loginRequest",
"name": "RequestInfo",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/demo.loginRequest"
"$ref": "#/definitions/user_model.CreateRequest"
}
}
],
"responses": {
"200": {
"description": "签名信息",
"description": "返回信息",
"schema": {
"$ref": "#/definitions/demo.loginResponse"
"$ref": "#/definitions/user_model.CreateResponse"
}
}
}
}
},
"/user/info/{username}": {
"get": {
"description": "用户详情",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Demo"
],
"summary": "用户详情",
"parameters": [
{
"type": "string",
"description": "用户名",
"name": "username",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "返回信息",
"schema": {
"$ref": "#/definitions/user_model.DetailResponse"
}
}
}
}
},
"/user/login": {
"post": {
"description": "用户登录",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Demo"
],
"summary": "用户登录",
"parameters": [
{
"description": "请求信息",
"name": "RequestInfo",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/user_model.LoginRequest"
}
}
],
"responses": {
"200": {
"description": "返回信息",
"schema": {
"$ref": "#/definitions/user_model.LoginResponse"
}
}
}
}
},
"/user/update": {
"post": {
"description": "更新用户名称",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Demo"
],
"summary": "更新用户名称",
"parameters": [
{
"description": "请求信息",
"name": "RequestInfo",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/user_model.UpdateNickNameByIDRequest"
}
}
],
"responses": {
"200": {
"description": "返回信息",
"schema": {
"$ref": "#/definitions/user_model.UpdateNickNameByIDResponse"
}
}
}
@@ -112,7 +212,54 @@ var doc = `{
}
},
"definitions": {
"demo.loginRequest": {
"user_model.CreateRequest": {
"type": "object",
"properties": {
"mobile": {
"description": "手机号",
"type": "string"
},
"nick_name": {
"description": "昵称",
"type": "string"
},
"user_name": {
"description": "用户名",
"type": "string"
}
}
},
"user_model.CreateResponse": {
"type": "object",
"properties": {
"id": {
"description": "主键ID",
"type": "integer"
}
}
},
"user_model.DetailResponse": {
"type": "object",
"properties": {
"id": {
"description": "用户主键ID",
"type": "integer"
},
"mobile": {
"description": "手机号",
"type": "string"
},
"nick_name": {
"description": "昵称",
"type": "string"
},
"user_name": {
"description": "用户名",
"type": "string"
}
}
},
"user_model.LoginRequest": {
"type": "object",
"properties": {
"user_id": {
@@ -125,12 +272,38 @@ var doc = `{
}
}
},
"demo.loginResponse": {
"user_model.LoginResponse": {
"type": "object",
"properties": {
"authorization": {
"description": "签名",
"type": "string"
},
"expire_time": {
"description": "过期时间",
"type": "integer"
}
}
},
"user_model.UpdateNickNameByIDRequest": {
"type": "object",
"properties": {
"id": {
"description": "用户主键ID",
"type": "integer"
},
"nick_name": {
"description": "昵称",
"type": "string"
}
}
},
"user_model.UpdateNickNameByIDResponse": {
"type": "object",
"properties": {
"id": {
"description": "用户主键ID",
"type": "integer"
}
}
}

View File

@@ -58,9 +58,9 @@
}
}
},
"/user/login": {
"/user/create": {
"post": {
"description": "登录获取 Authorization 码",
"description": "创建用户",
"consumes": [
"application/json"
],
@@ -70,23 +70,123 @@
"tags": [
"Demo"
],
"summary": "登录获取 Authorization 码",
"summary": "创建用户",
"parameters": [
{
"description": "请求信息",
"name": "loginRequest",
"name": "RequestInfo",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/demo.loginRequest"
"$ref": "#/definitions/user_model.CreateRequest"
}
}
],
"responses": {
"200": {
"description": "签名信息",
"description": "返回信息",
"schema": {
"$ref": "#/definitions/demo.loginResponse"
"$ref": "#/definitions/user_model.CreateResponse"
}
}
}
}
},
"/user/info/{username}": {
"get": {
"description": "用户详情",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Demo"
],
"summary": "用户详情",
"parameters": [
{
"type": "string",
"description": "用户名",
"name": "username",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "返回信息",
"schema": {
"$ref": "#/definitions/user_model.DetailResponse"
}
}
}
}
},
"/user/login": {
"post": {
"description": "用户登录",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Demo"
],
"summary": "用户登录",
"parameters": [
{
"description": "请求信息",
"name": "RequestInfo",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/user_model.LoginRequest"
}
}
],
"responses": {
"200": {
"description": "返回信息",
"schema": {
"$ref": "#/definitions/user_model.LoginResponse"
}
}
}
}
},
"/user/update": {
"post": {
"description": "更新用户名称",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Demo"
],
"summary": "更新用户名称",
"parameters": [
{
"description": "请求信息",
"name": "RequestInfo",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/user_model.UpdateNickNameByIDRequest"
}
}
],
"responses": {
"200": {
"description": "返回信息",
"schema": {
"$ref": "#/definitions/user_model.UpdateNickNameByIDResponse"
}
}
}
@@ -94,7 +194,54 @@
}
},
"definitions": {
"demo.loginRequest": {
"user_model.CreateRequest": {
"type": "object",
"properties": {
"mobile": {
"description": "手机号",
"type": "string"
},
"nick_name": {
"description": "昵称",
"type": "string"
},
"user_name": {
"description": "用户名",
"type": "string"
}
}
},
"user_model.CreateResponse": {
"type": "object",
"properties": {
"id": {
"description": "主键ID",
"type": "integer"
}
}
},
"user_model.DetailResponse": {
"type": "object",
"properties": {
"id": {
"description": "用户主键ID",
"type": "integer"
},
"mobile": {
"description": "手机号",
"type": "string"
},
"nick_name": {
"description": "昵称",
"type": "string"
},
"user_name": {
"description": "用户名",
"type": "string"
}
}
},
"user_model.LoginRequest": {
"type": "object",
"properties": {
"user_id": {
@@ -107,12 +254,38 @@
}
}
},
"demo.loginResponse": {
"user_model.LoginResponse": {
"type": "object",
"properties": {
"authorization": {
"description": "签名",
"type": "string"
},
"expire_time": {
"description": "过期时间",
"type": "integer"
}
}
},
"user_model.UpdateNickNameByIDRequest": {
"type": "object",
"properties": {
"id": {
"description": "用户主键ID",
"type": "integer"
},
"nick_name": {
"description": "昵称",
"type": "string"
}
}
},
"user_model.UpdateNickNameByIDResponse": {
"type": "object",
"properties": {
"id": {
"description": "用户主键ID",
"type": "integer"
}
}
}

View File

@@ -1,5 +1,38 @@
definitions:
demo.loginRequest:
user_model.CreateRequest:
properties:
mobile:
description: 手机号
type: string
nick_name:
description: 昵称
type: string
user_name:
description: 用户名
type: string
type: object
user_model.CreateResponse:
properties:
id:
description: 主键ID
type: integer
type: object
user_model.DetailResponse:
properties:
id:
description: 用户主键ID
type: integer
mobile:
description: 手机号
type: string
nick_name:
description: 昵称
type: string
user_name:
description: 用户名
type: string
type: object
user_model.LoginRequest:
properties:
user_id:
description: 用户ID>0
@@ -8,11 +41,29 @@ definitions:
description: 用户名
type: string
type: object
demo.loginResponse:
user_model.LoginResponse:
properties:
authorization:
description: 签名
type: string
expire_time:
description: 过期时间
type: integer
type: object
user_model.UpdateNickNameByIDRequest:
properties:
id:
description: 用户主键ID
type: integer
nick_name:
description: 昵称
type: string
type: object
user_model.UpdateNickNameByIDResponse:
properties:
id:
description: 用户主键ID
type: integer
type: object
host: 127.0.0.1:9999
info:
@@ -54,26 +105,91 @@ paths:
summary: 获取用户信息
tags:
- Demo
/user/login:
/user/create:
post:
consumes:
- application/json
description: 登录获取 Authorization 码
description: 创建用户
parameters:
- description: 请求信息
in: body
name: loginRequest
name: RequestInfo
required: true
schema:
$ref: '#/definitions/demo.loginRequest'
$ref: '#/definitions/user_model.CreateRequest'
produces:
- application/json
responses:
"200":
description: 签名信息
description: 返回信息
schema:
$ref: '#/definitions/demo.loginResponse'
summary: 登录获取 Authorization 码
$ref: '#/definitions/user_model.CreateResponse'
summary: 创建用户
tags:
- Demo
/user/info/{username}:
get:
consumes:
- application/json
description: 用户详情
parameters:
- description: 用户名
in: path
name: username
required: true
type: string
produces:
- application/json
responses:
"200":
description: 返回信息
schema:
$ref: '#/definitions/user_model.DetailResponse'
summary: 用户详情
tags:
- Demo
/user/login:
post:
consumes:
- application/json
description: 用户登录
parameters:
- description: 请求信息
in: body
name: RequestInfo
required: true
schema:
$ref: '#/definitions/user_model.LoginRequest'
produces:
- application/json
responses:
"200":
description: 返回信息
schema:
$ref: '#/definitions/user_model.LoginResponse'
summary: 用户登录
tags:
- Demo
/user/update:
post:
consumes:
- application/json
description: 更新用户名称
parameters:
- description: 请求信息
in: body
name: RequestInfo
required: true
schema:
$ref: '#/definitions/user_model.UpdateNickNameByIDRequest'
produces:
- application/json
responses:
"200":
description: 返回信息
schema:
$ref: '#/definitions/user_model.UpdateNickNameByIDResponse'
summary: 更新用户名称
tags:
- Demo
swagger: "2.0"

10
go.mod
View File

@@ -1,13 +1,19 @@
module github.com/xinliangnote/go-gin-api
go 1.12
go 1.15
require (
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
github.com/davecgh/go-spew v1.1.1
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gin-contrib/pprof v1.2.1
github.com/gin-gonic/gin v1.6.3
github.com/go-openapi/spec v0.20.0 // indirect
github.com/go-redis/redis/v7 v7.4.0
github.com/google/go-cmp v0.5.4 // indirect
github.com/jinzhu/gorm v1.9.16
github.com/onsi/ginkgo v1.14.2 // indirect
github.com/onsi/gomega v1.10.4 // indirect
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v0.9.3
github.com/rs/cors v1.7.0
@@ -26,4 +32,6 @@ require (
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/natefinch/lumberjack.v2 v2.0.0
gorm.io/driver/mysql v1.0.3
gorm.io/gorm v1.20.9
)

90
go.sum
View File

@@ -17,6 +17,7 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
@@ -26,6 +27,7 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
@@ -34,6 +36,7 @@ github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
@@ -41,17 +44,24 @@ github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/gzip v0.0.1 h1:ezvKOL6jH+jlzdHNE4h9h8q8uMpDQjyl0NN0Jd7jozc=
github.com/gin-contrib/gzip v0.0.1/go.mod h1:fGBJBCdt6qCZuCAOwWuFhBB4OOq9EFqlo5dEaFhhu5w=
@@ -95,9 +105,15 @@ github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD87
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/go-redis/redis/v7 v7.4.0 h1:7obg6wUoj05T0EpY0o8B59S9w5yeMWql7sw2kwNW1x4=
github.com/go-redis/redis/v7 v7.4.0/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
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/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
@@ -108,10 +124,21 @@ github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
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/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
@@ -146,6 +173,15 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o=
github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
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.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E=
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
@@ -170,6 +206,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
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.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
@@ -184,6 +222,8 @@ github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
@@ -203,7 +243,19 @@ github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M=
github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.10.4 h1:NiTx7EEvBzu9sFOD1zORteLSt3o8gnlvZZwSE9TnY9U=
github.com/onsi/gomega v1.10.4/go.mod h1:g/HbgYopi++010VEqkFgJHKC09uJiW9UkXvMUuKHUCQ=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
@@ -231,9 +283,11 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
@@ -282,7 +336,9 @@ github.com/ugorji/go/codec v0.0.0-20181022190402-e5e69e061d4f/go.mod h1:VFNgLljT
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.1.13 h1:013LbFhocBoIqgHeIHKlV4JWYhqogATYWZhIcH0WHn4=
github.com/ugorji/go/codec v1.1.13/go.mod h1:oNVt3Dq+FO91WNQ/9JnHKQP2QJxTzoN7wCBFCq1OeuU=
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -303,9 +359,12 @@ go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/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/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -329,8 +388,10 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0 h1:8pl+sMODzuvGJkmj2W4kZihvVb5mKm8pB/X44PIQHv8=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -346,8 +407,13 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -362,6 +428,7 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -375,7 +442,13 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201223074533-0d417f636930 h1:vRgIt+nup/B/BwIS0g2oC0haq0iqbV3ZA+u6+0TlNCo=
@@ -418,6 +491,7 @@ golang.org/x/tools v0.0.0-20201226215659-b1c90890d22a h1:pdfjQ7VswBeGam3EpuEJ4e8
golang.org/x/tools v0.0.0-20201226215659-b1c90890d22a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
@@ -441,15 +515,24 @@ google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvx
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
@@ -459,6 +542,8 @@ gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -471,6 +556,11 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.0.3 h1:+JKBYPfn1tygR1/of/Fh2T8iwuVwzt+PEJmKaXzMQXg=
gorm.io/driver/mysql v1.0.3/go.mod h1:twGxftLBlFgNVNakL7F+P/x9oYqoymG3YYT8cAfI9oI=
gorm.io/gorm v1.20.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v1.20.9 h1:M3aIZKXAC1PtPVu9t3WGwkBTE1le5c2telz3I/qjRNg=
gorm.io/gorm v1.20.9/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

5
init/README.md Normal file
View File

@@ -0,0 +1,5 @@
## init
项目初始脚本。
- DB 相关 SQL

10
init/db/tables.sql Normal file
View File

@@ -0,0 +1,10 @@
CREATE TABLE `user_demo` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_name` varchar(32) NOT NULL DEFAULT '' COMMENT '用户名',
`nick_name` varchar(100) NOT NULL DEFAULT '' COMMENT '昵称',
`mobile` varchar(20) NOT NULL DEFAULT '' COMMENT '手机号',
`is_deleted` tinyint(1) NOT NULL DEFAULT '-1' COMMENT '是否删除 1:是 -1:否',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户Demo表';

View File

@@ -1,7 +1,7 @@
## 错误码规则
- 错误码需在 `code.go` 文件中定义。
- 错误码需为 > 0 的数,反之表示正确。
- 错误码需为 > 1 的数,反之表示正确。
#### 错误码为 5 位数

28
internal/api/code/code.go Normal file
View File

@@ -0,0 +1,28 @@
package code
import (
"net/http"
"github.com/xinliangnote/go-gin-api/pkg/errno"
)
var (
// OK
OK = errno.NewError(1, "OK")
// 服务级错误码
ErrServer = errno.NewError(10101, http.StatusText(http.StatusInternalServerError))
ErrManyRequest = errno.NewError(10102, "Too many requests")
ErrParam = errno.NewError(10110, "参数有误")
ErrSignParam = errno.NewError(10111, "缺少签名")
ErrSign = errno.NewError(10112, "签名有误")
// 模块级错误码 - 用户模块
ErrUser = errno.NewError(20101, "非法用户")
ErrUserCreate = errno.NewError(20102, "创建用户失败")
ErrUserUpdate = errno.NewError(20103, "更新用户失败")
ErrUserSearch = errno.NewError(20104, "查询用户失败")
// ...
)

View File

@@ -0,0 +1,9 @@
## controller
控制器层。
主要接收参数、验证参数、调用 `service` 层的业务逻辑处理,最后返回数据。
命名规范:
- 包名以 `_handler` 结尾。

View File

@@ -1,18 +1,13 @@
package demo
import (
"fmt"
"net/url"
"time"
"github.com/xinliangnote/go-gin-api/internal/pkg/configs"
"github.com/xinliangnote/go-gin-api/internal/api/code"
"github.com/xinliangnote/go-gin-api/internal/pkg/core"
"github.com/xinliangnote/go-gin-api/internal/pkg/errno"
"github.com/xinliangnote/go-gin-api/internal/pkg/jsonparse"
"github.com/xinliangnote/go-gin-api/pkg/aes"
"github.com/xinliangnote/go-gin-api/pkg/httpclient"
"github.com/xinliangnote/go-gin-api/pkg/md5"
"github.com/xinliangnote/go-gin-api/pkg/rsa"
"go.uber.org/zap"
)
@@ -41,16 +36,16 @@ func (d *Demo) Get() core.HandlerFunc {
return func(c core.Context) {
req := new(request)
if err := c.ShouldBindURI(req); err != nil {
c.SetPayload(errno.ErrParam)
c.SetPayload(code.ErrParam)
return
}
if req.Name != "Tom" {
c.SetPayload(errno.ErrUser)
c.SetPayload(code.ErrUser)
return
}
c.SetPayload(errno.OK.WithData(&response{
c.SetPayload(code.OK.WithData(&response{
Name: "Tom",
Job: "Student",
}))
@@ -70,16 +65,16 @@ func (d *Demo) Post() core.HandlerFunc {
return func(c core.Context) {
req := new(request)
if err := c.ShouldBindPostForm(req); err != nil {
c.SetPayload(errno.ErrParam)
c.SetPayload(code.ErrParam)
return
}
if req.Name != "Jack" {
c.SetPayload(errno.ErrUser)
c.SetPayload(code.ErrUser)
return
}
c.SetPayload(errno.OK.WithData(&response{
c.SetPayload(code.OK.WithData(&response{
Name: "Jack",
Job: "Teacher",
}))
@@ -109,18 +104,18 @@ func (d *Demo) User() core.HandlerFunc {
return func(c core.Context) {
req := new(request)
if err := c.ShouldBindURI(req); err != nil {
c.SetPayload(errno.ErrParam)
c.SetPayload(code.ErrParam)
return
}
if req.Name != "Tom" {
c.SetPayload(errno.ErrUser)
c.SetPayload(code.ErrUser)
return
}
body1, err1 := httpclient.Get("http://127.0.0.1:9999/demo/get/"+req.Name, nil,
httpclient.WithTTL(time.Second*2),
httpclient.WithJournal(c.Journal()),
httpclient.WithTrace(c.Trace()),
httpclient.WithLogger(c.Logger()),
httpclient.WithHeader("Authorization", c.GetHeader("Authorization")),
)
@@ -132,7 +127,7 @@ func (d *Demo) User() core.HandlerFunc {
params.Set("name", "Jack")
body2, err2 := httpclient.PostForm("http://127.0.0.1:9999/demo/post", params,
httpclient.WithTTL(time.Second*2),
httpclient.WithJournal(c.Journal()),
httpclient.WithTrace(c.Trace()),
httpclient.WithLogger(c.Logger()),
httpclient.WithHeader("Authorization", c.GetHeader("Authorization")),
)
@@ -153,81 +148,6 @@ func (d *Demo) User() core.HandlerFunc {
},
}
}
c.SetPayload(errno.OK.WithData(data))
}
}
func (d *Demo) RsaTest() core.HandlerFunc {
return func(c core.Context) {
startTime := time.Now()
encryptStr := "param_1=xxx&param_2=xxx&ak=xxx&ts=1111111111"
count := 500
cfg := configs.Get().Rsa
rsaPublic := rsa.NewPublic(cfg.Public)
rsaPrivate := rsa.NewPrivate(cfg.Private)
for i := 0; i < count; i++ {
// 生成签名
sn, err := rsaPublic.Encrypt(encryptStr)
if err != nil {
d.logger.Error("rsa public encrypt err", zap.Error(err))
}
// 验证签名
_, err = rsaPrivate.Decrypt(sn)
if err != nil {
d.logger.Error("rsa private decrypt err", zap.Error(err))
}
}
c.SetPayload(errno.OK.
WithData(fmt.Sprintf("%v次 - %v", count, time.Since(startTime))),
)
}
}
func (d *Demo) AesTest() core.HandlerFunc {
return func(c core.Context) {
startTime := time.Now()
encryptStr := "param_1=xxx&param_2=xxx&ak=xxx&ts=1111111111"
count := 1000000
cfg := configs.Get().Aes
aes := aes.New(cfg.Key, cfg.Iv)
for i := 0; i < count; i++ {
// 生成签名
sn, err := aes.Encrypt(encryptStr)
if err != nil {
d.logger.Error("aes encrypt err", zap.Error(err))
}
// 验证签名
_, err = aes.Decrypt(sn)
if err != nil {
d.logger.Error("aes decrypt err", zap.Error(err))
}
}
c.SetPayload(errno.OK.
WithData(fmt.Sprintf("%v次 - %v", count, time.Since(startTime))))
}
}
func (d *Demo) MD5Test() core.HandlerFunc {
return func(c core.Context) {
startTime := time.Now()
encryptStr := "param_1=xxx&param_2=xxx&ak=xxx&ts=1111111111"
count := 1000000
md5 := md5.New()
for i := 0; i < count; i++ {
// 生成签名
md5.Encrypt(encryptStr)
// 验证签名
md5.Encrypt(encryptStr)
}
c.SetPayload(errno.OK.
WithData(fmt.Sprintf("%v次 - %v", count, time.Since(startTime))),
)
c.SetPayload(code.OK.WithData(data))
}
}

View File

@@ -1,47 +0,0 @@
package demo
import (
"github.com/xinliangnote/go-gin-api/internal/pkg/core"
"github.com/xinliangnote/go-gin-api/internal/pkg/errno"
"github.com/xinliangnote/go-gin-api/internal/pkg/token"
"go.uber.org/zap"
)
type loginRequest struct {
UserID int `json:"user_id" form:"user_id"` // 用户ID>0
UserName string `json:"user_name" form:"user_name"` // 用户名
}
type loginResponse struct {
Authorization string `json:"authorization"` // 签名
}
// 登录获取 Authorization 码
// @Summary 登录获取 Authorization 码
// @Description 登录获取 Authorization 码
// @Tags Demo
// @Accept json
// @Produce json
// @Param loginRequest body loginRequest true "请求信息"
// @Success 200 {object} loginResponse "签名信息"
// @Router /user/login [post]
func (d *Demo) Login() core.HandlerFunc {
return func(c core.Context) {
req := new(loginRequest)
res := new(loginResponse)
if err := c.ShouldBindJSON(req); err != nil {
c.SetPayload(errno.ErrParam)
return
}
tokenString, err := token.Sign(req.UserID, req.UserName)
if err != nil {
d.logger.Error("token sign err", zap.Error(err))
res.Authorization = ""
} else {
res.Authorization = tokenString
}
c.SetPayload(errno.OK.WithData(res))
}
}

View File

@@ -0,0 +1,180 @@
package user_handler
import (
"github.com/xinliangnote/go-gin-api/configs"
"github.com/xinliangnote/go-gin-api/internal/api/code"
"github.com/xinliangnote/go-gin-api/internal/api/model/user_model"
"github.com/xinliangnote/go-gin-api/internal/api/repository/cache_repo"
"github.com/xinliangnote/go-gin-api/internal/api/repository/db_repo"
"github.com/xinliangnote/go-gin-api/internal/api/service/user_service"
"github.com/xinliangnote/go-gin-api/internal/pkg/core"
"github.com/xinliangnote/go-gin-api/pkg/token"
"go.uber.org/zap"
)
var _ UserDemo = (*userDemo)(nil)
type UserDemo interface {
// i 为了避免被其他包实现
i()
// 创建用户
Create() core.HandlerFunc
// 通过用户主键ID更新用户昵称
UpdateNickNameByID() core.HandlerFunc
// 用户登录
Login() core.HandlerFunc
// 用户详情
Detail() core.HandlerFunc
}
type userDemo struct {
logger *zap.Logger
userService user_service.UserService
}
func NewUserDemo(logger *zap.Logger, db db_repo.Repo, cache cache_repo.Repo) UserDemo {
return &userDemo{
logger: logger,
userService: user_service.NewUserService(db, cache),
}
}
func (u *userDemo) i() {}
// 创建用户
// @Summary 创建用户
// @Description 创建用户
// @Tags Demo
// @Accept json
// @Produce json
// @Param RequestInfo body user_model.CreateRequest true "请求信息"
// @Success 200 {object} user_model.CreateResponse "返回信息"
// @Router /user/create [post]
func (u *userDemo) Create() core.HandlerFunc {
return func(c core.Context) {
req := new(user_model.CreateRequest)
res := new(user_model.CreateResponse)
if err := c.ShouldBindJSON(req); err != nil {
u.logger.Error("[user] should bind json err", zap.Error(err))
c.SetPayload(code.ErrParam)
return
}
id, err := u.userService.Create(c, req)
if err != nil {
u.logger.Error("[user] Create err", zap.Error(err))
c.SetPayload(code.ErrUserCreate)
return
}
res.Id = id
c.SetPayload(code.OK.WithData(res))
}
}
// 更新用户名称
// @Summary 更新用户名称
// @Description 更新用户名称
// @Tags Demo
// @Accept json
// @Produce json
// @Param RequestInfo body user_model.UpdateNickNameByIDRequest true "请求信息"
// @Success 200 {object} user_model.UpdateNickNameByIDResponse "返回信息"
// @Router /user/update [post]
func (u *userDemo) UpdateNickNameByID() core.HandlerFunc {
return func(c core.Context) {
req := new(user_model.UpdateNickNameByIDRequest)
res := new(user_model.UpdateNickNameByIDResponse)
if err := c.ShouldBindJSON(req); err != nil {
u.logger.Error("[user] should bind json err", zap.Error(err))
c.SetPayload(code.ErrParam)
return
}
err := u.userService.UpdateNickNameByID(c, req.Id, req.NickName)
if err != nil {
u.logger.Error("[user] UpdateNickNameByID err", zap.Error(err))
c.SetPayload(code.ErrUserUpdate)
return
}
res.Id = req.Id
c.SetPayload(code.OK.WithData(res))
}
}
// 用户登录
// @Summary 用户登录
// @Description 用户登录
// @Tags Demo
// @Accept json
// @Produce json
// @Param RequestInfo body user_model.LoginRequest true "请求信息"
// @Success 200 {object} user_model.LoginResponse "返回信息"
// @Router /user/login [post]
func (u *userDemo) Login() core.HandlerFunc {
return func(c core.Context) {
req := new(user_model.LoginRequest)
res := new(user_model.LoginResponse)
if err := c.ShouldBindJSON(req); err != nil {
u.logger.Error("should bind json err", zap.Error(err))
c.SetPayload(code.ErrParam)
return
}
cfg := configs.Get().JWT
tokenString, err := token.New(cfg.Secret).Sign(req.UserID, req.UserName)
if err != nil {
u.logger.Error("token sign err", zap.Error(err))
c.SetPayload(code.ErrSign)
return
}
claims, err := token.New(cfg.Secret).Parse(tokenString)
if err != nil {
u.logger.Error("token parse err", zap.Error(err))
c.SetPayload(code.ErrSign)
return
}
res.Authorization = tokenString
res.ExpireTime = claims.ExpiresAt
c.SetPayload(code.OK.WithData(res))
}
}
// 用户详情
// @Summary 用户详情
// @Description 用户详情
// @Tags Demo
// @Accept json
// @Produce json
// @Param username path string true "用户名"
// @Success 200 {object} user_model.DetailResponse "返回信息"
// @Router /user/info/{username} [get]
func (u *userDemo) Detail() core.HandlerFunc {
return func(c core.Context) {
req := new(user_model.DetailRequest)
res := new(user_model.DetailResponse)
if err := c.ShouldBindURI(req); err != nil {
u.logger.Error("should bind uri err", zap.Error(err))
c.SetPayload(code.ErrParam)
return
}
user, err := u.userService.GetUserByUserName(c, req.UserName)
if err != nil {
u.logger.Error("[user] GetUserByUserName err", zap.Error(err))
c.SetPayload(code.ErrUserSearch)
return
}
res.Id = user.Id
res.UserName = user.UserName
res.NickName = user.NickName
res.Mobile = user.Mobile
c.SetPayload(code.OK.WithData(res))
}
}

View File

@@ -0,0 +1,10 @@
## model
实体层。
- 请求实体、返回实体。
- 数据库实体。
命名规范:
- 包名以 `_model` 结尾。

View File

@@ -0,0 +1,68 @@
package user_model
import (
"time"
)
// 用户Demo表
type UserDemo struct {
Id uint `gorm:"column:id;primary_key;AUTO_INCREMENT"` // 主键
UserName string `gorm:"column:user_name;NOT NULL"` // 用户名
NickName string `gorm:"column:nick_name;NOT NULL"` // 昵称
Mobile string `gorm:"column:mobile;NOT NULL"` // 手机号
IsDeleted int `gorm:"column:is_deleted;default:-1;NOT NULL"` // 是否删除 1:是 -1:否
CreatedAt time.Time `gorm:"column:created_at;default:CURRENT_TIMESTAMP;NOT NULL"` // 创建时间
UpdatedAt time.Time `gorm:"column:updated_at;default:CURRENT_TIMESTAMP;NOT NULL"` // 更新时间
}
func (m *UserDemo) TableName() string {
return "user_demo"
}
// user_handler Create Request
type CreateRequest struct {
UserName string `json:"user_name"` // 用户名
NickName string `json:"nick_name"` // 昵称
Mobile string `json:"mobile"` // 手机号
}
// user_handler Create Response
type CreateResponse struct {
Id uint `json:"id"` //主键ID
}
// user_handler UpdateNickNameByID Request
type UpdateNickNameByIDRequest struct {
Id uint `json:"id"` // 用户主键ID
NickName string `json:"nick_name"` // 昵称
}
// user_handler UpdateNickNameByID Response
type UpdateNickNameByIDResponse struct {
Id uint `json:"id"` // 用户主键ID
}
// user_handler Login Request
type LoginRequest struct {
UserID int `json:"user_id" form:"user_id"` // 用户ID>0
UserName string `json:"user_name" form:"user_name"` // 用户名
}
// user_handler Login Response
type LoginResponse struct {
Authorization string `json:"authorization"` // 签名
ExpireTime int64 `json:"expire_time"` // 过期时间
}
// user_handler Detail Request
type DetailRequest struct {
UserName string `uri:"username"` // 用户名
}
// user_handler Detail Response
type DetailResponse struct {
Id uint `json:"id"` // 用户主键ID
UserName string `json:"user_name"` // 用户名
NickName string `json:"nick_name"` // 昵称
Mobile string `json:"mobile"` // 手机号
}

View File

@@ -0,0 +1,29 @@
## repository
数据访问层。
- `./db_repo` 访问 DB 数据
- `./cache_repo` 访问 Cache 数据
- `./third_party_request` 访问外部 HTTP 接口数据。
SQL 建议:
- 禁止使用 SQL k v 拼接,好处是避免 SQL 注入;
- 禁止使用连表查询,好处是易扩展,比如分库分表;
- 禁止使用万能方法,好处是便于后期维护,比如字段调整;
- 禁止使用删除方法,好处是避免数据丢失;
- 建议每张表需包含字段:主键(id)、标记删除(is_deteled)、创建时间(created_at)、更新时间(updated_at)
```mysql
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`is_deleted` tinyint(1) NOT NULL DEFAULT '-1' COMMENT '是否删除 1:是 -1:否',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
```
什么是万能方法?
指的是特别灵活的查询,比如通过非固定的参数返回全部字段,建议做到需要什么返回什么,不要返回大而全的数据,更新时也不能传递什么参数更新什么参数,更新字段要提前约定好。
命名规范:
- 包名应以 `_repo` 结尾;
- `./db_repo` 目录下的包名以 `数据表名`+ `_repo` 命名;

View File

@@ -0,0 +1,120 @@
package cache_repo
import (
"time"
"github.com/xinliangnote/go-gin-api/configs"
"github.com/go-redis/redis/v7"
"github.com/pkg/errors"
)
var _ Repo = (*cacheRepo)(nil)
type Repo interface {
i()
Set(key, value string, ttl time.Duration) error
Get(key string) (string, error)
TTL(key string) (time.Duration, error)
Expire(key string, ttl time.Duration) bool
ExpireAt(key string, ttl time.Time) bool
Del(keys ...string) bool
Incr(key string) int64
Close()
}
type cacheRepo struct {
client *redis.Client
}
func New() (Repo, error) {
client, err := redisConnect()
if err != nil {
return nil, err
}
return &cacheRepo{
client: client,
}, nil
}
func (c *cacheRepo) i() {}
func redisConnect() (*redis.Client, error) {
cfg := configs.Get().Redis
client := redis.NewClient(&redis.Options{
Addr: cfg.Addr,
Password: cfg.Pass,
DB: cfg.Db,
MaxRetries: cfg.MaxRetries,
PoolSize: cfg.PoolSize,
MinIdleConns: cfg.MinIdleConns,
})
if err := client.Ping().Err(); err != nil {
return nil, errors.Wrap(err, "ping redis err")
}
return client, nil
}
// Set set some <key,value> into redis
func (c *cacheRepo) Set(key, value string, ttl time.Duration) error {
if err := c.client.Set(key, value, ttl).Err(); err != nil {
return errors.Wrapf(err, "redis set key: %s err", key)
}
return nil
}
// Get get some key from redis
func (c *cacheRepo) Get(key string) (string, error) {
value, err := c.client.Get(key).Result()
if err != nil {
return "", errors.Wrapf(err, "redis get key: %s err", key)
}
return value, nil
}
// TTL get some key from redis
func (c *cacheRepo) TTL(key string) (time.Duration, error) {
ttl, err := c.client.TTL(key).Result()
if err != nil {
return -1, errors.Wrapf(err, "redis get key: %s err", key)
}
return ttl, nil
}
// Expire expire some key
func (c *cacheRepo) Expire(key string, ttl time.Duration) bool {
ok, _ := c.client.Expire(key, ttl).Result()
return ok
}
// ExpireAt expire some key at some time
func (c *cacheRepo) ExpireAt(key string, ttl time.Time) bool {
ok, _ := c.client.ExpireAt(key, ttl).Result()
return ok
}
// Del del some key from redis
func (c *cacheRepo) Del(keys ...string) bool {
if len(keys) == 0 {
return true
}
value, _ := c.client.Del(keys...).Result()
return value > 0
}
func (c *cacheRepo) Incr(key string) int64 {
value, _ := c.client.Incr(key).Result()
return value
}
// Close close redis client
func (c *cacheRepo) Close() {
c.client.Close()
}

View File

@@ -0,0 +1,112 @@
package db_repo
import (
"fmt"
"time"
"github.com/xinliangnote/go-gin-api/configs"
"github.com/pkg/errors"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var _ Repo = (*dbRepo)(nil)
type Repo interface {
i()
GetDbR() *gorm.DB
GetDbW() *gorm.DB
DbRClose() error
DbWClose() error
}
type dbRepo struct {
DbR *gorm.DB
DbW *gorm.DB
}
func New() (Repo, error) {
cfg := configs.Get().MySQL
dbr, err := dbConnect(cfg.Read.User, cfg.Read.Pass, cfg.Read.Addr, cfg.Read.Name)
if err != nil {
return nil, err
}
dbw, err := dbConnect(cfg.Write.User, cfg.Write.Pass, cfg.Write.Addr, cfg.Write.Name)
if err != nil {
return nil, err
}
return &dbRepo{
DbR: dbr,
DbW: dbw,
}, nil
}
func (d *dbRepo) i() {}
func (d *dbRepo) GetDbR() *gorm.DB {
return d.DbR
}
func (d *dbRepo) GetDbW() *gorm.DB {
return d.DbW
}
func (d *dbRepo) DbRClose() error {
sqlDB, err := d.DbR.DB()
if err != nil {
return err
}
return sqlDB.Close()
}
func (d *dbRepo) DbWClose() error {
sqlDB, err := d.DbW.DB()
if err != nil {
return err
}
return sqlDB.Close()
}
func dbConnect(user, pass, addr, dbName string) (*gorm.DB, error) {
dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=%t&loc=%s",
user,
pass,
addr,
dbName,
true,
"Local")
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
//Logger: logger.Default.LogMode(logger.Info), // 日志配置
})
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("[db connection failed] Database name: %s", dbName))
}
db.Set("gorm:table_options", "CHARSET=utf8mb4")
cfg := configs.Get().MySQL.Base
sqlDB, err := db.DB()
if err != nil {
return nil, err
}
// 设置连接池 用于设置最大打开的连接数默认值为0表示不限制.设置最大的连接数可以避免并发太高导致连接mysql出现too many connections的错误。
sqlDB.SetMaxOpenConns(cfg.MaxOpenConn)
// 设置最大连接数 用于设置闲置的连接数.设置闲置的连接数则当开启的一个连接使用完成后可以放在池里等候下一次使用。
sqlDB.SetMaxIdleConns(cfg.MaxIdleConn)
// 设置最大连接超时
sqlDB.SetConnMaxLifetime(time.Minute * cfg.ConnMaxLifeTime)
// 使用插件
db.Use(&TracePlugin{})
return db, nil
}

View File

@@ -0,0 +1,86 @@
package db_repo
import (
"time"
"github.com/xinliangnote/go-gin-api/internal/pkg/core"
"github.com/xinliangnote/go-gin-api/internal/pkg/trace"
"github.com/xinliangnote/go-gin-api/pkg/time_parse"
"gorm.io/gorm"
"gorm.io/gorm/utils"
)
const (
callBackBeforeName = "core:before"
callBackAfterName = "core:after"
coreContext = "_core_context"
startTime = "_start_time"
)
type TracePlugin struct{}
func (op *TracePlugin) Name() string {
return "tracePlugin"
}
func before(db *gorm.DB) {
db.InstanceSet(coreContext, db.Statement.Context)
db.InstanceSet(startTime, time.Now())
return
}
func after(db *gorm.DB) {
_ctx, isExist := db.InstanceGet(coreContext)
if !isExist {
return
}
ctx, ok := _ctx.(core.Context)
if !ok {
return
}
_ts, isExist := db.InstanceGet(startTime)
if !isExist {
return
}
ts, ok := _ts.(time.Time)
if !ok {
return
}
sql := db.Dialector.Explain(db.Statement.SQL.String(), db.Statement.Vars...)
sqlInfo := new(trace.SQL)
sqlInfo.Time = time_parse.CSTLayoutString()
sqlInfo.SQL = sql
sqlInfo.Src = utils.FileWithLineNum()
sqlInfo.Rows = db.Statement.RowsAffected
sqlInfo.Duration = time.Since(ts).Seconds()
ctx.Trace().AppendSQL(sqlInfo)
return
}
func (op *TracePlugin) Initialize(db *gorm.DB) (err error) {
// 开始前
_ = db.Callback().Create().Before("gorm:before_create").Register(callBackBeforeName, before)
_ = db.Callback().Query().Before("gorm:query").Register(callBackBeforeName, before)
_ = db.Callback().Delete().Before("gorm:before_delete").Register(callBackBeforeName, before)
_ = db.Callback().Update().Before("gorm:setup_reflect_value").Register(callBackBeforeName, before)
_ = db.Callback().Row().Before("gorm:row").Register(callBackBeforeName, before)
_ = db.Callback().Raw().Before("gorm:raw").Register(callBackBeforeName, before)
// 结束后
_ = db.Callback().Create().After("gorm:after_create").Register(callBackAfterName, after)
_ = db.Callback().Query().After("gorm:after_query").Register(callBackAfterName, after)
_ = db.Callback().Delete().After("gorm:after_delete").Register(callBackAfterName, after)
_ = db.Callback().Update().After("gorm:after_update").Register(callBackAfterName, after)
_ = db.Callback().Row().After("gorm:row").Register(callBackAfterName, after)
_ = db.Callback().Raw().After("gorm:raw").Register(callBackAfterName, after)
return
}
var _ gorm.Plugin = &TracePlugin{}

View File

@@ -0,0 +1,71 @@
package user_demo_repo
import (
"github.com/xinliangnote/go-gin-api/internal/api/model/user_model"
"github.com/xinliangnote/go-gin-api/internal/api/repository/db_repo"
"github.com/xinliangnote/go-gin-api/internal/pkg/core"
"github.com/jinzhu/gorm"
"github.com/pkg/errors"
)
var _ UserRepo = (*userRepo)(nil)
type UserRepo interface {
// i 为了避免被其他包实现
i()
Create(ctx core.Context, user user_model.UserDemo) (id uint, err error)
UpdateNickNameByID(ctx core.Context, id uint, username string) (err error)
GetUserByUserName(ctx core.Context, username string) (*user_model.UserDemo, error)
getUserByID(ctx core.Context, id uint) (*user_model.UserDemo, error)
}
type userRepo struct {
db db_repo.Repo
}
func NewUserRepo(db db_repo.Repo) UserRepo {
return &userRepo{
db: db,
}
}
func (u *userRepo) i() {}
func (u *userRepo) Create(ctx core.Context, user user_model.UserDemo) (id uint, err error) {
err = u.db.GetDbW().WithContext(ctx).Create(&user).Error
if err != nil {
return 0, errors.Wrap(err, "[user_repo] create user err")
}
return user.Id, nil
}
func (u *userRepo) getUserByID(ctx core.Context, id uint) (*user_model.UserDemo, error) {
data := new(user_model.UserDemo)
err := u.db.GetDbR().WithContext(ctx).First(data, id).Error
if err != nil && err != gorm.ErrRecordNotFound {
return nil, errors.Wrap(err, "[user_demo] get user data err")
}
return data, nil
}
func (u *userRepo) UpdateNickNameByID(ctx core.Context, id uint, nickname string) (err error) {
user, err := u.getUserByID(ctx, id)
if err != nil {
return errors.Wrap(err, "[user_demo] update user data err")
}
return u.db.GetDbW().WithContext(ctx).Model(user).Update("nick_name", nickname).Error
}
func (u *userRepo) GetUserByUserName(ctx core.Context, username string) (*user_model.UserDemo, error) {
data := new(user_model.UserDemo)
err := u.db.GetDbR().
WithContext(ctx).
Select([]string{"id", "user_name", "nick_name", "mobile"}).
Where("user_name = ?", username).
First(data).Error
if err != nil && err != gorm.ErrRecordNotFound {
return nil, errors.Wrap(err, "[user_demo] get user data err")
}
return data, nil
}

View File

@@ -1,27 +1,30 @@
package middleware
import (
"github.com/xinliangnote/go-gin-api/configs"
"github.com/xinliangnote/go-gin-api/internal/api/code"
"github.com/xinliangnote/go-gin-api/internal/pkg/core"
"github.com/xinliangnote/go-gin-api/internal/pkg/errno"
"github.com/xinliangnote/go-gin-api/internal/pkg/token"
"github.com/xinliangnote/go-gin-api/pkg/errno"
"github.com/xinliangnote/go-gin-api/pkg/token"
)
func AuthHandler(ctx core.Context) (userId int, userName string, err errno.Error) {
auth := ctx.GetHeader("Authorization")
if auth == "" {
err = errno.ErrSignParam
err = code.ErrSignParam
return
}
claims, errParse := token.Parse(auth)
cfg := configs.Get().JWT
claims, errParse := token.New(cfg.Secret).Parse(auth)
if errParse != nil {
err = errno.ErrSignParam
err = code.ErrSignParam
return
}
userId = claims.UserID
if userId <= 0 {
err = errno.ErrSignParam
err = code.ErrSignParam
return
}
userName = claims.UserName

View File

@@ -0,0 +1,55 @@
package router
import (
"github.com/xinliangnote/go-gin-api/internal/api/controller/demo"
"github.com/xinliangnote/go-gin-api/internal/api/controller/user_handler"
"github.com/xinliangnote/go-gin-api/internal/api/repository/cache_repo"
"github.com/xinliangnote/go-gin-api/internal/api/repository/db_repo"
auth "github.com/xinliangnote/go-gin-api/internal/api/router/middleware"
"github.com/xinliangnote/go-gin-api/internal/pkg/core"
"github.com/xinliangnote/go-gin-api/internal/pkg/metrics"
"github.com/xinliangnote/go-gin-api/internal/pkg/notify"
"github.com/pkg/errors"
"go.uber.org/zap"
)
func NewHTTPMux(logger *zap.Logger, db db_repo.Repo, cache cache_repo.Repo) (core.Mux, error) {
if logger == nil {
return nil, errors.New("logger required")
}
mux, err := core.New(logger,
core.WithEnableCors(),
core.WithEnableRate(),
core.WithPanicNotify(notify.OnPanicNotify),
core.WithRecordMetrics(metrics.RecordMetrics),
)
if err != nil {
panic(err)
}
demoHandler := demo.NewDemo(logger)
userHandler := user_handler.NewUserDemo(logger, db, cache)
u := mux.Group("/user")
{
u.POST("/login", userHandler.Login())
u.POST("/create", userHandler.Create())
u.GET("/info/:username", userHandler.Detail())
u.POST("/update", userHandler.UpdateNickNameByID())
}
d := mux.Group("/demo", core.WrapAuthHandler(auth.AuthHandler)) //使用 auth 验证
{
d.GET("user/:name", demoHandler.User())
// 模拟数据
d.GET("get/:name", demoHandler.Get(), core.DisableTrace)
d.POST("post", demoHandler.Post(), core.DisableTrace)
}
return mux, nil
}

View File

@@ -0,0 +1,10 @@
## service
业务逻辑层。
处于 `controller` 层和 `repository` 层之间,依赖接口开发。
命令规范:
- 包名以 `_service` 结尾。

View File

@@ -0,0 +1,67 @@
package user_service
import (
"github.com/xinliangnote/go-gin-api/internal/api/model/user_model"
"github.com/xinliangnote/go-gin-api/internal/api/repository/cache_repo"
"github.com/xinliangnote/go-gin-api/internal/api/repository/db_repo"
"github.com/xinliangnote/go-gin-api/internal/api/repository/db_repo/user_demo_repo"
"github.com/xinliangnote/go-gin-api/internal/pkg/core"
)
var _ UserService = (*userSer)(nil)
type UserService interface {
// i 为了避免被其他包实现
i()
Create(ctx core.Context, user *user_model.CreateRequest) (id uint, err error)
UpdateNickNameByID(ctx core.Context, id uint, username string) (err error)
GetUserByUserName(ctx core.Context, username string) (user *user_model.UserDemo, err error)
}
type userSer struct {
db db_repo.Repo
cache cache_repo.Repo
userRepo user_demo_repo.UserRepo
}
func NewUserService(db db_repo.Repo, cache cache_repo.Repo) UserService {
userRepo := user_demo_repo.NewUserRepo(db)
return &userSer{
db: db,
cache: cache,
userRepo: userRepo,
}
}
func (u *userSer) i() {}
func (u *userSer) Create(ctx core.Context, user *user_model.CreateRequest) (id uint, err error) {
create := user_model.UserDemo{
UserName: user.UserName,
NickName: user.NickName,
Mobile: user.Mobile,
}
id, err = u.userRepo.Create(ctx, create)
if err != nil {
return 0, err
}
return
}
func (u *userSer) UpdateNickNameByID(ctx core.Context, id uint, username string) (err error) {
err = u.userRepo.UpdateNickNameByID(ctx, id, username)
if err != nil {
return nil
}
return nil
}
func (u *userSer) GetUserByUserName(ctx core.Context, username string) (user *user_model.UserDemo, err error) {
user, err = u.userRepo.GetUserByUserName(ctx, username)
if err != nil {
return nil, err
}
return user, nil
}

View File

@@ -1,55 +0,0 @@
package configs
import (
"github.com/xinliangnote/go-gin-api/pkg/env"
"github.com/spf13/viper"
)
var config = new(Config)
type Config struct {
Mail struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
User string `mapstructure:"user"`
Pass string `mapstructure:"pass"`
To string `mapstructure:"to"`
} `mapstructure:"mail"`
JWT struct {
Secret string `mapstructure:"secret"`
} `mapstructure:"jwt"`
Aes struct {
Key string `mapstructure:"key"`
Iv string `mapstructure:"iv"`
} `mapstructure:"aes"`
Rsa struct {
Private string `mapstructure:"private"`
Public string `mapstructure:"public"`
} `mapstructure:"rsa"`
}
func init() {
viper.SetConfigName(env.Active().Value() + "_configs")
viper.SetConfigType("toml")
viper.AddConfigPath("./configs")
if err := viper.ReadInConfig(); err != nil {
panic(err)
}
if err := viper.Unmarshal(config); err != nil {
panic(err)
}
}
func Get() Config {
return *config
}
func ProjectName() string {
return "go-gin-api"
}

View File

@@ -7,9 +7,10 @@ import (
"net/url"
"strings"
"sync"
"time"
"github.com/xinliangnote/go-gin-api/internal/pkg/errno"
"github.com/xinliangnote/go-gin-api/internal/pkg/journal"
"github.com/xinliangnote/go-gin-api/internal/pkg/trace"
"github.com/xinliangnote/go-gin-api/pkg/errno"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
@@ -18,11 +19,11 @@ import (
type HandlerFunc func(c Context)
type Journal = journal.T
type Trace = trace.T
const (
_Alias = "_alias_"
_JournalName = "_journal_"
_TraceName = "_trace_"
_LoggerName = "_logger_"
_BodyName = "_body_"
_PayloadName = "_payload_"
@@ -78,9 +79,9 @@ type Context interface {
// Redirect 重定向
Redirect(code int, location string)
Journal() Journal
setJournal(journal Journal)
disableJournal()
Trace() Trace
setTrace(trace Trace)
disableTrace()
Logger() *zap.Logger
setLogger(logger *zap.Logger)
@@ -109,6 +110,11 @@ type Context interface {
Host() string
Path() string
URI() string
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
type context struct {
@@ -121,7 +127,7 @@ func (c *context) init() {
panic(err)
}
c.ctx.Set(_BodyName, body) // cache body是为了journal使用
c.ctx.Set(_BodyName, body) // cache body是为了trace使用
c.ctx.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body)) // re-construct req body
}
@@ -161,21 +167,21 @@ func (c *context) Redirect(code int, location string) {
c.ctx.Redirect(code, location)
}
func (c *context) Journal() Journal {
j, ok := c.ctx.Get(_JournalName)
if !ok || j == nil {
func (c *context) Trace() Trace {
t, ok := c.ctx.Get(_TraceName)
if !ok || t == nil {
return nil
}
return j.(Journal)
return t.(Trace)
}
func (c *context) setJournal(journal Journal) {
c.ctx.Set(_JournalName, journal)
func (c *context) setTrace(trace Trace) {
c.ctx.Set(_TraceName, trace)
}
func (c *context) disableJournal() {
c.setJournal(nil)
func (c *context) disableTrace() {
c.setTrace(nil)
}
func (c *context) Logger() *zap.Logger {
@@ -303,3 +309,26 @@ func (c *context) URI() string {
uri, _ := url.QueryUnescape(c.ctx.Request.URL.RequestURI())
return uri
}
func (c *context) Deadline() (deadline time.Time, ok bool) {
return
}
func (c *context) Done() <-chan struct{} {
return nil
}
func (c *context) Err() error {
return nil
}
func (c *context) Value(key interface{}) interface{} {
if key == 0 {
return c.ctx.Request
}
if keyAsString, ok := key.(string); ok {
val, _ := c.ctx.Get(keyAsString)
return val
}
return nil
}

View File

@@ -8,9 +8,11 @@ import (
"time"
_ "github.com/xinliangnote/go-gin-api/docs"
"github.com/xinliangnote/go-gin-api/internal/pkg/errno"
"github.com/xinliangnote/go-gin-api/internal/pkg/journal"
"github.com/xinliangnote/go-gin-api/internal/api/code"
"github.com/xinliangnote/go-gin-api/internal/pkg/trace"
"github.com/xinliangnote/go-gin-api/pkg/color"
"github.com/xinliangnote/go-gin-api/pkg/env"
"github.com/xinliangnote/go-gin-api/pkg/errno"
"github.com/gin-contrib/pprof"
"github.com/gin-gonic/gin"
@@ -105,9 +107,8 @@ func WithEnableRate() Option {
}
}
// DisableJournal 标识某些请求不记录journal
func DisableJournal(ctx Context) {
ctx.disableJournal()
func DisableTrace(ctx Context) {
ctx.disableTrace()
}
// AliasForRecordMetrics 对请求uri起个别名用于prometheus记录指标。
@@ -242,8 +243,10 @@ func New(logger *zap.Logger, options ...Option) (Mux, error) {
fmt.Println(color.Blue(_UI))
fmt.Println(color.Green(fmt.Sprintf("* [register -env %s]", env.Active().Value())))
// withoutLogPaths 这些请求,默认不记录日志
withoutJournalPaths := map[string]bool{
withoutTracePaths := map[string]bool{
"/metrics": true,
"/debug/pprof/": true,
@@ -259,6 +262,9 @@ func New(logger *zap.Logger, options ...Option) (Mux, error) {
"/debug/pprof/threadcreate": true,
"/favicon.ico": true,
"/h/ping": true,
"/h/info": true,
}
opt := new(option)
@@ -318,11 +324,11 @@ func New(logger *zap.Logger, options ...Option) (Mux, error) {
context.init()
context.setLogger(logger)
if !withoutJournalPaths[ctx.Request.URL.Path] {
if journalID := context.GetHeader(journal.JournalHeader); journalID != "" {
context.setJournal(journal.NewJournal(journalID))
if !withoutTracePaths[ctx.Request.URL.Path] {
if traceId := context.GetHeader(trace.Header); traceId != "" {
context.setTrace(trace.New(traceId))
} else {
context.setJournal(journal.NewJournal(""))
context.setTrace(trace.New(""))
}
}
@@ -330,7 +336,7 @@ func New(logger *zap.Logger, options ...Option) (Mux, error) {
if err := recover(); err != nil {
stackInfo := string(debug.Stack())
logger.Error("got panic", zap.String("panic", fmt.Sprintf("%+v", err)), zap.String("stack", stackInfo))
context.SetPayload(errno.ErrServer)
context.SetPayload(code.ErrServer)
if notify := opt.panicNotify; notify != nil {
notify(context, err, stackInfo)
@@ -342,8 +348,10 @@ func New(logger *zap.Logger, options ...Option) (Mux, error) {
}
var (
response errno.Error
abortErr error
response errno.Error
businessCode int
businessCodeMsg string
abortErr error
)
if ctx.IsAborted() {
@@ -360,12 +368,14 @@ func New(logger *zap.Logger, options ...Option) (Mux, error) {
}
if response != nil {
if x := context.Journal(); x != nil {
context.SetHeader(journal.JournalHeader, x.ID())
if x := context.Trace(); x != nil {
context.SetHeader(trace.Header, x.ID())
response.WithID(x.ID())
} else {
response.WithID("")
}
businessCode = response.GetCode()
businessCodeMsg = response.GetMsg()
ctx.JSON(http.StatusOK, response)
}
@@ -375,23 +385,18 @@ func New(logger *zap.Logger, options ...Option) (Mux, error) {
uri = alias
}
businessCode := 0
if response != nil {
businessCode = response.GetCode()
}
opt.recordMetrics(context.Method(), uri, !ctx.IsAborted() && ctx.Writer.Status() == http.StatusOK, ctx.Writer.Status(), businessCode, time.Since(ts).Seconds())
}
var j *journal.Journal
if x := context.Journal(); x != nil {
j = x.(*journal.Journal)
var t *trace.Trace
if x := context.Trace(); x != nil {
t = x.(*trace.Trace)
} else {
return
}
decodedURL, _ := url.QueryUnescape(ctx.Request.URL.RequestURI())
j.WithRequest(&journal.Request{
t.WithRequest(&trace.Request{
TTL: "un-limit",
Method: ctx.Request.Method,
DecodedURL: decodedURL,
@@ -399,17 +404,19 @@ func New(logger *zap.Logger, options ...Option) (Mux, error) {
Body: string(context.RawData()),
})
j.WithResponse(&journal.Response{
Header: ctx.Writer.Header(),
StatusCode: ctx.Writer.Status(),
Status: http.StatusText(ctx.Writer.Status()),
Body: response,
t.WithResponse(&trace.Response{
Header: ctx.Writer.Header(),
HttpCode: ctx.Writer.Status(),
HttpCodeMsg: http.StatusText(ctx.Writer.Status()),
BusinessCode: businessCode,
BusinessCodeMsg: businessCodeMsg,
Body: response,
})
j.Success = !ctx.IsAborted() && ctx.Writer.Status() == http.StatusOK
j.CostSeconds = time.Since(ts).Seconds()
t.Success = !ctx.IsAborted() && ctx.Writer.Status() == http.StatusOK
t.CostSeconds = time.Since(ts).Seconds()
logger.Info("interceptor", zap.Any("journal", j))
logger.Info("interceptor", zap.Any("trace", t))
}()
ctx.Next()
@@ -422,7 +429,7 @@ func New(logger *zap.Logger, options ...Option) (Mux, error) {
defer releaseContext(context)
if !limiter.Allow() {
context.SetPayload(errno.ErrManyRequest)
context.SetPayload(code.ErrManyRequest)
ctx.Abort()
return
}
@@ -430,24 +437,26 @@ func New(logger *zap.Logger, options ...Option) (Mux, error) {
})
}
mux.engine.NoMethod(wrapHandlers(DisableJournal)...)
mux.engine.NoRoute(wrapHandlers(DisableJournal)...)
mux.engine.NoMethod(wrapHandlers(DisableTrace)...)
mux.engine.NoRoute(wrapHandlers(DisableTrace)...)
h := mux.Group("/h", DisableJournal)
h := mux.Group("/h")
{
h.GET("/ping", func(ctx Context) {
ctx.SetPayload(errno.OK.WithData("pong"))
ctx.SetPayload(code.OK.WithData("pong"))
})
h.GET("/info", func(ctx Context) {
resp := &struct {
Header interface{} `json:"header"`
Ts time.Time `json:"ts"`
Env string `json:"env"`
}{
Header: ctx.Header(),
Ts: time.Now(),
Env: env.Active().Value(),
}
ctx.SetPayload(errno.OK.WithData(resp))
ctx.SetPayload(code.OK.WithData(resp))
})
}

View File

@@ -1,22 +0,0 @@
package errno
import "net/http"
var (
// OK
OK = NewError(0, "OK")
// 服务级错误码
ErrServer = NewError(10101, http.StatusText(http.StatusInternalServerError))
Err404 = NewError(10102, http.StatusText(http.StatusNotFound))
ErrManyRequest = NewError(10103, "Too many requests")
ErrParam = NewError(10002, "参数有误")
ErrSignParam = NewError(10003, "签名参数有误")
// 模块级错误码 - 用户模块
ErrUser = NewError(20101, "非法用户")
ErrUserCaptcha = NewError(20102, "用户验证码有误")
// ...
)

View File

@@ -1,121 +0,0 @@
package journal
import (
"crypto/rand"
"encoding/hex"
"io"
"sync"
)
// JournalHeader http/rpc header中的名字
const JournalHeader = "Journal-ID"
var _ T = (*Journal)(nil)
var _ T = (*Dialog)(nil)
// T 约束
type T interface {
ID() string
t()
}
// Journal 包含一次rpc请求的全部参数和内部调用其它方接口的过程
type Journal struct {
mux sync.Mutex
Identifier string `json:"id"`
Request *Request `json:"request"`
Response *Response `json:"response"`
Dialogs []*Dialog `json:"dialogs"`
Success bool `json:"success"`
CostSeconds float64 `json:"cost_seconds"`
}
// NewJournal 创建Journal
func NewJournal(id string) *Journal {
if id == "" {
buf := make([]byte, 10)
io.ReadFull(rand.Reader, buf)
id = string(hex.EncodeToString(buf))
}
return &Journal{
Identifier: id,
}
}
// ID 唯一标识符
func (j *Journal) ID() string {
return j.Identifier
}
// WithRequest 设置request
func (j *Journal) WithRequest(req *Request) *Journal {
j.Request = req
return j
}
// WithResponse 设置response
func (j *Journal) WithResponse(resp *Response) *Journal {
j.Response = resp
return j
}
// AppendDialog 安全的追加内部调用过程dialog
func (j *Journal) AppendDialog(dialog *Dialog) *Journal {
if dialog == nil {
return j
}
j.mux.Lock()
defer j.mux.Unlock()
j.Dialogs = append(j.Dialogs, dialog)
return j
}
func (j *Journal) t() {}
// Request 请求信息
type Request struct {
TTL string `json:"ttl"`
Method string `json:"method"`
DecodedURL string `json:"decoded_url"`
Header interface{} `json:"header"`
Body interface{} `json:"body"`
}
// Response 响应信息
type Response struct {
Header interface{} `json:"header"`
StatusCode int `json:"status_code"`
Status string `json:"status"`
Body interface{} `json:"body"`
CostSeconds float64 `json:"cost_seconds"`
}
// Dialog 内部调用其它方接口的会话信息;失败时会有retry操作所以response会有多次。
type Dialog struct {
mux sync.Mutex
Request *Request `json:"request"`
Responses []*Response `json:"responses"`
Success bool `json:"success"`
CostSeconds float64 `json:"cost_seconds"`
}
// ID ...
func (d *Dialog) ID() string {
return ""
}
// AppendResponse 按转的追加response信息
func (d *Dialog) AppendResponse(resp *Response) {
if resp == nil {
return
}
d.mux.Lock()
d.Responses = append(d.Responses, resp)
d.mux.Unlock()
}
func (d *Dialog) t() {}

View File

@@ -1,7 +1,7 @@
package notify
import (
"github.com/xinliangnote/go-gin-api/internal/pkg/configs"
"github.com/xinliangnote/go-gin-api/configs"
"github.com/xinliangnote/go-gin-api/internal/pkg/core"
"github.com/xinliangnote/go-gin-api/internal/pkg/notify/mail"
@@ -16,7 +16,7 @@ func OnPanicNotify(ctx core.Context, err interface{}, stackInfo string) {
return
}
subject, body, htmlErr := mail.NewPanicHTMLEmail(ctx.Method(), ctx.Host(), ctx.URI(), ctx.Journal().ID(), err, stackInfo)
subject, body, htmlErr := mail.NewPanicHTMLEmail(ctx.Method(), ctx.Host(), ctx.URI(), ctx.Trace().ID(), err, stackInfo)
if htmlErr != nil {
ctx.Logger().Error("NewPanicHTMLEmail error", zap.Error(htmlErr))
return

View File

@@ -0,0 +1,81 @@
## trace
一个用于开发调试的辅助工具。
可以实时显示当前页面的操作的请求信息、运行情况、SQL执行、错误提示等。
- `trace.go` 主入口文件;
- `dialog.go` 处理 third_party_requests 记录;
- `debug.go` 处理 debug 记录;
#### 数据格式
##### trace_id
当前 trace 的 ID例如938ff86be98439c6c1a7便于搜索使用。
##### request
请求信息,会包括:
- ttl 请求超时时间例如2s 或 un-limit
- method 请求方式例如GET 或 POST
- decoded_url 请求地址
- header 请求头信息
- body 请求体信息
##### response
- header 响应头信息
- body 响应提信息
- business_code 业务码例如10010
- business_code_msg 业务码信息,例如:签名错误
- http_code HTTP 状态码例如200
- http_code_msg HTTP 状态码信息例如OK
- cost_seconds 耗费时长:单位秒,比如 0.001105661
##### third_party_requests
每一个第三方 http 请求都会生成如下的一组数据,多个请求会生成多组数据。
- request同上 request 结构一致
- response同上 response 结构一致
- success是否成功true 或 false
- cost_seconds耗费时长单位秒
注意response 中的 business_code、business_code_msg 为空,因为各个第三方返回结构不同,这两个字段为空。
##### sqls
执行的 SQL 信息,多个 SQL 会记录多组数据。
- time时间格式2006-01-02 15:04:05
- src文件地址和行号
- duration执行时长单位
- sqlSQL 语句
- rows_affected影响行数
##### debugs
- key 打印的标示
- value 打印的值
```cassandraql
// 调试时,使用这个方法:
p.Print("key", "value", p.WithTrace(c.Trace()))
```
只有参数中增加了 `p.WithTrace(c.Trace())`,才会记录到 `debugs` 中。
##### success
是否成功true 或 false
```cassandraql
success = !ctx.IsAborted() && ctx.Writer.Status() == http.StatusOK
```
##### cost_seconds
耗费时长:单位秒,比如 0.001105661

View File

@@ -0,0 +1,6 @@
package trace
type Debug struct {
Key string `json:"key"` //标示
Value interface{} `json:"value"` //值
}

View File

@@ -0,0 +1,32 @@
package trace
import "sync"
var _ D = (*Dialog)(nil)
type D interface {
i()
AppendResponse(resp *Response)
}
// Dialog 内部调用其它方接口的会话信息;失败时会有retry操作所以response会有多次。
type Dialog struct {
mux sync.Mutex
Request *Request `json:"request"` // 请求信息
Responses []*Response `json:"responses"` // 返回信息
Success bool `json:"success"` // 是否成功true 或 false
CostSeconds float64 `json:"cost_seconds"` // 执行时长,单位:秒
}
func (d *Dialog) i() {}
// AppendResponse 按转的追加response信息
func (d *Dialog) AppendResponse(resp *Response) {
if resp == nil {
return
}
d.mux.Lock()
d.Responses = append(d.Responses, resp)
d.mux.Unlock()
}

View File

@@ -0,0 +1,9 @@
package trace
type SQL struct {
Time string `json:"time"` // 时间格式2006-01-02 15:04:05
Src string `json:"src"` // 文件地址和行号
Duration float64 `json:"duration"` // 执行时长,单位:秒
SQL string `json:"sql"` // SQL 语句
Rows int64 `json:"rows_affected"` // 影响行数
}

124
internal/pkg/trace/trace.go Normal file
View File

@@ -0,0 +1,124 @@
package trace
import (
"crypto/rand"
"encoding/hex"
"io"
"sync"
)
const Header = "TRACE-ID"
var _ T = (*Trace)(nil)
type T interface {
i()
ID() string
WithRequest(req *Request) *Trace
WithResponse(resp *Response) *Trace
AppendDialog(dialog *Dialog) *Trace
AppendSQL(sql *SQL) *Trace
}
// Trace 记录的参数
type Trace struct {
mux sync.Mutex
Identifier string `json:"trace_id"`
Request *Request `json:"request"`
Response *Response `json:"response"`
ThirdPartyRequests []*Dialog `json:"third_party_requests"`
Debugs []*Debug `json:"debugs"`
SQLs []*SQL `json:"sqls"`
Success bool `json:"success"`
CostSeconds float64 `json:"cost_seconds"`
}
// Request 请求信息
type Request struct {
TTL string `json:"ttl"`
Method string `json:"method"`
DecodedURL string `json:"decoded_url"`
Header interface{} `json:"header"`
Body interface{} `json:"body"`
}
// Response 响应信息
type Response struct {
Header interface{} `json:"header"`
Body interface{} `json:"body"`
BusinessCode int `json:"business_code,omitempty"`
BusinessCodeMsg string `json:"business_code_msg,omitempty"`
HttpCode int `json:"http_code"`
HttpCodeMsg string `json:"http_code_msg"`
CostSeconds float64 `json:"cost_seconds"`
}
func New(id string) *Trace {
if id == "" {
buf := make([]byte, 10)
io.ReadFull(rand.Reader, buf)
id = string(hex.EncodeToString(buf))
}
return &Trace{
Identifier: id,
}
}
func (t *Trace) i() {}
// ID 唯一标识符
func (t *Trace) ID() string {
return t.Identifier
}
// WithRequest 设置request
func (t *Trace) WithRequest(req *Request) *Trace {
t.Request = req
return t
}
// WithResponse 设置response
func (t *Trace) WithResponse(resp *Response) *Trace {
t.Response = resp
return t
}
// AppendDialog 安全的追加内部调用过程dialog
func (t *Trace) AppendDialog(dialog *Dialog) *Trace {
if dialog == nil {
return t
}
t.mux.Lock()
defer t.mux.Unlock()
t.ThirdPartyRequests = append(t.ThirdPartyRequests, dialog)
return t
}
// AppendDebug 追加 debug
func (t *Trace) AppendDebug(debug *Debug) *Trace {
if debug == nil {
return t
}
t.mux.Lock()
defer t.mux.Unlock()
t.Debugs = append(t.Debugs, debug)
return t
}
// AppendSQL 追加 SQL
func (t *Trace) AppendSQL(sql *SQL) *Trace {
if sql == nil {
return t
}
t.mux.Lock()
defer t.mux.Unlock()
t.SQLs = append(t.SQLs, sql)
return t
}

View File

@@ -1,53 +0,0 @@
package router
import (
"github.com/xinliangnote/go-gin-api/internal/api/controller/demo"
"github.com/xinliangnote/go-gin-api/internal/pkg/core"
"github.com/xinliangnote/go-gin-api/internal/pkg/metrics"
"github.com/xinliangnote/go-gin-api/internal/pkg/notify"
"github.com/xinliangnote/go-gin-api/internal/router/middleware"
"github.com/pkg/errors"
"go.uber.org/zap"
)
func NewHTTPMux(logger *zap.Logger) (core.Mux, error) {
if logger == nil {
return nil, errors.New("logger required")
}
mux, err := core.New(logger,
core.WithEnableCors(),
core.WithEnableRate(),
core.WithPanicNotify(notify.OnPanicNotify),
core.WithRecordMetrics(metrics.RecordMetrics),
)
if err != nil {
panic(err)
}
demoHandler := demo.NewDemo(logger)
u := mux.Group("/user")
{
u.POST("/login", demoHandler.Login())
}
d := mux.Group("/demo", core.WrapAuthHandler(middleware.AuthHandler)) //使用 auth 验证
{
d.GET("user/:name", demoHandler.User())
// 模拟数据
d.GET("get/:name", demoHandler.Get(), core.DisableJournal)
d.POST("post", demoHandler.Post(), core.DisableJournal)
// 测试加密性能
d.GET("/rsa/test", demoHandler.RsaTest())
d.GET("/aes/test", demoHandler.AesTest())
d.GET("/md5/test", demoHandler.MD5Test())
}
return mux, nil
}

View File

@@ -3,14 +3,23 @@ package aes
import "testing"
const (
Key = "IgkibX71IEf382PT"
Iv = "IgkibX71IEf382PT"
key = "IgkibX71IEf382PT"
iv = "IgkibX71IEf382PT"
)
func TestEncrypt(t *testing.T) {
t.Log(New(Key, Iv).Encrypt("123456"))
t.Log(New(key, iv).Encrypt("123456"))
}
func TestDecrypt(t *testing.T) {
t.Log(New(Key, Iv).Decrypt("GO-ri84zevE-z1biJwfQPw=="))
t.Log(New(key, iv).Decrypt("GO-ri84zevE-z1biJwfQPw=="))
}
func BenchmarkEncryptAndDecrypt(b *testing.B) {
b.ResetTimer()
aes := New(key, iv)
for i := 0; i < b.N; i++ {
encryptString, _ := aes.Encrypt("123456")
aes.Decrypt(encryptString)
}
}

10
pkg/env/env.go vendored
View File

@@ -1,8 +1,8 @@
package env
import (
"flag"
"fmt"
"os"
"strings"
)
@@ -53,8 +53,10 @@ func (e *environment) IsPro() bool {
func (e *environment) t() {}
func init() {
env := strings.ToLower(strings.TrimSpace(os.Getenv("ACTIVE")))
switch env {
env := flag.String("env", "", "请输入运行环境:\n dev:开发环境\n fat:测试环境\n uat:预上线环境\n pro:正式环境\n")
flag.Parse()
switch strings.ToLower(strings.TrimSpace(*env)) {
case "dev":
active = dev
case "fat":
@@ -65,7 +67,7 @@ func init() {
active = pro
default:
active = fat
fmt.Println("Warning: env'ACTIVE' cannot be found, or it is illegal. The default 'fat' will be used.")
fmt.Println("Warning: '-env' cannot be found, or it is illegal. The default 'fat' will be used.")
}
}

7
pkg/env/env_test.go vendored
View File

@@ -1,7 +0,0 @@
package env
import "testing"
func TestActive(t *testing.T) {
t.Log(Active().Value())
}

View File

@@ -7,7 +7,7 @@ import (
httpURL "net/url"
"time"
"github.com/xinliangnote/go-gin-api/internal/pkg/journal"
"github.com/xinliangnote/go-gin-api/internal/pkg/trace"
"github.com/pkg/errors"
)
@@ -74,10 +74,10 @@ func withoutBody(method, url string, form httpURL.Values, options ...Option) (bo
opt := newOption()
defer func() {
if opt.Journal != nil {
if opt.Trace != nil {
opt.Dialog.Success = err == nil
opt.Dialog.CostSeconds = time.Since(ts).Seconds()
opt.Journal.AppendDialog(opt.Dialog)
opt.Trace.AppendDialog(opt.Dialog)
}
}()
@@ -85,8 +85,8 @@ func withoutBody(method, url string, form httpURL.Values, options ...Option) (bo
f(opt)
}
opt.Header["Content-Type"] = "application/x-www-form-urlencoded; charset=utf-8"
if opt.Journal != nil {
opt.Header[journal.JournalHeader] = opt.Journal.ID()
if opt.Trace != nil {
opt.Header[trace.Header] = opt.Trace.ID()
}
ttl := opt.TTL
@@ -99,7 +99,7 @@ func withoutBody(method, url string, form httpURL.Values, options ...Option) (bo
if opt.Dialog != nil {
decodedURL, _ := httpURL.QueryUnescape(url)
opt.Dialog.Request = &journal.Request{
opt.Dialog.Request = &trace.Request{
TTL: ttl.String(),
Method: method,
DecodedURL: decodedURL,
@@ -172,10 +172,10 @@ func withFormBody(method, url string, form httpURL.Values, options ...Option) (b
opt := newOption()
defer func() {
if opt.Journal != nil {
if opt.Trace != nil {
opt.Dialog.Success = err == nil
opt.Dialog.CostSeconds = time.Since(ts).Seconds()
opt.Journal.AppendDialog(opt.Dialog)
opt.Trace.AppendDialog(opt.Dialog)
}
}()
@@ -183,8 +183,8 @@ func withFormBody(method, url string, form httpURL.Values, options ...Option) (b
f(opt)
}
opt.Header["Content-Type"] = "application/x-www-form-urlencoded; charset=utf-8"
if opt.Journal != nil {
opt.Header[journal.JournalHeader] = opt.Journal.ID()
if opt.Trace != nil {
opt.Header[trace.Header] = opt.Trace.ID()
}
ttl := opt.TTL
@@ -198,7 +198,7 @@ func withFormBody(method, url string, form httpURL.Values, options ...Option) (b
formValue := form.Encode()
if opt.Dialog != nil {
decodedURL, _ := httpURL.QueryUnescape(url)
opt.Dialog.Request = &journal.Request{
opt.Dialog.Request = &trace.Request{
TTL: ttl.String(),
Method: method,
DecodedURL: decodedURL,
@@ -242,10 +242,10 @@ func withJSONBody(method, url string, raw json.RawMessage, options ...Option) (b
opt := newOption()
defer func() {
if opt.Journal != nil {
if opt.Trace != nil {
opt.Dialog.Success = err == nil
opt.Dialog.CostSeconds = time.Since(ts).Seconds()
opt.Journal.AppendDialog(opt.Dialog)
opt.Trace.AppendDialog(opt.Dialog)
}
}()
@@ -253,8 +253,8 @@ func withJSONBody(method, url string, raw json.RawMessage, options ...Option) (b
f(opt)
}
opt.Header["Content-Type"] = "application/json; charset=utf-8"
if opt.Journal != nil {
opt.Header[journal.JournalHeader] = opt.Journal.ID()
if opt.Trace != nil {
opt.Header[trace.Header] = opt.Trace.ID()
}
ttl := opt.TTL
@@ -267,7 +267,7 @@ func withJSONBody(method, url string, raw json.RawMessage, options ...Option) (b
if opt.Dialog != nil {
decodedURL, _ := httpURL.QueryUnescape(url)
opt.Dialog.Request = &journal.Request{
opt.Dialog.Request = &trace.Request{
TTL: ttl.String(),
Method: method,
DecodedURL: decodedURL,

View File

@@ -3,13 +3,13 @@ package httpclient
import (
"time"
"github.com/xinliangnote/go-gin-api/internal/pkg/journal"
"github.com/xinliangnote/go-gin-api/internal/pkg/trace"
"go.uber.org/zap"
)
// Journal 记录内部流转信息
type Journal = journal.T
// Trace 记录内部流转信息
type Trace = trace.T
// Option 自定义设置http请求
type Option func(*option)
@@ -17,8 +17,8 @@ type Option func(*option)
type option struct {
TTL time.Duration
Header map[string]string
Journal *journal.Journal
Dialog *journal.Dialog
Trace *trace.Trace
Dialog *trace.Dialog
Logger *zap.Logger
RetryTimes int
RetryDelay time.Duration
@@ -44,12 +44,12 @@ func WithHeader(key, value string) Option {
}
}
// WithJournal 设置Journal以便记录内部流转信息
func WithJournal(j Journal) Option {
// WithTrace 设置trace信息
func WithTrace(t Trace) Option {
return func(opt *option) {
if j != nil {
opt.Journal = j.(*journal.Journal)
opt.Dialog = new(journal.Dialog)
if t != nil {
opt.Trace = t.(*trace.Trace)
opt.Dialog = new(trace.Dialog)
}
}
}

View File

@@ -9,7 +9,7 @@ import (
"net/url"
"time"
"github.com/xinliangnote/go-gin-api/internal/pkg/journal"
"github.com/xinliangnote/go-gin-api/internal/pkg/trace"
"github.com/pkg/errors"
"go.uber.org/zap"
@@ -49,7 +49,7 @@ func doHTTP(ctx context.Context, method, url string, payload []byte, opt *option
if err != nil {
err = errors.Wrapf(err, "do request [%s %s] err", method, url)
if opt.Dialog != nil {
opt.Dialog.AppendResponse(&journal.Response{
opt.Dialog.AppendResponse(&trace.Response{
Body: err.Error(),
CostSeconds: time.Since(ts).Seconds(),
})
@@ -66,7 +66,7 @@ func doHTTP(ctx context.Context, method, url string, payload []byte, opt *option
if err != nil {
err = errors.Wrapf(err, "read resp body from [%s %s] err", method, url)
if opt.Dialog != nil {
opt.Dialog.AppendResponse(&journal.Response{
opt.Dialog.AppendResponse(&trace.Response{
Body: err.Error(),
CostSeconds: time.Since(ts).Seconds(),
})
@@ -80,10 +80,10 @@ func doHTTP(ctx context.Context, method, url string, payload []byte, opt *option
defer func() {
if opt.Dialog != nil {
opt.Dialog.AppendResponse(&journal.Response{
opt.Dialog.AppendResponse(&trace.Response{
Header: resp.Header,
StatusCode: resp.StatusCode,
Status: resp.Status,
HttpCode: resp.StatusCode,
HttpCodeMsg: resp.Status,
Body: string(body), // unsafe
CostSeconds: time.Since(ts).Seconds(),
})

View File

@@ -5,3 +5,10 @@ import "testing"
func TestEncrypt(t *testing.T) {
t.Log(New().Encrypt("123456"))
}
func BenchmarkEncrypt(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
New().Encrypt("123456")
}
}

47
pkg/p/print.go Normal file
View File

@@ -0,0 +1,47 @@
package p
import (
"github.com/xinliangnote/go-gin-api/internal/pkg/trace"
"github.com/davecgh/go-spew/spew"
)
type Option func(*option)
type Trace = trace.T
type option struct {
Trace *trace.Trace
Debug *trace.Debug
}
func newOption() *option {
return &option{}
}
func Print(key string, value interface{}, options ...Option) {
opt := newOption()
defer func() {
if opt.Trace != nil {
opt.Debug.Key = key
opt.Debug.Value = value
opt.Trace.AppendDebug(opt.Debug)
}
}()
for _, f := range options {
f(opt)
}
spew.Dump(key, value)
}
// WithTrace 设置trace信息
func WithTrace(t Trace) Option {
return func(opt *option) {
if t != nil {
opt.Trace = t.(*trace.Trace)
opt.Debug = new(trace.Debug)
}
}
}

View File

@@ -4,8 +4,8 @@ import (
"testing"
)
func TestEncrypt(t *testing.T) {
publicKey := `-----BEGIN PUBLIC KEY-----
const (
publicKey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1O3p0JN0/RrP7eY3f81i
zPf16FS0WMNGCJkd+y5c6yBzUvN0IEeoxiIWIBhoMKH0pzlzBg0rfttojSodOgNo
m/UCAzAYEgdIsNee5LSN/7e0T2/QvsIAHINuA8gI8fGoGiSA2TEzpUo6aVXwhZT3
@@ -15,17 +15,7 @@ xLYEFN9h2MWYgxLm9Z0rLMrWwMM+E2rCs8tsxAD5sO9RZMJPl1C0FIsMR53ngqbz
owIDAQAB
-----END PUBLIC KEY-----`
rsaPublic := NewPublic(publicKey)
str, err := rsaPublic.Encrypt("123456")
if err != nil {
t.Error("rsa public encrypt error", err)
return
}
t.Log(str)
}
func TestDecrypt(t *testing.T) {
privateKey := `-----BEGIN RSA PRIVATE KEY-----
privateKey = `-----BEGIN RSA PRIVATE KEY-----
MIIEpgIBAAKCAQEA1O3p0JN0/RrP7eY3f81izPf16FS0WMNGCJkd+y5c6yBzUvN0
IEeoxiIWIBhoMKH0pzlzBg0rfttojSodOgNom/UCAzAYEgdIsNee5LSN/7e0T2/Q
vsIAHINuA8gI8fGoGiSA2TEzpUo6aVXwhZT34GGRdrSJ+m4iVk/Kt95tavBNk+ND
@@ -52,6 +42,19 @@ tN5Pb9To9gJTqpZRD+/cLOeFRrHBBjMK1z7fPKS/fN2B+JFVq7nD827t3+J0In4F
mS8gU20MMPAeV2z7khyDcSxlHsUyL73eLeaakbQov9NMW7cc99XX4wnP4W7FRpmr
QbHmKuHIRFHCFv+XX8c0aK2mDZMUlzJdy4FgD/YCEZ7kZMZKyvZW/ZuV
-----END RSA PRIVATE KEY-----`
)
func TestEncrypt(t *testing.T) {
rsaPublic := NewPublic(publicKey)
str, err := rsaPublic.Encrypt("123456")
if err != nil {
t.Error("rsa public encrypt error", err)
return
}
t.Log(str)
}
func TestDecrypt(t *testing.T) {
decryptStr := "KTKXckjkCLI6Vk_y_XROnY-a6nJpllruL-CX-v_2AFxfghA2tZ2nkQyS6d1-IIYMlgwm4ivwlzy-phLtaN9BB03htA5D9rwjA_JwYtqAG4iwuvgaDl2SiZ_H2ACv-aV1kNRgnyjh14hs0JiSt5VHEiJ3D2xYzOCKwtEzoo0WczJ-MYb3u_-bfcnm9YtvgtG5-y3Jy7WYr-IwXdBKqPO0E-jzrtY7m3Q1yC4znHdzjNpxCj0I6YRx4CZ362b706qNX7sl3E5KTJeSmYrsurB-SxQT1CaqGzVt7mshx1v2qGnv5NBNXpj7ZPKWGJbgaCUxcuxd1Mg0o81HnfbsGuSlFQ=="
rsaPrivate := NewPrivate(privateKey)
@@ -62,3 +65,13 @@ QbHmKuHIRFHCFv+XX8c0aK2mDZMUlzJdy4FgD/YCEZ7kZMZKyvZW/ZuV
}
t.Log(str)
}
func BenchmarkEncryptAndDecrypt(b *testing.B) {
b.ResetTimer()
rsaPublic := NewPublic(publicKey)
rsaPrivate := NewPrivate(privateKey)
for i := 0; i < b.N; i++ {
encryptString, _ := rsaPublic.Encrypt("123456")
rsaPrivate.Decrypt(encryptString)
}
}

View File

@@ -0,0 +1,33 @@
package time_parse
import "time"
var (
cst *time.Location
)
// CSTLayout China Standard Time Layout
const CSTLayout = "2006-01-02 15:04:05"
func init() {
var err error
if cst, err = time.LoadLocation("Asia/Shanghai"); err != nil {
panic(err)
}
}
// RFC3339ToCSTLayout convert rfc3339 value to china standard time layout
// 2020-11-08T08:18:46+08:00 => 2020-11-08 08:18:46
func RFC3339ToCSTLayout(value string) (string, error) {
ts, err := time.Parse(time.RFC3339, value)
if err != nil {
return "", err
}
return ts.In(cst).Format(CSTLayout), nil
}
func CSTLayoutString() string {
ts := time.Now()
return ts.In(cst).Format(CSTLayout)
}

View File

@@ -6,8 +6,18 @@ import (
"github.com/dgrijalva/jwt-go"
)
//var secret = configs.Get().JWT.Secret
var secret = "i1ydX9RtHyuJTrw7frcu"
var _ Token = (*token)(nil)
type Token interface {
// i 为了避免被其他包实现
i()
Sign(userId int, userName string) (tokenString string, err error)
Parse(tokenString string) (*claims, error)
}
type token struct {
secret string
}
type claims struct {
UserID int
@@ -15,7 +25,15 @@ type claims struct {
jwt.StandardClaims
}
func Sign(userId int, userName string) (tokenString string, err error) {
func New(secret string) Token {
return &token{
secret: secret,
}
}
func (t *token) i() {}
func (t *token) Sign(userId int, userName string) (tokenString string, err error) {
// The token content.
// iss: Issuer签发者
// iat: Issued At签发时间用Unix时间戳表示
@@ -34,13 +52,13 @@ func Sign(userId int, userName string) (tokenString string, err error) {
Issuer: "go-gin-api",
},
}
tokenString, err = jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(secret))
tokenString, err = jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(t.secret))
return
}
func Parse(tokenString string) (*claims, error) {
func (t *token) Parse(tokenString string) (*claims, error) {
tokenClaims, err := jwt.ParseWithClaims(tokenString, &claims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(secret), nil
return []byte(t.secret), nil
})
if tokenClaims != nil {

View File

@@ -4,10 +4,10 @@ import (
"testing"
)
// 执行 Test 时,先将 token.secret 设置值
const secret = "i1ydX9RtHyuJTrw7frcu"
func TestSign(t *testing.T) {
tokenString, err := Sign(123456789, "xinliangnote")
tokenString, err := New(secret).Sign(123456789, "xinliangnote")
if err != nil {
t.Error("sign error", err)
return
@@ -17,10 +17,19 @@ func TestSign(t *testing.T) {
func TestParse(t *testing.T) {
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEyMzQ1Njc4OSwidXNlcm5hbWUiOiJ4aW5saWFuZyIsImV4cCI6MTYwOTQ2NzcwNCwiaWF0IjoxNjA5MzgxMzA0LCJpc3MiOiJnby1naW4tYXBpIiwibmJmIjoxNjA5MzgxMzA0fQ.hccv8F713WpKcwiSldBrFLZz_2SZzOTPedPi-8ps7M4"
user, err := Parse(tokenString)
user, err := New(secret).Parse(tokenString)
if err != nil {
t.Error("parse error", err)
return
}
t.Log(user)
}
func BenchmarkSignAndParse(b *testing.B) {
b.ResetTimer()
token := New(secret)
for i := 0; i < b.N; i++ {
tokenString, _ := token.Sign(123456789, "xinliangnote")
token.Parse(tokenString)
}
}