01 / 08 screenshot-2026-08-14-at-12.13.31-2.png
Экран 1 из 8 Экран 2 из 8 Экран 3 из 8 Экран 4 из 8 Экран 5 из 8 Экран 6 из 8 Экран 7 из 8 Экран 8 из 8

CRM для книжного e-com проекта (WB, Ozon, Яндекс Маркет)

 

Для букинистического интернет-магазина разработана внутренняя CRM, которая ведёт каждую книгу от фотографии на складе до карточки сразу на трёх маркетплейсах — Ozon, Wildberries и Яндекс Маркете — и снимает её с двух оставшихся площадок в момент продажи на любой из них.

Задача отличалась от обычной автоматизации карточек тем, что здесь каждый товар — единственный экземпляр: одна и та же букинистическая книга не может быть продана дважды. Три площадки опрашиваются независимо, а книга физически одна, поэтому вся архитектура строится вокруг одного ограничения — оверселлинг должен быть исключён на уровне данных, а не на уровне договорённостей.

Задача

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

Из этого вытекали три требования к системе:

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

Как устроен конвейер

Каждая книга проходит семь стадий, каждая из которых — отдельная фоновая задача со своим статусом, счётчиком попыток и heartbeat. Оператор нигде не ждёт ответа внешнего API в браузере: загрузил фото — и перешёл к следующей стопке.

1. Приёмка

Кладовщику доступен один экран — массовая загрузка фотографий. Он фотографирует книгу, отмечает главный кадр и до двух технических снимков корешка, указывает полку. Артикул присваивается автоматически.

2. Распознавание зрением модели

Фотографии уходят в OpenAI Responses API со строгой JSON-схемой: модель обязана вернуть структуру, а не свободный текст. Каждому кадру передаётся его роль — обложка, корешок, разворот с копирайтом — и своя инструкция. Извлекаются название, автор, издательство, ISBN, год, серия, тип переплёта, состояние с перечнем дефектов, аннотация и хештеги. Детализация зрения принудительно выставлена в максимальный режим: в упрощённом режиме изображение сжимается примерно до 512 px, и твёрдый переплёт становится неотличим от мягкого.

3. Обогащение по рынку

По распознанным данным система ищет то же издание на Ozon и Wildberries, оценивает совпадения по степени схожести и добирает из карточек-аналогов то, чего нет на фото: количество страниц, размеры, вес, категорию Ozon с её обязательными характеристиками.

4. Ценообразование

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

5. Публикация

Для каждой площадки собирается свой payload: у Ozon — категорийные атрибуты и видеообложки, у Wildberries — карточка с медиа и артикулом продавца, у Яндекс Маркета — собственный формат оффера. Публикация логируется вместе со статусами и полным телом запроса и ответа, переживает ограничения площадки по частоте запросов с повторной отправкой по её же заголовку задержки, а завершающая фоновая задача проверяет, что карточка действительно опубликована, и прогревает остаток.

6. Продажа и снятие с остальных витрин

Заказы поступают опросом API каждой площадки и вебхуками. Заказ раскладывается на позиции, позиции — на движения по складу, движения записываются в единственную инвентарную запись книги, уникальность которой гарантирована ограничением на уровне базы данных. После этого срабатывает кросс-маркетплейсное снятие: остаток на двух других площадках уходит в ноль. Снятие разрешено только по подтверждённому событию заказа — предположение вида «карточка пропала из выдачи, наверное, продано» такого права не даёт.

7. Сверка и наблюдаемость

Каждый час строится снимок рассинхрона: локальный остаток сверяется с остатком на каждой площадке, с разбором причин расхождения и кнопкой массового исправления прямо из админки. Отдельная команда собирает состояние конвейера — зависшие распознавания, застрявшие публикации, упавшие за сутки фоновые задачи. Дополнительно настроены выгрузка опубликованных книг в Google Таблицы и резервное копирование базы данных на FTP каждые два часа в будни, фотографий — раз в сутки.

Четыре задачи, ради которых всё и затевалось

Гонка за единственный экземпляр

