study-api-v2/docs/异常处理使用指南.md

11 KiB
Raw Permalink Blame History

异常处理使用指南

概述

本项目采用统一的异常处理机制所有API接口的错误都将返回统一的JSON格式。开发者只需要使用简单的方法就能抛出业务异常。

快速使用

1. 引入异常处理器

use App\Exceptions\Handler;

2. 基本用法

// 最基本的用法 - 抛出默认错误
Handler::throw('用户不存在');

// 指定错误码
Handler::throw('用户不存在', 404);

// 使用别名方法
Handler::error('参数错误', 400);

// 操作失败
Handler::fail('删除失败');

可用方法

Handler::throw($message, $code = 400)

功能: 抛出业务异常(主要方法)

Handler::throw('数据不存在', 404);
Handler::throw('权限不足', 403);
Handler::throw('操作失败');  // 默认错误码400

Handler::error($message, $code = 400)

功能: throw方法的别名使用习惯更友好

Handler::error('用户名已存在', 409);
Handler::error('参数错误');

Handler::fail($message = '操作失败')

功能: 快速抛出操作失败异常

Handler::fail();                    // 默认消息:操作失败
Handler::fail('用户创建失败');
Handler::fail('文件上传失败');

使用场景

1. 控制器中使用

<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\BaseController;
use App\Exceptions\Handler;
use App\Models\User;
use Illuminate\Http\Request;

class UserController extends BaseController
{
    public function show($id)
    {
        // 参数验证
        if (empty($id)) {
            Handler::error('用户ID不能为空');
        }
        
        // 查找用户
        $user = User::find($id);
        if (!$user) {
            Handler::throw('用户不存在', 404);
        }
        
        // 权限检查
        if ($user->status === 0) {
            Handler::throw('用户已被禁用', 403);
        }
        
        return $this->SuccessObject($user);
    }
    
    public function store(Request $request)
    {
        $username = $request->input('username');
        
        // 检查用户名是否存在
        if (User::where('username', $username)->exists()) {
            Handler::error('用户名已存在', 409);
        }
        
        try {
            $user = User::create($request->all());
            return $this->SuccessObject($user);
        } catch (\Exception $e) {
            Handler::fail('用户创建失败');
        }
    }
    
    public function destroy($id)
    {
        $user = User::find($id);
        if (!$user) {
            Handler::throw('用户不存在', 404);
        }
        
        // 不能删除管理员
        if ($user->is_admin) {
            Handler::error('不能删除管理员账户', 403);
        }
        
        if (!$user->delete()) {
            Handler::fail('用户删除失败');
        }
        
        return $this->Success(['message' => '删除成功']);
    }
}

2. 服务类中使用

<?php

namespace App\Services;

use App\Exceptions\Handler;
use App\Models\User;

class UserService
{
    public function createUser(array $data)
    {
        // 验证用户名
        if (empty($data['username'])) {
            Handler::error('用户名不能为空');
        }
        
        // 检查重复
        if ($this->usernameExists($data['username'])) {
            Handler::error('用户名已存在', 409);
        }
        
        // 创建用户
        try {
            return User::create($data);
        } catch (\Exception $e) {
            Handler::fail('用户创建失败');
        }
    }
    
    public function updateUserStatus($userId, $status)
    {
        $user = User::find($userId);
        if (!$user) {
            Handler::throw('用户不存在', 404);
        }
        
        if ($user->is_admin && $status === 0) {
            Handler::error('不能禁用管理员账户', 403);
        }
        
        $user->status = $status;
        if (!$user->save()) {
            Handler::fail('状态更新失败');
        }
        
        return $user;
    }
    
    private function usernameExists($username)
    {
        return User::where('username', $username)->exists();
    }
}

3. 中间件中使用

<?php

namespace App\Http\Middleware;

use App\Exceptions\Handler;
use Closure;
use Illuminate\Http\Request;

class CheckUserStatus
{
    public function handle(Request $request, Closure $next)
    {
        $user = $request->user();
        
        if (!$user) {
            Handler::error('用户未登录', 401);
        }
        
        if ($user->status === 0) {
            Handler::throw('账户已被禁用', 403);
        }
        
        if ($user->deleted) {
            Handler::throw('账户不存在', 404);
        }
        
        return $next($request);
    }
}

4. 模型中使用

<?php

namespace App\Models;

use App\Exceptions\Handler;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    public function changePassword($newPassword)
    {
        if (strlen($newPassword) < 6) {
            Handler::error('密码长度不能少于6位');
        }
        
        $this->password = bcrypt($newPassword);
        
        if (!$this->save()) {
            Handler::fail('密码修改失败');
        }
    }
    
    public function assignRole($roleId)
    {
        if ($this->is_admin) {
            Handler::error('管理员不能修改角色', 403);
        }
        
        $role = Role::find($roleId);
        if (!$role) {
            Handler::throw('角色不存在', 404);
        }
        
        $this->role_id = $roleId;
        if (!$this->save()) {
            Handler::fail('角色分配失败');
        }
    }
}

响应格式

所有异常都会返回统一的JSON格式

