146 lines
4.2 KiB
Markdown
146 lines
4.2 KiB
Markdown
---
|
||
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** 的所有契约要求。
|