From 13cd6819f57c72548dec92ab50e54bb64f8cc8b9 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 26 Nov 2025 00:22:54 +0500 Subject: [PATCH] . --- marketing-strategy-api-frontend.md | 922 ++++++++++++++++++ .../controller/MarketingController.java | 79 +- .../parser/dto/MarketingStrategyRequest.java | 39 + .../parser/dto/MarketingStrategyResponse.java | 251 +++++ .../parser/model/MarketingStrategy.java | 282 ++++++ .../MarketingStrategyRepository.java | 14 + .../service/MarketingStrategyService.java | 501 ++++++++++ 7 files changed, 2087 insertions(+), 1 deletion(-) create mode 100644 marketing-strategy-api-frontend.md create mode 100644 src/main/java/kz/konturai/parser/dto/MarketingStrategyRequest.java create mode 100644 src/main/java/kz/konturai/parser/dto/MarketingStrategyResponse.java create mode 100644 src/main/java/kz/konturai/parser/model/MarketingStrategy.java create mode 100644 src/main/java/kz/konturai/parser/repository/MarketingStrategyRepository.java create mode 100644 src/main/java/kz/konturai/parser/service/MarketingStrategyService.java diff --git a/marketing-strategy-api-frontend.md b/marketing-strategy-api-frontend.md new file mode 100644 index 0000000..b16fbe7 --- /dev/null +++ b/marketing-strategy-api-frontend.md @@ -0,0 +1,922 @@ +# API Документация: Генерация стратегии продвижения (Frontend/AI Agent) + +## Базовый URL + +``` +https://api.konturai.kz +``` + +## Обзор + +API для генерации детальной стратегии продвижения продукта на основе маркетингового анализа. Стратегия включает: + +1. **Недельный план** - темы и рекомендации по контенту для каждой недели +2. **Календарь постов** - детальный план публикаций с датами, платформами, текстами и хештегами + +Процесс состоит из двух этапов: + +1. **Запуск генерации стратегии** - создание задачи и начало асинхронной обработки +2. **Получение результатов** - проверка статуса и получение готовой стратегии + +Генерация стратегии выполняется асинхронно и занимает примерно 3-5 минут. + +**Важно**: Для генерации стратегии требуется завершенный маркетинговый анализ. Сначала необходимо получить `analysisId` из завершенного анализа. + +--- + +## Эндпоинты + +### 1. Запуск генерации стратегии продвижения + +**POST** `/api/marketing/strategy/generate` + +Создает новую задачу на генерацию стратегии продвижения и запускает асинхронную обработку. + +#### Параметры запроса + +| Параметр | Тип | Расположение | Обязательный | Описание | +| ------------------- | ------- | ------------ | ------------ | --------------------------------------- | +| `analysisId` | string | Query | ✅ | ID завершенного маркетингового анализа | +| `durationWeeks` | integer | Body | ❌ | Длительность стратегии в неделях (1-12) | +| `priorityPlatforms` | array | Body | ❌ | Приоритетные платформы для продвижения | + +#### Заголовки запроса + +``` +Content-Type: application/json +``` + +#### Тело запроса (JSON, опционально) + +| Поле | Тип | Обязательный | Описание | Пример значения | +| ------------------- | ------- | ------------ | --------------------------------------- | ------------------------------------- | +| `durationWeeks` | integer | ❌ | Длительность стратегии в неделях (1-12) | 4 | +| `priorityPlatforms` | array | ❌ | Список приоритетных платформ | ["Instagram", "LinkedIn", "Telegram"] | + +#### Валидация полей + +**`durationWeeks`** (integer, опциональное) + +- Минимальное значение: 1 +- Максимальное значение: 12 +- По умолчанию: 4 (если не указано) + +**`priorityPlatforms`** (array, опциональное) + +- Допустимые платформы: `Instagram`, `Facebook`, `LinkedIn`, `Telegram`, `TikTok`, `YouTube`, `21MC` и другие +- Если не указано, используются все популярные платформы + +#### Пример запроса + +```http +POST /api/marketing/strategy/generate?analysisId=507f1f77bcf86cd799439011 +Content-Type: application/json + +{ + "durationWeeks": 4, + "priorityPlatforms": ["Instagram", "LinkedIn", "Telegram"] +} +``` + +Или без тела запроса (используются значения по умолчанию): + +```http +POST /api/marketing/strategy/generate?analysisId=507f1f77bcf86cd799439011 +``` + +#### Пример успешного ответа (200 OK) + +```json +{ + "success": true, + "message": "Генерация стратегии запущена успешно. Результаты будут готовы в течение 3-5 минут.", + "data": { + "strategyId": "507f1f77bcf86cd799439012", + "analysisId": "507f1f77bcf86cd799439011", + "status": "queued", + "createdAt": "2025-01-20T15:40:00", + "durationWeeks": 4, + "priorityPlatforms": ["Instagram", "LinkedIn", "Telegram"] + } +} +``` + +#### Структура ответа + +| Поле | Тип | Описание | +| ------------------------ | ------- | ----------------------------------------------------------------------- | +| `success` | boolean | Флаг успешности операции | +| `message` | string | Сообщение о результате операции | +| `data.strategyId` | string | Уникальный идентификатор стратегии (MongoDB ObjectId) | +| `data.analysisId` | string | ID маркетингового анализа | +| `data.status` | string | Статус стратегии: `"queued"`, `"processing"`, `"completed"`, `"failed"` | +| `data.createdAt` | string | ISO 8601 дата/время создания | +| `data.durationWeeks` | integer | Длительность стратегии в неделях | +| `data.priorityPlatforms` | array | Список приоритетных платформ | + +#### Пример ошибки (404 Not Found - анализ не найден) + +```json +{ + "success": false, + "message": "Анализ не найден", + "error": { + "code": "INVALID_ANALYSIS", + "message": "Анализ с ID 507f1f77bcf86cd799439011 не найден" + } +} +``` + +#### Пример ошибки (400 Bad Request - анализ не завершен) + +```json +{ + "success": false, + "message": "Анализ еще не завершен", + "error": { + "code": "ANALYSIS_NOT_COMPLETED", + "message": "Анализ еще не завершен. Статус: processing" + } +} +``` + +#### Пример ошибки валидации (400 Bad Request) + +```json +{ + "success": false, + "message": "Ошибка валидации", + "error": { + "code": "VALIDATION_ERROR", + "message": "Ошибка валидации входных данных", + "details": { + "durationWeeks": "Длительность стратегии должна быть не менее 1 недели" + } + } +} +``` + +--- + +### 2. Получение стратегии по ID + +**GET** `/api/marketing/strategy/{strategyId}` + +Возвращает статус и результаты стратегии по идентификатору. + +#### Параметры пути + +| Параметр | Тип | Описание | +| ------------ | ------ | ----------------------- | +| `strategyId` | string | Идентификатор стратегии | + +#### Пример запроса + +``` +GET /api/marketing/strategy/507f1f77bcf86cd799439012 +``` + +#### Пример ответа (когда стратегия завершена - 200 OK) + +```json +{ + "success": true, + "message": "Операция выполнена успешно", + "data": { + "strategyId": "507f1f77bcf86cd799439012", + "analysisId": "507f1f77bcf86cd799439011", + "status": "completed", + "createdAt": "2025-01-20T15:40:00", + "completedAt": "2025-01-20T15:43:00", + "durationWeeks": 4, + "priorityPlatforms": ["Instagram", "LinkedIn", "Telegram"], + "strategy": { + "weeklyPlans": [ + { + "weekNumber": 1, + "mainThemes": [ + "Презентация продукта", + "Ключевые преимущества", + "Решение проблем клиентов" + ], + "contentRecommendations": "Сфокусируйтесь на представлении продукта и его основных преимуществах. Используйте визуальный контент для привлечения внимания. Подчеркните уникальные особенности, которые выделяют ваш продукт на рынке.", + "priorityPlatforms": ["Instagram", "LinkedIn"] + }, + { + "weekNumber": 2, + "mainThemes": [ + "Кейсы успешных клиентов", + "Отзывы и рекомендации", + "Демонстрация результатов" + ], + "contentRecommendations": "Публикуйте реальные истории успеха ваших клиентов. Используйте отзывы и рекомендации для повышения доверия. Покажите конкретные результаты и достижения.", + "priorityPlatforms": ["Instagram", "Telegram"] + }, + { + "weekNumber": 3, + "mainThemes": [ + "Образовательный контент", + "Советы и рекомендации", + "Индустриальные инсайты" + ], + "contentRecommendations": "Создавайте образовательный контент, который помогает вашей целевой аудитории. Делитесь экспертными знаниями и инсайтами индустрии. Позиционируйте себя как эксперта в области.", + "priorityPlatforms": ["LinkedIn", "Telegram"] + }, + { + "weekNumber": 4, + "mainThemes": [ + "Призыв к действию", + "Специальные предложения", + "Завершение кампании" + ], + "contentRecommendations": "Активно призывайте к действию. Предлагайте специальные условия или бонусы. Подводите итоги кампании и демонстрируйте достигнутые результаты.", + "priorityPlatforms": ["Instagram", "LinkedIn", "Telegram"] + } + ], + "postCalendar": [ + { + "publishDate": "2025-01-21T10:00:00", + "platform": "Instagram", + "contentType": "пост", + "theme": "Презентация продукта", + "postText": "🚀 Представляем наш новый продукт! Мы создали решение, которое поможет вашему бизнесу достичь новых высот. Узнайте больше о ключевых преимуществах в нашем профиле. #бизнес #инновации #продукт", + "hashtags": [ + "#бизнес", + "#инновации", + "#продукт", + "#маркетинг", + "#развитие" + ], + "publishTime": "10:00" + }, + { + "publishDate": "2025-01-21T14:00:00", + "platform": "LinkedIn", + "contentType": "пост", + "theme": "Ключевые преимущества", + "postText": "Наш продукт предлагает уникальные преимущества для B2B клиентов: быстрая интеграция, масштабируемость и надежная поддержка. Свяжитесь с нами для консультации. #B2B #технологии #бизнес", + "hashtags": [ + "#B2B", + "#технологии", + "#бизнес", + "#решения", + "#консультация" + ], + "publishTime": "14:00" + }, + { + "publishDate": "2025-01-22T18:00:00", + "platform": "Instagram", + "contentType": "сторис", + "theme": "Решение проблем клиентов", + "postText": "Знаете ли вы, что 80% компаний сталкиваются с проблемой X? Наш продукт решает эту проблему эффективно и быстро. Swipe up для деталей! 👆", + "hashtags": ["#решение", "#проблемы", "#эффективность"], + "publishTime": "18:00" + }, + { + "publishDate": "2025-01-23T10:00:00", + "platform": "Telegram", + "contentType": "пост", + "theme": "Кейс успешного клиента", + "postText": "📊 Кейс: Как компания X увеличила эффективность на 150% с помощью нашего продукта. Читайте полную историю в нашем канале. #кейс #успех #результаты", + "hashtags": ["#кейс", "#успех", "#результаты", "#бизнес"], + "publishTime": "10:00" + } + ] + } + } +} +``` + +#### Пример ответа (когда стратегия еще обрабатывается - 200 OK) + +```json +{ + "success": true, + "message": "Операция выполнена успешно", + "data": { + "strategyId": "507f1f77bcf86cd799439012", + "analysisId": "507f1f77bcf86cd799439011", + "status": "processing", + "createdAt": "2025-01-20T15:40:00", + "completedAt": null, + "durationWeeks": 4, + "priorityPlatforms": ["Instagram", "LinkedIn", "Telegram"], + "strategy": null + } +} +``` + +#### Статусы стратегии + +| Статус | Описание | +| ------------ | ------------------------------- | +| `queued` | Запрос в очереди на обработку | +| `processing` | Стратегия генерируется | +| `completed` | Стратегия завершена успешно | +| `failed` | Стратегия завершилась с ошибкой | + +#### Структура ответа + +| Поле | Тип | Описание | +| ---------------------------------------------------- | ------- | ------------------------------------------------------ | +| `success` | boolean | Флаг успешности операции | +| `message` | string | Сообщение о результате операции | +| `data.strategyId` | string | Уникальный идентификатор стратегии | +| `data.analysisId` | string | ID маркетингового анализа | +| `data.status` | string | Статус стратегии | +| `data.createdAt` | string | ISO 8601 дата/время создания | +| `data.completedAt` | string | ISO 8601 дата/время завершения (null если не завершен) | +| `data.durationWeeks` | integer | Длительность стратегии в неделях | +| `data.priorityPlatforms` | array | Список приоритетных платформ | +| `data.strategy` | object | Объект со стратегией (null если не завершен) | +| `data.strategy.weeklyPlans` | array | Список недельных планов | +| `data.strategy.weeklyPlans[].weekNumber` | integer | Номер недели (1, 2, 3, ...) | +| `data.strategy.weeklyPlans[].mainThemes` | array | Основные темы недели (массив строк) | +| `data.strategy.weeklyPlans[].contentRecommendations` | string | Рекомендации по контенту для недели | +| `data.strategy.weeklyPlans[].priorityPlatforms` | array | Приоритетные платформы для недели | +| `data.strategy.postCalendar` | array | Календарь постов | +| `data.strategy.postCalendar[].publishDate` | string | ISO 8601 дата/время публикации | +| `data.strategy.postCalendar[].platform` | string | Платформа для публикации | +| `data.strategy.postCalendar[].contentType` | string | Тип контента (пост, сторис, видео, баннер) | +| `data.strategy.postCalendar[].theme` | string | Тема поста | +| `data.strategy.postCalendar[].postText` | string | Полный текст поста (готовый к публикации) | +| `data.strategy.postCalendar[].hashtags` | array | Список хештегов (массив строк) | +| `data.strategy.postCalendar[].publishTime` | string | Время публикации в формате HH:mm | + +#### Пример ошибки (404 Not Found) + +```json +{ + "success": false, + "message": "Стратегия не найдена", + "error": { + "code": "NOT_FOUND", + "message": "Стратегия с указанным ID не найдена" + } +} +``` + +--- + +### 3. Получение стратегии по ID анализа + +**GET** `/api/marketing/analysis/{analysisId}/strategy` + +Возвращает стратегию, связанную с указанным маркетинговым анализом. + +#### Параметры пути + +| Параметр | Тип | Описание | +| ------------ | ------ | --------------------- | +| `analysisId` | string | Идентификатор анализа | + +#### Пример запроса + +``` +GET /api/marketing/analysis/507f1f77bcf86cd799439011/strategy +``` + +#### Пример ответа + +Структура ответа идентична эндпоинту `GET /api/marketing/strategy/{strategyId}` (см. выше). + +#### Пример ошибки (404 Not Found) + +```json +{ + "success": false, + "message": "Стратегия не найдена", + "error": { + "code": "NOT_FOUND", + "message": "Стратегия для указанного анализа не найдена" + } +} +``` + +--- + +## Обработка ошибок + +### Коды ошибок + +| Код | HTTP статус | Описание | +| ------------------------ | ----------- | ------------------------------- | +| `VALIDATION_ERROR` | 400 | Ошибка валидации входных данных | +| `INVALID_ANALYSIS` | 404 | Анализ не найден | +| `ANALYSIS_NOT_COMPLETED` | 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 generateStrategy(analysisId, options = {}) { + const params = new URLSearchParams(); + params.append('analysisId', analysisId); + + const response = await fetch( + `https://api.konturai.kz/api/marketing/strategy/generate?${params}`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + durationWeeks: options.durationWeeks || 4, + priorityPlatforms: options.priorityPlatforms || [], + }), + } + ); + + const result = await response.json(); + + if (result.success) { + console.log('Strategy ID:', result.data.strategyId); + return result.data.strategyId; + } else { + console.error('Error:', result.error); + throw new Error(result.error.message); + } +} +``` + +#### Проверка статуса и получение результата + +```javascript +async function getStrategyResult(strategyId) { + const response = await fetch( + `https://api.konturai.kz/api/marketing/strategy/${strategyId}` + ); + + const result = await response.json(); + + if (result.success) { + const { status, strategy } = result.data; + + if (status === 'completed' && strategy) { + console.log('Strategy completed!'); + console.log('Weekly plans:', strategy.weeklyPlans); + console.log('Post calendar:', strategy.postCalendar); + return strategy; + } else if (status === 'processing') { + console.log('Strategy is still generating...'); + return null; // Повторить запрос позже + } else if (status === 'failed') { + throw new Error('Strategy generation failed'); + } + } else { + throw new Error(result.error.message); + } +} +``` + +#### Получение стратегии по ID анализа + +```javascript +async function getStrategyByAnalysis(analysisId) { + const response = await fetch( + `https://api.konturai.kz/api/marketing/analysis/${analysisId}/strategy` + ); + + const result = await response.json(); + + if (result.success) { + return result.data; + } else { + throw new Error(result.error.message); + } +} +``` + +#### Полный цикл с polling + +```javascript +async function waitForStrategyCompletion( + strategyId, + maxAttempts = 60, + intervalMs = 10000 +) { + for (let i = 0; i < maxAttempts; i++) { + const result = await getStrategyResult(strategyId); + + if (result) { + return result; // Стратегия завершена + } + + // Ждем перед следующей проверкой + await new Promise((resolve) => setTimeout(resolve, intervalMs)); + } + + throw new Error('Strategy generation timeout'); +} + +// Использование +async function runFullStrategyGeneration() { + try { + // 1. Получаем завершенный анализ (предполагается, что analysisId уже есть) + const analysisId = '507f1f77bcf86cd799439011'; + + // 2. Запускаем генерацию стратегии + const strategyId = await generateStrategy(analysisId, { + durationWeeks: 4, + priorityPlatforms: ['Instagram', 'LinkedIn', 'Telegram'], + }); + + console.log(`Strategy generation started: ${strategyId}`); + + // 3. Ждем завершения (проверяем каждые 10 секунд, максимум 10 минут) + const strategy = await waitForStrategyCompletion(strategyId, 60, 10000); + + // 4. Используем результаты + console.log('Weekly plans:', strategy.weeklyPlans); + console.log('Post calendar:', strategy.postCalendar); + + // Отображаем календарь постов + strategy.postCalendar.forEach((post) => { + console.log(`${post.publishDate} - ${post.platform}: ${post.theme}`); + }); + + return strategy; + } catch (error) { + console.error('Error:', error); + } +} +``` + +### React примеры + +#### Компонент для отображения стратегии + +```javascript +import React, { useState, useEffect } from 'react'; + +function StrategyView({ analysisId }) { + const [strategy, setStrategy] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + async function loadStrategy() { + try { + // Сначала пытаемся получить существующую стратегию + let response = await fetch( + `/api/marketing/analysis/${analysisId}/strategy` + ); + let result = await response.json(); + + if (result.success && result.data.status === 'completed') { + setStrategy(result.data); + setLoading(false); + return; + } + + // Если стратегии нет или она еще обрабатывается, запускаем генерацию + if (!result.success || result.data.status === 'processing') { + // Запускаем генерацию + response = await fetch( + `/api/marketing/strategy/generate?analysisId=${analysisId}`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + durationWeeks: 4, + priorityPlatforms: ['Instagram', 'LinkedIn', 'Telegram'], + }), + } + ); + result = await response.json(); + + if (result.success) { + // Polling для получения результата + pollStrategy(result.data.strategyId); + } else { + setError(result.error.message); + setLoading(false); + } + } + } catch (err) { + setError(err.message); + setLoading(false); + } + } + + async function pollStrategy(strategyId) { + const maxAttempts = 60; + let attempts = 0; + + const interval = setInterval(async () => { + attempts++; + try { + const response = await fetch(`/api/marketing/strategy/${strategyId}`); + const result = await response.json(); + + if (result.success) { + if (result.data.status === 'completed') { + setStrategy(result.data); + setLoading(false); + clearInterval(interval); + } else if (result.data.status === 'failed') { + setError('Strategy generation failed'); + setLoading(false); + clearInterval(interval); + } + } + + if (attempts >= maxAttempts) { + setError('Strategy generation timeout'); + setLoading(false); + clearInterval(interval); + } + } catch (err) { + setError(err.message); + setLoading(false); + clearInterval(interval); + } + }, 10000); // Проверяем каждые 10 секунд + } + + if (analysisId) { + loadStrategy(); + } + }, [analysisId]); + + if (loading) { + return
Генерация стратегии...
; + } + + if (error) { + return
Ошибка: {error}
; + } + + if (!strategy || !strategy.strategy) { + return
Стратегия не найдена
; + } + + return ( +
+

Стратегия продвижения

+ + {/* Недельный план */} +
+

Недельный план

+ {strategy.strategy.weeklyPlans.map((plan) => ( +
+

Неделя {plan.weekNumber}

+
+ Темы: +
    + {plan.mainThemes.map((theme, idx) => ( +
  • {theme}
  • + ))} +
+
+
+ Рекомендации: +

{plan.contentRecommendations}

+
+
+ Платформы: + {plan.priorityPlatforms.join(', ')} +
+
+ ))} +
+ + {/* Календарь постов */} +
+

Календарь постов

+
+ {strategy.strategy.postCalendar.map((post, idx) => ( +
+
+ + {new Date(post.publishDate).toLocaleDateString('ru-RU')} + + {post.publishTime} + {post.platform} + {post.contentType} +
+
+ Тема: {post.theme} +
+
{post.postText}
+
+ {post.hashtags.map((tag, tagIdx) => ( + + {tag} + + ))} +
+
+ ))} +
+
+
+ ); +} + +export default StrategyView; +``` + +#### Компонент для отображения календаря постов + +```javascript +import React from 'react'; + +function PostCalendar({ postCalendar }) { + // Группируем посты по датам + const postsByDate = postCalendar.reduce((acc, post) => { + const date = new Date(post.publishDate).toLocaleDateString('ru-RU'); + if (!acc[date]) { + acc[date] = []; + } + acc[date].push(post); + return acc; + }, {}); + + return ( +
+

Календарь публикаций

+ {Object.entries(postsByDate).map(([date, posts]) => ( +
+

{date}

+ {posts.map((post, idx) => ( +
+
+ {post.platform} + {post.contentType} + {post.publishTime} +
+
+
{post.theme}
+

{post.postText}

+
+ {post.hashtags.map((tag, tagIdx) => ( + + {tag} + + ))} +
+
+
+ ))} +
+ ))} +
+ ); +} + +export default PostCalendar; +``` + +### Vue.js примеры + +```javascript +// composable для работы со стратегией +export function useMarketingStrategy() { + const baseUrl = ''; + + const generateStrategy = async (analysisId, options = {}) => { + const params = new URLSearchParams(); + params.append('analysisId', analysisId); + + const response = await fetch( + `${baseUrl}/api/marketing/strategy/generate?${params}`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + durationWeeks: options.durationWeeks || 4, + priorityPlatforms: options.priorityPlatforms || [], + }), + } + ); + + const result = await response.json(); + if (!result.success) { + throw new Error(result.error.message); + } + + return result.data; + }; + + const getStrategy = async (strategyId) => { + const response = await fetch( + `${baseUrl}/api/marketing/strategy/${strategyId}` + ); + const result = await response.json(); + if (!result.success) { + throw new Error(result.error.message); + } + return result.data; + }; + + const getStrategyByAnalysis = async (analysisId) => { + const response = await fetch( + `${baseUrl}/api/marketing/analysis/${analysisId}/strategy` + ); + const result = await response.json(); + if (!result.success) { + throw new Error(result.error.message); + } + return result.data; + }; + + return { + generateStrategy, + getStrategy, + getStrategyByAnalysis, + }; +} +``` + +--- + +## Рекомендации по интеграции + +### 1. Polling стратегия + +Рекомендуется проверять статус стратегии каждые 10-15 секунд. Максимальное время ожидания - 5-7 минут. + +### 2. Обработка ошибок + +Всегда проверяйте поле `success` в ответе и обрабатывайте ошибки соответствующим образом. Особое внимание уделите случаям, когда анализ еще не завершен. + +### 3. Валидация на клиенте + +Перед отправкой запроса рекомендуется валидировать данные на клиенте: + +- Проверка наличия `analysisId` +- Проверка диапазона `durationWeeks` (1-12) +- Проверка формата массива `priorityPlatforms` + +### 4. UX рекомендации + +- Показывайте индикатор загрузки во время генерации стратегии +- Отображайте примерное время завершения (3-5 минут) +- Предоставьте возможность отменить ожидание и проверить результат позже +- Сохраняйте `strategyId` для последующей проверки статуса +- Отображайте календарь постов в удобном формате (календарь, список, таблица) +- Позвольте пользователю копировать текст постов и хештеги + +### 5. Кэширование + +После получения результатов можно кэшировать их локально, используя `strategyId` или `analysisId` как ключ. + +### 6. Экспорт данных + +Рассмотрите возможность экспорта стратегии в различных форматах: + +- CSV для календаря постов +- PDF для полной стратегии +- iCal для импорта в календарные приложения + +--- + +## Примечания + +1. **Формат даты**: Все даты возвращаются в формате ISO 8601 без timezone (LocalDateTime) +2. **Идентификаторы**: Используются MongoDB ObjectId (24 символа hex) +3. **Асинхронность**: Генерация стратегии выполняется асинхронно, не блокируя запрос +4. **Таймауты**: Рекомендуется устанавливать таймаут на запросы (минимум 30 секунд для запуска генерации) +5. **Зависимость от анализа**: Стратегия может быть сгенерирована только для завершенного анализа +6. **Повторная генерация**: Если стратегия уже существует для анализа, возвращается существующая стратегия +7. **Платформы**: Поддерживаются все популярные платформы: Instagram, Facebook, LinkedIn, Telegram, TikTok, YouTube, 21MC и другие + +--- + +## Поддержка + +При возникновении проблем с API обращайтесь в техническую поддержку с указанием: + +- `strategyId` (если есть) +- `analysisId` +- Время запроса +- Описание проблемы +- Код ошибки (если есть) diff --git a/src/main/java/kz/konturai/parser/controller/MarketingController.java b/src/main/java/kz/konturai/parser/controller/MarketingController.java index fc5575d..4ddce17 100644 --- a/src/main/java/kz/konturai/parser/controller/MarketingController.java +++ b/src/main/java/kz/konturai/parser/controller/MarketingController.java @@ -2,7 +2,9 @@ package kz.konturai.parser.controller; import kz.konturai.parser.dto.*; import kz.konturai.parser.model.MarketingAnalysis; +import kz.konturai.parser.model.MarketingStrategy; import kz.konturai.parser.service.MarketingAnalysisService; +import kz.konturai.parser.service.MarketingStrategyService; import kz.konturai.parser.service.MinIOService; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -22,10 +24,15 @@ import java.util.Optional; public class MarketingController { private final MarketingAnalysisService marketingAnalysisService; + private final MarketingStrategyService marketingStrategyService; private final MinIOService minIOService; - public MarketingController(MarketingAnalysisService marketingAnalysisService, MinIOService minIOService) { + public MarketingController( + MarketingAnalysisService marketingAnalysisService, + MarketingStrategyService marketingStrategyService, + MinIOService minIOService) { this.marketingAnalysisService = marketingAnalysisService; + this.marketingStrategyService = marketingStrategyService; this.minIOService = minIOService; } @@ -97,6 +104,76 @@ public class MarketingController { } } + @PostMapping("/strategy/generate") + public ResponseEntity generateStrategy( + @RequestParam String analysisId, + @Valid @RequestBody(required = false) MarketingStrategyRequest request) { + + if (request == null) { + request = new MarketingStrategyRequest(); + } + + try { + MarketingStrategy strategy = marketingStrategyService.generateStrategy(analysisId, request); + + 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)); + } + } + + @GetMapping("/strategy/{strategyId}") + public ResponseEntity getStrategy(@PathVariable String strategyId) { + MarketingStrategyResponse result = marketingStrategyService.getStrategyResult(strategyId); + + 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}/strategy") + public ResponseEntity getStrategyByAnalysis(@PathVariable String analysisId) { + 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)); + } + @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity> handleValidationException( MethodArgumentNotValidException ex) { diff --git a/src/main/java/kz/konturai/parser/dto/MarketingStrategyRequest.java b/src/main/java/kz/konturai/parser/dto/MarketingStrategyRequest.java new file mode 100644 index 0000000..8b8950f --- /dev/null +++ b/src/main/java/kz/konturai/parser/dto/MarketingStrategyRequest.java @@ -0,0 +1,39 @@ +package kz.konturai.parser.dto; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Max; +import java.util.List; + +public class MarketingStrategyRequest { + + @Min(value = 1, message = "Длительность стратегии должна быть не менее 1 недели") + @Max(value = 12, message = "Длительность стратегии должна быть не более 12 недель") + private Integer durationWeeks; + + private List priorityPlatforms; + + public MarketingStrategyRequest() { + } + + public MarketingStrategyRequest(Integer durationWeeks, List priorityPlatforms) { + this.durationWeeks = durationWeeks; + this.priorityPlatforms = priorityPlatforms; + } + + public Integer getDurationWeeks() { + return durationWeeks; + } + + public void setDurationWeeks(Integer durationWeeks) { + this.durationWeeks = durationWeeks; + } + + public List getPriorityPlatforms() { + return priorityPlatforms; + } + + public void setPriorityPlatforms(List priorityPlatforms) { + this.priorityPlatforms = priorityPlatforms; + } +} + diff --git a/src/main/java/kz/konturai/parser/dto/MarketingStrategyResponse.java b/src/main/java/kz/konturai/parser/dto/MarketingStrategyResponse.java new file mode 100644 index 0000000..1fef964 --- /dev/null +++ b/src/main/java/kz/konturai/parser/dto/MarketingStrategyResponse.java @@ -0,0 +1,251 @@ +package kz.konturai.parser.dto; + +import java.time.LocalDateTime; +import java.util.List; + +public class MarketingStrategyResponse { + private String strategyId; + private String analysisId; + private String status; + private LocalDateTime createdAt; + private LocalDateTime completedAt; + private Integer durationWeeks; + private List priorityPlatforms; + private StrategyContent strategy; + + public MarketingStrategyResponse() { + } + + public MarketingStrategyResponse(String strategyId, String analysisId, String status, LocalDateTime createdAt, LocalDateTime completedAt, Integer durationWeeks, List priorityPlatforms, StrategyContent strategy) { + this.strategyId = strategyId; + this.analysisId = analysisId; + this.status = status; + this.createdAt = createdAt; + this.completedAt = completedAt; + this.durationWeeks = durationWeeks; + this.priorityPlatforms = priorityPlatforms; + this.strategy = strategy; + } + + public String getStrategyId() { + return strategyId; + } + + public void setStrategyId(String strategyId) { + this.strategyId = strategyId; + } + + 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 Integer getDurationWeeks() { + return durationWeeks; + } + + public void setDurationWeeks(Integer durationWeeks) { + this.durationWeeks = durationWeeks; + } + + public List getPriorityPlatforms() { + return priorityPlatforms; + } + + public void setPriorityPlatforms(List priorityPlatforms) { + this.priorityPlatforms = priorityPlatforms; + } + + public StrategyContent getStrategy() { + return strategy; + } + + public void setStrategy(StrategyContent strategy) { + this.strategy = strategy; + } + + public static class StrategyContent { + private List weeklyPlans; + private List postCalendar; + + public StrategyContent() { + } + + public StrategyContent(List weeklyPlans, List postCalendar) { + this.weeklyPlans = weeklyPlans; + this.postCalendar = postCalendar; + } + + public List getWeeklyPlans() { + return weeklyPlans; + } + + public void setWeeklyPlans(List weeklyPlans) { + this.weeklyPlans = weeklyPlans; + } + + public List getPostCalendar() { + return postCalendar; + } + + public void setPostCalendar(List postCalendar) { + this.postCalendar = postCalendar; + } + } + + public static class WeeklyPlan { + private Integer weekNumber; + private List mainThemes; + private String contentRecommendations; + private List priorityPlatforms; + + public WeeklyPlan() { + } + + public WeeklyPlan(Integer weekNumber, List mainThemes, String contentRecommendations, List priorityPlatforms) { + this.weekNumber = weekNumber; + this.mainThemes = mainThemes; + this.contentRecommendations = contentRecommendations; + this.priorityPlatforms = priorityPlatforms; + } + + public Integer getWeekNumber() { + return weekNumber; + } + + public void setWeekNumber(Integer weekNumber) { + this.weekNumber = weekNumber; + } + + public List getMainThemes() { + return mainThemes; + } + + public void setMainThemes(List mainThemes) { + this.mainThemes = mainThemes; + } + + public String getContentRecommendations() { + return contentRecommendations; + } + + public void setContentRecommendations(String contentRecommendations) { + this.contentRecommendations = contentRecommendations; + } + + public List getPriorityPlatforms() { + return priorityPlatforms; + } + + public void setPriorityPlatforms(List priorityPlatforms) { + this.priorityPlatforms = priorityPlatforms; + } + } + + public static class PostCalendarItem { + private LocalDateTime publishDate; + private String platform; + private String contentType; + private String theme; + private String postText; + private List hashtags; + private String publishTime; + + public PostCalendarItem() { + } + + public PostCalendarItem(LocalDateTime publishDate, String platform, String contentType, String theme, String postText, List hashtags, String publishTime) { + this.publishDate = publishDate; + this.platform = platform; + this.contentType = contentType; + this.theme = theme; + this.postText = postText; + this.hashtags = hashtags; + this.publishTime = publishTime; + } + + public LocalDateTime getPublishDate() { + return publishDate; + } + + public void setPublishDate(LocalDateTime publishDate) { + this.publishDate = publishDate; + } + + public String getPlatform() { + return platform; + } + + public void setPlatform(String platform) { + this.platform = platform; + } + + public String getContentType() { + return contentType; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public String getTheme() { + return theme; + } + + public void setTheme(String theme) { + this.theme = theme; + } + + public String getPostText() { + return postText; + } + + public void setPostText(String postText) { + this.postText = postText; + } + + public List getHashtags() { + return hashtags; + } + + public void setHashtags(List hashtags) { + this.hashtags = hashtags; + } + + public String getPublishTime() { + return publishTime; + } + + public void setPublishTime(String publishTime) { + this.publishTime = publishTime; + } + } +} + diff --git a/src/main/java/kz/konturai/parser/model/MarketingStrategy.java b/src/main/java/kz/konturai/parser/model/MarketingStrategy.java new file mode 100644 index 0000000..b8809ec --- /dev/null +++ b/src/main/java/kz/konturai/parser/model/MarketingStrategy.java @@ -0,0 +1,282 @@ +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.List; +import java.util.Map; + +@Document(collection = "marketing_strategy") +public class MarketingStrategy { + + @Id + private String id; + + @Field("analysis_id") + private String analysisId; + + @Field("status") + private String status; // queued, processing, completed, failed + + @Field("duration_weeks") + private Integer durationWeeks; + + @Field("priority_platforms") + private List priorityPlatforms; + + @Field("weekly_plans") + private List weeklyPlans; + + @Field("post_calendar") + private List postCalendar; + + @Field("created_at") + private LocalDateTime createdAt; + + @Field("completed_at") + private LocalDateTime completedAt; + + @Field("strategy_data") + private Map strategyData; // JSON data with full strategy content + + public MarketingStrategy() { + this.createdAt = LocalDateTime.now(); + this.status = "queued"; + } + + public MarketingStrategy(String analysisId) { + this(); + this.analysisId = analysisId; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + 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 Integer getDurationWeeks() { + return durationWeeks; + } + + public void setDurationWeeks(Integer durationWeeks) { + this.durationWeeks = durationWeeks; + } + + public List getPriorityPlatforms() { + return priorityPlatforms; + } + + public void setPriorityPlatforms(List priorityPlatforms) { + this.priorityPlatforms = priorityPlatforms; + } + + public List getWeeklyPlans() { + return weeklyPlans; + } + + public void setWeeklyPlans(List weeklyPlans) { + this.weeklyPlans = weeklyPlans; + } + + public List getPostCalendar() { + return postCalendar; + } + + public void setPostCalendar(List postCalendar) { + this.postCalendar = postCalendar; + } + + 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 getStrategyData() { + return strategyData; + } + + public void setStrategyData(Map strategyData) { + this.strategyData = strategyData; + } + + public static class WeeklyPlan { + @Field("week_number") + private Integer weekNumber; + + @Field("main_themes") + private List mainThemes; + + @Field("content_recommendations") + private String contentRecommendations; + + @Field("priority_platforms") + private List priorityPlatforms; + + public WeeklyPlan() { + } + + public WeeklyPlan(Integer weekNumber, List mainThemes, String contentRecommendations, List priorityPlatforms) { + this.weekNumber = weekNumber; + this.mainThemes = mainThemes; + this.contentRecommendations = contentRecommendations; + this.priorityPlatforms = priorityPlatforms; + } + + public Integer getWeekNumber() { + return weekNumber; + } + + public void setWeekNumber(Integer weekNumber) { + this.weekNumber = weekNumber; + } + + public List getMainThemes() { + return mainThemes; + } + + public void setMainThemes(List mainThemes) { + this.mainThemes = mainThemes; + } + + public String getContentRecommendations() { + return contentRecommendations; + } + + public void setContentRecommendations(String contentRecommendations) { + this.contentRecommendations = contentRecommendations; + } + + public List getPriorityPlatforms() { + return priorityPlatforms; + } + + public void setPriorityPlatforms(List priorityPlatforms) { + this.priorityPlatforms = priorityPlatforms; + } + } + + public static class PostCalendarItem { + @Field("publish_date") + private LocalDateTime publishDate; + + @Field("platform") + private String platform; + + @Field("content_type") + private String contentType; // пост, сторис, видео, баннер + + @Field("theme") + private String theme; + + @Field("post_text") + private String postText; + + @Field("hashtags") + private List hashtags; + + @Field("publish_time") + private String publishTime; // время публикации в формате HH:mm + + public PostCalendarItem() { + } + + public PostCalendarItem(LocalDateTime publishDate, String platform, String contentType, String theme, String postText, List hashtags, String publishTime) { + this.publishDate = publishDate; + this.platform = platform; + this.contentType = contentType; + this.theme = theme; + this.postText = postText; + this.hashtags = hashtags; + this.publishTime = publishTime; + } + + public LocalDateTime getPublishDate() { + return publishDate; + } + + public void setPublishDate(LocalDateTime publishDate) { + this.publishDate = publishDate; + } + + public String getPlatform() { + return platform; + } + + public void setPlatform(String platform) { + this.platform = platform; + } + + public String getContentType() { + return contentType; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public String getTheme() { + return theme; + } + + public void setTheme(String theme) { + this.theme = theme; + } + + public String getPostText() { + return postText; + } + + public void setPostText(String postText) { + this.postText = postText; + } + + public List getHashtags() { + return hashtags; + } + + public void setHashtags(List hashtags) { + this.hashtags = hashtags; + } + + public String getPublishTime() { + return publishTime; + } + + public void setPublishTime(String publishTime) { + this.publishTime = publishTime; + } + } +} + diff --git a/src/main/java/kz/konturai/parser/repository/MarketingStrategyRepository.java b/src/main/java/kz/konturai/parser/repository/MarketingStrategyRepository.java new file mode 100644 index 0000000..ab7bcb3 --- /dev/null +++ b/src/main/java/kz/konturai/parser/repository/MarketingStrategyRepository.java @@ -0,0 +1,14 @@ +package kz.konturai.parser.repository; + +import kz.konturai.parser.model.MarketingStrategy; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface MarketingStrategyRepository extends MongoRepository { + Optional findById(String id); + Optional findByAnalysisId(String analysisId); +} + diff --git a/src/main/java/kz/konturai/parser/service/MarketingStrategyService.java b/src/main/java/kz/konturai/parser/service/MarketingStrategyService.java new file mode 100644 index 0000000..bfa89ba --- /dev/null +++ b/src/main/java/kz/konturai/parser/service/MarketingStrategyService.java @@ -0,0 +1,501 @@ +package kz.konturai.parser.service; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import kz.konturai.parser.dto.MarketingAnalysisResult; +import kz.konturai.parser.dto.MarketingStrategyRequest; +import kz.konturai.parser.dto.MarketingStrategyResponse; +import kz.konturai.parser.model.MarketingStrategy; +import kz.konturai.parser.repository.MarketingStrategyRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.stream.Collectors; + +@Service +public class MarketingStrategyService { + + private static final Logger logger = LoggerFactory.getLogger(MarketingStrategyService.class); + + private final MarketingStrategyRepository repository; + private final MarketingAnalysisService marketingAnalysisService; + private final OpenAIAnalyticsService openAIAnalyticsService; + private final ObjectMapper objectMapper = new ObjectMapper(); + + public MarketingStrategyService( + MarketingStrategyRepository repository, + MarketingAnalysisService marketingAnalysisService, + OpenAIAnalyticsService openAIAnalyticsService) { + this.repository = repository; + this.marketingAnalysisService = marketingAnalysisService; + this.openAIAnalyticsService = openAIAnalyticsService; + } + + public MarketingStrategy generateStrategy(String analysisId, MarketingStrategyRequest request) { + // Check if analysis exists and is completed + MarketingAnalysisResult analysisResult = marketingAnalysisService.getAnalysisResult(analysisId); + if (analysisResult == null) { + throw new IllegalArgumentException("Анализ с ID " + analysisId + " не найден"); + } + if (!"completed".equals(analysisResult.getStatus())) { + throw new IllegalStateException("Анализ еще не завершен. Статус: " + analysisResult.getStatus()); + } + + // Check if strategy already exists + Optional existingStrategy = repository.findByAnalysisId(analysisId); + if (existingStrategy.isPresent()) { + logger.info("Стратегия для анализа {} уже существует: {}", analysisId, existingStrategy.get().getId()); + return existingStrategy.get(); + } + + // Create new strategy + MarketingStrategy strategy = new MarketingStrategy(analysisId); + strategy.setDurationWeeks(request.getDurationWeeks() != null ? request.getDurationWeeks() : 4); + strategy.setPriorityPlatforms(request.getPriorityPlatforms()); + strategy.setStatus("queued"); + strategy = repository.save(strategy); + + logger.info("Marketing strategy created with ID: {}", strategy.getId()); + + // Start async processing + processStrategyGeneration(strategy.getId(), analysisId, analysisResult); + + return strategy; + } + + @Async("reportGenerationExecutor") + public void processStrategyGeneration(String strategyId, String analysisId, MarketingAnalysisResult analysisResult) { + try { + Optional optStrategy = repository.findById(strategyId); + if (optStrategy.isEmpty()) { + logger.error("Marketing strategy not found: {}", strategyId); + return; + } + + MarketingStrategy strategy = optStrategy.get(); + strategy.setStatus("processing"); + repository.save(strategy); + + logger.info("Starting marketing strategy generation for ID: {}", strategyId); + + // Build context from analysis + String context = buildContextFromAnalysis(analysisResult); + + // Generate weekly plans + List weeklyPlans = generateWeeklyPlans( + context, strategy.getDurationWeeks(), strategy.getPriorityPlatforms()); + + // Generate post calendar + List postCalendar = generatePostCalendar( + context, strategy.getDurationWeeks(), strategy.getPriorityPlatforms(), weeklyPlans); + + // Update strategy + strategy.setStatus("completed"); + strategy.setCompletedAt(LocalDateTime.now()); + strategy.setWeeklyPlans(weeklyPlans); + strategy.setPostCalendar(postCalendar); + + // Store full strategy data as JSON + Map strategyData = new HashMap<>(); + strategyData.put("weeklyPlans", weeklyPlans); + strategyData.put("postCalendar", postCalendar); + strategy.setStrategyData(strategyData); + + repository.save(strategy); + + logger.info("Marketing strategy generation completed successfully for ID: {}", strategyId); + + } catch (Exception e) { + logger.error("Error processing marketing strategy {}: {}", strategyId, e.getMessage(), e); + try { + Optional optStrategy = repository.findById(strategyId); + if (optStrategy.isPresent()) { + MarketingStrategy strategy = optStrategy.get(); + strategy.setStatus("failed"); + repository.save(strategy); + } + } catch (Exception saveError) { + logger.error("Failed to update strategy status to failed: {}", saveError.getMessage()); + } + } + } + + private String buildContextFromAnalysis(MarketingAnalysisResult analysisResult) { + StringBuilder context = new StringBuilder(); + + if (analysisResult.getReport() != null) { + MarketingAnalysisResult.MarketingReport report = analysisResult.getReport(); + + if (report.getSummary() != null) { + context.append("Резюме анализа: ").append(report.getSummary()).append("\n\n"); + } + + if (report.getTargetAudience() != null) { + MarketingAnalysisResult.TargetAudience audience = report.getTargetAudience(); + context.append("Целевая аудитория: ").append(audience.getDescription()).append("\n"); + if (audience.getChannels() != null && !audience.getChannels().isEmpty()) { + context.append("Рекомендуемые каналы: ").append(String.join(", ", audience.getChannels())).append("\n"); + } + context.append("\n"); + } + + if (report.getRecommendations() != null && !report.getRecommendations().isEmpty()) { + context.append("Рекомендации:\n"); + for (String rec : report.getRecommendations()) { + context.append("- ").append(rec).append("\n"); + } + context.append("\n"); + } + + if (report.getStrategy() != null) { + MarketingAnalysisResult.Strategy strategy = report.getStrategy(); + context.append("Базовая стратегия:\n"); + if (strategy.getDuration() != null) { + context.append("Длительность: ").append(strategy.getDuration()).append("\n"); + } + if (strategy.getChannels() != null && !strategy.getChannels().isEmpty()) { + context.append("Каналы: ").append(String.join(", ", strategy.getChannels())).append("\n"); + } + if (strategy.getContentTypes() != null && !strategy.getContentTypes().isEmpty()) { + context.append("Типы контента: ").append(String.join(", ", strategy.getContentTypes())).append("\n"); + } + } + } + + return context.toString(); + } + + private List generateWeeklyPlans( + String context, Integer durationWeeks, List priorityPlatforms) { + try { + String platformsStr = priorityPlatforms != null && !priorityPlatforms.isEmpty() + ? String.join(", ", priorityPlatforms) + : "Instagram, Facebook, LinkedIn, Telegram, TikTok, YouTube"; + + String prompt = String.format( + "На основе следующего маркетингового анализа создай детальный недельный план продвижения на %d недель.\n\n" + + "Требования:\n" + + "1. Создай план для каждой недели отдельно\n" + + "2. Для каждой недели укажи:\n" + + " - Основные темы недели (3-5 тем)\n" + + " - Рекомендации по контенту (1-2 абзаца)\n" + + " - Приоритетные платформы для этой недели\n" + + "3. Платформы для использования: %s\n" + + "4. Ответ должен быть структурированным и применимым на практике\n" + + "5. Ответ должен быть на русском языке\n\n" + + "Верни ответ в формате JSON со следующей структурой:\n" + + "{\n" + + " \"weeklyPlans\": [\n" + + " {\n" + + " \"weekNumber\": 1,\n" + + " \"mainThemes\": [\"тема1\", \"тема2\", \"тема3\"],\n" + + " \"contentRecommendations\": \"рекомендации по контенту\",\n" + + " \"priorityPlatforms\": [\"Instagram\", \"Facebook\"]\n" + + " }\n" + + " ]\n" + + "}\n\n" + + "Маркетинговый анализ:\n%s", + durationWeeks, platformsStr, context); + + String response = openAIAnalyticsService.generateWithInstruction(context, prompt, "ru"); + if (response == null || response.trim().isEmpty()) { + logger.warn("Failed to generate weekly plans, using default"); + return generateDefaultWeeklyPlans(durationWeeks, priorityPlatforms); + } + + // Extract JSON from response + String jsonStr = extractJsonFromResponse(response); + if (jsonStr == null) { + logger.warn("Could not extract JSON from weekly plans response, using default"); + return generateDefaultWeeklyPlans(durationWeeks, priorityPlatforms); + } + + // Parse JSON + Map jsonMap = objectMapper.readValue(jsonStr, new TypeReference>() {}); + @SuppressWarnings("unchecked") + List> weeklyPlansList = (List>) jsonMap.get("weeklyPlans"); + + if (weeklyPlansList == null || weeklyPlansList.isEmpty()) { + logger.warn("Weekly plans list is empty, using default"); + return generateDefaultWeeklyPlans(durationWeeks, priorityPlatforms); + } + + List plans = new ArrayList<>(); + for (Map planMap : weeklyPlansList) { + MarketingStrategy.WeeklyPlan plan = new MarketingStrategy.WeeklyPlan(); + plan.setWeekNumber(((Number) planMap.get("weekNumber")).intValue()); + + @SuppressWarnings("unchecked") + List themes = (List) planMap.get("mainThemes"); + plan.setMainThemes(themes != null ? themes : new ArrayList<>()); + + plan.setContentRecommendations((String) planMap.get("contentRecommendations")); + + @SuppressWarnings("unchecked") + List platforms = (List) planMap.get("priorityPlatforms"); + plan.setPriorityPlatforms(platforms != null ? platforms : new ArrayList<>()); + + plans.add(plan); + } + + return plans; + + } catch (Exception e) { + logger.error("Error generating weekly plans: {}", e.getMessage(), e); + return generateDefaultWeeklyPlans(durationWeeks, priorityPlatforms); + } + } + + private List generatePostCalendar( + String context, Integer durationWeeks, List priorityPlatforms, + List weeklyPlans) { + try { + String platformsStr = priorityPlatforms != null && !priorityPlatforms.isEmpty() + ? String.join(", ", priorityPlatforms) + : "Instagram, Facebook, LinkedIn, Telegram, TikTok, YouTube"; + + // Build weekly plans summary for context + String weeklyPlansSummary = ""; + if (weeklyPlans != null && !weeklyPlans.isEmpty()) { + weeklyPlansSummary = weeklyPlans.stream() + .map(plan -> String.format("Неделя %d: %s", plan.getWeekNumber(), + plan.getMainThemes() != null ? String.join(", ", plan.getMainThemes()) : "")) + .collect(Collectors.joining("\n")); + } + + LocalDateTime startDate = LocalDateTime.now().plusDays(1); // Start from tomorrow + + String prompt = String.format( + "На основе следующего маркетингового анализа и недельного плана создай детальный календарь постов на %d недель.\n\n" + + "Требования:\n" + + "1. Создай конкретные посты для публикации\n" + + "2. Для каждого поста укажи:\n" + + " - Дату публикации (в формате YYYY-MM-DD)\n" + + " - Время публикации (в формате HH:mm, например 10:00, 14:00, 18:00)\n" + + " - Платформу (одну из: %s)\n" + + " - Тип контента (пост, сторис, видео, баннер)\n" + + " - Тему поста\n" + + " - Полный текст поста (готовый к публикации, 100-300 символов)\n" + + " - Хештеги (5-10 релевантных хештегов)\n" + + "3. Распредели посты равномерно по неделям\n" + + "4. Рекомендуемое количество постов: 3-5 постов в неделю\n" + + "5. Чередуй платформы и типы контента\n" + + "6. Ответ должен быть на русском языке\n\n" + + "Верни ответ в формате JSON со следующей структурой:\n" + + "{\n" + + " \"postCalendar\": [\n" + + " {\n" + + " \"publishDate\": \"2024-01-15T10:00:00\",\n" + + " \"platform\": \"Instagram\",\n" + + " \"contentType\": \"пост\",\n" + + " \"theme\": \"тема поста\",\n" + + " \"postText\": \"полный текст поста\",\n" + + " \"hashtags\": [\"#хештег1\", \"#хештег2\"],\n" + + " \"publishTime\": \"10:00\"\n" + + " }\n" + + " ]\n" + + "}\n\n" + + "Начальная дата: %s\n\n" + + "Недельный план:\n%s\n\n" + + "Маркетинговый анализ:\n%s", + durationWeeks, platformsStr, startDate.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), weeklyPlansSummary, context); + + String response = openAIAnalyticsService.generateWithInstruction(context, prompt, "ru"); + if (response == null || response.trim().isEmpty()) { + logger.warn("Failed to generate post calendar, using default"); + return generateDefaultPostCalendar(durationWeeks, priorityPlatforms, startDate); + } + + // Extract JSON from response + String jsonStr = extractJsonFromResponse(response); + if (jsonStr == null) { + logger.warn("Could not extract JSON from post calendar response, using default"); + return generateDefaultPostCalendar(durationWeeks, priorityPlatforms, startDate); + } + + // Parse JSON + Map jsonMap = objectMapper.readValue(jsonStr, new TypeReference>() {}); + @SuppressWarnings("unchecked") + List> postCalendarList = (List>) jsonMap.get("postCalendar"); + + if (postCalendarList == null || postCalendarList.isEmpty()) { + logger.warn("Post calendar list is empty, using default"); + return generateDefaultPostCalendar(durationWeeks, priorityPlatforms, startDate); + } + + List calendar = new ArrayList<>(); + for (Map postMap : postCalendarList) { + MarketingStrategy.PostCalendarItem item = new MarketingStrategy.PostCalendarItem(); + + // Parse date + String dateStr = (String) postMap.get("publishDate"); + if (dateStr != null) { + try { + LocalDateTime publishDate = LocalDateTime.parse(dateStr, DateTimeFormatter.ISO_LOCAL_DATE_TIME); + item.setPublishDate(publishDate); + } catch (Exception e) { + logger.warn("Failed to parse date: {}", dateStr); + item.setPublishDate(startDate.plusDays(calendar.size())); + } + } else { + item.setPublishDate(startDate.plusDays(calendar.size())); + } + + item.setPlatform((String) postMap.get("platform")); + item.setContentType((String) postMap.get("contentType")); + item.setTheme((String) postMap.get("theme")); + item.setPostText((String) postMap.get("postText")); + + @SuppressWarnings("unchecked") + List hashtags = (List) postMap.get("hashtags"); + item.setHashtags(hashtags != null ? hashtags : new ArrayList<>()); + + item.setPublishTime((String) postMap.get("publishTime")); + + calendar.add(item); + } + + return calendar; + + } catch (Exception e) { + logger.error("Error generating post calendar: {}", e.getMessage(), e); + return generateDefaultPostCalendar(durationWeeks, priorityPlatforms, LocalDateTime.now().plusDays(1)); + } + } + + private String extractJsonFromResponse(String response) { + if (response == null) { + return null; + } + + // Try to find JSON object in response + int startIdx = response.indexOf("{"); + int endIdx = response.lastIndexOf("}"); + + if (startIdx >= 0 && endIdx > startIdx) { + return response.substring(startIdx, endIdx + 1); + } + + // Try to find JSON array + startIdx = response.indexOf("["); + endIdx = response.lastIndexOf("]"); + + if (startIdx >= 0 && endIdx > startIdx) { + return "{\"data\":" + response.substring(startIdx, endIdx + 1) + "}"; + } + + return null; + } + + private List generateDefaultWeeklyPlans( + Integer durationWeeks, List priorityPlatforms) { + List plans = new ArrayList<>(); + List platforms = priorityPlatforms != null && !priorityPlatforms.isEmpty() + ? priorityPlatforms + : Arrays.asList("Instagram", "Facebook", "LinkedIn"); + + for (int i = 1; i <= durationWeeks; i++) { + MarketingStrategy.WeeklyPlan plan = new MarketingStrategy.WeeklyPlan(); + plan.setWeekNumber(i); + plan.setMainThemes(Arrays.asList("Презентация продукта", "Преимущества", "Отзывы клиентов")); + plan.setContentRecommendations("Создавайте контент, который демонстрирует ценность продукта для целевой аудитории."); + plan.setPriorityPlatforms(platforms); + plans.add(plan); + } + + return plans; + } + + private List generateDefaultPostCalendar( + Integer durationWeeks, List priorityPlatforms, LocalDateTime startDate) { + List calendar = new ArrayList<>(); + List platforms = priorityPlatforms != null && !priorityPlatforms.isEmpty() + ? priorityPlatforms + : Arrays.asList("Instagram", "Facebook", "LinkedIn"); + List contentTypes = Arrays.asList("пост", "сторис", "видео"); + List times = Arrays.asList("10:00", "14:00", "18:00"); + + int postCount = durationWeeks * 3; // 3 posts per week + for (int i = 0; i < postCount; i++) { + MarketingStrategy.PostCalendarItem item = new MarketingStrategy.PostCalendarItem(); + item.setPublishDate(startDate.plusDays(i * 2)); // Every 2 days + item.setPlatform(platforms.get(i % platforms.size())); + item.setContentType(contentTypes.get(i % contentTypes.size())); + item.setTheme("Тема поста " + (i + 1)); + item.setPostText("Текст поста для публикации на платформе " + item.getPlatform()); + item.setHashtags(Arrays.asList("#маркетинг", "#бизнес", "#продвижение")); + item.setPublishTime(times.get(i % times.size())); + calendar.add(item); + } + + return calendar; + } + + public MarketingStrategyResponse getStrategyResult(String strategyId) { + Optional optStrategy = repository.findById(strategyId); + if (optStrategy.isEmpty()) { + return null; + } + + MarketingStrategy strategy = optStrategy.get(); + MarketingStrategyResponse response = new MarketingStrategyResponse(); + response.setStrategyId(strategy.getId()); + response.setAnalysisId(strategy.getAnalysisId()); + response.setStatus(strategy.getStatus()); + response.setCreatedAt(strategy.getCreatedAt()); + response.setCompletedAt(strategy.getCompletedAt()); + response.setDurationWeeks(strategy.getDurationWeeks()); + response.setPriorityPlatforms(strategy.getPriorityPlatforms()); + + if (strategy.getWeeklyPlans() != null && strategy.getPostCalendar() != null + && "completed".equals(strategy.getStatus())) { + MarketingStrategyResponse.StrategyContent strategyContent = new MarketingStrategyResponse.StrategyContent(); + + // Convert WeeklyPlans + List weeklyPlans = new ArrayList<>(); + for (MarketingStrategy.WeeklyPlan plan : strategy.getWeeklyPlans()) { + MarketingStrategyResponse.WeeklyPlan dtoPlan = new MarketingStrategyResponse.WeeklyPlan(); + dtoPlan.setWeekNumber(plan.getWeekNumber()); + dtoPlan.setMainThemes(plan.getMainThemes()); + dtoPlan.setContentRecommendations(plan.getContentRecommendations()); + dtoPlan.setPriorityPlatforms(plan.getPriorityPlatforms()); + weeklyPlans.add(dtoPlan); + } + strategyContent.setWeeklyPlans(weeklyPlans); + + // Convert PostCalendarItems + List postCalendar = new ArrayList<>(); + for (MarketingStrategy.PostCalendarItem item : strategy.getPostCalendar()) { + MarketingStrategyResponse.PostCalendarItem dtoItem = new MarketingStrategyResponse.PostCalendarItem(); + dtoItem.setPublishDate(item.getPublishDate()); + dtoItem.setPlatform(item.getPlatform()); + dtoItem.setContentType(item.getContentType()); + dtoItem.setTheme(item.getTheme()); + dtoItem.setPostText(item.getPostText()); + dtoItem.setHashtags(item.getHashtags()); + dtoItem.setPublishTime(item.getPublishTime()); + postCalendar.add(dtoItem); + } + strategyContent.setPostCalendar(postCalendar); + + response.setStrategy(strategyContent); + } + + return response; + } + + public MarketingStrategyResponse getStrategyByAnalysisId(String analysisId) { + Optional optStrategy = repository.findByAnalysisId(analysisId); + if (optStrategy.isEmpty()) { + return null; + } + + return getStrategyResult(optStrategy.get().getId()); + } +} +