This commit is contained in:
arys
2026-01-27 19:28:33 +05:00
parent 94cccd2230
commit 8c18c13e42
@@ -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<String, Object> 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<String, Object> buildResearchNotesForSection(Map<String, Object> 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<String, Object> 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<String, Object> researchPack = buildResearchPack(request);
List<String> competitors = resolveCompetitorCompaniesForBenchmarks(request, researchPack, 5);
List<String> 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<String> resolveCompetitorCompaniesForBenchmarks(MarketingAnalysisRequest request,
Map<String, Object> researchPack,
int limit) {
int max = Math.max(5, Math.min(limit, 15));
Map<String, Object> researchPack) {
int max = Math.max(5, Math.min(5, 15));
LinkedHashSet<String> names = new LinkedHashSet<>();
if (researchPack != null) {
@@ -1090,7 +842,7 @@ public class MarketingAnalysisService {
});
List<String> 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<String> 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<MarketingAnalysisResponseV2.CompetitorRow> 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<String, Integer> geography = new java.util.LinkedHashMap<>();
@@ -1387,13 +1081,13 @@ public class MarketingAnalysisService {
java.util.Map<String, Integer> 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<MarketingAnalysisResponseV2.CompetitorRow> 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("[[ВЫРУЧКА_МЕСЯЦ]]", "[[ВЫРУЧКА_ГОД]]"));