Laravel: Phase 3综合练习—ShopMetrics API与实时通知

最后更新:2026-08-26

Phase 3 综合练习是"交付高级功能"——把认证、API、中间件、事件、存储全部串起来,构建生产级 API。

1. 你将学到


2. Alice 的 Phase 3 验收故事

(1) 痛点:6 课知识零散,无法组装成完整 API

Alice 学完 Phase 3 后——Sanctum 在课 15、Resources 在课 16、中间件在课 17——但不知道怎么把它们组装成一套完整 API。Bob 让她写个 RESTful API,她发现认证、Resource 转换、中间件、事件广播、文件上传这些功能需要同时协作,单独看每个都会,组合起来就乱了。

(2) 综合练习的解法

本课从零搭建一套完整的 RESTful API——每个端点都经过认证、租户隔离、限流、Resource 转换、事件广播,最终交付一个生产级 API 系统。

BASH
# Phase 3 deliverable: Complete RESTful API with real-time features
php artisan migrate:fresh --seed
php artisan queue:work &
php artisan storage:link
# → 10+ endpoints, auth, rate limiting, broadcasting all working

(3) 收益

Alice 完成后,拥有了一套完整的 ShopMetrics API——从认证到广播端到端打通,可以直接交付给前端团队。


3. API 认证层

(1) Sanctum Token 认证流程

100%
flowchart TD
    A[Client] --> B["POST /api/auth/login<br/>(email+password)"]
    B --> C["Sanctum creates Token"]
    C --> D["Return plainTextToken"]
    D --> E["Client stores Token"]
    E --> F["API requests with<br/>Authorization: Bearer {token}"]
    F --> G["Sanctum middleware<br/>resolves User"]
    G --> H[TenantResolve middleware]
    H --> I[Controller]

(2) 认证端点

PHP
// routes/api.php
Route::prefix('auth')->group(function () {
    Route::post('/register', [Auth\RegisterController::class, 'register']);
    Route::post('/login', [Auth\ApiTokenController::class, 'login']);
    Route::post('/forgot-password', [Auth\PasswordResetController::class, 'sendResetLink']);
    Route::post('/reset-password', [Auth\PasswordResetController::class, 'reset']);

    Route::middleware('auth:sanctum')->group(function () {
        Route::get('/user', fn (Request $r) => new UserResource($r->user()));
        Route::post('/logout', [Auth\ApiTokenController::class, 'logout']);
        Route::apiResource('tokens', Auth\TokenController::class)->only(['store', 'index', 'destroy']);
    });
});
端点 方法 认证 说明
/auth/register POST 注册
/auth/login POST 获取 Token
/auth/user GET 当前用户
/auth/logout POST 撤销 Token
/auth/tokens POST 创建新 Token
/auth/tokens GET 列出 Tokens
/auth/tokens/{id} DELETE 删除 Token

▶ 示例:ShopMetrics Token 认证完整实现

PHP
// app/Http/Controllers/Auth/ApiTokenController.php
class ApiTokenController extends Controller
{
    public function login(Request $request): JsonResponse
    {
        $request->validate([
            'email' => 'required|email',
            'password' => 'required|string',
            'device_name' => 'sometimes|string|max:255',
        ]);

        $user = User::where('email', $request->email)->first();

        if (!$user || !Hash::check($request->password, $user->password)) {
            throw ValidationException::withMessages([
                'email' => ['Invalid credentials.'],
            ]);
        }

        if (!$user->tenant_id || $user->tenant->status !== 'active') {
            throw ValidationException::withMessages([
                'email' => ['Account is not active.'],
            ]);
        }

        $abilities = match ($user->role) {
            'super_admin' => ['*'],
            'tenant_owner' => ['read', 'write', 'manage-users'],
            'analyst' => ['read'],
            default => [],
        };

        $token = $user->createToken(
            $request->device_name ?? 'api-token',
            $abilities,
        );

        return response()->json([
            'user' => new UserResource($user),
            'token' => $token->plainTextToken,
            'abilities' => $abilities,
        ]);
    }

