This commit is contained in:
新亮
2021-01-23 16:46:01 +08:00
parent d17c58ffe1
commit 106d5b65d7
35 changed files with 811 additions and 502 deletions

View File

@@ -26,6 +26,7 @@
- third_party_requests当前请求涉及到调用第三方的信息
- debugs当前请求的调试信息
- sqls当前请求执行的 sql 信息
- redis当前请求执行的 redis 信息
- success当前请求结果
- cost_seconds执行时长单位秒
@@ -60,6 +61,7 @@
- [x] 支持设置 third_party_requests 三方请求信息
- [x] 支持设置 debugs 打印调试信息
- [x] 支持设置 sqls 执行 SQL 信息
- [x] 支持设置 redis 执行 Redis 信息
- [x] 可记录 cost_seconds 执行时长
- [x] [errno] 统一定义错误码
- [x] [env] 支持 FAT、UAT、PRO 环境

View File

@@ -49,7 +49,8 @@ type Config struct {
} `toml:"mail"`
JWT struct {
Secret string `toml:"secret"`
Secret string `toml:"secret"`
ExpireDuration time.Duration `toml:"expireDuration"`
} `toml:"jwt"`
Aes struct {

View File

@@ -1,37 +1,38 @@
[mysql]
[mysql.read]
addr = ''
user = ''
pass = ''
name = ''
[mysql.write]
addr = ''
user = ''
pass = ''
name = ''
[mysql.base]
maxOpenConn = 10
maxIdleConn = 60
connMaxLifeTime = 60
[mysql.read] # 从库信息,可读
addr = '127.0.0.1:3306' # MySQL 地址:端口
user = 'root' # 用户名
pass = 'root' # 密码
name = 'go_gin_api' # 数据库名称
[mysql.write] # 主库信息,可读写
addr = '127.0.0.1:3306' # MySQL 地址:端口
user = 'root' # 用户名
pass = 'root' # 密码
name = 'go_gin_api' # 数据库名称
[mysql.base] # 基础配置
maxOpenConn = 10 # 最大打开的连接数
maxIdleConn = 60 # 闲置的连接数
connMaxLifeTime = 60 # 最大连接超时(单位:分)
[redis]
addr = ''
pass = ''
db = 0
maxRetries = 3
poolSize = 10
minIdleConns = 5
addr = '127.0.0.1:6379' # Redis 地址:端口
pass = '' # 密码
db = 0 # 序号从 0 开始默认是0可以不用设置
maxRetries = 3 # 命令执行失败时,最多重试多少次,默认为 0 即不重试
poolSize = 10 # 连接池最大连接数,默认为 CPU 数 * 10
minIdleConns = 5 # 最小空闲连接数
[mail]
host = 'smtp.163.com'
port = 465
user = ""
pass = ""
to = ""
host = 'smtp.163.com' # 邮箱服务器,比如 smtp.163.com
port = 465 # 端口
user = "" # 发件人邮箱
pass = "" # 发件人邮箱密码或授权码,根据邮箱服务器而定
to = "" # 收件人邮箱,多个可以逗号(,)分割
[jwt]
secret = 'i1ydX9RtHyuJTrw7frcu'
secret = 'i1ydX9RtHyuJTrw7frcu' # JWT secret
expireDuration = 24 # JWT ExpiresAt 过期时间(单位:小时)
[aes]
key = 'IgkibX71IEf382PT'

View File

@@ -19,14 +19,18 @@ var doc = `{
"description": "{{.Description}}",
"title": "{{.Title}}",
"contact": {},
"license": {
"name": "MIT",
"url": "https://github.com/xinliangnote/go-gin-api/blob/master/LICENSE"
},
"version": "{{.Version}}"
},
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/demo/user/{name}": {
"get": {
"description": "获取用户信息",
"/auth/get": {
"post": {
"description": "获取授权信息",
"consumes": [
"application/json"
],
@@ -36,15 +40,31 @@ var doc = `{
"tags": [
"Demo"
],
"summary": "获取用户信息",
"summary": "获取授权信息",
"responses": {
"200": {
"description": "返回信息",
"schema": {
"$ref": "#/definitions/demo.authResponse"
}
}
}
}
},
"/demo/trace": {
"get": {
"description": "Trace 示例",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Demo"
],
"summary": "Trace 示例",
"parameters": [
{
"type": "string",
"description": "用户名(Tom)",
"name": "name",
"in": "path",
"required": true
},
{
"type": "string",
"description": "签名",
@@ -86,7 +106,7 @@ var doc = `{
"application/json"
],
"tags": [
"Demo"
"User"
],
"summary": "创建用户",
"parameters": [
@@ -98,6 +118,13 @@ var doc = `{
"schema": {
"$ref": "#/definitions/user_model.CreateRequest"
}
},
{
"type": "string",
"description": "签名",
"name": "Authorization",
"in": "header",
"required": true
}
],
"responses": {
@@ -110,6 +137,42 @@ var doc = `{
}
}
},
"/user/delete/{id}": {
"patch": {
"description": "删除用户 - 更新 is_deleted = 1",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"User"
],
"summary": "删除用户 - 更新 is_deleted = 1",
"parameters": [
{
"type": "integer",
"description": "用户ID",
"name": "id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "签名",
"name": "Authorization",
"in": "header",
"required": true
}
],
"responses": {
"200": {
"description": "返回信息"
}
}
}
},
"/user/info/{username}": {
"get": {
"description": "用户详情",
@@ -120,7 +183,7 @@ var doc = `{
"application/json"
],
"tags": [
"Demo"
"User"
],
"summary": "用户详情",
"parameters": [
@@ -130,6 +193,13 @@ var doc = `{
"name": "username",
"in": "path",
"required": true
},
{
"type": "string",
"description": "签名",
"name": "Authorization",
"in": "header",
"required": true
}
],
"responses": {
@@ -142,43 +212,9 @@ var doc = `{
}
}
},
"/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": "更新用户称",
"put": {
"description": "编辑用户 - 通过用户主键ID更新用户称",
"consumes": [
"application/json"
],
@@ -186,9 +222,9 @@ var doc = `{
"application/json"
],
"tags": [
"Demo"
"User"
],
"summary": "更新用户称",
"summary": "编辑用户 - 通过用户主键ID更新用户称",
"parameters": [
{
"description": "请求信息",
@@ -198,6 +234,13 @@ var doc = `{
"schema": {
"$ref": "#/definitions/user_model.UpdateNickNameByIDRequest"
}
},
{
"type": "string",
"description": "签名",
"name": "Authorization",
"in": "header",
"required": true
}
],
"responses": {
@@ -212,6 +255,19 @@ var doc = `{
}
},
"definitions": {
"demo.authResponse": {
"type": "object",
"properties": {
"authorization": {
"description": "签名",
"type": "string"
},
"expire_time": {
"description": "过期时间",
"type": "integer"
}
}
},
"user_model.CreateRequest": {
"type": "object",
"properties": {
@@ -259,32 +315,6 @@ var doc = `{
}
}
},
"user_model.LoginRequest": {
"type": "object",
"properties": {
"user_id": {
"description": "用户ID\u003e0",
"type": "integer"
},
"user_name": {
"description": "用户名",
"type": "string"
}
}
},
"user_model.LoginResponse": {
"type": "object",
"properties": {
"authorization": {
"description": "签名",
"type": "string"
},
"expire_time": {
"description": "过期时间",
"type": "integer"
}
}
},
"user_model.UpdateNickNameByIDRequest": {
"type": "object",
"properties": {

View File

@@ -2,13 +2,17 @@
"swagger": "2.0",
"info": {
"title": "go-gin-api docs api",
"contact": {}
"contact": {},
"license": {
"name": "MIT",
"url": "https://github.com/xinliangnote/go-gin-api/blob/master/LICENSE"
}
},
"host": "127.0.0.1:9999",
"paths": {
"/demo/user/{name}": {
"get": {
"description": "获取用户信息",
"/auth/get": {
"post": {
"description": "获取授权信息",
"consumes": [
"application/json"
],
@@ -18,15 +22,31 @@
"tags": [
"Demo"
],
"summary": "获取用户信息",
"summary": "获取授权信息",
"responses": {
"200": {
"description": "返回信息",
"schema": {
"$ref": "#/definitions/demo.authResponse"
}
}
}
}
},
"/demo/trace": {
"get": {
"description": "Trace 示例",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Demo"
],
"summary": "Trace 示例",
"parameters": [
{
"type": "string",
"description": "用户名(Tom)",
"name": "name",
"in": "path",
"required": true
},
{
"type": "string",
"description": "签名",
@@ -68,7 +88,7 @@
"application/json"
],
"tags": [
"Demo"
"User"
],
"summary": "创建用户",
"parameters": [
@@ -80,6 +100,13 @@
"schema": {
"$ref": "#/definitions/user_model.CreateRequest"
}
},
{
"type": "string",
"description": "签名",
"name": "Authorization",
"in": "header",
"required": true
}
],
"responses": {
@@ -92,6 +119,42 @@
}
}
},
"/user/delete/{id}": {
"patch": {
"description": "删除用户 - 更新 is_deleted = 1",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"User"
],
"summary": "删除用户 - 更新 is_deleted = 1",
"parameters": [
{
"type": "integer",
"description": "用户ID",
"name": "id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "签名",
"name": "Authorization",
"in": "header",
"required": true
}
],
"responses": {
"200": {
"description": "返回信息"
}
}
}
},
"/user/info/{username}": {
"get": {
"description": "用户详情",
@@ -102,7 +165,7 @@
"application/json"
],
"tags": [
"Demo"
"User"
],
"summary": "用户详情",
"parameters": [
@@ -112,6 +175,13 @@
"name": "username",
"in": "path",
"required": true
},
{
"type": "string",
"description": "签名",
"name": "Authorization",
"in": "header",
"required": true
}
],
"responses": {
@@ -124,43 +194,9 @@
}
}
},
"/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": "更新用户称",
"put": {
"description": "编辑用户 - 通过用户主键ID更新用户称",
"consumes": [
"application/json"
],
@@ -168,9 +204,9 @@
"application/json"
],
"tags": [
"Demo"
"User"
],
"summary": "更新用户称",
"summary": "编辑用户 - 通过用户主键ID更新用户称",
"parameters": [
{
"description": "请求信息",
@@ -180,6 +216,13 @@
"schema": {
"$ref": "#/definitions/user_model.UpdateNickNameByIDRequest"
}
},
{
"type": "string",
"description": "签名",
"name": "Authorization",
"in": "header",
"required": true
}
],
"responses": {
@@ -194,6 +237,19 @@
}
},
"definitions": {
"demo.authResponse": {
"type": "object",
"properties": {
"authorization": {
"description": "签名",
"type": "string"
},
"expire_time": {
"description": "过期时间",
"type": "integer"
}
}
},
"user_model.CreateRequest": {
"type": "object",
"properties": {
@@ -241,32 +297,6 @@
}
}
},
"user_model.LoginRequest": {
"type": "object",
"properties": {
"user_id": {
"description": "用户ID\u003e0",
"type": "integer"
},
"user_name": {
"description": "用户名",
"type": "string"
}
}
},
"user_model.LoginResponse": {
"type": "object",
"properties": {
"authorization": {
"description": "签名",
"type": "string"
},
"expire_time": {
"description": "过期时间",
"type": "integer"
}
}
},
"user_model.UpdateNickNameByIDRequest": {
"type": "object",
"properties": {

View File

@@ -1,4 +1,13 @@
definitions:
demo.authResponse:
properties:
authorization:
description: 签名
type: string
expire_time:
description: 过期时间
type: integer
type: object
user_model.CreateRequest:
properties:
mobile:
@@ -32,24 +41,6 @@ definitions:
description: 用户名
type: string
type: object
user_model.LoginRequest:
properties:
user_id:
description: 用户ID>0
type: integer
user_name:
description: 用户名
type: string
type: object
user_model.LoginResponse:
properties:
authorization:
description: 签名
type: string
expire_time:
description: 过期时间
type: integer
type: object
user_model.UpdateNickNameByIDRequest:
properties:
id:
@@ -68,19 +59,32 @@ definitions:
host: 127.0.0.1:9999
info:
contact: {}
license:
name: MIT
url: https://github.com/xinliangnote/go-gin-api/blob/master/LICENSE
title: go-gin-api docs api
paths:
/demo/user/{name}:
/auth/get:
post:
consumes:
- application/json
description: 获取授权信息
produces:
- application/json
responses:
"200":
description: 返回信息
schema:
$ref: '#/definitions/demo.authResponse'
summary: 获取授权信息
tags:
- Demo
/demo/trace:
get:
consumes:
- application/json
description: 获取用户信息
description: Trace 示例
parameters:
- description: 用户名(Tom)
in: path
name: name
required: true
type: string
- description: 签名
in: header
name: Authorization
@@ -102,7 +106,7 @@ paths:
type: string
type: object
type: array
summary: 获取用户信息
summary: Trace 示例
tags:
- Demo
/user/create:
@@ -117,6 +121,11 @@ paths:
required: true
schema:
$ref: '#/definitions/user_model.CreateRequest'
- description: 签名
in: header
name: Authorization
required: true
type: string
produces:
- application/json
responses:
@@ -126,7 +135,31 @@ paths:
$ref: '#/definitions/user_model.CreateResponse'
summary: 创建用户
tags:
- Demo
- User
/user/delete/{id}:
patch:
consumes:
- application/json
description: 删除用户 - 更新 is_deleted = 1
parameters:
- description: 用户ID
in: path
name: id
required: true
type: integer
- description: 签名
in: header
name: Authorization
required: true
type: string
produces:
- application/json
responses:
"200":
description: 返回信息
summary: 删除用户 - 更新 is_deleted = 1
tags:
- User
/user/info/{username}:
get:
consumes:
@@ -138,6 +171,11 @@ paths:
name: username
required: true
type: string
- description: 签名
in: header
name: Authorization
required: true
type: string
produces:
- application/json
responses:
@@ -147,34 +185,12 @@ paths:
$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
/user/update:
post:
put:
consumes:
- application/json
description: 更新用户
description: 编辑用户 - 通过用户主键ID更新用户
parameters:
- description: 请求信息
in: body
@@ -182,6 +198,11 @@ paths:
required: true
schema:
$ref: '#/definitions/user_model.UpdateNickNameByIDRequest'
- description: 签名
in: header
name: Authorization
required: true
type: string
produces:
- application/json
responses:
@@ -189,7 +210,7 @@ paths:
description: 返回信息
schema:
$ref: '#/definitions/user_model.UpdateNickNameByIDResponse'
summary: 更新用户
summary: 编辑用户 - 通过用户主键ID更新用户
tags:
- Demo
- User
swagger: "2.0"

1
go.mod
View File

@@ -4,7 +4,6 @@ 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

View File

@@ -18,10 +18,11 @@ var (
// 模块级错误码 - 用户模块
ErrUser = errno.NewError(http.StatusBadRequest, 20101, "非法用户")
ErrUserCreate = errno.NewError(http.StatusBadRequest, 20102, "创建用户失败")
ErrUserUpdate = errno.NewError(http.StatusBadRequest, 20103, "更新用户失败")
ErrUserSearch = errno.NewError(http.StatusBadRequest, 20104, "查询用户失败")
ErrUserHTTP = errno.NewError(http.StatusBadRequest, 20105, "调用他方接口失败")
ErrUserName = errno.NewError(http.StatusBadRequest, 20102, "账号不能为空")
ErrUserCreate = errno.NewError(http.StatusBadRequest, 20103, "创建用户失败")
ErrUserUpdate = errno.NewError(http.StatusBadRequest, 20104, "更新用户失败")
ErrUserSearch = errno.NewError(http.StatusBadRequest, 20105, "查询用户失败")
ErrUserHTTP = errno.NewError(http.StatusBadRequest, 20106, "调用他方接口失败")
// ...
)

View File

@@ -3,21 +3,32 @@ package demo
import (
"time"
"github.com/xinliangnote/go-gin-api/configs"
"github.com/xinliangnote/go-gin-api/internal/api/code"
"github.com/xinliangnote/go-gin-api/internal/api/repository/third_party_request/go_gin_api_repo"
"github.com/xinliangnote/go-gin-api/internal/api/service/user_service"
"github.com/xinliangnote/go-gin-api/internal/pkg/cache"
"github.com/xinliangnote/go-gin-api/internal/pkg/core"
"github.com/xinliangnote/go-gin-api/internal/pkg/db"
"github.com/xinliangnote/go-gin-api/pkg/httpclient"
"github.com/xinliangnote/go-gin-api/pkg/p"
"github.com/xinliangnote/go-gin-api/pkg/token"
"github.com/pkg/errors"
"go.uber.org/zap"
)
type Demo struct {
logger *zap.Logger
logger *zap.Logger
cache cache.Repo
userService user_service.UserService
}
func NewDemo(logger *zap.Logger) *Demo {
func NewDemo(logger *zap.Logger, db db.Repo, cache cache.Repo) *Demo {
return &Demo{
logger: logger,
logger: logger,
cache: cache,
userService: user_service.NewUserService(db, cache),
}
}
@@ -35,16 +46,16 @@ func (d *Demo) Get() core.HandlerFunc {
return func(c core.Context) {
req := new(request)
if err := c.ShouldBindURI(req); err != nil {
c.AbortWithError(code.ErrParamBind)
c.AbortWithError(code.ErrParamBind.WithErr(err))
return
}
if req.Name != "Tom" {
c.AbortWithError(code.ErrUser)
c.AbortWithError(code.ErrUser.WithErr(errors.New("req.Name != Tom")))
return
}
c.SetPayload(code.OK.WithData(&response{
c.Payload(code.OK.WithData(&response{
Name: "Tom",
Job: "Student",
}))
@@ -64,56 +75,70 @@ func (d *Demo) Post() core.HandlerFunc {
return func(c core.Context) {
req := new(request)
if err := c.ShouldBindPostForm(req); err != nil {
c.AbortWithError(code.ErrParamBind)
c.AbortWithError(code.ErrParamBind.WithErr(err))
return
}
if req.Name != "Jack" {
c.AbortWithError(code.ErrUser)
c.AbortWithError(code.ErrUser.WithErr(errors.New("req.Name != Jack")))
return
}
c.SetPayload(code.OK.WithData(&response{
c.Payload(code.OK.WithData(&response{
Name: "Jack",
Job: "Teacher",
}))
}
}
type request struct {
Name string `uri:"name"`
type authResponse struct {
Authorization string `json:"authorization"` // 签名
ExpireTime int64 `json:"expire_time"` // 过期时间
}
type response []struct {
type traceResponse []struct {
Name string `json:"name"` //用户名
Job string `json:"job"` //工作
}
// Get 获取用户信息
// @Summary 获取用户信息
// @Description 获取用户信息
// 获取授权信息
// @Summary 获取授权信息
// @Description 获取授权信息
// @Tags Demo
// @Accept json
// @Produce json
// @Param name path string true "用户名(Tom)"
// @Param Authorization header string true "签名"
// @Success 200 {object} response "用户信息"
// @Router /demo/user/{name} [get]
func (d *Demo) User() core.HandlerFunc {
// @Success 200 {object} authResponse "返回信息"
// @Router /auth/get [post]
func (d *Demo) Auth() core.HandlerFunc {
return func(c core.Context) {
req := new(request)
if err := c.ShouldBindURI(req); err != nil {
c.AbortWithError(code.ErrParamBind)
cfg := configs.Get().JWT
tokenString, err := token.New(cfg.Secret).Sign(1, "xinliangnote", time.Hour*cfg.ExpireDuration)
if err != nil {
c.AbortWithError(code.ErrAuthorization.WithErr(err))
return
}
if req.Name != "Tom" {
c.AbortWithError(code.ErrUser)
return
}
res := new(authResponse)
res.Authorization = tokenString
res.ExpireTime = time.Now().Add(time.Hour * cfg.ExpireDuration).Unix()
c.Payload(code.OK.WithData(res))
}
}
res1, err := go_gin_api_repo.DemoGet(req.Name,
// Trace 示例
// @Summary Trace 示例
// @Description Trace 示例
// @Tags Demo
// @Accept json
// @Produce json
// @Param Authorization header string true "签名"
// @Success 200 {object} traceResponse "用户信息"
// @Router /demo/trace [get]
func (d *Demo) Trace() core.HandlerFunc {
return func(c core.Context) {
// 三方请求信息
res1, err := go_gin_api_repo.DemoGet("Tom",
httpclient.WithTTL(time.Second*5),
httpclient.WithTrace(c.Trace()),
httpclient.WithLogger(c.Logger()),
@@ -123,10 +148,14 @@ func (d *Demo) User() core.HandlerFunc {
if err != nil {
d.logger.Error("get [demo/get] err", zap.Error(err))
c.AbortWithError(code.ErrUserHTTP)
c.AbortWithError(code.ErrUserHTTP.WithErr(err))
return
}
// 调试信息
p.Println("res1.Data.Name", res1.Data.Name, p.WithTrace(c.Trace()))
// 三方请求信息
res2, err := go_gin_api_repo.DemoPost("Jack",
httpclient.WithTTL(time.Second*5),
httpclient.WithTrace(c.Trace()),
@@ -137,11 +166,22 @@ func (d *Demo) User() core.HandlerFunc {
if err != nil {
d.logger.Error("post [demo/post] err", zap.Error(err))
c.AbortWithError(code.ErrUserHTTP)
c.AbortWithError(code.ErrUserHTTP.WithErr(err))
return
}
data := &response{
// 调试信息
p.Println("res2.Data.Name", res2.Data.Name, p.WithTrace(c.Trace()))
// 执行 SQL 信息
d.userService.GetUserByUserName(c, "test_user")
// 执行 Redis 信息
_ = d.cache.Set("name", "tom", time.Minute*10, cache.WithTrace(c.Trace()))
val, _ := d.cache.Get("name", cache.WithTrace(c.Trace()))
p.Println("redis-name", val, p.WithTrace(c.Trace()))
data := &traceResponse{
{
Name: res1.Data.Name,
Job: res1.Data.Job,
@@ -151,6 +191,6 @@ func (d *Demo) User() core.HandlerFunc {
Job: res2.Data.Job,
},
}
c.SetPayload(code.OK.WithData(data))
c.Payload(code.OK.WithData(data))
}
}

View File

@@ -1,14 +1,14 @@
package user_handler
import (
"github.com/xinliangnote/go-gin-api/configs"
"errors"
"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/cache"
"github.com/xinliangnote/go-gin-api/internal/pkg/core"
"github.com/xinliangnote/go-gin-api/pkg/token"
"github.com/xinliangnote/go-gin-api/internal/pkg/db"
"go.uber.org/zap"
)
@@ -18,24 +18,30 @@ var _ UserDemo = (*userDemo)(nil)
type UserDemo interface {
// i 为了避免被其他包实现
i()
// 创建用户
// Create 创建用户
Create() core.HandlerFunc
// 通过用户主键ID更新用户昵称
// UpdateNickNameByID 编辑用户 - 通过主键ID更新用户昵称
UpdateNickNameByID() core.HandlerFunc
// 用户登录
Login() core.HandlerFunc
// 用户详情
// Delete 删除用户 - 通过主键ID更新 is_deleted = 1
Delete() core.HandlerFunc
// Detail 用户详情
Detail() core.HandlerFunc
}
type userDemo struct {
logger *zap.Logger
cache cache.Repo
userService user_service.UserService
}
func NewUserDemo(logger *zap.Logger, db db_repo.Repo, cache cache_repo.Repo) UserDemo {
func NewUserDemo(logger *zap.Logger, db db.Repo, cache cache.Repo) UserDemo {
return &userDemo{
logger: logger,
cache: cache,
userService: user_service.NewUserService(db, cache),
}
}
@@ -45,10 +51,11 @@ func (u *userDemo) i() {}
// 创建用户
// @Summary 创建用户
// @Description 创建用户
// @Tags Demo
// @Tags User
// @Accept json
// @Produce json
// @Param RequestInfo body user_model.CreateRequest true "请求信息"
// @Param Authorization header string true "签名"
// @Success 200 {object} user_model.CreateResponse "返回信息"
// @Router /user/create [post]
func (u *userDemo) Create() core.HandlerFunc {
@@ -56,102 +63,92 @@ func (u *userDemo) Create() core.HandlerFunc {
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.AbortWithError(code.ErrParamBind)
c.AbortWithError(code.ErrParamBind.WithErr(err))
return
}
if req.UserName == "" {
c.AbortWithError(code.ErrUserName.WithErr(errors.New("req.UserName = ''")))
return
}
id, err := u.userService.Create(c, req)
if err != nil {
u.logger.Error("[user] Create err", zap.Error(err))
c.AbortWithError(code.ErrUserCreate)
c.AbortWithError(code.ErrUserCreate.WithErr(err))
return
}
res.Id = id
c.SetPayload(code.OK.WithData(res))
c.Payload(code.OK.WithData(res))
}
}
// 更新用户
// @Summary 更新用户
// @Description 更新用户
// @Tags Demo
// 编辑用户 - 通过用户主键ID更新用户
// @Summary 编辑用户 - 通过用户主键ID更新用户
// @Description 编辑用户 - 通过用户主键ID更新用户
// @Tags User
// @Accept json
// @Produce json
// @Param RequestInfo body user_model.UpdateNickNameByIDRequest true "请求信息"
// @Param Authorization header string true "签名"
// @Success 200 {object} user_model.UpdateNickNameByIDResponse "返回信息"
// @Router /user/update [post]
// @Router /user/update [put]
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.AbortWithError(code.ErrParamBind)
c.AbortWithError(code.ErrParamBind.WithErr(err))
return
}
err := u.userService.UpdateNickNameByID(c, req.Id, req.NickName)
if err != nil {
u.logger.Error("[user] UpdateNickNameByID err", zap.Error(err))
c.AbortWithError(code.ErrUserUpdate)
c.AbortWithError(code.ErrUserUpdate.WithErr(err))
return
}
res.Id = req.Id
c.SetPayload(code.OK.WithData(res))
c.Payload(code.OK.WithData(res))
}
}
// 用户登录
// @Summary 用户登录
// @Description 用户登录
// @Tags Demo
// 删除用户 - 更新 is_deleted = 1
// @Summary 删除用户 - 更新 is_deleted = 1
// @Description 删除用户 - 更新 is_deleted = 1
// @Tags User
// @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 {
// @Param id path int true "用户ID"
// @Param Authorization header string true "签名"
// @Success 200 "返回信息"
// @Router /user/delete/{id} [patch]
func (u *userDemo) Delete() 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.AbortWithError(code.ErrParamBind)
req := new(user_model.DeleteRequest)
if err := c.ShouldBindURI(req); err != nil {
c.AbortWithError(code.ErrParamBind.WithErr(err))
return
}
cfg := configs.Get().JWT
tokenString, err := token.New(cfg.Secret).Sign(req.UserID, req.UserName)
err := u.userService.Delete(c, req.Id)
if err != nil {
u.logger.Error("token sign err", zap.Error(err))
c.AbortWithError(code.ErrAuthorization)
c.AbortWithError(code.ErrUserUpdate.WithErr(err))
return
}
claims, err := token.New(cfg.Secret).Parse(tokenString)
if err != nil {
u.logger.Error("token parse err", zap.Error(err))
c.AbortWithError(code.ErrAuthorization)
return
}
res.Authorization = tokenString
res.ExpireTime = claims.ExpiresAt
c.SetPayload(code.OK.WithData(res))
c.Payload(code.OK.WithData(nil))
}
}
// 用户详情
// @Summary 用户详情
// @Description 用户详情
// @Tags Demo
// @Tags User
// @Accept json
// @Produce json
// @Param username path string true "用户名"
// @Param Authorization header string true "签名"
// @Success 200 {object} user_model.DetailResponse "返回信息"
// @Router /user/info/{username} [get]
func (u *userDemo) Detail() core.HandlerFunc {
@@ -159,15 +156,13 @@ func (u *userDemo) Detail() core.HandlerFunc {
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.AbortWithError(code.ErrParamBind)
c.AbortWithError(code.ErrParamBind.WithErr(err))
return
}
user, err := u.userService.GetUserByUserName(c, req.UserName)
if err != nil {
u.logger.Error("[user] GetUserByUserName err", zap.Error(err))
c.AbortWithError(code.ErrUserSearch)
c.AbortWithError(code.ErrUserSearch.WithErr(err))
return
}
@@ -175,6 +170,6 @@ func (u *userDemo) Detail() core.HandlerFunc {
res.UserName = user.UserName
res.NickName = user.NickName
res.Mobile = user.Mobile
c.SetPayload(code.OK.WithData(res))
c.Payload(code.OK.WithData(res))
}
}

View File

@@ -28,7 +28,7 @@ type CreateRequest struct {
// user_handler Create Response
type CreateResponse struct {
Id uint `json:"id"` //主键ID
Id uint `json:"id"` // 主键ID
}
// user_handler UpdateNickNameByID Request
@@ -42,16 +42,9 @@ 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 Delete Request
type DeleteRequest struct {
Id uint `uri:"id"` // 用户ID
}
// user_handler Detail Request

View File

@@ -2,8 +2,8 @@ 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/xinliangnote/go-gin-api/internal/pkg/db"
"github.com/pkg/errors"
)
@@ -16,14 +16,15 @@ type UserRepo interface {
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)
Delete(ctx core.Context, id uint) (err error)
getUserByID(ctx core.Context, id uint) (*user_model.UserDemo, error)
}
type userRepo struct {
db db_repo.Repo
db db.Repo
}
func NewUserRepo(db db_repo.Repo) UserRepo {
func NewUserRepo(db db.Repo) UserRepo {
return &userRepo{
db: db,
}
@@ -32,7 +33,7 @@ func NewUserRepo(db db_repo.Repo) UserRepo {
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
err = u.db.GetDbW().WithContext(ctx.RequestContext()).Create(&user).Error
if err != nil {
return 0, errors.Wrap(err, "[user_repo] create user err")
}
@@ -41,7 +42,7 @@ func (u *userRepo) Create(ctx core.Context, user user_model.UserDemo) (id uint,
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
err := u.db.GetDbR().WithContext(ctx.RequestContext()).First(data, id).Where("is_deleted = ?", -1).Error
if err != nil {
return nil, errors.Wrap(err, "[user_demo] get user data err")
}
@@ -53,15 +54,23 @@ func (u *userRepo) UpdateNickNameByID(ctx core.Context, id uint, nickname string
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
return u.db.GetDbW().WithContext(ctx.RequestContext()).Model(user).Update("nick_name", nickname).Error
}
func (u *userRepo) Delete(ctx core.Context, id uint) (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.RequestContext()).Model(user).Update("is_deleted", 1).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).
WithContext(ctx.RequestContext()).
Select([]string{"id", "user_name", "nick_name", "mobile"}).
Where("user_name = ?", username).
Where("user_name = ? and is_deleted = ?", username, -1).
First(data).Error
if err != nil {
return nil, errors.Wrap(err, "[user_demo] get user data err")

View File

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

View File

@@ -3,10 +3,10 @@ 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/api/router/middleware/auth"
"github.com/xinliangnote/go-gin-api/internal/pkg/cache"
"github.com/xinliangnote/go-gin-api/internal/pkg/core"
"github.com/xinliangnote/go-gin-api/internal/pkg/db"
"github.com/xinliangnote/go-gin-api/internal/pkg/metrics"
"github.com/xinliangnote/go-gin-api/internal/pkg/notify"
@@ -14,7 +14,7 @@ import (
"go.uber.org/zap"
)
func NewHTTPMux(logger *zap.Logger, db db_repo.Repo, cache cache_repo.Repo) (core.Mux, error) {
func NewHTTPMux(logger *zap.Logger, db db.Repo, cache cache.Repo) (core.Mux, error) {
if logger == nil {
return nil, errors.New("logger required")
@@ -31,20 +31,29 @@ func NewHTTPMux(logger *zap.Logger, db db_repo.Repo, cache cache_repo.Repo) (cor
panic(err)
}
demoHandler := demo.NewDemo(logger)
demoHandler := demo.NewDemo(logger, db, cache)
userHandler := user_handler.NewUserDemo(logger, db, cache)
u := mux.Group("/user")
// user_demo CURD
user := mux.Group("/user", core.WrapAuthHandler(auth.AuthHandler))
{
u.POST("/login", userHandler.Login())
u.POST("/create", userHandler.Create())
u.GET("/info/:username", core.AliasForRecordMetrics("/user/info"), userHandler.Detail())
u.POST("/update", userHandler.UpdateNickNameByID())
user.POST("/create", userHandler.Create())
user.PUT("/update", userHandler.UpdateNickNameByID())
user.PATCH("/delete/:id", userHandler.Delete())
user.GET("/info/:username", core.AliasForRecordMetrics("/user/info"), userHandler.Detail())
}
// auth
a := mux.Group("/auth")
{
a.POST("/get", demoHandler.Auth())
}
// demo
d := mux.Group("/demo", core.WrapAuthHandler(auth.AuthHandler)) //使用 auth 验证
{
d.GET("user/:name", core.AliasForRecordMetrics("/demo/user"), demoHandler.User())
// 为了演示 Trace ,增加了一些看起来无意义的调试信息和 SQL 信息。
d.GET("/trace", demoHandler.Trace())
// 模拟数据
d.GET("get/:name", core.AliasForRecordMetrics("/demo/get"), demoHandler.Get())

View File

@@ -2,10 +2,10 @@ 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/cache"
"github.com/xinliangnote/go-gin-api/internal/pkg/core"
"github.com/xinliangnote/go-gin-api/internal/pkg/db"
)
var _ UserService = (*userSer)(nil)
@@ -17,15 +17,16 @@ type UserService interface {
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)
Delete(ctx core.Context, id uint) (err error)
}
type userSer struct {
db db_repo.Repo
cache cache_repo.Repo
db db.Repo
cache cache.Repo
userRepo user_demo_repo.UserRepo
}
func NewUserService(db db_repo.Repo, cache cache_repo.Repo) UserService {
func NewUserService(db db.Repo, cache cache.Repo) UserService {
userRepo := user_demo_repo.NewUserRepo(db)
return &userSer{
db: db,
@@ -65,3 +66,11 @@ func (u *userSer) GetUserByUserName(ctx core.Context, username string) (user *us
}
return user, nil
}
func (u *userSer) Delete(ctx core.Context, id uint) (err error) {
err = u.userRepo.Delete(ctx, id)
if err != nil {
return nil
}
return nil
}

View File

@@ -1,25 +1,40 @@
package cache_repo
package cache
import (
"time"
"github.com/xinliangnote/go-gin-api/configs"
"github.com/xinliangnote/go-gin-api/internal/pkg/trace"
"github.com/xinliangnote/go-gin-api/pkg/time_parse"
"github.com/go-redis/redis/v7"
"github.com/pkg/errors"
)
type Option func(*option)
type Trace = trace.T
type option struct {
Trace *trace.Trace
Redis *trace.Redis
}
func newOption() *option {
return &option{}
}
var _ Repo = (*cacheRepo)(nil)
type Repo interface {
i()
Set(key, value string, ttl time.Duration) error
Get(key string) (string, error)
Set(key, value string, ttl time.Duration, options ...Option) error
Get(key string, options ...Option) (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
Incr(key string, options ...Option) int64
Close()
}
@@ -59,7 +74,23 @@ func redisConnect() (*redis.Client, error) {
}
// Set set some <key,value> into redis
func (c *cacheRepo) Set(key, value string, ttl time.Duration) error {
func (c *cacheRepo) Set(key, value string, ttl time.Duration, options ...Option) error {
opt := newOption()
defer func() {
if opt.Trace != nil {
opt.Redis.Timestamp = time_parse.CSTLayoutString()
opt.Redis.Handle = "set"
opt.Redis.Key = key
opt.Redis.Value = value
opt.Redis.TTL = ttl
opt.Trace.AppendRedis(opt.Redis)
}
}()
for _, f := range options {
f(opt)
}
if err := c.client.Set(key, value, ttl).Err(); err != nil {
return errors.Wrapf(err, "redis set key: %s err", key)
}
@@ -68,7 +99,21 @@ func (c *cacheRepo) Set(key, value string, ttl time.Duration) error {
}
// Get get some key from redis
func (c *cacheRepo) Get(key string) (string, error) {
func (c *cacheRepo) Get(key string, options ...Option) (string, error) {
opt := newOption()
defer func() {
if opt.Trace != nil {
opt.Redis.Timestamp = time_parse.CSTLayoutString()
opt.Redis.Handle = "get"
opt.Redis.Key = key
opt.Trace.AppendRedis(opt.Redis)
}
}()
for _, f := range options {
f(opt)
}
value, err := c.client.Get(key).Result()
if err != nil {
return "", errors.Wrapf(err, "redis get key: %s err", key)
@@ -109,7 +154,20 @@ func (c *cacheRepo) Del(keys ...string) bool {
return value > 0
}
func (c *cacheRepo) Incr(key string) int64 {
func (c *cacheRepo) Incr(key string, options ...Option) int64 {
opt := newOption()
defer func() {
if opt.Trace != nil {
opt.Redis.Timestamp = time_parse.CSTLayoutString()
opt.Redis.Handle = "incr"
opt.Redis.Key = key
opt.Trace.AppendRedis(opt.Redis)
}
}()
for _, f := range options {
f(opt)
}
value, _ := c.client.Incr(key).Result()
return value
}
@@ -118,3 +176,13 @@ func (c *cacheRepo) Incr(key string) int64 {
func (c *cacheRepo) Close() {
c.client.Close()
}
// WithTrace 设置trace信息
func WithTrace(t Trace) Option {
return func(opt *option) {
if t != nil {
opt.Trace = t.(*trace.Trace)
opt.Redis = new(trace.Redis)
}
}
}

View File

@@ -2,12 +2,12 @@ package core
import (
"bytes"
stdctx "context"
"io/ioutil"
"net/http"
"net/url"
"strings"
"sync"
"time"
"github.com/xinliangnote/go-gin-api/internal/pkg/trace"
"github.com/xinliangnote/go-gin-api/pkg/errno"
@@ -55,72 +55,90 @@ var _ Context = (*context)(nil)
type Context interface {
init()
// ShouldBindQuery 反序列化querystring
// tag: `form:"xxx"` (注不要写成query)
// ShouldBindQuery 反序列化 querystring
// tag: `form:"xxx"` (注:不要写成 query)
ShouldBindQuery(obj interface{}) error
// ShouldBindPostForm 反序列化postform(querystring会被忽略)
// ShouldBindPostForm 反序列化 postform (querystring会被忽略)
// tag: `form:"xxx"`
ShouldBindPostForm(obj interface{}) error
// ShouldBindForm 同时反序列化querystringpostform;
// 当querystringpostform存在相同字段时postform优先使用。
// ShouldBindForm 同时反序列化 querystringpostform;
// 当 querystringpostform 存在相同字段时postform 优先使用。
// tag: `form:"xxx"`
ShouldBindForm(obj interface{}) error
// ShouldBindJSON 反序列化postjson
// ShouldBindJSON 反序列化 postjson
// tag: `json:"xxx"`
ShouldBindJSON(obj interface{}) error
// ShouldBindURI 反序列化path参数(如路由路径为 /user/:name)
// ShouldBindURI 反序列化 path 参数(如路由路径为 /user/:name)
// tag: `uri:"xxx"`
ShouldBindURI(obj interface{}) error
// Redirect 重定向
Redirect(code int, location string)
// Trace 获取 Trace 对象
Trace() Trace
setTrace(trace Trace)
disableTrace()
// Logger 获取 Logger 对象
Logger() *zap.Logger
setLogger(logger *zap.Logger)
// Payload 正确返回
Payload(payload errno.Error)
getPayload() errno.Error
SetPayload(payload errno.Error)
Header() http.Header
GetHeader(key string) string
SetHeader(key, value string)
UserID() int
setUserID(userID int)
UserName() string
setUserName(userName string)
// AbortWithError 错误返回
AbortWithError(err errno.Error)
abortError() errno.Error
// Header 获取 Header 对象
Header() http.Header
// GetHeader 获取 Header
GetHeader(key string) string
// SetHeader 设置 Header
SetHeader(key, value string)
// UserID 获取 JWT 中 UserID
UserID() int64
setUserID(userID int64)
// UserName 获取 JWT 中 UserName
UserName() string
setUserName(userName string)
// Alias 设置路由别名 for metrics uri
Alias() string
setAlias(path string)
// RawData 获取 Request.Body
RawData() []byte
// Method 获取 Request.Method
Method() string
// Host 获取 Request.Host
Host() string
// Path 获取 请求的路径 Request.URL.Path (不附带 querystring)
Path() string
// URI 获取 unescape 后的 Request.URL.RequestURI()
URI() string
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
// RequestContext 获取请求的 context (当 client 关闭后,会自动 canceled)
RequestContext() StdContext
}
type context struct {
ctx *gin.Context
}
type StdContext struct {
stdctx.Context
Trace
*zap.Logger
}
func (c *context) init() {
body, err := c.ctx.GetRawData()
if err != nil {
@@ -137,7 +155,7 @@ func (c *context) ShouldBindQuery(obj interface{}) error {
return c.ctx.ShouldBindWith(obj, binding.Query)
}
// ShouldBindPostForm 反序列化postform(querystring会被忽略)
// ShouldBindPostForm 反序列化 postform (querystring 会被忽略)
// tag: `form:"xxx"`
func (c *context) ShouldBindPostForm(obj interface{}) error {
return c.ctx.ShouldBindWith(obj, binding.FormPost)
@@ -202,7 +220,7 @@ func (c *context) getPayload() errno.Error {
return payload.(errno.Error)
}
func (c *context) SetPayload(payload errno.Error) {
func (c *context) Payload(payload errno.Error) {
c.ctx.Set(_PayloadName, payload)
}
@@ -227,16 +245,16 @@ func (c *context) SetHeader(key, value string) {
c.ctx.Header(key, value)
}
func (c *context) UserID() int {
func (c *context) UserID() int64 {
val, ok := c.ctx.Get(_UserID)
if !ok {
return 0
}
return val.(int)
return val.(int64)
}
func (c *context) setUserID(userID int) {
func (c *context) setUserID(userID int64) {
c.ctx.Set(_UserID, userID)
}
@@ -315,25 +333,11 @@ func (c *context) URI() string {
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
// RequestContext 获取请求的 context (当client关闭后会自动canceled)
func (c *context) RequestContext() StdContext {
return StdContext{
c.ctx.Request.Context(),
c.Trace(),
c.Logger(),
}
if keyAsString, ok := key.(string); ok {
val, _ := c.ctx.Get(keyAsString)
return val
}
return nil
}

View File

@@ -121,11 +121,10 @@ func AliasForRecordMetrics(path string) HandlerFunc {
}
// WrapAuthHandler 用来处理 Auth 的入口在之后的handler中只需 ctx.UserID() ctx.UserName() 即可。
func WrapAuthHandler(handler func(Context) (userID int, userName string, err errno.Error)) HandlerFunc {
func WrapAuthHandler(handler func(Context) (userID int64, userName string, err errno.Error)) HandlerFunc {
return func(ctx Context) {
userID, userName, err := handler(ctx)
if err != nil {
ctx.Logger().Error("auth handler err", zap.Error(errors.WithStack(errors.New(err.GetMsg()))))
ctx.AbortWithError(err)
return
}
@@ -362,7 +361,7 @@ func New(logger *zap.Logger, options ...Option) (Mux, error) {
}
if err := context.abortError(); err != nil { // customer err
multierr.AppendInto(&abortErr, errors.New(err.GetMsg()))
multierr.AppendInto(&abortErr, err.GetErr())
response = err
}
} else {
@@ -389,7 +388,15 @@ func New(logger *zap.Logger, options ...Option) (Mux, error) {
uri = alias
}
opt.recordMetrics(context.Method(), uri, !ctx.IsAborted() && ctx.Writer.Status() == http.StatusOK, ctx.Writer.Status(), businessCode, time.Since(ts).Seconds(), traceId)
opt.recordMetrics(
context.Method(),
uri,
!ctx.IsAborted() && ctx.Writer.Status() == http.StatusOK,
ctx.Writer.Status(),
businessCode,
time.Since(ts).Seconds(),
traceId,
)
}
var t *trace.Trace
@@ -420,7 +427,12 @@ func New(logger *zap.Logger, options ...Option) (Mux, error) {
t.Success = !ctx.IsAborted() && ctx.Writer.Status() == http.StatusOK
t.CostSeconds = time.Since(ts).Seconds()
logger.Info("core-interceptor", zap.Any("trace", t))
if abortErr == nil {
logger.Info("core-interceptor", zap.Any("trace", t))
} else {
logger.Info("core-interceptor", zap.Any("trace", t), zap.Error(abortErr))
}
}()
ctx.Next()
@@ -436,6 +448,7 @@ func New(logger *zap.Logger, options ...Option) (Mux, error) {
context.AbortWithError(code.ErrManyRequest)
return
}
ctx.Next()
})
}
@@ -457,7 +470,7 @@ func New(logger *zap.Logger, options ...Option) (Mux, error) {
Host: ctx.Host(),
Status: "ok",
}
ctx.SetPayload(code.OK.WithData(resp))
ctx.Payload(code.OK.WithData(resp))
})
}

View File

@@ -1,4 +1,4 @@
package db_repo
package db
import (
"fmt"

View File

@@ -1,4 +1,4 @@
package db_repo
package db
import (
"time"
@@ -51,7 +51,7 @@ func before(db *gorm.DB) {
func after(db *gorm.DB) {
_ctx := db.Statement.Context
ctx, ok := _ctx.(core.Context)
ctx, ok := _ctx.(core.StdContext)
if !ok {
return
}
@@ -74,7 +74,7 @@ func after(db *gorm.DB) {
sqlInfo.Stack = utils.FileWithLineNum()
sqlInfo.Rows = db.Statement.RowsAffected
sqlInfo.CostSeconds = time.Since(ts).Seconds()
ctx.Trace().AppendSQL(sqlInfo)
ctx.Trace.AppendSQL(sqlInfo)
return
}

View File

@@ -5,30 +5,45 @@ import (
"github.com/spf13/cast"
)
// requestsCounter 定义计数器Counter
var requestsCounter = prometheus.NewCounterVec(
const (
namespace = "xinliangnote"
subsystem = "go_gin_api"
)
// metricsRequestsTotal metrics for request total 计数器Counter
var metricsRequestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "api_requests_total",
Namespace: namespace,
Subsystem: subsystem,
Name: "requests_total",
Help: "request(s) total",
},
[]string{"method", "path"},
)
// httpDurationsHistogram 定义累积直方图Histogram
var httpDurationsHistogram = prometheus.NewHistogramVec(
// metricsRequestsCost metrics for requests cost 累积直方图Histogram
var metricsRequestsCost = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "api_http_durations_histogram_seconds",
Buckets: []float64{0.01, 0.02, 0.03},
Namespace: namespace,
Subsystem: subsystem,
Name: "requests_cost",
Help: "request(s) cost seconds",
},
[]string{"method", "path", "success", "http_code", "business_code", "cost_seconds", "trace_id"},
)
func init() {
prometheus.MustRegister(requestsCounter, httpDurationsHistogram)
prometheus.MustRegister(metricsRequestsTotal, metricsRequestsCost)
}
// RecordMetrics 记录指标
func RecordMetrics(method, uri string, success bool, httpCode, businessCode int, costSeconds float64, traceId string) {
httpDurationsHistogram.With(prometheus.Labels{
metricsRequestsTotal.With(prometheus.Labels{
"method": method,
"path": uri,
}).Inc()
metricsRequestsCost.With(prometheus.Labels{
"method": method,
"path": uri,
"success": cast.ToString(success),
@@ -37,9 +52,4 @@ func RecordMetrics(method, uri string, success bool, httpCode, businessCode int,
"cost_seconds": cast.ToString(costSeconds),
"trace_id": traceId,
}).Observe(costSeconds)
requestsCounter.With(prometheus.Labels{
"method": method,
"path": uri,
}).Add(1)
}

View File

@@ -8,7 +8,7 @@ import (
"go.uber.org/zap"
)
// OnPanicNotify
// OnPanicNotify 发生 panic 时进行通知
func OnPanicNotify(ctx core.Context, err interface{}, stackInfo string) {
cfg := configs.Get().Mail
if cfg.Host == "" || cfg.Port == 0 || cfg.User == "" || cfg.Pass == "" || cfg.To == "" {
@@ -16,11 +16,19 @@ func OnPanicNotify(ctx core.Context, err interface{}, stackInfo string) {
return
}
subject, body, htmlErr := NewPanicHTMLEmail(ctx.Method(), ctx.Host(), ctx.URI(), ctx.Trace().ID(), err, stackInfo)
subject, body, htmlErr := NewPanicHTMLEmail(
ctx.Method(),
ctx.Host(),
ctx.URI(),
ctx.Trace().ID(),
err,
stackInfo,
)
if htmlErr != nil {
ctx.Logger().Error("NewPanicHTMLEmail error", zap.Error(htmlErr))
return
}
options := &mail.Options{
MailHost: cfg.Host,
MailPort: cfg.Port,
@@ -34,5 +42,6 @@ func OnPanicNotify(ctx core.Context, err interface{}, stackInfo string) {
if sendErr != nil {
ctx.Logger().Error("Mail Send error", zap.Error(sendErr))
}
return
}

View File

@@ -9,13 +9,13 @@ type D interface {
AppendResponse(resp *Response)
}
// Dialog 内部调用其它方接口的会话信息;失败时会有retry操作所以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"` // 执行时长单位
CostSeconds float64 `json:"cost_seconds"` // 执行时长(单位秒)
}
func (d *Dialog) i() {}

View File

@@ -0,0 +1,11 @@
package trace
import "time"
type Redis struct {
Timestamp string `json:"timestamp"` // 时间格式2006-01-02 15:04:05
Handle string `json:"handle"` // 操作function
Key string `json:"key"`
Value string `json:"value,omitempty"`
TTL time.Duration `json:"ttl,omitempty"`
}

View File

@@ -3,7 +3,7 @@ package trace
type SQL struct {
Timestamp string `json:"timestamp"` // 时间格式2006-01-02 15:04:05
Stack string `json:"stack"` // 文件地址和行号
CostSeconds float64 `json:"cost_seconds"` // 执行时长单位
CostSeconds float64 `json:"cost_seconds"` // 执行时长(单位秒)
SQL string `json:"sql"` // SQL 语句
Rows int64 `json:"rows_affected"` // 影响行数
}

View File

@@ -23,34 +23,35 @@ type T interface {
// 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"`
Identifier string `json:"trace_id"` // 链路ID
Request *Request `json:"request"` // 请求信息
Response *Response `json:"response"` // 返回信息
ThirdPartyRequests []*Dialog `json:"third_party_requests"` // 调用第三方接口的信息
Debugs []*Debug `json:"debugs"` // 调试信息
SQLs []*SQL `json:"sqls"` // 执行的 SQL 信息
Redis []*Redis `json:"redis"` // 执行的 Redis 信息
Success bool `json:"success"` // 请求结果 true or false
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"`
TTL string `json:"ttl"` // 请求超时时间
Method string `json:"method"` // 请求方式
DecodedURL string `json:"decoded_url"` // 请求地址
Header interface{} `json:"header"` // 请求 Header 信息
Body interface{} `json:"body"` // 请求 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"`
Header interface{} `json:"header"` // Header 信息
Body interface{} `json:"body"` // Body 信息
BusinessCode int `json:"business_code,omitempty"` // 业务码
BusinessCodeMsg string `json:"business_code_msg,omitempty"` // 提示信息
HttpCode int `json:"http_code"` // HTTP 状态码
HttpCodeMsg string `json:"http_code_msg"` // HTTP 状态码信息
CostSeconds float64 `json:"cost_seconds"` // 执行时间(单位秒)
}
func New(id string) *Trace {
@@ -122,3 +123,16 @@ func (t *Trace) AppendSQL(sql *SQL) *Trace {
t.SQLs = append(t.SQLs, sql)
return t
}
// AppendRedis 追加 Redis
func (t *Trace) AppendRedis(redis *Redis) *Trace {
if redis == nil {
return t
}
t.mux.Lock()
defer t.mux.Unlock()
t.Redis = append(t.Redis, redis)
return t
}

11
main.go
View File

@@ -7,9 +7,9 @@ import (
"time"
"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/internal/pkg/cache"
"github.com/xinliangnote/go-gin-api/internal/pkg/db"
"github.com/xinliangnote/go-gin-api/pkg/logger"
"github.com/xinliangnote/go-gin-api/pkg/shutdown"
@@ -24,6 +24,9 @@ import (
// @contact.url
// @contact.email
// @license.name MIT
// @license.url https://github.com/xinliangnote/go-gin-api/blob/master/LICENSE
// @host 127.0.0.1:9999
// @BasePath
func main() {
@@ -39,13 +42,13 @@ func main() {
defer loggers.Sync()
// 初始化数据库
dbRepo, err := db_repo.New()
dbRepo, err := db.New()
if err != nil {
loggers.Fatal("new db err", zap.Error(err))
}
// 初始化缓存
cacheRepo, err := cache_repo.New()
cacheRepo, err := cache.New()
if err != nil {
loggers.Fatal("new cache err", zap.Error(err))
}

View File

@@ -2,6 +2,8 @@ package errno
import (
"encoding/json"
"github.com/pkg/errors"
)
var _ Error = (*err)(nil)
@@ -13,12 +15,16 @@ type Error interface {
WithData(data interface{}) Error
// WithID 设置当前请求的唯一ID
WithID(id string) Error
// WithErr 设置错误信息
WithErr(err error) Error
// GetBusinessCode 获取 Business Code
GetBusinessCode() int
// GetHttpCode 获取 HTTP Code
GetHttpCode() int
// GetMsg 获取 Msg
GetMsg() string
// GetErr 获取错误信息
GetErr() error
// ToString 返回 JSON 格式的错误详情
ToString() string
}
@@ -26,8 +32,9 @@ type Error interface {
type err struct {
HttpCode int `json:"-"` // HTTP Code
BusinessCode int `json:"code"` // Business Code
Msg string `json:"msg"` // 错误描述
Data interface{} `json:"data"` // 成功时返回的数据
Msg string `json:"msg"` // 描述信息
Data interface{} `json:"data"` // 接口数据
Err error `json:"-"` // 错误信息
ID string `json:"id,omitempty"` // 当前请求的唯一ID便于问题定位忽略也可以
}
@@ -37,6 +44,7 @@ func NewError(httpCode, businessCode int, msg string) Error {
BusinessCode: businessCode,
Msg: msg,
Data: nil,
Err: nil,
}
}
@@ -52,6 +60,11 @@ func (e *err) WithID(id string) Error {
return e
}
func (e *err) WithErr(err error) Error {
e.Err = errors.WithStack(err)
return e
}
func (e *err) GetHttpCode() int {
return e.HttpCode
}
@@ -64,6 +77,10 @@ func (e *err) GetMsg() string {
return e.Msg
}
func (e *err) GetErr() error {
return e.Err
}
// ToString 返回 JSON 格式的错误详情
func (e *err) ToString() string {
err := &struct {

View File

@@ -29,6 +29,9 @@ var defaultClient = &http.Client{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
MaxIdleConns: 100,
MaxConnsPerHost: 100,
MaxIdleConnsPerHost: 100,
},
}
@@ -47,12 +50,11 @@ func doHTTP(ctx context.Context, method, url string, payload []byte, opt *option
return mock(), http.StatusOK, nil
}
req, err := http.NewRequest(method, url, bytes.NewReader(payload))
req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(payload))
if err != nil {
return nil, -1, errors.Wrapf(err, "new request [%s %s] err", method, url)
}
req = req.WithContext(ctx)
for key, value := range opt.header {
req.Header.Set(key, value[0])
}

View File

@@ -14,6 +14,7 @@ import (
const (
// DefaultLevel the default log level
DefaultLevel = zapcore.InfoLevel
// DefaultTimeLayout the default time layout;
DefaultTimeLayout = time.RFC3339
)

View File

@@ -7,7 +7,9 @@ import (
)
func TestJSONLogger(t *testing.T) {
logger, err := NewJSONLogger(WithField("defined_key", "defined_value"))
logger, err := NewJSONLogger(
WithField("defined_key", "defined_value"),
)
if err != nil {
t.Fatal(err)
}
@@ -18,3 +20,16 @@ func TestJSONLogger(t *testing.T) {
logger.Error("err occurs", WrapMeta(err, NewMeta("para1", "value1"), NewMeta("para2", "value2"))...)
}
func BenchmarkJsonLogger(b *testing.B) {
b.ResetTimer()
logger, err := NewJSONLogger(
WithField("defined_key", "defined_value"),
)
if err != nil {
b.Fatal(err)
}
defer logger.Sync()
}

View File

@@ -1,9 +1,9 @@
package p
import (
"github.com/xinliangnote/go-gin-api/internal/pkg/trace"
"fmt"
"github.com/davecgh/go-spew/spew"
"github.com/xinliangnote/go-gin-api/internal/pkg/trace"
)
type Option func(*option)
@@ -19,7 +19,7 @@ func newOption() *option {
return &option{}
}
func Print(key string, value interface{}, options ...Option) {
func Println(key string, value interface{}, options ...Option) {
opt := newOption()
defer func() {
if opt.Trace != nil {
@@ -33,7 +33,7 @@ func Print(key string, value interface{}, options ...Option) {
f(opt)
}
spew.Dump(key, value)
fmt.Println(fmt.Sprintf("KEY: %s | VALUE: %v", key, value))
}
// WithTrace 设置trace信息

View File

@@ -45,24 +45,24 @@ QbHmKuHIRFHCFv+XX8c0aK2mDZMUlzJdy4FgD/YCEZ7kZMZKyvZW/ZuV
)
func TestEncrypt(t *testing.T) {
rsaPublic := NewPublic(publicKey)
str, err := rsaPublic.Encrypt("123456")
str, err := NewPublic(publicKey).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)
str, err := rsaPrivate.Decrypt(decryptStr)
str, err := NewPrivate(privateKey).Decrypt(decryptStr)
if err != nil {
t.Error("rsa private decrypt error", err)
return
}
t.Log(str)
}

View File

@@ -11,7 +11,7 @@ var _ Token = (*token)(nil)
type Token interface {
// i 为了避免被其他包实现
i()
Sign(userId int, userName string) (tokenString string, err error)
Sign(userId int64, userName string, expireDuration time.Duration) (tokenString string, err error)
Parse(tokenString string) (*claims, error)
}
@@ -20,7 +20,7 @@ type token struct {
}
type claims struct {
UserID int
UserID int64
UserName string
jwt.StandardClaims
}
@@ -33,7 +33,7 @@ func New(secret string) Token {
func (t *token) i() {}
func (t *token) Sign(userId int, userName string) (tokenString string, err error) {
func (t *token) Sign(userId int64, userName string, expireDuration time.Duration) (tokenString string, err error) {
// The token content.
// iss: Issuer签发者
// iat: Issued At签发时间用Unix时间戳表示
@@ -48,8 +48,7 @@ func (t *token) Sign(userId int, userName string) (tokenString string, err error
jwt.StandardClaims{
NotBefore: time.Now().Unix(),
IssuedAt: time.Now().Unix(),
ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),
Issuer: "go-gin-api",
ExpiresAt: time.Now().Add(expireDuration).Unix(),
},
}
tokenString, err = jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(t.secret))

View File

@@ -2,12 +2,13 @@ package token
import (
"testing"
"time"
)
const secret = "i1ydX9RtHyuJTrw7frcu"
func TestSign(t *testing.T) {
tokenString, err := New(secret).Sign(123456789, "xinliangnote")
tokenString, err := New(secret).Sign(123456789, "xinliangnote", 24*time.Hour)
if err != nil {
t.Error("sign error", err)
return
@@ -29,7 +30,7 @@ func BenchmarkSignAndParse(b *testing.B) {
b.ResetTimer()
token := New(secret)
for i := 0; i < b.N; i++ {
tokenString, _ := token.Sign(123456789, "xinliangnote")
tokenString, _ := token.Sign(123456789, "xinliangnote", 24*time.Hour)
token.Parse(tokenString)
}
}