From 71f51d58d288face4554e2a9b2cc84c923931535 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 14 Dec 2025 22:31:13 +0500 Subject: [PATCH] . --- .../service/MarketingAnalysisService.java | 343 ++++++++++++++++-- 1 file changed, 304 insertions(+), 39 deletions(-) diff --git a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java index 954d31d..a222322 100644 --- a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java +++ b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java @@ -276,15 +276,33 @@ public class MarketingAnalysisService { return new HashMap<>(); }); + @SuppressWarnings("unchecked") + CompletableFuture> strategyFuture = (CompletableFuture>) CompletableFuture + .>supplyAsync(() -> { + logger.info("Starting strategy and funnel analysis generation"); + Map analysis = getStrategyAndFunnel(request); + if (analysis != null && !analysis.isEmpty()) { + logger.info("Strategy and funnel analysis generated successfully"); + } else { + logger.warn("Strategy and funnel analysis returned empty result"); + } + return analysis != null ? analysis : new HashMap<>(); + }) + .exceptionally(ex -> { + logger.error("Error in strategy analysis future: {}", ex.getMessage(), ex); + return new HashMap<>(); + }); + // Ожидаем завершения всех futures try { - CompletableFuture.allOf(audienceFuture, competitorFuture, seasonalityFuture) + CompletableFuture.allOf(audienceFuture, competitorFuture, seasonalityFuture, strategyFuture) .get(5, TimeUnit.MINUTES); // Таймаут 5 минут // Собираем результаты Map audienceAnalysis = audienceFuture.join(); Map competitorAnalysis = competitorFuture.join(); Map seasonalityAndMarket = seasonalityFuture.join(); + Map strategyAndFunnel = strategyFuture.join(); if (audienceAnalysis != null && !audienceAnalysis.isEmpty()) { result.putAll(audienceAnalysis); @@ -295,6 +313,9 @@ public class MarketingAnalysisService { if (seasonalityAndMarket != null && !seasonalityAndMarket.isEmpty()) { result.putAll(seasonalityAndMarket); } + if (strategyAndFunnel != null && !strategyAndFunnel.isEmpty()) { + result.putAll(strategyAndFunnel); + } } catch (Exception e) { logger.error("Error waiting for analysis futures: {}", e.getMessage(), e); @@ -303,6 +324,7 @@ public class MarketingAnalysisService { Map audienceAnalysis = audienceFuture.getNow(new HashMap<>()); Map competitorAnalysis = competitorFuture.getNow(new HashMap<>()); Map seasonalityAndMarket = seasonalityFuture.getNow(new HashMap<>()); + Map strategyAndFunnel = strategyFuture.getNow(new HashMap<>()); if (audienceAnalysis != null && !audienceAnalysis.isEmpty()) { result.putAll(audienceAnalysis); @@ -313,6 +335,9 @@ public class MarketingAnalysisService { if (seasonalityAndMarket != null && !seasonalityAndMarket.isEmpty()) { result.putAll(seasonalityAndMarket); } + if (strategyAndFunnel != null && !strategyAndFunnel.isEmpty()) { + result.putAll(strategyAndFunnel); + } } catch (Exception ex) { logger.error("Error getting partial results: {}", ex.getMessage(), ex); } @@ -771,6 +796,184 @@ public class MarketingAnalysisService { return new HashMap<>(); } + /** + * Генерация анализа стратегии, воронки, каналов и SWOT + * Модульный метод для 4-го этапа генерации + */ + private Map getStrategyAndFunnel(MarketingAnalysisRequest request) { + try { + String context = buildContextString(request); + + StringBuilder promptBuilder = new StringBuilder(); + promptBuilder.append( + "Ты — профессиональный маркетинговый аналитик и стратег, работающий с малым и средним бизнесом в Казахстане.\n\n"); + promptBuilder.append( + "Твоя задача — на основе входных данных о бизнесе выполнить глубокий анализ стратегии, воронки конверсии, потенциала каналов продвижения и SWOT-анализа бизнеса.\n\n"); + promptBuilder.append( + "КРИТИЧЕСКИ ВАЖНО: Предоставь детальный анализ с конкретными цифрами и инсайтами. " + + "Воронка должна отражать реалистичные этапы конверсии для данного бизнеса. " + + "Каналы должны быть оценены по их реальному потенциалу для данной ниши в Казахстане. " + + "SWOT должен содержать конкретные сильные и слабые стороны, возможности и угрозы.\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"); + + List regions = request.getRegion(); + String regionStr = (regions != null && !regions.isEmpty()) + ? String.join(", ", regions) + : "Не указан"; + promptBuilder.append("- Регион/город: ").append(regionStr).append("\n"); + promptBuilder.append("- Цель продвижения: ").append(request.getGoal()).append("\n\n"); + + promptBuilder.append("СФОРМИРУЙ JSON СЛЕДУЮЩЕЙ СТРУКТУРЫ:\n\n"); + promptBuilder.append("{\n"); + promptBuilder.append(" \"strategyAnalysis\": {\n"); + promptBuilder.append(" \"funnel\": {\n"); + promptBuilder.append(" \"stages\": [\n"); + promptBuilder.append(" {\n"); + promptBuilder.append(" \"stageName\": \"Охват\",\n"); + promptBuilder.append(" \"value\": число, // Количество людей на этапе\n"); + promptBuilder.append( + " \"conversionPercent\": 100 // Процент конверсии (для первого этапа всегда 100)\n"); + promptBuilder.append(" },\n"); + promptBuilder.append(" {\n"); + promptBuilder.append(" \"stageName\": \"Интерес\",\n"); + promptBuilder.append(" \"value\": число, // Количество людей на этапе\n"); + promptBuilder.append(" \"conversionPercent\": число // Процент конверсии от предыдущего этапа\n"); + promptBuilder.append(" },\n"); + promptBuilder.append(" {\n"); + promptBuilder.append(" \"stageName\": \"Заявка\",\n"); + promptBuilder.append(" \"value\": число,\n"); + promptBuilder.append(" \"conversionPercent\": число\n"); + promptBuilder.append(" },\n"); + promptBuilder.append(" {\n"); + promptBuilder.append(" \"stageName\": \"Покупка\",\n"); + promptBuilder.append(" \"value\": число,\n"); + promptBuilder.append(" \"conversionPercent\": число\n"); + promptBuilder.append(" }\n"); + promptBuilder.append(" ],\n"); + promptBuilder.append( + " \"analysis\": \"Детальный анализ воронки конверсии (3-4 предложения). Опиши, где находится узкое место воронки, какие этапы имеют низкую конверсию и почему. Дай конкретные рекомендации по улучшению каждого этапа.\"\n"); + promptBuilder.append(" },\n"); + promptBuilder.append(" \"channelsPotential\": [\n"); + promptBuilder.append(" {\n"); + promptBuilder.append(" \"name\": \"Instagram\",\n"); + promptBuilder.append(" \"potentialIndex\": число, // Индекс потенциала от 0 до 100\n"); + promptBuilder.append(" \"priority\": \"High\" // High, Medium или Low\n"); + promptBuilder.append(" },\n"); + promptBuilder.append(" {\n"); + promptBuilder.append(" \"name\": \"Google Ads\",\n"); + promptBuilder.append(" \"potentialIndex\": число,\n"); + promptBuilder.append(" \"priority\": \"Medium\"\n"); + promptBuilder.append(" },\n"); + promptBuilder.append(" {\n"); + promptBuilder.append(" \"name\": \"TikTok\",\n"); + promptBuilder.append(" \"potentialIndex\": число,\n"); + promptBuilder.append(" \"priority\": \"Medium\"\n"); + promptBuilder.append(" }\n"); + promptBuilder.append(" ],\n"); + promptBuilder.append(" \"globalSwot\": {\n"); + promptBuilder.append( + " \"strengths\": [\"Конкретная сильная сторона 1 (1-2 предложения)\", \"Конкретная сильная сторона 2 (1-2 предложения)\", \"Конкретная сильная сторона 3 (1-2 предложения)\"],\n"); + promptBuilder.append( + " \"weaknesses\": [\"Конкретная слабая сторона 1 (1-2 предложения)\", \"Конкретная слабая сторона 2 (1-2 предложения)\", \"Конкретная слабая сторона 3 (1-2 предложения)\"],\n"); + promptBuilder.append( + " \"opportunities\": [\"Конкретная возможность 1 (1-2 предложения)\", \"Конкретная возможность 2 (1-2 предложения)\", \"Конкретная возможность 3 (1-2 предложения)\"],\n"); + promptBuilder.append( + " \"threats\": [\"Конкретная угроза 1 (1-2 предложения)\", \"Конкретная угроза 2 (1-2 предложения)\", \"Конкретная угроза 3 (1-2 предложения)\"]\n"); + promptBuilder.append(" }\n"); + promptBuilder.append(" }\n"); + promptBuilder.append("}\n\n"); + promptBuilder.append("КРИТИЧЕСКИ ВАЖНЫЕ ТРЕБОВАНИЯ:\n\n"); + promptBuilder.append("1. Верни ТОЛЬКО JSON, без пояснений, комментариев, markdown-разметки.\n"); + promptBuilder.append("2. JSON должен быть синтаксически корректным и валидным.\n"); + promptBuilder.append("3. Воронка должна содержать минимум 4 этапа с реалистичными значениями.\n"); + promptBuilder.append( + "4. Значения в funnel.stages должны быть логичными: каждый следующий этап должен иметь меньшее или равное значение value по сравнению с предыдущим.\n"); + promptBuilder.append("5. conversionPercent должен отражать процент конверсии от предыдущего этапа.\n"); + promptBuilder.append( + "6. channelsPotential должен содержать минимум 3-5 каналов с оценкой их потенциала для данного бизнеса.\n"); + promptBuilder.append("7. potentialIndex должен быть числом от 0 до 100.\n"); + promptBuilder.append("8. priority должен быть одним из: \"High\", \"Medium\", \"Low\".\n"); + promptBuilder.append( + "9. globalSwot должен содержать конкретные пункты (по 3-4 в каждой категории), а не общие фразы.\n"); + promptBuilder.append( + "10. Поле analysis в funnel должно содержать детальный анализ (3-4 предложения) с конкретными рекомендациями.\n"); + promptBuilder.append("11. Все строки — на русском языке.\n"); + + String prompt = promptBuilder.toString(); + logger.debug( + "getStrategyAndFunnel: Generating strategy and funnel analysis with detailed prompt (length: {} chars)", + prompt.length()); + + // Retry logic for JsonParseException + int attempt = 0; + while (attempt < MAX_RETRY_ATTEMPTS) { + try { + attempt++; + logger.info( + "getStrategyAndFunnel: Calling OpenAI API for strategy and funnel analysis (attempt {}/{})", + attempt, MAX_RETRY_ATTEMPTS); + String jsonResponse = openAIAnalyticsService.generateWithInstruction(context, prompt, "ru"); + logger.debug("getStrategyAndFunnel: OpenAI response received. Length: {} chars", + jsonResponse != null ? jsonResponse.length() : 0); + + if (jsonResponse == null || jsonResponse.trim().isEmpty()) { + logger.warn( + "getStrategyAndFunnel: Empty JSON response from AI for strategy and funnel analysis"); + return new HashMap<>(); + } + + logger.debug("getStrategyAndFunnel: Cleaning JSON from markdown"); + String cleanedJson = cleanJsonFromMarkdown(jsonResponse); + logger.debug("getStrategyAndFunnel: Parsing JSON response"); + Map jsonMap = objectMapper.readValue(cleanedJson, + new TypeReference>() { + }); + logger.debug("getStrategyAndFunnel: JSON parsed successfully. Keys: {}", jsonMap.keySet()); + + logger.info("getStrategyAndFunnel: Strategy and funnel analysis generated successfully"); + return jsonMap; + } catch (JsonParseException e) { + logger.warn("getStrategyAndFunnel: JsonParseException on attempt {}/{}: {}. Will retry...", + attempt, MAX_RETRY_ATTEMPTS, e.getMessage()); + if (attempt < MAX_RETRY_ATTEMPTS) { + try { + Thread.sleep(RETRY_DELAY_MS); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + logger.error("getStrategyAndFunnel: Retry delay interrupted", ie); + return new HashMap<>(); + } + } else { + logger.error("getStrategyAndFunnel: JsonParseException after {} attempts: {}", + MAX_RETRY_ATTEMPTS, e.getMessage(), e); + logger.error("getStrategyAndFunnel: Stack trace: ", e); + return new HashMap<>(); + } + } catch (Exception e) { + logger.error("getStrategyAndFunnel: Error generating strategy and funnel analysis: {}", + e.getMessage(), e); + logger.error("getStrategyAndFunnel: Stack trace: ", e); + return new HashMap<>(); + } + } + + logger.error("getStrategyAndFunnel: Failed after {} attempts", MAX_RETRY_ATTEMPTS); + return new HashMap<>(); + } catch (Exception e) { + logger.error("getStrategyAndFunnel: Error generating strategy and funnel analysis: {}", e.getMessage(), e); + logger.error("getStrategyAndFunnel: Stack trace: ", e); + return new HashMap<>(); + } + } + /** * Промпт №2: Генерация текстового отчёта на основе JSON */ @@ -1734,51 +1937,113 @@ public class MarketingAnalysisService { chartsData.put("market", market); } - // 5. Обратная совместимость: проверяем старую структуру для channels, funnel, + // 5. Стратегия, воронка, каналы и SWOT (новая структура: strategyAnalysis) + @SuppressWarnings("unchecked") + Map strategyAnalysis = (Map) analysisJson.get("strategyAnalysis"); + if (strategyAnalysis != null) { + // 5.1. Воронка конверсии + @SuppressWarnings("unchecked") + Map funnel = (Map) strategyAnalysis.get("funnel"); + if (funnel != null) { + @SuppressWarnings("unchecked") + List> stages = (List>) funnel.get("stages"); + if (stages != null && !stages.isEmpty()) { + List> funnelList = new ArrayList<>(); + for (Map stage : stages) { + 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 { + stageData.put("conversion", 0); + } + funnelList.add(stageData); + } + chartsData.put("conversionFunnel", funnelList); + } + } + + // 5.2. Потенциал каналов (channelsPotential) + @SuppressWarnings("unchecked") + List> channelsPotential = (List>) strategyAnalysis + .get("channelsPotential"); + if (channelsPotential != null && !channelsPotential.isEmpty()) { + List> channelsList = new ArrayList<>(); + for (Map channel : channelsPotential) { + Map channelData = new HashMap<>(); + channelData.put("name", channel.getOrDefault("name", "Неизвестный канал")); + channelData.put("potential", channel.getOrDefault("potentialIndex", 50)); + channelData.put("priority", channel.getOrDefault("priority", "Medium")); + channelsList.add(channelData); + } + chartsData.put("channelsPotential", channelsList); + } + + // 5.3. SWOT-анализ (globalSwot) + @SuppressWarnings("unchecked") + Map globalSwot = (Map) strategyAnalysis.get("globalSwot"); + if (globalSwot != null) { + chartsData.put("swot", globalSwot); + } + } + + // 6. Обратная совместимость: проверяем старую структуру для channels, funnel, // swot // (на случай, если где-то еще используется старая структура) - @SuppressWarnings("unchecked") - List> channels = (List>) analysisJson.get("channels"); - if (channels != null && !channels.isEmpty()) { - List> channelsList = new ArrayList<>(); - for (Map channel : channels) { - Map channelData = new HashMap<>(); - channelData.put("name", channel.getOrDefault("name", "Неизвестный канал")); - channelData.put("potential", channel.getOrDefault("potentialIndex", 50)); - channelData.put("justification", channel.getOrDefault("justification", "Подходит для данной ниши")); - channelsList.add(channelData); - } - chartsData.put("channelsPotential", channelsList); - } - - @SuppressWarnings("unchecked") - Map funnel = (Map) analysisJson.get("funnel"); - if (funnel != null) { + // Примечание: новые данные из strategyAnalysis имеют приоритет - не + // перезаписываем, если уже есть + if (!chartsData.containsKey("channelsPotential")) { @SuppressWarnings("unchecked") - List> stages = (List>) funnel.get("stages"); - if (stages != null && !stages.isEmpty()) { - List> funnelList = new ArrayList<>(); - for (Map stage : stages) { - 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 { - stageData.put("conversion", 0); - } - funnelList.add(stageData); + List> channels = (List>) analysisJson.get("channels"); + if (channels != null && !channels.isEmpty()) { + List> channelsList = new ArrayList<>(); + for (Map channel : channels) { + Map channelData = new HashMap<>(); + channelData.put("name", channel.getOrDefault("name", "Неизвестный канал")); + channelData.put("potential", channel.getOrDefault("potentialIndex", 50)); + channelData.put("justification", + channel.getOrDefault("justification", "Подходит для данной ниши")); + channelsList.add(channelData); } - chartsData.put("conversionFunnel", funnelList); + chartsData.put("channelsPotential", channelsList); } } - @SuppressWarnings("unchecked") - Map swot = (Map) analysisJson.get("swot"); - if (swot != null) { - chartsData.put("swot", swot); + if (!chartsData.containsKey("conversionFunnel")) { + @SuppressWarnings("unchecked") + Map funnel = (Map) analysisJson.get("funnel"); + if (funnel != null) { + @SuppressWarnings("unchecked") + List> stages = (List>) funnel.get("stages"); + if (stages != null && !stages.isEmpty()) { + List> funnelList = new ArrayList<>(); + for (Map stage : stages) { + 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 { + stageData.put("conversion", 0); + } + funnelList.add(stageData); + } + chartsData.put("conversionFunnel", funnelList); + } + } + } + + if (!chartsData.containsKey("swot")) { + @SuppressWarnings("unchecked") + Map swot = (Map) analysisJson.get("swot"); + if (swot != null) { + chartsData.put("swot", swot); + } } } catch (Exception e) {