← Назад к вопросам

Каталог товаров интернет-магазина

2.8 Senior🔥 131 комментариев
#API и веб-протоколы#Архитектура и паттерны#Фреймворки

Условие

Спроектировать и реализовать JSON API для каталога товаров интернет-магазина.

Структура

  • Дерево категорий (максимальная вложенность - 3 уровня)
  • Товары с характеристиками
  • Корзина для авторизованных и неавторизованных пользователей
  • Заказы

API эндпоинты

  • GET /api/categories - дерево категорий
  • GET /api/products?category_id=X - товары категории
  • GET /api/products/{id} - товар с характеристиками
  • POST /api/cart/add - добавить в корзину
  • GET /api/cart - содержимое корзины
  • POST /api/orders - создать заказ

Требования

  • Авторизация через Laravel Sanctum
  • Пагинация
  • Фильтрация товаров

Технологии

Laravel 10+, MySQL, Laravel Sanctum

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение

1. Миграции

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

// Категории (иерархия)
return new class extends Migration {
    public function up(): void {
        Schema::create("categories", function (Blueprint $table) {
            $table->id();
            $table->string("name", 255);
            $table->string("slug", 255)->unique();
            $table->text("description")->nullable();
            $table->unsignedBigInteger("parent_id")->nullable();
            $table->integer("level")->default(1);
            $table->timestamps();
            $table->foreign("parent_id")->references("id")->on("categories")->nullOnDelete();
            $table->index("parent_id");
        });

        // Товары
        Schema::create("products", function (Blueprint $table) {
            $table->id();
            $table->foreignId("category_id")->constrained("categories");
            $table->string("name", 255);
            $table->string("slug", 255)->unique();
            $table->text("description")->nullable();
            $table->decimal("price", 10, 2);
            $table->integer("stock")->default(0);
            $table->boolean("is_active")->default(true);
            $table->timestamps();
            $table->index("category_id");
            $table->index("is_active");
        });

        // Характеристики товаров
        Schema::create("product_attributes", function (Blueprint $table) {
            $table->id();
            $table->foreignId("product_id")->constrained("products")->cascadeOnDelete();
            $table->string("name", 255);
            $table->string("value", 255);
            $table->timestamps();
            $table->index("product_id");
        });

        // Корзина
        Schema::create("carts", function (Blueprint $table) {
            $table->id();
            $table->foreignId("user_id")->nullable()->constrained("users")->cascadeOnDelete();
            $table->string("session_id", 255)->nullable();
            $table->timestamps();
            $table->index("user_id");
            $table->index("session_id");
        });

        // Товары в корзине
        Schema::create("cart_items", function (Blueprint $table) {
            $table->id();
            $table->foreignId("cart_id")->constrained("carts")->cascadeOnDelete();
            $table->foreignId("product_id")->constrained("products");
            $table->integer("quantity");
            $table->decimal("price", 10, 2);
            $table->timestamps();
            $table->unique(["cart_id", "product_id"]);
        });

        // Заказы
        Schema::create("orders", function (Blueprint $table) {
            $table->id();
            $table->foreignId("user_id")->constrained("users");
            $table->decimal("total", 10, 2);
            $table->enum("status", ["pending", "paid", "shipped", "delivered", "cancelled"])->default("pending");
            $table->string("customer_email", 255);
            $table->string("customer_phone", 20);
            $table->text("delivery_address");
            $table->timestamps();
            $table->index("user_id");
            $table->index("status");
        });

        // Товары в заказе
        Schema::create("order_items", function (Blueprint $table) {
            $table->id();
            $table->foreignId("order_id")->constrained("orders")->cascadeOnDelete();
            $table->foreignId("product_id")->constrained("products");
            $table->integer("quantity");
            $table->decimal("price", 10, 2);
            $table->timestamps();
        });
    }

    public function down(): void {
        Schema::dropIfExists("order_items");
        Schema::dropIfExists("orders");
        Schema::dropIfExists("cart_items");
        Schema::dropIfExists("carts");
        Schema::dropIfExists("product_attributes");
        Schema::dropIfExists("products");
        Schema::dropIfExists("categories");
    }
};

