Перейти к содержимому

Корзина гостя и вход покупателя - привязка, слияние, чистка

Разбираемся, к чему привязана корзина, что с ней происходит при входе на сайт и почему позиции иногда теряются или задваиваются.

Механика

Корзина принадлежит не пользователю сайта, а покупателю. Покупатель - отдельная сущность модуля продаж: он есть и у гостя, который никогда не входил на сайт.

Идентификатор покупателя-гостя живёт в куке браузера и переживает закрытие вкладки. Пока кука цела, гость возвращается к своей корзине, даже если между визитами прошло несколько дней.

При входе на сайт покупатель связывается с учётной записью. С этого момента корзина следует за человеком, а не за браузером, и открывается на любом устройстве.

Само связывание покупателя с учётной записью делает платформа, а не ваш код. Свой обработчик входа, который пересоздаёт сессию или чистит куки, ломает эту связь и выглядит как исчезнувшая корзина.

Позиции корзины хранятся отдельными строками с количеством, ценой и свойствами. Один и тот же товар с разными свойствами - это две разные строки, и складывать их платформа не станет.

Корзина привязана ещё и к конкретной витрине, то есть к коду сайта. На мультисайте у одного покупателя своя корзина на каждой витрине, и это осознанное поведение, а не ошибка.

Строки корзин гостей копятся годами и никем не удаляются сами по себе. Магазин с заметной посещаемостью набирает миллионы строк, и таблица корзины становится одной из самых больших в базе.

Содержимое корзины никогда не попадает в кэш компонентов витрины. Любой блок, показывающий корзину или её счётчик, собирается на каждом запросе заново и поэтому заметен в замерах скорости.

Отсюда два регулярных дела вокруг корзины в любом работающем магазине. Первое - беречь связь покупателя со входом, второе - чистить старые корзины по расписанию, не дожидаясь роста базы.

Разбор жалоб на корзину почти всегда сводится к одному вопросу. Тот ли идентификатор покупателя видит код в момент, когда посетитель жалуется на пропавшие товары.

Ответ на этот вопрос занимает минуту и сразу делит разбор надвое. Идентификатор прежний - ищем ошибку в самих позициях, идентификатор новый - ищем в коде входа и в куках.

Шаги

  1. Посмотреть текущего покупателя и содержимое его корзины прямо из кода.
  2. Проверить, меняется ли идентификатор покупателя при входе на сайт.
  3. Найти свои обработчики входа, которые трогают сессию или куки покупателя.
  4. Проверить показ счётчика корзины на страницах, отдаваемых из композитной копии.
  5. Настроить чистку старых корзин и следить за размером таблицы.

Код

Смотрим покупателя и его корзину:

\Bitrix\Main\Loader::includeModule('sale');
$fUserId = \Bitrix\Sale\Fuser::getId(); // покупатель текущего посетителя
$basket = \Bitrix\Sale\Basket::loadItemsForFUser($fUserId, SITE_ID);
foreach ($basket as $item) {
printf("%-30s %s x %s\n", $item->getField('NAME'),
$item->getQuantity(), $item->getPrice());
}
printf("позиций: %d, сумма: %s\n", count($basket), $basket->getPrice());
// корзина всегда читается парой: идентификатор покупателя и код сайта

Идентификатор покупателя - главное число этого разбора. Именно он связывает браузер, учётную запись и строки корзины, и почти все жалобы на корзину объясняются его сменой.

Сравниваем покупателя до и после входа:

printf("покупатель: %d, пользователь: %d\n", \Bitrix\Sale\Fuser::getId(), $USER->GetID());
printf("кука: %s\n", $_COOKIE['BITRIX_SM_SALE_UID'] ?? '(нет)');
printf("сайт: %s, сессия: %s\n", SITE_ID, session_id());
// разные значения до и после входа означают потерю корзины гостя

Смена идентификатора при входе - точный признак поломки. Дальше ищут свой код: обработчик входа, пересоздание сессии, чистку кук или сторонний вход через внешний сервис.

Переносим позиции гостя своим кодом:

$guest = \Bitrix\Sale\Basket::loadItemsForFUser($guestFUserId, SITE_ID);
$own = \Bitrix\Sale\Basket::loadItemsForFUser(\Bitrix\Sale\Fuser::getId(), SITE_ID);
foreach ($guest as $item) {
$exists = $own->getExistsItem('catalog', $item->getProductId());
$exists ? $exists->setField('QUANTITY', $exists->getQuantity() + $item->getQuantity())
: $own->createItem('catalog', $item->getProductId())->setFields($item->getFields()->getValues());
}
$own->save();
// после переноса корзину гостя удаляют, иначе позиции задвоятся при повторном входе

Свой перенос нужен там, где вход сделан нестандартно. Например, покупатель приходит из мобильного приложения по токену, и штатное связывание в этом сценарии не срабатывает.