Три площадки опрашиваются независимо, воркеров несколько, а книга — одна. Риск продать один и тот же товар дважды закрыт несколькими слоями защиты: уникальная инвентарная запись на книгу на уровне схемы базы данных, журнал движений only-append с фиксацией источника изменения, версионирование выгрузок остатков, уникальность фоновых задач в рамках одного кабинета площадки и запрет менять чужие остатки без подтверждённого заказа.

Цены конкурентов без публичного API

Ни Ozon, ни Wildberries не отдают поиск по каталогу наружу, а без цен аналогов невозможно ценообразование. Для этого написаны два отдельных Node-сервиса на headless Chromium: они работают во внутренней docker-сети без портов, открытых наружу, ходят под сервисным токеном, сначала пробуют JSON-эндпоинт поиска и только потом разбирают HTML-каталог, перехватывая те же ответы, что площадка загружает сама себе. Cookies подключены только на чтение, у каждого запроса есть диагностика и жёсткий общий таймаут.

Маршрут до площадки, которого нет

С боевого IP оказалась недоступна часть маршрутов до маркетплейсов. Решением стал sidecar-контейнер с SOCKS5-прокси в той же internal docker-сети, куда браузерный сервис обращается по имени контейнера; ключ доступа хранится отдельным read-only секретом, сам контейнер поднят в режиме read-only без повышения привилегий. Прокси включается точечно через переменные окружения, без изменений в коде.

Всё падает, и это норма

Внешние API отвечают ошибками, отдают остаток с задержкой и иногда сообщают неверный статус товара. Поэтому каждый тяжёлый шаг конвейера — идемпотентная фоновая задача с уникальным ключом, счётчиком перезапусков и heartbeat, а поверх есть набор ремонтных Artisan-команд: поднять зависший конвейер, починить ложно «проданные» товары Ozon, восстановить потерянный остаток Wildberries, свести каталог с реальностью, схлопнуть дублирующиеся карточки.

Архитектура

Ядро — обычный Laravel-монолит: это удобно, пока вся сложность в бизнес-логике, а не в нагрузке. Наружу вынесено только то, что физически не помещается в PHP-процесс — два браузерных сервиса и прокси. Всё поднимается одним docker compose:

  • app — Laravel 13 на PHP-FPM, очереди под supervisor, планировщик задач;
  • nginx — фронт, статика, TLS через certbot;
  • mysql — MySQL 8, 37 таблиц;
  • wildberries_search — Node + Chromium: поиск аналогов на Wildberries, health-эндпоинт;
  • ozon_search — Node + Chromium с постоянным профилем браузера: поиск аналогов на Ozon;
  • outline_proxy — SOCKS5-выход для браузерного сервиса Ozon.

Стек:

  • PHP 8.3, Laravel 13, Eloquent, очереди на базе данных, Laravel Sanctum, Intervention Image для обработки фотографий, PhpSpreadsheet для импорта остатков из выгрузок;
  • админка — Blade, Alpine.js, Tailwind 4, Vite, без SPA: интерфейс складской, ему важна скорость и плотность, а не богатый клиентский стейт;
  • интеграции — Seller API Ozon, Wildberries и Яндекс Маркета, OpenAI Responses API, Google Sheets, FTP-бэкапы, уведомления в мессенджеры;
  • три роли доступа: администратор, менеджер с урезанными разделами и складской пользователь, которому доступен ровно один экран — загрузка фото.

Масштаб

Это не витрина и не интернет-магазин — покупатель сюда не заходит вообще. Это внутренний инструмент склада и контент-менеджеров, у которого вся сложность спрятана в очередях, интеграциях и сверках:

  • около 69 000 строк PHP;
  • 56 сервисных классов;
  • 28 фоновых очередей (queue-джобов);
  • 42 Artisan-команды, в основном ремонтные и диагностические;
  • 68 миграций, 37 таблиц;
  • 63 файла тестов на бизнес-логику интеграций;
  • 76 Blade-шаблонов админки;
  • 3 маркетплейса, у каждого свой мультикабинет.

Результат

Карточка книги на трёх площадках собирается из фотографий без ручного ввода: человек тратит время на съёмку и полку, а не на описание, категорию и цену. Цена перестала быть вкусовой — политика оценки описана кодом, с фильтрами аналогов, отсечением выбросов и надбавками, и одинаково применяется к любой книге.

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

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