feat(api): align response envelope schema

Switch response envelope business code to numeric and make message
consistently present. Add trace_id and optional details, and remove the
duplicate DTO envelope definition. Improve middleware path exclusion
handling and add a time-based trace ID fallback if crypto RNG fails.

BREAKING CHANGE: response envelope `code` is now `int` (was `string`) and
`message` semantics/defaults changed; clients must update parsing.
This commit is contained in:
zenfun
2026-01-10 01:09:05 +08:00
parent 26733be020
commit ac6a1858cf
3 changed files with 20 additions and 66 deletions

View File

@@ -1,12 +1,16 @@
package api package api
// ResponseEnvelope is the standard wrapper for EZ-API JSON responses. // ResponseEnvelope is the standard wrapper for EZ-API JSON responses.
// Code is a stable business code string (e.g., ok, invalid_request, not_found). // Code is a numeric business code: 0 for success, non-zero for errors.
// Message is empty for success and mirrors the top-level error for failures. // Message is "success" for success, error description for failures.
// Data holds the original response payload. // Data holds the original response payload for success; null for errors.
// TraceID is a request correlation identifier.
// Details holds optional structured error information.
// swagger:model ResponseEnvelope // swagger:model ResponseEnvelope
type ResponseEnvelope struct { type ResponseEnvelope struct {
Code string `json:"code" example:"ok"` Code int `json:"code" example:"0"`
Message string `json:"message" example:"success"`
Data any `json:"data" swaggertype:"object"` Data any `json:"data" swaggertype:"object"`
Message string `json:"message" example:""` TraceID string `json:"trace_id" example:"a1b2c3d4e5f6g7h8"`
Details any `json:"details,omitempty" swaggertype:"object"`
} }

View File

@@ -1,60 +0,0 @@
package dto
// ResponseEnvelope is the standard API response wrapper.
// All JSON API responses are wrapped in this envelope format.
// @Description Standard API response envelope
type ResponseEnvelope struct {
// Business code: 0 for success, non-zero for errors
// 0 = success
// 1xxx = common errors (invalid param, unauthorized, forbidden, rate limited)
// 4xxx = client errors (resource not found, conflict, invalid state)
// 5xxx = server errors (internal error, service unavailable, timeout)
Code int `json:"code" example:"0"`
// Human-readable message: "success" for success, error description for errors
Message string `json:"message" example:"success"`
// Response data: business payload for success, null for errors
Data any `json:"data"`
// Request trace ID for debugging and log correlation
TraceID string `json:"trace_id" example:"a1b2c3d4e5f6g7h8"`
// Optional error details (e.g., field validation errors)
Details any `json:"details,omitempty"`
}
// ErrorResponse is returned when an error occurs.
// @Description Error response format
type ErrorResponse struct {
// Business error code (non-zero)
Code int `json:"code" example:"4001"`
// Error message
Message string `json:"message" example:"resource not found"`
// Always null for errors
Data any `json:"data" example:"null"`
// Request trace ID
TraceID string `json:"trace_id" example:"a1b2c3d4e5f6g7h8"`
// Optional structured error details
Details any `json:"details,omitempty"`
}
// SuccessResponse is returned for successful operations.
// @Description Success response format
type SuccessResponse struct {
// Always 0 for success
Code int `json:"code" example:"0"`
// Always "success"
Message string `json:"message" example:"success"`
// Response payload
Data any `json:"data"`
// Request trace ID
TraceID string `json:"trace_id" example:"a1b2c3d4e5f6g7h8"`
}

View File

@@ -5,8 +5,10 @@ import (
"crypto/rand" "crypto/rand"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"strings" "strings"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -227,6 +229,12 @@ func ResponseEnvelope() gin.HandlerFunc {
} }
func isExcludedPath(path string) bool { func isExcludedPath(path string) bool {
if path == "" {
return false
}
for len(path) > 1 && strings.HasSuffix(path, "/") {
path = strings.TrimSuffix(path, "/")
}
if excludedPaths[path] { if excludedPaths[path] {
return true return true
} }
@@ -298,7 +306,9 @@ func getTraceID(c *gin.Context) string {
func generateTraceID() string { func generateTraceID() string {
b := make([]byte, 16) b := make([]byte, 16)
_, _ = rand.Read(b) if _, err := rand.Read(b); err != nil {
return fmt.Sprintf("%032x", time.Now().UnixNano())
}
return hex.EncodeToString(b) return hex.EncodeToString(b)
} }