SaaS-платформа для централизованного управления товарами, остатками и заказами на различных маркетплейсах (Wildberries, Ozon, Yandex Market, Avito) с интеграцией со складскими системами (МойСклад).
- Backend: Laravel 13, PHP 8.4, PostgreSQL, Redis, RabbitMQ, Horizon, Reverb (Websockets), Scout (Meilisearch).
- Microservices: Go 1.23, Python 3.11.
- Frontend: Vue 3 (Composition API), Inertia.js v3, Pinia (State Management), Tailwind CSS 4, Vuetify 3, Shadcn/UI.
- Analytics: ApexCharts.
- Testing: Pest 4.
- Monitoring: Prometheus, Grafana.
UniSellerHub построен на событийно-ориентированной архитектуре, где МойСклад выступает в роли "источника истины" для данных об остатках.
-
МойСклад — Источник Истины (Source of Truth):
- Все актуальные данные о товарах (названия, характеристики, штрихкоды, цены) и, главное, остатках хранятся в МойСклад.
- Изменения в МойСклад (поступления, продажи, списания) являются триггером для обновления данных в UniSellerHub и на маркетплейсах.
-
Централизованное управление остатками:
- Пользователь может видеть все свои остатки со всех маркетплейсов и МойСклад в едином интерфейсе (
/inventory). - Изменение остатка в UniSellerHub: Если пользователь вручную изменяет остаток товара в приложении, это изменение сначала отправляется в МойСклад.
- Распространение изменений: После успешного обновления в МойСклад, UniSellerHub автоматически обновляет локальную базу данных и пушит новый остаток на все подключенные маркетплейсы (WB, Ozon, Yandex Market).
- Пользователь может видеть все свои остатки со всех маркетплейсов и МойСклад в едином интерфейсе (
-
Двусторонняя синхронизация:
- Pull (Получение данных):
- Кнопка "Pull all Stocks from MP" на странице инвентаря: Запускает процесс получения актуальных остатков со всех подключенных маркетплейсов (WB, Ozon, Yandex Market) и МойСклад в локальную БД приложения. Это полезно для первоначального наполнения и периодической сверки.
- Кнопка "Sync Products" / "Sync Orders": Получает товары/заказы со всех подключенных маркетплейсов.
- Push (Отправка данных):
- Кнопка "Sync from MoySklad": Забирает остатки только из МойСклад и пушит их на все подключенные маркетплейсы (WB, Ozon, Yandex Market), а также обновляет локальный инвентарь.
- Ручное изменение остатка в таблице инвентаря: После обновления в МойСклад, пушит изменение на маркетплейсы.
- Webhooks (Реакция в реальном времени):
- Внешние системы (МойСклад, Маркетплейсы) отправляют уведомления о событиях (новый заказ, изменение остатка) на webhook-эндпоинты.
- UniSellerHub мгновенно реагирует на эти события, обновляет локальные данные и запускает соответствующие процессы (например, пуш остатков на маркетплейсы после изменения в МойСклад).
- Pull (Получение данных):
Для обеспечения высокой производительности и масштабируемости, UniSellerHub использует распределенную систему микросервисов, взаимодействующих через RabbitMQ.
- Go Sync Service (
/services/go_sync_service):- Отвечает за высокоскоростную синхронизацию с API маркетплейсов.
- Использует конкурентные горутины и строгий Rate Limiting (Token Bucket) для предотвращения блокировок.
- Price Analyzer (
/services/price_analyzer):- Python-сервис на базе Pandas для расчета скорости продаж и прогнозирования складских запасов. Поддерживает пакетную обработку данных для всех продуктов организации.
- Telegram Bot (
/services/telegram_bot):- Go-сервис для асинхронной рассылки уведомлений администраторам и пользователям.
- Report Generator (
/services/report_generator):- Python-сервис для генерации тяжелых аналитических отчетов в форматах Excel/PDF.
- Laravel (Оркестратор) ставит задачу в очередь
sync.tasks. - Микросервис выполняет задачу и возвращает результат в общую очередь
sync.results. - Команда Laravel
app:consume-sync-resultsобрабатывает ответ, обновляет данные и вызывает события.
- Пользователь инициирует генерацию отчета через UI (например, кнопка "Analyze Prices" на странице продуктов).
- Laravel диспатчит Job
InitiatePriceAnalysisReportJob, который:- Генерирует уникальный
batch_id. - Собирает данные (SKU, текущий запас, история продаж) для всех продуктов организации.
- Сохраняет метаданные батча (ID организации, ID пользователя, статус) во временном хранилище Redis.
- Отправляет пакет данных в очередь
price.tasksRabbitMQ для обработки сервисомPrice Analyzer.
- Генерирует уникальный
- Сервис
Price Analyzerобрабатывает пакет данных и отправляет агрегированный результат (для всего батча) обратно в очередьsync.results. - Laravel Consumer
app:process-price-analysis-sync-resultsслушает очередьsync.results:- При получении результата от
Price Analyzer(operation: "price_analysis_batch"):- Сохраняет результаты анализа цен во временном хранилище Redis под ключом
price_analysis:results:{batchId}. - Обновляет статус батча в Redis.
- Диспатчит Job
GenerateReportForBatchJob.
- Сохраняет результаты анализа цен во временном хранилище Redis под ключом
- При получении результата от
Report Generator(operation: "report_generation"):- Обновляет статус батча в Redis, сохраняет
report_filenameиdownload_url. - Диспатчит Job
NotifyUserOfReportJob.
- Обновляет статус батча в Redis, сохраняет
- При получении результата от
- Job
GenerateReportForBatchJobизвлекает результаты анализа цен из Redis, форматирует их в JSON-формат, ожидаемыйReport Generator, и отправляет задачу в очередьreport.tasksRabbitMQ. - Сервис
Report Generatorгенерирует Excel-файл и отправляет ссылку на него обратно в очередьsync.results. - Job
NotifyUserOfReportJobизвлекаетdownload_urlиз Redis, генерирует подписанную ссылку для скачивания и отправляет уведомление пользователю черезNotificationService. - После завершения процесса, временные данные батча удаляются из Redis.
- Dashboard: Интерактивные графики выручки и распределения заказов (ApexCharts). Виджеты "Inventory Health" для мониторинга товаров, которые заканчиваются.
- Global Search (Cmd+K): Мгновенный поиск по SKU, названиям товаров и ID заказов по всей базе данных (Scout + Meilisearch).
- Vector/Semantic Search: Поиск товаров по смыслу с использованием встроенного векторного поиска Laravel 13, PostgreSQL с расширением
pgvectorи эмбеддингов Laravel AI SDK. Позволяет находить релевантные продукты, понимая намерение пользователя, а не только ключевые слова. - Real-time Activity Feed: Живая лента событий синхронизации и новых заказов (Reverb + Spatie Activity Log).
- Marketplace Context: Динамический сайдбар с индивидуальными страницами для каждого подключения.
- Bulk Actions: Массовая синхронизация выбранных позиций в таблицах.
- Advanced Filters: Фильтрация заказов по диапазону дат и множественным статусам.
Для разработки и тестирования без реальных API-ключей реализован Mock Layer (/api/mock), который имитирует поведение внешних систем.
Mock-слой имитирует авторизацию реальных API для идентификации аккаунта:
- Wildberries: Заголовок
Authorization: <token> - Ozon: Заголовки
Client-Id: <client_id>иApi-Key: <api_key> - Yandex Market: Заголовок
Api-Key: <api_key>(также используютсяcampaign_idиbusiness_idизcredentials) - МойСклад: Заголовок
Authorization: Bearer <ms_token> - Avito: Заголовок
Authorization: Bearer <token>
Middleware IdentifyMockMarketplaceAccount перехватывает эти заголовки, находит соответствующий mock_marketplace_account_id и передает его в контроллеры, позволяя каждому тестовому аккаунту работать со своим набором мок-данных.
- Latency Simulation: Имитация сетевой задержки (0.5 - 1.5 сек) для реалистичного поведения.
- Rate Limiting: Ограничение до 60 запросов в минуту на аккаунт (возвращает HTTP 429 Too Many Requests).
- Idempotency: Поддержка заголовка
X-Idempotency-Keyдля предотвращения дублирования запросов, изменяющих состояние (например, обновление остатков). Ответы кешируются в Redis.
php artisan mock:simulate-activity: Консольная команда, запускаемая по расписанию (everyTenSeconds), которая имитирует внешнюю активность:- Генерирует новые заказы на WB/Ozon/Yandex.
- Изменяет остатки в МойСклад.
- Отправляет POST-запросы на внутренние webhook-эндпоинты (
/api/webhooks/wb,/api/webhooks/ozon,/api/webhooks/ms,/api/webhooks/avito), эмулируя поведение реальных сервисов.
- Webhook Handlers: Контроллеры (
WebhookController) принимают эти запросы, обрабатывают их и запускают соответствующую логику синхронизации.
- Продукты: Используется Content API v2 (
/content/v2/get/cards/list) для получения детальной информации о товарах. - Остатки: API остатков (
/api/v3/stocks) возвращаетsku(артикул), а неnmId(внешний ID). Логика синхронизации учитывает это, пытаясь найти листинг сначала поexternal_id, затем поvendor_code(SKU). - Обновление остатков: Используется
PUT /api/v3/stocks/{warehouseId}.
- Продукты: Получение товаров происходит в два этапа: сначала список
product_idиoffer_id(/v1/product/list), затем детали по этим ID (/v1/product/info/list). - Остатки: API остатков (
/v1/product/info/stocks) возвращаетproduct_idиoffer_id. - Обновление остатков: Используется
POST /v1/product/import/stocks.
- Структура: Использует понятия
businessId(для товаров и цен) иcampaignId(для заказов). - Продукты: Получение через
POST /v2/businesses/{businessId}/offer-mappings. - Остатки: Получение и обновление через
POST /v2/businesses/{businessId}/offers/stocks. - Авторизация: Использует
Api-Keyin header.
- Остатки: Синхронизация остатков по
itemIdчерез Avito API.
- Продукты/Остатки/Заказы: Использует
/api/remap/1.2/entity/assortment,/report/stock/all,/entity/customerorder. - Авторизация: Использует Bearer-токен.
- Источник истины: Остатки из МойСклад являются приоритетными при синхронизации.
Приложение использует Laravel Reverb для бродкастинга событий и Vue-компоненты для отображения уведомлений:
UserNotification: Событие, отправляемое пользователю через приватный канал.NotificationService: Сервис для отправки уведомлений конкретному пользователю или всем пользователям организации.AppShell.vue: Глобальный компонент, который слушает событияuser.notificationи отображает их черезSnackbar.- Flash-сообщения: При ручном запуске синхронизации через UI, Inertia Flash-сообщения используются для немедленной обратной связи.
-
Клонируйте репозиторий:
git clone https://github.com/a1lan1/unisellerhub.git
-
Настройте
.envфайл: Скопируйте.env.exampleв.envи настройте подключение к базе данных, Redis, RabbitMQ и Telegram. Убедитесь, что переменные для Mock API и Scout настроены корректно:# Marketplace Mock Endpoints WB_BASE_URL="${APP_URL}/api/mock/wb" OZON_BASE_URL="${APP_URL}/api/mock/ozon" YANDEX_BASE_URL="${APP_URL}/api/mock/yandex" MOYSKLAD_BASE_URL="${APP_URL}/api/mock/ms" # Telegram Bot TELEGRAM_BOT_TOKEN=your_token TELEGRAM_ADMIN_CHAT_ID=your_chat_id
-
Запуск в Docker:
make install
-
Важно! Запустите Consumer результатов в Laravel: Для того чтобы приложение начало принимать ответы от микросервисов (Go/Python), выполните:
docker compose exec app php artisan app:consume-sync-results -
Выполните миграции и сидеры:
docker compose exec app php artisan migrate:fresh --seed
- Prometheus:
http://localhost:9090 - Grafana:
http://localhost:3000(Стандартные дашборды: "Horizon", "Marketplace Sync Overview") - RabbitMQ UI:
http://localhost:15672(Логин/Пароль: guest/guest)