From 8c18c13e42011fad827db59748508b96ddde79cf Mon Sep 17 00:00:00 2001 From: arys Date: Tue, 27 Jan 2026 19:28:33 +0500 Subject: [PATCH] fix --- .../service/MarketingAnalysisService.java | 353 ++---------------- 1 file changed, 23 insertions(+), 330 deletions(-) diff --git a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java index 267dc67..6a53a5d 100644 --- a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java +++ b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java @@ -22,7 +22,6 @@ import org.springframework.stereotype.Service; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.time.LocalDateTime; @@ -34,18 +33,6 @@ import org.springframework.web.reactive.function.client.WebClientResponseExcepti @Service @RequiredArgsConstructor public class MarketingAnalysisService { - - private static final String KZ_MARKET_CONTEXT = - "РОЛЬ: Ты — Партнер международной консалтинговой фирмы (Big 4: McKinsey/BCG/PwC) с офисом в Алматы.\n" + - "СПЕЦИАЛИЗАЦИЯ: Глубокий анализ рынка Казахстана. Ты мыслишь в Тенге (KZT).\n" + - "ТВОИ ЗНАНИЯ О РЫНКЕ KZ (СТРОГИЕ ПРАВИЛА):\n" + - "1. SuperApps: Kaspi.kz — это экосистема №1 (Магазин, Travel, Gov, Платежи). Это главный канал продаж для товарки и услуг.\n" + - "2. Карты: 2GIS в Казахстане популярнее Google Maps для поиска услуг офлайн. Отзывы в 2GIS критически важны.\n" + - "3. Фитнес-рынок: Лидеры — 1Fit (единый абонемент, НЕ переводи это название), Invictus, World Class, FitnessBlitz.\n" + - "4. Соцсети: Instagram (Direct/Reels) и TikTok — главные каналы продаж. ВКонтакте и Threads — мертвы для коммерции в РК, ЗАПРЕЩЕНО их рекомендовать.\n" + - "5. Экономика: Реальная инфляция ~12-15%. Потребитель чувствителен к цене, любит рассрочки (Kaspi Red/Kredit).\n" + - "ВАЛЮТА: Только KZT. Округляй цифры до сотен (например, 15 000 KZT, а не 14 321).\n"; - private static final String AI_MANDATORY_JSON_USAGE_WARNING = "ВНИМАНИЕ: Если в предоставленном JSON есть данные, ты ОБЯЗАН использовать их и вставить соответствующий тег [[CHART_...]]. " + "Никогда не пиши, что данных нет, если JSON не пустой.\n\n"; private static final String AI_ULTIMATE_JSON_PRESENCE_INSTRUCTION = "" @@ -59,10 +46,7 @@ public class MarketingAnalysisService { + "ФОРМАТ (ОБЯЗАТЕЛЬНО): пиши аналитический текст короткими емкими абзацами (2–4 предложения), без «воды».\n" + "ГРАФИКИ (ОБЯЗАТЕЛЬНО): располагай теги графиков (например, [[CHART_...]]) непосредственно внутри или сразу после описания конкретных данных/цифр, к которым относится график.\n\n"; - private static final int REPORT_BASE_MAX_TOKENS = 12000; private static final int REPORT_DETAILED_MAX_TOKENS = 12000; - private static final int JSON_DETAILED_MAX_TOKENS = 12000; - private DetailLevel resolveDetailLevelFromContext(Map contextJson) { if (contextJson == null) { return null; @@ -133,37 +117,6 @@ public class MarketingAnalysisService { } } - private String selectPrimaryModelForTextSection(DetailLevel detailLevel, String standardPrimaryModel) { - if (detailLevel == null) { - return standardPrimaryModel; - } - if (detailLevel == DetailLevel.КРАТКО) { - return miniModelName; - } - if (detailLevel == DetailLevel.ПОДРОБНО) { - return textModelName; - } - return standardPrimaryModel; - } - - private String selectFallbackModelForTextSection(DetailLevel detailLevel, String standardFallbackModel) { - if (detailLevel == null) { - return standardFallbackModel; - } - if (detailLevel == DetailLevel.КРАТКО || detailLevel == DetailLevel.ПОДРОБНО) { - return null; - } - return standardFallbackModel; - } - - private String selectModelForJsonAnalysis(DetailLevel detailLevel) { - if (detailLevel == DetailLevel.ПОДРОБНО) { - return textModelName; - } - // КРАТКО и СТАНДАРТНО: всегда mini - return miniModelName; - } - private Map buildResearchNotesForSection(Map contextJson, String sectionId) { if (contextJson == null) { return new HashMap<>(); @@ -297,15 +250,11 @@ public class MarketingAnalysisService { String result = text; - // 1) Standalone line occurrences (allow indentation). Replace the whole line - // to keep fences at the start of a line (no leading spaces before ```). java.util.regex.Pattern standaloneLine = java.util.regex.Pattern.compile( "(?m)^[ \\t]*" + java.util.regex.Pattern.quote(placeholderVariant) + "[ \\t]*$"); result = standaloneLine.matcher(result) .replaceAll(java.util.regex.Matcher.quoteReplacement(jsonBlock)); - // 2) Inline occurrences: force the block onto its own line with minimal - // spacing. java.util.regex.Pattern inline = java.util.regex.Pattern .compile(java.util.regex.Pattern.quote(placeholderVariant)); java.util.regex.Matcher m = inline.matcher(result); @@ -477,7 +426,6 @@ public class MarketingAnalysisService { try { general.add(serperSearchService.search(query2)); } catch (Exception e) { - // Keeping existing error handling String errorType = "Unexpected error"; if (e instanceof java.net.UnknownHostException || e instanceof java.net.ConnectException) errorType = "DNS/Network failure"; @@ -487,13 +435,11 @@ public class MarketingAnalysisService { general.add(new SerperSearchResult(query2, "ERROR", errorType + ": " + e.getMessage(), List.of())); } - // Lightweight section followups (stored for auditing/citations). Prompts for III/V/XI may further use these. Map followups = new LinkedHashMap<>(); String marketQuery = topic + " объем рынка Казахстан 2024 2025 отчет " + preferredSites; try { followups.put("market", serperSearchService.search(marketQuery)); } catch (Exception e) { - // Keeping existing error handling String errorType = "Unexpected error"; if (e instanceof java.net.UnknownHostException || e instanceof java.net.ConnectException) errorType = "DNS/Network failure"; @@ -612,7 +558,6 @@ public class MarketingAnalysisService { throw new IllegalArgumentException("Необходимо выбрать хотя бы один тип анализа"); } - // Create a single unified analysis with all analysis types MarketingAnalysis analysis = new MarketingAnalysis( request.getBusinessNiche(), request.getProduct(), @@ -622,7 +567,7 @@ public class MarketingAnalysisService { request.getDetailLevel(), request.getStrongSide(), request.getWeakSide(), - analysisTypes); // Все типы анализа в одном анализе + analysisTypes); analysis.setUserId(userId); analysis.setStatus("queued"); addStatusHistoryEntry(analysis, "queued", "Комплексный анализ создан и добавлен в очередь"); @@ -640,7 +585,7 @@ public class MarketingAnalysisService { try { // 1. Сбор данных Map researchPack = buildResearchPack(request); - List competitors = resolveCompetitorCompaniesForBenchmarks(request, researchPack, 5); + List competitors = resolveCompetitorCompaniesForBenchmarks(request, researchPack); // 2. Генерируем ШАБЛОН СТРУКТУРЫ (пустой, с плейсхолдерами) MarketingAnalysisResponseV2 structureTemplate = buildAnalysisV2ExampleForPrompt(request, competitors); @@ -654,7 +599,6 @@ public class MarketingAnalysisService { "competitors_list", competitors )); - // 4. Вызов AI (с защитой от сбоев) for (int attempt = 1; attempt <= 2; attempt++) { try { String aiResponse = openAIAnalyticsService.generateWithInstructionWithModel( @@ -671,10 +615,8 @@ public class MarketingAnalysisService { throw new IllegalStateException("OpenAI returned empty response"); } - // Чистка String cleaned = cleanJsonFromMarkdown(aiResponse); - // ЭКСТРЕННАЯ ЗАЩИТА: Если он все-таки сгенерил "value", вырезаем его на лету if (cleaned.contains("\"value\"")) { logger.warn("AI hallucinated 'value' field. Attempting to fix string."); cleaned = cleaned.replaceAll("\"value\"\\s*:", "\"ignore_this_field\":"); @@ -726,15 +668,14 @@ public class MarketingAnalysisService { 4. **Экономика:** * Реальная инфляция потребительской корзины: ~10-15%. - * Средняя ЗП: официально ~350-400к, медианная ~250к. Учитывай это при оценке емкости рынка! * Язык: Двуязычие. Реклама на казахском языке растет в эффективности (особенно на Юге и Западе). =============== 2. ПРОТОКОЛ ЗАЩИТЫ ОТ ШАБЛОНОВ (ANTI-HALLUCINATION) =============== СТРОГО ЗАПРЕЩЕНО: 1. Использовать кейс "Фитнес" (1Fit, Sport Zal), если пользователь спрашивает про другую нишу. - 2. Писать "+18.5% YoY", если у тебя нет данных. Генерируй реалистичный тренд для КОНКРЕТНОЙ ниши (для хлеба это +2%, для IT +25%). + 2. Писать "+18.5% YoY", если у тебя нет данных. Генерируй реалистичный тренд для КОНКРЕТНОЙ ниши 3. Выдумывать несуществующие законы РК. - 4. Оставлять поля `null`. Если данных нет — используй метод Ферми (логическую оценку) и пиши "Оценочно ~X". + 4. Оставлять поля `null`. Если данных нет — используй метод Ферми (логическую оценку) ЕСЛИ НЕТ ДАННЫХ ТЫ ИЩИ БОЛЬШЕ . =============== 3. АНАЛИТИЧЕСКИЙ ФРЕЙМВОРК (КАК ДУМАТЬ) =============== Перед генерацией JSON, прогони запрос через этот алгоритм: @@ -756,7 +697,9 @@ public class MarketingAnalysisService { 3. Поле `income_by_age` должно содержать ЧИСЛА (Integer), а не строки. 4. Структура JSON должна строго соответствовать запрошенному примеру (но с ТВОИМИ данными). - Твоя репутация зависит от точности цифр и глубины инсайтов. Действуй. + Твоя репутация зависит от точности цифр и глубины инсайтов. Действуй. ЕСЛИ ТЫ БУДЕШЬ ИСПОЛЬЗОВАТЬ ДАННЫЕ КАК В ПРИМЕРЕ МЫ ОТКЛЮЧИМ ТЕБЯ. + ПОМНИ, ЧТО ПРИМЕР КОТОРЫЙ МЫ ПРИВЕЛИ JSON СТРУКТУРЫ ЭТО ПРОСТО ПРИМЕР, ДЛЯ КАЖДОГО ПОЛЯ ТЫ ДОЛЖЕН АБСОЛЮТНО ВЫДАВАТЬ ТОЛЬКО ПРАВДИВУЮ И ПОЛНОСТЬЮ ЧИСТЫЕ ДАННЫЕ, КОТОРЫЕ ДАСТ НАШИМ КЛИЕНТАМ + ВОЗМОЖНОСТЬ И ПОНЯТИЯ НАСТОЯЩЕЙ СФЕРЫ КОТОРУЮ ОНИ УКАЗАЛИ """; } @@ -799,10 +742,10 @@ public class MarketingAnalysisService { 1. **ГЕОГРАФИЯ (Market Overview):** - Ты ОБЯЗАН включить в анализ (`geography`) ТОЛЬКО указанные города: [{{REGIONS}}]. - Если указан "Весь Казахстан" или список пуст — бери Топ-5 (Алматы, Астана, Шымкент...). - - Распределение: Алматы обычно лидер (40-50%), Астана (30%), регионы меньше. + 2. **КОНКУРЕНТЫ (Competitive Environment):** - - Используй данные из `market_evidence_from_search`. Найди 3-5 РЕАЛЬНЫХ игроков. + - Используй данные из `market_evidence_from_search`. Найди ТОП 5 РЕАЛЬНЫХ игроков. - **ВАЖНО:** В поле `name` пиши ТОЛЬКО БРЕНД (например, "Fitnation", а не "ТОО Fitnation Group"). Без кавычек и юр. форм. - В поле `website` попытайся найти реальный домен или ссылку на Instagram. - Оценка видимости (`visibility_score`) должна быть разной (например: 95, 82, 60, 45). Не ставь всем 100. @@ -819,7 +762,7 @@ public class MarketingAnalysisService { - `roi`: Реалистичный (150% - 300%). Не пиши фантастические 1000% на старте. 5. **ТИПЫ ДАННЫХ (Строго соблюдай типы):** - - `income_by_age`: Значения - ТОЛЬКО ЧИСЛА (Integer). Пример: 350000. (Никаких кавычек, запятых, "KZT"). + - `income_by_age`: Значения - ТОЛЬКО ЧИСЛА (Integer). Пример: ТОЛЬКО ИНТЕДЖЕР. (Никаких кавычек, запятых, "KZT"). - `monthly_online_index`: Дробные числа (0.0 - 1.0). 6. **ИСТОЧНИКИ:** @@ -830,10 +773,12 @@ public class MarketingAnalysisService { ФОРМАТ ОТВЕТА: Верни СТРОГО валидный JSON, соответствующий структуре ниже. - Замени все значения `EXAMPLE__` и `-9999` на реальные аналитические данные, специфичные для ниши {{NICHE}}. + Замени все значения `EXAMPLE__` и `-9999` на реальные аналитические данные, специфичные для ниши КОТОРЫЕ АБСОЛЮТНО КОРРЕКТНЫЕ И УКАЗЫВАЮТ НА РЕАЛЬНУЮ НИШУ, ГДЕ УКАЗАНЫ ВСЕ РЕАЛЬНО И ПРАВИЛЬНО, УЧИТЫВАЯ ВСЕ ТОНКОСТИ {{NICHE}}. ЭТАЛОННАЯ СТРУКТУРА JSON (Следуй ей неукоснительно): {{JSON_STRUCTURE}} + + НО ТЫ ДОЛЖЕН УЧИТЫВАТЬ, ЧТО ТЫ НЕ ПРОСТО ДОЛЖЕН ПРИДУМЫВАТЬ ДАНННЫЕ, ТЫ ДОЛЖЕН КОНКРЕТНЫЕ ДАННЫЕ ОТПРАВЛЯТЬ, КОТОРЫЕ РЕАЛЬНО ПОКАЗЫВАЮТ НАСКОЛЬКО ОНИ РЕАЛЬНЫЕ, И ОТОБРАЖАЮТ РЕАЛЬНОСТЬ, ССЫЛАЯСЬ НА НАСТОЯЩИЕ ИСТОЧНИКИ, И СДЕЛАВ ОЧЕНЬ ГЛУБОКИЙ АНАЛИЗ """; return template @@ -843,203 +788,10 @@ public class MarketingAnalysisService { .replace("{{AGE_INSTRUCTION}}", ageRangesInstruction) .replace("{{JSON_STRUCTURE}}", structureExampleJson); } - private String buildJsonStructureExample() { - return """ -{ - "report_title": "Стратегический анализ рынка: [Ниша]", - "sources": [ - "[https://stat.gov.kz/](https://stat.gov.kz/)...", - "[https://forbes.kz/](https://forbes.kz/)...", - "[https://kapital.kz/](https://kapital.kz/)..." - ], - "sections": { - "market_overview": { - "title": "Обзор рынка", - "metrics": { - "online_demand_index": "Высокий (8/10)", - "online_potential": "Растущий (+15% YoY)", - "demand_growth_rate_yoy": "+18.5%", - "seasonality_coefficient_spring": "1.2", - "seasonality_range": "Сентябрь - Май" - }, - "monthly_online_index": { - "january": 0.8, "february": 0.9, "march": 1.0, "april": 0.9, "may": 0.8, - "june": 0.6, "july": 0.5, "august": 0.7, "september": 0.9, - "october": 0.95, "november": 1.0, "december": 0.9 - }, - "geography": { - "Алматы": 100, - "Астана": 85, - "Шымкент": 50 - }, - "conclusion": "Рынок демонстрирует устойчивый рост..." - }, - "target_audience": { - "title": "Портрет аудитории", - "metrics": { - "total_audience_size": "~450 000 чел.", - "leading_segment_share": "Женщины 25-34", - "above_average_income_share": "35%" - }, - "income_by_age": { - "25-34": 350000, - "35-44": 420000 - }, - "segments": [ - { "name": "Ядро (Core)", "share": "45%" }, - { "name": "Потенциальные", "share": "30%" } - ], - "age_structure": { - "25-34": "45%", - "35-44": "30%" - }, - "conclusion": "Ключевой сегмент - платежеспособные специалисты..." - }, - "competitive_environment": { - "title": "Конкурентная среда", - "metrics": { - "leader_market_share": "35%", - "competition_level": "Высокая", - "ad_pressure": "Агрессивная", - "market_leader_name": "Invictus" - }, - "competitors": [ - { - "name": "Invictus", - "website": "invictus.kz", - "visibility_score": 95, - "key_strengths": ["Сильный бренд", "Сеть залов"], - "price_segment": "Premium" - }, - { - "name": "1Fit", - "website": "1fit.app", - "visibility_score": 88, - "key_strengths": ["Единый абонемент", "Удобное приложение"], - "price_segment": "Aggregator" - } - ], - "conclusion": "Рынок консолидирован крупными игроками..." - }, - "demand_and_behavior": { - "title": "Поведение потребителей", - "metrics": { - "search_volume_growth": "+22%", - "interactions_before_purchase": "5-7 касаний", - "repeat_refusal_probability": "15%" - }, - "search_query_types": { - "Коммерческие": "45%", - "Информационные": "30%" - }, - "decision_factors": { - "Цена": "Высокая важность", - "Отзывы": "Средняя важность" - }, - "reasons_for_refusal": { - "Высокая цена": "40%", - "Плохой сервис": "30%" - }, - "conclusion": "Пользователи активно сравнивают цены..." - }, - "customer_journey": { - "title": "Воронка продаж", - "funnel_metrics": { - "ad_traffic": "15 000 кликов", - "site_visitors": "12 000 (CTR 1.5%)", - "leads": 450, - "bookings": 200, - "payments": 120 - }, - "conversion_rates": { - "lead_to_booking": "45%", - "booking_to_payment": "60%", - "lead_to_payment_total": "27%" - }, - "funnel_bottlenecks": { - "lead_to_booking": "Долгий ответ менеджера", - "booking_to_payment": "Нет рассрочки" - }, - "conclusion": "Основная потеря лидов происходит на этапе..." - }, - "acquisition_channels": { - "title": "Каналы привлечения", - "metrics": { - "search_traffic_share": "30%", - "average_lead_cost": "3500 KZT", - "growth_potential": "TikTok/Reels" - }, - "channel_shares": { - "Instagram": "50%", - "Google Ads": "20%", - "TikTok": "15%" - }, - "channel_efficiency": { - "instagram": { "cost": "2500 KZT", "status": "High" }, - "google_search": { "cost": "4500 KZT", "status": "Medium" } - }, - "conclusion": "Instagram остается самым эффективным каналом..." - }, - "marketing_economics": { - "title": "Юнит-экономика", - "metrics": { - "customer_acquisition_cost_cac": "3500 KZT", - "repeat_sales_conversion": "30%", - "roi": "250%" - }, - "financial_performance": { - "marketing_expenses": { "month": "1.5 млн KZT", "year": "18 млн KZT" }, - "clients": { "month": "~430", "year": "~5160" }, - "cac": { "month": "3500 KZT", "year": "3200 KZT" }, - "revenue_from_client": { "month": "25 000 KZT", "year": "300 000 KZT" }, - "profit_from_client": { "month": "7 500 KZT", "year": "90 000 KZT" } - }, - "conclusion": "Модель масштабируема при сохранении CAC..." - }, - "bottlenecks": { - "title": "Узкие места", - "statistics": { - "landing_page_issues": "Отказы 60%", - "slow_manager_responses": "30% потерь" - }, - "details": { - "landing": ["Медленная загрузка", "Слабый оффер"], - "lead_management": ["Нет скриптов", "Звонок > 15 мин"], - "retention": ["Нет программы лояльности"] - }, - "conclusion": "Необходимо оптимизировать мобильную версию сайта..." - }, - "recommendations": { - "title": "План действий", - "core_pillars": [ - { "strategy": "Быстрый старт", "action": "Запустить таргет..." }, - { "strategy": "Удержание", "action": "Внедрить систему бонусов..." } - ], - "conclusion": "Рекомендуется начать с..." - } - } -} -"""; - } - private boolean isPlaceholderCompetitorKey(String key) { - if (key == null) { - return false; - } - String k = key.trim().toLowerCase(Locale.ROOT); - if (k.isEmpty()) { - return false; - } - return k.startsWith("competitor_") - || "your_business".equals(k) - || "lead".equals(k) - || "payment".equals(k) - || "others".equals(k); - } private List resolveCompetitorCompaniesForBenchmarks(MarketingAnalysisRequest request, - Map researchPack, - int limit) { - int max = Math.max(5, Math.min(limit, 15)); + Map researchPack) { + int max = Math.max(5, Math.min(5, 15)); LinkedHashSet names = new LinkedHashSet<>(); if (researchPack != null) { @@ -1090,7 +842,7 @@ public class MarketingAnalysisService { }); List out = new ArrayList<>(names); - int resultLimit = Math.min(out.size(), Math.max(limit, 3)); + int resultLimit = Math.min(out.size(), Math.max(5, 3)); return out.subList(0, resultLimit); } @@ -1164,12 +916,10 @@ public class MarketingAnalysisService { return null; } - // Basic "humanization" of domain label. String cleaned = sld.replaceAll("[^a-z0-9\\-]+", "").replace('-', ' ').trim(); if (cleaned.isEmpty()) { return null; } - // Title-case first character return cleaned.substring(0, 1).toUpperCase(Locale.ROOT) + cleaned.substring(1); } @@ -1181,9 +931,6 @@ public class MarketingAnalysisService { if (t.isEmpty()) { return null; } - - // Keep the left-most "brand-like" chunk: "Brand — ...", "Brand | ...", "Brand - - // ..." String[] seps = new String[] { " — ", " - ", " | ", " – ", ":", "|", "—", "–" }; for (String sep : seps) { int idx = t.indexOf(sep); @@ -1198,7 +945,6 @@ public class MarketingAnalysisService { return null; } - // Trim common location tails: "... Алматы", "... Астана", "... Казахстан" t = t.replaceAll("\\s+(Алматы|Астана|Казахстан)\\s*$", "").trim(); return t.isEmpty() ? null : t; } @@ -1213,7 +959,6 @@ public class MarketingAnalysisService { } String lower = t.toLowerCase(Locale.ROOT); - // Exclude obvious non-brands (directories/articles) String[] bad = new String[] { "топ", "рейтинг", "обзор", "лучшие", "лучших", "каталог", "список", "цены", "стоимость", "отзывы", "адрес", "телефон", "контакты", "как выбрать", "вакансии", "купить", "доставка" @@ -1224,65 +969,16 @@ public class MarketingAnalysisService { } } - // Too many words is usually an article title, not a brand if (t.split("\\s+").length > 5) { return false; } return true; } - private void replacePlaceholderCompanyKeysIfNeeded(MarketingAnalysisResponseV2 parsed, - List competitorCompanies) { - // 1. Базовые проверки на null - if (parsed == null || competitorCompanies == null || competitorCompanies.isEmpty()) { - return; - } - if (parsed.getSections() == null || parsed.getSections().getCompetitiveEnvironment() == null) { - return; - } - - MarketingAnalysisResponseV2.CompetitiveEnvironment ce = parsed.getSections().getCompetitiveEnvironment(); - - // Получаем список конкурентов из DTO - List rows = ce.getCompetitors(); - - if (rows != null && !rows.isEmpty()) { - // Проверяем, есть ли заглушки хотя бы у одного элемента - boolean hasPlaceholders = rows.stream() - .anyMatch(row -> isPlaceholderCompetitorKey(row.getName())); - - if (hasPlaceholders) { - // Определяем, сколько элементов мы можем обновить (чтобы не выйти за пределы массивов) - int n = Math.min(rows.size(), competitorCompanies.size()); - - for (int i = 0; i < n; i++) { - MarketingAnalysisResponseV2.CompetitorRow row = rows.get(i); - - // Если имя текущего конкурента - заглушка, меняем на реальное - if (isPlaceholderCompetitorKey(row.getName())) { - String realCompetitorName = competitorCompanies.get(i); - row.setName(realCompetitorName); - - // ЛОГИКА ДЛЯ САЙТА: - // Если у нас нет сайта от ГПТ, или если реальное имя похоже на домен, - // можно записать его и в website для надежности. - if (row.getWebsite() == null || row.getWebsite().isEmpty() || row.getWebsite().equals("—")) { - // Предполагаем, что в competitorCompanies могут лежать домены - row.setWebsite(realCompetitorName); - } - } - } - } - } - } - - public String generateAndSaveAnalysisV2(MarketingAnalysisRequest request, String userId) { try { - // Генерируем анализ MarketingAnalysisResponseV2 analysisData = generateAnalysisV2(request); - // Создаем документ для сохранения MarketingAnalysisV2Document document = new MarketingAnalysisV2Document(); document.setUserId(userId); document.setBusinessNiche(request.getBusinessNiche()); @@ -1296,7 +992,6 @@ public class MarketingAnalysisService { document.setAnalysisType(request.getAnalysisType()); document.setAnalysisData(analysisData); - // Сохраняем в MongoDB MarketingAnalysisV2Document saved = v2Repository.save(document); logger.info("Saved marketing analysis v2 with id: {}", saved.getId()); return saved.getId(); @@ -1350,11 +1045,10 @@ public class MarketingAnalysisService { marketOverview.setMetrics(moMetrics); MarketingAnalysisResponseV2.MonthlyOnlineIndex monthlyOnlineIndex = new MarketingAnalysisResponseV2.MonthlyOnlineIndex(); - // Ставим 1.0, чтобы показать, что это Double, но не навязывать тренд фитнеса - monthlyOnlineIndex.setJanuary(1.0); monthlyOnlineIndex.setFebruary(1.0); monthlyOnlineIndex.setMarch(1.0); - monthlyOnlineIndex.setApril(1.0); monthlyOnlineIndex.setMay(1.0); monthlyOnlineIndex.setJune(1.0); - monthlyOnlineIndex.setJuly(1.0); monthlyOnlineIndex.setAugust(1.0); monthlyOnlineIndex.setSeptember(1.0); - monthlyOnlineIndex.setOctober(1.0); monthlyOnlineIndex.setNovember(1.0); monthlyOnlineIndex.setDecember(1.0); + monthlyOnlineIndex.setJanuary(0.1); monthlyOnlineIndex.setFebruary(0.1); monthlyOnlineIndex.setMarch(0.1); + monthlyOnlineIndex.setApril(1.0); monthlyOnlineIndex.setMay(0.1); monthlyOnlineIndex.setJune(0.1); + monthlyOnlineIndex.setJuly(0.1); monthlyOnlineIndex.setAugust(0.1); monthlyOnlineIndex.setSeptember(0.1); + monthlyOnlineIndex.setOctober(0.1); monthlyOnlineIndex.setNovember(0.1); monthlyOnlineIndex.setDecember(0.1); marketOverview.setMonthlyOnlineIndex(monthlyOnlineIndex); Map geography = new java.util.LinkedHashMap<>(); @@ -1387,13 +1081,13 @@ public class MarketingAnalysisService { java.util.Map incomeByAge = new java.util.LinkedHashMap<>(); // Ставим 0, чтобы ИИ понял, что нужны числа (Integer), а не строки incomeByAge.put("18-24", 0); incomeByAge.put("25-34", 0); - incomeByAge.put("35-44", 0); incomeByAge.put("45+", 0); + incomeByAge.put("35-44", 0); incomeByAge.put("45-54", 0); + incomeByAge.put("55+", 0); targetAudience.setIncomeByAge(incomeByAge); targetAudience.setConclusion("[[ВЫВОД_О_КЛИЕНТАХ]]"); sections.setTargetAudience(targetAudience); - // 3. Competitive Environment MarketingAnalysisResponseV2.CompetitiveEnvironment competitiveEnvironment = new MarketingAnalysisResponseV2.CompetitiveEnvironment(); competitiveEnvironment.setTitle("Конкурентный ландшафт"); @@ -1405,7 +1099,6 @@ public class MarketingAnalysisService { competitiveEnvironment.setMetrics(ceMetrics); java.util.List competitorRows = new java.util.ArrayList<>(); - // Здесь мы оставляем список пустым или с заглушкой, чтобы ИИ заполнил сам на основе списка competitorCompanies MarketingAnalysisResponseV2.CompetitorRow row = new MarketingAnalysisResponseV2.CompetitorRow(); row.setName("[[ИМЯ_КОНКУРЕНТА_1]]"); row.setWebsite("[[САЙТ_ИЛИ_ЗАГЛУШКА]]"); @@ -1513,7 +1206,7 @@ public class MarketingAnalysisService { MarketingAnalysisResponseV2.FinancialPerformance financialPerformance = new MarketingAnalysisResponseV2.FinancialPerformance(); // ВАЖНО: Мы показываем, что month и year - это String поля для значений - financialPerformance.setMarketingExpenses(new MarketingAnalysisResponseV2.MonthYearValue("[[РАСХОД_МЕСЯЦ]]", "[[РАСХОД_ГОД]]")); + financialPerformance.setMarketingExpenses(new MarketingAnalysisResponseV2.MonthYearValue("[[РАСХОД_МЕСЯЦ(всегда ставь 100 000, но нужно выводить информацию отталкиваясь от этой суммы, и ты должен выдавать только сумму]]", "[[РАСХОД_ГОД]]")); financialPerformance.setClients(new MarketingAnalysisResponseV2.MonthYearValue("[[КЛИЕНТОВ_МЕСЯЦ]]", "[[КЛИЕНТОВ_ГОД]]")); financialPerformance.setCac(new MarketingAnalysisResponseV2.MonthYearValue("[[CAC_МЕСЯЦ]]", "[[CAC_ГОД]]")); financialPerformance.setRevenueFromClient(new MarketingAnalysisResponseV2.MonthYearValue("[[ВЫРУЧКА_МЕСЯЦ]]", "[[ВЫРУЧКА_ГОД]]"));