package middleware import ( "fmt" "io" "server/models" "server/services" "strconv" "strings" "time" "github.com/beego/beego/v2/server/web/context" ) // OperationLogMiddleware 操作日志中间件 - 记录所有的CREATE、UPDATE、DELETE操作 func OperationLogMiddleware(ctx *context.Context) { // 记录所有重要操作,包括修改类(POST/PUT/PATCH/DELETE)和读取类(GET)用于统计账户访问功能 method := ctx.Input.Method() // 获取用户信息和租户信息(由 JWT 中间件设置在 Input.Data 中) userId := 0 tenantId := 0 username := "" 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 } } // 如果无法获取用户ID,继续记录为匿名访问(userId=0),以便统计未登录或授权失败的访问 if userId == 0 { // debug: 输出一些上下文信息,帮助定位为何未能获取 userId fmt.Printf("OperationLogMiddleware: anonymous request %s %s, Authorization header length=%d\n", method, ctx.Input.URL(), len(ctx.Input.Header("Authorization"))) // 确保 username 有值,便于区分 if username == "" { username = "anonymous" } } // 读取请求体(对于有请求体的方法) var requestBody string if method == "POST" || method == "PUT" || method == "PATCH" { body, err := io.ReadAll(ctx.Request.Body) if err == nil { requestBody = string(body) // 重置请求体,使其可以被后续处理 ctx.Request.Body = io.NopCloser(strings.NewReader(requestBody)) } } startTime := time.Now() // 使用延迟函数来记录操作 defer func() { duration := time.Since(startTime) // 解析操作类型 operation := parseOperationType(method, ctx.Input.URL()) resourceType := parseResourceType(ctx.Input.URL()) resourceId := parseResourceId(ctx.Input.URL()) module := parseModule(ctx.Input.URL()) // 如果是读取/访问行为,写入访问日志(sys_access_log),否则写入操作日志(sys_operation_log) if operation == "READ" { access := &models.AccessLog{ TenantId: tenantId, UserId: userId, Username: username, Module: module, ResourceType: resourceType, ResourceId: &resourceId, RequestUrl: ctx.Input.URL(), IpAddress: ctx.Input.IP(), UserAgent: ctx.Input.Header("User-Agent"), RequestMethod: method, Duration: int(duration.Milliseconds()), } // 在 QueryString 中记录查询参数和匿名标识 qs := ctx.Request.URL.RawQuery if qs != "" { access.QueryString = qs } if userId == 0 { // 将匿名标识拼入 QueryString 以便查询(也可改为独立字段) if access.QueryString != "" { access.QueryString = "anonymous=true; " + access.QueryString } else { access.QueryString = "anonymous=true" } } if err := services.AddAccessLog(access); err != nil { fmt.Printf("Failed to save access log: %v\n", err) } return } // 创建操作日志 log := &models.OperationLog{ TenantId: tenantId, UserId: userId, Username: username, Module: module, ResourceType: resourceType, ResourceId: &resourceId, Operation: operation, IpAddress: ctx.Input.IP(), UserAgent: ctx.Input.Header("User-Agent"), RequestMethod: method, RequestUrl: ctx.Input.URL(), Status: 1, // 默认成功,实际应该根据响应状态码更新 Duration: int(duration.Milliseconds()), CreateTime: time.Now(), } // 如果是写操作,保存请求体作为新值;对于读取操作可以在Description里记录query if requestBody != "" { log.NewValue = requestBody } else if method == "GET" { // 把查询字符串放到描述里,便于分析访问参数 qs := ctx.Request.URL.RawQuery if qs != "" { log.Description = "query=" + qs } } // 标记匿名访问信息(当 userId==0) if userId == 0 { if log.Description != "" { log.Description = "anonymous=true; " + log.Description } else { log.Description = "anonymous=true" } } // 调用服务层保存日志 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" }