package middleware import ( "encoding/json" "strconv" "strings" "time" "server/models" "github.com/beego/beego/v2/server/web/context" ) const ( oplogStartKey = "__oplog_start" oplogReqBodyKey = "__oplog_req_body" ) // BeginOperationLog 在 BeforeRouter 采集请求信息 func BeginOperationLog(ctx *context.Context) { url := ctx.Input.URL() method := ctx.Input.Method() if shouldSkipLogging(method, url) { return } ctx.Input.SetData(oplogStartKey, time.Now()) // 请求体由 main.go 的 CopyBody 保留在 Input.RequestBody if rb := ctx.Input.RequestBody; len(rb) > 0 { s := string(rb) ctx.Input.SetData(oplogReqBodyKey, truncateString(maskSensitive(s), 5000)) } } // FinishOperationLog 在 FinishRouter 统一落库到 yz_system_operation_log func FinishOperationLog(ctx *context.Context) { url := ctx.Input.URL() method := ctx.Input.Method() if shouldSkipLogging(method, url) { return } start, _ := ctx.Input.GetData(oplogStartKey).(time.Time) if start.IsZero() { start = time.Now() } execSec := float64(time.Since(start).Milliseconds()) / 1000.0 uid := parseUint64FromCtx(ctx.Input.GetData("userId")) tidVal := parseUint64FromCtx(ctx.Input.GetData("tenantId")) var tid *uint64 if tidVal > 0 { tid = &tidVal } module := parseModule(url) action := parseAction(method, url) ip := ctx.Input.IP() userAgent := truncateString(ctx.Input.Header("User-Agent"), 500) status := int8(1) if code := ctx.ResponseWriter.Status; code >= 400 { status = 0 } var reqData *string if v, ok := ctx.Input.GetData(oplogReqBodyKey).(string); ok && strings.TrimSpace(v) != "" { reqData = &v } else if q := strings.TrimSpace(ctx.Request.URL.RawQuery); q != "" { q = truncateString(maskSensitive(q), 5000) reqData = &q } var respData *string if code := ctx.ResponseWriter.Status; code >= 400 { msg := "HTTP " + strconv.Itoa(code) respData = &msg } var errMsg *string if status == 0 { msg := "请求失败" if respData != nil { msg = *respData } errMsg = &msg } logRow := &models.SystemOperationLog{ Tid: tid, UserID: uid, Module: module, Action: action, Method: method, URL: truncateString(url, 255), IP: truncateString(ip, 50), UserAgent: userAgent, RequestData: reqData, ResponseData: respData, Status: status, ErrorMessage: errMsg, ExecutionTime: execSec, } _, _ = models.Orm.Insert(logRow) } func parseAction(method, url string) string { u := strings.ToLower(url) if strings.Contains(u, "login") { return "登录" } if strings.Contains(u, "logout") { return "退出" } if strings.Contains(u, "upload") { return "上传" } switch method { case "POST": if strings.Contains(u, "delete") { return "删除" } if strings.Contains(u, "update") || strings.Contains(u, "edit") || strings.Contains(u, "rename") { return "编辑" } if strings.Contains(u, "create") || strings.Contains(u, "add") { return "新增" } return "提交" case "PUT", "PATCH": return "编辑" case "DELETE": return "删除" default: return "查询" } } func parseModule(url string) string { path := strings.Trim(strings.ToLower(url), "/") parts := strings.Split(path, "/") if len(parts) >= 2 { return truncateString(parts[1], 50) } if len(parts) == 1 && parts[0] != "" { return truncateString(parts[0], 50) } return "unknown" } func shouldSkipLogging(method, url string) bool { skipPatterns := []string{ "/static/", "/uploads/", "/favicon.ico", "/health", "/ping", } for _, pattern := range skipPatterns { if strings.HasPrefix(url, pattern) { return true } } // 高频噪声接口:默认跳过(可按需再扩充) if method == "GET" { noisyExact := map[string]bool{ "/platform/currentUser": true, "/platform/allmenu": true, "/platform/getOpenVerify": true, // 若未来改名/迁移可再调整 } if noisyExact[url] { return true } // 菜单详情/列表类:频率高且多为前端路由加载 if strings.HasPrefix(url, "/platform/menu/") { return true } // 登录页极验配置轮询/获取(不影响关键业务) if strings.HasPrefix(url, "/platform/login/getGeetest") || strings.HasPrefix(url, "/platform/login/getOpenVerify") { return true } // 客户端高频版本检查 if strings.HasPrefix(url, "/api/softwareupgrade/check") { return true } } return false } func parseUint64FromCtx(v interface{}) uint64 { switch x := v.(type) { case int: if x > 0 { return uint64(x) } case int64: if x > 0 { return uint64(x) } case uint64: return x case float64: if x > 0 { return uint64(x) } } return 0 } func truncateString(s string, maxLen int) string { if len(s) <= maxLen { return s } return s[:maxLen] + "..." } func maskSensitive(s string) string { // 尝试 JSON 脱敏(失败则返回原文) var obj interface{} if err := json.Unmarshal([]byte(s), &obj); err != nil { return s } maskInObj(&obj) bs, err := json.Marshal(obj) if err != nil { return s } return string(bs) } func maskInObj(v *interface{}) { switch t := (*v).(type) { case map[string]interface{}: for k, val := range t { lk := strings.ToLower(k) if lk == "password" || lk == "pwd" || lk == "token" || lk == "api_key" || lk == "api_secret" || lk == "authorization" { t[k] = "***" continue } tmp := val maskInObj(&tmp) t[k] = tmp } case []interface{}: for i := range t { tmp := t[i] maskInObj(&tmp) t[i] = tmp } } }