# Backend Endpoints Competence Map Analytics Назначение: спецификация backend-контракта для страницы `Все сервисы / Карта компетенций / Аналитическая панель`. Документ нужен как задача для backend-разработки. Цель: реализовать endpoint-ы, которые возвращают все данные аналитической панели по инновационным объединениям: - значения фильтров; - верхние KPI-карточки; - состав инновационных объединений; - аналитику по ОЭЗ; - аналитику по технопаркам; - аналитику по промышленным кластерам; - рейтинги субъектов РФ; - топ отраслей; - временную динамику; - численность сотрудников. ## Принципы контракта - аналитика страницы должна возвращаться одним page-level endpoint-ом; - справочники фильтров должны возвращаться отдельным endpoint-ом; - все значения должны возвращаться в 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-а. --- ## Термины и правила подсчёта ### `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` Численность сотрудников. Для показателей сотрудников нужно явно различать: - сотрудников организаций ОПК; - сотрудников организаций ОПК-резидентов; - сотрудников всех резидентов; - сотрудников участников кластера; - сотрудников выбранного типа объединений. --- ## Нужно реализовать ## `competence-map analytics filters` ### `GET /api/v1/competence-map/analytics/filters/` Описание: - возвращает справочники для фильтров аналитической панели; - учитывает права пользователя; - учитывает текущие выбранные фильтры, если они переданы в query params; - не возвращает аналитические KPI, рейтинги и графики. Request: ```text GET /api/v1/competence-map/analytics/filters/?association_type=all&federal_district_id=®ion_id=&industry_id=&integrated_structure_id=&organization_id= ``` 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. #### Массивы справочников Для массивов `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&federal_district_id=®ion_id=&industry_id=&integrated_structure_id=&organization_id= ``` 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 } ``` --- ## 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" }, "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": "moscow", "region_name": "г. Москва", "cluster_opk_organizations_count": 28, "technopark_opk_organizations_count": 28, "sez_opk_organizations_count": 32, "total_opk_organizations_count": 88 }, { "rank": 3, "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 } ] }, "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 } ] }, "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", "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" }, "rows": [ { "rank": 1, "direction_id": "aircraft-maintenance", "direction_name": "Обслуживание и ремонт воздушных судов", "sez_count": 3 }, { "rank": 2, "direction_id": "ict", "direction_name": "Информационно-коммуникационные технологии", "sez_count": 4 }, { "rank": 3, "direction_id": "wood-processing", "direction_name": "Лесопереработка", "sez_count": 2 } ] } }, "region_ranking": { "title": "Рейтинг субъектов РФ по участию организаций ОПК в ОЭЗ", "sort": { "field": "opk_organizations_count", "direction": "desc" }, "rows": [ { "rank": 1, "region_id": "moscow-oblast", "region_name": "Московская область", "sez_count": 41, "opk_organizations_count": 41 }, { "rank": 2, "region_id": "moscow", "region_name": "г. Москва", "sez_count": 28, "opk_organizations_count": 28 } ] }, "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 } ] }, "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", "technoparks_count": 12 }, { "area_segment": "medium", "label": "средние", "area_range_label": "50-150", "technoparks_count": 14 }, { "area_segment": "small", "label": "мелкие", "area_range_label": "0-50", "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" } ] } }, "region_ranking": { "title": "Рейтинг субъектов РФ по участию организаций ОПК в технопарках", "sort": { "field": "opk_organizations_count", "direction": "desc" }, "rows": [ { "rank": 1, "region_id": "moscow-oblast", "region_name": "Московская область", "technoparks_count": 41, "opk_organizations_count": 41 }, { "rank": 2, "region_id": "moscow", "region_name": "г. Москва", "technoparks_count": 28, "opk_organizations_count": 28 } ] }, "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 } ] }, "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" }, "rows": [ { "rank": 1, "specialization_id": "aircraft-maintenance", "specialization_name": "Обслуживание и ремонт воздушных судов", "clusters_count": 3 }, { "rank": 2, "specialization_id": "ict", "specialization_name": "Информационно-коммуникационные технологии", "clusters_count": 4 }, { "rank": 3, "specialization_id": "wood-processing", "specialization_name": "Лесопереработка", "clusters_count": 2 } ] } }, "region_ranking": { "title": "Рейтинг субъектов РФ по участию организаций ОПК в кластерах", "sort": { "field": "opk_organizations_count", "direction": "desc" }, "rows": [ { "rank": 1, "region_id": "moscow-oblast", "region_name": "Московская область", "clusters_count": 41, "opk_organizations_count": 41 }, { "rank": 2, "region_id": "moscow", "region_name": "г. Москва", "clusters_count": 28, "opk_organizations_count": 28 } ] }, "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 } ] }, "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`; - массивы внутри блока должны быть отсортированы. ### Блок `region_ranking` Поля: - `title`; - `sort`; - `rows`. Требования: - `title` обязательный; - `sort.field` обязательный; - `sort.direction` должен быть `asc | desc`; - `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`). ### `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`; - `top_industries.max_value` не меньше максимального значения в `top_industries.rows`; - `rank` в рейтингах не дублируется, если не используется специальная tie-ranking методика; - все `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": "unknown", "region_id": null, "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 } ``` --- ## Ошибка валидации query params Для некорректного `association_type`: HTTP status: ```text 400 Bad Request ``` Response: ```json { "error": { "code": "invalid_association_type", "message": "Unsupported association_type", "details": { "received_value": "parks", "allowed_values": ["all", "sez", "technoparks", "clusters"] } } } ``` Для несуществующего фильтра: HTTP status: ```text 400 Bad Request ``` Response: ```json { "error": { "code": "invalid_filter_value", "message": "Unknown filter value", "details": { "field": "region_id", "received_value": "unknown" } } } ``` --- ## Ожидаемая схема запросов При загрузке страницы: ```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 ```