# Backend Endpoints Competence Map Analytics Назначение: спецификация backend-контракта для страницы `Все сервисы / Карта компетенций / Аналитическая панель`. Документ нужен как задача для backend-разработки. Цель: реализовать endpoint-ы, которые возвращают все данные аналитической панели по инновационным объединениям: - значения фильтров; - верхние KPI-карточки; - состав инновационных объединений; - аналитику по ОЭЗ; - аналитику по технопаркам; - аналитику по промышленным кластерам; - рейтинги субъектов РФ; - топ отраслей; - временную динамику; - численность сотрудников. ## Принципы контракта - аналитика страницы должна возвращаться одним page-level endpoint-ом; - справочники фильтров должны возвращаться отдельным endpoint-ом; - контракт описывает серверное состояние страницы, а не конкретную визуальную реализацию UI; - frontend может локально решать, показывать `association_type` как select или tabs, какие иконки использовать, как располагать карточки, как рисовать donut/bar/line charts; - backend должен возвращать все данные, необходимые для всех серверно значимых состояний страницы: значения фильтров, применённые фильтры, KPI, строки таблиц, точки графиков, числители/знаменатели, ранги, id сущностей и значения для drilldown; - все значения должны возвращаться в raw-формате: number, string, boolean, null; - проценты должны возвращаться числом `0..100`, а не долей `0..1`; - даты должны возвращаться в ISO 8601 с timezone; - каждый объект, который может использоваться для drilldown или ссылки, должен иметь стабильный `id`; - все массивы должны возвращаться уже отсортированными; - все KPI должны иметь стабильный `code`; - все проценты должны иметь `numerator` и `denominator`, если знаменатель известен; - `0` должен возвращаться как валидное значение, а не как `null`, строка или `—`; - `null` допустим только для реально отсутствующего или неприменимого значения; - backend должен обеспечивать консистентность KPI, таблиц и графиков внутри одного response. Важное ограничение по числам в примерах: - числовые значения в JSON-примерах взяты из референсных экранов и нужны для описания формы контракта; - реальные знаменатели процентов должны быть рассчитаны по утверждённой бизнес-методике; - если методика конкретного процента не утверждена, её нужно зафиксировать до реализации endpoint-а. - если число на референсном экране противоречит смыслу поля, backend-контракт должен сохранять корректную семантику поля, а не повторять ошибочное или placeholder-значение макета. --- ## REST и HTTP правила ### Модель ресурсов Endpoint-ы описывают серверные представления ресурсов, а не действия UI. В рамках этой страницы есть два ресурса: - `/api/v1/competence-map/analytics/filters/` - справочники и доступные значения фильтров; - `/api/v1/competence-map/analytics/` - аналитический snapshot страницы для выбранных фильтров. Требования: - не добавлять action-style endpoint-ы вида `/getAnalytics`, `/loadFilters`, `/calculateDashboard`; - не дробить текущую страницу на endpoint-ы по виджетам, пока нет отдельной серверной причины: экспорт, серверная пагинация большой таблицы, drilldown, async job; - `association_type` является query-параметром одного аналитического ресурса, а не отдельным URI-сегментом; - 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 Для `/analytics/filters/` и `/analytics/` используется единый набор query params: - `association_type`: `all | sez | technoparks | clusters`, optional, default `all`; - `federal_district_id`: string, optional; - `region_id`: string, optional; - `industry_id`: string, optional; - `integrated_structure_id`: string, optional; - `organization_id`: string, optional. Правила: - если фильтр не выбран, параметр лучше не передавать; - если frontend передал пустую строку, backend должен нормализовать её в `null` в `applied_filters`; - неизвестные query params должны возвращать `400 Bad Request`, чтобы опечатки не игнорировались молча; - повтор одного и того же query param запрещён, если для параметра явно не описана поддержка массива; - значения `id` сравниваются как case-sensitive строки; - все применённые значения backend возвращает в `applied_filters` уже нормализованными; - если `region_id` передан вместе с `federal_district_id`, backend должен проверить, что регион входит в указанный федеральный округ; - если фильтры валидны, но данных по ним нет, возвращается `200 OK` с пустыми массивами; - если значение фильтра не существует или недоступно пользователю, возвращается ошибка валидации. Canonical examples: ```text GET /api/v1/competence-map/analytics/filters/ GET /api/v1/competence-map/analytics/?association_type=all GET /api/v1/competence-map/analytics/?association_type=technoparks&federal_district_id=central®ion_id=moscow-oblast ``` Не использовать как canonical form: ```text GET /api/v1/competence-map/analytics/?association_type=all&federal_district_id=®ion_id= ``` Такой request можно поддержать для совместимости, но backend всё равно должен вернуть пустые значения как `null` в `applied_filters`. ### 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` должен быть единым для всех блоков response; - если разные блоки приходят из источников с разной актуальностью, backend должен либо привести их к единому согласованному snapshot-у, либо явно согласовать и задокументировать методику; - версия методики расчёта процентов, уникальности организаций, участий и сотрудников должна быть зафиксирована в backend-документации; - если backend поддерживает версионирование методик, response может дополнительно возвращать `calculation_methodology_version`; - изменение методики, которое меняет смысл существующих полей, считается breaking change для `/api/v1`. ### Большие справочники и массивы Dashboard-массивы `rows`, `segments`, `points` возвращаются полностью, если для блока не описана серверная пагинация. Для справочника `organizations` возможен большой объём данных. Если список может превышать безопасный размер для одного response, backend должен выбрать один из вариантов до реализации: - возвращать только доступные организации в пределах текущих фильтров, если объём гарантированно небольшой; - добавить к `/analytics/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` Тип инновационного объединения. Допустимые значения: - `all`: все типы объединений; - `sez`: особые экономические зоны; - `technoparks`: технопарки; - `clusters`: промышленные кластеры. ### `associations_count` Количество инновационных объединений выбранного типа после применения фильтров. Примеры: - для `sez` это количество ОЭЗ; - для `technoparks` это количество технопарков; - для `clusters` это количество промышленных кластеров; - для `all` это сумма объединений всех типов. ### `opk_organizations_count` Количество уникальных организаций ОПК после применения фильтров. Одна организация ОПК может входить в несколько объединений. Поэтому это поле не должно считаться как простая сумма участий. ### `opk_memberships_count` Количество связей `организация ОПК x инновационное объединение`. Это поле может быть больше `opk_organizations_count`. ### `residents_count` Количество резидентов. Используется для ОЭЗ и технопарков, если источник данных различает резидентов и участников. ### `participants_count` Количество участников. Используется для промышленных кластеров. ### `employees_count` Численность сотрудников. Для показателей сотрудников нужно явно различать: - сотрудников организаций ОПК; - сотрудников организаций ОПК-резидентов; - сотрудников всех резидентов; - сотрудников участников кластера; - сотрудников выбранного типа объединений. ### Региональные count-поля В рейтингах субъектов РФ count-поля должны описывать разные сущности однозначно. Для `association_type = sez`: - `sez_count`: количество ОЭЗ в субъекте РФ после применения фильтров; - `opk_organizations_count`: количество уникальных организаций ОПК, связанных с ОЭЗ в субъекте РФ; - `opk_memberships_count`: количество связей `организация ОПК x ОЭЗ`, если backend показывает именно участия, а не уникальные организации. Для `association_type = technoparks`: - `technoparks_count`: количество технопарков в субъекте РФ после применения фильтров; - `opk_organizations_count`: количество уникальных организаций ОПК, связанных с технопарками в субъекте РФ; - `opk_memberships_count`: количество связей `организация ОПК x технопарк`, если backend показывает участия. Для `association_type = clusters`: - `clusters_count`: количество промышленных кластеров в субъекте РФ после применения фильтров; - `opk_organizations_count`: количество уникальных организаций ОПК, участвующих в кластерах в субъекте РФ; - `opk_memberships_count`: количество связей `организация ОПК x промышленный кластер`, если backend показывает участия. Нельзя использовать `sez_count`, `technoparks_count` или `clusters_count`, если значение означает количество организаций ОПК. Для организаций ОПК нужно использовать `opk_organizations_count` или более точное поле с префиксом выбранного типа объединения. ### `rank` Позиция строки в рейтинге или топе. Требования: - `rank` возвращается backend-ом как integer `>= 1`; - если используется обычное порядковое ранжирование, `rank` не должен дублироваться; - если бизнес-методика допускает одинаковые места при равных значениях, блок должен вернуть `ranking_method`; - допустимые значения `ranking_method`: `ordinal`, `dense`, `competition`; - если `ranking_method` не передан, frontend считает методику `ordinal`. ### Полнота массивов Если в блоке нет явного `limit`, массив `rows`, `segments` или `points` должен возвращаться полностью для текущего состояния страницы и текущих фильтров. Frontend может отображать часть данных в видимой области и скроллить контейнер локально, но backend не должен неявно обрезать массив до первых 2-3 строк только потому, что в макете виден короткий пример. --- ## Нужно реализовать ## `competence-map analytics filters` ### `GET /api/v1/competence-map/analytics/filters/` Описание: - возвращает справочники для фильтров аналитической панели; - учитывает права пользователя; - учитывает текущие выбранные фильтры, если они переданы в query params; - возвращает серверно доступные значения `association_type`; - не возвращает аналитические KPI, рейтинги и графики; - не управляет визуальным представлением фильтров: frontend может локально показывать типы объединений как select, tabs или segmented control. Request: ```text GET /api/v1/competence-map/analytics/filters/?association_type=all ``` Query params: - `association_type`: `all | sez | technoparks | clusters`, optional, default `all`; - `federal_district_id`: string, optional; - `region_id`: string, optional; - `industry_id`: string, optional; - `integrated_structure_id`: string, optional; - `organization_id`: string, optional. Response: ```json { "generated_at": "2026-06-19T15:20:00+03:00", "applied_filters": { "association_type": "all", "federal_district_id": null, "region_id": null, "industry_id": null, "integrated_structure_id": null, "organization_id": null }, "association_types": [ { "id": "all", "label": "все", "associations_count": 176, "disabled": false }, { "id": "sez", "label": "ОЭЗ", "associations_count": 17, "disabled": false }, { "id": "technoparks", "label": "технопарки", "associations_count": 35, "disabled": false }, { "id": "clusters", "label": "кластеры", "associations_count": 124, "disabled": false } ], "federal_districts": [ { "id": "central", "label": "Центральный федеральный округ", "associations_count": 58, "disabled": false } ], "regions": [ { "id": "moscow-oblast", "federal_district_id": "central", "label": "Московская область", "associations_count": 41, "disabled": false } ], "industries": [ { "id": "aviation", "label": "Авиационная промышленность", "opk_organizations_count": 180, "disabled": false } ], "integrated_structures": [ { "id": "rostec", "label": "ГК Ростех", "opk_organizations_count": 42, "disabled": false } ], "organizations": [ { "id": "org-001", "label": "АО Пример", "inn": "7700000000", "ogrn": "1027700000000", "association_memberships_count": 1, "disabled": false } ] } ``` ### Требования к `analytics/filters/` #### Поле `generated_at` Требования: - ISO 8601 с timezone; - обязательное поле; - единое время генерации для всего payload. #### Блок `applied_filters` Требования: - возвращает фактически применённые фильтры; - все отсутствующие значения возвращаются как `null`; - `association_type` всегда заполнен. #### Массив `association_types` Требования: - содержит все доступные пользователю типы объединений; - `id` должен быть одним из `all`, `sez`, `technoparks`, `clusters`; - `label` обязательный; - `associations_count` integer `>= 0`; - `disabled` boolean. Назначение: - является серверным источником доступных состояний аналитической страницы; - порядок массива определяет рекомендуемый порядок типов объединений; - если типы объединений показываются в UI как tabs, каждый tab соответствует одному `id`; - при выборе tab/select frontend передаёт выбранный `id` как query param `association_type`; - `associations_count` может использоваться как серверное значение для badge/count рядом с типом; - `disabled = true` означает, что тип недоступен пользователю или неприменим при текущих фильтрах. #### Массивы справочников Для массивов `federal_districts`, `regions`, `industries`, `integrated_structures`, `organizations`: - `id` обязательный; - `label` обязательный; - `disabled` обязательный; - count-поля должны быть integer `>= 0`; - если по текущим фильтрам список пустой, возвращается пустой массив. --- ## `competence-map analytics` ### `GET /api/v1/competence-map/analytics/` Описание: - возвращает всю аналитику страницы для выбранного `association_type`; - применяет все переданные фильтры; - возвращает KPI, основной аналитический блок, рейтинги, топ отраслей, временную динамику и дополнительные показатели; - не требует дополнительных аналитических endpoint-ов для отдельных виджетов. Request: ```text GET /api/v1/competence-map/analytics/?association_type=all ``` Query params: - `association_type`: `all | sez | technoparks | clusters`, optional, default `all`; - `federal_district_id`: string, optional; - `region_id`: string, optional; - `industry_id`: string, optional; - `integrated_structure_id`: string, optional; - `organization_id`: string, optional. Общий response envelope: ```json { "generated_at": "2026-06-19T15:20:00+03:00", "data_actual_at": "2026-06-19T00:00:00+03:00", "association_type": "all", "applied_filters": { "association_type": "all", "federal_district_id": null, "region_id": null, "industry_id": null, "integrated_structure_id": null, "organization_id": null }, "kpis": [], "main_block": {}, "region_ranking": {}, "top_industries": {}, "trend_block": null, "employee_stats": null } ``` ### Серверные состояния страницы по `association_type` Страница имеет четыре серверных состояния. Они отличаются составом данных, но используют один и тот же endpoint `/api/v1/competence-map/analytics/`. | `association_type` | KPI | `main_block.type` | `region_ranking` | `top_industries.mode` | `trend_block` | `employee_stats` | |---|---|---|---|---|---|---| | `all` | 4 карточки по всем объединениям | `all_associations_composition` | 3 метрики по типам объединений | `stacked_by_association_type` | `opk_participation_by_year` | `null` | | `sez` | 4 карточки по ОЭЗ | `sez_types_and_priority_directions` | ОЭЗ + организации ОПК | `single_association_type` | `null` | сотрудники ОПК-резидентов ОЭЗ и сотрудники ОЭЗ | | `technoparks` | 4 карточки по технопаркам | `technopark_area_distribution` | технопарки + организации ОПК | `single_association_type` | `null` | сотрудники ОПК-резидентов технопарков и их доля | | `clusters` | 4 карточки по кластерам | `cluster_specializations` | кластеры + организации ОПК | `single_association_type` | `cluster_creation_dynamics` | `null` | Требования: - `association_type` в корне response должен определять структуру `main_block`, применимость `trend_block` и применимость `employee_stats`; - frontend не должен делать дополнительные аналитические запросы при переключении между этими состояниями, кроме повторного запроса `/analytics/filters/` и `/analytics/`; - если тип объединения вынесен в UI как tabs, выбранный tab всё равно передаётся как query param `association_type`. --- ## Response для `association_type=all` ```json { "generated_at": "2026-06-19T15:20:00+03:00", "data_actual_at": "2026-06-19T00:00:00+03:00", "association_type": "all", "applied_filters": { "association_type": "all", "federal_district_id": null, "region_id": null, "industry_id": null, "integrated_structure_id": null, "organization_id": null }, "kpis": [ { "code": "innovation_associations_count", "label": "Количество инновационных объединений", "value": 176, "value_type": "integer", "numerator": 176, "denominator": null }, { "code": "opk_organizations_count", "label": "Количество организаций ОПК", "value": 605, "value_type": "integer", "numerator": 605, "denominator": null }, { "code": "opk_organizations_in_associations_share_percent", "label": "Составляют организации ОПК в инновационных объединениях", "value": 20.0, "value_type": "percent", "numerator": 605, "denominator": 3025 }, { "code": "associations_with_opk_share_percent", "label": "Процент инновационных объединений, в которых участвуют организации ОПК", "value": 42.0, "value_type": "percent", "numerator": 176, "denominator": 419 } ], "main_block": { "type": "all_associations_composition", "title": "Организации ОПК в инновационных объединениях", "total_associations_count": 176, "segments": [ { "association_type": "sez", "label": "особые экономические зоны", "associations_count": 17, "opk_organizations_count": 64, "opk_memberships_count": 64 }, { "association_type": "technoparks", "label": "технопарки", "associations_count": 35, "opk_organizations_count": 66, "opk_memberships_count": 66 }, { "association_type": "clusters", "label": "промышленные кластеры", "associations_count": 124, "opk_organizations_count": 517, "opk_memberships_count": 517 } ] }, "region_ranking": { "title": "Рейтинг субъектов РФ по участию организаций ОПК в инновационных объединениях", "sort": { "field": "total_opk_organizations_count", "direction": "desc" }, "ranking_method": "ordinal", "rows": [ { "rank": 1, "region_id": "moscow-oblast", "region_name": "Московская область", "cluster_opk_organizations_count": 41, "technopark_opk_organizations_count": 41, "sez_opk_organizations_count": 63, "total_opk_organizations_count": 145 }, { "rank": 2, "region_id": "chelyabinsk-oblast", "region_name": "Челябинская область", "cluster_opk_organizations_count": 23, "technopark_opk_organizations_count": 23, "sez_opk_organizations_count": 46, "total_opk_organizations_count": 92 }, { "rank": 3, "region_id": "moscow", "region_name": "г. Москва", "cluster_opk_organizations_count": 28, "technopark_opk_organizations_count": 28, "sez_opk_organizations_count": 32, "total_opk_organizations_count": 88 }, { "rank": 4, "region_id": "tatarstan", "region_name": "Республика Татарстан", "cluster_opk_organizations_count": 22, "technopark_opk_organizations_count": 22, "sez_opk_organizations_count": 31, "total_opk_organizations_count": 75 }, { "rank": 5, "region_id": "samara-region", "region_name": "Самарская область", "cluster_opk_organizations_count": 19, "technopark_opk_organizations_count": 19, "sez_opk_organizations_count": 21, "total_opk_organizations_count": 59 }, { "rank": 6, "region_id": "penza-region", "region_name": "Пензенская область", "cluster_opk_organizations_count": 17, "technopark_opk_organizations_count": 17, "sez_opk_organizations_count": 19, "total_opk_organizations_count": 53 }, { "rank": 7, "region_id": "tomsk-region", "region_name": "Томская область", "cluster_opk_organizations_count": 12, "technopark_opk_organizations_count": 12, "sez_opk_organizations_count": 17, "total_opk_organizations_count": 41 }, { "rank": 8, "region_id": "yaroslavl-region", "region_name": "Ярославская область", "cluster_opk_organizations_count": 10, "technopark_opk_organizations_count": 10, "sez_opk_organizations_count": 18, "total_opk_organizations_count": 38 }, { "rank": 9, "region_id": "udmurtia", "region_name": "Удмуртская Республика", "cluster_opk_organizations_count": 2, "technopark_opk_organizations_count": 2, "sez_opk_organizations_count": 20, "total_opk_organizations_count": 24 }, { "rank": 10, "region_id": "ulyanovsk-region", "region_name": "Ульяновская область", "cluster_opk_organizations_count": 5, "technopark_opk_organizations_count": 5, "sez_opk_organizations_count": 13, "total_opk_organizations_count": 23 }, { "rank": 11, "region_id": "khabarovsk-krai", "region_name": "Хабаровский край", "cluster_opk_organizations_count": 4, "technopark_opk_organizations_count": 4, "sez_opk_organizations_count": 14, "total_opk_organizations_count": 22 }, { "rank": 12, "region_id": "volgograd-region", "region_name": "Волгоградская область", "cluster_opk_organizations_count": 2, "technopark_opk_organizations_count": 2, "sez_opk_organizations_count": 14, "total_opk_organizations_count": 18 }, { "rank": 13, "region_id": "bashkortostan", "region_name": "Республика Башкортостан", "cluster_opk_organizations_count": 4, "technopark_opk_organizations_count": 4, "sez_opk_organizations_count": 8, "total_opk_organizations_count": 16 }, { "rank": 14, "region_id": "krasnodar-krai", "region_name": "Краснодарский край", "cluster_opk_organizations_count": 3, "technopark_opk_organizations_count": 3, "sez_opk_organizations_count": 10, "total_opk_organizations_count": 16 }, { "rank": 15, "region_id": "belgorod-region", "region_name": "Белгородская область", "cluster_opk_organizations_count": 2, "technopark_opk_organizations_count": 2, "sez_opk_organizations_count": 7, "total_opk_organizations_count": 11 } ] }, "top_industries": { "title": "Топ отраслей по количеству организаций ОПК, участвующих в инновационных объединениях", "mode": "stacked_by_association_type", "association_type": null, "max_value": 180, "rows": [ { "industry_id": "aviation", "industry_name": "Авиационная промышленность", "total_opk_organizations_count": 180, "cluster_opk_organizations_count": 84, "technopark_opk_organizations_count": 82, "sez_opk_organizations_count": 14 }, { "industry_id": "conventional-weapons", "industry_name": "Промышленность обычных вооружений", "total_opk_organizations_count": 124, "cluster_opk_organizations_count": 72, "technopark_opk_organizations_count": 38, "sez_opk_organizations_count": 14 }, { "industry_id": "ammunition-and-special-chemistry", "industry_name": "Промышленность боеприпасов и спецхимии", "total_opk_organizations_count": 110, "cluster_opk_organizations_count": 45, "technopark_opk_organizations_count": 55, "sez_opk_organizations_count": 10 }, { "industry_id": "radio-electronics", "industry_name": "Радиоэлектронная промышленность", "total_opk_organizations_count": 101, "cluster_opk_organizations_count": 43, "technopark_opk_organizations_count": 48, "sez_opk_organizations_count": 10 }, { "industry_id": "shipbuilding", "industry_name": "Судостроительная промышленность", "total_opk_organizations_count": 78, "cluster_opk_organizations_count": 46, "technopark_opk_organizations_count": 20, "sez_opk_organizations_count": 12 }, { "industry_id": "robotics", "industry_name": "Беспилотные системы и робототехника", "total_opk_organizations_count": 12, "cluster_opk_organizations_count": 4, "technopark_opk_organizations_count": 6, "sez_opk_organizations_count": 2 } ] }, "trend_block": { "type": "opk_participation_by_year", "title": "Участие организаций ОПК в инновационных объединениях", "points": [ { "year": 2014, "cluster_opk_organizations_count": 24, "technopark_opk_organizations_count": 7, "sez_opk_organizations_count": 5, "total_opk_organizations_count": 36 }, { "year": 2016, "cluster_opk_organizations_count": 42, "technopark_opk_organizations_count": 9, "sez_opk_organizations_count": 6, "total_opk_organizations_count": 57 }, { "year": 2018, "cluster_opk_organizations_count": 54, "technopark_opk_organizations_count": 11, "sez_opk_organizations_count": 8, "total_opk_organizations_count": 73 }, { "year": 2020, "cluster_opk_organizations_count": 58, "technopark_opk_organizations_count": 16, "sez_opk_organizations_count": 10, "total_opk_organizations_count": 84 }, { "year": 2022, "cluster_opk_organizations_count": 66, "technopark_opk_organizations_count": 18, "sez_opk_organizations_count": 12, "total_opk_organizations_count": 96 }, { "year": 2024, "cluster_opk_organizations_count": 80, "technopark_opk_organizations_count": 22, "sez_opk_organizations_count": 14, "total_opk_organizations_count": 116 } ] }, "employee_stats": null } ``` --- ## Response для `association_type=sez` ```json { "generated_at": "2026-06-19T15:20:00+03:00", "data_actual_at": "2026-06-19T00:00:00+03:00", "association_type": "sez", "applied_filters": { "association_type": "sez", "federal_district_id": null, "region_id": null, "industry_id": null, "integrated_structure_id": null, "organization_id": null }, "kpis": [ { "code": "sez_count", "label": "Количество ОЭЗ", "value": 17, "value_type": "integer", "numerator": 17, "denominator": null }, { "code": "sez_with_opk_residents_share_percent", "label": "Процент ОЭЗ, в числе резидентов которых есть организации ОПК", "value": 32.0, "value_type": "percent", "numerator": 17, "denominator": 53 }, { "code": "opk_resident_organizations_count", "label": "Количество организаций ОПК-резидентов ОЭЗ", "value": 64, "value_type": "integer", "numerator": 64, "denominator": null }, { "code": "opk_organizations_in_sez_share_percent", "label": "Составляют организации ОПК в ОЭЗ", "value": 15.0, "value_type": "percent", "numerator": 64, "denominator": 427 } ], "main_block": { "type": "sez_types_and_priority_directions", "title": "Типы и приоритетные направления деятельности ОЭЗ", "sez_types": { "title": "Типы ОЭЗ", "total_sez_count": 17, "segments": [ { "sez_type": "industrial_production", "label": "промышленно-производственные ОЭЗ", "sez_count": 8, "opk_organizations_count": 31 }, { "sez_type": "technology_implementation", "label": "технико-внедренческие ОЭЗ", "sez_count": 7, "opk_organizations_count": 27 }, { "sez_type": "port", "label": "портовые ОЭЗ", "sez_count": 2, "opk_organizations_count": 6 } ] }, "priority_directions": { "title": "Приоритетные направления деятельности ОЭЗ", "sort": { "field": "sez_count", "direction": "desc" }, "ranking_method": "ordinal", "rows": [ { "rank": 1, "direction_id": "ict", "direction_name": "Информационно-коммуникационные технологии", "sez_count": 4 }, { "rank": 2, "direction_id": "aircraft-maintenance", "direction_name": "Обслуживание и ремонт воздушных судов", "sez_count": 3 }, { "rank": 3, "direction_id": "wood-processing", "direction_name": "Лесопереработка", "sez_count": 2 }, { "rank": 4, "direction_id": "logistics", "direction_name": "Логистика", "sez_count": 2 }, { "rank": 5, "direction_id": "nanotechnology-and-new-materials", "direction_name": "Нанотехнологии и новые материалы", "sez_count": 2 }, { "rank": 6, "direction_id": "titanium-products", "direction_name": "Производство изделий из титана", "sez_count": 1 }, { "rank": 7, "direction_id": "pharmaceutical-industry", "direction_name": "Фармацевтическая промышленность", "sez_count": 1 }, { "rank": 8, "direction_id": "storage-and-logistics", "direction_name": "Хранение и логистика", "sez_count": 1 }, { "rank": 9, "direction_id": "high-tech-processing-production", "direction_name": "Высокотехнологичные обрабатывающие производства", "sez_count": 1 }, { "rank": 10, "direction_id": "woodworking", "direction_name": "Деревообработка", "sez_count": 1 } ] } }, "region_ranking": { "title": "Рейтинг субъектов РФ по участию организаций ОПК в ОЭЗ", "sort": { "field": "opk_organizations_count", "direction": "desc" }, "ranking_method": "ordinal", "rows": [ { "rank": 1, "region_id": "moscow-oblast", "region_name": "Московская область", "sez_count": 3, "opk_organizations_count": 41 }, { "rank": 2, "region_id": "moscow", "region_name": "г. Москва", "sez_count": 2, "opk_organizations_count": 28 }, { "rank": 3, "region_id": "chelyabinsk-oblast", "region_name": "Челябинская область", "sez_count": 2, "opk_organizations_count": 23 }, { "rank": 4, "region_id": "tatarstan", "region_name": "Республика Татарстан", "sez_count": 1, "opk_organizations_count": 22 }, { "rank": 5, "region_id": "samara-region", "region_name": "Самарская область", "sez_count": 1, "opk_organizations_count": 19 }, { "rank": 6, "region_id": "penza-region", "region_name": "Пензенская область", "sez_count": 1, "opk_organizations_count": 17 }, { "rank": 7, "region_id": "tomsk-region", "region_name": "Томская область", "sez_count": 1, "opk_organizations_count": 12 }, { "rank": 8, "region_id": "yaroslavl-region", "region_name": "Ярославская область", "sez_count": 1, "opk_organizations_count": 10 }, { "rank": 9, "region_id": "ulyanovsk-region", "region_name": "Ульяновская область", "sez_count": 1, "opk_organizations_count": 5 }, { "rank": 10, "region_id": "khabarovsk-krai", "region_name": "Хабаровский край", "sez_count": 1, "opk_organizations_count": 4 }, { "rank": 11, "region_id": "bashkortostan", "region_name": "Республика Башкортостан", "sez_count": 1, "opk_organizations_count": 4 }, { "rank": 12, "region_id": "krasnodar-krai", "region_name": "Краснодарский край", "sez_count": 1, "opk_organizations_count": 3 }, { "rank": 13, "region_id": "belgorod-region", "region_name": "Белгородская область", "sez_count": 1, "opk_organizations_count": 2 } ] }, "top_industries": { "title": "Топ отраслей по количеству организаций ОПК, участвующих в ОЭЗ", "mode": "single_association_type", "association_type": "sez", "max_value": 180, "rows": [ { "industry_id": "aviation", "industry_name": "Авиационная промышленность", "opk_organizations_count": 180 }, { "industry_id": "conventional-weapons", "industry_name": "Промышленность обычных вооружений", "opk_organizations_count": 124 }, { "industry_id": "ammunition-and-special-chemistry", "industry_name": "Промышленность боеприпасов и спецхимии", "opk_organizations_count": 110 }, { "industry_id": "radio-electronics", "industry_name": "Радиоэлектронная промышленность", "opk_organizations_count": 101 }, { "industry_id": "shipbuilding", "industry_name": "Судостроительная промышленность", "opk_organizations_count": 78 }, { "industry_id": "robotics", "industry_name": "Беспилотные системы и робототехника", "opk_organizations_count": 12 } ] }, "trend_block": null, "employee_stats": { "items": [ { "code": "opk_resident_employees_count", "label": "Численность сотрудников организаций ОПК-резидентов ОЭЗ", "value": 1678, "value_type": "integer", "numerator": 1678, "denominator": null }, { "code": "sez_employees_count", "label": "Численность сотрудников ОЭЗ", "value": 5235, "value_type": "integer", "numerator": 5235, "denominator": null } ] } } ``` --- ## Response для `association_type=technoparks` ```json { "generated_at": "2026-06-19T15:20:00+03:00", "data_actual_at": "2026-06-19T00:00:00+03:00", "association_type": "technoparks", "applied_filters": { "association_type": "technoparks", "federal_district_id": null, "region_id": null, "industry_id": null, "integrated_structure_id": null, "organization_id": null }, "kpis": [ { "code": "technoparks_with_opk_residents_count", "label": "Количество технопарков, в числе резидентов которых есть организации ОПК", "value": 35, "value_type": "integer", "numerator": 35, "denominator": null }, { "code": "opk_organizations_in_technoparks_share_percent", "label": "Доля организаций ОПК в технопарках", "value": 10.0, "value_type": "percent", "numerator": 66, "denominator": 660 }, { "code": "opk_organizations_in_technoparks_count", "label": "Количество организаций ОПК в технопарках", "value": 66, "value_type": "integer", "numerator": 66, "denominator": null }, { "code": "technoparks_with_opk_residents_share_percent", "label": "Доля технопарков, в числе резидентов которых есть организации ОПК", "value": 21.0, "value_type": "percent", "numerator": 35, "denominator": 167 } ], "main_block": { "type": "technopark_area_distribution", "title": "Распределение технопарков по площадям", "total_technoparks_count": 35, "area_segments": [ { "area_segment": "large", "label": "крупные", "area_range_label": "150-200", "area_min_value": 150, "area_max_value": 200, "area_unit": "ha", "technoparks_count": 12 }, { "area_segment": "medium", "label": "средние", "area_range_label": "50-150", "area_min_value": 50, "area_max_value": 150, "area_unit": "ha", "technoparks_count": 14 }, { "area_segment": "small", "label": "мелкие", "area_range_label": "0-50", "area_min_value": 0, "area_max_value": 50, "area_unit": "ha", "technoparks_count": 9 } ], "technoparks": { "sort": { "field": "area_value", "direction": "desc" }, "rows": [ { "rank": 1, "technopark_id": "technopark-001", "technopark_name": "Индустриальный технопарк Пример", "region_id": "moscow-oblast", "region_name": "Московская область", "area_value": 41, "area_unit": "ha" }, { "rank": 2, "technopark_id": "technopark-002", "technopark_name": "Технопарк Развитие", "region_id": "moscow", "region_name": "г. Москва", "area_value": 28, "area_unit": "ha" }, { "rank": 3, "technopark_id": "technopark-003", "technopark_name": "Технопарк Южный Урал", "region_id": "chelyabinsk-oblast", "region_name": "Челябинская область", "area_value": 23, "area_unit": "ha" }, { "rank": 4, "technopark_id": "technopark-004", "technopark_name": "Технопарк Иннополис", "region_id": "tatarstan", "region_name": "Республика Татарстан", "area_value": 22, "area_unit": "ha" }, { "rank": 5, "technopark_id": "technopark-005", "technopark_name": "Технопарк Самара", "region_id": "samara-region", "region_name": "Самарская область", "area_value": 19, "area_unit": "ha" }, { "rank": 6, "technopark_id": "technopark-006", "technopark_name": "Технопарк Пенза", "region_id": "penza-region", "region_name": "Пензенская область", "area_value": 17, "area_unit": "ha" }, { "rank": 7, "technopark_id": "technopark-007", "technopark_name": "Технопарк Томск", "region_id": "tomsk-region", "region_name": "Томская область", "area_value": 12, "area_unit": "ha" }, { "rank": 8, "technopark_id": "technopark-008", "technopark_name": "Технопарк Ярославль", "region_id": "yaroslavl-region", "region_name": "Ярославская область", "area_value": 10, "area_unit": "ha" }, { "rank": 9, "technopark_id": "technopark-009", "technopark_name": "Технопарк Ульяновск", "region_id": "ulyanovsk-region", "region_name": "Ульяновская область", "area_value": 5, "area_unit": "ha" }, { "rank": 10, "technopark_id": "technopark-010", "technopark_name": "Технопарк Хабаровск", "region_id": "khabarovsk-krai", "region_name": "Хабаровский край", "area_value": 4, "area_unit": "ha" }, { "rank": 11, "technopark_id": "technopark-011", "technopark_name": "Технопарк Башкортостан", "region_id": "bashkortostan", "region_name": "Республика Башкортостан", "area_value": 4, "area_unit": "ha" } ] } }, "region_ranking": { "title": "Рейтинг субъектов РФ по участию организаций ОПК в технопарках", "sort": { "field": "opk_organizations_count", "direction": "desc" }, "ranking_method": "ordinal", "rows": [ { "rank": 1, "region_id": "moscow-oblast", "region_name": "Московская область", "technoparks_count": 7, "opk_organizations_count": 41 }, { "rank": 2, "region_id": "moscow", "region_name": "г. Москва", "technoparks_count": 5, "opk_organizations_count": 28 }, { "rank": 3, "region_id": "chelyabinsk-oblast", "region_name": "Челябинская область", "technoparks_count": 4, "opk_organizations_count": 23 }, { "rank": 4, "region_id": "tatarstan", "region_name": "Республика Татарстан", "technoparks_count": 3, "opk_organizations_count": 22 }, { "rank": 5, "region_id": "samara-region", "region_name": "Самарская область", "technoparks_count": 3, "opk_organizations_count": 19 }, { "rank": 6, "region_id": "penza-region", "region_name": "Пензенская область", "technoparks_count": 2, "opk_organizations_count": 17 }, { "rank": 7, "region_id": "tomsk-region", "region_name": "Томская область", "technoparks_count": 2, "opk_organizations_count": 12 }, { "rank": 8, "region_id": "yaroslavl-region", "region_name": "Ярославская область", "technoparks_count": 2, "opk_organizations_count": 10 }, { "rank": 9, "region_id": "ulyanovsk-region", "region_name": "Ульяновская область", "technoparks_count": 2, "opk_organizations_count": 5 }, { "rank": 10, "region_id": "khabarovsk-krai", "region_name": "Хабаровский край", "technoparks_count": 1, "opk_organizations_count": 4 }, { "rank": 11, "region_id": "bashkortostan", "region_name": "Республика Башкортостан", "technoparks_count": 1, "opk_organizations_count": 4 }, { "rank": 12, "region_id": "krasnodar-krai", "region_name": "Краснодарский край", "technoparks_count": 1, "opk_organizations_count": 3 }, { "rank": 13, "region_id": "belgorod-region", "region_name": "Белгородская область", "technoparks_count": 1, "opk_organizations_count": 2 }, { "rank": 14, "region_id": "volgograd-region", "region_name": "Волгоградская область", "technoparks_count": 1, "opk_organizations_count": 2 } ] }, "top_industries": { "title": "Топ отраслей по количеству организаций ОПК, участвующих в технопарках", "mode": "single_association_type", "association_type": "technoparks", "max_value": 180, "rows": [ { "industry_id": "aviation", "industry_name": "Авиационная промышленность", "opk_organizations_count": 180 }, { "industry_id": "conventional-weapons", "industry_name": "Промышленность обычных вооружений", "opk_organizations_count": 124 }, { "industry_id": "ammunition-and-special-chemistry", "industry_name": "Промышленность боеприпасов и спецхимии", "opk_organizations_count": 110 }, { "industry_id": "radio-electronics", "industry_name": "Радиоэлектронная промышленность", "opk_organizations_count": 101 }, { "industry_id": "shipbuilding", "industry_name": "Судостроительная промышленность", "opk_organizations_count": 78 }, { "industry_id": "robotics", "industry_name": "Беспилотные системы и робототехника", "opk_organizations_count": 12 } ] }, "trend_block": null, "employee_stats": { "items": [ { "code": "opk_resident_employees_count", "label": "Численность сотрудников организаций ОПК-резидентов технопарков", "value": 1678, "value_type": "integer", "numerator": 1678, "denominator": null }, { "code": "opk_resident_employees_share_percent", "label": "Доля сотрудников организаций ОПК-резидентов технопарков", "value": 35.0, "value_type": "percent", "numerator": 1678, "denominator": 4794 } ] } } ``` --- ## Response для `association_type=clusters` ```json { "generated_at": "2026-06-19T15:20:00+03:00", "data_actual_at": "2026-06-19T00:00:00+03:00", "association_type": "clusters", "applied_filters": { "association_type": "clusters", "federal_district_id": null, "region_id": null, "industry_id": null, "integrated_structure_id": null, "organization_id": null }, "kpis": [ { "code": "clusters_with_opk_participants_count", "label": "Количество кластеров, в числе участников которых есть организации ОПК", "value": 124, "value_type": "integer", "numerator": 124, "denominator": null }, { "code": "clusters_with_opk_participants_share_percent", "label": "Процент кластеров, в числе участников которых есть организации ОПК", "value": 62.0, "value_type": "percent", "numerator": 124, "denominator": 200 }, { "code": "opk_organizations_in_clusters_count", "label": "Количество организаций ОПК, участвующих в кластерах", "value": 517, "value_type": "integer", "numerator": 517, "denominator": null }, { "code": "opk_organizations_in_clusters_share_percent", "label": "Составляют организации ОПК в кластерах", "value": 15.0, "value_type": "percent", "numerator": 517, "denominator": 3447 } ], "main_block": { "type": "cluster_specializations", "title": "Специализации промышленных кластеров", "specializations": { "sort": { "field": "clusters_count", "direction": "desc" }, "ranking_method": "ordinal", "rows": [ { "rank": 1, "specialization_id": "ict", "specialization_name": "Информационно-коммуникационные технологии", "clusters_count": 4 }, { "rank": 2, "specialization_id": "aircraft-maintenance", "specialization_name": "Обслуживание и ремонт воздушных судов", "clusters_count": 3 }, { "rank": 3, "specialization_id": "wood-processing", "specialization_name": "Лесопереработка", "clusters_count": 2 }, { "rank": 4, "specialization_id": "logistics", "specialization_name": "Логистика", "clusters_count": 2 }, { "rank": 5, "specialization_id": "nanotechnology-and-new-materials", "specialization_name": "Нанотехнологии и новые материалы", "clusters_count": 2 }, { "rank": 6, "specialization_id": "titanium-products", "specialization_name": "Производство изделий из титана", "clusters_count": 1 }, { "rank": 7, "specialization_id": "pharmaceutical-industry", "specialization_name": "Фармацевтическая промышленность", "clusters_count": 1 }, { "rank": 8, "specialization_id": "storage-and-logistics", "specialization_name": "Хранение и логистика", "clusters_count": 1 }, { "rank": 9, "specialization_id": "high-tech-processing-production", "specialization_name": "Высокотехнологичные обрабатывающие производства", "clusters_count": 1 }, { "rank": 10, "specialization_id": "woodworking", "specialization_name": "Деревообработка", "clusters_count": 1 } ] } }, "region_ranking": { "title": "Рейтинг субъектов РФ по участию организаций ОПК в кластерах", "sort": { "field": "opk_organizations_count", "direction": "desc" }, "ranking_method": "ordinal", "rows": [ { "rank": 1, "region_id": "moscow-oblast", "region_name": "Московская область", "clusters_count": 20, "opk_organizations_count": 41 }, { "rank": 2, "region_id": "moscow", "region_name": "г. Москва", "clusters_count": 15, "opk_organizations_count": 28 }, { "rank": 3, "region_id": "chelyabinsk-oblast", "region_name": "Челябинская область", "clusters_count": 12, "opk_organizations_count": 23 }, { "rank": 4, "region_id": "tatarstan", "region_name": "Республика Татарстан", "clusters_count": 10, "opk_organizations_count": 22 }, { "rank": 5, "region_id": "samara-region", "region_name": "Самарская область", "clusters_count": 9, "opk_organizations_count": 19 }, { "rank": 6, "region_id": "penza-region", "region_name": "Пензенская область", "clusters_count": 8, "opk_organizations_count": 17 }, { "rank": 7, "region_id": "tomsk-region", "region_name": "Томская область", "clusters_count": 7, "opk_organizations_count": 12 }, { "rank": 8, "region_id": "yaroslavl-region", "region_name": "Ярославская область", "clusters_count": 6, "opk_organizations_count": 10 }, { "rank": 9, "region_id": "ulyanovsk-region", "region_name": "Ульяновская область", "clusters_count": 5, "opk_organizations_count": 5 }, { "rank": 10, "region_id": "khabarovsk-krai", "region_name": "Хабаровский край", "clusters_count": 4, "opk_organizations_count": 4 }, { "rank": 11, "region_id": "bashkortostan", "region_name": "Республика Башкортостан", "clusters_count": 4, "opk_organizations_count": 4 }, { "rank": 12, "region_id": "krasnodar-krai", "region_name": "Краснодарский край", "clusters_count": 3, "opk_organizations_count": 3 }, { "rank": 13, "region_id": "belgorod-region", "region_name": "Белгородская область", "clusters_count": 2, "opk_organizations_count": 2 }, { "rank": 14, "region_id": "volgograd-region", "region_name": "Волгоградская область", "clusters_count": 2, "opk_organizations_count": 2 }, { "rank": 15, "region_id": "udmurtia", "region_name": "Удмуртская Республика", "clusters_count": 2, "opk_organizations_count": 2 } ] }, "top_industries": { "title": "Топ отраслей по количеству организаций ОПК, участвующих в промышленных кластерах", "mode": "single_association_type", "association_type": "clusters", "max_value": 180, "rows": [ { "industry_id": "aviation", "industry_name": "Авиационная промышленность", "opk_organizations_count": 180 }, { "industry_id": "conventional-weapons", "industry_name": "Промышленность обычных вооружений", "opk_organizations_count": 124 }, { "industry_id": "ammunition-and-special-chemistry", "industry_name": "Промышленность боеприпасов и спецхимии", "opk_organizations_count": 110 }, { "industry_id": "radio-electronics", "industry_name": "Радиоэлектронная промышленность", "opk_organizations_count": 101 }, { "industry_id": "shipbuilding", "industry_name": "Судостроительная промышленность", "opk_organizations_count": 78 }, { "industry_id": "robotics", "industry_name": "Беспилотные системы и робототехника", "opk_organizations_count": 12 } ] }, "trend_block": { "type": "cluster_creation_dynamics", "title": "Динамика создания промышленных кластеров 2020-2024 гг.", "points": [ { "year": 2020, "clusters_count": 15, "growth_percent": null }, { "year": 2021, "clusters_count": 24, "growth_percent": 60.0 }, { "year": 2022, "clusters_count": 47, "growth_percent": 95.8 }, { "year": 2023, "clusters_count": 66, "growth_percent": 40.4 }, { "year": 2024, "clusters_count": 124, "growth_percent": 87.9 } ] }, "employee_stats": null } ``` --- ## Требования к общим полям analytics response ### Поле `generated_at` Требования: - ISO 8601 с timezone; - обязательное; - показывает время формирования response. ### Поле `data_actual_at` Требования: - ISO 8601 с timezone; - обязательное; - показывает дату и время актуальности аналитических данных. ### Поле `association_type` Требования: - обязательное; - должно совпадать с фактически применённым режимом; - допустимые значения: `all`, `sez`, `technoparks`, `clusters`. ### Блок `applied_filters` Требования: - обязательный; - содержит нормализованные значения применённых фильтров; - отсутствующие фильтры возвращаются как `null`; - `association_type` всегда заполнен. ### Массив `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`; - для percent-метрик `numerator` и `denominator` обязательны, если знаменатель известен; - порядок массива должен соответствовать порядку карточек на странице. ### Блок `main_block` Полиморфный блок. Структура зависит от `association_type`. Допустимые `type`: - `all_associations_composition`; - `sez_types_and_priority_directions`; - `technopark_area_distribution`; - `cluster_specializations`. Требования: - `type` обязательный; - `title` обязательный; - структура должна соответствовать текущему `association_type`; - массивы внутри блока должны быть отсортированы. #### `main_block.type = all_associations_composition` Используется для `association_type = all`. Обязательные поля: - `total_associations_count`; - `segments`. Требования к `segments`: - каждый segment должен иметь `association_type`, `label`, `associations_count`, `opk_organizations_count`, `opk_memberships_count`; - порядок `segments`: `sez`, `technoparks`, `clusters`; - сумма `segments[*].associations_count` должна быть равна `total_associations_count`. #### `main_block.type = sez_types_and_priority_directions` Используется для `association_type = sez`. Обязательные поля: - `sez_types`; - `priority_directions`. Требования к `sez_types.segments`: - каждый segment должен иметь `sez_type`, `label`, `sez_count`, `opk_organizations_count`; - сумма `segments[*].sez_count` должна быть равна `sez_types.total_sez_count`, если типы ОЭЗ взаимоисключающие; - если одна ОЭЗ может относиться к нескольким типам, backend должен явно зафиксировать методику до реализации endpoint-а. Требования к `priority_directions.rows`: - каждая строка должна иметь `rank`, `direction_id`, `direction_name`, `sez_count`; - `direction_id` должен быть стабильным id направления; - `rows` должны быть отсортированы по `sort`; - если направлений больше, чем помещается на экране, backend всё равно возвращает полный массив. #### `main_block.type = technopark_area_distribution` Используется для `association_type = technoparks`. Обязательные поля: - `total_technoparks_count`; - `area_segments`; - `technoparks`. Требования к `area_segments`: - каждый segment должен иметь `area_segment`, `label`, `area_range_label`, `area_min_value`, `area_max_value`, `area_unit`, `technoparks_count`; - `area_min_value` и `area_max_value` возвращаются как number или `null` для открытого диапазона; - `area_unit` должен быть стабильным кодом единицы измерения, например `ha`; - сумма `area_segments[*].technoparks_count` должна быть равна `total_technoparks_count`, если сегменты площадей взаимоисключающие. Требования к `technoparks.rows`: - каждая строка должна иметь `rank`, `technopark_id`, `technopark_name`, `region_id`, `region_name`, `area_value`, `area_unit`; - `technopark_id` должен быть стабильным id для drilldown; - `area_value` возвращается как number; - `rows` должны быть отсортированы по `technoparks.sort`. #### `main_block.type = cluster_specializations` Используется для `association_type = clusters`. Обязательные поля: - `specializations`. Требования к `specializations.rows`: - каждая строка должна иметь `rank`, `specialization_id`, `specialization_name`, `clusters_count`; - `specialization_id` должен быть стабильным id специализации; - `clusters_count` возвращается как integer `>= 0`; - `rows` должны быть отсортированы по `specializations.sort`; - если специализаций больше, чем помещается на экране, backend всё равно возвращает полный массив. ### Блок `region_ranking` Поля: - `title`; - `sort`; - `ranking_method`; - `rows`. Требования: - `title` обязательный; - `sort.field` обязательный; - `sort.direction` должен быть `asc | desc`; - `ranking_method` optional, default `ordinal`; - `rows` может быть пустым массивом; - `rank` возвращается в каждой строке; - `region_id` обязательный; - `region_name` обязательный; - count-поля должны иметь однозначные имена. Нельзя использовать неоднозначное поле: ```json { "sez_count": 63 } ``` если значение означает количество организаций ОПК. Нужно использовать одно из точных полей: ```json { "sez_opk_organizations_count": 63 } ``` или: ```json { "sez_opk_memberships_count": 63 } ``` ### Блок `top_industries` Поля: - `title`; - `mode`; - `association_type`; - `max_value`; - `rows`. Требования: - `mode` должен быть `stacked_by_association_type | single_association_type`; - для `association_type = all` должен использоваться `mode = stacked_by_association_type`; - для `association_type = sez | technoparks | clusters` должен использоваться `mode = single_association_type`; - `max_value` должен быть number `>= 0`; - `max_value` должен быть больше или равен максимальному значению в `rows`; - `rows` должны быть отсортированы по убыванию основного значения. ### Блок `trend_block` Может быть `null`. Допустимые `type`: - `opk_participation_by_year`; - `cluster_creation_dynamics`. Требования: - если блок не применим для режима, возвращается `null`; - `points` должны быть отсортированы по `year` по возрастанию; - `year` integer; - `growth_percent` может быть `null` для первого года; - проценты роста возвращаются в диапазоне `0..100+`, если рост больше 100% возможен. ### Блок `employee_stats` Может быть `null`. Поля: - `items`. Требования: - если блок не применим для режима, возвращается `null`; - каждый item должен иметь `code`, `label`, `value`, `value_type`; - для percent-метрик должны быть `numerator` и `denominator`, если знаменатель известен; - подписи должны соответствовать текущему `association_type`. --- ## Порядок массивов ### `kpis` Порядок должен соответствовать порядку карточек на странице. ### `main_block.segments` Для `all_associations_composition` порядок: 1. `sez` 2. `technoparks` 3. `clusters` ### `region_ranking.rows` Порядок сортировки по умолчанию: 1. основной показатель по убыванию; 2. при равенстве - `opk_organizations_count` по убыванию; 3. при равенстве - `region_name` по возрастанию (`ru-RU`). Если в блоке передан `ranking_method = dense` или `competition`, одинаковые `rank` допустимы только для строк с равным основным показателем. ### `top_industries.rows` Порядок сортировки: 1. основной показатель по убыванию; 2. при равенстве - `industry_name` по возрастанию (`ru-RU`). ### `trend_block.points` Порядок сортировки: 1. `year` по возрастанию. --- ## Проверки консистентности Backend должен гарантировать: - `association_type` в корне response совпадает с `applied_filters.association_type`; - `kpis[*].value` согласованы с соответствующими блоками аналитики; - сумма `main_block.segments[*].associations_count` для `all` равна `total_associations_count`; - сумма `main_block.sez_types.segments[*].sez_count` для `sez` равна `main_block.sez_types.total_sez_count`, если типы ОЭЗ взаимоисключающие; - сумма `main_block.area_segments[*].technoparks_count` для `technoparks` равна `main_block.total_technoparks_count`, если сегменты площадей взаимоисключающие; - `top_industries.max_value` не меньше максимального значения в `top_industries.rows`; - `rank` в рейтингах не дублируется, если `ranking_method = ordinal`; - все `id` стабильны между запросами; - все count-поля возвращаются как integer `>= 0`; - все percent-поля возвращаются как number; - пустые данные возвращаются пустыми массивами, а не отсутствующими полями. --- ## Успешный пустой ответ Если по фильтрам данных нет, возвращается `200 OK`. Пример: ```json { "generated_at": "2026-06-19T15:20:00+03:00", "data_actual_at": "2026-06-19T00:00:00+03:00", "association_type": "clusters", "applied_filters": { "association_type": "clusters", "federal_district_id": "northwestern", "region_id": "nenets-autonomous-okrug", "industry_id": null, "integrated_structure_id": null, "organization_id": null }, "kpis": [], "main_block": { "type": "cluster_specializations", "title": "Специализации промышленных кластеров", "specializations": { "sort": { "field": "clusters_count", "direction": "desc" }, "rows": [] } }, "region_ranking": { "title": "Рейтинг субъектов РФ по участию организаций ОПК в кластерах", "sort": { "field": "opk_organizations_count", "direction": "desc" }, "rows": [] }, "top_industries": { "title": "Топ отраслей по количеству организаций ОПК, участвующих в промышленных кластерах", "mode": "single_association_type", "association_type": "clusters", "max_value": 0, "rows": [] }, "trend_block": null, "employee_stats": null } ``` --- ## Статус-коды и ошибки ### 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: parks", "instance": "/api/v1/competence-map/analytics/?association_type=parks", "code": "invalid_association_type", "request_id": "req-01JYX4M2K7Y6WQ9RJ8T2V7VY5R", "received_value": "parks", "allowed_values": ["all", "sez", "technoparks", "clusters"] } ``` Для несуществующего фильтра: 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/analytics/?association_type=clusters®ion_id=unknown", "code": "invalid_filter_value", "request_id": "req-01JYX4M2K7Y6WQ9RJ8T2V7VY5R", "field": "region_id", "received_value": "unknown" } ``` Для неизвестного 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/analytics/?district=central", "code": "unknown_query_param", "request_id": "req-01JYX4M2K7Y6WQ9RJ8T2V7VY5R", "field": "district", "allowed_params": [ "association_type", "federal_district_id", "region_id", "industry_id", "integrated_structure_id", "organization_id" ] } ``` ### Ошибки доступа и инфраструктуры Для `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 analytics", "instance": "/api/v1/competence-map/analytics/", "code": "forbidden", "request_id": "req-01JYX4M2K7Y6WQ9RJ8T2V7VY5R" } ``` Для `429 Too Many Requests` backend должен вернуть header: ```text Retry-After: ``` --- ## Ожидаемая схема запросов При загрузке страницы: ```text GET /api/v1/competence-map/analytics/filters/ GET /api/v1/competence-map/analytics/?association_type=all ``` При смене типа объединения: ```text GET /api/v1/competence-map/analytics/filters/?association_type=clusters GET /api/v1/competence-map/analytics/?association_type=clusters ``` При применении нескольких фильтров: ```text GET /api/v1/competence-map/analytics/filters/?association_type=technoparks&federal_district_id=central®ion_id=moscow-oblast GET /api/v1/competence-map/analytics/?association_type=technoparks&federal_district_id=central®ion_id=moscow-oblast ``` Если тип объединения отображается в интерфейсе как tabs, схема запросов не меняется. Tab является только UI-представлением `association_type`. Не требуется реализовывать отдельные endpoint-ы вида: ```text GET /api/v1/competence-map/analytics/sez/ GET /api/v1/competence-map/analytics/technoparks/ GET /api/v1/competence-map/analytics/clusters/ ``` Если в будущем появятся отдельные действия, например экспорт, полноэкранная таблица с серверной пагинацией или drilldown по строке, для них можно добавить отдельные endpoint-ы. Для текущих состояний страницы из референсных экранов достаточно `/analytics/filters/` и `/analytics/`.