Files
nl-im-service/internal/middleware/request_log.go
2026-07-08 08:18:58 +08:00

277 lines
6.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* package middleware
* 作用:接口请求日志中间件
* 说明记录所有API请求信息异步写入数据库
*/
package middleware
import (
"bytes"
"encoding/json"
"fmt"
"io"
"strings"
"time"
"xk-websocket-v2/internal/model"
"xk-websocket-v2/internal/utils"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// 二进制 Content-Type 前缀列表
var binaryContentTypes = []string{
"image/",
"audio/",
"video/",
"application/octet-stream",
"application/pdf",
"application/zip",
"application/x-rar",
"application/x-7z",
"application/gzip",
"application/x-tar",
"font/",
}
// 不需要记录响应体的路由前缀
var skipResponseBodyRoutes = []string{
"/uploads/",
"/static/",
}
// 需要完全跳过中间件的路由(如 WebSocket
var skipMiddlewareRoutes = []string{
"/ws",
"/api/call/ws-push",
}
// isBinaryContentType 检测是否为二进制 Content-Type
func isBinaryContentType(contentType string) bool {
contentType = strings.ToLower(contentType)
for _, prefix := range binaryContentTypes {
if strings.HasPrefix(contentType, prefix) {
return true
}
}
return false
}
// isBinaryData 检测数据是否为二进制(通过检查是否包含非 UTF-8 字符)
func isBinaryData(data []byte) bool {
if len(data) == 0 {
return false
}
// 检查前 512 字节是否包含二进制特征
checkLen := len(data)
if checkLen > 512 {
checkLen = 512
}
for i := 0; i < checkLen; i++ {
// 检测常见的二进制文件头
if data[i] == 0 {
return true
}
}
// 检查是否以常见的二进制文件头开始
if len(data) >= 2 {
// JPEG: FF D8
if data[0] == 0xFF && data[1] == 0xD8 {
return true
}
// PNG: 89 50
if data[0] == 0x89 && data[1] == 0x50 {
return true
}
// GIF: 47 49
if data[0] == 0x47 && data[1] == 0x49 {
return true
}
// PDF: 25 50
if data[0] == 0x25 && data[1] == 0x50 {
return true
}
// ZIP/DOCX/XLSX: 50 4B
if data[0] == 0x50 && data[1] == 0x4B {
return true
}
}
return false
}
// shouldSkipResponseBody 检测是否应该跳过响应体记录
func shouldSkipResponseBody(path string) bool {
for _, prefix := range skipResponseBodyRoutes {
if strings.HasPrefix(path, prefix) {
return true
}
}
return false
}
// RequestLogMiddleware 请求日志中间件
type RequestLogMiddleware struct {
DB *gorm.DB
}
// NewRequestLogMiddleware 创建请求日志中间件
func NewRequestLogMiddleware(db *gorm.DB) *RequestLogMiddleware {
return &RequestLogMiddleware{DB: db}
}
// shouldSkipMiddleware 检测是否应该完全跳过中间件
func shouldSkipMiddleware(path string) bool {
for _, route := range skipMiddlewareRoutes {
if strings.HasPrefix(path, route) {
return true
}
}
return false
}
// Handler 中间件处理函数
func (m *RequestLogMiddleware) Handler() gin.HandlerFunc {
return func(c *gin.Context) {
// 跳过OPTIONS请求
if c.Request.Method == "OPTIONS" {
c.Next()
return
}
// 跳过 WebSocket 路由(包装 Writer 会干扰 WebSocket 升级)
if shouldSkipMiddleware(c.Request.URL.Path) {
c.Next()
return
}
// 获取请求IP
ip := utils.GetClientIP(c)
// 本地IP不记录
if utils.GetIPLocation(ip) == "本地" {
c.Next()
return
}
// 获取请求参数
var requestBody []byte
if c.Request.Body != nil {
requestBody, _ = io.ReadAll(c.Request.Body)
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
}
// 获取用户ID从Context中获取未登录为"0"
userID := "0"
if uid, exists := c.Get("user_id"); exists {
if uidStr, ok := uid.(string); ok {
userID = uidStr
}
}
// 记录开始时间
startTime := time.Now()
// 创建响应写入器
writer := &responseWriter{
ResponseWriter: c.Writer,
body: &bytes.Buffer{},
}
c.Writer = writer
// 处理请求
c.Next()
// 计算请求时间
duration := time.Since(startTime)
// 异步记录日志(避免影响性能)
go m.logRequest(c, ip, userID, requestBody, writer.body.Bytes(), writer.status, duration)
}
}
// logRequest 记录请求日志
func (m *RequestLogMiddleware) logRequest(c *gin.Context, ip, userID string, requestBody, responseBody []byte, httpStatus int, duration time.Duration) {
// 获取IP归属地
location := utils.GetIPLocation(ip)
// 获取响应code从响应体中解析
responseCode := 0
if len(responseBody) > 0 && !isBinaryData(responseBody) {
// 尝试解析响应体获取code仅对非二进制数据
var resp model.ApiResponse
if err := json.Unmarshal(responseBody, &resp); err == nil {
responseCode = resp.Code
}
}
// 处理请求参数
var requestParams string
requestContentType := c.GetHeader("Content-Type")
if isBinaryContentType(requestContentType) || isBinaryData(requestBody) {
// 二进制请求体,只记录大小
requestParams = fmt.Sprintf("[binary data: %d bytes]", len(requestBody))
} else {
requestParams = string(requestBody)
if len(requestParams) > 5000 {
requestParams = requestParams[:5000] + "...(truncated)"
}
}
// 处理响应参数
var responseParams string
responseContentType := c.Writer.Header().Get("Content-Type")
requestPath := c.Request.URL.Path
if shouldSkipResponseBody(requestPath) {
// 静态文件路由,跳过响应体记录
responseParams = fmt.Sprintf("[static file: %d bytes]", len(responseBody))
} else if isBinaryContentType(responseContentType) || isBinaryData(responseBody) {
// 二进制响应体,只记录大小
responseParams = fmt.Sprintf("[binary data: %d bytes]", len(responseBody))
} else {
responseParams = string(responseBody)
if len(responseParams) > 5000 {
responseParams = responseParams[:5000] + "...(truncated)"
}
}
// 创建日志记录
log := model.ApiRequestLog{
Route: c.FullPath(),
IP: ip,
IPLocation: location,
UserID: userID,
Method: c.Request.Method,
RequestParams: requestParams,
ResponseParams: responseParams,
ResponseCode: responseCode,
HTTPStatus: httpStatus,
RequestTime: time.Now(),
}
// 异步写入数据库
m.DB.Create(&log)
}
// responseWriter 响应写入器(用于捕获响应内容)
type responseWriter struct {
gin.ResponseWriter
body *bytes.Buffer
status int
}
func (w *responseWriter) Write(b []byte) (int, error) {
w.body.Write(b)
return w.ResponseWriter.Write(b)
}
func (w *responseWriter) WriteString(s string) (int, error) {
w.body.WriteString(s)
return w.ResponseWriter.WriteString(s)
}
func (w *responseWriter) WriteHeader(statusCode int) {
w.status = statusCode
w.ResponseWriter.WriteHeader(statusCode)
}