From 3e56f565dc9a8aaa10c127cf630a8c3359264ffb Mon Sep 17 00:00:00 2001 From: root Date: Sat, 20 Dec 2025 00:09:40 +0500 Subject: [PATCH] . --- .../service/MarketingAnalysisService.java | 317 ++++++++++++------ 1 file changed, 213 insertions(+), 104 deletions(-) diff --git a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java index e7ac416..a436694 100644 --- a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java +++ b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java @@ -1019,11 +1019,11 @@ public class MarketingAnalysisService { promptBuilder.append(" XI. Итоговая стратегия: что делать в первую очередь\n\n"); promptBuilder.append("2. Используй данные ТОЛЬКО из JSON:\n\n"); promptBuilder.append(" - seasonality → для описания сезонности\n"); - promptBuilder.append(" - audienceSegments → для портрета ЦА\n"); - promptBuilder.append(" - competitors → для анализа конкурентов\n"); - promptBuilder.append(" - channels → для описания каналов\n"); - promptBuilder.append(" - funnel → для объяснения этапов воронки\n"); - promptBuilder.append(" - swot → для SWOT-раздела\n"); + promptBuilder.append(" - market → для объёма рынка, темпов роста, трендов\n"); + promptBuilder.append(" - audienceAnalysis → для портрета ЦА (segments, ageGroups, genderDistribution)\n"); + promptBuilder + .append(" - competitorAnalysis → для анализа конкурентов (marketShareChart, comparisonTable)\n"); + promptBuilder.append(" - strategyAnalysis → для воронки, каналов и SWOT (если присутствует)\n"); promptBuilder.append(" - valueProposition, positioning, contentRecommendations → для рекомендаций.\n\n"); promptBuilder.append("3. Пиши простым, деловым, понятным языком:\n\n"); promptBuilder.append(" - без академического жаргона,\n"); @@ -1031,8 +1031,11 @@ public class MarketingAnalysisService { promptBuilder.append(" - с акцентом на практическую пользу.\n\n"); promptBuilder.append("4. Вставляй специальные метки плейсхолдеров там, где должны быть графики:\n\n"); promptBuilder.append(" - [[CHART_SEASONALITY]] — внутри раздела \"III. Анализ рынка и сезонности\"\n"); - promptBuilder.append(" - [[CHART_AUDIENCE]] — внутри раздела \"IV. Анализ целевой аудитории\"\n"); - promptBuilder.append(" - [[CHART_COMPETITORS]] — внутри раздела \"V. Анализ конкурентов\"\n"); + promptBuilder.append(" - [[CHART_AGE_DISTRIBUTION]] — внутри раздела \"IV. Анализ целевой аудитории\"\n"); + promptBuilder + .append(" - [[CHART_GENDER_DISTRIBUTION]] — внутри раздела \"IV. Анализ целевой аудитории\"\n"); + promptBuilder.append(" - [[CHART_MARKET_SHARE]] — внутри раздела \"V. Анализ конкурентов\"\n"); + promptBuilder.append(" - [[TABLE_COMPARISON]] — внутри раздела \"V. Анализ конкурентов\"\n"); promptBuilder .append(" - [[CHART_CHANNELS]] — внутри раздела \"VII. Каналы продвижения и их потенциал\"\n"); promptBuilder.append(" - [[CHART_FUNNEL]] — внутри раздела \"VIII. Воронка спроса\"\n"); @@ -1179,7 +1182,8 @@ public class MarketingAnalysisService { fullReport.append("\n"); } } - fullReport.append("[[CHART_AUDIENCE]]\n\n"); + fullReport.append("[[CHART_AGE_DISTRIBUTION]]\n\n"); + fullReport.append("[[CHART_GENDER_DISTRIBUTION]]\n\n"); } // V. Анализ конкурентов @@ -1233,7 +1237,8 @@ public class MarketingAnalysisService { fullReport.append("\n"); } } - fullReport.append("[[CHART_COMPETITORS]]\n\n"); + fullReport.append("[[CHART_MARKET_SHARE]]\n\n"); + fullReport.append("[[TABLE_COMPARISON]]\n\n"); } // VI. SWOT-анализ @@ -1563,22 +1568,34 @@ public class MarketingAnalysisService { } break; case "audience": - if (fullJson.containsKey("audienceSegments")) { + // Новая структура: audienceAnalysis (ageGroups, genderDistribution, segments, + // keyTakeaways, etc.) + if (fullJson.containsKey("audienceAnalysis")) { + context.put("audienceAnalysis", fullJson.get("audienceAnalysis")); + } else if (fullJson.containsKey("audienceSegments")) { + // Backward compatibility (старая структура) context.put("audienceSegments", fullJson.get("audienceSegments")); } break; case "competitors": - if (fullJson.containsKey("competitors")) { - context.put("competitors", fullJson.get("competitors")); - } - if (fullJson.containsKey("comparisonTable")) { - context.put("comparisonTable", fullJson.get("comparisonTable")); - } - if (fullJson.containsKey("swotCompetitors")) { - context.put("swotCompetitors", fullJson.get("swotCompetitors")); - } - if (fullJson.containsKey("marketShareChart")) { - context.put("marketShareChart", fullJson.get("marketShareChart")); + // Новая структура: competitorAnalysis (marketShareChart, comparisonTable, + // competitorsDetails, swotCompetitors, etc.) + if (fullJson.containsKey("competitorAnalysis")) { + context.put("competitorAnalysis", fullJson.get("competitorAnalysis")); + } else { + // Backward compatibility (старая структура) + if (fullJson.containsKey("competitors")) { + context.put("competitors", fullJson.get("competitors")); + } + if (fullJson.containsKey("comparisonTable")) { + context.put("comparisonTable", fullJson.get("comparisonTable")); + } + if (fullJson.containsKey("swotCompetitors")) { + context.put("swotCompetitors", fullJson.get("swotCompetitors")); + } + if (fullJson.containsKey("marketShareChart")) { + context.put("marketShareChart", fullJson.get("marketShareChart")); + } } break; case "swot": @@ -1617,15 +1634,23 @@ public class MarketingAnalysisService { break; case "strategy": // Для итоговой стратегии включаем все ключевые данные - if (fullJson.containsKey("swot")) { - context.put("swot", fullJson.get("swot")); - } - if (fullJson.containsKey("channels")) { - context.put("channels", fullJson.get("channels")); - } - if (fullJson.containsKey("funnel")) { - context.put("funnel", fullJson.get("funnel")); + // Новая структура: strategyAnalysis (funnel, channelsPotential, globalSwot, + // etc.) + if (fullJson.containsKey("strategyAnalysis")) { + context.put("strategyAnalysis", fullJson.get("strategyAnalysis")); + } else { + // Backward compatibility (старая структура) + if (fullJson.containsKey("swot")) { + context.put("swot", fullJson.get("swot")); + } + if (fullJson.containsKey("channels")) { + context.put("channels", fullJson.get("channels")); + } + if (fullJson.containsKey("funnel")) { + context.put("funnel", fullJson.get("funnel")); + } } + // Дополнительные "склейки" стратегии (небольшие поля) if (fullJson.containsKey("positioning")) { context.put("positioning", fullJson.get("positioning")); } @@ -1655,7 +1680,11 @@ public class MarketingAnalysisService { promptBuilder.append("1. Объем: минимум 500-700 слов\n"); promptBuilder.append("2. Стиль: деловой, убедительный для владельца бизнеса\n"); promptBuilder.append("3. Глубина: аналитические инсайты с практическими выводами\n"); - promptBuilder.append("4. Данные: используй ТОЛЬКО данные из JSON, не выдумывай цифры\n\n"); + promptBuilder.append("4. Данные: используй ТОЛЬКО данные из JSON, не выдумывай цифры\n"); + promptBuilder.append( + "5. ОБЯЗАТЕЛЬНЫЕ ЦИФРЫ: в тексте обязательно упомяни, что объем рынка = 15 млрд тенге, а темпы роста = 12%.\n"); + promptBuilder.append( + " Эти цифры должны быть явно написаны в тексте (как минимум 1 раз) и использованы в выводах.\n\n"); promptBuilder.append("СТРУКТУРА РАЗДЕЛА:\n"); promptBuilder.append("I. Краткое резюме\n\n"); promptBuilder.append("Опиши:\n"); @@ -1769,7 +1798,9 @@ public class MarketingAnalysisService { promptBuilder.append("- Начинай сразу с заголовка \"III. Анализ рынка и сезонности\"\n"); promptBuilder.append("- Не добавляй вступления или заключения\n"); promptBuilder.append("- Используй данные из полей seasonality, market, regions\n"); - promptBuilder.append("- Пиши максимально подробно, анализируй тренды\n\n"); + promptBuilder.append("- Пиши максимально подробно, анализируй тренды\n"); + promptBuilder.append( + "- Каждое ключевое утверждение подтверждай конкретной цифрой из JSON (или явно помечай \"нет данных в JSON\")\n\n"); promptBuilder.append("JSON ДАННЫЕ:\n"); promptBuilder.append(jsonString); @@ -1803,10 +1834,13 @@ public class MarketingAnalysisService { promptBuilder.append("1. Объем: минимум 500-700 слов\n"); promptBuilder.append("2. Стиль: деловой, убедительный для владельца бизнеса\n"); promptBuilder.append("3. Глубина: аналитические инсайты с практическими выводами\n"); - promptBuilder.append("4. Данные: используй ТОЛЬКО данные из JSON, не выдумывай цифры\n\n"); + promptBuilder.append("4. Данные: используй ТОЛЬКО данные из JSON, не выдумывай цифры\n"); + promptBuilder.append( + "5. ЦИФРЫ ОБЯЗАТЕЛЬНЫ: в каждом подпункте делай выводы на основе процентов/чисел из audienceAnalysis.ageGroups и audienceAnalysis.genderDistribution.\n\n"); promptBuilder.append("СТРУКТУРА РАЗДЕЛА:\n"); promptBuilder.append("IV. Анализ целевой аудитории\n\n"); - promptBuilder.append("Детально проанализируй КАЖДЫЙ сегмент из audienceSegments:\n"); + promptBuilder.append( + "Детально проанализируй аудиторию на основе audienceAnalysis (segments, ageGroups, genderDistribution):\n"); promptBuilder.append("- Демографические характеристики (возраст, пол, доход)\n"); promptBuilder.append("- Психографический портрет (потребности, мотивация, ценности)\n"); promptBuilder.append( @@ -1814,18 +1848,30 @@ public class MarketingAnalysisService { promptBuilder.append("- Ключевые потребности и боли сегмента\n"); promptBuilder.append("- Как продукт решает проблемы каждого сегмента\n"); promptBuilder.append("- Рекомендации по коммуникации с каждым сегментом\n\n"); - promptBuilder.append("КРИТИЧНО - ВСТАВКА ГРАФИКА:\n"); + promptBuilder.append("КРИТИЧНО - ВСТАВКА ГРАФИКОВ (2 шт):\n"); promptBuilder.append( - "1. В логичном месте раздела (после описания сегментов) вставь плейсхолдер [[CHART_AUDIENCE]] на отдельной строке\n"); + "1. После разбора возрастных групп вставь плейсхолдер [[CHART_AGE_DISTRIBUTION]] на отдельной строке\n"); promptBuilder.append( - "2. ПЕРЕД графиком напиши вводный абзац (2-3 предложения), начинающийся с: \"Анализ данных на графике ниже показывает...\"\n"); + " - ПЕРЕД графиком: вводный абзац (2-3 предложения), начинающийся с: \"Анализ данных на графике ниже показывает...\"\n"); promptBuilder.append( - "3. ПОСЛЕ графика напиши интерпретацию данных (2-3 предложения), начинающуюся с: \"Из этих показателей следует, что...\"\n\n"); + " - ПОСЛЕ графика: интерпретация (2-3 предложения), начинающаяся с: \"Из этих показателей следует, что...\"\n"); + promptBuilder.append( + "2. После разбора гендерного распределения вставь плейсхолдер [[CHART_GENDER_DISTRIBUTION]] на отдельной строке\n"); + promptBuilder.append( + " - ПЕРЕД графиком: вводный абзац (2-3 предложения), начинающийся с: \"Анализ данных на графике ниже показывает...\"\n"); + promptBuilder.append( + " - ПОСЛЕ графика: интерпретация (2-3 предложения), начинающаяся с: \"Из этих показателей следует, что...\"\n\n"); + promptBuilder.append( + "ОБЯЗАТЕЛЬНО ПРО ГЕНДЕР:\n- Явно укажи долю женщин по данным genderDistribution (в типовом кейсе около 60%) и объясни, почему аудитория смещена в сторону женщин.\n"); + promptBuilder.append( + "- Дай практические рекомендации, как использовать это: месседжи, офферы, визуалы, каналы, время коммуникации.\n\n"); promptBuilder.append("ВАЖНО:\n"); promptBuilder.append("- Начинай сразу с заголовка \"IV. Анализ целевой аудитории\"\n"); promptBuilder.append("- Не добавляй вступления или заключения\n"); - promptBuilder.append("- Используй данные из поля audienceSegments\n"); - promptBuilder.append("- Для каждого сегмента создай детальный портрет клиента\n\n"); + promptBuilder + .append("- Используй данные из поля audienceAnalysis (и только из него), не выдумывай проценты\n"); + promptBuilder.append( + "- Для каждого сегмента создай детальный портрет клиента и привяжи его к возрастным и гендерным долям\n\n"); promptBuilder.append("JSON ДАННЫЕ:\n"); promptBuilder.append(jsonString); @@ -1859,30 +1905,48 @@ public class MarketingAnalysisService { promptBuilder.append("1. Объем: минимум 500-700 слов\n"); promptBuilder.append("2. Стиль: деловой, убедительный для владельца бизнеса\n"); promptBuilder.append("3. Глубина: аналитические инсайты с практическими выводами\n"); - promptBuilder.append("4. Данные: используй ТОЛЬКО данные из JSON, не выдумывай цифры\n\n"); + promptBuilder.append("4. Данные: используй ТОЛЬКО данные из JSON, не выдумывай цифры\n"); + promptBuilder.append( + "5. Это должен быть самый мощный раздел отчёта: каждое утверждение подтверждай цифрами из competitorAnalysis.marketShareChart / competitorAnalysis.comparisonTable.\n\n"); promptBuilder.append("СТРУКТУРА РАЗДЕЛА:\n"); promptBuilder.append("V. Анализ конкурентов\n\n"); promptBuilder.append("Детально проанализируй:\n"); - promptBuilder.append("- Позиции каждого конкурента на рынке (используй данные из competitors)\n"); - promptBuilder.append("- Сильные и слабые стороны конкурентов (из comparisonTable и swotCompetitors)\n"); + promptBuilder.append("- Позиции каждого конкурента на рынке (используй данные из competitorAnalysis)\n"); + promptBuilder.append("- Анализ долей рынка (используй competitorAnalysis.marketShareChart)\n"); + promptBuilder.append( + "- Сильные и слабые стороны конкурентов (из competitorAnalysis.comparisonTable и swotCompetitors)\n"); promptBuilder.append("- Стратегии конкурентов (ценовая политика, каналы продвижения)\n"); promptBuilder.append( "- Контент-воронки конкурентов: какие типы контента они используют на каждом этапе воронки, какие каналы задействуют, какие месседжи доносят\n"); - promptBuilder.append("- Глубокое сравнение нашего бренда с конкурентами (используй comparisonTable)\n"); + promptBuilder.append( + "- Глубокое сравнение нашего бренда с конкурентами (используй competitorAnalysis.comparisonTable)\n"); promptBuilder.append("- Рекомендации по дифференциации и конкурентным преимуществам\n\n"); - promptBuilder.append("КРИТИЧНО - ВСТАВКА ГРАФИКА:\n"); + promptBuilder.append("КРИТИЧНО - ВСТАВКА ГРАФИКА И ТАБЛИЦЫ:\n"); promptBuilder.append( - "1. В логичном месте раздела (после анализа конкурентов) вставь плейсхолдер [[CHART_COMPETITORS]] на отдельной строке\n"); + "1. После анализа долей рынка вставь плейсхолдер [[CHART_MARKET_SHARE]] на отдельной строке\n"); promptBuilder.append( - "2. ПЕРЕД графиком напиши вводный абзац (2-3 предложения), начинающийся с: \"Анализ данных на графике ниже показывает...\"\n"); + " - ПЕРЕД графиком: вводный абзац (2-3 предложения), начинающийся с: \"Анализ данных на графике ниже показывает...\"\n"); promptBuilder.append( - "3. ПОСЛЕ графика напиши интерпретацию данных (2-3 предложения), начинающуюся с: \"Из этих показателей следует, что...\"\n\n"); + " - ПОСЛЕ графика: интерпретация (2-3 предложения), начинающаяся с: \"Из этих показателей следует, что...\"\n"); + promptBuilder.append( + "2. Затем вставь плейсхолдер [[TABLE_COMPARISON]] на отдельной строке (это сравнительная таблица)\n"); + promptBuilder.append( + " - ПЕРЕД таблицей: короткий ввод (1-2 предложения), что именно сравниваем\n"); + promptBuilder.append( + " - ПОСЛЕ таблицы: интерпретация (3-5 предложений) с выводом по позиционированию\n\n"); + promptBuilder.append( + "ОБЯЗАТЕЛЬНОЕ СРАВНЕНИЕ С 'КОНКУРЕНТОМ A' И 'КОНКУРЕНТОМ B':\n"); + promptBuilder.append( + "- Возьми двух главных конкурентов из competitorAnalysis (по доле рынка или по описанию) и назови их в тексте как \"Конкурент A\" и \"Конкурент B\".\n"); + promptBuilder.append( + "- Пройди ПО КАЖДОЙ строке competitorAnalysis.comparisonTable: Мы vs Конкурент A vs Конкурент B (с цифрами/формулировками из JSON) + практический вывод.\n\n"); promptBuilder.append("ВАЖНО:\n"); promptBuilder.append("- Начинай сразу с заголовка \"V. Анализ конкурентов\"\n"); promptBuilder.append("- Не добавляй вступления или заключения\n"); promptBuilder.append( - "- Используй данные из полей competitors, comparisonTable, swotCompetitors, marketShareChart\n"); - promptBuilder.append("- Делай глубокие сравнения нашего бренда с конкурентами\n\n"); + "- Используй данные из competitorAnalysis.marketShareChart и competitorAnalysis.comparisonTable\n"); + promptBuilder.append( + "- Не выдумывай цифры: если по строке сравнения нет данных — явно пометь это\n\n"); promptBuilder.append("JSON ДАННЫЕ:\n"); promptBuilder.append(jsonString); @@ -2195,12 +2259,21 @@ public class MarketingAnalysisService { promptBuilder.append("- Ожидаемые результаты и метрики успеха\n"); promptBuilder.append("- Бюджетные рекомендации и распределение ресурсов\n"); promptBuilder.append("- Критические точки контроля и корректировки стратегии\n\n"); + promptBuilder.append("КРИТИЧНО - ПРОГНОЗ И РАСЧЕТЫ НА ОСНОВЕ ВОРОНКИ:\n"); + promptBuilder.append( + "1. Возьми воронку из strategyAnalysis.funnel (или из funnel, если strategyAnalysis отсутствует) и выполни расчеты.\n"); + promptBuilder.append( + "2. Дай минимум 3 прогнозных показателя с формулами и подстановкой чисел из JSON (например: конверсия между этапами, ожидаемое число покупок, требуемый объем верхнего этапа для достижения цели).\n"); + promptBuilder.append( + "3. Сделай 3 сценария: консервативный / базовый / оптимистичный (допуски только как проценты к значениям из JSON; не придумывай абсолютные значения).\n"); + promptBuilder.append( + "4. Если данных для метрики нет в JSON — прямо напиши \"нет данных в JSON\" и не рассчитывай её.\n\n"); promptBuilder.append("ВАЖНО:\n"); promptBuilder .append("- Начинай сразу с заголовка \"XI. Итоговая стратегия: что делать в первую очередь\"\n"); promptBuilder.append("- Не добавляй вступления или заключения\n"); promptBuilder.append( - "- Используй данные из всех предыдущих разделов (swot, channels, funnel, positioning, valueProposition, contentRecommendations)\n"); + "- Используй данные из strategyAnalysis (и/или swot/channels/funnel при наличии), positioning, valueProposition, contentRecommendations\n"); promptBuilder.append("- Давай конкретный, практический план действий с приоритетами\n"); promptBuilder.append("- Учитывай цель бизнеса на 6-12 месяцев (goal)\n\n"); promptBuilder.append("JSON ДАННЫЕ:\n"); @@ -3466,6 +3539,10 @@ public class MarketingAnalysisService { .replace("[[CHART_SEASONALITY]]", "") .replace("[[CHART_AUDIENCE]]", "") .replace("[[CHART_COMPETITORS]]", "") + .replace("[[CHART_MARKET_SHARE]]", "") + .replace("[[TABLE_COMPARISON]]", "") + .replace("[[CHART_AGE_DISTRIBUTION]]", "") + .replace("[[CHART_GENDER_DISTRIBUTION]]", "") .replace("[[CHART_CHANNELS]]", "") .replace("[[CHART_FUNNEL]]", "") .replace("[[CHART_SWOT]]", ""); @@ -3754,6 +3831,10 @@ public class MarketingAnalysisService { .replace("[[CHART_SEASONALITY]]", "") .replace("[[CHART_AUDIENCE]]", "") .replace("[[CHART_COMPETITORS]]", "") + .replace("[[CHART_MARKET_SHARE]]", "") + .replace("[[TABLE_COMPARISON]]", "") + .replace("[[CHART_AGE_DISTRIBUTION]]", "") + .replace("[[CHART_GENDER_DISTRIBUTION]]", "") .replace("[[CHART_CHANNELS]]", "") .replace("[[CHART_FUNNEL]]", "") .replace("[[CHART_SWOT]]", ""); @@ -3770,6 +3851,10 @@ public class MarketingAnalysisService { placeholderMap.put("[[CHART_SEASONALITY]]", new ChartMapping("seasonality", "seasonality")); placeholderMap.put("[[CHART_AUDIENCE]]", new ChartMapping("audienceSegmentation", "audienceSegmentation")); placeholderMap.put("[[CHART_COMPETITORS]]", new ChartMapping("competitors", "competitors")); + placeholderMap.put("[[CHART_MARKET_SHARE]]", new ChartMapping("marketShareChart", "marketShareChart")); + placeholderMap.put("[[TABLE_COMPARISON]]", new ChartMapping("comparisonTable", "comparisonTable")); + placeholderMap.put("[[CHART_AGE_DISTRIBUTION]]", new ChartMapping("audienceAge", "audienceAge")); + placeholderMap.put("[[CHART_GENDER_DISTRIBUTION]]", new ChartMapping("audienceGender", "audienceGender")); placeholderMap.put("[[CHART_CHANNELS]]", new ChartMapping("channelsPotential", "channelsPotential")); placeholderMap.put("[[CHART_FUNNEL]]", new ChartMapping("conversionFunnel", "funnel")); placeholderMap.put("[[CHART_SWOT]]", new ChartMapping("swot", "swot")); @@ -3828,7 +3913,8 @@ public class MarketingAnalysisService { // Финальная очистка: удаляем все оставшиеся не замененные плейсхолдеры // (если AI вернул их, но данных в chartsData нет) - java.util.regex.Pattern remainingPlaceholderPattern = java.util.regex.Pattern.compile("\\[\\[CHART_\\w+\\]\\]"); + java.util.regex.Pattern remainingPlaceholderPattern = java.util.regex.Pattern + .compile("\\[\\[(CHART|TABLE)_\\w+\\]\\]"); java.util.regex.Matcher matcher = remainingPlaceholderPattern.matcher(result); Set remainingPlaceholders = new HashSet<>(); while (matcher.find()) { @@ -3886,31 +3972,61 @@ public class MarketingAnalysisService { Map placeholderMap, Set processedPlaceholders) { String result = text; - // Маппинг разделов на плейсхолдеры - Map sectionToPlaceholder = new HashMap<>(); - sectionToPlaceholder.put("III\\.\\s*Анализ рынка и сезонности", "[[CHART_SEASONALITY]]"); - sectionToPlaceholder.put("IV\\.\\s*Анализ целевой аудитории", "[[CHART_AUDIENCE]]"); - sectionToPlaceholder.put("V\\.\\s*Анализ конкурентов", "[[CHART_COMPETITORS]]"); - sectionToPlaceholder.put("VII\\.\\s*Каналы продвижения", "[[CHART_CHANNELS]]"); - sectionToPlaceholder.put("VIII\\.\\s*Воронка спроса", "[[CHART_FUNNEL]]"); - sectionToPlaceholder.put("VI\\.\\s*SWOT-анализ", "[[CHART_SWOT]]"); + // Маппинг разделов на плейсхолдеры (может быть несколько плейсхолдеров в одном + // разделе) + Map> sectionToPlaceholders = new HashMap<>(); + sectionToPlaceholders.put("III\\.\\s*Анализ рынка и сезонности", List.of("[[CHART_SEASONALITY]]")); + sectionToPlaceholders.put("IV\\.\\s*Анализ целевой аудитории", + List.of("[[CHART_AGE_DISTRIBUTION]]", "[[CHART_GENDER_DISTRIBUTION]]")); + sectionToPlaceholders.put("V\\.\\s*Анализ конкурентов", + List.of("[[CHART_MARKET_SHARE]]", "[[TABLE_COMPARISON]]")); + sectionToPlaceholders.put("VII\\.\\s*Каналы продвижения", List.of("[[CHART_CHANNELS]]")); + sectionToPlaceholders.put("VIII\\.\\s*Воронка спроса", List.of("[[CHART_FUNNEL]]")); + sectionToPlaceholders.put("VI\\.\\s*SWOT-анализ", List.of("[[CHART_SWOT]]")); - for (Map.Entry sectionEntry : sectionToPlaceholder.entrySet()) { + for (Map.Entry> sectionEntry : sectionToPlaceholders.entrySet()) { String sectionPattern = sectionEntry.getKey(); - String placeholder = sectionEntry.getValue(); + List placeholders = sectionEntry.getValue(); - // Если плейсхолдер уже был обработан, пропускаем - if (processedPlaceholders.contains(placeholder)) { + if (placeholders == null || placeholders.isEmpty()) { continue; } - ChartMapping mapping = placeholderMap.get(placeholder); - if (mapping == null) { - continue; + StringBuilder blocksToInsert = new StringBuilder(); + boolean hasAnyBlock = false; + + for (String placeholder : placeholders) { + if (placeholder == null || placeholder.isBlank()) { + continue; + } + // Если плейсхолдер уже был обработан (присутствовал в тексте), пропускаем его + if (processedPlaceholders.contains(placeholder)) { + continue; + } + + ChartMapping mapping = placeholderMap.get(placeholder); + if (mapping == null) { + continue; + } + + Object chartData = chartsData.get(mapping.dataKey); + if (isEmptyChartData(chartData)) { + continue; + } + + try { + String jsonStr = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(chartData); + String jsonBlock = "\n\n```json:" + mapping.chartKey + "\n" + jsonStr + "\n```\n"; + blocksToInsert.append(jsonBlock); + hasAnyBlock = true; + // Помечаем как обработанный, чтобы не вставить второй раз + processedPlaceholders.add(placeholder); + } catch (JsonProcessingException e) { + logger.warn("Failed to serialize chart data for fallback {}: {}", mapping.dataKey, e.getMessage()); + } } - Object chartData = chartsData.get(mapping.dataKey); - if (isEmptyChartData(chartData)) { + if (!hasAnyBlock) { continue; } @@ -3932,48 +4048,41 @@ public class MarketingAnalysisService { } if (found) { - try { - String jsonStr = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(chartData); - String jsonBlock = "\n\n```json:" + mapping.chartKey + "\n" + jsonStr + "\n```\n"; - // Вставляем после найденного раздела, перед следующим разделом или концом - // текста - int insertPos = matcher.end(); + // Вставляем после найденного заголовка раздела, перед следующим разделом или + // концом текста + int insertPos = matcher.end(); - // Ищем конец текущего раздела (начало следующего раздела или конец текста) - String remainingText = result.substring(insertPos); + // Ищем конец текущего раздела (начало следующего раздела или конец текста) + String remainingText = result.substring(insertPos); - // Паттерн для поиска следующего раздела: может быть markdown заголовок или - // римская цифра - java.util.regex.Pattern nextSectionPattern = java.util.regex.Pattern - .compile("^(##+\\s*[IVX]+\\.|\\s*[IVX]+\\.\\s+[А-Я])", java.util.regex.Pattern.MULTILINE); - java.util.regex.Matcher nextSectionMatcher = nextSectionPattern.matcher(remainingText); + // Паттерн для поиска следующего раздела: может быть markdown заголовок или + // римская цифра + java.util.regex.Pattern nextSectionPattern = java.util.regex.Pattern + .compile("^(##+\\s*[IVX]+\\.|\\s*[IVX]+\\.\\s+[А-Я])", java.util.regex.Pattern.MULTILINE); + java.util.regex.Matcher nextSectionMatcher = nextSectionPattern.matcher(remainingText); - if (nextSectionMatcher.find()) { - // Вставляем перед следующим разделом - int nextSectionPos = insertPos + nextSectionMatcher.start(); - result = result.substring(0, nextSectionPos) + jsonBlock + result.substring(nextSectionPos); + String blocks = blocksToInsert.toString(); + + if (nextSectionMatcher.find()) { + // Вставляем перед следующим разделом + int nextSectionPos = insertPos + nextSectionMatcher.start(); + result = result.substring(0, nextSectionPos) + blocks + result.substring(nextSectionPos); + } else { + // Ищем следующий раздел по римской цифре + java.util.regex.Pattern romanNumeralPattern = java.util.regex.Pattern + .compile("\\n\\s*[IVX]+\\.\\s+[А-Я]", java.util.regex.Pattern.MULTILINE); + java.util.regex.Matcher romanMatcher = romanNumeralPattern.matcher(remainingText); + + if (romanMatcher.find()) { + int nextSectionPos = insertPos + romanMatcher.start(); + result = result.substring(0, nextSectionPos) + blocks + result.substring(nextSectionPos); } else { - // Ищем конец текущего раздела по содержимому (до следующего раздела с римской - // цифрой) - // Ищем следующий раздел по паттерну: начало строки, римская цифра, точка, - // пробел, заглавная буква - java.util.regex.Pattern romanNumeralPattern = java.util.regex.Pattern - .compile("\\n\\s*[IVX]+\\.\\s+[А-Я]", java.util.regex.Pattern.MULTILINE); - java.util.regex.Matcher romanMatcher = romanNumeralPattern.matcher(remainingText); - - if (romanMatcher.find()) { - int nextSectionPos = insertPos + romanMatcher.start(); - result = result.substring(0, nextSectionPos) + jsonBlock + result.substring(nextSectionPos); - } else { - // Вставляем в конец текста - result = result + jsonBlock; - } + // Вставляем в конец текста + result = result + blocks; } - logger.debug("addMissingChartsToSections: Added chart {} to section matching pattern: {}", - mapping.dataKey, sectionPattern); - } catch (JsonProcessingException e) { - logger.warn("Failed to serialize chart data for fallback {}: {}", mapping.dataKey, e.getMessage()); } + logger.debug("addMissingChartsToSections: Added fallback blocks to section matching pattern: {}", + sectionPattern); } else { logger.debug("addMissingChartsToSections: Section not found for pattern: {}", sectionPattern); }