本文目录导读:

在 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 就知道是后端不可用,混乱度将大幅降低。