Files
tyc-server/app/main/api/internal/middleware/logging/userOperationMiddleware.go
2025-08-31 14:18:31 +08:00

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