init
This commit is contained in:
183
serve/internal/middleware/auth.go
Normal file
183
serve/internal/middleware/auth.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/config"
|
||||
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/internal/common"
|
||||
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/internal/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
// JWTClaims JWT声明结构
|
||||
type JWTClaims struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Type string `json:"type"` // access, refresh
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// GenerateTokens 生成访问令牌和刷新令牌
|
||||
func GenerateTokens(userID int64, username, email string) (accessToken, refreshToken string, err error) {
|
||||
cfg := config.GlobalConfig
|
||||
now := time.Now()
|
||||
|
||||
// 生成访问令牌
|
||||
accessClaims := JWTClaims{
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
Email: email,
|
||||
Type: "access",
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(time.Duration(cfg.JWT.AccessTokenTTL) * time.Second)),
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
NotBefore: jwt.NewNumericDate(now),
|
||||
Issuer: cfg.App.Name,
|
||||
Subject: utils.Int64ToString(userID),
|
||||
ID: utils.GenerateUUID(),
|
||||
},
|
||||
}
|
||||
|
||||
accessTokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
|
||||
accessToken, err = accessTokenObj.SignedString([]byte(cfg.JWT.Secret))
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// 生成刷新令牌
|
||||
refreshClaims := JWTClaims{
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
Email: email,
|
||||
Type: "refresh",
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(time.Duration(cfg.JWT.RefreshTokenTTL) * time.Second)),
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
NotBefore: jwt.NewNumericDate(now),
|
||||
Issuer: cfg.App.Name,
|
||||
Subject: utils.Int64ToString(userID),
|
||||
ID: utils.GenerateUUID(),
|
||||
},
|
||||
}
|
||||
|
||||
refreshTokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
|
||||
refreshToken, err = refreshTokenObj.SignedString([]byte(cfg.JWT.Secret))
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return accessToken, refreshToken, nil
|
||||
}
|
||||
|
||||
// ParseToken 解析JWT令牌
|
||||
func ParseToken(tokenString string) (*JWTClaims, error) {
|
||||
cfg := config.GlobalConfig
|
||||
|
||||
token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(cfg.JWT.Secret), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
return nil, jwt.ErrInvalidKey
|
||||
}
|
||||
|
||||
// AuthMiddleware JWT认证中间件
|
||||
func AuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 从请求头获取token
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
common.UnauthorizedResponse(c, "缺少认证令牌")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查Bearer前缀
|
||||
parts := strings.SplitN(authHeader, " ", 2)
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
common.UnauthorizedResponse(c, "认证令牌格式错误")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
tokenString := parts[1]
|
||||
|
||||
// 解析token
|
||||
claims, err := ParseToken(tokenString)
|
||||
if err != nil {
|
||||
if err == jwt.ErrTokenExpired {
|
||||
common.ErrorResponse(c, http.StatusUnauthorized, "访问令牌已过期")
|
||||
} else {
|
||||
common.UnauthorizedResponse(c, "无效的访问令牌")
|
||||
}
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查token类型
|
||||
if claims.Type != "access" {
|
||||
common.UnauthorizedResponse(c, "令牌类型错误")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 将用户信息存储到上下文中
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("username", claims.Username)
|
||||
c.Set("email", claims.Email)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// OptionalAuthMiddleware 可选认证中间件(不强制要求登录)
|
||||
func OptionalAuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 从请求头获取token
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查Bearer前缀
|
||||
parts := strings.SplitN(authHeader, " ", 2)
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
tokenString := parts[1]
|
||||
|
||||
// 解析token
|
||||
claims, err := ParseToken(tokenString)
|
||||
if err != nil {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查token类型
|
||||
if claims.Type != "access" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 将用户信息存储到上下文中
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("username", claims.Username)
|
||||
c.Set("email", claims.Email)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
40
serve/internal/middleware/cors.go
Normal file
40
serve/internal/middleware/cors.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// CORS 跨域中间件
|
||||
func CORS() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
method := c.Request.Method
|
||||
origin := c.Request.Header.Get("Origin")
|
||||
|
||||
// 设置允许的域名
|
||||
if origin != "" {
|
||||
c.Header("Access-Control-Allow-Origin", origin)
|
||||
}
|
||||
|
||||
// 设置允许的请求头
|
||||
c.Header("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
|
||||
|
||||
// 设置允许的请求方法
|
||||
c.Header("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE, PATCH")
|
||||
|
||||
// 设置是否允许携带凭证
|
||||
c.Header("Access-Control-Allow-Credentials", "true")
|
||||
|
||||
// 设置预检请求的缓存时间
|
||||
c.Header("Access-Control-Max-Age", "86400")
|
||||
|
||||
// 处理预检请求
|
||||
if method == "OPTIONS" {
|
||||
c.AbortWithStatus(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
68
serve/internal/middleware/error_handler.go
Normal file
68
serve/internal/middleware/error_handler.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/internal/logger"
|
||||
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/internal/common"
|
||||
)
|
||||
|
||||
// ErrorHandler 全局错误处理中间件
|
||||
func ErrorHandler() gin.HandlerFunc {
|
||||
return gin.CustomRecovery(func(c *gin.Context, recovered interface{}) {
|
||||
if err, ok := recovered.(string); ok {
|
||||
logger.WithFields(map[string]interface{}{
|
||||
"error": err,
|
||||
"method": c.Request.Method,
|
||||
"path": c.Request.URL.Path,
|
||||
"ip": c.ClientIP(),
|
||||
"user_agent": c.Request.UserAgent(),
|
||||
"stack": string(debug.Stack()),
|
||||
}).Error("Panic recovered")
|
||||
}
|
||||
if err, ok := recovered.(error); ok {
|
||||
logger.WithFields(map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"method": c.Request.Method,
|
||||
"path": c.Request.URL.Path,
|
||||
"ip": c.ClientIP(),
|
||||
"user_agent": c.Request.UserAgent(),
|
||||
"stack": string(debug.Stack()),
|
||||
}).Error("Panic recovered")
|
||||
}
|
||||
|
||||
// 返回统一的错误响应
|
||||
common.ErrorResponse(c, http.StatusInternalServerError, "Internal server error")
|
||||
c.Abort()
|
||||
})
|
||||
}
|
||||
|
||||
// RequestLogger 请求日志中间件
|
||||
func RequestLogger() gin.HandlerFunc {
|
||||
return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
|
||||
logger.WithFields(map[string]interface{}{
|
||||
"timestamp": param.TimeStamp.Format("2006-01-02 15:04:05"),
|
||||
"status_code": param.StatusCode,
|
||||
"latency": param.Latency.String(),
|
||||
"client_ip": param.ClientIP,
|
||||
"method": param.Method,
|
||||
"path": param.Path,
|
||||
"user_agent": param.Request.UserAgent(),
|
||||
"error": param.ErrorMessage,
|
||||
}).Info("HTTP Request")
|
||||
return ""
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
// RateLimiter 简单的速率限制中间件(基于IP)
|
||||
func RateLimiter() gin.HandlerFunc {
|
||||
// 这里可以集成更复杂的限流库,如 golang.org/x/time/rate
|
||||
return func(c *gin.Context) {
|
||||
// 简单实现,实际项目中应该使用更完善的限流算法
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
304
serve/internal/middleware/logger.go
Normal file
304
serve/internal/middleware/logger.go
Normal file
@@ -0,0 +1,304 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/internal/logger"
|
||||
)
|
||||
|
||||
// Logger 保留原始Gin格式化日志(如需)
|
||||
func Logger() gin.HandlerFunc {
|
||||
return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
|
||||
return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
|
||||
param.ClientIP,
|
||||
param.TimeStamp.Format(time.RFC1123),
|
||||
param.Method,
|
||||
param.Path,
|
||||
param.Request.Proto,
|
||||
param.StatusCode,
|
||||
param.Latency,
|
||||
param.Request.UserAgent(),
|
||||
param.ErrorMessage,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// Recovery 恢复中间件
|
||||
func Recovery() gin.HandlerFunc {
|
||||
return gin.Recovery()
|
||||
}
|
||||
|
||||
// bodyLogWriter 用于捕获响应体内容
|
||||
type bodyLogWriter struct {
|
||||
gin.ResponseWriter
|
||||
body *bytes.Buffer
|
||||
}
|
||||
|
||||
func (w *bodyLogWriter) Write(b []byte) (int, error) {
|
||||
if w.body != nil {
|
||||
w.body.Write(b)
|
||||
}
|
||||
return w.ResponseWriter.Write(b)
|
||||
}
|
||||
|
||||
// RequestResponseLogger 记录清晰的请求入参与响应信息
|
||||
func RequestResponseLogger() gin.HandlerFunc {
|
||||
const maxBodyLogSize = 10000 // 最大记录的body长度,超过则截断(增加到10000)
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
|
||||
// 捕获请求体(仅记录JSON,避免记录文件/二进制数据)
|
||||
reqCT := c.GetHeader("Content-Type")
|
||||
var reqBodyStr string
|
||||
if strings.Contains(reqCT, "application/json") {
|
||||
if c.Request.Body != nil {
|
||||
data, _ := io.ReadAll(c.Request.Body)
|
||||
// 复位Body以便后续业务读取
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(data))
|
||||
reqBodyStr = maskSensitiveJSON(string(data))
|
||||
reqBodyStr = truncate(reqBodyStr, maxBodyLogSize)
|
||||
}
|
||||
} else {
|
||||
// 对于表单/多部分/其他类型,不直接记录内容,避免污染日志与隐私泄露
|
||||
reqBodyStr = "(body skipped for non-JSON content)"
|
||||
}
|
||||
|
||||
// 包装响应写入器,捕获响应体
|
||||
blw := &bodyLogWriter{ResponseWriter: c.Writer, body: &bytes.Buffer{}}
|
||||
c.Writer = blw
|
||||
|
||||
// 执行后续处理
|
||||
c.Next()
|
||||
|
||||
latency := time.Since(start)
|
||||
status := c.Writer.Status()
|
||||
respCT := c.Writer.Header().Get("Content-Type")
|
||||
respBodyStr := blw.body.String()
|
||||
if strings.Contains(respCT, "application/json") {
|
||||
respBodyStr = truncate(respBodyStr, maxBodyLogSize)
|
||||
} else if respBodyStr != "" {
|
||||
respBodyStr = fmt.Sprintf("(non-JSON response, %d bytes)", len(respBodyStr))
|
||||
} else {
|
||||
respBodyStr = "(empty)"
|
||||
}
|
||||
|
||||
// 路径(优先使用路由匹配的完整路径)
|
||||
path := c.FullPath()
|
||||
if path == "" {
|
||||
path = c.Request.URL.Path
|
||||
}
|
||||
|
||||
// 头信息(仅记录关键字段,并进行脱敏)
|
||||
auth := c.GetHeader("Authorization")
|
||||
if auth != "" {
|
||||
auth = maskToken(auth)
|
||||
}
|
||||
|
||||
// 错误聚合
|
||||
var errMsg string
|
||||
if len(c.Errors) > 0 {
|
||||
errMsg = c.Errors.String()
|
||||
}
|
||||
|
||||
// 根据状态码选择emoji和日志级别
|
||||
statusEmoji := getStatusEmoji(status)
|
||||
logLevel := getLogLevel(status)
|
||||
|
||||
// 获取或生成Request ID
|
||||
requestID := c.GetString("request_id")
|
||||
if requestID == "" {
|
||||
requestID = "N/A"
|
||||
}
|
||||
|
||||
// 获取User ID(如果有)
|
||||
userID := "N/A"
|
||||
if uid, exists := c.Get("user_id"); exists {
|
||||
userID = fmt.Sprintf("%v", uid)
|
||||
}
|
||||
|
||||
// 格式化耗时
|
||||
latencyStr := formatLatency(latency)
|
||||
|
||||
// 构建美化的控制台日志
|
||||
separator := "================================================================================"
|
||||
fmt.Printf("\n%s\n", separator)
|
||||
fmt.Printf("🌐 %s %s | %s %d | ⏱️ %s\n", c.Request.Method, path, statusEmoji, status, latencyStr)
|
||||
fmt.Printf("📍 IP: %s | 🆔 Request ID: %s | 👤 User ID: %s\n", c.ClientIP(), requestID, userID)
|
||||
|
||||
if c.Request.URL.RawQuery != "" {
|
||||
fmt.Printf("🔗 Query: %s\n", c.Request.URL.RawQuery)
|
||||
}
|
||||
|
||||
fmt.Printf("🔍 User-Agent: %s\n", c.Request.UserAgent())
|
||||
|
||||
// 显示请求体(如果有)
|
||||
if reqBodyStr != "" && reqBodyStr != "(body skipped for non-JSON content)" {
|
||||
fmt.Printf("📤 Request Body:\n %s\n", reqBodyStr)
|
||||
}
|
||||
|
||||
// 显示响应体
|
||||
if respBodyStr != "" && respBodyStr != "(empty)" {
|
||||
fmt.Printf("📥 Response Body:\n %s\n", respBodyStr)
|
||||
}
|
||||
|
||||
// 显示错误(如果有)
|
||||
if errMsg != "" {
|
||||
fmt.Printf("❌ Error: %s\n", errMsg)
|
||||
}
|
||||
|
||||
fmt.Printf("🕐 Time: %s\n", time.Now().Format("2006-01-02 15:04:05"))
|
||||
fmt.Printf("%s\n", separator)
|
||||
|
||||
// 同时记录结构化日志到文件
|
||||
logEntry := logger.WithFields(map[string]interface{}{
|
||||
"type": "http_request",
|
||||
"timestamp": time.Now().Unix(),
|
||||
"method": c.Request.Method,
|
||||
"path": path,
|
||||
"raw_query": c.Request.URL.RawQuery,
|
||||
"ip": c.ClientIP(),
|
||||
"user_agent": truncate(c.Request.UserAgent(), 200),
|
||||
"status": status,
|
||||
"request_id": requestID,
|
||||
"user_id": userID,
|
||||
"duration": latency.Milliseconds(),
|
||||
})
|
||||
|
||||
logMsg := fmt.Sprintf("HTTP Request")
|
||||
|
||||
// 根据状态码选择日志级别
|
||||
switch logLevel {
|
||||
case "error":
|
||||
logEntry.Error(logMsg)
|
||||
case "warn":
|
||||
logEntry.Warn(logMsg)
|
||||
default:
|
||||
logEntry.Info(logMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// formatLatency 格式化延迟时间
|
||||
func formatLatency(d time.Duration) string {
|
||||
if d < time.Millisecond {
|
||||
return fmt.Sprintf("%dµs", d.Microseconds())
|
||||
} else if d < time.Second {
|
||||
return fmt.Sprintf("%dms", d.Milliseconds())
|
||||
} else {
|
||||
return fmt.Sprintf("%.1fs", d.Seconds())
|
||||
}
|
||||
}
|
||||
|
||||
// getStatusEmoji 根据HTTP状态码返回对应的emoji
|
||||
func getStatusEmoji(status int) string {
|
||||
switch {
|
||||
case status >= 200 && status < 300:
|
||||
return "✅" // 成功
|
||||
case status >= 300 && status < 400:
|
||||
return "🔄" // 重定向
|
||||
case status >= 400 && status < 500:
|
||||
return "⚠️" // 客户端错误
|
||||
case status >= 500:
|
||||
return "❌" // 服务器错误
|
||||
default:
|
||||
return "📝" // 其他
|
||||
}
|
||||
}
|
||||
|
||||
// getMethodEmoji 根据HTTP方法返回对应的emoji
|
||||
func getMethodEmoji(method string) string {
|
||||
switch method {
|
||||
case "GET":
|
||||
return "📖" // 读取
|
||||
case "POST":
|
||||
return "📝" // 创建
|
||||
case "PUT":
|
||||
return "✏️" // 更新
|
||||
case "DELETE":
|
||||
return "🗑️" // 删除
|
||||
case "PATCH":
|
||||
return "🔧" // 修补
|
||||
case "OPTIONS":
|
||||
return "🔍" // 选项
|
||||
default:
|
||||
return "📌" // 其他
|
||||
}
|
||||
}
|
||||
|
||||
// getLogLevel 根据状态码返回日志级别
|
||||
func getLogLevel(status int) string {
|
||||
switch {
|
||||
case status >= 500:
|
||||
return "error"
|
||||
case status >= 400:
|
||||
return "warn"
|
||||
default:
|
||||
return "info"
|
||||
}
|
||||
}
|
||||
|
||||
// truncate 截断过长日志内容
|
||||
func truncate(s string, max int) string {
|
||||
if len(s) <= max {
|
||||
return s
|
||||
}
|
||||
if max <= 3 {
|
||||
return s[:max]
|
||||
}
|
||||
return s[:max-3] + "..."
|
||||
}
|
||||
|
||||
// maskToken 脱敏令牌或认证头
|
||||
func maskToken(s string) string {
|
||||
if s == "" {
|
||||
return s
|
||||
}
|
||||
// 只保留前后少量字符
|
||||
if len(s) <= 10 {
|
||||
return "***"
|
||||
}
|
||||
return s[:6] + "***" + s[len(s)-4:]
|
||||
}
|
||||
|
||||
// maskSensitiveJSON 尝试解析并递归脱敏JSON中的敏感字段
|
||||
func maskSensitiveJSON(s string) string {
|
||||
var obj interface{}
|
||||
if err := json.Unmarshal([]byte(s), &obj); err != nil {
|
||||
// 解析失败时直接返回原始字符串(随后由truncate处理)
|
||||
return s
|
||||
}
|
||||
masked := maskRecursive(obj)
|
||||
b, err := json.Marshal(masked)
|
||||
if err != nil {
|
||||
return s
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func maskRecursive(v interface{}) interface{} {
|
||||
switch t := v.(type) {
|
||||
case map[string]interface{}:
|
||||
for k, val := range t {
|
||||
lk := strings.ToLower(k)
|
||||
if lk == "password" || lk == "token" || lk == "secret" || lk == "authorization" {
|
||||
t[k] = "***"
|
||||
continue
|
||||
}
|
||||
t[k] = maskRecursive(val)
|
||||
}
|
||||
return t
|
||||
case []interface{}:
|
||||
for i := range t {
|
||||
t[i] = maskRecursive(t[i])
|
||||
}
|
||||
return t
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
27
serve/internal/middleware/request_id.go
Normal file
27
serve/internal/middleware/request_id.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// RequestID 为每个请求生成唯一ID
|
||||
func RequestID() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 尝试从请求头获取Request ID
|
||||
requestID := c.GetHeader("X-Request-ID")
|
||||
|
||||
// 如果没有,生成新的UUID
|
||||
if requestID == "" {
|
||||
requestID = uuid.New().String()
|
||||
}
|
||||
|
||||
// 设置到上下文中
|
||||
c.Set("request_id", requestID)
|
||||
|
||||
// 设置到响应头中
|
||||
c.Header("X-Request-ID", requestID)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user