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

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, 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

Розробка мультисклада для E-Commerce

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

Коли це стає необхідним

  • Магазин працює з кількома поставщиками, кожен зберігає свій товар
  • Є кілька регіональних складів, і доставка йде з найближчого до покупця
  • Частина товару — дропшиппінг, частина — власний склад
  • Офлайн-точки беруть участь у обробці онлайн-замовлень (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 тижня.