.
This commit is contained in:
@@ -62,10 +62,10 @@ public class MarketingAnalysisService {
|
||||
analysis.setStatus("queued");
|
||||
addStatusHistoryEntry(analysis, "queued", "Комплексный анализ создан и добавлен в очередь");
|
||||
analysis = repository.save(analysis);
|
||||
|
||||
|
||||
logger.info("Unified marketing analysis created with ID: {} for user: {} with types: {}",
|
||||
analysis.getId(), userId, analysisTypes);
|
||||
|
||||
|
||||
return List.of(analysis);
|
||||
}
|
||||
|
||||
@@ -173,25 +173,30 @@ public class MarketingAnalysisService {
|
||||
|
||||
// Форматирование региона
|
||||
List<String> regions = request.getRegion();
|
||||
String regionStr = (regions != null && !regions.isEmpty())
|
||||
? String.join(", ", regions)
|
||||
: "Не указан";
|
||||
String regionStr = (regions != null && !regions.isEmpty())
|
||||
? String.join(", ", regions)
|
||||
: "Не указан";
|
||||
|
||||
// Построение промпта №1
|
||||
StringBuilder promptBuilder = new StringBuilder();
|
||||
promptBuilder.append("Ты — профессиональный маркетинговый аналитик, работающий с малым и средним бизнесом в Казахстане.\n\n");
|
||||
promptBuilder.append("Твоя задача — на основе входных данных о бизнесе пользователя выполнить маркетинговый анализ и выдать результат ТОЛЬКО в виде корректного JSON-объекта строго по указанной структуре.\n\n");
|
||||
promptBuilder.append(
|
||||
"Ты — профессиональный маркетинговый аналитик, работающий с малым и средним бизнесом в Казахстане.\n\n");
|
||||
promptBuilder.append(
|
||||
"Твоя задача — на основе входных данных о бизнесе пользователя выполнить маркетинговый анализ и выдать результат ТОЛЬКО в виде корректного JSON-объекта строго по указанной структуре.\n\n");
|
||||
promptBuilder.append("Не добавляй пояснений, комментариев, текста до или после JSON.\n");
|
||||
promptBuilder.append("Ответ должен быть валидным JSON.\n\n");
|
||||
promptBuilder.append("---\n\n");
|
||||
promptBuilder.append("ВХОДНЫЕ ДАННЫЕ ПОЛЬЗОВАТЕЛЯ:\n\n");
|
||||
promptBuilder.append("- Название компании/продукта: ").append(request.getProduct()).append("\n");
|
||||
promptBuilder.append("- Описание продукта/услуг: ").append(request.getProduct()).append(" (").append(request.getBusinessNiche()).append(")\n");
|
||||
promptBuilder.append("- Описание продукта/услуг: ").append(request.getProduct()).append(" (")
|
||||
.append(request.getBusinessNiche()).append(")\n");
|
||||
promptBuilder.append("- Целевая аудитория (если указана): ").append(targetAudienceStr).append("\n");
|
||||
promptBuilder.append("- Конкуренты (если указаны): ").append(request.getBusinessNiche()).append(" (уточни основных конкурентов)\n");
|
||||
promptBuilder.append("- Конкуренты (если указаны): ").append(request.getBusinessNiche())
|
||||
.append(" (уточни основных конкурентов)\n");
|
||||
promptBuilder.append("- Цель продвижения: ").append(request.getGoal()).append("\n");
|
||||
promptBuilder.append("- Регион/город: ").append(regionStr).append("\n\n");
|
||||
promptBuilder.append("## Если каких-то данных не хватает, ты можешь логично дополнить предположениями, НО в таких полях добавь флаг \"estimated\": true.\n\n");
|
||||
promptBuilder.append(
|
||||
"## Если каких-то данных не хватает, ты можешь логично дополнить предположениями, НО в таких полях добавь флаг \"estimated\": true.\n\n");
|
||||
promptBuilder.append("СФОРМИРУЙ ОДИН JSON СЛЕДУЮЩЕЙ СТРУКТУРЫ:\n\n");
|
||||
promptBuilder.append("{\n");
|
||||
promptBuilder.append("\"meta\": {\n");
|
||||
@@ -233,7 +238,8 @@ public class MarketingAnalysisService {
|
||||
promptBuilder.append("},\n\n");
|
||||
promptBuilder.append("\"channels\": [\n");
|
||||
promptBuilder.append("{\n");
|
||||
promptBuilder.append("\"name\": string, // \"Instagram\", \"TikTok\", \"Telegram\", \"2GIS\", \"Google Ads\" и т.п.\n");
|
||||
promptBuilder.append(
|
||||
"\"name\": string, // \"Instagram\", \"TikTok\", \"Telegram\", \"2GIS\", \"Google Ads\" и т.п.\n");
|
||||
promptBuilder.append("\"potentialIndex\": integer, // 0–100\n");
|
||||
promptBuilder.append("\"priorityLevel\": string, // \"высокий\", \"средний\", \"низкий\"\n");
|
||||
promptBuilder.append("\"roleInStrategy\": string, // коротко: для чего этот канал\n");
|
||||
@@ -245,7 +251,8 @@ public class MarketingAnalysisService {
|
||||
promptBuilder.append("\"description\": string,\n");
|
||||
promptBuilder.append("\"stages\": [\n");
|
||||
promptBuilder.append("{\n");
|
||||
promptBuilder.append("\"stageName\": string, // \"Охват\", \"Вовлечённость\", \"Клики\", \"Переходы\", \"Заявки\", \"Покупки\"\n");
|
||||
promptBuilder.append(
|
||||
"\"stageName\": string, // \"Охват\", \"Вовлечённость\", \"Клики\", \"Переходы\", \"Заявки\", \"Покупки\"\n");
|
||||
promptBuilder.append("\"value\": integer, // количество (должно логично уменьшаться вниз по воронке)\n");
|
||||
promptBuilder.append("\"conversionPercent\": number // от предыдущего этапа, с одним знаком после запятой\n");
|
||||
promptBuilder.append("}\n");
|
||||
@@ -287,7 +294,7 @@ public class MarketingAnalysisService {
|
||||
|
||||
String prompt = promptBuilder.toString();
|
||||
String context = buildContextString(request);
|
||||
|
||||
|
||||
try {
|
||||
String jsonResponse = openAIAnalyticsService.generateWithInstruction(context, prompt, "ru");
|
||||
if (jsonResponse == null || jsonResponse.trim().isEmpty()) {
|
||||
@@ -309,12 +316,13 @@ public class MarketingAnalysisService {
|
||||
cleanedJson = cleanedJson.trim();
|
||||
|
||||
// Парсинг JSON
|
||||
Map<String, Object> jsonMap = objectMapper.readValue(cleanedJson, new TypeReference<Map<String, Object>>() {});
|
||||
|
||||
Map<String, Object> jsonMap = objectMapper.readValue(cleanedJson, new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
|
||||
// Валидация JSON
|
||||
if (validateJsonAnalysis(jsonMap)) {
|
||||
return jsonMap;
|
||||
} else {
|
||||
} else {
|
||||
logger.warn("JSON validation failed, but returning parsed JSON anyway");
|
||||
return jsonMap;
|
||||
}
|
||||
@@ -342,7 +350,8 @@ public class MarketingAnalysisService {
|
||||
promptBuilder.append("Ты — профессиональный маркетолог и аналитик.\n\n");
|
||||
promptBuilder.append("Тебе передан JSON с результатами маркетингового анализа в следующей структуре:\n");
|
||||
promptBuilder.append(jsonString).append("\n\n");
|
||||
promptBuilder.append("На основе этих данных подготовь ПОНЯТНЫЙ для предпринимателя малого/среднего бизнеса текстовый отчёт.\n\n");
|
||||
promptBuilder.append(
|
||||
"На основе этих данных подготовь ПОНЯТНЫЙ для предпринимателя малого/среднего бизнеса текстовый отчёт.\n\n");
|
||||
promptBuilder.append("ТРЕБОВАНИЯ К ОТЧЁТУ:\n\n");
|
||||
promptBuilder.append("1. Структура разделов:\n");
|
||||
promptBuilder.append(" I. Краткое резюме (что за бизнес, какая цель, общий вывод)\n");
|
||||
@@ -393,10 +402,10 @@ public class MarketingAnalysisService {
|
||||
|
||||
try {
|
||||
// Проверка обязательных полей
|
||||
if (!jsonMap.containsKey("meta") || !jsonMap.containsKey("seasonality") ||
|
||||
!jsonMap.containsKey("competitors") || !jsonMap.containsKey("audienceSegments") ||
|
||||
!jsonMap.containsKey("channels") || !jsonMap.containsKey("funnel") ||
|
||||
!jsonMap.containsKey("swot")) {
|
||||
if (!jsonMap.containsKey("meta") || !jsonMap.containsKey("seasonality") ||
|
||||
!jsonMap.containsKey("competitors") || !jsonMap.containsKey("audienceSegments") ||
|
||||
!jsonMap.containsKey("channels") || !jsonMap.containsKey("funnel") ||
|
||||
!jsonMap.containsKey("swot")) {
|
||||
logger.warn("Missing required fields in JSON analysis");
|
||||
return false;
|
||||
}
|
||||
@@ -504,7 +513,7 @@ public class MarketingAnalysisService {
|
||||
*/
|
||||
private Map<String, Object> parseChartDataFromJson(Map<String, Object> analysisJson) {
|
||||
Map<String, Object> chartsData = new HashMap<>();
|
||||
|
||||
|
||||
if (analysisJson == null || analysisJson.isEmpty()) {
|
||||
return chartsData;
|
||||
}
|
||||
@@ -518,7 +527,7 @@ public class MarketingAnalysisService {
|
||||
List<String> months = (List<String>) seasonality.get("months");
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Object> demandIndex = (List<Object>) seasonality.get("demandIndex");
|
||||
|
||||
|
||||
if (months != null && demandIndex != null && months.size() == demandIndex.size()) {
|
||||
Map<String, Integer> seasonalityMap = new LinkedHashMap<>();
|
||||
for (int i = 0; i < months.size() && i < demandIndex.size(); i++) {
|
||||
@@ -560,37 +569,37 @@ public class MarketingAnalysisService {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Преобразуем в нужный формат
|
||||
List<Map<String, String>> ageGroupsList = new ArrayList<>();
|
||||
for (Map.Entry<String, Integer> entry : ageGroups.entrySet()) {
|
||||
@@ -599,7 +608,7 @@ public class MarketingAnalysisService {
|
||||
ageGroup.put("value", String.valueOf(entry.getValue()));
|
||||
ageGroupsList.add(ageGroup);
|
||||
}
|
||||
|
||||
|
||||
List<Map<String, String>> gendersList = new ArrayList<>();
|
||||
for (Map.Entry<String, Integer> entry : genders.entrySet()) {
|
||||
Map<String, String> genderData = new HashMap<>();
|
||||
@@ -607,7 +616,7 @@ public class MarketingAnalysisService {
|
||||
genderData.put("value", String.valueOf(entry.getValue()));
|
||||
gendersList.add(genderData);
|
||||
}
|
||||
|
||||
|
||||
if (!ageGroupsList.isEmpty()) {
|
||||
segmentationData.put("ageGroups", ageGroupsList);
|
||||
}
|
||||
@@ -617,7 +626,7 @@ public class MarketingAnalysisService {
|
||||
if (!segmentsList.isEmpty()) {
|
||||
segmentationData.put("segments", segmentsList);
|
||||
}
|
||||
|
||||
|
||||
if (!segmentationData.isEmpty()) {
|
||||
chartsData.put("audienceSegmentation", segmentationData);
|
||||
}
|
||||
@@ -651,11 +660,11 @@ public class MarketingAnalysisService {
|
||||
Map<String, Object> stageData = new HashMap<>();
|
||||
stageData.put("stage", stage.getOrDefault("stageName", "Этап"));
|
||||
stageData.put("value", stage.getOrDefault("value", 0));
|
||||
|
||||
|
||||
Object conversionObj = stage.get("conversionPercent");
|
||||
if (conversionObj instanceof Number) {
|
||||
stageData.put("conversion", ((Number) conversionObj).intValue());
|
||||
} else {
|
||||
} else {
|
||||
stageData.put("conversion", 0);
|
||||
}
|
||||
funnelList.add(stageData);
|
||||
@@ -688,7 +697,7 @@ public class MarketingAnalysisService {
|
||||
}
|
||||
DetailLevel detailLevel = request.getDetailLevelEnum();
|
||||
String detailLevelName = detailLevel != null ? detailLevel.getDisplayName() : "Стандартно";
|
||||
|
||||
|
||||
// Build list of analysis type names for display
|
||||
List<String> analysisTypeNames = analysisTypes.stream()
|
||||
.map(AnalysisType::getDisplayName)
|
||||
@@ -726,7 +735,7 @@ public class MarketingAnalysisService {
|
||||
}
|
||||
contextBuilder.append("Целевая аудитория: ").append(audienceBuilder.toString().trim()).append("\n");
|
||||
}
|
||||
|
||||
|
||||
// Format regions
|
||||
List<String> regions = request.getRegion();
|
||||
if (regions != null && !regions.isEmpty()) {
|
||||
@@ -745,7 +754,7 @@ public class MarketingAnalysisService {
|
||||
|
||||
// Шаг 1: Генерация JSON-анализа (Промпт №1)
|
||||
Map<String, Object> analysisJson = generateJsonAnalysis(request);
|
||||
|
||||
|
||||
if (analysisJson == null || analysisJson.isEmpty()) {
|
||||
logger.error("Failed to generate JSON analysis");
|
||||
// При ошибке возвращаем базовую структуру с сообщением об ошибке
|
||||
@@ -757,19 +766,21 @@ public class MarketingAnalysisService {
|
||||
} else {
|
||||
// Сохраняем JSON в отчёт
|
||||
report.put("analysisJson", analysisJson);
|
||||
|
||||
|
||||
// Шаг 2: Генерация текстового отчёта на основе JSON (Промпт №2)
|
||||
String textReport = generateTextReportFromJson(analysisJson);
|
||||
|
||||
|
||||
// Извлекаем данные для графиков из JSON
|
||||
Map<String, Object> chartsData = parseChartDataFromJson(analysisJson);
|
||||
report.put("chartsData", chartsData);
|
||||
|
||||
|
||||
// Сохраняем данные анализа
|
||||
report.put("analysisTypes", analysisTypeNames);
|
||||
report.put("detailLevel", detailLevelName);
|
||||
report.put("fullAnalysis", textReport != null ? textReport : "Анализ не удалось сгенерировать.");
|
||||
report.put("summary", textReport != null ? textReport : "Анализ не удалось сгенерировать."); // Keep for backward compatibility
|
||||
report.put("summary", textReport != null ? textReport : "Анализ не удалось сгенерировать."); // Keep for
|
||||
// backward
|
||||
// compatibility
|
||||
}
|
||||
|
||||
// Generate target audience description (adjusted by detail level)
|
||||
@@ -1072,7 +1083,7 @@ public class MarketingAnalysisService {
|
||||
Map<String, Object> chartsData = (Map<String, Object>) reportData.get("chartsData");
|
||||
if (chartsData != null && !chartsData.isEmpty()) {
|
||||
markdown.append("## 📊 Числовые данные для графиков\n\n");
|
||||
|
||||
|
||||
// Seasonality table
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Integer> seasonality = (Map<String, Integer>) chartsData.get("seasonality");
|
||||
@@ -1085,14 +1096,16 @@ public class MarketingAnalysisService {
|
||||
}
|
||||
markdown.append("\n");
|
||||
}
|
||||
|
||||
|
||||
// Competitors table
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> competitors = (List<Map<String, Object>>) chartsData.get("competitors");
|
||||
if (competitors != null && !competitors.isEmpty()) {
|
||||
markdown.append("### 2️⃣ Сравнительный анализ конкурентов\n\n");
|
||||
markdown.append("| Конкурент | Охват аудитории | Активность в соцсетях (0–100) | Количество отзывов | Сильные стороны | Слабые стороны |\n");
|
||||
markdown.append("|-----------|-----------------|-------------------------------|-------------------|----------------|----------------|\n");
|
||||
markdown.append(
|
||||
"| Конкурент | Охват аудитории | Активность в соцсетях (0–100) | Количество отзывов | Сильные стороны | Слабые стороны |\n");
|
||||
markdown.append(
|
||||
"|-----------|-----------------|-------------------------------|-------------------|----------------|----------------|\n");
|
||||
for (Map<String, Object> comp : competitors) {
|
||||
markdown.append("| ").append(comp.get("name")).append(" | ")
|
||||
.append(comp.get("reach")).append(" | ")
|
||||
@@ -1103,13 +1116,13 @@ public class MarketingAnalysisService {
|
||||
}
|
||||
markdown.append("\n");
|
||||
}
|
||||
|
||||
|
||||
// Audience segmentation tables
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> audienceSegmentation = (Map<String, Object>) chartsData.get("audienceSegmentation");
|
||||
if (audienceSegmentation != null && !audienceSegmentation.isEmpty()) {
|
||||
markdown.append("### 3️⃣ Сегментация целевой аудитории\n\n");
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, String>> ageGroups = (List<Map<String, String>>) audienceSegmentation.get("ageGroups");
|
||||
if (ageGroups != null && !ageGroups.isEmpty()) {
|
||||
@@ -1117,11 +1130,12 @@ public class MarketingAnalysisService {
|
||||
markdown.append("| Возрастная группа | Процент (%) |\n");
|
||||
markdown.append("|-------------------|-------------|\n");
|
||||
for (Map<String, String> group : ageGroups) {
|
||||
markdown.append("| ").append(group.get("label")).append(" | ").append(group.get("value")).append(" |\n");
|
||||
markdown.append("| ").append(group.get("label")).append(" | ").append(group.get("value"))
|
||||
.append(" |\n");
|
||||
}
|
||||
markdown.append("\n");
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, String>> genders = (List<Map<String, String>>) audienceSegmentation.get("genders");
|
||||
if (genders != null && !genders.isEmpty()) {
|
||||
@@ -1129,11 +1143,12 @@ public class MarketingAnalysisService {
|
||||
markdown.append("| Пол | Процент (%) |\n");
|
||||
markdown.append("|-----|-------------|\n");
|
||||
for (Map<String, String> gender : genders) {
|
||||
markdown.append("| ").append(gender.get("label")).append(" | ").append(gender.get("value")).append(" |\n");
|
||||
markdown.append("| ").append(gender.get("label")).append(" | ").append(gender.get("value"))
|
||||
.append(" |\n");
|
||||
}
|
||||
markdown.append("\n");
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, String>> segments = (List<Map<String, String>>) audienceSegmentation.get("segments");
|
||||
if (segments != null && !segments.isEmpty()) {
|
||||
@@ -1141,15 +1156,17 @@ public class MarketingAnalysisService {
|
||||
markdown.append("| Сегмент | Процент (%) |\n");
|
||||
markdown.append("|---------|-------------|\n");
|
||||
for (Map<String, String> segment : segments) {
|
||||
markdown.append("| ").append(segment.get("label")).append(" | ").append(segment.get("value")).append(" |\n");
|
||||
markdown.append("| ").append(segment.get("label")).append(" | ").append(segment.get("value"))
|
||||
.append(" |\n");
|
||||
}
|
||||
markdown.append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Channels potential table
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> channelsPotential = (List<Map<String, Object>>) chartsData.get("channelsPotential");
|
||||
List<Map<String, Object>> channelsPotential = (List<Map<String, Object>>) chartsData
|
||||
.get("channelsPotential");
|
||||
if (channelsPotential != null && !channelsPotential.isEmpty()) {
|
||||
markdown.append("### 4️⃣ Потенциал каналов продвижения\n\n");
|
||||
markdown.append("| Канал | Потенциал (0–100) | Обоснование |\n");
|
||||
@@ -1161,7 +1178,7 @@ public class MarketingAnalysisService {
|
||||
}
|
||||
markdown.append("\n");
|
||||
}
|
||||
|
||||
|
||||
// Conversion funnel table
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> conversionFunnel = (List<Map<String, Object>>) chartsData.get("conversionFunnel");
|
||||
@@ -1273,7 +1290,8 @@ public class MarketingAnalysisService {
|
||||
|
||||
// Content Recommendations section
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> contentRecommendations = (Map<String, Object>) analysisJson.get("contentRecommendations");
|
||||
Map<String, Object> contentRecommendations = (Map<String, Object>) analysisJson
|
||||
.get("contentRecommendations");
|
||||
if (contentRecommendations != null) {
|
||||
markdown.append("## Рекомендации по контенту\n\n");
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -1322,10 +1340,10 @@ public class MarketingAnalysisService {
|
||||
markdown.append(fullAnalysis).append("\n\n");
|
||||
} else {
|
||||
// Fallback to summary if fullAnalysis is not available
|
||||
String summary = (String) reportData.get("summary");
|
||||
if (summary != null && !summary.trim().isEmpty()) {
|
||||
markdown.append("## Комплексный маркетинговый анализ\n\n");
|
||||
markdown.append(summary).append("\n\n");
|
||||
String summary = (String) reportData.get("summary");
|
||||
if (summary != null && !summary.trim().isEmpty()) {
|
||||
markdown.append("## Комплексный маркетинговый анализ\n\n");
|
||||
markdown.append(summary).append("\n\n");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1429,7 +1447,7 @@ public class MarketingAnalysisService {
|
||||
|
||||
// Set summary (backward compatibility)
|
||||
report.setSummary((String) reportData.get("summary"));
|
||||
|
||||
|
||||
// Set full analysis (new field)
|
||||
String fullAnalysis = (String) reportData.get("fullAnalysis");
|
||||
if (fullAnalysis != null) {
|
||||
|
||||
Reference in New Issue
Block a user