.
This commit is contained in:
@@ -0,0 +1,551 @@
|
||||
# API Документация: Маркетинговый анализ (Frontend/AI Agent)
|
||||
|
||||
## Базовый URL
|
||||
|
||||
```
|
||||
https://api.konturai.kz
|
||||
```
|
||||
|
||||
## Обзор
|
||||
|
||||
API для генерации маркетингового анализа на основе данных о бизнесе. Процесс состоит из двух этапов:
|
||||
|
||||
1. **Запуск анализа** - создание задачи и начало асинхронной обработки
|
||||
2. **Получение результатов** - проверка статуса и получение готового отчета
|
||||
|
||||
Анализ выполняется асинхронно и занимает примерно 5-10 минут.
|
||||
|
||||
---
|
||||
|
||||
## Эндпоинты
|
||||
|
||||
### 1. Запуск маркетингового анализа
|
||||
|
||||
**POST** `/api/marketing/analysis/start`
|
||||
|
||||
Создает новую задачу на генерацию маркетингового анализа и запускает асинхронную обработку.
|
||||
|
||||
#### Заголовки запроса
|
||||
|
||||
```
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
#### Тело запроса (JSON)
|
||||
|
||||
| Поле | Тип | Обязательный | Описание | Пример значения |
|
||||
| ---------------- | ------ | ------------ | ------------------------------ | -------------------------------- |
|
||||
| `product` | string | ✅ | Название продукта или услуги | "Веб-разработка" |
|
||||
| `location` | string | ✅ | Географическая локация работы | "Алматы, Казахстан" |
|
||||
| `client` | string | ✅ | Тип целевой аудитории | "B2B клиенты" |
|
||||
| `differentiator` | string | ✅ | Уникальные особенности бизнеса | "Быстрая разработка за 2 недели" |
|
||||
|
||||
#### Валидация полей
|
||||
|
||||
**`product`** (string, обязательное)
|
||||
|
||||
- Минимальная длина: 3 символа
|
||||
- Максимальная длина: 200 символов
|
||||
- Разрешены: буквы, цифры, пробелы, дефисы, запятые
|
||||
|
||||
**`location`** (string, обязательное)
|
||||
|
||||
- Минимальная длина: 2 символа
|
||||
- Максимальная длина: 150 символов
|
||||
- Разрешены: буквы, цифры, пробелы, запятые, дефисы
|
||||
|
||||
**`client`** (string, обязательное)
|
||||
|
||||
- Допустимые значения (точно):
|
||||
- `"B2B клиенты"`
|
||||
- `"B2C клиенты"`
|
||||
- `"Частные лица"`
|
||||
- `"Корпорации"`
|
||||
- `"Малый бизнес"`
|
||||
|
||||
**`differentiator`** (string, обязательное)
|
||||
|
||||
- Минимальная длина: 10 символов
|
||||
- Максимальная длина: 500 символов
|
||||
- Разрешены любые символы
|
||||
|
||||
#### Пример запроса
|
||||
|
||||
```json
|
||||
{
|
||||
"product": "Разработка мобильных приложений",
|
||||
"location": "Нур-Султан, Казахстан",
|
||||
"client": "B2B клиенты",
|
||||
"differentiator": "Специализируемся на быстрой разработке MVP за 4 недели с использованием современных технологий"
|
||||
}
|
||||
```
|
||||
|
||||
#### Пример успешного ответа (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Анализ запущен успешно",
|
||||
"data": {
|
||||
"analysisId": "507f1f77bcf86cd799439011",
|
||||
"status": "processing",
|
||||
"estimatedCompletionTime": "2025-01-20T15:38:00",
|
||||
"message": "Анализ запущен успешно. Результаты будут готовы в течение 5-10 минут."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Структура ответа
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
| ------------------------------ | ------- | --------------------------------------------------------- |
|
||||
| `success` | boolean | Флаг успешности операции |
|
||||
| `message` | string | Сообщение о результате операции |
|
||||
| `data.analysisId` | string | Уникальный идентификатор анализа (MongoDB ObjectId) |
|
||||
| `data.status` | string | Статус анализа: `"processing"` |
|
||||
| `data.estimatedCompletionTime` | string | ISO 8601 дата/время ожидаемого завершения (LocalDateTime) |
|
||||
| `data.message` | string | Информационное сообщение для пользователя |
|
||||
|
||||
#### Пример ошибки валидации (400 Bad Request)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Ошибка валидации",
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "Ошибка валидации входных данных",
|
||||
"details": {
|
||||
"product": "Поле 'product' должно содержать от 3 до 200 символов",
|
||||
"client": "Поле 'client' должно быть одним из: B2B клиенты, B2C клиенты, Частные лица, Корпорации, Малый бизнес"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Получение результата анализа
|
||||
|
||||
**GET** `/api/marketing/analysis/{analysisId}`
|
||||
|
||||
Возвращает статус и результаты анализа по идентификатору.
|
||||
|
||||
#### Параметры пути
|
||||
|
||||
| Параметр | Тип | Описание |
|
||||
| ------------ | ------ | --------------------- |
|
||||
| `analysisId` | string | Идентификатор анализа |
|
||||
|
||||
#### Пример запроса
|
||||
|
||||
```
|
||||
GET /api/marketing/analysis/507f1f77bcf86cd799439011
|
||||
```
|
||||
|
||||
#### Пример ответа (когда анализ завершен - 200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Операция выполнена успешно",
|
||||
"data": {
|
||||
"analysisId": "507f1f77bcf86cd799439011",
|
||||
"status": "completed",
|
||||
"createdAt": "2025-01-20T15:30:00",
|
||||
"completedAt": "2025-01-20T15:38:00",
|
||||
"report": {
|
||||
"summary": "Краткое резюме анализа...",
|
||||
"targetAudience": {
|
||||
"description": "Описание целевой аудитории...",
|
||||
"channels": ["Instagram", "LinkedIn", "Telegram"]
|
||||
},
|
||||
"recommendations": ["Рекомендация 1", "Рекомендация 2", "Рекомендация 3"],
|
||||
"strategy": {
|
||||
"duration": "2 недели",
|
||||
"channels": ["Instagram", "Telegram", "21MC"],
|
||||
"contentTypes": ["посты", "сторис", "баннеры"]
|
||||
},
|
||||
"pdfUrl": "/api/marketing/analysis/507f1f77bcf86cd799439011/download"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Пример ответа (когда анализ еще обрабатывается - 200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Операция выполнена успешно",
|
||||
"data": {
|
||||
"analysisId": "507f1f77bcf86cd799439011",
|
||||
"status": "processing",
|
||||
"createdAt": "2025-01-20T15:30:00",
|
||||
"completedAt": null,
|
||||
"report": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Статусы анализа
|
||||
|
||||
| Статус | Описание |
|
||||
| ------------ | ----------------------------- |
|
||||
| `queued` | Запрос в очереди на обработку |
|
||||
| `processing` | Анализ выполняется |
|
||||
| `completed` | Анализ завершен успешно |
|
||||
| `failed` | Анализ завершился с ошибкой |
|
||||
|
||||
#### Структура ответа
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
| ---------------------------------------- | ------- | ------------------------------------------------------ |
|
||||
| `success` | boolean | Флаг успешности операции |
|
||||
| `message` | string | Сообщение о результате операции |
|
||||
| `data.analysisId` | string | Уникальный идентификатор анализа |
|
||||
| `data.status` | string | Статус анализа |
|
||||
| `data.createdAt` | string | ISO 8601 дата/время создания |
|
||||
| `data.completedAt` | string | ISO 8601 дата/время завершения (null если не завершен) |
|
||||
| `data.report` | object | Объект с результатами (null если не завершен) |
|
||||
| `data.report.summary` | string | Краткое резюме анализа |
|
||||
| `data.report.targetAudience` | object | Информация о целевой аудитории |
|
||||
| `data.report.targetAudience.description` | string | Описание целевой аудитории |
|
||||
| `data.report.targetAudience.channels` | array | Список рекомендуемых каналов |
|
||||
| `data.report.recommendations` | array | Список рекомендаций (массив строк) |
|
||||
| `data.report.strategy` | object | Маркетинговая стратегия |
|
||||
| `data.report.strategy.duration` | string | Длительность кампании (например, "2 недели") |
|
||||
| `data.report.strategy.channels` | array | Каналы коммуникации (массив строк) |
|
||||
| `data.report.strategy.contentTypes` | array | Типы контента (массив строк) |
|
||||
| `data.report.pdfUrl` | string | URL для скачивания PDF отчета |
|
||||
|
||||
#### Пример ошибки (404 Not Found)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Анализ не найден",
|
||||
"error": {
|
||||
"code": "NOT_FOUND",
|
||||
"message": "Анализ с указанным ID не найден"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Скачивание PDF отчета
|
||||
|
||||
**GET** `/api/marketing/analysis/{analysisId}/download`
|
||||
|
||||
Возвращает PDF файл с полным маркетинговым отчетом.
|
||||
|
||||
#### Параметры пути
|
||||
|
||||
| Параметр | Тип | Описание |
|
||||
| ------------ | ------ | --------------------- |
|
||||
| `analysisId` | string | Идентификатор анализа |
|
||||
|
||||
#### Пример запроса
|
||||
|
||||
```
|
||||
GET /api/marketing/analysis/507f1f77bcf86cd799439011/download
|
||||
```
|
||||
|
||||
#### Успешный ответ (200 OK)
|
||||
|
||||
- **Content-Type**: `application/pdf`
|
||||
- **Content-Disposition**: `attachment; filename="marketing_analysis_507f1f77bcf86cd799439011_1234567890.pdf"`
|
||||
- **Body**: Бинарные данные PDF файла
|
||||
|
||||
#### Ошибки
|
||||
|
||||
- **404 Not Found** - Анализ не найден или PDF еще не сгенерирован
|
||||
- **500 Internal Server Error** - Ошибка при получении файла
|
||||
|
||||
---
|
||||
|
||||
## Обработка ошибок
|
||||
|
||||
### Коды ошибок
|
||||
|
||||
| Код | HTTP статус | Описание |
|
||||
| ----------------------- | ----------- | ------------------------------- |
|
||||
| `VALIDATION_ERROR` | 400 | Ошибка валидации входных данных |
|
||||
| `NOT_FOUND` | 404 | Ресурс не найден |
|
||||
| `INTERNAL_SERVER_ERROR` | 500 | Внутренняя ошибка сервера |
|
||||
|
||||
### Формат ошибки
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Описание ошибки",
|
||||
"error": {
|
||||
"code": "ERROR_CODE",
|
||||
"message": "Детальное сообщение об ошибке",
|
||||
"details": {
|
||||
"field1": "Сообщение об ошибке для поля 1",
|
||||
"field2": "Сообщение об ошибке для поля 2"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Примечание**: Поле `details` присутствует только для ошибок валидации (`VALIDATION_ERROR`).
|
||||
|
||||
---
|
||||
|
||||
## Примеры использования
|
||||
|
||||
### JavaScript/TypeScript (Fetch API)
|
||||
|
||||
#### Запуск анализа
|
||||
|
||||
```javascript
|
||||
async function startMarketingAnalysis(data) {
|
||||
const response = await fetch(
|
||||
'https://api.konturai.kz/api/marketing/analysis/start',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
product: 'Разработка мобильных приложений',
|
||||
location: 'Нур-Султан, Казахстан',
|
||||
client: 'B2B клиенты',
|
||||
differentiator:
|
||||
'Специализируемся на быстрой разработке MVP за 4 недели',
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
console.log('Analysis ID:', result.data.analysisId);
|
||||
return result.data.analysisId;
|
||||
} else {
|
||||
console.error('Error:', result.error);
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Проверка статуса и получение результата
|
||||
|
||||
```javascript
|
||||
async function getAnalysisResult(analysisId) {
|
||||
const response = await fetch(
|
||||
`https://api.konturai.kz/api/marketing/analysis/${analysisId}`
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
const { status, report } = result.data;
|
||||
|
||||
if (status === 'completed' && report) {
|
||||
console.log('Analysis completed!');
|
||||
console.log('Summary:', report.summary);
|
||||
console.log('Recommendations:', report.recommendations);
|
||||
return report;
|
||||
} else if (status === 'processing') {
|
||||
console.log('Analysis is still processing...');
|
||||
return null; // Повторить запрос позже
|
||||
} else if (status === 'failed') {
|
||||
throw new Error('Analysis failed');
|
||||
}
|
||||
} else {
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Полный цикл с polling
|
||||
|
||||
```javascript
|
||||
async function waitForAnalysisCompletion(
|
||||
analysisId,
|
||||
maxAttempts = 60,
|
||||
intervalMs = 10000
|
||||
) {
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
const result = await getAnalysisResult(analysisId);
|
||||
|
||||
if (result) {
|
||||
return result; // Анализ завершен
|
||||
}
|
||||
|
||||
// Ждем перед следующей проверкой
|
||||
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
||||
}
|
||||
|
||||
throw new Error('Analysis timeout');
|
||||
}
|
||||
|
||||
// Использование
|
||||
async function runFullAnalysis() {
|
||||
try {
|
||||
// 1. Запускаем анализ
|
||||
const analysisId = await startMarketingAnalysis({
|
||||
product: 'Веб-разработка',
|
||||
location: 'Алматы, Казахстан',
|
||||
client: 'B2B клиенты',
|
||||
differentiator: 'Быстрая разработка за 2 недели',
|
||||
});
|
||||
|
||||
console.log(`Analysis started: ${analysisId}`);
|
||||
|
||||
// 2. Ждем завершения (проверяем каждые 10 секунд, максимум 10 минут)
|
||||
const report = await waitForAnalysisCompletion(analysisId, 60, 10000);
|
||||
|
||||
// 3. Используем результаты
|
||||
console.log('Report summary:', report.summary);
|
||||
console.log('Channels:', report.targetAudience.channels);
|
||||
console.log('PDF URL:', report.pdfUrl);
|
||||
|
||||
return report;
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Скачивание PDF
|
||||
|
||||
```javascript
|
||||
async function downloadPdf(analysisId) {
|
||||
const response = await fetch(
|
||||
`https://api.konturai.kz/api/marketing/analysis/${analysisId}/download`
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to download PDF');
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `marketing_analysis_${analysisId}.pdf`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import requests
|
||||
import time
|
||||
|
||||
BASE_URL = "https://api.konturai.kz"
|
||||
|
||||
def start_analysis(product, location, client, differentiator):
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/api/marketing/analysis/start",
|
||||
json={
|
||||
"product": product,
|
||||
"location": location,
|
||||
"client": client,
|
||||
"differentiator": differentiator
|
||||
}
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
if data["success"]:
|
||||
return data["data"]["analysisId"]
|
||||
else:
|
||||
raise Exception(data["error"]["message"])
|
||||
|
||||
def get_analysis_result(analysis_id):
|
||||
response = requests.get(
|
||||
f"{BASE_URL}/api/marketing/analysis/{analysis_id}"
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()["data"]
|
||||
|
||||
def wait_for_completion(analysis_id, max_attempts=60, interval=10):
|
||||
for _ in range(max_attempts):
|
||||
result = get_analysis_result(analysis_id)
|
||||
|
||||
if result["status"] == "completed":
|
||||
return result["report"]
|
||||
elif result["status"] == "failed":
|
||||
raise Exception("Analysis failed")
|
||||
|
||||
time.sleep(interval)
|
||||
|
||||
raise Exception("Analysis timeout")
|
||||
|
||||
# Использование
|
||||
analysis_id = start_analysis(
|
||||
product="Разработка мобильных приложений",
|
||||
location="Нур-Султан, Казахстан",
|
||||
client="B2B клиенты",
|
||||
differentiator="Быстрая разработка MVP за 4 недели"
|
||||
)
|
||||
|
||||
print(f"Analysis started: {analysis_id}")
|
||||
|
||||
report = wait_for_completion(analysis_id)
|
||||
print(f"Summary: {report['summary']}")
|
||||
print(f"Channels: {report['targetAudience']['channels']}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Рекомендации по интеграции
|
||||
|
||||
### 1. Polling стратегия
|
||||
|
||||
Рекомендуется проверять статус анализа каждые 10-15 секунд. Максимальное время ожидания - 10-15 минут.
|
||||
|
||||
### 2. Обработка ошибок
|
||||
|
||||
Всегда проверяйте поле `success` в ответе и обрабатывайте ошибки соответствующим образом.
|
||||
|
||||
### 3. Валидация на клиенте
|
||||
|
||||
Перед отправкой запроса рекомендуется валидировать данные на клиенте:
|
||||
|
||||
- Проверка длины полей
|
||||
- Проверка допустимых значений для `client`
|
||||
- Проверка обязательных полей
|
||||
|
||||
### 4. UX рекомендации
|
||||
|
||||
- Показывайте индикатор загрузки во время обработки
|
||||
- Отображайте примерное время завершения
|
||||
- Предоставьте возможность отменить ожидание и проверить результат позже
|
||||
- Сохраняйте `analysisId` для последующей проверки статуса
|
||||
|
||||
### 5. Кэширование
|
||||
|
||||
После получения результатов можно кэшировать их локально, используя `analysisId` как ключ.
|
||||
|
||||
---
|
||||
|
||||
## Примечания
|
||||
|
||||
1. **Формат даты**: Все даты возвращаются в формате ISO 8601 без timezone (LocalDateTime)
|
||||
2. **Идентификаторы**: Используются MongoDB ObjectId (24 символа hex)
|
||||
3. **Асинхронность**: Анализ выполняется асинхронно, не блокируя запрос
|
||||
4. **Таймауты**: Рекомендуется устанавливать таймаут на запросы (минимум 30 секунд для запуска анализа)
|
||||
5. **Rate Limiting**: В будущем может быть добавлено ограничение на количество запросов
|
||||
|
||||
---
|
||||
|
||||
## Поддержка
|
||||
|
||||
При возникновении проблем с API обращайтесь в техническую поддержку с указанием:
|
||||
|
||||
- `analysisId` (если есть)
|
||||
- Время запроса
|
||||
- Описание проблемы
|
||||
- Код ошибки (если есть)
|
||||
@@ -0,0 +1,358 @@
|
||||
# API Документация: Маркетинговый анализ
|
||||
|
||||
## Обзор
|
||||
|
||||
API для запуска маркетингового анализа на основе данных о бизнесе пользователя. Эндпоинт принимает информацию о продукте, локации, типе клиентов и уникальных особенностях бизнеса, затем генерирует маркетинговый отчет.
|
||||
|
||||
## Эндпоинт
|
||||
|
||||
**POST** `/api/marketing/analysis/start`
|
||||
|
||||
### Базовый URL
|
||||
|
||||
```
|
||||
https://api.konturai.kz/api/marketing/analysis/start
|
||||
```
|
||||
|
||||
## Запрос
|
||||
|
||||
### Заголовки
|
||||
|
||||
```
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {access_token} // Опционально, если требуется аутентификация
|
||||
```
|
||||
|
||||
### Тело запроса (JSON)
|
||||
|
||||
| Поле | Тип | Обязательный | Описание | Пример значения |
|
||||
| ---------------- | ------ | ------------ | ------------------------------ | -------------------------------- |
|
||||
| `product` | string | ✅ | Название продукта или услуги | "Веб-разработка" |
|
||||
| `location` | string | ✅ | Географическая локация работы | "Алматы, Казахстан" |
|
||||
| `client` | string | ✅ | Тип целевой аудитории | "B2B клиенты" |
|
||||
| `differentiator` | string | ✅ | Уникальные особенности бизнеса | "Быстрая разработка за 2 недели" |
|
||||
|
||||
### Валидация полей
|
||||
|
||||
#### `product` (string, обязательное)
|
||||
|
||||
- **Минимальная длина**: 3 символа
|
||||
- **Максимальная длина**: 200 символов
|
||||
- **Паттерн**: Разрешены буквы, цифры, пробелы, дефисы, запятые
|
||||
- **Ошибка валидации**: `"product": "Поле 'product' должно содержать от 3 до 200 символов"`
|
||||
|
||||
#### `location` (string, обязательное)
|
||||
|
||||
- **Минимальная длина**: 2 символа
|
||||
- **Максимальная длина**: 150 символов
|
||||
- **Паттерн**: Разрешены буквы, цифры, пробелы, запятые, дефисы
|
||||
- **Ошибка валидации**: `"location": "Поле 'location' должно содержать от 2 до 150 символов"`
|
||||
|
||||
#### `client` (string, обязательное)
|
||||
|
||||
- **Допустимые значения**:
|
||||
- `"B2B клиенты"`
|
||||
- `"B2C клиенты"`
|
||||
- `"Частные лица"`
|
||||
- `"Корпорации"`
|
||||
- `"Малый бизнес"`
|
||||
- **Ошибка валидации**: `"client": "Поле 'client' должно быть одним из: B2B клиенты, B2C клиенты, Частные лица, Корпорации, Малый бизнес"`
|
||||
|
||||
#### `differentiator` (string, обязательное)
|
||||
|
||||
- **Минимальная длина**: 10 символов
|
||||
- **Максимальная длина**: 500 символов
|
||||
- **Паттерн**: Разрешены любые символы
|
||||
- **Ошибка валидации**: `"differentiator": "Поле 'differentiator' должно содержать от 10 до 500 символов"`
|
||||
|
||||
### Пример запроса
|
||||
|
||||
```json
|
||||
{
|
||||
"product": "Разработка мобильных приложений",
|
||||
"location": "Нур-Султан, Казахстан",
|
||||
"client": "B2B клиенты",
|
||||
"differentiator": "Специализируемся на быстрой разработке MVP за 4 недели с использованием современных технологий"
|
||||
}
|
||||
```
|
||||
|
||||
## Ответ
|
||||
|
||||
### Успешный ответ (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"analysisId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"status": "processing",
|
||||
"estimatedCompletionTime": "2025-01-20T15:30:00Z",
|
||||
"message": "Анализ запущен успешно. Результаты будут готовы в течение 5-10 минут."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Поля ответа
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
| ------------------------------ | ------- | --------------------------------------------------------- |
|
||||
| `success` | boolean | Флаг успешности операции |
|
||||
| `data.analysisId` | string | Уникальный идентификатор анализа (UUID) |
|
||||
| `data.status` | string | Статус анализа: `"processing"`, `"completed"`, `"failed"` |
|
||||
| `data.estimatedCompletionTime` | string | ISO 8601 дата/время ожидаемого завершения анализа |
|
||||
| `data.message` | string | Информационное сообщение для пользователя |
|
||||
|
||||
### Асинхронная обработка (202 Accepted)
|
||||
|
||||
Если анализ требует длительной обработки, сервер может вернуть статус 202:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"analysisId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"status": "queued",
|
||||
"queuePosition": 3,
|
||||
"estimatedWaitTime": 300,
|
||||
"message": "Запрос добавлен в очередь. Примерное время ожидания: 5 минут."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Обработка ошибок
|
||||
|
||||
### Ошибки валидации (400 Bad Request)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "Ошибка валидации входных данных",
|
||||
"details": {
|
||||
"product": "Поле 'product' обязательно для заполнения",
|
||||
"client": "Недопустимое значение поля 'client'"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Ошибка аутентификации (401 Unauthorized)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "UNAUTHORIZED",
|
||||
"message": "Требуется аутентификация"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Ошибка сервера (500 Internal Server Error)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "INTERNAL_SERVER_ERROR",
|
||||
"message": "Произошла внутренняя ошибка сервера. Попробуйте позже."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Ошибка таймаута (504 Gateway Timeout)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "TIMEOUT",
|
||||
"message": "Превышено время ожидания ответа от сервиса анализа"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Получение результатов анализа
|
||||
|
||||
После успешного запуска анализа, результаты можно получить по идентификатору:
|
||||
|
||||
**GET** `/api/marketing/analysis/{analysisId}`
|
||||
|
||||
### Пример запроса
|
||||
|
||||
```
|
||||
GET /api/marketing/analysis/550e8400-e29b-41d4-a716-446655440000
|
||||
```
|
||||
|
||||
### Пример ответа (когда анализ завершен)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"analysisId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"status": "completed",
|
||||
"createdAt": "2025-01-20T15:00:00Z",
|
||||
"completedAt": "2025-01-20T15:08:00Z",
|
||||
"report": {
|
||||
"summary": "Краткое резюме анализа...",
|
||||
"targetAudience": {
|
||||
"description": "Описание целевой аудитории...",
|
||||
"channels": ["Instagram", "LinkedIn", "Telegram"]
|
||||
},
|
||||
"recommendations": ["Рекомендация 1", "Рекомендация 2"],
|
||||
"strategy": {
|
||||
"duration": "2 недели",
|
||||
"channels": ["Instagram", "Telegram", "21MC"],
|
||||
"contentTypes": ["посты", "сторис", "баннеры"]
|
||||
},
|
||||
"pdfUrl": "/api/marketing/analysis/550e8400-e29b-41d4-a716-446655440000/download"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Статусы анализа
|
||||
|
||||
- `queued` - Запрос в очереди на обработку
|
||||
- `processing` - Анализ выполняется
|
||||
- `completed` - Анализ завершен успешно
|
||||
- `failed` - Анализ завершился с ошибкой
|
||||
|
||||
## Рекомендации по реализации
|
||||
|
||||
### 1. Валидация на бэкенде
|
||||
|
||||
```java
|
||||
// Пример валидации (Java/Spring Boot)
|
||||
@PostMapping("/api/marketing/analysis/start")
|
||||
public ResponseEntity<?> startAnalysis(@Valid @RequestBody MarketingAnalysisRequest request) {
|
||||
// Валидация выполняется автоматически через @Valid
|
||||
// Дополнительная бизнес-логика валидации
|
||||
if (!isValidClientType(request.getClient())) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(new ErrorResponse("VALIDATION_ERROR", "Недопустимый тип клиента"));
|
||||
}
|
||||
// Обработка запроса
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Асинхронная обработка
|
||||
|
||||
Рекомендуется использовать асинхронную обработку для длительных операций:
|
||||
|
||||
```java
|
||||
@Async
|
||||
public CompletableFuture<AnalysisResult> processAnalysis(MarketingAnalysisRequest request) {
|
||||
// Длительная обработка
|
||||
// Генерация отчета
|
||||
// Сохранение результатов
|
||||
return CompletableFuture.completedFuture(result);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Хранение данных
|
||||
|
||||
Рекомендуемая структура таблицы в БД:
|
||||
|
||||
```sql
|
||||
CREATE TABLE marketing_analysis (
|
||||
id UUID PRIMARY KEY,
|
||||
product VARCHAR(200) NOT NULL,
|
||||
location VARCHAR(150) NOT NULL,
|
||||
client_type VARCHAR(50) NOT NULL,
|
||||
differentiator TEXT NOT NULL,
|
||||
status VARCHAR(20) NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
completed_at TIMESTAMP,
|
||||
user_id UUID, -- Если требуется аутентификация
|
||||
report_data JSONB, -- JSON с результатами анализа
|
||||
CONSTRAINT valid_client_type CHECK (client_type IN (
|
||||
'B2B клиенты', 'B2C клиенты', 'Частные лица', 'Корпорации', 'Малый бизнес'
|
||||
)),
|
||||
CONSTRAINT valid_status CHECK (status IN (
|
||||
'queued', 'processing', 'completed', 'failed'
|
||||
))
|
||||
);
|
||||
```
|
||||
|
||||
### 4. Интеграция с AI сервисом
|
||||
|
||||
Если используется внешний AI сервис для генерации анализа:
|
||||
|
||||
```java
|
||||
public AnalysisResult generateAnalysis(MarketingAnalysisRequest request) {
|
||||
// Подготовка промпта для AI
|
||||
String prompt = String.format(
|
||||
"Проанализируй бизнес:\n" +
|
||||
"Продукт: %s\n" +
|
||||
"Локация: %s\n" +
|
||||
"Клиенты: %s\n" +
|
||||
"Уникальность: %s\n" +
|
||||
"Создай маркетинговую стратегию...",
|
||||
request.getProduct(),
|
||||
request.getLocation(),
|
||||
request.getClient(),
|
||||
request.getDifferentiator()
|
||||
);
|
||||
|
||||
// Вызов AI API
|
||||
return aiService.generateReport(prompt);
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Обработка ошибок
|
||||
|
||||
```java
|
||||
@ExceptionHandler(ValidationException.class)
|
||||
public ResponseEntity<ErrorResponse> handleValidationException(ValidationException e) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(new ErrorResponse("VALIDATION_ERROR", e.getMessage(), e.getDetails()));
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ErrorResponse> handleGenericException(Exception e) {
|
||||
log.error("Unexpected error", e);
|
||||
return ResponseEntity.status(500)
|
||||
.body(new ErrorResponse("INTERNAL_SERVER_ERROR", "Внутренняя ошибка сервера"));
|
||||
}
|
||||
```
|
||||
|
||||
## Тестирование
|
||||
|
||||
### Примеры тестовых запросов
|
||||
|
||||
#### Успешный запрос
|
||||
|
||||
```bash
|
||||
curl -X POST https://api.konturai.kz/api/marketing/analysis/start \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"product": "Разработка мобильных приложений",
|
||||
"location": "Алматы, Казахстан",
|
||||
"client": "B2B клиенты",
|
||||
"differentiator": "Специализируемся на быстрой разработке MVP за 4 недели"
|
||||
}'
|
||||
```
|
||||
|
||||
#### Запрос с ошибкой валидации
|
||||
|
||||
```bash
|
||||
curl -X POST https://api.konturai.kz/api/marketing/analysis/start \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"product": "AB",
|
||||
"location": "Алматы",
|
||||
"client": "Неверный тип",
|
||||
"differentiator": "Коротко"
|
||||
}'
|
||||
```
|
||||
|
||||
## Примечания
|
||||
|
||||
1. **Аутентификация**: Если требуется аутентификация, используйте JWT токен в заголовке `Authorization`
|
||||
2. **Rate Limiting**: Рекомендуется ограничить количество запросов на пользователя (например, 10 запросов в час)
|
||||
3. **Кэширование**: Можно кэшировать результаты для одинаковых запросов
|
||||
4. **Логирование**: Все запросы должны логироваться для отладки и аналитики
|
||||
5. **Мониторинг**: Отслеживайте время выполнения анализа и процент успешных завершений
|
||||
@@ -0,0 +1,127 @@
|
||||
package kz.konturai.parser.controller;
|
||||
|
||||
import kz.konturai.parser.dto.*;
|
||||
import kz.konturai.parser.model.MarketingAnalysis;
|
||||
import kz.konturai.parser.service.MarketingAnalysisService;
|
||||
import kz.konturai.parser.service.MinIOService;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import java.io.InputStream;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/marketing/analysis")
|
||||
public class MarketingController {
|
||||
|
||||
private final MarketingAnalysisService marketingAnalysisService;
|
||||
private final MinIOService minIOService;
|
||||
|
||||
public MarketingController(MarketingAnalysisService marketingAnalysisService, MinIOService minIOService) {
|
||||
this.marketingAnalysisService = marketingAnalysisService;
|
||||
this.minIOService = minIOService;
|
||||
}
|
||||
|
||||
@PostMapping("/start")
|
||||
public ResponseEntity<ApiResponse<MarketingAnalysisResponse>> startAnalysis(
|
||||
@Valid @RequestBody MarketingAnalysisRequest request) {
|
||||
|
||||
// Create analysis record
|
||||
MarketingAnalysis analysis = marketingAnalysisService.startAnalysis(request);
|
||||
|
||||
// Start async processing
|
||||
marketingAnalysisService.processAnalysis(analysis.getId(), request);
|
||||
|
||||
// Calculate estimated completion time (5-10 minutes)
|
||||
LocalDateTime estimatedCompletion = LocalDateTime.now().plusMinutes(8);
|
||||
|
||||
MarketingAnalysisResponse response = new MarketingAnalysisResponse(
|
||||
analysis.getId(),
|
||||
"processing",
|
||||
estimatedCompletion,
|
||||
"Анализ запущен успешно. Результаты будут готовы в течение 5-10 минут.");
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success("Анализ запущен успешно", response));
|
||||
}
|
||||
|
||||
@GetMapping("/{analysisId}")
|
||||
public ResponseEntity<?> getAnalysis(
|
||||
@PathVariable String analysisId) {
|
||||
|
||||
MarketingAnalysisResult result = marketingAnalysisService.getAnalysisResult(analysisId);
|
||||
|
||||
if (result == null) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"NOT_FOUND",
|
||||
"Анализ с указанным ID не найден");
|
||||
return ResponseEntity.status(404)
|
||||
.body(ApiResponse.error("Анализ не найден", error));
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(result));
|
||||
}
|
||||
|
||||
@GetMapping("/{analysisId}/download")
|
||||
public ResponseEntity<byte[]> downloadPdf(@PathVariable String analysisId) {
|
||||
MarketingAnalysisResult result = marketingAnalysisService.getAnalysisResult(analysisId);
|
||||
|
||||
if (result == null || result.getReport() == null || result.getReport().getPdfUrl() == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
// Get the analysis to find PDF filename
|
||||
Optional<MarketingAnalysis> optAnalysis = marketingAnalysisService.getAnalysisById(analysisId);
|
||||
if (optAnalysis.isEmpty() || optAnalysis.get().getPdfFilePath() == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
try {
|
||||
InputStream inputStream = minIOService.downloadFile(optAnalysis.get().getPdfFilePath());
|
||||
byte[] bytes = inputStream.readAllBytes();
|
||||
inputStream.close();
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||
"attachment; filename=\"" + optAnalysis.get().getPdfFilename() + "\"")
|
||||
.contentType(MediaType.APPLICATION_PDF)
|
||||
.body(bytes);
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
}
|
||||
|
||||
@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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package kz.konturai.parser.dto;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class ErrorResponse {
|
||||
private String code;
|
||||
private String message;
|
||||
private Map<String, String> details;
|
||||
|
||||
public ErrorResponse() {
|
||||
}
|
||||
|
||||
public ErrorResponse(String code, String message) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public ErrorResponse(String code, String message, Map<String, String> details) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
this.details = details;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public Map<String, String> getDetails() {
|
||||
return details;
|
||||
}
|
||||
|
||||
public void setDetails(Map<String, String> details) {
|
||||
this.details = details;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package kz.konturai.parser.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import kz.konturai.parser.validator.ValidClientType;
|
||||
|
||||
public class MarketingAnalysisRequest {
|
||||
|
||||
@NotBlank(message = "Поле 'product' обязательно для заполнения")
|
||||
@Size(min = 3, max = 200, message = "Поле 'product' должно содержать от 3 до 200 символов")
|
||||
@Pattern(regexp = "^[\\p{L}\\p{N}\\s\\-,]+$", message = "Поле 'product' содержит недопустимые символы")
|
||||
private String product;
|
||||
|
||||
@NotBlank(message = "Поле 'location' обязательно для заполнения")
|
||||
@Size(min = 2, max = 150, message = "Поле 'location' должно содержать от 2 до 150 символов")
|
||||
@Pattern(regexp = "^[\\p{L}\\p{N}\\s\\-,]+$", message = "Поле 'location' содержит недопустимые символы")
|
||||
private String location;
|
||||
|
||||
@NotBlank(message = "Поле 'client' обязательно для заполнения")
|
||||
@ValidClientType(message = "Поле 'client' должно быть одним из: B2B клиенты, B2C клиенты, Частные лица, Корпорации, Малый бизнес")
|
||||
private String client;
|
||||
|
||||
@NotBlank(message = "Поле 'differentiator' обязательно для заполнения")
|
||||
@Size(min = 10, max = 500, message = "Поле 'differentiator' должно содержать от 10 до 500 символов")
|
||||
private String differentiator;
|
||||
|
||||
public MarketingAnalysisRequest() {
|
||||
}
|
||||
|
||||
public MarketingAnalysisRequest(String product, String location, String client, String differentiator) {
|
||||
this.product = product;
|
||||
this.location = location;
|
||||
this.client = client;
|
||||
this.differentiator = differentiator;
|
||||
}
|
||||
|
||||
public String getProduct() {
|
||||
return product;
|
||||
}
|
||||
|
||||
public void setProduct(String product) {
|
||||
this.product = product;
|
||||
}
|
||||
|
||||
public String getLocation() {
|
||||
return location;
|
||||
}
|
||||
|
||||
public void setLocation(String location) {
|
||||
this.location = location;
|
||||
}
|
||||
|
||||
public String getClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
public void setClient(String client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
public String getDifferentiator() {
|
||||
return differentiator;
|
||||
}
|
||||
|
||||
public void setDifferentiator(String differentiator) {
|
||||
this.differentiator = differentiator;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package kz.konturai.parser.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class MarketingAnalysisResponse {
|
||||
private String analysisId;
|
||||
private String status;
|
||||
private LocalDateTime estimatedCompletionTime;
|
||||
private String message;
|
||||
private Integer queuePosition;
|
||||
private Integer estimatedWaitTime;
|
||||
|
||||
public MarketingAnalysisResponse() {
|
||||
}
|
||||
|
||||
public MarketingAnalysisResponse(String analysisId, String status, LocalDateTime estimatedCompletionTime,
|
||||
String message) {
|
||||
this.analysisId = analysisId;
|
||||
this.status = status;
|
||||
this.estimatedCompletionTime = estimatedCompletionTime;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getAnalysisId() {
|
||||
return analysisId;
|
||||
}
|
||||
|
||||
public void setAnalysisId(String analysisId) {
|
||||
this.analysisId = analysisId;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public LocalDateTime getEstimatedCompletionTime() {
|
||||
return estimatedCompletionTime;
|
||||
}
|
||||
|
||||
public void setEstimatedCompletionTime(LocalDateTime estimatedCompletionTime) {
|
||||
this.estimatedCompletionTime = estimatedCompletionTime;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public Integer getQueuePosition() {
|
||||
return queuePosition;
|
||||
}
|
||||
|
||||
public void setQueuePosition(Integer queuePosition) {
|
||||
this.queuePosition = queuePosition;
|
||||
}
|
||||
|
||||
public Integer getEstimatedWaitTime() {
|
||||
return estimatedWaitTime;
|
||||
}
|
||||
|
||||
public void setEstimatedWaitTime(Integer estimatedWaitTime) {
|
||||
this.estimatedWaitTime = estimatedWaitTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package kz.konturai.parser.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public class MarketingAnalysisResult {
|
||||
private String analysisId;
|
||||
private String status;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime completedAt;
|
||||
private MarketingReport report;
|
||||
|
||||
public MarketingAnalysisResult() {
|
||||
}
|
||||
|
||||
public MarketingAnalysisResult(String analysisId, String status, LocalDateTime createdAt, LocalDateTime completedAt,
|
||||
MarketingReport report) {
|
||||
this.analysisId = analysisId;
|
||||
this.status = status;
|
||||
this.createdAt = createdAt;
|
||||
this.completedAt = completedAt;
|
||||
this.report = report;
|
||||
}
|
||||
|
||||
public String getAnalysisId() {
|
||||
return analysisId;
|
||||
}
|
||||
|
||||
public void setAnalysisId(String analysisId) {
|
||||
this.analysisId = analysisId;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getCompletedAt() {
|
||||
return completedAt;
|
||||
}
|
||||
|
||||
public void setCompletedAt(LocalDateTime completedAt) {
|
||||
this.completedAt = completedAt;
|
||||
}
|
||||
|
||||
public MarketingReport getReport() {
|
||||
return report;
|
||||
}
|
||||
|
||||
public void setReport(MarketingReport report) {
|
||||
this.report = report;
|
||||
}
|
||||
|
||||
public static class MarketingReport {
|
||||
private String summary;
|
||||
private TargetAudience targetAudience;
|
||||
private List<String> recommendations;
|
||||
private Strategy strategy;
|
||||
private String pdfUrl;
|
||||
|
||||
public MarketingReport() {
|
||||
}
|
||||
|
||||
public String getSummary() {
|
||||
return summary;
|
||||
}
|
||||
|
||||
public void setSummary(String summary) {
|
||||
this.summary = summary;
|
||||
}
|
||||
|
||||
public TargetAudience getTargetAudience() {
|
||||
return targetAudience;
|
||||
}
|
||||
|
||||
public void setTargetAudience(TargetAudience targetAudience) {
|
||||
this.targetAudience = targetAudience;
|
||||
}
|
||||
|
||||
public List<String> getRecommendations() {
|
||||
return recommendations;
|
||||
}
|
||||
|
||||
public void setRecommendations(List<String> recommendations) {
|
||||
this.recommendations = recommendations;
|
||||
}
|
||||
|
||||
public Strategy getStrategy() {
|
||||
return strategy;
|
||||
}
|
||||
|
||||
public void setStrategy(Strategy strategy) {
|
||||
this.strategy = strategy;
|
||||
}
|
||||
|
||||
public String getPdfUrl() {
|
||||
return pdfUrl;
|
||||
}
|
||||
|
||||
public void setPdfUrl(String pdfUrl) {
|
||||
this.pdfUrl = pdfUrl;
|
||||
}
|
||||
}
|
||||
|
||||
public static class TargetAudience {
|
||||
private String description;
|
||||
private List<String> channels;
|
||||
|
||||
public TargetAudience() {
|
||||
}
|
||||
|
||||
public TargetAudience(String description, List<String> channels) {
|
||||
this.description = description;
|
||||
this.channels = channels;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public List<String> getChannels() {
|
||||
return channels;
|
||||
}
|
||||
|
||||
public void setChannels(List<String> channels) {
|
||||
this.channels = channels;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Strategy {
|
||||
private String duration;
|
||||
private List<String> channels;
|
||||
private List<String> contentTypes;
|
||||
|
||||
public Strategy() {
|
||||
}
|
||||
|
||||
public Strategy(String duration, List<String> channels, List<String> contentTypes) {
|
||||
this.duration = duration;
|
||||
this.channels = channels;
|
||||
this.contentTypes = contentTypes;
|
||||
}
|
||||
|
||||
public String getDuration() {
|
||||
return duration;
|
||||
}
|
||||
|
||||
public void setDuration(String duration) {
|
||||
this.duration = duration;
|
||||
}
|
||||
|
||||
public List<String> getChannels() {
|
||||
return channels;
|
||||
}
|
||||
|
||||
public void setChannels(List<String> channels) {
|
||||
this.channels = channels;
|
||||
}
|
||||
|
||||
public List<String> getContentTypes() {
|
||||
return contentTypes;
|
||||
}
|
||||
|
||||
public void setContentTypes(List<String> contentTypes) {
|
||||
this.contentTypes = contentTypes;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package kz.konturai.parser.model;
|
||||
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
import org.springframework.data.mongodb.core.mapping.Field;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
@Document(collection = "marketing_analysis")
|
||||
public class MarketingAnalysis {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Field("product")
|
||||
private String product;
|
||||
|
||||
@Field("location")
|
||||
private String location;
|
||||
|
||||
@Field("client_type")
|
||||
private String clientType;
|
||||
|
||||
@Field("differentiator")
|
||||
private String differentiator;
|
||||
|
||||
@Field("status")
|
||||
private String status; // queued, processing, completed, failed
|
||||
|
||||
@Field("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Field("completed_at")
|
||||
private LocalDateTime completedAt;
|
||||
|
||||
@Field("report_data")
|
||||
private Map<String, Object> reportData; // JSON data with report content
|
||||
|
||||
@Field("pdf_filename")
|
||||
private String pdfFilename;
|
||||
|
||||
@Field("pdf_file_path")
|
||||
private String pdfFilePath;
|
||||
|
||||
@Field("user_id")
|
||||
private String userId; // Optional, for future authentication
|
||||
|
||||
public MarketingAnalysis() {
|
||||
this.createdAt = LocalDateTime.now();
|
||||
this.status = "queued";
|
||||
}
|
||||
|
||||
public MarketingAnalysis(String product, String location, String clientType, String differentiator) {
|
||||
this();
|
||||
this.product = product;
|
||||
this.location = location;
|
||||
this.clientType = clientType;
|
||||
this.differentiator = differentiator;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getProduct() {
|
||||
return product;
|
||||
}
|
||||
|
||||
public void setProduct(String product) {
|
||||
this.product = product;
|
||||
}
|
||||
|
||||
public String getLocation() {
|
||||
return location;
|
||||
}
|
||||
|
||||
public void setLocation(String location) {
|
||||
this.location = location;
|
||||
}
|
||||
|
||||
public String getClientType() {
|
||||
return clientType;
|
||||
}
|
||||
|
||||
public void setClientType(String clientType) {
|
||||
this.clientType = clientType;
|
||||
}
|
||||
|
||||
public String getDifferentiator() {
|
||||
return differentiator;
|
||||
}
|
||||
|
||||
public void setDifferentiator(String differentiator) {
|
||||
this.differentiator = differentiator;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getCompletedAt() {
|
||||
return completedAt;
|
||||
}
|
||||
|
||||
public void setCompletedAt(LocalDateTime completedAt) {
|
||||
this.completedAt = completedAt;
|
||||
}
|
||||
|
||||
public Map<String, Object> getReportData() {
|
||||
return reportData;
|
||||
}
|
||||
|
||||
public void setReportData(Map<String, Object> reportData) {
|
||||
this.reportData = reportData;
|
||||
}
|
||||
|
||||
public String getPdfFilename() {
|
||||
return pdfFilename;
|
||||
}
|
||||
|
||||
public void setPdfFilename(String pdfFilename) {
|
||||
this.pdfFilename = pdfFilename;
|
||||
}
|
||||
|
||||
public String getPdfFilePath() {
|
||||
return pdfFilePath;
|
||||
}
|
||||
|
||||
public void setPdfFilePath(String pdfFilePath) {
|
||||
this.pdfFilePath = pdfFilePath;
|
||||
}
|
||||
|
||||
public String getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(String userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package kz.konturai.parser.repository;
|
||||
|
||||
import kz.konturai.parser.model.MarketingAnalysis;
|
||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface MarketingAnalysisRepository extends MongoRepository<MarketingAnalysis, String> {
|
||||
Optional<MarketingAnalysis> findById(String id);
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
package kz.konturai.parser.service;
|
||||
|
||||
import kz.konturai.parser.dto.MarketingAnalysisRequest;
|
||||
import kz.konturai.parser.dto.MarketingAnalysisResult;
|
||||
import kz.konturai.parser.model.MarketingAnalysis;
|
||||
import kz.konturai.parser.repository.MarketingAnalysisRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
@Service
|
||||
public class MarketingAnalysisService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(MarketingAnalysisService.class);
|
||||
|
||||
private final MarketingAnalysisRepository repository;
|
||||
private final OpenAIAnalyticsService openAIAnalyticsService;
|
||||
private final MinIOService minIOService;
|
||||
private final ResearchPdfService researchPdfService;
|
||||
|
||||
public MarketingAnalysisService(
|
||||
MarketingAnalysisRepository repository,
|
||||
OpenAIAnalyticsService openAIAnalyticsService,
|
||||
MinIOService minIOService,
|
||||
ResearchPdfService researchPdfService) {
|
||||
this.repository = repository;
|
||||
this.openAIAnalyticsService = openAIAnalyticsService;
|
||||
this.minIOService = minIOService;
|
||||
this.researchPdfService = researchPdfService;
|
||||
}
|
||||
|
||||
public MarketingAnalysis startAnalysis(MarketingAnalysisRequest request) {
|
||||
MarketingAnalysis analysis = new MarketingAnalysis(
|
||||
request.getProduct(),
|
||||
request.getLocation(),
|
||||
request.getClient(),
|
||||
request.getDifferentiator());
|
||||
analysis.setStatus("queued");
|
||||
analysis = repository.save(analysis);
|
||||
logger.info("Marketing analysis created with ID: {}", analysis.getId());
|
||||
return analysis;
|
||||
}
|
||||
|
||||
@Async("reportGenerationExecutor")
|
||||
public void processAnalysis(String analysisId, MarketingAnalysisRequest request) {
|
||||
try {
|
||||
Optional<MarketingAnalysis> optAnalysis = repository.findById(analysisId);
|
||||
if (optAnalysis.isEmpty()) {
|
||||
logger.error("Marketing analysis not found: {}", analysisId);
|
||||
return;
|
||||
}
|
||||
|
||||
MarketingAnalysis analysis = optAnalysis.get();
|
||||
analysis.setStatus("processing");
|
||||
repository.save(analysis);
|
||||
|
||||
logger.info("Starting marketing analysis processing for ID: {}", analysisId);
|
||||
|
||||
// Generate marketing report
|
||||
Map<String, Object> reportData = generateMarketingReport(request);
|
||||
|
||||
// Generate markdown content for PDF
|
||||
String markdownContent = buildMarkdownFromReport(reportData, request);
|
||||
|
||||
// Generate PDF
|
||||
byte[] pdfBytes = researchPdfService.generatePdfReport(
|
||||
"Маркетинговый анализ: " + request.getProduct(),
|
||||
markdownContent,
|
||||
new ArrayList<>());
|
||||
|
||||
// Save PDF to MinIO
|
||||
String filename = "marketing_analysis_" + analysisId + "_" + System.currentTimeMillis() + ".pdf";
|
||||
minIOService.uploadFile(filename, pdfBytes, MediaType.APPLICATION_PDF.toString());
|
||||
|
||||
// Update analysis with results
|
||||
analysis.setStatus("completed");
|
||||
analysis.setCompletedAt(LocalDateTime.now());
|
||||
analysis.setReportData(reportData);
|
||||
analysis.setPdfFilename(filename);
|
||||
analysis.setPdfFilePath(filename);
|
||||
repository.save(analysis);
|
||||
|
||||
logger.info("Marketing analysis completed successfully for ID: {}", analysisId);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Error processing marketing analysis {}: {}", analysisId, e.getMessage(), e);
|
||||
try {
|
||||
Optional<MarketingAnalysis> optAnalysis = repository.findById(analysisId);
|
||||
if (optAnalysis.isPresent()) {
|
||||
MarketingAnalysis analysis = optAnalysis.get();
|
||||
analysis.setStatus("failed");
|
||||
repository.save(analysis);
|
||||
}
|
||||
} catch (Exception saveError) {
|
||||
logger.error("Failed to update analysis status to failed: {}", saveError.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> generateMarketingReport(MarketingAnalysisRequest request) {
|
||||
Map<String, Object> report = new HashMap<>();
|
||||
|
||||
// Build context for AI
|
||||
String context = String.format(
|
||||
"Продукт/услуга: %s\n" +
|
||||
"Локация: %s\n" +
|
||||
"Тип клиентов: %s\n" +
|
||||
"Уникальные особенности: %s\n",
|
||||
request.getProduct(),
|
||||
request.getLocation(),
|
||||
request.getClient(),
|
||||
request.getDifferentiator());
|
||||
|
||||
// Generate summary
|
||||
String summaryPrompt = "На основе следующей информации о бизнесе создай краткое резюме маркетингового анализа (2-3 абзаца). "
|
||||
+
|
||||
"Выдели ключевые возможности и особенности бизнеса. " +
|
||||
"Ответ должен быть на русском языке, деловым стилем.\n\n" + context;
|
||||
String summary = openAIAnalyticsService.generateWithInstruction(context, summaryPrompt, "ru");
|
||||
report.put("summary", summary != null ? summary : "Резюме не удалось сгенерировать.");
|
||||
|
||||
// Generate target audience description
|
||||
String audiencePrompt = "На основе информации о бизнесе опиши целевую аудиторию (1-2 абзаца). " +
|
||||
"Укажи, какие каналы коммуникации наиболее подходят для этой аудитории. " +
|
||||
"Ответ должен быть на русском языке.\n\n" + context;
|
||||
String audienceDescription = openAIAnalyticsService.generateWithInstruction(context, audiencePrompt, "ru");
|
||||
|
||||
// Extract channels from description or generate separately
|
||||
String channelsPrompt = "На основе описания бизнеса перечисли 3-5 наиболее подходящих маркетинговых каналов. " +
|
||||
"Ответ должен быть простым списком через запятую, без нумерации. Пример: Instagram, LinkedIn, Telegram\n\n"
|
||||
+ context;
|
||||
String channelsStr = openAIAnalyticsService.generateWithInstruction(context, channelsPrompt, "ru");
|
||||
List<String> channels = parseChannels(channelsStr);
|
||||
|
||||
Map<String, Object> targetAudience = new HashMap<>();
|
||||
targetAudience.put("description", audienceDescription != null ? audienceDescription
|
||||
: "Описание целевой аудитории не удалось сгенерировать.");
|
||||
targetAudience.put("channels",
|
||||
channels.isEmpty() ? Arrays.asList("Instagram", "LinkedIn", "Telegram") : channels);
|
||||
report.put("targetAudience", targetAudience);
|
||||
|
||||
// Generate recommendations
|
||||
String recommendationsPrompt = "На основе информации о бизнесе сформулируй 4-6 практических рекомендаций для маркетинговой стратегии. "
|
||||
+
|
||||
"Каждая рекомендация должна быть конкретной и применимой. " +
|
||||
"Ответ должен быть списком рекомендаций, каждая с новой строки, без нумерации.\n\n" + context;
|
||||
String recommendationsStr = openAIAnalyticsService.generateWithInstruction(context, recommendationsPrompt,
|
||||
"ru");
|
||||
List<String> recommendations = parseRecommendations(recommendationsStr);
|
||||
report.put("recommendations",
|
||||
recommendations.isEmpty() ? Arrays.asList("Рекомендации не удалось сгенерировать.") : recommendations);
|
||||
|
||||
// Generate strategy
|
||||
String strategyPrompt = "На основе информации о бизнесе создай краткую маркетинговую стратегию. " +
|
||||
"Укажи рекомендуемую длительность кампании (например, '2 недели', '1 месяц'), " +
|
||||
"3-5 каналов коммуникации и типы контента (например, 'посты', 'сторис', 'баннеры'). " +
|
||||
"Ответ должен быть структурированным текстом на русском языке.\n\n" + context;
|
||||
String strategyText = openAIAnalyticsService.generateWithInstruction(context, strategyPrompt, "ru");
|
||||
|
||||
Map<String, Object> strategy = parseStrategy(strategyText, channels);
|
||||
report.put("strategy", strategy);
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
private List<String> parseChannels(String channelsStr) {
|
||||
if (channelsStr == null || channelsStr.trim().isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
List<String> channels = new ArrayList<>();
|
||||
String[] parts = channelsStr.split("[,;\\n]");
|
||||
for (String part : parts) {
|
||||
String trimmed = part.trim();
|
||||
if (!trimmed.isEmpty() && trimmed.length() > 1) {
|
||||
channels.add(trimmed);
|
||||
}
|
||||
}
|
||||
return channels;
|
||||
}
|
||||
|
||||
private List<String> parseRecommendations(String recommendationsStr) {
|
||||
if (recommendationsStr == null || recommendationsStr.trim().isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
List<String> recommendations = new ArrayList<>();
|
||||
String[] lines = recommendationsStr.split("\\n");
|
||||
for (String line : lines) {
|
||||
String trimmed = line.trim();
|
||||
// Remove numbering and bullets
|
||||
trimmed = trimmed.replaceAll("^[\\d\\-\\.\\*]+\\s*", "");
|
||||
if (!trimmed.isEmpty() && trimmed.length() > 10) {
|
||||
recommendations.add(trimmed);
|
||||
}
|
||||
}
|
||||
return recommendations;
|
||||
}
|
||||
|
||||
private Map<String, Object> parseStrategy(String strategyText, List<String> defaultChannels) {
|
||||
Map<String, Object> strategy = new HashMap<>();
|
||||
|
||||
// Try to extract duration
|
||||
String duration = "2 недели"; // default
|
||||
if (strategyText != null) {
|
||||
if (strategyText.contains("неделя") || strategyText.contains("недели") || strategyText.contains("недель")) {
|
||||
// Extract duration pattern
|
||||
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("(\\d+)\\s*(неделя|недели|недель)");
|
||||
java.util.regex.Matcher matcher = pattern.matcher(strategyText);
|
||||
if (matcher.find()) {
|
||||
duration = matcher.group(1) + " " + matcher.group(2);
|
||||
}
|
||||
} else if (strategyText.contains("месяц") || strategyText.contains("месяца")
|
||||
|| strategyText.contains("месяцев")) {
|
||||
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("(\\d+)\\s*(месяц|месяца|месяцев)");
|
||||
java.util.regex.Matcher matcher = pattern.matcher(strategyText);
|
||||
if (matcher.find()) {
|
||||
duration = matcher.group(1) + " " + matcher.group(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
strategy.put("duration", duration);
|
||||
|
||||
// Extract channels from strategy text or use default
|
||||
List<String> channels = parseChannels(strategyText);
|
||||
if (channels.isEmpty()) {
|
||||
channels = defaultChannels.isEmpty() ? Arrays.asList("Instagram", "Telegram", "21MC") : defaultChannels;
|
||||
}
|
||||
strategy.put("channels", channels);
|
||||
|
||||
// Extract content types
|
||||
List<String> contentTypes = new ArrayList<>();
|
||||
if (strategyText != null) {
|
||||
String lowerText = strategyText.toLowerCase();
|
||||
if (lowerText.contains("пост") || lowerText.contains("посты")) {
|
||||
contentTypes.add("посты");
|
||||
}
|
||||
if (lowerText.contains("сторис") || lowerText.contains("stories")) {
|
||||
contentTypes.add("сторис");
|
||||
}
|
||||
if (lowerText.contains("баннер") || lowerText.contains("banner")) {
|
||||
contentTypes.add("баннеры");
|
||||
}
|
||||
if (lowerText.contains("видео") || lowerText.contains("video")) {
|
||||
contentTypes.add("видео");
|
||||
}
|
||||
}
|
||||
if (contentTypes.isEmpty()) {
|
||||
contentTypes = Arrays.asList("посты", "сторис", "баннеры");
|
||||
}
|
||||
strategy.put("contentTypes", contentTypes);
|
||||
|
||||
return strategy;
|
||||
}
|
||||
|
||||
private String buildMarkdownFromReport(Map<String, Object> reportData, MarketingAnalysisRequest request) {
|
||||
StringBuilder markdown = new StringBuilder();
|
||||
|
||||
markdown.append("# Маркетинговый анализ\n\n");
|
||||
markdown.append("## Информация о бизнесе\n\n");
|
||||
markdown.append("- **Продукт/услуга:** ").append(request.getProduct()).append("\n");
|
||||
markdown.append("- **Локация:** ").append(request.getLocation()).append("\n");
|
||||
markdown.append("- **Тип клиентов:** ").append(request.getClient()).append("\n");
|
||||
markdown.append("- **Уникальные особенности:** ").append(request.getDifferentiator()).append("\n\n");
|
||||
|
||||
markdown.append("## Резюме анализа\n\n");
|
||||
markdown.append(reportData.get("summary")).append("\n\n");
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> targetAudience = (Map<String, Object>) reportData.get("targetAudience");
|
||||
if (targetAudience != null) {
|
||||
markdown.append("## Целевая аудитория\n\n");
|
||||
markdown.append(targetAudience.get("description")).append("\n\n");
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> channels = (List<String>) targetAudience.get("channels");
|
||||
if (channels != null && !channels.isEmpty()) {
|
||||
markdown.append("### Рекомендуемые каналы:\n\n");
|
||||
for (String channel : channels) {
|
||||
markdown.append("- ").append(channel).append("\n");
|
||||
}
|
||||
markdown.append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> recommendations = (List<String>) reportData.get("recommendations");
|
||||
if (recommendations != null && !recommendations.isEmpty()) {
|
||||
markdown.append("## Рекомендации\n\n");
|
||||
for (String rec : recommendations) {
|
||||
markdown.append("- ").append(rec).append("\n");
|
||||
}
|
||||
markdown.append("\n");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> strategy = (Map<String, Object>) reportData.get("strategy");
|
||||
if (strategy != null) {
|
||||
markdown.append("## Маркетинговая стратегия\n\n");
|
||||
markdown.append("### Длительность кампании: ").append(strategy.get("duration")).append("\n\n");
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> strategyChannels = (List<String>) strategy.get("channels");
|
||||
if (strategyChannels != null && !strategyChannels.isEmpty()) {
|
||||
markdown.append("### Каналы коммуникации:\n\n");
|
||||
for (String channel : strategyChannels) {
|
||||
markdown.append("- ").append(channel).append("\n");
|
||||
}
|
||||
markdown.append("\n");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> contentTypes = (List<String>) strategy.get("contentTypes");
|
||||
if (contentTypes != null && !contentTypes.isEmpty()) {
|
||||
markdown.append("### Типы контента:\n\n");
|
||||
for (String type : contentTypes) {
|
||||
markdown.append("- ").append(type).append("\n");
|
||||
}
|
||||
markdown.append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
return markdown.toString();
|
||||
}
|
||||
|
||||
public MarketingAnalysisResult getAnalysisResult(String analysisId) {
|
||||
Optional<MarketingAnalysis> optAnalysis = repository.findById(analysisId);
|
||||
if (optAnalysis.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
MarketingAnalysis analysis = optAnalysis.get();
|
||||
MarketingAnalysisResult result = new MarketingAnalysisResult();
|
||||
result.setAnalysisId(analysis.getId());
|
||||
result.setStatus(analysis.getStatus());
|
||||
result.setCreatedAt(analysis.getCreatedAt());
|
||||
result.setCompletedAt(analysis.getCompletedAt());
|
||||
|
||||
if (analysis.getReportData() != null && "completed".equals(analysis.getStatus())) {
|
||||
MarketingAnalysisResult.MarketingReport report = buildReportFromData(analysis.getReportData(), analysis);
|
||||
result.setReport(report);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public Optional<MarketingAnalysis> getAnalysisById(String analysisId) {
|
||||
return repository.findById(analysisId);
|
||||
}
|
||||
|
||||
private MarketingAnalysisResult.MarketingReport buildReportFromData(Map<String, Object> reportData,
|
||||
MarketingAnalysis analysis) {
|
||||
MarketingAnalysisResult.MarketingReport report = new MarketingAnalysisResult.MarketingReport();
|
||||
|
||||
report.setSummary((String) reportData.get("summary"));
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> targetAudienceData = (Map<String, Object>) reportData.get("targetAudience");
|
||||
if (targetAudienceData != null) {
|
||||
MarketingAnalysisResult.TargetAudience targetAudience = new MarketingAnalysisResult.TargetAudience();
|
||||
targetAudience.setDescription((String) targetAudienceData.get("description"));
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> channels = (List<String>) targetAudienceData.get("channels");
|
||||
targetAudience.setChannels(channels);
|
||||
report.setTargetAudience(targetAudience);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> recommendations = (List<String>) reportData.get("recommendations");
|
||||
report.setRecommendations(recommendations);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> strategyData = (Map<String, Object>) reportData.get("strategy");
|
||||
if (strategyData != null) {
|
||||
MarketingAnalysisResult.Strategy strategy = new MarketingAnalysisResult.Strategy();
|
||||
strategy.setDuration((String) strategyData.get("duration"));
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> channels = (List<String>) strategyData.get("channels");
|
||||
strategy.setChannels(channels);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> contentTypes = (List<String>) strategyData.get("contentTypes");
|
||||
strategy.setContentTypes(contentTypes);
|
||||
report.setStrategy(strategy);
|
||||
}
|
||||
|
||||
if (analysis.getPdfFilename() != null) {
|
||||
report.setPdfUrl("/api/marketing/analysis/" + analysis.getId() + "/download");
|
||||
}
|
||||
|
||||
return report;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package kz.konturai.parser.validator;
|
||||
|
||||
import jakarta.validation.ConstraintValidator;
|
||||
import jakarta.validation.ConstraintValidatorContext;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class ClientTypeValidator implements ConstraintValidator<ValidClientType, String> {
|
||||
|
||||
private static final List<String> VALID_CLIENT_TYPES = Arrays.asList(
|
||||
"B2B клиенты",
|
||||
"B2C клиенты",
|
||||
"Частные лица",
|
||||
"Корпорации",
|
||||
"Малый бизнес");
|
||||
|
||||
@Override
|
||||
public void initialize(ValidClientType constraintAnnotation) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid(String value, ConstraintValidatorContext context) {
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
return true; // @NotBlank will handle null/empty
|
||||
}
|
||||
return VALID_CLIENT_TYPES.contains(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
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 = ClientTypeValidator.class)
|
||||
@Target({ ElementType.FIELD, ElementType.PARAMETER })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ValidClientType {
|
||||
String message() default "Поле 'client' должно быть одним из: B2B клиенты, B2C клиенты, Частные лица, Корпорации, Малый бизнес";
|
||||
|
||||
Class<?>[] groups() default {};
|
||||
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
Reference in New Issue
Block a user