--- tags: [] aliases: - 📦 统一响应结构定义 (The Contract) date created: 星期三, 十二月 10日 2025, 11:23:15 上午 date modified: 星期三, 十二月 10日 2025, 12:12:46 中午 --- # Phase 1 统一响应结构定义 (The Contract) ## 📦 统一响应结构定义 (The Contract) 所有 HTTP 接口(无论成功与否)必须严格返回以下 JSON 结构: ```JSON { "code": 20001, // 业务状态码 (0=成功, 非0=错误) "msg": "用户已存在", // 用户可见的提示文案 (Safe Message) "data": { ... }, // 业务数据 payload (成功时返回,失败时通常为 null) "trace_id": "a1b2-c3d4" // 全链路追踪 ID (必填,用于 SRE 排查) } ``` --- ## 🎨 场景示例与设计理由 ### 🟢 场景 A: 成功返回对象 (Single Object) 请求: GET /api/v1/users/1001 HTTP Status: 200 OK ```JSON { "code": 0, "msg": "OK", "data": { "user_id": 1001, "nickname": "TechLead_01", "avatar": "https://cdn.example.com/u/1001.jpg" }, "trace_id": "0a1b2c3d-4e5f-6789-1234-567890abcdef" } ``` **📌 设计理由:** - **Code 0:** 符合业界惯例(如 Google/Tencent API),`0` 明确表示逻辑执行成功。 - **Data 类型:** 返回具体的 Object。 --- ### 🟡 场景 B: 成功返回空列表 (Empty List) 请求: GET /api/v1/articles?category=golang (假设该分类下无文章) HTTP Status: 200 OK ```JSON { "code": 0, "msg": "OK", "data": { "list": [], "total": 0 }, "trace_id": "0a1b2c3d-4e5f-6789-1234-567890abcdef" } ``` **📌 设计理由:** - **Data 不为 `null`:** 对于列表型接口,`data` 内部的 `list` 字段必须返回空数组 `[]`,而不是 `null`。 - _原因:_ 前端可以直接调用 `.map()` 或 `.forEach()` 而无需判空,极大降低前端出现 `Cannot read property 'map' of null` 的崩溃风险。 - **结构一致性:** 即使是列表,建议包裹在 Object 中(如 `{list: [], total: 0}`),方便未来扩展分页字段。 --- ### 🔴 场景 C: 业务/系统错误 (Error Handling) 这里我们需要区分 **“预期内的业务错误”** 和 **“预期外的系统错误”**,但在 JSON 表现上它们必须是一致的。 Case C-1: 预期内的业务错误 场景: 用户尝试注册已存在的邮箱。 Service 层返回: ecode.UserAlreadyExist (Code: 20001) ```JSON { "code": 20001, "msg": "用户已存在", "data": null, "trace_id": "0a1b2c3d-4e5f-6789-1234-567890abcdef" } ``` Case C-2: 预期外的系统错误 (触发安全防御) 场景: 数据库突然断连,GORM 返回 dial tcp 127.0.0.1:5432: connect: connection refused。 Service 层返回: 原生 error 对象。 ```JSON { "code": 50000, "msg": "Internal Server Error", // <--- 严禁显示 "dial tcp ..." "data": null, "trace_id": "0a1b2c3d-4e5f-6789-1234-567890abcdef" } ``` **📌 设计理由:** - **Security (安全降级):** - `app.Error(err)` 的内部逻辑必须执行 **Type Assertion (类型断言)**。 - 若 `err` 是 `*ecode.Error`,则透传其 `msg`(Case C-1)。 - 若 `err` 是普通 `error`(Case C-2),视为系统级异常。**必须**将 JSON 中的 `msg` 强制重写为 `"Internal Server Error"` 或通用文案,防止数据库表结构、IP 地址等敏感信息泄露给攻击者。 - **HTTP 200:** 即使是 Code 50000,HTTP Status 依然保持 200。这确保了网关层(Nginx/Gateway)不会拦截 Body,前端始终能解析 JSON 拿到 `code` 和 `trace_id` 用于展示和报错。 --- ## 🛡️ 关键实现逻辑预告 为了实现上述契约,在接下来的 **Step 5: `internal/pkg/app/response.go`** 中,我们将实现如下核心逻辑: 1. **Trace ID 注入:** 在 `New(c)` 时执行 `r.traceID = c.GetString("trace_id")`。 2. **错误清洗:** ```Go // 伪代码逻辑预览 func (r *Response) Error(err error) { if e, ok := err.(*ecode.Error); ok { // 业务错误:直接使用 r.json(e.Code(), e.Msg()) } else { // 系统错误:记录原始日志,但在 JSON 中降级 log.Error("System Error", zap.Error(err), zap.String("trace_id", r.traceID)) r.json(ecode.ServerErr, "Internal Server Error") // 🔒 安全替换 } } ``` 这套设计已经满足了 **Phase 1** 的所有契约要求。