重构项目的实现,优化使用方法与使用逻辑

This commit is contained in:
2026-06-25 00:03:59 +08:00
parent a6e8101e09
commit 6072ec57e8
49 changed files with 1663 additions and 12534 deletions

26
middleware/clientip.go Normal file
View File

@@ -0,0 +1,26 @@
package middleware
import "net/http"
// GetClientIP 获取客户端真实 IP
func GetClientIP(r *http.Request) string {
xff := r.Header.Get("X-Forwarded-For")
if xff != "" {
for i := 0; i < len(xff); i++ {
if xff[i] == ',' {
return xff[:i]
}
}
return xff
}
if xri := r.Header.Get("X-Real-IP"); xri != "" {
return xri
}
remoteAddr := r.RemoteAddr
for i := len(remoteAddr) - 1; i >= 0; i-- {
if remoteAddr[i] == ':' {
return remoteAddr[:i]
}
}
return remoteAddr
}

View File

@@ -4,10 +4,9 @@ import (
"context"
"net/http"
"strings"
)
// LanguageKey context中存储语言的key
type languageKey struct{}
"git.toowon.com/jimmy/go-common/requestctx"
)
// LanguageHeaderName 语言请求头名称
const LanguageHeaderName = "X-Language"
@@ -15,15 +14,9 @@ const LanguageHeaderName = "X-Language"
// AcceptLanguageHeaderName Accept-Language 请求头名称
const AcceptLanguageHeaderName = "Accept-Language"
// DefaultLanguage 默认语言
const DefaultLanguage = "zh-CN"
// GetLanguageFromContext 从context中获取语言
// GetLanguageFromContext 从 context 中获取语言
func GetLanguageFromContext(ctx context.Context) string {
if lang, ok := ctx.Value(languageKey{}).(string); ok && lang != "" {
return lang
}
return DefaultLanguage
return requestctx.Language(ctx)
}
// Language 语言处理中间件
@@ -44,11 +37,10 @@ func Language(next http.Handler) http.Handler {
// 3. 如果都未设置,使用默认语言
if lang == "" {
lang = DefaultLanguage
lang = requestctx.DefaultLanguage
}
// 将语言存储到context中
ctx := context.WithValue(r.Context(), languageKey{}, lang)
ctx := requestctx.WithLanguage(r.Context(), lang)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
@@ -74,8 +66,7 @@ func LanguageWithDefault(defaultLanguage string) func(http.Handler) http.Handler
lang = defaultLanguage
}
// 将语言存储到context中
ctx := context.WithValue(r.Context(), languageKey{}, lang)
ctx := requestctx.WithLanguage(r.Context(), lang)
next.ServeHTTP(w, r.WithContext(ctx))
})
}

View File

@@ -2,17 +2,15 @@ package middleware
import (
"net/http"
"strconv"
"time"
"git.toowon.com/jimmy/go-common/logger"
)
// responseWriter 包装 http.ResponseWriter 以捕获状态码和响应大小
// responseWriter 包装 ResponseWriter 以捕获状态码
type responseWriter struct {
http.ResponseWriter
statusCode int
size int
}
func (rw *responseWriter) WriteHeader(statusCode int) {
@@ -20,202 +18,49 @@ func (rw *responseWriter) WriteHeader(statusCode int) {
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 跳过记录的路径列表(如健康检查接口)
Logger *logger.Logger
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"},
// }),
// )
// Logging HTTP 访问日志(固定 Info 级别)
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,
}
// 处理请求
start := time.Now()
rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(rw, r)
// 计算处理时间
duration := time.Since(startTime)
fields := map[string]any{
"method": r.Method,
"path": r.URL.Path,
"duration": time.Since(start).Milliseconds(),
}
// 记录日志
logHTTPRequest(config.Logger, r, rw, duration)
log := logger.FromContext(r.Context())
if config.Logger != nil {
log = logger.FromContextWithLogger(r.Context(), config.Logger)
}
log.Info("HTTP Request", fields)
})
}
}
// shouldSkipPath 检查是否应该跳过该路径
func shouldSkipPath(path string, skipPaths []string) bool {
for _, skipPath := range skipPaths {
if path == skipPath {
for _, skip := range skipPaths {
if path == skip {
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
}

View File

@@ -5,145 +5,43 @@ import (
"net/http"
"runtime/debug"
commonhttp "git.toowon.com/jimmy/go-common/http"
"git.toowon.com/jimmy/go-common/i18n"
"git.toowon.com/jimmy/go-common/logger"
)
// RecoveryConfig Recovery中间件配置
// 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{})
I18n *i18n.I18n
}
// 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"}`))
// },
// }),
// )
// Recovery Panic 恢复中间件,响应统一 JSON 200 + system.internal_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
fields := map[string]any{
"method": r.Method,
"path": r.URL.Path,
"error": fmt.Sprintf("%v", err),
"stack": string(debug.Stack()),
}
log := logger.FromContextWithLogger(r.Context(), nil)
if config != nil && config.Logger != nil {
log = logger.FromContextWithLogger(r.Context(), config.Logger)
}
log.Error("panic recovered", fields)
// 默认错误响应
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
h := commonhttp.NewHandler(w, r)
if config != nil && config.I18n != nil {
h = commonhttp.NewHandler(w, r, commonhttp.WithI18n(config.I18n))
}
h.Error("system.internal_error")
}
}()
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,
})
}

