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) + } +}