package logging import ( "bytes" "encoding/json" "fmt" "io" "net" "net/http" "net/url" "os" "path/filepath" "strings" "sync" "time" "tyc-server/app/main/api/internal/config" "github.com/zeromicro/go-zero/core/logx" ) // userOperation 用户操作记录 type userOperation struct { Timestamp string `json:"timestamp"` // 操作时间戳 RequestID string `json:"requestId"` // 请求ID UserID string `json:"userId"` // 用户ID Username string `json:"username"` // 用户名 IP string `json:"ip"` // 客户端IP UserAgent string `json:"userAgent"` // 用户代理 Method string `json:"method"` // HTTP方法 Path string `json:"path"` // 请求路径 QueryParams map[string]string `json:"queryParams"` // 查询参数 StatusCode int `json:"statusCode"` // 响应状态码 ResponseTime int64 `json:"responseTime"` // 响应时间(毫秒) RequestSize int64 `json:"requestSize"` // 请求大小 ResponseSize int64 `json:"responseSize"` // 响应大小 Operation string `json:"operation"` // 操作类型 Details map[string]interface{} `json:"details"` // 详细信息 Error string `json:"error,omitempty"` // 错误信息 } // UserOperationMiddleware 用户操作日志中间件 type UserOperationMiddleware struct { config *config.LoggingConfig logDir string maxFileSize int64 // 单个日志文件最大大小(字节) maxDays int // 日志保留天数 jwtExtractor *jwtExtractor mu sync.Mutex currentFile *os.File currentSize int64 currentDate string } // NewUserOperationMiddleware 创建用户操作日志中间件 func NewUserOperationMiddleware(config *config.LoggingConfig, jwtSecret string) *UserOperationMiddleware { middleware := &UserOperationMiddleware{ config: config, logDir: config.UserOperationLogDir, maxFileSize: config.MaxFileSize, maxDays: 180, // 6个月 jwtExtractor: newJWTExtractor(jwtSecret), } // 确保日志目录存在 if err := os.MkdirAll(middleware.logDir, 0755); err != nil { logx.Errorf("创建用户操作日志目录失败: %v", err) } // 启动日志清理协程 go middleware.startLogCleanup() return middleware } // Handle 处理HTTP请求并记录用户操作 func (m *UserOperationMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { startTime := time.Now() // 创建响应记录器 responseRecorder := &responseWriter{ ResponseWriter: w, body: &bytes.Buffer{}, statusCode: http.StatusOK, } // 读取请求体 var requestBody []byte if r.Body != nil { requestBody, _ = io.ReadAll(r.Body) r.Body = io.NopCloser(bytes.NewBuffer(requestBody)) } // 执行下一个处理器 next(responseRecorder, r) // 计算响应时间 responseTime := time.Since(startTime).Milliseconds() // 记录用户操作 m.recordUserOperation(r, responseRecorder, requestBody, responseTime) } } // recordUserOperation 记录用户操作 func (m *UserOperationMiddleware) recordUserOperation(r *http.Request, w *responseWriter, requestBody []byte, responseTime int64) { // 获取用户信息 userID, username := m.extractUserInfo(r) // 获取客户端IP clientIP := m.getClientIP(r) // 确定操作类型 operationType := m.determineOperation(r.Method, r.URL.Path) // 创建操作记录 operation := &userOperation{ Timestamp: time.Now().Format("2006-01-02 15:04:05.000"), RequestID: m.generateRequestID(), UserID: userID, Username: username, IP: clientIP, UserAgent: r.UserAgent(), Method: r.Method, Path: r.URL.Path, QueryParams: m.parseQueryParams(r.URL.RawQuery), StatusCode: w.statusCode, ResponseTime: responseTime, RequestSize: int64(len(requestBody)), ResponseSize: int64(w.body.Len()), Operation: operationType, Details: m.extractOperationDetails(r, w), } // 如果有错误,记录错误信息 if w.statusCode >= 400 { operation.Error = w.body.String() } // 写入日志 m.writeLog(operation) } // extractUserInfo 提取用户信息 func (m *UserOperationMiddleware) extractUserInfo(r *http.Request) (userID, username string) { // 从JWT Token中提取用户信息 if token := r.Header.Get("Authorization"); token != "" { userID, username = m.jwtExtractor.ExtractUserInfo(token) } // 如果没有Token,尝试从其他头部获取 if userID == "" { userID = r.Header.Get("X-User-ID") } if username == "" { username = r.Header.Get("X-Username") } // 如果都没有,使用默认值 if userID == "" { userID = "anonymous" } if username == "" { username = "anonymous" } return userID, username } // getClientIP 获取客户端真实IP func (m *UserOperationMiddleware) getClientIP(r *http.Request) string { // 优先级: X-Forwarded-For > X-Real-IP > RemoteAddr if forwardedFor := r.Header.Get("X-Forwarded-For"); forwardedFor != "" { if ips := strings.Split(forwardedFor, ","); len(ips) > 0 { return strings.TrimSpace(ips[0]) } } if realIP := r.Header.Get("X-Real-IP"); realIP != "" { return realIP } if r.RemoteAddr != "" { if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil { return host } return r.RemoteAddr } return "unknown" } // determineOperation 确定操作类型 func (m *UserOperationMiddleware) determineOperation(method, path string) string { // 根据HTTP方法和路径确定操作类型 switch { case strings.Contains(path, "/login"): return "用户登录" case strings.Contains(path, "/logout"): return "用户退出" case strings.Contains(path, "/register"): return "用户注册" case strings.Contains(path, "/password"): return "密码操作" case strings.Contains(path, "/profile"): return "个人信息" case strings.Contains(path, "/admin"): return "管理操作" case method == "GET": return "查询操作" case method == "POST": return "创建操作" case method == "PUT", method == "PATCH": return "更新操作" case method == "DELETE": return "删除操作" default: return "其他操作" } } // parseQueryParams 解析查询参数 func (m *UserOperationMiddleware) parseQueryParams(rawQuery string) map[string]string { params := make(map[string]string) if rawQuery == "" { return params } for _, pair := range strings.Split(rawQuery, "&") { if kv := strings.SplitN(pair, "=", 2); len(kv) == 2 { key, _ := url.QueryUnescape(kv[0]) value, _ := url.QueryUnescape(kv[1]) params[key] = value } } return params } // extractOperationDetails 提取操作详细信息 func (m *UserOperationMiddleware) extractOperationDetails(r *http.Request, w *responseWriter) map[string]interface{} { details := make(map[string]interface{}) // 记录请求头信息(排除敏感信息) headers := make(map[string]string) for key, values := range r.Header { lowerKey := strings.ToLower(key) // 排除敏感头部 if !strings.Contains(lowerKey, "authorization") && !strings.Contains(lowerKey, "cookie") && !strings.Contains(lowerKey, "password") { headers[key] = values[0] } } details["headers"] = headers // 记录响应头信息 responseHeaders := make(map[string]string) for key, values := range w.Header() { responseHeaders[key] = values[0] } details["responseHeaders"] = responseHeaders // 记录其他有用信息 details["referer"] = r.Referer() details["origin"] = r.Header.Get("Origin") details["contentType"] = r.Header.Get("Content-Type") return details } // generateRequestID 生成请求ID func (m *UserOperationMiddleware) generateRequestID() string { return fmt.Sprintf("req_%d_%d", time.Now().UnixNano(), os.Getpid()) } // writeLog 写入日志 func (m *UserOperationMiddleware) writeLog(operation *userOperation) { m.mu.Lock() defer m.mu.Unlock() // 检查是否需要切换日志文件 m.checkAndSwitchLogFile() // 序列化操作记录 data, err := json.Marshal(operation) if err != nil { logx.Errorf("序列化用户操作记录失败: %v", err) return } // 添加换行符 data = append(data, '\n') // 写入日志文件 if m.currentFile != nil { if _, err := m.currentFile.Write(data); err != nil { logx.Errorf("写入用户操作日志失败: %v", err) return } // 更新当前文件大小 m.currentSize += int64(len(data)) // 强制刷新到磁盘 m.currentFile.Sync() } } // checkAndSwitchLogFile 检查并切换日志文件 func (m *UserOperationMiddleware) checkAndSwitchLogFile() { now := time.Now() currentDate := now.Format("2006-01-02") // 检查日期是否变化 if m.currentDate != currentDate { m.closeCurrentFile() m.currentDate = currentDate } // 检查文件大小是否超过限制 if m.currentFile != nil && m.currentSize >= m.maxFileSize { m.closeCurrentFile() } // 如果当前没有文件,创建新文件 if m.currentFile == nil { m.createNewLogFile() } } // createNewLogFile 创建新的日志文件 func (m *UserOperationMiddleware) createNewLogFile() { // 生成文件名 timestamp := time.Now().Format("2006-01-02_15-04-05") filename := fmt.Sprintf("user_operation_%s_%s.log", m.currentDate, timestamp) filePath := filepath.Join(m.logDir, m.currentDate, filename) // 确保日期目录存在 dateDir := filepath.Join(m.logDir, m.currentDate) if err := os.MkdirAll(dateDir, 0755); err != nil { logx.Errorf("创建日期目录失败: %v", err) return } // 创建日志文件 file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) if err != nil { logx.Errorf("创建日志文件失败: %v", err) return } m.currentFile = file m.currentSize = 0 logx.Infof("创建新的用户操作日志文件: %s", filePath) } // closeCurrentFile 关闭当前日志文件 func (m *UserOperationMiddleware) closeCurrentFile() { if m.currentFile != nil { m.currentFile.Close() m.currentFile = nil m.currentSize = 0 } } // startLogCleanup 启动日志清理协程 func (m *UserOperationMiddleware) startLogCleanup() { ticker := time.NewTicker(24 * time.Hour) // 每天检查一次 defer ticker.Stop() for range ticker.C { m.cleanupOldLogs() } } // cleanupOldLogs 清理旧日志 func (m *UserOperationMiddleware) cleanupOldLogs() { cutoffDate := time.Now().AddDate(0, 0, -m.maxDays) // 遍历日志目录 err := filepath.Walk(m.logDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } // 只处理目录 if !info.IsDir() { return nil } // 检查是否是日期目录 if date, err := time.Parse("2006-01-02", info.Name()); err == nil { if date.Before(cutoffDate) { // 删除超过保留期的日志目录 if err := os.RemoveAll(path); err != nil { logx.Errorf("删除过期日志目录失败: %s, %v", path, err) } else { logx.Infof("删除过期日志目录: %s", path) } } } return nil }) if err != nil { logx.Errorf("清理旧日志失败: %v", err) } } // Close 关闭中间件 func (m *UserOperationMiddleware) Close() error { m.mu.Lock() defer m.mu.Unlock() if m.currentFile != nil { return m.currentFile.Close() } return nil } // responseWriter 响应记录器 type responseWriter struct { http.ResponseWriter body *bytes.Buffer statusCode int } func (w *responseWriter) WriteHeader(statusCode int) { w.statusCode = statusCode w.ResponseWriter.WriteHeader(statusCode) } func (w *responseWriter) Write(data []byte) (int, error) { w.body.Write(data) return w.ResponseWriter.Write(data) } func (w *responseWriter) Header() http.Header { return w.ResponseWriter.Header() }