调整工具类的方法,优化方法调用及增加迁移工具及其用法

This commit is contained in:
2025-12-04 22:30:48 +08:00
parent de8fc13f18
commit 0650feb0d2
28 changed files with 3753 additions and 162 deletions

View File

@@ -43,6 +43,36 @@ func DefaultCORSConfig() *CORSConfig {
}
}
// NewCORSConfig 从配置参数创建 CORSConfig
// 用于从 config 包的 CORSConfig 转换为 middleware 的 CORSConfig
// 避免循环依赖
func NewCORSConfig(allowedOrigins, allowedMethods, allowedHeaders, exposedHeaders []string, allowCredentials bool, maxAge int) *CORSConfig {
cfg := &CORSConfig{
AllowedOrigins: allowedOrigins,
AllowedMethods: allowedMethods,
AllowedHeaders: allowedHeaders,
ExposedHeaders: exposedHeaders,
AllowCredentials: allowCredentials,
MaxAge: maxAge,
}
// 设置默认值(如果为空)
if len(cfg.AllowedOrigins) == 0 {
cfg.AllowedOrigins = []string{"*"}
}
if len(cfg.AllowedMethods) == 0 {
cfg.AllowedMethods = []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"}
}
if len(cfg.AllowedHeaders) == 0 {
cfg.AllowedHeaders = []string{"Content-Type", "Authorization", "X-Requested-With", "X-Timezone"}
}
if cfg.MaxAge == 0 {
cfg.MaxAge = 86400
}
return cfg
}
// CORS CORS中间件
func CORS(config ...*CORSConfig) func(http.Handler) http.Handler {
var cfg *CORSConfig

221
middleware/logging.go Normal file
View File

@@ -0,0 +1,221 @@
package middleware
import (
"net/http"
"strconv"
"time"
"git.toowon.com/jimmy/go-common/logger"
)
// responseWriter 包装 http.ResponseWriter 以捕获状态码和响应大小
type responseWriter struct {
http.ResponseWriter
statusCode int
size int
}
func (rw *responseWriter) WriteHeader(statusCode int) {
rw.statusCode = statusCode
rw.ResponseWriter.WriteHeader(statusCode)
}
func (rw *responseWriter) Write(b []byte) (int, error) {
size, err := rw.ResponseWriter.Write(b)
rw.size += size
return size, err
}
// LoggingConfig 日志中间件配置
type LoggingConfig struct {
// Logger 日志记录器可选如果为nil则使用默认logger
Logger *logger.Logger
// SkipPaths 跳过记录的路径列表(如健康检查接口)
SkipPaths []string
// LogRequestBody 是否记录请求体(谨慎使用,可能影响性能)
LogRequestBody bool
// LogResponseBody 是否记录响应体(谨慎使用,可能影响性能和内存)
LogResponseBody bool
}
// Logging HTTP请求日志中间件
// 记录每个HTTP请求的详细信息包括
// - 请求方法、路径、IP、User-Agent
// - 响应状态码、响应大小
// - 请求处理时间
//
// 使用方式1使用默认logger
//
// chain := middleware.NewChain(
// middleware.Logging(nil),
// )
//
// 使用方式2使用自定义logger
//
// myLogger, _ := logger.NewLogger(loggerConfig)
// chain := middleware.NewChain(
// middleware.Logging(&middleware.LoggingConfig{
// Logger: myLogger,
// SkipPaths: []string{"/health", "/metrics"},
// }),
// )
func Logging(config *LoggingConfig) func(http.Handler) http.Handler {
// 如果没有配置,使用默认配置
if config == nil {
config = &LoggingConfig{}
}
// 如果没有提供logger创建一个默认的
if config.Logger == nil {
// 使用默认配置创建logger输出到stdoutinfo级别
defaultLogger, err := logger.NewLogger(nil)
if err != nil {
// 如果创建失败使用nil后面会降级处理
config.Logger = nil
} else {
config.Logger = defaultLogger
}
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 检查是否跳过此路径
if shouldSkipPath(r.URL.Path, config.SkipPaths) {
next.ServeHTTP(w, r)
return
}
// 记录开始时间
startTime := time.Now()
// 包装 ResponseWriter 以捕获状态码和响应大小
rw := &responseWriter{
ResponseWriter: w,
statusCode: http.StatusOK, // 默认200
size: 0,
}
// 处理请求
next.ServeHTTP(rw, r)
// 计算处理时间
duration := time.Since(startTime)
// 记录日志
logHTTPRequest(config.Logger, r, rw, duration)
})
}
}
// shouldSkipPath 检查是否应该跳过该路径
func shouldSkipPath(path string, skipPaths []string) bool {
for _, skipPath := range skipPaths {
if path == skipPath {
return true
}
}
return false
}
// logHTTPRequest 记录HTTP请求日志
func logHTTPRequest(log *logger.Logger, r *http.Request, rw *responseWriter, duration time.Duration) {
// 获取客户端IP
clientIP := getClientIP(r)
// 构建日志字段
fields := map[string]interface{}{
"method": r.Method,
"path": r.URL.Path,
"query": r.URL.RawQuery,
"status": rw.statusCode,
"size": rw.size,
"duration": duration.Milliseconds(), // 毫秒
"ip": clientIP,
"user_agent": r.UserAgent(),
"referer": r.Referer(),
}
// 构建日志消息
message := "HTTP Request"
// 根据状态码选择日志级别
if log != nil {
// 使用提供的logger
if rw.statusCode >= 500 {
log.Errorf(fields, message)
} else if rw.statusCode >= 400 {
log.Warnf(fields, message)
} else {
log.Infof(fields, message)
}
} else {
// 降级处理:使用标准输出
// 注意:这是同步的,不会有性能问题
if rw.statusCode >= 500 {
logToStdout("ERROR", fields, message)
} else if rw.statusCode >= 400 {
logToStdout("WARN", fields, message)
} else {
logToStdout("INFO", fields, message)
}
}
}
// logToStdout 降级处理输出到标准输出当logger不可用时
func logToStdout(level string, fields map[string]interface{}, message string) {
// 简单的标准输出日志
var fieldStr string
for k, v := range fields {
fieldStr += " " + k + "=" + formatValue(v)
}
println("[" + level + "] " + message + fieldStr)
}
// formatValue 格式化值(用于日志输出)
func formatValue(v interface{}) string {
switch val := v.(type) {
case string:
return val
case int:
return strconv.Itoa(val)
case int64:
return strconv.FormatInt(val, 10)
default:
return ""
}
}
// getClientIP 获取客户端真实IP
// 优先级X-Forwarded-For > X-Real-IP > RemoteAddr
func getClientIP(r *http.Request) string {
// 尝试从 X-Forwarded-For 获取
xff := r.Header.Get("X-Forwarded-For")
if xff != "" {
// X-Forwarded-For 可能包含多个IP取第一个
for idx := 0; idx < len(xff); idx++ {
if xff[idx] == ',' {
return xff[:idx]
}
}
return xff
}
// 尝试从 X-Real-IP 获取
xri := r.Header.Get("X-Real-IP")
if xri != "" {
return xri
}
// 使用 RemoteAddr
remoteAddr := r.RemoteAddr
// 移除端口号
for i := len(remoteAddr) - 1; i >= 0; i-- {
if remoteAddr[i] == ':' {
return remoteAddr[:i]
}
}
return remoteAddr
}

