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 ведёт себя не по документации, есть набор ремонтных команд.








