.
This commit is contained in:
@@ -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("Женщины, 25–34 года", "29%"),
|
||||
new MarketingAnalysisResponseV2.AudienceSegment("Мужчины, 25–44 года", "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<>());
|
||||
|
||||
Reference in New Issue
Block a user