This commit is contained in:
arys
2026-02-22 09:13:15 +05:00
parent 6f626e2c6f
commit 2d88269196
@@ -75,12 +75,23 @@ public class MarketingAnalysisV3Service {
}
}
private String sanitizeInput(String input) {
if (input == null || input.isBlank()) {
return "";
}
return input.trim()
.replaceAll("\\s+", " ")
.replaceAll("(?i)залл\\b", "зал")
.replaceAll("(?i)салонн\\b", "салон")
.replaceAll("(?i)бизнес\\b", "бизнес");
}
private Map<String, Object> executeDeepResearch(MarketingAnalysisV3Request request) {
Map<String, Object> pack = new ConcurrentHashMap<>();
List<String> queries = new ArrayList<>();
String niche = request.getBusinessNiche() != null ? request.getBusinessNiche() : "";
String product = request.getProductName() != null ? request.getProductName() : "";
String niche = sanitizeInput(request.getBusinessNiche());
String product = sanitizeInput(request.getProductName());
List<String> cities = request.getPromotionCities() != null && !request.getPromotionCities().isEmpty()
? request.getPromotionCities()
@@ -90,7 +101,9 @@ public class MarketingAnalysisV3Service {
String geoContext = String.join(" ", cities);
String topic = (niche + " " + product).trim();
if (topic.isEmpty()) topic = request.getProductDescription();
if (topic.isEmpty()) {
topic = sanitizeInput(request.getProductDescription());
}
String kzSites = "(site:stat.gov.kz OR site:kapital.kz OR site:kursiv.media OR site:forbes.kz OR site:ranking.kz)";
String retailSites = "(site:2gis.kz OR site:kaspi.kz OR site:kolesa.kz OR site:krisha.kz OR site:chocofood.kz OR site:instagram.com)";
@@ -102,10 +115,7 @@ public class MarketingAnalysisV3Service {
queries.add(String.format("кейс продвижение SMM %s казахстан", niche));
queries.parallelStream().forEach(q -> {
// КРИТИЧЕСКИ ВАЖНО: MongoDB не поддерживает точки (.) в названиях ключей!
// Поэтому заменяем точки на нижнее подчеркивание перед сохранением в Map.
String safeMongoKey = q.replace(".", "_").replace("$", "");
try {
log.debug("Executing search query: {}", q);
pack.put(safeMongoKey, searchService.search(q));
@@ -139,7 +149,6 @@ public class MarketingAnalysisV3Service {
String cleaned = cleanJson(response);
if (cleaned != null && cleaned.startsWith("{") && cleaned.endsWith("}")) {
// Проверка валидности JSON
objectMapper.readTree(cleaned);
return cleaned;
} else {
@@ -160,7 +169,6 @@ public class MarketingAnalysisV3Service {
}
private MarketingAnalysisV3Result parseAndValidateResult(String json) throws Exception {
// Создаем безопасную копию маппера, чтобы он не падал, если ИИ придумает новые поля
ObjectMapper safeMapper = this.objectMapper.copy();
safeMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return safeMapper.readValue(json, MarketingAnalysisV3Result.class);
@@ -196,7 +204,7 @@ public class MarketingAnalysisV3Service {
});
} catch (Exception e) {
log.error("[Analysis ID: {}] CRITICAL: Failed to save research metadata to MongoDB. Check for invalid characters in keys. Error: {}", id, e.getMessage());
throw e; // Пробрасываем дальше, чтобы уйти в статус FAILED
throw e;
}
}
@@ -233,10 +241,11 @@ public class MarketingAnalysisV3Service {
КРИТИЧЕСКИЕ ПРАВИЛА (ANTI-HALLUCINATION PROTOCOL):
1. ТОЛЬКО РЕАЛЬНЫЕ ДАННЫЕ: Вся аналитика строится на переданном SEARCH EVIDENCE.
2. РЕАЛЬНЫЕ КОНКУРЕНТЫ: В блоке `3_competitor_map` ОБЯЗАНО быть от 3 до 5 реально существующих компаний, найденных в SEARCH EVIDENCE (например "Invictus", "Sulpak", "Korean House"). КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНО писать "Конкурент 1", "Компания А", "Пример".
3. ОЦИФРОВКА: В Казахстане все измеряется в KZT. Приложение Kaspi.kz и 2GIS — основа рынка. Учитывай это в анализе.
4. СВЯЗЬ СО СТРАТЕГИЕЙ: Твой анализ — это фундамент для будущего SMM-Scoring Model (Entry/Authority/Trust/Conversion). Анализируй данные так, чтобы выявить доминирующий барьер аудитории.
5. НИКАКИХ NULL: Заполни абсолютно все поля JSON. Если точной цифры нет, примени метод Ферми и сделай экстраполяцию, характерную для рынка РК.
2. РЕАЛЬНЫЕ КОНКУРЕНТЫ: В блоке `3_competitor_map` ОБЯЗАНО быть строго от 5 до 10 РЕАЛЬНЫХ компаний из предоставленных данных. Ровно 3 — ЗАПРЕЩЕНО. Использовать плейсхолдеры — ЗАПРЕЩЕНО.
3. ПОЛНАЯ РУСИФИКАЦИЯ: Все значения статусов, уровней и трендов (businessStage, competitionLevel, demandTrend, intensityLabel, status) СТРОГО на русском языке. Запрещено копировать английские слова (JUST_STARTING, HIGH, STABLE, NONE) из DTO. Переводи их по смыслу (Например: 'Запуск', 'Высокая', 'Стабильный', 'Отсутствует').
4. ОЦИФРОВКА: В Казахстане все измеряется в KZT. Приложение Kaspi.kz и 2GIS — основа рынка. Учитывай это в анализе.
5. СВЯЗЬ СО СТРАТЕГИЕЙ: Твой анализ — это фундамент для будущего SMM-Scoring Model.
6. НИКАКИХ NULL: Заполни абсолютно все поля JSON. Если точной цифры нет, примени метод Ферми и сделай экстраполяцию, характерную для рынка РК.
""";
}
@@ -246,6 +255,13 @@ public class MarketingAnalysisV3Service {
String evidenceJson = objectMapper.writeValueAsString(researchPack);
String schemaTemplate = getJsonStructureTemplate();
List<String> targetCitiesList = request.getPromotionCities() != null && !request.getPromotionCities().isEmpty()
? request.getPromotionCities()
: (request.getPresenceCities() != null && !request.getPresenceCities().isEmpty()
? request.getPresenceCities()
: List.of("Казахстан"));
String targetCities = String.join(", ", targetCitiesList);
return """
СФОРМИРУЙ ОТЧЕТ "MARKETING ANALYSIS V4.0" ДЛЯ РЫНКА КАЗАХСТАНА.
@@ -258,19 +274,19 @@ public class MarketingAnalysisV3Service {
ИНСТРУКЦИИ ПО СЕКЦИЯМ (СТРОГО):
[0_executive_summary]
- activeCompetitors: Реальное количество найденных в поиске игроков.
- activeCompetitors: Реальное количество найденных в поиске игроков (минимум 5).
- keyFigures: 5-7 мощных метрик. Обязательно укажи оценку объема рынка/спроса.
[1_market_landscape]
- demandDynamics: 12 объектов. Поле "period" СТРОГО на русском (Январь, Февраль...). "value" отражает сезонность.
[1_market_landscape] и [2_geo_structure]
- ОБЯЗАТЕЛЬНО выведи метрики в этих блоках для КАЖДОГО из этих городов: %s. Пропуск любого города — критическая ошибка.
- demandDynamics: 12 объектов. Поле "period" СТРОГО на русском (Январь, Февраль...).
[3_competitor_map]
- ВЫВЕДИ 3, 4 ИЛИ 5 РЕАЛЬНЫХ КОНКУРЕНТОВ.
- Вытащи их сильные/слабые стороны из отзывов в SEARCH EVIDENCE.
- ОБЯЗАНО быть строго от 5 до 10 РЕАЛЬНЫХ компаний. Ровно 3 — ЗАПРЕЩЕНО.
- Рассчитай реалистичный ER (от 0.01 до 0.08).
[4_content_profile]
- Сумма всех ContentPercent должна быть ровно 100.0. Распредели в зависимости от ниши (если визуал важен - демо/видео выше; если B2B - экспертность выше).
- Сумма всех ContentPercent должна быть ровно 100.0.
[5_competition_intensity]
- ciiIndex: 0-100. Оцени по плотности выдачи 2GIS и Google в EVIDENCE.
@@ -279,14 +295,17 @@ public class MarketingAnalysisV3Service {
- Отрази привычки КЗ: Kaspi Red, WhatsApp, чувствительность к скидкам.
[9_user_positioning]
- Если DTO smmStatus == "NONE", радар-чарт по нулям.
- Если DTO smmStatus == "NONE", радар-чарт по нулям, а статус напиши "Отсутствует".
[10_structured_conclusions]
- Подведи итог: Какой фактор доминирует? (Быстрая покупка, долгий цикл, перегретый рынок, или запуск). Это нужно для будущей скоринг-модели.
- Подведи итог: Какой фактор доминирует?
[11_smm_strategy_rationale]
- МИНИМУМ 50 СЛОВ. Обоснуй стратегию на основе реальных данных из блоков 5 и 7. Свяжи барьеры аудитории с выбором SMM.
ЭТАЛОННЫЙ JSON:
%s
""".formatted(requestJson, evidenceJson, schemaTemplate);
""".formatted(requestJson, evidenceJson, targetCities, schemaTemplate);
} catch (Exception e) {
throw new RuntimeException("Error building prompt", e);
}
@@ -296,13 +315,13 @@ public class MarketingAnalysisV3Service {
return """
{
"0_executive_summary": {
"businessStage": "string",
"geography": "string",
"businessStage": "СТРОГО НА РУССКОМ (например 'На старте', 'Действующий бизнес')",
"geography": "СТРОГО НА РУССКОМ (например 'Несколько городов', 'Один город')",
"activeCompetitors": 0,
"competitionLevel": "string",
"competitionLevel": "СТРОГО НА РУССКОМ (например 'Высокий', 'Средний')",
"averageNicheEr": "string",
"averageRating": 0.0,
"demandTrend": "string",
"demandTrend": "СТРОГО НА РУССКОМ (например 'Стабильный', 'Растущий')",
"keyFigures": ["string", "string", "string"]
},
"1_market_landscape": {
@@ -353,7 +372,7 @@ public class MarketingAnalysisV3Service {
},
"5_competition_intensity": {
"ciiIndex": 0,
"intensityLabel": "string",
"intensityLabel": "СТРОГО НА РУССКОМ (например 'Высокая', 'Низкая')",
"contributingFactors": ["string"]
},
"6_reputation_analysis": {
@@ -368,11 +387,11 @@ public class MarketingAnalysisV3Service {
"bookingFrequency": "string",
"dmRequestFrequency": "string",
"priceVisibility": "string",
"avgCycleDuration": "string",
"avgCycleDuration": "СТРОГО числовой диапазон, например '5-7 дней', НИКАКИХ абстрактных слов",
"commonCta": ["string"]
},
"8_search_demand": {
"avgFrequency": "string",
"avgFrequency": "СТРОГО числовой диапазон, например '10000-15000', НИКАКИХ абстрактных слов",
"seasonality": [
{"period": "Январь", "value": 0.0},
{"period": "Февраль", "value": 0.0},
@@ -398,11 +417,11 @@ public class MarketingAnalysisV3Service {
"reputation": 0,
"frequency": 0
},
"status": "string",
"status": "СТРОГО НА РУССКОМ (например 'Отсутствует', 'Активный')",
"gaps": ["string"]
},
"10_structured_conclusions": ["string", "string", "string"],
"11_smm_strategy_rationale": "string"
"11_smm_strategy_rationale": "МИНИМУМ 50 СЛОВ. Обосновать стратегию на основе данных конкуренции и паттернов поведения."
}
""";
}