Files
anal-front/docs/info/backend-endpoints-competence-map-analytics-from-layouts.md
2026-06-21 01:56:39 +03:00

90 KiB
Raw Blame History

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:

Accept: application/json

Успешный response:

Content-Type: application/json; charset=utf-8
Content-Language: ru-RU

Error response:

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:

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&region_id=moscow-oblast

Не использовать как canonical form:

GET /api/v1/competence-map/analytics/?association_type=all&federal_district_id=&region_id=

Такой request можно поддержать для совместимости, но backend всё равно должен вернуть пустые значения как null в applied_filters.

Headers и трассировка

Каждый response должен содержать:

X-Request-Id: <request id>

Требования:

  • если frontend передал X-Request-Id, backend должен использовать его или вернуть связанный id;
  • если id не передан, backend генерирует его сам;
  • этот id должен попадать в backend logs, чтобы можно было связать ошибку UI и серверный запрос;
  • технические debug-поля не добавляются в JSON payload, для них используются headers и логи.

Авторизация и права доступа

Endpoint-ы должны учитывать права текущего пользователя.

Правила:

  • 401 Unauthorized возвращается, если пользователь не аутентифицирован;
  • 403 Forbidden возвращается, если пользователь аутентифицирован, но не имеет доступа к странице или разделу;
  • для 401 Unauthorized backend должен вернуть WWW-Authenticate, если это предусмотрено текущей схемой аутентификации;
  • если пользователь имеет доступ к странице, но часть справочников или данных ему недоступна, backend должен отфильтровать эти данные на сервере;
  • filters не должен раскрывать id сущностей, к которым у пользователя нет доступа;
  • disabled = true можно использовать только для значений, которые пользователь видит, но которые неприменимы при текущих фильтрах;
  • значение фильтра, недоступное пользователю, обрабатывается как invalid filter value и не должно раскрывать, существует ли такая сущность в системе.

Кэширование и условные GET

Данные зависят от пользователя, прав доступа и фильтров, поэтому shared caching должен быть запрещён.

Recommended headers для успешных responses:

Cache-Control: private, max-age=60
Vary: Authorization, Accept
ETag: "<user-and-query-specific-etag>"
Last-Modified: <data_actual_at as HTTP-date>

Требования:

  • ETag должен учитывать пользователя, права доступа, endpoint path, query params и версию/актуальность данных;
  • Last-Modified должен соответствовать актуальности данных, а не времени генерации response;
  • backend должен поддерживать If-None-Match и может вернуть 304 Not Modified, если представление не изменилось;
  • при 304 Not Modified тело response не возвращается;
  • для персональных данных нельзя использовать Cache-Control: public;
  • если backend пока не готов поддерживать validators, нужно явно вернуть Cache-Control: no-store.

Актуальность и методика расчёта

Response уже содержит два разных времени:

  • generated_at - когда backend сформировал текущий response;
  • data_actual_at - на какой момент актуальны аналитические данные.

Требования:

  • generated_at не использовать как дату актуальности данных;
  • data_actual_at должен быть единым для всех блоков 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:

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:

{
  "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:

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:

{
  "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

{
  "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

{
  "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

{
  "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

{
  "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-поля должны иметь однозначные имена.

Нельзя использовать неоднозначное поле:

{
  "sez_count": 63
}

если значение означает количество организаций ОПК.

Нужно использовать одно из точных полей:

{
  "sez_opk_organizations_count": 63
}

или:

{
  "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.

Пример:

{
  "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:

400 Bad Request

Response:

{
  "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:

400 Bad Request

Response:

{
  "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&region_id=unknown",
  "code": "invalid_filter_value",
  "request_id": "req-01JYX4M2K7Y6WQ9RJ8T2V7VY5R",
  "field": "region_id",
  "received_value": "unknown"
}

Для неизвестного query param:

HTTP status:

400 Bad Request

Response:

{
  "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:

{
  "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:

Retry-After: <seconds>

Ожидаемая схема запросов

При загрузке страницы:

GET /api/v1/competence-map/analytics/filters/
GET /api/v1/competence-map/analytics/?association_type=all

При смене типа объединения:

GET /api/v1/competence-map/analytics/filters/?association_type=clusters
GET /api/v1/competence-map/analytics/?association_type=clusters

При применении нескольких фильтров:

GET /api/v1/competence-map/analytics/filters/?association_type=technoparks&federal_district_id=central&region_id=moscow-oblast
GET /api/v1/competence-map/analytics/?association_type=technoparks&federal_district_id=central&region_id=moscow-oblast

Если тип объединения отображается в интерфейсе как tabs, схема запросов не меняется. Tab является только UI-представлением association_type.

Не требуется реализовывать отдельные endpoint-ы вида:

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/.