本文目录导读:

在 PHP 业务开发中,业务异常分类 的核心目标不是“捕获错误”,而是让错误处理变得可预测、可追踪、可恢复,一个好的异常分类体系,能让你的代码从“满是 if-else 的泥潭”进化为“优雅的契约式编程”。
以下是针对 PHP 后端的业务异常分类实战指南,分为顶层分类、常见商业场景和代码落地三个层面。
顶层分类(按错误性质)
这是最基础的分层,决定了异常是否应该被用户看到。
| 分类 | 核心特征 | 是否提示用户 | 是否记录日志 | 典型示例 |
|---|---|---|---|---|
| 系统异常 | 代码BUG、依赖宕机、不可控因素 | ❌(显示友好提示) | ✅(必须详细记录堆栈) | 数据库连接失败、Redis超时、NPE、文件权限错误 |
| 业务异常 | 业务规则不允许、状态冲突、参数不满足 | ✅(显示具体原因) | ⚠️(一般记录,便于排查) | 库存不足、余额不足、订单已取消、密码错误 |
| 参数校验异常 | 外部输入不合法(格式/范围) | ✅(显示具体字段错误) | ❌(通常不记录堆栈,防日志刷屏) | 手机号格式错误、年龄超出范围、邮箱为空 |
| 安全/权限异常 | 未登录、无权限、越权操作 | ✅(跳转登录/提示403) | ✅(必须记录,防攻击) | Token过期、无权访问该资源、CSRF校验失败 |
业务异常细分(按商业场景)
在顶层分类下,业务领域内通常根据业务状态机和资源类型划分:
领域状态冲突异常
- 场景:业务流程有先后顺序(如:已发货的订单不能再改地址)。
- 类型:
OrderStateException、ApprovalFlowException。 - 处理:通常需要捕获后提示“当前状态不允许此操作”。
资源耗尽/不足异常
- 场景:库存扣减、金额扣减、积分消耗。
- 类型:
InsufficientStockException、InsufficientBalanceException、QuotaLimitExceededException。 - 处理:必须伴随事务回滚,且可能需要判断是否并发(乐观锁失败)。
依赖服务异常
- 场景:调用第三方支付、短信、物流API失败。
- 类型:
PaymentGatewayException、SmsServiceException。 - 处理:必须设计重试机制,且对下游返回的错误进行分类(可重试 vs 不可重试)。
幂等性异常
- 场景:客户端重复提交订单、重复点击支付按钮。
- 类型:
DuplicateRequestException、IdempotencyConflictException。 - 处理:通常返回原来的成功结果,或提示“请勿重复操作”。
版本冲突异常
- 场景:多人编辑同一数据(乐观锁)。
- 类型:
OptimisticLockException、DataConflictException。 - 处理:提示用户“数据已更新,请刷新”。
核心落地代码(PHP 实现示例)
为了避免直接用 Exception 类,建议使用 异常接口 + 基础抽象类 设计。
步骤 1:定义接口(标记唯一性)
<?php
namespace App\Exceptions;
/**
* 业务异常接口
* 实现该接口的异常,会被全局处理器捕获并输出给用户(而非500错误)
*/
interface BusinessExceptionInterface
{
public function getErrorCode(): string|int;
public function getErrorMessage(): string;
public function getExtraData(): array;
}
步骤 2:创建基础业务异常类
<?php
namespace App\Exceptions;
class BusinessException extends \RuntimeException implements BusinessExceptionInterface
{
protected string|int $errorCode;
protected array $extraData = [];
public function __construct(
string|int $errorCode,
string $message,
array $extraData = [],
?\Throwable $previous = null
) {
$this->errorCode = $errorCode;
$this->extraData = $extraData;
parent::__construct($message, (int)$errorCode, $previous);
}
public function getErrorCode(): string|int { return $this->errorCode; }
public function getErrorMessage(): string { return $this->getMessage(); }
public function getExtraData(): array { return $this->extraData; }
}
步骤 3:根据场景定义子类(分类落地)
<?php
namespace App\Exceptions\Order;
use App\Exceptions\BusinessException;
class InsufficientStockException extends BusinessException
{
// 构造函数中直接写死错误码和默认提示
public function __construct(int $remaningStock = 0)
{
parent::__construct(
10001, // 错误码
'库存不足',
['remaning_stock' => $remaningStock]
);
}
}
class OrderStateConflictException extends BusinessException
{
public function __construct(string $currentState, string $action)
{
parent::__construct(
10002,
"订单当前状态 [{$currentState}] 不允许执行 [{$action}] 操作",
['current_state' => $currentState]
);
}
}
全局捕获与响应处理
在框架层面(如 Laravel 的 Handler.php 或原生 PHP 的 set_exception_handler),需要根据接口类型做分流:
<?php
// 全局异常处理器核心逻辑 (Laravel Example)
public function render($request, \Throwable $e)
{
// 1. 业务异常 & 参数校验异常 -> 返回 200/422 状态码,但带业务错误码
if ($e instanceof \App\Exceptions\BusinessExceptionInterface) {
return response()->json([
'code' => $e->getErrorCode(),
'message' => $e->getErrorMessage(),
'data' => $e->getExtraData(),
'status' => 'error',
], 200); // HTTP状态码建议用200,避免前端网关误判
}
// 2. 参数校验异常 -> 返回 422
if ($e instanceof \Illuminate\Validation\ValidationException) {
return response()->json([
'code' => 422,
'message' => '请求参数不合法',
'errors' => $e->errors(),
], 422);
}
// 3. 系统异常 -> 返回 500 (隐藏细节)
if ($e instanceof \Throwable) {
\Log::error($e->getMessage(), ['trace' => $e->getTraceAsString()]);
return response()->json([
'code' => 500,
'message' => '服务器开小差了,请稍后重试',
], 500);
}
}
研发团队必须遵守的规范
没有规范,分类就形同虚设:
错误码分段制度
- 1xxx:用户输入/权限错误(1000~1999)
- 2xxx:订单/交易域(2000~2999)
- 3xxx:库存/商品域(3000~3999)
- 4xxx:支付/财务域(4000~4999)
- 5xxx:外部系统依赖错误(5000~5999)
禁止跨层捕获
- 避免在
service层用try...catch (\Exception $e)去吞掉一切,只捕获你知道如何恢复的异常。
日志分级
- BusinessException:记录
error_code和message,不记录堆栈(除非 Debug 模式)。 - SystemException:必须记录完整堆栈,并关联
request_id方便追踪。
异步队列不抛业务异常
- 在队列中抛出
BusinessException会导致死循环重试。队列中应该捕获业务异常并记录,然后丢弃该消息。
判定表
| 条件 | 处理方式 |
|---|---|
| 用户能理解原因? | 能 → 业务异常;不能 → 系统异常 |
| 用户能通过改输入解决? | 能 → 参数异常;不能 → 业务状态冲突 |
| 重试会成功吗? | 会 → 依赖服务异常(需重试);不会 → 业务逻辑异常(需人工干预) |
| 是用户重复操作吗? | 是 → 幂等异常(直接返回成功或提示) |
构建这样的分类体系后,前端拿到 code=2001 就知道是订单状态冲突,拿到 code=3001 就知道是库存不足,拿 code=500 就知道是后端不可用,混乱度将大幅降低。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播