gendesign/tradein-mvp/backend/data/sql/003_seed_listings.sql
lekss361 02267d0306 feat: add tradein-mvp subproject (Trade-In Estimator под /trade-in)
- FastAPI backend: PostGIS estimator + 3 scrapers (Avito/Cian/Yandex)
- Next.js 15 frontend: tradein.html mockup design, basePath=/trade-in
- WeasyPrint PDF (Брусника-style 4-page report)
- Address autocomplete с typo-tolerance + 6 EKB presets
- Изолированный docker stack gendesign-tradein (отдельная postgres БД)
- Caddy inline routes: gendsgn.ru/trade-in/* и /trade-in/api/v1/*
- Forgejo Actions: .forgejo/workflows/deploy-tradein.yml (shell-based GHCR login)
- Триггер только по paths: tradein-mvp/** (не пересекается с deploy.yml)
- Образы: ghcr.io/lekss361/gendesign-tradein-{backend,frontend}:latest

Первый запуск на сервере (вручную, один раз):
  - создать /opt/gendesign/tradein-mvp/.env.runtime (postgres pwd, contact email)
  - docker network create gendesign_shared (если нет)
  - docker compose -p gendesign-tradein up -d
  - docker compose -p gendesign exec caddy caddy reload
2026-05-21 00:25:39 +03:00

147 lines
6.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.

-- Trade-In MVP — Seed данные для разработки (v2, фикс распределения).
-- 120 синтетических объявлений + 60 фактических сделок в центре ЕКБ.
-- Использовать пока парсеры не подключены к продакшну.
BEGIN;
TRUNCATE listings, deals RESTART IDENTITY CASCADE;
-- ── Helper: 10 улиц ЕКБ с координатами ─────────────────────────────────────
WITH streets(idx, name, lat, lon) AS (VALUES
(0, 'ул. Малышева', 56.8332::float, 60.5944::float),
(1, 'ул. Куйбышева', 56.8282, 60.6114),
(2, 'ул. 8 Марта', 56.8240, 60.6000),
(3, 'ул. Белинского', 56.8160, 60.6080),
(4, 'пр. Ленина', 56.8400, 60.6100),
(5, 'ул. Толмачёва', 56.8390, 60.6020),
(6, 'ул. Радищева', 56.8300, 60.5890),
(7, 'ул. Мамина-Сибиряка', 56.8420, 60.6190),
(8, 'ул. Луначарского', 56.8350, 60.6280),
(9, 'ул. Первомайская', 56.8440, 60.6240)
),
-- 5 типов квартир (rooms, базовая площадь, базовая цена в руб 2026)
flat_types(idx, rooms, base_area, base_price) AS (VALUES
(0, 0, 28.0::float, 6500000::bigint),
(1, 1, 40.0, 9000000),
(2, 2, 60.0, 12500000),
(3, 3, 80.0, 17000000),
(4, 4, 110.0, 22000000)
),
sources(idx, name) AS (VALUES
(0, 'avito'),
(1, 'cian'),
(2, 'domklik')
),
counts(rooms, n) AS (VALUES
(0, 12), -- студии
(1, 30), -- 1к
(2, 45), -- 2к
(3, 25), -- 3к
(4, 8) -- 4+к
)
INSERT INTO listings (
source, source_url, dedup_hash, address, lat, lon, region_code,
rooms, area_m2, floor, total_floors, year_built, house_type,
price_rub, price_per_m2, listing_date, days_on_market, scraped_at
)
SELECT
src.name AS source,
'https://' || src.name || '.example/lot/' || row_num AS source_url,
md5(src.name || row_num::text || street.name || ft.rooms::text) AS dedup_hash,
street.name || ', ' || (10 + (row_num * 7) % 150)::text AS address,
street.lat + ((random() - 0.5) * 0.012) AS lat, -- ≈±650м jitter
street.lon + ((random() - 0.5) * 0.012) AS lon,
66 AS region_code,
ft.rooms,
ft.base_area + (random() * 6 - 3) AS area_m2, -- ±3м² jitter
1 + (row_num * 3) % 18 AS floor,
9 + (row_num * 5) % 14 AS total_floors,
1960 + (row_num * 11) % 64 AS year_built,
(ARRAY['panel', 'brick', 'monolith', 'monolith_brick'])[1 + (row_num * 7) % 4] AS house_type,
(ft.base_price * (0.85 + random() * 0.30))::bigint AS price_rub,
((ft.base_price * (0.85 + random() * 0.30)) / (ft.base_area + (random() * 6 - 3)))::int AS price_per_m2,
CURRENT_DATE - ((row_num % 90)::text || ' days')::interval AS listing_date,
row_num % 90 AS days_on_market,
NOW() - ((row_num % 14)::text || ' days')::interval AS scraped_at
FROM (
-- Раскрываем counts в rows: для rooms=0 12 строк, для rooms=1 30 строк, ...
SELECT
c.rooms,
row_number() OVER (PARTITION BY c.rooms) AS rooms_row,
row_number() OVER () AS row_num
FROM counts c
CROSS JOIN LATERAL generate_series(1, c.n) gs
) AS expanded
JOIN flat_types ft ON ft.rooms = expanded.rooms
JOIN streets street ON street.idx = expanded.row_num % 10
JOIN sources src ON src.idx = expanded.row_num % 3;
-- ── DEALS (60 шт., цены на ~7% ниже листингов) ────────────────────────────
WITH streets(idx, name, lat, lon) AS (VALUES
(0, 'ул. Малышева', 56.8332::float, 60.5944::float),
(1, 'ул. Куйбышева', 56.8282, 60.6114),
(2, 'ул. 8 Марта', 56.8240, 60.6000),
(3, 'ул. Белинского', 56.8160, 60.6080),
(4, 'пр. Ленина', 56.8400, 60.6100),
(5, 'ул. Толмачёва', 56.8390, 60.6020),
(6, 'ул. Радищева', 56.8300, 60.5890),
(7, 'ул. Мамина-Сибиряка', 56.8420, 60.6190)
),
flat_types(idx, rooms, base_area, base_price) AS (VALUES
(1, 1, 40.0::float, 9000000::bigint),
(2, 2, 60.0, 12500000),
(3, 3, 80.0, 17000000)
),
deal_counts(rooms, n) AS (VALUES
(1, 18),
(2, 28),
(3, 14)
)
INSERT INTO deals (
source, dedup_hash, address, lat, lon, region_code,
rooms, area_m2, floor, total_floors, year_built, house_type,
deal_date, price_rub, price_per_m2, days_on_market, scraped_at
)
SELECT
'rosreestr' AS source,
md5('deal:' || row_num::text || street.name || ft.rooms::text) AS dedup_hash,
street.name || ', ' || (10 + (row_num * 5) % 140)::text AS address,
street.lat + ((random() - 0.5) * 0.012) AS lat,
street.lon + ((random() - 0.5) * 0.012) AS lon,
66,
ft.rooms,
ft.base_area + (random() * 6 - 3) AS area_m2,
1 + (row_num * 3) % 18 AS floor,
9 + (row_num * 5) % 14 AS total_floors,
1960 + (row_num * 11) % 64 AS year_built,
(ARRAY['panel', 'brick', 'monolith'])[1 + (row_num * 7) % 3] AS house_type,
CURRENT_DATE - ((row_num * 6)::text || ' days')::interval AS deal_date,
-- Сделки на 5-12% ниже листингов (вывод PDF Брусники)
(ft.base_price * 0.92 * (0.85 + random() * 0.20))::bigint AS price_rub,
((ft.base_price * 0.92 * (0.85 + random() * 0.20)) / (ft.base_area + (random() * 6 - 3)))::int AS price_per_m2,
20 + (row_num % 100) AS days_on_market,
NOW() - ((row_num * 6)::text || ' days')::interval AS scraped_at
FROM (
SELECT
c.rooms,
row_number() OVER () AS row_num
FROM deal_counts c
CROSS JOIN LATERAL generate_series(1, c.n) gs
) AS expanded
JOIN flat_types ft ON ft.rooms = expanded.rooms
JOIN streets street ON street.idx = expanded.row_num % 8;
-- ── Sanity ─────────────────────────────────────────────────────────────────
DO $$
DECLARE
n_listings int;
n_deals int;
BEGIN
SELECT COUNT(*) INTO n_listings FROM listings;
SELECT COUNT(*) INTO n_deals FROM deals;
RAISE NOTICE 'Seed: % listings, % deals', n_listings, n_deals;
END $$;
COMMIT;