Реализация мультиканальных продаж (Omnichannel) на сайте
Omnichannel — это не просто продажи на нескольких площадках. Это единый клиентский опыт: покупатель видит одинаковые цены, историю заказов доступна в любом канале, возврат сделанного на маркетплейсе возможен через сайт. Технически — это сложная интеграция нескольких систем с единым источником правды.
Компоненты Omnichannel-системы
┌─────────────────────────────┐
│ Unified Commerce Core │
│ (инвентарь, клиенты, заказы)│
└──────────────┬──────────────┘
│
┌──────────────────────┼──────────────────────┐
│ │ │
┌──────▼──────┐ ┌────────▼───────┐ ┌────────▼────────┐
│ Сайт/App │ │ Маркетплейсы │ │ Офлайн / POS │
│ │ │ Ozon WB YM │ │ (если есть) │
└─────────────┘ └────────────────┘ └─────────────────┘
Единый профиль клиента
class CustomerIdentityResolver
{
// Объединяем клиентов из разных каналов
public function resolve(array $customerData, string $source): Customer
{
// Ищем по email → телефону → имени + адресу
$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;
}
}
Единая история заказов
// Клиент видит все свои заказы — с сайта и маркетплейсов — в личном кабинете
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 InventoryManager
{
// Резервы по каналам позволяют управлять распределением запасов
private array $channelReservePercent = [
'site' => 40, // 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
);
}
}
Единое управление промо
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');
}
}
Аналитика по каналам
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;
Сроки
Базовая Omnichannel-система (3 канала, единый инвентарь, единая история заказов): 20–30 рабочих дней.







