Tirebase — IT-платформа для шинного бизнеса: пять физических точек в Петербурге, десяток поставщиков, присутствие на трёх маркетплейсах. Я отвечаю за всю техническую часть — от инфраструктуры до кода. Сейчас это экосистема из девяти сервисов, каждый из которых появился не из архитектурного перфекционизма, а потому что без него что-то конкретное не работало.
Общая картина
Прежде чем разбирать каждый сервис — вот как всё связано:
┌─────────────────────────────────────┐
│ Маркетплейсы │
│ Avito Ozon (x3) Wildberries │
└──────▲──────▲──────────▲────────────┘
│ │ │
XML │ YML/API WB SDK│
feeds │ │ │
┌──────┴──────┴──────────┴────────────┐
│ AUTOLOAD ENGINE │
│ (autoload.tirebase.ru) │
│ FastAPI + RabbitMQ Worker │
│ Pricing Engine │ Feed Generator │
└──────┬──────────┬───────────────────┘
│ │
REST API │ │ RabbitMQ
│ │ (feed.*.generate)
┌───────────────────────┤ │
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────┐
│ API │ │ ERP (1C) │ │ RabbitMQ │
│ api.tirebase │ │ + PIM │ │ │
│ GET /tires │ │ │ └────┬─────┘
│ GET /wheels │ └──────┬───────┘ │
└──────┬────────┘ │ │
│ │ ▼
│ REST │ ┌───────────────┐
▼ │ │ EVC │
┌──────────────┐ │ │ evc.tirebase │
│ SEARCH │ │ │ FastStream + │
│ search.tirebase│◄────────┘ │ WebSocket │
│ Vue 3 + TS │ └───────┬────────┘
│ PWA + Dexie │ │
└──────┬────────┘ WS Push │
│ │
│ Cart → CRM ▼
│ ┌───────────────┐
▼ │ SEARCH / TS │
┌──────────────┐ │ (браузер) │
│ OCRM │ └────────────────┘
│ ocrm.tirebase │
│ CRM + Avito │ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ б/у шины │ │ BSCAN │ │ PDF │ │ BOT │
└───────────────┘ │ CRPT │ │ HTML→PDF│ │ Telegram │
│ DataMatrix│ │ port 8888│ │ pyTBA │
└──────────┘ └──────────┘ └──────────┘
Каждый прямоугольник — это отдельный деплой, отдельный репозиторий, часто — отдельная база данных. Связи между ними — REST API, RabbitMQ и WebSocket. Никакого общего монолита.
API: фундамент всего
api.tirebase.ru — самый первый сервис. REST API, который отдаёт актуальные данные по шинам и дискам. GET /tires с фильтрами по бренду, модели, ширине, высоте, диаметру, сезону, шипам, RunFlat, грузовым, индексам нагрузки и скорости. GET /wheels — аналогично для дисков. Плюс /tires/params и /wheels/params — фотографии, описания, характеристики.
Это тонкий слой поверх PostgreSQL, который знает структуру шинных данных. Все остальные сервисы ходят сюда за товарными данными. Если API лежит — лежит всё.
# Типичный ответ API
{
"title": "Continental PremiumContact 7 205/55 R16 91V",
"brand": "Continental",
"model": "PremiumContact 7",
"width": 205, "height": 55, "diam": 16,
"season": "summer", "spike": false, "runflat": false,
"quantity": 48,
"storehouses": ["Московский пр.", "Лиговский пр."],
"rrc_price": 8900, "opt_price": 6200, "retail_price": 7800
}
Почему отдельный сервис, а не часть Autoload Engine? Потому что API должен быть стабильным и быстрым. Он обслуживает Search, Bot, внешних партнёров. Autoload Engine при этом постоянно эволюционирует — новые маркетплейсы, новые алгоритмы. Если бы API жил внутри Autoload, каждый деплой с новой фичей для Ozon мог бы уронить поиск для менеджеров.
Autoload Engine: мозг маркетплейсов
autoload.tirebase.ru — самый сложный сервис в экосистеме. Python 3.12, FastAPI, SQLAlchemy 2.0 async, PostgreSQL через asyncpg, Redis 7.2, RabbitMQ. Управляет присутствием на трёх маркетплейсах: Avito, Ozon, Wildberries.
Две ключевые подсистемы работают как отдельные процессы. API Server (main.py) обрабатывает HTTP-запросы — управление товарами, настройки магазинов, мониторинг. Feed Worker (worker.py) слушает очереди RabbitMQ и генерирует фиды для маркетплейсов.
Зачем два процесса? Feed generation — CPU-bound операция. Генерация XML-фида для Avito на 5000 товаров занимает 10-15 секунд. Если делать это в том же процессе, что обрабатывает API-запросы, latency улетает. RabbitMQ развязал руки — API ставит задачу в очередь (feed.avito.generate, feed.ozon.generate), worker подхватывает когда может.
Pricing Engine
Ценообразование на маркетплейсах — не просто «оптовая + наценка». Трёхуровневый алгоритм:
def calculate_marketplace_price(opt_price: Decimal, category: str) -> PriceResult:
# Уровень 1: Базовая наценка 18%
base_price = opt_price * Decimal("1.18")
# Уровень 2: Маржинальный буфер
# +5% но не менее 500 руб для шин, 1000 руб для дисков
margin = max(base_price * Decimal("0.05"),
Decimal("500") if category == "tire" else Decimal("1000"))
sell_price = base_price + margin
# Уровень 3: "Старая цена" — маркетплейс покажет перечёркнутую цену
old_price = sell_price * Decimal("1.06")
return PriceResult(
price=round_to_market(sell_price),
old_price=round_to_market(old_price),
)
def round_to_market(price: Decimal) -> int:
"""8743 → 8790. Цена на 90 выглядит 'рыночной'."""
return int(price) - int(price) % 100 + 90
Без минимального буфера дешёвые шины (бюджетные по 2000 руб) продавались бы с маржой 60 рублей — меньше стоимости упаковки. Округление к 90 — психология, 8743 выглядит «случайной», 8790 — «рыночной». Каждое число в алгоритме пришло из реального опыта торговли.
Умный выбор поставщика
Одна шина может быть у пяти поставщиков по разным ценам. Самая дешёвая — не всегда лучшая:
async def select_best_supplier(offers: list[SupplierOffer], min_stock: int = 16):
# Минимальный остаток 16 штук — при меньшем высок шанс out-of-stock
viable = [o for o in offers if o.stock >= min_stock]
# Цена на 30%+ ниже средней — подозрительно (ошибка в прайсе)
avg_price = sum(o.price for o in viable) / len(viable)
viable = [o for o in viable if abs(o.price - avg_price) / avg_price < 0.30]
# Надёжный поставщик с ценой чуть выше лучше ненадёжного с минимумом
viable.sort(key=lambda o: float(o.price) * (1.1 - o.reliability_score * 0.1))
return viable[0] if viable else None
Число 16 — эмпирическое. Если у поставщика 4 шины на складе, к моменту подтверждения заказа их купит кто-то другой. Reliability scoring — рейтинг по истории поставок: поставщик, который 3 раза из 10 сорвал сроки, проигрывает тому, кто на 3% дороже, но надёжен.
Фиды: три маркетплейса — три разных мира
Avito — XML-фиды, по одному на каждый из пяти магазинов. Менеджеры назначаются через itertools.cycle, описания рандомизируются из пула 15+ шаблонов — чтобы объявления выглядели «живыми», а не массовой публикацией. Avito понижает в выдаче аккаунты, которые выглядят как боты.
Ozon — три аккаунта, батчевый импорт по 100 позиций. Rate limit 50 req/min, решается простым asyncio.sleep(1.2) — пробовал token bucket, sliding window, но для batch import раз в 20 минут простой sleep надёжнее. offer_id через MD5 от brand:model:size — стабильный идентификатор, без него Ozon дважды создавал дубликаты.
Wildberries — пришлось написать свой SDK (wbapi/). Их API документация… неполная. TireCard — 11+ обязательных характеристик. Cursor pagination, batch sync по 1000 позиций, background upload медиа с прогрессом через Redis.
Кеширование и расписание
Три уровня: TTLCache в памяти (микросекунды), Redis через fastapi-cache2 (1-2 мс), готовые XML/YML на файловой системе. Nginx отдаёт файлы третьего уровня напрямую — Python не просыпается при каждом запросе маркетплейса.
Три шедулера: FeedScheduler — фиды раз в час, OzonFeedScheduler — каждые 20 минут (Ozon жёстче штрафует за неактуальные остатки), CacheScheduler — полный прогрев в 9:00 и 21:00 по Москве.
Search: рабочий инструмент менеджеров
search.tirebase.ru — Vue 3 + TypeScript + Vite + Tailwind CSS. PWA, работает офлайн. Версия 2.8.5 — это реально зрелый продукт, который каждый день используют менеджеры в пяти точках.
Три роли: менеджер по продажам, контент-менеджер, администратор. Менеджер ищет шины для клиента — мгновенная фильтрация по бренду, модели, сезону, размеру, поставщику. Три режима отображения: плоская таблица, группировка по бренду, подбор разноширинной комплектации (staggered fitment — когда передние и задние колёса разного размера, типичный запрос для BMW и Mercedes).
Фильтр по автомобилю — ввёл марку и модель, получил подходящие размеры, включая разноширинные конфигурации. Это экономит менеджеру минуты на каждом клиенте.
Offline-first архитектура была осознанным решением. Интернет в торговых точках бывает нестабильным. PWA с IndexedDB через Dexie — локальный кеш данных, фоновое обновление каждые 5 минут. Менеджер открывает приложение и сразу видит актуальный ассортимент, даже если WiFi только что отвалился.
Корзина с созданием заказа интегрирована с CRM. Менеджер набрал позиции — заказ уходит в систему. Excel-экспорт с rate limiting — чтобы никто не выгрузил весь прайс-лист одной кнопкой. Есть управление маркетплейсами: синхронизация Wildberries, подготовка данных для Ozon и Яндекс.Маркета.
EVC: нервная система
evc.tirebase.ru — Event Communication service. Python 3.13, FastAPI, FastStream + RabbitMQ, PostgreSQL, Redis. Микросервис, который я сначала думал не делать, а потом не мог без него жить.
Задача: когда Autoload Engine обновил цены на Wildberries, менеджер в Search должен увидеть уведомление. Когда CRM получила новый заказ — менеджер нужной точки должен узнать мгновенно. Когда поставщик обновил прайс — контент-менеджер должен увидеть это сейчас, а не через 5 минут при следующем обновлении.
Архитектура:
Autoload/CRM/другие сервисы
│
RabbitMQ events.topic
│
▼
Notification Processor
│
direct.user exchange
│
▼
WebSocket → браузер
Любой сервис кидает событие в RabbitMQ topic exchange. EVC подхватывает, определяет, кому отправить, и пушит через WebSocket. JWT-авторизация через ERP API с кешированием в Redis на 24 часа — не дёргаем ERP на каждый WebSocket connect.
Kubernetes-ready health checks — потому что это единственный сервис, который я планирую вынести в Kubernetes, если нагрузка вырастет. Остальные пока живут в Docker Compose. Не уверен, что Kubernetes здесь вообще нужен при текущих объёмах, но хочется быть готовым.
TS: шиномонтаж как POS-терминал
ts.tirebase.ru — система управления заказами шиномонтажа. Vue 3 + TypeScript + Pinia + Tailwind CSS. Интерфейс в стиле POS-терминала, оптимизированный под тач-скрин.
Менеджер шиномонтажа выбирает категории услуг, добавляет сервисы в корзину, выбирает рабочий пост (у нас несколько подъёмников). Управление сменами — кто сегодня работает, какая выручка по сменам. Backend — crm.tireshop.ru.
Почему отдельный сервис? Потому что шиномонтаж и торговля шинами — это разные бизнес-процессы с разными ролями, разным UX и разными циклами релизов. Менеджер шиномонтажа не должен видеть маркетплейсы, менеджер продаж не должен видеть подъёмники.
BSCAN: маркировка товаров
bscan.tireshop.ru — интеграция с системой «Честный знак» (CRPT). FastAPI, работа с DataMatrix-кодами. Генерация, валидация, проверка статусов через API CRPT. Поиск данных по ИНН через DADATA. Интеграция с PIM-системой.
Маркировка шин стала обязательной. Каждая шина должна иметь DataMatrix-код, его нужно проверить при приёмке, обновить статус при продаже. BSCAN автоматизирует весь цикл. Без него менеджеры вводили бы коды вручную на сайте CRPT — по 30 секунд на шину.
PDF: генерация документов
pdf.tirebase.ru — микросервис с одним эндпоинтом. POST /generate-pdf/ принимает HTML, возвращает PDF. Работает как systemd-сервис на порту 8888.
Зачем отдельный сервис? Генерация PDF — ресурсоёмкая операция, которая использует headless-браузер. Если бы это жило в API или в CRM, пиковая нагрузка на генерацию счетов могла бы замедлить всё остальное. Отдельный сервис на отдельном порту — изолированная нагрузка. Любой другой сервис может вызвать его через HTTP. Накладные, коммерческие предложения, акты — всё генерируется здесь.
BOT: Telegram-бот для клиентов
bot.tiredrop — Telegram-бот для поиска шин. pyTelegramBotAPI + Flask + SQLAlchemy + PostgreSQL. Клиент пишет размер «205/50/16» — бот возвращает предложения с ценами. Фильтры по сезону, ценовому диапазону, грузовые шины. Inline-кнопки, пагинация.
Этот бот появился из конкретной потребности: мелкие клиенты не хотят звонить, не хотят заходить на сайт. Они хотят написать размер в Telegram и получить ответ. Бот ходит в API за данными и отдаёт их в удобном формате.
OCRM: CRM для б/у шин
ocrm.tirebase.ru — FastAPI, PostgreSQL, Redis, Alembic. REST API для управления объявлениями б/у шин на Avito. Отдельная подсистема, потому что б/у шины — специфический бизнес-процесс. Другие данные (фотографии реального товара, не каталожные), другие правила ценообразования (оценка состояния), другие требования к объявлениям.
Как всё это общается
Три типа коммуникации:
REST API (синхронный). Search вызывает API для получения товаров. Autoload вызывает ERP для получения прайсов. Любой сервис вызывает PDF для генерации документов. Простая HTTP-связь, которую легко отлаживать и мониторить.
RabbitMQ (асинхронный). Autoload ставит задачи на генерацию фидов. EVC получает доменные события. Это development decoupling — отправитель не знает и не заботится, кто обработает сообщение.
WebSocket (real-time). EVC пушит уведомления в браузеры менеджеров. Единственный stateful протокол в системе, и он изолирован в одном сервисе.
Почему FastAPI, а не Django
Ни в одном из сервисов нет Django. Причина простая: ни одному не нужна админка. Каждый сервис — чистый API или SPA. FastAPI даёт async из коробки, автоматическую документацию через Pydantic, минимальный overhead. Когда у тебя десяток сервисов, каждый лишний мегабайт RAM и миллисекунда latency множатся.
SQLAlchemy 2.0 async с asyncpg — потому что ORM нужен (шинные данные — это десятки связанных таблиц), но синхронный ORM в async-мире — это боль. Redis — для кеширования и для pub/sub. RabbitMQ — для надёжной доставки задач.
Почему Vue 3 на фронтенде
Search и TS — оба на Vue 3 + TypeScript + Tailwind CSS. Composition API с <script setup> — компактный, типизированный, легко переиспользуемый код. Pinia для состояния. Tailwind — потому что при двух фронтенд-приложениях писать кастомный CSS бессмысленно.
PWA на Search — потому что менеджеры в торговых точках работают с планшетов и телефонов. IndexedDB через Dexie — потому что нужен офлайн. Vite — потому что Webpack в 2025 году — это мазохизм.
Аутентификация: единая точка входа
Все сервисы используют гибридную схему ERP + JWT. Авторизация происходит через ERP API (1C/Custom). JWT-токен с TTL 12 часов. OAuth2 Password Flow. Один логин — доступ ко всем системам. EVC кеширует JWT-валидацию в Redis на 24 часа, чтобы не нагружать ERP при каждом WebSocket heartbeat.
Что бы я сделал по-другому
Общий API Gateway. Сейчас сервисы ходят друг к другу напрямую. При девяти сервисах это управляемо, но уже на грани. Traefik или HAProxy с единой точкой маршрутизации — следующий шаг.
Централизованное логирование раньше. Первые полгода я grep’ил логи по SSH на разных серверах. structlog + агрегация нужны с первого дня.
Контракты между сервисами. OpenAPI-спеки генерируются автоматически, но никто не проверяет обратную совместимость. Contract testing — то, что нужно внедрить.
Главный вывод
Каждый сервис в этой экосистеме появился потому, что конкретная проблема не решалась в рамках существующих компонентов. API отделился от Autoload, потому что нужна стабильность. EVC отделился, потому что WebSocket — stateful, а всё остальное — stateless. BSCAN отделился, потому что маркировка — регуляторное требование с собственным жизненным циклом.
Микросервисы — это не архитектурное решение, которое принимается один раз. Это процесс, в котором система постепенно декомпозируется по мере роста бизнес-требований. Каждый раз, когда я добавлял новый сервис, я сначала пробовал решить задачу в существующем. И только когда это создавало больше проблем, чем решало, выделял отдельный компонент.
Девять сервисов в продакшене — это девять деплоев, девять мониторингов, девять потенциальных точек отказа. Но это и девять независимых частей, каждая из которых может упасть, обновиться или масштабироваться, не затрагивая остальные. Для бизнеса с пятью точками, тремя маркетплейсами и десятком поставщиков — это работает. Хотя, честно говоря, иногда задаюсь вопросом, не стоило ли объединить OCRM и Autoload Engine — у них много общего кода по работе с Avito.