gendesign/backend/app/workers/tasks/connection_capacity_sync.py
Light1YT e285cbe33a
All checks were successful
CI / changes (pull_request) Successful in 8s
CI / frontend-tests (pull_request) Successful in 1m17s
CI / openapi-codegen-check (pull_request) Successful in 2m6s
CI / backend-tests (pull_request) Successful in 14m28s
feat(site-finder): резервы мощности для ТП — электро+вода на карте §3 (#2119 Фаза A)
Наполнение карты точками подключения с ХАРАКТЕРИСТИКАМИ (свободная мощность)
из верифицированных источников (research #2119). Все источники гео-блокируют
не-RU IP — лоадеры выполняются на проде (Celery weekly + ручной docker exec).

Backend:
- Миграция 180: power_supply_centers (ПС 35-220кВ, точки + резерв МВА),
  power_tp_rp_reserves (10.7k ТП/РП 6-10кВ, геокод — Фаза B),
  water_supply_reserves (ЦСВ/ЦСК Водоканала). UNIQUE NULLS NOT DISTINCT на
  nullable-ключах (грабли #140).
- rosseti_wfs_loader: открытый WFS портал-тп.рф (punycode) → 488 ПС Свердл обл,
  индекс загрузки 256184/6/8 → open/limited/closed, voltage из имени.
- rosseti_reserve_loader: раскрытие ПП№24 (xlsx) — ЦП 35-110кВ (строка 8+)
  матчится к WFS-точкам по нормализованному имени; ТП/РП <35кВ (строка 9+),
  кВА-санити (>2.5 МВА для ТП → ÷1000), «н/д»→NULL, SAVEPOINT per-row.
- vodokanal_reserve_loader: DOCX-раскрытие ПП№6 (stdlib parse, vMerge
  forward-fill), отрицательные резервы (город: дефицит) сохраняются со знаком.
- Endpoint GET /{cad}/connection-capacity: ПС в радиусе (ST_DWithin geography)
  + summary + вода за последний период per-system_kind. Celery: вт 04:00/04:30.

Frontend (§3):
- Карточка «Ресурсные резервы»: ближайший ЦП с резервом МВА + статус-бейдж;
  вода/канализация с красным «дефицит» при отрицательном резерве + примечание.
- Слой «Центры питания (резерв)» на карте: цвет по индексу загрузки
  (зелёный/янтарь/красный), попап с напряжением/мощностью/загрузкой/резервом,
  легенда под картой. Числа через toFiniteNumber (Decimal→str coercion).

api-types.ts перегенерён офлайн (+131/-0, аддитивно). 39 тестов зелёные.
Deep-review: ⚠️ MINOR → все 3 замечания исправлены (NULLS NOT DISTINCT,
per-kind MAX(period), settlement-комментарий).
2026-07-02 12:45:55 +05:00

95 lines
3.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Celery tasks: еженедельная синхронизация точек подключения со свободной
мощностью — электро (Россети WFS + xlsx-резервы) и вода (Водоканал docx). #2119.
Источники ГЕО-БЛОКИРУЮТ non-RU IP → таски выполняются НА ПРОДЕ (Celery worker
на VPS). Persistent breadcrumbs в nspd_geo_log (как utility/poi/noise sync).
Import-light: тяжёлые загрузчики импортируются лениво внутри тасков.
"""
import logging
from sqlalchemy import text
from app.workers.celery_app import celery_app
logger = logging.getLogger(__name__)
def _log_breadcrumb(stage: str, level: str, message: str) -> None:
"""Записать строку в nspd_geo_log (job_id=NULL) — диагностика без SSH."""
try:
from app.core.db import SessionLocal
db = SessionLocal()
try:
db.execute(
text(
"INSERT INTO nspd_geo_log (job_id, level, stage, message) "
"VALUES (NULL, :lvl, :stage, :msg)"
),
{"lvl": level, "stage": stage, "msg": message[:500]},
)
db.commit()
finally:
db.close()
except Exception as e:
logger.warning("connection_capacity breadcrumb failed: %s", e)
@celery_app.task(
name="tasks.connection_capacity_sync.sync_power_supply_centers",
queue="celery",
)
def sync_power_supply_centers() -> dict:
"""WFS центры питания + xlsx-резервы (ЦП + ТП/РП) → power_supply_centers / ТП-РП.
RUN-ON-PROD (гео-блок источников). Сначала WFS (координаты + напряжение +
индекс загрузки), затем резервы из xlsx-раскрытия. Каждый шаг graceful —
сбой резервов не откатывает загруженные WFS-точки.
"""
_log_breadcrumb("power_capacity_sync", "info", "task started")
result: dict = {}
try:
from app.services.site_finder.rosseti_wfs_loader import load_power_supply_centers
result["wfs"] = load_power_supply_centers()
except Exception as e:
logger.exception("sync_power_supply_centers: WFS failed: %s", e)
_log_breadcrumb("power_capacity_sync", "error", f"wfs {type(e).__name__}: {e}")
result["wfs"] = {"error": str(e)}
try:
from app.services.site_finder.rosseti_reserve_loader import load_all_reserves
result["reserves"] = load_all_reserves()
except Exception as e:
logger.exception("sync_power_supply_centers: reserves failed: %s", e)
_log_breadcrumb("power_capacity_sync", "error", f"reserves {type(e).__name__}: {e}")
result["reserves"] = {"error": str(e)}
logger.info("sync_power_supply_centers done: %s", result)
_log_breadcrumb("power_capacity_sync", "info", f"done: {result}")
return result
@celery_app.task(
name="tasks.connection_capacity_sync.sync_water_reserves",
queue="celery",
)
def sync_water_reserves() -> dict:
"""Резервы ЦСВ/ЦСК Водоканала ЕКБ (docx) → water_supply_reserves.
RUN-ON-PROD (гео-блок). Обе страницы (водоснабжение/водоотведение) graceful.
"""
_log_breadcrumb("water_capacity_sync", "info", "task started")
try:
from app.services.site_finder.vodokanal_reserve_loader import load_water_reserves
result = load_water_reserves()
logger.info("sync_water_reserves done: %s", result)
_log_breadcrumb("water_capacity_sync", "info", f"done: {result}")
return result
except Exception as e:
logger.exception("sync_water_reserves failed: %s", e)
_log_breadcrumb("water_capacity_sync", "error", f"{type(e).__name__}: {e}")
raise