.
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
|
- Для долгосрочного использования рекомендуется использовать Long-Lived Token
|
||||||
- Токен должен иметь разрешения `pages_manage_posts` для публикации
|
- Токен должен иметь разрешения `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 компонентов
|
## Примеры 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")
|
@RequestMapping("/api/marketing/analysis")
|
||||||
public class MarketingController {
|
public class MarketingController {
|
||||||
|
|
||||||
private final MarketingAnalysisService marketingAnalysisService;
|
private final MarketingAnalysisService marketingAnalysisService;
|
||||||
private final MarketingStrategyService marketingStrategyService;
|
private final MarketingStrategyService marketingStrategyService;
|
||||||
private final MinIOService minIOService;
|
private final MinIOService minIOService;
|
||||||
private final JwtService jwtService;
|
private final JwtService jwtService;
|
||||||
private final PostingTaskService postingTaskService;
|
private final PostingTaskService postingTaskService;
|
||||||
|
|
||||||
public MarketingController(
|
public MarketingController(
|
||||||
MarketingAnalysisService marketingAnalysisService,
|
MarketingAnalysisService marketingAnalysisService,
|
||||||
MarketingStrategyService marketingStrategyService,
|
MarketingStrategyService marketingStrategyService,
|
||||||
MinIOService minIOService,
|
MinIOService minIOService,
|
||||||
JwtService jwtService,
|
JwtService jwtService,
|
||||||
PostingTaskService postingTaskService) {
|
PostingTaskService postingTaskService) {
|
||||||
this.marketingAnalysisService = marketingAnalysisService;
|
this.marketingAnalysisService = marketingAnalysisService;
|
||||||
this.marketingStrategyService = marketingStrategyService;
|
this.marketingStrategyService = marketingStrategyService;
|
||||||
this.minIOService = minIOService;
|
this.minIOService = minIOService;
|
||||||
this.jwtService = jwtService;
|
this.jwtService = jwtService;
|
||||||
this.postingTaskService = postingTaskService;
|
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create analysis record
|
private String extractUserIdFromHeader(String authHeader) {
|
||||||
MarketingAnalysis analysis = marketingAnalysisService.startAnalysis(request, userId);
|
if (authHeader == null || authHeader.isEmpty()) {
|
||||||
|
return null;
|
||||||
// Start async processing
|
}
|
||||||
marketingAnalysisService.processAnalysis(analysis.getId(), request);
|
return jwtService.extractUserIdFromHeader(authHeader);
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<MarketingAnalysis> optAnalysis = marketingAnalysisService.getAnalysisById(analysisId);
|
private ResponseEntity<?> unauthorizedResponse() {
|
||||||
if (optAnalysis.isEmpty()) {
|
ErrorResponse error = new ErrorResponse(
|
||||||
ErrorResponse error = new ErrorResponse(
|
"UNAUTHORIZED",
|
||||||
"NOT_FOUND",
|
"Требуется аутентификация. Пожалуйста, предоставьте валидный JWT токен.");
|
||||||
"Анализ с указанным ID не найден");
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||||
return ResponseEntity.status(404)
|
.body(ApiResponse.error("Не авторизован", error));
|
||||||
.body(ApiResponse.error("Анализ не найден", error));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MarketingAnalysis analysis = optAnalysis.get();
|
@PostMapping("/start")
|
||||||
if (!userId.equals(analysis.getUserId())) {
|
public ResponseEntity<?> startAnalysis(
|
||||||
ErrorResponse error = new ErrorResponse(
|
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||||
"FORBIDDEN",
|
@Valid @RequestBody MarketingAnalysisRequest request) {
|
||||||
"У вас нет доступа к этому анализу");
|
|
||||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
String userId = extractUserIdFromHeader(authHeader);
|
||||||
.body(ApiResponse.error("Доступ запрещен", error));
|
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);
|
@GetMapping("/{analysisId}")
|
||||||
return ResponseEntity.ok(ApiResponse.success(result));
|
public ResponseEntity<?> getAnalysis(
|
||||||
}
|
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||||
|
@PathVariable String analysisId) {
|
||||||
|
|
||||||
@GetMapping("/{analysisId}/download")
|
String userId = extractUserIdFromHeader(authHeader);
|
||||||
public ResponseEntity<?> downloadPdf(
|
if (userId == null) {
|
||||||
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
return unauthorizedResponse();
|
||||||
@PathVariable String analysisId) {
|
}
|
||||||
|
|
||||||
String userId = extractUserIdFromHeader(authHeader);
|
Optional<MarketingAnalysis> optAnalysis = marketingAnalysisService.getAnalysisById(analysisId);
|
||||||
if (userId == null) {
|
if (optAnalysis.isEmpty()) {
|
||||||
return unauthorizedResponse();
|
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);
|
@GetMapping("/{analysisId}/download")
|
||||||
if (optAnalysis.isEmpty()) {
|
public ResponseEntity<?> downloadPdf(
|
||||||
return ResponseEntity.notFound().build();
|
@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();
|
@PostMapping("/strategy/generate")
|
||||||
if (!userId.equals(analysis.getUserId())) {
|
public ResponseEntity<?> generateStrategy(
|
||||||
ErrorResponse error = new ErrorResponse(
|
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||||
"FORBIDDEN",
|
@RequestParam String analysisId,
|
||||||
"У вас нет доступа к этому анализу");
|
@Valid @RequestBody(required = false) MarketingStrategyRequest request) {
|
||||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
|
||||||
.body(ApiResponse.error("Доступ запрещен", error));
|
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) {
|
@GetMapping("/strategy/{strategyId}")
|
||||||
return ResponseEntity.notFound().build();
|
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 {
|
@GetMapping("/{analysisId}/strategy")
|
||||||
InputStream inputStream = minIOService.downloadFile(analysis.getPdfFilePath());
|
public ResponseEntity<?> getStrategyByAnalysis(
|
||||||
byte[] bytes = inputStream.readAllBytes();
|
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||||
inputStream.close();
|
@PathVariable String analysisId) {
|
||||||
|
|
||||||
return ResponseEntity.ok()
|
String userId = extractUserIdFromHeader(authHeader);
|
||||||
.header(HttpHeaders.CONTENT_DISPOSITION,
|
if (userId == null) {
|
||||||
"attachment; filename=\"" + analysis.getPdfFilename() + "\"")
|
return unauthorizedResponse();
|
||||||
.contentType(MediaType.APPLICATION_PDF)
|
}
|
||||||
.body(bytes);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return ResponseEntity.internalServerError().build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/strategy/generate")
|
// Check if user owns the analysis
|
||||||
public ResponseEntity<?> generateStrategy(
|
Optional<MarketingAnalysis> optAnalysis = marketingAnalysisService.getAnalysisById(analysisId);
|
||||||
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
if (optAnalysis.isEmpty()) {
|
||||||
@RequestParam String analysisId,
|
ErrorResponse error = new ErrorResponse(
|
||||||
@Valid @RequestBody(required = false) MarketingStrategyRequest request) {
|
"NOT_FOUND",
|
||||||
|
"Анализ с указанным ID не найден");
|
||||||
|
return ResponseEntity.status(404)
|
||||||
|
.body(ApiResponse.error("Анализ не найден", error));
|
||||||
|
}
|
||||||
|
|
||||||
String userId = extractUserIdFromHeader(authHeader);
|
MarketingAnalysis analysis = optAnalysis.get();
|
||||||
if (userId == null) {
|
if (!userId.equals(analysis.getUserId())) {
|
||||||
return unauthorizedResponse();
|
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
|
@GetMapping("/my")
|
||||||
Optional<MarketingAnalysis> optAnalysis = marketingAnalysisService.getAnalysisById(analysisId);
|
public ResponseEntity<?> getMyAnalyses(
|
||||||
if (optAnalysis.isEmpty()) {
|
@RequestHeader(value = "Authorization", required = false) String authHeader) {
|
||||||
ErrorResponse error = new ErrorResponse("INVALID_ANALYSIS", "Анализ не найден");
|
|
||||||
return ResponseEntity.status(404)
|
String userId = extractUserIdFromHeader(authHeader);
|
||||||
.body(ApiResponse.error("Анализ не найден", error));
|
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();
|
@GetMapping("/strategy/my")
|
||||||
if (!userId.equals(analysis.getUserId())) {
|
public ResponseEntity<?> getMyStrategies(
|
||||||
ErrorResponse error = new ErrorResponse(
|
@RequestHeader(value = "Authorization", required = false) String authHeader) {
|
||||||
"FORBIDDEN",
|
|
||||||
"У вас нет доступа к этому анализу");
|
String userId = extractUserIdFromHeader(authHeader);
|
||||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
if (userId == null) {
|
||||||
.body(ApiResponse.error("Доступ запрещен", error));
|
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) {
|
@GetMapping("/{analysisId}/history")
|
||||||
request = new MarketingStrategyRequest();
|
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 {
|
@GetMapping("/strategy/{strategyId}/history")
|
||||||
MarketingStrategy strategy = marketingStrategyService.generateStrategy(analysisId, request, userId);
|
public ResponseEntity<?> getStrategyHistory(
|
||||||
|
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||||
|
@PathVariable String strategyId) {
|
||||||
|
|
||||||
MarketingStrategyResponse response = new MarketingStrategyResponse();
|
String userId = extractUserIdFromHeader(authHeader);
|
||||||
response.setStrategyId(strategy.getId());
|
if (userId == null) {
|
||||||
response.setAnalysisId(strategy.getAnalysisId());
|
return unauthorizedResponse();
|
||||||
response.setStatus(strategy.getStatus());
|
}
|
||||||
response.setCreatedAt(strategy.getCreatedAt());
|
|
||||||
response.setDurationWeeks(strategy.getDurationWeeks());
|
|
||||||
response.setPriorityPlatforms(strategy.getPriorityPlatforms());
|
|
||||||
|
|
||||||
return ResponseEntity.ok(ApiResponse.success(
|
Optional<MarketingStrategy> optStrategy = marketingStrategyService.getStrategyById(strategyId);
|
||||||
"Генерация стратегии запущена успешно. Результаты будут готовы в течение 3-5 минут.",
|
if (optStrategy.isEmpty()) {
|
||||||
response));
|
ErrorResponse error = new ErrorResponse(
|
||||||
} catch (IllegalArgumentException e) {
|
"NOT_FOUND",
|
||||||
ErrorResponse error = new ErrorResponse("INVALID_ANALYSIS", e.getMessage());
|
"Стратегия с указанным ID не найдена");
|
||||||
return ResponseEntity.status(404)
|
return ResponseEntity.status(404)
|
||||||
.body(ApiResponse.error("Анализ не найден", error));
|
.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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/strategy/{strategyId}")
|
MarketingStrategy strategy = optStrategy.get();
|
||||||
public ResponseEntity<?> getStrategy(
|
if (!userId.equals(strategy.getUserId())) {
|
||||||
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
ErrorResponse error = new ErrorResponse(
|
||||||
@PathVariable String strategyId) {
|
"FORBIDDEN",
|
||||||
|
"У вас нет доступа к этой стратегии");
|
||||||
|
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||||
|
.body(ApiResponse.error("Доступ запрещен", error));
|
||||||
|
}
|
||||||
|
|
||||||
String userId = extractUserIdFromHeader(authHeader);
|
StrategyHistoryResponse response = convertToStrategyHistoryResponse(strategy);
|
||||||
if (userId == null) {
|
return ResponseEntity.ok(ApiResponse.success(response));
|
||||||
return unauthorizedResponse();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<MarketingStrategy> optStrategy = marketingStrategyService.getStrategyById(strategyId);
|
private AnalysisHistoryResponse convertToHistoryResponse(MarketingAnalysis analysis) {
|
||||||
if (optStrategy.isEmpty()) {
|
AnalysisHistoryResponse response = new AnalysisHistoryResponse();
|
||||||
ErrorResponse error = new ErrorResponse(
|
response.setAnalysisId(analysis.getId());
|
||||||
"NOT_FOUND",
|
response.setBusinessNiche(analysis.getBusinessNiche());
|
||||||
"Стратегия с указанным ID не найдена");
|
response.setProduct(analysis.getProduct());
|
||||||
return ResponseEntity.status(404)
|
response.setTargetAudience(analysis.getTargetAudience());
|
||||||
.body(ApiResponse.error("Стратегия не найдена", error));
|
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();
|
private StrategyHistoryResponse convertToStrategyHistoryResponse(MarketingStrategy strategy) {
|
||||||
if (!userId.equals(strategy.getUserId())) {
|
StrategyHistoryResponse response = new StrategyHistoryResponse();
|
||||||
ErrorResponse error = new ErrorResponse(
|
response.setStrategyId(strategy.getId());
|
||||||
"FORBIDDEN",
|
response.setAnalysisId(strategy.getAnalysisId());
|
||||||
"У вас нет доступа к этой стратегии");
|
response.setStatus(strategy.getStatus());
|
||||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
response.setUserId(strategy.getUserId());
|
||||||
.body(ApiResponse.error("Доступ запрещен", error));
|
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);
|
@PostMapping("/strategy/{strategyId}/start")
|
||||||
return ResponseEntity.ok(ApiResponse.success(result));
|
public ResponseEntity<?> startStrategy(
|
||||||
}
|
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||||
|
@PathVariable String strategyId,
|
||||||
|
@RequestBody(required = false) StartStrategyRequest request) {
|
||||||
|
|
||||||
@GetMapping("/{analysisId}/strategy")
|
String userId = extractUserIdFromHeader(authHeader);
|
||||||
public ResponseEntity<?> getStrategyByAnalysis(
|
if (userId == null) {
|
||||||
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
return unauthorizedResponse();
|
||||||
@PathVariable String analysisId) {
|
}
|
||||||
|
|
||||||
String userId = extractUserIdFromHeader(authHeader);
|
// Проверяем существование стратегии и права доступа
|
||||||
if (userId == null) {
|
Optional<MarketingStrategy> optStrategy = marketingStrategyService.getStrategyById(strategyId);
|
||||||
return unauthorizedResponse();
|
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
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
Optional<MarketingAnalysis> optAnalysis = marketingAnalysisService.getAnalysisById(analysisId);
|
public ResponseEntity<ApiResponse<ErrorResponse>> handleValidationException(
|
||||||
if (optAnalysis.isEmpty()) {
|
MethodArgumentNotValidException ex) {
|
||||||
ErrorResponse error = new ErrorResponse(
|
|
||||||
"NOT_FOUND",
|
Map<String, String> details = new HashMap<>();
|
||||||
"Анализ с указанным ID не найден");
|
ex.getBindingResult().getFieldErrors().forEach(error -> {
|
||||||
return ResponseEntity.status(404)
|
details.put(error.getField(), error.getDefaultMessage());
|
||||||
.body(ApiResponse.error("Анализ не найден", error));
|
});
|
||||||
|
|
||||||
|
ErrorResponse errorResponse = new ErrorResponse(
|
||||||
|
"VALIDATION_ERROR",
|
||||||
|
"Ошибка валидации входных данных",
|
||||||
|
details);
|
||||||
|
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(ApiResponse.error("Ошибка валидации", errorResponse));
|
||||||
}
|
}
|
||||||
|
|
||||||
MarketingAnalysis analysis = optAnalysis.get();
|
@ExceptionHandler(Exception.class)
|
||||||
if (!userId.equals(analysis.getUserId())) {
|
public ResponseEntity<ApiResponse<ErrorResponse>> handleGenericException(Exception e) {
|
||||||
ErrorResponse error = new ErrorResponse(
|
ErrorResponse errorResponse = new ErrorResponse(
|
||||||
"FORBIDDEN",
|
"INTERNAL_SERVER_ERROR",
|
||||||
"У вас нет доступа к этому анализу");
|
"Произошла внутренняя ошибка сервера. Попробуйте позже.");
|
||||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
|
||||||
.body(ApiResponse.error("Доступ запрещен", 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 {
|
public class AnalysisHistoryResponse {
|
||||||
private String analysisId;
|
private String analysisId;
|
||||||
|
private String businessNiche;
|
||||||
private String product;
|
private String product;
|
||||||
private String location;
|
private String targetAudience;
|
||||||
private String clientType;
|
private String region;
|
||||||
private String differentiator;
|
private String goal;
|
||||||
|
private String detailLevel;
|
||||||
|
private String strongSide;
|
||||||
|
private String weakSide;
|
||||||
private String analysisType;
|
private String analysisType;
|
||||||
private String status;
|
private String status;
|
||||||
private String userId;
|
private String userId;
|
||||||
@@ -27,6 +31,14 @@ public class AnalysisHistoryResponse {
|
|||||||
this.analysisId = analysisId;
|
this.analysisId = analysisId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getBusinessNiche() {
|
||||||
|
return businessNiche;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBusinessNiche(String businessNiche) {
|
||||||
|
this.businessNiche = businessNiche;
|
||||||
|
}
|
||||||
|
|
||||||
public String getProduct() {
|
public String getProduct() {
|
||||||
return product;
|
return product;
|
||||||
}
|
}
|
||||||
@@ -35,28 +47,52 @@ public class AnalysisHistoryResponse {
|
|||||||
this.product = product;
|
this.product = product;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getLocation() {
|
public String getTargetAudience() {
|
||||||
return location;
|
return targetAudience;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setLocation(String location) {
|
public void setTargetAudience(String targetAudience) {
|
||||||
this.location = location;
|
this.targetAudience = targetAudience;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getClientType() {
|
public String getRegion() {
|
||||||
return clientType;
|
return region;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setClientType(String clientType) {
|
public void setRegion(String region) {
|
||||||
this.clientType = clientType;
|
this.region = region;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getDifferentiator() {
|
public String getGoal() {
|
||||||
return differentiator;
|
return goal;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDifferentiator(String differentiator) {
|
public void setGoal(String goal) {
|
||||||
this.differentiator = differentiator;
|
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() {
|
public String getStatus() {
|
||||||
|
|||||||
@@ -4,28 +4,44 @@ import jakarta.validation.constraints.NotBlank;
|
|||||||
import jakarta.validation.constraints.Pattern;
|
import jakarta.validation.constraints.Pattern;
|
||||||
import jakarta.validation.constraints.Size;
|
import jakarta.validation.constraints.Size;
|
||||||
import kz.konturai.parser.model.AnalysisType;
|
import kz.konturai.parser.model.AnalysisType;
|
||||||
|
import kz.konturai.parser.model.DetailLevel;
|
||||||
import kz.konturai.parser.validator.ValidAnalysisType;
|
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 {
|
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' обязательно для заполнения")
|
@NotBlank(message = "Поле 'product' обязательно для заполнения")
|
||||||
@Size(min = 3, max = 200, message = "Поле 'product' должно содержать от 3 до 200 символов")
|
@Size(min = 3, max = 200, message = "Поле 'product' должно содержать от 3 до 200 символов")
|
||||||
@Pattern(regexp = "^[\\p{L}\\p{N}\\s\\-,]+$", message = "Поле 'product' содержит недопустимые символы")
|
@Pattern(regexp = "^[\\p{L}\\p{N}\\s\\-,]+$", message = "Поле 'product' содержит недопустимые символы")
|
||||||
private String product;
|
private String product;
|
||||||
|
|
||||||
@NotBlank(message = "Поле 'location' обязательно для заполнения")
|
@NotBlank(message = "Поле 'targetAudience' обязательно для заполнения")
|
||||||
@Size(min = 2, max = 150, message = "Поле 'location' должно содержать от 2 до 150 символов")
|
@Size(min = 3, max = 300, message = "Поле 'targetAudience' должно содержать от 3 до 300 символов")
|
||||||
@Pattern(regexp = "^[\\p{L}\\p{N}\\s\\-,]+$", message = "Поле 'location' содержит недопустимые символы")
|
private String targetAudience;
|
||||||
private String location;
|
|
||||||
|
|
||||||
@NotBlank(message = "Поле 'client' обязательно для заполнения")
|
@NotBlank(message = "Поле 'region' обязательно для заполнения")
|
||||||
@ValidClientType(message = "Поле 'client' должно быть одним из: B2B клиенты, B2C клиенты, Частные лица, Корпорации, Малый бизнес")
|
@ValidRegion(message = "Поле 'region' должно быть одним из допустимых городов Казахстана")
|
||||||
private String client;
|
private String region;
|
||||||
|
|
||||||
@NotBlank(message = "Поле 'differentiator' обязательно для заполнения")
|
@NotBlank(message = "Поле 'goal' обязательно для заполнения")
|
||||||
@Size(min = 10, max = 500, message = "Поле 'differentiator' должно содержать от 10 до 500 символов")
|
@Size(min = 10, max = 500, message = "Поле 'goal' должно содержать от 10 до 500 символов")
|
||||||
private String differentiator;
|
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' обязательно для заполнения")
|
@NotBlank(message = "Поле 'analysisType' обязательно для заполнения")
|
||||||
@ValidAnalysisType(message = "Поле 'analysisType' должно быть одним из: РЫНОК, КОНКУРЕНТЫ, ЦА, КАНАЛЫ, SWOT")
|
@ValidAnalysisType(message = "Поле 'analysisType' должно быть одним из: РЫНОК, КОНКУРЕНТЫ, ЦА, КАНАЛЫ, SWOT")
|
||||||
@@ -34,15 +50,27 @@ public class MarketingAnalysisRequest {
|
|||||||
public MarketingAnalysisRequest() {
|
public MarketingAnalysisRequest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public MarketingAnalysisRequest(String product, String location, String client, String differentiator,
|
public MarketingAnalysisRequest(String businessNiche, String product, String targetAudience, String region,
|
||||||
String analysisType) {
|
String goal, String detailLevel, String strongSide, String weakSide, String analysisType) {
|
||||||
|
this.businessNiche = businessNiche;
|
||||||
this.product = product;
|
this.product = product;
|
||||||
this.location = location;
|
this.targetAudience = targetAudience;
|
||||||
this.client = client;
|
this.region = region;
|
||||||
this.differentiator = differentiator;
|
this.goal = goal;
|
||||||
|
this.detailLevel = detailLevel;
|
||||||
|
this.strongSide = strongSide;
|
||||||
|
this.weakSide = weakSide;
|
||||||
this.analysisType = analysisType;
|
this.analysisType = analysisType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getBusinessNiche() {
|
||||||
|
return businessNiche;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBusinessNiche(String businessNiche) {
|
||||||
|
this.businessNiche = businessNiche;
|
||||||
|
}
|
||||||
|
|
||||||
public String getProduct() {
|
public String getProduct() {
|
||||||
return product;
|
return product;
|
||||||
}
|
}
|
||||||
@@ -51,28 +79,56 @@ public class MarketingAnalysisRequest {
|
|||||||
this.product = product;
|
this.product = product;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getLocation() {
|
public String getTargetAudience() {
|
||||||
return location;
|
return targetAudience;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setLocation(String location) {
|
public void setTargetAudience(String targetAudience) {
|
||||||
this.location = location;
|
this.targetAudience = targetAudience;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getClient() {
|
public String getRegion() {
|
||||||
return client;
|
return region;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setClient(String client) {
|
public void setRegion(String region) {
|
||||||
this.client = client;
|
this.region = region;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getDifferentiator() {
|
public String getGoal() {
|
||||||
return differentiator;
|
return goal;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDifferentiator(String differentiator) {
|
public void setGoal(String goal) {
|
||||||
this.differentiator = differentiator;
|
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() {
|
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
|
@Id
|
||||||
private String id;
|
private String id;
|
||||||
|
|
||||||
|
@Field("business_niche")
|
||||||
|
private String businessNiche;
|
||||||
|
|
||||||
@Field("product")
|
@Field("product")
|
||||||
private String product;
|
private String product;
|
||||||
|
|
||||||
@Field("location")
|
@Field("target_audience")
|
||||||
private String location;
|
private String targetAudience;
|
||||||
|
|
||||||
@Field("client_type")
|
@Field("region")
|
||||||
private String clientType;
|
private String region;
|
||||||
|
|
||||||
@Field("differentiator")
|
@Field("goal")
|
||||||
private String differentiator;
|
private String goal;
|
||||||
|
|
||||||
|
@Field("detail_level")
|
||||||
|
private String detailLevel; // КРАТКО, СТАНДАРТНО, ПОДРОБНО
|
||||||
|
|
||||||
|
@Field("strong_side")
|
||||||
|
private String strongSide;
|
||||||
|
|
||||||
|
@Field("weak_side")
|
||||||
|
private String weakSide;
|
||||||
|
|
||||||
@Field("analysis_type")
|
@Field("analysis_type")
|
||||||
private String analysisType; // РЫНОК, КОНКУРЕНТЫ, ЦА, КАНАЛЫ, SWOT
|
private String analysisType; // РЫНОК, КОНКУРЕНТЫ, ЦА, КАНАЛЫ, SWOT
|
||||||
@@ -61,21 +73,17 @@ public class MarketingAnalysis {
|
|||||||
this.statusHistory = new ArrayList<>();
|
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();
|
||||||
|
this.businessNiche = businessNiche;
|
||||||
this.product = product;
|
this.product = product;
|
||||||
this.location = location;
|
this.targetAudience = targetAudience;
|
||||||
this.clientType = clientType;
|
this.region = region;
|
||||||
this.differentiator = differentiator;
|
this.goal = goal;
|
||||||
}
|
this.detailLevel = detailLevel;
|
||||||
|
this.strongSide = strongSide;
|
||||||
public MarketingAnalysis(String product, String location, String clientType, String differentiator,
|
this.weakSide = weakSide;
|
||||||
String analysisType) {
|
|
||||||
this();
|
|
||||||
this.product = product;
|
|
||||||
this.location = location;
|
|
||||||
this.clientType = clientType;
|
|
||||||
this.differentiator = differentiator;
|
|
||||||
this.analysisType = analysisType;
|
this.analysisType = analysisType;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,28 +103,60 @@ public class MarketingAnalysis {
|
|||||||
this.product = product;
|
this.product = product;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getLocation() {
|
public String getBusinessNiche() {
|
||||||
return location;
|
return businessNiche;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setLocation(String location) {
|
public void setBusinessNiche(String businessNiche) {
|
||||||
this.location = location;
|
this.businessNiche = businessNiche;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getClientType() {
|
public String getTargetAudience() {
|
||||||
return clientType;
|
return targetAudience;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setClientType(String clientType) {
|
public void setTargetAudience(String targetAudience) {
|
||||||
this.clientType = clientType;
|
this.targetAudience = targetAudience;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getDifferentiator() {
|
public String getRegion() {
|
||||||
return differentiator;
|
return region;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDifferentiator(String differentiator) {
|
public void setRegion(String region) {
|
||||||
this.differentiator = differentiator;
|
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() {
|
public String getStatus() {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import kz.konturai.parser.dto.MarketingAnalysisRequest;
|
|||||||
import kz.konturai.parser.dto.MarketingAnalysisResult;
|
import kz.konturai.parser.dto.MarketingAnalysisResult;
|
||||||
import kz.konturai.parser.dto.StatusHistoryEntry;
|
import kz.konturai.parser.dto.StatusHistoryEntry;
|
||||||
import kz.konturai.parser.model.AnalysisType;
|
import kz.konturai.parser.model.AnalysisType;
|
||||||
|
import kz.konturai.parser.model.DetailLevel;
|
||||||
import kz.konturai.parser.model.MarketingAnalysis;
|
import kz.konturai.parser.model.MarketingAnalysis;
|
||||||
import kz.konturai.parser.repository.MarketingAnalysisRepository;
|
import kz.konturai.parser.repository.MarketingAnalysisRepository;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -38,10 +39,14 @@ public class MarketingAnalysisService {
|
|||||||
|
|
||||||
public MarketingAnalysis startAnalysis(MarketingAnalysisRequest request, String userId) {
|
public MarketingAnalysis startAnalysis(MarketingAnalysisRequest request, String userId) {
|
||||||
MarketingAnalysis analysis = new MarketingAnalysis(
|
MarketingAnalysis analysis = new MarketingAnalysis(
|
||||||
|
request.getBusinessNiche(),
|
||||||
request.getProduct(),
|
request.getProduct(),
|
||||||
request.getLocation(),
|
request.getTargetAudience(),
|
||||||
request.getClient(),
|
request.getRegion(),
|
||||||
request.getDifferentiator(),
|
request.getGoal(),
|
||||||
|
request.getDetailLevel(),
|
||||||
|
request.getStrongSide(),
|
||||||
|
request.getWeakSide(),
|
||||||
request.getAnalysisType());
|
request.getAnalysisType());
|
||||||
analysis.setUserId(userId);
|
analysis.setUserId(userId);
|
||||||
analysis.setStatus("queued");
|
analysis.setStatus("queued");
|
||||||
@@ -122,32 +127,52 @@ public class MarketingAnalysisService {
|
|||||||
private Map<String, Object> generateMarketingReport(MarketingAnalysisRequest request) {
|
private Map<String, Object> generateMarketingReport(MarketingAnalysisRequest request) {
|
||||||
Map<String, Object> report = new HashMap<>();
|
Map<String, Object> report = new HashMap<>();
|
||||||
|
|
||||||
// Get analysis type
|
// Get analysis type and detail level
|
||||||
AnalysisType analysisType = request.getAnalysisTypeEnum();
|
AnalysisType analysisType = request.getAnalysisTypeEnum();
|
||||||
String analysisTypeName = analysisType != null ? analysisType.getDisplayName() : "Общий";
|
String analysisTypeName = analysisType != null ? analysisType.getDisplayName() : "Общий";
|
||||||
|
DetailLevel detailLevel = request.getDetailLevelEnum();
|
||||||
|
String detailLevelName = detailLevel != null ? detailLevel.getDisplayName() : "Стандартно";
|
||||||
|
|
||||||
// Build context for AI
|
// Build context for AI with all new fields
|
||||||
String context = String.format(
|
StringBuilder contextBuilder = new StringBuilder();
|
||||||
"Продукт/услуга: %s\n" +
|
contextBuilder.append("Ниша бизнеса: ").append(request.getBusinessNiche()).append("\n");
|
||||||
"Локация: %s\n" +
|
contextBuilder.append("Продукт/услуга: ").append(request.getProduct()).append("\n");
|
||||||
"Тип клиентов: %s\n" +
|
contextBuilder.append("Целевая аудитория: ").append(request.getTargetAudience()).append("\n");
|
||||||
"Уникальные особенности: %s\n" +
|
contextBuilder.append("Регион: ").append(request.getRegion()).append("\n");
|
||||||
"Тип анализа: %s\n",
|
contextBuilder.append("Цель на 6-12 месяцев: ").append(request.getGoal()).append("\n");
|
||||||
request.getProduct(),
|
contextBuilder.append("Уровень детализации: ").append(detailLevelName).append("\n");
|
||||||
request.getLocation(),
|
if (request.getStrongSide() != null && !request.getStrongSide().trim().isEmpty()) {
|
||||||
request.getClient(),
|
contextBuilder.append("Сильная сторона: ").append(request.getStrongSide()).append("\n");
|
||||||
request.getDifferentiator(),
|
}
|
||||||
analysisTypeName);
|
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
|
// Generate analysis based on type and detail level
|
||||||
String analysisContent = generateAnalysisByType(context, analysisType);
|
String analysisContent = generateAnalysisByType(context, analysisType, detailLevel);
|
||||||
report.put("analysisType", analysisTypeName);
|
report.put("analysisType", analysisTypeName);
|
||||||
|
report.put("detailLevel", detailLevelName);
|
||||||
report.put("summary", analysisContent != null ? analysisContent : "Анализ не удалось сгенерировать.");
|
report.put("summary", analysisContent != null ? analysisContent : "Анализ не удалось сгенерировать.");
|
||||||
|
|
||||||
// Generate target audience description
|
// Generate target audience description (adjusted by detail level)
|
||||||
String audiencePrompt = "На основе информации о бизнесе опиши целевую аудиторию (1-2 абзаца). " +
|
String audiencePrompt;
|
||||||
"Укажи, какие каналы коммуникации наиболее подходят для этой аудитории. " +
|
if (detailLevel == DetailLevel.КРАТКО) {
|
||||||
"Ответ должен быть на русском языке.\n\n" + context;
|
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");
|
String audienceDescription = openAIAnalyticsService.generateWithInstruction(context, audiencePrompt, "ru");
|
||||||
|
|
||||||
// Extract channels from description or generate separately
|
// Extract channels from description or generate separately
|
||||||
@@ -164,22 +189,50 @@ public class MarketingAnalysisService {
|
|||||||
channels.isEmpty() ? Arrays.asList("Instagram", "LinkedIn", "Telegram") : channels);
|
channels.isEmpty() ? Arrays.asList("Instagram", "LinkedIn", "Telegram") : channels);
|
||||||
report.put("targetAudience", targetAudience);
|
report.put("targetAudience", targetAudience);
|
||||||
|
|
||||||
// Generate recommendations
|
// Generate recommendations (adjusted by detail level)
|
||||||
String recommendationsPrompt = "На основе информации о бизнесе сформулируй 4-6 практических рекомендаций для маркетинговой стратегии. "
|
String recommendationsPrompt;
|
||||||
+
|
if (detailLevel == DetailLevel.КРАТКО) {
|
||||||
"Каждая рекомендация должна быть конкретной и применимой. " +
|
recommendationsPrompt = "На основе информации о бизнесе сформулируй 3-4 краткие практические рекомендации для маркетинговой стратегии. "
|
||||||
"Ответ должен быть списком рекомендаций, каждая с новой строки, без нумерации.\n\n" + context;
|
+
|
||||||
|
"Каждая рекомендация должна быть конкретной. " +
|
||||||
|
"Ответ должен быть списком рекомендаций, каждая с новой строки, без нумерации.\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,
|
String recommendationsStr = openAIAnalyticsService.generateWithInstruction(context, recommendationsPrompt,
|
||||||
"ru");
|
"ru");
|
||||||
List<String> recommendations = parseRecommendations(recommendationsStr);
|
List<String> recommendations = parseRecommendations(recommendationsStr);
|
||||||
report.put("recommendations",
|
report.put("recommendations",
|
||||||
recommendations.isEmpty() ? Arrays.asList("Рекомендации не удалось сгенерировать.") : recommendations);
|
recommendations.isEmpty() ? Arrays.asList("Рекомендации не удалось сгенерировать.") : recommendations);
|
||||||
|
|
||||||
// Generate strategy
|
// Generate strategy (adjusted by detail level)
|
||||||
String strategyPrompt = "На основе информации о бизнесе создай краткую маркетинговую стратегию. " +
|
String strategyPrompt;
|
||||||
"Укажи рекомендуемую длительность кампании (например, '2 недели', '1 месяц'), " +
|
if (detailLevel == DetailLevel.КРАТКО) {
|
||||||
"3-5 каналов коммуникации и типы контента (например, 'посты', 'сторис', 'баннеры'). " +
|
strategyPrompt = "На основе информации о бизнесе создай краткую маркетинговую стратегию. " +
|
||||||
"Ответ должен быть структурированным текстом на русском языке.\n\n" + context;
|
"Укажи рекомендуемую длительность кампании (например, '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");
|
String strategyText = openAIAnalyticsService.generateWithInstruction(context, strategyPrompt, "ru");
|
||||||
|
|
||||||
Map<String, Object> strategy = parseStrategy(strategyText, channels);
|
Map<String, Object> strategy = parseStrategy(strategyText, channels);
|
||||||
@@ -188,49 +241,60 @@ public class MarketingAnalysisService {
|
|||||||
return report;
|
return report;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String generateAnalysisByType(String context, AnalysisType analysisType) {
|
private String generateAnalysisByType(String context, AnalysisType analysisType, DetailLevel detailLevel) {
|
||||||
if (analysisType == null) {
|
if (analysisType == null) {
|
||||||
analysisType = AnalysisType.РЫНОК; // Default
|
analysisType = AnalysisType.РЫНОК; // Default
|
||||||
}
|
}
|
||||||
|
if (detailLevel == null) {
|
||||||
|
detailLevel = DetailLevel.СТАНДАРТНО; // Default
|
||||||
|
}
|
||||||
|
|
||||||
String prompt;
|
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) {
|
switch (analysisType) {
|
||||||
case РЫНОК:
|
case РЫНОК:
|
||||||
prompt = "Проведи детальный анализ рынка на основе предоставленной информации о бизнесе. " +
|
prompt = "Проведи анализ рынка на основе предоставленной информации о бизнесе. " +
|
||||||
"Включи: размер рынка, динамику роста, основные сегменты, тренды и перспективы развития. " +
|
"Включи: размер рынка, динамику роста, основные сегменты, тренды и перспективы развития. " +
|
||||||
"Ответ должен быть структурированным текстом на русском языке (3-5 абзацев).\n\n" + context;
|
lengthInstruction + "\n\n" + context;
|
||||||
break;
|
break;
|
||||||
case КОНКУРЕНТЫ:
|
case КОНКУРЕНТЫ:
|
||||||
prompt = "Проведи анализ конкурентов на основе предоставленной информации о бизнесе. " +
|
prompt = "Проведи анализ конкурентов на основе предоставленной информации о бизнесе. " +
|
||||||
"Включи: основных конкурентов, их сильные и слабые стороны, позиционирование, " +
|
"Включи: основных конкурентов, их сильные и слабые стороны, позиционирование, " +
|
||||||
"ценовую политику и маркетинговые стратегии. Ответ должен быть структурированным текстом на русском языке (3-5 абзацев).\n\n"
|
"ценовую политику и маркетинговые стратегии. " + lengthInstruction + "\n\n" + context;
|
||||||
+ context;
|
|
||||||
break;
|
break;
|
||||||
case ЦА:
|
case ЦА:
|
||||||
prompt = "Проведи детальный анализ целевой аудитории на основе предоставленной информации о бизнесе. " +
|
prompt = "Проведи анализ целевой аудитории на основе предоставленной информации о бизнесе. " +
|
||||||
"Включи: демографические характеристики, психографический профиль, потребности и боли, " +
|
"Включи: демографические характеристики, психографический профиль, потребности и боли, " +
|
||||||
"поведенческие паттерны и предпочтения. Ответ должен быть структурированным текстом на русском языке (3-5 абзацев).\n\n"
|
"поведенческие паттерны и предпочтения. " + lengthInstruction + "\n\n" + context;
|
||||||
+ context;
|
|
||||||
break;
|
break;
|
||||||
case КАНАЛЫ:
|
case КАНАЛЫ:
|
||||||
prompt = "Проведи анализ маркетинговых каналов на основе предоставленной информации о бизнесе. " +
|
prompt = "Проведи анализ маркетинговых каналов на основе предоставленной информации о бизнесе. " +
|
||||||
"Включи: оценку эффективности различных каналов коммуникации, рекомендации по выбору каналов, "
|
"Включи: оценку эффективности различных каналов коммуникации, рекомендации по выбору каналов, "
|
||||||
+
|
+
|
||||||
"особенности использования каждого канала и бюджетные рекомендации. Ответ должен быть структурированным текстом на русском языке (3-5 абзацев).\n\n"
|
"особенности использования каждого канала и бюджетные рекомендации. " + lengthInstruction
|
||||||
+ context;
|
+ "\n\n" + context;
|
||||||
break;
|
break;
|
||||||
case SWOT:
|
case SWOT:
|
||||||
prompt = "Проведи SWOT-анализ на основе предоставленной информации о бизнесе. " +
|
prompt = "Проведи SWOT-анализ на основе предоставленной информации о бизнесе. " +
|
||||||
"Включи детальный анализ: Сильных сторон (Strengths), Слабых сторон (Weaknesses), " +
|
"Включи анализ: Сильных сторон (Strengths), Слабых сторон (Weaknesses), " +
|
||||||
"Возможностей (Opportunities) и Угроз (Threats). Ответ должен быть структурированным текстом на русском языке, "
|
"Возможностей (Opportunities) и Угроз (Threats). " +
|
||||||
+
|
"Ответ должен быть структурированным текстом на русском языке, " +
|
||||||
"с четким разделением по каждому разделу SWOT (3-5 абзацев).\n\n" + context;
|
"с четким разделением по каждому разделу SWOT. " + lengthInstruction + "\n\n" + context;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
prompt = "На основе следующей информации о бизнесе создай краткое резюме маркетингового анализа (2-3 абзаца). "
|
prompt = "На основе следующей информации о бизнесе создай резюме маркетингового анализа. " +
|
||||||
+
|
|
||||||
"Выдели ключевые возможности и особенности бизнеса. " +
|
"Выдели ключевые возможности и особенности бизнеса. " +
|
||||||
"Ответ должен быть на русском языке, деловым стилем.\n\n" + context;
|
lengthInstruction + "\n\n" + context;
|
||||||
}
|
}
|
||||||
|
|
||||||
return openAIAnalyticsService.generateWithInstruction(context, prompt, "ru");
|
return openAIAnalyticsService.generateWithInstruction(context, prompt, "ru");
|
||||||
@@ -329,10 +393,23 @@ public class MarketingAnalysisService {
|
|||||||
|
|
||||||
markdown.append("# Маркетинговый анализ\n\n");
|
markdown.append("# Маркетинговый анализ\n\n");
|
||||||
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.getProduct()).append("\n");
|
||||||
markdown.append("- **Локация:** ").append(request.getLocation()).append("\n");
|
markdown.append("- **Целевая аудитория:** ").append(request.getTargetAudience()).append("\n");
|
||||||
markdown.append("- **Тип клиентов:** ").append(request.getClient()).append("\n");
|
markdown.append("- **Регион:** ").append(request.getRegion()).append("\n");
|
||||||
markdown.append("- **Уникальные особенности:** ").append(request.getDifferentiator()).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");
|
String analysisTypeName = (String) reportData.get("analysisType");
|
||||||
if (analysisTypeName != null) {
|
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