Files
anal-front/docs/backend-endpoints-competence-map-association-map-from-layouts.md
Gleb Korotkiy 20a3e0a843
Some checks failed
CI/CD Pipeline / Quality Gate (push) Failing after 41s
CI/CD Pipeline / Build and Deploy Dev via Compose (push) Has been skipped
feat: add maps pages
2026-06-24 21:46:08 +03:00

2017 lines
83 KiB
Markdown
Raw Permalink 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.
# Backend Endpoints Competence Map Association Map
Назначение: спецификация backend-контракта для страниц:
- `Все сервисы / Карта компетенций / Карта кластеров` (`/new-map`);
- `Все сервисы / Карта компетенций / Карта технопарков` (`/new-map/technoparks`);
- `Все сервисы / Карта компетенций / Карта ОЭЗ` (`/new-map/sez`).
Документ нужен как задача для backend-разработки.
Цель: реализовать endpoint-ы, которые возвращают все серверное состояние новых страниц карты
по участию организаций ОПК в инновационных объединениях:
- значения фильтров;
- доступные разделы карты;
- доступные типы отображения карты;
- выбранные субъекты РФ или федеральные округа как фильтр карты;
- KPI-карточки;
- данные для раскраски и tooltip-ов карты субъектов РФ;
- данные для раскраски и tooltip-ов карты федеральных округов;
- список объединений, в числе участников которых есть организации ОПК;
- рейтинг субъектов РФ;
- счетчик и перечень организаций ОПК для табличного представления.
Документ является дополнением к
`docs/backend-endpoints-competence-map-analytics-from-layouts.md`.
Он не заменяет endpoint-ы аналитической панели `/dashboard`.
## Принципы контракта
- snapshot страницы карты должен возвращаться одним page-level endpoint-ом;
- справочники фильтров должны возвращаться отдельным endpoint-ом;
- табличный перечень организаций должен возвращаться отдельным collection endpoint-ом, потому что это
потенциально большой массив и он открывается отдельным состоянием страницы;
- контракт описывает серверное состояние страницы, а не конкретную визуальную реализацию UI;
- frontend может локально решать, показывать `association_type` как route tabs или navigation links,
какие иконки использовать, как отрисовывать SVG-карту и как скроллить таблицы;
- backend должен возвращать все данные, необходимые для всех серверно значимых состояний страницы:
фильтры, выбранные субъекты/округа, KPI, строки рейтинга, строки списка объединений, строки таблицы
организаций, id сущностей и значения для drilldown;
- все значения должны возвращаться в raw-формате: number, string, boolean, null;
- проценты должны возвращаться числом `0..100`, а не долей `0..1`;
- даты должны возвращаться в ISO 8601 с timezone;
- каждый объект, который может использоваться для drilldown или ссылки, должен иметь стабильный `id`;
- все массивы должны возвращаться уже отсортированными, если для блока задан серверный порядок;
- все KPI должны иметь стабильный `code`;
- `0` должен возвращаться как валидное значение, а не как `null`, строка или `—`;
- `null` допустим только для реально отсутствующего или неприменимого значения;
- backend должен обеспечивать консистентность KPI, карты, рейтинга, списка объединений и таблицы
организаций внутри одного набора фильтров.
Важное ограничение по числам в примерах:
- числовые значения в JSON-примерах взяты из референсных экранов и текущих mock-данных страницы;
- реальные значения должны быть рассчитаны по утвержденной backend-методике;
- если методика конкретного показателя не утверждена, ее нужно зафиксировать до реализации endpoint-а;
- если число на референсном экране противоречит смыслу поля, backend-контракт должен сохранять
корректную семантику поля, а не повторять ошибочное или placeholder-значение макета.
---
## REST и HTTP правила
### Модель ресурсов
Endpoint-ы описывают серверные представления ресурсов, а не действия UI.
В рамках этих страниц есть три ресурса:
- `/api/v1/competence-map/association-map/filters/` - справочники и доступные значения фильтров;
- `/api/v1/competence-map/association-map/` - snapshot карты для выбранных фильтров;
- `/api/v1/competence-map/association-map/organizations/` - табличный перечень организаций ОПК
для выбранных фильтров.
Требования:
- не добавлять action-style endpoint-ы вида `/getMap`, `/loadMapFilters`, `/selectRegion`;
- не использовать frontend path `/new-map` в API path;
- `association_type` является query-параметром одного ресурса карты, а не отдельным URI-сегментом;
- выбранные на карте субъекты РФ и федеральные округа являются query-параметрами snapshot-а,
а не отдельными action endpoint-ами;
- path должен оставаться стабильным в рамках `/api/v1`; breaking changes требуют новой версии API.
### HTTP методы
Для всех endpoint-ов используется только `GET`.
Причина:
- запросы только читают данные;
- запросы не меняют серверное состояние;
- результат полностью определяется URI, query params, правами пользователя и актуальностью данных.
GET request body не используется. Все параметры выбора представления передаются только через query params
и headers.
`POST` для этих экранов не нужен. Его можно вводить только для отдельных будущих сценариев:
- создание export job;
- сохранение пользовательского набора фильтров;
- тяжелый search/report request, который не помещается в URL и имеет отдельный жизненный цикл.
### Media type
Request:
```text
Accept: application/json
```
Успешный response:
```text
Content-Type: application/json; charset=utf-8
Content-Language: ru-RU
```
Error response:
```text
Content-Type: application/problem+json; charset=utf-8
```
### Query params
Для `/association-map/filters/` и `/association-map/` используется единый набор query params:
- `association_type`: `clusters | technoparks | sez`, optional, default `clusters`;
- `map_view_mode`: `regions | federal_districts`, optional, default `regions`;
- `federal_district_id`: string, optional;
- `region_id`: string, optional;
- `federal_district_ids`: array of string, optional;
- `region_ids`: array of string, optional;
- `industry_id`: string, optional;
- `integrated_structure_id`: string, optional;
- `organization_id`: string, optional;
- `competence_id`: string, optional.
Для `/association-map/organizations/` дополнительно используются query params:
- `limit`: integer, optional, default `500`, maximum `1000`;
- `offset`: integer, optional, default `0`;
- `sort_field`: string, optional, default `serial_number`;
- `sort_direction`: `asc | desc`, optional, default `asc`.
Правила:
- если фильтр не выбран, параметр лучше не передавать;
- если frontend передал пустую строку, backend должен нормализовать ее в `null` или пустой массив
в `applied_filters`;
- неизвестные query params должны возвращать `400 Bad Request`, чтобы опечатки не игнорировались молча;
- повтор одного и того же query param запрещен, кроме массивов `region_ids` и `federal_district_ids`;
- массивы передаются в canonical form как repeated query params:
```text
region_ids=RU-MOS&region_ids=RU-MOW
federal_district_ids=cfo&federal_district_ids=pfo
```
- значения `id` сравниваются как case-sensitive строки;
- все примененные значения backend возвращает в `applied_filters` уже нормализованными;
- `region_id` и элементы `region_ids` для карты должны соответствовать SVG-id субъектов РФ
в формате `RU-*`, например `RU-MOS`, `RU-MOW`, `RU-TA`;
- `federal_district_id` и элементы `federal_district_ids` должны соответствовать стабильным id
федеральных округов, например `cfo`, `pfo`, `ufo`, `sfo`, `dfo`, `nwfo`, `skfo`, `yufo`;
- если `region_id` передан вместе с `federal_district_id`, backend должен проверить, что регион входит
в указанный федеральный округ;
- нельзя одновременно передавать `region_id` и `region_ids`;
- нельзя одновременно передавать `federal_district_id` и `federal_district_ids`;
- нельзя одновременно передавать `region_ids` и `federal_district_ids`;
- нельзя одновременно передавать одиночные geography-фильтры (`region_id`, `federal_district_id`)
и multi-selection-фильтры карты (`region_ids`, `federal_district_ids`);
- если фильтры валидны, но данных по ним нет, возвращается `200 OK` с пустыми массивами;
- если значение фильтра не существует или недоступно пользователю, возвращается ошибка валидации.
Canonical examples:
```text
GET /api/v1/competence-map/association-map/filters/?association_type=clusters
GET /api/v1/competence-map/association-map/?association_type=clusters
GET /api/v1/competence-map/association-map/?association_type=technoparks&map_view_mode=regions&region_ids=RU-MOS&region_ids=RU-MOW
GET /api/v1/competence-map/association-map/?association_type=sez&map_view_mode=federal_districts&federal_district_ids=cfo&federal_district_ids=pfo
GET /api/v1/competence-map/association-map/organizations/?association_type=clusters&region_ids=RU-MOS&limit=500&offset=0
```
Не использовать как canonical form:
```text
GET /api/v1/competence-map/association-map/?association_type=clusters&region_id=&federal_district_id=
GET /api/v1/competence-map/association-map/?association_type=clusters&region_id=RU-MOS&region_ids=RU-MOW
```
Первый request можно поддержать для совместимости, но backend все равно должен вернуть пустые значения как
`null` или `[]` в `applied_filters`.
Второй request должен возвращать `400 Bad Request`, потому что одиночный фильтр и multi-selection фильтр
конфликтуют.
### Headers и трассировка
Каждый response должен содержать:
```text
X-Request-Id: <request id>
```
Требования:
- если frontend передал `X-Request-Id`, backend должен использовать его или вернуть связанный id;
- если id не передан, backend генерирует его сам;
- этот id должен попадать в backend logs, чтобы можно было связать ошибку UI и серверный запрос;
- технические debug-поля не добавляются в JSON payload, для них используются headers и логи.
### Авторизация и права доступа
Endpoint-ы должны учитывать права текущего пользователя.
Правила:
- `401 Unauthorized` возвращается, если пользователь не аутентифицирован;
- `403 Forbidden` возвращается, если пользователь аутентифицирован, но не имеет доступа к странице или разделу;
- для `401 Unauthorized` backend должен вернуть `WWW-Authenticate`, если это предусмотрено текущей схемой
аутентификации;
- если пользователь имеет доступ к странице, но часть справочников или данных ему недоступна, backend должен
отфильтровать эти данные на сервере;
- `filters` не должен раскрывать id сущностей, к которым у пользователя нет доступа;
- `disabled = true` можно использовать только для значений, которые пользователь видит, но которые неприменимы
при текущих фильтрах;
- значение фильтра, недоступное пользователю, обрабатывается как invalid filter value и не должно раскрывать,
существует ли такая сущность в системе.
### Кэширование и условные GET
Данные зависят от пользователя, прав доступа и фильтров, поэтому shared caching должен быть запрещен.
Recommended headers для успешных responses:
```text
Cache-Control: private, max-age=60
Vary: Authorization, Accept
ETag: "<user-and-query-specific-etag>"
Last-Modified: <data_actual_at as HTTP-date>
```
Требования:
- `ETag` должен учитывать пользователя, права доступа, endpoint path, query params и версию/актуальность данных;
- `Last-Modified` должен соответствовать актуальности данных, а не времени генерации response;
- backend должен поддерживать `If-None-Match` и может вернуть `304 Not Modified`, если представление не изменилось;
- при `304 Not Modified` тело response не возвращается;
- для персональных данных нельзя использовать `Cache-Control: public`;
- если backend пока не готов поддерживать validators, нужно явно вернуть `Cache-Control: no-store`.
### Актуальность и методика расчета
Response содержит два разных времени:
- `generated_at` - когда backend сформировал текущий response;
- `data_actual_at` - на какой момент актуальны данные карты и таблиц.
Требования:
- `generated_at` не использовать как дату актуальности данных;
- `data_actual_at` должен быть единым для snapshot-а карты и таблицы организаций при одинаковых фильтрах;
- если разные блоки приходят из источников с разной актуальностью, backend должен либо привести их к единому
согласованному snapshot-у, либо явно согласовать и задокументировать методику;
- версия методики расчета уникальности организаций, участий и принадлежности к регионам должна быть зафиксирована
в backend-документации;
- если backend поддерживает версионирование методик, response может дополнительно возвращать
`calculation_methodology_version`;
- изменение методики, которое меняет смысл существующих полей, считается breaking change для `/api/v1`.
### Большие справочники и массивы
Dashboard-массивы `map.regions`, `map.federal_districts`, `association_list.rows` и
`region_ranking.rows` возвращаются полностью.
Табличный перечень организаций может быть большим, поэтому он вынесен в отдельный endpoint
`/association-map/organizations/` с `limit` и `offset`.
Требования к таблице организаций:
- `limit` default `500`;
- `limit` maximum `1000`;
- `offset` default `0`;
- `total_count` всегда возвращается независимо от `limit`;
- `unique_organizations_count` возвращается отдельно от количества строк, потому что одна организация
может иметь несколько строк участия;
- если текущий UI хочет показать таблицу без пагинации, frontend может запросить `limit=1000`;
- если `total_count > limit`, frontend должен догружать следующие страницы или включить пагинацию/виртуализацию.
Для справочника `organizations` в `/association-map/filters/` также возможен большой объем данных.
Если список может превышать безопасный размер для одного response, backend должен выбрать один из вариантов
до реализации:
- возвращать только доступные организации в пределах текущих фильтров, если объем гарантированно небольшой;
- добавить к `/association-map/filters/` query params `organization_search` и `organization_limit`;
- вынести поиск организаций в отдельный endpoint справочника.
Если вводятся `organization_search` и `organization_limit`, правила должны быть такими:
- `organization_search`: string, optional, минимум 2 символа после trim;
- `organization_limit`: integer, optional, default `50`, maximum `100`;
- сортировка организаций: релевантность поиска, затем `label` по возрастанию (`ru-RU`);
- выбранная в `organization_id` организация должна присутствовать в `organizations`, даже если она не попала бы
в первые `organization_limit` результатов.
---
## Термины и правила подсчета
### `association_type`
Тип инновационного объединения для карты.
Допустимые значения:
- `clusters`: промышленные кластеры;
- `technoparks`: технопарки;
- `sez`: особые экономические зоны.
Для страниц карты значение `all` не используется.
Объединенный режим всех типов относится к аналитической панели `/dashboard` и описан в отдельном документе.
Соответствие frontend routes:
| Route | `association_type` |
| ---------------------- | ------------------ |
| `/new-map` | `clusters` |
| `/new-map/technoparks` | `technoparks` |
| `/new-map/sez` | `sez` |
### `map_view_mode`
Тип отображения карты.
Допустимые значения:
- `regions`: карта субъектов РФ;
- `federal_districts`: карта федеральных округов.
`map_view_mode` сам по себе не меняет методику подсчета данных. Он определяет активное визуальное
представление и то, какие id frontend будет передавать при клике по карте.
### `region_id`
Идентификатор субъекта РФ для карты.
Требования:
- должен совпадать с SVG-id субъекта РФ в компоненте карты;
- формат: `RU-*`;
- примеры: `RU-MOS`, `RU-MOW`, `RU-TA`, `RU-CHE`;
- если у backend-а есть внутренний id региона, backend должен уметь сопоставить его с `map_region_id`.
### `federal_district_id`
Идентификатор федерального округа.
Требования:
- стабильный string id;
- используется в фильтрах, карте федеральных округов и связке субъектов РФ с ФО;
- рекомендуемые значения: `cfo`, `nwfo`, `yufo`, `pfo`, `ufo`, `sfo`, `dfo`, `skfo`.
### `region_ids` и `federal_district_ids`
Множественный выбор на карте.
Назначение:
- `region_ids` используется, когда пользователь выбрал один или несколько субъектов РФ на карте;
- `federal_district_ids` используется, когда пользователь выбрал один или несколько федеральных округов;
- оба массива работают как фильтр данных страницы.
Правила:
- массивы могут быть пустыми;
- пустой массив означает, что multi-selection на карте не применен;
- если передан массив, соответствующий одиночный select-фильтр должен быть пустым;
- `region_ids` и `federal_district_ids` взаимоисключающие;
- порядок id в `applied_filters` должен сохранять порядок, нормализованный backend-ом.
### `associations_count`
Количество инновационных объединений выбранного `association_type` после применения фильтров.
Примеры:
- для `clusters` это количество промышленных кластеров;
- для `technoparks` это количество технопарков;
- для `sez` это количество ОЭЗ.
### `opk_organizations_count`
Количество уникальных организаций ОПК после применения фильтров.
Одна организация ОПК может входить в несколько объединений. Поэтому это поле не должно считаться как простая
сумма строк таблицы организаций, если таблица возвращает строки участия.
### `opk_memberships_count`
Количество связей `организация ОПК x инновационное объединение`.
Это поле может быть больше `opk_organizations_count`.
Если таблица организаций возвращает строки участия, `total_count` таблицы может совпадать с
`opk_memberships_count`, но не обязан совпадать с `opk_organizations_count`.
### `organization_directory.total_count`
Количество строк в табличном перечне организаций после применения фильтров и до применения `limit`/`offset`.
Требования:
- значение используется для UI-счетчика `Найдено: N`;
- если таблица возвращает строки участия, а не уникальные организации, значение может быть больше
`unique_organizations_count`;
- семантика строки таблицы должна быть зафиксирована в поле `row_granularity`.
### `rank`
Позиция строки в рейтинге.
Требования:
- `rank` возвращается backend-ом как integer `>= 1`;
- если используется обычное порядковое ранжирование, `rank` не должен дублироваться;
- если бизнес-методика допускает одинаковые места при равных значениях, блок должен вернуть
`ranking_method`;
- допустимые значения `ranking_method`: `ordinal`, `dense`, `competition`;
- если `ranking_method` не передан, frontend считает методику `ordinal`.
### Полнота массивов
Если в блоке нет явного `limit`, массив `rows`, `regions` или `federal_districts` должен возвращаться полностью
для текущего состояния страницы и текущих фильтров.
Frontend может отображать часть данных в видимой области и скроллить контейнер локально, но backend не должен
неявно обрезать массив до первых 5-10 строк только потому, что в макете виден короткий пример.
---
## Нужно реализовать
## `competence-map association-map filters`
### `GET /api/v1/competence-map/association-map/filters/`
Описание:
- возвращает справочники для фильтров страниц карты;
- учитывает права пользователя;
- учитывает текущие выбранные фильтры, если они переданы в query params;
- возвращает серверно доступные значения `association_type`;
- возвращает доступные типы отображения карты;
- не возвращает KPI, рейтинг, список объединений и таблицу организаций;
- не управляет визуальным представлением фильтров: frontend может локально показывать типы объединений
как route navigation, tabs или segmented control.
Request:
```text
GET /api/v1/competence-map/association-map/filters/?association_type=clusters
```
Query params:
- `association_type`: `clusters | technoparks | sez`, optional, default `clusters`;
- `map_view_mode`: `regions | federal_districts`, optional, default `regions`;
- `federal_district_id`: string, optional;
- `region_id`: string, optional;
- `federal_district_ids`: array of string, optional;
- `region_ids`: array of string, optional;
- `industry_id`: string, optional;
- `integrated_structure_id`: string, optional;
- `organization_id`: string, optional;
- `competence_id`: string, optional.
Response:
```json
{
"generated_at": "2026-06-24T15:20:00+03:00",
"applied_filters": {
"association_type": "clusters",
"map_view_mode": "regions",
"federal_district_id": null,
"region_id": null,
"federal_district_ids": [],
"region_ids": [],
"industry_id": null,
"integrated_structure_id": null,
"organization_id": null,
"competence_id": null
},
"association_types": [
{
"id": "clusters",
"label": "Кластеры",
"route_path": "/new-map",
"associations_count": 124,
"disabled": false
},
{
"id": "technoparks",
"label": "Технопарки",
"route_path": "/new-map/technoparks",
"associations_count": 35,
"disabled": false
},
{
"id": "sez",
"label": "Особые экономические зоны",
"route_path": "/new-map/sez",
"associations_count": 17,
"disabled": false
}
],
"map_view_modes": [
{
"id": "regions",
"label": "Субъекты РФ",
"disabled": false
},
{
"id": "federal_districts",
"label": "Федеральные округа",
"disabled": false
}
],
"federal_districts": [
{
"id": "cfo",
"label": "Центральный ФО",
"region_ids": ["RU-BEL", "RU-MOS", "RU-MOW", "RU-YAR"],
"associations_count": 58,
"opk_organizations_count": 163,
"disabled": false
}
],
"regions": [
{
"id": "RU-MOS",
"map_region_id": "RU-MOS",
"federal_district_id": "cfo",
"label": "Московская область",
"associations_count": 17,
"opk_organizations_count": 68,
"disabled": false
}
],
"industries": [
{
"id": "radio-electronics",
"label": "Радиоэлектронная промышленность",
"opk_organizations_count": 180,
"disabled": false
}
],
"integrated_structures": [
{
"id": "rostec",
"label": "Госкорпорация Ростех",
"opk_organizations_count": 42,
"disabled": false
}
],
"organizations": [
{
"id": "org-concern-agat",
"label": "АО \"Концерн \"Моринсис - Агат\"",
"inn": "7700000000",
"ogrn": "1027700000000",
"association_memberships_count": 1,
"disabled": false
}
],
"competences": [
{
"id": "microelectronics",
"label": "Микроэлектроника",
"opk_organizations_count": 64,
"disabled": false
}
]
}
```
### Требования к `association-map/filters/`
#### Поле `generated_at`
Требования:
- ISO 8601 с timezone;
- обязательное поле;
- единое время генерации для всего payload.
#### Блок `applied_filters`
Требования:
- возвращает фактически примененные фильтры;
- все отсутствующие одиночные значения возвращаются как `null`;
- все отсутствующие multi-selection значения возвращаются как `[]`;
- `association_type` всегда заполнен;
- `map_view_mode` всегда заполнен.
#### Массив `association_types`
Требования:
- содержит все доступные пользователю типы объединений для страниц карты;
- `id` должен быть одним из `clusters`, `technoparks`, `sez`;
- `label` обязательный;
- `route_path` optional, но если возвращается, должен соответствовать frontend route;
- `associations_count` integer `>= 0`;
- `disabled` boolean.
Назначение:
- является серверным источником доступных разделов карты;
- порядок массива определяет рекомендуемый порядок навигации;
- при выборе раздела frontend передает выбранный `id` как query param `association_type`;
- `associations_count` может использоваться как серверное значение для badge/count рядом с разделом;
- `disabled = true` означает, что тип недоступен пользователю или неприменим при текущих фильтрах.
#### Массив `map_view_modes`
Требования:
- содержит `regions` и `federal_districts`, если оба режима доступны пользователю;
- `id` обязательный;
- `label` обязательный;
- `disabled` обязательный.
#### Массивы справочников
Для массивов `federal_districts`, `regions`, `industries`, `integrated_structures`, `organizations`,
`competences`:
- `id` обязательный;
- `label` обязательный;
- `disabled` обязательный;
- count-поля должны быть integer `>= 0`;
- если по текущим фильтрам список пустой, возвращается пустой массив;
- выбранное значение должно присутствовать в справочнике, если пользователь имеет к нему доступ.
Для `regions` дополнительно:
- `map_region_id` обязательный;
- `federal_district_id` обязательный;
- `map_region_id` должен совпадать с id региона в SVG-карте субъектов РФ.
Для `federal_districts` дополнительно:
- `region_ids` обязательный;
- каждый `region_ids[*]` должен существовать в справочнике `regions`, если регион доступен пользователю.
---
## `competence-map association-map`
### `GET /api/v1/competence-map/association-map/`
Описание:
- возвращает основную аналитическую часть страницы карты для выбранного `association_type`;
- применяет все переданные фильтры;
- применяет выбранные на карте субъекты РФ или федеральные округа как фильтр;
- возвращает KPI, данные карты, список объединений, рейтинг субъектов РФ и summary таблицы организаций;
- не возвращает полный табличный перечень организаций, для него используется отдельный endpoint
`/association-map/organizations/`.
Request:
```text
GET /api/v1/competence-map/association-map/?association_type=clusters
```
Query params:
- `association_type`: `clusters | technoparks | sez`, optional, default `clusters`;
- `map_view_mode`: `regions | federal_districts`, optional, default `regions`;
- `federal_district_id`: string, optional;
- `region_id`: string, optional;
- `federal_district_ids`: array of string, optional;
- `region_ids`: array of string, optional;
- `industry_id`: string, optional;
- `integrated_structure_id`: string, optional;
- `organization_id`: string, optional;
- `competence_id`: string, optional.
Общий response envelope:
```json
{
"generated_at": "2026-06-24T15:20:00+03:00",
"data_actual_at": "2026-06-24T00:00:00+03:00",
"association_type": "clusters",
"applied_filters": {
"association_type": "clusters",
"map_view_mode": "regions",
"federal_district_id": null,
"region_id": null,
"federal_district_ids": [],
"region_ids": [],
"industry_id": null,
"integrated_structure_id": null,
"organization_id": null,
"competence_id": null
},
"page": {},
"kpis": [],
"map": {},
"association_list": {},
"region_ranking": {},
"organization_directory": {}
}
```
### Серверные состояния страницы по `association_type`
Страница имеет три серверных состояния. Они используют один и тот же endpoint
`/api/v1/competence-map/association-map/`.
| Frontend route | `association_type` | Primary KPI | Список объединений | Рейтинг |
| ---------------------- | ------------------ | ------------------- | --------------------- | ---------------------------------------------------- |
| `/new-map` | `clusters` | `clusters_count` | промышленные кластеры | субъекты РФ по участию организаций ОПК в кластерах |
| `/new-map/technoparks` | `technoparks` | `technoparks_count` | технопарки | субъекты РФ по участию организаций ОПК в технопарках |
| `/new-map/sez` | `sez` | `sez_count` | ОЭЗ | субъекты РФ по участию организаций ОПК в ОЭЗ |
Требования:
- `association_type` в корне response должен совпадать с `applied_filters.association_type`;
- frontend не должен делать отдельные endpoint-ы `/clusters`, `/technoparks`, `/sez`;
- при переходе между frontend routes меняется только `association_type` и повторно запрашиваются
`/association-map/filters/` и `/association-map/`;
- `map_view_mode` меняет активное представление карты, но не требует отдельного backend resource.
---
## Response для `association_type=clusters`
```json
{
"generated_at": "2026-06-24T15:20:00+03:00",
"data_actual_at": "2026-06-24T00:00:00+03:00",
"association_type": "clusters",
"applied_filters": {
"association_type": "clusters",
"map_view_mode": "regions",
"federal_district_id": null,
"region_id": null,
"federal_district_ids": [],
"region_ids": [],
"industry_id": null,
"integrated_structure_id": null,
"organization_id": null,
"competence_id": null
},
"page": {
"title": "Участие организаций ОПК в кластерах",
"subtitle": "Российская Федерация",
"legend_label": "Присутствие одной или более организаций ОПК в кластере",
"empty_list_label": "По текущим фильтрам кластеры не найдены."
},
"kpis": [
{
"code": "clusters_count",
"label": "Количество кластеров",
"value": 124,
"value_type": "integer",
"numerator": 124,
"denominator": null
},
{
"code": "opk_organizations_count",
"label": "Количество организаций ОПК",
"value": 517,
"value_type": "integer",
"numerator": 517,
"denominator": null
}
],
"map": {
"view_mode": "regions",
"selected_scope": null,
"selected_region_ids": [],
"selected_federal_district_ids": [],
"selected_chip_text": null,
"highlight_color": "#022456",
"regions": [
{
"region_id": "RU-MOS",
"map_region_id": "RU-MOS",
"region_name": "Московская область",
"federal_district_id": "cfo",
"associations_count": 17,
"opk_organizations_count": 68,
"has_data": true,
"is_highlighted": false,
"tooltip_label": "Московская область: 17 кластеров, 68 организаций ОПК"
},
{
"region_id": "RU-MOW",
"map_region_id": "RU-MOW",
"region_name": "Москва",
"federal_district_id": "cfo",
"associations_count": 11,
"opk_organizations_count": 34,
"has_data": true,
"is_highlighted": false,
"tooltip_label": "Москва: 11 кластеров, 34 организации ОПК"
}
],
"federal_districts": [
{
"federal_district_id": "cfo",
"federal_district_name": "Центральный ФО",
"region_ids": ["RU-MOS", "RU-MOW", "RU-YAR", "RU-BEL"],
"associations_count": 37,
"opk_organizations_count": 143,
"has_data": true,
"is_highlighted": false,
"tooltip_label": "Центральный ФО: 37 кластеров, 143 организации ОПК"
}
]
},
"association_list": {
"title": "Кластеры, в числе участников которых есть организации ОПК",
"row_metric_label": "Количество организаций ОПК",
"total_count": 124,
"rows": [
{
"association_id": "cluster-ru-mos-001",
"association_type": "clusters",
"association_name": "Радиоэлектронный кластер Подмосковья",
"region_id": "RU-MOS",
"region_name": "Московская область",
"federal_district_id": "cfo",
"opk_organizations_count": 9,
"opk_memberships_count": 9
}
]
},
"region_ranking": {
"title": "Рейтинг субъектов РФ по участию организаций ОПК в кластерах",
"sort": {
"field": "associations_count",
"direction": "desc"
},
"ranking_method": "ordinal",
"columns": [
{
"field": "associations_count",
"label": "Количество кластеров",
"value_type": "integer"
},
{
"field": "opk_organizations_count",
"label": "Количество организаций ОПК",
"value_type": "integer"
}
],
"rows": [
{
"rank": 1,
"region_id": "RU-MOS",
"region_name": "Московская область",
"federal_district_id": "cfo",
"associations_count": 17,
"opk_organizations_count": 68,
"opk_memberships_count": 68
},
{
"rank": 2,
"region_id": "RU-MOW",
"region_name": "Москва",
"federal_district_id": "cfo",
"associations_count": 11,
"opk_organizations_count": 34,
"opk_memberships_count": 34
}
]
},
"organization_directory": {
"title": "Перечень организаций ОПК",
"endpoint": "/api/v1/competence-map/association-map/organizations/",
"row_granularity": "organization_association_membership",
"total_count": 517,
"unique_organizations_count": 517
}
}
```
---
## Response для `association_type=technoparks`
```json
{
"generated_at": "2026-06-24T15:20:00+03:00",
"data_actual_at": "2026-06-24T00:00:00+03:00",
"association_type": "technoparks",
"applied_filters": {
"association_type": "technoparks",
"map_view_mode": "regions",
"federal_district_id": null,
"region_id": null,
"federal_district_ids": [],
"region_ids": [],
"industry_id": null,
"integrated_structure_id": null,
"organization_id": null,
"competence_id": null
},
"page": {
"title": "Участие организаций ОПК в технопарках",
"subtitle": "Российская Федерация",
"legend_label": "Присутствие одной или более организаций ОПК в технопарке",
"empty_list_label": "По текущим фильтрам технопарки не найдены."
},
"kpis": [
{
"code": "technoparks_count",
"label": "Количество технопарков",
"value": 35,
"value_type": "integer",
"numerator": 35,
"denominator": null
},
{
"code": "opk_organizations_count",
"label": "Количество организаций ОПК",
"value": 66,
"value_type": "integer",
"numerator": 66,
"denominator": null
}
],
"map": {
"view_mode": "regions",
"selected_scope": null,
"selected_region_ids": [],
"selected_federal_district_ids": [],
"selected_chip_text": null,
"highlight_color": "#022456",
"regions": [
{
"region_id": "RU-MOS",
"map_region_id": "RU-MOS",
"region_name": "Московская область",
"federal_district_id": "cfo",
"associations_count": 6,
"opk_organizations_count": 12,
"has_data": true,
"is_highlighted": false,
"tooltip_label": "Московская область: 6 технопарков, 12 организаций ОПК"
}
],
"federal_districts": [
{
"federal_district_id": "cfo",
"federal_district_name": "Центральный ФО",
"region_ids": ["RU-MOS", "RU-MOW", "RU-YAR", "RU-BEL"],
"associations_count": 14,
"opk_organizations_count": 28,
"has_data": true,
"is_highlighted": false,
"tooltip_label": "Центральный ФО: 14 технопарков, 28 организаций ОПК"
}
]
},
"association_list": {
"title": "Технопарки, в числе участников которых есть организации ОПК",
"row_metric_label": "Количество организаций ОПК",
"total_count": 35,
"rows": [
{
"association_id": "technopark-ru-mos-001",
"association_type": "technoparks",
"association_name": "Подмосковный технопарк высоких технологий",
"region_id": "RU-MOS",
"region_name": "Московская область",
"federal_district_id": "cfo",
"opk_organizations_count": 4,
"opk_memberships_count": 4
}
]
},
"region_ranking": {
"title": "Рейтинг субъектов РФ по участию организаций ОПК в технопарках",
"sort": {
"field": "associations_count",
"direction": "desc"
},
"ranking_method": "ordinal",
"columns": [
{
"field": "associations_count",
"label": "Количество технопарков",
"value_type": "integer"
},
{
"field": "opk_organizations_count",
"label": "Количество организаций ОПК",
"value_type": "integer"
}
],
"rows": [
{
"rank": 1,
"region_id": "RU-MOS",
"region_name": "Московская область",
"federal_district_id": "cfo",
"associations_count": 6,
"opk_organizations_count": 12,
"opk_memberships_count": 12
}
]
},
"organization_directory": {
"title": "Перечень организаций ОПК",
"endpoint": "/api/v1/competence-map/association-map/organizations/",
"row_granularity": "organization_association_membership",
"total_count": 66,
"unique_organizations_count": 66
}
}
```
---
## Response для `association_type=sez`
```json
{
"generated_at": "2026-06-24T15:20:00+03:00",
"data_actual_at": "2026-06-24T00:00:00+03:00",
"association_type": "sez",
"applied_filters": {
"association_type": "sez",
"map_view_mode": "regions",
"federal_district_id": null,
"region_id": null,
"federal_district_ids": [],
"region_ids": [],
"industry_id": null,
"integrated_structure_id": null,
"organization_id": null,
"competence_id": null
},
"page": {
"title": "Участие организаций ОПК в особых экономических зонах",
"subtitle": "Российская Федерация",
"legend_label": "Присутствие одной или более организаций ОПК в особой экономической зоне",
"empty_list_label": "По текущим фильтрам особые экономические зоны не найдены."
},
"kpis": [
{
"code": "sez_count",
"label": "Количество ОЭЗ",
"value": 17,
"value_type": "integer",
"numerator": 17,
"denominator": null
},
{
"code": "opk_organizations_count",
"label": "Количество организаций ОПК",
"value": 64,
"value_type": "integer",
"numerator": 64,
"denominator": null
}
],
"map": {
"view_mode": "regions",
"selected_scope": null,
"selected_region_ids": [],
"selected_federal_district_ids": [],
"selected_chip_text": null,
"highlight_color": "#022456",
"regions": [
{
"region_id": "RU-KGN",
"map_region_id": "RU-KGN",
"region_name": "Курганская область",
"federal_district_id": "ufo",
"associations_count": 9,
"opk_organizations_count": 9,
"has_data": true,
"is_highlighted": false,
"tooltip_label": "Курганская область: 9 ОЭЗ, 9 организаций ОПК"
}
],
"federal_districts": [
{
"federal_district_id": "ufo",
"federal_district_name": "Уральский ФО",
"region_ids": ["RU-CHE", "RU-KGN", "RU-SVE"],
"associations_count": 15,
"opk_organizations_count": 21,
"has_data": true,
"is_highlighted": false,
"tooltip_label": "Уральский ФО: 15 ОЭЗ, 21 организация ОПК"
}
]
},
"association_list": {
"title": "Особые экономические зоны, в числе участников которых есть организации ОПК",
"row_metric_label": "Количество организаций ОПК",
"total_count": 17,
"rows": [
{
"association_id": "sez-kurgan-4",
"association_type": "sez",
"association_name": "Особая экономическая зона \"Курганский 4\"",
"region_id": "RU-KGN",
"region_name": "Курганская область",
"federal_district_id": "ufo",
"opk_organizations_count": 9,
"opk_memberships_count": 9
}
]
},
"region_ranking": {
"title": "Рейтинг субъектов РФ по участию организаций ОПК в особых экономических зонах",
"sort": {
"field": "associations_count",
"direction": "desc"
},
"ranking_method": "ordinal",
"columns": [
{
"field": "associations_count",
"label": "Количество ОЭЗ",
"value_type": "integer"
},
{
"field": "opk_organizations_count",
"label": "Количество организаций ОПК",
"value_type": "integer"
}
],
"rows": [
{
"rank": 1,
"region_id": "RU-KGN",
"region_name": "Курганская область",
"federal_district_id": "ufo",
"associations_count": 9,
"opk_organizations_count": 9,
"opk_memberships_count": 9
}
]
},
"organization_directory": {
"title": "Перечень организаций ОПК",
"endpoint": "/api/v1/competence-map/association-map/organizations/",
"row_granularity": "organization_association_membership",
"total_count": 64,
"unique_organizations_count": 64
}
}
```
---
## `competence-map association-map organizations`
### `GET /api/v1/competence-map/association-map/organizations/`
Описание:
- возвращает табличный перечень организаций ОПК для выбранного состояния карты;
- применяет те же фильтры, что `/association-map/`;
- используется для экрана, который открывается по ссылке `Перейти к перечню организаций`;
- возвращает строки таблицы, счетчик `Найдено`, pagination metadata и нормализованные примененные фильтры.
Request:
```text
GET /api/v1/competence-map/association-map/organizations/?association_type=clusters&region_ids=RU-MOS&limit=500&offset=0
```
Query params:
- `association_type`: `clusters | technoparks | sez`, optional, default `clusters`;
- `map_view_mode`: `regions | federal_districts`, optional, default `regions`;
- `federal_district_id`: string, optional;
- `region_id`: string, optional;
- `federal_district_ids`: array of string, optional;
- `region_ids`: array of string, optional;
- `industry_id`: string, optional;
- `integrated_structure_id`: string, optional;
- `organization_id`: string, optional;
- `competence_id`: string, optional;
- `limit`: integer, optional, default `500`, maximum `1000`;
- `offset`: integer, optional, default `0`;
- `sort_field`: string, optional, default `serial_number`;
- `sort_direction`: `asc | desc`, optional, default `asc`.
Response:
```json
{
"generated_at": "2026-06-24T15:20:00+03:00",
"data_actual_at": "2026-06-24T00:00:00+03:00",
"association_type": "clusters",
"applied_filters": {
"association_type": "clusters",
"map_view_mode": "regions",
"federal_district_id": null,
"region_id": null,
"federal_district_ids": [],
"region_ids": ["RU-MOS"],
"industry_id": null,
"integrated_structure_id": null,
"organization_id": null,
"competence_id": null
},
"row_granularity": "organization_association_membership",
"total_count": 68,
"unique_organizations_count": 68,
"pagination": {
"limit": 500,
"offset": 0,
"has_next": false,
"has_previous": false
},
"sort": {
"field": "serial_number",
"direction": "asc"
},
"columns": [
{
"field": "serial_number",
"label": "№ п/п",
"value_type": "integer"
},
{
"field": "organization_name",
"label": "Наименование",
"value_type": "string"
},
{
"field": "director_full_name",
"label": "ФИО руководителя",
"value_type": "string"
},
{
"field": "official_emails",
"label": "Официальная почта",
"value_type": "string_array"
},
{
"field": "official_phones",
"label": "Официальный телефон",
"value_type": "string_array"
},
{
"field": "industry_name",
"label": "Отрасль",
"value_type": "string"
},
{
"field": "activity_type_name",
"label": "Вид деятельности",
"value_type": "string"
},
{
"field": "ownership_type_name",
"label": "Тип собственности",
"value_type": "string"
}
],
"rows": [
{
"row_id": "cluster-ru-mos-001:org-concern-agat",
"serial_number": 1,
"organization_id": "org-concern-agat",
"organization_name": "АО \"Концерн \"Моринсис - Агат\"",
"director_full_name": "Храмов М.Ю.",
"official_emails": ["info@concern-agat.ru", "gen_director@concern-agat.ru"],
"official_phones": ["603-90-05"],
"industry_id": "radio-electronics",
"industry_name": "Радиоэлектронная промышленность",
"activity_type_id": "research-organizations",
"activity_type_name": "Научные организации",
"ownership_type_id": "directly-or-indirectly-controlled",
"ownership_type_name": "Прямо или косвенно контролируемые",
"association_membership": {
"association_type": "clusters",
"association_id": "cluster-ru-mos-001",
"association_name": "Радиоэлектронный кластер Подмосковья",
"region_id": "RU-MOS",
"region_name": "Московская область",
"federal_district_id": "cfo"
}
}
]
}
```
### Требования к `association-map/organizations/`
#### Поле `row_granularity`
Допустимые значения:
- `unique_organization`;
- `organization_association_membership`.
Для текущего табличного представления рекомендуется `organization_association_membership`, потому что
одна организация может участвовать в нескольких объединениях или отображаться в разных отраслевых контекстах.
Требования:
- если `row_granularity = unique_organization`, `total_count` должен быть равен
`unique_organizations_count`;
- если `row_granularity = organization_association_membership`, `total_count` может быть больше
`unique_organizations_count`;
- `row_id` должен быть стабильным id строки, а не только `organization_id`.
#### Поля организации
Требования:
- `organization_id` обязательный;
- `organization_name` обязательный;
- `director_full_name` может быть `null`, если данных нет;
- `official_emails` возвращается массивом строк, может быть пустым массивом;
- `official_phones` возвращается массивом строк, может быть пустым массивом;
- `industry_id` и `industry_name` обязательны, если строка имеет отраслевой контекст;
- `activity_type_id` и `activity_type_name` обязательны, если источник данных содержит вид деятельности;
- `ownership_type_id` и `ownership_type_name` обязательны, если источник данных содержит тип собственности;
- неизвестные значения возвращаются как `null` или пустой массив, а не как строка `-`.
#### Блок `association_membership`
Требования:
- обязателен, если `row_granularity = organization_association_membership`;
- `association_type` должен совпадать с корневым `association_type`;
- `association_id` обязательный;
- `association_name` обязательный;
- `region_id` должен быть совместим с SVG-картой;
- `federal_district_id` обязательный.
---
## Требования к общим полям association-map response
### Поле `generated_at`
Требования:
- ISO 8601 с timezone;
- обязательное;
- показывает время формирования response.
### Поле `data_actual_at`
Требования:
- ISO 8601 с timezone;
- обязательное для `/association-map/` и `/association-map/organizations/`;
- показывает дату и время актуальности данных.
### Поле `association_type`
Требования:
- обязательное;
- должно совпадать с фактически примененным режимом;
- допустимые значения: `clusters`, `technoparks`, `sez`.
### Блок `applied_filters`
Требования:
- обязательный;
- содержит нормализованные значения примененных фильтров;
- отсутствующие одиночные фильтры возвращаются как `null`;
- отсутствующие multi-selection фильтры возвращаются как `[]`;
- `association_type` и `map_view_mode` всегда заполнены.
### Блок `page`
Поля:
- `title`;
- `subtitle`;
- `legend_label`;
- `empty_list_label`.
Требования:
- все поля обязательны для `/association-map/`;
- значения должны соответствовать текущему `association_type`;
- frontend может использовать эти строки напрямую или локализовать их на своей стороне.
### Массив `kpis`
Каждый объект массива - одна KPI-метрика.
Поля:
- `code`: стабильный системный идентификатор метрики;
- `label`: человекочитаемое название метрики;
- `value`: raw numeric value;
- `value_type`: `integer | percent | decimal`;
- `numerator`: числитель, если применимо;
- `denominator`: знаменатель, если применимо.
Требования:
- все поля, кроме `numerator` и `denominator`, обязательны;
- `value` должен быть number, не string;
- `0` допустим как валидное значение;
- для `value_type = percent` значение возвращается в диапазоне `0..100`;
- порядок массива должен соответствовать порядку карточек на странице;
- для текущих страниц карты порядок: count выбранного типа объединения, затем количество организаций ОПК.
### Блок `map`
Поля:
- `view_mode`;
- `selected_scope`;
- `selected_region_ids`;
- `selected_federal_district_ids`;
- `selected_chip_text`;
- `highlight_color`;
- `regions`;
- `federal_districts`.
Требования:
- `view_mode` должен совпадать с `applied_filters.map_view_mode`;
- `selected_scope` должен быть `regions`, `federal_districts` или `null`;
- если применен `region_id` или `region_ids`, `selected_scope = regions`;
- если применен `federal_district_id` или `federal_district_ids`, `selected_scope = federal_districts`;
- `selected_region_ids` и `selected_federal_district_ids` обязательны, даже если пустые;
- selected ids описывают эффективный выбор географии, включая одиночные select-фильтры
`region_id`/`federal_district_id` и multi-selection фильтры `region_ids`/`federal_district_ids`;
- `selected_chip_text` может быть `null`, если ничего не выбрано;
- `highlight_color` optional, но если возвращается, должен быть CSS-compatible color string;
- `regions` должен включать все доступные пользователю субъекты РФ, а не только подсвеченные;
- `federal_districts` должен включать все доступные пользователю федеральные округа, а не только подсвеченные.
### `map.regions`
Каждый объект массива - один субъект РФ.
Поля:
- `region_id`;
- `map_region_id`;
- `region_name`;
- `federal_district_id`;
- `associations_count`;
- `opk_organizations_count`;
- `has_data`;
- `is_highlighted`;
- `tooltip_label`.
Требования:
- `region_id` обязательный;
- `map_region_id` обязательный;
- `map_region_id` должен совпадать с SVG-id региона;
- `associations_count` integer `>= 0`;
- `opk_organizations_count` integer `>= 0`;
- `has_data = true`, если по текущим фильтрам в регионе есть хотя бы одно объединение выбранного типа
или хотя бы одна организация ОПК;
- `is_highlighted = true`, если регион должен быть подсвечен при текущих фильтрах/выборе;
- `tooltip_label` optional, frontend может построить подпись локально из raw-полей.
### `map.federal_districts`
Каждый объект массива - один федеральный округ.
Поля:
- `federal_district_id`;
- `federal_district_name`;
- `region_ids`;
- `associations_count`;
- `opk_organizations_count`;
- `has_data`;
- `is_highlighted`;
- `tooltip_label`.
Требования:
- `federal_district_id` обязательный;
- `region_ids` обязательный;
- `region_ids` должен содержать id субъектов РФ, входящих в ФО и доступных пользователю;
- count-поля должны быть агрегированы по регионам округа с учетом текущих фильтров;
- `is_highlighted = true`, если округ должен быть подсвечен при текущих фильтрах/выборе.
### Блок `association_list`
Поля:
- `title`;
- `row_metric_label`;
- `total_count`;
- `rows`.
Требования:
- `title` должен соответствовать текущему `association_type`;
- `row_metric_label` для текущего UI: `Количество организаций ОПК`;
- `total_count` integer `>= 0`;
- `rows` возвращаются полностью для текущих фильтров;
- строки должны быть отсортированы по `opk_organizations_count` по убыванию, затем
`association_name` по возрастанию (`ru-RU`), если не задан другой backend-порядок.
Каждая строка:
- `association_id` обязательный;
- `association_type` обязательный;
- `association_name` обязательный;
- `region_id` обязательный;
- `region_name` обязательный;
- `federal_district_id` обязательный;
- `opk_organizations_count` integer `>= 0`;
- `opk_memberships_count` integer `>= 0`.
### Блок `region_ranking`
Поля:
- `title`;
- `sort`;
- `ranking_method`;
- `columns`;
- `rows`.
Требования:
- `title` обязательный;
- `sort.field` обязательный;
- `sort.direction` должен быть `asc | desc`;
- `ranking_method` optional, default `ordinal`;
- `columns` обязательный, чтобы frontend не хардкодил `Количество кластеров` для страниц ОЭЗ/технопарков;
- `rows` может быть пустым массивом;
- `rank` возвращается в каждой строке;
- `region_id` обязательный;
- `region_name` обязательный;
- `associations_count` integer `>= 0`;
- `opk_organizations_count` integer `>= 0`.
### Блок `organization_directory`
Поля:
- `title`;
- `endpoint`;
- `row_granularity`;
- `total_count`;
- `unique_organizations_count`.
Требования:
- используется как summary для кнопки/ссылки перехода к таблице организаций;
- `endpoint` должен указывать на `/api/v1/competence-map/association-map/organizations/`;
- `total_count` должен совпадать с `total_count`, который вернет organizations endpoint при тех же фильтрах;
- `unique_organizations_count` должен совпадать с `unique_organizations_count`, который вернет organizations endpoint
при тех же фильтрах.
---
## Порядок массивов
### `association_types`
Рекомендуемый порядок:
1. `clusters`
2. `technoparks`
3. `sez`
### `map_view_modes`
Рекомендуемый порядок:
1. `regions`
2. `federal_districts`
### `map.regions`
Порядок сортировки:
1. `federal_district_id` в порядке справочника федеральных округов;
2. `region_name` по возрастанию (`ru-RU`).
### `map.federal_districts`
Порядок сортировки должен соответствовать порядку федеральных округов на backend-е.
Если такого порядка нет, использовать:
1. `cfo`
2. `nwfo`
3. `yufo`
4. `skfo`
5. `pfo`
6. `ufo`
7. `sfo`
8. `dfo`
### `association_list.rows`
Порядок сортировки:
1. `opk_organizations_count` по убыванию;
2. `association_name` по возрастанию (`ru-RU`);
3. `association_id` по возрастанию.
### `region_ranking.rows`
Порядок сортировки по умолчанию:
1. `associations_count` по убыванию;
2. при равенстве - `opk_organizations_count` по убыванию;
3. при равенстве - `region_name` по возрастанию (`ru-RU`).
Если в блоке передан `ranking_method = dense` или `competition`, одинаковые `rank` допустимы
только для строк с равным основным показателем.
### `organizations.rows`
Порядок сортировки по умолчанию:
1. `serial_number` по возрастанию.
`serial_number` должен соответствовать позиции строки после применения фильтров и сортировки,
а не позиции внутри текущей страницы `limit`/`offset`.
---
## Проверки консистентности
Backend должен гарантировать:
- `association_type` в корне response совпадает с `applied_filters.association_type`;
- `map.view_mode` совпадает с `applied_filters.map_view_mode`;
- `kpis[*].value` согласованы с соответствующими блоками карты и таблицы;
- primary KPI count выбранного типа объединения равен `association_list.total_count`;
- `organization_directory.total_count` равен `total_count` organizations endpoint-а при тех же фильтрах;
- `organization_directory.unique_organizations_count` равен `unique_organizations_count` organizations endpoint-а
при тех же фильтрах;
- `map.regions[*].region_id` существует в справочнике `regions`;
- `map.federal_districts[*].region_ids` ссылаются на существующие `map.regions[*].region_id`;
- `region_ranking.rows[*].region_id` существует в `map.regions`;
- если `selected_scope = regions`, `selected_region_ids` не пустой и `selected_federal_district_ids` пустой;
- если `selected_scope = federal_districts`, `selected_federal_district_ids` не пустой и `selected_region_ids`
пустой;
- если `selected_scope = null`, оба массива selected ids пустые;
- `rank` в рейтингах не дублируется, если `ranking_method = ordinal`;
- все `id` стабильны между запросами;
- все count-поля возвращаются как integer `>= 0`;
- пустые данные возвращаются пустыми массивами, а не отсутствующими полями.
---
## Успешный пустой ответ
Если по фильтрам данных нет, возвращается `200 OK`.
Пример:
```json
{
"generated_at": "2026-06-24T15:20:00+03:00",
"data_actual_at": "2026-06-24T00:00:00+03:00",
"association_type": "clusters",
"applied_filters": {
"association_type": "clusters",
"map_view_mode": "regions",
"federal_district_id": null,
"region_id": "RU-NEN",
"federal_district_ids": [],
"region_ids": [],
"industry_id": null,
"integrated_structure_id": null,
"organization_id": null,
"competence_id": null
},
"page": {
"title": "Участие организаций ОПК в кластерах",
"subtitle": "Российская Федерация",
"legend_label": "Присутствие одной или более организаций ОПК в кластере",
"empty_list_label": "По текущим фильтрам кластеры не найдены."
},
"kpis": [
{
"code": "clusters_count",
"label": "Количество кластеров",
"value": 0,
"value_type": "integer",
"numerator": 0,
"denominator": null
},
{
"code": "opk_organizations_count",
"label": "Количество организаций ОПК",
"value": 0,
"value_type": "integer",
"numerator": 0,
"denominator": null
}
],
"map": {
"view_mode": "regions",
"selected_scope": "regions",
"selected_region_ids": ["RU-NEN"],
"selected_federal_district_ids": [],
"selected_chip_text": "Ненецкий автономный округ: 0 кластеров",
"highlight_color": "#022456",
"regions": [
{
"region_id": "RU-NEN",
"map_region_id": "RU-NEN",
"region_name": "Ненецкий автономный округ",
"federal_district_id": "nwfo",
"associations_count": 0,
"opk_organizations_count": 0,
"has_data": false,
"is_highlighted": false,
"tooltip_label": "Ненецкий автономный округ: данных по текущим фильтрам нет"
}
],
"federal_districts": [
{
"federal_district_id": "nwfo",
"federal_district_name": "Северо-Западный ФО",
"region_ids": ["RU-NEN"],
"associations_count": 0,
"opk_organizations_count": 0,
"has_data": false,
"is_highlighted": false,
"tooltip_label": "Северо-Западный ФО: данных по текущим фильтрам нет"
}
]
},
"association_list": {
"title": "Кластеры, в числе участников которых есть организации ОПК",
"row_metric_label": "Количество организаций ОПК",
"total_count": 0,
"rows": []
},
"region_ranking": {
"title": "Рейтинг субъектов РФ по участию организаций ОПК в кластерах",
"sort": {
"field": "associations_count",
"direction": "desc"
},
"ranking_method": "ordinal",
"columns": [
{
"field": "associations_count",
"label": "Количество кластеров",
"value_type": "integer"
},
{
"field": "opk_organizations_count",
"label": "Количество организаций ОПК",
"value_type": "integer"
}
],
"rows": []
},
"organization_directory": {
"title": "Перечень организаций ОПК",
"endpoint": "/api/v1/competence-map/association-map/organizations/",
"row_granularity": "organization_association_membership",
"total_count": 0,
"unique_organizations_count": 0
}
}
```
---
## Статус-коды и ошибки
### Status code matrix
| Status | Когда возвращается |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `200 OK` | Успешный ответ, включая валидные фильтры без данных. |
| `304 Not Modified` | Conditional GET по `If-None-Match` или `If-Modified-Since`, если представление не изменилось. |
| `400 Bad Request` | Некорректный query param, неизвестный query param, повтор параметра, неверный формат значения, конфликт фильтров. |
| `401 Unauthorized` | Пользователь не аутентифицирован. |
| `403 Forbidden` | Пользователь аутентифицирован, но не имеет доступа к странице или разделу. |
| `429 Too Many Requests` | Превышен rate limit. |
| `500 Internal Server Error` | Непредвиденная ошибка backend-а. |
| `503 Service Unavailable` | Временная недоступность сервиса или источника данных карты. |
Для collection/report endpoint-ов не использовать `404 Not Found`, если path существует, но по валидным фильтрам
нет данных. В этом случае нужен `200 OK` с пустыми массивами.
### Problem Details
Все ошибки возвращаются в формате `application/problem+json`.
Обязательные поля:
- `type`: стабильный URI типа ошибки;
- `title`: короткое человекочитаемое название;
- `status`: HTTP status code;
- `detail`: конкретное описание ошибки;
- `instance`: path и query текущего request-а;
- `code`: стабильный backend-код ошибки для frontend-логики;
- `request_id`: id запроса из `X-Request-Id`.
Frontend не должен парсить `title` или `detail` для бизнес-логики. Для логики используется только `code`
и дополнительные машинные поля.
### Ошибка валидации query params
Для некорректного `association_type`:
HTTP status:
```text
400 Bad Request
```
Response:
```json
{
"type": "https://api.example.ru/problems/invalid-association-type",
"title": "Invalid association_type",
"status": 400,
"detail": "Unsupported association_type: all",
"instance": "/api/v1/competence-map/association-map/?association_type=all",
"code": "invalid_association_type",
"request_id": "req-01JYX4M2K7Y6WQ9RJ8T2V7VY5R",
"received_value": "all",
"allowed_values": ["clusters", "technoparks", "sez"]
}
```
Для несуществующего фильтра:
HTTP status:
```text
400 Bad Request
```
Response:
```json
{
"type": "https://api.example.ru/problems/invalid-filter-value",
"title": "Invalid filter value",
"status": 400,
"detail": "Unknown or unavailable filter value for region_id",
"instance": "/api/v1/competence-map/association-map/?association_type=clusters&region_id=unknown",
"code": "invalid_filter_value",
"request_id": "req-01JYX4M2K7Y6WQ9RJ8T2V7VY5R",
"field": "region_id",
"received_value": "unknown"
}
```
Для конфликтующих фильтров:
HTTP status:
```text
400 Bad Request
```
Response:
```json
{
"type": "https://api.example.ru/problems/conflicting-filters",
"title": "Conflicting filters",
"status": 400,
"detail": "region_id cannot be used together with region_ids",
"instance": "/api/v1/competence-map/association-map/?association_type=clusters&region_id=RU-MOS&region_ids=RU-MOW",
"code": "conflicting_filters",
"request_id": "req-01JYX4M2K7Y6WQ9RJ8T2V7VY5R",
"fields": ["region_id", "region_ids"]
}
```
Для неизвестного query param:
HTTP status:
```text
400 Bad Request
```
Response:
```json
{
"type": "https://api.example.ru/problems/unknown-query-param",
"title": "Unknown query parameter",
"status": 400,
"detail": "Unknown query parameter: district",
"instance": "/api/v1/competence-map/association-map/?district=central",
"code": "unknown_query_param",
"request_id": "req-01JYX4M2K7Y6WQ9RJ8T2V7VY5R",
"field": "district",
"allowed_params": [
"association_type",
"map_view_mode",
"federal_district_id",
"region_id",
"federal_district_ids",
"region_ids",
"industry_id",
"integrated_structure_id",
"organization_id",
"competence_id",
"limit",
"offset",
"sort_field",
"sort_direction"
]
}
```
### Ошибки доступа и инфраструктуры
Для `401`, `403`, `429`, `500`, `503` используется тот же формат `application/problem+json`.
Пример `403 Forbidden`:
```json
{
"type": "https://api.example.ru/problems/forbidden",
"title": "Forbidden",
"status": 403,
"detail": "User does not have access to competence map association map",
"instance": "/api/v1/competence-map/association-map/",
"code": "forbidden",
"request_id": "req-01JYX4M2K7Y6WQ9RJ8T2V7VY5R"
}
```
Для `429 Too Many Requests` backend должен вернуть header:
```text
Retry-After: <seconds>
```
---
## Ожидаемая схема запросов
При загрузке `/new-map`:
```text
GET /api/v1/competence-map/association-map/filters/?association_type=clusters
GET /api/v1/competence-map/association-map/?association_type=clusters
```
При загрузке `/new-map/technoparks`:
```text
GET /api/v1/competence-map/association-map/filters/?association_type=technoparks
GET /api/v1/competence-map/association-map/?association_type=technoparks
```
При загрузке `/new-map/sez`:
```text
GET /api/v1/competence-map/association-map/filters/?association_type=sez
GET /api/v1/competence-map/association-map/?association_type=sez
```
При выборе нескольких субъектов РФ на карте:
```text
GET /api/v1/competence-map/association-map/filters/?association_type=clusters&map_view_mode=regions&region_ids=RU-MOS&region_ids=RU-MOW
GET /api/v1/competence-map/association-map/?association_type=clusters&map_view_mode=regions&region_ids=RU-MOS&region_ids=RU-MOW
```
При выборе нескольких федеральных округов на карте:
```text
GET /api/v1/competence-map/association-map/filters/?association_type=sez&map_view_mode=federal_districts&federal_district_ids=cfo&federal_district_ids=pfo
GET /api/v1/competence-map/association-map/?association_type=sez&map_view_mode=federal_districts&federal_district_ids=cfo&federal_district_ids=pfo
```
При переходе к перечню организаций:
```text
GET /api/v1/competence-map/association-map/organizations/?association_type=clusters&map_view_mode=regions&region_ids=RU-MOS&region_ids=RU-MOW&limit=500&offset=0
```
Не требуется реализовывать отдельные endpoint-ы вида:
```text
GET /api/v1/competence-map/association-map/clusters/
GET /api/v1/competence-map/association-map/technoparks/
GET /api/v1/competence-map/association-map/sez/
GET /api/v1/competence-map/association-map/select-region/
GET /api/v1/competence-map/association-map/select-federal-district/
```
Если в будущем появятся отдельные действия, например экспорт таблицы организаций или drilldown по строке,
для них можно добавить отдельные endpoint-ы. Для текущих состояний страниц из референсных экранов достаточно
`/association-map/filters/`, `/association-map/` и `/association-map/organizations/`.