package middleware import ( "bytes" "io" "net/http" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/service" "github.com/gin-gonic/gin" ) // operationLogConfig 记录操作日志的路径配置。 // key: HTTP method + path pattern, value: {action, targetType} type operationLogConfig struct { Action string TargetType string } // auditRoutes 需要记录操作日志的路由配置。 // 仅记录增删改操作,不记录查询操作。 var auditRoutes = map[string]operationLogConfig{ // 渠道管理 "POST /api/channel/": {Action: model.OperationActionCreate, TargetType: "channel"}, "PUT /api/channel/": {Action: model.OperationActionUpdate, TargetType: "channel"}, "DELETE /api/channel/:id": {Action: model.OperationActionDelete, TargetType: "channel"}, "POST /api/channel/batch": {Action: model.OperationActionDelete, TargetType: "channel"}, "POST /api/channel/copy/:id": {Action: model.OperationActionCreate, TargetType: "channel"}, // 令牌管理 "POST /api/token/": {Action: model.OperationActionCreate, TargetType: "token"}, "PUT /api/token/": {Action: model.OperationActionUpdate, TargetType: "token"}, "DELETE /api/token/:id": {Action: model.OperationActionDelete, TargetType: "token"}, "POST /api/token/batch": {Action: model.OperationActionDelete, TargetType: "token"}, // 用户管理 "POST /api/user/": {Action: model.OperationActionCreate, TargetType: "user"}, "PUT /api/user/": {Action: model.OperationActionUpdate, TargetType: "user"}, "DELETE /api/user/:id": {Action: model.OperationActionDelete, TargetType: "user"}, "POST /api/user/manage": {Action: model.OperationActionUpdate, TargetType: "user"}, // 兑换码管理 "POST /api/redemption/": {Action: model.OperationActionCreate, TargetType: "redemption"}, "PUT /api/redemption/": {Action: model.OperationActionUpdate, TargetType: "redemption"}, "DELETE /api/redemption/:id": {Action: model.OperationActionDelete, TargetType: "redemption"}, "DELETE /api/redemption/invalid": {Action: model.OperationActionDelete, TargetType: "redemption"}, // 模型管理 "POST /api/models/": {Action: model.OperationActionCreate, TargetType: "model"}, "PUT /api/models/": {Action: model.OperationActionUpdate, TargetType: "model"}, "DELETE /api/models/:id": {Action: model.OperationActionDelete, TargetType: "model"}, // 供应商管理 "POST /api/user/supplier/application": {Action: model.OperationActionCreate, TargetType: "supplier_application"}, "PUT /api/user/supplier/application/self": {Action: model.OperationActionUpdate, TargetType: "supplier_application"}, "PUT /api/user/supplier/application/:id/capability": {Action: model.OperationActionUpdate, TargetType: "supplier_capability"}, // 系统设置 "PUT /api/option/": {Action: model.OperationActionUpdate, TargetType: "setting"}, } // OperationLogMiddleware 自动记录匹配路由的增删改操作。 // 对于路径参数路由(如 /api/channel/:id),尝试用实际路径匹配。 func OperationLogMiddleware() gin.HandlerFunc { return func(c *gin.Context) { method := c.Request.Method // 仅拦截写操作 if method != http.MethodPost && method != http.MethodPut && method != http.MethodPatch && method != http.MethodDelete { c.Next() return } path := c.Request.URL.Path key := method + " " + path config, matched := auditRoutes[key] if !matched { // 尝试匹配带 :id 的路由模式 // 通过遍历已知模式,将 :id 部分替换为实际值进行匹配 for pattern, cfg := range auditRoutes { if matchRoutePattern(pattern, method, path) { config = cfg matched = true break } } } if !matched { c.Next() return } // 读取请求体用于审计 var requestBody string if c.Request.Body != nil && (method == http.MethodPost || method == http.MethodPut || method == http.MethodPatch) { bodyBytes, err := io.ReadAll(c.Request.Body) if err == nil { requestBody = string(bodyBytes) // 将请求体放回,供后续处理器读取 c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) } } // 先执行后续处理器 c.Next() // 如果 controller 已经手动记录了操作日志,则跳过中间件记录,避免重复 if already, _ := c.Get("_operation_logged"); already != nil { if logged, ok := already.(bool); ok && logged { return } } // 只有成功时才记录(2xx 状态码) status := c.Writer.Status() if status < 200 || status >= 300 { return } userId := c.GetInt("id") if userId == 0 { return } // 从上下文中获取目标 ID 和名称(由各 controller 设置) targetId, _ := c.Get("operation_target_id") targetName, _ := c.Get("operation_target_name") content, _ := c.Get("operation_content") targetIdInt := 0 if v, ok := targetId.(int); ok { targetIdInt = v } targetNameStr := "" if v, ok := targetName.(string); ok { targetNameStr = v } contentStr := "" if v, ok := content.(string); ok { contentStr = v } else { // 自动生成默认描述 contentStr = commonActionLabel(config.Action, config.TargetType, targetNameStr) } sanitizedBody := model.SanitizeRequestBody(requestBody) model.RecordOperationLog(c, userId, config.Action, config.TargetType, targetIdInt, targetNameStr, contentStr, sanitizedBody) } } // matchRoutePattern 检查实际请求路径是否匹配带 :param 的路由模式。 func matchRoutePattern(pattern string, method string, path string) bool { // 简单实现:检查方法前缀和路径段数量是否一致 if len(pattern) < len(method)+2 || pattern[:len(method)+1] != method+" " { return false } patternPath := pattern[len(method)+1:] // 分段比较 patternSegments := splitPath(patternPath) pathSegments := splitPath(path) if len(patternSegments) != len(pathSegments) { return false } for i := range patternSegments { if len(patternSegments[i]) > 0 && patternSegments[i][0] == ':' { // 动态段,跳过 continue } if patternSegments[i] != pathSegments[i] { return false } } return true } func splitPath(path string) []string { var segments []string start := 0 for i := 0; i <= len(path); i++ { if i == len(path) || path[i] == '/' { if i > start { segments = append(segments, path[start:i]) } start = i + 1 } } return segments } // commonActionLabel 生成默认的操作描述。 func commonActionLabel(action string, targetType string, targetName string) string { actionLabel := "" switch action { case model.OperationActionCreate: actionLabel = "创建" case model.OperationActionUpdate: actionLabel = "更新" case model.OperationActionDelete: actionLabel = "删除" default: actionLabel = action } typeLabel := targetType switch targetType { case "channel": typeLabel = "渠道" case "token": typeLabel = "令牌" case "user": typeLabel = "用户" case "redemption": typeLabel = "兑换码" case "model": typeLabel = "模型" case "setting": typeLabel = "系统设置" case "supplier_application": typeLabel = "供应商申请" case "supplier_capability": typeLabel = "供应商能力" } if targetName != "" { return actionLabel + typeLabel + ": " + targetName } return actionLabel + typeLabel } // SetOperationTarget 在 controller 中设置操作目标信息,供中间件读取。 func SetOperationTarget(c *gin.Context, targetId int, targetName string, content string) { c.Set("operation_target_id", targetId) c.Set("operation_target_name", targetName) c.Set("operation_content", content) } // RecordOperationDirectly 在 controller 中直接记录操作日志(不走中间件)。 // 适用于需要自定义逻辑的场景。 func RecordOperationDirectly(c *gin.Context, action string, targetType string, targetId int, targetName string, content string) { service.RecordOperation(c, action, targetType, targetId, targetName, content, "") }