From 07047750be627f4409a9878de20674c826b24268 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Wed, 16 Sep 2026 23:18:58 +0300 Subject: [PATCH] =?UTF-8?q?feat(trade-in):=20=D1=80=D0=B5=D0=B3=D1=83?= =?UTF-8?q?=D0=BB=D1=8F=D1=80=D0=BD=D1=8B=D0=B9=20=D1=81=D0=B1=D0=BE=D1=80?= =?UTF-8?q?=20=D0=94=D0=BE=D0=BC=D0=9A=D0=BB=D0=B8=D0=BA=D0=B0=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D0=9C=D0=BE=D1=81=D0=BA=D0=B2=D0=B5=20=D0=B8=20=D0=BE?= =?UTF-8?q?=D0=B1=D0=BB=D0=B0=D1=81=D1=82=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Миграция 305 завела регулярный сбор 77/50 для avito/cian/yandex и прямо отложила ДомКлик: "у него нет wildcard-хендлера, а единственная возможная строка source='domclick_city_sweep' занята Екатеринбургом". Это вторая половина: код плюс строки расписания. ДомКлик даёт около 23% корпуса обоих регионов — без него свежий срез 77/50 теряет почти четверть массы, а вместе с ней часть домов, по которым вообще существуют аналоги. Wildcard заведён на ОБЕИХ сторонах, и это не дубль. Продуктовый _job_domclick_city_sweep перекрывает kit-native ради инъекции куки сессии Sber ID (#3264); kit-native тело ходит на BFF без кук. Поставь wildcard только в _default_kit_handlers — перекрытие осталось бы у ЕКБ и молча пропало бы у регионов, а московская строка упёрлась бы в QRATOR ровно так, как было до #3264. Новый тест test_domclick_region_wildcard_resolves_to_product_handler пинит именно это: все три source'а обязаны резолвиться на продуктовый job. Гео-скоуп задаёт ТОЛЬКО region_code — профиль региона (DOMCLICK_GEO_PROFILES) несёт address GUID, страницу прогрева QRATOR, bbox гарда и имя города; профили 77 и 50 заведены и проверены живыми запросами 12-15.09.2026 (#3533). city_id в default_params намеренно не задан: он vestigial, город штампуется из профиля. Неизвестный region_code роняет прогон до первого запроса, тихого отката на ЕКБ нет. Claude-Session: https://claude.ai/code/session_01NQb6WeJtagZwZnUsSjDizs --- .../backend/app/services/product_handlers.py | 6 ++ ...306_scrape_schedules_seed_domclick_msk.sql | 59 +++++++++++++++++++ .../test_scraper_kit_scheduler_parity.py | 32 +++++++++- .../scraper_kit/orchestration/scheduler.py | 1 + 4 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 tradein-mvp/backend/data/sql/306_scrape_schedules_seed_domclick_msk.sql diff --git a/tradein-mvp/backend/app/services/product_handlers.py b/tradein-mvp/backend/app/services/product_handlers.py index da13dfb2..42630b40 100644 --- a/tradein-mvp/backend/app/services/product_handlers.py +++ b/tradein-mvp/backend/app/services/product_handlers.py @@ -1008,6 +1008,12 @@ def build_product_handlers(ctx: SchedulerContext) -> dict[str, Handler]: ), # Override kit-native (#3264) — инъекция куки сессии, см. докстринг job'а выше. "domclick_city_sweep": Handler(_job_domclick_city_sweep, "domclick_city_sweep"), + # Вне ЕКБ каждый регион получает СВОЙ source ("domclick_city_sweep_moskva"), + # потому что scrape_schedules.source UNIQUE. Wildcard обязан стоять и здесь, + # а не только в _default_kit_handlers: kit-native тело ходит БЕЗ кук сессии, + # и московская строка молча резолвилась бы на версию без инъекции (#3264), + # то есть на ту, что упирается в QRATOR. Ключ продукта перекрывает kit-native. + "domclick_city_sweep_*": Handler(_job_domclick_city_sweep, "domclick_city_sweep_*"), "cadastral_geo_match": Handler(_job_cadastral_geo_match, "cadastral_geo_match"), "osm_poi_ekb_refresh": Handler(_job_osm_poi_ekb_refresh, "osm_poi_ekb_refresh"), "dtp_stat_refresh": Handler(_job_dtp_stat_refresh, "dtp_stat_refresh"), diff --git a/tradein-mvp/backend/data/sql/306_scrape_schedules_seed_domclick_msk.sql b/tradein-mvp/backend/data/sql/306_scrape_schedules_seed_domclick_msk.sql new file mode 100644 index 00000000..7b1e0618 --- /dev/null +++ b/tradein-mvp/backend/data/sql/306_scrape_schedules_seed_domclick_msk.sql @@ -0,0 +1,59 @@ +-- 306_scrape_schedules_seed_domclick_msk.sql +-- Регулярный сбор ДомКлика по Москве (77) и Московской области (50). +-- +-- Apply after: 305_scrape_schedules_seed_msk_city_sweeps.sql +-- +-- WHY: +-- Миграция 305 завела регулярный сбор 77/50 для avito / cian / yandex и прямо +-- отложила ДомКлик: "у него нет wildcard-хендлера, а единственная возможная +-- строка source='domclick_city_sweep' занята Екатеринбургом. Нужна правка кода". +-- Правка сделана в этом же PR — "domclick_city_sweep_*" добавлен в +-- _default_kit_handlers (scraper-kit) И в build_product_handlers (app-side +-- override с инъекцией куки Sber ID, #3264). Эта миграция — вторая половина. +-- +-- Цена вопроса: ДомКлик даёт ≈23% корпуса обоих регионов. Без него свежий срез +-- 77/50 теряет почти четверть массы, а вместе с ней — часть домов, по которым +-- вообще существуют аналоги. +-- +-- Гео-скоуп задаётся ТОЛЬКО region_code: профиль региона +-- (providers/domclick/serp.py DOMCLICK_GEO_PROFILES) несёт address GUID, +-- страницу прогрева QRATOR, bbox гарда и имя города. Профили 77 и 50 заведены и +-- проверены живыми запросами 12-15.09.2026 (#3533). city_id здесь НЕ задаётся +-- намеренно: он vestigial — run_domclick_city_sweep штампует город из профиля, +-- а не из city_id (pipeline.py, комментарий у save_listings). Неизвестный +-- region_code роняет прогон ДО первого запроса, тихого отката на ЕКБ нет. +-- +-- pages_per_anchor=100 — как у ЕКБ-строки: ДомКлик citywide, anchor-loop у него +-- отсутствует (fetch_around → NotImplementedError), "pages" здесь означает +-- глубину пагинации на комнатный бакет, а не число якорей. Москва отдаёт ≈23 690 +-- лотов вторички, то есть глубину надо брать полную, а не свипную. +-- +-- Окна 0-3 и 3-6 UTC: 305 занял 16-23, ЕКБ-домклик сидит в 3-6. Ставим Москву в +-- 0-3 (пусто), область — в 3-6 рядом с ЕКБ; прогоны всё равно сериализуются +-- планировщиком, окно лишь ограничивает старт. +-- +-- interval_days=3 — как у остальных строк 305: корпус живёт 14 суток +-- (LISTINGS_FRESH_DAYS), трёхсуточный такт даёт четырёхкратный запас на +-- пропуски из-за QRATOR-банов. +-- +-- ВКЛЮЧЕНЫ СРАЗУ (enabled=true) по той же причине, что и 305: регионы уходят +-- тестировщикам, корпус истекает. +-- +-- ИДЕМПОТЕНТНОСТЬ: ON CONFLICT (source) DO NOTHING — повторный прогон no-op и не +-- сбрасывает enabled, если строку потом выключили вручную. + +BEGIN; + +SET LOCAL lock_timeout = '5s'; + +INSERT INTO scrape_schedules (source, enabled, window_start_hour, window_end_hour, default_params) +VALUES + ('domclick_city_sweep_moskva', true, 0, 3, + '{"region_code": 77, "pages_per_anchor": 100, "interval_days": 3, + "request_delay_sec": 6}'::jsonb), + ('domclick_city_sweep_moskovskaya_oblast', true, 3, 6, + '{"region_code": 50, "pages_per_anchor": 100, "interval_days": 3, + "request_delay_sec": 6}'::jsonb) +ON CONFLICT (source) DO NOTHING; + +COMMIT; diff --git a/tradein-mvp/backend/tests/test_scraper_kit_scheduler_parity.py b/tradein-mvp/backend/tests/test_scraper_kit_scheduler_parity.py index b657bf60..475298c0 100644 --- a/tradein-mvp/backend/tests/test_scraper_kit_scheduler_parity.py +++ b/tradein-mvp/backend/tests/test_scraper_kit_scheduler_parity.py @@ -108,6 +108,7 @@ _KIT_NATIVE_SOURCES = { "cian_city_sweep_*", "cian_full_load", "domclick_city_sweep", + "domclick_city_sweep_*", } @@ -180,9 +181,11 @@ def test_real_build_product_handlers_covers_all_scheduled_sources() -> None: def test_kit_native_handler_set() -> None: - """Ровно 11 kit-native sweep-обработчиков зарегистрированы встроенно (8 + 3 oblast + """Ровно 12 kit-native sweep-обработчиков зарегистрированы встроенно (8 + 4 wildcard). - "*_city_sweep_*" wildcard, #179). + "*_city_sweep_*" — per-city/per-region source'ы вне ЕКБ: avito/cian/yandex завела + oblast B1 rollout (region 66, #179), domclick — регулярный сбор 77/50 (миграция 306), + у него суффикс несёт регион, а не город. """ registry = build_registry() assert set(registry) == _KIT_NATIVE_SOURCES @@ -190,6 +193,31 @@ def test_kit_native_handler_set() -> None: assert resolve_handler(src, registry).log_name == src +def test_domclick_region_wildcard_resolves_to_product_handler() -> None: + """'domclick_city_sweep_moskva' резолвится на ПРОДУКТОВЫЙ handler, не на kit-native. + + Развилка не косметическая: kit-native тело ходит на BFF без кук сессии Sber ID, и + московская строка, севшая на него, упёрлась бы в QRATOR ровно так, как было до #3264. + Продуктовый ключ обязан перекрывать kit-native и на wildcard, а не только на точном + 'domclick_city_sweep' — иначе перекрытие есть у ЕКБ и молча отсутствует у регионов. + """ + from app.services.product_handlers import ( + _job_domclick_city_sweep as product_job, + ) + from app.services.product_handlers import build_product_handlers + + # ctx=None безопасен — job'ы его не замыкают при сборке dict (см. тест выше). + registry = build_registry(build_product_handlers(ctx=None)) # type: ignore[arg-type] + for src in ( + "domclick_city_sweep", + "domclick_city_sweep_moskva", + "domclick_city_sweep_moskovskaya_oblast", + ): + handler = resolve_handler(src, registry) + assert handler is not None, src + assert handler.job is product_job, src + + def test_unknown_source_resolves_none() -> None: registry, _ = _build_recording_registry() assert resolve_handler("totally_unknown_source", registry) is None diff --git a/tradein-mvp/packages/scraper-kit/src/scraper_kit/orchestration/scheduler.py b/tradein-mvp/packages/scraper-kit/src/scraper_kit/orchestration/scheduler.py index 2611fa02..7923efbe 100644 --- a/tradein-mvp/packages/scraper-kit/src/scraper_kit/orchestration/scheduler.py +++ b/tradein-mvp/packages/scraper-kit/src/scraper_kit/orchestration/scheduler.py @@ -1296,6 +1296,7 @@ def _default_kit_handlers() -> dict[str, Handler]: "cian_city_sweep_*": Handler(_job_cian_city_sweep, "cian_city_sweep_*"), "cian_full_load": Handler(_job_cian_full_load, "cian_full_load"), "domclick_city_sweep": Handler(_job_domclick_city_sweep, "domclick_city_sweep"), + "domclick_city_sweep_*": Handler(_job_domclick_city_sweep, "domclick_city_sweep_*"), }