2. Модели

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Category extends Model {
    protected $fillable = ["name", "slug", "description", "parent_id", "level"];

    public function parent(): BelongsTo {
        return $this->belongsTo(Category::class, "parent_id");
    }

    public function children(): HasMany {
        return $this->hasMany(Category::class, "parent_id");
    }

    public function products(): HasMany {
        return $this->hasMany(Product::class);
    }

    public function allProducts(): HasMany {
        return $this->hasMany(Product::class);
    }
}

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Product extends Model {
    protected $fillable = ["category_id", "name", "slug", "description", "price", "stock", "is_active"];
    protected $casts = ["price" => "decimal:2", "is_active" => "boolean"];

    public function category(): BelongsTo {
        return $this->belongsTo(Category::class);
    }

    public function attributes(): HasMany {
        return $this->hasMany(ProductAttribute::class);
    }
}

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class ProductAttribute extends Model {
    protected $fillable = ["product_id", "name", "value"];

    public function product(): BelongsTo {
        return $this->belongsTo(Product::class);
    }
}

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Cart extends Model {
    protected $fillable = ["user_id", "session_id"];

    public function user(): BelongsTo {
        return $this->belongsTo(User::class);
    }

    public function items(): HasMany {
        return $this->hasMany(CartItem::class);
    }
}

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class CartItem extends Model {
    protected $fillable = ["cart_id", "product_id", "quantity", "price"];

    public function cart(): BelongsTo {
        return $this->belongsTo(Cart::class);
    }

    public function product(): BelongsTo {
        return $this->belongsTo(Product::class);
    }
}

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Order extends Model {
    protected $fillable = ["user_id", "total", "status", "customer_email", "customer_phone", "delivery_address"];
    protected $casts = ["total" => "decimal:2"];

    public function user(): BelongsTo {
        return $this->belongsTo(User::class);
    }

    public function items(): HasMany {
        return $this->hasMany(OrderItem::class);
    }
}

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class OrderItem extends Model {
    protected $fillable = ["order_id", "product_id", "quantity", "price"];

    public function order(): BelongsTo {
        return $this->belongsTo(Order::class);
    }

    public function product(): BelongsTo {
        return $this->belongsTo(Product::class);
    }
}

3. Service для корзины

<?php

namespace App\Services;

use App\Models\Cart;
use App\Models\Product;
use Auth;
use Illuminate\Validation\ValidationException;
use Session;

class CartService {
    public function getCart(): Cart {
        if (Auth::check()) {
            return Cart::firstOrCreate(["user_id" => Auth::id()]);
        } else {
            $sessionId = Session::getId();
            return Cart::firstOrCreate(["session_id" => $sessionId]);
        }
    }

    public function addToCart(int $productId, int $quantity): void {
        $product = Product::findOrFail($productId);
        
        if ($product->stock < $quantity) {
            throw ValidationException::withMessages([
                "quantity" => "Недостаточно товара на складе"
            ]);
        }

        $cart = $this->getCart();
        $cartItem = $cart->items()->where("product_id", $productId)->first();

        if ($cartItem) {
            $cartItem->quantity += $quantity;
            $cartItem->save();
        } else {
            $cart->items()->create([
                "product_id" => $productId,
                "quantity" => $quantity,
                "price" => $product->price,
            ]);
        }
    }

    public function removeFromCart(int $cartItemId): void {
        $cart = $this->getCart();
        $cart->items()->where("id", $cartItemId)->delete();
    }

    public function clearCart(): void {
        $this->getCart()->items()->delete();
    }

    public function getCartTotal(): float {
        return $this->getCart()->items()->sum("price" * "quantity");
    }
}

4. API контроллеры

<?php

namespace App\Http\Controllers\Api;

use App\Models\Category;
use Illuminate\Http\JsonResponse;