286
middleware/ratelimit.go Normal file
View File

@@ -0,0 +1,286 @@
package middleware
import (
"net/http"
"sync"
"time"
)
// RateLimiter 限流器接口
type RateLimiter interface {
// Allow 检查是否允许请求
// key: 限流键如IP地址、用户ID等
// 返回: 是否允许, 剩余配额, 重置时间
Allow(key string) (allowed bool, remaining int, resetTime time.Time)
}
// tokenBucketLimiter 令牌桶限流器
type tokenBucketLimiter struct {
rate int // 每个窗口期允许的请求数
windowSize time.Duration // 窗口大小
buckets map[string]*bucket
mu sync.RWMutex
cleanupTicker *time.Ticker
stopCleanup chan struct{}
}
// bucket 令牌桶
type bucket struct {
tokens int // 当前令牌数
lastRefill time.Time // 上次填充时间
mu sync.Mutex
}
// NewTokenBucketLimiter 创建令牌桶限流器
func NewTokenBucketLimiter(rate int, windowSize time.Duration) RateLimiter {
limiter := &tokenBucketLimiter{
rate: rate,
windowSize: windowSize,
buckets: make(map[string]*bucket),
stopCleanup: make(chan struct{}),
}
// 启动清理goroutine定期清理过期的bucket
limiter.cleanupTicker = time.NewTicker(windowSize * 2)
go limiter.cleanup()
return limiter
}
// cleanup 定期清理过期的bucket
func (l *tokenBucketLimiter) cleanup() {
for {
select {
case <-l.cleanupTicker.C:
l.mu.Lock()
now := time.Now()
for key, bkt := range l.buckets {
bkt.mu.Lock()
// 如果bucket超过2个窗口期没有使用删除它
if now.Sub(bkt.lastRefill) > l.windowSize*2 {
delete(l.buckets, key)
}
bkt.mu.Unlock()
}
l.mu.Unlock()
case <-l.stopCleanup:
l.cleanupTicker.Stop()
return
}
}
}
// Allow 检查是否允许请求
func (l *tokenBucketLimiter) Allow(key string) (bool, int, time.Time) {
now := time.Now()
// 获取或创建bucket
l.mu.Lock()
bkt, exists := l.buckets[key]
if !exists {
bkt = &bucket{
tokens: l.rate,
lastRefill: now,
}
l.buckets[key] = bkt
}
l.mu.Unlock()
// 尝试消费令牌
bkt.mu.Lock()
defer bkt.mu.Unlock()
// 计算需要填充的令牌数
elapsed := now.Sub(bkt.lastRefill)
if elapsed >= l.windowSize {
// 窗口期已过,重新填充
bkt.tokens = l.rate
bkt.lastRefill = now
}
// 检查是否有可用令牌
if bkt.tokens > 0 {
bkt.tokens--
resetTime := bkt.lastRefill.Add(l.windowSize)
return true, bkt.tokens, resetTime
}
// 没有可用令牌
resetTime := bkt.lastRefill.Add(l.windowSize)
return false, 0, resetTime
}
// RateLimitConfig 限流中间件配置
type RateLimitConfig struct {
// Limiter 限流器(必需)
// 如果为nil会使用默认的令牌桶限流器100请求/分钟)
Limiter RateLimiter
// KeyFunc 生成限流键的函数(可选)
// 默认使用客户端IP作为键
// 可以自定义为用户ID、API Key等
KeyFunc func(r *http.Request) string
// OnRateLimitExceeded 当限流被触发时的回调(可选)
// 可以用于记录日志、发送告警等
OnRateLimitExceeded func(w http.ResponseWriter, r *http.Request, key string)
}
// RateLimit 限流中间件
// 实现基于令牌桶算法的请求限流
//
// 使用方式1使用默认配置100请求/分钟按IP限流
//
// chain := middleware.NewChain(
// middleware.RateLimit(nil),
// )
//
// 使用方式2自定义限流规则
//
// limiter := middleware.NewTokenBucketLimiter(10, time.Minute) // 10请求/分钟
// chain := middleware.NewChain(
// middleware.RateLimit(&middleware.RateLimitConfig{
// Limiter: limiter,
// }),
// )
//
// 使用方式3按用户ID限流
//
// chain := middleware.NewChain(
// middleware.RateLimit(&middleware.RateLimitConfig{
// Limiter: limiter,
// KeyFunc: func(r *http.Request) string {
// // 从请求头或token中获取用户ID
// return r.Header.Get("X-User-ID")
// },
// }),
// )
func RateLimit(config *RateLimitConfig) func(http.Handler) http.Handler {
// 如果没有配置,使用默认配置
if config == nil {
config = &RateLimitConfig{}
}
// 如果没有提供限流器创建默认的100请求/分钟)
if config.Limiter == nil {
config.Limiter = NewTokenBucketLimiter(100, time.Minute)
}
// 如果没有提供KeyFunc使用默认的客户端IP
if config.KeyFunc == nil {
config.KeyFunc = func(r *http.Request) string {
return getClientIP(r)
}
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 生成限流键
key := config.KeyFunc(r)
if key == "" {
// 如果无法生成键,允许请求通过
next.ServeHTTP(w, r)
return
}
// 检查是否允许请求
allowed, remaining, resetTime := config.Limiter.Allow(key)
// 设置限流相关的响应头
w.Header().Set("X-RateLimit-Limit", formatInt(config.Limiter.(*tokenBucketLimiter).rate))
w.Header().Set("X-RateLimit-Remaining", formatInt(remaining))
w.Header().Set("X-RateLimit-Reset", formatInt64(resetTime.Unix()))
if !allowed {
// 触发限流回调
if config.OnRateLimitExceeded != nil {
config.OnRateLimitExceeded(w, r, key)
}
// 返回429错误
w.Header().Set("Retry-After", formatInt64(int64(time.Until(resetTime).Seconds())))
http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
return
}
// 允许请求通过
next.ServeHTTP(w, r)
})
}
}
// RateLimitWithRate 使用指定速率创建限流中间件(便捷函数)
// rate: 每个窗口期允许的请求数
// windowSize: 窗口大小
func RateLimitWithRate(rate int, windowSize time.Duration) func(http.Handler) http.Handler {
return RateLimit(&RateLimitConfig{
Limiter: NewTokenBucketLimiter(rate, windowSize),
})
}
// RateLimitByIP 按IP限流便捷函数
func RateLimitByIP(rate int, windowSize time.Duration) func(http.Handler) http.Handler {
return RateLimit(&RateLimitConfig{
Limiter: NewTokenBucketLimiter(rate, windowSize),
KeyFunc: func(r *http.Request) string {
return getClientIP(r)
},
})
}
// formatInt 格式化int为字符串
func formatInt(n int) string {
if n == 0 {
return "0"
}
// 简单的int转字符串
var buf [20]byte
i := len(buf) - 1
negative := n < 0
if negative {
n = -n
}
for n > 0 {
buf[i] = byte('0' + n%10)
n /= 10
i--
}
if negative {
buf[i] = '-'
i--
}
return string(buf[i+1:])
}
// formatInt64 格式化int64为字符串
func formatInt64(n int64) string {
if n == 0 {
return "0"
}
// 简单的int64转字符串
var buf [20]byte
i := len(buf) - 1
negative := n < 0
if negative {
n = -n
}
for n > 0 {
buf[i] = byte('0' + n%10)
n /= 10
i--
}
if negative {
buf[i] = '-'
i--
}
return string(buf[i+1:])
}

