yunzer_go/server/middleware/operationLog.go

223 lines
5.1 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
import (
"bytes"
"fmt"
"io"
"server/models"
"server/services"
"strconv"
"strings"
"time"
"github.com/beego/beego/v2/server/web/context"
)
// OperationLogMiddleware 操作日志中间件 - 记录所有接口的调用记录
func OperationLogMiddleware(ctx *context.Context) {
// 跳过静态资源和内部路由
url := ctx.Input.URL()
if shouldSkipLogging(url) {
return
}
method := ctx.Input.Method()
// 获取用户信息和租户信息(由 JWT 中间件设置在 Input.Data 中)
userId := 0
tenantId := 0
username := ""
userType := "" // 用户类型user(平台用户) 或 employee(租户员工)
if v := ctx.Input.GetData("userId"); v != nil {
if id, ok := v.(int); ok {
userId = id
}
}
if v := ctx.Input.GetData("tenantId"); v != nil {
if id, ok := v.(int); ok {
tenantId = id
}
}
if v := ctx.Input.GetData("username"); v != nil {
if s, ok := v.(string); ok {
username = s
}
}
if v := ctx.Input.GetData("userType"); v != nil {
if s, ok := v.(string); ok {
userType = s
}
}
// 用户信息补全
if username == "" {
username = "anonymous"
}
// 读取请求体(对于有请求体的方法)
var requestBody string
if method == "POST" || method == "PUT" || method == "PATCH" {
body, err := io.ReadAll(ctx.Request.Body)
if err == nil && len(body) > 0 {
requestBody = string(body)
// 重置请求体,使其可以被后续处理
ctx.Request.Body = io.NopCloser(bytes.NewBuffer(body))
}
}
startTime := time.Now()
ipAddress := ctx.Input.IP()
userAgent := ctx.Input.Header("User-Agent")
queryString := ctx.Request.URL.RawQuery
// 使用延迟函数来记录操作
defer func() {
duration := time.Since(startTime)
// 解析操作相关信息
operation := parseOperationType(method, url)
module := parseModule(url)
resourceType := parseResourceType(url)
resourceId := parseResourceId(url)
// 为所有接口都记录日志
log := &models.OperationLog{
TenantId: tenantId,
UserId: userId,
Username: username,
Module: module,
ResourceType: resourceType,
Operation: operation,
IpAddress: ipAddress,
UserAgent: userAgent,
RequestMethod: method,
RequestUrl: url,
Status: 1, // 默认成功
Duration: int(duration.Milliseconds()),
CreateTime: time.Now(),
}
// 设置资源ID
if resourceId > 0 {
log.ResourceId = &resourceId
}
// 记录请求信息到Description
var description strings.Builder
if requestBody != "" {
description.WriteString("Request: " + truncateString(requestBody, 500))
}
if queryString != "" {
if description.Len() > 0 {
description.WriteString(" | ")
}
description.WriteString("Query: " + queryString)
}
log.Description = description.String()
// 如果有请求体作为NewValue保存
if requestBody != "" {
log.NewValue = requestBody
}
// 添加用户类型信息到Description
if userType != "" {
if log.Description != "" {
log.Description += " | "
}
log.Description += "UserType: " + userType
}
// 调用服务层保存日志
if err := services.AddOperationLog(log); err != nil {
fmt.Printf("Failed to save operation log: %v\n", err)
}
}()
}
// parseOperationType 根据HTTP方法解析操作类型
func parseOperationType(method, url string) string {
switch method {
case "POST":
// 检查URL是否包含特定的操作关键字
if strings.Contains(url, "login") {
return "LOGIN"
}
if strings.Contains(url, "logout") {
return "LOGOUT"
}
if strings.Contains(url, "add") || strings.Contains(url, "create") {
return "CREATE"
}
return "CREATE"
case "PUT", "PATCH":
return "UPDATE"
case "DELETE":
return "DELETE"
default:
return "READ"
}
}
// parseResourceType 根据URL解析资源类型
func parseResourceType(url string) string {
parts := strings.Split(strings.TrimPrefix(url, "/api/"), "/")
if len(parts) > 0 {
// 移除复数形式的s
resourceType := strings.TrimSuffix(parts[0], "s")
return resourceType
}
return "unknown"
}
// parseResourceId 从URL中提取资源ID
func parseResourceId(url string) int {
parts := strings.Split(strings.TrimPrefix(url, "/api/"), "/")
if len(parts) >= 2 {
// 尝试解析第二个部分为ID
if id, err := strconv.Atoi(parts[1]); err == nil {
return id
}
}
return 0
}
// parseModule 根据URL解析模块名称
func parseModule(url string) string {
// 返回与 sys_operation_log.module 字段匹配的短code例如 dict、user 等)
parts := strings.Split(strings.TrimPrefix(url, "/api/"), "/")
if len(parts) > 0 {
return strings.ToLower(parts[0])
}
return "unknown"
}
// shouldSkipLogging 判断是否需要跳过日志记录
func shouldSkipLogging(url string) bool {
// 跳过静态资源、健康检查等
skipPatterns := []string{
"/static/",
"/uploads/",
"/favicon.ico",
"/health",
"/ping",
}
for _, pattern := range skipPatterns {
if strings.HasPrefix(url, pattern) {
return true
}
}
return false
}
// truncateString 截断字符串到指定长度
func truncateString(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
return s[:maxLen] + "..."
}