feat: add nightly source record exports
All checks were successful
CI/CD Pipeline / Code Quality Checks (push) Successful in 3m25s
CI/CD Pipeline / Run Tests (push) Successful in 5m12s
CI/CD Pipeline / Build and Push Dev Images (push) Successful in 34s
CI/CD Pipeline / Deploy Dev via Compose (push) Successful in 34s

This commit is contained in:
2026-08-03 19:27:08 +02:00
parent d6ca9f5399
commit 05292a1c16
18 changed files with 1907 additions and 1 deletions

View File

@@ -0,0 +1,78 @@
# Матрица файловых выгрузок внешних данных State Corp
## Пользовательский контракт
Администраторский frontend отправляет
`POST /api/v2/organization-source-records/export-ticket/` с массивом `sources`
и форматом. Backend проверяет последнее полностью опубликованное поколение и
возвращает короткоживущий одноразовый ticket. Затем frontend передаёт ticket в
теле обычной HTML-формы на
`POST /api/v2/organization-source-records/export-download/`.
Браузер получает потоковый ZIP напрямую, без многогигабайтного `Blob` в
JavaScript. Ticket не попадает в URL и после первого запроса становится
недействительным. Совместимый администраторский endpoint
`POST /api/v2/organization-source-records/export/` сразу возвращает тот же ZIP
для API-клиентов.
Во время скачивания таблицы `external_data` не читаются: endpoint упаковывает
готовые файлы последнего ночного поколения. При отсутствии поколения API
возвращает `503` с кодом `source_export_not_ready`.
## Матрица
| Группа API | Таблицы State Corp | Файл | CSV | XLSX | JSON |
|---|---|---|:---:|:---:|:---:|
| `financial_indicators` | `FinancialReport`, `FinancialReportLine` | `financial-indicators` | — | — | да |
| `government_procurements` | `PublicProcurement` | `public-procurements` | да | да | да |
| `industrial_production` | `IndustrialProduct`, `IndustrialCertificate`, `ManufacturerRegistryEntry` | `manufacturers-and-products` | да | да | да |
| `planned_inspections` | `ProsecutorCheck` | `planned-inspections` | да | да | да |
| `bankruptcy` | `BankruptcyProcedure` | `bankruptcy-procedures` | да | да | да |
| `defense_suppliers` | `DefenseUnreliableSupplier` | `defense-unreliable-suppliers` | да | да | да |
| `arbitration` | `ArbitrationCase` | `arbitration-cases` | да | да | да |
| `security_registries` | `InformationSecurityRegistryEntry` | `information-security-registries` | да | да | да |
| `vacancies` | `LaborVacancy` | `labor-vacancies` | да | да | да |
Итого формируется 25 логических артефактов. Финансовые показатели всегда
выгружаются в JSON с вложенным массивом `financial_lines`. Промышленная группа
объединяет три таблицы, а поле `record_type` различает тип строки. Все строки
содержат реквизиты организации, включая ОКПО.
Физических XLSX-файлов может быть больше: по умолчанию один файл содержит не
более 100 000 строк данных и получает суффикс `-part-001`, `-part-002` и далее.
## Ночная генерация
Celery Beat запускает
`apps.external_data.tasks.refresh_source_record_export_artifacts` ежедневно в
`05:30 Europe/Moscow`.
Генератор:
1. читает каждую нормализованную таблицу один раз без model-level сортировки;
2. создаёт компактный JSON-массив и переиспользует его как готовый JSON;
3. потоково формирует CSV и write-only XLSX;
4. атомарно публикует `current.json` только после готовности всей матрицы;
5. при ошибке удаляет staging и продолжает отдавать предыдущее поколение;
6. сохраняет текущее и предыдущее поколения по умолчанию.
Web и Celery worker должны использовать общий read-write volume `/app/media`.
## Первый запуск и настройки
Первое поколение можно сформировать вручную:
```bash
PYTHONPATH=src uv run python src/manage.py build_source_record_exports
```
| Настройка | Значение по умолчанию | Назначение |
|---|---:|---|
| `SOURCE_RECORD_EXPORT_DIRECTORY` | `media/source-record-exports` | Общий каталог поколений |
| `SOURCE_RECORD_EXPORT_GENERATIONS_TO_KEEP` | `2` | Число успешных поколений |
| `SOURCE_RECORD_EXPORT_LOCK_TTL_SECONDS` | `21600` | TTL распределённой блокировки |
| `SOURCE_RECORD_EXPORT_XLSX_ROWS_PER_FILE` | `100000` | Строк данных в одной XLSX-части |
| `SOURCE_RECORD_EXPORT_DOWNLOAD_TICKET_TTL_SECONDS` | `300` | Срок действия download-ticket |
Для атомарной генерации требуется свободное место не меньше
`(GENERATIONS_TO_KEEP + 1) * размер поколения` плюс запас файловой системы.