Проверяем счётчик корзины на витрине:

// шаблон компонента корзины: файл template.php
$frame = $this->createFrame()->begin('');
?><span class="cart-count"><?= $basket->getQuantityList() ? count($basket) : 0 ?></span><?php
$frame->end();
// счётчик обязан быть динамической областью, иначе он попадёт в общую копию

Счётчик корзины в статике - классическая поломка композитного сайта. Посетитель видит чужое число товаров, и выглядит это страшнее, чем есть на самом деле.

Чистим старые корзины порциями:

$old = \Bitrix\Sale\Internals\BasketTable::getList(['filter' => [
'<DATE_UPDATE' => (new \Bitrix\Main\Type\DateTime())->add('-180 days'),
'=ORDER_ID' => null], 'select' => ['ID'], 'limit' => 500])->fetchAll();
foreach ($old as $row) { \Bitrix\Sale\Internals\BasketTable::delete($row['ID']); }
printf("удалено строк: %d\n", count($old)); // порция за один запуск
// строки, привязанные к заказам, не трогаем: это история покупок

Чистят только строки без заказа. Позиции оформленных заказов - это документы, и их удаление ломает историю покупок и отчёты по продажам.

Срок хранения корзин выбирают по поведению покупателей конкретного магазина. Для товаров повседневного спроса хватает трёх месяцев, а для дорогой техники с долгим выбором разумнее полгода.

Смотрим размер таблицы корзины:

SELECT COUNT(*) AS rows_total,
SUM(ORDER_ID IS NULL) AS without_order,
MIN(DATE_UPDATE) AS oldest_row
FROM b_sale_basket;
-- миллионы строк без заказа означают, что чистка не настроена
-- дата самой старой строки показывает, с какого года копится таблица

Ограничения

Корзина консольного скрипта не определяется сама. В агенте и в задании по расписанию куки нет, поэтому идентификатор покупателя передают явным параметром.

Количество товара в корзине не проверяет складской остаток само по себе. Проверку наличия делает оформление заказа, а до него в корзине может лежать что угодно.

Резервирование товара при простом добавлении его в корзину не происходит вовсе. Резерв появляется на этапе заказа, и до него количество на складе никак не защищено.

Корзина не переезжает между разными доменами одного и того же проекта. Кука покупателя привязана к домену, и на поддомене корзина будет своя, если куку не расширили на весь домен.

Слияние корзин при входе не складывает строки с разными свойствами. Один товар в двух комплектациях останется двумя строками, и покупателя это иногда удивляет.

Типичные проблемы

После входа на сайт корзина оказывается пустой.

Свой обработчик входа пересоздаёт сессию или чистит куки, разрывая связь с покупателем. Сравнивают идентификатор покупателя до и после входа и убирают лишние действия из обработчика.

Один товар лежит в корзине двумя строками.

Позиции отличаются свойствами или разными торговыми предложениями одного товара. Такие строки платформа не объединяет, и это ожидаемое поведение корзины.

Счётчик корзины показывает чужое количество товаров.

Счётчик попал в общую копию страницы и отдаётся всем посетителям одинаково. Счётчик выносят в динамическую область, которая догружается вторым запросом после показа.

На поддомене корзина каждый раз пустая.

Кука покупателя выдана на конкретный домен и на поддомен не приходит. Область действия куки расширяют на весь домен настройками главного модуля.

Таблица корзины разрослась до миллионов строк.

Строки корзин гостей копятся годами и никогда не чистятся. Позиции без заказа старше срока хранения удаляют по расписанию порциями.

Скрипт по расписанию работает не с той корзиной.

В консоли нет куки покупателя, и код берёт корзину случайного идентификатора. Идентификатор покупателя в таких скриптах передают явным параметром вызова функции.

Частые вопросы

К чему привязана корзина: к пользователю или к браузеру?

К покупателю модуля продаж: у гостя он живёт в куке, у вошедшего связан с учётной записью. Поэтому корзина гостя переживает закрытие браузера, а корзина покупателя открывается на другом устройстве.

Почему после входа пропали товары?

Чаще всего свой обработчик входа пересоздаёт сессию или чистит куки покупателя. Сравнение идентификатора покупателя до и после входа показывает это за минуту.

Складываются ли одинаковые товары при слиянии корзин?

Складываются только полностью одинаковые позиции: тот же товар с теми же свойствами. Разные комплектации остаются отдельными строками корзины.

Как долго живёт корзина гостя?

Пока цела кука покупателя и пока строки не удалила чистка старых корзин. Оба срока задаются настройками и обычно измеряются месяцами.

Можно ли удалять старые строки корзины?

Да, но только те, что не привязаны к заказам: позиции заказов - это документы. Чистку делают порциями по расписанию и следят за размером таблицы.

Смежное

Первоисточник