feat(api): wrap JSON responses in envelope

Add response envelope middleware to standardize JSON responses as
`{code,data,message}` with consistent business codes across endpoints.
Update Swagger annotations and tests to reflect the new response shape.

BREAKING CHANGE: API responses are now wrapped in a response envelope; clients must read payloads from `data` and handle `code`/`message` fields.
This commit is contained in:
zenfun
2026-01-10 00:15:08 +08:00
parent f400ffde95
commit 33838b1e2c
40 changed files with 771 additions and 371 deletions

View File

@@ -9,6 +9,7 @@ import (
"testing"
"time"
"github.com/ez-api/ez-api/internal/middleware"
"github.com/ez-api/ez-api/internal/model"
"github.com/gin-gonic/gin"
"gorm.io/driver/sqlite"
@@ -61,6 +62,7 @@ func TestMaster_ListSelfLogs_FiltersByMaster(t *testing.T) {
}
r := gin.New()
r.Use(middleware.ResponseEnvelope())
r.GET("/v1/logs", withMaster(mh.ListSelfLogs))
rr := httptest.NewRecorder()
@@ -70,9 +72,7 @@ func TestMaster_ListSelfLogs_FiltersByMaster(t *testing.T) {
t.Fatalf("expected 200, got %d body=%s", rr.Code, rr.Body.String())
}
var resp ListMasterLogsResponse
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
t.Fatalf("unmarshal: %v", err)
}
env := decodeEnvelope(t, rr, &resp)
if resp.Total != 1 || len(resp.Items) != 1 {
t.Fatalf("expected 1 item, got total=%d len=%d body=%s", resp.Total, len(resp.Items), rr.Body.String())
}
@@ -81,7 +81,7 @@ func TestMaster_ListSelfLogs_FiltersByMaster(t *testing.T) {
}
var raw map[string]any
if err := json.Unmarshal(rr.Body.Bytes(), &raw); err != nil {
if err := json.Unmarshal(env.Data, &raw); err != nil {
t.Fatalf("unmarshal raw: %v", err)
}
items, ok := raw["items"].([]any)
@@ -141,6 +141,7 @@ func TestAdmin_DeleteLogs_BeforeFilters(t *testing.T) {
h := &Handler{db: db, logDB: db}
r := gin.New()
r.Use(middleware.ResponseEnvelope())
r.DELETE("/admin/logs", h.DeleteLogs)
body := []byte(fmt.Sprintf(`{"before":"%s","key_id":1,"model":"m1"}`, cutoff.Format(time.RFC3339)))
@@ -152,9 +153,7 @@ func TestAdmin_DeleteLogs_BeforeFilters(t *testing.T) {
}
var resp DeleteLogsResponse
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
t.Fatalf("unmarshal: %v", err)
}
decodeEnvelope(t, rr, &resp)
if resp.DeletedCount != 1 {
t.Fatalf("expected deleted_count=1, got %d", resp.DeletedCount)
}
@@ -182,6 +181,7 @@ func TestAdmin_DeleteLogs_RequiresBefore(t *testing.T) {
h := &Handler{db: db, logDB: db}
r := gin.New()
r.Use(middleware.ResponseEnvelope())
r.DELETE("/admin/logs", h.DeleteLogs)
req := httptest.NewRequest(http.MethodDelete, "/admin/logs", bytes.NewReader([]byte(`{}`)))
@@ -217,6 +217,7 @@ func TestAdmin_ListLogs_IncludesRequestBody(t *testing.T) {
h := &Handler{db: db, logDB: db}
r := gin.New()
r.Use(middleware.ResponseEnvelope())
r.GET("/admin/logs", h.ListLogs)
req := httptest.NewRequest(http.MethodGet, "/admin/logs", nil)
@@ -227,9 +228,7 @@ func TestAdmin_ListLogs_IncludesRequestBody(t *testing.T) {
}
var resp ListLogsResponse
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
t.Fatalf("unmarshal: %v", err)
}
decodeEnvelope(t, rr, &resp)
if resp.Total != 1 || len(resp.Items) != 1 {
t.Fatalf("expected 1 item, got total=%d len=%d", resp.Total, len(resp.Items))
}
@@ -264,6 +263,7 @@ func TestLogStats_GroupByModel(t *testing.T) {
h := &Handler{db: db, logDB: db}
r := gin.New()
r.Use(middleware.ResponseEnvelope())
r.GET("/admin/logs/stats", h.LogStats)
req := httptest.NewRequest(http.MethodGet, "/admin/logs/stats?group_by=model", nil)
@@ -274,9 +274,7 @@ func TestLogStats_GroupByModel(t *testing.T) {
}
var resp GroupedStatsResponse
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
t.Fatalf("unmarshal: %v", err)
}
decodeEnvelope(t, rr, &resp)
if len(resp.Items) != 2 {
t.Fatalf("expected 2 groups, got %d: %+v", len(resp.Items), resp.Items)
}
@@ -324,6 +322,7 @@ func TestLogStats_DefaultBehavior(t *testing.T) {
h := &Handler{db: db, logDB: db}
r := gin.New()
r.Use(middleware.ResponseEnvelope())
r.GET("/admin/logs/stats", h.LogStats)
// Without group_by, should return aggregated stats
@@ -335,9 +334,7 @@ func TestLogStats_DefaultBehavior(t *testing.T) {
}
var resp LogStatsResponse
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
t.Fatalf("unmarshal: %v", err)
}
decodeEnvelope(t, rr, &resp)
if resp.Total != 2 {
t.Fatalf("expected total=2, got %d", resp.Total)
}
@@ -452,6 +449,7 @@ func TestTrafficChart_MinuteGranularityValidation(t *testing.T) {
h := &Handler{db: db, logDB: db}
r := gin.New()
r.Use(middleware.ResponseEnvelope())
r.GET("/admin/logs/stats/traffic-chart", h.GetTrafficChart)
tests := []struct {
@@ -503,8 +501,9 @@ func TestTrafficChart_MinuteGranularityValidation(t *testing.T) {
}
var resp map[string]any
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
t.Fatalf("unmarshal: %v", err)
env := decodeEnvelope(t, rr, &resp)
if env.Message != tt.wantError {
t.Fatalf("expected message=%q, got %q", tt.wantError, env.Message)
}
if errMsg, ok := resp["error"].(string); !ok || errMsg != tt.wantError {
t.Fatalf("expected error=%q, got %v", tt.wantError, resp["error"])