Корзина гостя и вход покупателя - привязка, слияние, чистка
Разбираемся, к чему привязана корзина, что с ней происходит при входе на сайт и почему позиции иногда теряются или задваиваются.
Механика
Корзина принадлежит не пользователю сайта, а покупателю. Покупатель - отдельная сущность модуля продаж: он есть и у гостя, который никогда не входил на сайт.
Идентификатор покупателя-гостя живёт в куке браузера и переживает закрытие вкладки. Пока кука цела, гость возвращается к своей корзине, даже если между визитами прошло несколько дней.
При входе на сайт покупатель связывается с учётной записью. С этого момента корзина следует за человеком, а не за браузером, и открывается на любом устройстве.
Само связывание покупателя с учётной записью делает платформа, а не ваш код. Свой обработчик входа, который пересоздаёт сессию или чистит куки, ломает эту связь и выглядит как исчезнувшая корзина.
Позиции корзины хранятся отдельными строками с количеством, ценой и свойствами. Один и тот же товар с разными свойствами - это две разные строки, и складывать их платформа не станет.
Корзина привязана ещё и к конкретной витрине, то есть к коду сайта. На мультисайте у одного покупателя своя корзина на каждой витрине, и это осознанное поведение, а не ошибка.
Строки корзин гостей копятся годами и никем не удаляются сами по себе. Магазин с заметной посещаемостью набирает миллионы строк, и таблица корзины становится одной из самых больших в базе.
Содержимое корзины никогда не попадает в кэш компонентов витрины. Любой блок, показывающий корзину или её счётчик, собирается на каждом запросе заново и поэтому заметен в замерах скорости.
Отсюда два регулярных дела вокруг корзины в любом работающем магазине. Первое - беречь связь покупателя со входом, второе - чистить старые корзины по расписанию, не дожидаясь роста базы.
Разбор жалоб на корзину почти всегда сводится к одному вопросу. Тот ли идентификатор покупателя видит код в момент, когда посетитель жалуется на пропавшие товары.
Ответ на этот вопрос занимает минуту и сразу делит разбор надвое. Идентификатор прежний - ищем ошибку в самих позициях, идентификатор новый - ищем в коде входа и в куках.
Шаги
- Посмотреть текущего покупателя и содержимое его корзины прямо из кода.
- Проверить, меняется ли идентификатор покупателя при входе на сайт.
- Найти свои обработчики входа, которые трогают сессию или куки покупателя.
- Проверить показ счётчика корзины на страницах, отдаваемых из композитной копии.
- Настроить чистку старых корзин и следить за размером таблицы.
Код
Смотрим покупателя и его корзину:
\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_rowFROM b_sale_basket;-- миллионы строк без заказа означают, что чистка не настроена-- дата самой старой строки показывает, с какого года копится таблицаОграничения
Корзина консольного скрипта не определяется сама. В агенте и в задании по расписанию куки нет, поэтому идентификатор покупателя передают явным параметром.
Количество товара в корзине не проверяет складской остаток само по себе. Проверку наличия делает оформление заказа, а до него в корзине может лежать что угодно.
Резервирование товара при простом добавлении его в корзину не происходит вовсе. Резерв появляется на этапе заказа, и до него количество на складе никак не защищено.
Корзина не переезжает между разными доменами одного и того же проекта. Кука покупателя привязана к домену, и на поддомене корзина будет своя, если куку не расширили на весь домен.
Слияние корзин при входе не складывает строки с разными свойствами. Один товар в двух комплектациях останется двумя строками, и покупателя это иногда удивляет.
Типичные проблемы
После входа на сайт корзина оказывается пустой.
Свой обработчик входа пересоздаёт сессию или чистит куки, разрывая связь с покупателем. Сравнивают идентификатор покупателя до и после входа и убирают лишние действия из обработчика.
Один товар лежит в корзине двумя строками.
Позиции отличаются свойствами или разными торговыми предложениями одного товара. Такие строки платформа не объединяет, и это ожидаемое поведение корзины.
Счётчик корзины показывает чужое количество товаров.
Счётчик попал в общую копию страницы и отдаётся всем посетителям одинаково. Счётчик выносят в динамическую область, которая догружается вторым запросом после показа.
На поддомене корзина каждый раз пустая.
Кука покупателя выдана на конкретный домен и на поддомен не приходит. Область действия куки расширяют на весь домен настройками главного модуля.
Таблица корзины разрослась до миллионов строк.
Строки корзин гостей копятся годами и никогда не чистятся. Позиции без заказа старше срока хранения удаляют по расписанию порциями.
Скрипт по расписанию работает не с той корзиной.
В консоли нет куки покупателя, и код берёт корзину случайного идентификатора. Идентификатор покупателя в таких скриптах передают явным параметром вызова функции.
Частые вопросы
К чему привязана корзина: к пользователю или к браузеру?
К покупателю модуля продаж: у гостя он живёт в куке, у вошедшего связан с учётной записью. Поэтому корзина гостя переживает закрытие браузера, а корзина покупателя открывается на другом устройстве.
Почему после входа пропали товары?
Чаще всего свой обработчик входа пересоздаёт сессию или чистит куки покупателя. Сравнение идентификатора покупателя до и после входа показывает это за минуту.
Складываются ли одинаковые товары при слиянии корзин?
Складываются только полностью одинаковые позиции: тот же товар с теми же свойствами. Разные комплектации остаются отдельными строками корзины.
Как долго живёт корзина гостя?
Пока цела кука покупателя и пока строки не удалила чистка старых корзин. Оба срока задаются настройками и обычно измеряются месяцами.
Можно ли удалять старые строки корзины?
Да, но только те, что не привязаны к заказам: позиции заказов - это документы. Чистку делают порциями по расписанию и следят за размером таблицы.
Смежное
- Корзина и оформление - оглавление подтемы
- Работа с корзиной из кода: добавление, количество, пересчёт - основные операции с корзиной
- Добавление в корзину: из списка, из кода, с нужными свойствами - откуда берутся свойства позиций
- События корзины: подарки, ограничения, пересчёт - вмешательство в состав корзины
- Брошенные корзины: поиск, письмо, чистка старых - что делать с накопленными корзинами
- Композитный сайт: включение, динамические области, сброс - счётчик корзины и статика
- Состояние в куках: срок, домен, подпись значения - как устроена кука покупателя
- Каталог и продажи - устройство продаж целиком
- Оформление требует входа: разбор причин - когда вход рвёт корзину гостя