diff --git a/FRONTEND_API_CHANGES_MARKETING_ANALYSIS.md b/FRONTEND_API_CHANGES_MARKETING_ANALYSIS.md new file mode 100644 index 0000000..b93a31b --- /dev/null +++ b/FRONTEND_API_CHANGES_MARKETING_ANALYSIS.md @@ -0,0 +1,323 @@ +# Изменения в API маркетингового анализа + +## Обзор изменений + +Проведен рефакторинг метода генерации JSON-анализа для повышения детализации данных. Структура ответа API для фронтенда **осталась прежней**, но изменилась внутренняя структура данных в `chartsData`. + +## Структура ответа API (без изменений) + +Ответ API остается прежним: + +```json +{ + "analysisId": "string", + "status": "completed", + "createdAt": "2024-01-01T00:00:00", + "completedAt": "2024-01-01T00:05:00", + "report": { + "summary": "string", + "fullAnalysis": "string", + "chartsData": { + /* ← ИЗМЕНЕНИЯ ЗДЕСЬ */ + }, + "targetAudience": { + "description": "string", + "channels": ["Instagram", "Telegram"] + }, + "recommendations": ["string"], + "strategy": { + "duration": "2 недели", + "channels": ["Instagram", "Telegram"], + "contentTypes": ["посты", "сторис"] + }, + "pdfUrl": "/api/marketing/analysis/{id}/download" + } +} +``` + +## Изменения в `chartsData` + +### 1. Анализ аудитории (`audienceSegmentation`) + +**Было:** + +```json +{ + "audienceSegmentation": { + "ageGroups": [{ "label": "25-34", "value": "40" }], + "genders": [{ "label": "Женщины", "value": "60" }], + "segments": [{ "label": "Сегмент 1", "value": "35" }] + } +} +``` + +**Стало (расширенная структура):** + +```json +{ + "audienceSegmentation": { + "ageGroups": [ + { "label": "18-24", "value": "15" }, + { "label": "25-34", "value": "40" }, + { "label": "35-44", "value": "30" }, + { "label": "45+", "value": "15" } + ], + "genders": [ + { "label": "Женщины", "value": "60" }, + { "label": "Мужчины", "value": "40" } + ], + "segments": [ + { + "label": "Молодые профессионалы", + "value": "35" + } + ], + "segmentsChannelMatrix": [ + // ← НОВОЕ + { + "segmentName": "Молодые профи", + "instagram": "high", + "telegram": "medium", + "youtube": "low" + } + ], + "keyTakeaways": [ + // ← НОВОЕ + "Вывод 1", + "Вывод 2", + "Вывод 3" + ] + } +} +``` + +**Что изменилось:** + +- ✅ Структура `ageGroups`, `genders`, `segments` осталась прежней +- ➕ Добавлено поле `segmentsChannelMatrix` — матрица соответствия сегментов и каналов (heatmap) +- ➕ Добавлено поле `keyTakeaways` — ключевые выводы по аудитории + +### 2. Анализ конкурентов (`competitors`) + +**Было:** + +```json +{ + "competitors": [ + { + "name": "Конкурент А", + "reach": 50000, + "activity": 85, + "reviews": 1200, + "strengths": "Не указано", + "weaknesses": "Не указано" + } + ] +} +``` + +**Стало (расширенная структура):** + +```json +{ + "marketShareChart": [ + // ← НОВОЕ + { "name": "Наш бренд", "value": 25 }, + { "name": "Конкурент А", "value": 30 }, + { "name": "Конкурент B", "value": 20 }, + { "name": "Другие", "value": 25 } + ], + "competitors": [ + { + "name": "Конкурент А", + "reach": 50000, + "followers": 12000, // ← НОВОЕ + "activity": 85, + "priceStrategy": "Высокая", // ← НОВОЕ + "channelsHeatmap": { + // ← НОВОЕ + "Instagram": 90, + "TikTok": 20, + "Telegram": 70, + "YouTube": 40 + } + } + ], + "comparisonTable": [ + // ← НОВОЕ + { + "feature": "Ценовая политика", + "us": "Средняя", + "compA": "Высокая", + "compB": "Низкая" + } + ], + "swotCompetitors": { + // ← НОВОЕ + "strengths": ["..."], + "weaknesses": ["..."] + } +} +``` + +**Что изменилось:** + +- ✅ Массив `competitors` остался, но с дополнительными полями +- ➕ Добавлено поле `marketShareChart` — доли рынка для графика +- ➕ Добавлено поле `comparisonTable` — сравнительная таблица характеристик +- ➕ Добавлено поле `swotCompetitors` — SWOT-анализ по рынку +- ➕ В объектах конкурентов добавлены: `followers`, `priceStrategy`, `channelsHeatmap` + +### 3. Сезонность (`seasonality`) + +**Без изменений:** + +```json +{ + "seasonality": { + "Январь": 45, + "Февраль": 50, + "Март": 60 + // ... остальные месяцы + } +} +``` + +### 4. Рынок (`market`) — НОВОЕ + +**Добавлено:** + +```json +{ + "market": { + "size": "средний", + "growthRate": "растущий", + "trends": ["Тренд 1", "Тренд 2"], + "opportunities": ["Возможность 1"], + "threats": ["Угроза 1"] + } +} +``` + +## Обратная совместимость + +Для обеспечения обратной совместимости, следующие поля могут присутствовать в `chartsData` (если использовалась старая структура): + +- `channelsPotential` — каналы продвижения +- `conversionFunnel` — воронка конверсии +- `swot` — SWOT-анализ + +**Рекомендация:** Используйте новые поля, но проверяйте наличие старых для обратной совместимости. + +## Миграция фронтенда + +### Шаг 1: Обновите обработку `audienceSegmentation` + +```typescript +// Было +const ageGroups = chartsData.audienceSegmentation?.ageGroups || []; +const genders = chartsData.audienceSegmentation?.genders || []; +const segments = chartsData.audienceSegmentation?.segments || []; + +// Стало (добавьте новые поля) +const ageGroups = chartsData.audienceSegmentation?.ageGroups || []; +const genders = chartsData.audienceSegmentation?.genders || []; +const segments = chartsData.audienceSegmentation?.segments || []; +const segmentsChannelMatrix = + chartsData.audienceSegmentation?.segmentsChannelMatrix || []; // НОВОЕ +const keyTakeaways = chartsData.audienceSegmentation?.keyTakeaways || []; // НОВОЕ +``` + +### Шаг 2: Обновите обработку конкурентов + +```typescript +// Было +const competitors = chartsData.competitors || []; + +// Стало (добавьте новые поля) +const marketShareChart = chartsData.marketShareChart || []; // НОВОЕ +const competitors = chartsData.competitors || []; +const comparisonTable = chartsData.comparisonTable || []; // НОВОЕ +const swotCompetitors = chartsData.swotCompetitors || {}; // НОВОЕ + +// Обновите обработку объектов конкурентов +competitors.forEach((comp) => { + const followers = comp.followers; // НОВОЕ + const priceStrategy = comp.priceStrategy; // НОВОЕ + const channelsHeatmap = comp.channelsHeatmap; // НОВОЕ +}); +``` + +### Шаг 3: Добавьте обработку рынка + +```typescript +// НОВОЕ +const market = chartsData.market || {}; +const marketSize = market.size; +const growthRate = market.growthRate; +const trends = market.trends || []; +const opportunities = market.opportunities || []; +const threats = market.threats || []; +``` + +## Примеры использования новых данных + +### 1. Heatmap сегментов и каналов + +```typescript +const segmentsChannelMatrix = + chartsData.audienceSegmentation?.segmentsChannelMatrix || []; + +// Отображение heatmap +segmentsChannelMatrix.forEach((row) => { + const segmentName = row.segmentName; + const instagram = row.instagram; // "high" | "medium" | "low" + const telegram = row.telegram; + const youtube = row.youtube; + // ... отрисовка heatmap +}); +``` + +### 2. График долей рынка + +```typescript +const marketShareChart = chartsData.marketShareChart || []; + +// Отображение pie chart +marketShareChart.forEach((item) => { + const name = item.name; // "Наш бренд", "Конкурент А", etc. + const value = item.value; // процент доли рынка + // ... отрисовка графика +}); +``` + +### 3. Сравнительная таблица + +```typescript +const comparisonTable = chartsData.comparisonTable || []; + +// Отображение таблицы +comparisonTable.forEach((row) => { + const feature = row.feature; // "Ценовая политика", "УТП", etc. + const us = row.us; + const compA = row.compA; + const compB = row.compB; + // ... отрисовка таблицы +}); +``` + +## Резюме изменений + +| Поле | Статус | Описание | +| --------------------------------- | ---------------- | -------------------------------------------------------------- | +| `chartsData.audienceSegmentation` | ✅ Расширено | Добавлены `segmentsChannelMatrix` и `keyTakeaways` | +| `chartsData.competitors` | ✅ Расширено | Добавлены поля `followers`, `priceStrategy`, `channelsHeatmap` | +| `chartsData.marketShareChart` | ➕ Новое | График долей рынка | +| `chartsData.comparisonTable` | ➕ Новое | Сравнительная таблица | +| `chartsData.swotCompetitors` | ➕ Новое | SWOT-анализ по рынку | +| `chartsData.market` | ➕ Новое | Данные о рынке | +| `chartsData.seasonality` | ✅ Без изменений | Осталось прежним | + +## Вопросы? + +Если возникнут вопросы по миграции, обращайтесь к бэкенд-команде. diff --git a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java index dfb1818..3157832 100644 --- a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java +++ b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java @@ -19,6 +19,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.time.LocalDateTime; import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; @Service public class MarketingAnalysisService { @@ -147,34 +149,100 @@ public class MarketingAnalysisService { private Map generateJsonAnalysis(MarketingAnalysisRequest request) { Map result = new HashMap<>(); - // 1. Получаем анализ целевой аудитории - logger.info("Starting audience analysis generation"); - Map audienceAnalysis = getAudienceAnalysis(request); - if (audienceAnalysis != null && !audienceAnalysis.isEmpty()) { - result.putAll(audienceAnalysis); - logger.info("Audience analysis generated successfully"); - } else { - logger.warn("Audience analysis returned empty result"); - } + logger.info("Starting parallel JSON analysis generation"); - // 2. Получаем анализ конкурентов - logger.info("Starting competitor analysis generation"); - Map competitorAnalysis = getCompetitorAnalysis(request); - if (competitorAnalysis != null && !competitorAnalysis.isEmpty()) { - result.putAll(competitorAnalysis); - logger.info("Competitor analysis generated successfully"); - } else { - logger.warn("Competitor analysis returned empty result"); - } + // Создаем CompletableFuture для каждого типа анализа + @SuppressWarnings("unchecked") + CompletableFuture> audienceFuture = (CompletableFuture>) CompletableFuture + .>supplyAsync(() -> { + logger.info("Starting audience analysis generation"); + Map analysis = getAudienceAnalysis(request); + if (analysis != null && !analysis.isEmpty()) { + logger.info("Audience analysis generated successfully"); + } else { + logger.warn("Audience analysis returned empty result"); + } + return analysis != null ? analysis : new HashMap<>(); + }) + .exceptionally(ex -> { + logger.error("Error in audience analysis future: {}", ex.getMessage(), ex); + return new HashMap<>(); + }); - // 3. Получаем анализ сезонности и рынка - logger.info("Starting seasonality and market analysis generation"); - Map seasonalityAndMarket = getSeasonalityAndMarket(request); - if (seasonalityAndMarket != null && !seasonalityAndMarket.isEmpty()) { - result.putAll(seasonalityAndMarket); - logger.info("Seasonality and market analysis generated successfully"); - } else { - logger.warn("Seasonality and market analysis returned empty result"); + @SuppressWarnings("unchecked") + CompletableFuture> competitorFuture = (CompletableFuture>) CompletableFuture + .>supplyAsync(() -> { + logger.info("Starting competitor analysis generation"); + Map analysis = getCompetitorAnalysis(request); + if (analysis != null && !analysis.isEmpty()) { + logger.info("Competitor analysis generated successfully"); + } else { + logger.warn("Competitor analysis returned empty result"); + } + return analysis != null ? analysis : new HashMap<>(); + }) + .exceptionally(ex -> { + logger.error("Error in competitor analysis future: {}", ex.getMessage(), ex); + return new HashMap<>(); + }); + + @SuppressWarnings("unchecked") + CompletableFuture> seasonalityFuture = (CompletableFuture>) CompletableFuture + .>supplyAsync(() -> { + logger.info("Starting seasonality and market analysis generation"); + Map analysis = getSeasonalityAndMarket(request); + if (analysis != null && !analysis.isEmpty()) { + logger.info("Seasonality and market analysis generated successfully"); + } else { + logger.warn("Seasonality and market analysis returned empty result"); + } + return analysis != null ? analysis : new HashMap<>(); + }) + .exceptionally(ex -> { + logger.error("Error in seasonality analysis future: {}", ex.getMessage(), ex); + return new HashMap<>(); + }); + + // Ожидаем завершения всех futures + try { + CompletableFuture.allOf(audienceFuture, competitorFuture, seasonalityFuture) + .get(5, TimeUnit.MINUTES); // Таймаут 5 минут + + // Собираем результаты + Map audienceAnalysis = audienceFuture.join(); + Map competitorAnalysis = competitorFuture.join(); + Map seasonalityAndMarket = seasonalityFuture.join(); + + if (audienceAnalysis != null && !audienceAnalysis.isEmpty()) { + result.putAll(audienceAnalysis); + } + if (competitorAnalysis != null && !competitorAnalysis.isEmpty()) { + result.putAll(competitorAnalysis); + } + if (seasonalityAndMarket != null && !seasonalityAndMarket.isEmpty()) { + result.putAll(seasonalityAndMarket); + } + + } catch (Exception e) { + logger.error("Error waiting for analysis futures: {}", e.getMessage(), e); + // Пытаемся получить результаты даже при ошибке + try { + Map audienceAnalysis = audienceFuture.getNow(new HashMap<>()); + Map competitorAnalysis = competitorFuture.getNow(new HashMap<>()); + Map seasonalityAndMarket = seasonalityFuture.getNow(new HashMap<>()); + + if (audienceAnalysis != null && !audienceAnalysis.isEmpty()) { + result.putAll(audienceAnalysis); + } + if (competitorAnalysis != null && !competitorAnalysis.isEmpty()) { + result.putAll(competitorAnalysis); + } + if (seasonalityAndMarket != null && !seasonalityAndMarket.isEmpty()) { + result.putAll(seasonalityAndMarket); + } + } catch (Exception ex) { + logger.error("Error getting partial results: {}", ex.getMessage(), ex); + } } // Проверяем, что хотя бы одна секция была успешно сгенерирована @@ -209,6 +277,7 @@ public class MarketingAnalysisService { /** * Генерация анализа целевой аудитории + * Модульный метод с глубоким психологическим профилированием сегментов */ private Map getAudienceAnalysis(MarketingAnalysisRequest request) { try { @@ -216,9 +285,15 @@ public class MarketingAnalysisService { StringBuilder promptBuilder = new StringBuilder(); promptBuilder.append( - "Ты — профессиональный маркетинговый аналитик, работающий с малым и средним бизнесом в Казахстане.\n\n"); + "Ты — профессиональный маркетинговый аналитик и психолог, работающий с малым и средним бизнесом в Казахстане.\n\n"); promptBuilder.append( - "Твоя задача — на основе входных данных о бизнесе выполнить глубокий анализ целевой аудитории и выдать результат ТОЛЬКО в виде корректного JSON-объекта строго по указанной структуре.\n\n"); + "Твоя задача — на основе входных данных о бизнесе выполнить ГЛУБОКИЙ анализ целевой аудитории с психологическим профилированием сегментов.\n\n"); + promptBuilder.append( + "КРИТИЧЕСКИ ВАЖНО: Проведи глубокое психологическое профилирование. Распиши мотивацию и барьеры подробно. " + + "Не используй общие фразы типа 'хотят качество' или 'боятся потратить деньги'. " + + "Опиши КОНКРЕТНЫЕ психологические механизмы, жизненные ситуации, эмоциональные триггеры и реальные препятствия для каждого сегмента.\n\n"); + promptBuilder.append( + "Выдай результат ТОЛЬКО в виде корректного JSON-объекта строго по указанной структуре.\n\n"); promptBuilder.append("Не добавляй пояснений, комментариев, текста до или после JSON.\n"); promptBuilder.append("Ответ должен быть валидным JSON.\n\n"); promptBuilder.append("---\n\n"); @@ -265,44 +340,67 @@ public class MarketingAnalysisService { promptBuilder.append("- Регион/город: ").append(regionStr).append("\n"); promptBuilder.append("- Цель продвижения: ").append(request.getGoal()).append("\n\n"); - promptBuilder.append("СФОРМИРУЙ JSON СЛЕДУЮЩЕЙ СТРУКТУРЫ:\n\n"); + promptBuilder.append("СФОРМИРУЙ JSON СЛЕДУЮЩЕЙ СТРУКТУРЫ (СТРОГО СОБЛЮДАЙ ФОРМАТ):\n\n"); promptBuilder.append("{\n"); - promptBuilder.append("\"audienceAnalysis\": {\n"); + promptBuilder.append(" \"audienceAnalysis\": {\n"); promptBuilder.append( - "\"ageGroups\": { \"18-24\": число, \"25-34\": число, \"35-44\": число, \"45+\": число }, // Pie chart data, сумма = 100\n"); - promptBuilder.append("\"ageComment\": \"Текст вывода под графиком возраста (1-2 предложения)\",\n"); + " \"ageGroups\": { \"18-24\": число, \"25-34\": число, \"35-44\": число, \"45+\": число },\n"); promptBuilder.append( - "\"genderDistribution\": { \"Женщины\": число, \"Мужчины\": число }, // Donut chart, сумма = 100\n"); - promptBuilder.append("\"genderComment\": \"Текст вывода под графиком пола\",\n"); - promptBuilder.append("\"segments\": [ // Список из 4 детальных карточек сегментов\n"); - promptBuilder.append("{\n"); - promptBuilder.append("\"name\": \"Название (например 'Молодые профессионалы')\",\n"); - promptBuilder.append("\"sharePercent\": число, // сумма всех sharePercent = 100\n"); - promptBuilder.append("\"ageRange\": \"25-34\",\n"); - promptBuilder.append("\"incomeLevel\": \"Средний+\",\n"); - promptBuilder.append("\"motivation\": \"Ключевая мотивация покупки\",\n"); - promptBuilder.append("\"triggers\": \"Триггеры (на что реагируют)\",\n"); - promptBuilder - .append("\"primaryChannels\": [\"Instagram\", \"TikTok\"], // Топ каналы для этого сегмента\n"); - promptBuilder.append("\"secondaryChannels\": [\"Telegram\"]\n"); - promptBuilder.append("}\n"); - promptBuilder.append("],\n"); - promptBuilder.append("\"segmentsChannelMatrix\": [ // Для таблицы heatmap: Сегмент vs Канал\n"); + " \"ageComment\": \"Подробный вывод о возрасте (2-3 предложения с конкретными инсайтами о том, почему именно эти возрастные группы являются целевыми)\",\n"); + promptBuilder.append(" \"genderDistribution\": { \"Женщины\": число, \"Мужчины\": число },\n"); promptBuilder.append( - "{ \"segmentName\": \"Молодые профи\", \"instagram\": \"high\", \"telegram\": \"medium\", \"youtube\": \"low\" }\n"); - promptBuilder.append("],\n"); - promptBuilder.append("\"keyTakeaways\": [\"Вывод 1\", \"Вывод 2\", \"Вывод 3\"]\n"); - promptBuilder.append("}\n"); + " \"genderComment\": \"Подробный вывод о гендере (2-3 предложения с конкретными инсайтами о гендерных особенностях потребления)\",\n"); + promptBuilder.append(" \"segments\": [\n"); + promptBuilder.append(" {\n"); + promptBuilder.append(" \"name\": \"Название сегмента (например 'Молодые мамы')\",\n"); + promptBuilder.append(" \"sharePercent\": число,\n"); + promptBuilder.append(" \"ageRange\": \"25-34\",\n"); + promptBuilder.append(" \"incomeLevel\": \"Средний\",\n"); + promptBuilder.append( + " \"motivation\": \"Детальное описание мотивации покупки (3-4 предложения). Опиши психологические и практические причины, почему этот сегмент покупает продукт/услугу. " + + "Используй конкретные примеры жизненных ситуаций, эмоциональных потребностей, ценностей. НЕ используй общие фразы.\",\n"); + promptBuilder.append( + " \"triggers\": \"Список триггеров (что заставляет действовать). Опиши конкретные ситуации, события, эмоции, которые побуждают к покупке (2-3 предложения). " + + "Например: 'Сезонные события (Новый год, 8 марта), жизненные изменения (рождение ребенка, переезд), эмоциональные состояния (стресс, радость, желание перемен)'.\",\n"); + promptBuilder.append( + " \"barriers\": \"Что мешает купить (страхи, сомнения, препятствия). Опиши конкретные барьеры: финансовые, психологические, информационные (2-3 предложения). " + + "Например: 'Опасение переплатить, недоверие к онлайн-покупкам, отсутствие времени на изучение предложений, страх выбрать неправильно'.\",\n"); + promptBuilder.append(" \"primaryChannels\": [\"Instagram\", \"TikTok\"],\n"); + promptBuilder.append(" \"secondaryChannels\": [\"Telegram\"]\n"); + promptBuilder.append(" }\n"); + promptBuilder.append(" ],\n"); + promptBuilder.append(" \"segmentsChannelMatrix\": [\n"); + promptBuilder.append( + " { \"segmentName\": \"Молодые мамы\", \"Instagram\": \"High\", \"Facebook\": \"Medium\", \"YouTube\": \"Low\", \"TikTok\": \"High\", \"Telegram\": \"Low\" }\n"); + promptBuilder.append(" ],\n"); + promptBuilder.append( + " \"keyTakeaways\": [\"Глубокий вывод 1 (1-2 предложения с конкретным инсайтом)\", \"Глубокий вывод 2 (1-2 предложения с конкретным инсайтом)\", \"Глубокий вывод 3 (1-2 предложения с конкретным инсайтом)\"]\n"); + promptBuilder.append(" }\n"); promptBuilder.append("}\n\n"); - promptBuilder.append("ТРЕБОВАНИЯ:\n\n"); - promptBuilder.append("- Верни ТОЛЬКО JSON, без пояснений.\n"); - promptBuilder.append("- Следи, чтобы JSON был синтаксически корректным.\n"); - promptBuilder.append("- Сумма значений в ageGroups должна быть = 100.\n"); - promptBuilder.append("- Сумма значений в genderDistribution должна быть = 100.\n"); - promptBuilder.append("- Сумма sharePercent по всем segments должна быть = 100.\n"); - promptBuilder.append("- Все строки — на русском языке.\n"); + promptBuilder.append("КРИТИЧЕСКИ ВАЖНЫЕ ТРЕБОВАНИЯ:\n\n"); + promptBuilder.append("1. Верни ТОЛЬКО JSON, без пояснений, комментариев, markdown-разметки.\n"); + promptBuilder.append("2. JSON должен быть синтаксически корректным и валидным.\n"); + promptBuilder.append("3. Сумма значений в ageGroups должна быть = 100.\n"); + promptBuilder.append("4. Сумма значений в genderDistribution должна быть = 100.\n"); + promptBuilder.append("5. Сумма sharePercent по всем segments должна быть = 100.\n"); + promptBuilder.append("6. Минимум 4 подробных сегмента в массиве segments.\n"); + promptBuilder.append( + "7. Для каждого сегмента заполни ВСЕ поля: name, sharePercent, ageRange, incomeLevel, motivation, triggers, barriers, primaryChannels, secondaryChannels.\n"); + promptBuilder.append( + "8. Поля motivation, triggers, barriers должны содержать ДЕТАЛЬНЫЕ описания (не общие фразы, а конкретные инсайты с примерами). " + + "Каждое поле должно быть минимум 2-3 предложения с конкретными деталями.\n"); + promptBuilder.append( + "9. В segmentsChannelMatrix должна быть строка для КАЖДОГО сегмента из массива segments. " + + "Используй значения \"High\", \"Medium\", \"Low\" (с заглавной буквы) для каждого канала: Instagram, Facebook, YouTube, TikTok, Telegram.\n"); + promptBuilder.append("10. Все строки — на русском языке.\n"); + promptBuilder.append( + "11. keyTakeaways должны содержать глубокие инсайты, а не общие фразы. Каждый вывод должен быть конкретным и практичным.\n"); + promptBuilder.append( + "12. segmentsChannelMatrix должна отражать реальное поведение каждого сегмента в социальных сетях. " + + "High = сегмент активно использует канал, Medium = умеренное использование, Low = редкое использование.\n"); String prompt = promptBuilder.toString(); + logger.debug("Generating audience analysis with detailed prompt (length: {} chars)", prompt.length()); String jsonResponse = openAIAnalyticsService.generateWithInstruction(context, prompt, "ru"); if (jsonResponse == null || jsonResponse.trim().isEmpty()) { @@ -314,6 +412,7 @@ public class MarketingAnalysisService { Map jsonMap = objectMapper.readValue(cleanedJson, new TypeReference>() { }); + logger.info("Audience analysis generated successfully"); return jsonMap; } catch (Exception e) { logger.error("Error generating audience analysis: {}", e.getMessage(), e); @@ -333,6 +432,10 @@ public class MarketingAnalysisService { "Ты — профессиональный маркетинговый аналитик, работающий с малым и средним бизнесом в Казахстане.\n\n"); promptBuilder.append( "Твоя задача — на основе входных данных о бизнесе выполнить глубокий анализ конкурентов и выдать результат ТОЛЬКО в виде корректного JSON-объекта строго по указанной структуре.\n\n"); + promptBuilder.append( + "КРИТИЧЕСКИ ВАЖНО: Найди или смоделируй реалистичных конкурентов для этой ниши. " + + "Оцени их цифровые метрики и контент-стратегию. Если ты не знаешь реальных конкурентов, " + + "создай реалистичные модели на основе типичных игроков в этой нише в Казахстане.\n\n"); promptBuilder.append("Не добавляй пояснений, комментариев, текста до или после JSON.\n"); promptBuilder.append("Ответ должен быть валидным JSON.\n\n"); promptBuilder.append("---\n\n"); @@ -352,44 +455,74 @@ public class MarketingAnalysisService { promptBuilder.append("СФОРМИРУЙ JSON СЛЕДУЮЩЕЙ СТРУКТУРЫ:\n\n"); promptBuilder.append("{\n"); - promptBuilder.append("\"competitorAnalysis\": {\n"); - promptBuilder.append("\"marketShareChart\": [ // Оценочные доли рынка для графика\n"); - promptBuilder.append("{ \"name\": \"Наш бренд\", \"value\": число },\n"); - promptBuilder.append("{ \"name\": \"Конкурент А\", \"value\": число },\n"); - promptBuilder.append("{ \"name\": \"Конкурент B\", \"value\": число },\n"); - promptBuilder.append("{ \"name\": \"Другие\", \"value\": число }\n"); - promptBuilder.append("], // сумма всех value должна быть = 100\n"); - promptBuilder.append("\"marketShareComment\": \"Вывод по доле рынка\",\n"); - promptBuilder.append("\"competitorsDetails\": [ // Детали по 3-4 конкурентам\n"); - promptBuilder.append("{\n"); - promptBuilder.append("\"name\": \"Конкурент А\",\n"); - promptBuilder.append("\"priceStrategy\": \"Высокая/Средняя/Низкая\",\n"); + promptBuilder.append(" \"competitorAnalysis\": {\n"); + promptBuilder.append(" \"marketShareChart\": [ // Оценочные доли рынка для графика\n"); + promptBuilder.append(" { \"name\": \"Наш бренд\", \"value\": число },\n"); + promptBuilder.append(" { \"name\": \"Конкурент А\", \"value\": число },\n"); + promptBuilder.append(" { \"name\": \"Конкурент B\", \"value\": число },\n"); + promptBuilder.append(" { \"name\": \"Другие\", \"value\": число }\n"); + promptBuilder.append(" ], // сумма всех value должна быть = 100\n"); promptBuilder.append( - "\"digitalMetrics\": { \"reach\": число, \"followers\": число, \"activityIndex\": число }, // activityIndex от 0 до 100\n"); - promptBuilder.append("\"channelsHeatmap\": { // Оценка (0-100) качества ведения канала\n"); - promptBuilder.append("\"Instagram\": число, \"TikTok\": число, \"Telegram\": число, \"YouTube\": число\n"); - promptBuilder.append("}\n"); - promptBuilder.append("}\n"); - promptBuilder.append("],\n"); - promptBuilder.append("\"comparisonTable\": [ // Сравнительная таблица (5-6 характеристик)\n"); + " \"marketShareComment\": \"Детальный анализ позиций на рынке (2-3 предложения с конкретными инсайтами)\",\n"); + promptBuilder.append(" \"competitorsDetails\": [ // Детали по 3-4 конкурентам\n"); + promptBuilder.append(" {\n"); + promptBuilder.append(" \"name\": \"Конкурент А\",\n"); + promptBuilder.append(" \"digitalMetrics\": { \"reach\": число, \"followers\": число, \"activityIndex\": число }, // activityIndex от 0 до 100\n"); + promptBuilder.append(" \"channelsHeatmap\": { // Оценка (0-100) качества ведения канала\n"); + promptBuilder.append(" \"Instagram\": число, \"TikTok\": число, \"Telegram\": число, \"YouTube\": число, \"Facebook\": число\n"); + promptBuilder.append(" },\n"); + promptBuilder.append(" \"priceStrategy\": \"Премиум/Средняя/Эконом\",\n"); promptBuilder.append( - "{ \"feature\": \"Ценовая политика\", \"us\": \"Средняя\", \"compA\": \"Высокая\", \"compB\": \"Низкая\" },\n"); + " \"contentStrategy\": \"Детальное описание контент-стратегии конкурента (3-4 предложения). " + + "Опиши типы контента, стиль подачи, частоту публикаций, основные темы, тон коммуникации. " + + "Проанализируй, что работает у конкурента и почему.\",\n"); + promptBuilder.append(" \"swot\": {\n"); promptBuilder.append( - "{ \"feature\": \"УТП\", \"us\": \"Сервис\", \"compA\": \"Бренд\", \"compB\": \"Дешевизна\" }\n"); - promptBuilder.append("],\n"); - promptBuilder.append("\"swotCompetitors\": { // Краткий SWOT по рынку в целом\n"); - promptBuilder.append("\"strengths\": [\"...\"], \"weaknesses\": [\"...\"]\n"); - promptBuilder.append("},\n"); - promptBuilder.append("\"keyInsights\": [\"Вывод 1\", \"Вывод 2\"]\n"); - promptBuilder.append("}\n"); + " \"strengths\": [\"Конкретная сильная сторона 1\", \"Конкретная сильная сторона 2\", \"Конкретная сильная сторона 3\"],\n"); + promptBuilder.append( + " \"weaknesses\": [\"Конкретная слабая сторона 1\", \"Конкретная слабая сторона 2\", \"Конкретная слабая сторона 3\"]\n"); + promptBuilder.append(" }\n"); + promptBuilder.append(" }\n"); + promptBuilder.append(" ],\n"); + promptBuilder.append(" \"comparisonTable\": [ // Сравнительная таблица (5-6 характеристик)\n"); + promptBuilder.append( + " { \"feature\": \"Цена\", \"us\": \"Средняя\", \"compA\": \"Высокая\", \"compB\": \"Низкая\" },\n"); + promptBuilder.append( + " { \"feature\": \"Продукт\", \"us\": \"Онлайн\", \"compA\": \"Офлайн\", \"compB\": \"Гибрид\" },\n"); + promptBuilder.append( + " { \"feature\": \"УТП\", \"us\": \"Сервис\", \"compA\": \"Бренд\", \"compB\": \"Дешевизна\" },\n"); + promptBuilder.append( + " { \"feature\": \"Целевая аудитория\", \"us\": \"...\", \"compA\": \"...\", \"compB\": \"...\" },\n"); + promptBuilder.append( + " { \"feature\": \"Каналы продвижения\", \"us\": \"...\", \"compA\": \"...\", \"compB\": \"...\" },\n"); + promptBuilder.append( + " { \"feature\": \"Позиционирование\", \"us\": \"...\", \"compA\": \"...\", \"compB\": \"...\" }\n"); + promptBuilder.append(" ],\n"); + promptBuilder.append( + " \"keyInsights\": [\"Глубокий инсайт 1 (1-2 предложения)\", \"Глубокий инсайт 2 (1-2 предложения)\"]\n"); + promptBuilder.append(" }\n"); promptBuilder.append("}\n\n"); - promptBuilder.append("ТРЕБОВАНИЯ:\n\n"); - promptBuilder.append("- Верни ТОЛЬКО JSON, без пояснений.\n"); - promptBuilder.append("- Следи, чтобы JSON был синтаксически корректным.\n"); - promptBuilder.append("- Сумма всех value в marketShareChart должна быть = 100.\n"); - promptBuilder.append("- Все строки — на русском языке.\n"); + promptBuilder.append("КРИТИЧЕСКИ ВАЖНЫЕ ТРЕБОВАНИЯ:\n\n"); + promptBuilder.append("1. Верни ТОЛЬКО JSON, без пояснений, комментариев, markdown-разметки.\n"); + promptBuilder.append("2. JSON должен быть синтаксически корректным и валидным.\n"); + promptBuilder.append("3. Сумма всех value в marketShareChart должна быть = 100.\n"); + promptBuilder.append("4. В массиве competitorsDetails должно быть 3-4 детальных описания конкурентов.\n"); + promptBuilder.append( + "5. Для каждого конкурента заполни ВСЕ поля: name, digitalMetrics, channelsHeatmap, priceStrategy, contentStrategy, swot.\n"); + promptBuilder.append( + "6. Поле contentStrategy должно содержать ДЕТАЛЬНОЕ описание (3-4 предложения), а не общие фразы.\n"); + promptBuilder.append( + "7. В swot для каждого конкурента укажи конкретные strengths и weaknesses (по 3-4 пункта), а не общие формулировки.\n"); + promptBuilder.append("8. В comparisonTable должно быть 5-6 строк сравнения различных характеристик.\n"); + promptBuilder.append( + "9. marketShareComment должен содержать детальный анализ позиций на рынке (2-3 предложения).\n"); + promptBuilder.append("10. keyInsights должны содержать глубокие инсайты, а не общие фразы.\n"); + promptBuilder.append("11. Все строки — на русском языке.\n"); + promptBuilder.append( + "12. channelsHeatmap должна содержать оценки (0-100) для всех основных каналов: Instagram, TikTok, Telegram, YouTube, Facebook.\n"); String prompt = promptBuilder.toString(); + logger.debug("Generating competitor analysis with detailed prompt (length: {} chars)", prompt.length()); String jsonResponse = openAIAnalyticsService.generateWithInstruction(context, prompt, "ru"); if (jsonResponse == null || jsonResponse.trim().isEmpty()) { @@ -401,6 +534,7 @@ public class MarketingAnalysisService { Map jsonMap = objectMapper.readValue(cleanedJson, new TypeReference>() { }); + logger.info("Competitor analysis generated successfully"); return jsonMap; } catch (Exception e) { logger.error("Error generating competitor analysis: {}", e.getMessage(), e); @@ -420,6 +554,9 @@ public class MarketingAnalysisService { "Ты — профессиональный маркетинговый аналитик, работающий с малым и средним бизнесом в Казахстане.\n\n"); promptBuilder.append( "Твоя задача — на основе входных данных о бизнесе выполнить глубокий анализ сезонности и рынка и выдать результат ТОЛЬКО в виде корректного JSON-объекта строго по указанной структуре.\n\n"); + promptBuilder.append( + "КРИТИЧЕСКИ ВАЖНО: Предоставь детальный анализ сезонных колебаний спроса с конкретными объяснениями. " + + "Опиши рыночные тренды, возможности и угрозы с конкретными инсайтами, а не общими фразами.\n\n"); promptBuilder.append("Не добавляй пояснений, комментариев, текста до или после JSON.\n"); promptBuilder.append("Ответ должен быть валидным JSON.\n\n"); promptBuilder.append("---\n\n"); @@ -437,29 +574,51 @@ public class MarketingAnalysisService { promptBuilder.append("СФОРМИРУЙ JSON СЛЕДУЮЩЕЙ СТРУКТУРЫ:\n\n"); promptBuilder.append("{\n"); - promptBuilder.append("\"seasonality\": {\n"); - promptBuilder.append("\"description\": \"Описание сезонных колебаний спроса\",\n"); + promptBuilder.append(" \"seasonality\": {\n"); promptBuilder.append( - "\"months\": [ \"Январь\", \"Февраль\", \"Март\", \"Апрель\", \"Май\", \"Июнь\", \"Июль\", \"Август\", \"Сентябрь\", \"Октябрь\", \"Ноябрь\", \"Декабрь\" ],\n"); - promptBuilder.append("\"demandIndex\": [12 чисел от 0 до 100], // Индекс спроса по месяцам\n"); - promptBuilder.append("\"estimated\": boolean\n"); - promptBuilder.append("},\n"); - promptBuilder.append("\"market\": {\n"); - promptBuilder.append("\"size\": \"Размер рынка (например, 'средний', 'крупный')\",\n"); - promptBuilder.append("\"growthRate\": \"Темп роста рынка\",\n"); - promptBuilder.append("\"trends\": [\"Тренд 1\", \"Тренд 2\", \"Тренд 3\"],\n"); - promptBuilder.append("\"opportunities\": [\"Возможность 1\", \"Возможность 2\"],\n"); - promptBuilder.append("\"threats\": [\"Угроза 1\", \"Угроза 2\"],\n"); - promptBuilder.append("\"estimated\": boolean\n"); - promptBuilder.append("}\n"); + " \"months\": [ \"Январь\", \"Февраль\", \"Март\", \"Апрель\", \"Май\", \"Июнь\", \"Июль\", \"Август\", \"Сентябрь\", \"Октябрь\", \"Ноябрь\", \"Декабрь\" ],\n"); + promptBuilder.append(" \"demandIndex\": [12 чисел от 0 до 100], // Индекс спроса по месяцам\n"); + promptBuilder.append( + " \"description\": \"Детальное описание сезонности (3-4 предложения). Опиши конкретные причины сезонных колебаний: " + + "праздники, климатические условия, культурные особенности, жизненные циклы целевой аудитории. " + + "Укажи, какие месяцы являются пиковыми и почему, какие - низкими и что это означает для бизнеса.\"\n"); + promptBuilder.append(" },\n"); + promptBuilder.append(" \"market\": {\n"); + promptBuilder.append( + " \"marketSize\": \"Оценка объема рынка (например, 'Оценка объема рынка составляет X млн тенге в год' или 'Средний рынок с потенциалом роста'). " + + "Дай конкретную оценку или описание масштаба рынка в Казахстане для данной ниши.\",\n"); + promptBuilder.append( + " \"growthRate\": \"Темп роста рынка (например, '+15% в год' или 'Стабильный рост 10-12% ежегодно')\",\n"); + promptBuilder.append( + " \"trends\": [\"Конкретный тренд 1 (1-2 предложения с деталями)\", \"Конкретный тренд 2 (1-2 предложения с деталями)\", \"Конкретный тренд 3 (1-2 предложения с деталями)\"],\n"); + promptBuilder.append( + " \"opportunities\": [\"Конкретная возможность 1 (1-2 предложения с деталями)\", \"Конкретная возможность 2 (1-2 предложения с деталями)\"],\n"); + promptBuilder.append( + " \"threats\": [\"Конкретная угроза 1 (1-2 предложения с деталями)\", \"Конкретная угроза 2 (1-2 предложения с деталями)\"]\n"); + promptBuilder.append(" }\n"); promptBuilder.append("}\n\n"); - promptBuilder.append("ТРЕБОВАНИЯ:\n\n"); - promptBuilder.append("- Верни ТОЛЬКО JSON, без пояснений.\n"); - promptBuilder.append("- Следи, чтобы JSON был синтаксически корректным.\n"); - promptBuilder.append("- Значения \"demandIndex\" должны быть от 0 до 100.\n"); - promptBuilder.append("- Все строки — на русском языке.\n"); + promptBuilder.append("КРИТИЧЕСКИ ВАЖНЫЕ ТРЕБОВАНИЯ:\n\n"); + promptBuilder.append("1. Верни ТОЛЬКО JSON, без пояснений, комментариев, markdown-разметки.\n"); + promptBuilder.append("2. JSON должен быть синтаксически корректным и валидным.\n"); + promptBuilder.append("3. Значения \"demandIndex\" должны быть от 0 до 100 (12 чисел для каждого месяца).\n"); + promptBuilder.append( + "4. Поле \"description\" в seasonality должно содержать ДЕТАЛЬНОЕ описание (3-4 предложения) с конкретными причинами сезонных колебаний.\n"); + promptBuilder.append( + "5. Поле \"marketSize\" должно содержать конкретную оценку объема рынка, а не просто слова 'средний' или 'крупный'.\n"); + promptBuilder.append( + "6. Массивы trends, opportunities, threats должны содержать КОНКРЕТНЫЕ инсайты (по 1-2 предложения каждый), а не общие фразы.\n"); + promptBuilder.append( + "7. Каждый элемент в trends должен описывать реальный тренд с конкретными деталями (например, 'Рост популярности онлайн-заказов на 30% за последний год').\n"); + promptBuilder.append( + "8. Каждый элемент в opportunities должен описывать конкретную возможность с практическими деталями.\n"); + promptBuilder.append( + "9. Каждый элемент в threats должен описывать конкретную угрозу с практическими деталями.\n"); + promptBuilder.append("10. Все строки — на русском языке.\n"); + promptBuilder.append( + "11. НЕ включай поле \"estimated\" ни в seasonality, ни в market.\n"); String prompt = promptBuilder.toString(); + logger.debug("Generating seasonality and market analysis with detailed prompt (length: {} chars)", prompt.length()); String jsonResponse = openAIAnalyticsService.generateWithInstruction(context, prompt, "ru"); if (jsonResponse == null || jsonResponse.trim().isEmpty()) { @@ -471,6 +630,7 @@ public class MarketingAnalysisService { Map jsonMap = objectMapper.readValue(cleanedJson, new TypeReference>() { }); + logger.info("Seasonality and market analysis generated successfully"); return jsonMap; } catch (Exception e) { logger.error("Error generating seasonality and market analysis: {}", e.getMessage(), e); @@ -984,6 +1144,8 @@ public class MarketingAnalysisService { /** * Парсинг данных для графиков из JSON-анализа + * Обновлено для работы с новой структурой: audienceAnalysis, + * competitorAnalysis, seasonality, market */ private Map parseChartDataFromJson(Map analysisJson) { Map chartsData = new HashMap<>(); @@ -993,7 +1155,7 @@ public class MarketingAnalysisService { } try { - // 1. Сезонность + // 1. Сезонность (структура не изменилась) @SuppressWarnings("unchecked") Map seasonality = (Map) analysisJson.get("seasonality"); if (seasonality != null) { @@ -1017,97 +1179,143 @@ public class MarketingAnalysisService { } } - // 2. Конкуренты + // 2. Конкуренты (новая структура: competitorAnalysis) @SuppressWarnings("unchecked") - List> competitors = (List>) analysisJson.get("competitors"); - if (competitors != null && !competitors.isEmpty()) { - List> competitorsList = new ArrayList<>(); - for (Map comp : competitors) { - Map competitorData = new HashMap<>(); - competitorData.put("name", comp.getOrDefault("name", "Неизвестный конкурент")); - competitorData.put("reach", comp.getOrDefault("reach", 10000)); - competitorData.put("activity", comp.getOrDefault("socialActivityIndex", 50)); - competitorData.put("reviews", comp.getOrDefault("reviewsCount", 100)); - competitorData.put("strengths", comp.getOrDefault("strengths", "Не указано")); - competitorData.put("weaknesses", comp.getOrDefault("weaknesses", "Не указано")); - competitorsList.add(competitorData); + Map competitorAnalysis = (Map) analysisJson.get("competitorAnalysis"); + if (competitorAnalysis != null) { + // 2.1. Market Share Chart + @SuppressWarnings("unchecked") + List> marketShareChart = (List>) competitorAnalysis + .get("marketShareChart"); + if (marketShareChart != null && !marketShareChart.isEmpty()) { + chartsData.put("marketShareChart", marketShareChart); + } + + // 2.2. Competitors Details (аналог старого competitors) + @SuppressWarnings("unchecked") + List> competitorsDetails = (List>) competitorAnalysis + .get("competitorsDetails"); + if (competitorsDetails != null && !competitorsDetails.isEmpty()) { + List> competitorsList = new ArrayList<>(); + for (Map comp : competitorsDetails) { + Map competitorData = new HashMap<>(); + competitorData.put("name", comp.getOrDefault("name", "Неизвестный конкурент")); + + @SuppressWarnings("unchecked") + Map digitalMetrics = (Map) comp.get("digitalMetrics"); + if (digitalMetrics != null) { + competitorData.put("reach", digitalMetrics.getOrDefault("reach", 10000)); + competitorData.put("followers", digitalMetrics.getOrDefault("followers", 0)); + competitorData.put("activity", digitalMetrics.getOrDefault("activityIndex", 50)); + } else { + competitorData.put("reach", 10000); + competitorData.put("followers", 0); + competitorData.put("activity", 50); + } + + competitorData.put("priceStrategy", comp.getOrDefault("priceStrategy", "Не указано")); + competitorData.put("channelsHeatmap", comp.getOrDefault("channelsHeatmap", new HashMap<>())); + competitorsList.add(competitorData); + } + chartsData.put("competitors", competitorsList); + } + + // 2.3. Comparison Table + @SuppressWarnings("unchecked") + List> comparisonTable = (List>) competitorAnalysis + .get("comparisonTable"); + if (comparisonTable != null && !comparisonTable.isEmpty()) { + chartsData.put("comparisonTable", comparisonTable); + } + + // 2.4. SWOT Competitors (частичный SWOT из competitorAnalysis) + @SuppressWarnings("unchecked") + Map swotCompetitors = (Map) competitorAnalysis.get("swotCompetitors"); + if (swotCompetitors != null) { + chartsData.put("swotCompetitors", swotCompetitors); } - chartsData.put("competitors", competitorsList); } - // 3. Сегментация аудитории + // 3. Анализ аудитории (новая структура: audienceAnalysis) @SuppressWarnings("unchecked") - Map audienceSegments = (Map) analysisJson.get("audienceSegments"); - if (audienceSegments != null) { + Map audienceAnalysis = (Map) analysisJson.get("audienceAnalysis"); + if (audienceAnalysis != null) { + Map segmentationData = new HashMap<>(); + + // 3.1. Age Groups (готовые данные из новой структуры) @SuppressWarnings("unchecked") - List> segments = (List>) audienceSegments.get("segments"); - if (segments != null && !segments.isEmpty()) { - Map segmentationData = new HashMap<>(); - - // Группировка по возрасту - Map ageGroups = new HashMap<>(); - Map genders = new HashMap<>(); - List> segmentsList = new ArrayList<>(); - - for (Map segment : segments) { - String name = (String) segment.getOrDefault("name", "Сегмент"); - Object percentObj = segment.get("sharePercent"); - int percent = percentObj instanceof Number ? ((Number) percentObj).intValue() : 0; - - String ageRange = (String) segment.getOrDefault("ageRange", ""); - String gender = (String) segment.getOrDefault("gender", ""); - - // Добавляем в общий список сегментов - Map segmentData = new HashMap<>(); - segmentData.put("label", name); - segmentData.put("value", String.valueOf(percent)); - segmentsList.add(segmentData); - - // Группируем по возрасту - if (!ageRange.isEmpty()) { - ageGroups.put(ageRange, ageGroups.getOrDefault(ageRange, 0) + percent); - } - - // Группируем по полу - if (!gender.isEmpty()) { - genders.put(gender, genders.getOrDefault(gender, 0) + percent); - } - } - - // Преобразуем в нужный формат + Map ageGroupsMap = (Map) audienceAnalysis.get("ageGroups"); + if (ageGroupsMap != null) { List> ageGroupsList = new ArrayList<>(); - for (Map.Entry entry : ageGroups.entrySet()) { + for (Map.Entry entry : ageGroupsMap.entrySet()) { Map ageGroup = new HashMap<>(); ageGroup.put("label", entry.getKey()); ageGroup.put("value", String.valueOf(entry.getValue())); ageGroupsList.add(ageGroup); } + segmentationData.put("ageGroups", ageGroupsList); + } + // 3.2. Gender Distribution (готовые данные из новой структуры) + @SuppressWarnings("unchecked") + Map genderDistributionMap = (Map) audienceAnalysis + .get("genderDistribution"); + if (genderDistributionMap != null) { List> gendersList = new ArrayList<>(); - for (Map.Entry entry : genders.entrySet()) { + for (Map.Entry entry : genderDistributionMap.entrySet()) { Map genderData = new HashMap<>(); genderData.put("label", entry.getKey()); genderData.put("value", String.valueOf(entry.getValue())); gendersList.add(genderData); } + segmentationData.put("genders", gendersList); + } - if (!ageGroupsList.isEmpty()) { - segmentationData.put("ageGroups", ageGroupsList); - } - if (!gendersList.isEmpty()) { - segmentationData.put("genders", gendersList); - } - if (!segmentsList.isEmpty()) { - segmentationData.put("segments", segmentsList); + // 3.3. Segments (детальные карточки сегментов) + @SuppressWarnings("unchecked") + List> segments = (List>) audienceAnalysis.get("segments"); + if (segments != null && !segments.isEmpty()) { + List> segmentsList = new ArrayList<>(); + for (Map segment : segments) { + Map segmentData = new HashMap<>(); + segmentData.put("label", (String) segment.getOrDefault("name", "Сегмент")); + Object percentObj = segment.get("sharePercent"); + segmentData.put("value", String.valueOf(percentObj instanceof Number ? percentObj : 0)); + segmentsList.add(segmentData); } + segmentationData.put("segments", segmentsList); + } - if (!segmentationData.isEmpty()) { - chartsData.put("audienceSegmentation", segmentationData); - } + // 3.4. Segments Channel Matrix (heatmap) + @SuppressWarnings("unchecked") + List> segmentsChannelMatrix = (List>) audienceAnalysis + .get("segmentsChannelMatrix"); + if (segmentsChannelMatrix != null && !segmentsChannelMatrix.isEmpty()) { + segmentationData.put("segmentsChannelMatrix", segmentsChannelMatrix); + } + + // 3.5. Key Takeaways + @SuppressWarnings("unchecked") + List keyTakeaways = (List) audienceAnalysis.get("keyTakeaways"); + if (keyTakeaways != null && !keyTakeaways.isEmpty()) { + segmentationData.put("keyTakeaways", keyTakeaways); + } + + if (!segmentationData.isEmpty()) { + chartsData.put("audienceSegmentation", segmentationData); } } - // 4. Каналы продвижения + // 4. Рынок (новая секция) + @SuppressWarnings("unchecked") + Map market = (Map) analysisJson.get("market"); + if (market != null) { + chartsData.put("market", market); + } + + // 5. Обратная совместимость: проверяем старую структуру для channels, funnel, + // swot + // (на случай, если где-то еще используется старая структура) @SuppressWarnings("unchecked") List> channels = (List>) analysisJson.get("channels"); if (channels != null && !channels.isEmpty()) { @@ -1122,7 +1330,6 @@ public class MarketingAnalysisService { chartsData.put("channelsPotential", channelsList); } - // 5. Воронка конверсии @SuppressWarnings("unchecked") Map funnel = (Map) analysisJson.get("funnel"); if (funnel != null) { @@ -1147,7 +1354,6 @@ public class MarketingAnalysisService { } } - // 6. SWOT (для таблицы) @SuppressWarnings("unchecked") Map swot = (Map) analysisJson.get("swot"); if (swot != null) {