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

@@ -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,75 +0,0 @@
package errno
import (
"encoding/json"
)
var _ Error = (*err)(nil)
type Error interface {
// i 为了避免被其他包实现
i()
// WithData 设置成功时返回的数据
WithData(data interface{}) Error
// WithID 设置当前请求的唯一ID
WithID(id string) Error
// GetCode 获取 Code
GetCode() int
// GetMsg 获取 Msg
GetMsg() string
// ToString 返回 JSON 格式的错误详情
ToString() string
}
type err struct {
Code int `json:"code"` // 业务编码
Msg string `json:"msg"` // 错误描述
Data interface{} `json:"data"` // 成功时返回的数据
ID string `json:"id,omitempty"` // 当前请求的唯一ID便于问题定位忽略也可以
}
func NewError(code int, msg string) Error {
return &err{
Code: code,
Msg: msg,
Data: nil,
}
}
func (e *err) i() {}
func (e *err) WithData(data interface{}) Error {
e.Data = data
return e
}
func (e *err) WithID(id string) Error {
e.ID = id
return e
}
func (e *err) GetCode() int {
return e.Code
}
func (e *err) GetMsg() string {
return e.Msg
}
// ToString 返回 JSON 格式的错误详情
func (e *err) ToString() string {
err := &struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
ID string `json:"id,omitempty"`
}{
Code: e.Code,
Msg: e.Msg,
Data: e.Data,
ID: e.ID,
}
raw, _ := json.Marshal(err)
return string(raw)
}

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

@@ -1,53 +0,0 @@
package token
import (
"time"
"github.com/dgrijalva/jwt-go"
)
//var secret = configs.Get().JWT.Secret
var secret = "i1ydX9RtHyuJTrw7frcu"
type claims struct {
UserID int
UserName string
jwt.StandardClaims
}
func Sign(userId int, userName string) (tokenString string, err error) {
// The token content.
// iss: Issuer签发者
// iat: Issued At签发时间用Unix时间戳表示
// exp: Expiration Time过期时间用Unix时间戳表示
// aud: Audience接收该JWT的一方
// sub: Subject该JWT的主题
// nbf: Not Before不要早于这个时间
// jti: JWT ID用于标识JWT的唯一ID
claims := claims{
userId,
userName,
jwt.StandardClaims{
NotBefore: time.Now().Unix(),
IssuedAt: time.Now().Unix(),
ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),
Issuer: "go-gin-api",
},
}
tokenString, err = jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(secret))
return
}
func Parse(tokenString string) (*claims, error) {
tokenClaims, err := jwt.ParseWithClaims(tokenString, &claims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(secret), nil
})
if tokenClaims != nil {
if claims, ok := tokenClaims.Claims.(*claims); ok && tokenClaims.Valid {
return claims, nil
}
}
return nil, err
}

View File

@@ -1,26 +0,0 @@
package token
import (
"testing"
)
// 执行 Test 时,先将 token.secret 设置值
func TestSign(t *testing.T) {
tokenString, err := Sign(123456789, "xinliangnote")
if err != nil {
t.Error("sign error", err)
return
}
t.Log(tokenString)
}
func TestParse(t *testing.T) {
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEyMzQ1Njc4OSwidXNlcm5hbWUiOiJ4aW5saWFuZyIsImV4cCI6MTYwOTQ2NzcwNCwiaWF0IjoxNjA5MzgxMzA0LCJpc3MiOiJnby1naW4tYXBpIiwibmJmIjoxNjA5MzgxMzA0fQ.hccv8F713WpKcwiSldBrFLZz_2SZzOTPedPi-8ps7M4"
user, err := Parse(tokenString)
if err != nil {
t.Error("parse error", err)
return
}
t.Log(user)
}

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
}