文章目录
- 一、企业级统一返回体通常包含哪些字段
-
- 常用字段说明
-
- [1. `success`](#1.
success) - [2. `code`](#2.
code) - [3. `message`](#3.
message) - [4. `data`](#4.
data) - [5. `timestamp`](#5.
timestamp) - [6. `traceId`](#6.
traceId) - [7. 可选扩展字段](#7. 可选扩展字段)
- [1. `success`](#1.
- 二、企业级统一返回体有哪些常见"类型"
-
- [1. 普通成功返回](#1. 普通成功返回)
- [2. 无数据成功返回](#2. 无数据成功返回)
- [3. 列表返回](#3. 列表返回)
- [4. 分页返回](#4. 分页返回)
- [5. 业务失败返回](#5. 业务失败返回)
- [6. 参数校验失败返回](#6. 参数校验失败返回)
- [7. 系统异常返回](#7. 系统异常返回)
- 三、企业级错误码体系通常有哪些分类
-
- [1. 按大类划分](#1. 按大类划分)
- [2. 常见错误码明细](#2. 常见错误码明细)
- [A 类:客户端错误](#A 类:客户端错误)
- [B 类:业务错误](#B 类:业务错误)
- [C 类:系统错误](#C 类:系统错误)
- [D 类:第三方服务错误](#D 类:第三方服务错误)
- 四、错误码体系设计原则
-
- [1. 全局唯一](#1. 全局唯一)
- [2. 可读可分段](#2. 可读可分段)
- [3. 不要把 HTTP 状态码当业务码](#3. 不要把 HTTP 状态码当业务码)
- [4. 业务码稳定,message 可变化](#4. 业务码稳定,message 可变化)
- [5. 错误码集中维护](#5. 错误码集中维护)
- [五、HTTP 状态码和业务错误码如何配合](#五、HTTP 状态码和业务错误码如何配合)
- 六、企业级全局异常处理器通常处理哪些异常
-
- [1. 业务异常 `BusinessException`](#1. 业务异常
BusinessException) - [2. 参数校验异常](#2. 参数校验异常)
- [3. 认证异常](#3. 认证异常)
- [4. 授权异常](#4. 授权异常)
- [5. 404 / 方法不支持 / 媒体类型不支持](#5. 404 / 方法不支持 / 媒体类型不支持)
- [6. 系统运行时异常](#6. 系统运行时异常)
- [7. 第三方接口异常](#7. 第三方接口异常)
- [1. 业务异常 `BusinessException`](#1. 业务异常
- 七、企业级代码结构通常怎么设计
-
- [1. 返回体对象](#1. 返回体对象)
- [2. 错误码枚举](#2. 错误码枚举)
- [3. 自定义业务异常](#3. 自定义业务异常)
- [4. 全局异常处理器](#4. 全局异常处理器)
- 八、企业里常见的增强设计
-
- [1. 字段级错误明细对象](#1. 字段级错误明细对象)
- [2. 统一日志打印](#2. 统一日志打印)
- [3. 敏感信息脱敏](#3. 敏感信息脱敏)
- [4. 国际化](#4. 国际化)
- [5. 与网关、监控打通](#5. 与网关、监控打通)
- 九、不推荐的做法
-
- [1. Controller 到处 try-catch](#1. Controller 到处 try-catch)
- [2. 直接把 Exception.getMessage() 返回前端](#2. 直接把 Exception.getMessage() 返回前端)
- [3. 业务异常和系统异常不区分](#3. 业务异常和系统异常不区分)
- [4. message 当主判断依据](#4. message 当主判断依据)
- [5. 错误码随手写](#5. 错误码随手写)
- 十、推荐模板
- 十一、目录结构
- 十二、理解
企业级一般会把这三块一起设计:
- 统一返回体
- 错误码体系
- 全局异常处理器
它们是配套的,不是孤立的。
你可以理解成:
- 统一返回体:接口"长什么样"
- 错误码体系:错误"怎么编号"
- 全局异常处理器:异常"怎么统一收口并返回"
下面我按企业项目常见做法给你拆开讲。
一、企业级统一返回体通常包含哪些字段
最常见的是这套:
json
{
"success": true,
"code": "00000",
"message": "操作成功",
"data": {},
"timestamp": 1735800000000,
"traceId": "a1b2c3d4"
}
常用字段说明
1. success
是否成功,布尔值。
json
"success": true
作用:
- 前端快速判断
- 日志、监控、网关便于统一识别
有些团队不加它,只看 code,也可以。
但企业里加上以后,可读性更强。
2. code
业务状态码,最核心字段。
json
"code": "00000"
作用:
- 区分成功/失败类型
- 前端按码处理业务分支
- 便于文档化、监控统计、问题排查
一般建议:
- 用 字符串 比用 int 更稳
- 便于保留前导 0
- 便于分段编码
3. message
给调用方看的说明信息。
json
"message": "参数校验失败"
作用:
- 给前端提示
- 给联调人员快速定位问题
- 给日志补充语义
注意:
- 面向用户的提示不要暴露内部实现
- 系统异常别直接把堆栈信息返回前端
4. data
实际业务数据。
json
"data": {
"id": 1,
"name": "Ethan"
}
特点:
- 成功时放业务结果
- 失败时一般为
null - 尽量始终保留这个字段,避免前端判空混乱
5. timestamp
响应时间戳。
json
"timestamp": 1735800000000
作用:
- 排查问题
- 和日志时间做对齐
- 便于客户端诊断
6. traceId
链路追踪 ID。
json
"traceId": "a1b2c3d4"
作用:
- 微服务排障核心字段
- 前端报错时把 traceId 带给后端,定位很快
企业项目里这个字段非常常见。
7. 可选扩展字段
有些项目还会加:
requestIdpatherrors:字段级校验错误明细extra:扩展信息
例如参数校验失败时:
json
{
"success": false,
"code": "A0400",
"message": "参数校验失败",
"data": null,
"errors": [
{ "field": "name", "message": "姓名不能为空" },
{ "field": "age", "message": "年龄必须大于0" }
],
"timestamp": 1735800000000,
"traceId": "a1b2c3d4"
}
二、企业级统一返回体有哪些常见"类型"
这里说的不是结构变种,而是业务使用场景。
1. 普通成功返回
json
{
"success": true,
"code": "00000",
"message": "操作成功",
"data": {
"id": 1001,
"username": "ethan"
}
}
2. 无数据成功返回
比如删除、修改状态。
json
{
"success": true,
"code": "00000",
"message": "删除成功",
"data": null
}
3. 列表返回
json
{
"success": true,
"code": "00000",
"message": "查询成功",
"data": [
{ "id": 1, "name": "A" },
{ "id": 2, "name": "B" }
]
}
4. 分页返回
企业里一般会单独定义分页对象:
json
{
"success": true,
"code": "00000",
"message": "查询成功",
"data": {
"records": [
{ "id": 1, "name": "A" },
{ "id": 2, "name": "B" }
],
"total": 120,
"pageNum": 1,
"pageSize": 10,
"pages": 12
}
}
5. 业务失败返回
json
{
"success": false,
"code": "B2001",
"message": "库存不足",
"data": null
}
6. 参数校验失败返回
json
{
"success": false,
"code": "A0400",
"message": "参数校验失败",
"data": null,
"errors": [
{ "field": "mobile", "message": "手机号格式不正确" }
]
}
7. 系统异常返回
json
{
"success": false,
"code": "C5000",
"message": "系统繁忙,请稍后重试",
"data": null,
"traceId": "a1b2c3d4"
}
三、企业级错误码体系通常有哪些分类
企业项目里,错误码不会乱写,一般会分层、分段、分域。
1. 按大类划分
一个常见设计:
| 分类 | 示例 | 含义 |
|---|---|---|
| 00000 | 成功 | 请求成功 |
| Axxxx | 客户端错误 | 参数、认证、权限等 |
| Bxxxx | 业务错误 | 库存不足、订单状态异常 |
| Cxxxx | 系统错误 | 服务异常、数据库异常 |
| Dxxxx | 第三方错误 | 调用支付、短信、物流失败 |
这是企业里很实用的一种分法。
2. 常见错误码明细
A 类:客户端错误
| 错误码 | 含义 |
|---|---|
| A0001 | 用户端错误 |
| A0100 | 用户注册错误 |
| A0200 | 用户登录错误 |
| A0300 | 访问权限错误 |
| A0400 | 请求参数错误 |
| A0401 | 必填参数缺失 |
| A0402 | 参数格式错误 |
| A0403 | 参数值超出范围 |
| A0410 | 请求方法不支持 |
| A0415 | 媒体类型不支持 |
| A0500 | 请求过于频繁 |
B 类:业务错误
| 错误码 | 含义 |
|---|---|
| B0001 | 业务处理失败 |
| B1001 | 用户不存在 |
| B1002 | 用户已禁用 |
| B2001 | 库存不足 |
| B2002 | 订单不存在 |
| B2003 | 订单状态不允许取消 |
| B3001 | 优惠券不存在 |
| B3002 | 优惠券已过期 |
| B3003 | 优惠券已使用 |
C 类:系统错误
| 错误码 | 含义 |
|---|---|
| C0001 | 系统执行出错 |
| C1001 | 数据库操作失败 |
| C1002 | 数据库连接异常 |
| C2001 | 缓存服务异常 |
| C3001 | 文件上传失败 |
| C4001 | 消息队列异常 |
| C5000 | 未知系统异常 |
D 类:第三方服务错误
| 错误码 | 含义 |
|---|---|
| D1001 | 支付调用失败 |
| D1002 | 短信发送失败 |
| D1003 | 物流接口调用失败 |
| D1004 | OCR 服务异常 |
四、错误码体系设计原则
1. 全局唯一
不能两个完全不同的问题共用一个码。
2. 可读可分段
看到错误码就大概知道归属:
A:客户端B:业务C:系统D:第三方
3. 不要把 HTTP 状态码当业务码
比如不要直接:
- 200 = 成功
- 500 = 失败
- 404 = 用户不存在
因为 HTTP 状态码和业务码是两套体系。
4. 业务码稳定,message 可变化
前端联调应该依赖 code,不要依赖 message 文案。
因为提示语后续可能改。
5. 错误码集中维护
建议统一放在:
- 枚举
enum - 常量类
- 错误码配置中心
不要散落在每个 service 里硬编码。
五、HTTP 状态码和业务错误码如何配合
企业项目里常见两种风格。
方案一:HTTP 状态码表达协议层,业务码表达业务层
这是更推荐的。
成功
- HTTP 200
- code =
00000
参数错误
- HTTP 400
- code =
A0400
未登录
- HTTP 401
- code =
A0201
无权限
- HTTP 403
- code =
A0301
资源不存在
- HTTP 404
- code =
B2002或A0404
系统异常
- HTTP 500
- code =
C5000
这套最清晰。
方案二:HTTP 永远 200,业务码区分全部状态
有些老项目会这样做。
例如:
json
{
"success": false,
"code": "A0301",
"message": "无访问权限",
"data": null
}
HTTP 仍返回 200。
这种做法不是不能用,但问题是:
- 不利于网关治理
- 不利于监控告警
- 不符合 REST 语义
- 第三方调用方体验差
所以企业新项目一般更偏向HTTP + 业务码并用。
六、企业级全局异常处理器通常处理哪些异常
全局异常处理器的目的,是把各种散乱异常统一转成标准返回体。
在 Spring Boot 里,一般用:
@RestControllerAdvice@ExceptionHandler
来做。
通常要处理这些异常。
1. 业务异常 BusinessException
这是最重要的一类。
你自己主动抛出的异常,表示"业务不满足"。
例如:
- 库存不足
- 订单状态非法
- 用户已被冻结
返回:
- 指定业务码
- 指定 message
- HTTP 一般 200/400/409,取决于团队规范
2. 参数校验异常
常见包括:
MethodArgumentNotValidExceptionBindExceptionConstraintViolationExceptionHttpMessageNotReadableException
对应场景:
@RequestBody参数校验失败- 表单对象绑定失败
@RequestParam/@PathVariable校验失败- JSON 格式错误
这类异常通常统一返回:
A0400参数校验失败- 附带字段错误列表
3. 认证异常
例如:
- 未登录
- token 无效
- token 过期
如果用了 Spring Security,还可能涉及:
AuthenticationException
返回:
- HTTP 401
A0201/A0202
4. 授权异常
例如:
- 没有访问某接口的权限
- 没有某角色/某资源权限
常见异常:
AccessDeniedException
返回:
- HTTP 403
A0301
5. 404 / 方法不支持 / 媒体类型不支持
例如:
NoHandlerFoundExceptionHttpRequestMethodNotSupportedExceptionHttpMediaTypeNotSupportedException
分别对应:
- 资源不存在
- GET/POST 用错
- Content-Type 不对
6. 系统运行时异常
例如:
NullPointerExceptionSQLExceptionRuntimeException
这类不能直接把原始异常暴露给前端。
应该:
- 后端日志打印完整堆栈
- 前端只返回统一兜底信息
例如:
C5000"系统繁忙,请稍后重试"
7. 第三方接口异常
比如:
- 支付网关超时
- 短信服务失败
- 调用外部接口 502
可以封装成统一异常:
ThirdPartyExceptionD1001/D1002
七、企业级代码结构通常怎么设计
一个比较常见的分层如下。
1. 返回体对象
java
public class ApiResponse<T> {
private boolean success;
private String code;
private String message;
private T data;
private Long timestamp;
private String traceId;
public static <T> ApiResponse<T> success(T data) {
ApiResponse<T> resp = new ApiResponse<>();
resp.setSuccess(true);
resp.setCode("00000");
resp.setMessage("操作成功");
resp.setData(data);
resp.setTimestamp(System.currentTimeMillis());
return resp;
}
public static <T> ApiResponse<T> fail(String code, String message) {
ApiResponse<T> resp = new ApiResponse<>();
resp.setSuccess(false);
resp.setCode(code);
resp.setMessage(message);
resp.setData(null);
resp.setTimestamp(System.currentTimeMillis());
return resp;
}
}
2. 错误码枚举
java
public enum ErrorCode {
SUCCESS("00000", "操作成功"),
PARAM_ERROR("A0400", "参数校验失败"),
PARAM_MISSING("A0401", "必填参数缺失"),
PARAM_FORMAT_ERROR("A0402", "参数格式错误"),
UNAUTHORIZED("A0201", "未登录或登录已过期"),
FORBIDDEN("A0301", "无访问权限"),
USER_NOT_FOUND("B1001", "用户不存在"),
STOCK_NOT_ENOUGH("B2001", "库存不足"),
ORDER_STATUS_INVALID("B2003", "订单状态异常"),
SYSTEM_ERROR("C5000", "系统繁忙,请稍后重试"),
DB_ERROR("C1001", "数据库异常"),
THIRD_PARTY_ERROR("D1001", "第三方服务异常");
private final String code;
private final String message;
ErrorCode(String code, String message) {
this.code = code;
this.message = message;
}
public String code() {
return code;
}
public String message() {
return message;
}
}
3. 自定义业务异常
java
public class BusinessException extends RuntimeException {
private final String code;
private final String message;
public BusinessException(ErrorCode errorCode) {
super(errorCode.message());
this.code = errorCode.code();
this.message = errorCode.message();
}
public BusinessException(String code, String message) {
super(message);
this.code = code;
this.message = message;
}
public String getCode() {
return code;
}
@Override
public String getMessage() {
return message;
}
}
4. 全局异常处理器
java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ApiResponse<Void>> handleBusinessException(BusinessException ex) {
return ResponseEntity
.badRequest()
.body(ApiResponse.fail(ex.getCode(), ex.getMessage()));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse<List<String>>> handleValidException(MethodArgumentNotValidException ex) {
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(err -> err.getField() + ":" + err.getDefaultMessage())
.toList();
ApiResponse<List<String>> resp = ApiResponse.fail(ErrorCode.PARAM_ERROR.code(), ErrorCode.PARAM_ERROR.message());
resp.setData(errors);
return ResponseEntity.badRequest().body(resp);
}
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ApiResponse<Void>> handleConstraintViolationException(ConstraintViolationException ex) {
return ResponseEntity
.badRequest()
.body(ApiResponse.fail(ErrorCode.PARAM_ERROR.code(), ex.getMessage()));
}
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseEntity<ApiResponse<Void>> handleMethodNotSupported(HttpRequestMethodNotSupportedException ex) {
return ResponseEntity
.status(HttpStatus.METHOD_NOT_ALLOWED)
.body(ApiResponse.fail("A0410", "请求方法不支持"));
}
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ApiResponse<Void>> handleAccessDeniedException(AccessDeniedException ex) {
return ResponseEntity
.status(HttpStatus.FORBIDDEN)
.body(ApiResponse.fail(ErrorCode.FORBIDDEN.code(), ErrorCode.FORBIDDEN.message()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleException(Exception ex) {
// 这里应该记录完整日志和堆栈
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.fail(ErrorCode.SYSTEM_ERROR.code(), ErrorCode.SYSTEM_ERROR.message()));
}
}
八、企业里常见的增强设计
1. 字段级错误明细对象
不要只返回一句"参数错误",要能指出哪个字段错了。
例如:
java
public class FieldErrorInfo {
private String field;
private Object rejectedValue;
private String message;
}
2. 统一日志打印
全局异常处理器里通常要配合日志:
- 业务异常:warn
- 系统异常:error
- 打印 traceId
- 打印请求路径、参数、用户信息
3. 敏感信息脱敏
不能把这些直接返回前端:
- SQL 语句
- 堆栈
- 数据库表名
- 文件路径
- 第三方密钥信息
4. 国际化
message 有时会走 i18n:
code固定message根据 locale 翻译
5. 与网关、监控打通
通过 code + traceId + path + httpStatus 做:
- 错误统计
- 告警
- SLA 分析
- 问题定位
九、不推荐的做法
1. Controller 到处 try-catch
java
try {
...
} catch (Exception e) {
return xxx;
}
问题:
- 重复代码太多
- 风格不统一
- 容易漏处理
应该交给全局异常处理器。
2. 直接把 Exception.getMessage() 返回前端
这很危险,容易泄露内部实现。
3. 业务异常和系统异常不区分
比如全部返回"操作失败"。
这样前端没法处理,排障也困难。
4. message 当主判断依据
前端不能写这种逻辑:
java
if (message.equals("库存不足")) { ... }
应该判断 code。
5. 错误码随手写
例如:
1001err_1fail_stock500
混在一起非常难维护。
十、推荐模板
如果你是 Java / Spring Boot 项目,我比较推荐这套:
返回体
json
{
"success": true,
"code": "00000",
"message": "操作成功",
"data": {},
"timestamp": 1735800000000,
"traceId": "xxx"
}
错误码分层
00000:成功Axxxx:客户端错误Bxxxx:业务错误Cxxxx:系统错误Dxxxx:第三方错误
异常分类
BusinessException- 参数校验异常
- 认证异常
- 授权异常
- 方法不支持异常
- 媒体类型异常
- 系统兜底异常
HTTP 规范
- 200:成功
- 400:参数错误
- 401:未认证
- 403:无权限
- 404:资源不存在
- 409:状态冲突
- 500:系统异常
十一、目录结构
text
common
├─ response
│ ├─ ApiResponse.java
│ ├─ PageResponse.java
│ └─ ResultUtils.java
├─ error
│ ├─ ErrorCode.java
│ ├─ BusinessException.java
│ ├─ ThirdPartyException.java
│ └─ ErrorCodes.java
├─ exception
│ └─ GlobalExceptionHandler.java
└─ model
└─ FieldErrorInfo.java
十二、理解
为了解决这些问题:
- 前后端联调成本高
- 错误处理不一致
- 日志追踪困难
- 微服务排障复杂
- 文档和测试难统一
所以它的核心价值是:
规范接口契约,统一错误语义,降低系统复杂度。