166 lines
6.1 KiB
Markdown
166 lines
6.1 KiB
Markdown
# ТЗ: Дополнение расписания из FR24 flight-summary/full
|
||
|
||
**Дата:** 2026-04-21
|
||
**Статус:** READY FOR DEV
|
||
**Приоритет:** Высокий
|
||
|
||
---
|
||
|
||
## Контекст
|
||
|
||
FR24 `/api/flight-summary/full` уже используется в `fr24_worker.py` и возвращает
|
||
**все реальные рейсы** через московские аэропорты за сутки (до 20 000 записей).
|
||
|
||
Текущее поведение:
|
||
- `enrich_schedule()` — только **UPDATE** существующих строк в `fr24_ext.schedule`
|
||
- Рейсы из `flight_actual`, которых нет в Яндекс.Расписании, **теряются**
|
||
|
||
Проблема: Яндекс не покрывает 100% рейсов (чартеры, грузовые, технические, задержанные).
|
||
FR24 имеет полные данные. Нужно использовать `flight_actual` как дополнительный источник расписания.
|
||
|
||
---
|
||
|
||
## Что нужно сделать
|
||
|
||
### 1. Функция `supplement_schedule()` в `fr24_worker.py`
|
||
|
||
```python
|
||
def supplement_schedule(conn, target_date: date) -> int:
|
||
"""
|
||
Insert into fr24_ext.schedule flights from flight_actual
|
||
that have no matching schedule record.
|
||
|
||
Source: fr24_ext.flight_actual
|
||
Target: fr24_ext.schedule (source='fr24')
|
||
|
||
Returns: number of rows inserted
|
||
"""
|
||
```
|
||
|
||
**Логика:**
|
||
1. Найти все рейсы в `flight_actual` за дату, у которых нет соответствия в `schedule`
|
||
2. Определить direction: если `origin_icao` в московских аэропортах → departure, иначе arrival
|
||
3. INSERT в `schedule` с `source='fr24'`, `status='actual'`
|
||
|
||
**SQL-скелет для поиска незаматченных:**
|
||
```sql
|
||
SELECT fa.*
|
||
FROM fr24_ext.flight_actual fa
|
||
WHERE fa.flight_date = %(date)s
|
||
AND NOT EXISTS (
|
||
SELECT 1 FROM fr24_ext.schedule s
|
||
WHERE UPPER(REPLACE(s.flight_number,' ','')) = UPPER(REPLACE(fa.flight,' ',''))
|
||
AND s.flight_date = fa.flight_date
|
||
)
|
||
AND fa.flight IS NOT NULL
|
||
AND fa.flight != ''
|
||
```
|
||
|
||
**Поля для INSERT в schedule:**
|
||
```
|
||
flight_date = target_date
|
||
airport_iata = ICAO_TO_IATA[origin_icao или dest_icao] (московский аэропорт)
|
||
direction = 'departure' если origin_icao московский, иначе 'arrival'
|
||
flight_number = fa.flight (нормализовать: убрать лишние пробелы)
|
||
airline_iata = первые 2 символа IATA (если можно определить)
|
||
origin_iata = ICAO_TO_IATA.get(fa.origin_icao)
|
||
destination_iata = ICAO_TO_IATA.get(fa.dest_icao)
|
||
aircraft_type = NULL (нет в flight_actual)
|
||
scheduled_at = fa.datetime_takeoff (или datetime_landed для arrival)
|
||
actual_takeoff = fa.datetime_takeoff
|
||
actual_landed = fa.datetime_landed
|
||
status = 'actual'
|
||
source = 'fr24'
|
||
fr24_id = fa.fr24_id
|
||
```
|
||
|
||
**ON CONFLICT:** `(flight_number, airport_iata, scheduled_at, direction)` — уже есть.
|
||
|
||
### 2. Также исправить баги из ревью
|
||
|
||
#### Баг A: ZBAD/PKX не в словаре ICAO_TO_IATA
|
||
Файл: `build_mart.py`
|
||
Добавить в словарь `ICAO_TO_IATA`:
|
||
```python
|
||
"ZBAD": "PKX", # Beijing Daxing
|
||
"RKSI": "ICN", # Seoul Incheon (уже есть, проверить)
|
||
```
|
||
|
||
#### Баг B: Soft match для FA/FR24 когда destination не совпадает
|
||
Файл: `build_mart.py`, функции `find_fa_track()` и `find_fr24_track()`
|
||
|
||
Текущая логика:
|
||
```python
|
||
if origin_iata and not destination_iata: # только если dest == NULL
|
||
# soft match by origin only
|
||
```
|
||
|
||
Нужно изменить на:
|
||
```python
|
||
# Если полный матч не нашёл → fallback на origin only
|
||
if origin_iata:
|
||
for row in rows:
|
||
orig_iata = ICAO_TO_IATA.get(row[2])
|
||
if orig_iata == origin_iata:
|
||
return (row[0], row[1])
|
||
```
|
||
(убрать условие `not destination_iata`)
|
||
|
||
### 3. Вызов `supplement_schedule()` в `run()` fr24_worker.py
|
||
|
||
После `enrich_schedule()`:
|
||
```python
|
||
# 4. Supplement schedule with flights from FR24 not in Yandex
|
||
try:
|
||
supplemented = supplement_schedule(conn, target_date)
|
||
conn.commit()
|
||
stats["schedule_supplemented"] = supplemented
|
||
log.info("FR24 worker: supplemented %d new schedule rows", supplemented)
|
||
except Exception as e:
|
||
conn.rollback()
|
||
log.error("FR24 worker: supplement_schedule failed: %s", e)
|
||
stats["errors"] += 1
|
||
```
|
||
|
||
---
|
||
|
||
## Что НЕ нужно делать
|
||
|
||
- НЕ менять DDL/схему БД (колонки `fr24_id`, `actual_takeoff`, `actual_landed` в schedule уже есть или добавить как nullable)
|
||
- НЕ трогать RTL-SDR матч — он работает корректно
|
||
- НЕ запускать backfill — только текущая логика для новых дат
|
||
|
||
---
|
||
|
||
## Проверка результата
|
||
|
||
1. Запустить build для 19.04 или 20.04 (данные уже есть в `flight_actual`)
|
||
2. Проверить:
|
||
```sql
|
||
SELECT source, count(*) FROM fr24_ext.schedule
|
||
WHERE flight_date = '2026-04-19' GROUP BY source;
|
||
-- Ожидаемо: yandex=~1570, fr24=N (новые)
|
||
|
||
SELECT count(*) FROM fr24_ext.schedule s
|
||
JOIN fr24_ext.flight_actual fa ON UPPER(REPLACE(fa.flight,' ','')) = UPPER(REPLACE(s.flight_number,' ',''))
|
||
WHERE s.flight_date = '2026-04-19' AND s.source = 'fr24';
|
||
```
|
||
3. Перезапустить mart build для даты — посмотреть увеличение `schedule_flights` и `with_track`
|
||
|
||
---
|
||
|
||
## Файлы для изменения
|
||
|
||
| Файл | Изменение |
|
||
|------|-----------|
|
||
| `ingest/tracks_fr24/fr24_worker.py` | Добавить `supplement_schedule()`, вызвать в `run()` |
|
||
| `ingest/mart/build_mart.py` | Добавить `ZBAD→PKX` в словарь, исправить soft match |
|
||
|
||
---
|
||
|
||
## Путь к проекту на VM
|
||
|
||
`/home/fr24/fr24/ingest/`
|
||
|
||
Деплой: `docker cp <file> fr24-tracks-fr24:/app/<file> && docker restart fr24-tracks-fr24`
|