Реализация мультиканальных продаж (Omnichannel) на сайте
Представьте: клиент заказывает стул на сайте, через час забирает его в магазине, но на витрине стул ещё висит — уже продан. Или промокод работает только на сайте, а в приложении нет. Это классические последствия разрозненных каналов. Мы интегрируем мультиканальные продажи так, что клиент видит единую картину: одинаковые цены на сайте и маркетплейсах, историю заказов в личном кабинете, возможность вернуть товар с Ozon через магазин. Технически — это связка API, message broker (RabbitMQ) и единого профиля в PostgreSQL.
Почему Omnichannel — это не просто несколько магазинов?
Многие думают: «Подключили API маркетплейсов — вот вам омниканальность». В реальности возникает хаос: на сайте товар есть, на Ozon закончился, клиент жалуется. Или скидка действует только в офлайн-магазине — пользователь раздражён. Корень проблемы — разрозненные источники данных: цены, остатки, заказы живут в разных системах без общего ключа.
Мы решаем это через Unified Commerce Core — микросервис, который держит инвентарь, клиентов и промо в одном месте. Все каналы обращаются к нему, а не друг к другу. Это устраняет N+1 запросов и конфликты. Для глубокого понимания — механизм мультиканальности в ритейле.
Как мы обеспечиваем единый профиль клиента?
Клиент может зарегистрироваться на сайте, купить через WB, а вернуть в магазине — и система должна понять, что это один человек. Без этого невозможна персонализация и история заказов.
Мы реализуем Customer Identity Resolution через цепочку: email → телефон → имя+адрес. Пример кода:
class CustomerIdentityResolver { public function resolve(array $customerData, string $source): Customer { $customer = null; if (!empty($customerData['email'])) { $customer = Customer::where('email', $customerData['email'])->first(); } if (!$customer && !empty($customerData['phone'])) { $normalized = $this->normalizePhone($customerData['phone']); $customer = Customer::where('phone_normalized', $normalized)->first(); } if (!$customer) { $customer = Customer::create([ 'name' => $customerData['name'], 'email' => $customerData['email'] ?? null, 'phone_normalized' => $normalized ?? null, 'source_first' => $source, ]); } $customer->channelIds()->updateOrCreate( ['channel' => $source], ['external_id' => $customerData['id'] ?? null] ); return $customer; } } Как мы решаем проблемы синхронизации?
Единая история заказов. Клиент в личном кабинете видит все заказы — с сайта, Ozon, WB, POS — отсортированные по дате. Технически это означает объединение заказов из разных источников в одну таблицу с привязкой к customer_id. Мы используем паттерн Repository для абстракции:
public function getOrderHistory(Customer $customer): Collection { return Order::where('customer_id', $customer->id) ->with(['items.product', 'source_details']) ->orderByDesc('created_at') ->get() ->map(fn($order) => [ 'id' => $order->id, 'source' => $order->source, 'source_label' => $this->sourceLabel($order->source), 'status' => $order->status, 'total' => $order->total, 'items' => $order->items->count(), 'date' => $order->created_at->format('d.m.Y'), ]); } Единое управление промо. Промокод, созданный для сайта, должен работать и на маркетплейсе, но только если канал разрешён.
class OmnichannelPromotion { public function apply(string $promoCode, Order $order): void { $promo = Promotion::where('code', $promoCode)->first(); if (!in_array($order->source, $promo->applicable_channels)) { throw new PromoNotApplicableException("Промокод недействителен для {$order->source}"); } $discount = $promo->calculateDiscount($order->total); $order->applyDiscount($discount, $promoCode); $promo->increment('used_count'); } } Как мы резервируем товары по каналам?
Без общей картины остатков вы либо теряете продажи (товар есть в системе, но не выложен на маркетплейс), либо получаете овербукинг (два канала продают одну единицу).
Реализуем это через InventoryManager с процентовкой по каналам:
class InventoryManager { private array $channelReservePercent = [ 'site' => 40, 'ozon' => 30, 'wb' => 20, 'buffer' => 10, ]; public function getChannelAllocation(int $productId): array { $total = WarehouseItem::where('product_id', $productId)->sum('quantity'); return array_map( fn($pct) => (int)floor($total * $pct / 100), $this->channelReservePercent ); } } Аналитика по каналам
SELECT source, COUNT(*) AS orders_count, SUM(total) AS revenue, AVG(total) AS aov, COUNT(DISTINCT customer_id) AS unique_customers FROM orders WHERE created_at > NOW() - INTERVAL '30 days' GROUP BY source ORDER BY revenue DESC; Сравнение подходов к интеграции
| Подход | Время внедрения | Сложность | Консистентность |
|---|---|---|---|
| ETL (периодическая загрузка) | 2-4 недели | Низкая | Низкая (задержки) |
| API Gateway с синхронными запросами | 4-8 недель | Средняя | Средняя (риск таймаутов) |
| Event-driven (CDC + очереди) | 6-12 недель | Высокая | Высокая (реальное время) |
Мы обычно выбираем третий вариант — он даёт наименьшие задержки и масштабируется. Event-driven архитектура в 2-3 раза быстрее ETL при синхронизации остатков. Подробнее о CDC — в документации Debezium.
Сравнение стратегий резервирования
| Стратегия | Риск овербукинга | Гибкость распределения | Сложность реализации |
|---|---|---|---|
| Фиксированный процент | Низкий | Низкая | Низкая |
| Динамический по продажам | Средний | Высокая | Средняя |
| Полный сток (пул) | Высокий | Средняя | Высокая |
Процесс работы
- Аналитика — изучаем текущую архитектуру, список каналов, объёмы данных, бизнес-правила.
- Проектирование — рисуем модель данных, API-спецификации (OpenAPI), схему обмена.
- Реализация — пишем Core, интеграционные адаптеры, тесты (unit + integration).
- Тестирование — сквозные сценарии: заказ на сайте → отмена в мобильном приложении → возврат на маркетплейсе.
- Деплой — roll-out по каналам, мониторинг через Grafana и алерты.
Мы гарантируем консистентность данных в реальном времени. Наши инженеры имеют 5+ лет опыта в построении распределённых систем. Для реального проекта — например, для сети магазинов мебели мы интегрировали 5 каналов, что позволило сократить количество возвратов на 25% за счёт точного отображения остатков.
Что входит в работу
- Разработка Unified Commerce Core (инвентарь, клиенты, заказы, промо).
- Интеграция с 3+ каналами (сайт, маркетплейсы, мобильное приложение).
- Документация API и схемы данных.
- Нагрузочное тестирование и оптимизация.
- Обучение команды заказчика.
Сроки
Базовая система на 3 канала — от 20 до 30 рабочих дней. Для 5+ каналов — до 50 дней. Стоимость рассчитывается индивидуально после аудита существующей инфраструктуры. Получите консультацию для оценки вашего проекта.
Наш опыт
Мы реализовали 15+ omnichannel-проектов для e-commerce. Среди клиентов — магазины с оборотом от 10 млн руб/мес. Инженеры имеют 5+ лет опыта в построении распределённых систем. Свяжитесь с нами — мы поможем определить оптимальную архитектуру для вашего бизнеса.







