From eac0137069cc172341f622039f767ec3826ef451 Mon Sep 17 00:00:00 2001 From: shanshanzhong147 Date: Sun, 12 Jul 2026 19:55:35 -0700 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D(#4):=20=E7=BB=9F=E4=B8=80=20?= =?UTF-8?q?API=20=E5=93=8D=E5=BA=94=E6=A0=BC=E5=BC=8F=20=E2=80=94=20Respon?= =?UTF-8?q?seErrorBean=20=E8=A1=A5=20data=20=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes part of HIF-4 (Stage 2 前端集成反馈 F9 - 全站响应格式) F9 (P1): 全站响应统一 {code, msg, data} 三字段固定 shape - ResponseSuccessBean.Data 去掉 omitempty → 空 payload 也返 "data":null - ResponseErrorBean 加 Data interface{} 字段 → 错误响应也带 "data":null - App 端强类型 decoder 依赖固定字段 shape,缺 data 键会解码失败 回归护栏 3 用例: - TestResponseErrorBean_HasDataField: 错误响应必须含 "data":null - TestResponseSuccessBean_DataAlwaysPresent: Success(nil) 也含 "data":null(防 omitempty 回退) - TestResponseSuccessBean_WithPayload: 正常 payload 序列化正确 影响面:全站所有响应(不止 lottery),JSON 加字段/字段值变 null 对现有 client 无 breaking(宽松 decoder 全兼容) CI 全绿;单 file 改动 revert 一步搞定 --- pkg/result/responseBean.go | 23 +++++++++++--- pkg/result/responseBean_test.go | 54 +++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 pkg/result/responseBean_test.go diff --git a/pkg/result/responseBean.go b/pkg/result/responseBean.go index f6ca3ea..7b15881 100644 --- a/pkg/result/responseBean.go +++ b/pkg/result/responseBean.go @@ -1,9 +1,21 @@ +// Package result 定义 HTTP 响应统一 envelope。 +// +// HIF-4 F9:所有响应必须是三字段固定 shape {code, msg, data},data 为空时 +// 也要显式 `null` —— 而不是靠 omitempty 丢字段。App 端强类型 decoder(Retrofit / +// serde / typed structs)依赖这个 shape,缺字段会解码失败。 +// +// 修改前: +// - Success 的 Data 带 omitempty,nil payload 时 JSON 里没有 data 键 +// - Error 结构体没有 Data 字段,错误响应永远缺 data 键 +// 修改后:Success 和 Error 都有 Data 字段,且都 **不带 omitempty**。空场景下 JSON +// 一律出现 "data":null。加字段对旧 client 无 breaking impact(JSON 忽略未知字段/ +// 已知字段变 null 都能 decode 过)。 package result type ResponseSuccessBean struct { Code uint32 `json:"code"` Msg string `json:"msg"` - Data interface{} `json:"data,omitempty"` + Data interface{} `json:"data"` // F9: 不带 omitempty,空 payload 也返 data:null } type NullJson struct{} @@ -11,11 +23,14 @@ func Success(data interface{}) *ResponseSuccessBean { return &ResponseSuccessBean{200, "success", data} } +// ResponseErrorBean 与 ResponseSuccessBean 结构对齐(都有 Data 字段), +// Data 在错误场景永远为 nil;序列化后 JSON 里显式为 "data":null。 type ResponseErrorBean struct { - Code uint32 `json:"code"` - Msg string `json:"msg"` + Code uint32 `json:"code"` + Msg string `json:"msg"` + Data interface{} `json:"data"` // F9: 错误响应也必须有 data 字段(值永远为 null) } func Error(errCode uint32, errMsg string) *ResponseErrorBean { - return &ResponseErrorBean{errCode, errMsg} + return &ResponseErrorBean{errCode, errMsg, nil} } diff --git a/pkg/result/responseBean_test.go b/pkg/result/responseBean_test.go new file mode 100644 index 0000000..cfdab2e --- /dev/null +++ b/pkg/result/responseBean_test.go @@ -0,0 +1,54 @@ +// responseBean_test.go — HIF-4 F9 护栏: +// 所有 HTTP 响应必须是 {code, msg, data} 三字段固定 shape,data 为空时也要 +// 显式 "null"(不是丢字段)。App 端强类型 decoder 依赖这个 shape。 +package result + +import ( + "encoding/json" + "strings" + "testing" +) + +// TestResponseErrorBean_HasDataField 断言错误响应的 JSON 一定包含 data 字段 +// 且为 null。如果未来有人给 Data 加了 omitempty,或者删掉 Data 字段,这里会挂。 +func TestResponseErrorBean_HasDataField(t *testing.T) { + body, err := json.Marshal(Error(40003, "User token is invalid")) + if err != nil { + t.Fatalf("marshal: %v", err) + } + s := string(body) + if !strings.Contains(s, `"data":null`) { + t.Fatalf("F9 regression: error response missing 'data:null', got %s", s) + } + // 顺带断言 code + msg 也在(防止未来无脑重构删字段) + if !strings.Contains(s, `"code":40003`) || !strings.Contains(s, `"msg":"User token is invalid"`) { + t.Fatalf("error response missing code/msg, got %s", s) + } +} + +// TestResponseSuccessBean_DataAlwaysPresent 断言成功响应在 data 为 nil 时 +// 也显式返回 "data":null(不是丢字段)。这是 F9 的另一半——如果有人未来把 +// omitempty 加回去,只解决错误响应的对称性会被打破。 +func TestResponseSuccessBean_DataAlwaysPresent(t *testing.T) { + body, err := json.Marshal(Success(nil)) + if err != nil { + t.Fatalf("marshal: %v", err) + } + s := string(body) + if !strings.Contains(s, `"data":null`) { + t.Fatalf("F9 regression: Success(nil) missing 'data:null' (omitempty regression?), got %s", s) + } +} + +// TestResponseSuccessBean_WithPayload 副断言:非 nil payload 时 +// data 字段确实带上,且序列化正常。 +func TestResponseSuccessBean_WithPayload(t *testing.T) { + body, err := json.Marshal(Success(map[string]any{"id": 42, "name": "abc"})) + if err != nil { + t.Fatalf("marshal: %v", err) + } + s := string(body) + if !strings.Contains(s, `"data":{`) || !strings.Contains(s, `"id":42`) { + t.Fatalf("success payload not serialized correctly, got %s", s) + } +}