This commit is contained in:
root
2025-12-14 22:31:13 +05:00
parent d344fecc94
commit 71f51d58d2
@@ -276,15 +276,33 @@ public class MarketingAnalysisService {
return new HashMap<>();
});
@SuppressWarnings("unchecked")
CompletableFuture<Map<String, Object>> strategyFuture = (CompletableFuture<Map<String, Object>>) CompletableFuture
.<Map<String, Object>>supplyAsync(() -> {
logger.info("Starting strategy and funnel analysis generation");
Map<String, Object> analysis = getStrategyAndFunnel(request);
if (analysis != null && !analysis.isEmpty()) {
logger.info("Strategy and funnel analysis generated successfully");
} else {
logger.warn("Strategy and funnel analysis returned empty result");
}
return analysis != null ? analysis : new HashMap<>();
})
.exceptionally(ex -> {
logger.error("Error in strategy analysis future: {}", ex.getMessage(), ex);
return new HashMap<>();
});
// Ожидаем завершения всех futures
try {
CompletableFuture.allOf(audienceFuture, competitorFuture, seasonalityFuture)
CompletableFuture.allOf(audienceFuture, competitorFuture, seasonalityFuture, strategyFuture)
.get(5, TimeUnit.MINUTES); // Таймаут 5 минут
// Собираем результаты
Map<String, Object> audienceAnalysis = audienceFuture.join();
Map<String, Object> competitorAnalysis = competitorFuture.join();
Map<String, Object> seasonalityAndMarket = seasonalityFuture.join();
Map<String, Object> strategyAndFunnel = strategyFuture.join();
if (audienceAnalysis != null && !audienceAnalysis.isEmpty()) {
result.putAll(audienceAnalysis);
@@ -295,6 +313,9 @@ public class MarketingAnalysisService {
if (seasonalityAndMarket != null && !seasonalityAndMarket.isEmpty()) {
result.putAll(seasonalityAndMarket);
}
if (strategyAndFunnel != null && !strategyAndFunnel.isEmpty()) {
result.putAll(strategyAndFunnel);
}
} catch (Exception e) {
logger.error("Error waiting for analysis futures: {}", e.getMessage(), e);
@@ -303,6 +324,7 @@ public class MarketingAnalysisService {
Map<String, Object> audienceAnalysis = audienceFuture.getNow(new HashMap<>());
Map<String, Object> competitorAnalysis = competitorFuture.getNow(new HashMap<>());
Map<String, Object> seasonalityAndMarket = seasonalityFuture.getNow(new HashMap<>());
Map<String, Object> strategyAndFunnel = strategyFuture.getNow(new HashMap<>());
if (audienceAnalysis != null && !audienceAnalysis.isEmpty()) {
result.putAll(audienceAnalysis);
@@ -313,6 +335,9 @@ public class MarketingAnalysisService {
if (seasonalityAndMarket != null && !seasonalityAndMarket.isEmpty()) {
result.putAll(seasonalityAndMarket);
}
if (strategyAndFunnel != null && !strategyAndFunnel.isEmpty()) {
result.putAll(strategyAndFunnel);
}
} catch (Exception ex) {
logger.error("Error getting partial results: {}", ex.getMessage(), ex);
}
@@ -771,6 +796,184 @@ public class MarketingAnalysisService {
return new HashMap<>();
}
/**
* Генерация анализа стратегии, воронки, каналов и SWOT
* Модульный метод для 4-го этапа генерации
*/
private Map<String, Object> getStrategyAndFunnel(MarketingAnalysisRequest request) {
try {
String context = buildContextString(request);
StringBuilder promptBuilder = new StringBuilder();
promptBuilder.append(
"Ты — профессиональный маркетинговый аналитик и стратег, работающий с малым и средним бизнесом в Казахстане.\n\n");
promptBuilder.append(
"Твоя задача — на основе входных данных о бизнесе выполнить глубокий анализ стратегии, воронки конверсии, потенциала каналов продвижения и SWOT-анализа бизнеса.\n\n");
promptBuilder.append(
"КРИТИЧЕСКИ ВАЖНО: Предоставь детальный анализ с конкретными цифрами и инсайтами. "
+ "Воронка должна отражать реалистичные этапы конверсии для данного бизнеса. "
+ "Каналы должны быть оценены по их реальному потенциалу для данной ниши в Казахстане. "
+ "SWOT должен содержать конкретные сильные и слабые стороны, возможности и угрозы.\n\n");
promptBuilder.append(
"Выдай результат ТОЛЬКО в виде корректного JSON-объекта строго по указанной структуре.\n\n");
promptBuilder.append("Не добавляй пояснений, комментариев, текста до или после JSON.\n");
promptBuilder.append("Ответ должен быть валидным JSON.\n\n");
promptBuilder.append("---\n\n");
promptBuilder.append("ВХОДНЫЕ ДАННЫЕ ПОЛЬЗОВАТЕЛЯ:\n\n");
promptBuilder.append("- Название компании/продукта: ").append(request.getProduct()).append("\n");
promptBuilder.append("- Описание продукта/услуг: ").append(request.getProduct()).append(" (")
.append(request.getBusinessNiche()).append(")\n");
List<String> regions = request.getRegion();
String regionStr = (regions != null && !regions.isEmpty())
? String.join(", ", regions)
: "Не указан";
promptBuilder.append("- Регион/город: ").append(regionStr).append("\n");
promptBuilder.append("- Цель продвижения: ").append(request.getGoal()).append("\n\n");
promptBuilder.append("СФОРМИРУЙ JSON СЛЕДУЮЩЕЙ СТРУКТУРЫ:\n\n");
promptBuilder.append("{\n");
promptBuilder.append(" \"strategyAnalysis\": {\n");
promptBuilder.append(" \"funnel\": {\n");
promptBuilder.append(" \"stages\": [\n");
promptBuilder.append(" {\n");
promptBuilder.append(" \"stageName\": \"Охват\",\n");
promptBuilder.append(" \"value\": число, // Количество людей на этапе\n");
promptBuilder.append(
" \"conversionPercent\": 100 // Процент конверсии (для первого этапа всегда 100)\n");
promptBuilder.append(" },\n");
promptBuilder.append(" {\n");
promptBuilder.append(" \"stageName\": \"Интерес\",\n");
promptBuilder.append(" \"value\": число, // Количество людей на этапе\n");
promptBuilder.append(" \"conversionPercent\": число // Процент конверсии от предыдущего этапа\n");
promptBuilder.append(" },\n");
promptBuilder.append(" {\n");
promptBuilder.append(" \"stageName\": \"Заявка\",\n");
promptBuilder.append(" \"value\": число,\n");
promptBuilder.append(" \"conversionPercent\": число\n");
promptBuilder.append(" },\n");
promptBuilder.append(" {\n");
promptBuilder.append(" \"stageName\": \"Покупка\",\n");
promptBuilder.append(" \"value\": число,\n");
promptBuilder.append(" \"conversionPercent\": число\n");
promptBuilder.append(" }\n");
promptBuilder.append(" ],\n");
promptBuilder.append(
" \"analysis\": \"Детальный анализ воронки конверсии (3-4 предложения). Опиши, где находится узкое место воронки, какие этапы имеют низкую конверсию и почему. Дай конкретные рекомендации по улучшению каждого этапа.\"\n");
promptBuilder.append(" },\n");
promptBuilder.append(" \"channelsPotential\": [\n");
promptBuilder.append(" {\n");
promptBuilder.append(" \"name\": \"Instagram\",\n");
promptBuilder.append(" \"potentialIndex\": число, // Индекс потенциала от 0 до 100\n");
promptBuilder.append(" \"priority\": \"High\" // High, Medium или Low\n");
promptBuilder.append(" },\n");
promptBuilder.append(" {\n");
promptBuilder.append(" \"name\": \"Google Ads\",\n");
promptBuilder.append(" \"potentialIndex\": число,\n");
promptBuilder.append(" \"priority\": \"Medium\"\n");
promptBuilder.append(" },\n");
promptBuilder.append(" {\n");
promptBuilder.append(" \"name\": \"TikTok\",\n");
promptBuilder.append(" \"potentialIndex\": число,\n");
promptBuilder.append(" \"priority\": \"Medium\"\n");
promptBuilder.append(" }\n");
promptBuilder.append(" ],\n");
promptBuilder.append(" \"globalSwot\": {\n");
promptBuilder.append(
" \"strengths\": [\"Конкретная сильная сторона 1 (1-2 предложения)\", \"Конкретная сильная сторона 2 (1-2 предложения)\", \"Конкретная сильная сторона 3 (1-2 предложения)\"],\n");
promptBuilder.append(
" \"weaknesses\": [\"Конкретная слабая сторона 1 (1-2 предложения)\", \"Конкретная слабая сторона 2 (1-2 предложения)\", \"Конкретная слабая сторона 3 (1-2 предложения)\"],\n");
promptBuilder.append(
" \"opportunities\": [\"Конкретная возможность 1 (1-2 предложения)\", \"Конкретная возможность 2 (1-2 предложения)\", \"Конкретная возможность 3 (1-2 предложения)\"],\n");
promptBuilder.append(
" \"threats\": [\"Конкретная угроза 1 (1-2 предложения)\", \"Конкретная угроза 2 (1-2 предложения)\", \"Конкретная угроза 3 (1-2 предложения)\"]\n");
promptBuilder.append(" }\n");
promptBuilder.append(" }\n");
promptBuilder.append("}\n\n");
promptBuilder.append("КРИТИЧЕСКИ ВАЖНЫЕ ТРЕБОВАНИЯ:\n\n");
promptBuilder.append("1. Верни ТОЛЬКО JSON, без пояснений, комментариев, markdown-разметки.\n");
promptBuilder.append("2. JSON должен быть синтаксически корректным и валидным.\n");
promptBuilder.append("3. Воронка должна содержать минимум 4 этапа с реалистичными значениями.\n");
promptBuilder.append(
"4. Значения в funnel.stages должны быть логичными: каждый следующий этап должен иметь меньшее или равное значение value по сравнению с предыдущим.\n");
promptBuilder.append("5. conversionPercent должен отражать процент конверсии от предыдущего этапа.\n");
promptBuilder.append(
"6. channelsPotential должен содержать минимум 3-5 каналов с оценкой их потенциала для данного бизнеса.\n");
promptBuilder.append("7. potentialIndex должен быть числом от 0 до 100.\n");
promptBuilder.append("8. priority должен быть одним из: \"High\", \"Medium\", \"Low\".\n");
promptBuilder.append(
"9. globalSwot должен содержать конкретные пункты (по 3-4 в каждой категории), а не общие фразы.\n");
promptBuilder.append(
"10. Поле analysis в funnel должно содержать детальный анализ (3-4 предложения) с конкретными рекомендациями.\n");
promptBuilder.append("11. Все строки — на русском языке.\n");
String prompt = promptBuilder.toString();
logger.debug(
"getStrategyAndFunnel: Generating strategy and funnel analysis with detailed prompt (length: {} chars)",
prompt.length());
// Retry logic for JsonParseException
int attempt = 0;
while (attempt < MAX_RETRY_ATTEMPTS) {
try {
attempt++;
logger.info(
"getStrategyAndFunnel: Calling OpenAI API for strategy and funnel analysis (attempt {}/{})",
attempt, MAX_RETRY_ATTEMPTS);
String jsonResponse = openAIAnalyticsService.generateWithInstruction(context, prompt, "ru");
logger.debug("getStrategyAndFunnel: OpenAI response received. Length: {} chars",
jsonResponse != null ? jsonResponse.length() : 0);
if (jsonResponse == null || jsonResponse.trim().isEmpty()) {
logger.warn(
"getStrategyAndFunnel: Empty JSON response from AI for strategy and funnel analysis");
return new HashMap<>();
}
logger.debug("getStrategyAndFunnel: Cleaning JSON from markdown");
String cleanedJson = cleanJsonFromMarkdown(jsonResponse);
logger.debug("getStrategyAndFunnel: Parsing JSON response");
Map<String, Object> jsonMap = objectMapper.readValue(cleanedJson,
new TypeReference<Map<String, Object>>() {
});
logger.debug("getStrategyAndFunnel: JSON parsed successfully. Keys: {}", jsonMap.keySet());
logger.info("getStrategyAndFunnel: Strategy and funnel analysis generated successfully");
return jsonMap;
} catch (JsonParseException e) {
logger.warn("getStrategyAndFunnel: JsonParseException on attempt {}/{}: {}. Will retry...",
attempt, MAX_RETRY_ATTEMPTS, e.getMessage());
if (attempt < MAX_RETRY_ATTEMPTS) {
try {
Thread.sleep(RETRY_DELAY_MS);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
logger.error("getStrategyAndFunnel: Retry delay interrupted", ie);
return new HashMap<>();
}
} else {
logger.error("getStrategyAndFunnel: JsonParseException after {} attempts: {}",
MAX_RETRY_ATTEMPTS, e.getMessage(), e);
logger.error("getStrategyAndFunnel: Stack trace: ", e);
return new HashMap<>();
}
} catch (Exception e) {
logger.error("getStrategyAndFunnel: Error generating strategy and funnel analysis: {}",
e.getMessage(), e);
logger.error("getStrategyAndFunnel: Stack trace: ", e);
return new HashMap<>();
}
}
logger.error("getStrategyAndFunnel: Failed after {} attempts", MAX_RETRY_ATTEMPTS);
return new HashMap<>();
} catch (Exception e) {
logger.error("getStrategyAndFunnel: Error generating strategy and funnel analysis: {}", e.getMessage(), e);
logger.error("getStrategyAndFunnel: Stack trace: ", e);
return new HashMap<>();
}
}
/**
* Промпт №2: Генерация текстового отчёта на основе JSON
*/
@@ -1734,51 +1937,113 @@ public class MarketingAnalysisService {
chartsData.put("market", market);
}
// 5. Обратная совместимость: проверяем старую структуру для channels, funnel,
// 5. Стратегия, воронка, каналы и SWOT (новая структура: strategyAnalysis)
@SuppressWarnings("unchecked")
Map<String, Object> strategyAnalysis = (Map<String, Object>) analysisJson.get("strategyAnalysis");
if (strategyAnalysis != null) {
// 5.1. Воронка конверсии
@SuppressWarnings("unchecked")
Map<String, Object> funnel = (Map<String, Object>) strategyAnalysis.get("funnel");
if (funnel != null) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> stages = (List<Map<String, Object>>) funnel.get("stages");
if (stages != null && !stages.isEmpty()) {
List<Map<String, Object>> funnelList = new ArrayList<>();
for (Map<String, Object> stage : stages) {
Map<String, Object> stageData = new HashMap<>();
stageData.put("stage", stage.getOrDefault("stageName", "Этап"));
stageData.put("value", stage.getOrDefault("value", 0));
Object conversionObj = stage.get("conversionPercent");
if (conversionObj instanceof Number) {
stageData.put("conversion", ((Number) conversionObj).intValue());
} else {
stageData.put("conversion", 0);
}
funnelList.add(stageData);
}
chartsData.put("conversionFunnel", funnelList);
}
}
// 5.2. Потенциал каналов (channelsPotential)
@SuppressWarnings("unchecked")
List<Map<String, Object>> channelsPotential = (List<Map<String, Object>>) strategyAnalysis
.get("channelsPotential");
if (channelsPotential != null && !channelsPotential.isEmpty()) {
List<Map<String, Object>> channelsList = new ArrayList<>();
for (Map<String, Object> channel : channelsPotential) {
Map<String, Object> channelData = new HashMap<>();
channelData.put("name", channel.getOrDefault("name", "Неизвестный канал"));
channelData.put("potential", channel.getOrDefault("potentialIndex", 50));
channelData.put("priority", channel.getOrDefault("priority", "Medium"));
channelsList.add(channelData);
}
chartsData.put("channelsPotential", channelsList);
}
// 5.3. SWOT-анализ (globalSwot)
@SuppressWarnings("unchecked")
Map<String, Object> globalSwot = (Map<String, Object>) strategyAnalysis.get("globalSwot");
if (globalSwot != null) {
chartsData.put("swot", globalSwot);
}
}
// 6. Обратная совместимость: проверяем старую структуру для channels, funnel,
// swot
// (на случай, если где-то еще используется старая структура)
@SuppressWarnings("unchecked")
List<Map<String, Object>> channels = (List<Map<String, Object>>) analysisJson.get("channels");
if (channels != null && !channels.isEmpty()) {
List<Map<String, Object>> channelsList = new ArrayList<>();
for (Map<String, Object> channel : channels) {
Map<String, Object> channelData = new HashMap<>();
channelData.put("name", channel.getOrDefault("name", "Неизвестный канал"));
channelData.put("potential", channel.getOrDefault("potentialIndex", 50));
channelData.put("justification", channel.getOrDefault("justification", "Подходит для данной ниши"));
channelsList.add(channelData);
}
chartsData.put("channelsPotential", channelsList);
}
@SuppressWarnings("unchecked")
Map<String, Object> funnel = (Map<String, Object>) analysisJson.get("funnel");
if (funnel != null) {
// Примечание: новые данные из strategyAnalysis имеют приоритет - не
// перезаписываем, если уже есть
if (!chartsData.containsKey("channelsPotential")) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> stages = (List<Map<String, Object>>) funnel.get("stages");
if (stages != null && !stages.isEmpty()) {
List<Map<String, Object>> funnelList = new ArrayList<>();
for (Map<String, Object> stage : stages) {
Map<String, Object> stageData = new HashMap<>();
stageData.put("stage", stage.getOrDefault("stageName", "Этап"));
stageData.put("value", stage.getOrDefault("value", 0));
Object conversionObj = stage.get("conversionPercent");
if (conversionObj instanceof Number) {
stageData.put("conversion", ((Number) conversionObj).intValue());
} else {
stageData.put("conversion", 0);
}
funnelList.add(stageData);
List<Map<String, Object>> channels = (List<Map<String, Object>>) analysisJson.get("channels");
if (channels != null && !channels.isEmpty()) {
List<Map<String, Object>> channelsList = new ArrayList<>();
for (Map<String, Object> channel : channels) {
Map<String, Object> channelData = new HashMap<>();
channelData.put("name", channel.getOrDefault("name", "Неизвестный канал"));
channelData.put("potential", channel.getOrDefault("potentialIndex", 50));
channelData.put("justification",
channel.getOrDefault("justification", "Подходит для данной ниши"));
channelsList.add(channelData);
}
chartsData.put("conversionFunnel", funnelList);
chartsData.put("channelsPotential", channelsList);
}
}
@SuppressWarnings("unchecked")
Map<String, Object> swot = (Map<String, Object>) analysisJson.get("swot");
if (swot != null) {
chartsData.put("swot", swot);
if (!chartsData.containsKey("conversionFunnel")) {
@SuppressWarnings("unchecked")
Map<String, Object> funnel = (Map<String, Object>) analysisJson.get("funnel");
if (funnel != null) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> stages = (List<Map<String, Object>>) funnel.get("stages");
if (stages != null && !stages.isEmpty()) {
List<Map<String, Object>> funnelList = new ArrayList<>();
for (Map<String, Object> stage : stages) {
Map<String, Object> stageData = new HashMap<>();
stageData.put("stage", stage.getOrDefault("stageName", "Этап"));
stageData.put("value", stage.getOrDefault("value", 0));
Object conversionObj = stage.get("conversionPercent");
if (conversionObj instanceof Number) {
stageData.put("conversion", ((Number) conversionObj).intValue());
} else {
stageData.put("conversion", 0);
}
funnelList.add(stageData);
}
chartsData.put("conversionFunnel", funnelList);
}
}
}
if (!chartsData.containsKey("swot")) {
@SuppressWarnings("unchecked")
Map<String, Object> swot = (Map<String, Object>) analysisJson.get("swot");
if (swot != null) {
chartsData.put("swot", swot);
}
}
} catch (Exception e) {