Реализация мультиканальных продаж (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+ лет опыта в построении распределённых систем. Свяжитесь с нами — мы поможем определить оптимальную архитектуру для вашего бизнеса.







