Версия: 1.0 Дата: 2026-03-20 Ветка: feature/PARSER-733-wb-enrich-task
Все задачи (search, enrich, recom, check) проходят единый жизненный цикл:
HTTP POST /tasks/{type}
→ TaskProducerService.create_*_task() — создание WBParserTask
→ TaskProducerService.publish_task() — INSERT в tasks + XADD в parser:tasks
→ ParserWorker._process_task() — consumer из Redis Stream
→ ParserWorker._parse_*_task() — выполнение
→ XADD parser:results — публикация WBParserResult
→ UPDATE tasks SET status = completed — финализация
| Статус | Описание |
|---|---|
pending | Задача создана, ожидает worker |
running | Worker взял задачу |
completed | Успешно завершена |
failed | Ошибка при выполнении |
class TaskType(str, Enum):
SEARCH = "search"
CHECK = "check"
ENRICH = "enrich"
RECOM = "recom"
class ParseMethod(str, Enum):
TOKEN = "token" # JSON API c WB-токеном (рекомендуемый)
HTML = "html" # Playwright DOM-парсинг через прокси
class SourceType(str, Enum):
KEYWORD = "keyword"
CATEGORY = "category"
NM_IDS = "nm_ids"
class ErrorType(str, Enum):
NETWORK = "network"
CAPTCHA = "captcha"
RATE_LIMIT = "rate_limit"
PARSING = "parsing"
VALIDATION = "validation"
UNKNOWN = "unknown"
Поиск товаров по ключевому слову или категории с отслеживанием позиций, сохранением товаров и быстрой выдачей целевых артикулов (QuickData).
| Параметр | Keyword-режим | Category-режим |
|---|---|---|
| Поле запроса | keyword | category_id |
| SourceType | keyword | category |
| WB API | Search API (search.wb.ru) | Catalog API (catalog.wb.ru/{shard}/v4/catalog) |
| URL (html) | /catalog/0/search.aspx?search={kw} | /catalog/{category_id}?page={n} |
| Token-метод | JSON API с x_wbaas_token | Catalog API (без токена) |
| Поле позиции | keyword в search_positions | category:{id} в search_positions |
| Default pages | 3 | 3 |
POST /tasks/searchRequest Body — CreateSearchTaskRequest:
| Поле | Тип | Обязательное | Default | Ограничения | Описание |
|---|---|---|---|---|---|
keyword | str | null | нет* | null | — | Ключевое слово |
category_id | str | null | нет* | null | — | ID категории WB |
pages | int | нет | 3 | 1..30 | Количество страниц |
parse_method | ParseMethod | нет | token | token | html | Метод парсинга |
target_nm_id | int | null | нет | null | — | Один целевой артикул |
target_nm_ids | list[int] | null | нет | null | 1..50 элементов | Список целевых артикулов |
top_n | int | нет | 50 | 1..2000 | Топ-N товаров в QuickData |
* Обязательно указать одно из
keywordилиcategory_id. Указание обоих →400.
Пример (keyword):
{
"keyword": "термокружка",
"pages": 3,
"parse_method": "token",
"target_nm_id": 124203940,
"top_n": 50
}
Пример (category):
{
"category_id": "9468",
"pages": 5,
"parse_method": "token",
"target_nm_ids": [124203940, 94608652],
"top_n": 100
}
Response — TaskResponse:
{
"task_id": "550e8400-e29b-41d4-a716-446655440000",
"stream_entry_id": "1711000000000-0",
"db_task_id": 123,
"status": "queued"
}
parser:tasks)WBParserTask (search + keyword):
{
"task_id": "550e8400-e29b-41d4-a716-446655440000",
"db_task_id": 123,
"task_type": "search",
"parse_method": "token",
"request": {
"source_type": "keyword",
"source_value": "термокружка",
"nm_ids": null,
"pages": 3,
"target": {
"target_nm_id": 124203940,
"target_nm_ids": null,
"top_n": 50
}
},
"created_at": "2026-03-20T10:00:00Z"
}
WBParserTask (search + category):
{
"task_id": "660f9500-f30c-52e5-b827-557766551111",
"db_task_id": 124,
"task_type": "search",
"parse_method": "token",
"request": {
"source_type": "category",
"source_value": "9468",
"nm_ids": null,
"pages": 5,
"target": {
"target_nm_id": null,
"target_nm_ids": [124203940, 94608652],
"top_n": 100
}
},
"created_at": "2026-03-20T10:00:00Z"
}
parser:results)WBParserResult (search):
{
"task_id": "550e8400-e29b-41d4-a716-446655440000",
"task_type": "search",
"parse_method": "token",
"duration_seconds": 12.5,
"request": { "..." : "..." },
"summary": {
"items_found": 250,
"items_saved": 250,
"items_failed": 0,
"pages_parsed": 3
},
"data": {
"target_results": [
{
"nm_id": 124203940,
"found_in_listing": true,
"exists": null,
"position": 5,
"page": 1,
"position_on_page": 5,
"is_promoted": false,
"price": 643.0,
"rating": 4.8,
"feedbacks": 1200,
"total_stock": 540,
"error": null
}
],
"top_products": [
{
"nm_id": 111222333,
"position": 1,
"page": 1,
"position_on_page": 1,
"is_promoted": false,
"price": 590.0,
"rating": 4.9,
"feedbacks": 3200,
"total_stock": 1200
}
]
},
"details_in_db": true,
"db_pointers": { "task_db_id": 123 },
"error": null
}
Catalog API: GET https://catalog.wb.ru/{shard}/v4/catalog
| Параметр | Значение |
|---|---|
appType | "1" |
curr | "rub" |
dest | "-1257786" |
spp | "30" |
cat | "{category_id}" |
page | "1".. |
Shard-маппинг:
https://static-basket-01.wbbasket.ru/vol0/data/main-menu-ru-ru-v3.jsonWBApiClient._shard_map: Dict[int, str])shard = "blackhole" → навигационный контейнер, НЕ парсабельныйФормат элемента в выдаче (keyword и category одинаковый):
{
"nm_id": int,
"position": int, # (page-1)*100 + i
"page": int,
"position_on_page": int, # 1..100
"keyword": str, # или "category_id": str
"is_promoted": bool,
"panel_promo_id": int | None,
}
nm_idkeyword = "{ключевое слово}"keyword = "category:{category_id}"Полное обогащение данных товаров: search + Cards API v4 + basket CDN (card.json, price-history.json) + Feedbacks API + 3D-фото проверка + SPP.
| Контракт | Описание | Модель |
|---|---|---|
| HTTP | Создание задачи через REST API | CreateEnrichTaskRequest → TaskResponse |
| Redis Task | Входящее сообщение для worker | WBParserTask (task_type=enrich) |
| Redis Result | Результат выполнения | WBParserResult (data=null, summary) |
POST /tasks/enrichRequest Body — CreateEnrichTaskRequest:
| Поле | Тип | Обязательное | Default | Ограничения | Описание |
|---|---|---|---|---|---|
keyword | str | null | нет* | null | — | Ключевое слово |
category_id | str | null | нет* | null | — | ID категории WB |
pages | int | нет | 5 | 1..30 | Количество страниц |
parse_method | ParseMethod | нет | token | token | html | Метод парсинга |
* Обязательно указать одно из
keywordилиcategory_id. Указание обоих →400.
Отличия от Search: нет
target_nm_id,target_nm_ids,top_n. Default pages = 5 (вместо 3).
Пример (keyword):
{
"keyword": "термокружка",
"pages": 5,
"parse_method": "token"
}
Пример (category):
{
"category_id": "9468",
"pages": 3,
"parse_method": "token"
}
Response — TaskResponse:
{
"task_id": "a1b2c3d4-e5f6-47g8-h9i0-j1k2l3m4n5o6",
"stream_entry_id": "1711000000000-0",
"db_task_id": 456,
"status": "queued"
}
Коды ответа:
| Код | Условие |
|---|---|
| 200 | Задача создана |
| 400 | Не указан keyword/category_id или оба сразу |
| 500 | Внутренняя ошибка (Redis/DB) |
parser:tasks)WBParserTask (enrich + keyword):
{
"task_id": "a1b2c3d4-e5f6-47g8-h9i0-j1k2l3m4n5o6",
"db_task_id": 456,
"task_type": "enrich",
"parse_method": "token",
"request": {
"source_type": "keyword",
"source_value": "термокружка",
"nm_ids": null,
"pages": 5,
"target": null
},
"created_at": "2026-03-20T12:00:00Z"
}
WBParserTask (enrich + category):
{
"task_id": "b2c3d4e5-f6a7-48h9-i0j1-k2l3m4n5o6p7",
"db_task_id": 457,
"task_type": "enrich",
"parse_method": "token",
"request": {
"source_type": "category",
"source_value": "9468",
"nm_ids": null,
"pages": 3,
"target": null
},
"created_at": "2026-03-20T12:05:00Z"
}
Отличия от Search-задачи:
task_type = "enrich" (вместо "search")target = null (enrich обрабатывает ВСЕ найденные товары)nm_ids = null (enrich не работает по списку артикулов)parser:results)WBParserResult (enrich, success):
{
"task_id": "a1b2c3d4-e5f6-47g8-h9i0-j1k2l3m4n5o6",
"task_type": "enrich",
"parse_method": "token",
"duration_seconds": 205.2,
"request": {
"source_type": "keyword",
"source_value": "термокружка",
"nm_ids": null,
"pages": 5,
"target": null
},
"summary": {
"items_found": 245,
"items_saved": 245,
"items_failed": 0,
"pages_parsed": 5
},
"data": null,
"details_in_db": true,
"db_pointers": { "task_db_id": 456 },
"error": null
}
WBParserResult (enrich, error):
{
"task_id": "a1b2c3d4-e5f6-47g8-h9i0-j1k2l3m4n5o6",
"task_type": "enrich",
"parse_method": "token",
"duration_seconds": 8.5,
"request": { "...": "..." },
"summary": {
"items_found": 0,
"items_saved": 0,
"items_failed": 0,
"pages_parsed": 0
},
"data": null,
"details_in_db": true,
"db_pointers": { "task_db_id": 456 },
"error": {
"type": "network",
"message": "Connection timeout after 30s"
}
}
Отличия от Search-результата:
data = null всегда (enrich не формирует QuickData/TargetResults)summary.items_saved = кол-во товаров, обогащённых в БД (маппинг из items_enriched)summary.items_failed = 0 (per-product ошибки не трекаются в enrich)Phase 1: Search (parse_with_positions)
├── Keyword → JSON API (search.wb.ru) → nm_ids + positions
└── Category → Catalog API (catalog.wb.ru) → nm_ids + positions
↓
Phase 2: Cards API v4 (card.wb.ru/cards/v4/detail)
→ batch по 100 nm_ids → базовые данные + imt_id
↓
Phase 3: Параллельный сбор (asyncio.gather)
├── card.json (basket CDN, concurrency=10) → composition, description, vendor_code, ...
├── price-history (basket CDN, concurrency=10) → [{dt, price}, ...]
├── feedbacks (feedbacks1.wb.ru, concurrency=10) → rating_distribution, feedbacks_with_*
├── 3D photo (basket CDN HEAD, concurrency=20) → has_3d_photo
└── SPP (1 запрос) → spp value
↓
Phase 4: Merge + DB persist
├── UPDATE products (card.json + feedbacks + 3D)
├── Backfill price_history для новых товаров
└── COMMIT
enrich_products(){
"items_found": int, # Товаров найдено в search
"items_enriched": int, # Товаров обновлено в БД
"items_backfilled": int, # Товаров с подгруженной историей цен
"feedbacks_collected": int, # Уникальных root_id с собранными отзывами
"has_3d_photo": int, # Товаров с 3D-фото
"spp": float | None, # Значение SPP
}
| Поле БД | Тип | Источник | Описание |
|---|---|---|---|
composition | TEXT | card.json | Состав товара |
feedbacks_with_photo | INTEGER | Feedbacks API | Кол-во отзывов с фото |
feedbacks_with_text | INTEGER | Feedbacks API | Кол-во отзывов с текстом |
feedbacks_with_video | INTEGER | Feedbacks API | Кол-во отзывов с видео |
rating_distribution | JSONB | Feedbacks API | {1: N, 2: N, ..., 5: N} |
matching_size | JSONB | Feedbacks API | {bigger: %, ok: %, smaller: %} |
nm_rating_distribution | JSONB | Feedbacks API | Рейтинг по nm_id в склейке |
first_feedback_date | TIMESTAMP | Feedbacks API | Дата старейшего отзыва |
avg_recent_rating | DECIMAL(3,2) | Feedbacks API | Средний рейтинг последних 50 отзывов |
negative_feedbacks_pct | DECIMAL(5,1) | Feedbacks API | % отзывов с оценкой 1-3 |
| Аспект | Search | Enrich |
|---|---|---|
| Endpoint | POST /tasks/search | POST /tasks/enrich |
| Default pages | 3 | 5 |
| Target config | Да (target_nm_id, top_n) | Нет |
| Cards API v4 batch | Да (для деталей) | Да (для imt_id) |
| card.json CDN | Нет | Да |
| Price history | Нет (только текущая) | Да (backfill из CDN) |
| Feedbacks API | Нет | Да |
| 3D photo check | Нет | Да |
| SPP | Нет | Да |
| QuickData в result | Да (data.target_results) | Нет (data = null) |
| Время выполнения | ~10-30 сек | ~150-250 сек |
Enrich не возвращает данные товаров в Redis — только summary-статистику.
Все данные сохраняются in-place в PostgreSQL и доступны через связку
search_positions → products по keyword или category_id.
Enrich при выполнении:
search_positions с текущим recorded_atproducts in-place (enrich-поля, цены, отзывы)Для получения результатов последнего enrich — берём позиции с максимальным recorded_at
по нужному keyword/category и JOIN'им с products.
SELECT
p.nm_id,
p.name,
p.brand,
p.seller_name,
p.price_current,
p.price_basic,
p.discount_percent,
p.rating,
p.feedbacks,
p.total_stock,
-- enrich-поля
p.composition,
p.feedbacks_with_photo,
p.feedbacks_with_text,
p.feedbacks_with_video,
p.rating_distribution,
p.matching_size,
p.first_feedback_date,
p.avg_recent_rating,
p.negative_feedbacks_pct,
p.has_3d_photo,
p.has_video,
p.photo_count,
p.description,
p.characteristics,
p.colors,
p.sizes,
p.country,
p.gender,
-- позиция из последнего сбора
sp.position,
sp.page_number,
sp.position_on_page,
sp.is_promoted,
sp.recorded_at
FROM search_positions sp
JOIN products p ON p.id = sp.product_id
WHERE sp.keyword = :keyword -- например 'термокружка'
AND sp.recorded_at = (
SELECT MAX(s2.recorded_at)
FROM search_positions s2
WHERE s2.keyword = sp.keyword
)
ORDER BY sp.position;
-- Для категорий keyword хранится как 'category:{id}'
SELECT p.*, sp.position, sp.page_number, sp.is_promoted, sp.recorded_at
FROM search_positions sp
JOIN products p ON p.id = sp.product_id
WHERE sp.keyword = 'category:9468'
AND sp.recorded_at = (
SELECT MAX(s2.recorded_at)
FROM search_positions s2
WHERE s2.keyword = sp.keyword
)
ORDER BY sp.position;
SELECT ph.price_current, ph.price_basic, ph.discount_percent, ph.spp, ph.recorded_at
FROM price_history ph
JOIN products p ON p.id = ph.product_id
WHERE p.nm_id = :nm_id
ORDER BY ph.recorded_at DESC;
SELECT
keyword,
recorded_at,
COUNT(*) AS products_count
FROM search_positions
WHERE keyword = :keyword
GROUP BY keyword, recorded_at
ORDER BY recorded_at DESC;
SELECT p.*, sp.position, sp.page_number, sp.is_promoted
FROM search_positions sp
JOIN products p ON p.id = sp.product_id
WHERE sp.keyword = :keyword
AND sp.recorded_at = :target_recorded_at -- timestamp конкретного прогона
ORDER BY sp.position;
products содержит актуальное состояние, а не снэпшот на момент enrich.
Если после enrich по "термокружка" запустить enrich по "кружка для кофе" —
товары, попавшие в оба запроса, будут содержать данные последнего обновления.*_history-таблицах с recorded_at и доступны по product_id.search_positions.keyword для категорий хранится как "category:{id}" —
это строковое соглашение, не отдельное поле source_type.Анализ рекомендательных полок WB: «Показывает ли WB мой товар на страницах конкурентов?»
| Режим | Условие | Описание |
|---|---|---|
monitor | competitor_nm_ids задан | Прямая проверка: ищем target в рекомендациях каждого конкурента |
cross_reference | keyword задан | Конкуренты из поисковой выдачи + свои рекомендации + пересечение |
POST /tasks/recomRequest Body — CreateRecomTaskRequest:
| Поле | Тип | Обязательное | Default | Ограничения | Описание |
|---|---|---|---|---|---|
target_nm_id | int | да | — | — | Мой артикул — ищем его в чужих рекомендациях |
keyword | str | null | нет* | null | — | Ключевое слово (→ cross_reference) |
competitor_nm_ids | list[int] | null | нет* | null | 1..50 элементов | Артикулы конкурентов (→ monitor) |
top_n | int | нет | 10 | 1..100 | Лимит рекомендаций для сохранения |
* Обязательно указать хотя бы одно из
keywordилиcompetitor_nm_ids. Можно указать оба — тогдаkeywordопределяет режим (cross_reference), аcompetitor_nm_idsигнорируются (конкуренты берутся из выдачи).
Пример (monitor):
{
"target_nm_id": 124203940,
"competitor_nm_ids": [94608652, 376929502],
"top_n": 10
}
Пример (cross_reference):
{
"target_nm_id": 124203940,
"keyword": "термокружка",
"top_n": 10
}
Response — TaskResponse:
{
"task_id": "d4e5f6a7-b8c9-50j1-k2l3-m4n5o6p7q8r9",
"stream_entry_id": "1711000010000-0",
"db_task_id": 789,
"status": "queued"
}
parser:tasks)WBParserTask (recom, monitor):
{
"task_id": "d4e5f6a7-b8c9-50j1-k2l3-m4n5o6p7q8r9",
"db_task_id": 789,
"task_type": "recom",
"parse_method": "token",
"request": {
"source_type": "nm_ids",
"source_value": null,
"nm_ids": [124203940, 94608652, 376929502],
"pages": 1,
"target": {
"target_nm_id": 124203940,
"target_nm_ids": [94608652, 376929502],
"top_n": 10
}
},
"created_at": "2026-03-20T14:00:00Z"
}
nm_ids[0]= target, остальные — конкуренты.
WBParserTask (recom, cross_reference):
{
"task_id": "e5f6a7b8-c9d0-51k2-l3m4-n5o6p7q8r9s0",
"db_task_id": 790,
"task_type": "recom",
"parse_method": "token",
"request": {
"source_type": "keyword",
"source_value": "термокружка",
"nm_ids": [124203940],
"pages": 1,
"target": {
"target_nm_id": 124203940,
"target_nm_ids": null,
"top_n": 10
}
},
"created_at": "2026-03-20T14:00:00Z"
}
parser:results)WBParserResult (recom):
{
"task_id": "d4e5f6a7-b8c9-50j1-k2l3-m4n5o6p7q8r9",
"task_type": "recom",
"parse_method": "token",
"duration_seconds": 45.3,
"request": { "...": "..." },
"summary": {
"items_found": 2,
"items_saved": 47,
"items_failed": 1,
"pages_parsed": 1
},
"data": null,
"details_in_db": true,
"db_pointers": { "task_db_id": 789 },
"error": null
}
Маппинг summary для recom:
| Поле | Значение |
|---|---|
items_found | Кол-во конкурентов, у которых target НАЙДЕН в рекомендациях |
items_saved | Кол-во записей, сохранённых в product_recommendations |
items_failed | Кол-во конкурентов, у которых target НЕ найден |
pages_parsed | Всегда 1 |
Для каждого competitor_nm_id:
├── GET recom.wb.ru/recom/ru/common/v5/search?query=похожие+{comp_id}
├── Парсинг списка рекомендованных товаров (_parse_product)
├── Поиск target_nm_id в рекомендациях
│ ├── Найден → we_appear_in[comp_id] = {position: N}
│ └── Не найден → we_dont_appear_in.append(comp_id)
└── INSERT в product_recommendations (top_n записей)
Шаг 1: Search (parse_with_positions, keyword, 3 страницы)
→ Получаем search_positions: {nm_id: position}
→ competitor_nm_ids = top_n из выдачи (без target)
Шаг 2: Свои рекомендации
├── GET recom.wb.ru?query=похожие+{target_nm_id}
├── Пересечение с search_positions → from_search_top
├── overlap_percent = len(пересечение) / len(рекомендации) * 100
└── INSERT в product_recommendations (is_in_search_top, search_position)
Шаг 3: Обратная проверка (как в monitor)
└── Для каждого конкурента → ищем target в его рекомендациях
Endpoint: GET https://recom.wb.ru/recom/ru/common/v5/search
| Параметр | Значение |
|---|---|
appType | "1" |
curr | "rub" |
dest | "-446116" |
page | "1" |
query | "похожие {nm_id}" |
resultset | "catalog" |
spp | "30" |
suppressSpellcheck | "false" |
uclusters | "0" |
Response: data.products[] — массив товаров в формате Cards API v4.
Каждый товар парсится через _parse_product() и получает дополнительное поле:
{
"recom_position": int, # 1-based позиция в рекомендациях
# + все стандартные поля Cards API v4 (nm_id, name, price, rating, ...)
}
analyze_recommendations(){
"mode": "cross_reference" | "monitor",
"target_nm_id": 124203940,
"we_appear_in": {
94608652: {"position": 3, "search_position": 5},
376929502: {"position": 1}
},
"we_dont_appear_in": [999999],
"saved": 47,
# Только cross_reference:
"our_recommendations": {
"total": 12,
"from_search_top": [
{"nm_id": 123, "position_in_recom": 2, "search_position": 8}
],
"overlap_percent": 25.0
}
}
product_recommendations (миграция 004)CREATE TABLE product_recommendations (
id BIGSERIAL PRIMARY KEY,
mode VARCHAR(20) NOT NULL, -- 'monitor', 'cross_reference'
keyword VARCHAR(255), -- keyword (только cross_reference)
source_nm_id BIGINT NOT NULL, -- чьи рекомендации смотрим
recommended_nm_id BIGINT NOT NULL, -- кого рекомендуют
position INTEGER, -- позиция в рекомендациях (1-based)
is_in_search_top BOOLEAN DEFAULT FALSE, -- входит в top-N по keyword
search_position INTEGER, -- позиция в поиске
checked_at TIMESTAMP DEFAULT NOW() AT TIME ZONE 'Europe/Moscow',
created_at TIMESTAMP DEFAULT NOW() AT TIME ZONE 'Europe/Moscow'
);
Записи создаются двумя способами:
source_nm_id = target, is_in_search_top и search_position заполняютсяsource_nm_id = competitor, is_in_search_top и search_position = NULLПолучение дерева категорий WB для выбора category_id при создании search/enrich задач.
GET /categories/| Параметр | Тип | Default | Описание |
|---|---|---|---|
flat | bool | false | true = плоский список, false = дерево |
Response (flat=false) — Иерархическое дерево:
{
"tree": [
{
"id": 1000,
"name": "Женщинам",
"url": "zhenshchinam",
"shard": "blackhole",
"childs": [
{
"id": 8126,
"name": "Платья",
"url": "zhenshchinam/odezhda/platya",
"shard": "wc2",
"childs": []
}
]
}
]
}
Категории с
shard = "blackhole"— навигационные контейнеры. Только категории с конкретным shard (не blackhole) можно использовать в search/enrich.
Response (flat=true) — Плоский список (без blackhole):
{
"count": 1234,
"categories": [
{
"id": 8126,
"name": "Платья",
"parent_id": 1000,
"parent_name": "Женщинам",
"shard": "wc2",
"url": "zhenshchinam/odezhda/platya"
},
{
"id": 9468,
"name": "Термосы и термокружки",
"parent_id": 5001,
"parent_name": "Дом",
"shard": "c5",
"url": "dom/posuda/termosy-i-termokruzhki"
}
]
}
URL: https://static-basket-01.wbbasket.ru/vol0/data/main-menu-ru-ru-v3.json
Кэширование: в памяти WBApiClient (один раз при первом вызове)
| Поле | Тип | Описание |
|---|---|---|
task_id | str | UUID задачи |
stream_entry_id | str | ID записи в Redis Stream |
db_task_id | int | ID в таблице tasks (PostgreSQL) |
status | str | Всегда "queued" при создании |
GET /tasks/{task_id}/status| Поле | Тип | Описание |
|---|---|---|
db_id | int | ID в таблице tasks |
task_type | str | search | check | enrich | recom |
status | str | pending | running | completed | failed |
items_found | int | null | Товаров найдено |
items_processed | int | null | Товаров обработано |
error_message | str | null | Текст ошибки (max 500 символов) |
started_at | str | null | ISO8601 |
completed_at | str | null | ISO8601 |
created_at | str | null | ISO8601 |
class WBParserTask(BaseModel):
task_id: UUID # auto: uuid4()
db_task_id: int | None # заполняется при publish_task
task_type: TaskType # search | check | enrich | recom
parse_method: ParseMethod # token | html
request: RequestStructure # вложенная структура
created_at: datetime # auto: utcnow()
class RequestStructure(BaseModel):
source_type: SourceType # keyword | category | nm_ids
source_value: str | None # значение keyword или category_id
nm_ids: list[int] | None # список артикулов (check, recom)
pages: int = 3 # 1..30
target: TargetConfiguration | None
class TargetConfiguration(BaseModel):
target_nm_id: int | None # один целевой артикул
target_nm_ids: list[int] | None # 1..50 целевых артикулов
top_n: int | None = 50 # 1..2000
class WBParserResult(BaseModel):
task_id: UUID
task_type: TaskType
parse_method: ParseMethod
duration_seconds: float # >= 0
request: RequestStructure # эхо запроса
summary: SummaryData
data: QuickData | None # только для search
details_in_db: bool = True
db_pointers: DBPointers | None
error: ErrorStructure | None
class SummaryData(BaseModel):
items_found: int # >= 0
items_saved: int | None # >= 0
items_failed: int = 0 # >= 0
pages_parsed: int | None # >= 0
class QuickData(BaseModel): # только search
target_results: list[TargetResult] | None # max 50
top_products: list[TopProduct] | None # max 2000
class ErrorStructure(BaseModel):
type: ErrorType
message: str
class DBPointers(BaseModel):
task_db_id: int | None
| Метод | Путь | Описание |
|---|---|---|
| GET | /tasks/{task_id}/status | Статус задачи по UUID |
| GET | /tasks/pending | Список зависших задач |
| GET | /tasks/streams/health | Здоровье Redis Streams |
| GET | /health | K8s liveness probe |
| GET | /readiness | K8s readiness probe |
| GET | /worker/stats | Метрики worker'а |
| Stream | Направление | Consumer Group | Описание |
|---|---|---|---|
parser:tasks | → Worker | parser-workers | Входящие задачи |
parser:results | → Backend | bidder-backend | Результаты парсинга |
| Параметр | Значение | Описание |
|---|---|---|
batch_size | 10 | Задач за один XREADGROUP |
block_timeout | 5000 ms | Блокировка при отсутствии задач |
max_entries | 10000 | Авто-очистка (XTRIM) |
idle_timeout | 60 с | Перехват зависших задач |
Redis Stream entry = { "data": "<JSON-сериализация WBParserTask или WBParserResult>" }
parser.tasks — очередь задач
parser.products — товары WB (+ enrich-поля из 003)
parser.price_history — история цен
parser.stock_history — история остатков
parser.rating_history — история рейтингов
parser.search_positions — позиции в поиске/категории
parser.product_recommendations — рекомендательные полки (004)
parser.categories — категории WB
{
"task_uuid": "UUID строка",
"task_type_original": "search | enrich | recom | check",
"parse_method": "token | html",
"request": {
"source_type": "keyword | category | nm_ids",
"source_value": "значение или null",
"nm_ids": [int] | null,
"pages": int,
"target": { ... } | null
}
}
Поиск задачи по UUID: WHERE metadata->>'task_uuid' = :uuid