package middleware import ( "encoding/json" "fmt" "strconv" "strings" "time" "server/models" "server/pkg/jwtutil" "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 } fmt.Printf("[oplog-begin] method=%s url=%s\n", method, url) // 解析用户信息(直接从 header 解析,不依赖 JWT 中间件设置的 context) var uid uint64 var tid *uint64 if auth := ctx.Request.Header.Get("Authorization"); auth != "" { parts := strings.SplitN(auth, " ", 2) if len(parts) == 2 && parts[0] == "Bearer" { if claims, err := jwtutil.ParseToken(parts[1]); err == nil { uid = uint64(claims.UserID) if claims.TenantId > 0 { tidVal := uint64(claims.TenantId) tid = &tidVal } } } } // 采集请求信息 reqBody := "" if rb := ctx.Input.RequestBody; len(rb) > 0 { reqBody = truncateString(maskSensitive(string(rb)), 5000) } else if q := strings.TrimSpace(ctx.Request.URL.RawQuery); q != "" { reqBody = truncateString(maskSensitive(q), 5000) } module := parseModule(url) action := parseAction(method, url) ip := truncateString(ctx.Input.IP(), 50) userAgent := truncateString(ctx.Request.Header.Get("User-Agent"), 500) // 异步写入数据库 go func() { time.Sleep(100 * time.Millisecond) // 等待响应完成 logRow := &models.SystemOperationLog{ Tid: tid, UserID: uid, Module: module, Action: action, Method: method, URL: truncateString(url, 255), IP: ip, UserAgent: userAgent, RequestData: strPtr(reqBody), Status: 1, ExecutionTime: 0, } _, err := models.Orm.Insert(logRow) if err != nil { fmt.Printf("[oplog] INSERT ERROR: %v\n", err) } else { fmt.Printf("[oplog] INSERTED uid=%d module=%s action=%s\n", uid, module, action) } }() } func strPtr(s string) *string { if s == "" { return nil } return &s } // WriteDeleteLog 控制器删除后直接写操作日志(带标题) func WriteDeleteLog(userID uint64, tid int, module, title string) { moduleCN := oplogModuleLabel(module) desc := fmt.Sprintf("您删除了%s:【%s】", moduleCN, truncateString(title, 20)) tidVal := uint64(tid) logRow := &models.SystemOperationLog{ UserID: userID, Tid: &tidVal, Module: module, Action: "删除", Method: "DELETE", URL: fmt.Sprintf("/app/%s", module), Status: 1, RequestData: &desc, ExecutionTime: 0, } models.Orm.Insert(logRow) } func oplogModuleLabel(m string) string { switch m { case "notebook": return "记事本" case "schedule": return "日程提醒" default: return m } } // FinishOperationLog 在 FinishRouter 统一落库到 yz_system_operation_log func FinishOperationLog(ctx *context.Context) { url := ctx.Input.URL() method := ctx.Input.Method() fmt.Printf("[oplog-finish] method=%s url=%s\n", method, url) if shouldSkipLogging(method, url) { fmt.Printf("[oplog-finish] SKIPPED\n") 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")) // 如果 context 中没有用户信息,尝试从 Authorization header 解析 if uid == 0 { if auth := ctx.Request.Header.Get("Authorization"); auth != "" { parts := strings.SplitN(auth, " ", 2) if len(parts) == 2 && parts[0] == "Bearer" { if claims, err := jwtutil.ParseToken(parts[1]); err == nil { uid = uint64(claims.UserID) if claims.TenantId > 0 { tidVal = uint64(claims.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, } fmt.Printf("[oplog] method=%s url=%s uid=%d tid=%v module=%s action=%s\n", method, url, uid, tid, module, action) _, err := models.Orm.Insert(logRow) if err != nil { fmt.Printf("[oplog] INSERT ERROR: %v\n", err) } } 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 } // App 端读接口跳过(只记录写操作:POST/PUT/DELETE) if strings.HasPrefix(url, "/app/") && method == "GET" { return true } // App 端 DELETE 由控制器自行记录(需要标题信息) if strings.HasPrefix(url, "/app/") && method == "DELETE" { 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 } } }