    public function logout(Request $request): JsonResponse
    {
        $request->user()->currentAccessToken()->delete();
        return response()->json(['message' => 'Token revoked.']);
    }
}

输出:

TEXT 📖 仅展示
// 执行成功

4. API Resource 端点

(1) 完整端点清单

端点 方法 Resource 中间件
/v1/shops GET ShopResource::collection auth,tenant,throttle
/v1/shops POST ShopResource auth,tenant,throttle,role:owner
/v1/shops/{id} GET ShopResource auth,tenant
/v1/shops/{id} PUT ShopResource auth,tenant,role:owner
/v1/shops/{id} DELETE - auth,tenant,role:owner
/v1/shops/{id}/products GET ProductResource::collection auth,tenant
/v1/products POST ProductResource auth,tenant,role:owner
/v1/products/{id} GET ProductResource auth,tenant
/v1/products/{id} PUT ProductResource auth,tenant,role:owner
/v1/orders GET OrderResource::collection auth,tenant
/v1/orders/{id} GET OrderResource auth,tenant
/v1/orders/{id}/status PATCH OrderResource auth,tenant,role:owner
/v1/analytics/overview GET AnalyticsResource auth,tenant,ability:read
/v1/reports/generate POST ReportResource auth,tenant,ability:write

(2) 路由定义

PHP
// routes/api.php
Route::middleware(['auth:sanctum', 'tenant.resolve', 'throttle:tenant-api'])
    ->prefix('v1')->name('api.v1.')->group(function () {
        // Shops
        Route::apiResource('shops', Api\V1\ShopController::class);
        Route::post('shops/{shop}/logo', Api\V1\ShopLogoController::class)->name('shops.logo');

        // Products
        Route::apiResource('products', Api\V1\ProductController::class);

        // Orders
        Route::apiResource('orders', Api\V1\OrderController::class)->only(['index', 'show']);
        Route::patch('orders/{order}/status', [Api\V1\OrderController::class, 'updateStatus']);

        // Analytics & Reports
        Route::get('analytics/overview', [Api\V1\AnalyticsController::class, 'overview']);
        Route::post('reports/generate', [Api\V1\ReportController::class, 'generate']);

        // Media
        Route::post('media/upload', [Api\V1\MediaController::class, 'upload']);
        Route::post('media/presign', [Api\V1\MediaController::class, 'presign']);
        Route::get('media/{media}/download', [Api\V1\MediaController::class, 'download']);
    });

▶ 示例:ShopMetrics Order API 端点

PHP
// app/Http/Controllers/Api/V1/OrderController.php
class OrderController extends Controller
{
    public function index(Request $request): JsonResponse
    {
        $query = Order::where('tenant_id', tenant()->id)
            ->with(['shop', 'user'])
            ->withCount('items');

        if ($request->filled('status')) {
            $query->where('status', $request->status);
        }
        if ($request->filled('shop_id')) {
            $query->where('shop_id', $request->shop_id);
        }
        if ($request->filled('date_from')) {
            $query->where('created_at', '>=', $request->date('date_from'));
        }

        $orders = $query->latest()->paginate($request->integer('per_page', 15));

        return OrderResource::collection($orders);
    }

    public function show(Order $order): JsonResponse
    {
        $this->authorize('view', $order);
        $order->load(['items.product', 'shop', 'user']);
        return new OrderResource($order);
    }

    public function updateStatus(Request $request, Order $order): JsonResponse
    {
        $this->authorize('update', $order);

        $validated = $request->validate([
            'status' => 'required|in:processing,completed,cancelled,refunded',
        ]);

        $oldStatus = $order->status;
        $order->update(['status' => $validated['status']]);

        event(new OrderStatusChanged($order, $oldStatus, $validated['status']));

        return new OrderResource($order->fresh());
    }
}

输出:

TEXT 📖 仅展示
// 执行成功

5. 自定义中间件层

▶ 示例:ShopMetrics 中间件体系

