From 1513bad538efdb8ff0cc845aff478c4000f2d719 Mon Sep 17 00:00:00 2001 From: viktot14-ai Date: Wed, 9 Sep 2026 23:25:06 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=BE=D0=B3=D0=BB=D0=B0=D1=81=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D1=80=D0=B0=D0=B4=D0=B8=D1=83?= =?UTF-8?q?=D1=81=D0=B0:=20summary=20=3D=20max=5Fdistance=20=C3=97=20?= =?UTF-8?q?=D1=81=D1=80=D0=B5=D0=B4=D0=BD=D0=B8=D0=B9=20=D0=BF=D1=81=D0=B8?= =?UTF-8?q?=D1=85=D0=BE=D1=82=D0=B8=D0=BF-=D0=BC=D0=BD=D0=BE=D0=B6=D0=B8?= =?UTF-8?q?=D1=82=D0=B5=D0=BB=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - search_engine: max_distance_km передаётся в analyzer (case_data). - rules_analysis: search_radius = max_distance × _psychotype_radius_multiplier (среднее по полосам 0-500/0.5-1.5/1.5-2.5/2.5+). Убран двойной учёт age/season: score_zone.distance_multiplier их уже содержит, а max_distance их тоже учёл — раньше radius = multiplier×2.0 (хардкод) давал 1.6 км при радиусе 2.4 км. - UI: подсказка у «×0.8» — коэффициент скоринга, не радиуса. - Тест: согласованность summary↔формула. --- backend/tests/test_zone_reason.py | 19 +++++++++++++++++- .../src/pages/analysis/AnalysisResult.jsx | 5 ++++- services/rules_analysis.py | 20 +++++++++++++++++-- services/search_engine.py | 1 + 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/backend/tests/test_zone_reason.py b/backend/tests/test_zone_reason.py index 77c4f47..31f3c1d 100644 --- a/backend/tests/test_zone_reason.py +++ b/backend/tests/test_zone_reason.py @@ -69,4 +69,21 @@ class TestZoneReason: r1 = _build_zone_reason(zone, case) r2 = _build_zone_reason(dict(zone), dict(case)) assert r1 == r2 - assert 'совпадает с направлением' in r1 \ No newline at end of file + assert 'совпадает с направлением' in r1 +class TestRadiusConsistency: + def test_summary_radius_matches_max_times_multiplier(self): + """Радиус в summary = max_distance × психотип-множитель (согласован с хедером).""" + import asyncio + from services.rules_analysis import rules_analyze + case = { + 'age': 9, 'gender': 'м', 'season': 'лето', 'elapsed_hours': 4.0, + 'profiles': ['гармоничный'], 'psychotype': 'harmonic', + 'psychotype_modifiers': {'zone_0_500': 1.0, 'zone_500_1500': 1.0, + 'zone_1500_2500': 1.1, 'zone_2500plus': 1.0}, + 'max_distance_km': 2.4, + 'lat': 54.5, 'lon': 28.5, + } + result = asyncio.run(rules_analyze(case)) + # Средний психотип-множитель полос (1.0+1.0+1.1+1.0)/4 = 1.025 → 2.4×1.025=2.46→2.5 + assert result.search_radius_km == 2.5, result.search_radius_km + assert f'{result.search_radius_km} км' in result.summary diff --git a/frontend/src/pages/analysis/AnalysisResult.jsx b/frontend/src/pages/analysis/AnalysisResult.jsx index 3b28ea1..bf91591 100644 --- a/frontend/src/pages/analysis/AnalysisResult.jsx +++ b/frontend/src/pages/analysis/AnalysisResult.jsx @@ -199,7 +199,10 @@ const AnalysisResult = () => {
{analysisData?.max_distance_km} км {analysisData?.distance_multiplier !== 1 && ( - ×{analysisData?.distance_multiplier} + ×{analysisData?.distance_multiplier} )}
diff --git a/services/rules_analysis.py b/services/rules_analysis.py index efc20d1..ebb695d 100644 --- a/services/rules_analysis.py +++ b/services/rules_analysis.py @@ -178,6 +178,17 @@ def _psychotype_phrase(case_data: dict, distance_km: float) -> str | None: return None +def _psychotype_radius_multiplier(case_data: dict) -> float: + """Средний множитель психотипа по полосам дистанции (без двойного учёта + возраст/сезон — они уже в max_distance).""" + mods = case_data.get('psychotype_modifiers') + if not mods: + return 1.0 + vals = [mods.get(k, 1.0) for k in ('zone_0_500', 'zone_500_1500', + 'zone_1500_2500', 'zone_2500plus')] + return sum(vals) / len(vals) + + def _build_zone_reason(zone: dict, case_data: dict) -> str: """Словесное пояснение приоритета зоны — по тем же факторам, что считает WeightedScorer (детерминированно, без выдумок): рельеф, вода, дороги, НП, @@ -238,7 +249,8 @@ async def rules_analyze(case_data: dict[str, Any]) -> AnalysisResult: urgency = _age_urgency(age, profiles) primary_zones: list[PrimaryZone] = [] - search_radius_km = 5.0 + base_km = float(case_data.get('max_distance_km') or 5.0) + search_radius_km = round(base_km * 1.0, 1) if case_data.get('lat') and case_data.get('lon'): try: @@ -273,7 +285,11 @@ async def rules_analyze(case_data: dict[str, Any]) -> AnalysisResult: distance=zone['distance_km'], reason=_build_zone_reason(zone, case_data), )) - search_radius_km = scorer.distance_multiplier * 2.0 + # Радиус = max_distance (НормС×коэффициенты) × средний психотип-множитель. + # score_zone.distance_multiplier НЕ используется: он накопил age/season, + # уже учтённые в max_distance — двойной учёт. + base_km = float(case_data.get('max_distance_km') or 2.0) + search_radius_km = round(base_km * _psychotype_radius_multiplier(case_data), 1) except Exception as e: logger.warning(f'Geo/scoring service error: {e}') diff --git a/services/search_engine.py b/services/search_engine.py index 3b844a2..b6f1c4f 100644 --- a/services/search_engine.py +++ b/services/search_engine.py @@ -269,6 +269,7 @@ async def build_search_model( case_data['psychotype_modifiers'] = psychotype_modifiers max_distance_km = calculate_max_distance(case_data) + case_data['max_distance_km'] = max_distance_km analysis = await analyzer(case_data) scorer = WeightedScorer()