gendesign/data/sql/180_connection_capacity.sql
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

143 lines
9.6 KiB
PL/PgSQL
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.

-- 180_connection_capacity.sql
-- Forgejo #2119 Phase A — «точки подключения СО СВОБОДНОЙ МОЩНОСТЬЮ» для ПТИЦА §3.
--
-- Три таблицы верифицированных источников свободной мощности (электро + вода):
-- power_supply_centers — центры питания (ЦП: ПС 35/110 кВ) из Россети WFS +
-- резервы свободной мощности по ЦП (rosseti-ural.ru xlsx).
-- power_tp_rp_reserves — резервы ТП/РП напряжением ниже 35 кВ (10k+ строк,
-- в основном БЕЗ координат — Phase B геокод).
-- water_supply_reserves — резервы ЦСВ/ЦСК Водоканала ЕКБ (docx, city-level).
--
-- Источники гео-блокируют non-RU IP → загрузчики РАБОТАЮТ НА ПРОДЕ (Celery/manual
-- docker exec). Код не должен предполагать локальный сетевой доступ.
--
-- Refresh: weekly via tasks.connection_capacity_sync (WFS + reserves + water).
-- Порядок применения: миграция ПЕРЕД деплоем кода (schema-first, sql.md).
BEGIN;
-- ── power_supply_centers ─────────────────────────────────────────────────────
-- ЦП = центр питания (ПС 35/110 кВ). geom из Россети WFS (координаты округлены
-- ~100 м — для §3 «есть ли точка рядом» этого достаточно). Резервы свободной
-- мощности (installed/current/reserve) допихиваются из xlsx-раскрытия по
-- sc_name_norm-матчу. sc_name_norm — нормализованное имя для сопоставления
-- (lower, «ПС»/voltage-префикс срезан, ё→е, тире/пробелы схлопнуты).
CREATE TABLE IF NOT EXISTS power_supply_centers (
id SERIAL PRIMARY KEY,
source TEXT NOT NULL, -- 'rosseti_wfs'
external_id TEXT, -- feature id или stable hash
sc_name TEXT NOT NULL, -- исходное имя ЦП
sc_name_norm TEXT NOT NULL, -- нормализованное для матча
dzo_name TEXT, -- ДЗО (напр. «Россети Урал»)
org_name TEXT,
branch_name TEXT, -- филиал (br_name)
voltage_class TEXT, -- напр. '110/35/10'
load_index TEXT, -- 'open'|'limited'|'closed'|NULL
installed_capacity_mva NUMERIC, -- установленная, МВА
current_load_mva NUMERIC, -- текущая нагрузка, МВА
reserve_mva NUMERIC, -- свободная мощность, МВА
reserve_asof DATE, -- дата актуальности резерва
capacity_source TEXT, -- 'cp_35_110'|'tp_rp_sub35'|NULL
municipality TEXT,
place_city TEXT, -- sc_placecity
geom GEOMETRY(POINT, 4326),
raw JSONB, -- сырая WFS-фича целиком
fetched_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (source, external_id)
);
CREATE INDEX IF NOT EXISTS gist_power_supply_centers_geom
ON power_supply_centers USING GIST (geom);
CREATE INDEX IF NOT EXISTS idx_power_supply_centers_name_norm
ON power_supply_centers (sc_name_norm);
COMMENT ON TABLE power_supply_centers IS
'Центры питания (ЦП, ПС 35/110 кВ) Россети + резервы свободной мощности по ЦП. '
'Source: Россети WFS (geom) + rosseti-ural.ru xlsx (reserve). '
'Refresh: weekly via tasks.connection_capacity_sync. Forgejo #2119 Phase A.';
COMMENT ON COLUMN power_supply_centers.sc_name_norm IS
'Нормализованное имя ЦП для матча с xlsx-резервом (lower, «ПС»/voltage-префикс '
'срезан, ё→е, тире/пробелы схлопнуты).';
COMMENT ON COLUMN power_supply_centers.geom IS
'Координаты Россети WFS округлены ~100 м — для §3 «есть ли ЦП рядом» достаточно.';
-- ── power_tp_rp_reserves ─────────────────────────────────────────────────────
-- Резервы ТП/РП напряжением ниже 35 кВ (10 741 строк в верифицированном xlsx).
-- В основном БЕЗ координат — geocode_status='pending', гео придёт в Phase B.
-- UNIQUE(name_norm, municipality, voltage_kv) — одно и то же имя ТП может
-- встречаться в разных МО / на разном напряжении.
CREATE TABLE IF NOT EXISTS power_tp_rp_reserves (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL, -- напр. «ТП-721»
name_norm TEXT NOT NULL, -- нормализованное для матча
res_unit TEXT, -- РЭС
municipality TEXT,
settlement TEXT,
voltage_kv TEXT, -- класс напряжения, кВ
installed_capacity_mva NUMERIC, -- установленная, МВА
current_load_mva NUMERIC, -- текущая нагрузка, МВА
reserve_mva NUMERIC, -- свободная мощность, МВА
reserve_asof DATE,
geom GEOMETRY(POINT, 4326), -- NULL до Phase B геокода
geocode_status TEXT DEFAULT 'pending',
raw JSONB,
fetched_at TIMESTAMPTZ DEFAULT NOW(),
-- NULLS NOT DISTINCT: municipality/voltage_kv бывают NULL (пустые ячейки xlsx);
-- без этого ON CONFLICT не матчится и weekly-прогон плодит дубли
-- (те же грабли, что 140_domrf_documents_unique_nulls_not_distinct.sql).
UNIQUE NULLS NOT DISTINCT (name_norm, municipality, voltage_kv)
);
CREATE INDEX IF NOT EXISTS gist_power_tp_rp_reserves_geom
ON power_tp_rp_reserves USING GIST (geom);
CREATE INDEX IF NOT EXISTS idx_power_tp_rp_reserves_name_norm
ON power_tp_rp_reserves (name_norm);
CREATE INDEX IF NOT EXISTS idx_power_tp_rp_reserves_geocode_status
ON power_tp_rp_reserves (geocode_status);
COMMENT ON TABLE power_tp_rp_reserves IS
'Резервы свободной мощности ТП/РП <35 кВ (rosseti-ural.ru xlsx, ~10.7k строк). '
'geom NULL до Phase B геокода (geocode_status). Forgejo #2119 Phase A.';
-- ── water_supply_reserves ────────────────────────────────────────────────────
-- Резервы ЦСВ (центр. система водоснабжения) / ЦСК (водоотведения) Водоканала
-- ЕКБ (docx-раскрытие). Городской уровень (без geom) — фронт показывает ЦСВ
-- «ЗФС, ГСВ» как атрибут города. reserve может быть ОТРИЦАТЕЛЬНЫМ (дефицит) —
-- знак сохраняем. UNIQUE(system_kind, system_name, period).
CREATE TABLE IF NOT EXISTS water_supply_reserves (
id SERIAL PRIMARY KEY,
system_kind TEXT NOT NULL, -- 'water'|'sewerage'
system_name TEXT NOT NULL, -- ЦСВ/ЦСК, напр. «ЗФС, ГСВ»
reserve_thousand_m3_day NUMERIC, -- резерв, тыс. м³/сут (может быть <0)
connections_requested INT, -- заявок на подключение
connections_refused INT, -- отказов
note TEXT,
period TEXT, -- напр. '2026-Q1'
source_url TEXT,
fetched_at TIMESTAMPTZ DEFAULT NOW(),
-- NULLS NOT DISTINCT: period может быть NULL при неудачном infer_period.
UNIQUE NULLS NOT DISTINCT (system_kind, system_name, period)
);
CREATE INDEX IF NOT EXISTS idx_water_supply_reserves_period
ON water_supply_reserves (period);
CREATE INDEX IF NOT EXISTS idx_water_supply_reserves_kind
ON water_supply_reserves (system_kind);
COMMENT ON TABLE water_supply_reserves IS
'Резервы ЦСВ/ЦСК Водоканала ЕКБ (docx-раскрытие, city-level, без geom). '
'reserve_thousand_m3_day может быть отрицательным (дефицит). Forgejo #2119 Phase A.';
COMMENT ON COLUMN water_supply_reserves.reserve_thousand_m3_day IS
'Свободная мощность ЦСВ/ЦСК, тыс. м³/сут. Отрицательное значение = дефицит.';
COMMIT;