This commit is contained in:
root
2026-01-03 01:04:20 +05:00
parent eb9aaa1725
commit 414c9ff697
3 changed files with 1455 additions and 0 deletions
@@ -110,6 +110,20 @@ public class MarketingController {
return ResponseEntity.ok(ApiResponse.success("Анализ запущен успешно", response));
}
@PostMapping("/start/v2")
public ResponseEntity<?> startAnalysisV2(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@Valid @RequestBody MarketingAnalysisRequest request) {
String userId = extractUserIdFromHeader(authHeader);
if (userId == null) {
return unauthorizedResponse();
}
MarketingAnalysisResponseV2 response = marketingAnalysisService.generateAnalysisV2(request);
return ResponseEntity.ok(response);
}
@GetMapping("/{analysisId}")
public ResponseEntity<?> getAnalysis(
@RequestHeader(value = "Authorization", required = false) String authHeader,
File diff suppressed because it is too large Load Diff
@@ -2,6 +2,7 @@ package kz.konturai.parser.service;
import kz.konturai.parser.dto.MarketingAnalysisRequest;
import kz.konturai.parser.dto.MarketingAnalysisResult;
import kz.konturai.parser.dto.MarketingAnalysisResponseV2;
import kz.konturai.parser.dto.SerperSearchItem;
import kz.konturai.parser.dto.SerperSearchResult;
import kz.konturai.parser.dto.StatusHistoryEntry;
@@ -633,6 +634,295 @@ public class MarketingAnalysisService {
return List.of(analysis);
}
/**
* V2 endpoint payload for frontend integration testing.
* Generates a strict JSON response via OpenAI, grounded on Serper web evidence.
*/
public MarketingAnalysisResponseV2 generateAnalysisV2(MarketingAnalysisRequest request) {
try {
// 1) Gather web evidence
Map<String, Object> researchPack = buildResearchPack(request);
// 2) Build strict schema instructions + provide an example JSON structure
String exampleJson = objectMapper.writeValueAsString(buildAnalysisV2ExampleForPrompt());
String systemPrompt = ""
+ "Ты — партнер Big Four (управленческий консалтинг) и senior marketing analyst.\n"
+ "Пиши уверенно, структурно, с причинно-следственной логикой, без «воды».\n"
+ "КРИТИЧЕСКИ ВАЖНО: Твой ответ должен быть ТОЛЬКО валидный JSON, без markdown, без ```.\n";
String instruction = ""
+ "Сгенерируй один JSON-объект, который СТРОГО соответствует схеме MarketingAnalysisResponseV2.\n"
+ "Обязательные правила:\n"
+ "1) Верни ТОЛЬКО JSON (без текста до/после).\n"
+ "2) Заполни ВСЕ вложенные поля: sections.market_overview, target_audience, competitive_environment, "
+ "demand_and_behavior, customer_journey, acquisition_channels, marketing_economics, bottlenecks, recommendations.\n"
+ "3) Используй реальные цифры/темпы роста/проценты из evidence (researchPack). Если конкретных цифр нет — сделай Analytical Extrapolation, "
+ "но НИКОГДА не пиши «данных нет/не найдено/недоступно».\n"
+ "4) Соблюдай типы:\n"
+ " - проценты/строки формата \"76/100\", \"+27%\", \"1.34x\" => string\n"
+ " - monthly_online_index.* => number (double)\n"
+ " - funnel_metrics.leads/bookings/payments => integer\n"
+ "5) Пиши на русском языке.\n\n"
+ "Ниже пример правильной структуры (пример для ориентира; цифры должны быть основаны на evidence):\n"
+ exampleJson;
String contextJson = objectMapper.writeValueAsString(Map.of(
"request", request,
"evidence", researchPack));
// 3) Call OpenAI (sleep to reduce 429 rate-limit bursts)
try {
Thread.sleep(2000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
String aiResponse = openAIAnalyticsService.generateWithInstructionWithModel(
contextJson,
instruction,
"ru",
textModelName,
systemPrompt,
15000,
180000L);
if (aiResponse == null || aiResponse.isBlank()) {
throw new IllegalStateException("OpenAI returned empty response for analysis v2");
}
// 4) Clean + parse
String cleaned = cleanJsonFromMarkdown(aiResponse);
if (cleaned != null) {
cleaned = cleaned.replace("\uFEFF", "").trim();
}
return objectMapper.readValue(cleaned, MarketingAnalysisResponseV2.class);
} catch (Exception e) {
logger.error("generateAnalysisV2: Failed to generate/parse v2 response: {}", e.getMessage(), e);
throw new RuntimeException("Не удалось сгенерировать marketing analysis v2", e);
}
}
/**
* Example payload used ONLY for prompting the AI with the expected JSON
* schema/shape.
* This is not returned to the client.
*/
private MarketingAnalysisResponseV2 buildAnalysisV2ExampleForPrompt() {
MarketingAnalysisResponseV2 response = new MarketingAnalysisResponseV2();
response.setReportTitle("Маркетинговый анализ для интернет-продвижения");
MarketingAnalysisResponseV2.Sections sections = new MarketingAnalysisResponseV2.Sections();
// market_overview
MarketingAnalysisResponseV2.MarketOverview marketOverview = new MarketingAnalysisResponseV2.MarketOverview();
marketOverview.setTitle("Обзор рынка");
MarketingAnalysisResponseV2.MarketOverviewMetrics moMetrics = new MarketingAnalysisResponseV2.MarketOverviewMetrics();
moMetrics.setOnlineDemandIndex("76/100");
moMetrics.setOnlinePotential("Высокий");
moMetrics.setDemandGrowthRateYoy("+27%");
moMetrics.setSeasonalityCoefficientSpring("1.34x");
moMetrics.setSeasonalityRange("1.29 - 1.38");
marketOverview.setMetrics(moMetrics);
MarketingAnalysisResponseV2.MonthlyOnlineIndex monthlyOnlineIndex = new MarketingAnalysisResponseV2.MonthlyOnlineIndex();
monthlyOnlineIndex.setMarch(1.32);
monthlyOnlineIndex.setApril(1.34);
monthlyOnlineIndex.setMay(1.38);
marketOverview.setMonthlyOnlineIndex(monthlyOnlineIndex);
marketOverview.setGeography("Казахстан (анализ по регионам представлен на тепловой карте)");
marketOverview.setConclusion(
"Онлайн-спрос показывает устойчивый рост в течение года, весной достигая пика. Интернет-продвижение даст максимальный эффект.");
sections.setMarketOverview(marketOverview);
// target_audience
MarketingAnalysisResponseV2.TargetAudience targetAudience = new MarketingAnalysisResponseV2.TargetAudience();
targetAudience.setTitle("Целевая аудитория");
MarketingAnalysisResponseV2.TargetAudienceMetrics taMetrics = new MarketingAnalysisResponseV2.TargetAudienceMetrics();
taMetrics.setTotalAudienceSize("1.8 млн человек");
taMetrics.setLeadingSegmentShare("29%");
taMetrics.setAboveAverageIncomeShare("35%");
targetAudience.setMetrics(taMetrics);
targetAudience.setSegments(List.of(
new MarketingAnalysisResponseV2.AudienceSegment("Женщины, 2534 года", "29%"),
new MarketingAnalysisResponseV2.AudienceSegment("Мужчины, 2544 года", "24%"),
new MarketingAnalysisResponseV2.AudienceSegment("Доход выше среднего", "35%")));
Map<String, String> ageStructure = new LinkedHashMap<>();
ageStructure.put("18-24", "10%");
ageStructure.put("25-34", "29%");
ageStructure.put("35-44", "Высокая доля (визуально на графике)");
ageStructure.put("45-54", "15-24%");
ageStructure.put("55_plus", "Низкая доля");
targetAudience.setAgeStructure(ageStructure);
targetAudience.setConclusion(
"Основную ценность представляют мужчины и женщины 25–44 лет с доходом выше среднего (до 35% аудитории).");
sections.setTargetAudience(targetAudience);
// competitive_environment
MarketingAnalysisResponseV2.CompetitiveEnvironment competitiveEnvironment = new MarketingAnalysisResponseV2.CompetitiveEnvironment();
competitiveEnvironment.setTitle("Конкурентная среда");
MarketingAnalysisResponseV2.CompetitiveEnvironmentMetrics ceMetrics = new MarketingAnalysisResponseV2.CompetitiveEnvironmentMetrics();
ceMetrics.setMarketShareTraffic("16%");
ceMetrics.setVisibilityRank("#3");
ceMetrics.setCompetitorShareInContextAds("27%");
ceMetrics.setBenchmarkAstana("13-25%");
competitiveEnvironment.setMetrics(ceMetrics);
Map<String, Integer> digitalVisibilityBenchmarks = new LinkedHashMap<>();
digitalVisibilityBenchmarks.put("competitor_a", 1050);
digitalVisibilityBenchmarks.put("your_business", 600);
digitalVisibilityBenchmarks.put("lead", 300);
digitalVisibilityBenchmarks.put("payment", 450);
digitalVisibilityBenchmarks.put("others", 680);
competitiveEnvironment.setDigitalVisibilityBenchmarks(digitalVisibilityBenchmarks);
Map<String, List<String>> competitorStrengths = new LinkedHashMap<>();
competitorStrengths.put("your_business", List.of("Цена", "Оффер"));
competitorStrengths.put("competitor_a", List.of("Отзывы", "Рекламный бюджет"));
competitorStrengths.put("competitor_b", List.of("Контент", "Соцсети", "Экспириенс"));
competitorStrengths.put("competitor_c", List.of("Контент", "Сайт"));
competitiveEnvironment.setCompetitorStrengths(competitorStrengths);
competitiveEnvironment.setConclusion(
"Ваш бизнес занимает 3-е место по видимости. Основные преимущества конкурентов — соцдоказательства и контент. Ценовая борьба не является оптимальной.");
sections.setCompetitiveEnvironment(competitiveEnvironment);
// demand_and_behavior
MarketingAnalysisResponseV2.DemandAndBehavior demandAndBehavior = new MarketingAnalysisResponseV2.DemandAndBehavior();
demandAndBehavior.setTitle("Спрос и поведение");
MarketingAnalysisResponseV2.DemandAndBehaviorMetrics dabMetrics = new MarketingAnalysisResponseV2.DemandAndBehaviorMetrics();
dabMetrics.setSearchVolumeGrowth("+62%");
dabMetrics.setInteractionsBeforePurchase("3-4");
dabMetrics.setRepeatRefusalProbability("54%");
demandAndBehavior.setMetrics(dabMetrics);
Map<String, String> searchQueryTypes = new LinkedHashMap<>();
searchQueryTypes.put("direct_interest_catalog", "52%");
searchQueryTypes.put("question_based_articles", "28%");
searchQueryTypes.put("competitors_reviews", "13%");
searchQueryTypes.put("problem_solving_hot_promo", "7%");
demandAndBehavior.setSearchQueryTypes(searchQueryTypes);
Map<String, String> decisionFactors = new LinkedHashMap<>();
decisionFactors.put("promo_discount", "43%");
decisionFactors.put("review_recommendation", "36%");
decisionFactors.put("warranty_quality", "33%");
decisionFactors.put("convenience_service", "25%");
decisionFactors.put("availability", "21%");
decisionFactors.put("price", "19%");
demandAndBehavior.setDecisionFactors(decisionFactors);
Map<String, String> reasonsForRefusal = new LinkedHashMap<>();
reasonsForRefusal.put("high_price", "37%");
reasonsForRefusal.put("no_reviews_social_proof", "34%");
reasonsForRefusal.put("choice_difficulty_unclear_offer", "29%");
demandAndBehavior.setReasonsForRefusal(reasonsForRefusal);
demandAndBehavior.setConclusion(
"Основной трафик приходит из поиска. Для отклика ключевую роль играют акции и отзывы.");
sections.setDemandAndBehavior(demandAndBehavior);
// customer_journey
MarketingAnalysisResponseV2.CustomerJourney customerJourney = new MarketingAnalysisResponseV2.CustomerJourney();
customerJourney.setTitle("Путь клиента");
MarketingAnalysisResponseV2.FunnelMetrics funnelMetrics = new MarketingAnalysisResponseV2.FunnelMetrics();
funnelMetrics.setAdTraffic("Реклама");
funnelMetrics.setSiteVisitors("Сайт / Лендинг");
funnelMetrics.setLeads(5600);
funnelMetrics.setBookings(1200);
funnelMetrics.setPayments(680);
customerJourney.setFunnelMetrics(funnelMetrics);
MarketingAnalysisResponseV2.ConversionRates conversionRates = new MarketingAnalysisResponseV2.ConversionRates();
conversionRates.setLeadToBooking("21%");
conversionRates.setBookingToPayment("57%");
conversionRates.setLeadToPaymentTotal("12%");
customerJourney.setConversionRates(conversionRates);
MarketingAnalysisResponseV2.FunnelBottlenecks funnelBottlenecks = new MarketingAnalysisResponseV2.FunnelBottlenecks();
funnelBottlenecks.setLeadToBooking("Медленная скорость ответа");
funnelBottlenecks.setBookingToPayment("Недостаточно касаний / напоминаний");
customerJourney.setFunnelBottlenecks(funnelBottlenecks);
customerJourney.setConclusion(
"Основные потери происходят на этапе лид -> запись (конверсия 21%) из-за медленных ответов менеджеров.");
sections.setCustomerJourney(customerJourney);
// acquisition_channels
MarketingAnalysisResponseV2.AcquisitionChannels acquisitionChannels = new MarketingAnalysisResponseV2.AcquisitionChannels();
acquisitionChannels.setTitle("Каналы привлечения");
MarketingAnalysisResponseV2.AcquisitionChannelsMetrics acMetrics = new MarketingAnalysisResponseV2.AcquisitionChannelsMetrics();
acMetrics.setSearchTrafficShare("48%");
acMetrics.setAverageLeadCost("37 P");
acMetrics.setGrowthPotential("+21%");
acquisitionChannels.setMetrics(acMetrics);
Map<String, String> channelShares = new LinkedHashMap<>();
channelShares.put("seo", "48%");
channelShares.put("yandex_direct", "31%");
channelShares.put("retargeting", "13%");
channelShares.put("vkontakte", "6%");
acquisitionChannels.setChannelShares(channelShares);
Map<String, MarketingAnalysisResponseV2.ChannelEfficiency> channelEfficiency = new LinkedHashMap<>();
channelEfficiency.put("seo", new MarketingAnalysisResponseV2.ChannelEfficiency("25 P", "Эффективен"));
channelEfficiency.put("yandex", new MarketingAnalysisResponseV2.ChannelEfficiency("45 P", "Эффективен"));
channelEfficiency.put("retargeting",
new MarketingAnalysisResponseV2.ChannelEfficiency("40 P", "Удовлетворительно"));
channelEfficiency.put("vkontakte", new MarketingAnalysisResponseV2.ChannelEfficiency("57 P", "Завышено"));
channelEfficiency.put("youtube", new MarketingAnalysisResponseV2.ChannelEfficiency("63 P", "Завышено"));
channelEfficiency.put("instagram", new MarketingAnalysisResponseV2.ChannelEfficiency("77 P", "Завышено"));
acquisitionChannels.setChannelEfficiency(channelEfficiency);
acquisitionChannels.setConclusion(
"Основной источник лидов — SEO. VK, Youtube и Instagram требуют оптимизации из-за завышенной стоимости лида.");
sections.setAcquisitionChannels(acquisitionChannels);
// marketing_economics
MarketingAnalysisResponseV2.MarketingEconomics marketingEconomics = new MarketingAnalysisResponseV2.MarketingEconomics();
marketingEconomics.setTitle("Экономика маркетинга");
MarketingAnalysisResponseV2.MarketingEconomicsMetrics meMetrics = new MarketingAnalysisResponseV2.MarketingEconomicsMetrics();
meMetrics.setCustomerAcquisitionCostCac("2900 P");
meMetrics.setRepeatSalesConversion("32%");
meMetrics.setRoi("3.5x");
marketingEconomics.setMetrics(meMetrics);
MarketingAnalysisResponseV2.FinancialPerformance financialPerformance = new MarketingAnalysisResponseV2.FinancialPerformance();
financialPerformance
.setMarketingExpenses(new MarketingAnalysisResponseV2.MonthYearValue("1.97 млн", "23.6 млн"));
financialPerformance.setClients(new MarketingAnalysisResponseV2.MonthYearValue("680", "8160"));
financialPerformance.setCac(new MarketingAnalysisResponseV2.MonthYearValue("2900 P", "2900 P"));
financialPerformance
.setRevenueFromClient(new MarketingAnalysisResponseV2.MonthYearValue("10.200 P", "23.040 P"));
financialPerformance.setProfitFromClient(new MarketingAnalysisResponseV2.MonthYearValue("7.300 P", "16.720 P"));
marketingEconomics.setFinancialPerformance(financialPerformance);
marketingEconomics.setConclusion(
"Экономика digital-маркетинга эффективна: возврат инвестиций 3.5х. Точки роста — повторные продажи и удержание.");
sections.setMarketingEconomics(marketingEconomics);
// bottlenecks
MarketingAnalysisResponseV2.Bottlenecks bottlenecks = new MarketingAnalysisResponseV2.Bottlenecks();
bottlenecks.setTitle("Проблемы и узкие места");
Map<String, String> statistics = new LinkedHashMap<>();
statistics.put("landing_page_issues", "51%");
statistics.put("slow_manager_responses", "60%");
statistics.put("repeat_interaction_refusal", "36%");
bottlenecks.setStatistics(statistics);
MarketingAnalysisResponseV2.BottlenecksDetails details = new MarketingAnalysisResponseV2.BottlenecksDetails();
details.setLanding(List.of("Низкая конверсия (2%)", "Проблемы с формой записи", "Уход посетителей"));
details.setLeadManagement(
List.of("Медленные ответы (30-40+ мин)", "Недостаток напоминаний", "Пропущенные лиды"));
details.setRetention(List.of("Недостаток повторных предложений", "Нет прогрева", "Слабая работа с CRM"));
bottlenecks.setDetails(details);
bottlenecks.setConclusion(
"Теряются десятки процентов лидов из-за медленных ответов менеджеров и слабой ретаргетинговой работы.");
sections.setBottlenecks(bottlenecks);
// recommendations
MarketingAnalysisResponseV2.Recommendations recommendations = new MarketingAnalysisResponseV2.Recommendations();
recommendations.setTitle("Рекомендации (итоги)");
recommendations.setCorePillars(List.of(
new MarketingAnalysisResponseV2.CorePillar("Фокус на эффективных каналах",
"Перераспределение бюджета на каналы с устойчивым спросом (Яндекс/SEO)"),
new MarketingAnalysisResponseV2.CorePillar("Увеличение повторных касаний",
"Усиление удерживающих кампаний и серий касаний с клиентами"),
new MarketingAnalysisResponseV2.CorePillar("Приоритет высокоценных аудиторий",
"Смещение акцента на платёжеспособные сегменты и B2B"),
new MarketingAnalysisResponseV2.CorePillar("Рост эффективности трафика",
"Снижение зависимости от дорогих источников и оптимизация каналов")));
recommendations.setConclusion(
"Рекомендации фокусируют усилия на эффективности интернет-продвижения и служат базой для следующих шагов.");
sections.setRecommendations(recommendations);
response.setSections(sections);
return response;
}
private void addStatusHistoryEntry(MarketingAnalysis analysis, String status, String message) {
if (analysis.getStatusHistory() == null) {
analysis.setStatusHistory(new ArrayList<>());