target fix
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(select-object *)",
|
||||
"Bash(mvnw.cmd *)",
|
||||
"Read(c:\\Users\\777\\IdeaProjects/**)",
|
||||
"Read(c:\\Users\\777\\IdeaProjects\\api-gateway-konturAi/**)",
|
||||
"Read(c:\\Users\\777\\IdeaProjects\\konturai-front/**)",
|
||||
"Read(c:\\Users\\777\\IdeaProjects\\api-gateway-konturAi\\src\\main\\java\\konturai\\kz\\api_gateway\\configuration/**)",
|
||||
"Read(c:\\Users\\777\\IdeaProjects\\marketing/**)",
|
||||
"Read(c:\\Users\\777\\IdeaProjects\\marketing\\src/**)",
|
||||
"Read(c:\\Users\\777\\IdeaProjects\\marketing\\src\\service/**)",
|
||||
"Read(c:\\Users\\777\\IdeaProjects\\marketing\\src\\config/**)",
|
||||
"Bash(findstr *)"
|
||||
]
|
||||
},
|
||||
"$version": 3
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(select-object *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
# Рефакторинг Маркетинговой Стратегии - Changelog
|
||||
|
||||
## Дата: 11 апреля 2026
|
||||
|
||||
### 📋 Описание изменений
|
||||
|
||||
Полностью переработана логика генерации маркетинговой стратегии для решения проблем:
|
||||
- Недостаточное количество постов (было 8, стало 16)
|
||||
- Неравномерное распределение постов (пропуски 2-3 дня)
|
||||
- Отсутствие связи между постами и неделями
|
||||
- Непрофессиональное отображение на фронте
|
||||
|
||||
---
|
||||
|
||||
### ✅ Что изменено
|
||||
|
||||
#### 1. **Увеличено количество постов: 8 → 16**
|
||||
- **Было:** 8 постов за 4 недели (2 поста/неделю)
|
||||
- **Стало:** 16 постов за 4 недели (4 поста/неделю)
|
||||
- **Распределение:** Пн 09:00, Ср 12:00, Пт 18:00, Сб 20:00
|
||||
|
||||
#### 2. **Добавлено поле `weekNumber`**
|
||||
- **Model:** `MarketingStrategy.PostCalendarItem.weekNumber`
|
||||
- **DTO:** `MarketingStrategyResponse.PostCalendarItem.weekNumber`
|
||||
- **Цель:** Чёткая связь каждого поста с конкретной неделей стратегии
|
||||
|
||||
#### 3. **Нормализация расписания**
|
||||
- Новый метод: `normalizePostSchedule()`
|
||||
- Автоматически выравнивает посты по дням недели
|
||||
- Гарантирует равномерный ритм публикаций
|
||||
- Сохраняет валидные даты от AI (если в пределах ±2 дней)
|
||||
|
||||
#### 4. **Усиленный промпт для AI**
|
||||
- Чёткие инструкции по распределению 16 постов
|
||||
- Примеры правильного JSON с `weekNumber`
|
||||
- Явные требования: 4 поста/неделю, Пн-Ср-Пт-Сб
|
||||
- Увеличено количество видео: 2 → 4 (по 1 в неделю)
|
||||
|
||||
#### 5. **Улучшенная валидация**
|
||||
- Логирование при < 12 постах от AI
|
||||
- Авто-расчёт дат если AI вернул некорректные
|
||||
- Нормализация `weekNumber` для всех постов
|
||||
|
||||
---
|
||||
|
||||
### 📁 Изменённые файлы
|
||||
|
||||
1. **`MarketingStrategy.java`** (Model)
|
||||
- Добавлено поле `weekNumber` в `PostCalendarItem`
|
||||
|
||||
2. **`MarketingStrategyResponse.java`** (DTO)
|
||||
- Добавлено поле `weekNumber` в `PostCalendarItem`
|
||||
- Добавлены геттеры/сеттеры + поля `videoUrl`, `videoFilename`
|
||||
|
||||
3. **`MarketingStrategyV3Service.java`** (Service)
|
||||
- Усилен `buildUserPrompt()` — детальные инструкции по распределению
|
||||
- Переписан `populateStrategyEntity()` — обработка `weekNumber`
|
||||
- Новый метод `normalizePostSchedule()` — выравнивание расписания
|
||||
- Новый метод `getDayOffset()` — расчёт дней недели
|
||||
- Обновлён `buildPostCountPlan()` — 16 постов вместо 12
|
||||
- Увеличен лимит видео: 2 → 4
|
||||
|
||||
4. **`getStrategyResult()`**
|
||||
- Добавлено поле `weekNumber` в ответ API
|
||||
|
||||
---
|
||||
|
||||
### 🎯 Результат
|
||||
|
||||
#### **До рефакторинга:**
|
||||
```json
|
||||
{
|
||||
"postCalendar": [
|
||||
{ "publishDate": "2026-04-10T09:00:00", "theme": "Кейс: ..." },
|
||||
{ "publishDate": "2026-04-12T18:00:00", "theme": "Процесс: ..." },
|
||||
{ "publishDate": "2026-04-15T12:00:00", "theme": "Отзыв: ..." }
|
||||
// ... всего 8 постов, пропуски 2-3 дня
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### **После рефакторинга:**
|
||||
```json
|
||||
{
|
||||
"postCalendar": [
|
||||
{
|
||||
"postIndex": 0,
|
||||
"publishDate": "2026-04-13T09:00:00",
|
||||
"weekNumber": 1,
|
||||
"platform": "Instagram",
|
||||
"contentType": "видео",
|
||||
"theme": "Кейс: Как мы увеличили продажи на 40%",
|
||||
"publishTime": "09:00"
|
||||
},
|
||||
{
|
||||
"postIndex": 1,
|
||||
"publishDate": "2026-04-15T12:00:00",
|
||||
"weekNumber": 1,
|
||||
"platform": "Instagram",
|
||||
"contentType": "фото",
|
||||
"theme": "Процесс: За кадром нашей работы",
|
||||
"publishTime": "12:00"
|
||||
},
|
||||
// ... ещё 14 постов, ровно 4 в неделю
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 📊 Расписание постов (пример)
|
||||
|
||||
| Неделя | День | Время | Тип контента |
|
||||
|--------|------|-------|--------------|
|
||||
| 1 | Понедельник (день 1) | 09:00 | 🎥 Видео (Кейс) |
|
||||
| 1 | Среда (день 3) | 12:00 | 📸 Фото (Процесс) |
|
||||
| 1 | Пятница (день 5) | 18:00 | 📸 Фото (Отзыв) |
|
||||
| 1 | Суббота (день 6) | 20:00 | 📸 Фото (Экспертный) |
|
||||
| 2 | Понедельник (день 8) | 09:00 | 🎥 Видео (Кейс) |
|
||||
| 2 | Среда (день 10) | 12:00 | 📸 Фото (Процесс) |
|
||||
| ... | ... | ... | ... |
|
||||
|
||||
---
|
||||
|
||||
### 🚀 Преимущества
|
||||
|
||||
✅ **Профессиональный вид** — равномерное расписание, как у SMM-агентств
|
||||
✅ **Предсказуемость** — клиент знает когда будет каждый пост
|
||||
✅ **Связность** — каждый пост привязан к неделе и теме
|
||||
✅ **Гибкость** — AI может предлагать даты, но система нормализует
|
||||
✅ **Масштабируемость** — легко изменить на 3 или 5 постов/неделю
|
||||
|
||||
---
|
||||
|
||||
### ⚠️ Важно для фронтенда
|
||||
|
||||
Теперь в ответе API появляется новое поле `weekNumber` у каждого поста:
|
||||
|
||||
```typescript
|
||||
interface PostCalendarItem {
|
||||
postIndex: number;
|
||||
publishDate: string;
|
||||
weekNumber: number; // ← НОВОЕ ПОЛЕ
|
||||
platform: string;
|
||||
contentType: string;
|
||||
theme: string;
|
||||
postText: string;
|
||||
hashtags: string[];
|
||||
publishTime: string;
|
||||
imageUrl?: string;
|
||||
videoUrl?: string;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Рекомендуется использовать `weekNumber` для группировки постов на фронте!
|
||||
|
||||
---
|
||||
|
||||
### 🧪 Тестирование
|
||||
|
||||
После деплоя проверьте:
|
||||
1. Генерацию новой стратегии — должно быть 16 постов
|
||||
2. Распределение по неделям — по 4 поста в каждой
|
||||
3. Даты публикаций — Пн-Ср-Пт-Сб
|
||||
4. Поле `weekNumber` — должно присутствовать у всех постов
|
||||
5. Видео-посты — ровно 4 (по 1 в неделю)
|
||||
|
||||
---
|
||||
|
||||
### 📝 Компиляция
|
||||
|
||||
```bash
|
||||
cd c:\Users\777\IdeaProjects\telecomBackend
|
||||
mvnw.cmd compile -DskipTests
|
||||
```
|
||||
|
||||
✅ **BUILD SUCCESS** — все изменения компилируются без ошибок!
|
||||
|
||||
---
|
||||
|
||||
**Автор:** AI Assistant
|
||||
**Дата:** 11.04.2026
|
||||
**Версия:** 2.0
|
||||
+6
-6
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"type": "service_account",
|
||||
"project_id": "data-totality-483616-t5",
|
||||
"private_key_id": "e9d6df08cfaf391f2b45ccf0473d22221a14fe1e",
|
||||
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCkA41lMjU7GSLj\nS5CeXJjT3gsKTp2/Y9adieLrjIXA8GY3Dhm+dwj1+hVF1fKB1qYI072LNku9jpuA\nGbKtDAIDXfTr0NKF097z51+X7Sprnq3lamqzAXnH8PEAdv5A97GtNBDBB6E9S40q\n6uEBfIjeaVENcrt8bnS/jgvtQeDFj/0RoFVOUfapCgFW2zkvqU549z0kZumF9zNF\nNXkKGOelmWm1430QyCWhw4P5SIxfCehz0pD17NSMlUZQvbav/ZZ286SnDzloRT+q\njXyRIXnMMuTnKWov7zqKflmfg8omxkwf/HqGIu2c0+hgmcyQhy8oXj/X9ocXLNsA\n+IclAskFAgMBAAECggEAD+ZHl5GhnGbdksg5WZ4OCK05R8NYQnyktPqayg0szYfg\neMzVTQ6lX+cMQeFcKXRhUxtnqq5Vnad3Rpih0Wmya5jOTJVJRe+Z6O4Qk8ifeKBW\nKOvDMogy7IwxvhhK9zRWf+e7X2MPIUMziP9iVu5iJvbHBIqPR0rpPBqX6GT0VmJs\n6CG1C1FtL3QFvxiZSiWwIzGb0ozZKIHBhqUxapQcUlTSqsSNguJEGeHUXkD3Eg1R\n3gQJs4JKCiQnsCeWcpEcoo/GaP+feTjXDJzPlx6EDzpaFAeyUXPWNm8s8VBHj/dM\nvD9FSL2/YiouublF7TW+cvCCfiYALLJx41hOeeEZTwKBgQDhtbdY1FQwh6T8ciF2\nCH+kWfIUIIiNUwlyllc05Vx6nC9oSuChDOWFMqe2TOpcibAvPCWgLN4lHjxsd0zj\nInTomGFg8mPu7l9g0AUCt6R2DoWWnS2omtyRTCIiq0Ms8WHUwil3U57IS7+2MgK7\nlKwBQtrBQoWZPkgwiuxypLf/uwKBgQC6BkP3B2u2LbP5v0VlNYI1Lvr96Bk6sWCh\ny1kOO0DI/6ubC5WFgTDtTBakbZvRdk5X7OmJCaMRtNCgrAK1twq8dLT5EZFNRrQ/\n+Q/mGsMBnZlf15hqUVnjmYuXy+o9hnU1F84TOjz0AYwvZFmoWicYWf6NDoHfnXyw\nMQOlQMHuPwKBgQC4wBFPuJ0hDCbULGePQe2ZDbbFGswrQejdzombVpKYGfgEGiY8\nLV5B5PC6m8LJ9Dour8Dsl01OfK1g61YYJUAkk3ZB8KY0ERIpIJy5WLBZohFe81FI\nTVp15Cec0SqvQvWdPUDL17qEoNxPuifaexs//QUqY9UPVpeohE5+mK2zuQKBgD8S\nGMEMlwxxem/W1eR2u+rkPt0PcF5h/W1d0Jfupwx/AuPSjaNyJfCOt66QKyBdiDpv\nbM61ocBDGekf6NtLcCxtNV6tAiK9MifufeF6eVt236WLJ3YElwF/8cBJtuWdTOfv\nZkNGn5vrWmhHEc7uogqzmIaiwI3ci4YBWEcTKa1pAoGAQAGOKD6/9m15rGoypQK0\ndM7RHoFiPAga0l6RaRmjInhkPItood84nWvSGwmfDMovvLuMXT4/kqhgoAJBCTp4\nZMWlEnk5XgU3XVATV9fWogSEdPiXq+mwikl2A7xF/PsS79ekc6wVAFS6aog08GH+\ndwLlquOtv2Dd9qW6d6Pe+4I=\n-----END PRIVATE KEY-----\n",
|
||||
"client_email": "vertex-ai-user@data-totality-483616-t5.iam.gserviceaccount.com",
|
||||
"client_id": "116751741947593459625",
|
||||
"project_id": "gen-lang-client-0719272404",
|
||||
"private_key_id": "feb98d8fa2bd9c7139b3c03fac63900877b652f0",
|
||||
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDDB5ZQNAc4C6Eu\nFA1SD1GE4IxIHuKWPE0aUPe8X1YuX1l3S94XiXzwwXHgaFi8BCJJRVBRPtfvCAM+\nQvv03kfs9KEc6UvkUlaPIgF3cjddshMJuj/7W+YpEKjuuWc649l5NO4uPcug+g1S\nZJhd7UGXbx1Uglw3LSsODJ2b4wNIQUuulGym4VCLQKhu41eGElyIVbecRxrDC2N4\nR3ROgFYilAL7QYzVojTEYea3zIBAXncXvgJcFyhSWDuJq5yJbUkbPNbVNhQ7uw7G\nCFufQWMhMBeBrO62kyufQg+xI+xqMvaDOSs+JReni57ZtUqxnpjD7Gh+BxHnduJJ\n8y/osHB5AgMBAAECggEADvuWe99fXZohHgva0R7Npa5lgIXk5ONJjhfBzHYrhgD7\nu8s3iuncUBegYrvJc1pC8wuQ5cj06E9ehmo5doCcGHNbrD+s/aOotDLJ144N/FP5\nm/Hzx+Dxv95giAR0zoSiGKptEN7c9ac4HkgNn4uiUhqub2dbY77D07663fa08hg1\ntGAtqt7lbw8MZbrCrMDdqkM552xmjxn0MnS8Itkiby1MdFi4H+0xQUO7xavsOLZ6\nQXHexNgUZsjD2ZfxFoo/U/VUaWUHn1/acQh0oJyMJD9/ZerR5Cq7XNKJgUrJUn7B\ndGNaXzymOyIWHbaCkF08+KBX2th65AjSEoJgTZiYnQKBgQDqJmzik/ZomqxySUoh\nRt0WNxO6Ed775qgluQOW9TumYDRLUP4cpRqt1vA/voGpyc7C+bnQRnQnJM0gceH0\nW6AmqdmJVLX+1DJnfkVNichI30Iz5rTYzMqneirEi2kuH6Zdh7VI6jqt3fM6QB0b\nHj2ZOXT70pS3eIGMSLjQgiBZ7QKBgQDVOp6LLWSKguqaJeVCBGbkdOGgqWUb2R6/\nk2nRJCuxnK9JJICotfxTQb+MIbFZlvQGiyb6AKUQadOHrt0VdkDeP0VCTeXw88hf\npKn3fepzxUwP8qsdEg9rmI5Y0jL23oJHPT6EHf3jnUQtpR4Wy1u+c46MRiSpCqPS\nyoHIl0qvPQKBgH2vBn9QgNGBRNNLt7jI+t9zzOjxsslv9A3bk6uyEsSpmyhaXywt\nC3cIiokMzzi+FHff5/d9+dcRD/I8btblX7+hKB1pFQ+mAzA+PSDztkg3XqfRTH2a\n36SqFM7PLjQN1RUbRRnQcJLnMimwRG9L+jCHAN4zGJSq4I7Wci2py4e5AoGAbDNa\n0HUwV7knIyGnvGBPdg55j/VlgUQj/kLnp2Ebav1/VTZYs0PlzrnGYW2XeUbIfamP\nFb3OBQ4ucOOi/IkNUbn5gzPUQvjx7Na6CQS0PBsaBd22RwIrIQZA9YF0qyEuKshB\nOPmfJClht2faZOa8n2A0LapKYB0uFID+arObCmECgYEAx2PS/d178Nfi4oG6nYIR\nAHpjC3JPlt62ySSxqfag+DPdTeOyOK8YglsyLWHGU6vI2OhYCEhuU9LMawj4tVui\nS+4D3A1CS69iOMdJVBkG1jlTP7UMZHlh43jMRW+Tcu/h7fHJaSS9YWPwyX0KUAqo\n7dB0niDoH6m8Cvb9YKXCEzs=\n-----END PRIVATE KEY-----\n",
|
||||
"client_email": "vertex-express@gen-lang-client-0719272404.iam.gserviceaccount.com",
|
||||
"client_id": "114082700099175512857",
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/vertex-ai-user%40data-totality-483616-t5.iam.gserviceaccount.com",
|
||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/vertex-express%40gen-lang-client-0719272404.iam.gserviceaccount.com",
|
||||
"universe_domain": "googleapis.com"
|
||||
}
|
||||
|
||||
@@ -28,5 +28,10 @@ public class TargetingAIAnalysisResult {
|
||||
private String aiRecommendationsRationale;
|
||||
private List<String> quickWins;
|
||||
private List<String> warnings;
|
||||
private String technicalHealthSummary;
|
||||
private Integer lpMatchScore;
|
||||
private String policyRiskLevel;
|
||||
private Integer suggestedAutomationRulesCount;
|
||||
private Integer matrixCellsCount;
|
||||
private TargetingRecommendationDto targetingRecommendation;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package kz.konturai.parser.dto.targeting;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class FunnelStrategyDto {
|
||||
|
||||
private String tof;
|
||||
private String mof;
|
||||
private String bof;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package kz.konturai.parser.dto.targeting;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class HypothesisCellDto {
|
||||
|
||||
private int hookIndex;
|
||||
private int visualIndex;
|
||||
private String specificAdIdea;
|
||||
private String counterHypothesis;
|
||||
private String funnelStage;
|
||||
private String expectedOutcome;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package kz.konturai.parser.dto.targeting;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class HypothesisMatrixDto {
|
||||
|
||||
private int rowCount;
|
||||
private int colCount;
|
||||
private List<String> rows;
|
||||
private List<String> cols;
|
||||
private List<HypothesisCellDto> matrix;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package kz.konturai.parser.dto.targeting;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class LandingPageAuditDto {
|
||||
|
||||
private int matchScore;
|
||||
private String executiveSummary;
|
||||
private List<String> criticalFixes;
|
||||
private List<String> trustBoosters;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package kz.konturai.parser.dto.targeting;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class PixelCheckDto {
|
||||
|
||||
private boolean fbp;
|
||||
private boolean capi;
|
||||
private boolean gtm;
|
||||
private String setupInstructions;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package kz.konturai.parser.dto.targeting;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class RiskAnalysisDto {
|
||||
|
||||
private String fatigueWarning;
|
||||
private String policyRiskLevel;
|
||||
private List<String> moderationTriggers;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package kz.konturai.parser.dto.targeting;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class ScalingPlanDto {
|
||||
|
||||
private String horizontalScaling;
|
||||
private String budgetScaling;
|
||||
private List<String> geoExpansion;
|
||||
}
|
||||
@@ -15,69 +15,50 @@ import java.util.List;
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class TargetingRecommendationDto {
|
||||
|
||||
// ── Тип таргетинга ────────────────────────────────────────────────────────
|
||||
/**
|
||||
* Рекомендуемый тип: COLD_INTEREST / BEHAVIORAL / LOOKALIKE /
|
||||
* RETARGETING / LEAD_GEN / AWARENESS / MIXED
|
||||
*/
|
||||
private String recommendedType;
|
||||
|
||||
/** Читаемое название типа */
|
||||
private String recommendedTypeLabel;
|
||||
|
||||
/** Почему AI выбрал именно этот тип — на основе данных опросника */
|
||||
private String typeRationale;
|
||||
|
||||
// ── Цель кампании ─────────────────────────────────────────────────────────
|
||||
/** AWARENESS / TRAFFIC / LEADS / ENGAGEMENT / SALES */
|
||||
private String campaignObjective;
|
||||
|
||||
/** Читаемая цель */
|
||||
private String campaignObjectiveLabel;
|
||||
|
||||
/** Обоснование цели */
|
||||
private String campaignObjectiveRationale;
|
||||
|
||||
// ── Аудитория ─────────────────────────────────────────────────────────────
|
||||
private AudienceProfileDto audienceProfile;
|
||||
|
||||
// ── Платформы и форматы ───────────────────────────────────────────────────
|
||||
private List<String> recommendedPlatforms;
|
||||
private List<String> recommendedAdFormats;
|
||||
private List<String> recommendedPlacements;
|
||||
|
||||
// ── Бюджет ────────────────────────────────────────────────────────────────
|
||||
private TargetingBudgetDto budgetRecommendation;
|
||||
|
||||
// ── Фазы таргетинга ───────────────────────────────────────────────────────
|
||||
private List<TargetingPhaseDto> phases;
|
||||
|
||||
// ── KPI ───────────────────────────────────────────────────────────────────
|
||||
private String estimatedCtr;
|
||||
private String estimatedCpl;
|
||||
private String estimatedReach;
|
||||
private String estimatedFrequency;
|
||||
private String estimatedConversions;
|
||||
|
||||
// ── Ключевые выводы из опросника ──────────────────────────────────────────
|
||||
/** Сигналы из опросника, которые повлияли на рекомендацию */
|
||||
private List<String> questionnaireSignals;
|
||||
|
||||
// ── Конкурентный контекст ─────────────────────────────────────────────────
|
||||
private String competitiveContext;
|
||||
private String ciiLevel;
|
||||
private String differentiationAdvice;
|
||||
|
||||
// ── Быстрые победы и предупреждения ──────────────────────────────────────
|
||||
private List<String> quickWins;
|
||||
private List<String> warnings;
|
||||
|
||||
// ── Контент-стратегия ─────────────────────────────────────────────────────
|
||||
private PixelCheckDto pixelHealth;
|
||||
private FunnelStrategyDto funnelStrategy;
|
||||
private HypothesisMatrixDto hypothesisMatrix;
|
||||
private LandingPageAuditDto lpAudit;
|
||||
private List<TargetingOrchestrationDto.AutomationRuleDto> suggestedRules;
|
||||
private ScalingPlanDto scalingPlan;
|
||||
private RiskAnalysisDto risks;
|
||||
|
||||
private List<String> contentMix;
|
||||
private String postingFrequency;
|
||||
private List<String> bestPostingTimes;
|
||||
|
||||
// ── CTA и воронка ─────────────────────────────────────────────────────────
|
||||
private String primaryCta;
|
||||
private String funnelStage;
|
||||
private String conversionMechanism;
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import kz.konturai.parser.dto.MarketingAnalysisV3Request;
|
||||
import kz.konturai.parser.dto.MarketingAnalysisV3Result;
|
||||
import kz.konturai.parser.dto.targeting.*;
|
||||
import kz.konturai.parser.enums.StrategyModel;
|
||||
import kz.konturai.parser.model.MarketingAnalysisV3Document;
|
||||
import kz.konturai.parser.model.MarketingStrategy;
|
||||
import kz.konturai.parser.repository.MarketingAnalysisV3Repository;
|
||||
@@ -15,7 +16,14 @@ import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* AI Targeting System — два независимых режима:
|
||||
@@ -164,23 +172,26 @@ public class AiTargetingSystemService {
|
||||
- Бюджеты указывай в тенге (KZT).
|
||||
""";
|
||||
|
||||
String instruction = buildTargetingPromptInstruction();
|
||||
String context = buildFullContext(req, res, strategy);
|
||||
systemPrompt = buildEnhancedTargetingSystemPrompt();
|
||||
String instruction = buildEnhancedTargetingPromptInstruction();
|
||||
String context = buildEnhancedFullContext(req, res, strategy);
|
||||
|
||||
String raw = openAIAnalyticsService.generateWithInstructionWithModel(
|
||||
context, instruction, "ru", "gpt-4o",
|
||||
systemPrompt, 3000, 150000L);
|
||||
context, instruction, "ru", strategyModel,
|
||||
systemPrompt, 4500, 150000L);
|
||||
|
||||
if (raw == null) {
|
||||
logger.warn("AI returned null for targeting recommendation, using fallback");
|
||||
return buildFallbackRecommendation(req, res);
|
||||
return buildFallbackRecommendation(req, res, strategy);
|
||||
}
|
||||
|
||||
try {
|
||||
return objectMapper.readValue(extractJson(raw), TargetingRecommendationDto.class);
|
||||
TargetingRecommendationDto recommendation =
|
||||
objectMapper.readValue(extractJson(raw), TargetingRecommendationDto.class);
|
||||
return validateAndNormalizeRecommendation(recommendation, req, res, strategy);
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to parse targeting recommendation. Raw: {}", raw, e);
|
||||
return buildFallbackRecommendation(req, res);
|
||||
logger.error("Failed to parse targeting recommendation", e);
|
||||
return buildFallbackRecommendation(req, res, strategy);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,6 +474,619 @@ public class AiTargetingSystemService {
|
||||
return ctx.toString();
|
||||
}
|
||||
|
||||
private String buildEnhancedTargetingSystemPrompt() {
|
||||
return """
|
||||
You are NOT just a generator. You are a Senior Performance Architect with 20 years of experience in paid acquisition,
|
||||
funnel strategy, landing page diagnostics, Meta Ads scaling, and growth systems.
|
||||
Your recommendations must be surgical, commercially sharp, aggressive for growth, but cautious about technical health
|
||||
and burn-out risk. You speak like a real performance lead and use concepts such as LTV, ROAS, CPL, CAC,
|
||||
Attribution Window, Retargeting windows, budget pacing, creative fatigue, and conversion friction.
|
||||
|
||||
Rules:
|
||||
- Return valid JSON only.
|
||||
- Every narrative field inside JSON must be in Russian.
|
||||
- Keep recommendations practical and compatible with real paid traffic operations.
|
||||
- Budget values must stay in KZT.
|
||||
- Do not omit mandatory sections even if data is limited.
|
||||
""";
|
||||
}
|
||||
|
||||
private String buildEnhancedTargetingPromptInstruction() {
|
||||
return """
|
||||
На основе всех данных опросника, анализа рынка, текущих digital assets, сильных сторон конкурентов и модели стратегии
|
||||
верни только валидный JSON без markdown и без пояснений вне JSON.
|
||||
|
||||
Правила:
|
||||
- Все текстовые поля на русском языке.
|
||||
- Используй профессиональную performance-терминологию: ROAS, CPL, CTR, LTV, CAC, Pixel, CAPI, GTM, Retargeting.
|
||||
- Не пропускай ни один обязательный блок.
|
||||
- hypothesisMatrix обязан содержать ровно 4 rows, 4 cols и 16 cells.
|
||||
- lpAudit.matchScore должен быть числом от 0 до 100.
|
||||
- suggestedRules должен содержать 3 реалистичных automation rules.
|
||||
- Funnel strategy обязана разделять холодную, теплую и горячую аудиторию.
|
||||
- LP audit должен указывать 3 типичных conversion killers ниши.
|
||||
- Risk analysis обязан учитывать policy risk и creative fatigue.
|
||||
|
||||
Верни JSON со всеми существующими полями recommendation и обязательными новыми блоками:
|
||||
pixelHealth, funnelStrategy, hypothesisMatrix, lpAudit, suggestedRules, scalingPlan, risks.
|
||||
""";
|
||||
}
|
||||
|
||||
private String buildEnhancedFullContext(
|
||||
MarketingAnalysisV3Request req,
|
||||
MarketingAnalysisV3Result res,
|
||||
MarketingStrategy strategy) {
|
||||
StringBuilder ctx = new StringBuilder(buildFullContext(req, res, strategy));
|
||||
appendSocialLinksContext(ctx, req);
|
||||
appendCompetitorStrengthsContext(ctx, res);
|
||||
appendCompetitiveInterpretation(ctx, req, res);
|
||||
appendStrategyModelContext(ctx, strategy);
|
||||
return ctx.toString();
|
||||
}
|
||||
|
||||
private void appendSocialLinksContext(StringBuilder ctx, MarketingAnalysisV3Request req) {
|
||||
if (req == null || req.getUserSocialLinks() == null || req.getUserSocialLinks().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
ctx.append("\n=== CURRENT DIGITAL ASSETS ===\n");
|
||||
req.getUserSocialLinks().stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(String::trim)
|
||||
.filter(link -> !link.isBlank())
|
||||
.forEach(link -> ctx.append("- ").append(link).append("\n"));
|
||||
}
|
||||
|
||||
private void appendCompetitorStrengthsContext(StringBuilder ctx, MarketingAnalysisV3Result res) {
|
||||
List<String> strengths = extractTopCompetitorStrengths(res);
|
||||
if (strengths.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
ctx.append("\n=== TOP COMPETITOR STRENGTHS ===\n");
|
||||
strengths.forEach(strength -> ctx.append("- ").append(strength).append("\n"));
|
||||
}
|
||||
|
||||
private List<String> extractTopCompetitorStrengths(MarketingAnalysisV3Result res) {
|
||||
if (res == null || res.getCompetitorMap() == null) {
|
||||
return List.of();
|
||||
}
|
||||
return res.getCompetitorMap().stream()
|
||||
.filter(Objects::nonNull)
|
||||
.sorted(Comparator
|
||||
.comparingInt((MarketingAnalysisV3Result.CompetitorProfile profile) ->
|
||||
profile.getStrengths() != null ? profile.getStrengths().size() : 0)
|
||||
.reversed()
|
||||
.thenComparingInt(MarketingAnalysisV3Result.CompetitorProfile::getFollowers).reversed())
|
||||
.limit(3)
|
||||
.flatMap(profile -> {
|
||||
List<String> strengths = profile.getStrengths();
|
||||
if (strengths == null || strengths.isEmpty()) {
|
||||
return List.<String>of().stream();
|
||||
}
|
||||
return strengths.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isBlank())
|
||||
.map(s -> safeStr(profile.getName()) + ": " + s);
|
||||
})
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private void appendCompetitiveInterpretation(
|
||||
StringBuilder ctx,
|
||||
MarketingAnalysisV3Request req,
|
||||
MarketingAnalysisV3Result res) {
|
||||
if (res == null || res.getCompetitionIntensity() == null) {
|
||||
return;
|
||||
}
|
||||
int cii = res.getCompetitionIntensity().getCiiIndex();
|
||||
ctx.append("\n=== PERFORMANCE INTERPRETATION ===\n");
|
||||
if (cii >= 70) {
|
||||
ctx.append("- Нужна агрессивная differentiation strategy и жесткий контроль creative fatigue.\n");
|
||||
} else if (cii >= 40) {
|
||||
ctx.append("- Нужен баланс между дифференциацией и cost-efficient закупкой.\n");
|
||||
} else {
|
||||
ctx.append("- Можно активнее тестировать холодный трафик и более широкий охват.\n");
|
||||
}
|
||||
if (req != null && req.getAverageCheck() != null && "HIGH".equalsIgnoreCase(req.getAverageCheck().name())) {
|
||||
ctx.append("- Высокий чек: усиливаем trust layer, BOF и качественную аргументацию оффера.\n");
|
||||
}
|
||||
if (res.getContentProfile() != null) {
|
||||
ctx.append("- Доля video content у конкурентов: ")
|
||||
.append(res.getContentProfile().getVideoShare())
|
||||
.append("%.\n");
|
||||
}
|
||||
}
|
||||
|
||||
private void appendStrategyModelContext(StringBuilder ctx, MarketingStrategy strategy) {
|
||||
if (strategy == null) {
|
||||
return;
|
||||
}
|
||||
ctx.append("\n=== STRATEGY MODEL CONTEXT ===\n");
|
||||
ctx.append("- scoringModelName: ").append(safeStr(strategy.getScoringModelName())).append("\n");
|
||||
ctx.append("- scoringModelTitle: ").append(safeStr(strategy.getScoringModelTitle())).append("\n");
|
||||
}
|
||||
|
||||
private TargetingRecommendationDto validateAndNormalizeRecommendation(
|
||||
TargetingRecommendationDto recommendation,
|
||||
MarketingAnalysisV3Request req,
|
||||
MarketingAnalysisV3Result res,
|
||||
MarketingStrategy strategy) {
|
||||
TargetingRecommendationDto base = buildFallbackRecommendation(req, res);
|
||||
TargetingRecommendationDto normalized = recommendation != null ? recommendation : base;
|
||||
|
||||
if (isBlank(normalized.getRecommendedType())) normalized.setRecommendedType(base.getRecommendedType());
|
||||
if (isBlank(normalized.getRecommendedTypeLabel())) normalized.setRecommendedTypeLabel(base.getRecommendedTypeLabel());
|
||||
if (isBlank(normalized.getTypeRationale())) normalized.setTypeRationale(base.getTypeRationale());
|
||||
if (isBlank(normalized.getCampaignObjective())) normalized.setCampaignObjective(base.getCampaignObjective());
|
||||
if (isBlank(normalized.getCampaignObjectiveLabel())) normalized.setCampaignObjectiveLabel(base.getCampaignObjectiveLabel());
|
||||
if (isBlank(normalized.getCampaignObjectiveRationale())) normalized.setCampaignObjectiveRationale(base.getCampaignObjectiveRationale());
|
||||
if (normalized.getAudienceProfile() == null) normalized.setAudienceProfile(base.getAudienceProfile());
|
||||
if (normalized.getBudgetRecommendation() == null) normalized.setBudgetRecommendation(base.getBudgetRecommendation());
|
||||
if (normalized.getRecommendedPlatforms() == null || normalized.getRecommendedPlatforms().isEmpty()) {
|
||||
normalized.setRecommendedPlatforms(base.getRecommendedPlatforms());
|
||||
}
|
||||
if (normalized.getRecommendedAdFormats() == null || normalized.getRecommendedAdFormats().isEmpty()) {
|
||||
normalized.setRecommendedAdFormats(base.getRecommendedAdFormats());
|
||||
}
|
||||
if (normalized.getQuickWins() == null || normalized.getQuickWins().isEmpty()) {
|
||||
normalized.setQuickWins(base.getQuickWins());
|
||||
}
|
||||
if (normalized.getWarnings() == null || normalized.getWarnings().isEmpty()) {
|
||||
normalized.setWarnings(base.getWarnings());
|
||||
}
|
||||
|
||||
normalized.setQuestionnaireSignals(normalizeTextList(
|
||||
normalized.getQuestionnaireSignals(),
|
||||
List.of(
|
||||
"Покупатель принимает решение не мгновенно и требует прогрева.",
|
||||
"Сегменту важна понятная ценность оффера до клика.",
|
||||
"Конкурентная среда влияет на стоимость лида и частоту тестов.",
|
||||
"Таргетинг должен опираться на реальный customer behavior, а не на абстрактные интересы."
|
||||
),
|
||||
4));
|
||||
|
||||
normalized.setRecommendedPlacements(normalizeTextList(
|
||||
normalized.getRecommendedPlacements(),
|
||||
List.of("Instagram Feed", "Instagram Stories", "Facebook Feed"),
|
||||
3));
|
||||
|
||||
if (normalized.getPixelHealth() == null) {
|
||||
normalized.setPixelHealth(buildDefaultPixelHealth(req, res, strategy));
|
||||
} else {
|
||||
normalized.getPixelHealth().setSetupInstructions(truncateText(
|
||||
defaultIfBlank(normalized.getPixelHealth().getSetupInstructions(),
|
||||
buildDefaultPixelHealth(req, res, strategy).getSetupInstructions()),
|
||||
600));
|
||||
}
|
||||
|
||||
if (normalized.getFunnelStrategy() == null) {
|
||||
normalized.setFunnelStrategy(buildDefaultFunnelStrategy(req, res, strategy));
|
||||
} else {
|
||||
FunnelStrategyDto fallbackFunnel = buildDefaultFunnelStrategy(req, res, strategy);
|
||||
normalized.getFunnelStrategy().setTof(truncateText(defaultIfBlank(normalized.getFunnelStrategy().getTof(), fallbackFunnel.getTof()), 450));
|
||||
normalized.getFunnelStrategy().setMof(truncateText(defaultIfBlank(normalized.getFunnelStrategy().getMof(), fallbackFunnel.getMof()), 450));
|
||||
normalized.getFunnelStrategy().setBof(truncateText(defaultIfBlank(normalized.getFunnelStrategy().getBof(), fallbackFunnel.getBof()), 450));
|
||||
}
|
||||
|
||||
normalized.setHypothesisMatrix(normalizeMatrix(normalized.getHypothesisMatrix(), req, res, strategy));
|
||||
|
||||
if (normalized.getLpAudit() == null) {
|
||||
normalized.setLpAudit(buildDefaultLandingPageAudit(req, res, strategy));
|
||||
} else {
|
||||
LandingPageAuditDto fallbackAudit = buildDefaultLandingPageAudit(req, res, strategy);
|
||||
normalized.getLpAudit().setMatchScore(Math.max(0, Math.min(100, normalized.getLpAudit().getMatchScore())));
|
||||
normalized.getLpAudit().setExecutiveSummary(truncateText(
|
||||
defaultIfBlank(normalized.getLpAudit().getExecutiveSummary(), fallbackAudit.getExecutiveSummary()),
|
||||
500));
|
||||
normalized.getLpAudit().setCriticalFixes(normalizeTextList(
|
||||
normalized.getLpAudit().getCriticalFixes(),
|
||||
fallbackAudit.getCriticalFixes(),
|
||||
3));
|
||||
normalized.getLpAudit().setTrustBoosters(normalizeTextList(
|
||||
normalized.getLpAudit().getTrustBoosters(),
|
||||
fallbackAudit.getTrustBoosters(),
|
||||
3));
|
||||
}
|
||||
|
||||
normalized.setSuggestedRules(normalizeRules(normalized.getSuggestedRules(), req, res, strategy));
|
||||
|
||||
if (normalized.getScalingPlan() == null) {
|
||||
normalized.setScalingPlan(buildDefaultScalingPlan(req, res, strategy));
|
||||
} else {
|
||||
ScalingPlanDto fallbackScaling = buildDefaultScalingPlan(req, res, strategy);
|
||||
normalized.getScalingPlan().setHorizontalScaling(truncateText(
|
||||
defaultIfBlank(normalized.getScalingPlan().getHorizontalScaling(), fallbackScaling.getHorizontalScaling()), 400));
|
||||
normalized.getScalingPlan().setBudgetScaling(truncateText(
|
||||
defaultIfBlank(normalized.getScalingPlan().getBudgetScaling(), fallbackScaling.getBudgetScaling()), 400));
|
||||
normalized.getScalingPlan().setGeoExpansion(normalizeTextList(
|
||||
normalized.getScalingPlan().getGeoExpansion(),
|
||||
fallbackScaling.getGeoExpansion(),
|
||||
3));
|
||||
}
|
||||
|
||||
if (normalized.getRisks() == null) {
|
||||
normalized.setRisks(buildDefaultRiskAnalysis(req, res, strategy));
|
||||
} else {
|
||||
RiskAnalysisDto fallbackRisk = buildDefaultRiskAnalysis(req, res, strategy);
|
||||
normalized.getRisks().setFatigueWarning(truncateText(
|
||||
defaultIfBlank(normalized.getRisks().getFatigueWarning(), fallbackRisk.getFatigueWarning()), 400));
|
||||
normalized.getRisks().setPolicyRiskLevel(normalizePolicyRiskLevel(
|
||||
defaultIfBlank(normalized.getRisks().getPolicyRiskLevel(), fallbackRisk.getPolicyRiskLevel())));
|
||||
normalized.getRisks().setModerationTriggers(normalizeTextList(
|
||||
normalized.getRisks().getModerationTriggers(),
|
||||
fallbackRisk.getModerationTriggers(),
|
||||
3));
|
||||
}
|
||||
|
||||
normalized.setCompetitiveContext(truncateText(defaultIfBlank(normalized.getCompetitiveContext(), "Конкурентная среда требует отдельного тестирования офферов, аудиторий и креативных углов."), 400));
|
||||
normalized.setCiiLevel(truncateText(defaultIfBlank(normalized.getCiiLevel(), buildDefaultCiiLevel(res)), 200));
|
||||
normalized.setDifferentiationAdvice(truncateText(defaultIfBlank(normalized.getDifferentiationAdvice(), "Нужен отдельный message-market fit в креативах и посадочной странице, чтобы не конкурировать только ценой."), 400));
|
||||
normalized.setPrimaryCta(truncateText(defaultIfBlank(normalized.getPrimaryCta(), "Записаться на консультацию"), 120));
|
||||
normalized.setFunnelStage(truncateText(defaultIfBlank(normalized.getFunnelStage(), "Середина воронки - прогрев и квалификация лидов"), 200));
|
||||
normalized.setConversionMechanism(truncateText(defaultIfBlank(normalized.getConversionMechanism(), "Lead Ads -> менеджер -> квалификация -> продажа"), 220));
|
||||
normalized.setContentMix(normalizeTextList(
|
||||
normalized.getContentMix(),
|
||||
List.of("40% - демонстрационный контент", "30% - социальные доказательства", "20% - экспертный контент", "10% - продажи"),
|
||||
4));
|
||||
normalized.setBestPostingTimes(normalizeTextList(
|
||||
normalized.getBestPostingTimes(),
|
||||
List.of("09:00", "13:00", "19:00 - 21:00"),
|
||||
3));
|
||||
normalized.setPostingFrequency(defaultIfBlank(normalized.getPostingFrequency(), "1 раз в день в рабочие дни, 2 раза в выходные"));
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private HypothesisMatrixDto normalizeMatrix(
|
||||
HypothesisMatrixDto matrix,
|
||||
MarketingAnalysisV3Request req,
|
||||
MarketingAnalysisV3Result res,
|
||||
MarketingStrategy strategy) {
|
||||
HypothesisMatrixDto fallback = buildDefaultStrategicMatrix(req, res, strategy);
|
||||
if (matrix == null) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
List<String> rows = normalizeTextList(matrix.getRows(), fallback.getRows(), 4);
|
||||
List<String> cols = normalizeTextList(matrix.getCols(), fallback.getCols(), 4);
|
||||
List<HypothesisCellDto> fallbackCells = fallback.getMatrix();
|
||||
List<HypothesisCellDto> incoming = matrix.getMatrix() != null ? matrix.getMatrix() : List.of();
|
||||
List<HypothesisCellDto> normalizedCells = new ArrayList<>();
|
||||
|
||||
for (int row = 0; row < 4; row++) {
|
||||
for (int col = 0; col < 4; col++) {
|
||||
final int currentRow = row;
|
||||
final int currentCol = col;
|
||||
HypothesisCellDto source = incoming.stream()
|
||||
.filter(cell -> cell != null && cell.getHookIndex() == currentRow && cell.getVisualIndex() == currentCol)
|
||||
.findFirst()
|
||||
.orElse(fallbackCells.get(row * 4 + col));
|
||||
|
||||
normalizedCells.add(HypothesisCellDto.builder()
|
||||
.hookIndex(row)
|
||||
.visualIndex(col)
|
||||
.specificAdIdea(truncateText(defaultIfBlank(source.getSpecificAdIdea(),
|
||||
fallbackCells.get(row * 4 + col).getSpecificAdIdea()), 240))
|
||||
.counterHypothesis(truncateText(defaultIfBlank(source.getCounterHypothesis(),
|
||||
fallbackCells.get(row * 4 + col).getCounterHypothesis()), 220))
|
||||
.funnelStage(normalizeFunnelStage(defaultIfBlank(source.getFunnelStage(),
|
||||
fallbackCells.get(row * 4 + col).getFunnelStage())))
|
||||
.expectedOutcome(truncateText(defaultIfBlank(source.getExpectedOutcome(),
|
||||
fallbackCells.get(row * 4 + col).getExpectedOutcome()), 220))
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
return HypothesisMatrixDto.builder()
|
||||
.rowCount(4)
|
||||
.colCount(4)
|
||||
.rows(rows)
|
||||
.cols(cols)
|
||||
.matrix(normalizedCells)
|
||||
.build();
|
||||
}
|
||||
|
||||
private List<TargetingOrchestrationDto.AutomationRuleDto> normalizeRules(
|
||||
List<TargetingOrchestrationDto.AutomationRuleDto> rules,
|
||||
MarketingAnalysisV3Request req,
|
||||
MarketingAnalysisV3Result res,
|
||||
MarketingStrategy strategy) {
|
||||
List<TargetingOrchestrationDto.AutomationRuleDto> fallback = buildDefaultAutomationRules(req, res, strategy);
|
||||
if (rules == null || rules.isEmpty()) {
|
||||
return fallback;
|
||||
}
|
||||
List<TargetingOrchestrationDto.AutomationRuleDto> normalized = new ArrayList<>();
|
||||
for (TargetingOrchestrationDto.AutomationRuleDto rule : rules) {
|
||||
if (rule == null || rule.getConditions() == null || rule.getConditions().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
normalized.add(TargetingOrchestrationDto.AutomationRuleDto.builder()
|
||||
.ruleId(defaultIfBlank(rule.getRuleId(), "rule-" + (normalized.size() + 1)))
|
||||
.entityLevel(defaultIfBlank(rule.getEntityLevel(), "ADSET").toUpperCase(Locale.ROOT))
|
||||
.conditionGroup(rule.getConditionGroup() != null ? rule.getConditionGroup() : TargetingOrchestrationDto.ConditionGroup.AND)
|
||||
.conditions(rule.getConditions().stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(condition -> TargetingOrchestrationDto.ConditionDto.builder()
|
||||
.metric(defaultIfBlank(condition.getMetric(), "spend").toLowerCase(Locale.ROOT))
|
||||
.operator(condition.getOperator() != null ? condition.getOperator() : TargetingOrchestrationDto.Operator.GREATER_THAN)
|
||||
.value(condition.getValue() != null ? condition.getValue() : BigDecimal.ZERO)
|
||||
.build())
|
||||
.collect(Collectors.toList()))
|
||||
.action(defaultIfBlank(rule.getAction(), "PAUSE").toUpperCase(Locale.ROOT))
|
||||
.actionValue(rule.getActionValue() != null ? rule.getActionValue() : BigDecimal.ZERO)
|
||||
.build());
|
||||
if (normalized.size() == 3) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
while (normalized.size() < 3) {
|
||||
normalized.add(fallback.get(normalized.size()));
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private TargetingRecommendationDto buildFallbackRecommendation(
|
||||
MarketingAnalysisV3Request req,
|
||||
MarketingAnalysisV3Result res,
|
||||
MarketingStrategy strategy) {
|
||||
TargetingRecommendationDto recommendation = buildFallbackRecommendation(req, res);
|
||||
return validateAndNormalizeRecommendation(recommendation, req, res, strategy);
|
||||
}
|
||||
|
||||
private PixelCheckDto buildDefaultPixelHealth(
|
||||
MarketingAnalysisV3Request req,
|
||||
MarketingAnalysisV3Result res,
|
||||
MarketingStrategy strategy) {
|
||||
boolean capi = req != null && req.getAverageCheck() != null && "HIGH".equalsIgnoreCase(req.getAverageCheck().name());
|
||||
boolean gtm = req != null && req.getUserSocialLinks() != null && !req.getUserSocialLinks().isEmpty();
|
||||
return PixelCheckDto.builder()
|
||||
.fbp(true)
|
||||
.capi(capi)
|
||||
.gtm(gtm)
|
||||
.setupInstructions("Настройте Pixel события PageView, ViewContent и Lead, передайте fbp/fbc, подключите GTM для контроля событий и включите CAPI там, где важна стабильная атрибуция.")
|
||||
.build();
|
||||
}
|
||||
|
||||
private FunnelStrategyDto buildDefaultFunnelStrategy(
|
||||
MarketingAnalysisV3Request req,
|
||||
MarketingAnalysisV3Result res,
|
||||
MarketingStrategy strategy) {
|
||||
String niche = req != null ? safeStr(req.getBusinessNiche()) : "продукта";
|
||||
return FunnelStrategyDto.builder()
|
||||
.tof("На холодную аудиторию показываем понятную ценность " + niche + ", демонстрацию результата и мягкий CTA без давления на покупку.")
|
||||
.mof("Для Retargeting по 50%+ видео, вовлеченным и посетителям лендинга усиливаем кейсы, FAQ, social proof и снятие возражений.")
|
||||
.bof("Горячим лидам даем hard-sell оффер: дедлайн, бонус, ограничение по местам и быстрый путь до менеджера или формы.")
|
||||
.build();
|
||||
}
|
||||
|
||||
private HypothesisMatrixDto buildDefaultStrategicMatrix(
|
||||
MarketingAnalysisV3Request req,
|
||||
MarketingAnalysisV3Result res,
|
||||
MarketingStrategy strategy) {
|
||||
StrategyModel model = resolveStrategyModel(strategy);
|
||||
List<String> rows = switch (model) {
|
||||
case AUTHORITY -> List.of("Экспертный разбор проблемы", "Кейсы с цифрами и исходом", "Методология и процесс работы", "Результат и измеримый эффект");
|
||||
case TRUST -> List.of("Отзывы и подтверждения", "Гарантии и прозрачность", "Реальный процесс без прикрас", "Доказательства качества");
|
||||
case CONVERSION -> List.of("Дедлайн и ограничение", "Выгода и конкретный оффер", "Прямой призыв к действию", "Сравнение потерь и выгоды");
|
||||
case ENTRY -> List.of("Простой первый шаг", "Доступность и понятный старт", "Быстрый результат без сложности", "Минимальный барьер входа");
|
||||
};
|
||||
List<String> cols = List.of("UGC-style видео", "Карточка с оффером", "Скриншот или proof-format", "До/после или демонстрация");
|
||||
List<String> competitorStrengths = extractTopCompetitorStrengths(res);
|
||||
List<HypothesisCellDto> cells = new ArrayList<>();
|
||||
for (int row = 0; row < rows.size(); row++) {
|
||||
for (int col = 0; col < cols.size(); col++) {
|
||||
String stage = row < 2 ? "TOF" : row == 2 ? "MOF" : "BOF";
|
||||
String counter = competitorStrengths.isEmpty()
|
||||
? "Контр-гипотеза: усиливаем дифференциацию оффера и message-market fit."
|
||||
: "Контр-гипотеза: переиграть сильную сторону конкурента \"" + competitorStrengths.get((row + col) % competitorStrengths.size()) + "\" через более конкретный value proposition.";
|
||||
cells.add(HypothesisCellDto.builder()
|
||||
.hookIndex(row)
|
||||
.visualIndex(col)
|
||||
.specificAdIdea(rows.get(row) + " + " + cols.get(col) + ": показать реальный сценарий использования, оффер и следующий шаг до заявки.")
|
||||
.counterHypothesis(counter)
|
||||
.funnelStage(stage)
|
||||
.expectedOutcome("Проверить CTR, вовлечение и влияние на CPL на стадии " + stage + ".")
|
||||
.build());
|
||||
}
|
||||
}
|
||||
return HypothesisMatrixDto.builder()
|
||||
.rowCount(4)
|
||||
.colCount(4)
|
||||
.rows(rows)
|
||||
.cols(cols)
|
||||
.matrix(cells)
|
||||
.build();
|
||||
}
|
||||
|
||||
private LandingPageAuditDto buildDefaultLandingPageAudit(
|
||||
MarketingAnalysisV3Request req,
|
||||
MarketingAnalysisV3Result res,
|
||||
MarketingStrategy strategy) {
|
||||
String niche = req != null ? safeStr(req.getBusinessNiche()) : "продукта";
|
||||
return LandingPageAuditDto.builder()
|
||||
.matchScore(72)
|
||||
.executiveSummary("Лендинг для " + niche + " должен быстрее раскрывать оффер, social proof и путь до конверсии. Сейчас критична ясность первого экрана и связка с рекламным сообщением.")
|
||||
.criticalFixes(List.of(
|
||||
"Упростить первый экран: один оффер, один CTA, без перегруза текстом.",
|
||||
"Добавить кейсы, отзывы или доказательства результата выше первого сильного скролла.",
|
||||
"Сократить путь до заявки: меньше полей, понятная форма или быстрый мессенджер-канал."
|
||||
))
|
||||
.trustBoosters(List.of(
|
||||
"Показать реальные отзывы, кейсы или цифры результата с конкретикой.",
|
||||
"Добавить прозрачные условия: сроки, стоимость, что входит в оффер.",
|
||||
"Подсветить экспертность команды, реальные лица и подтверждения качества."
|
||||
))
|
||||
.build();
|
||||
}
|
||||
|
||||
private List<TargetingOrchestrationDto.AutomationRuleDto> buildDefaultAutomationRules(
|
||||
MarketingAnalysisV3Request req,
|
||||
MarketingAnalysisV3Result res,
|
||||
MarketingStrategy strategy) {
|
||||
return List.of(
|
||||
TargetingOrchestrationDto.AutomationRuleDto.builder()
|
||||
.ruleId("rule-kill-zero-leads")
|
||||
.entityLevel("ADSET")
|
||||
.conditionGroup(TargetingOrchestrationDto.ConditionGroup.AND)
|
||||
.conditions(List.of(
|
||||
buildCondition("spend", TargetingOrchestrationDto.Operator.GREATER_THAN, 6000),
|
||||
buildCondition("leads", TargetingOrchestrationDto.Operator.EQUAL, 0)
|
||||
))
|
||||
.action("PAUSE")
|
||||
.actionValue(BigDecimal.ZERO)
|
||||
.build(),
|
||||
TargetingOrchestrationDto.AutomationRuleDto.builder()
|
||||
.ruleId("rule-scale-winning-adset")
|
||||
.entityLevel("ADSET")
|
||||
.conditionGroup(TargetingOrchestrationDto.ConditionGroup.AND)
|
||||
.conditions(List.of(
|
||||
buildCondition("roas", TargetingOrchestrationDto.Operator.GREATER_THAN, 4),
|
||||
buildCondition("purchases", TargetingOrchestrationDto.Operator.GREATER_THAN_OR_EQUAL, 3)
|
||||
))
|
||||
.action("INCREASE_BUDGET")
|
||||
.actionValue(BigDecimal.valueOf(20))
|
||||
.build(),
|
||||
TargetingOrchestrationDto.AutomationRuleDto.builder()
|
||||
.ruleId("rule-stop-fatigue")
|
||||
.entityLevel("AD")
|
||||
.conditionGroup(TargetingOrchestrationDto.ConditionGroup.AND)
|
||||
.conditions(List.of(
|
||||
buildCondition("ctr", TargetingOrchestrationDto.Operator.LESS_THAN, 1),
|
||||
buildCondition("frequency", TargetingOrchestrationDto.Operator.GREATER_THAN, 3)
|
||||
))
|
||||
.action("PAUSE")
|
||||
.actionValue(BigDecimal.ZERO)
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
private ScalingPlanDto buildDefaultScalingPlan(
|
||||
MarketingAnalysisV3Request req,
|
||||
MarketingAnalysisV3Result res,
|
||||
MarketingStrategy strategy) {
|
||||
List<String> cities = req != null && req.getPromotionCities() != null && !req.getPromotionCities().isEmpty()
|
||||
? normalizeTextList(req.getPromotionCities(), List.of("Алматы", "Астана", "Шымкент"), 3)
|
||||
: List.of("Алматы", "Астана", "Шымкент");
|
||||
return ScalingPlanDto.builder()
|
||||
.horizontalScaling("Масштабируйте через новые lookalike/interest-кластеры, отдельные creative angles и раздельные placements для Reels, Stories и Feed.")
|
||||
.budgetScaling("Повышайте бюджет ступенчато на 15-20% каждые 48-72 часа только на связках с устойчивым CPL/ROAS.")
|
||||
.geoExpansion(cities)
|
||||
.build();
|
||||
}
|
||||
|
||||
private RiskAnalysisDto buildDefaultRiskAnalysis(
|
||||
MarketingAnalysisV3Request req,
|
||||
MarketingAnalysisV3Result res,
|
||||
MarketingStrategy strategy) {
|
||||
int cii = res != null && res.getCompetitionIntensity() != null ? res.getCompetitionIntensity().getCiiIndex() : 50;
|
||||
String level = cii >= 70
|
||||
? "High - высокий риск модерации и быстрого выгорания креативов."
|
||||
: cii >= 40
|
||||
? "Medium - умеренный риск, нужен контроль claims и частоты."
|
||||
: "Low - низкий риск, но формулировки все равно нужно проверять.";
|
||||
return RiskAnalysisDto.builder()
|
||||
.fatigueWarning("При текущей конкурентности креативы могут выгорать за 7-14 дней, поэтому нужно заранее готовить новую матрицу углов и визуалов.")
|
||||
.policyRiskLevel(level)
|
||||
.moderationTriggers(List.of(
|
||||
"Категоричные обещания результата без доказательств.",
|
||||
"Формулировки, давящие на страх, боль или личную уязвимость пользователя.",
|
||||
"Слишком агрессивные before/after claims, медицинские или финансовые обещания."
|
||||
))
|
||||
.build();
|
||||
}
|
||||
|
||||
private TargetingOrchestrationDto.ConditionDto buildCondition(
|
||||
String metric,
|
||||
TargetingOrchestrationDto.Operator operator,
|
||||
double value) {
|
||||
return TargetingOrchestrationDto.ConditionDto.builder()
|
||||
.metric(metric)
|
||||
.operator(operator)
|
||||
.value(BigDecimal.valueOf(value))
|
||||
.build();
|
||||
}
|
||||
|
||||
private StrategyModel resolveStrategyModel(MarketingStrategy strategy) {
|
||||
if (strategy == null || strategy.getScoringModelName() == null || strategy.getScoringModelName().isBlank()) {
|
||||
return StrategyModel.ENTRY;
|
||||
}
|
||||
try {
|
||||
return StrategyModel.valueOf(strategy.getScoringModelName().trim().toUpperCase(Locale.ROOT));
|
||||
} catch (IllegalArgumentException ex) {
|
||||
return StrategyModel.ENTRY;
|
||||
}
|
||||
}
|
||||
|
||||
private String buildDefaultCiiLevel(MarketingAnalysisV3Result res) {
|
||||
if (res == null || res.getCompetitionIntensity() == null) {
|
||||
return "Средний уровень конкуренции - нужен контролируемый тест офферов и аудиторий.";
|
||||
}
|
||||
MarketingAnalysisV3Result.CompetitionIntensity ci = res.getCompetitionIntensity();
|
||||
return safeStr(ci.getIntensityLabel()) + " (CII=" + ci.getCiiIndex() + "/100)";
|
||||
}
|
||||
|
||||
private List<String> normalizeTextList(List<String> source, List<String> fallback, int minSize) {
|
||||
LinkedHashSet<String> values = new LinkedHashSet<>();
|
||||
if (source != null) {
|
||||
source.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isBlank())
|
||||
.map(value -> truncateText(value, 220))
|
||||
.forEach(values::add);
|
||||
}
|
||||
if (fallback != null) {
|
||||
fallback.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isBlank())
|
||||
.forEach(values::add);
|
||||
}
|
||||
List<String> normalized = new ArrayList<>(values);
|
||||
while (normalized.size() < minSize && fallback != null && !fallback.isEmpty()) {
|
||||
normalized.add(fallback.get(normalized.size() % fallback.size()));
|
||||
}
|
||||
if (normalized.size() > minSize) {
|
||||
return normalized.subList(0, minSize);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private String normalizePolicyRiskLevel(String value) {
|
||||
String normalized = defaultIfBlank(value, "Medium - умеренный риск модерации.");
|
||||
String lower = normalized.toLowerCase(Locale.ROOT);
|
||||
if (lower.contains("high") || lower.contains("выс")) {
|
||||
return "High - высокий риск модерации и выгорания креативов.";
|
||||
}
|
||||
if (lower.contains("low") || lower.contains("низ")) {
|
||||
return "Low - низкий риск модерации, но формулировки все равно нужно проверять.";
|
||||
}
|
||||
return "Medium - умеренный риск модерации, нужен контроль claims и pressure tactics.";
|
||||
}
|
||||
|
||||
private String normalizeFunnelStage(String value) {
|
||||
String upper = defaultIfBlank(value, "TOF").toUpperCase(Locale.ROOT);
|
||||
if (upper.contains("BOF")) return "BOF";
|
||||
if (upper.contains("MOF")) return "MOF";
|
||||
return "TOF";
|
||||
}
|
||||
|
||||
private String defaultIfBlank(String value, String fallback) {
|
||||
return isBlank(value) ? fallback : value.trim();
|
||||
}
|
||||
|
||||
private boolean isBlank(String value) {
|
||||
return value == null || value.isBlank();
|
||||
}
|
||||
|
||||
private String truncateText(String value, int maxLen) {
|
||||
if (value == null || value.length() <= maxLen) {
|
||||
return value;
|
||||
}
|
||||
return value.substring(0, maxLen - 3).trim() + "...";
|
||||
}
|
||||
|
||||
// ── Публикация первого поста ──────────────────────────────────────────────
|
||||
|
||||
private PublishedPostDto publishFirstPost(MarketingStrategy strategy) {
|
||||
|
||||
@@ -549,10 +549,37 @@ public class TargetingCampaignService {
|
||||
.aiRecommendationsRationale(recommendation != null ? recommendation.getTypeRationale() : null)
|
||||
.quickWins(recommendation != null ? recommendation.getQuickWins() : List.of())
|
||||
.warnings(recommendation != null ? recommendation.getWarnings() : List.of())
|
||||
.technicalHealthSummary(buildTechnicalHealthSummary(recommendation))
|
||||
.lpMatchScore(recommendation != null && recommendation.getLpAudit() != null
|
||||
? recommendation.getLpAudit().getMatchScore() : null)
|
||||
.policyRiskLevel(recommendation != null && recommendation.getRisks() != null
|
||||
? recommendation.getRisks().getPolicyRiskLevel() : null)
|
||||
.suggestedAutomationRulesCount(recommendation != null && recommendation.getSuggestedRules() != null
|
||||
? recommendation.getSuggestedRules().size() : 0)
|
||||
.matrixCellsCount(recommendation != null
|
||||
&& recommendation.getHypothesisMatrix() != null
|
||||
&& recommendation.getHypothesisMatrix().getMatrix() != null
|
||||
? recommendation.getHypothesisMatrix().getMatrix().size() : 0)
|
||||
.targetingRecommendation(recommendation)
|
||||
.build();
|
||||
}
|
||||
|
||||
private String buildTechnicalHealthSummary(TargetingRecommendationDto recommendation) {
|
||||
if (recommendation == null || recommendation.getPixelHealth() == null) {
|
||||
return null;
|
||||
}
|
||||
int enabled = 0;
|
||||
if (recommendation.getPixelHealth().isFbp()) enabled++;
|
||||
if (recommendation.getPixelHealth().isCapi()) enabled++;
|
||||
if (recommendation.getPixelHealth().isGtm()) enabled++;
|
||||
|
||||
return switch (enabled) {
|
||||
case 3 -> "Strong";
|
||||
case 2 -> "Medium";
|
||||
default -> "Poor";
|
||||
};
|
||||
}
|
||||
|
||||
private TargetingAudienceProfile buildAudienceProfile(
|
||||
TargetingRecommendationDto recommendation,
|
||||
kz.konturai.parser.dto.AudienceOverrideDto overrideDto) {
|
||||
|
||||
@@ -109,9 +109,6 @@ deep-research.api.timeout=1800000
|
||||
|
||||
security.jwt.secret-base64=ZmFrZV9zZWNyZXRfMTIzNDU2Nzg5MGFiY2RlZmFrZV9zZWNyZXRfMTIzNDU2Nzg5MGFiY2Rl
|
||||
|
||||
# Claude (Anthropic) API — для AI предсказаний кампаний
|
||||
claude.api.key=${CLAUDE_API_KEY:}
|
||||
|
||||
encryption.secret-key=ZmFrZV9zZWNyZXRfMTIzNDU2Nzg5MGFiY2RlZmFrZV9zZWNyZXRfMTIzNDU2Nzg5MGFiY2Rl
|
||||
|
||||
posting.scheduler.enabled=true
|
||||
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
package kz.konturai.parser.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import kz.konturai.parser.dto.MarketingAnalysisV3Request;
|
||||
import kz.konturai.parser.dto.MarketingAnalysisV3Result;
|
||||
import kz.konturai.parser.dto.targeting.AudienceProfileDto;
|
||||
import kz.konturai.parser.dto.targeting.HypothesisCellDto;
|
||||
import kz.konturai.parser.dto.targeting.HypothesisMatrixDto;
|
||||
import kz.konturai.parser.dto.targeting.LandingPageAuditDto;
|
||||
import kz.konturai.parser.dto.targeting.RiskAnalysisDto;
|
||||
import kz.konturai.parser.dto.targeting.TargetingBudgetDto;
|
||||
import kz.konturai.parser.dto.targeting.TargetingRecommendationDto;
|
||||
import kz.konturai.parser.enums.AverageCheck;
|
||||
import kz.konturai.parser.model.MarketingStrategy;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class AiTargetingSystemServiceManagementLayerTest {
|
||||
|
||||
private final AiTargetingSystemService service =
|
||||
new AiTargetingSystemService(null, new ObjectMapper(), null, null, null, null);
|
||||
|
||||
@Test
|
||||
void validateAndNormalizeRecommendation_shouldFillManagementBlocksAndClampValues() throws Exception {
|
||||
MarketingAnalysisV3Request req = new MarketingAnalysisV3Request();
|
||||
req.setBusinessNiche("Стоматология");
|
||||
req.setPromotionCities(List.of("Алматы"));
|
||||
req.setAverageCheck(AverageCheck.HIGH);
|
||||
req.setUserSocialLinks(List.of("https://site.kz", "https://instagram.com/brand"));
|
||||
|
||||
MarketingAnalysisV3Result.CompetitionIntensity intensity = new MarketingAnalysisV3Result.CompetitionIntensity();
|
||||
intensity.setCiiIndex(82);
|
||||
intensity.setIntensityLabel("Высокий");
|
||||
|
||||
MarketingAnalysisV3Result.CompetitorProfile competitor = new MarketingAnalysisV3Result.CompetitorProfile();
|
||||
competitor.setName("Strong Clinic");
|
||||
competitor.setFollowers(12000);
|
||||
competitor.setStrengths(List.of("Много видео-отзывов", "Сильный social proof"));
|
||||
|
||||
MarketingAnalysisV3Result res = new MarketingAnalysisV3Result();
|
||||
res.setCompetitionIntensity(intensity);
|
||||
res.setCompetitorMap(List.of(competitor));
|
||||
|
||||
MarketingStrategy strategy = new MarketingStrategy();
|
||||
strategy.setScoringModelName("TRUST");
|
||||
strategy.setScoringModelTitle("Trust");
|
||||
strategy.setStatus("completed");
|
||||
strategy.setDurationWeeks(4);
|
||||
|
||||
TargetingRecommendationDto partial = TargetingRecommendationDto.builder()
|
||||
.recommendedType("MIXED")
|
||||
.recommendedTypeLabel("Комбинированный таргетинг")
|
||||
.typeRationale("Базовая логика")
|
||||
.campaignObjective("LEADS")
|
||||
.campaignObjectiveLabel("Лиды")
|
||||
.campaignObjectiveRationale("Нужно больше заявок")
|
||||
.audienceProfile(AudienceProfileDto.builder()
|
||||
.primarySegment("Жители Алматы")
|
||||
.ageRange("25-44")
|
||||
.gender("Женщины 60%, Мужчины 40%")
|
||||
.build())
|
||||
.budgetRecommendation(TargetingBudgetDto.builder()
|
||||
.dailyBudget("10 000 KZT")
|
||||
.monthlyBudget("300 000 KZT")
|
||||
.build())
|
||||
.hypothesisMatrix(HypothesisMatrixDto.builder()
|
||||
.rowCount(1)
|
||||
.colCount(1)
|
||||
.rows(List.of("Только один angle"))
|
||||
.cols(List.of("Только один visual"))
|
||||
.matrix(List.of(HypothesisCellDto.builder()
|
||||
.hookIndex(0)
|
||||
.visualIndex(0)
|
||||
.specificAdIdea("")
|
||||
.build()))
|
||||
.build())
|
||||
.lpAudit(LandingPageAuditDto.builder()
|
||||
.matchScore(140)
|
||||
.criticalFixes(List.of())
|
||||
.trustBoosters(List.of())
|
||||
.build())
|
||||
.risks(RiskAnalysisDto.builder()
|
||||
.policyRiskLevel("высокий")
|
||||
.moderationTriggers(List.of())
|
||||
.build())
|
||||
.suggestedRules(List.of())
|
||||
.build();
|
||||
|
||||
Method method = AiTargetingSystemService.class.getDeclaredMethod(
|
||||
"validateAndNormalizeRecommendation",
|
||||
TargetingRecommendationDto.class,
|
||||
MarketingAnalysisV3Request.class,
|
||||
MarketingAnalysisV3Result.class,
|
||||
MarketingStrategy.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
TargetingRecommendationDto normalized = (TargetingRecommendationDto) method.invoke(
|
||||
service, partial, req, res, strategy);
|
||||
|
||||
assertNotNull(normalized.getPixelHealth());
|
||||
assertNotNull(normalized.getFunnelStrategy());
|
||||
assertNotNull(normalized.getHypothesisMatrix());
|
||||
assertNotNull(normalized.getLpAudit());
|
||||
assertNotNull(normalized.getScalingPlan());
|
||||
assertNotNull(normalized.getRisks());
|
||||
assertEquals(4, normalized.getHypothesisMatrix().getRowCount());
|
||||
assertEquals(4, normalized.getHypothesisMatrix().getColCount());
|
||||
assertEquals(16, normalized.getHypothesisMatrix().getMatrix().size());
|
||||
assertEquals(100, normalized.getLpAudit().getMatchScore());
|
||||
assertEquals(3, normalized.getLpAudit().getCriticalFixes().size());
|
||||
assertEquals(3, normalized.getLpAudit().getTrustBoosters().size());
|
||||
assertEquals(3, normalized.getSuggestedRules().size());
|
||||
assertEquals(3, normalized.getRisks().getModerationTriggers().size());
|
||||
assertTrue(normalized.getRisks().getPolicyRiskLevel().startsWith("High"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildEnhancedFullContext_shouldIncludeContextEnrichment() throws Exception {
|
||||
MarketingAnalysisV3Request req = new MarketingAnalysisV3Request();
|
||||
req.setBusinessNiche("Онлайн-курсы");
|
||||
req.setProductName("Course X");
|
||||
req.setGoal("Рост лидов");
|
||||
req.setPromotionCities(List.of("Астана"));
|
||||
req.setUserSocialLinks(List.of("https://site.kz", "https://instagram.com/coursex"));
|
||||
|
||||
MarketingAnalysisV3Result.CompetitionIntensity intensity = new MarketingAnalysisV3Result.CompetitionIntensity();
|
||||
intensity.setCiiIndex(68);
|
||||
intensity.setIntensityLabel("Средний");
|
||||
|
||||
MarketingAnalysisV3Result.ContentProfile contentProfile = new MarketingAnalysisV3Result.ContentProfile();
|
||||
contentProfile.setVideoShare(74.0);
|
||||
|
||||
MarketingAnalysisV3Result.CompetitorProfile competitor = new MarketingAnalysisV3Result.CompetitorProfile();
|
||||
competitor.setName("Competitor A");
|
||||
competitor.setFollowers(9000);
|
||||
competitor.setStrengths(List.of("Сильный экспертный контент"));
|
||||
|
||||
MarketingAnalysisV3Result res = new MarketingAnalysisV3Result();
|
||||
res.setCompetitionIntensity(intensity);
|
||||
res.setContentProfile(contentProfile);
|
||||
res.setCompetitorMap(List.of(competitor));
|
||||
|
||||
MarketingStrategy strategy = new MarketingStrategy();
|
||||
strategy.setScoringModelName("AUTHORITY");
|
||||
strategy.setScoringModelTitle("Authority");
|
||||
strategy.setStatus("completed");
|
||||
strategy.setDurationWeeks(4);
|
||||
|
||||
Method method = AiTargetingSystemService.class.getDeclaredMethod(
|
||||
"buildEnhancedFullContext",
|
||||
MarketingAnalysisV3Request.class,
|
||||
MarketingAnalysisV3Result.class,
|
||||
MarketingStrategy.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
String context = (String) method.invoke(service, req, res, strategy);
|
||||
|
||||
assertTrue(context.contains("https://instagram.com/coursex"));
|
||||
assertTrue(context.contains("Competitor A"));
|
||||
assertTrue(context.contains("AUTHORITY"));
|
||||
assertTrue(context.contains("CURRENT DIGITAL ASSETS"));
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
package kz.konturai.parser.service;
|
||||
|
||||
import kz.konturai.parser.dto.MarketingAnalysisV3Request;
|
||||
import kz.konturai.parser.dto.TargetingAIAnalysisResult;
|
||||
import kz.konturai.parser.dto.targeting.HypothesisCellDto;
|
||||
import kz.konturai.parser.dto.targeting.HypothesisMatrixDto;
|
||||
import kz.konturai.parser.dto.targeting.LandingPageAuditDto;
|
||||
import kz.konturai.parser.dto.targeting.PixelCheckDto;
|
||||
import kz.konturai.parser.dto.targeting.RiskAnalysisDto;
|
||||
import kz.konturai.parser.dto.targeting.TargetingOrchestrationDto;
|
||||
import kz.konturai.parser.dto.targeting.TargetingRecommendationDto;
|
||||
import kz.konturai.parser.model.MarketingAnalysisV3Document;
|
||||
import kz.konturai.parser.model.MarketingStrategy;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
|
||||
class TargetingCampaignServiceManagementLayerTest {
|
||||
|
||||
private final TargetingCampaignService service =
|
||||
new TargetingCampaignService(null, null, null, null, null, null, null, null, null);
|
||||
|
||||
@Test
|
||||
void buildAiAnalysisResult_shouldPopulateSummaryFields() throws Exception {
|
||||
MarketingStrategy strategy = new MarketingStrategy();
|
||||
strategy.setId("strategy-1");
|
||||
strategy.setAnalysisId("analysis-1");
|
||||
|
||||
MarketingAnalysisV3Request request = new MarketingAnalysisV3Request();
|
||||
request.setBusinessNiche("Автосервис");
|
||||
request.setProductName("Oil Change");
|
||||
request.setGoal("Лиды");
|
||||
|
||||
MarketingAnalysisV3Document analysis = new MarketingAnalysisV3Document();
|
||||
analysis.setRequestData(request);
|
||||
|
||||
List<HypothesisCellDto> cells = new ArrayList<>();
|
||||
for (int i = 0; i < 16; i++) {
|
||||
cells.add(HypothesisCellDto.builder()
|
||||
.hookIndex(i / 4)
|
||||
.visualIndex(i % 4)
|
||||
.specificAdIdea("Idea " + i)
|
||||
.build());
|
||||
}
|
||||
|
||||
List<TargetingOrchestrationDto.AutomationRuleDto> rules = List.of(
|
||||
buildRule("rule-1"),
|
||||
buildRule("rule-2"),
|
||||
buildRule("rule-3"));
|
||||
|
||||
TargetingRecommendationDto recommendation = TargetingRecommendationDto.builder()
|
||||
.recommendedType("MIXED")
|
||||
.recommendedTypeLabel("Комбинированный")
|
||||
.campaignObjective("LEADS")
|
||||
.questionnaireSignals(List.of("signal"))
|
||||
.typeRationale("reason")
|
||||
.quickWins(List.of("win"))
|
||||
.warnings(List.of("warning"))
|
||||
.pixelHealth(PixelCheckDto.builder().fbp(true).capi(true).gtm(true).build())
|
||||
.lpAudit(LandingPageAuditDto.builder().matchScore(88).build())
|
||||
.risks(RiskAnalysisDto.builder().policyRiskLevel("High - высокий риск").build())
|
||||
.suggestedRules(rules)
|
||||
.hypothesisMatrix(HypothesisMatrixDto.builder().matrix(cells).build())
|
||||
.build();
|
||||
|
||||
Method method = TargetingCampaignService.class.getDeclaredMethod(
|
||||
"buildAiAnalysisResult",
|
||||
MarketingStrategy.class,
|
||||
MarketingAnalysisV3Document.class,
|
||||
TargetingRecommendationDto.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
TargetingAIAnalysisResult result = (TargetingAIAnalysisResult) method.invoke(
|
||||
service, strategy, analysis, recommendation);
|
||||
|
||||
assertEquals("Strong", result.getTechnicalHealthSummary());
|
||||
assertEquals(88, result.getLpMatchScore());
|
||||
assertEquals("High - высокий риск", result.getPolicyRiskLevel());
|
||||
assertEquals(3, result.getSuggestedAutomationRulesCount());
|
||||
assertEquals(16, result.getMatrixCellsCount());
|
||||
assertSame(recommendation, result.getTargetingRecommendation());
|
||||
}
|
||||
|
||||
private TargetingOrchestrationDto.AutomationRuleDto buildRule(String id) {
|
||||
return TargetingOrchestrationDto.AutomationRuleDto.builder()
|
||||
.ruleId(id)
|
||||
.entityLevel("ADSET")
|
||||
.conditionGroup(TargetingOrchestrationDto.ConditionGroup.AND)
|
||||
.conditions(List.of(TargetingOrchestrationDto.ConditionDto.builder()
|
||||
.metric("spend")
|
||||
.operator(TargetingOrchestrationDto.Operator.GREATER_THAN)
|
||||
.value(BigDecimal.TEN)
|
||||
.build()))
|
||||
.action("PAUSE")
|
||||
.actionValue(BigDecimal.ZERO)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user