Files
mostovik-backend/docs/source-record-export-matrix-ru.md

122 lines
9.2 KiB
Markdown
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.
# Матрица файловых выгрузок источников
## Пользовательский контракт
Frontend отправляет администраторский
`POST /api/v2/organization-source-records/export-ticket/` с массивом `sources`
и выбранным `format`. В ответ он получает короткоживущий одноразовый ticket и
передаёт его обычной HTML-формой в
`POST /api/v2/organization-source-records/export-download/`. Поэтому браузер
сохраняет потоковый ZIP напрямую на диск, не удерживая весь архив как Blob в
JavaScript. Ticket передаётся в теле формы, не попадает в URL и после первого
запроса становится недействительным.
Имя скачиваемого архива формируется по выбранным источникам и времени создания
запроса: `<source-stem>_YYYYMMDD_HHMMSS.zip`, а для нескольких источников их
имена соединяются через `__`. Это же имя возвращается в `file_name` при выдаче
ticket и затем используется в `Content-Disposition`.
Совместимый администраторский
`POST /api/v2/organization-source-records/export/` по-прежнему сразу возвращает
тот же ZIP API-клиентам. Крупный XLSX может состоять из нескольких файлов
`*-part-001.xlsx`, `*-part-002.xlsx` и далее.
При скачивании endpoint не читает таблицы записей источников и не строит CSV,
XLSX или JSON заново. Он упаковывает файлы последнего полностью опубликованного
ночного поколения и сразу потоково отправляет ZIP без временной копии всего
архива. Поэтому `Content-Length` у ответа отсутствует. Если ни одного поколения
ещё нет, API отвечает `503` с кодом `source_export_not_ready`.
Каждое поколение содержит только текущий календарный год в timezone сервиса.
Для записей с предметной датой год определяется по ней, для записей без такой
даты — по `created_at`, для финансовых отчётов — по году `financial_lines`.
Вложенные финансовые строки других лет исключаются. После смены года поколение
прошлого года не раздаётся: до первой успешной сборки нового года API отвечает
`503 source_export_not_ready`.
## Матрица
| Группа API | Файл | CSV | XLSX | JSON |
|---|---|:---:|:---:|:---:|
| `financial_indicators` | `financial-indicators` | — | — | да |
| `government_procurements` | `public-procurements` | да | да | да |
| `industrial_production` | `manufacturers-and-products` | да | да | да |
| `planned_inspections` | `planned-inspections` | да | да | да |
| `bankruptcy` | `bankruptcy-procedures` | да | да | да |
| `defense_suppliers` | `defense-unreliable-suppliers` | да | да | да |
| `arbitration` | `arbitration-cases` | да | да | да |
| `security_registries` | `information-security-registries` | да | да | да |
| `vacancies` | `labor-vacancies` | да | да | да |
Итого формируется 25 логических артефактов: один JSON для финансовых показателей
и по три формата для остальных восьми групп. Физических файлов может быть
больше из-за разбиения крупных XLSX. Если финансовые показатели выбраны вместе
с другим форматом, в ZIP для них всё равно включается JSON.
Все форматы начинают строку организации с полей `Наименование`, `ИНН`, `ОГРН`,
`КПП`, `ОКПО`, после которых следуют поля исходной записи и развёрнутого
`payload`. Все записи текущего года включаются в публичные файлы, а техническое
наименование внешнего поставщика нейтрализуется. Исходные значения в БД
сохраняются для работы интеграции и дедупликации.
## Ночная генерация
Celery Beat запускает
`organizations.tasks.refresh_source_record_export_artifacts` ежедневно в
`05:30 Europe/Moscow`, после ежедневного обновления organization sources в
`04:30`.
Генератор:
1. читает каждую группу из БД один раз без глобальной сортировки миллионов строк;
выборка текущего года использует функциональный индекс по году строковой
`record_date`, а записи без предметной даты — индекс `created_at`;
2. пишет compact JSON-массив на диск и использует его как готовый JSON без второй копии;
3. потоково создаёт CSV и XLSX без накопления всех строк в памяти;
4. разбивает XLSX по умолчанию по 100 000 строк на отдельные файлы, ограничивая временный XML и не превышая лимит Excel;
5. записывает размеры файлов, номера частей и календарный `export_year` в manifest;
6. атомарно переключает `current.json` только после готовности всей матрицы;
7. сохраняет текущее и предыдущее поколения по умолчанию.
При ошибке незавершённое поколение удаляется, а download endpoint продолжает
отдавать предыдущую успешную версию.
### Расчёт диска
Атомарная публикация требует одновременно хранить уже опубликованные поколения
и одно новое поколение в staging. Минимальный запас под артефакты рассчитывается
как `(SOURCE_RECORD_EXPORT_GENERATIONS_TO_KEEP + 1) * размер поколения`, плюс
рабочий запас файловой системы. Исторический снимок dev от 2026-08-03 до
ограничения по году занимал 16,55 ГБ (54 физических файла); актуальный размер
годового поколения нужно брать из `total_size` результата команды сборки.
Временная копия целого ZIP при скачивании не создаётся.
На локальном снимке от 2026-08-04 поколение за 2026 год содержит 122 582 записи
и занимает 809 768 139 байт (772,3 MiB) для всей матрицы из 25 файлов. Архив со
всеми источниками оценивается в 410,1 MiB для JSON, 289,9 MiB для CSV и
72,3 MiB для XLSX; ZIP использует `ZIP_STORED`, поэтому к сумме файлов
добавляется только небольшой служебный overhead.
## Хранение и первый запуск
Каталог задаётся через `SOURCE_RECORD_EXPORT_DIRECTORY`, по умолчанию —
`media/source-record-exports`. Он должен быть общим read-write volume для web и
Celery worker. В Docker Compose используется `./media:/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 распределённой блокировки Celery |
| `SOURCE_RECORD_EXPORT_XLSX_ROWS_PER_FILE` | `100000` | Максимум строк данных в одной XLSX-части |
| `SOURCE_RECORD_EXPORT_DOWNLOAD_TICKET_TTL_SECONDS` | `300` | Срок действия одноразового browser-download ticket |