102 lines
7.2 KiB
Markdown
102 lines
7.2 KiB
Markdown
# Матрица файловых выгрузок источников
|
||
|
||
## Пользовательский контракт
|
||
|
||
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 и после первого
|
||
запроса становится недействительным.
|
||
|
||
Совместимый администраторский
|
||
`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`.
|
||
|
||
## Матрица
|
||
|
||
| Группа 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. читает каждую группу из БД один раз без глобальной сортировки миллионов строк;
|
||
2. пишет compact JSON-массив на диск и использует его как готовый JSON без второй копии;
|
||
3. потоково создаёт CSV и XLSX без накопления всех строк в памяти;
|
||
4. разбивает XLSX по умолчанию по 100 000 строк на отдельные файлы, ограничивая временный XML и не превышая лимит Excel;
|
||
5. записывает размеры файлов и номера частей в manifest;
|
||
6. атомарно переключает `current.json` только после готовности всей матрицы;
|
||
7. сохраняет текущее и предыдущее поколения по умолчанию.
|
||
|
||
При ошибке незавершённое поколение удаляется, а download endpoint продолжает
|
||
отдавать предыдущую успешную версию.
|
||
|
||
### Расчёт диска
|
||
|
||
Атомарная публикация требует одновременно хранить уже опубликованные поколения
|
||
и одно новое поколение в staging. Минимальный запас под артефакты рассчитывается
|
||
как `(SOURCE_RECORD_EXPORT_GENERATIONS_TO_KEEP + 1) * размер поколения`, плюс
|
||
рабочий запас файловой системы. На снимке dev от 2026-08-03 одно поколение
|
||
заняло 16,55 ГБ (54 физических файла), поэтому при значении `2` следует
|
||
выделить не менее 55 ГБ свободного места под каталог выгрузок. Временная копия
|
||
целого ZIP при скачивании не создаётся.
|
||
|
||
## Хранение и первый запуск
|
||
|
||
Каталог задаётся через `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 |
|