.
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user