Кейс · 02
PickMe — интернет-магазин и бот, который его наполняет
Overview
Полноценный интернет-магазин в проде — и Telegram-бот, который сам наполняет каталог по фото из приватного канала владелицы. Собрано одним разработчиком за месяц.
Разработчик и владелица магазина один человек, и каждое продуктовое решение приходило из собственной практики. PickMe Store — магазин брендовой одежды с двумя визуальными темами под мужскую и женскую аудиторию и отдельным режимом страницы товара для подарка. PickMe Import Bot превращает пост в приватном канале в товар на сайте за 36 секунд.
Context
Problem
До сайта у магазина было три параллельных проблемы, каждая стоила денег.
Репутация
Продажи через канал в Telegram и витрину на Авито считываются клиентом как «у одного частного продавца», а не как магазин: нет домена, нет привычной адресной строки, нет «https»-замочка в браузере, нет страницы товара, на которую можно прислать ссылку другу для совета. Покупатель колеблется и не возвращается.
Каталог разорван по площадкам
Один и тот же товар нужно вручную вести в трёх местах одновременно: в Telegram-канале, на Авито и в личных складских табличках. Нет фильтров, нет поиска, нет страницы товара с замерами. Если клиент спросит «а есть размер L в чёрном?» — ответ ищется руками.
Ручная рутина наполнения
На каждый товар уходило 15–30 минут: открыть админку, залить четыре-пять фото, придумать описание, проставить категорию и пол, написать отдельный текст для Авито (там другой формат, другие требования к продающим словам, другая длина), вбить артикул и цену.
Constraints
На входе у проекта было четыре ограничения, и одно из них во многом определило всю топологию системы.
- 01 Сама себе клиент → бюджет околонулевой. Нельзя купить Shopify, нанять команду, заказать брендинг. Бесплатные/дешёвые компоненты везде, где можно.
- 02 Сайт-сервер должен быть в РФ. Российский домен
.ru, индексация в Яндекс, репутация перед покупателями. А Gemini API на российских IP заблокирован — это автоматически отрезает прямой доступ к нему. Топология заранее оказалась двухзвенной: магазин в РФ, бот с обращениями к Gemini — на зарубежном VPS. - 03 Один разработчик и параллельная жизнь магазина. Продажи в Telegram + Авито шли всё время разработки. Сайт строился поверх живого бизнеса, без даунтайма для клиентов.
- 04 Две аудитории в одном магазине. Мужская и женская одежда примерно в равных долях. Универсально-серая витрина «для всех» не считывается как «свой магазин» ни одной из сторон.
Solution
Самый общий принцип — собрать связку из двух минимально достаточных продуктов так, чтобы вместе они закрывали всю операционную рутину магазина.
PickMe Store — собственный e-commerce
Без Tilda и Shopify. Современный фронт-стек на React 19 с TypeScript и Vite, серверная часть на Node.js + Express, база на PostgreSQL и собственная админка с фильтрами, валидацией полей, массовым обновлением статусов. Своё API сделано как отдельный набор внутренних эндпоинтов под токен-секрет, чтобы извне в каталог никто не смог записать.
Темы оформления через ThemeContext
Одни и те же компоненты, разные наборы цветовых и шрифтовых токенов. Переключение темы — одной кнопкой в шапке, без перезагрузки страницы. Поверх — отдельный режим страницы товара по адресу /gift/:id: то же фото, то же описание, но без цены и без кнопок действий, чтобы можно было переслать ссылку родственнику для согласования подарка.
PickMe Import Bot — AI-наполнение каталога
Telegram-бот на Python и aiogram, который превращает обычное поведение владелицы (постить фото вещи в свой приватный канал с короткой подписью) в публикацию товара на сайте.
Бот ловит пост, собирает фото из медиа-группы, скачивает их и парсит текст подписи: артикул, цена, размер, флаг «Авито: да». Дальше фото и текст уходят в Gemini 2.5 Flash через структурированный JSON-вывод — формат запроса, при котором модель обязана вернуть данные по заранее заданной схеме, а не свободный текст. На выходе получаем бренд, название, категорию, пол, описание для сайта и отдельное описание для Авито. Через внутреннее API сайта создаётся товар-черновик — он появляется в админке, и задача владелицы только проверить и нажать «опубликовать».
Окно «передумать» — 5 минут отложенной очереди
Между постом и обработкой проходит пять минут. Это сознательное окно: если пост опубликован по ошибке или в нём опечатка, его можно удалить из канала, и в каталог он не попадёт. Если за эти пять минут в канал прилетают добавочные фото с тем же media_group_id — они доклеиваются к уже стоящему в очереди посту.
Главный инженерный сюжет — обход сетевой блокировки
Прямой HTTPS-запрос с зарубежного сервера на сайт-сервер в РФ обрывался стабильно после ~9 КБ переданных данных: достаточно для короткого ответа, но мало для загрузки фото товара на сайт. Выяснилось, что это особенность фильтрации маршрута Нидерланды → РФ, не локальная конфигурация.
Решение — поднять SSH-туннель через autossh, завернуть его в systemd-юнит, который автоматически перезапускается при сбое, и сделать сам бот зависимым от этого юнита: пока туннель не поднят, бот не стартует. Для туннеля выпущен отдельный SSH-ключ с явным ограничением — он умеет только пробрасывать порт на конкретный сервис и больше ничего. Это не временный обход и не «костыль с туннелем», а штатный сервис на двух серверах с правильной изоляцией прав.
Главный инженерный сюжет
SSH-туннель — обход сетевой фильтрации NL → РФ
Tradeoffs
Часть решений — сознательный отказ от того, что выглядит привлекательно, но на текущем объёме не окупается.
- Не Tilda и не Shopify, а свой стек. Tilda — потолок по кастомизации; Shopify — избыточен для нашего масштаба.
- Не выкатывали векторную БД. Поиск по подстроке в Postgres + фильтры покрывают задачу. Когда каталог станет 1000+ — добавим.
- SQLite в боте вместо Postgres. Боту нужно только отсеивать повторы постов и хранить историю — SQLite этого хватает.
- Не делали мобильное приложение. Сайт адаптивен, на мобильных есть все три темы — нативное приложение не добавляет ценности на текущем объёме.
Results
Магазин работает в проде. На момент сборки кейса — 200+ товаров в каталоге, оформленных через бот, с фото, описаниями для сайта и отдельными описаниями для Авито.
- ~36 секунд от поста владелицы в приватный канал до появления товара-черновика в админке: ловля поста, сборка медиа-группы, скачивание фото, парсинг текста, обращение к Gemini, создание записи через API.
- Сайт в проде российский VPS в reg.ru (Москва), SSL от Let's Encrypt, индексация в Яндекс и Google, кэш статики в Nginx, ежесуточный бэкап БД в 3:00.
- 3 темы оформления женская розовая, мужская светлая, мужская тёмная. Все шрифты, цвета, фоны и декор лежат в токенах — добавление новой темы делается без правки компонентов.
- Режим подарка страница товара по адресу
/gift/:idбез цены и без кнопок, шеринг через системный диалог браузера (Web Share API). - Обход сетевой блокировки NL→РФ SSH-туннель с изолированным ed25519-ключом. Поднимается за 10 секунд после потери связи, восстанавливается без участия человека.
Tech stack
Frontend — что отдаёт интерфейс покупателю
React 19 с TypeScript, сборка на Vite, Tailwind CSS 4 + shadcn/ui. Темы устроены через ThemeContext поверх CSS-переменных — три набора токенов (female / male-light / male-dark), переключение мгновенное. Web Share API для подарочной ссылки + fallback на копирование в буфер.
Backend — что обслуживает данные и интеграцию с ботом
Node.js на Express 5, логи через Pino. PostgreSQL + Drizzle ORM. Клиент API сгенерирован из OpenAPI через Orval, монорепо организовано через pnpm workspaces (9 пакетов). Внутренние эндпоинты для бота закрыты заголовком X-Bot-Token со сравнением через crypto.timingSafeEqual.
AI и бот — что наполняет каталог автоматически
Python 3.10+, aiogram 3.27+ в режиме long polling. Gemini 2.5 Flash с JSON Structured Output через Pydantic-схемы. Три отдельных системных промпта: на извлечение полей, на описание для сайта, на описание для Авито. httpx async, SQLite для дедупликации постов, tenacity для повторов на 5xx и сетевых ошибках.
DevOps и ops — на чём всё это живёт круглосуточно
Сайт — Ubuntu 22.04 на VPS в reg.ru (Москва), Nginx + PM2 + Certbot SSL, ежесуточный бэкап БД в 3:00. Бот — Ubuntu 22.04 на зарубежном VPS, systemd для запуска. Канал между серверами — autossh-туннель под управлением systemd-юнита pickme-tunnel.service, выделенный SSH-ключ с ограничением restrict, port-forwarding, permitopen="localhost:3000". SEO — sitemap.xml, robots.txt, Open Graph для каждого товара, регистрация в Яндекс.Вебмастере и Google Search Console.