From a88917f0cffa861f7a60357e2602093b7837a819 Mon Sep 17 00:00:00 2001 From: lekss361 Date: Sat, 16 May 2026 11:52:08 +0300 Subject: [PATCH] fix(best-layouts): replace non-existent geom_3857 with on-the-fly geography (#113 PR C) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit domrf_kn_objects has no geom_3857 column (only latitude/longitude NUMERIC). Switch _COMPETITORS_IN_RADIUS_SQL and _SUPPLY_BATCH_SQL to use ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)::geography — consistent with competitors.py pattern. Rename centroid params x_3857/y_3857 → center_lon/center_lat. Remove dead _round_to_5 function. Fixes review-bot 🔴 BLOCK on fdc6456 PR #196. Tests: 10/10 pass (mock keys updated к center_lon/center_lat), 43/43 regression OK. --- .../app/services/site_finder/best_layouts.py | 44 ++++++++++--------- .../tests/api/v1/test_parcel_best_layouts.py | 6 +-- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/backend/app/services/site_finder/best_layouts.py b/backend/app/services/site_finder/best_layouts.py index 0a67a196..7be4bf6b 100644 --- a/backend/app/services/site_finder/best_layouts.py +++ b/backend/app/services/site_finder/best_layouts.py @@ -2,7 +2,7 @@ Источники: cad_parcels_geom / cad_quarters_geom — центроид участка - domrf_kn_objects — ЖК в радиусе (geom_3857) + domrf_kn_objects — ЖК в радиусе (latitude/longitude → geography) mv_layout_velocity — (obj_id, room_bucket) → агрегат продаж 24 мес domrf_kn_flats — supply count по (room_bucket, area_bin) @@ -49,8 +49,8 @@ _VELOCITY_DIVISORS: dict[str, float] = { # ── SQL: центроид участка ───────────────────────────────────────────────────── _PARCEL_CENTROID_SQL = text(""" - SELECT ST_X(ST_Transform(pt, 3857)) AS x_3857, - ST_Y(ST_Transform(pt, 3857)) AS y_3857 + SELECT ST_X(pt) AS center_lon, + ST_Y(pt) AS center_lat FROM ( SELECT ST_Centroid(geom) AS pt FROM cad_parcels_geom @@ -64,18 +64,22 @@ _PARCEL_CENTROID_SQL = text(""" """) # ── SQL: obj_id конкурентов в радиусе ───────────────────────────────────────── -# Примечание: геометрия domrf_kn_objects.geom_3857 уже в EPSG:3857. -# ST_DWithin на EPSG:3857 — метры (проекция, не geography). +# Геометрия domrf_kn_objects вычисляется on-the-fly из (latitude, longitude) +# как ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)::geography +# (consistency с competitors.py). # obj_class_filter: NULL = все классы. # filter_competitor_obj_ids: NULL = не фильтровать по списку. _COMPETITORS_IN_RADIUS_SQL = text(""" SELECT DISTINCT ON (obj_id) obj_id FROM domrf_kn_objects - WHERE geom_3857 IS NOT NULL + WHERE latitude IS NOT NULL AND longitude IS NOT NULL AND ST_DWithin( - geom_3857, - ST_SetSRID(ST_MakePoint(CAST(:x_3857 AS float), CAST(:y_3857 AS float)), 3857), + ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)::geography, + ST_SetSRID( + ST_MakePoint(CAST(:center_lon AS float), CAST(:center_lat AS float)), + 4326 + )::geography, CAST(:radius_m AS float) ) AND ( @@ -126,10 +130,13 @@ _SUPPLY_BATCH_SQL = text(""" COUNT(*) AS units FROM domrf_kn_flats f JOIN domrf_kn_objects o ON f.obj_id = o.obj_id - WHERE o.geom_3857 IS NOT NULL + WHERE o.latitude IS NOT NULL AND o.longitude IS NOT NULL AND ST_DWithin( - o.geom_3857, - ST_SetSRID(ST_MakePoint(CAST(:x_3857 AS float), CAST(:y_3857 AS float)), 3857), + ST_SetSRID(ST_MakePoint(o.longitude, o.latitude), 4326)::geography, + ST_SetSRID( + ST_MakePoint(CAST(:center_lon AS float), CAST(:center_lat AS float)), + 4326 + )::geography, CAST(:radius_m AS float) ) AND f.snapshot_date = (SELECT MAX(snapshot_date) FROM domrf_kn_flats) @@ -179,11 +186,6 @@ def _normalize_pct(buckets: dict[str, float]) -> dict[str, int]: return floors -def _round_to_5(pct: int) -> int: - """Округлить до ближайших 5%.""" - return int(round(pct / 5.0) * 5) - - # ── Главная функция ─────────────────────────────────────────────────────────── @@ -217,8 +219,8 @@ def get_best_layouts( if not coord_row: raise ValueError(f"Геометрия для {cad_num} не найдена") - x_3857 = float(coord_row["x_3857"]) - y_3857 = float(coord_row["y_3857"]) + center_lon = float(coord_row["center_lon"]) + center_lat = float(coord_row["center_lat"]) # ── Step 2: obj_id конкурентов в радиусе ──────────────────────────────── try: @@ -226,8 +228,8 @@ def get_best_layouts( db.execute( _COMPETITORS_IN_RADIUS_SQL, { - "x_3857": x_3857, - "y_3857": y_3857, + "center_lon": center_lon, + "center_lat": center_lat, "radius_m": radius_m, "obj_class_filter": request.obj_class_filter, }, @@ -281,7 +283,7 @@ def get_best_layouts( supply_rows = ( db.execute( _SUPPLY_BATCH_SQL, - {"x_3857": x_3857, "y_3857": y_3857, "radius_m": radius_m}, + {"center_lon": center_lon, "center_lat": center_lat, "radius_m": radius_m}, ) .mappings() .all() diff --git a/backend/tests/api/v1/test_parcel_best_layouts.py b/backend/tests/api/v1/test_parcel_best_layouts.py index 5beed80f..779a9190 100644 --- a/backend/tests/api/v1/test_parcel_best_layouts.py +++ b/backend/tests/api/v1/test_parcel_best_layouts.py @@ -26,10 +26,10 @@ CAD_NUM = "66:41:0303161:123" _TODAY = dt.date.today() -def _coord_row(x: float = 6747000.0, y: float = 7846000.0) -> MagicMock: - """Центроид участка в EPSG:3857.""" +def _coord_row(lon: float = 60.6, lat: float = 56.85) -> MagicMock: + """Центроид участка (EPSG:4326 lon/lat).""" r = MagicMock() - r.__getitem__ = lambda self, k: {"x_3857": x, "y_3857": y}[k] + r.__getitem__ = lambda self, k: {"center_lon": lon, "center_lat": lat}[k] return r