统一响应体设计
概述
统一响应体是一种标准化 API 响应格式的设计模式。本规范定义了基于 Ant Design Pro 标准的响应体格式,适用于前后端分离的 Web 应用。
设计原则
为什么需要统一响应体
统一响应体的优势:
- 前端处理简化:前端可用统一逻辑处理所有响应
- 错误处理标准化:成功和失败响应结构一致
- 类型安全:TypeScript 可准确推断响应类型
- 易于扩展:支持分页、元数据等扩展字段
对比示例:
go
// ❌ 不统一的响应格式
// 成功响应
{"data": {"id": 1, "name": "张三"}}
// 错误响应
{"error": "用户不存在"}
// 另一个接口的错误响应
{"message": "参数错误", "code": 400}
// ✅ 统一响应格式
// 成功响应
{"success": true, "data": {"id": 1, "name": "张三"}}
// 错误响应
{"success": false, "errorMessage": "用户不存在", "errorCode": 2001}Ant Design Pro 响应格式
标准格式定义
Ant Design Pro 定义了以下标准响应格式:
成功响应(单条数据)
json
{
"success": true,
"data": {
"id": 1,
"name": "张三",
"email": "user@example.com"
}
}成功响应(列表数据,不分页)
json
{
"success": true,
"data": [
{"id": 1, "name": "商品1"},
{"id": 2, "name": "商品2"}
]
}成功响应(分页数据)
json
{
"success": true,
"data": [
{"id": 1, "name": "用户1"},
{"id": 2, "name": "用户2"}
],
"total": 100,
"current": 1,
"pageSize": 20
}失败响应
json
{
"success": false,
"errorMessage": "参数格式错误",
"errorCode": 1000
}字段说明
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
success | boolean | 是 | 请求是否成功,true 成功,false 失败 |
data | any | 否 | 响应数据,成功时返回 |
errorMessage | string | 否 | 错误信息,失败时返回 |
errorCode | number | 否 | 错误码,失败时返回 |
total | number | 否 | 总记录数,分页时返回 |
current | number | 否 | 当前页码,分页时返回 |
pageSize | number | 否 | 每页大小,分页时返回 |
Go 后端实现
响应体结构体定义
go
// httpapi/response/response.go
package response
// BaseResponse 基础统一响应体
type BaseResponse struct {
Success bool `json:"success"` // 响应状态
Data interface{} `json:"data,omitempty"` // 响应数据(成功时返回)
ErrorMessage string `json:"errorMessage,omitempty"` // 错误信息(失败时返回)
ErrorCode int `json:"errorCode,omitempty"` // 错误码(失败时返回)
}
// PagedResponse 分页统一响应体
type PagedResponse struct {
Success bool `json:"success"` // 响应状态
Data interface{} `json:"data,omitempty"` // 响应数据列表
Total int64 `json:"total,omitempty"` // 总记录数
Current int `json:"current,omitempty"` // 当前页码
PageSize int `json:"pageSize,omitempty"` // 每页大小
ErrorMessage string `json:"errorMessage,omitempty"` // 错误信息
ErrorCode int `json:"errorCode,omitempty"` // 错误码
}构造函数
go
// Success 创建成功响应
func Success(data interface{}) BaseResponse {
return BaseResponse{
Success: true,
Data: data,
}
}
// Fail 创建失败响应
func Fail(errorCode int, errorMessage string) BaseResponse {
return BaseResponse{
Success: false,
ErrorMessage: errorMessage,
ErrorCode: errorCode,
}
}
// SuccessPaged 创建分页成功响应
func SuccessPaged(data interface{}, total int64, current, pageSize int) PagedResponse {
return PagedResponse{
Success: true,
Data: data,
Total: total,
Current: current,
PageSize: pageSize,
}
}
// FailPaged 创建分页失败响应
func FailPaged(errorCode int, errorMessage string) PagedResponse {
return PagedResponse{
Success: false,
ErrorMessage: errorMessage,
ErrorCode: errorCode,
}
}Handler 使用示例
单条数据响应
go
// handlers/user.go
package handlers
import (
"net/http"
"your-project/httpapi/response"
"github.com/labstack/echo/v4"
)
type UserHandler struct {
userService *services.UserService
}
// Get 获取用户详情
func (h *UserHandler) Get(c echo.Context) error {
id := c.Param("id")
// 调用 Service
user, err := h.userService.GetByID(c.Request().Context(), id)
if err != nil {
// ✅ 使用 response.Fail 返回错误响应
return c.JSON(http.StatusNotFound, response.Fail(2001, "用户不存在"))
}
// ✅ 使用 response.Success 返回成功响应
return c.JSON(http.StatusOK, response.Success(user))
}响应示例:
json
// 成功时
{
"success": true,
"data": {
"id": 1,
"name": "张三",
"email": "user@example.com"
}
}
// 失败时
{
"success": false,
"errorMessage": "用户不存在",
"errorCode": 2001
}创建资源响应
go
// Create 创建用户
func (h *UserHandler) Create(c echo.Context) error {
req := new(CreateUserRequest)
if err := c.Bind(req); err != nil {
return c.JSON(http.StatusBadRequest, response.Fail(1000, "参数格式错误"))
}
if err := c.Validate(req); err != nil {
return c.JSON(http.StatusBadRequest, response.Fail(1001, err.Error()))
}
user, err := h.userService.Create(c.Request().Context(), req)
if err != nil {
return c.JSON(http.StatusInternalServerError, response.Fail(5000, "创建用户失败"))
}
// ✅ 创建成功返回 201 状态码
return c.JSON(http.StatusCreated, response.Success(user))
}分页列表响应
go
// List 分页查询用户列表
func (h *UserHandler) List(c echo.Context) error {
page := c.QueryParam("current") // 当前页码
pageSize := c.QueryParam("pageSize") // 每页大小
// 调用 Service
users, total, err := h.userService.List(c.Request().Context(), page, pageSize)
if err != nil {
return c.JSON(http.StatusInternalServerError, response.Fail(5000, "查询失败"))
}
// ✅ 使用 response.SuccessPaged 返回分页响应
return c.JSON(http.StatusOK, response.SuccessPaged(users, total, page, pageSize))
}响应示例:
json
{
"success": true,
"data": [
{"id": 1, "name": "用户1", "email": "user1@example.com"},
{"id": 2, "name": "用户2", "email": "user2@example.com"}
],
"total": 100,
"current": 1,
"pageSize": 20
}删除操作响应
go
// Delete 删除用户
func (h *UserHandler) Delete(c echo.Context) error {
id := c.Param("id")
err := h.userService.Delete(c.Request().Context(), id)
if err != nil {
return c.JSON(http.StatusInternalServerError, response.Fail(5000, "删除失败"))
}
// ✅ 删除成功返回空数据
return c.JSON(http.StatusOK, response.Success(nil))
}响应示例:
json
{
"success": true
}TypeScript 前端实现
类型定义
typescript
// types/response.ts
// 基础响应类型
export interface BaseResponse<T = any> {
success: boolean
data?: T
errorMessage?: string
errorCode?: number
}
// 分页响应类型
export interface PagedResponse<T = any> {
success: boolean
data?: T[]
total?: number
current?: number
pageSize?: number
errorMessage?: string
errorCode?: number
}
// 分页请求参数
export interface PageParams {
current: number
pageSize: number
}前端请求封装
typescript
// utils/request.ts
import axios, { AxiosResponse } from 'axios'
import type { BaseResponse, PagedResponse } from '@/types/response'
// 创建 axios 实例
const request = axios.create({
baseURL: '/api',
timeout: 10000,
})
// 响应拦截器
request.interceptors.response.use(
(response: AxiosResponse<BaseResponse>) => {
const data = response.data
// ✅ 统一处理响应
if (data.success) {
return data // 成功时返回整个响应对象
} else {
// 失败时抛出错误
const error = new Error(data.errorMessage || '请求失败')
error.code = data.errorCode
throw error
}
},
(error) => {
// 处理 HTTP 错误
return Promise.reject(error)
}
)
export default request前端 API 调用
typescript
// services/user.ts
import request from '@/utils/request'
import type { BaseResponse, PagedResponse, PageParams } from '@/types/response'
// 用户类型定义
export interface User {
id: number
name: string
email: string
}
// 获取用户详情
export async function getUser(id: number): Promise<User> {
const res = await request.get<BaseResponse<User>>(`/users/${id}`)
return res.data! // 类型安全:success=true 时 data 必定存在
}
// 创建用户
export async function createUser(params: {
name: string
email: string
}): Promise<User> {
const res = await request.post<BaseResponse<User>>('/users', params)
return res.data!
}
// 分页查询用户列表
export async function getUserList(params: PageParams): Promise<PagedResponse<User>> {
const res = await request.get<PagedResponse<User>>('/users', { params })
return res // 返回完整的分页响应(包含 data、total、current、pageSize)
}
// 删除用户
export async function deleteUser(id: number): Promise<void> {
await request.delete(`/users/${id}`)
}React 组件使用
typescript
// pages/UserList.tsx
import React, { useEffect, useState } from 'react'
import { Table, message } from 'antd'
import { getUserList } from '@/services/user'
import type { User } from '@/services/user'
const UserList: React.FC = () => {
const [loading, setLoading] = useState(false)
const [users, setUsers] = useState<User[]>([])
const [total, setTotal] = useState(0)
const [current, setCurrent] = useState(1)
const [pageSize, setPageSize] = useState(20)
const fetchUsers = async (page: number, size: number) => {
setLoading(true)
try {
// ✅ 调用 API,自动处理统一响应格式
const res = await getUserList({ current: page, pageSize: size })
setUsers(res.data || [])
setTotal(res.total || 0)
setCurrent(res.current || 1)
setPageSize(res.pageSize || 20)
} catch (error) {
// ❌ 错误处理:显示 errorMessage
message.error(error.message || '查询失败')
} finally {
setLoading(false)
}
}
useEffect(() => {
fetchUsers(current, pageSize)
}, [])
return (
<Table
dataSource={users}
loading={loading}
pagination={{
current,
pageSize,
total,
onChange: (page, size) => fetchUsers(page, size),
}}
columns={[
{ title: 'ID', dataIndex: 'id' },
{ title: '姓名', dataIndex: 'name' },
{ title: '邮箱', dataIndex: 'email' },
]}
/>
)
}
export default UserList错误码规范
错误码分类
统一的错误码设计:
| 错误码范围 | 分类 | 示例 |
|---|---|---|
| 1000-1999 | 通用错误 | 1000: 参数错误, 1001: 参数校验失败 |
| 2000-2999 | 认证错误 | 2000: 未登录, 2001: 用户不存在, 2002: 密码错误 |
| 3000-3999 | 权限错误 | 3000: 无权限, 3001: 账号已禁用 |
| 4000-4999 | 业务错误 | 4000: 库存不足, 4001: 订单已关闭 |
| 5000-5999 | 系统错误 | 5000: 服务器内部错误, 5001: 数据库错误 |
详见 错误码规范
Go 错误码定义
go
// constants/error_codes.go
package constants
const (
// 通用错误 1000-1999
ErrCodeInvalidParams = 1000 // 参数格式错误
ErrCodeValidationFailed = 1001 // 参数校验失败
// 认证错误 2000-2999
ErrCodeNotLogin = 2000 // 未登录
ErrCodeUserNotFound = 2001 // 用户不存在
ErrCodeWrongPassword = 2002 // 密码错误
ErrCodeTokenExpired = 2003 // Token 过期
// 权限错误 3000-3999
ErrCodeNoPermission = 3000 // 无权限
ErrCodeAccountDisabled = 3001 // 账号已禁用
// 业务错误 4000-4999
ErrCodeInsufficientStock = 4000 // 库存不足
ErrCodeOrderClosed = 4001 // 订单已关闭
// 系统错误 5000-5999
ErrCodeInternalError = 5000 // 服务器内部错误
ErrCodeDatabaseError = 5001 // 数据库错误
)Handler 使用错误码
go
func (h *UserHandler) Login(c echo.Context) error {
req := new(LoginRequest)
if err := c.Bind(req); err != nil {
return c.JSON(http.StatusBadRequest,
response.Fail(constants.ErrCodeInvalidParams, "参数格式错误"))
}
user, err := h.userService.Login(c.Request().Context(), req.Email, req.Password)
if err != nil {
// 根据不同错误返回不同错误码
if err.Error() == "用户不存在" {
return c.JSON(http.StatusUnauthorized,
response.Fail(constants.ErrCodeUserNotFound, "用户名或密码错误"))
}
if err.Error() == "密码错误" {
return c.JSON(http.StatusUnauthorized,
response.Fail(constants.ErrCodeWrongPassword, "用户名或密码错误"))
}
return c.JSON(http.StatusInternalServerError,
response.Fail(constants.ErrCodeInternalError, "登录失败"))
}
return c.JSON(http.StatusOK, response.Success(user))
}最佳实践
1. 统一使用响应体
所有 Handler 必须使用统一响应体:
go
// ✅ 正确
return c.JSON(http.StatusOK, response.Success(user))
// ❌ 错误:直接返回业务数据
return c.JSON(http.StatusOK, user)
// ❌ 错误:自定义响应格式
return c.JSON(http.StatusOK, map[string]interface{}{
"code": 200,
"data": user,
})2. HTTP 状态码与 success 字段的关系
遵循以下规则:
| success | HTTP 状态码 | 说明 |
|---|---|---|
true | 200/201 | 成功响应 |
false | 400/401/403/404/500 | 错误响应 |
go
// ✅ 正确:success=true 时使用 2xx 状态码
return c.JSON(http.StatusOK, response.Success(data)) // 200
return c.JSON(http.StatusCreated, response.Success(data)) // 201
// ✅ 正确:success=false 时使用 4xx/5xx 状态码
return c.JSON(http.StatusBadRequest, response.Fail(1000, "参数错误")) // 400
return c.JSON(http.StatusUnauthorized, response.Fail(2000, "未登录")) // 401
return c.JSON(http.StatusForbidden, response.Fail(3000, "无权限")) // 403
return c.JSON(http.StatusNotFound, response.Fail(2001, "用户不存在")) // 404
return c.JSON(http.StatusInternalServerError, response.Fail(5000, "服务器错误")) // 5003. 空数据处理
成功但无数据时返回 null 或空数组:
go
// 删除操作:返回 null
return c.JSON(http.StatusOK, response.Success(nil))
// 查询列表为空:返回空数组
return c.JSON(http.StatusOK, response.SuccessPaged([]User{}, 0, 1, 20))4. 分页参数命名
使用 Ant Design Pro 的分页参数命名:
| 参数 | 说明 | 类型 |
|---|---|---|
current | 当前页码(从 1 开始) | number |
pageSize | 每页大小 | number |
total | 总记录数 | number |
检查清单
实施统一响应体时,确保以下要点:
- [ ] 所有 Handler 使用
response.Success或response.Fail - [ ] 不直接返回业务数据或自定义响应格式
- [ ] 错误响应包含
errorCode和errorMessage - [ ] 分页响应包含
data、total、current、pageSize - [ ] HTTP 状态码与
success字段保持一致 - [ ] 错误码遵循统一分类规范