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 统一请求与响应流程

下图展示了从客户端发起请求到服务端返回响应的完整数据流转,以及各层对统一结构的处理职责:

统一请求与响应数据流转 客户端 组装 UnifiedRequest API 网关 校验签名 / 提取 TraceID 业务服务 解析 Data / 执行业务 响应处理 组装 UnifiedResponse 错误码映射 Code / Message 赋值 返回客户端 JSON 序列化 + HTTP 响应 关键约束 1. 所有接口必须使用 UnifiedRequest 作为请求体,UnifiedResponse 作为响应体。 2. 错误码必须从 errorcode 包引用,禁止直接写数字。 3. 分页查询必须使用 PageParam 请求,PageResponse 响应。 4. TraceID 由网关生成并透传,业务服务不得修改。

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 内部结构,外层结构固定不变。
  • 测试:可编写通用的响应断言工具函数,提升测试效率。