4、统一请求与响应模型:定义通用Request/Response结构体、错误码枚举、分页参数封装
在支付订单聚合系统中,数十个接口如果各自定义请求/响应格式,将导致客户端适配成本激增、服务端校验逻辑分散、文档维护困难。本章通过定义通用 Request/Response 结构体、错误码枚举和分页参数封装,建立一套契约级别的通信规范,确保所有接口的输入输出格式一致、错误语义明确、分页行为可预测。
4.1 统一响应模型:Response 结构体
所有接口的响应体必须遵循以下结构,不允许额外包装或遗漏字段:
// UnifiedResponse 是所有API响应的统一外壳
type UnifiedResponse struct {
Code int `json:"code"` // 业务错误码,0表示成功
Message string `json:"message"` // 可读的错误描述或成功提示
Data interface{} `json:"data"` // 实际业务数据,可为nil
TraceID string `json:"trace_id"`// 全链路追踪ID,用于问题排查
Timestamp int64 `json:"timestamp"` // 服务器时间戳(Unix毫秒)
}
设计要点:
- Code 优先:HTTP Status Code 仅表示传输层状态(200/400/500),业务成功与否由
code字段决定。 - Data 泛化:使用
interface{}承载任意业务结构,避免为每个接口定义独立响应类型。 - TraceID 必填:支付场景下,一次请求可能跨越多个微服务,TraceID 是串联日志的唯一凭证。
4.2 统一请求模型:Request 结构体
对于需要请求体的接口(POST/PUT),统一使用以下结构体作为外层容器:
// UnifiedRequest 是所有API请求的统一外壳
type UnifiedRequest struct {
RequestID string `json:"request_id"` // 客户端生成的幂等键,全局唯一
Timestamp int64 `json:"timestamp"` // 客户端时间戳(Unix毫秒)
Data interface{} `json:"data"` // 实际业务请求参数
}
幂等性保障:
RequestID 由客户端生成,服务端根据该字段进行去重。对于支付创建、退款等敏感操作,重复提交相同的 RequestID 只会生效一次,返回第一次的处理结果。
4.3 错误码枚举:ErrorCode
错误码采用三段式数字编码:ABBBC,其中 A 表示错误级别,BBB 表示业务模块,C 表示具体错误序号。所有错误码集中定义在 errorcode 包中,不允许在业务代码中硬编码数字。
| 错误码 | 级别 | 模块 | 描述 | 处理建议 |
|---|---|---|---|---|
| 0 | 成功 | - | 请求处理成功 | 正常解析 Data |
| 1001 | 参数错误 | 通用 | 请求参数校验失败 | 检查必填字段和格式 |
| 1002 | 参数错误 | 通用 | 请求ID重复(幂等冲突) | 检查 RequestID 是否已使用 |
| 2001 | 业务错误 | 订单 | 订单不存在 | 确认订单号是否正确 |
| 2002 | 业务错误 | 订单 | 订单状态不允许当前操作 | 查询订单状态后重试 |
| 3001 | 支付错误 | 渠道 | 支付渠道调用超时 | 稍后重试或切换渠道 |
| 4001 | 系统错误 | 通用 | 内部服务器异常 | 联系运维查看日志 |
| 5001 | 鉴权错误 | 通用 | 签名验证失败 | 检查签名算法和密钥 |
// errorcode/errorcode.go
package errorcode
const (
Success = 0 // 成功
ErrParamInvalid = 1001 // 参数校验失败
ErrRequestIDDuplicate = 1002 // 幂等ID重复
ErrOrderNotFound = 2001 // 订单不存在
ErrOrderStatusInvalid = 2002 // 订单状态不允许
ErrChannelTimeout = 3001 // 支付渠道超时
ErrInternalServer = 4001 // 内部错误
ErrSignatureInvalid = 5001 // 签名验证失败
)
// MessageMap 错误码到描述文字的映射
var MessageMap = map[int]string{
Success: "success",
ErrParamInvalid: "invalid parameter",
ErrRequestIDDuplicate: "duplicate request_id",
ErrOrderNotFound: "order not found",
ErrOrderStatusInvalid: "order status not allowed",
ErrChannelTimeout: "payment channel timeout",
ErrInternalServer: "internal server error",
ErrSignatureInvalid: "signature verification failed",
}
4.4 分页参数封装:PageParam 与 PageResponse
所有列表查询接口必须使用统一的分页结构,禁止各自定义 page/limit 或 offset/size 等不同命名。
// PageParam 请求分页参数
type PageParam struct {
Page int `json:"page"` // 当前页码,从1开始
PageSize int `json:"page_size"` // 每页记录数,默认20,最大100
}
// PageResponse 分页响应结构
type PageResponse struct {
Total int64 `json:"total"` // 总记录数
Page int `json:"page"` // 当前页码
PageSize int `json:"page_size"` // 每页记录数
List interface{} `json:"list"` // 当前页数据列表
}
分页边界规则:
page小于 1 时,服务端自动修正为 1。page_size小于 1 时,使用默认值 20;大于 100 时,截断为 100。- 总页数由服务端计算,客户端不应依赖
total做分页循环,应使用page+page_size递增请求。
4.5 统一请求与响应流程
下图展示了从客户端发起请求到服务端返回响应的完整数据流转,以及各层对统一结构的处理职责:
4.6 完整示例:创建支付订单接口
以下代码展示了一个实际接口如何应用上述统一模型:
// handler/create_order.go
func CreateOrderHandler(c *gin.Context) {
// 1. 解析统一请求
var req UnifiedRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, UnifiedResponse{
Code: errorcode.ErrParamInvalid,
Message: errorcode.MessageMap[errorcode.ErrParamInvalid],
TraceID: c.GetString("trace_id"),
Timestamp: time.Now().UnixMilli(),
})
return
}
// 2. 解析业务数据
var orderReq CreateOrderData
if err := mapstructure.Decode(req.Data, &orderReq); err != nil {
c.JSON(400, UnifiedResponse{
Code: errorcode.ErrParamInvalid,
Message: "invalid order data format",
TraceID: c.GetString("trace_id"),
Timestamp: time.Now().UnixMilli(),
})
return
}
// 3. 幂等校验
if exists := idempotent.Check(req.RequestID); exists {
c.JSON(200, UnifiedResponse{
Code: errorcode.ErrRequestIDDuplicate,
Message: errorcode.MessageMap[errorcode.ErrRequestIDDuplicate],
TraceID: c.GetString("trace_id"),
Timestamp: time.Now().UnixMilli(),
})
return
}
// 4. 执行业务
order, err := svc.CreateOrder(orderReq)
if err != nil {
c.JSON(500, UnifiedResponse{
Code: errorcode.ErrInternalServer,
Message: err.Error(),
TraceID: c.GetString("trace_id"),
Timestamp: time.Now().UnixMilli(),
})
return
}
// 5. 成功返回
c.JSON(200, UnifiedResponse{
Code: errorcode.Success,
Message: errorcode.MessageMap[errorcode.Success],
Data: order,
TraceID: c.GetString("trace_id"),
Timestamp: time.Now().UnixMilli(),
})
}
4.7 分页查询示例:订单列表接口
// handler/list_order.go
func ListOrderHandler(c *gin.Context) {
var page PageParam
if err := c.ShouldBindQuery(&page); err != nil {
c.JSON(400, UnifiedResponse{
Code: errorcode.ErrParamInvalid,
Message: "page or page_size invalid",
TraceID: c.GetString("trace_id"),
Timestamp: time.Now().UnixMilli(),
})
return
}
// 修正边界
if page.Page < 1 {
page.Page = 1
}
if page.PageSize < 1 {
page.PageSize = 20
} else if page.PageSize > 100 {
page.PageSize = 100
}
total, list, err := svc.ListOrder(page.Page, page.PageSize)
if err != nil {
c.JSON(500, UnifiedResponse{
Code: errorcode.ErrInternalServer,
Message: err.Error(),
TraceID: c.GetString("trace_id"),
Timestamp: time.Now().UnixMilli(),
})
return
}
c.JSON(200, UnifiedResponse{
Code: errorcode.Success,
Message: errorcode.MessageMap[errorcode.Success],
Data: PageResponse{
Total: total,
Page: page.Page,
PageSize: page.PageSize,
List: list,
},
TraceID: c.GetString("trace_id"),
Timestamp: time.Now().UnixMilli(),
})
}
契约收益总结:
- 客户端:只需一套反序列化逻辑,即可处理所有接口响应。
- 服务端:错误码集中管理,避免重复定义和歧义。
- 文档:接口文档只需描述 Data 内部结构,外层结构固定不变。
- 测试:可编写通用的响应断言工具函数,提升测试效率。