From dbfd441c281bd32262469cc639a487da268ab497 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 18 Dec 2025 23:28:41 +0500 Subject: [PATCH] . --- .../service/MarketingAnalysisService.java | 451 ++---------------- 1 file changed, 51 insertions(+), 400 deletions(-) diff --git a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java index 586dc04..f0f4541 100644 --- a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java +++ b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java @@ -1429,17 +1429,28 @@ public class MarketingAnalysisService { logger.debug("generatePolishedTextReport: JSON string length: {}", jsonString.length()); StringBuilder promptBuilder = new StringBuilder(); - promptBuilder.append("Ты — опытный маркетинговый аналитик и бизнес-консультант.\n\n"); + promptBuilder.append( + "Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик с многолетним опытом.\n\n"); promptBuilder.append("На основе следующего JSON с маркетинговым анализом напиши ПРОФЕССИОНАЛЬНЫЙ, "); promptBuilder.append("ГЛУБОКИЙ маркетинговый отчет на русском языке.\n\n"); + promptBuilder.append("СТИЛЬ ПИСЬМА:\n"); + promptBuilder + .append("Пиши как высокооплачиваемый бизнес-консультант. Избегай воды, используй цифры из JSON, "); + promptBuilder.append( + "делай глубокие сравнения нашего бренда с конкурентами (используй данные из competitorAnalysis и comparisonTable). "); + promptBuilder.append("Каждое утверждение должно быть подкреплено конкретными данными из JSON.\n\n"); + promptBuilder.append("ТРЕБОВАНИЯ К ОТЧЕТУ:\n"); promptBuilder.append("1. Используй деловой стиль письма, убедительный для владельца бизнеса\n"); promptBuilder.append("2. Добавляй аналитические инсайты и практические выводы\n"); promptBuilder.append("3. Структурируй текст по разделам с римскими цифрами (I, II, III и т.д.)\n"); - promptBuilder.append("4. Каждый раздел должен быть содержательным (минимум 2-3 абзаца)\n"); + promptBuilder.append( + "4. КРИТИЧНО: Каждый раздел (I-XI) должен содержать минимум 3-4 детальных абзаца с глубокой аналитикой\n"); promptBuilder.append("5. Используй данные ТОЛЬКО из переданного JSON, не выдумывай цифры\n"); - promptBuilder.append("6. Пиши конкретно и практично, избегай общих фраз\n\n"); + promptBuilder.append("6. Пиши конкретно и практично, избегай общих фраз\n"); + promptBuilder.append( + "7. Обязательно используй данные из полей product, businessNiche, goal, regions для контекста анализа\n\n"); promptBuilder.append("ОБЯЗАТЕЛЬНАЯ СТРУКТУРА ОТЧЕТА:\n"); promptBuilder.append("I. Краткое резюме (общий обзор бизнеса и ключевые выводы)\n"); @@ -1460,6 +1471,13 @@ public class MarketingAnalysisService { promptBuilder.append("X. Рекомендации по контенту (типы контента для разных целей)\n"); promptBuilder.append("XI. Итоговая стратегия: что делать в первую очередь (конкретный план действий)\n\n"); + promptBuilder.append("КРИТИЧНО - ПЛЕЙСХОЛДЕРЫ ГРАФИКОВ:\n"); + promptBuilder.append( + "Обязательно расставь ВСЕ плейсхолдеры графиков: [[CHART_SEASONALITY]], [[CHART_AUDIENCE]], "); + promptBuilder.append("[[CHART_COMPETITORS]], [[CHART_SWOT]], [[CHART_CHANNELS]], [[CHART_FUNNEL]]. "); + promptBuilder.append("Каждый плейсхолдер должен быть на отдельной строке в соответствующем разделе, "); + promptBuilder.append("как указано в структуре выше. НЕ пропускай ни одного плейсхолдера!\n\n"); + promptBuilder.append("ВАЖНО:\n"); promptBuilder.append("- Плейсхолдеры [[CHART_...]] вставляй ТОЧНО как указано выше, на отдельной строке\n"); promptBuilder.append("- Начинай сразу с 'I. Краткое резюме', без вступлений\n"); @@ -2432,22 +2450,9 @@ public class MarketingAnalysisService { report.put("analysisTypes", analysisTypeNames); report.put("detailLevel", detailLevelName); report.put("fullAnalysis", "Не удалось сгенерировать анализ. Пожалуйста, попробуйте позже."); - report.put("summary", "Не удалось сгенерировать анализ. Пожалуйста, попробуйте позже."); + report.put("summary", "Маркетинговый анализ для " + request.getProduct()); report.put("chartsData", new HashMap<>()); - // Добавляем дефолтные значения для обратной совместимости - Map defaultTargetAudience = new HashMap<>(); - defaultTargetAudience.put("description", "Не удалось сгенерировать описание целевой аудитории."); - defaultTargetAudience.put("channels", Arrays.asList("Instagram", "LinkedIn", "Telegram")); - report.put("targetAudience", defaultTargetAudience); - report.put("recommendations", Arrays.asList("Не удалось сгенерировать рекомендации.")); - - Map defaultStrategy = new HashMap<>(); - defaultStrategy.put("duration", "1 месяц"); - defaultStrategy.put("channels", Arrays.asList("Instagram", "Telegram", "LinkedIn")); - defaultStrategy.put("contentTypes", Arrays.asList("посты", "сторис", "видео")); - report.put("strategy", defaultStrategy); - logger.warn("generateMarketingReport: Returning report with error message"); } else { logger.info("generateMarketingReport: JSON analysis generated successfully. Keys: {}", @@ -2507,27 +2512,12 @@ public class MarketingAnalysisService { report.put("fullAnalysis", processedTextReport != null ? processedTextReport : "Анализ не удалось сгенерировать."); - // Извлекаем только первую секцию (I. Краткое резюме) для поля summary - // чтобы избежать дублирования полного текста отчета - String shortSummary = extractSummarySection(processedTextReport); - report.put("summary", shortSummary != null ? shortSummary : "Анализ не удалось сгенерировать."); - - // Извлекаем данные из analysisJson вместо повторных вызовов AI - logger.info("generateMarketingReport: Extracting targetAudience from analysisJson"); - Map targetAudience = extractTargetAudienceFromJson(analysisJson); - report.put("targetAudience", targetAudience); - logger.info("generateMarketingReport: TargetAudience extracted. Channels count: {}", - ((List) targetAudience.get("channels")).size()); - - logger.info("generateMarketingReport: Extracting recommendations from analysisJson"); - List recommendations = extractRecommendationsFromJson(analysisJson); - report.put("recommendations", recommendations); - logger.info("generateMarketingReport: Recommendations extracted. Count: {}", recommendations.size()); - - logger.info("generateMarketingReport: Extracting strategy from analysisJson"); - Map strategy = extractStrategyFromJson(analysisJson); - report.put("strategy", strategy); - logger.info("generateMarketingReport: Strategy extracted. Channels: {}", strategy.get("channels")); + // Summary содержит только короткую вводную фразу + String summary = "Маркетинговый анализ для " + request.getProduct(); + if (request.getBusinessNiche() != null && !request.getBusinessNiche().trim().isEmpty()) { + summary += " (" + request.getBusinessNiche() + ")"; + } + report.put("summary", summary); } logger.info("generateMarketingReport: Report generation completed. Report keys: {}", report.keySet()); @@ -2682,322 +2672,6 @@ public class MarketingAnalysisService { return strategy; } - /** - * Извлекает описание целевой аудитории и каналы из JSON-анализа. - * Заменяет повторный вызов AI для генерации описания аудитории. - * - * @param analysisJson JSON с данными маркетингового анализа - * @return Map с description и channels - */ - @SuppressWarnings("unchecked") - private Map extractTargetAudienceFromJson(Map analysisJson) { - Map targetAudience = new HashMap<>(); - StringBuilder descriptionBuilder = new StringBuilder(); - List channels = new ArrayList<>(); - - try { - // Извлекаем данные из audienceAnalysis - Map audienceAnalysis = (Map) analysisJson.get("audienceAnalysis"); - if (audienceAnalysis != null) { - // Получаем ключевые выводы - List keyTakeaways = (List) audienceAnalysis.get("keyTakeaways"); - if (keyTakeaways != null && !keyTakeaways.isEmpty()) { - descriptionBuilder.append("Ключевые характеристики целевой аудитории:\n"); - for (String takeaway : keyTakeaways) { - descriptionBuilder.append("• ").append(takeaway).append("\n"); - } - descriptionBuilder.append("\n"); - } - - // Получаем информацию о сегментах - List> segments = (List>) audienceAnalysis.get("segments"); - if (segments != null && !segments.isEmpty()) { - descriptionBuilder.append("Основные сегменты аудитории:\n"); - for (Map segment : segments) { - Object name = segment.get("name"); - Object sharePercent = segment.get("sharePercent"); - Object ageRange = segment.get("ageRange"); - Object motivation = segment.get("motivation"); - - if (name != null) { - descriptionBuilder.append("\n• ").append(name); - if (sharePercent != null) { - descriptionBuilder.append(" (").append(sharePercent).append("%)"); - } - descriptionBuilder.append("\n"); - } - if (ageRange != null) { - descriptionBuilder.append(" Возраст: ").append(ageRange).append("\n"); - } - if (motivation != null) { - descriptionBuilder.append(" Мотивация: ").append(motivation).append("\n"); - } - - // Собираем каналы из сегментов - List primaryChannels = (List) segment.get("primaryChannels"); - if (primaryChannels != null) { - for (String channel : primaryChannels) { - if (!channels.contains(channel)) { - channels.add(channel); - } - } - } - } - } - - // Комментарии по гендеру и возрасту - Object genderComment = audienceAnalysis.get("genderComment"); - Object ageComment = audienceAnalysis.get("ageComment"); - if (genderComment != null || ageComment != null) { - descriptionBuilder.append("\n"); - if (ageComment != null) { - descriptionBuilder.append(ageComment).append("\n"); - } - if (genderComment != null) { - descriptionBuilder.append(genderComment).append("\n"); - } - } - } - - // Извлекаем каналы из channels.recommended - Map channelsData = (Map) analysisJson.get("channels"); - if (channelsData != null) { - List> recommended = (List>) channelsData.get("recommended"); - if (recommended != null) { - for (Map channel : recommended) { - Object channelName = channel.get("name"); - if (channelName != null && !channels.contains(channelName.toString())) { - channels.add(channelName.toString()); - } - } - } - } - - } catch (Exception e) { - logger.warn("extractTargetAudienceFromJson: Error extracting data: {}", e.getMessage()); - } - - // Если описание пустое, добавляем дефолтное - if (descriptionBuilder.length() == 0) { - descriptionBuilder.append("Описание целевой аудитории доступно в полном отчете."); - } - - // Если каналы пустые, добавляем дефолтные - if (channels.isEmpty()) { - channels = Arrays.asList("Instagram", "LinkedIn", "Telegram"); - } - - targetAudience.put("description", descriptionBuilder.toString()); - targetAudience.put("channels", channels); - - return targetAudience; - } - - /** - * Извлекает рекомендации из JSON-анализа. - * Заменяет повторный вызов AI для генерации рекомендаций. - * - * @param analysisJson JSON с данными маркетингового анализа - * @return Список рекомендаций - */ - @SuppressWarnings("unchecked") - private List extractRecommendationsFromJson(Map analysisJson) { - List recommendations = new ArrayList<>(); - - try { - // Извлекаем из contentRecommendations - Map contentRecommendations = (Map) analysisJson - .get("contentRecommendations"); - if (contentRecommendations != null) { - // Собираем рекомендации из всех типов контента - List salesContent = (List) contentRecommendations.get("sales"); - List expertContent = (List) contentRecommendations.get("expertise"); - List trustContent = (List) contentRecommendations.get("trust"); - - if (salesContent != null && !salesContent.isEmpty()) { - recommendations.add("Продающий контент: " + String.join(", ", salesContent)); - } - if (expertContent != null && !expertContent.isEmpty()) { - recommendations.add("Экспертный контент: " + String.join(", ", expertContent)); - } - if (trustContent != null && !trustContent.isEmpty()) { - recommendations.add("Контент для доверия: " + String.join(", ", trustContent)); - } - } - - // Извлекаем из valueProposition - Map valueProposition = (Map) analysisJson.get("valueProposition"); - if (valueProposition != null) { - List mainBenefits = (List) valueProposition.get("mainBenefits"); - if (mainBenefits != null && !mainBenefits.isEmpty()) { - recommendations.add("Основные преимущества для продвижения: " + String.join(", ", mainBenefits)); - } - } - - // Извлекаем из SWOT - opportunities - Map swot = (Map) analysisJson.get("swot"); - if (swot != null) { - List opportunities = (List) swot.get("opportunities"); - if (opportunities != null && !opportunities.isEmpty()) { - for (int i = 0; i < Math.min(3, opportunities.size()); i++) { - recommendations.add("Использовать возможность: " + opportunities.get(i)); - } - } - } - - // Извлекаем из positioning - Map positioning = (Map) analysisJson.get("positioning"); - if (positioning != null) { - Object keyMessage = positioning.get("keyMessage"); - if (keyMessage != null) { - recommendations.add("Ключевое сообщение: " + keyMessage); - } - } - - } catch (Exception e) { - logger.warn("extractRecommendationsFromJson: Error extracting data: {}", e.getMessage()); - } - - // Если рекомендации пустые, добавляем дефолтные - if (recommendations.isEmpty()) { - recommendations = Arrays.asList( - "Рекомендации доступны в полном маркетинговом отчете", - "Изучите раздел с SWOT-анализом для понимания возможностей", - "Обратите внимание на рекомендуемые каналы продвижения"); - } - - return recommendations; - } - - /** - * Извлекает стратегию из JSON-анализа. - * Заменяет повторный вызов AI для генерации стратегии. - * - * @param analysisJson JSON с данными маркетингового анализа - * @return Map со стратегией (duration, channels, contentTypes) - */ - @SuppressWarnings("unchecked") - private Map extractStrategyFromJson(Map analysisJson) { - Map strategy = new HashMap<>(); - List channels = new ArrayList<>(); - List contentTypes = new ArrayList<>(); - String duration = "1 месяц"; // default - - try { - // Извлекаем каналы из channels.recommended - Map channelsData = (Map) analysisJson.get("channels"); - if (channelsData != null) { - List> recommended = (List>) channelsData.get("recommended"); - if (recommended != null) { - for (Map channel : recommended) { - Object channelName = channel.get("name"); - if (channelName != null) { - channels.add(channelName.toString()); - } - } - } - } - - // Извлекаем типы контента из contentRecommendations - Map contentRecommendations = (Map) analysisJson - .get("contentRecommendations"); - if (contentRecommendations != null) { - if (contentRecommendations.containsKey("sales")) { - contentTypes.add("продающий контент"); - } - if (contentRecommendations.containsKey("expertise")) { - contentTypes.add("экспертный контент"); - } - if (contentRecommendations.containsKey("trust")) { - contentTypes.add("доверительный контент"); - } - if (contentRecommendations.containsKey("entertainment")) { - contentTypes.add("развлекательный контент"); - } - } - - // Извлекаем информацию о воронке для определения длительности - Map funnel = (Map) analysisJson.get("funnel"); - if (funnel != null) { - List> stages = (List>) funnel.get("stages"); - if (stages != null && stages.size() > 3) { - // Если воронка сложная (более 3 этапов), рекомендуем более длительную кампанию - duration = "2-3 месяца"; - } - } - - } catch (Exception e) { - logger.warn("extractStrategyFromJson: Error extracting data: {}", e.getMessage()); - } - - // Дефолтные значения если данные не найдены - if (channels.isEmpty()) { - channels = Arrays.asList("Instagram", "Telegram", "LinkedIn"); - } - if (contentTypes.isEmpty()) { - contentTypes = Arrays.asList("посты", "сторис", "видео"); - } - - strategy.put("duration", duration); - strategy.put("channels", channels); - strategy.put("contentTypes", contentTypes); - - return strategy; - } - - /** - * Извлекает только первую секцию "I. Краткое резюме" из полного текстового - * отчета. - * Используется для поля summary, чтобы избежать дублирования полного текста. - * - * @param fullReport Полный текстовый отчет - * @return Текст только первой секции (краткое резюме) - */ - private String extractSummarySection(String fullReport) { - if (fullReport == null || fullReport.trim().isEmpty()) { - return null; - } - - try { - // Ищем начало первой секции "I. Краткое резюме" - int startIndex = fullReport.indexOf("I. Краткое резюме"); - if (startIndex == -1) { - // Если не нашли, пробуем найти просто "Краткое резюме" - startIndex = fullReport.indexOf("Краткое резюме"); - } - - if (startIndex == -1) { - // Если нет секции резюме, возвращаем первые 500 символов - return fullReport.length() > 500 - ? fullReport.substring(0, 500) + "..." - : fullReport; - } - - // Ищем начало второй секции "II." - int endIndex = fullReport.indexOf("II.", startIndex + 1); - if (endIndex == -1) { - // Пробуем найти "II. " с пробелом - endIndex = fullReport.indexOf("\nII. ", startIndex + 1); - } - - if (endIndex == -1) { - // Если не нашли вторую секцию, возвращаем от начала до 1000 символов - int maxLength = Math.min(startIndex + 1000, fullReport.length()); - return fullReport.substring(startIndex, maxLength).trim(); - } - - // Извлекаем текст между I. и II. - return fullReport.substring(startIndex, endIndex).trim(); - - } catch (Exception e) { - logger.warn("extractSummarySection: Error extracting summary: {}", e.getMessage()); - // В случае ошибки возвращаем первые 500 символов - return fullReport.length() > 500 - ? fullReport.substring(0, 500) + "..." - : fullReport; - } - } - private String buildMarkdownFromReport(Map reportData, MarketingAnalysisRequest request) { logger.info("buildMarkdownFromReport: Starting markdown generation for product: {}", request.getProduct()); logger.debug("buildMarkdownFromReport: ReportData keys: {}", reportData != null ? reportData.keySet() : "null"); @@ -3008,34 +2682,6 @@ public class MarketingAnalysisService { markdown.append("- **Ниша бизнеса:** ").append(request.getBusinessNiche()).append("\n"); markdown.append("- **Продукт/услуга:** ").append(request.getProduct()).append("\n"); - // Format target audience - Map targetAudienceData = request.getTargetAudience(); - if (targetAudienceData != null) { - StringBuilder audienceBuilder = new StringBuilder(); - if (targetAudienceData.containsKey("genders")) { - @SuppressWarnings("unchecked") - List genders = (List) targetAudienceData.get("genders"); - if (genders != null && !genders.isEmpty()) { - audienceBuilder.append("Гендер: ").append(String.join(", ", genders)).append(". "); - } - } - if (targetAudienceData.containsKey("ageRanges")) { - @SuppressWarnings("unchecked") - List ageRanges = (List) targetAudienceData.get("ageRanges"); - if (ageRanges != null && !ageRanges.isEmpty()) { - audienceBuilder.append("Возраст: ").append(String.join(", ", ageRanges)).append(". "); - } - } - if (targetAudienceData.containsKey("types")) { - @SuppressWarnings("unchecked") - List types = (List) targetAudienceData.get("types"); - if (types != null && !types.isEmpty()) { - audienceBuilder.append("Тип: ").append(String.join(", ", types)).append(". "); - } - } - markdown.append("- **Целевая аудитория:** ").append(audienceBuilder.toString().trim()).append("\n"); - } - // Format regions List regions = request.getRegion(); if (regions != null && !regions.isEmpty()) { @@ -3063,11 +2709,6 @@ public class MarketingAnalysisService { } markdown.append("\n"); - // ВАЖНО: Секции "Позиционирование", "УТП", "Рекомендации по контенту" УДАЛЕНЫ, - // так как они уже включены в fullAnalysis (генерируется методом - // generatePolishedTextReport) - // и вызывали дублирование контента в отчете - // Display full analysis - это основной блок с полным отчетом // Все секции (I-XI) уже включены в fullAnalysis благодаря // generatePolishedTextReport @@ -3082,11 +2723,8 @@ public class MarketingAnalysisService { // так как fullAnalysis уже начинается с "I. Краткое резюме" markdown.append(processedAnalysis).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(summary).append("\n\n"); - } + // Если fullAnalysis пустой, выводим сообщение об ошибке + markdown.append("Не удалось сгенерировать анализ. Пожалуйста, попробуйте позже.\n\n"); } return markdown.toString(); @@ -3145,14 +2783,17 @@ public class MarketingAnalysisService { if (chartsData == null || chartsData.isEmpty()) { logger.warn("replaceChartPlaceholders: chartsData is null or empty, removing all placeholders"); - // Если данных нет, просто удаляем все плейсхолдеры - return fullAnalysis + // Если данных нет, удаляем все плейсхолдеры и очищаем пустые строки + String cleaned = fullAnalysis .replace("[[CHART_SEASONALITY]]", "") .replace("[[CHART_AUDIENCE]]", "") .replace("[[CHART_COMPETITORS]]", "") .replace("[[CHART_CHANNELS]]", "") .replace("[[CHART_FUNNEL]]", "") .replace("[[CHART_SWOT]]", ""); + // Удаляем множественные пустые строки (более 2 подряд) + cleaned = cleaned.replaceAll("(\\r?\\n){3,}", "\n\n"); + return cleaned; } String result = fullAnalysis; @@ -3190,19 +2831,26 @@ public class MarketingAnalysisService { } catch (JsonProcessingException e) { logger.error("Error serializing data for placeholder {} (key: {}): {}", placeholder, mapping.dataKey, e.getMessage(), e); - // Удаляем плейсхолдер при ошибке сериализации - result = result.replaceAll(regex, ""); + // Удаляем плейсхолдер при ошибке сериализации вместе с окружающими переносами + // строк + String placeholderWithNewlines = "(\\r?\\n)?\\s*" + regex + "\\s*(\\r?\\n)?"; + result = result.replaceAll(placeholderWithNewlines, "\n"); } catch (Exception e) { logger.error("Unexpected error processing placeholder {} (key: {}): {}", placeholder, mapping.dataKey, e.getMessage(), e); - // Удаляем плейсхолдер при неожиданной ошибке - result = result.replaceAll(regex, ""); + // Удаляем плейсхолдер при неожиданной ошибке вместе с окружающими переносами + // строк + String placeholderWithNewlines = "(\\r?\\n)?\\s*" + regex + "\\s*(\\r?\\n)?"; + result = result.replaceAll(placeholderWithNewlines, "\n"); } } else { logger.warn("MISSING DATA for placeholder: {} (key: {}). Removing placeholder.", placeholder, mapping.dataKey); - // Если данных нет, удаляем плейсхолдер, чтобы не светить им пользователю - result = result.replaceAll(regex, ""); + // Если данных нет, удаляем плейсхолдер и окружающие пустые строки + // Удаляем плейсхолдер вместе с предшествующим и последующим переносом строки, + // если они есть + String placeholderWithNewlines = "(\\r?\\n)?\\s*" + regex + "\\s*(\\r?\\n)?"; + result = result.replaceAll(placeholderWithNewlines, "\n"); } } } @@ -3212,6 +2860,9 @@ public class MarketingAnalysisService { logger.debug("replaceChartPlaceholders: Adding missing charts to sections"); result = addMissingChartsToSections(result, chartsData, placeholderMap, processedPlaceholders); + // Финальная очистка: удаляем множественные пустые строки (более 2 подряд) + result = result.replaceAll("(\\r?\\n){3,}", "\n\n"); + logger.info( "replaceChartPlaceholders: Placeholder replacement completed. Processed placeholders: {}, Result length: {} chars", processedPlaceholders.size(), result.length());