.
This commit is contained in:
@@ -0,0 +1,340 @@
|
||||
# Учетные данные Facebook для запуска рекламы
|
||||
|
||||
## Обзор
|
||||
|
||||
Для запуска рекламных кампаний в Facebook через Marketing API (ранее Ads API) требуются специальные учетные данные, которые отличаются от простого Access Token для публикации постов.
|
||||
|
||||
---
|
||||
|
||||
## Необходимые учетные данные
|
||||
|
||||
### 1. **Access Token (обязательно)**
|
||||
|
||||
Access Token с расширенными разрешениями для управления рекламой.
|
||||
|
||||
**Требуемые разрешения (Permissions):**
|
||||
|
||||
- `ads_management` - Управление рекламными кампаниями
|
||||
- `ads_read` - Чтение данных о рекламе
|
||||
- `business_management` - Управление бизнес-аккаунтом
|
||||
- `pages_read_engagement` - Чтение данных страниц (опционально)
|
||||
|
||||
**Типы токенов:**
|
||||
|
||||
- **User Access Token** - краткосрочный (1-2 часа)
|
||||
- **Long-Lived User Access Token** - долгосрочный (60 дней)
|
||||
- **Page Access Token** - для управления страницами
|
||||
- **System User Access Token** - для серверных приложений (рекомендуется для продакшена)
|
||||
|
||||
---
|
||||
|
||||
### 2. **Ad Account ID (обязательно)**
|
||||
|
||||
ID рекламного аккаунта Facebook, в котором будут создаваться кампании.
|
||||
|
||||
**Формат:** `act_XXXXXXXXX` (например: `act_123456789`)
|
||||
|
||||
**Где найти:**
|
||||
|
||||
1. Перейдите в [Facebook Ads Manager](https://business.facebook.com/adsmanager)
|
||||
2. В настройках аккаунта найдите "Account ID"
|
||||
3. Или используйте API: `GET /me/adaccounts`
|
||||
|
||||
---
|
||||
|
||||
### 3. **App ID и App Secret (обязательно для серверных приложений)**
|
||||
|
||||
Учетные данные Facebook приложения.
|
||||
|
||||
**Где найти:**
|
||||
|
||||
1. Перейдите на [Facebook Developers](https://developers.facebook.com/)
|
||||
2. Выберите ваше приложение
|
||||
3. В разделе "Settings" → "Basic" найдите:
|
||||
- **App ID**
|
||||
- **App Secret** (нажмите "Show" для отображения)
|
||||
|
||||
**Важно:** App Secret должен храниться в безопасности и никогда не передаваться на клиент.
|
||||
|
||||
---
|
||||
|
||||
### 4. **Page ID (опционально, но рекомендуется)**
|
||||
|
||||
ID страницы Facebook, связанной с рекламным аккаунтом.
|
||||
|
||||
**Где найти:**
|
||||
|
||||
1. Перейдите на вашу страницу Facebook
|
||||
2. В настройках страницы найдите "Page ID"
|
||||
3. Или используйте API: `GET /me/accounts`
|
||||
|
||||
---
|
||||
|
||||
## Пошаговая инструкция получения учетных данных
|
||||
|
||||
### Шаг 1: Создание Facebook приложения
|
||||
|
||||
1. Перейдите на [Facebook Developers](https://developers.facebook.com/)
|
||||
2. Нажмите "My Apps" → "Create App"
|
||||
3. Выберите тип приложения: **"Business"** или **"Other"**
|
||||
4. Заполните название и контактный email
|
||||
5. Нажмите "Create App"
|
||||
|
||||
### Шаг 2: Добавление продукта "Marketing API"
|
||||
|
||||
1. В панели управления приложением найдите раздел "Add Products"
|
||||
2. Найдите "Marketing API" и нажмите "Set Up"
|
||||
3. Следуйте инструкциям для настройки
|
||||
|
||||
### Шаг 3: Получение App ID и App Secret
|
||||
|
||||
1. В левом меню выберите "Settings" → "Basic"
|
||||
2. Скопируйте **App ID**
|
||||
3. Нажмите "Show" рядом с **App Secret** и скопируйте его
|
||||
4. **Сохраните эти данные в безопасном месте**
|
||||
|
||||
### Шаг 4: Настройка разрешений (Permissions)
|
||||
|
||||
1. В левом меню выберите "Settings" → "Advanced"
|
||||
2. Добавьте в "Valid OAuth Redirect URIs" ваш callback URL
|
||||
3. В разделе "Permissions and Features" запросите:
|
||||
- `ads_management`
|
||||
- `ads_read`
|
||||
- `business_management`
|
||||
- `pages_read_engagement`
|
||||
|
||||
### Шаг 5: Получение Access Token
|
||||
|
||||
#### Вариант A: User Access Token (для тестирования)
|
||||
|
||||
1. Перейдите в [Graph API Explorer](https://developers.facebook.com/tools/explorer/)
|
||||
2. Выберите ваше приложение
|
||||
3. Нажмите "Generate Access Token"
|
||||
4. Выберите необходимые разрешения
|
||||
5. Скопируйте полученный токен
|
||||
|
||||
#### Вариант B: Long-Lived Token (для разработки)
|
||||
|
||||
```bash
|
||||
# Обмен краткосрочного токена на долгосрочный
|
||||
curl -X GET "https://graph.facebook.com/v18.0/oauth/access_token?grant_type=fb_exchange_token&client_id={app-id}&client_secret={app-secret}&fb_exchange_token={short-lived-token}"
|
||||
```
|
||||
|
||||
#### Вариант C: System User Token (для продакшена - рекомендуется)
|
||||
|
||||
1. В панели управления приложением перейдите в "Business Settings"
|
||||
2. Создайте System User
|
||||
3. Назначьте ему доступ к рекламному аккаунту
|
||||
4. Сгенерируйте токен для System User
|
||||
|
||||
### Шаг 6: Получение Ad Account ID
|
||||
|
||||
**Через Ads Manager:**
|
||||
|
||||
1. Перейдите в [Facebook Ads Manager](https://business.facebook.com/adsmanager)
|
||||
2. В настройках аккаунта найдите "Account ID"
|
||||
|
||||
**Через API:**
|
||||
|
||||
```bash
|
||||
curl -X GET "https://graph.facebook.com/v18.0/me/adaccounts?access_token={access-token}"
|
||||
```
|
||||
|
||||
Ответ будет содержать массив с `id` в формате `act_XXXXXXXXX`.
|
||||
|
||||
---
|
||||
|
||||
## Структура учетных данных для вашего API
|
||||
|
||||
Для интеграции с вашей системой, учетные данные Facebook для рекламы должны быть сохранены в следующем формате:
|
||||
|
||||
### Формат JSON для сохранения credentials
|
||||
|
||||
```json
|
||||
{
|
||||
"platform": "facebook_ads",
|
||||
"credentials": {
|
||||
"accessToken": "EAABwzLix...",
|
||||
"adAccountId": "act_123456789",
|
||||
"appId": "1234567890123456",
|
||||
"appSecret": "your-app-secret-here",
|
||||
"pageId": "1234567890123456",
|
||||
"tokenType": "LONG_LIVED",
|
||||
"expiresAt": "2024-12-31T23:59:59Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Пример сохранения через API
|
||||
|
||||
```javascript
|
||||
const facebookAdsCredentials = {
|
||||
platform: 'facebook_ads',
|
||||
credentials: JSON.stringify({
|
||||
accessToken: 'EAABwzLix...',
|
||||
adAccountId: 'act_123456789',
|
||||
appId: '1234567890123456',
|
||||
appSecret: 'your-app-secret-here',
|
||||
pageId: '1234567890123456',
|
||||
tokenType: 'LONG_LIVED',
|
||||
expiresAt: '2024-12-31T23:59:59Z',
|
||||
}),
|
||||
};
|
||||
|
||||
// Сохранение через ваш API
|
||||
const response = await fetch('/api/social-media/credentials', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwtToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(facebookAdsCredentials),
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Требования и ограничения
|
||||
|
||||
### Ограничения Facebook Marketing API
|
||||
|
||||
1. **Rate Limits:**
|
||||
|
||||
- 200 вызовов в час на пользователя
|
||||
- 4800 вызовов в час на приложение
|
||||
|
||||
2. **Минимальные требования:**
|
||||
|
||||
- Рекламный аккаунт должен быть активен
|
||||
- У пользователя должны быть права администратора на аккаунте
|
||||
- Приложение должно пройти ревью Facebook (для продакшена)
|
||||
|
||||
3. **Версия API:**
|
||||
- Текущая версия: v18.0
|
||||
- Facebook регулярно обновляет API, следите за изменениями
|
||||
|
||||
### Безопасность
|
||||
|
||||
1. **Никогда не храните App Secret в открытом виде**
|
||||
2. **Используйте шифрование для хранения credentials** (ваша система уже использует шифрование)
|
||||
3. **Регулярно обновляйте токены** (Long-Lived токены истекают через 60 дней)
|
||||
4. **Используйте System User Token для продакшена** вместо User Token
|
||||
|
||||
---
|
||||
|
||||
## Проверка учетных данных
|
||||
|
||||
### Проверка Access Token
|
||||
|
||||
```bash
|
||||
curl -X GET "https://graph.facebook.com/v18.0/me?access_token={access-token}"
|
||||
```
|
||||
|
||||
Если токен валиден, вы получите информацию о пользователе.
|
||||
|
||||
### Проверка доступа к Ad Account
|
||||
|
||||
```bash
|
||||
curl -X GET "https://graph.facebook.com/v18.0/{ad-account-id}?access_token={access-token}&fields=id,name,account_id"
|
||||
```
|
||||
|
||||
Если доступ есть, вы получите информацию об аккаунте.
|
||||
|
||||
### Проверка разрешений
|
||||
|
||||
```bash
|
||||
curl -X GET "https://graph.facebook.com/v18.0/me/permissions?access_token={access-token}"
|
||||
```
|
||||
|
||||
Проверьте, что в ответе есть:
|
||||
|
||||
- `ads_management` со статусом `granted`
|
||||
- `ads_read` со статусом `granted`
|
||||
- `business_management` со статусом `granted`
|
||||
|
||||
---
|
||||
|
||||
## Примеры использования для создания рекламной кампании
|
||||
|
||||
### Создание кампании
|
||||
|
||||
```bash
|
||||
curl -X POST "https://graph.facebook.com/v18.0/{ad-account-id}/campaigns" \
|
||||
-d "name=Test Campaign" \
|
||||
-d "objective=OUTCOME_TRAFFIC" \
|
||||
-d "status=PAUSED" \
|
||||
-d "access_token={access-token}"
|
||||
```
|
||||
|
||||
### Создание Ad Set
|
||||
|
||||
```bash
|
||||
curl -X POST "https://graph.facebook.com/v18.0/{ad-account-id}/adsets" \
|
||||
-d "name=Test Ad Set" \
|
||||
-d "campaign_id={campaign-id}" \
|
||||
-d "billing_event=IMPRESSIONS" \
|
||||
-d "optimization_goal=REACH" \
|
||||
-d "bid_amount=100" \
|
||||
-d "daily_budget=1000" \
|
||||
-d "targeting={'geo_locations':{'countries':['KZ']}}" \
|
||||
-d "access_token={access-token}"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Обновление токенов
|
||||
|
||||
Access Token имеет срок действия. Для автоматического обновления:
|
||||
|
||||
1. **Отслеживайте срок действия токена** (`expiresAt`)
|
||||
2. **Используйте refresh token** (если доступен)
|
||||
3. **Или запросите новый токен** перед истечением старого
|
||||
|
||||
### Обмен краткосрочного токена на долгосрочный
|
||||
|
||||
```javascript
|
||||
async function exchangeToken(shortLivedToken, appId, appSecret) {
|
||||
const response = await fetch(
|
||||
`https://graph.facebook.com/v18.0/oauth/access_token?grant_type=fb_exchange_token&client_id=${appId}&client_secret=${appSecret}&fb_exchange_token=${shortLivedToken}`
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
accessToken: data.access_token,
|
||||
expiresIn: data.expires_in, // в секундах
|
||||
expiresAt: new Date(Date.now() + data.expires_in * 1000).toISOString(),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Рекомендации
|
||||
|
||||
1. **Для разработки:** Используйте Long-Lived User Access Token
|
||||
2. **Для продакшена:** Используйте System User Access Token
|
||||
3. **Храните credentials в зашифрованном виде** (ваша система уже это делает)
|
||||
4. **Реализуйте автоматическое обновление токенов**
|
||||
5. **Логируйте все операции с рекламой** для отладки
|
||||
6. **Обрабатывайте ошибки API** (rate limits, invalid tokens, etc.)
|
||||
|
||||
---
|
||||
|
||||
## Полезные ссылки
|
||||
|
||||
- [Facebook Marketing API Documentation](https://developers.facebook.com/docs/marketing-apis)
|
||||
- [Facebook Graph API Explorer](https://developers.facebook.com/tools/explorer/)
|
||||
- [Facebook Business Settings](https://business.facebook.com/settings)
|
||||
- [Facebook Ads Manager](https://business.facebook.com/adsmanager)
|
||||
- [Access Token Debugger](https://developers.facebook.com/tools/debug/accesstoken/)
|
||||
|
||||
---
|
||||
|
||||
## Поддержка
|
||||
|
||||
При возникновении проблем с получением или использованием учетных данных:
|
||||
|
||||
1. Проверьте документацию Facebook Marketing API
|
||||
2. Используйте [Access Token Debugger](https://developers.facebook.com/tools/debug/accesstoken/) для проверки токена
|
||||
3. Убедитесь, что все разрешения запрошены и одобрены
|
||||
4. Проверьте, что рекламный аккаунт активен и имеет необходимые права
|
||||
@@ -586,6 +586,17 @@ try {
|
||||
- Для долгосрочного использования рекомендуется использовать Long-Lived Token
|
||||
- Токен должен иметь разрешения `pages_manage_posts` для публикации
|
||||
|
||||
**Для запуска рекламных кампаний в Facebook требуется дополнительная настройка:**
|
||||
|
||||
📖 **Подробная инструкция:** См. [FACEBOOK_ADS_CREDENTIALS.md](./FACEBOOK_ADS_CREDENTIALS.md)
|
||||
|
||||
Для рекламы нужны:
|
||||
|
||||
- Access Token с разрешениями `ads_management`, `ads_read`, `business_management`
|
||||
- Ad Account ID (формат: `act_XXXXXXXXX`)
|
||||
- App ID и App Secret
|
||||
- Page ID (опционально)
|
||||
|
||||
---
|
||||
|
||||
## Примеры React компонентов
|
||||
|
||||
@@ -0,0 +1,700 @@
|
||||
# API Документация: Обновленные поля для генерации бизнес-анализа (Frontend/AI Agent)
|
||||
|
||||
## Обзор изменений
|
||||
|
||||
API для генерации маркетингового анализа был обновлен с новыми полями, которые более точно отражают требования бизнес-анализа. Все старые поля были заменены новыми для улучшения качества анализа.
|
||||
|
||||
---
|
||||
|
||||
## Изменения в структуре запроса
|
||||
|
||||
### Удаленные поля (больше не используются)
|
||||
|
||||
Следующие поля были **удалены** из API и больше не принимаются:
|
||||
|
||||
- ❌ `location` - заменено на `region`
|
||||
- ❌ `client` - заменено на `targetAudience`
|
||||
- ❌ `differentiator` - заменено на комбинацию `businessNiche`, `strongSide`, `weakSide`
|
||||
|
||||
### Новые обязательные поля
|
||||
|
||||
| Поле | Тип | Обязательный | Описание | Пример значения |
|
||||
| ---------------- | ------ | ------------ | -------------------------------------- | ------------------------------------------------------ |
|
||||
| `businessNiche` | string | ✅ | Ниша бизнеса | "E-commerce платформы" |
|
||||
| `product` | string | ✅ | Продукт или услуга | "Разработка мобильных приложений" |
|
||||
| `targetAudience` | string | ✅ | Целевая аудитория (детальное описание) | "Малый и средний бизнес, владельцы интернет-магазинов" |
|
||||
| `region` | string | ✅ | Регион (город Казахстана) | "Алматы" |
|
||||
| `goal` | string | ✅ | Цель на 6-12 месяцев | "Увеличить количество клиентов на 50%" |
|
||||
| `detailLevel` | string | ✅ | Уровень детализации анализа | "СТАНДАРТНО" |
|
||||
| `analysisType` | string | ✅ | Тип анализа | "РЫНОК" |
|
||||
|
||||
### Новые опциональные поля
|
||||
|
||||
| Поле | Тип | Обязательный | Описание | Пример значения |
|
||||
| ------------ | ------ | ------------ | ----------------------- | ------------------------------------------------- |
|
||||
| `strongSide` | string | ❌ | Сильная сторона бизнеса | "Опытная команда разработчиков, быстрая доставка" |
|
||||
| `weakSide` | string | ❌ | Слабая сторона бизнеса | "Ограниченный маркетинговый бюджет" |
|
||||
|
||||
---
|
||||
|
||||
## Детальное описание полей
|
||||
|
||||
### 1. `businessNiche` (обязательное)
|
||||
|
||||
**Описание**: Ниша бизнеса, в которой работает компания.
|
||||
|
||||
**Валидация**:
|
||||
|
||||
- Минимальная длина: 3 символа
|
||||
- Максимальная длина: 200 символов
|
||||
- Разрешены: буквы, цифры, пробелы, дефисы, запятые
|
||||
- Паттерн: `^[\p{L}\p{N}\s\-,]+$`
|
||||
|
||||
**Примеры**:
|
||||
|
||||
```json
|
||||
"businessNiche": "E-commerce платформы"
|
||||
"businessNiche": "Образовательные технологии"
|
||||
"businessNiche": "Финансовые услуги для малого бизнеса"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. `product` (обязательное)
|
||||
|
||||
**Описание**: Конкретный продукт или услуга, которую предоставляет компания.
|
||||
|
||||
**Валидация**:
|
||||
|
||||
- Минимальная длина: 3 символа
|
||||
- Максимальная длина: 200 символов
|
||||
- Разрешены: буквы, цифры, пробелы, дефисы, запятые
|
||||
- Паттерн: `^[\p{L}\p{N}\s\-,]+$`
|
||||
|
||||
**Примеры**:
|
||||
|
||||
```json
|
||||
"product": "Разработка мобильных приложений"
|
||||
"product": "Консультации по маркетингу"
|
||||
"product": "Веб-разработка и дизайн"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. `targetAudience` (обязательное)
|
||||
|
||||
**Описание**: Детальное описание целевой аудитории. Это поле заменяет старое поле `client` и должно содержать более подробную информацию.
|
||||
|
||||
**Валидация**:
|
||||
|
||||
- Минимальная длина: 3 символа
|
||||
- Максимальная длина: 300 символов
|
||||
- Разрешены любые символы
|
||||
|
||||
**Примеры**:
|
||||
|
||||
```json
|
||||
"targetAudience": "Малый и средний бизнес, владельцы интернет-магазинов в возрасте 30-50 лет"
|
||||
"targetAudience": "Стартапы и технологические компании, нуждающиеся в быстрой разработке MVP"
|
||||
"targetAudience": "Частные лица, желающие создать личный бренд в социальных сетях"
|
||||
```
|
||||
|
||||
**Примечание**: В отличие от старого поля `client`, которое принимало только фиксированные значения, `targetAudience` принимает свободный текст для более гибкого описания.
|
||||
|
||||
---
|
||||
|
||||
### 4. `region` (обязательное)
|
||||
|
||||
**Описание**: Регион (город Казахстана), в котором работает бизнес. Это поле заменяет старое поле `location`.
|
||||
|
||||
**Валидация**:
|
||||
|
||||
- Должно быть одним из допустимых городов Казахстана
|
||||
- Проверка выполняется через валидатор `@ValidRegion`
|
||||
|
||||
**Допустимые значения** (точное совпадение):
|
||||
|
||||
- `"Алматы"`
|
||||
- `"Астана"`
|
||||
- `"Шымкент"`
|
||||
- `"Караганда"`
|
||||
- `"Актобе"`
|
||||
- `"Тараз"`
|
||||
- `"Павлодар"`
|
||||
- `"Усть-Каменогорск"`
|
||||
- `"Семей"`
|
||||
- `"Костанай"`
|
||||
- `"Кызылорда"`
|
||||
- `"Уральск"`
|
||||
- `"Петропавловск"`
|
||||
- `"Атырау"`
|
||||
- `"Актау"`
|
||||
- `"Туркестан"`
|
||||
- `"Кокшетау"`
|
||||
- `"Талдыкорган"`
|
||||
- `"Экибастуз"`
|
||||
- `"Рудный"`
|
||||
|
||||
**Примеры**:
|
||||
|
||||
```json
|
||||
"region": "Алматы"
|
||||
"region": "Астана"
|
||||
"region": "Шымкент"
|
||||
```
|
||||
|
||||
**Важно**: Значение должно точно совпадать с одним из допустимых городов (регистр важен).
|
||||
|
||||
---
|
||||
|
||||
### 5. `goal` (обязательное)
|
||||
|
||||
**Описание**: Цель бизнеса на период 6-12 месяцев. Это новое поле, которое помогает AI лучше понять приоритеты бизнеса.
|
||||
|
||||
**Валидация**:
|
||||
|
||||
- Минимальная длина: 10 символов
|
||||
- Максимальная длина: 500 символов
|
||||
- Разрешены любые символы
|
||||
|
||||
**Примеры**:
|
||||
|
||||
```json
|
||||
"goal": "Увеличить количество клиентов на 50% за следующие 6 месяцев"
|
||||
"goal": "Выйти на рынок соседних регионов и открыть 3 новых филиала"
|
||||
"goal": "Повысить узнаваемость бренда и увеличить продажи через онлайн-каналы на 30%"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. `detailLevel` (обязательное)
|
||||
|
||||
**Описание**: Уровень детализации анализа. Влияет на объем и глубину генерируемого анализа.
|
||||
|
||||
**Валидация**:
|
||||
|
||||
- Должно быть одним из допустимых значений
|
||||
- Проверка выполняется через валидатор `@ValidDetailLevel`
|
||||
|
||||
**Допустимые значения** (точное совпадение, регистр важен):
|
||||
|
||||
- `"КРАТКО"` - Краткий анализ (1-2 абзаца)
|
||||
- `"СТАНДАРТНО"` - Стандартный анализ (3-5 абзацев) - **рекомендуется по умолчанию**
|
||||
- `"ПОДРОБНО"` - Подробный анализ (5-8 абзацев)
|
||||
|
||||
**Влияние на анализ**:
|
||||
|
||||
| Уровень | Длина ответов | Количество рекомендаций | Детализация стратегии |
|
||||
| ------------ | ------------- | ----------------------- | ------------------------ |
|
||||
| `КРАТКО` | 1-2 абзаца | 3-4 рекомендации | Краткая |
|
||||
| `СТАНДАРТНО` | 3-5 абзацев | 4-6 рекомендаций | Стандартная |
|
||||
| `ПОДРОБНО` | 5-8 абзацев | 6-8 рекомендаций | Детальная с обоснованием |
|
||||
|
||||
**Примеры**:
|
||||
|
||||
```json
|
||||
"detailLevel": "КРАТКО"
|
||||
"detailLevel": "СТАНДАРТНО"
|
||||
"detailLevel": "ПОДРОБНО"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. `strongSide` (опциональное)
|
||||
|
||||
**Описание**: Сильная сторона бизнеса. Помогает AI лучше понять конкурентные преимущества.
|
||||
|
||||
**Валидация**:
|
||||
|
||||
- Максимальная длина: 500 символов
|
||||
- Разрешены любые символы
|
||||
- Может быть пустым или отсутствовать
|
||||
|
||||
**Примеры**:
|
||||
|
||||
```json
|
||||
"strongSide": "Опытная команда разработчиков с 10+ летним опытом, быстрая доставка проектов"
|
||||
"strongSide": "Уникальная технология, низкие цены, отличная поддержка клиентов"
|
||||
```
|
||||
|
||||
**Примечание**: Если поле не указано, AI будет анализировать сильные стороны на основе других данных.
|
||||
|
||||
---
|
||||
|
||||
### 8. `weakSide` (опциональное)
|
||||
|
||||
**Описание**: Слабая сторона бизнеса. Помогает AI лучше понять области для улучшения.
|
||||
|
||||
**Валидация**:
|
||||
|
||||
- Максимальная длина: 500 символов
|
||||
- Разрешены любые символы
|
||||
- Может быть пустым или отсутствовать
|
||||
|
||||
**Примеры**:
|
||||
|
||||
```json
|
||||
"weakSide": "Ограниченный маркетинговый бюджет, недостаточная узнаваемость бренда"
|
||||
"weakSide": "Небольшая команда, ограниченные ресурсы для масштабирования"
|
||||
```
|
||||
|
||||
**Примечание**: Если поле не указано, AI будет анализировать слабые стороны на основе других данных.
|
||||
|
||||
---
|
||||
|
||||
### 9. `analysisType` (обязательное)
|
||||
|
||||
**Описание**: Тип анализа, который необходимо провести. Поле осталось без изменений.
|
||||
|
||||
**Допустимые значения**:
|
||||
|
||||
- `"РЫНОК"` - Анализ рынка
|
||||
- `"КОНКУРЕНТЫ"` - Анализ конкурентов
|
||||
- `"ЦА"` - Анализ целевой аудитории
|
||||
- `"КАНАЛЫ"` - Анализ маркетинговых каналов
|
||||
- `"SWOT"` - SWOT-анализ
|
||||
|
||||
---
|
||||
|
||||
## Полный пример запроса
|
||||
|
||||
### POST `/api/marketing/analysis/start`
|
||||
|
||||
```json
|
||||
{
|
||||
"businessNiche": "E-commerce платформы",
|
||||
"product": "Разработка мобильных приложений для интернет-магазинов",
|
||||
"targetAudience": "Малый и средний бизнес, владельцы интернет-магазинов в возрасте 30-50 лет, нуждающиеся в мобильных решениях",
|
||||
"region": "Алматы",
|
||||
"goal": "Увеличить количество клиентов на 50% за следующие 6 месяцев и выйти на рынок соседних регионов",
|
||||
"detailLevel": "СТАНДАРТНО",
|
||||
"strongSide": "Опытная команда разработчиков с 10+ летним опытом, быстрая доставка проектов, использование современных технологий",
|
||||
"weakSide": "Ограниченный маркетинговый бюджет, недостаточная узнаваемость бренда в новых регионах",
|
||||
"analysisType": "РЫНОК"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Примеры использования на фронтенде
|
||||
|
||||
### JavaScript/TypeScript
|
||||
|
||||
```typescript
|
||||
interface MarketingAnalysisRequest {
|
||||
businessNiche: string;
|
||||
product: string;
|
||||
targetAudience: string;
|
||||
region: string;
|
||||
goal: string;
|
||||
detailLevel: 'КРАТКО' | 'СТАНДАРТНО' | 'ПОДРОБНО';
|
||||
strongSide?: string;
|
||||
weakSide?: string;
|
||||
analysisType: 'РЫНОК' | 'КОНКУРЕНТЫ' | 'ЦА' | 'КАНАЛЫ' | 'SWOT';
|
||||
}
|
||||
|
||||
// Список допустимых регионов
|
||||
const VALID_REGIONS = [
|
||||
'Алматы',
|
||||
'Астана',
|
||||
'Шымкент',
|
||||
'Караганда',
|
||||
'Актобе',
|
||||
'Тараз',
|
||||
'Павлодар',
|
||||
'Усть-Каменогорск',
|
||||
'Семей',
|
||||
'Костанай',
|
||||
'Кызылорда',
|
||||
'Уральск',
|
||||
'Петропавловск',
|
||||
'Атырау',
|
||||
'Актау',
|
||||
'Туркестан',
|
||||
'Кокшетау',
|
||||
'Талдыкорган',
|
||||
'Экибастуз',
|
||||
'Рудный',
|
||||
];
|
||||
|
||||
// Список уровней детализации
|
||||
const DETAIL_LEVELS = ['КРАТКО', 'СТАНДАРТНО', 'ПОДРОБНО'] as const;
|
||||
|
||||
// Функция для отправки запроса
|
||||
async function startMarketingAnalysis(
|
||||
data: MarketingAnalysisRequest
|
||||
): Promise<string> {
|
||||
const response = await fetch(
|
||||
'https://api.konturai.kz/api/marketing/analysis/start',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer YOUR_JWT_TOKEN', // Если требуется
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error?.message || 'Failed to start analysis');
|
||||
}
|
||||
|
||||
return result.data.analysisId;
|
||||
}
|
||||
|
||||
// Пример использования
|
||||
const analysisId = await startMarketingAnalysis({
|
||||
businessNiche: 'E-commerce платформы',
|
||||
product: 'Разработка мобильных приложений',
|
||||
targetAudience: 'Малый и средний бизнес, владельцы интернет-магазинов',
|
||||
region: 'Алматы',
|
||||
goal: 'Увеличить количество клиентов на 50% за следующие 6 месяцев',
|
||||
detailLevel: 'СТАНДАРТНО',
|
||||
strongSide: 'Опытная команда, быстрая доставка',
|
||||
weakSide: 'Ограниченный маркетинговый бюджет',
|
||||
analysisType: 'РЫНОК',
|
||||
});
|
||||
```
|
||||
|
||||
### React компонент с формой
|
||||
|
||||
```tsx
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const MarketingAnalysisForm: React.FC = () => {
|
||||
const [formData, setFormData] = useState<MarketingAnalysisRequest>({
|
||||
businessNiche: '',
|
||||
product: '',
|
||||
targetAudience: '',
|
||||
region: '',
|
||||
goal: '',
|
||||
detailLevel: 'СТАНДАРТНО',
|
||||
strongSide: '',
|
||||
weakSide: '',
|
||||
analysisType: 'РЫНОК',
|
||||
});
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
const analysisId = await startMarketingAnalysis(formData);
|
||||
console.log('Analysis started:', analysisId);
|
||||
// Перенаправление на страницу с результатами
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<label>Ниша бизнеса *</label>
|
||||
<input
|
||||
type='text'
|
||||
value={formData.businessNiche}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, businessNiche: e.target.value })
|
||||
}
|
||||
required
|
||||
minLength={3}
|
||||
maxLength={200}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Продукт / услуга *</label>
|
||||
<input
|
||||
type='text'
|
||||
value={formData.product}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, product: e.target.value })
|
||||
}
|
||||
required
|
||||
minLength={3}
|
||||
maxLength={200}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Целевая аудитория *</label>
|
||||
<textarea
|
||||
value={formData.targetAudience}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, targetAudience: e.target.value })
|
||||
}
|
||||
required
|
||||
minLength={3}
|
||||
maxLength={300}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Регион *</label>
|
||||
<select
|
||||
value={formData.region}
|
||||
onChange={(e) => setFormData({ ...formData, region: e.target.value })}
|
||||
required
|
||||
>
|
||||
<option value=''>Выберите регион</option>
|
||||
{VALID_REGIONS.map((region) => (
|
||||
<option key={region} value={region}>
|
||||
{region}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Цель на 6-12 месяцев *</label>
|
||||
<textarea
|
||||
value={formData.goal}
|
||||
onChange={(e) => setFormData({ ...formData, goal: e.target.value })}
|
||||
required
|
||||
minLength={10}
|
||||
maxLength={500}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Уровень детализации анализа *</label>
|
||||
<div>
|
||||
{DETAIL_LEVELS.map((level) => (
|
||||
<label key={level}>
|
||||
<input
|
||||
type='radio'
|
||||
name='detailLevel'
|
||||
value={level}
|
||||
checked={formData.detailLevel === level}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
detailLevel: e.target.value as any,
|
||||
})
|
||||
}
|
||||
/>
|
||||
{level === 'КРАТКО' && ' Кратко'}
|
||||
{level === 'СТАНДАРТНО' && ' Стандартно'}
|
||||
{level === 'ПОДРОБНО' && ' Подробно'}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Сильная сторона</label>
|
||||
<textarea
|
||||
value={formData.strongSide}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, strongSide: e.target.value })
|
||||
}
|
||||
maxLength={500}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Слабая сторона</label>
|
||||
<textarea
|
||||
value={formData.weakSide}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, weakSide: e.target.value })
|
||||
}
|
||||
maxLength={500}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Тип анализа *</label>
|
||||
<select
|
||||
value={formData.analysisType}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, analysisType: e.target.value as any })
|
||||
}
|
||||
required
|
||||
>
|
||||
<option value='РЫНОК'>Рынок</option>
|
||||
<option value='КОНКУРЕНТЫ'>Конкуренты</option>
|
||||
<option value='ЦА'>Целевая аудитория</option>
|
||||
<option value='КАНАЛЫ'>Каналы</option>
|
||||
<option value='SWOT'>SWOT</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type='submit'>Сгенерировать анализ бизнеса</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Валидация на клиенте
|
||||
|
||||
Рекомендуется выполнять валидацию на клиенте перед отправкой запроса:
|
||||
|
||||
```typescript
|
||||
function validateMarketingAnalysisRequest(data: MarketingAnalysisRequest): {
|
||||
valid: boolean;
|
||||
errors: Record<string, string>;
|
||||
} {
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
// Валидация businessNiche
|
||||
if (!data.businessNiche || data.businessNiche.trim().length < 3) {
|
||||
errors.businessNiche = 'Ниша бизнеса должна содержать минимум 3 символа';
|
||||
}
|
||||
if (data.businessNiche && data.businessNiche.length > 200) {
|
||||
errors.businessNiche = 'Ниша бизнеса не должна превышать 200 символов';
|
||||
}
|
||||
|
||||
// Валидация product
|
||||
if (!data.product || data.product.trim().length < 3) {
|
||||
errors.product = 'Продукт должен содержать минимум 3 символа';
|
||||
}
|
||||
if (data.product && data.product.length > 200) {
|
||||
errors.product = 'Продукт не должен превышать 200 символов';
|
||||
}
|
||||
|
||||
// Валидация targetAudience
|
||||
if (!data.targetAudience || data.targetAudience.trim().length < 3) {
|
||||
errors.targetAudience =
|
||||
'Целевая аудитория должна содержать минимум 3 символа';
|
||||
}
|
||||
if (data.targetAudience && data.targetAudience.length > 300) {
|
||||
errors.targetAudience =
|
||||
'Целевая аудитория не должна превышать 300 символов';
|
||||
}
|
||||
|
||||
// Валидация region
|
||||
if (!data.region || !VALID_REGIONS.includes(data.region)) {
|
||||
errors.region = 'Выберите допустимый регион из списка';
|
||||
}
|
||||
|
||||
// Валидация goal
|
||||
if (!data.goal || data.goal.trim().length < 10) {
|
||||
errors.goal = 'Цель должна содержать минимум 10 символов';
|
||||
}
|
||||
if (data.goal && data.goal.length > 500) {
|
||||
errors.goal = 'Цель не должна превышать 500 символов';
|
||||
}
|
||||
|
||||
// Валидация detailLevel
|
||||
if (!data.detailLevel || !DETAIL_LEVELS.includes(data.detailLevel)) {
|
||||
errors.detailLevel = 'Выберите допустимый уровень детализации';
|
||||
}
|
||||
|
||||
// Валидация strongSide (опциональное)
|
||||
if (data.strongSide && data.strongSide.length > 500) {
|
||||
errors.strongSide = 'Сильная сторона не должна превышать 500 символов';
|
||||
}
|
||||
|
||||
// Валидация weakSide (опциональное)
|
||||
if (data.weakSide && data.weakSide.length > 500) {
|
||||
errors.weakSide = 'Слабая сторона не должна превышать 500 символов';
|
||||
}
|
||||
|
||||
// Валидация analysisType
|
||||
const validAnalysisTypes = ['РЫНОК', 'КОНКУРЕНТЫ', 'ЦА', 'КАНАЛЫ', 'SWOT'];
|
||||
if (!data.analysisType || !validAnalysisTypes.includes(data.analysisType)) {
|
||||
errors.analysisType = 'Выберите допустимый тип анализа';
|
||||
}
|
||||
|
||||
return {
|
||||
valid: Object.keys(errors).length === 0,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Миграция со старого API
|
||||
|
||||
Если у вас есть код, использующий старые поля, необходимо обновить его следующим образом:
|
||||
|
||||
### Старый формат (больше не работает):
|
||||
|
||||
```json
|
||||
{
|
||||
"product": "Разработка мобильных приложений",
|
||||
"location": "Алматы, Казахстан",
|
||||
"client": "B2B клиенты",
|
||||
"differentiator": "Быстрая разработка за 2 недели",
|
||||
"analysisType": "РЫНОК"
|
||||
}
|
||||
```
|
||||
|
||||
### Новый формат:
|
||||
|
||||
```json
|
||||
{
|
||||
"businessNiche": "Разработка программного обеспечения",
|
||||
"product": "Разработка мобильных приложений",
|
||||
"targetAudience": "B2B клиенты, технологические компании, стартапы",
|
||||
"region": "Алматы",
|
||||
"goal": "Увеличить количество клиентов на 50% за следующие 6 месяцев",
|
||||
"detailLevel": "СТАНДАРТНО",
|
||||
"strongSide": "Быстрая разработка за 2 недели, опытная команда",
|
||||
"weakSide": "",
|
||||
"analysisType": "РЫНОК"
|
||||
}
|
||||
```
|
||||
|
||||
### Маппинг старых полей на новые:
|
||||
|
||||
| Старое поле | Новое поле(я) | Примечание |
|
||||
| ---------------- | ----------------------------------------- | ------------------------------------------------ |
|
||||
| `location` | `region` | Только название города из списка |
|
||||
| `client` | `targetAudience` | Более детальное описание (свободный текст) |
|
||||
| `differentiator` | `businessNiche`, `strongSide`, `weakSide` | Разделено на несколько полей для лучшего анализа |
|
||||
|
||||
---
|
||||
|
||||
## Обработка ошибок валидации
|
||||
|
||||
При ошибках валидации API возвращает следующий формат:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Ошибка валидации",
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "Ошибка валидации входных данных",
|
||||
"details": {
|
||||
"businessNiche": "Поле 'businessNiche' должно содержать от 3 до 200 символов",
|
||||
"region": "Поле 'region' должно быть одним из допустимых городов Казахстана",
|
||||
"detailLevel": "Поле 'detailLevel' должно быть одним из: КРАТКО, СТАНДАРТНО, ПОДРОБНО"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Важные замечания
|
||||
|
||||
1. **Регистр важен**: Значения `region`, `detailLevel` и `analysisType` чувствительны к регистру. Используйте точные значения из списка допустимых.
|
||||
|
||||
2. **Все новые поля передаются в OpenAI**: Все указанные поля включаются в контекст для генерации анализа, что улучшает качество и релевантность результатов.
|
||||
|
||||
3. **Уровень детализации влияет на результат**: Выбор `detailLevel` напрямую влияет на объем и глубину генерируемого анализа.
|
||||
|
||||
4. **Опциональные поля улучшают анализ**: Хотя `strongSide` и `weakSide` опциональны, их указание помогает AI лучше понять бизнес и дать более точные рекомендации.
|
||||
|
||||
5. **Обратная совместимость**: Старые поля больше не поддерживаются. Необходимо обновить все клиентские приложения.
|
||||
|
||||
---
|
||||
|
||||
## Поддержка
|
||||
|
||||
При возникновении проблем с API обращайтесь в техническую поддержку с указанием:
|
||||
|
||||
- `analysisId` (если есть)
|
||||
- Время запроса
|
||||
- Описание проблемы
|
||||
- Код ошибки (если есть)
|
||||
- Пример запроса (без чувствительных данных)
|
||||
@@ -29,512 +29,517 @@ import java.util.stream.Collectors;
|
||||
@RequestMapping("/api/marketing/analysis")
|
||||
public class MarketingController {
|
||||
|
||||
private final MarketingAnalysisService marketingAnalysisService;
|
||||
private final MarketingStrategyService marketingStrategyService;
|
||||
private final MinIOService minIOService;
|
||||
private final JwtService jwtService;
|
||||
private final PostingTaskService postingTaskService;
|
||||
private final MarketingAnalysisService marketingAnalysisService;
|
||||
private final MarketingStrategyService marketingStrategyService;
|
||||
private final MinIOService minIOService;
|
||||
private final JwtService jwtService;
|
||||
private final PostingTaskService postingTaskService;
|
||||
|
||||
public MarketingController(
|
||||
MarketingAnalysisService marketingAnalysisService,
|
||||
MarketingStrategyService marketingStrategyService,
|
||||
MinIOService minIOService,
|
||||
JwtService jwtService,
|
||||
PostingTaskService postingTaskService) {
|
||||
this.marketingAnalysisService = marketingAnalysisService;
|
||||
this.marketingStrategyService = marketingStrategyService;
|
||||
this.minIOService = minIOService;
|
||||
this.jwtService = jwtService;
|
||||
this.postingTaskService = postingTaskService;
|
||||
}
|
||||
|
||||
private String extractUserIdFromHeader(String authHeader) {
|
||||
if (authHeader == null || authHeader.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return jwtService.extractUserIdFromHeader(authHeader);
|
||||
}
|
||||
|
||||
private ResponseEntity<?> unauthorizedResponse() {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"UNAUTHORIZED",
|
||||
"Требуется аутентификация. Пожалуйста, предоставьте валидный JWT токен.");
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||
.body(ApiResponse.error("Не авторизован", error));
|
||||
}
|
||||
|
||||
@PostMapping("/start")
|
||||
public ResponseEntity<?> startAnalysis(
|
||||
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||
@Valid @RequestBody MarketingAnalysisRequest request) {
|
||||
|
||||
String userId = extractUserIdFromHeader(authHeader);
|
||||
if (userId == null) {
|
||||
return unauthorizedResponse();
|
||||
public MarketingController(
|
||||
MarketingAnalysisService marketingAnalysisService,
|
||||
MarketingStrategyService marketingStrategyService,
|
||||
MinIOService minIOService,
|
||||
JwtService jwtService,
|
||||
PostingTaskService postingTaskService) {
|
||||
this.marketingAnalysisService = marketingAnalysisService;
|
||||
this.marketingStrategyService = marketingStrategyService;
|
||||
this.minIOService = minIOService;
|
||||
this.jwtService = jwtService;
|
||||
this.postingTaskService = postingTaskService;
|
||||
}
|
||||
|
||||
// Create analysis record
|
||||
MarketingAnalysis analysis = marketingAnalysisService.startAnalysis(request, userId);
|
||||
|
||||
// Start async processing
|
||||
marketingAnalysisService.processAnalysis(analysis.getId(), request);
|
||||
|
||||
// Calculate estimated completion time (5-10 minutes)
|
||||
LocalDateTime estimatedCompletion = LocalDateTime.now().plusMinutes(8);
|
||||
|
||||
MarketingAnalysisResponse response = new MarketingAnalysisResponse(
|
||||
analysis.getId(),
|
||||
"processing",
|
||||
estimatedCompletion,
|
||||
"Анализ запущен успешно. Результаты будут готовы в течение 5-10 минут.");
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success("Анализ запущен успешно", response));
|
||||
}
|
||||
|
||||
@GetMapping("/{analysisId}")
|
||||
public ResponseEntity<?> getAnalysis(
|
||||
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||
@PathVariable String analysisId) {
|
||||
|
||||
String userId = extractUserIdFromHeader(authHeader);
|
||||
if (userId == null) {
|
||||
return unauthorizedResponse();
|
||||
private String extractUserIdFromHeader(String authHeader) {
|
||||
if (authHeader == null || authHeader.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return jwtService.extractUserIdFromHeader(authHeader);
|
||||
}
|
||||
|
||||
Optional<MarketingAnalysis> optAnalysis = marketingAnalysisService.getAnalysisById(analysisId);
|
||||
if (optAnalysis.isEmpty()) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"NOT_FOUND",
|
||||
"Анализ с указанным ID не найден");
|
||||
return ResponseEntity.status(404)
|
||||
.body(ApiResponse.error("Анализ не найден", error));
|
||||
private ResponseEntity<?> unauthorizedResponse() {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"UNAUTHORIZED",
|
||||
"Требуется аутентификация. Пожалуйста, предоставьте валидный JWT токен.");
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||
.body(ApiResponse.error("Не авторизован", error));
|
||||
}
|
||||
|
||||
MarketingAnalysis analysis = optAnalysis.get();
|
||||
if (!userId.equals(analysis.getUserId())) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"FORBIDDEN",
|
||||
"У вас нет доступа к этому анализу");
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body(ApiResponse.error("Доступ запрещен", error));
|
||||
@PostMapping("/start")
|
||||
public ResponseEntity<?> startAnalysis(
|
||||
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||
@Valid @RequestBody MarketingAnalysisRequest request) {
|
||||
|
||||
String userId = extractUserIdFromHeader(authHeader);
|
||||
if (userId == null) {
|
||||
return unauthorizedResponse();
|
||||
}
|
||||
|
||||
// Create analysis record
|
||||
MarketingAnalysis analysis = marketingAnalysisService.startAnalysis(request, userId);
|
||||
|
||||
// Start async processing
|
||||
marketingAnalysisService.processAnalysis(analysis.getId(), request);
|
||||
|
||||
// Calculate estimated completion time (5-10 minutes)
|
||||
LocalDateTime estimatedCompletion = LocalDateTime.now().plusMinutes(8);
|
||||
|
||||
MarketingAnalysisResponse response = new MarketingAnalysisResponse(
|
||||
analysis.getId(),
|
||||
"processing",
|
||||
estimatedCompletion,
|
||||
"Анализ запущен успешно. Результаты будут готовы в течение 5-10 минут.");
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success("Анализ запущен успешно", response));
|
||||
}
|
||||
|
||||
MarketingAnalysisResult result = marketingAnalysisService.getAnalysisResult(analysisId);
|
||||
return ResponseEntity.ok(ApiResponse.success(result));
|
||||
}
|
||||
@GetMapping("/{analysisId}")
|
||||
public ResponseEntity<?> getAnalysis(
|
||||
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||
@PathVariable String analysisId) {
|
||||
|
||||
@GetMapping("/{analysisId}/download")
|
||||
public ResponseEntity<?> downloadPdf(
|
||||
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||
@PathVariable String analysisId) {
|
||||
String userId = extractUserIdFromHeader(authHeader);
|
||||
if (userId == null) {
|
||||
return unauthorizedResponse();
|
||||
}
|
||||
|
||||
String userId = extractUserIdFromHeader(authHeader);
|
||||
if (userId == null) {
|
||||
return unauthorizedResponse();
|
||||
Optional<MarketingAnalysis> optAnalysis = marketingAnalysisService.getAnalysisById(analysisId);
|
||||
if (optAnalysis.isEmpty()) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"NOT_FOUND",
|
||||
"Анализ с указанным ID не найден");
|
||||
return ResponseEntity.status(404)
|
||||
.body(ApiResponse.error("Анализ не найден", error));
|
||||
}
|
||||
|
||||
MarketingAnalysis analysis = optAnalysis.get();
|
||||
if (!userId.equals(analysis.getUserId())) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"FORBIDDEN",
|
||||
"У вас нет доступа к этому анализу");
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body(ApiResponse.error("Доступ запрещен", error));
|
||||
}
|
||||
|
||||
MarketingAnalysisResult result = marketingAnalysisService.getAnalysisResult(analysisId);
|
||||
return ResponseEntity.ok(ApiResponse.success(result));
|
||||
}
|
||||
|
||||
Optional<MarketingAnalysis> optAnalysis = marketingAnalysisService.getAnalysisById(analysisId);
|
||||
if (optAnalysis.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
@GetMapping("/{analysisId}/download")
|
||||
public ResponseEntity<?> downloadPdf(
|
||||
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||
@PathVariable String analysisId) {
|
||||
|
||||
String userId = extractUserIdFromHeader(authHeader);
|
||||
if (userId == null) {
|
||||
return unauthorizedResponse();
|
||||
}
|
||||
|
||||
Optional<MarketingAnalysis> optAnalysis = marketingAnalysisService.getAnalysisById(analysisId);
|
||||
if (optAnalysis.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
MarketingAnalysis analysis = optAnalysis.get();
|
||||
if (!userId.equals(analysis.getUserId())) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"FORBIDDEN",
|
||||
"У вас нет доступа к этому анализу");
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body(ApiResponse.error("Доступ запрещен", error));
|
||||
}
|
||||
|
||||
if (analysis.getPdfFilePath() == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
try {
|
||||
InputStream inputStream = minIOService.downloadFile(analysis.getPdfFilePath());
|
||||
byte[] bytes = inputStream.readAllBytes();
|
||||
inputStream.close();
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||
"attachment; filename=\"" + analysis.getPdfFilename() + "\"")
|
||||
.contentType(MediaType.APPLICATION_PDF)
|
||||
.body(bytes);
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
}
|
||||
|
||||
MarketingAnalysis analysis = optAnalysis.get();
|
||||
if (!userId.equals(analysis.getUserId())) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"FORBIDDEN",
|
||||
"У вас нет доступа к этому анализу");
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body(ApiResponse.error("Доступ запрещен", error));
|
||||
@PostMapping("/strategy/generate")
|
||||
public ResponseEntity<?> generateStrategy(
|
||||
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||
@RequestParam String analysisId,
|
||||
@Valid @RequestBody(required = false) MarketingStrategyRequest request) {
|
||||
|
||||
String userId = extractUserIdFromHeader(authHeader);
|
||||
if (userId == null) {
|
||||
return unauthorizedResponse();
|
||||
}
|
||||
|
||||
// Check if user owns the analysis
|
||||
Optional<MarketingAnalysis> optAnalysis = marketingAnalysisService.getAnalysisById(analysisId);
|
||||
if (optAnalysis.isEmpty()) {
|
||||
ErrorResponse error = new ErrorResponse("INVALID_ANALYSIS", "Анализ не найден");
|
||||
return ResponseEntity.status(404)
|
||||
.body(ApiResponse.error("Анализ не найден", error));
|
||||
}
|
||||
|
||||
MarketingAnalysis analysis = optAnalysis.get();
|
||||
if (!userId.equals(analysis.getUserId())) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"FORBIDDEN",
|
||||
"У вас нет доступа к этому анализу");
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body(ApiResponse.error("Доступ запрещен", error));
|
||||
}
|
||||
|
||||
if (request == null) {
|
||||
request = new MarketingStrategyRequest();
|
||||
}
|
||||
|
||||
try {
|
||||
MarketingStrategy strategy = marketingStrategyService.generateStrategy(analysisId, request,
|
||||
userId);
|
||||
|
||||
MarketingStrategyResponse response = new MarketingStrategyResponse();
|
||||
response.setStrategyId(strategy.getId());
|
||||
response.setAnalysisId(strategy.getAnalysisId());
|
||||
response.setStatus(strategy.getStatus());
|
||||
response.setCreatedAt(strategy.getCreatedAt());
|
||||
response.setDurationWeeks(strategy.getDurationWeeks());
|
||||
response.setPriorityPlatforms(strategy.getPriorityPlatforms());
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(
|
||||
"Генерация стратегии запущена успешно. Результаты будут готовы в течение 3-5 минут.",
|
||||
response));
|
||||
} catch (IllegalArgumentException e) {
|
||||
ErrorResponse error = new ErrorResponse("INVALID_ANALYSIS", e.getMessage());
|
||||
return ResponseEntity.status(404)
|
||||
.body(ApiResponse.error("Анализ не найден", error));
|
||||
} catch (IllegalStateException e) {
|
||||
ErrorResponse error = new ErrorResponse("ANALYSIS_NOT_COMPLETED", e.getMessage());
|
||||
return ResponseEntity.status(400)
|
||||
.body(ApiResponse.error("Анализ еще не завершен", error));
|
||||
} catch (Exception e) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"INTERNAL_SERVER_ERROR",
|
||||
"Произошла ошибка при запуске генерации стратегии");
|
||||
return ResponseEntity.status(500)
|
||||
.body(ApiResponse.error("Внутренняя ошибка сервера", error));
|
||||
}
|
||||
}
|
||||
|
||||
if (analysis.getPdfFilePath() == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
@GetMapping("/strategy/{strategyId}")
|
||||
public ResponseEntity<?> getStrategy(
|
||||
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||
@PathVariable String strategyId) {
|
||||
|
||||
String userId = extractUserIdFromHeader(authHeader);
|
||||
if (userId == null) {
|
||||
return unauthorizedResponse();
|
||||
}
|
||||
|
||||
Optional<MarketingStrategy> optStrategy = marketingStrategyService.getStrategyById(strategyId);
|
||||
if (optStrategy.isEmpty()) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"NOT_FOUND",
|
||||
"Стратегия с указанным ID не найдена");
|
||||
return ResponseEntity.status(404)
|
||||
.body(ApiResponse.error("Стратегия не найдена", error));
|
||||
}
|
||||
|
||||
MarketingStrategy strategy = optStrategy.get();
|
||||
if (!userId.equals(strategy.getUserId())) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"FORBIDDEN",
|
||||
"У вас нет доступа к этой стратегии");
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body(ApiResponse.error("Доступ запрещен", error));
|
||||
}
|
||||
|
||||
MarketingStrategyResponse result = marketingStrategyService.getStrategyResult(strategyId);
|
||||
return ResponseEntity.ok(ApiResponse.success(result));
|
||||
}
|
||||
|
||||
try {
|
||||
InputStream inputStream = minIOService.downloadFile(analysis.getPdfFilePath());
|
||||
byte[] bytes = inputStream.readAllBytes();
|
||||
inputStream.close();
|
||||
@GetMapping("/{analysisId}/strategy")
|
||||
public ResponseEntity<?> getStrategyByAnalysis(
|
||||
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||
@PathVariable String analysisId) {
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||
"attachment; filename=\"" + analysis.getPdfFilename() + "\"")
|
||||
.contentType(MediaType.APPLICATION_PDF)
|
||||
.body(bytes);
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
}
|
||||
String userId = extractUserIdFromHeader(authHeader);
|
||||
if (userId == null) {
|
||||
return unauthorizedResponse();
|
||||
}
|
||||
|
||||
@PostMapping("/strategy/generate")
|
||||
public ResponseEntity<?> generateStrategy(
|
||||
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||
@RequestParam String analysisId,
|
||||
@Valid @RequestBody(required = false) MarketingStrategyRequest request) {
|
||||
// Check if user owns the analysis
|
||||
Optional<MarketingAnalysis> optAnalysis = marketingAnalysisService.getAnalysisById(analysisId);
|
||||
if (optAnalysis.isEmpty()) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"NOT_FOUND",
|
||||
"Анализ с указанным ID не найден");
|
||||
return ResponseEntity.status(404)
|
||||
.body(ApiResponse.error("Анализ не найден", error));
|
||||
}
|
||||
|
||||
String userId = extractUserIdFromHeader(authHeader);
|
||||
if (userId == null) {
|
||||
return unauthorizedResponse();
|
||||
MarketingAnalysis analysis = optAnalysis.get();
|
||||
if (!userId.equals(analysis.getUserId())) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"FORBIDDEN",
|
||||
"У вас нет доступа к этому анализу");
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body(ApiResponse.error("Доступ запрещен", error));
|
||||
}
|
||||
|
||||
MarketingStrategyResponse result = marketingStrategyService.getStrategyByAnalysisId(analysisId);
|
||||
|
||||
if (result == null) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"NOT_FOUND",
|
||||
"Стратегия для указанного анализа не найдена");
|
||||
return ResponseEntity.status(404)
|
||||
.body(ApiResponse.error("Стратегия не найдена", error));
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(result));
|
||||
}
|
||||
|
||||
// Check if user owns the analysis
|
||||
Optional<MarketingAnalysis> optAnalysis = marketingAnalysisService.getAnalysisById(analysisId);
|
||||
if (optAnalysis.isEmpty()) {
|
||||
ErrorResponse error = new ErrorResponse("INVALID_ANALYSIS", "Анализ не найден");
|
||||
return ResponseEntity.status(404)
|
||||
.body(ApiResponse.error("Анализ не найден", error));
|
||||
@GetMapping("/my")
|
||||
public ResponseEntity<?> getMyAnalyses(
|
||||
@RequestHeader(value = "Authorization", required = false) String authHeader) {
|
||||
|
||||
String userId = extractUserIdFromHeader(authHeader);
|
||||
if (userId == null) {
|
||||
return unauthorizedResponse();
|
||||
}
|
||||
|
||||
List<MarketingAnalysis> analyses = marketingAnalysisService.getUserAnalyses(userId);
|
||||
List<AnalysisHistoryResponse> responseList = analyses.stream()
|
||||
.map(this::convertToHistoryResponse)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(responseList));
|
||||
}
|
||||
|
||||
MarketingAnalysis analysis = optAnalysis.get();
|
||||
if (!userId.equals(analysis.getUserId())) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"FORBIDDEN",
|
||||
"У вас нет доступа к этому анализу");
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body(ApiResponse.error("Доступ запрещен", error));
|
||||
@GetMapping("/strategy/my")
|
||||
public ResponseEntity<?> getMyStrategies(
|
||||
@RequestHeader(value = "Authorization", required = false) String authHeader) {
|
||||
|
||||
String userId = extractUserIdFromHeader(authHeader);
|
||||
if (userId == null) {
|
||||
return unauthorizedResponse();
|
||||
}
|
||||
|
||||
List<MarketingStrategy> strategies = marketingStrategyService.getUserStrategies(userId);
|
||||
List<StrategyHistoryResponse> responseList = strategies.stream()
|
||||
.map(this::convertToStrategyHistoryResponse)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(responseList));
|
||||
}
|
||||
|
||||
if (request == null) {
|
||||
request = new MarketingStrategyRequest();
|
||||
@GetMapping("/{analysisId}/history")
|
||||
public ResponseEntity<?> getAnalysisHistory(
|
||||
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||
@PathVariable String analysisId) {
|
||||
|
||||
String userId = extractUserIdFromHeader(authHeader);
|
||||
if (userId == null) {
|
||||
return unauthorizedResponse();
|
||||
}
|
||||
|
||||
Optional<MarketingAnalysis> optAnalysis = marketingAnalysisService.getAnalysisById(analysisId);
|
||||
if (optAnalysis.isEmpty()) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"NOT_FOUND",
|
||||
"Анализ с указанным ID не найден");
|
||||
return ResponseEntity.status(404)
|
||||
.body(ApiResponse.error("Анализ не найден", error));
|
||||
}
|
||||
|
||||
MarketingAnalysis analysis = optAnalysis.get();
|
||||
if (!userId.equals(analysis.getUserId())) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"FORBIDDEN",
|
||||
"У вас нет доступа к этому анализу");
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body(ApiResponse.error("Доступ запрещен", error));
|
||||
}
|
||||
|
||||
AnalysisHistoryResponse response = convertToHistoryResponse(analysis);
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
}
|
||||
|
||||
try {
|
||||
MarketingStrategy strategy = marketingStrategyService.generateStrategy(analysisId, request, userId);
|
||||
@GetMapping("/strategy/{strategyId}/history")
|
||||
public ResponseEntity<?> getStrategyHistory(
|
||||
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||
@PathVariable String strategyId) {
|
||||
|
||||
MarketingStrategyResponse response = new MarketingStrategyResponse();
|
||||
response.setStrategyId(strategy.getId());
|
||||
response.setAnalysisId(strategy.getAnalysisId());
|
||||
response.setStatus(strategy.getStatus());
|
||||
response.setCreatedAt(strategy.getCreatedAt());
|
||||
response.setDurationWeeks(strategy.getDurationWeeks());
|
||||
response.setPriorityPlatforms(strategy.getPriorityPlatforms());
|
||||
String userId = extractUserIdFromHeader(authHeader);
|
||||
if (userId == null) {
|
||||
return unauthorizedResponse();
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(
|
||||
"Генерация стратегии запущена успешно. Результаты будут готовы в течение 3-5 минут.",
|
||||
response));
|
||||
} catch (IllegalArgumentException e) {
|
||||
ErrorResponse error = new ErrorResponse("INVALID_ANALYSIS", e.getMessage());
|
||||
return ResponseEntity.status(404)
|
||||
.body(ApiResponse.error("Анализ не найден", error));
|
||||
} catch (IllegalStateException e) {
|
||||
ErrorResponse error = new ErrorResponse("ANALYSIS_NOT_COMPLETED", e.getMessage());
|
||||
return ResponseEntity.status(400)
|
||||
.body(ApiResponse.error("Анализ еще не завершен", error));
|
||||
} catch (Exception e) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"INTERNAL_SERVER_ERROR",
|
||||
"Произошла ошибка при запуске генерации стратегии");
|
||||
return ResponseEntity.status(500)
|
||||
.body(ApiResponse.error("Внутренняя ошибка сервера", error));
|
||||
}
|
||||
}
|
||||
Optional<MarketingStrategy> optStrategy = marketingStrategyService.getStrategyById(strategyId);
|
||||
if (optStrategy.isEmpty()) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"NOT_FOUND",
|
||||
"Стратегия с указанным ID не найдена");
|
||||
return ResponseEntity.status(404)
|
||||
.body(ApiResponse.error("Стратегия не найдена", error));
|
||||
}
|
||||
|
||||
@GetMapping("/strategy/{strategyId}")
|
||||
public ResponseEntity<?> getStrategy(
|
||||
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||
@PathVariable String strategyId) {
|
||||
MarketingStrategy strategy = optStrategy.get();
|
||||
if (!userId.equals(strategy.getUserId())) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"FORBIDDEN",
|
||||
"У вас нет доступа к этой стратегии");
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body(ApiResponse.error("Доступ запрещен", error));
|
||||
}
|
||||
|
||||
String userId = extractUserIdFromHeader(authHeader);
|
||||
if (userId == null) {
|
||||
return unauthorizedResponse();
|
||||
StrategyHistoryResponse response = convertToStrategyHistoryResponse(strategy);
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
}
|
||||
|
||||
Optional<MarketingStrategy> optStrategy = marketingStrategyService.getStrategyById(strategyId);
|
||||
if (optStrategy.isEmpty()) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"NOT_FOUND",
|
||||
"Стратегия с указанным ID не найдена");
|
||||
return ResponseEntity.status(404)
|
||||
.body(ApiResponse.error("Стратегия не найдена", error));
|
||||
private AnalysisHistoryResponse convertToHistoryResponse(MarketingAnalysis analysis) {
|
||||
AnalysisHistoryResponse response = new AnalysisHistoryResponse();
|
||||
response.setAnalysisId(analysis.getId());
|
||||
response.setBusinessNiche(analysis.getBusinessNiche());
|
||||
response.setProduct(analysis.getProduct());
|
||||
response.setTargetAudience(analysis.getTargetAudience());
|
||||
response.setRegion(analysis.getRegion());
|
||||
response.setGoal(analysis.getGoal());
|
||||
response.setDetailLevel(analysis.getDetailLevel());
|
||||
response.setStrongSide(analysis.getStrongSide());
|
||||
response.setWeakSide(analysis.getWeakSide());
|
||||
response.setAnalysisType(analysis.getAnalysisType());
|
||||
response.setStatus(analysis.getStatus());
|
||||
response.setUserId(analysis.getUserId());
|
||||
response.setCreatedAt(analysis.getCreatedAt());
|
||||
response.setCompletedAt(analysis.getCompletedAt());
|
||||
response.setStatusHistory(analysis.getStatusHistory());
|
||||
return response;
|
||||
}
|
||||
|
||||
MarketingStrategy strategy = optStrategy.get();
|
||||
if (!userId.equals(strategy.getUserId())) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"FORBIDDEN",
|
||||
"У вас нет доступа к этой стратегии");
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body(ApiResponse.error("Доступ запрещен", error));
|
||||
private StrategyHistoryResponse convertToStrategyHistoryResponse(MarketingStrategy strategy) {
|
||||
StrategyHistoryResponse response = new StrategyHistoryResponse();
|
||||
response.setStrategyId(strategy.getId());
|
||||
response.setAnalysisId(strategy.getAnalysisId());
|
||||
response.setStatus(strategy.getStatus());
|
||||
response.setUserId(strategy.getUserId());
|
||||
response.setDurationWeeks(strategy.getDurationWeeks());
|
||||
response.setPriorityPlatforms(strategy.getPriorityPlatforms());
|
||||
response.setCreatedAt(strategy.getCreatedAt());
|
||||
response.setCompletedAt(strategy.getCompletedAt());
|
||||
response.setStatusHistory(strategy.getStatusHistory());
|
||||
return response;
|
||||
}
|
||||
|
||||
MarketingStrategyResponse result = marketingStrategyService.getStrategyResult(strategyId);
|
||||
return ResponseEntity.ok(ApiResponse.success(result));
|
||||
}
|
||||
@PostMapping("/strategy/{strategyId}/start")
|
||||
public ResponseEntity<?> startStrategy(
|
||||
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||
@PathVariable String strategyId,
|
||||
@RequestBody(required = false) StartStrategyRequest request) {
|
||||
|
||||
@GetMapping("/{analysisId}/strategy")
|
||||
public ResponseEntity<?> getStrategyByAnalysis(
|
||||
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||
@PathVariable String analysisId) {
|
||||
String userId = extractUserIdFromHeader(authHeader);
|
||||
if (userId == null) {
|
||||
return unauthorizedResponse();
|
||||
}
|
||||
|
||||
String userId = extractUserIdFromHeader(authHeader);
|
||||
if (userId == null) {
|
||||
return unauthorizedResponse();
|
||||
// Проверяем существование стратегии и права доступа
|
||||
Optional<MarketingStrategy> optStrategy = marketingStrategyService.getStrategyById(strategyId);
|
||||
if (optStrategy.isEmpty()) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"NOT_FOUND",
|
||||
"Стратегия с указанным ID не найдена");
|
||||
return ResponseEntity.status(404)
|
||||
.body(ApiResponse.error("Стратегия не найдена", error));
|
||||
}
|
||||
|
||||
MarketingStrategy strategy = optStrategy.get();
|
||||
if (!userId.equals(strategy.getUserId())) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"FORBIDDEN",
|
||||
"У вас нет доступа к этой стратегии");
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body(ApiResponse.error("Доступ запрещен", error));
|
||||
}
|
||||
|
||||
if (!"completed".equals(strategy.getStatus())) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"INVALID_STATUS",
|
||||
"Стратегия еще не завершена. Статус: " + strategy.getStatus());
|
||||
return ResponseEntity.status(400)
|
||||
.body(ApiResponse.error("Стратегия не готова к запуску", error));
|
||||
}
|
||||
|
||||
try {
|
||||
// Создаем задачи из стратегии
|
||||
List<PostingTask> tasks = postingTaskService.createTasksFromStrategy(strategyId);
|
||||
|
||||
// Получаем список платформ
|
||||
List<String> platforms = tasks.stream()
|
||||
.map(PostingTask::getPlatform)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
StartStrategyResponse response = new StartStrategyResponse(
|
||||
strategyId,
|
||||
tasks.size(),
|
||||
platforms,
|
||||
"Стратегия успешно запущена. Создано задач: " + tasks.size());
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(
|
||||
"Стратегия успешно запущена",
|
||||
response));
|
||||
|
||||
} catch (IllegalStateException e) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"MISSING_CREDENTIALS",
|
||||
e.getMessage());
|
||||
return ResponseEntity.status(400)
|
||||
.body(ApiResponse.error("Не удалось запустить стратегию", error));
|
||||
} catch (IllegalArgumentException e) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"INVALID_STRATEGY",
|
||||
e.getMessage());
|
||||
return ResponseEntity.status(404)
|
||||
.body(ApiResponse.error("Стратегия не найдена", error));
|
||||
} catch (Exception e) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"INTERNAL_SERVER_ERROR",
|
||||
"Произошла ошибка при запуске стратегии");
|
||||
return ResponseEntity.status(500)
|
||||
.body(ApiResponse.error("Внутренняя ошибка сервера", error));
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user owns the analysis
|
||||
Optional<MarketingAnalysis> optAnalysis = marketingAnalysisService.getAnalysisById(analysisId);
|
||||
if (optAnalysis.isEmpty()) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"NOT_FOUND",
|
||||
"Анализ с указанным ID не найден");
|
||||
return ResponseEntity.status(404)
|
||||
.body(ApiResponse.error("Анализ не найден", error));
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public ResponseEntity<ApiResponse<ErrorResponse>> handleValidationException(
|
||||
MethodArgumentNotValidException ex) {
|
||||
|
||||
Map<String, String> details = new HashMap<>();
|
||||
ex.getBindingResult().getFieldErrors().forEach(error -> {
|
||||
details.put(error.getField(), error.getDefaultMessage());
|
||||
});
|
||||
|
||||
ErrorResponse errorResponse = new ErrorResponse(
|
||||
"VALIDATION_ERROR",
|
||||
"Ошибка валидации входных данных",
|
||||
details);
|
||||
|
||||
return ResponseEntity.badRequest()
|
||||
.body(ApiResponse.error("Ошибка валидации", errorResponse));
|
||||
}
|
||||
|
||||
MarketingAnalysis analysis = optAnalysis.get();
|
||||
if (!userId.equals(analysis.getUserId())) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"FORBIDDEN",
|
||||
"У вас нет доступа к этому анализу");
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body(ApiResponse.error("Доступ запрещен", error));
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ApiResponse<ErrorResponse>> handleGenericException(Exception e) {
|
||||
ErrorResponse errorResponse = new ErrorResponse(
|
||||
"INTERNAL_SERVER_ERROR",
|
||||
"Произошла внутренняя ошибка сервера. Попробуйте позже.");
|
||||
|
||||
return ResponseEntity.status(500)
|
||||
.body(ApiResponse.error("Внутренняя ошибка сервера", errorResponse));
|
||||
}
|
||||
|
||||
MarketingStrategyResponse result = marketingStrategyService.getStrategyByAnalysisId(analysisId);
|
||||
|
||||
if (result == null) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"NOT_FOUND",
|
||||
"Стратегия для указанного анализа не найдена");
|
||||
return ResponseEntity.status(404)
|
||||
.body(ApiResponse.error("Стратегия не найдена", error));
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(result));
|
||||
}
|
||||
|
||||
@GetMapping("/my")
|
||||
public ResponseEntity<?> getMyAnalyses(
|
||||
@RequestHeader(value = "Authorization", required = false) String authHeader) {
|
||||
|
||||
String userId = extractUserIdFromHeader(authHeader);
|
||||
if (userId == null) {
|
||||
return unauthorizedResponse();
|
||||
}
|
||||
|
||||
List<MarketingAnalysis> analyses = marketingAnalysisService.getUserAnalyses(userId);
|
||||
List<AnalysisHistoryResponse> responseList = analyses.stream()
|
||||
.map(this::convertToHistoryResponse)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(responseList));
|
||||
}
|
||||
|
||||
@GetMapping("/strategy/my")
|
||||
public ResponseEntity<?> getMyStrategies(
|
||||
@RequestHeader(value = "Authorization", required = false) String authHeader) {
|
||||
|
||||
String userId = extractUserIdFromHeader(authHeader);
|
||||
if (userId == null) {
|
||||
return unauthorizedResponse();
|
||||
}
|
||||
|
||||
List<MarketingStrategy> strategies = marketingStrategyService.getUserStrategies(userId);
|
||||
List<StrategyHistoryResponse> responseList = strategies.stream()
|
||||
.map(this::convertToStrategyHistoryResponse)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(responseList));
|
||||
}
|
||||
|
||||
@GetMapping("/{analysisId}/history")
|
||||
public ResponseEntity<?> getAnalysisHistory(
|
||||
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||
@PathVariable String analysisId) {
|
||||
|
||||
String userId = extractUserIdFromHeader(authHeader);
|
||||
if (userId == null) {
|
||||
return unauthorizedResponse();
|
||||
}
|
||||
|
||||
Optional<MarketingAnalysis> optAnalysis = marketingAnalysisService.getAnalysisById(analysisId);
|
||||
if (optAnalysis.isEmpty()) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"NOT_FOUND",
|
||||
"Анализ с указанным ID не найден");
|
||||
return ResponseEntity.status(404)
|
||||
.body(ApiResponse.error("Анализ не найден", error));
|
||||
}
|
||||
|
||||
MarketingAnalysis analysis = optAnalysis.get();
|
||||
if (!userId.equals(analysis.getUserId())) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"FORBIDDEN",
|
||||
"У вас нет доступа к этому анализу");
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body(ApiResponse.error("Доступ запрещен", error));
|
||||
}
|
||||
|
||||
AnalysisHistoryResponse response = convertToHistoryResponse(analysis);
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
}
|
||||
|
||||
@GetMapping("/strategy/{strategyId}/history")
|
||||
public ResponseEntity<?> getStrategyHistory(
|
||||
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||
@PathVariable String strategyId) {
|
||||
|
||||
String userId = extractUserIdFromHeader(authHeader);
|
||||
if (userId == null) {
|
||||
return unauthorizedResponse();
|
||||
}
|
||||
|
||||
Optional<MarketingStrategy> optStrategy = marketingStrategyService.getStrategyById(strategyId);
|
||||
if (optStrategy.isEmpty()) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"NOT_FOUND",
|
||||
"Стратегия с указанным ID не найдена");
|
||||
return ResponseEntity.status(404)
|
||||
.body(ApiResponse.error("Стратегия не найдена", error));
|
||||
}
|
||||
|
||||
MarketingStrategy strategy = optStrategy.get();
|
||||
if (!userId.equals(strategy.getUserId())) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"FORBIDDEN",
|
||||
"У вас нет доступа к этой стратегии");
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body(ApiResponse.error("Доступ запрещен", error));
|
||||
}
|
||||
|
||||
StrategyHistoryResponse response = convertToStrategyHistoryResponse(strategy);
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
}
|
||||
|
||||
private AnalysisHistoryResponse convertToHistoryResponse(MarketingAnalysis analysis) {
|
||||
AnalysisHistoryResponse response = new AnalysisHistoryResponse();
|
||||
response.setAnalysisId(analysis.getId());
|
||||
response.setProduct(analysis.getProduct());
|
||||
response.setLocation(analysis.getLocation());
|
||||
response.setClientType(analysis.getClientType());
|
||||
response.setDifferentiator(analysis.getDifferentiator());
|
||||
response.setAnalysisType(analysis.getAnalysisType());
|
||||
response.setStatus(analysis.getStatus());
|
||||
response.setUserId(analysis.getUserId());
|
||||
response.setCreatedAt(analysis.getCreatedAt());
|
||||
response.setCompletedAt(analysis.getCompletedAt());
|
||||
response.setStatusHistory(analysis.getStatusHistory());
|
||||
return response;
|
||||
}
|
||||
|
||||
private StrategyHistoryResponse convertToStrategyHistoryResponse(MarketingStrategy strategy) {
|
||||
StrategyHistoryResponse response = new StrategyHistoryResponse();
|
||||
response.setStrategyId(strategy.getId());
|
||||
response.setAnalysisId(strategy.getAnalysisId());
|
||||
response.setStatus(strategy.getStatus());
|
||||
response.setUserId(strategy.getUserId());
|
||||
response.setDurationWeeks(strategy.getDurationWeeks());
|
||||
response.setPriorityPlatforms(strategy.getPriorityPlatforms());
|
||||
response.setCreatedAt(strategy.getCreatedAt());
|
||||
response.setCompletedAt(strategy.getCompletedAt());
|
||||
response.setStatusHistory(strategy.getStatusHistory());
|
||||
return response;
|
||||
}
|
||||
|
||||
@PostMapping("/strategy/{strategyId}/start")
|
||||
public ResponseEntity<?> startStrategy(
|
||||
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||
@PathVariable String strategyId,
|
||||
@RequestBody(required = false) StartStrategyRequest request) {
|
||||
|
||||
String userId = extractUserIdFromHeader(authHeader);
|
||||
if (userId == null) {
|
||||
return unauthorizedResponse();
|
||||
}
|
||||
|
||||
// Проверяем существование стратегии и права доступа
|
||||
Optional<MarketingStrategy> optStrategy = marketingStrategyService.getStrategyById(strategyId);
|
||||
if (optStrategy.isEmpty()) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"NOT_FOUND",
|
||||
"Стратегия с указанным ID не найдена");
|
||||
return ResponseEntity.status(404)
|
||||
.body(ApiResponse.error("Стратегия не найдена", error));
|
||||
}
|
||||
|
||||
MarketingStrategy strategy = optStrategy.get();
|
||||
if (!userId.equals(strategy.getUserId())) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"FORBIDDEN",
|
||||
"У вас нет доступа к этой стратегии");
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body(ApiResponse.error("Доступ запрещен", error));
|
||||
}
|
||||
|
||||
if (!"completed".equals(strategy.getStatus())) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"INVALID_STATUS",
|
||||
"Стратегия еще не завершена. Статус: " + strategy.getStatus());
|
||||
return ResponseEntity.status(400)
|
||||
.body(ApiResponse.error("Стратегия не готова к запуску", error));
|
||||
}
|
||||
|
||||
try {
|
||||
// Создаем задачи из стратегии
|
||||
List<PostingTask> tasks = postingTaskService.createTasksFromStrategy(strategyId);
|
||||
|
||||
// Получаем список платформ
|
||||
List<String> platforms = tasks.stream()
|
||||
.map(PostingTask::getPlatform)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
StartStrategyResponse response = new StartStrategyResponse(
|
||||
strategyId,
|
||||
tasks.size(),
|
||||
platforms,
|
||||
"Стратегия успешно запущена. Создано задач: " + tasks.size());
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(
|
||||
"Стратегия успешно запущена",
|
||||
response));
|
||||
|
||||
} catch (IllegalStateException e) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"MISSING_CREDENTIALS",
|
||||
e.getMessage());
|
||||
return ResponseEntity.status(400)
|
||||
.body(ApiResponse.error("Не удалось запустить стратегию", error));
|
||||
} catch (IllegalArgumentException e) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"INVALID_STRATEGY",
|
||||
e.getMessage());
|
||||
return ResponseEntity.status(404)
|
||||
.body(ApiResponse.error("Стратегия не найдена", error));
|
||||
} catch (Exception e) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"INTERNAL_SERVER_ERROR",
|
||||
"Произошла ошибка при запуске стратегии");
|
||||
return ResponseEntity.status(500)
|
||||
.body(ApiResponse.error("Внутренняя ошибка сервера", error));
|
||||
}
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public ResponseEntity<ApiResponse<ErrorResponse>> handleValidationException(
|
||||
MethodArgumentNotValidException ex) {
|
||||
|
||||
Map<String, String> details = new HashMap<>();
|
||||
ex.getBindingResult().getFieldErrors().forEach(error -> {
|
||||
details.put(error.getField(), error.getDefaultMessage());
|
||||
});
|
||||
|
||||
ErrorResponse errorResponse = new ErrorResponse(
|
||||
"VALIDATION_ERROR",
|
||||
"Ошибка валидации входных данных",
|
||||
details);
|
||||
|
||||
return ResponseEntity.badRequest()
|
||||
.body(ApiResponse.error("Ошибка валидации", errorResponse));
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ApiResponse<ErrorResponse>> handleGenericException(Exception e) {
|
||||
ErrorResponse errorResponse = new ErrorResponse(
|
||||
"INTERNAL_SERVER_ERROR",
|
||||
"Произошла внутренняя ошибка сервера. Попробуйте позже.");
|
||||
|
||||
return ResponseEntity.status(500)
|
||||
.body(ApiResponse.error("Внутренняя ошибка сервера", errorResponse));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,14 @@ import java.util.List;
|
||||
|
||||
public class AnalysisHistoryResponse {
|
||||
private String analysisId;
|
||||
private String businessNiche;
|
||||
private String product;
|
||||
private String location;
|
||||
private String clientType;
|
||||
private String differentiator;
|
||||
private String targetAudience;
|
||||
private String region;
|
||||
private String goal;
|
||||
private String detailLevel;
|
||||
private String strongSide;
|
||||
private String weakSide;
|
||||
private String analysisType;
|
||||
private String status;
|
||||
private String userId;
|
||||
@@ -27,6 +31,14 @@ public class AnalysisHistoryResponse {
|
||||
this.analysisId = analysisId;
|
||||
}
|
||||
|
||||
public String getBusinessNiche() {
|
||||
return businessNiche;
|
||||
}
|
||||
|
||||
public void setBusinessNiche(String businessNiche) {
|
||||
this.businessNiche = businessNiche;
|
||||
}
|
||||
|
||||
public String getProduct() {
|
||||
return product;
|
||||
}
|
||||
@@ -35,28 +47,52 @@ public class AnalysisHistoryResponse {
|
||||
this.product = product;
|
||||
}
|
||||
|
||||
public String getLocation() {
|
||||
return location;
|
||||
public String getTargetAudience() {
|
||||
return targetAudience;
|
||||
}
|
||||
|
||||
public void setLocation(String location) {
|
||||
this.location = location;
|
||||
public void setTargetAudience(String targetAudience) {
|
||||
this.targetAudience = targetAudience;
|
||||
}
|
||||
|
||||
public String getClientType() {
|
||||
return clientType;
|
||||
public String getRegion() {
|
||||
return region;
|
||||
}
|
||||
|
||||
public void setClientType(String clientType) {
|
||||
this.clientType = clientType;
|
||||
public void setRegion(String region) {
|
||||
this.region = region;
|
||||
}
|
||||
|
||||
public String getDifferentiator() {
|
||||
return differentiator;
|
||||
public String getGoal() {
|
||||
return goal;
|
||||
}
|
||||
|
||||
public void setDifferentiator(String differentiator) {
|
||||
this.differentiator = differentiator;
|
||||
public void setGoal(String goal) {
|
||||
this.goal = goal;
|
||||
}
|
||||
|
||||
public String getDetailLevel() {
|
||||
return detailLevel;
|
||||
}
|
||||
|
||||
public void setDetailLevel(String detailLevel) {
|
||||
this.detailLevel = detailLevel;
|
||||
}
|
||||
|
||||
public String getStrongSide() {
|
||||
return strongSide;
|
||||
}
|
||||
|
||||
public void setStrongSide(String strongSide) {
|
||||
this.strongSide = strongSide;
|
||||
}
|
||||
|
||||
public String getWeakSide() {
|
||||
return weakSide;
|
||||
}
|
||||
|
||||
public void setWeakSide(String weakSide) {
|
||||
this.weakSide = weakSide;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
|
||||
@@ -4,28 +4,44 @@ import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import kz.konturai.parser.model.AnalysisType;
|
||||
import kz.konturai.parser.model.DetailLevel;
|
||||
import kz.konturai.parser.validator.ValidAnalysisType;
|
||||
import kz.konturai.parser.validator.ValidClientType;
|
||||
import kz.konturai.parser.validator.ValidDetailLevel;
|
||||
import kz.konturai.parser.validator.ValidRegion;
|
||||
|
||||
public class MarketingAnalysisRequest {
|
||||
|
||||
@NotBlank(message = "Поле 'businessNiche' обязательно для заполнения")
|
||||
@Size(min = 3, max = 200, message = "Поле 'businessNiche' должно содержать от 3 до 200 символов")
|
||||
@Pattern(regexp = "^[\\p{L}\\p{N}\\s\\-,]+$", message = "Поле 'businessNiche' содержит недопустимые символы")
|
||||
private String businessNiche;
|
||||
|
||||
@NotBlank(message = "Поле 'product' обязательно для заполнения")
|
||||
@Size(min = 3, max = 200, message = "Поле 'product' должно содержать от 3 до 200 символов")
|
||||
@Pattern(regexp = "^[\\p{L}\\p{N}\\s\\-,]+$", message = "Поле 'product' содержит недопустимые символы")
|
||||
private String product;
|
||||
|
||||
@NotBlank(message = "Поле 'location' обязательно для заполнения")
|
||||
@Size(min = 2, max = 150, message = "Поле 'location' должно содержать от 2 до 150 символов")
|
||||
@Pattern(regexp = "^[\\p{L}\\p{N}\\s\\-,]+$", message = "Поле 'location' содержит недопустимые символы")
|
||||
private String location;
|
||||
@NotBlank(message = "Поле 'targetAudience' обязательно для заполнения")
|
||||
@Size(min = 3, max = 300, message = "Поле 'targetAudience' должно содержать от 3 до 300 символов")
|
||||
private String targetAudience;
|
||||
|
||||
@NotBlank(message = "Поле 'client' обязательно для заполнения")
|
||||
@ValidClientType(message = "Поле 'client' должно быть одним из: B2B клиенты, B2C клиенты, Частные лица, Корпорации, Малый бизнес")
|
||||
private String client;
|
||||
@NotBlank(message = "Поле 'region' обязательно для заполнения")
|
||||
@ValidRegion(message = "Поле 'region' должно быть одним из допустимых городов Казахстана")
|
||||
private String region;
|
||||
|
||||
@NotBlank(message = "Поле 'differentiator' обязательно для заполнения")
|
||||
@Size(min = 10, max = 500, message = "Поле 'differentiator' должно содержать от 10 до 500 символов")
|
||||
private String differentiator;
|
||||
@NotBlank(message = "Поле 'goal' обязательно для заполнения")
|
||||
@Size(min = 10, max = 500, message = "Поле 'goal' должно содержать от 10 до 500 символов")
|
||||
private String goal;
|
||||
|
||||
@NotBlank(message = "Поле 'detailLevel' обязательно для заполнения")
|
||||
@ValidDetailLevel(message = "Поле 'detailLevel' должно быть одним из: КРАТКО, СТАНДАРТНО, ПОДРОБНО")
|
||||
private String detailLevel;
|
||||
|
||||
@Size(max = 500, message = "Поле 'strongSide' должно содержать не более 500 символов")
|
||||
private String strongSide;
|
||||
|
||||
@Size(max = 500, message = "Поле 'weakSide' должно содержать не более 500 символов")
|
||||
private String weakSide;
|
||||
|
||||
@NotBlank(message = "Поле 'analysisType' обязательно для заполнения")
|
||||
@ValidAnalysisType(message = "Поле 'analysisType' должно быть одним из: РЫНОК, КОНКУРЕНТЫ, ЦА, КАНАЛЫ, SWOT")
|
||||
@@ -34,15 +50,27 @@ public class MarketingAnalysisRequest {
|
||||
public MarketingAnalysisRequest() {
|
||||
}
|
||||
|
||||
public MarketingAnalysisRequest(String product, String location, String client, String differentiator,
|
||||
String analysisType) {
|
||||
public MarketingAnalysisRequest(String businessNiche, String product, String targetAudience, String region,
|
||||
String goal, String detailLevel, String strongSide, String weakSide, String analysisType) {
|
||||
this.businessNiche = businessNiche;
|
||||
this.product = product;
|
||||
this.location = location;
|
||||
this.client = client;
|
||||
this.differentiator = differentiator;
|
||||
this.targetAudience = targetAudience;
|
||||
this.region = region;
|
||||
this.goal = goal;
|
||||
this.detailLevel = detailLevel;
|
||||
this.strongSide = strongSide;
|
||||
this.weakSide = weakSide;
|
||||
this.analysisType = analysisType;
|
||||
}
|
||||
|
||||
public String getBusinessNiche() {
|
||||
return businessNiche;
|
||||
}
|
||||
|
||||
public void setBusinessNiche(String businessNiche) {
|
||||
this.businessNiche = businessNiche;
|
||||
}
|
||||
|
||||
public String getProduct() {
|
||||
return product;
|
||||
}
|
||||
@@ -51,28 +79,56 @@ public class MarketingAnalysisRequest {
|
||||
this.product = product;
|
||||
}
|
||||
|
||||
public String getLocation() {
|
||||
return location;
|
||||
public String getTargetAudience() {
|
||||
return targetAudience;
|
||||
}
|
||||
|
||||
public void setLocation(String location) {
|
||||
this.location = location;
|
||||
public void setTargetAudience(String targetAudience) {
|
||||
this.targetAudience = targetAudience;
|
||||
}
|
||||
|
||||
public String getClient() {
|
||||
return client;
|
||||
public String getRegion() {
|
||||
return region;
|
||||
}
|
||||
|
||||
public void setClient(String client) {
|
||||
this.client = client;
|
||||
public void setRegion(String region) {
|
||||
this.region = region;
|
||||
}
|
||||
|
||||
public String getDifferentiator() {
|
||||
return differentiator;
|
||||
public String getGoal() {
|
||||
return goal;
|
||||
}
|
||||
|
||||
public void setDifferentiator(String differentiator) {
|
||||
this.differentiator = differentiator;
|
||||
public void setGoal(String goal) {
|
||||
this.goal = goal;
|
||||
}
|
||||
|
||||
public String getDetailLevel() {
|
||||
return detailLevel;
|
||||
}
|
||||
|
||||
public void setDetailLevel(String detailLevel) {
|
||||
this.detailLevel = detailLevel;
|
||||
}
|
||||
|
||||
public DetailLevel getDetailLevelEnum() {
|
||||
return DetailLevel.fromString(detailLevel);
|
||||
}
|
||||
|
||||
public String getStrongSide() {
|
||||
return strongSide;
|
||||
}
|
||||
|
||||
public void setStrongSide(String strongSide) {
|
||||
this.strongSide = strongSide;
|
||||
}
|
||||
|
||||
public String getWeakSide() {
|
||||
return weakSide;
|
||||
}
|
||||
|
||||
public void setWeakSide(String weakSide) {
|
||||
this.weakSide = weakSide;
|
||||
}
|
||||
|
||||
public String getAnalysisType() {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package kz.konturai.parser.model;
|
||||
|
||||
public enum DetailLevel {
|
||||
КРАТКО("Кратко"),
|
||||
СТАНДАРТНО("Стандартно"),
|
||||
ПОДРОБНО("Подробно");
|
||||
|
||||
private final String displayName;
|
||||
|
||||
DetailLevel(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public static DetailLevel fromString(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return DetailLevel.valueOf(value.toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Try to match by display name
|
||||
for (DetailLevel level : DetailLevel.values()) {
|
||||
if (level.getDisplayName().equalsIgnoreCase(value) ||
|
||||
level.name().equalsIgnoreCase(value)) {
|
||||
return level;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,17 +16,29 @@ public class MarketingAnalysis {
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Field("business_niche")
|
||||
private String businessNiche;
|
||||
|
||||
@Field("product")
|
||||
private String product;
|
||||
|
||||
@Field("location")
|
||||
private String location;
|
||||
@Field("target_audience")
|
||||
private String targetAudience;
|
||||
|
||||
@Field("client_type")
|
||||
private String clientType;
|
||||
@Field("region")
|
||||
private String region;
|
||||
|
||||
@Field("differentiator")
|
||||
private String differentiator;
|
||||
@Field("goal")
|
||||
private String goal;
|
||||
|
||||
@Field("detail_level")
|
||||
private String detailLevel; // КРАТКО, СТАНДАРТНО, ПОДРОБНО
|
||||
|
||||
@Field("strong_side")
|
||||
private String strongSide;
|
||||
|
||||
@Field("weak_side")
|
||||
private String weakSide;
|
||||
|
||||
@Field("analysis_type")
|
||||
private String analysisType; // РЫНОК, КОНКУРЕНТЫ, ЦА, КАНАЛЫ, SWOT
|
||||
@@ -61,21 +73,17 @@ public class MarketingAnalysis {
|
||||
this.statusHistory = new ArrayList<>();
|
||||
}
|
||||
|
||||
public MarketingAnalysis(String product, String location, String clientType, String differentiator) {
|
||||
public MarketingAnalysis(String businessNiche, String product, String targetAudience, String region,
|
||||
String goal, String detailLevel, String strongSide, String weakSide, String analysisType) {
|
||||
this();
|
||||
this.businessNiche = businessNiche;
|
||||
this.product = product;
|
||||
this.location = location;
|
||||
this.clientType = clientType;
|
||||
this.differentiator = differentiator;
|
||||
}
|
||||
|
||||
public MarketingAnalysis(String product, String location, String clientType, String differentiator,
|
||||
String analysisType) {
|
||||
this();
|
||||
this.product = product;
|
||||
this.location = location;
|
||||
this.clientType = clientType;
|
||||
this.differentiator = differentiator;
|
||||
this.targetAudience = targetAudience;
|
||||
this.region = region;
|
||||
this.goal = goal;
|
||||
this.detailLevel = detailLevel;
|
||||
this.strongSide = strongSide;
|
||||
this.weakSide = weakSide;
|
||||
this.analysisType = analysisType;
|
||||
}
|
||||
|
||||
@@ -95,28 +103,60 @@ public class MarketingAnalysis {
|
||||
this.product = product;
|
||||
}
|
||||
|
||||
public String getLocation() {
|
||||
return location;
|
||||
public String getBusinessNiche() {
|
||||
return businessNiche;
|
||||
}
|
||||
|
||||
public void setLocation(String location) {
|
||||
this.location = location;
|
||||
public void setBusinessNiche(String businessNiche) {
|
||||
this.businessNiche = businessNiche;
|
||||
}
|
||||
|
||||
public String getClientType() {
|
||||
return clientType;
|
||||
public String getTargetAudience() {
|
||||
return targetAudience;
|
||||
}
|
||||
|
||||
public void setClientType(String clientType) {
|
||||
this.clientType = clientType;
|
||||
public void setTargetAudience(String targetAudience) {
|
||||
this.targetAudience = targetAudience;
|
||||
}
|
||||
|
||||
public String getDifferentiator() {
|
||||
return differentiator;
|
||||
public String getRegion() {
|
||||
return region;
|
||||
}
|
||||
|
||||
public void setDifferentiator(String differentiator) {
|
||||
this.differentiator = differentiator;
|
||||
public void setRegion(String region) {
|
||||
this.region = region;
|
||||
}
|
||||
|
||||
public String getGoal() {
|
||||
return goal;
|
||||
}
|
||||
|
||||
public void setGoal(String goal) {
|
||||
this.goal = goal;
|
||||
}
|
||||
|
||||
public String getDetailLevel() {
|
||||
return detailLevel;
|
||||
}
|
||||
|
||||
public void setDetailLevel(String detailLevel) {
|
||||
this.detailLevel = detailLevel;
|
||||
}
|
||||
|
||||
public String getStrongSide() {
|
||||
return strongSide;
|
||||
}
|
||||
|
||||
public void setStrongSide(String strongSide) {
|
||||
this.strongSide = strongSide;
|
||||
}
|
||||
|
||||
public String getWeakSide() {
|
||||
return weakSide;
|
||||
}
|
||||
|
||||
public void setWeakSide(String weakSide) {
|
||||
this.weakSide = weakSide;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
|
||||
@@ -4,6 +4,7 @@ import kz.konturai.parser.dto.MarketingAnalysisRequest;
|
||||
import kz.konturai.parser.dto.MarketingAnalysisResult;
|
||||
import kz.konturai.parser.dto.StatusHistoryEntry;
|
||||
import kz.konturai.parser.model.AnalysisType;
|
||||
import kz.konturai.parser.model.DetailLevel;
|
||||
import kz.konturai.parser.model.MarketingAnalysis;
|
||||
import kz.konturai.parser.repository.MarketingAnalysisRepository;
|
||||
import org.slf4j.Logger;
|
||||
@@ -38,10 +39,14 @@ public class MarketingAnalysisService {
|
||||
|
||||
public MarketingAnalysis startAnalysis(MarketingAnalysisRequest request, String userId) {
|
||||
MarketingAnalysis analysis = new MarketingAnalysis(
|
||||
request.getBusinessNiche(),
|
||||
request.getProduct(),
|
||||
request.getLocation(),
|
||||
request.getClient(),
|
||||
request.getDifferentiator(),
|
||||
request.getTargetAudience(),
|
||||
request.getRegion(),
|
||||
request.getGoal(),
|
||||
request.getDetailLevel(),
|
||||
request.getStrongSide(),
|
||||
request.getWeakSide(),
|
||||
request.getAnalysisType());
|
||||
analysis.setUserId(userId);
|
||||
analysis.setStatus("queued");
|
||||
@@ -122,32 +127,52 @@ public class MarketingAnalysisService {
|
||||
private Map<String, Object> generateMarketingReport(MarketingAnalysisRequest request) {
|
||||
Map<String, Object> report = new HashMap<>();
|
||||
|
||||
// Get analysis type
|
||||
// Get analysis type and detail level
|
||||
AnalysisType analysisType = request.getAnalysisTypeEnum();
|
||||
String analysisTypeName = analysisType != null ? analysisType.getDisplayName() : "Общий";
|
||||
DetailLevel detailLevel = request.getDetailLevelEnum();
|
||||
String detailLevelName = detailLevel != null ? detailLevel.getDisplayName() : "Стандартно";
|
||||
|
||||
// Build context for AI
|
||||
String context = String.format(
|
||||
"Продукт/услуга: %s\n" +
|
||||
"Локация: %s\n" +
|
||||
"Тип клиентов: %s\n" +
|
||||
"Уникальные особенности: %s\n" +
|
||||
"Тип анализа: %s\n",
|
||||
request.getProduct(),
|
||||
request.getLocation(),
|
||||
request.getClient(),
|
||||
request.getDifferentiator(),
|
||||
analysisTypeName);
|
||||
// Build context for AI with all new fields
|
||||
StringBuilder contextBuilder = new StringBuilder();
|
||||
contextBuilder.append("Ниша бизнеса: ").append(request.getBusinessNiche()).append("\n");
|
||||
contextBuilder.append("Продукт/услуга: ").append(request.getProduct()).append("\n");
|
||||
contextBuilder.append("Целевая аудитория: ").append(request.getTargetAudience()).append("\n");
|
||||
contextBuilder.append("Регион: ").append(request.getRegion()).append("\n");
|
||||
contextBuilder.append("Цель на 6-12 месяцев: ").append(request.getGoal()).append("\n");
|
||||
contextBuilder.append("Уровень детализации: ").append(detailLevelName).append("\n");
|
||||
if (request.getStrongSide() != null && !request.getStrongSide().trim().isEmpty()) {
|
||||
contextBuilder.append("Сильная сторона: ").append(request.getStrongSide()).append("\n");
|
||||
}
|
||||
if (request.getWeakSide() != null && !request.getWeakSide().trim().isEmpty()) {
|
||||
contextBuilder.append("Слабая сторона: ").append(request.getWeakSide()).append("\n");
|
||||
}
|
||||
contextBuilder.append("Тип анализа: ").append(analysisTypeName).append("\n");
|
||||
String context = contextBuilder.toString();
|
||||
|
||||
// Generate analysis based on type
|
||||
String analysisContent = generateAnalysisByType(context, analysisType);
|
||||
// Generate analysis based on type and detail level
|
||||
String analysisContent = generateAnalysisByType(context, analysisType, detailLevel);
|
||||
report.put("analysisType", analysisTypeName);
|
||||
report.put("detailLevel", detailLevelName);
|
||||
report.put("summary", analysisContent != null ? analysisContent : "Анализ не удалось сгенерировать.");
|
||||
|
||||
// Generate target audience description
|
||||
String audiencePrompt = "На основе информации о бизнесе опиши целевую аудиторию (1-2 абзаца). " +
|
||||
"Укажи, какие каналы коммуникации наиболее подходят для этой аудитории. " +
|
||||
"Ответ должен быть на русском языке.\n\n" + context;
|
||||
// Generate target audience description (adjusted by detail level)
|
||||
String audiencePrompt;
|
||||
if (detailLevel == DetailLevel.КРАТКО) {
|
||||
audiencePrompt = "На основе информации о бизнесе опиши целевую аудиторию кратко (1 абзац). " +
|
||||
"Укажи основные каналы коммуникации. Ответ должен быть на русском языке.\n\n" + context;
|
||||
} else if (detailLevel == DetailLevel.ПОДРОБНО) {
|
||||
audiencePrompt = "На основе информации о бизнесе проведи детальный анализ целевой аудитории (3-4 абзаца). "
|
||||
+
|
||||
"Включи демографические характеристики, психографический профиль, потребности, поведенческие паттерны. "
|
||||
+
|
||||
"Укажи, какие каналы коммуникации наиболее подходят для этой аудитории и почему. " +
|
||||
"Ответ должен быть на русском языке.\n\n" + context;
|
||||
} else {
|
||||
audiencePrompt = "На основе информации о бизнесе опиши целевую аудиторию (1-2 абзаца). " +
|
||||
"Укажи, какие каналы коммуникации наиболее подходят для этой аудитории. " +
|
||||
"Ответ должен быть на русском языке.\n\n" + context;
|
||||
}
|
||||
String audienceDescription = openAIAnalyticsService.generateWithInstruction(context, audiencePrompt, "ru");
|
||||
|
||||
// Extract channels from description or generate separately
|
||||
@@ -164,22 +189,50 @@ public class MarketingAnalysisService {
|
||||
channels.isEmpty() ? Arrays.asList("Instagram", "LinkedIn", "Telegram") : channels);
|
||||
report.put("targetAudience", targetAudience);
|
||||
|
||||
// Generate recommendations
|
||||
String recommendationsPrompt = "На основе информации о бизнесе сформулируй 4-6 практических рекомендаций для маркетинговой стратегии. "
|
||||
+
|
||||
"Каждая рекомендация должна быть конкретной и применимой. " +
|
||||
"Ответ должен быть списком рекомендаций, каждая с новой строки, без нумерации.\n\n" + context;
|
||||
// Generate recommendations (adjusted by detail level)
|
||||
String recommendationsPrompt;
|
||||
if (detailLevel == DetailLevel.КРАТКО) {
|
||||
recommendationsPrompt = "На основе информации о бизнесе сформулируй 3-4 краткие практические рекомендации для маркетинговой стратегии. "
|
||||
+
|
||||
"Каждая рекомендация должна быть конкретной. " +
|
||||
"Ответ должен быть списком рекомендаций, каждая с новой строки, без нумерации.\n\n" + context;
|
||||
} else if (detailLevel == DetailLevel.ПОДРОБНО) {
|
||||
recommendationsPrompt = "На основе информации о бизнесе сформулируй 6-8 детальных практических рекомендаций для маркетинговой стратегии. "
|
||||
+
|
||||
"Каждая рекомендация должна быть конкретной, применимой и содержать обоснование. " +
|
||||
"Ответ должен быть списком рекомендаций, каждая с новой строки, без нумерации.\n\n" + context;
|
||||
} else {
|
||||
recommendationsPrompt = "На основе информации о бизнесе сформулируй 4-6 практических рекомендаций для маркетинговой стратегии. "
|
||||
+
|
||||
"Каждая рекомендация должна быть конкретной и применимой. " +
|
||||
"Ответ должен быть списком рекомендаций, каждая с новой строки, без нумерации.\n\n" + context;
|
||||
}
|
||||
String recommendationsStr = openAIAnalyticsService.generateWithInstruction(context, recommendationsPrompt,
|
||||
"ru");
|
||||
List<String> recommendations = parseRecommendations(recommendationsStr);
|
||||
report.put("recommendations",
|
||||
recommendations.isEmpty() ? Arrays.asList("Рекомендации не удалось сгенерировать.") : recommendations);
|
||||
|
||||
// Generate strategy
|
||||
String strategyPrompt = "На основе информации о бизнесе создай краткую маркетинговую стратегию. " +
|
||||
"Укажи рекомендуемую длительность кампании (например, '2 недели', '1 месяц'), " +
|
||||
"3-5 каналов коммуникации и типы контента (например, 'посты', 'сторис', 'баннеры'). " +
|
||||
"Ответ должен быть структурированным текстом на русском языке.\n\n" + context;
|
||||
// Generate strategy (adjusted by detail level)
|
||||
String strategyPrompt;
|
||||
if (detailLevel == DetailLevel.КРАТКО) {
|
||||
strategyPrompt = "На основе информации о бизнесе создай краткую маркетинговую стратегию. " +
|
||||
"Укажи рекомендуемую длительность кампании (например, '2 недели', '1 месяц'), " +
|
||||
"3-4 канала коммуникации и типы контента (например, 'посты', 'сторис', 'баннеры'). " +
|
||||
"Ответ должен быть кратким структурированным текстом на русском языке.\n\n" + context;
|
||||
} else if (detailLevel == DetailLevel.ПОДРОБНО) {
|
||||
strategyPrompt = "На основе информации о бизнесе создай детальную маркетинговую стратегию. " +
|
||||
"Укажи рекомендуемую длительность кампании с обоснованием (например, '2 недели', '1 месяц'), " +
|
||||
"5-7 каналов коммуникации с описанием их преимуществ, типы контента (например, 'посты', 'сторис', 'баннеры') "
|
||||
+
|
||||
"и примерный план действий. Ответ должен быть подробным структурированным текстом на русском языке.\n\n"
|
||||
+ context;
|
||||
} else {
|
||||
strategyPrompt = "На основе информации о бизнесе создай маркетинговую стратегию. " +
|
||||
"Укажи рекомендуемую длительность кампании (например, '2 недели', '1 месяц'), " +
|
||||
"3-5 каналов коммуникации и типы контента (например, 'посты', 'сторис', 'баннеры'). " +
|
||||
"Ответ должен быть структурированным текстом на русском языке.\n\n" + context;
|
||||
}
|
||||
String strategyText = openAIAnalyticsService.generateWithInstruction(context, strategyPrompt, "ru");
|
||||
|
||||
Map<String, Object> strategy = parseStrategy(strategyText, channels);
|
||||
@@ -188,49 +241,60 @@ public class MarketingAnalysisService {
|
||||
return report;
|
||||
}
|
||||
|
||||
private String generateAnalysisByType(String context, AnalysisType analysisType) {
|
||||
private String generateAnalysisByType(String context, AnalysisType analysisType, DetailLevel detailLevel) {
|
||||
if (analysisType == null) {
|
||||
analysisType = AnalysisType.РЫНОК; // Default
|
||||
}
|
||||
if (detailLevel == null) {
|
||||
detailLevel = DetailLevel.СТАНДАРТНО; // Default
|
||||
}
|
||||
|
||||
String prompt;
|
||||
String lengthInstruction;
|
||||
|
||||
// Adjust length based on detail level
|
||||
if (detailLevel == DetailLevel.КРАТКО) {
|
||||
lengthInstruction = "Ответ должен быть кратким структурированным текстом на русском языке (1-2 абзаца).";
|
||||
} else if (detailLevel == DetailLevel.ПОДРОБНО) {
|
||||
lengthInstruction = "Ответ должен быть подробным структурированным текстом на русском языке (5-8 абзацев) с глубоким анализом.";
|
||||
} else {
|
||||
lengthInstruction = "Ответ должен быть структурированным текстом на русском языке (3-5 абзацев).";
|
||||
}
|
||||
|
||||
switch (analysisType) {
|
||||
case РЫНОК:
|
||||
prompt = "Проведи детальный анализ рынка на основе предоставленной информации о бизнесе. " +
|
||||
prompt = "Проведи анализ рынка на основе предоставленной информации о бизнесе. " +
|
||||
"Включи: размер рынка, динамику роста, основные сегменты, тренды и перспективы развития. " +
|
||||
"Ответ должен быть структурированным текстом на русском языке (3-5 абзацев).\n\n" + context;
|
||||
lengthInstruction + "\n\n" + context;
|
||||
break;
|
||||
case КОНКУРЕНТЫ:
|
||||
prompt = "Проведи анализ конкурентов на основе предоставленной информации о бизнесе. " +
|
||||
"Включи: основных конкурентов, их сильные и слабые стороны, позиционирование, " +
|
||||
"ценовую политику и маркетинговые стратегии. Ответ должен быть структурированным текстом на русском языке (3-5 абзацев).\n\n"
|
||||
+ context;
|
||||
"ценовую политику и маркетинговые стратегии. " + lengthInstruction + "\n\n" + context;
|
||||
break;
|
||||
case ЦА:
|
||||
prompt = "Проведи детальный анализ целевой аудитории на основе предоставленной информации о бизнесе. " +
|
||||
prompt = "Проведи анализ целевой аудитории на основе предоставленной информации о бизнесе. " +
|
||||
"Включи: демографические характеристики, психографический профиль, потребности и боли, " +
|
||||
"поведенческие паттерны и предпочтения. Ответ должен быть структурированным текстом на русском языке (3-5 абзацев).\n\n"
|
||||
+ context;
|
||||
"поведенческие паттерны и предпочтения. " + lengthInstruction + "\n\n" + context;
|
||||
break;
|
||||
case КАНАЛЫ:
|
||||
prompt = "Проведи анализ маркетинговых каналов на основе предоставленной информации о бизнесе. " +
|
||||
"Включи: оценку эффективности различных каналов коммуникации, рекомендации по выбору каналов, "
|
||||
+
|
||||
"особенности использования каждого канала и бюджетные рекомендации. Ответ должен быть структурированным текстом на русском языке (3-5 абзацев).\n\n"
|
||||
+ context;
|
||||
"особенности использования каждого канала и бюджетные рекомендации. " + lengthInstruction
|
||||
+ "\n\n" + context;
|
||||
break;
|
||||
case SWOT:
|
||||
prompt = "Проведи SWOT-анализ на основе предоставленной информации о бизнесе. " +
|
||||
"Включи детальный анализ: Сильных сторон (Strengths), Слабых сторон (Weaknesses), " +
|
||||
"Возможностей (Opportunities) и Угроз (Threats). Ответ должен быть структурированным текстом на русском языке, "
|
||||
+
|
||||
"с четким разделением по каждому разделу SWOT (3-5 абзацев).\n\n" + context;
|
||||
"Включи анализ: Сильных сторон (Strengths), Слабых сторон (Weaknesses), " +
|
||||
"Возможностей (Opportunities) и Угроз (Threats). " +
|
||||
"Ответ должен быть структурированным текстом на русском языке, " +
|
||||
"с четким разделением по каждому разделу SWOT. " + lengthInstruction + "\n\n" + context;
|
||||
break;
|
||||
default:
|
||||
prompt = "На основе следующей информации о бизнесе создай краткое резюме маркетингового анализа (2-3 абзаца). "
|
||||
+
|
||||
prompt = "На основе следующей информации о бизнесе создай резюме маркетингового анализа. " +
|
||||
"Выдели ключевые возможности и особенности бизнеса. " +
|
||||
"Ответ должен быть на русском языке, деловым стилем.\n\n" + context;
|
||||
lengthInstruction + "\n\n" + context;
|
||||
}
|
||||
|
||||
return openAIAnalyticsService.generateWithInstruction(context, prompt, "ru");
|
||||
@@ -329,10 +393,23 @@ public class MarketingAnalysisService {
|
||||
|
||||
markdown.append("# Маркетинговый анализ\n\n");
|
||||
markdown.append("## Информация о бизнесе\n\n");
|
||||
markdown.append("- **Ниша бизнеса:** ").append(request.getBusinessNiche()).append("\n");
|
||||
markdown.append("- **Продукт/услуга:** ").append(request.getProduct()).append("\n");
|
||||
markdown.append("- **Локация:** ").append(request.getLocation()).append("\n");
|
||||
markdown.append("- **Тип клиентов:** ").append(request.getClient()).append("\n");
|
||||
markdown.append("- **Уникальные особенности:** ").append(request.getDifferentiator()).append("\n");
|
||||
markdown.append("- **Целевая аудитория:** ").append(request.getTargetAudience()).append("\n");
|
||||
markdown.append("- **Регион:** ").append(request.getRegion()).append("\n");
|
||||
markdown.append("- **Цель на 6-12 месяцев:** ").append(request.getGoal()).append("\n");
|
||||
|
||||
String detailLevelName = (String) reportData.get("detailLevel");
|
||||
if (detailLevelName != null) {
|
||||
markdown.append("- **Уровень детализации:** ").append(detailLevelName).append("\n");
|
||||
}
|
||||
|
||||
if (request.getStrongSide() != null && !request.getStrongSide().trim().isEmpty()) {
|
||||
markdown.append("- **Сильная сторона:** ").append(request.getStrongSide()).append("\n");
|
||||
}
|
||||
if (request.getWeakSide() != null && !request.getWeakSide().trim().isEmpty()) {
|
||||
markdown.append("- **Слабая сторона:** ").append(request.getWeakSide()).append("\n");
|
||||
}
|
||||
|
||||
String analysisTypeName = (String) reportData.get("analysisType");
|
||||
if (analysisTypeName != null) {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package kz.konturai.parser.validator;
|
||||
|
||||
import jakarta.validation.ConstraintValidator;
|
||||
import jakarta.validation.ConstraintValidatorContext;
|
||||
import kz.konturai.parser.model.DetailLevel;
|
||||
|
||||
public class DetailLevelValidator implements ConstraintValidator<ValidDetailLevel, String> {
|
||||
|
||||
@Override
|
||||
public void initialize(ValidDetailLevel constraintAnnotation) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid(String value, ConstraintValidatorContext context) {
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
return true; // @NotBlank will handle null/empty
|
||||
}
|
||||
return DetailLevel.fromString(value) != null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package kz.konturai.parser.validator;
|
||||
|
||||
import jakarta.validation.ConstraintValidator;
|
||||
import jakarta.validation.ConstraintValidatorContext;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class RegionValidator implements ConstraintValidator<ValidRegion, String> {
|
||||
|
||||
private static final List<String> VALID_REGIONS = Arrays.asList(
|
||||
"Алматы",
|
||||
"Астана",
|
||||
"Шымкент",
|
||||
"Караганда",
|
||||
"Актобе",
|
||||
"Тараз",
|
||||
"Павлодар",
|
||||
"Усть-Каменогорск",
|
||||
"Семей",
|
||||
"Костанай",
|
||||
"Кызылорда",
|
||||
"Уральск",
|
||||
"Петропавловск",
|
||||
"Атырау",
|
||||
"Актау",
|
||||
"Туркестан",
|
||||
"Кокшетау",
|
||||
"Талдыкорган",
|
||||
"Экибастуз",
|
||||
"Рудный");
|
||||
|
||||
@Override
|
||||
public void initialize(ValidRegion constraintAnnotation) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid(String value, ConstraintValidatorContext context) {
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
return true; // @NotBlank will handle null/empty
|
||||
}
|
||||
return VALID_REGIONS.contains(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package kz.konturai.parser.validator;
|
||||
|
||||
import jakarta.validation.Constraint;
|
||||
import jakarta.validation.Payload;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Documented
|
||||
@Constraint(validatedBy = DetailLevelValidator.class)
|
||||
@Target({ ElementType.FIELD, ElementType.PARAMETER })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ValidDetailLevel {
|
||||
String message() default "Поле 'detailLevel' должно быть одним из: КРАТКО, СТАНДАРТНО, ПОДРОБНО";
|
||||
|
||||
Class<?>[] groups() default {};
|
||||
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package kz.konturai.parser.validator;
|
||||
|
||||
import jakarta.validation.Constraint;
|
||||
import jakarta.validation.Payload;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Documented
|
||||
@Constraint(validatedBy = RegionValidator.class)
|
||||
@Target({ ElementType.FIELD, ElementType.PARAMETER })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ValidRegion {
|
||||
String message() default "Поле 'region' должно быть одним из допустимых городов Казахстана";
|
||||
|
||||
Class<?>[] groups() default {};
|
||||
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
ТЕХНИЧЕСКОЕ ЗАДАНИЕ v0.2
|
||||
Подсистема: модуль «Маркетинг» (Анализ + Продвижение + Результаты) платформы NAK.AI
|
||||
|
||||
1. Общие положения
|
||||
Цель подсистемы:Реализовать единый модуль «Маркетинг», который для предпринимателя МСБ выполняет полный цикл:
|
||||
Собирает минимальные данные о бизнесе.
|
||||
Проводит автоматический маркетинговый анализ (рынок, конкуренты, ЦА, каналы, SWOT).
|
||||
Формирует понятный аналитический отчёт с рекомендациями простым языком.
|
||||
Позволяет на основе анализа создать и запустить продвижение (кампанию).
|
||||
Отображает результаты продвижения в виде метрик и простых выводов.
|
||||
Роль пользователя: владелец или представитель МСБ (без штатного маркетолога).Интерфейс: полностью на русском языке, без перегруза терминами.
|
||||
|
||||
2. Целевая аудитория и сценарий использования
|
||||
Малый и средний бизнес в Казахстане.
|
||||
Пользователь не обязан разбираться в маркетинге.
|
||||
Ожидание: “я отвечу на несколько вопросов → система сама всё посчитает → покажет выводы → предложит готовое продвижение”.
|
||||
Важно:Нельзя заставлять пользователя:
|
||||
выбирать типы рекламы (таргет/контекст и т.д.),
|
||||
выбирать форматы продвижения “по-умному”,
|
||||
считать бюджеты и вручную настраивать каналы.
|
||||
Система сама принимает сложные решения и даёт гибкий, но простой интерфейс.
|
||||
|
||||
3. Общая структура модуля «Маркетинг»
|
||||
Модуль «Маркетинг» включает 3 логических блока:
|
||||
Блок А. Маркетинговый анализ
|
||||
Блок B. Автоматическое продвижение
|
||||
Блок C. Результаты кампаний
|
||||
В UI это один раздел “Маркетинг” с навигацией по шагам.
|
||||
|
||||
4. Блок А. Маркетинговый анализ
|
||||
4.1. Назначение блока
|
||||
Собрать минимальный набор входных данных и выполнить автоматический анализ, чтобы:
|
||||
объяснить предпринимателю, на каком рынке он работает,
|
||||
кто его конкуренты,
|
||||
кто его клиент,
|
||||
какие каналы и форматы для него перспективны,
|
||||
в чём его сильные и слабые стороны.
|
||||
4.2. Пользовательский поток (step-by-step)
|
||||
Шаг А1. Экран «Начать анализ»
|
||||
Поля ввода (всё максимально простым языком):
|
||||
Чем занимается ваш бизнес?Тип: строка, max 255Примеры подсказок:
|
||||
«Студия маникюра»
|
||||
«Магазин детской одежды»
|
||||
«СТО по ремонту авто»
|
||||
Где вы работаете?Тип: строкаПримеры:
|
||||
«Алматы»
|
||||
«Астана»
|
||||
«Онлайн по всему Казахстану»
|
||||
Что вы продаёте?Тип: строкаПримеры:
|
||||
«Наращивание ресниц»
|
||||
«Пальто зимние»
|
||||
«Установка дверей»
|
||||
Кто ваш клиент? (опционально)Тип: строка + быстрые кнопки выбора:
|
||||
Женщины 20–40
|
||||
Мужчины 25–45
|
||||
Семьи
|
||||
Молодёжь
|
||||
Все подряд
|
||||
Кнопка: «Начать анализ»
|
||||
После нажатия создаётся сущность MarketingAnalysis со статусом PENDING.
|
||||
|
||||
Шаг А2. Обработка анализа
|
||||
Бэкенд:
|
||||
Меняет статус PENDING → IN_PROGRESS.
|
||||
Запускает внутренние алгоритмы анализа (в рамках MVP можно сделать заглушки/правила).
|
||||
Подзадачи анализа:
|
||||
Анализ рынка (MarketInsights)
|
||||
Анализ конкурентов (CompetitorInsights)
|
||||
Анализ ЦА (AudienceProfile)
|
||||
Анализ каналов (ChannelInsights)
|
||||
SWOT (SwotAnalysis)
|
||||
Формирование рекомендаций (Recommendation)
|
||||
|
||||
Шаг А3. Экран «Анализ выполняется»
|
||||
Пользователь видит:
|
||||
Прогресс-бар (этапы: рынок → конкуренты → ЦА → SWOT → отчёт)
|
||||
Сообщение:
|
||||
«Система анализирует ваш рынок, конкурентов и целевую аудиторию. Это может занять немного времени.»
|
||||
Опрашивается GET /api/marketing/analyses/{id}/status до состояния COMPLETED или FAILED.
|
||||
|
||||
Шаг А4. Экран «Готовый отчёт»
|
||||
После завершения анализа:
|
||||
UI запрашивает GET /api/marketing/analyses/{id} и отображает блоки:
|
||||
Обзор бизнеса (кратко, на основе BusinessProfile + входных)
|
||||
Рынок (MarketInsights)
|
||||
Конкуренты (CompetitorInsights)
|
||||
Целевая аудитория (AudienceProfile)
|
||||
Каналы (ChannelInsights)
|
||||
SWOT (SwotAnalysis)
|
||||
Рекомендации (список Recommendation)
|
||||
Кнопки:
|
||||
«Сохранить отчёт» (на будущее – выгрузка PDF/Word)
|
||||
«Перейти к продвижению» → открывает блок B.
|
||||
|
||||
5. Блок B. Автоматическое продвижение
|
||||
5.1. Назначение блока
|
||||
На основе данных анализа автоматически:
|
||||
сформировать цель кампании,
|
||||
предложить стратегию,
|
||||
создать структуру кампании и контент-пакет,— чтобы предпринимателю не пришлось самому разбираться в каналах и форматах.
|
||||
5.2. Пользовательский поток (step-by-step)
|
||||
Шаг B1. Экран «Выбор цели продвижения»
|
||||
Появляется после клика «Перейти к продвижению» или при заходе в маркетинг → вкладка «Продвижение».
|
||||
Пользователь видит вопрос:
|
||||
«Какой результат вы хотите получить?»
|
||||
Карточки-цели (CampaignGoal):
|
||||
Увеличить продажи (increase_sales)
|
||||
Получить больше заявок/звонков (get_leads)
|
||||
Повысить узнаваемость бренда (awareness)
|
||||
Продвигать акцию или спецпредложение (promo)
|
||||
Кнопка: «Продолжить»
|
||||
После выбора вызывается POST /api/promotion/campaigns с:
|
||||
{
|
||||
"business_id": "<id>",
|
||||
"analysis_id": "<last_or_selected>",
|
||||
"goal_id": "increase_sales"
|
||||
}
|
||||
Создаётся Campaign со статусом DRAFT.
|
||||
|
||||
Шаг B2. Автоматическое формирование стратегии
|
||||
Бэкенд:
|
||||
Читает MarketingAnalysis по analysis_id.
|
||||
Использует данные:
|
||||
AudienceProfile
|
||||
ChannelInsights
|
||||
MarketInsights
|
||||
Recommendation
|
||||
Формирует сущности:
|
||||
Strategy: общее описание, длительность (например, 7–14 дней).
|
||||
CampaignChannel: какие каналы будут использоваться.
|
||||
ContentItem: список контентных единиц (черновики).
|
||||
Пример:
|
||||
Каналы: Instagram, Telegram, 2GIS
|
||||
План:
|
||||
3 поста в Instagram
|
||||
7 сторис
|
||||
1 подборка на маркетплейсе/каталоге
|
||||
1 простая рассылка по клиентам (если есть база – позже)
|
||||
|
||||
Шаг B3. Экран «Стратегия продвижения»
|
||||
UI запрашивает GET /api/promotion/campaigns/{campaign_id} и отображает:
|
||||
Цель кампании (человекочитаемый текст)
|
||||
Каналы, которые выбрала система
|
||||
Краткое текстовое описание стратегии:
|
||||
«На основе вашего анализа система рекомендует 2 недели активного продвижения в Instagram и Telegram с упором на отзывы и примеры работ.»
|
||||
Пользователь может:
|
||||
«Принять стратегию» → переход к контенту
|
||||
«Редактировать детали» (в MVP можно просто подсветить, что это появится позже или минимально корректировать текст/названия)
|
||||
|
||||
Шаг B4. Экран «Материалы кампании» (контент-пакет)
|
||||
Отображаются ContentItem:
|
||||
Список:
|
||||
Тип: пост / сторис / баннер / email
|
||||
Канал
|
||||
Черновой текст (который может быть сгенерирован системой)
|
||||
Подсказка по визуалу (например: «фото до/после», «фото товара на человеке»)
|
||||
Пользователь может:
|
||||
Просмотреть
|
||||
При необходимости подправить текст (на фронте)
|
||||
Нажать «Готово, перейти к запуску»
|
||||
|
||||
Шаг B5. Экран «Запуск кампании»
|
||||
Простой экран подтверждения:
|
||||
«Кампания готова к запуску.Вы можете использовать эти материалы для публикации на своих площадках.В следующих версиях система сможет автоматически публиковать материалы через интеграции.»
|
||||
Кнопка:
|
||||
«Зафиксировать запуск кампании» → POST /api/promotion/campaigns/{id}/activate
|
||||
Статус Campaign меняется на ACTIVE.
|
||||
|
||||
6. Блок C. Результаты кампаний
|
||||
6.1. Назначение
|
||||
Показать предпринимателю понятную картину, как сработало продвижение:
|
||||
без сложных метрик,
|
||||
с упором на результат: охват, вовлечённость, заявки, наиболее эффективные каналы.
|
||||
6.2. Пользовательский поток
|
||||
Шаг C1. Список кампаний
|
||||
Экран:
|
||||
Таблица или карточки кампаний:
|
||||
Название кампании
|
||||
Цель
|
||||
Статус: DRAFT, ACTIVE, FINISHED
|
||||
Краткая метрика: например, «Охват: 13 200, Заявок: 31»
|
||||
Данные: GET /api/promotion/campaigns?business_id=...
|
||||
|
||||
Шаг C2. Экран «Результаты продвижения» (детали кампании)
|
||||
Показывает:
|
||||
Основные показатели (CampaignMetrics):
|
||||
Охват (суммарно)
|
||||
Вовлечённость (лайки/клики/сообщения)
|
||||
Заявки/обращения (если будут интеграции/ручной ввод)
|
||||
График:
|
||||
Ось X: дни
|
||||
Ось Y: охват/клики или композитный KPI
|
||||
По каналам:
|
||||
Instagram: охват, вовлечённость
|
||||
Telegram: охват, клики
|
||||
Прочие
|
||||
Итоговый вывод системы (короткий текст):
|
||||
«Лучше всего сработал Instagram, основная активность пришлась на 3–5 день кампании. Рекомендуется повторить подобный формат постов и усилить работу с отзывами.»
|
||||
|
||||
7. Сущности данных (укрупнённо)
|
||||
(Ты их уже видел, но теперь в рамках одного модуля.)
|
||||
7.1. Общие
|
||||
BusinessProfile
|
||||
MarketingAnalysis
|
||||
MarketInsights
|
||||
CompetitorInsights
|
||||
AudienceProfile
|
||||
ChannelInsights
|
||||
SwotAnalysis
|
||||
Recommendation
|
||||
7.2. Продвижение
|
||||
Campaign
|
||||
CampaignGoal
|
||||
Strategy
|
||||
Channel
|
||||
CampaignChannel
|
||||
ContentItem
|
||||
CampaignMetrics
|
||||
Связь главная:Campaign.analysis_id → MarketingAnalysis.id
|
||||
|
||||
8. Важные бизнес-правила и нюансы
|
||||
Пользователь не выбирает тип продвижения (“таргет” / “контекст”) — только цель, всё остальное решает система.
|
||||
Не задаём вопрос про бюджет на этом этапе (подписная модель).
|
||||
Не перегружаем терминами: CTA, CTR, CPC и т.д. — только человекочитаемые объяснения.
|
||||
В модуле «Маркетинг» нет технических слов “AI”, “нейросеть”, вместо этого — “система”, “анализ”.
|
||||
Переход от блока Анализа к Продвижению — мягкий, логичный, без ощущения “другого продукта”.
|
||||
|
||||
9. Нефункциональные требования (кратко)
|
||||
Все тексты для пользователя — на русском.
|
||||
Логирование действий пользователя и ключевых событий.
|
||||
API версионируемый: /api/v1/...
|
||||
Архитектура должна позволять вынести анализ и кампании в отдельные сервисы/очереди при росте нагрузки.
|
||||
@@ -0,0 +1,400 @@
|
||||
# Отличия между ТЗ и текущей реализацией
|
||||
|
||||
## Обзор
|
||||
|
||||
Документ описывает расхождения между техническим заданием (ТЗ v0.2) и текущей реализацией модуля «Маркетинг».
|
||||
|
||||
---
|
||||
|
||||
## Блок А. Маркетинговый анализ
|
||||
|
||||
### 1. Поля ввода формы
|
||||
|
||||
#### ТЗ (Шаг А1):
|
||||
|
||||
- **Чем занимается ваш бизнес?** (строка, max 255)
|
||||
- **Где вы работаете?** (строка) - примеры: "Алматы", "Астана", "Онлайн по всему Казахстану"
|
||||
- **Что вы продаёте?** (строка)
|
||||
- **Кто ваш клиент?** (опционально, строка + быстрые кнопки выбора)
|
||||
|
||||
#### Реализация:
|
||||
|
||||
- ✅ **businessNiche** (ниша бизнеса) - соответствует "Чем занимается ваш бизнес?"
|
||||
- ✅ **product** (продукт/услуга) - соответствует "Что вы продаёте?"
|
||||
- ✅ **targetAudience** (целевая аудитория) - соответствует "Кто ваш клиент?", но **обязательное поле** (в ТЗ опциональное)
|
||||
- ✅ **region** (регион) - соответствует "Где вы работаете?", но **только города Казахстана** (в ТЗ допускается "Онлайн по всему Казахстану")
|
||||
- ❌ **goal** (цель на 6-12 месяцев) - **новое поле, отсутствует в ТЗ**
|
||||
- ❌ **detailLevel** (уровень детализации) - **новое поле, отсутствует в ТЗ**
|
||||
- ❌ **strongSide** (сильная сторона) - **новое поле, отсутствует в ТЗ**
|
||||
- ❌ **weakSide** (слабая сторона) - **новое поле, отсутствует в ТЗ**
|
||||
|
||||
**Вывод**: Реализация расширена дополнительными полями, которые улучшают качество анализа, но не соответствуют минималистичному подходу из ТЗ.
|
||||
|
||||
---
|
||||
|
||||
### 2. Статусы анализа
|
||||
|
||||
#### ТЗ:
|
||||
|
||||
- `PENDING` → `IN_PROGRESS` → `COMPLETED` / `FAILED`
|
||||
|
||||
#### Реализация:
|
||||
|
||||
- `queued` → `processing` → `completed` / `failed`
|
||||
|
||||
**Вывод**: Разные названия статусов, но логика идентична.
|
||||
|
||||
---
|
||||
|
||||
### 3. Эндпоинты API
|
||||
|
||||
#### ТЗ:
|
||||
|
||||
- `POST /api/marketing/analyses` - создание анализа
|
||||
- `GET /api/marketing/analyses/{id}/status` - проверка статуса
|
||||
- `GET /api/marketing/analyses/{id}` - получение результата
|
||||
|
||||
#### Реализация:
|
||||
|
||||
- ✅ `POST /api/marketing/analysis/start` - создание анализа (путь отличается: `analysis` вместо `analyses`)
|
||||
- ❌ `GET /api/marketing/analysis/{id}/status` - **отсутствует отдельный эндпоинт для статуса**
|
||||
- ✅ `GET /api/marketing/analysis/{id}` - получение результата (путь отличается)
|
||||
|
||||
**Вывод**: Пути API отличаются (единственное число vs множественное), отсутствует отдельный эндпоинт для проверки статуса (статус возвращается вместе с результатом).
|
||||
|
||||
---
|
||||
|
||||
### 4. Структура ответа анализа
|
||||
|
||||
#### ТЗ (Шаг А4):
|
||||
|
||||
Отчёт должен содержать блоки:
|
||||
|
||||
- Обзор бизнеса
|
||||
- Рынок (MarketInsights)
|
||||
- Конкуренты (CompetitorInsights)
|
||||
- Целевая аудитория (AudienceProfile)
|
||||
- Каналы (ChannelInsights)
|
||||
- SWOT (SwotAnalysis)
|
||||
- Рекомендации (список Recommendation)
|
||||
|
||||
#### Реализация:
|
||||
|
||||
Ответ содержит:
|
||||
|
||||
- ✅ `summary` - резюме анализа (соответствует обзору)
|
||||
- ✅ `targetAudience` - целевая аудитория с описанием и каналами
|
||||
- ✅ `recommendations` - список рекомендаций
|
||||
- ✅ `strategy` - маркетинговая стратегия с каналами и типами контента
|
||||
- ❌ **Нет отдельных блоков** для Рынка, Конкурентов, SWOT, Каналов - всё объединено в `summary` на основе `analysisType`
|
||||
|
||||
**Вывод**: В ТЗ предполагается комплексный анализ со всеми блоками, в реализации анализ зависит от выбранного `analysisType` (один тип за раз).
|
||||
|
||||
---
|
||||
|
||||
### 5. Тип анализа
|
||||
|
||||
#### ТЗ:
|
||||
|
||||
Анализ включает **все типы одновременно**:
|
||||
|
||||
- Анализ рынка (MarketInsights)
|
||||
- Анализ конкурентов (CompetitorInsights)
|
||||
- Анализ ЦА (AudienceProfile)
|
||||
- Анализ каналов (ChannelInsights)
|
||||
- SWOT (SwotAnalysis)
|
||||
|
||||
#### Реализация:
|
||||
|
||||
Пользователь **выбирает один тип анализа**:
|
||||
|
||||
- `РЫНОК` - только анализ рынка
|
||||
- `КОНКУРЕНТЫ` - только анализ конкурентов
|
||||
- `ЦА` - только анализ целевой аудитории
|
||||
- `КАНАЛЫ` - только анализ каналов
|
||||
- `SWOT` - только SWOT-анализ
|
||||
|
||||
**Вывод**: Критическое отличие - в ТЗ анализ комплексный, в реализации пользователь выбирает один тип.
|
||||
|
||||
---
|
||||
|
||||
## Блок B. Автоматическое продвижение
|
||||
|
||||
### 1. Название сущности
|
||||
|
||||
#### ТЗ:
|
||||
|
||||
- `Campaign` (кампания)
|
||||
- `CampaignGoal` (цель кампании)
|
||||
- `Strategy` (стратегия)
|
||||
- `CampaignChannel` (каналы кампании)
|
||||
- `ContentItem` (контент-единицы)
|
||||
|
||||
#### Реализация:
|
||||
|
||||
- `MarketingStrategy` (маркетинговая стратегия) - **другое название**
|
||||
- Нет сущности `Campaign` - вместо неё используется `MarketingStrategy`
|
||||
- Нет сущности `CampaignGoal` - цели не реализованы
|
||||
- ✅ `MarketingStrategy.WeeklyPlan` - недельные планы
|
||||
- ✅ `MarketingStrategy.PostCalendarItem` - календарь постов (аналог ContentItem)
|
||||
|
||||
**Вывод**: Концептуальное отличие - в ТЗ есть отдельные сущности Campaign и Strategy, в реализации всё объединено в MarketingStrategy.
|
||||
|
||||
---
|
||||
|
||||
### 2. Эндпоинты продвижения
|
||||
|
||||
#### ТЗ:
|
||||
|
||||
- `POST /api/promotion/campaigns` - создание кампании с `business_id`, `analysis_id`, `goal_id`
|
||||
- `GET /api/promotion/campaigns/{campaign_id}` - получение стратегии кампании
|
||||
- `POST /api/promotion/campaigns/{id}/activate` - активация кампании
|
||||
|
||||
#### Реализация:
|
||||
|
||||
- ✅ `POST /api/marketing/analysis/strategy/generate` - генерация стратегии (путь отличается, нет `goal_id`)
|
||||
- ✅ `GET /api/marketing/analysis/strategy/{strategyId}` - получение стратегии
|
||||
- ✅ `POST /api/marketing/analysis/strategy/{strategyId}/start` - запуск стратегии (аналог активации)
|
||||
|
||||
**Вывод**: Пути API отличаются (`/api/promotion/campaigns` vs `/api/marketing/analysis/strategy`), отсутствует выбор цели кампании (`goal_id`).
|
||||
|
||||
---
|
||||
|
||||
### 3. Выбор цели продвижения
|
||||
|
||||
#### ТЗ (Шаг B1):
|
||||
|
||||
Пользователь выбирает цель из карточек:
|
||||
|
||||
- Увеличить продажи (`increase_sales`)
|
||||
- Получить больше заявок/звонков (`get_leads`)
|
||||
- Повысить узнаваемость бренда (`awareness`)
|
||||
- Продвигать акцию или спецпредложение (`promo`)
|
||||
|
||||
#### Реализация:
|
||||
|
||||
- ❌ **Выбор цели отсутствует** - стратегия генерируется автоматически без выбора цели пользователем
|
||||
|
||||
**Вывод**: Критическое отличие - в ТЗ пользователь выбирает цель, в реализации цель определяется автоматически системой.
|
||||
|
||||
---
|
||||
|
||||
### 4. Формирование стратегии
|
||||
|
||||
#### ТЗ (Шаг B2):
|
||||
|
||||
Стратегия формируется на основе:
|
||||
|
||||
- `AudienceProfile`
|
||||
- `ChannelInsights`
|
||||
- `MarketInsights`
|
||||
- `Recommendation`
|
||||
|
||||
Создаются:
|
||||
|
||||
- `Strategy` - описание, длительность (7-14 дней)
|
||||
- `CampaignChannel` - каналы
|
||||
- `ContentItem` - контент-единицы (черновики)
|
||||
|
||||
#### Реализация:
|
||||
|
||||
Стратегия формируется на основе:
|
||||
|
||||
- ✅ Данных из `MarketingAnalysis`
|
||||
- ✅ `targetAudience` из анализа
|
||||
- ✅ `recommendations` из анализа
|
||||
|
||||
Создаются:
|
||||
|
||||
- ✅ `MarketingStrategy` - описание, длительность в неделях
|
||||
- ✅ `priorityPlatforms` - приоритетные платформы
|
||||
- ✅ `WeeklyPlan` - недельные планы с темами
|
||||
- ✅ `PostCalendarItem` - календарь постов с текстами и хештегами
|
||||
|
||||
**Вывод**: Логика похожа, но структура данных отличается (недельные планы и календарь постов вместо простых ContentItem).
|
||||
|
||||
---
|
||||
|
||||
### 5. Экран "Материалы кампании"
|
||||
|
||||
#### ТЗ (Шаг B4):
|
||||
|
||||
Отображаются `ContentItem`:
|
||||
|
||||
- Тип: пост / сторис / баннер / email
|
||||
- Канал
|
||||
- Черновой текст
|
||||
- Подсказка по визуалу
|
||||
|
||||
#### Реализация:
|
||||
|
||||
Отображаются `PostCalendarItem`:
|
||||
|
||||
- ✅ `platform` - канал
|
||||
- ✅ `contentType` - тип контента (пост, сторис, видео, баннер)
|
||||
- ✅ `postText` - текст поста
|
||||
- ✅ `hashtags` - хештеги
|
||||
- ✅ `publishDate` - дата публикации
|
||||
- ✅ `publishTime` - время публикации
|
||||
- ❌ **Нет подсказок по визуалу**
|
||||
|
||||
**Вывод**: Реализация более детальная (даты, время, хештеги), но отсутствуют подсказки по визуалу.
|
||||
|
||||
---
|
||||
|
||||
## Блок C. Результаты кампаний
|
||||
|
||||
### 1. Эндпоинты результатов
|
||||
|
||||
#### ТЗ:
|
||||
|
||||
- `GET /api/promotion/campaigns?business_id=...` - список кампаний
|
||||
- Детали кампании через тот же эндпоинт
|
||||
|
||||
#### Реализация:
|
||||
|
||||
- ❌ **Эндпоинты для результатов кампаний отсутствуют**
|
||||
- ✅ Есть `GET /api/marketing/analysis/strategy/my` - список стратегий пользователя
|
||||
- ✅ Есть `PostingTask` - задачи на публикацию, но нет метрик
|
||||
|
||||
**Вывод**: Блок C (Результаты кампаний) **не реализован**. Нет метрик, графиков, итоговых выводов.
|
||||
|
||||
---
|
||||
|
||||
### 2. Метрики кампаний
|
||||
|
||||
#### ТЗ (Шаг C2):
|
||||
|
||||
Должны отображаться:
|
||||
|
||||
- Охват (суммарно)
|
||||
- Вовлечённость (лайки/клики/сообщения)
|
||||
- Заявки/обращения
|
||||
- График по дням
|
||||
- Метрики по каналам (Instagram, Telegram и т.д.)
|
||||
- Итоговый вывод системы
|
||||
|
||||
#### Реализация:
|
||||
|
||||
- ❌ **Метрики отсутствуют**
|
||||
- ❌ **Графики отсутствуют**
|
||||
- ❌ **Итоговые выводы отсутствуют**
|
||||
- ✅ Есть `PostingTask` - задачи на публикацию, но без метрик выполнения
|
||||
|
||||
**Вывод**: Функционал отслеживания результатов кампаний полностью отсутствует.
|
||||
|
||||
---
|
||||
|
||||
## Общие отличия
|
||||
|
||||
### 1. API версионирование
|
||||
|
||||
#### ТЗ:
|
||||
|
||||
- API должен быть версионируемым: `/api/v1/...`
|
||||
|
||||
#### Реализация:
|
||||
|
||||
- ❌ **Версионирование отсутствует** - используется `/api/marketing/...`
|
||||
|
||||
**Вывод**: Не соответствует требованию версионирования API.
|
||||
|
||||
---
|
||||
|
||||
### 2. Структура данных
|
||||
|
||||
#### ТЗ:
|
||||
|
||||
Сущности:
|
||||
|
||||
- `BusinessProfile`
|
||||
- `MarketingAnalysis`
|
||||
- `MarketInsights`
|
||||
- `CompetitorInsights`
|
||||
- `AudienceProfile`
|
||||
- `ChannelInsights`
|
||||
- `SwotAnalysis`
|
||||
- `Recommendation`
|
||||
- `Campaign`
|
||||
- `CampaignGoal`
|
||||
- `Strategy`
|
||||
- `CampaignChannel`
|
||||
- `ContentItem`
|
||||
- `CampaignMetrics`
|
||||
|
||||
#### Реализация:
|
||||
|
||||
Сущности:
|
||||
|
||||
- ✅ `MarketingAnalysis`
|
||||
- ✅ `MarketingStrategy`
|
||||
- ✅ `PostingTask`
|
||||
- ❌ **Нет отдельных сущностей** для `MarketInsights`, `CompetitorInsights`, `AudienceProfile`, `ChannelInsights`, `SwotAnalysis` - данные хранятся в `reportData` как Map
|
||||
- ❌ **Нет `Campaign`** - используется `MarketingStrategy`
|
||||
- ❌ **Нет `CampaignGoal`**
|
||||
- ❌ **Нет `CampaignMetrics`**
|
||||
|
||||
**Вывод**: Структура данных упрощена - многие сущности объединены или отсутствуют.
|
||||
|
||||
---
|
||||
|
||||
### 3. Минималистичный подход
|
||||
|
||||
#### ТЗ:
|
||||
|
||||
> "Собирает минимальные данные о бизнесе"
|
||||
> "Нельзя заставлять пользователя выбирать типы рекламы, форматы продвижения"
|
||||
|
||||
#### Реализация:
|
||||
|
||||
- ❌ Пользователь должен выбрать `analysisType` (тип анализа)
|
||||
- ❌ Пользователь должен выбрать `detailLevel` (уровень детализации)
|
||||
- ❌ Добавлены дополнительные поля (`goal`, `strongSide`, `weakSide`)
|
||||
|
||||
**Вывод**: Реализация требует больше данных от пользователя, чем предполагалось в ТЗ.
|
||||
|
||||
---
|
||||
|
||||
## Резюме критических отличий
|
||||
|
||||
### 🔴 Критические расхождения:
|
||||
|
||||
1. **Тип анализа**: В ТЗ анализ комплексный (все типы сразу), в реализации - выбор одного типа
|
||||
2. **Блок C (Результаты)**: Полностью не реализован - нет метрик, графиков, выводов
|
||||
3. **Выбор цели кампании**: Отсутствует в реализации
|
||||
4. **Структура данных**: Упрощена, многие сущности объединены или отсутствуют
|
||||
5. **API версионирование**: Отсутствует
|
||||
|
||||
### 🟡 Значительные отличия:
|
||||
|
||||
1. **Поля формы**: Добавлены дополнительные поля, не указанные в ТЗ
|
||||
2. **Пути API**: Отличаются от указанных в ТЗ
|
||||
3. **Названия сущностей**: `Campaign` → `MarketingStrategy`
|
||||
4. **Статусы**: Разные названия (`PENDING` vs `queued`)
|
||||
|
||||
### 🟢 Незначительные отличия:
|
||||
|
||||
1. **Детализация стратегии**: Реализация более детальная (недельные планы, календарь)
|
||||
2. **Структура ответа**: Данные организованы по-другому, но информация присутствует
|
||||
|
||||
---
|
||||
|
||||
## Рекомендации по приведению к ТЗ
|
||||
|
||||
### Приоритет 1 (Критично):
|
||||
|
||||
1. **Реализовать комплексный анализ** - генерировать все типы анализа одновременно, а не по выбору
|
||||
2. **Реализовать Блок C** - добавить метрики, графики, итоговые выводы
|
||||
3. **Добавить выбор цели кампании** - перед генерацией стратегии
|
||||
|
||||
### Приоритет 2 (Важно):
|
||||
|
||||
1. **Упростить форму** - убрать лишние поля или сделать их опциональными
|
||||
2. **Привести пути API** к указанным в ТЗ или обновить ТЗ
|
||||
3. **Добавить версионирование API** - `/api/v1/...`
|
||||
|
||||
### Приоритет 3 (Желательно):
|
||||
|
||||
1. **Переименовать сущности** или обновить ТЗ под текущую реализацию
|
||||
2. **Добавить подсказки по визуалу** в материалы кампании
|
||||
3. **Привести статусы** к единому виду
|
||||
Binary file not shown.
Reference in New Issue
Block a user