成功响应

{
    "success": true,
    "message": "success",
    "code": 200,
    "data": {
        // 具体数据
    }
}

异常响应

{
    "success": false,
    "message": "具体错误信息",
    "code": 400,
    "data": null
}

参数验证错误

{
    "success": false,
    "message": "参数错误",
    "code": 422,
    "data": null,
    "errors": {
        "username": ["用户名不能为空"],
        "email": ["邮箱格式不正确"]
    }
}

常用错误码

错误码 说明 使用场景
400 通用错误 默认业务错误
401 未授权 用户未登录
403 权限不足 没有操作权限
404 资源不存在 数据不存在
409 冲突 数据已存在
422 参数错误 验证失败
500 服务器错误 系统异常

测试异常处理

项目提供了测试接口来验证异常处理(仅开发环境可用):

# 测试参数验证异常
curl -X POST http://localhost:8000/admin/test/validation \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'

# 测试业务异常
curl -X GET http://localhost:8000/admin/test/business-exception \
  -H "Authorization: Bearer YOUR_TOKEN"

# 测试参数错误
curl -X GET http://localhost:8000/admin/test/param-error \
  -H "Authorization: Bearer YOUR_TOKEN"

# 测试操作失败
curl -X GET http://localhost:8000/admin/test/fail \
  -H "Authorization: Bearer YOUR_TOKEN"

# 测试系统异常
curl -X GET http://localhost:8000/admin/test/system-exception \
  -H "Authorization: Bearer YOUR_TOKEN"

最佳实践

1. 异常使用原则

  • 业务逻辑错误: 使用 Handler::throw()Handler::error()
  • 操作失败: 使用 Handler::fail()
  • 参数验证: 使用Laravel的Validation会自动处理
  • 系统错误: 直接throw Exception会自动处理

2. 错误信息编写

  • 错误信息要简洁明确
  • 面向用户,避免技术术语
  • 提供解决建议(如果可能)
// ✅ 好的错误信息
Handler::error('用户名已存在,请尝试其他用户名');
Handler::throw('文件大小不能超过2MB', 413);

// ❌ 不好的错误信息
Handler::error('数据库约束冲突');
Handler::throw('系统错误');

3. 错误码规范

// 常用错误码
Handler::throw('数据不存在', 404);
Handler::throw('权限不足', 403);
Handler::throw('数据已存在', 409);
Handler::error('参数错误', 400);

4. 性能考虑

  • 不要在循环中频繁抛出异常
  • 异常只用于错误处理,不要用于控制程序流程
  • 在抛出异常前先做基本检查
// ✅ 好的做法
if (empty($users)) {
    return $this->Success([]);  // 返回空数据
}

foreach ($users as $user) {
    // 正常处理
}

// ❌ 不好的做法
foreach ($users as $user) {
    if ($user->invalid) {
        Handler::error('用户无效');  // 在循环中抛异常
    }
}

注意事项

  1. 生产环境: 系统异常会隐藏详细信息,只返回"服务器错误"
  2. 开发环境: 系统异常会显示详细的调试信息
  3. 日志记录: 所有异常都会自动记录到日志文件
  4. 测试路由: 仅在开发环境可用,生产环境会自动禁用

迁移指南

如果您之前使用的是复杂的异常处理方式,可以按以下方式迁移:

// 之前的方式
throw new BusinessException(ResponseEnum::DATA_NOT_FOUND_ERROR, '用户不存在');

// 现在的方式
Handler::throw('用户不存在', 404);

// 之前的方式
$this->throwBusinessException(ResponseEnum::CLIENT_PARAMETER_ERROR, '参数错误');

// 现在的方式
Handler::error('参数错误');

这样大大简化了异常处理的使用,让开发更加便捷!

API认证中间件

为了解决API项目中认证失败时返回重定向错误的问题我们创建了专用的API认证中间件

中间件文件

  • app/Http/Middleware/AdminApiAuthenticate.php - 专用API认证中间件

功能特点

  1. 统一的401错误响应认证失败时返回JSON格式的401错误而不是重定向
  2. 支持Sanctum认证:默认使用sanctum守护器进行认证
  3. 扩展性强:可轻松扩展支持其他认证方式

响应格式

{
    "success": false,
    "message": "未授权访问,请先登录",
    "code": 401,
    "data": null
}

使用方式

在路由中使用admin.auth中间件:

Route::middleware('admin.auth')->group(function () {
    // 需要认证的路由
});

扩展示例

如果需要支持更多认证方式,可以修改中间件:

public function handle(Request $request, Closure $next, ...$guards)
{
    $guards = empty($guards) ? ['sanctum'] : $guards;
    
    // 可以在这里添加其他认证逻辑
    // 例如JWT、API Key等
    
    foreach ($guards as $guard) {
        if (auth()->guard($guard)->check()) {
            auth()->shouldUse($guard);
            return $next($request);
        }
    }
    
    // 自定义认证失败响应
    return response()->json([
        'success' => false,
        'message' => '未授权访问,请先登录',
        'code' => 401,
        'data' => null
    ], 401);
}