Files
tyc-server/app/main/api/internal/middleware/logging/userOperationMiddleware.go

444 lines
12 KiB
Go
Raw Normal View History

2025-08-31 14:18:31 +08:00
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()
}