PHP
// app/Http/Middleware/TenantResolve.php
class TenantResolve
{
    public function handle(Request $request, Closure $next): Response
    {
        $user = $request->user();
        if (!$user?->tenant_id) abort(403, 'No tenant.');
        $tenant = $user->tenant;
        if ($tenant->status !== 'active') abort(403, 'Tenant inactive.');
        app()->instance(Tenant::class, $tenant);
        return $next($request);
    }
}

// app/Http/Middleware/CheckAbility.php
class CheckAbility
{
    public function handle(Request $request, Closure $next, string $ability): Response
    {
        if ($request->user()->tokenCan('*') || $request->user()->tokenCan($ability)) {
            return $next($request);
        }
        abort(403, "Missing ability: {$ability}");
    }
}

// app/Http/Middleware/EnsureSubscriptionActive.php
class EnsureSubscriptionActive
{
    public function handle(Request $request, Closure $next): Response
    {
        $tenant = app()->make(Tenant::class);
        if (!$tenant->subscription?->isActive()) {
            abort(402, 'Active subscription required.');
        }
        return $next($request);
    }
}

输出:

TEXT 📖 仅展示
// 执行成功

▶ 示例:ShopMetrics 限流配置

PHP
// app/Providers/AppServiceProvider.php
public function boot(): void
{
    RateLimiter::for('tenant-api', function (Request $request) {
        $tenant = app()->make(Tenant::class);
        $plan = $tenant->plan ?? null;

        return Limit::perMinute(match ($plan->slug ?? 'starter') {
            'enterprise' => 600,
            'pro' => 120,
            default => 60,
        })->by($tenant->id);
    });
}

输出:

TEXT 📖 仅展示
// 执行成功

6. 事件广播集成

(1) 广播事件

PHP
// app/Events/OrderPlaced.php
class OrderPlaced implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct(public Order $order) {}

    public function broadcastOn(): array
    {
        return [new PrivateChannel('tenant.' . $this->order->tenant_id)];
    }

    public function broadcastWith(): array
    {
        return [
            'order_id' => $this->order->id,
            'order_number' => $this->order->order_number,
            'total' => (float) $this->order->total,
            'shop_name' => $this->order->shop->name,
        ];
    }

    public function broadcastAs(): string { return 'order.placed'; }
}

(2) 前端 Echo 监听

JAVASCRIPT
// resources/js/app.js
window.Echo.private(`tenant.${tenantId}`)
    .listen('.order.placed', (e) => {
        showNotification(`New order: ${e.order_number} ($${e.total})`);
    })
    .listen('.order.status_changed', (e) => {
        updateOrderRow(e.order_id, e.new_status);
    });

7. S3 文件上传集成

▶ 示例:ShopMetrics 图片上传端点

PHP
// app/Http/Controllers/Api/V1/ShopLogoController.php
class ShopLogoController extends Controller
{
    public function __invoke(Request $request, Shop $shop): JsonResponse
    {
        $this->authorize('update', $shop);

        $validated = $request->validate([
            'logo' => 'required|image|mimes:jpeg,png,webp|max:2048',
        ]);

        if ($shop->logo_path) {
            Storage::disk('s3')->delete($shop->logo_path);
        }

        $path = $request->file('logo')->store(
            "shops/{$shop->id}/logos",
            's3',
        );

        $shop->update(['logo_path' => $path]);

        return response()->json([
            'message' => 'Logo uploaded.',
            'logo_url' => Storage::disk('s3')->url($path),
        ]);
    }
}

// app/Http/Controllers/Api/V1/MediaController.php
class MediaController extends Controller
{
    public function presign(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'filename' => 'required|string',
            'mime_type' => 'required|in:image/jpeg,image/png,image/webp',
        ]);

        $path = 'uploads/' . tenant()->id . '/' . Str::uuid() . '/' . $validated['filename'];
        $url = Storage::disk('s3')->temporaryUploadUrl($path, now()->addMinutes(30));

        return response()->json(['upload_url' => $url, 'path' => $path]);
    }
}

输出:

TEXT 📖 仅展示
// 执行成功

8. 综合示例:ShopMetrics Phase 3 完整 API

PHP
// ============================================
// Comprehensive: ShopMetrics Phase 3 Complete API
// Covers: auth, resources, middleware, events, storage
// ============================================

