fix(site-finder): cascade InFailedSqlTransaction — velocity internal rollback #155
2 changed files with 76 additions and 57 deletions
|
|
@ -1658,33 +1658,34 @@ def analyze_parcel(
|
|||
"lon": centroid_lon,
|
||||
}
|
||||
try:
|
||||
zoning_row = (
|
||||
db.execute(
|
||||
text("""
|
||||
SELECT zone_code, zone_name, description, rosreestr_id
|
||||
FROM pzz_zones_ekb
|
||||
WHERE ST_Within(
|
||||
ST_Centroid(ST_GeomFromText(:wkt, 4326)),
|
||||
geom
|
||||
)
|
||||
LIMIT 1
|
||||
"""),
|
||||
{"wkt": geom_wkt},
|
||||
)
|
||||
.mappings()
|
||||
.first()
|
||||
)
|
||||
if zoning_row:
|
||||
zoning.update(
|
||||
{
|
||||
"zone_code": zoning_row["zone_code"],
|
||||
"zone_name": zoning_row["zone_name"],
|
||||
"description": zoning_row["description"],
|
||||
"rosreestr_id": zoning_row["rosreestr_id"],
|
||||
"data_available": True,
|
||||
"source": "rosreestr-pkk6-cached",
|
||||
}
|
||||
with db.begin_nested():
|
||||
zoning_row = (
|
||||
db.execute(
|
||||
text("""
|
||||
SELECT zone_code, zone_name, description, rosreestr_id
|
||||
FROM pzz_zones_ekb
|
||||
WHERE ST_Within(
|
||||
ST_Centroid(ST_GeomFromText(:wkt, 4326)),
|
||||
geom
|
||||
)
|
||||
LIMIT 1
|
||||
"""),
|
||||
{"wkt": geom_wkt},
|
||||
)
|
||||
.mappings()
|
||||
.first()
|
||||
)
|
||||
if zoning_row:
|
||||
zoning.update(
|
||||
{
|
||||
"zone_code": zoning_row["zone_code"],
|
||||
"zone_name": zoning_row["zone_name"],
|
||||
"description": zoning_row["description"],
|
||||
"rosreestr_id": zoning_row["rosreestr_id"],
|
||||
"data_available": True,
|
||||
"source": "rosreestr-pkk6-cached",
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("zoning query failed for %s: %s", cad_num, e)
|
||||
|
||||
|
|
@ -1692,9 +1693,10 @@ def analyze_parcel(
|
|||
success_recommendation: dict[str, Any] | None = None
|
||||
if district_row:
|
||||
try:
|
||||
success_rows = (
|
||||
db.execute(
|
||||
text("""
|
||||
with db.begin_nested():
|
||||
success_rows = (
|
||||
db.execute(
|
||||
text("""
|
||||
SELECT bucket, success_score, n_deals, avg_price_per_m2, avg_area_m2,
|
||||
velocity_z, price_z, area_z
|
||||
FROM v_bucket_success_score
|
||||
|
|
@ -1702,35 +1704,35 @@ def analyze_parcel(
|
|||
ORDER BY success_score DESC
|
||||
LIMIT 5
|
||||
"""),
|
||||
{"dn": district_row["district_name"]},
|
||||
{"dn": district_row["district_name"]},
|
||||
)
|
||||
.mappings()
|
||||
.all()
|
||||
)
|
||||
.mappings()
|
||||
.all()
|
||||
)
|
||||
if success_rows:
|
||||
success_recommendation = {
|
||||
"district": district_row["district_name"],
|
||||
"ranking": [
|
||||
{
|
||||
"bucket": r["bucket"],
|
||||
"success_score": round(float(r["success_score"]), 2),
|
||||
"n_deals": int(r["n_deals"]),
|
||||
"avg_price_per_m2": (
|
||||
int(r["avg_price_per_m2"]) if r["avg_price_per_m2"] else None
|
||||
),
|
||||
"avg_area_m2": (
|
||||
round(float(r["avg_area_m2"]), 1) if r["avg_area_m2"] else None
|
||||
),
|
||||
}
|
||||
for r in success_rows
|
||||
],
|
||||
"top_bucket": dict(success_rows[0]) if success_rows else None,
|
||||
"note": (
|
||||
"Топ комнатность по 'успешности' = z-scores: velocity×0.5 + price×0.3 "
|
||||
"- area×0.2. Min 30 сделок в группе за 24 мес. "
|
||||
"Используй для квартирографии проекта."
|
||||
),
|
||||
}
|
||||
if success_rows:
|
||||
success_recommendation = {
|
||||
"district": district_row["district_name"],
|
||||
"ranking": [
|
||||
{
|
||||
"bucket": r["bucket"],
|
||||
"success_score": round(float(r["success_score"]), 2),
|
||||
"n_deals": int(r["n_deals"]),
|
||||
"avg_price_per_m2": (
|
||||
int(r["avg_price_per_m2"]) if r["avg_price_per_m2"] else None
|
||||
),
|
||||
"avg_area_m2": (
|
||||
round(float(r["avg_area_m2"]), 1) if r["avg_area_m2"] else None
|
||||
),
|
||||
}
|
||||
for r in success_rows
|
||||
],
|
||||
"top_bucket": dict(success_rows[0]) if success_rows else None,
|
||||
"note": (
|
||||
"Топ комнатность по 'успешности' = z-scores: velocity×0.5 + price×0.3 "
|
||||
"- area×0.2. Min 30 сделок в группе за 24 мес. "
|
||||
"Используй для квартирографии проекта."
|
||||
),
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning("success_recommendation query failed for %s: %s", cad_num, e)
|
||||
success_recommendation = None
|
||||
|
|
|
|||
|
|
@ -126,6 +126,13 @@ def compute_velocity(
|
|||
)
|
||||
except Exception:
|
||||
logger.exception("velocity: competitor query failed for wkt=%s", parcel_geom_wkt[:80])
|
||||
# КРИТИЧНО: rollback aborted transaction чтобы caller (analyze_parcel)
|
||||
# мог продолжить выполнять последующие queries. Иначе caller получает
|
||||
# cascade InFailedSqlTransaction на следующей SQL execute.
|
||||
try:
|
||||
db.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
if not comp_rows:
|
||||
|
|
@ -186,6 +193,11 @@ def compute_velocity(
|
|||
)
|
||||
except Exception:
|
||||
logger.exception("velocity: sale_graph query failed for obj_ids=%s", obj_ids[:5])
|
||||
# КРИТИЧНО: rollback aborted transaction (см. competitor query комментарий)
|
||||
try:
|
||||
db.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
if not sales_rows:
|
||||
|
|
@ -311,6 +323,11 @@ def _get_ekb_median(db: Session, months_window: int = 6) -> float | None:
|
|||
)
|
||||
except Exception:
|
||||
logger.warning("velocity: ekb_median query failed, using fallback")
|
||||
# КРИТИЧНО: rollback aborted transaction
|
||||
try:
|
||||
db.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
if row and row["median"] is not None:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue