后端重构

This commit is contained in:
Yvan
2026-01-08 20:16:27 +08:00
parent 9cb6b965e0
commit f5f77b32fe
23 changed files with 1450 additions and 2 deletions
+86
View File
@@ -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()
}
+95
View File
@@ -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()
}
+55
View File
@@ -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
}
}
}