package web import ( "log" "net/http" "github.com/gin-gonic/gin" ) // CtxRequestID Gin Context 中存放 request_id 的键(由 middleware.RequestID 写入)。 const CtxRequestID = "req_id" // RequestIDOf 从 Gin Context 读取本次请求的 request_id(无则返回空串)。 func RequestIDOf(c *gin.Context) string { if v, ok := c.Get(CtxRequestID); ok { if s, ok := v.(string); ok { return s } } return "" } // OK 成功响应信封 {"code":0,"message":"success","data":...,"error":null} func OK(c *gin.Context, data any) { c.JSON(http.StatusOK, gin.H{ "code": CodeOK, "message": "success", "data": data, "error": nil, }) } // Fail 错误响应信封,统一输出四层模型: // // {"code": 稳定业务错误码, "message": 用户主提示, "data": null, // "error": {"category":分类, "status":HTTP状态码, "hint":下一步建议, "request_id":追踪编号, "details":开发上下文}} // // 并回写 X-Request-ID 响应头,便于前后端与日志联查。 func Fail(c *gin.Context, e *AppError) { requestID := RequestIDOf(c) e.RequestID = requestID c.Header("X-Request-ID", requestID) errorObj := gin.H{ "category": e.Category, "status": e.StatusCode, "hint": e.Hint, "request_id": requestID, } if e.Details != nil { errorObj["details"] = e.Details } logFail(c, e) c.JSON(e.StatusCode, gin.H{ "code": e.Code, "message": e.Message, "data": nil, "error": errorObj, }) } // logFail 记录结构化错误日志,满足可定位要求:request_id / method / path / status / code / category。 func logFail(c *gin.Context, e *AppError) { if e == nil { return } req := c.Request method := "" path := "" if req != nil { method = req.Method path = req.URL.Path } log.Printf("HTTP ERROR [%s] status=%d code=%d %s %s -> %s request_id=%s", e.Category, e.StatusCode, e.Code, method, path, e.Message, e.RequestID) }