444 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			444 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| 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()
 | ||
| }
 |