修复(#4): 统一 API 响应格式 — ResponseErrorBean 补 data 字段

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 一步搞定
This commit is contained in:
2026-07-12 19:55:35 -07:00
committed by GitHub
parent cce147108c
commit eac0137069
2 changed files with 73 additions and 4 deletions
+17 -2
View File
@@ -1,9 +1,21 @@
// Package result 定义 HTTP 响应统一 envelope。
//
// HIF-4 F9:所有响应必须是三字段固定 shape {code, msg, data}data 为空时
// 也要显式 `null` —— 而不是靠 omitempty 丢字段。App 端强类型 decoderRetrofit /
// serde / typed structs)依赖这个 shape,缺字段会解码失败。
//
// 修改前:
// - Success 的 Data 带 omitemptynil payload 时 JSON 里没有 data 键
// - Error 结构体没有 Data 字段,错误响应永远缺 data 键
// 修改后:Success 和 Error 都有 Data 字段,且都 **不带 omitempty**。空场景下 JSON
// 一律出现 "data":null。加字段对旧 client 无 breaking impactJSON 忽略未知字段/
// 已知字段变 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"`
Data interface{} `json:"data"` // F9: 错误响应也必须有 data 字段(值永远为 null)
}
func Error(errCode uint32, errMsg string) *ResponseErrorBean {
return &ResponseErrorBean{errCode, errMsg}
return &ResponseErrorBean{errCode, errMsg, nil}
}
+54
View File
@@ -0,0 +1,54 @@
// responseBean_test.go — HIF-4 F9 护栏:
// 所有 HTTP 响应必须是 {code, msg, data} 三字段固定 shapedata 为空时也要
// 显式 "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)
}
}