Разработка мультисклада для интернет-магазина

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Разработка мультисклада для интернет-магазина
Сложная
~1-2 недели
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Разработка мультисклада для интернет-магазина

Один склад — простая задача. Два и больше — принципиально другая архитектура. Мультисклад нужен, когда товар хранится на нескольких физических точках: центральный склад + региональные, собственное хранение + дропшиппинг-поставщики, магазин + офлайн-точки. Задача системы — не просто считать остатки по каждому складу, но и маршрутизировать заказы к оптимальному источнику отгрузки.

Когда это становится нужным

  • Магазин работает с несколькими поставщиками, каждый хранит свой товар
  • Есть несколько региональных складов, и доставка идёт с ближайшего к покупателю
  • Часть товара — дропшиппинг, часть — собственный склад
  • Офлайн-точки участвуют в обработке онлайн-заказов (BOPIS — Buy Online, Pick Up In Store)

Схема данных

Расширение односкладовой схемы: добавляется измерение warehouse_id:

CREATE TABLE warehouses (
    id          BIGSERIAL PRIMARY KEY,
    name        VARCHAR(255) NOT NULL,
    code        VARCHAR(50) NOT NULL UNIQUE,
    address     TEXT,
    is_active   BOOLEAN NOT NULL DEFAULT true,
    priority    INTEGER NOT NULL DEFAULT 0,  -- приоритет при маршрутизации
    type        VARCHAR(50) NOT NULL DEFAULT 'internal' -- internal | dropship | store
);

CREATE TABLE warehouse_stock (
    id           BIGSERIAL PRIMARY KEY,
    warehouse_id BIGINT NOT NULL REFERENCES warehouses(id),
    variant_id   BIGINT NOT NULL REFERENCES product_variants(id),
    stock_qty    INTEGER NOT NULL DEFAULT 0,
    reserved_qty INTEGER NOT NULL DEFAULT 0,
    UNIQUE (warehouse_id, variant_id),
    CHECK (stock_qty >= 0),
    CHECK (reserved_qty >= 0)
);

-- Агрегированный остаток по всем складам (materialized view или generated)
CREATE MATERIALIZED VIEW product_total_stock AS
SELECT
    variant_id,
    SUM(stock_qty)    AS total_stock,
    SUM(reserved_qty) AS total_reserved,
    SUM(stock_qty - reserved_qty) AS available_qty
FROM warehouse_stock
GROUP BY variant_id;

Materialized view обновляется после каждого изменения через триггер или REFRESH MATERIALIZED VIEW CONCURRENTLY по расписанию (каждую минуту).

Маршрутизация заказа к складу

Это ключевая бизнес-логика мультисклада. Алгоритм выбора склада:

class WarehouseRouter
{
    public function resolve(OrderItem $item, Address $destination): Warehouse
    {
        $candidates = WarehouseStock::query()
            ->where('variant_id', $item->variant_id)
            ->whereRaw('stock_qty - reserved_qty >= ?', [$item->qty])
            ->with('warehouse')
            ->get()
            ->filter(fn($ws) => $ws->warehouse->is_active)
            ->sortBy([
                fn($a, $b) => $this->byProximity($a->warehouse, $destination) <=> $this->byProximity($b->warehouse, $destination),
                fn($a, $b) => $a->warehouse->priority <=> $b->warehouse->priority,
            ]);

        return $candidates->first()?->warehouse
            ?? throw new NoWarehouseAvailableException($item->variant_id);
    }
}

Стратегии маршрутизации — конфигурируемые:

Стратегия Описание Применение
Proximity Ближайший к адресу доставки Региональные сети
Priority По приоритету склада Дропшиппинг как запасной
Cost Минимальная стоимость отгрузки Интеграция с транспортными API
Consolidation Минимум источников в заказе Снижение стоимости упаковки
FIFO по SKU Первый пришедший первым уходит Управление сроками годности

Для большинства проектов достаточно proximity + priority fallback.

Расщепление заказа по складам

Если товары из одного заказа есть на разных складах — заказ расщепляется на несколько отгрузок (shipments):

class OrderSplitter
{
    public function split(Order $order): Collection
    {
        $assignments = collect();

        foreach ($order->items as $item) {
            $warehouse = $this->router->resolve($item, $order->delivery_address);
            $assignments->push([
                'warehouse' => $warehouse,
                'item'      => $item,
            ]);
        }

        return $assignments
            ->groupBy(fn($a) => $a['warehouse']->id)
            ->map(fn($group) => new Shipment($group));
    }
}

