From 7f77f48020facd59f3b5900b439e3d04fc53f923 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 9 Dec 2025 22:17:32 +0500 Subject: [PATCH] . --- .../service/MarketingAnalysisService.java | 142 ++++++++++-------- 1 file changed, 80 insertions(+), 62 deletions(-) diff --git a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java index b0287d2..669100f 100644 --- a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java +++ b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java @@ -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 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 jsonMap = objectMapper.readValue(cleanedJson, new TypeReference>() {}); - + Map jsonMap = objectMapper.readValue(cleanedJson, new TypeReference>() { + }); + // Валидация 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 parseChartDataFromJson(Map analysisJson) { Map chartsData = new HashMap<>(); - + if (analysisJson == null || analysisJson.isEmpty()) { return chartsData; } @@ -518,7 +527,7 @@ public class MarketingAnalysisService { List months = (List) seasonality.get("months"); @SuppressWarnings("unchecked") List demandIndex = (List) seasonality.get("demandIndex"); - + if (months != null && demandIndex != null && months.size() == demandIndex.size()) { Map seasonalityMap = new LinkedHashMap<>(); for (int i = 0; i < months.size() && i < demandIndex.size(); i++) { @@ -560,37 +569,37 @@ public class MarketingAnalysisService { 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); } } - + // Преобразуем в нужный формат List> ageGroupsList = new ArrayList<>(); for (Map.Entry entry : ageGroups.entrySet()) { @@ -599,7 +608,7 @@ public class MarketingAnalysisService { ageGroup.put("value", String.valueOf(entry.getValue())); ageGroupsList.add(ageGroup); } - + List> gendersList = new ArrayList<>(); for (Map.Entry entry : genders.entrySet()) { Map 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 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 analysisTypeNames = analysisTypes.stream() .map(AnalysisType::getDisplayName) @@ -726,7 +735,7 @@ public class MarketingAnalysisService { } contextBuilder.append("Целевая аудитория: ").append(audienceBuilder.toString().trim()).append("\n"); } - + // Format regions List regions = request.getRegion(); if (regions != null && !regions.isEmpty()) { @@ -745,7 +754,7 @@ public class MarketingAnalysisService { // Шаг 1: Генерация JSON-анализа (Промпт №1) Map 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 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 chartsData = (Map) reportData.get("chartsData"); if (chartsData != null && !chartsData.isEmpty()) { markdown.append("## 📊 Числовые данные для графиков\n\n"); - + // Seasonality table @SuppressWarnings("unchecked") Map seasonality = (Map) chartsData.get("seasonality"); @@ -1085,14 +1096,16 @@ public class MarketingAnalysisService { } markdown.append("\n"); } - + // Competitors table @SuppressWarnings("unchecked") List> competitors = (List>) 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 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 audienceSegmentation = (Map) chartsData.get("audienceSegmentation"); if (audienceSegmentation != null && !audienceSegmentation.isEmpty()) { markdown.append("### 3️⃣ Сегментация целевой аудитории\n\n"); - + @SuppressWarnings("unchecked") List> ageGroups = (List>) audienceSegmentation.get("ageGroups"); if (ageGroups != null && !ageGroups.isEmpty()) { @@ -1117,11 +1130,12 @@ public class MarketingAnalysisService { markdown.append("| Возрастная группа | Процент (%) |\n"); markdown.append("|-------------------|-------------|\n"); for (Map 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> genders = (List>) audienceSegmentation.get("genders"); if (genders != null && !genders.isEmpty()) { @@ -1129,11 +1143,12 @@ public class MarketingAnalysisService { markdown.append("| Пол | Процент (%) |\n"); markdown.append("|-----|-------------|\n"); for (Map 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> segments = (List>) audienceSegmentation.get("segments"); if (segments != null && !segments.isEmpty()) { @@ -1141,15 +1156,17 @@ public class MarketingAnalysisService { markdown.append("| Сегмент | Процент (%) |\n"); markdown.append("|---------|-------------|\n"); for (Map 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> channelsPotential = (List>) chartsData.get("channelsPotential"); + List> channelsPotential = (List>) 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> conversionFunnel = (List>) chartsData.get("conversionFunnel"); @@ -1273,7 +1290,8 @@ public class MarketingAnalysisService { // Content Recommendations section @SuppressWarnings("unchecked") - Map contentRecommendations = (Map) analysisJson.get("contentRecommendations"); + Map contentRecommendations = (Map) 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) {