This commit is contained in:
root
2025-12-18 23:28:41 +05:00
parent 332cd7c7f2
commit dbfd441c28
@@ -1429,17 +1429,28 @@ public class MarketingAnalysisService {
logger.debug("generatePolishedTextReport: JSON string length: {}", jsonString.length());
StringBuilder promptBuilder = new StringBuilder();
promptBuilder.append("Ты — опытный маркетинговый аналитик и бизнес-консультант.\n\n");
promptBuilder.append(
"Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик с многолетним опытом.\n\n");
promptBuilder.append("На основе следующего JSON с маркетинговым анализом напиши ПРОФЕССИОНАЛЬНЫЙ, ");
promptBuilder.append("ГЛУБОКИЙ маркетинговый отчет на русском языке.\n\n");
promptBuilder.append("СТИЛЬ ПИСЬМА:\n");
promptBuilder
.append("Пиши как высокооплачиваемый бизнес-консультант. Избегай воды, используй цифры из JSON, ");
promptBuilder.append(
"делай глубокие сравнения нашего бренда с конкурентами (используй данные из competitorAnalysis и comparisonTable). ");
promptBuilder.append("Каждое утверждение должно быть подкреплено конкретными данными из JSON.\n\n");
promptBuilder.append("ТРЕБОВАНИЯ К ОТЧЕТУ:\n");
promptBuilder.append("1. Используй деловой стиль письма, убедительный для владельца бизнеса\n");
promptBuilder.append("2. Добавляй аналитические инсайты и практические выводы\n");
promptBuilder.append("3. Структурируй текст по разделам с римскими цифрами (I, II, III и т.д.)\n");
promptBuilder.append("4. Каждый раздел должен быть содержательным (минимум 2-3 абзаца)\n");
promptBuilder.append(
"4. КРИТИЧНО: Каждый раздел (I-XI) должен содержать минимум 3-4 детальных абзаца с глубокой аналитикой\n");
promptBuilder.append("5. Используй данные ТОЛЬКО из переданного JSON, не выдумывай цифры\n");
promptBuilder.append("6. Пиши конкретно и практично, избегай общих фраз\n\n");
promptBuilder.append("6. Пиши конкретно и практично, избегай общих фраз\n");
promptBuilder.append(
"7. Обязательно используй данные из полей product, businessNiche, goal, regions для контекста анализа\n\n");
promptBuilder.append("ОБЯЗАТЕЛЬНАЯ СТРУКТУРА ОТЧЕТА:\n");
promptBuilder.append("I. Краткое резюме (общий обзор бизнеса и ключевые выводы)\n");
@@ -1460,6 +1471,13 @@ public class MarketingAnalysisService {
promptBuilder.append("X. Рекомендации по контенту (типы контента для разных целей)\n");
promptBuilder.append("XI. Итоговая стратегия: что делать в первую очередь (конкретный план действий)\n\n");
promptBuilder.append("КРИТИЧНО - ПЛЕЙСХОЛДЕРЫ ГРАФИКОВ:\n");
promptBuilder.append(
"Обязательно расставь ВСЕ плейсхолдеры графиков: [[CHART_SEASONALITY]], [[CHART_AUDIENCE]], ");
promptBuilder.append("[[CHART_COMPETITORS]], [[CHART_SWOT]], [[CHART_CHANNELS]], [[CHART_FUNNEL]]. ");
promptBuilder.append("Каждый плейсхолдер должен быть на отдельной строке в соответствующем разделе, ");
promptBuilder.append("как указано в структуре выше. НЕ пропускай ни одного плейсхолдера!\n\n");
promptBuilder.append("ВАЖНО:\n");
promptBuilder.append("- Плейсхолдеры [[CHART_...]] вставляй ТОЧНО как указано выше, на отдельной строке\n");
promptBuilder.append("- Начинай сразу с 'I. Краткое резюме', без вступлений\n");
@@ -2432,22 +2450,9 @@ public class MarketingAnalysisService {
report.put("analysisTypes", analysisTypeNames);
report.put("detailLevel", detailLevelName);
report.put("fullAnalysis", "Не удалось сгенерировать анализ. Пожалуйста, попробуйте позже.");
report.put("summary", "Не удалось сгенерировать анализ. Пожалуйста, попробуйте позже.");
report.put("summary", "Маркетинговый анализ для " + request.getProduct());
report.put("chartsData", new HashMap<>());
// Добавляем дефолтные значения для обратной совместимости
Map<String, Object> defaultTargetAudience = new HashMap<>();
defaultTargetAudience.put("description", "Не удалось сгенерировать описание целевой аудитории.");
defaultTargetAudience.put("channels", Arrays.asList("Instagram", "LinkedIn", "Telegram"));
report.put("targetAudience", defaultTargetAudience);
report.put("recommendations", Arrays.asList("Не удалось сгенерировать рекомендации."));
Map<String, Object> defaultStrategy = new HashMap<>();
defaultStrategy.put("duration", "1 месяц");
defaultStrategy.put("channels", Arrays.asList("Instagram", "Telegram", "LinkedIn"));
defaultStrategy.put("contentTypes", Arrays.asList("посты", "сторис", "видео"));
report.put("strategy", defaultStrategy);
logger.warn("generateMarketingReport: Returning report with error message");
} else {
logger.info("generateMarketingReport: JSON analysis generated successfully. Keys: {}",
@@ -2507,27 +2512,12 @@ public class MarketingAnalysisService {
report.put("fullAnalysis",
processedTextReport != null ? processedTextReport : "Анализ не удалось сгенерировать.");
// Извлекаем только первую секцию (I. Краткое резюме) для поля summary
// чтобы избежать дублирования полного текста отчета
String shortSummary = extractSummarySection(processedTextReport);
report.put("summary", shortSummary != null ? shortSummary : "Анализ не удалось сгенерировать.");
// Извлекаем данные из analysisJson вместо повторных вызовов AI
logger.info("generateMarketingReport: Extracting targetAudience from analysisJson");
Map<String, Object> targetAudience = extractTargetAudienceFromJson(analysisJson);
report.put("targetAudience", targetAudience);
logger.info("generateMarketingReport: TargetAudience extracted. Channels count: {}",
((List<?>) targetAudience.get("channels")).size());
logger.info("generateMarketingReport: Extracting recommendations from analysisJson");
List<String> recommendations = extractRecommendationsFromJson(analysisJson);
report.put("recommendations", recommendations);
logger.info("generateMarketingReport: Recommendations extracted. Count: {}", recommendations.size());
logger.info("generateMarketingReport: Extracting strategy from analysisJson");
Map<String, Object> strategy = extractStrategyFromJson(analysisJson);
report.put("strategy", strategy);
logger.info("generateMarketingReport: Strategy extracted. Channels: {}", strategy.get("channels"));
// Summary содержит только короткую вводную фразу
String summary = "Маркетинговый анализ для " + request.getProduct();
if (request.getBusinessNiche() != null && !request.getBusinessNiche().trim().isEmpty()) {
summary += " (" + request.getBusinessNiche() + ")";
}
report.put("summary", summary);
}
logger.info("generateMarketingReport: Report generation completed. Report keys: {}", report.keySet());
@@ -2682,322 +2672,6 @@ public class MarketingAnalysisService {
return strategy;
}
/**
* Извлекает описание целевой аудитории и каналы из JSON-анализа.
* Заменяет повторный вызов AI для генерации описания аудитории.
*
* @param analysisJson JSON с данными маркетингового анализа
* @return Map с description и channels
*/
@SuppressWarnings("unchecked")
private Map<String, Object> extractTargetAudienceFromJson(Map<String, Object> analysisJson) {
Map<String, Object> targetAudience = new HashMap<>();
StringBuilder descriptionBuilder = new StringBuilder();
List<String> channels = new ArrayList<>();
try {
// Извлекаем данные из audienceAnalysis
Map<String, Object> audienceAnalysis = (Map<String, Object>) analysisJson.get("audienceAnalysis");
if (audienceAnalysis != null) {
// Получаем ключевые выводы
List<String> keyTakeaways = (List<String>) audienceAnalysis.get("keyTakeaways");
if (keyTakeaways != null && !keyTakeaways.isEmpty()) {
descriptionBuilder.append("Ключевые характеристики целевой аудитории:\n");
for (String takeaway : keyTakeaways) {
descriptionBuilder.append("").append(takeaway).append("\n");
}
descriptionBuilder.append("\n");
}
// Получаем информацию о сегментах
List<Map<String, Object>> segments = (List<Map<String, Object>>) audienceAnalysis.get("segments");
if (segments != null && !segments.isEmpty()) {
descriptionBuilder.append("Основные сегменты аудитории:\n");
for (Map<String, Object> segment : segments) {
Object name = segment.get("name");
Object sharePercent = segment.get("sharePercent");
Object ageRange = segment.get("ageRange");
Object motivation = segment.get("motivation");
if (name != null) {
descriptionBuilder.append("\n• ").append(name);
if (sharePercent != null) {
descriptionBuilder.append(" (").append(sharePercent).append("%)");
}
descriptionBuilder.append("\n");
}
if (ageRange != null) {
descriptionBuilder.append(" Возраст: ").append(ageRange).append("\n");
}
if (motivation != null) {
descriptionBuilder.append(" Мотивация: ").append(motivation).append("\n");
}
// Собираем каналы из сегментов
List<String> primaryChannels = (List<String>) segment.get("primaryChannels");
if (primaryChannels != null) {
for (String channel : primaryChannels) {
if (!channels.contains(channel)) {
channels.add(channel);
}
}
}
}
}
// Комментарии по гендеру и возрасту
Object genderComment = audienceAnalysis.get("genderComment");
Object ageComment = audienceAnalysis.get("ageComment");
if (genderComment != null || ageComment != null) {
descriptionBuilder.append("\n");
if (ageComment != null) {
descriptionBuilder.append(ageComment).append("\n");
}
if (genderComment != null) {
descriptionBuilder.append(genderComment).append("\n");
}
}
}
// Извлекаем каналы из channels.recommended
Map<String, Object> channelsData = (Map<String, Object>) analysisJson.get("channels");
if (channelsData != null) {
List<Map<String, Object>> recommended = (List<Map<String, Object>>) channelsData.get("recommended");
if (recommended != null) {
for (Map<String, Object> channel : recommended) {
Object channelName = channel.get("name");
if (channelName != null && !channels.contains(channelName.toString())) {
channels.add(channelName.toString());
}
}
}
}
} catch (Exception e) {
logger.warn("extractTargetAudienceFromJson: Error extracting data: {}", e.getMessage());
}
// Если описание пустое, добавляем дефолтное
if (descriptionBuilder.length() == 0) {
descriptionBuilder.append("Описание целевой аудитории доступно в полном отчете.");
}
// Если каналы пустые, добавляем дефолтные
if (channels.isEmpty()) {
channels = Arrays.asList("Instagram", "LinkedIn", "Telegram");
}
targetAudience.put("description", descriptionBuilder.toString());
targetAudience.put("channels", channels);
return targetAudience;
}
/**
* Извлекает рекомендации из JSON-анализа.
* Заменяет повторный вызов AI для генерации рекомендаций.
*
* @param analysisJson JSON с данными маркетингового анализа
* @return Список рекомендаций
*/
@SuppressWarnings("unchecked")
private List<String> extractRecommendationsFromJson(Map<String, Object> analysisJson) {
List<String> recommendations = new ArrayList<>();
try {
// Извлекаем из contentRecommendations
Map<String, Object> contentRecommendations = (Map<String, Object>) analysisJson
.get("contentRecommendations");
if (contentRecommendations != null) {
// Собираем рекомендации из всех типов контента
List<String> salesContent = (List<String>) contentRecommendations.get("sales");
List<String> expertContent = (List<String>) contentRecommendations.get("expertise");
List<String> trustContent = (List<String>) contentRecommendations.get("trust");
if (salesContent != null && !salesContent.isEmpty()) {
recommendations.add("Продающий контент: " + String.join(", ", salesContent));
}
if (expertContent != null && !expertContent.isEmpty()) {
recommendations.add("Экспертный контент: " + String.join(", ", expertContent));
}
if (trustContent != null && !trustContent.isEmpty()) {
recommendations.add("Контент для доверия: " + String.join(", ", trustContent));
}
}
// Извлекаем из valueProposition
Map<String, Object> valueProposition = (Map<String, Object>) analysisJson.get("valueProposition");
if (valueProposition != null) {
List<String> mainBenefits = (List<String>) valueProposition.get("mainBenefits");
if (mainBenefits != null && !mainBenefits.isEmpty()) {
recommendations.add("Основные преимущества для продвижения: " + String.join(", ", mainBenefits));
}
}
// Извлекаем из SWOT - opportunities
Map<String, Object> swot = (Map<String, Object>) analysisJson.get("swot");
if (swot != null) {
List<String> opportunities = (List<String>) swot.get("opportunities");
if (opportunities != null && !opportunities.isEmpty()) {
for (int i = 0; i < Math.min(3, opportunities.size()); i++) {
recommendations.add("Использовать возможность: " + opportunities.get(i));
}
}
}
// Извлекаем из positioning
Map<String, Object> positioning = (Map<String, Object>) analysisJson.get("positioning");
if (positioning != null) {
Object keyMessage = positioning.get("keyMessage");
if (keyMessage != null) {
recommendations.add("Ключевое сообщение: " + keyMessage);
}
}
} catch (Exception e) {
logger.warn("extractRecommendationsFromJson: Error extracting data: {}", e.getMessage());
}
// Если рекомендации пустые, добавляем дефолтные
if (recommendations.isEmpty()) {
recommendations = Arrays.asList(
"Рекомендации доступны в полном маркетинговом отчете",
"Изучите раздел с SWOT-анализом для понимания возможностей",
"Обратите внимание на рекомендуемые каналы продвижения");
}
return recommendations;
}
/**
* Извлекает стратегию из JSON-анализа.
* Заменяет повторный вызов AI для генерации стратегии.
*
* @param analysisJson JSON с данными маркетингового анализа
* @return Map со стратегией (duration, channels, contentTypes)
*/
@SuppressWarnings("unchecked")
private Map<String, Object> extractStrategyFromJson(Map<String, Object> analysisJson) {
Map<String, Object> strategy = new HashMap<>();
List<String> channels = new ArrayList<>();
List<String> contentTypes = new ArrayList<>();
String duration = "1 месяц"; // default
try {
// Извлекаем каналы из channels.recommended
Map<String, Object> channelsData = (Map<String, Object>) analysisJson.get("channels");
if (channelsData != null) {
List<Map<String, Object>> recommended = (List<Map<String, Object>>) channelsData.get("recommended");
if (recommended != null) {
for (Map<String, Object> channel : recommended) {
Object channelName = channel.get("name");
if (channelName != null) {
channels.add(channelName.toString());
}
}
}
}
// Извлекаем типы контента из contentRecommendations
Map<String, Object> contentRecommendations = (Map<String, Object>) analysisJson
.get("contentRecommendations");
if (contentRecommendations != null) {
if (contentRecommendations.containsKey("sales")) {
contentTypes.add("продающий контент");
}
if (contentRecommendations.containsKey("expertise")) {
contentTypes.add("экспертный контент");
}
if (contentRecommendations.containsKey("trust")) {
contentTypes.add("доверительный контент");
}
if (contentRecommendations.containsKey("entertainment")) {
contentTypes.add("развлекательный контент");
}
}
// Извлекаем информацию о воронке для определения длительности
Map<String, Object> funnel = (Map<String, Object>) analysisJson.get("funnel");
if (funnel != null) {
List<Map<String, Object>> stages = (List<Map<String, Object>>) funnel.get("stages");
if (stages != null && stages.size() > 3) {
// Если воронка сложная (более 3 этапов), рекомендуем более длительную кампанию
duration = "2-3 месяца";
}
}
} catch (Exception e) {
logger.warn("extractStrategyFromJson: Error extracting data: {}", e.getMessage());
}
// Дефолтные значения если данные не найдены
if (channels.isEmpty()) {
channels = Arrays.asList("Instagram", "Telegram", "LinkedIn");
}
if (contentTypes.isEmpty()) {
contentTypes = Arrays.asList("посты", "сторис", "видео");
}
strategy.put("duration", duration);
strategy.put("channels", channels);
strategy.put("contentTypes", contentTypes);
return strategy;
}
/**
* Извлекает только первую секцию "I. Краткое резюме" из полного текстового
* отчета.
* Используется для поля summary, чтобы избежать дублирования полного текста.
*
* @param fullReport Полный текстовый отчет
* @return Текст только первой секции (краткое резюме)
*/
private String extractSummarySection(String fullReport) {
if (fullReport == null || fullReport.trim().isEmpty()) {
return null;
}
try {
// Ищем начало первой секции "I. Краткое резюме"
int startIndex = fullReport.indexOf("I. Краткое резюме");
if (startIndex == -1) {
// Если не нашли, пробуем найти просто "Краткое резюме"
startIndex = fullReport.indexOf("Краткое резюме");
}
if (startIndex == -1) {
// Если нет секции резюме, возвращаем первые 500 символов
return fullReport.length() > 500
? fullReport.substring(0, 500) + "..."
: fullReport;
}
// Ищем начало второй секции "II."
int endIndex = fullReport.indexOf("II.", startIndex + 1);
if (endIndex == -1) {
// Пробуем найти "II. " с пробелом
endIndex = fullReport.indexOf("\nII. ", startIndex + 1);
}
if (endIndex == -1) {
// Если не нашли вторую секцию, возвращаем от начала до 1000 символов
int maxLength = Math.min(startIndex + 1000, fullReport.length());
return fullReport.substring(startIndex, maxLength).trim();
}
// Извлекаем текст между I. и II.
return fullReport.substring(startIndex, endIndex).trim();
} catch (Exception e) {
logger.warn("extractSummarySection: Error extracting summary: {}", e.getMessage());
// В случае ошибки возвращаем первые 500 символов
return fullReport.length() > 500
? fullReport.substring(0, 500) + "..."
: fullReport;
}
}
private String buildMarkdownFromReport(Map<String, Object> reportData, MarketingAnalysisRequest request) {
logger.info("buildMarkdownFromReport: Starting markdown generation for product: {}", request.getProduct());
logger.debug("buildMarkdownFromReport: ReportData keys: {}", reportData != null ? reportData.keySet() : "null");
@@ -3008,34 +2682,6 @@ public class MarketingAnalysisService {
markdown.append("- **Ниша бизнеса:** ").append(request.getBusinessNiche()).append("\n");
markdown.append("- **Продукт/услуга:** ").append(request.getProduct()).append("\n");
// Format target audience
Map<String, Object> targetAudienceData = request.getTargetAudience();
if (targetAudienceData != null) {
StringBuilder audienceBuilder = new StringBuilder();
if (targetAudienceData.containsKey("genders")) {
@SuppressWarnings("unchecked")
List<String> genders = (List<String>) targetAudienceData.get("genders");
if (genders != null && !genders.isEmpty()) {
audienceBuilder.append("Гендер: ").append(String.join(", ", genders)).append(". ");
}
}
if (targetAudienceData.containsKey("ageRanges")) {
@SuppressWarnings("unchecked")
List<String> ageRanges = (List<String>) targetAudienceData.get("ageRanges");
if (ageRanges != null && !ageRanges.isEmpty()) {
audienceBuilder.append("Возраст: ").append(String.join(", ", ageRanges)).append(". ");
}
}
if (targetAudienceData.containsKey("types")) {
@SuppressWarnings("unchecked")
List<String> types = (List<String>) targetAudienceData.get("types");
if (types != null && !types.isEmpty()) {
audienceBuilder.append("Тип: ").append(String.join(", ", types)).append(". ");
}
}
markdown.append("- **Целевая аудитория:** ").append(audienceBuilder.toString().trim()).append("\n");
}
// Format regions
List<String> regions = request.getRegion();
if (regions != null && !regions.isEmpty()) {
@@ -3063,11 +2709,6 @@ public class MarketingAnalysisService {
}
markdown.append("\n");
// ВАЖНО: Секции "Позиционирование", "УТП", "Рекомендации по контенту" УДАЛЕНЫ,
// так как они уже включены в fullAnalysis (генерируется методом
// generatePolishedTextReport)
// и вызывали дублирование контента в отчете
// Display full analysis - это основной блок с полным отчетом
// Все секции (I-XI) уже включены в fullAnalysis благодаря
// generatePolishedTextReport
@@ -3082,11 +2723,8 @@ public class MarketingAnalysisService {
// так как fullAnalysis уже начинается с "I. Краткое резюме"
markdown.append(processedAnalysis).append("\n\n");
} else {
// Fallback to summary if fullAnalysis is not available
String summary = (String) reportData.get("summary");
if (summary != null && !summary.trim().isEmpty()) {
markdown.append(summary).append("\n\n");
}
// Если fullAnalysis пустой, выводим сообщение об ошибке
markdown.append("Не удалось сгенерировать анализ. Пожалуйста, попробуйте позже.\n\n");
}
return markdown.toString();
@@ -3145,14 +2783,17 @@ public class MarketingAnalysisService {
if (chartsData == null || chartsData.isEmpty()) {
logger.warn("replaceChartPlaceholders: chartsData is null or empty, removing all placeholders");
// Если данных нет, просто удаляем все плейсхолдеры
return fullAnalysis
// Если данных нет, удаляем все плейсхолдеры и очищаем пустые строки
String cleaned = fullAnalysis
.replace("[[CHART_SEASONALITY]]", "")
.replace("[[CHART_AUDIENCE]]", "")
.replace("[[CHART_COMPETITORS]]", "")
.replace("[[CHART_CHANNELS]]", "")
.replace("[[CHART_FUNNEL]]", "")
.replace("[[CHART_SWOT]]", "");
// Удаляем множественные пустые строки (более 2 подряд)
cleaned = cleaned.replaceAll("(\\r?\\n){3,}", "\n\n");
return cleaned;
}
String result = fullAnalysis;
@@ -3190,19 +2831,26 @@ public class MarketingAnalysisService {
} catch (JsonProcessingException e) {
logger.error("Error serializing data for placeholder {} (key: {}): {}",
placeholder, mapping.dataKey, e.getMessage(), e);
// Удаляем плейсхолдер при ошибке сериализации
result = result.replaceAll(regex, "");
// Удаляем плейсхолдер при ошибке сериализации вместе с окружающими переносами
// строк
String placeholderWithNewlines = "(\\r?\\n)?\\s*" + regex + "\\s*(\\r?\\n)?";
result = result.replaceAll(placeholderWithNewlines, "\n");
} catch (Exception e) {
logger.error("Unexpected error processing placeholder {} (key: {}): {}",
placeholder, mapping.dataKey, e.getMessage(), e);
// Удаляем плейсхолдер при неожиданной ошибке
result = result.replaceAll(regex, "");
// Удаляем плейсхолдер при неожиданной ошибке вместе с окружающими переносами
// строк
String placeholderWithNewlines = "(\\r?\\n)?\\s*" + regex + "\\s*(\\r?\\n)?";
result = result.replaceAll(placeholderWithNewlines, "\n");
}
} else {
logger.warn("MISSING DATA for placeholder: {} (key: {}). Removing placeholder.",
placeholder, mapping.dataKey);
// Если данных нет, удаляем плейсхолдер, чтобы не светить им пользователю
result = result.replaceAll(regex, "");
// Если данных нет, удаляем плейсхолдер и окружающие пустые строки
// Удаляем плейсхолдер вместе с предшествующим и последующим переносом строки,
// если они есть
String placeholderWithNewlines = "(\\r?\\n)?\\s*" + regex + "\\s*(\\r?\\n)?";
result = result.replaceAll(placeholderWithNewlines, "\n");
}
}
}
@@ -3212,6 +2860,9 @@ public class MarketingAnalysisService {
logger.debug("replaceChartPlaceholders: Adding missing charts to sections");
result = addMissingChartsToSections(result, chartsData, placeholderMap, processedPlaceholders);
// Финальная очистка: удаляем множественные пустые строки (более 2 подряд)
result = result.replaceAll("(\\r?\\n){3,}", "\n\n");
logger.info(
"replaceChartPlaceholders: Placeholder replacement completed. Processed placeholders: {}, Result length: {} chars",
processedPlaceholders.size(), result.length());