Покупатель видит один заказ, но внутри — несколько отгрузок с разными трек-номерами. Статус заказа агрегируется из статусов отгрузок: «выполнен» только когда все shipments доставлены.

Синхронизация остатков с несколькими источниками

Каждый склад может иметь свой канал обновления остатков:

  • Внутренний склад: WMS через REST API или файловый обмен (XLSX, CSV)
  • Дропшиппер: их API с rate-limit, часто нестандартный формат
  • Офлайн-точка: кассовая система (1С:Розница, iiko, r_keeper)

Для каждого источника — отдельный адаптер:

interface WarehouseStockProvider
{
    public function fetchStock(Carbon $since): Collection; // варианты + количества
    public function confirmReservation(string $reservationId): bool;
    public function releaseReservation(string $reservationId): bool;
}

class MoyskladWarehouseProvider implements WarehouseStockProvider { ... }
class DropshipperApiProvider implements WarehouseStockProvider { ... }
class OneCWarehouseProvider implements WarehouseStockProvider { ... }

Синхронизация запускается независимо для каждого провайдера по своему расписанию:

* * * * * php artisan stock:sync --warehouse=central
*/5 * * * * php artisan stock:sync --warehouse=dropshipper-a
*/15 * * * * php artisan stock:sync --warehouse=store-minsk

Резервирование в мультисклад-контексте

При резервировании важно фиксировать конкретный склад, а не только вариант:

CREATE TABLE stock_reservations (
    id           BIGSERIAL PRIMARY KEY,
    warehouse_id BIGINT NOT NULL REFERENCES warehouses(id),
    variant_id   BIGINT NOT NULL REFERENCES product_variants(id),
    order_id     BIGINT NOT NULL REFERENCES orders(id),
    qty          INTEGER NOT NULL,
    status       VARCHAR(50) NOT NULL DEFAULT 'active',
    expires_at   TIMESTAMP NOT NULL,
    created_at   TIMESTAMP NOT NULL DEFAULT NOW()
);

Атомарная операция резервирования — тот же UPDATE ... WHERE available >= qty на warehouse_stock, плюс запись в stock_reservations.

Виджет выбора склада для самовывоза

Если включён BOPIS, покупатель выбирает точку самовывоза. Для этого нужна карта точек с реальными остатками:

GET /api/warehouses/availability?variant_ids[]=123&variant_ids[]=456

Ответ содержит список складов с available_qty по каждому товару. Фронтенд строит список точек и карту (Leaflet, Яндекс.Карты), фильтруя только те, где есть весь заказ.

Отчётность по складам

Аналитика, которая реально нужна операционному менеджеру:

  • Остатки по каждому складу в разрезе категорий
  • Оборачиваемость (sold_qty / avg_stock за период)
  • Перемещение между складами (transfer orders)
  • Прогноз дефицита по дням продаж

Базовый запрос оборачиваемости:

SELECT
    w.name AS warehouse,
    pv.sku,
    ws.stock_qty,
    COALESCE(sales.sold_30d, 0) AS sold_30d,
    CASE
        WHEN COALESCE(sales.sold_30d, 0) = 0 THEN NULL
        ELSE ROUND(ws.stock_qty / (sales.sold_30d / 30.0))
    END AS days_of_stock
FROM warehouse_stock ws
JOIN warehouses w ON ws.warehouse_id = w.id
JOIN product_variants pv ON ws.variant_id = pv.id
LEFT JOIN (
    SELECT variant_id, SUM(qty) AS sold_30d
    FROM order_items oi
    JOIN orders o ON oi.order_id = o.id
    WHERE o.completed_at >= NOW() - INTERVAL '30 days'
    GROUP BY variant_id
) sales ON sales.variant_id = pv.id;

Сроки реализации

  • Схема мультисклада + маршрутизация по приоритету: 5–7 дней
  • Расщепление заказов + управление отгрузками: 3–5 дней
  • Интеграция с одним внешним источником (1С, МойСклад): 3–5 дней
  • BOPIS с картой самовывоза: +3–4 дня
  • Аналитические отчёты по складам: +2–3 дня

Полноценная мультисклад-система для магазина с 2–5 точками хранения: 2–4 недели.