Следующие шаги на пути к созданию собственной системы электронной коммерции

вступление

Некоторое время назад у меня возникла дикая идея использовать Kesytone.js для создания системы электронной коммерции. Это путешествие началось пару недель назад, и до сих пор мы говорили о системных требованиях, настройке среды и базовых моделях и контроле доступа. В этой статье давайте сосредоточимся на основных функциях корзины. Также готовый код этой статьи доступен на моем GitHub.

Корзина

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

Кроме того, есть одна важная проблема, которую следует учитывать, и это скорее деловая, чем техническая проблема. Должны ли мы позволять пользователям добавлять в корзину больше продуктов, чем доступно в настоящее время? Я имею в виду, что в случае, когда товар есть в наличии, например, в наличии четыре товара, но пользователь пытается добавить в корзину пять, что должно произойти в этом случае?

Конечно, эта проблема должна быть защищена в пользовательском интерфейсе, но в некоторых случаях это все равно может произойти. Использование SSR и Next.js имеет некоторые недостатки, и в самом простом случае количество доступных продуктов проверяется только при рендеринге страницы. Это может привести к тому, что наличие товара может измениться в промежутке между рендерингом и моментом добавления товара в корзину.

Есть два основных решения: во-первых, заблокировать добавление в корзину в этом случае или перенести этот шаг проверки вперед и заблокировать создание заказов с товарами, которых нет в наличии. Несмотря на наше решение, этот шаг необходим с точки зрения безопасности.

Я считаю, что есть третье решение этой проблемы — что-то среднее между двумя ранее упомянутыми. Схема Stock включает в себя информацию о следующей доставке, поэтому, если товаров на складе недостаточно, но вместе их достаточно, пользователь может добавить их в корзину. Но заказ будет задержан из-за этого.

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

После этого мы можем сосредоточиться на каждой из этих трех операций. Сначала добавьте товар в корзину. В основном это два шага. Проверяйте наличие и обновляйте товары в корзине. То же самое относится и к обновлению количества. Удаление продуктов — это просто обновление модели корзины, верно? Не совсем. Давайте посмотрим на нашу схему Cart:

Есть связанный со списком Product, но нет возможности хранить информацию о количестве добавленных товаров. Итак, мы должны создать промежуточный список (называемый сводной таблицей в номенклатуре SQL) для обработки отношения «многие ко многим» и хранения информации о количестве.

Список товаров в корзине

Основная цель этого списка — хранить взаимосвязи между Cart и Product объектами и информацию о количестве. По сути, его следует запрашивать только как отношение от Cart. Нет смысла запрашивать его напрямую. Создадим cart-product.schema.ts:

Но подождите, там только два поля? Все просто, этому списку не нужно ничего знать о Cart или о том, к чему относятся эти продукты. Но, с другой стороны, эта информация нужна модели Cart, поэтому мы должны обновить этот список и изменить отношение с Product на CartProduct. Кроме того, больше нет необходимости скрывать возможность создания этого объекта из пользовательского интерфейса администратора.

products: relationship({
  ref: 'CartProduct',
  many: true,
}),

Хорошо, теперь мы можем обновить наши потоки:

  • Добавление в корзину:
    1. Подтвердить запас
    2. Создать CartProduct объектов
    3. Обновить Cart модель
  • Удалить товар из корзины:
    1. Удалить CartProduct объект
    2. Обновить Cart
  • Изменить количество в корзине:
    1. Подтвердить запас
    2. Обновить CartProduct сущность
    3. Обновить Cart

Пара слов об абстракциях фреймворка и их ограничениях

Но зачем все эти проблемы? Давайте посмотрим на текущую ER-диаграмму нашей базы данных:

У нас есть таблицы Cart, CartProduct и Product, но также есть таблица _Cart_products. Мы не создавали последний, верно? Undelaying Prisma сделала это за нас. Вот почему хорошо иметь общее представление об инструментах, которые мы используем.

У Prisma есть два способа создания отношений многие ко многим (дополнительная информация доступна в документации), явный или неявный. В первом мы отвечаем за создание сводных таблиц и связей в других таблицах в нашем файле schema.prisma. Во втором мы пропускаем сводную таблицу, и ORM создает ее за нас.

Но в нашем случае у нас нет прямого контроля над файлом schema.prisma; Keystone позаботится об этом и использует неявный метод. В большинстве случаев это прекрасно, но иногда могут быть некоторые недостатки, как вот эта ненужная таблица.

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

Хуки и поток проверки

Для выполнения всех шагов, связанных с каждой операцией корзины, нам нужен инструмент, который позволяет нам выполнять некоторые побочные эффекты, включая дополнительную проверку при обновлении схемы Cart. К счастью, у Keystone есть идеальный инструмент для этого.

Есть Hooks API, который делает то же самое со всей схемой или отдельными полями в ней. Их пять:

  • resolveInput позволяет нам изменять входные данные перед проверкой при создании или обновлении.
  • validateInput и validateDelete дают нам возможность возвращать дополнительные ошибки проверки в операциях создания/обновления и удаления соответственно.
  • beforeOperation обрабатывает побочные эффекты перед операцией с базой данных
  • afterOperation делает то же самое, но уже после операции.

Подробнее о хуках читайте в документах.

Хорошо, вернемся к нашей системе. Весь процесс проще, чем кажется; нам нужно использовать только два крючка (третий бонус). Во-первых, давайте предположим, что каждая мутация updateCart должна иметь все продукты, находящиеся в настоящее время в корзине (также добавленные ранее). Таким образом, когда мы отправляем список продуктов, содержимое корзины устанавливается в этот список. При наличии пустого списка содержимое корзины очищается, а при отсутствии списка товаров содержимое корзины не изменяется. Так, например, мутация должна выглядеть так:

Чтобы справиться с этим, мы должны удалить все CartProduct сущности и добавлять новую при каждом обновлении. Для этого нам нужно использовать хук beforeOperation в схеме Cart:

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

Хорошо, это часть об обновлении содержимого корзины, но как насчет проверки запасов. Разве это не должно было произойти раньше? Да, но это должно происходить в схеме CartProduct, а не прямо в корзине. Мы собираемся добавить хук validateInput:

Здесь он проверяет запас каждого продукта и сравнивает запрошенное количество с объединенным запасом и количеством в следующей доставке. Если этого недостаточно, мы вызываем функцию addValidationError, чтобы создать ошибку проверки. Этот метод почти идеален. Есть только одна проблема: объекты CartProduct создаются до обновления корзины, и при ошибке проверки объект Cart не обновляется.

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

Как насчет последнего бонусного хука? В Cart есть поле sum, содержащее информацию о стоимости всей корзины, и нам нужен способ ее расчета. Хук resolveInput работает лучше всего:

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

Краткое содержание

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

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

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

Не поймите меня неправильно, я все еще планирую закончить эту серию и построить эту систему, но чтобы не потерять от этого удовольствие — и чтобы это не превратилось в рутинную работу в следующей статье — я возьму на себя сломай и напиши о чем-нибудь другом.

Увидимся там!