# 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®ion_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®ion_ids=RU-MOS®ion_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®ion_ids=RU-MOS&limit=500&offset=0 ``` Не использовать как canonical form: ```text GET /api/v1/competence-map/association-map/?association_type=clusters®ion_id=&federal_district_id= GET /api/v1/competence-map/association-map/?association_type=clusters®ion_id=RU-MOS®ion_ids=RU-MOW ``` Первый request можно поддержать для совместимости, но backend все равно должен вернуть пустые значения как `null` или `[]` в `applied_filters`. Второй request должен возвращать `400 Bad Request`, потому что одиночный фильтр и multi-selection фильтр конфликтуют. ### Headers и трассировка Каждый response должен содержать: ```text X-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: "" Last-Modified: ``` Требования: - `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®ion_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®ion_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®ion_id=RU-MOS®ion_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: ``` --- ## Ожидаемая схема запросов При загрузке `/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®ion_ids=RU-MOS®ion_ids=RU-MOW GET /api/v1/competence-map/association-map/?association_type=clusters&map_view_mode=regions®ion_ids=RU-MOS®ion_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®ion_ids=RU-MOS®ion_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/`.