149
middleware/recovery.go Normal file
View File

@@ -0,0 +1,149 @@
package middleware
import (
"fmt"
"net/http"
"runtime/debug"
"git.toowon.com/jimmy/go-common/logger"
)
// RecoveryConfig Recovery中间件配置
type RecoveryConfig struct {
// Logger 日志记录器可选如果为nil则使用默认logger
Logger *logger.Logger
// EnableStackTrace 是否在日志中包含堆栈跟踪
EnableStackTrace bool
// CustomHandler 自定义错误处理函数(可选)
// 如果设置了,会在记录日志后调用此函数
// 可以用于自定义错误响应格式
CustomHandler func(w http.ResponseWriter, r *http.Request, err interface{})
}
// Recovery Panic恢复中间件
// 捕获HTTP处理过程中的panic记录错误日志并返回500错误
// 防止panic导致整个服务崩溃
//
// 使用方式1使用默认配置
//
// chain := middleware.NewChain(
// middleware.Recovery(nil),
// )
//
// 使用方式2使用自定义配置
//
// myLogger, _ := logger.NewLogger(loggerConfig)
// chain := middleware.NewChain(
// middleware.Recovery(&middleware.RecoveryConfig{
// Logger: myLogger,
// EnableStackTrace: true,
// }),
// )
//
// 使用方式3自定义错误响应
//
// chain := middleware.NewChain(
// middleware.Recovery(&middleware.RecoveryConfig{
// Logger: myLogger,
// CustomHandler: func(w http.ResponseWriter, r *http.Request, err interface{}) {
// // 自定义JSON响应
// w.Header().Set("Content-Type", "application/json")
// w.WriteHeader(http.StatusInternalServerError)
// w.Write([]byte(`{"code":500,"message":"Internal Server Error"}`))
// },
// }),
// )
func Recovery(config *RecoveryConfig) func(http.Handler) http.Handler {
// 如果没有配置,使用默认配置
if config == nil {
config = &RecoveryConfig{
EnableStackTrace: true, // 默认启用堆栈跟踪
}
}
// 如果没有提供logger创建一个默认的
if config.Logger == nil {
defaultLogger, err := logger.NewLogger(nil)
if err != nil {
config.Logger = nil
} else {
config.Logger = defaultLogger
}
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
// 记录panic信息
logPanic(config.Logger, r, err, config.EnableStackTrace)
// 如果提供了自定义处理函数,调用它
if config.CustomHandler != nil {
config.CustomHandler(w, r, err)
return
}
// 默认错误响应
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
}
// logPanic 记录panic日志
func logPanic(log *logger.Logger, r *http.Request, err interface{}, enableStackTrace bool) {
// 获取堆栈跟踪
var stack string
if enableStackTrace {
stack = string(debug.Stack())
}
// 构建日志字段
fields := map[string]interface{}{
"method": r.Method,
"path": r.URL.Path,
"query": r.URL.RawQuery,
"ip": getClientIP(r),
"error": fmt.Sprintf("%v", err),
}
// 构建日志消息
message := "Panic recovered"
if enableStackTrace && stack != "" {
message += "\n" + stack
}
// 记录错误日志
if log != nil {
log.Errorf(fields, message)
} else {
// 降级处理:输出到标准错误
fmt.Printf("[ERROR] %s\n", message)
for k, v := range fields {
fmt.Printf(" %s: %v\n", k, v)
}
}
}
// RecoveryWithLogger 使用指定logger的Recovery中间件便捷函数
func RecoveryWithLogger(log *logger.Logger) func(http.Handler) http.Handler {
return Recovery(&RecoveryConfig{
Logger: log,
EnableStackTrace: true,
})
}
// RecoveryWithCustomHandler 使用自定义错误处理的Recovery中间件便捷函数
func RecoveryWithCustomHandler(customHandler func(w http.ResponseWriter, r *http.Request, err interface{})) func(http.Handler) http.Handler {
return Recovery(&RecoveryConfig{
EnableStackTrace: true,
CustomHandler: customHandler,
})
}