This commit is contained in:
root
2025-12-14 20:43:09 +05:00
parent fff887df89
commit 65df259fc8
2 changed files with 708 additions and 179 deletions
+323
View File
@@ -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` | ✅ Без изменений | Осталось прежним |
## Вопросы?
Если возникнут вопросы по миграции, обращайтесь к бэкенд-команде.
@@ -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<String, Object> generateJsonAnalysis(MarketingAnalysisRequest request) {
Map<String, Object> result = new HashMap<>();
// 1. Получаем анализ целевой аудитории
logger.info("Starting audience analysis generation");
Map<String, Object> 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<String, Object> 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<Map<String, Object>> audienceFuture = (CompletableFuture<Map<String, Object>>) CompletableFuture
.<Map<String, Object>>supplyAsync(() -> {
logger.info("Starting audience analysis generation");
Map<String, Object> 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<String, Object> 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<Map<String, Object>> competitorFuture = (CompletableFuture<Map<String, Object>>) CompletableFuture
.<Map<String, Object>>supplyAsync(() -> {
logger.info("Starting competitor analysis generation");
Map<String, Object> 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<Map<String, Object>> seasonalityFuture = (CompletableFuture<Map<String, Object>>) CompletableFuture
.<Map<String, Object>>supplyAsync(() -> {
logger.info("Starting seasonality and market analysis generation");
Map<String, Object> 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<String, Object> audienceAnalysis = audienceFuture.join();
Map<String, Object> competitorAnalysis = competitorFuture.join();
Map<String, Object> 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<String, Object> audienceAnalysis = audienceFuture.getNow(new HashMap<>());
Map<String, Object> competitorAnalysis = competitorFuture.getNow(new HashMap<>());
Map<String, Object> 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<String, Object> 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<String, Object> jsonMap = objectMapper.readValue(cleanedJson, new TypeReference<Map<String, Object>>() {
});
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<String, Object> jsonMap = objectMapper.readValue(cleanedJson, new TypeReference<Map<String, Object>>() {
});
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<String, Object> jsonMap = objectMapper.readValue(cleanedJson, new TypeReference<Map<String, Object>>() {
});
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<String, Object> parseChartDataFromJson(Map<String, Object> analysisJson) {
Map<String, Object> chartsData = new HashMap<>();
@@ -993,7 +1155,7 @@ public class MarketingAnalysisService {
}
try {
// 1. Сезонность
// 1. Сезонность (структура не изменилась)
@SuppressWarnings("unchecked")
Map<String, Object> seasonality = (Map<String, Object>) analysisJson.get("seasonality");
if (seasonality != null) {
@@ -1017,97 +1179,143 @@ public class MarketingAnalysisService {
}
}
// 2. Конкуренты
// 2. Конкуренты (новая структура: competitorAnalysis)
@SuppressWarnings("unchecked")
List<Map<String, Object>> competitors = (List<Map<String, Object>>) analysisJson.get("competitors");
if (competitors != null && !competitors.isEmpty()) {
List<Map<String, Object>> competitorsList = new ArrayList<>();
for (Map<String, Object> comp : competitors) {
Map<String, Object> 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<String, Object> competitorAnalysis = (Map<String, Object>) analysisJson.get("competitorAnalysis");
if (competitorAnalysis != null) {
// 2.1. Market Share Chart
@SuppressWarnings("unchecked")
List<Map<String, Object>> marketShareChart = (List<Map<String, Object>>) competitorAnalysis
.get("marketShareChart");
if (marketShareChart != null && !marketShareChart.isEmpty()) {
chartsData.put("marketShareChart", marketShareChart);
}
// 2.2. Competitors Details (аналог старого competitors)
@SuppressWarnings("unchecked")
List<Map<String, Object>> competitorsDetails = (List<Map<String, Object>>) competitorAnalysis
.get("competitorsDetails");
if (competitorsDetails != null && !competitorsDetails.isEmpty()) {
List<Map<String, Object>> competitorsList = new ArrayList<>();
for (Map<String, Object> comp : competitorsDetails) {
Map<String, Object> competitorData = new HashMap<>();
competitorData.put("name", comp.getOrDefault("name", "Неизвестный конкурент"));
@SuppressWarnings("unchecked")
Map<String, Object> digitalMetrics = (Map<String, Object>) 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<Map<String, Object>> comparisonTable = (List<Map<String, Object>>) competitorAnalysis
.get("comparisonTable");
if (comparisonTable != null && !comparisonTable.isEmpty()) {
chartsData.put("comparisonTable", comparisonTable);
}
// 2.4. SWOT Competitors (частичный SWOT из competitorAnalysis)
@SuppressWarnings("unchecked")
Map<String, Object> swotCompetitors = (Map<String, Object>) competitorAnalysis.get("swotCompetitors");
if (swotCompetitors != null) {
chartsData.put("swotCompetitors", swotCompetitors);
}
chartsData.put("competitors", competitorsList);
}
// 3. Сегментация аудитории
// 3. Анализ аудитории (новая структура: audienceAnalysis)
@SuppressWarnings("unchecked")
Map<String, Object> audienceSegments = (Map<String, Object>) analysisJson.get("audienceSegments");
if (audienceSegments != null) {
Map<String, Object> audienceAnalysis = (Map<String, Object>) analysisJson.get("audienceAnalysis");
if (audienceAnalysis != null) {
Map<String, Object> segmentationData = new HashMap<>();
// 3.1. Age Groups (готовые данные из новой структуры)
@SuppressWarnings("unchecked")
List<Map<String, Object>> segments = (List<Map<String, Object>>) audienceSegments.get("segments");
if (segments != null && !segments.isEmpty()) {
Map<String, Object> segmentationData = new HashMap<>();
// Группировка по возрасту
Map<String, Integer> ageGroups = new HashMap<>();
Map<String, Integer> genders = new HashMap<>();
List<Map<String, String>> segmentsList = new ArrayList<>();
for (Map<String, Object> 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<String, String> 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<String, Object> ageGroupsMap = (Map<String, Object>) audienceAnalysis.get("ageGroups");
if (ageGroupsMap != null) {
List<Map<String, String>> ageGroupsList = new ArrayList<>();
for (Map.Entry<String, Integer> entry : ageGroups.entrySet()) {
for (Map.Entry<String, Object> entry : ageGroupsMap.entrySet()) {
Map<String, String> 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<String, Object> genderDistributionMap = (Map<String, Object>) audienceAnalysis
.get("genderDistribution");
if (genderDistributionMap != null) {
List<Map<String, String>> gendersList = new ArrayList<>();
for (Map.Entry<String, Integer> entry : genders.entrySet()) {
for (Map.Entry<String, Object> entry : genderDistributionMap.entrySet()) {
Map<String, String> 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<Map<String, Object>> segments = (List<Map<String, Object>>) audienceAnalysis.get("segments");
if (segments != null && !segments.isEmpty()) {
List<Map<String, String>> segmentsList = new ArrayList<>();
for (Map<String, Object> segment : segments) {
Map<String, String> 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<Map<String, Object>> segmentsChannelMatrix = (List<Map<String, Object>>) audienceAnalysis
.get("segmentsChannelMatrix");
if (segmentsChannelMatrix != null && !segmentsChannelMatrix.isEmpty()) {
segmentationData.put("segmentsChannelMatrix", segmentsChannelMatrix);
}
// 3.5. Key Takeaways
@SuppressWarnings("unchecked")
List<String> keyTakeaways = (List<String>) audienceAnalysis.get("keyTakeaways");
if (keyTakeaways != null && !keyTakeaways.isEmpty()) {
segmentationData.put("keyTakeaways", keyTakeaways);
}
if (!segmentationData.isEmpty()) {
chartsData.put("audienceSegmentation", segmentationData);
}
}
// 4. Каналы продвижения
// 4. Рынок (новая секция)
@SuppressWarnings("unchecked")
Map<String, Object> market = (Map<String, Object>) analysisJson.get("market");
if (market != null) {
chartsData.put("market", market);
}
// 5. Обратная совместимость: проверяем старую структуру для channels, funnel,
// swot
// (на случай, если где-то еще используется старая структура)
@SuppressWarnings("unchecked")
List<Map<String, Object>> channels = (List<Map<String, Object>>) analysisJson.get("channels");
if (channels != null && !channels.isEmpty()) {
@@ -1122,7 +1330,6 @@ public class MarketingAnalysisService {
chartsData.put("channelsPotential", channelsList);
}
// 5. Воронка конверсии
@SuppressWarnings("unchecked")
Map<String, Object> funnel = (Map<String, Object>) analysisJson.get("funnel");
if (funnel != null) {
@@ -1147,7 +1354,6 @@ public class MarketingAnalysisService {
}
}
// 6. SWOT (для таблицы)
@SuppressWarnings("unchecked")
Map<String, Object> swot = (Map<String, Object>) analysisJson.get("swot");
if (swot != null) {