Розробка блоків Cross-Sell та Up-Sell для E-Commerce
Cross-sell та up-sell — два окремі механізми збільшення середньої вартості замовлення, які часто плутають. Cross-sell — це пропозиція супутніх товарів ("до цього ноутбука підходить це миша"). Up-sell — це пропозиція дорожчої версії того самого товару ("за 2000 ₽ більше — версія Pro"). Розробка обох блоків займає 4–6 робочих днів.
Розрізнення Cross-Sell та Up-Sell
| Параметр | Cross-Sell | Up-Sell |
|---|---|---|
| Що пропонується | Супутні товари | Преміум-версія поточного |
| Де показується | Сторінка товару, кошик | Сторінка товару, перед додаванням у кошик |
| Метрика | Збільшення кількості позицій | Збільшення суми однієї позиції |
| Приклад | Чохол до телефону | 128 ГБ замість 64 ГБ |
Модель даних: ручні зв'язки
Для малих каталогів — ручне встановлення зв'язків у панелі адміністратора:
CREATE TABLE product_relations (
id BIGSERIAL PRIMARY KEY,
product_id BIGINT NOT NULL REFERENCES products(id) ON DELETE CASCADE,
related_product_id BIGINT NOT NULL REFERENCES products(id) ON DELETE CASCADE,
type VARCHAR(20) NOT NULL, -- 'cross_sell', 'up_sell', 'accessory', 'spare_part'
sort_order SMALLINT DEFAULT 0,
UNIQUE(product_id, related_product_id, type)
);
CREATE INDEX idx_product_relations_pid_type ON product_relations(product_id, type);
У панелі адміністратора — пошук товарів та додавання зв'язків drag-and-drop з вибором типу.
Автоматичний Cross-Sell через категорії
Якщо зв'язки не встановлені вручну — автоматичний fallback за сумісними покупками або категоріями аксесуарів:
class CrossSellResolver
{
public function resolve(Product $product, int $limit = 4): Collection
{
// 1. Ручні зв'язки
$manual = $product->relations()
->where('type', 'cross_sell')
->with('relatedProduct')
->orderBy('sort_order')
->limit($limit)
->get()
->map(fn($r) => $r->relatedProduct);
if ($manual->count() >= $limit) return $manual;
// 2. Автоматичні зі спільних покупок (якщо достатньо даних)
$needed = $limit - $manual->count();
$auto = DB::table('product_cooccurrences')
->where('product_a', $product->id)
->whereNotIn('product_b', $manual->pluck('id'))
->orderByDesc('cooccurrence_count')
->limit($needed)
->pluck('product_b');
$autoProducts = Product::whereIn('id', $auto)->where('is_active', true)->get();
return $manual->merge($autoProducts);
}
}
Up-Sell: варіанти одного товару
Для варіативних товарів (телефони з різним об'ємом пам'яті) up-sell — це навігація між варіантами з акцентом на преміум:
const UpSellVariants = ({ currentVariant, variants }: UpSellProps) => {
const betterVariants = variants.filter(v => v.price > currentVariant.price);
if (!betterVariants.length) return null;
return (
<div className="border rounded-lg p-4 bg-amber-50">
<p className="text-sm font-medium mb-2">Розгляньте покращену версію:</p>
{betterVariants.slice(0, 2).map(variant => (
<div key={variant.id} className="flex items-center justify-between py-2">
<span className="text-sm">{variant.label}</span>
<div className="flex items-center gap-2">
<span className="text-xs text-gray-500">
+{formatPrice(variant.price - currentVariant.price)}
</span>
<Button size="sm" variant="outline" onClick={() => selectVariant(variant)}>
Вибрати
</Button>
</div>
</div>
))}
</div>
);
};
Cross-Sell у кошику: "Доповніть замовлення"
Найбільш конверсійний момент для cross-sell — сторінка кошика. Блок "Часто беруть разом" агрегує рекомендації зі всіх товарів у кошику:
public function getCartCrossSells(Cart $cart): Collection
{
$cartProductIds = $cart->items->pluck('product_id');
// Об'єднуємо рекомендації cross-sell усіх товарів у кошику
$recommendations = DB::table('product_relations')
->join('products', 'products.id', '=', 'product_relations.related_product_id')
->whereIn('product_relations.product_id', $cartProductIds)
->whereNotIn('product_relations.related_product_id', $cartProductIds)
->where('product_relations.type', 'cross_sell')
->where('products.is_active', true)
->where('products.stock', '>', 0)
->select('products.*', DB::raw('COUNT(*) as relevance_score'))
->groupBy('products.id')
->orderByDesc('relevance_score')
->limit(4)
->get();
return $recommendations;
}
Товар, рекомендований кількома позиціями кошика, отримує більш високий relevance_score і показується першим.
Quick-Add у блоці рекомендацій
Кнопка "Додати у кошик" прямо в карточці cross-sell — без переходу на сторінку товару:
const CrossSellCard = ({ product }: { product: Product }) => {
const { addToCart, isLoading } = useCart();
return (
<div className="border rounded-lg p-3 flex gap-3">
<img src={product.thumb} alt={product.name} className="w-16 h-16 object-cover rounded" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{product.name}</p>
<p className="text-sm text-gray-700">{formatPrice(product.price)}</p>
</div>
<Button
size="sm"
loading={isLoading(product.id)}
onClick={() => addToCart(product.id, 1)}
>
+ В кошик
</Button>
</div>
);
};
Bundle: фіксовані комплекти
Окремий тип cross-sell — бандл зі скидкою на комплект:
CREATE TABLE product_bundles (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255),
discount_percent NUMERIC(5,2) DEFAULT 0,
is_active BOOLEAN DEFAULT TRUE
);
CREATE TABLE product_bundle_items (
bundle_id BIGINT REFERENCES product_bundles(id) ON DELETE CASCADE,
product_id BIGINT REFERENCES products(id),
is_primary BOOLEAN DEFAULT FALSE,
PRIMARY KEY(bundle_id, product_id)
);
На сторінці основного товару показується блок "Купити комплектом": усі товари бандла + підсумкова ціна зі скидкою. Додавання у кошик — однією кнопкою.
Аналітика ефективності блоків
Кожен показ cross-sell/up-sell логується. Метрики:
- Impressions — скільки разів блок показаний
- CTR — клики на рекомендації / покази
- Add-to-cart rate — додавання / клики
- Uplift — середня вартість замовлення з cross-sell vs без
Дані дозволяють оптимізувати розташування, кількість рекомендацій та вибір алгоритму. Зазвичай 4 рекомендації показують кращий CTR, ніж 2 або 8.







