RUEN
Обсудить проект

Инженерный кейс

Данные карточки товара по артикулу

Сервис отдаёт название, характеристики, цену и остаток товара маркетплейса по одному артикулу — с ключами доступа, тарифами и суточными квотами. Поверх — слежение за ценами конкурентов.

артикулов за один запрос
100
канала с разной свежестью
2
попытки со сменой адреса
4

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

Карточка разрезана надвое — по доступности источника, а не для красоты

Неизменная часть — название, бренд, описание, характеристики — лежит в открытой раздаче и почти не режется. Цена, остаток и рейтинг приходят из защищённого канала, которому нужен чистый адрес. У каждой части своя свежесть: сутки против пяти минут.

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

Адрес неизменной части вычисляется из артикула, а не ищется

Хост и путь считаются арифметикой прямо из номера товара — без предварительного запроса к площадке.

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

Ответ всегда говорит, что случилось с каждой частью

Вместе с данными приходит пометка по каждому каналу: свежие данные, кэш, не найдено, отказ, пропущено.

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

Пул адресов бросает «исчерпан» вместо пустого результата

Точки выхода раздаются по кругу, заблокированная уходит в остывание. Когда свободных нет, поднимается ошибка, а не возвращается пустота.

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

Остывание — это отметка времени, а не счётчик неудач

У адреса хранится момент, до которого его не трогать; доступность — сравнение с текущим временем.

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

Кэш общий на всех клиентов

Ключ кэша — площадка и артикул, без клиента. Запрос на сотню артикулов сначала берёт из кэша всё живое и идёт наружу только за недостающим.

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

Минутный лимит в памяти, суточная квота на диске

Ограничение частоты живёт скользящим окном в памяти. Суточная квота — счётчик в базе, обновляемый атомарно.

Асимметрия намеренная. Потеря минутного лимита при перезапуске не стоит ничего, а запись в базу на каждый запрос — лишняя работа на горячем пути. Суточная квота — это биллинг: её сброс при перезапуске означает бесплатные запросы столько раз, сколько было перезапусков.

Слежение разделяет «кто что отслеживает» и «что было с товаром»

Одна таблица — подписки клиента, другая — снимки цены по товару, без привязки к клиенту. Оценка качества карточки считается в момент снимка и туда же пишется.

История цены объективна, она не «чья-то». Хранить её по клиенту — значит для двух клиентов, следящих за одним конкурентом, держать две истории и делать два запроса. А оценка пишется сразу потому, что иначе график «как менялось качество карточки» потребовал бы заново скачать товар, которого может уже не быть в продаже.

Рекомендация по цене сравнивает с конкурентами, а не с рынком целиком

Свои товары исключаются из базы сравнения; средняя и границы считаются только по чужим.

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

Python · FastAPI · SQLite · systemd