23
middleware/requestid.go Normal file
View File

@@ -0,0 +1,23 @@
package middleware
import (
"net/http"
"git.toowon.com/jimmy/go-common/logger"
"github.com/google/uuid"
)
// RequestID 为每个请求生成或透传 Request ID写入 context 与响应头
func RequestID() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Request-ID")
if id == "" {
id = uuid.NewString()
}
w.Header().Set("X-Request-ID", id)
ctx := logger.WithRequestID(r.Context(), id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}

View File

@@ -4,77 +4,48 @@ import (
"context"
"net/http"
"git.toowon.com/jimmy/go-common/requestctx"
"git.toowon.com/jimmy/go-common/tools"
)
// TimezoneKey context中存储时区的key
type timezoneKey struct{}
// TimezoneHeaderName 时区请求头名称
const TimezoneHeaderName = "X-Timezone"
// DefaultTimezone 默认时区
const DefaultTimezone = tools.AsiaShanghai
// GetTimezoneFromContext 从context中获取时区
// GetTimezoneFromContext 从 context 中获取时区
func GetTimezoneFromContext(ctx context.Context) string {
if tz, ok := ctx.Value(timezoneKey{}).(string); ok && tz != "" {
return tz
}
return DefaultTimezone
return requestctx.Timezone(ctx)
}
// Timezone 时区处理中间件
// 从请求头 X-Timezone 读取时区信息,如果未传递则使用默认时区 AsiaShanghai
// 时区信息会存储到context中可以通过 GetTimezoneFromContext 获取
func Timezone(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 从请求头获取时区
timezone := r.Header.Get(TimezoneHeaderName)
// 如果未传递时区信息,使用默认时区
if timezone == "" {
timezone = DefaultTimezone
timezone = requestctx.DefaultTimezone
}
// 验证时区是否有效
if _, err := tools.GetLocation(timezone); err != nil {
// 如果时区无效,使用默认时区
timezone = DefaultTimezone
timezone = requestctx.DefaultTimezone
}
// 将时区存储到context中
ctx := context.WithValue(r.Context(), timezoneKey{}, timezone)
ctx := requestctx.WithTimezone(r.Context(), timezone)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// TimezoneWithDefault 时区处理中间件(可自定义默认时区)
// defaultTimezone: 默认时区,如果未指定则使用 AsiaShanghai
func TimezoneWithDefault(defaultTimezone string) func(http.Handler) http.Handler {
// 验证默认时区是否有效
if _, err := tools.GetLocation(defaultTimezone); err != nil {
defaultTimezone = DefaultTimezone
defaultTimezone = requestctx.DefaultTimezone
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 从请求头获取时区
timezone := r.Header.Get(TimezoneHeaderName)
// 如果未传递时区信息,使用指定的默认时区
if timezone == "" {
timezone = defaultTimezone
}
// 验证时区是否有效
if _, err := tools.GetLocation(timezone); err != nil {
// 如果时区无效,使用默认时区
timezone = defaultTimezone
}
// 将时区存储到context中
ctx := context.WithValue(r.Context(), timezoneKey{}, timezone)
ctx := requestctx.WithTimezone(r.Context(), timezone)
next.ServeHTTP(w, r.WithContext(ctx))
})
}