后端重构
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"kra/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// 全局错误日志实例
|
||||
var errorLogger *zap.Logger
|
||||
|
||||
// ErrorCreator 错误记录创建接口
|
||||
type ErrorCreator interface {
|
||||
CreateError(form, info string) error
|
||||
}
|
||||
|
||||
// 全局错误创建器
|
||||
var errorCreator ErrorCreator
|
||||
|
||||
// SetErrorLogger 设置错误日志
|
||||
func SetErrorLogger(logger *zap.Logger) {
|
||||
errorLogger = logger
|
||||
}
|
||||
|
||||
// SetErrorCreator 设置错误创建器
|
||||
func SetErrorCreator(creator ErrorCreator) {
|
||||
errorCreator = creator
|
||||
}
|
||||
|
||||
// ErrorHandler 错误处理中间件
|
||||
// 用于统一处理请求过程中产生的错误
|
||||
func ErrorHandler() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Next()
|
||||
|
||||
// 处理请求过程中产生的错误
|
||||
if len(c.Errors) > 0 {
|
||||
for _, e := range c.Errors {
|
||||
// 记录错误日志
|
||||
if errorLogger != nil {
|
||||
errorLogger.Error("请求错误",
|
||||
zap.String("path", c.Request.URL.Path),
|
||||
zap.String("method", c.Request.Method),
|
||||
zap.String("error", e.Error()),
|
||||
)
|
||||
}
|
||||
|
||||
// 保存错误到数据库
|
||||
if errorCreator != nil {
|
||||
_ = errorCreator.CreateError(c.Request.URL.Path, e.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// 如果还没有写入响应,返回错误信息
|
||||
if !c.Writer.Written() {
|
||||
lastErr := c.Errors.Last()
|
||||
response.FailWithMessage(lastErr.Error(), c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorResponse 错误响应结构
|
||||
type ErrorResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
}
|
||||
|
||||
// HandleError 处理错误并返回响应
|
||||
func HandleError(c *gin.Context, err error, message string) {
|
||||
if errorLogger != nil {
|
||||
errorLogger.Error(message,
|
||||
zap.String("path", c.Request.URL.Path),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
|
||||
// 保存错误到数据库
|
||||
if errorCreator != nil {
|
||||
_ = errorCreator.CreateError(c.Request.URL.Path, err.Error())
|
||||
}
|
||||
|
||||
response.FailWithMessage(message, c)
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"kra/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// LimitConfig IP限流配置
|
||||
type LimitConfig struct {
|
||||
// GenerationKey 根据业务生成key
|
||||
GenerationKey func(c *gin.Context) string
|
||||
// CheckOrMark 检查函数
|
||||
CheckOrMark func(key string, expire int, limit int) error
|
||||
// Expire key 过期时间(秒)
|
||||
Expire int
|
||||
// Limit 周期内限制次数
|
||||
Limit int
|
||||
}
|
||||
|
||||
var redisClient redis.UniversalClient
|
||||
|
||||
// SetRedisClient 设置Redis客户端
|
||||
func SetRedisClient(client redis.UniversalClient) {
|
||||
redisClient = client
|
||||
}
|
||||
|
||||
// LimitWithTime 返回限流中间件
|
||||
func (l LimitConfig) LimitWithTime() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if err := l.CheckOrMark(l.GenerationKey(c), l.Expire, l.Limit); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": response.ERROR, "msg": err.Error()})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultGenerationKey 默认生成key
|
||||
func DefaultGenerationKey(c *gin.Context) string {
|
||||
return "KRA_Limit" + c.ClientIP()
|
||||
}
|
||||
|
||||
// DefaultCheckOrMark 默认检查函数
|
||||
func DefaultCheckOrMark(key string, expire int, limit int) error {
|
||||
if redisClient == nil {
|
||||
return nil // Redis未配置,跳过限流
|
||||
}
|
||||
return SetLimitWithTime(key, limit, time.Duration(expire)*time.Second)
|
||||
}
|
||||
|
||||
// DefaultLimit 默认限流中间件
|
||||
func DefaultLimit(expire, limit int) gin.HandlerFunc {
|
||||
return LimitConfig{
|
||||
GenerationKey: DefaultGenerationKey,
|
||||
CheckOrMark: DefaultCheckOrMark,
|
||||
Expire: expire,
|
||||
Limit: limit,
|
||||
}.LimitWithTime()
|
||||
}
|
||||
|
||||
// SetLimitWithTime 设置访问次数
|
||||
func SetLimitWithTime(key string, limit int, expiration time.Duration) error {
|
||||
ctx := context.Background()
|
||||
count, err := redisClient.Exists(ctx, key).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
pipe := redisClient.TxPipeline()
|
||||
pipe.Incr(ctx, key)
|
||||
pipe.Expire(ctx, key, expiration)
|
||||
_, err = pipe.Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
times, err := redisClient.Get(ctx, key).Int()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if times >= limit {
|
||||
t, err := redisClient.PTTL(ctx, key).Result()
|
||||
if err != nil {
|
||||
return errors.New("请求太过频繁,请稍后再试")
|
||||
}
|
||||
return errors.New("请求太过频繁, 请 " + t.String() + " 后尝试")
|
||||
}
|
||||
|
||||
return redisClient.Incr(ctx, key).Err()
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// LogLayout 日志layout
|
||||
type LogLayout struct {
|
||||
Time time.Time `json:"time"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
Path string `json:"path"`
|
||||
Query string `json:"query,omitempty"`
|
||||
Body string `json:"body,omitempty"`
|
||||
IP string `json:"ip"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Cost time.Duration `json:"cost"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
// Logger 日志中间件配置
|
||||
type Logger struct {
|
||||
// Filter 用户自定义过滤
|
||||
Filter func(c *gin.Context) bool
|
||||
// FilterKeyword 关键字过滤
|
||||
FilterKeyword func(layout *LogLayout) bool
|
||||
// AuthProcess 鉴权处理
|
||||
AuthProcess func(c *gin.Context, layout *LogLayout)
|
||||
// Print 日志处理
|
||||
Print func(LogLayout)
|
||||
// Source 服务唯一标识
|
||||
Source string
|
||||
}
|
||||
|
||||
// SetLoggerMiddleware 设置日志中间件
|
||||
func (l Logger) SetLoggerMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
path := c.Request.URL.Path
|
||||
query := c.Request.URL.RawQuery
|
||||
|
||||
var body []byte
|
||||
if l.Filter != nil && !l.Filter(c) {
|
||||
body, _ = c.GetRawData()
|
||||
// 将原body塞回去
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(body))
|
||||
}
|
||||
|
||||
c.Next()
|
||||
|
||||
cost := time.Since(start)
|
||||
layout := LogLayout{
|
||||
Time: time.Now(),
|
||||
Path: path,
|
||||
Query: query,
|
||||
IP: c.ClientIP(),
|
||||
UserAgent: c.Request.UserAgent(),
|
||||
Error: strings.TrimRight(c.Errors.ByType(gin.ErrorTypePrivate).String(), "\n"),
|
||||
Cost: cost,
|
||||
Source: l.Source,
|
||||
}
|
||||
|
||||
if l.Filter != nil && !l.Filter(c) {
|
||||
layout.Body = string(body)
|
||||
}
|
||||
|
||||
if l.AuthProcess != nil {
|
||||
l.AuthProcess(c, &layout)
|
||||
}
|
||||
|
||||
if l.FilterKeyword != nil {
|
||||
l.FilterKeyword(&layout)
|
||||
}
|
||||
|
||||
l.Print(layout)
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultLogger 默认日志中间件
|
||||
func DefaultLogger() gin.HandlerFunc {
|
||||
return Logger{
|
||||
Print: func(layout LogLayout) {
|
||||
v, _ := json.Marshal(layout)
|
||||
fmt.Println(string(v))
|
||||
},
|
||||
Source: "KRA",
|
||||
}.SetLoggerMiddleware()
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// TimeoutMiddleware 创建超时中间件
|
||||
// timeout: 超时时间(例如:time.Second * 30)
|
||||
// 使用示例: router.GET("path", middleware.TimeoutMiddleware(30*time.Second), HandleFunc)
|
||||
func TimeoutMiddleware(timeout time.Duration) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), timeout)
|
||||
defer cancel()
|
||||
|
||||
c.Request = c.Request.WithContext(ctx)
|
||||
|
||||
// 使用 buffered channel 避免 goroutine 泄漏
|
||||
done := make(chan struct{}, 1)
|
||||
panicChan := make(chan interface{}, 1)
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
select {
|
||||
case panicChan <- p:
|
||||
default:
|
||||
}
|
||||
}
|
||||
select {
|
||||
case done <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}()
|
||||
c.Next()
|
||||
}()
|
||||
|
||||
select {
|
||||
case p := <-panicChan:
|
||||
panic(p)
|
||||
case <-done:
|
||||
return
|
||||
case <-ctx.Done():
|
||||
c.Header("Connection", "close")
|
||||
c.AbortWithStatusJSON(http.StatusGatewayTimeout, gin.H{
|
||||
"code": 504,
|
||||
"msg": "请求超时",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user