// routes/api.php — Complete API routes
Route::prefix('auth')->group(function () {
    Route::post('/register', [Auth\RegisterController::class, 'register']);
    Route::post('/login', [Auth\ApiTokenController::class, 'login']);
    Route::middleware('auth:sanctum')->group(function () {
        Route::get('/user', fn (Request $r) => new UserResource($r->user()));
        Route::post('/logout', [Auth\ApiTokenController::class, 'logout']);
    });
});

Route::middleware(['auth:sanctum', 'tenant.resolve', 'throttle:tenant-api', 'subscription.active'])
    ->prefix('v1')->group(function () {
        Route::apiResource('shops', Api\V1\ShopController::class);
        Route::post('shops/{shop}/logo', Api\V1\ShopLogoController::class);

        Route::apiResource('products', Api\V1\ProductController::class);
        Route::post('products/{product}/images', Api\V1\ProductImageController::class);

        Route::apiResource('orders', Api\V1\OrderController::class)->only(['index', 'show']);
        Route::patch('orders/{order}/status', [Api\V1\OrderController::class, 'updateStatus']);

        Route::get('analytics/overview', [Api\V1\AnalyticsController::class, 'overview'])
            ->middleware('ability:read');
        Route::post('reports/generate', [Api\V1\ReportController::class, 'generate'])
            ->middleware('ability:write');

        Route::post('media/upload', [Api\V1\MediaController::class, 'upload']);
        Route::post('media/presign', [Api\V1\MediaController::class, 'presign']);
        Route::get('media/{media}/download', [Api\V1\MediaController::class, 'download']);
    });

// routes/channels.php
Broadcast::channel('tenant.{tenantId}', fn ($user, $tenantId) => $user->tenant_id === (int) $tenantId);

// Broadcasting configuration
// BROADCAST_CONNECTION=redis
// QUEUE_CONNECTION=redis
// Run: php artisan queue:work
// Run: php artisan reverb:start (Laravel 11 built-in WebSocket server)

❓ 常见问题

Q Phase 3 练习需要多少时间?
A 大约 6-8 小时。认证 1.5h,API Resources + 端点 2h,中间件 1h,事件广播 1.5h,文件上传 1h,联调 1h。建议每完成一个模块就用 Postman 测试。
Q 如何联调前后端 API?
A 用 Postman/Newman 先测试所有端点,记录 Collection。前端开发时用 Postman 的 Mock Server 或直接连后端。确保 CORS 配置正确。
Q WebSocket 服务用 Reverb 还是 Pusher?
A Laravel 11 内置 Reverb(自托管 WebSocket 服务器),开发用 Reverb 免费快速;生产环境根据规模选 Reverb(自托管)或 Pusher(托管)。
Q API 文档怎么生成?
A 推荐用 Scribe(knuckleswtf/scribe)自动从代码生成 OpenAPI 文档,比手写 Postman 更不容易过时。每个控制器方法加 PHPDoc 注释即可。
Q 限流如何测试?
A 用 Postman Runner 或 ab(Apache Bench)发送超过限额的请求,验证返回 429 状态码和 Retry-After 头。也可以写 PHPUnit 测试模拟限流。
Q S3 上传在本地开发怎么测试?
A 用 MinIO(S3 兼容的本地服务)替代真实 S3:docker run -p 9000:9000 minio/minio server /data。修改 AWS_URL 指向 localhost:9000 即可。

📖 小节


📝 作业

  1. 基础题(⭐):搭建 ShopMetrics API 认证层,实现 register/login/logout 三个端点,用 Postman 测试获取 Token 并访问受保护端点。

  2. 进阶题(⭐⭐):实现完整的 Shop + Product CRUD API,包含 Resource 转换、TenantResolve 隔离、基于订阅的限流,使用 Postman Collection 测试所有端点。

  3. 挑战题(⭐⭐⭐):实现订单状态变更的实时广播——后端 PATCH /orders/{id}/status 触发 OrderStatusChanged 事件,前端 Echo 监听更新 UI,同时记录 API 日志到数据库(使用 terminate 中间件)。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