class CategoryController extends Controller {
    public function index(): JsonResponse {
        $categories = Category::where("level", 1)
            ->with("children.children")
            ->get();
        return response()->json($categories);
    }
}

<?php

namespace App\Http\Controllers\Api;

use App\Models\Product;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class ProductController extends Controller {
    public function index(Request $request): JsonResponse {
        $query = Product::where("is_active", true);

        if ($request->has("category_id")) {
            $query->where("category_id", $request->get("category_id"));
        }

        if ($request->has("search")) {
            $query->where("name", "like", "%" . $request->get("search") . "%");
        }

        $products = $query->paginate(20);
        return response()->json($products);
    }

    public function show(Product $product): JsonResponse {
        $product->load("attributes");
        return response()->json($product);
    }
}

<?php

namespace App\Http\Controllers\Api;

use App\Services\CartService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class CartController extends Controller {
    protected $cartService;

    public function __construct(CartService $cartService) {
        $this->cartService = $cartService;
    }

    public function show(): JsonResponse {
        $cart = $this->cartService->getCart();
        return response()->json([
            "items" => $cart->items()->with("product")->get(),
            "total" => $this->cartService->getCartTotal(),
        ]);
    }

    public function add(Request $request): JsonResponse {
        $validated = $request->validate([
            "product_id" => "required|exists:products,id",
            "quantity" => "required|integer|min:1",
        ]);

        $this->cartService->addToCart($validated["product_id"], $validated["quantity"]);
        return response()->json(["message" => "Item added to cart"], 201);
    }

    public function remove(Request $request): JsonResponse {
        $validated = $request->validate(["item_id" => "required|exists:cart_items,id"]);
        $this->cartService->removeFromCart($validated["item_id"]);
        return response()->json(["message" => "Item removed"]);
    }
}

<?php

namespace App\Http\Controllers\Api;

use App\Models\Order;
use App\Services\CartService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;

class OrderController extends Controller {
    protected $cartService;

    public function __construct(CartService $cartService) {
        $this->cartService = $cartService;
    }

    public function store(Request $request): JsonResponse {
        $validated = $request->validate([
            "customer_email" => "required|email",
            "customer_phone" => "required|string",
            "delivery_address" => "required|string",
        ]);

        $cart = $this->cartService->getCart();
        
        if ($cart->items()->count() === 0) {
            return response()->json(["message" => "Cart is empty"], 400);
        }

        return DB::transaction(function () use ($validated, $cart) {
            $total = $this->cartService->getCartTotal();

            $order = Order::create([
                "user_id" => Auth::id(),
                "total" => $total,
                "customer_email" => $validated["customer_email"],
                "customer_phone" => $validated["customer_phone"],
                "delivery_address" => $validated["delivery_address"],
            ]);

            foreach ($cart->items as $item) {
                $order->items()->create([
                    "product_id" => $item->product_id,
                    "quantity" => $item->quantity,
                    "price" => $item->price,
                ]);
            }

            $this->cartService->clearCart();
            return response()->json($order, 201);
        });
    }
}

5. Маршруты API

<?php

use App\Http\Controllers\Api\CategoryController;
use App\Http\Controllers\Api\ProductController;
use App\Http\Controllers\Api\CartController;
use App\Http\Controllers\Api\OrderController;
use Illuminate\Support\Facades\Route;

Route::get("/categories", [CategoryController::class, "index"]);
Route::get("/products", [ProductController::class, "index"]);
Route::get("/products/{product}", [ProductController::class, "show"]);

Route::middleware("auth:sanctum")->group(function () {
    Route::get("/cart", [CartController::class, "show"]);
    Route::post("/cart/add", [CartController::class, "add"]);
    Route::post("/cart/remove", [CartController::class, "remove"]);
    Route::post("/orders", [OrderController::class, "store"]);
});

Это полное решение интернет-магазина с иерархией категорий, товарами, корзиной и заказами.

Каталог товаров интернет-магазина | PrepBro