This commit is contained in:
root
2025-12-01 11:01:56 +05:00
parent 41932973e4
commit 24cc534fed
11 changed files with 2547 additions and 456 deletions
+752
View File
@@ -0,0 +1,752 @@
# API для запуска стратегий продвижения
## Обзор
Данный документ описывает API для управления credentials социальных сетей и запуска маркетинговых стратегий с автоматической публикацией постов.
## Базовый URL
```
http://your-server:port/api
```
## Аутентификация
Все endpoints требуют JWT токен в заголовке `Authorization`:
```
Authorization: Bearer <your-jwt-token>
```
## Управление Credentials социальных сетей
### 1. Сохранение/обновление credentials
Сохраняет или обновляет credentials для указанной платформы. Credentials автоматически шифруются перед сохранением.
**Endpoint:** `POST /api/social-media/credentials`
**Headers:**
```
Authorization: Bearer <token>
Content-Type: application/json
```
**Request Body:**
```json
{
"platform": "facebook",
"credentials": "your-facebook-access-token"
}
```
**Параметры:**
- `platform` (string, required) - Название платформы (например: "facebook", "instagram")
- `credentials` (string, required) - Access token или другие credentials для платформы
**Response 200 OK:**
```json
{
"success": true,
"message": "Credentials успешно сохранены для платформы facebook",
"data": {
"platform": "facebook",
"hasCredentials": true,
"createdAt": "2024-01-15T10:30:00",
"updatedAt": "2024-01-15T10:30:00"
}
}
```
**Response 400 Bad Request:**
```json
{
"success": false,
"message": "Ошибка валидации",
"error": {
"code": "VALIDATION_ERROR",
"message": "Platform is required",
"details": {
"platform": "Platform is required"
}
}
}
```
**Response 401 Unauthorized:**
```json
{
"success": false,
"message": "Не авторизован",
"error": {
"code": "UNAUTHORIZED",
"message": "Требуется аутентификация. Пожалуйста, предоставьте валидный JWT токен."
}
}
```
**Пример запроса (JavaScript):**
```javascript
const saveCredentials = async (platform, accessToken) => {
const response = await fetch('/api/social-media/credentials', {
method: 'POST',
headers: {
Authorization: `Bearer ${jwtToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
platform: platform,
credentials: accessToken,
}),
});
const data = await response.json();
return data;
};
// Использование
await saveCredentials('facebook', 'EAABwzLix...');
```
---
### 2. Получение информации о credentials
Проверяет наличие credentials для указанной платформы.
**Endpoint:** `GET /api/social-media/credentials/{platform}`
**Headers:**
```
Authorization: Bearer <token>
```
**Path Parameters:**
- `platform` (string) - Название платформы
**Response 200 OK:**
```json
{
"success": true,
"message": "Операция выполнена успешно",
"data": {
"platform": "facebook",
"hasCredentials": true
}
}
```
**Пример запроса:**
```javascript
const checkCredentials = async (platform) => {
const response = await fetch(`/api/social-media/credentials/${platform}`, {
method: 'GET',
headers: {
Authorization: `Bearer ${jwtToken}`,
},
});
const data = await response.json();
return data.data.hasCredentials;
};
```
---
### 3. Получение списка всех credentials
Возвращает список всех платформ, для которых у пользователя настроены credentials.
**Endpoint:** `GET /api/social-media/credentials`
**Headers:**
```
Authorization: Bearer <token>
```
**Response 200 OK:**
```json
{
"success": true,
"message": "Операция выполнена успешно",
"data": [
{
"platform": "facebook",
"hasCredentials": true,
"createdAt": "2024-01-15T10:30:00",
"updatedAt": "2024-01-15T10:30:00"
}
]
}
```
**Пример запроса:**
```javascript
const getAllCredentials = async () => {
const response = await fetch('/api/social-media/credentials', {
method: 'GET',
headers: {
Authorization: `Bearer ${jwtToken}`,
},
});
const data = await response.json();
return data.data;
};
```
---
### 4. Удаление credentials
Удаляет credentials для указанной платформы.
**Endpoint:** `DELETE /api/social-media/credentials/{platform}`
**Headers:**
```
Authorization: Bearer <token>
```
**Path Parameters:**
- `platform` (string) - Название платформы
**Response 200 OK:**
```json
{
"success": true,
"message": "Credentials для платформы facebook успешно удалены",
"data": null
}
```
**Response 404 Not Found:**
```json
{
"success": false,
"message": "Credentials не найдены",
"error": {
"code": "NOT_FOUND",
"message": "Credentials для платформы facebook не найдены"
}
}
```
**Пример запроса:**
```javascript
const deleteCredentials = async (platform) => {
const response = await fetch(`/api/social-media/credentials/${platform}`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${jwtToken}`,
},
});
const data = await response.json();
return data;
};
```
---
## Запуск стратегии продвижения
### Запуск стратегии
Запускает выполнение маркетинговой стратегии. Система автоматически создает задачи публикации из календаря постов стратегии и добавляет их в очередь для выполнения.
**Endpoint:** `POST /api/marketing/analysis/strategy/{strategyId}/start`
**Headers:**
```
Authorization: Bearer <token>
Content-Type: application/json
```
**Path Parameters:**
- `strategyId` (string) - ID стратегии для запуска
**Request Body (опционально):**
```json
{}
```
**Response 200 OK:**
```json
{
"success": true,
"message": "Стратегия успешно запущена",
"data": {
"strategyId": "67890abcdef",
"tasksCreated": 12,
"platforms": ["facebook", "instagram"],
"message": "Стратегия успешно запущена. Создано задач: 12"
}
}
```
**Response 400 Bad Request (стратегия не завершена):**
```json
{
"success": false,
"message": "Стратегия не готова к запуску",
"error": {
"code": "INVALID_STATUS",
"message": "Стратегия еще не завершена. Статус: processing"
}
}
```
**Response 400 Bad Request (нет credentials):**
```json
{
"success": false,
"message": "Не удалось запустить стратегию",
"error": {
"code": "MISSING_CREDENTIALS",
"message": "Credentials not found for platform: facebook. Please configure credentials first."
}
}
```
**Response 404 Not Found:**
```json
{
"success": false,
"message": "Стратегия не найдена",
"error": {
"code": "NOT_FOUND",
"message": "Стратегия с указанным ID не найдена"
}
}
```
**Response 403 Forbidden:**
```json
{
"success": false,
"message": "Доступ запрещен",
"error": {
"code": "FORBIDDEN",
"message": "У вас нет доступа к этой стратегии"
}
}
```
**Пример запроса:**
```javascript
const startStrategy = async (strategyId) => {
const response = await fetch(
`/api/marketing/analysis/strategy/${strategyId}/start`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${jwtToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
}
);
const data = await response.json();
if (data.success) {
console.log(`Создано задач: ${data.data.tasksCreated}`);
console.log(`Платформы: ${data.data.platforms.join(', ')}`);
} else {
console.error('Ошибка:', data.error.message);
}
return data;
};
```
---
## Полный пример использования
### Шаг 1: Настройка credentials для Facebook
```javascript
// Сохраняем Facebook Access Token
const facebookToken = 'EAABwzLix...'; // Получить из Facebook Developer Console
const result = await saveCredentials('facebook', facebookToken);
if (result.success) {
console.log('Facebook credentials сохранены');
}
```
### Шаг 2: Получение стратегии
```javascript
// Получаем список стратегий пользователя
const response = await fetch('/api/marketing/analysis/strategy/my', {
headers: {
Authorization: `Bearer ${jwtToken}`,
},
});
const data = await response.json();
const strategies = data.data;
// Выбираем завершенную стратегию
const completedStrategy = strategies.find((s) => s.status === 'completed');
```
### Шаг 3: Запуск стратегии
```javascript
if (completedStrategy) {
// Проверяем наличие credentials для платформ стратегии
const platforms = completedStrategy.priorityPlatforms || [];
for (const platform of platforms) {
const hasCreds = await checkCredentials(platform);
if (!hasCreds) {
console.warn(`Необходимо настроить credentials для ${platform}`);
// Показать пользователю форму для ввода credentials
}
}
// Запускаем стратегию
const startResult = await startStrategy(completedStrategy.strategyId);
if (startResult.success) {
console.log(
`Стратегия запущена! Создано ${startResult.data.tasksCreated} задач`
);
// Показать уведомление пользователю
}
}
```
---
## Обработка ошибок
### Типичные ошибки и их обработка
1. **UNAUTHORIZED (401)**
- Причина: Невалидный или отсутствующий JWT токен
- Решение: Обновить токен или перенаправить на страницу входа
2. **VALIDATION_ERROR (400)**
- Причина: Невалидные данные в запросе
- Решение: Проверить обязательные поля и их формат
3. **MISSING_CREDENTIALS (400)**
- Причина: Не настроены credentials для платформы
- Решение: Предложить пользователю настроить credentials
4. **INVALID_STATUS (400)**
- Причина: Стратегия еще не завершена
- Решение: Дождаться завершения генерации стратегии
5. **NOT_FOUND (404)**
- Причина: Стратегия или credentials не найдены
- Решение: Проверить правильность ID
6. **FORBIDDEN (403)**
- Причина: Пользователь не имеет доступа к ресурсу
- Решение: Проверить права доступа
### Пример обработки ошибок
```javascript
const handleApiError = (error) => {
switch (error.code) {
case 'UNAUTHORIZED':
// Перенаправить на страницу входа
window.location.href = '/login';
break;
case 'MISSING_CREDENTIALS':
// Показать модальное окно для настройки credentials
showCredentialsModal(error.message);
break;
case 'INVALID_STATUS':
// Показать сообщение о том, что стратегия еще не готова
showNotification(
'Стратегия еще не завершена. Пожалуйста, подождите.',
'warning'
);
break;
case 'VALIDATION_ERROR':
// Показать ошибки валидации
showValidationErrors(error.details);
break;
default:
showNotification('Произошла ошибка. Попробуйте позже.', 'error');
}
};
// Использование
try {
const result = await startStrategy(strategyId);
if (!result.success) {
handleApiError(result.error);
}
} catch (error) {
console.error('Network error:', error);
showNotification('Ошибка сети. Проверьте подключение.', 'error');
}
```
---
## Статусы задач публикации
После запуска стратегии создаются задачи со следующими статусами:
- `pending` - Задача ожидает выполнения (дата публикации еще не наступила)
- `processing` - Задача выполняется в данный момент
- `completed` - Задача успешно выполнена
- `failed` - Задача не выполнена из-за ошибки
**Примечание:** Задачи с датой публикации в прошлом выполняются сразу после создания.
---
## Планировщик задач
Система автоматически проверяет очередь задач каждую минуту и выполняет задачи, у которых наступило время публикации.
- Планировщик включен по умолчанию
- Можно отключить через настройку `posting.scheduler.enabled=false`
- Задачи выполняются асинхронно
---
## Поддерживаемые платформы
На данный момент поддерживается:
- **Facebook** - через Facebook Graph API v18.0
В будущем планируется поддержка:
- Instagram
- LinkedIn
- Telegram
- TikTok
- YouTube
---
## Получение Facebook Access Token
Для получения Facebook Access Token:
1. Перейдите на [Facebook Developers](https://developers.facebook.com/)
2. Создайте приложение
3. Добавьте продукт "Facebook Login"
4. Настройте OAuth и получите Access Token
5. Используйте полученный токен в API
**Важно:**
- Access Token имеет срок действия
- Для долгосрочного использования рекомендуется использовать Long-Lived Token
- Токен должен иметь разрешения `pages_manage_posts` для публикации
---
## Примеры React компонентов
### Компонент для настройки credentials
```jsx
import React, { useState } from 'react';
const CredentialsForm = ({ platform, onSave }) => {
const [token, setToken] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
setError(null);
try {
const response = await fetch('/api/social-media/credentials', {
method: 'POST',
headers: {
Authorization: `Bearer ${jwtToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
platform: platform,
credentials: token,
}),
});
const data = await response.json();
if (data.success) {
onSave();
setToken('');
} else {
setError(data.error.message);
}
} catch (err) {
setError('Ошибка сети');
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>Access Token для {platform}:</label>
<input
type='text'
value={token}
onChange={(e) => setToken(e.target.value)}
required
/>
</div>
{error && <div className='error'>{error}</div>}
<button type='submit' disabled={loading}>
{loading ? 'Сохранение...' : 'Сохранить'}
</button>
</form>
);
};
export default CredentialsForm;
```
### Компонент для запуска стратегии
```jsx
import React, { useState } from 'react';
const StartStrategyButton = ({ strategyId, onStart }) => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const handleStart = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(
`/api/marketing/analysis/strategy/${strategyId}/start`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${jwtToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
}
);
const data = await response.json();
if (data.success) {
onStart(data.data);
} else {
setError(data.error.message);
}
} catch (err) {
setError('Ошибка сети');
} finally {
setLoading(false);
}
};
return (
<div>
<button onClick={handleStart} disabled={loading}>
{loading ? 'Запуск...' : 'Запустить стратегию'}
</button>
{error && <div className='error'>{error}</div>}
</div>
);
};
export default StartStrategyButton;
```
---
## Часто задаваемые вопросы
### Q: Как часто проверяются задачи на выполнение?
A: Планировщик проверяет очередь каждую минуту.
### Q: Что происходит, если credentials истекли?
A: Задача получит статус `failed` с сообщением об ошибке. Необходимо обновить credentials и перезапустить стратегию.
### Q: Можно ли отменить выполнение стратегии?
A: На данный момент нет, но можно удалить credentials для платформы, что предотвратит выполнение будущих задач.
### Q: Как узнать статус выполнения задач?
A: Статусы задач можно получить через API (будет добавлено в будущих версиях).
### Q: Поддерживается ли публикация с изображениями?
A: На данный момент поддерживается только текстовая публикация. Поддержка изображений планируется в будущем.
---
## Версионирование API
Текущая версия: **v1**
Все endpoints могут изменяться в будущих версиях. При изменении API будет указана новая версия.
---
## Поддержка
При возникновении проблем:
1. Проверьте логи в консоли браузера
2. Убедитесь, что JWT токен валиден
3. Проверьте формат запросов согласно документации
4. Обратитесь к разработчикам с описанием проблемы
File diff suppressed because it is too large Load Diff
+411
View File
@@ -0,0 +1,411 @@
# Обновление API: Виды анализа для маркетингового анализа (Frontend/AI Agent)
## Обзор изменений
В API маркетингового анализа добавлена поддержка **опциональных видов анализа**, которые могут быть переданы от фронтенда и будут включены в промпт для AI-генерации отчета.
**Дата обновления**: 2025-01-20
---
## Что изменилось
### Новые опциональные поля в запросе
В эндпоинт `POST /api/marketing/analysis/start` добавлены 5 новых опциональных полей для видов анализа:
1. **`market`** - Анализ рынка
2. **`competitors`** - Анализ конкурентов
3. **`targetAudienceAnalysis`** - Анализ целевой аудитории
4. **`channels`** - Анализ каналов
5. **`swot`** - SWOT анализ
Эти данные будут:
- ✅ Включены в контекст промпта для AI-генерации
- ✅ Сохранены в отчете
- ✅ Отображены в PDF-отчете в разделе "Виды анализа"
---
## Обновленная структура запроса
### Эндпоинт
**POST** `/api/marketing/analysis/start`
### Тело запроса (JSON)
#### Обязательные поля (без изменений)
| Поле | Тип | Обязательный | Описание | Пример значения |
| ---------------- | ------ | ------------ | ------------------------------ | -------------------------------- |
| `product` | string | ✅ | Название продукта или услуги | "Веб-разработка" |
| `location` | string | ✅ | Географическая локация работы | "Алматы, Казахстан" |
| `client` | string | ✅ | Тип целевой аудитории | "B2B клиенты" |
| `differentiator` | string | ✅ | Уникальные особенности бизнеса | "Быстрая разработка за 2 недели" |
#### Новые опциональные поля
| Поле | Тип | Обязательный | Описание | Максимальная длина | Пример значения |
| ------------------------ | ------ | ------------ | ----------------------------------------------------------- | ------------------ | ------------------------------------------------------------------------------- |
| `market` | string | ❌ | Анализ рынка (размер, тренды, возможности) | 2000 символов | "Рынок веб-разработки в Казахстане растет на 15% ежегодно..." |
| `competitors` | string | ❌ | Анализ конкурентов (основные игроки, их преимущества) | 2000 символов | "Основные конкуренты: Компания X, Компания Y..." |
| `targetAudienceAnalysis` | string | ❌ | Детальный анализ целевой аудитории | 2000 символов | "Целевая аудитория: IT-директора средних компаний..." |
| `channels` | string | ❌ | Анализ маркетинговых каналов | 2000 символов | "Рекомендуемые каналы: LinkedIn для B2B, Instagram для визуального контента..." |
| `swot` | string | ❌ | SWOT анализ (Strengths, Weaknesses, Opportunities, Threats) | 2000 символов | "Strengths: Быстрая разработка... Weaknesses: Ограниченный бюджет..." |
### Валидация новых полей
Все новые поля имеют одинаковые правила валидации:
- **Тип**: `string`
- **Обязательность**: ❌ Опциональное (можно не передавать)
- **Максимальная длина**: 2000 символов
- **Минимальная длина**: Нет ограничений (может быть пустой строкой)
---
## Примеры использования
### Пример 1: Запрос только с обязательными полями (как раньше)
```javascript
const response = await fetch(
'https://api.konturai.kz/api/marketing/analysis/start',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${jwtToken}`,
},
body: JSON.stringify({
product: 'Разработка мобильных приложений',
location: 'Нур-Султан, Казахстан',
client: 'B2B клиенты',
differentiator: 'Специализируемся на быстрой разработке MVP за 4 недели',
}),
}
);
```
**Результат**: Работает как раньше, без изменений в функциональности.
---
### Пример 2: Запрос с одним видом анализа
```javascript
const response = await fetch(
'https://api.konturai.kz/api/marketing/analysis/start',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${jwtToken}`,
},
body: JSON.stringify({
product: 'Разработка мобильных приложений',
location: 'Нур-Султан, Казахстан',
client: 'B2B клиенты',
differentiator: 'Специализируемся на быстрой разработке MVP за 4 недели',
market:
'Рынок мобильной разработки в Казахстане показывает стабильный рост на 20% в год. Основные драйверы: цифровизация бизнеса, рост числа стартапов, увеличение инвестиций в IT-сектор.',
}),
}
);
```
**Результат**: Данные об анализе рынка будут включены в промпт и отображены в отчете.
---
### Пример 3: Запрос со всеми видами анализа
```javascript
const response = await fetch(
'https://api.konturai.kz/api/marketing/analysis/start',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${jwtToken}`,
},
body: JSON.stringify({
product: 'Разработка мобильных приложений',
location: 'Нур-Султан, Казахстан',
client: 'B2B клиенты',
differentiator: 'Специализируемся на быстрой разработке MVP за 4 недели',
// Виды анализа
market:
'Рынок мобильной разработки в Казахстане растет на 20% ежегодно. Основные сегменты: корпоративные приложения, e-commerce, финтех.',
competitors:
'Основные конкуренты: TechCorp (лидер рынка, 30% доля), DevStudio (средний сегмент, 15% доля), StartupDev (нишевый игрок, 5% доля). Их преимущества: большая команда, долгосрочные контракты.',
targetAudienceAnalysis:
'Целевая аудитория: IT-директора средних и крупных компаний (50-500 сотрудников), возраст 35-50 лет, техническое образование. Боли: долгие сроки разработки, высокие цены, отсутствие гибкости.',
channels:
'Рекомендуемые каналы: LinkedIn (основной для B2B), Telegram-каналы для IT-сообщества, отраслевые конференции, контент-маркетинг через блог.',
swot: 'Strengths: Быстрая разработка MVP, опытная команда, гибкие методологии. Weaknesses: Ограниченный бюджет на маркетинг, небольшая команда. Opportunities: Рост спроса на мобильные решения, цифровизация госсектора. Threats: Конкуренция с крупными игроками, экономическая нестабильность.',
}),
}
);
```
**Результат**: Все виды анализа будут включены в промпт для более точной генерации отчета и отображены в PDF.
---
## Как это влияет на генерацию отчета
### Включение в промпт
Когда вы передаете виды анализа, они автоматически добавляются в контекст промпта для AI:
```
Продукт/услуга: Разработка мобильных приложений
Локация: Нур-Султан, Казахстан
Тип клиентов: B2B клиенты
Уникальные особенности: Специализируемся на быстрой разработке MVP за 4 недели
Анализ рынка:
Рынок мобильной разработки в Казахстане растет на 20% ежегодно...
Анализ конкурентов:
Основные конкуренты: TechCorp...
...
```
Это позволяет AI генерировать более точные и релевантные рекомендации, учитывая предоставленную аналитику.
### Отображение в PDF-отчете
Если виды анализа были переданы, они будут отображены в PDF-отчете в отдельном разделе:
```markdown
## Виды анализа
### Анализ рынка
Рынок мобильной разработки в Казахстане растет на 20% ежегодно...
### Анализ конкурентов
Основные конкуренты: TechCorp...
### Анализ целевой аудитории
Целевая аудитория: IT-директора средних и крупных компаний...
### Анализ каналов
Рекомендуемые каналы: LinkedIn (основной для B2B)...
### SWOT анализ
Strengths: Быстрая разработка MVP...
```
---
## Обратная совместимость
**Полная обратная совместимость**: Старые запросы без новых полей продолжают работать без изменений.
**Опциональные поля**: Все новые поля опциональны, их можно не передавать.
**Валидация**: Если поля переданы, они валидируются (максимум 2000 символов).
---
## Рекомендации для фронтенда
### 1. UI/UX
Рекомендуется добавить в форму создания анализа:
- **Опциональные секции** для каждого вида анализа
- **Текстовые поля** (textarea) с ограничением в 2000 символов
- **Подсказки** о том, какую информацию следует включить в каждый вид анализа
- **Индикатор прогресса** заполнения (опционально)
### 2. Валидация на фронтенде
```javascript
// Пример валидации на фронтенде
const validateAnalysisTypes = (data) => {
const errors = {};
const analysisTypes = [
'market',
'competitors',
'targetAudienceAnalysis',
'channels',
'swot',
];
analysisTypes.forEach((field) => {
if (data[field] && data[field].length > 2000) {
errors[field] = `Поле "${field}" не должно превышать 2000 символов`;
}
});
return errors;
};
```
### 3. Структура данных
```typescript
// TypeScript интерфейс для запроса
interface MarketingAnalysisRequest {
// Обязательные поля
product: string; // 3-200 символов
location: string; // 2-150 символов
client: string; // Одно из допустимых значений
differentiator: string; // 10-500 символов
// Опциональные виды анализа
market?: string; // до 2000 символов
competitors?: string; // до 2000 символов
targetAudienceAnalysis?: string; // до 2000 символов
channels?: string; // до 2000 символов
swot?: string; // до 2000 символов
}
```
---
## Примеры ответов API
### Успешный ответ (без изменений)
```json
{
"success": true,
"message": "Анализ запущен успешно",
"data": {
"analysisId": "507f1f77bcf86cd799439011",
"status": "processing",
"estimatedCompletionTime": "2025-01-20T15:38:00",
"message": "Анализ запущен успешно. Результаты будут готовы в течение 5-10 минут."
}
}
```
### Ошибка валидации (если поле превышает лимит)
```json
{
"success": false,
"message": "Ошибка валидации",
"error": {
"code": "VALIDATION_ERROR",
"message": "Ошибка валидации входных данных",
"details": {
"market": "Поле 'market' должно содержать не более 2000 символов"
}
}
}
```
**HTTP статус**: `400 Bad Request`
---
## Часто задаваемые вопросы (FAQ)
### Q: Обязательно ли передавать все виды анализа?
**A**: Нет, все поля опциональны. Вы можете передать только те, которые у вас есть.
### Q: Что произойдет, если я передам пустую строку?
**A**: Пустые строки игнорируются, они не будут включены в промпт и отчет.
### Q: Можно ли передать только часть видов анализа?
**A**: Да, вы можете передать любое количество видов анализа (от 0 до 5).
### Q: Влияет ли это на время генерации отчета?
**A**: Нет, время генерации остается прежним (5-10 минут). Дополнительные данные могут улучшить качество отчета.
### Q: Будут ли виды анализа отображаться в JSON-ответе?
**A**: Виды анализа сохраняются в отчете и могут быть доступны через структуру `reportData`, но в текущей версии API они не возвращаются в стандартном JSON-ответе. Они включены в PDF-отчет.
---
## Миграция для существующих клиентов
### Для существующих интеграций
**Никаких изменений не требуется!** Старые запросы продолжают работать без изменений.
### Для новых интеграций
Если вы хотите использовать новые возможности:
1. Добавьте новые поля в форму запроса (опционально)
2. Соблюдайте лимит в 2000 символов для каждого поля
3. Передавайте только те виды анализа, которые у вас есть
---
## Технические детали
### Структура хранения
Виды анализа сохраняются в MongoDB в поле `reportData.analysisTypes`:
```json
{
"reportData": {
"summary": "...",
"targetAudience": {...},
"recommendations": [...],
"strategy": {...},
"analysisTypes": {
"market": "Рынок...",
"competitors": "Конкуренты...",
"targetAudienceAnalysis": "ЦА...",
"channels": "Каналы...",
"swot": "SWOT..."
}
}
}
```
### Включение в промпт
Виды анализа добавляются в контекст промпта в следующем формате:
```
Анализ рынка:
[содержимое поля market]
Анализ конкурентов:
[содержимое поля competitors]
...
```
Это позволяет AI учитывать предоставленную аналитику при генерации резюме, рекомендаций и стратегии.
---
## Поддержка
Если у вас возникли вопросы или проблемы с использованием новых полей, обратитесь к документации API или свяжитесь с командой разработки.
---
**Версия документа**: 1.0
**Дата обновления**: 2025-01-20
**Статус**: ✅ Актуально
+6 -1
View File
@@ -62,7 +62,12 @@ export const API_CONFIG = {
MARKETING_STRATEGY_GET: '/api/marketing/analysis/strategy', MARKETING_STRATEGY_GET: '/api/marketing/analysis/strategy',
MARKETING_STRATEGY_BY_ANALYSIS: '/api/marketing/analysis', MARKETING_STRATEGY_BY_ANALYSIS: '/api/marketing/analysis',
MARKETING_STRATEGY_MY: '/api/marketing/analysis/strategy/my', MARKETING_STRATEGY_MY: '/api/marketing/analysis/strategy/my',
MARKETING_STRATEGY_HISTORY: '/api/marketing/analysis/strategy' MARKETING_STRATEGY_HISTORY: '/api/marketing/analysis/strategy',
MARKETING_STRATEGY_START: '/api/marketing/analysis/strategy',
// SocialMediaCredentialsController - Управление credentials социальных сетей
SOCIAL_MEDIA_CREDENTIALS: '/api/social-media/credentials',
SOCIAL_MEDIA_CREDENTIALS_BY_PLATFORM: '/api/social-media/credentials'
} }
}; };
+10 -5
View File
@@ -248,11 +248,16 @@ const router = createRouter({
name: 'marketing-analyses-list', name: 'marketing-analyses-list',
component: () => import('@/views/pages/marketing/MarketingAnalysesList.vue') component: () => import('@/views/pages/marketing/MarketingAnalysesList.vue')
}, },
{ {
path: '/marketing-analysis/strategies', path: '/marketing-analysis/strategies',
name: 'marketing-strategies-list', name: 'marketing-strategies-list',
component: () => import('@/views/pages/marketing/MarketingStrategiesList.vue') component: () => import('@/views/pages/marketing/MarketingStrategiesList.vue')
}, },
{
path: '/marketing-analysis/credentials',
name: 'marketing-credentials',
component: () => import('@/views/pages/marketing/MarketingCredentials.vue')
},
{ {
path: '/pages/notfound', path: '/pages/notfound',
name: 'notfound', name: 'notfound',
+181 -1
View File
@@ -12,6 +12,7 @@ class MarketingService {
* @param {string} data.location - Географическая локация работы * @param {string} data.location - Географическая локация работы
* @param {string} data.client - Тип целевой аудитории * @param {string} data.client - Тип целевой аудитории
* @param {string} data.differentiator - Уникальные особенности бизнеса * @param {string} data.differentiator - Уникальные особенности бизнеса
* @param {string} data.analysisType - Тип анализа (РЫНОК, КОНКУРЕНТЫ, ЦА, КАНАЛЫ, SWOT)
* @returns {Promise<Object>} Объект с analysisId и статусом * @returns {Promise<Object>} Объект с analysisId и статусом
*/ */
async startAnalysis(data) { async startAnalysis(data) {
@@ -23,7 +24,8 @@ class MarketingService {
product: data.product, product: data.product,
location: data.location, location: data.location,
client: data.client, client: data.client,
differentiator: data.differentiator differentiator: data.differentiator,
analysisType: data.analysisType
}) })
}); });
@@ -318,6 +320,184 @@ class MarketingService {
throw error; throw error;
} }
} }
/**
* Запуск стратегии продвижения
* POST /api/marketing/analysis/strategy/{strategyId}/start
* @param {string} strategyId - Идентификатор стратегии для запуска
* @returns {Promise<Object>} Объект с информацией о запуске (tasksCreated, platforms)
*/
async startStrategy(strategyId) {
try {
const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.MARKETING_STRATEGY_START}/${strategyId}/start`, {
method: 'POST',
...DEFAULT_REQUEST_CONFIG,
body: JSON.stringify({})
});
const result = await response.json();
if (!response.ok) {
const error = {
message: result.message || result.error?.message || 'Ошибка при запуске стратегии',
code: result.error?.code,
details: result.error?.details
};
throw error;
}
if (!result.success) {
const error = {
message: result.message || 'Ошибка при запуске стратегии',
code: result.error?.code,
details: result.error?.details
};
throw error;
}
return result.data;
} catch (error) {
console.error('Ошибка при запуске стратегии:', error);
throw error;
}
}
/**
* Сохранение/обновление credentials для социальной сети
* POST /api/social-media/credentials
* @param {string} platform - Название платформы (facebook, instagram, etc.)
* @param {string} credentials - Access token или другие credentials
* @returns {Promise<Object>} Объект с информацией о сохраненных credentials
*/
async saveCredentials(platform, credentials) {
try {
const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.SOCIAL_MEDIA_CREDENTIALS}`, {
method: 'POST',
...DEFAULT_REQUEST_CONFIG,
body: JSON.stringify({
platform: platform,
credentials: credentials
})
});
const result = await response.json();
if (!response.ok) {
const error = {
message: result.message || result.error?.message || 'Ошибка при сохранении credentials',
code: result.error?.code,
details: result.error?.details
};
throw error;
}
if (!result.success) {
const error = {
message: result.message || 'Ошибка при сохранении credentials',
code: result.error?.code,
details: result.error?.details
};
throw error;
}
return result.data;
} catch (error) {
console.error('Ошибка при сохранении credentials:', error);
throw error;
}
}
/**
* Получение информации о credentials для платформы
* GET /api/social-media/credentials/{platform}
* @param {string} platform - Название платформы
* @returns {Promise<Object>} Объект с информацией о наличии credentials
*/
async getCredentials(platform) {
try {
const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.SOCIAL_MEDIA_CREDENTIALS_BY_PLATFORM}/${platform}`);
const result = await response.json();
if (!response.ok) {
throw new Error(result.message || result.error?.message || 'Ошибка при получении credentials');
}
if (!result.success) {
throw new Error(result.message || 'Ошибка при получении credentials');
}
return result.data;
} catch (error) {
console.error('Ошибка при получении credentials:', error);
throw error;
}
}
/**
* Получение списка всех credentials
* GET /api/social-media/credentials
* @returns {Promise<Array>} Массив объектов с информацией о credentials для каждой платформы
*/
async getAllCredentials() {
try {
const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.SOCIAL_MEDIA_CREDENTIALS}`);
const result = await response.json();
if (!response.ok) {
throw new Error(result.message || result.error?.message || 'Ошибка при получении списка credentials');
}
if (!result.success) {
throw new Error(result.message || 'Ошибка при получении списка credentials');
}
return result.data;
} catch (error) {
console.error('Ошибка при получении списка credentials:', error);
throw error;
}
}
/**
* Удаление credentials для платформы
* DELETE /api/social-media/credentials/{platform}
* @param {string} platform - Название платформы
* @returns {Promise<void>}
*/
async deleteCredentials(platform) {
try {
const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.SOCIAL_MEDIA_CREDENTIALS_BY_PLATFORM}/${platform}`, {
method: 'DELETE'
});
const result = await response.json();
if (!response.ok) {
const error = {
message: result.message || result.error?.message || 'Ошибка при удалении credentials',
code: result.error?.code,
details: result.error?.details
};
throw error;
}
if (!result.success) {
const error = {
message: result.message || 'Ошибка при удалении credentials',
code: result.error?.code,
details: result.error?.details
};
throw error;
}
return result.data;
} catch (error) {
console.error('Ошибка при удалении credentials:', error);
throw error;
}
}
} }
export default new MarketingService(); export default new MarketingService();
@@ -21,7 +21,7 @@
:sortField="'createdAt'" :sortField="'createdAt'"
:sortOrder="-1" :sortOrder="-1"
filterDisplay="row" filterDisplay="row"
:globalFilterFields="['product', 'location', 'clientType', 'status']" :globalFilterFields="['product', 'location', 'clientType', 'status', 'analysisType']"
v-model:filters="filters" v-model:filters="filters"
:filters="filters" :filters="filters"
dataKey="analysisId" dataKey="analysisId"
@@ -54,6 +54,13 @@
</template> </template>
</Column> </Column>
<Column field="analysisType" header="Тип анализа" :sortable="true" style="min-width: 180px">
<template #body="slotProps">
<Tag v-if="slotProps.data.analysisType" :value="getAnalysisTypeLabel(slotProps.data.analysisType)" severity="secondary" />
<span v-else class="text-surface-400"></span>
</template>
</Column>
<Column field="status" header="Статус" :sortable="true" style="min-width: 120px"> <Column field="status" header="Статус" :sortable="true" style="min-width: 120px">
<template #body="slotProps"> <template #body="slotProps">
<Tag :value="getStatusLabel(slotProps.data.status)" :severity="getStatusSeverity(slotProps.data.status)" /> <Tag :value="getStatusLabel(slotProps.data.status)" :severity="getStatusSeverity(slotProps.data.status)" />
@@ -309,6 +316,18 @@ const getStatusIcon = (status) => {
return icons[status] || 'pi pi-circle'; return icons[status] || 'pi pi-circle';
}; };
// Get analysis type label
const getAnalysisTypeLabel = (type) => {
const labels = {
РЫНОК: 'Анализ рынка',
КОНКУРЕНТЫ: 'Анализ конкурентов',
ЦА: 'Анализ целевой аудитории',
КАНАЛЫ: 'Анализ маркетинговых каналов',
SWOT: 'SWOT-анализ'
};
return labels[type] || type;
};
// Load on mount // Load on mount
onMounted(() => { onMounted(() => {
loadAnalyses(); loadAnalyses();
+101 -6
View File
@@ -33,6 +33,22 @@
<small v-if="errors.differentiator" class="p-error">{{ errors.differentiator }}</small> <small v-if="errors.differentiator" class="p-error">{{ errors.differentiator }}</small>
</div> </div>
<div class="field">
<label for="analysisType" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Тип анализа </label>
<Dropdown
id="analysisType"
v-model="formData.analysisType"
:options="analysisTypeOptions"
optionLabel="label"
optionValue="value"
placeholder="Выберите тип анализа"
class="w-full"
:class="{ 'p-invalid': errors.analysisType }"
/>
<small v-if="errors.analysisType" class="p-error">{{ errors.analysisType }}</small>
<small class="text-surface-500 dark:text-surface-400 mt-1 block">Выберите тип анализа, который будет сгенерирован</small>
</div>
<Button type="submit" label="Начать анализ" icon="pi pi-send" class="w-full p-button-primary" :loading="submitting" /> <Button type="submit" label="Начать анализ" icon="pi pi-send" class="w-full p-button-primary" :loading="submitting" />
<p class="text-sm text-surface-500 dark:text-surface-400 mt-3 text-center">На основе ваших ответов будет создан подробный и полезный маркетинговый отчет</p> <p class="text-sm text-surface-500 dark:text-surface-400 mt-3 text-center">На основе ваших ответов будет создан подробный и полезный маркетинговый отчет</p>
@@ -77,7 +93,12 @@
<div class="card"> <div class="card">
<div class="card-header mb-4"> <div class="card-header mb-4">
<div class="flex justify-content-between align-items-center"> <div class="flex justify-content-between align-items-center">
<h2 class="text-2xl font-bold text-surface-900 dark:text-surface-0">Результаты анализа</h2> <div>
<h2 class="text-2xl font-bold text-surface-900 dark:text-surface-0">Результаты анализа</h2>
<div v-if="currentAnalysisType" class="mt-2">
<Tag :value="getAnalysisTypeLabel(currentAnalysisType)" severity="info" />
</div>
</div>
<Button label="Скачать PDF" icon="pi pi-download" @click="handleDownloadPdf" :loading="downloadingPdf" /> <Button label="Скачать PDF" icon="pi pi-download" @click="handleDownloadPdf" :loading="downloadingPdf" />
</div> </div>
</div> </div>
@@ -179,22 +200,32 @@ import ProgressSpinner from 'primevue/progressspinner';
import Tag from 'primevue/tag'; import Tag from 'primevue/tag';
import Toast from 'primevue/toast'; import Toast from 'primevue/toast';
import { useToast } from 'primevue/usetoast'; import { useToast } from 'primevue/usetoast';
import { onBeforeUnmount, ref } from 'vue'; import { onBeforeUnmount, onMounted, ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
const toast = useToast(); const toast = useToast();
const router = useRouter(); const router = useRouter();
const route = useRoute();
// Form data // Form data
const formData = ref({ const formData = ref({
product: '', product: '',
location: '', location: '',
client: null, client: null,
differentiator: '' differentiator: '',
analysisType: null
}); });
const clientOptions = ['B2B клиенты', 'B2C клиенты', 'Частные лица', 'Корпорации', 'Малый бизнес']; const clientOptions = ['B2B клиенты', 'B2C клиенты', 'Частные лица', 'Корпорации', 'Малый бизнес'];
const analysisTypeOptions = [
{ label: 'РЫНОК - Анализ рынка', value: 'РЫНОК' },
{ label: 'КОНКУРЕНТЫ - Анализ конкурентов', value: 'КОНКУРЕНТЫ' },
{ label: 'ЦА - Анализ целевой аудитории', value: 'ЦА' },
{ label: 'КАНАЛЫ - Анализ маркетинговых каналов', value: 'КАНАЛЫ' },
{ label: 'SWOT - SWOT-анализ', value: 'SWOT' }
];
const submitting = ref(false); const submitting = ref(false);
const errors = ref({}); const errors = ref({});
@@ -202,6 +233,7 @@ const errors = ref({});
const analysisId = ref(null); const analysisId = ref(null);
const status = ref(null); const status = ref(null);
const report = ref(null); const report = ref(null);
const currentAnalysisType = ref(null);
const estimatedCompletionTime = ref(null); const estimatedCompletionTime = ref(null);
const pollingInterval = ref(null); const pollingInterval = ref(null);
const pollingAttempts = ref(0); const pollingAttempts = ref(0);
@@ -241,6 +273,16 @@ const validateForm = () => {
errors.value.differentiator = 'Описание уникальности не должно превышать 500 символов'; errors.value.differentiator = 'Описание уникальности не должно превышать 500 символов';
} }
// AnalysisType validation
if (!formData.value.analysisType) {
errors.value.analysisType = 'Пожалуйста, выберите тип анализа';
} else {
const validTypes = ['РЫНОК', 'КОНКУРЕНТЫ', 'ЦА', 'КАНАЛЫ', 'SWOT'];
if (!validTypes.includes(formData.value.analysisType)) {
errors.value.analysisType = 'Выбран недопустимый тип анализа';
}
}
return Object.keys(errors.value).length === 0; return Object.keys(errors.value).length === 0;
}; };
@@ -268,6 +310,10 @@ const startPolling = (id) => {
if (result.status === 'completed' && result.report) { if (result.status === 'completed' && result.report) {
stopPolling(); stopPolling();
report.value = result.report; report.value = result.report;
// Сохраняем тип анализа из результата, если он есть
if (result.analysisType) {
currentAnalysisType.value = result.analysisType;
}
toast.add({ toast.add({
severity: 'success', severity: 'success',
summary: 'Анализ завершен', summary: 'Анализ завершен',
@@ -317,11 +363,13 @@ const handleSubmit = async () => {
product: formData.value.product.trim(), product: formData.value.product.trim(),
location: formData.value.location.trim(), location: formData.value.location.trim(),
client: formData.value.client, client: formData.value.client,
differentiator: formData.value.differentiator.trim() differentiator: formData.value.differentiator.trim(),
analysisType: formData.value.analysisType
}); });
analysisId.value = result.analysisId; analysisId.value = result.analysisId;
status.value = result.status; status.value = result.status;
currentAnalysisType.value = formData.value.analysisType;
estimatedCompletionTime.value = result.estimatedCompletionTime; estimatedCompletionTime.value = result.estimatedCompletionTime;
toast.add({ toast.add({
@@ -379,13 +427,15 @@ const resetAnalysis = () => {
analysisId.value = null; analysisId.value = null;
status.value = null; status.value = null;
report.value = null; report.value = null;
currentAnalysisType.value = null;
estimatedCompletionTime.value = null; estimatedCompletionTime.value = null;
pollingAttempts.value = 0; pollingAttempts.value = 0;
formData.value = { formData.value = {
product: '', product: '',
location: '', location: '',
client: null, client: null,
differentiator: '' differentiator: '',
analysisType: null
}; };
errors.value = {}; errors.value = {};
}; };
@@ -407,6 +457,18 @@ const renderMarkdown = (markdown) => {
} }
}; };
// Get analysis type label
const getAnalysisTypeLabel = (type) => {
const labels = {
РЫНОК: 'Анализ рынка',
КОНКУРЕНТЫ: 'Анализ конкурентов',
ЦА: 'Анализ целевой аудитории',
КАНАЛЫ: 'Анализ маркетинговых каналов',
SWOT: 'SWOT-анализ'
};
return labels[type] || type;
};
// Format date time // Format date time
const formatDateTime = (dateString) => { const formatDateTime = (dateString) => {
if (!dateString) return ''; if (!dateString) return '';
@@ -438,6 +500,39 @@ const goToStrategy = () => {
} }
}; };
// Load analysis from query params
onMounted(async () => {
const queryAnalysisId = route.query.analysisId;
if (queryAnalysisId) {
// Try to load existing analysis
try {
analysisId.value = queryAnalysisId;
const result = await MarketingService.getAnalysisResult(queryAnalysisId);
status.value = result.status;
// Сохраняем тип анализа, если он есть
if (result.analysisType) {
currentAnalysisType.value = result.analysisType;
}
if (result.status === 'completed' && result.report) {
report.value = result.report;
} else if (result.status === 'processing' || result.status === 'queued') {
estimatedCompletionTime.value = result.estimatedCompletionTime;
startPolling(queryAnalysisId);
}
} catch (error) {
console.error('Ошибка при загрузке анализа:', error);
toast.add({
severity: 'error',
summary: 'Ошибка',
detail: error.message || 'Не удалось загрузить анализ',
life: 5000
});
}
}
});
// Cleanup on unmount // Cleanup on unmount
onBeforeUnmount(() => { onBeforeUnmount(() => {
stopPolling(); stopPolling();
@@ -0,0 +1,384 @@
<template>
<div class="marketing-credentials-page bg-surface-50 dark:bg-surface-900 min-h-screen p-6">
<Toast />
<div class="max-w-[1200px] mx-auto">
<div class="card mb-4">
<div class="card-header mb-4">
<h2 class="text-2xl font-bold text-surface-900 dark:text-surface-0">Управление credentials социальных сетей</h2>
<p class="text-sm text-surface-500 dark:text-surface-400 mt-2">Настройте access tokens для платформ, на которых вы хотите автоматически публиковать контент</p>
</div>
<div class="card-content">
<form @submit.prevent="handleSaveCredentials" class="space-y-4">
<div class="grid">
<div class="col-12 md:col-4">
<div class="field">
<label for="platform" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300">
Платформа <span class="text-red-500">*</span>
</label>
<Dropdown
id="platform"
v-model="formData.platform"
:options="platformOptions"
placeholder="Выберите платформу"
class="w-full"
:class="{ 'p-invalid': errors.platform }"
/>
<small v-if="errors.platform" class="p-error">{{ errors.platform }}</small>
</div>
</div>
<div class="col-12 md:col-6">
<div class="field">
<label for="credentials" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300">
Access Token <span class="text-red-500">*</span>
</label>
<InputText
id="credentials"
v-model="formData.credentials"
type="password"
placeholder="Введите access token"
class="w-full"
:class="{ 'p-invalid': errors.credentials }"
/>
<small v-if="errors.credentials" class="p-error">{{ errors.credentials }}</small>
<small class="text-surface-500 dark:text-surface-400 mt-1 block">
Токен будет зашифрован перед сохранением
</small>
</div>
</div>
<div class="col-12 md:col-2">
<div class="field">
<label class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300">&nbsp;</label>
<Button
type="submit"
label="Сохранить"
icon="pi pi-save"
class="w-full"
:loading="saving"
/>
</div>
</div>
</div>
</form>
</div>
</div>
<div class="card">
<div class="card-header mb-4">
<div class="flex justify-content-between align-items-center">
<h3 class="text-xl font-semibold text-surface-900 dark:text-surface-0">Настроенные платформы</h3>
<Button label="Обновить" icon="pi pi-refresh" severity="secondary" @click="loadCredentials" :loading="loading" />
</div>
</div>
<div class="card-content">
<DataTable
:value="credentials"
:loading="loading"
paginator
:rows="10"
:rowsPerPageOptions="[5, 10, 20]"
dataKey="platform"
:emptyMessage="'Нет настроенных платформ'"
>
<Column field="platform" header="Платформа" :sortable="true" style="min-width: 150px">
<template #body="slotProps">
<Tag :value="slotProps.data.platform" severity="info" />
</template>
</Column>
<Column field="hasCredentials" header="Статус" :sortable="true" style="min-width: 120px">
<template #body="slotProps">
<Tag
:value="slotProps.data.hasCredentials ? 'Настроено' : 'Не настроено'"
:severity="slotProps.data.hasCredentials ? 'success' : 'warning'"
/>
</template>
</Column>
<Column field="createdAt" header="Создано" :sortable="true" style="min-width: 180px">
<template #body="slotProps">
<span v-if="slotProps.data.createdAt">{{ formatDateTime(slotProps.data.createdAt) }}</span>
<span v-else class="text-surface-400"></span>
</template>
</Column>
<Column field="updatedAt" header="Обновлено" :sortable="true" style="min-width: 180px">
<template #body="slotProps">
<span v-if="slotProps.data.updatedAt">{{ formatDateTime(slotProps.data.updatedAt) }}</span>
<span v-else class="text-surface-400"></span>
</template>
</Column>
<Column header="Действия" style="min-width: 150px">
<template #body="slotProps">
<Button
icon="pi pi-trash"
severity="danger"
size="small"
v-tooltip.top="'Удалить credentials'"
@click="confirmDelete(slotProps.data.platform)"
:disabled="!slotProps.data.hasCredentials"
/>
</template>
</Column>
</DataTable>
</div>
</div>
</div>
<!-- Диалог подтверждения удаления -->
<Dialog
v-model:visible="deleteDialogVisible"
:header="'Подтверждение удаления'"
:style="{ width: '450px' }"
:modal="true"
>
<div class="confirmation-content">
<i class="pi pi-exclamation-triangle mr-3" style="font-size: 2rem" />
<span>Вы уверены, что хотите удалить credentials для платформы <b>{{ platformToDelete }}</b>?</span>
</div>
<template #footer>
<Button label="Отмена" icon="pi pi-times" @click="deleteDialogVisible = false" severity="secondary" />
<Button label="Удалить" icon="pi pi-check" @click="handleDeleteCredentials" severity="danger" :loading="deleting" />
</template>
</Dialog>
</div>
</template>
<script setup>
import MarketingService from '@/service/MarketingService';
import Button from 'primevue/button';
import Column from 'primevue/column';
import DataTable from 'primevue/datatable';
import Dialog from 'primevue/dialog';
import Dropdown from 'primevue/dropdown';
import InputText from 'primevue/inputtext';
import Tag from 'primevue/tag';
import Toast from 'primevue/toast';
import { useToast } from 'primevue/usetoast';
import { onMounted, ref } from 'vue';
const toast = useToast();
// Form data
const formData = ref({
platform: null,
credentials: ''
});
const platformOptions = ['facebook', 'instagram', 'linkedin', 'telegram', 'tiktok', 'youtube'];
const saving = ref(false);
const errors = ref({});
// Data
const credentials = ref([]);
const loading = ref(false);
const deleteDialogVisible = ref(false);
const platformToDelete = ref(null);
const deleting = ref(false);
// Validation
const validateForm = () => {
errors.value = {};
if (!formData.value.platform) {
errors.value.platform = 'Пожалуйста, выберите платформу';
}
if (!formData.value.credentials || formData.value.credentials.trim().length === 0) {
errors.value.credentials = 'Access token обязателен';
} else if (formData.value.credentials.trim().length < 10) {
errors.value.credentials = 'Access token слишком короткий';
}
return Object.keys(errors.value).length === 0;
};
// Load credentials
const loadCredentials = async () => {
loading.value = true;
try {
const data = await MarketingService.getAllCredentials();
credentials.value = data || [];
} catch (error) {
console.error('Ошибка при загрузке credentials:', error);
toast.add({
severity: 'error',
summary: 'Ошибка',
detail: error.message || 'Не удалось загрузить список credentials',
life: 5000
});
} finally {
loading.value = false;
}
};
// Handle save credentials
const handleSaveCredentials = async () => {
if (!validateForm()) {
toast.add({
severity: 'error',
summary: 'Ошибка валидации',
detail: 'Пожалуйста, исправьте ошибки в форме',
life: 3000
});
return;
}
saving.value = true;
try {
await MarketingService.saveCredentials(formData.value.platform, formData.value.credentials.trim());
toast.add({
severity: 'success',
summary: 'Успешно',
detail: `Credentials для ${formData.value.platform} успешно сохранены`,
life: 3000
});
// Reset form
formData.value = {
platform: null,
credentials: ''
};
errors.value = {};
// Reload credentials list
await loadCredentials();
} catch (error) {
console.error('Ошибка при сохранении credentials:', error);
// Handle specific error codes
if (error.code === 'VALIDATION_ERROR') {
toast.add({
severity: 'error',
summary: 'Ошибка валидации',
detail: error.message || 'Проверьте правильность введенных данных',
life: 5000
});
} else if (error.code === 'UNAUTHORIZED') {
toast.add({
severity: 'error',
summary: 'Ошибка авторизации',
detail: 'Необходимо войти в систему',
life: 5000
});
} else {
toast.add({
severity: 'error',
summary: 'Ошибка',
detail: error.message || 'Не удалось сохранить credentials',
life: 5000
});
}
} finally {
saving.value = false;
}
};
// Confirm delete
const confirmDelete = (platform) => {
platformToDelete.value = platform;
deleteDialogVisible.value = true;
};
// Handle delete credentials
const handleDeleteCredentials = async () => {
if (!platformToDelete.value) return;
deleting.value = true;
try {
await MarketingService.deleteCredentials(platformToDelete.value);
toast.add({
severity: 'success',
summary: 'Успешно',
detail: `Credentials для ${platformToDelete.value} успешно удалены`,
life: 3000
});
deleteDialogVisible.value = false;
platformToDelete.value = null;
// Reload credentials list
await loadCredentials();
} catch (error) {
console.error('Ошибка при удалении credentials:', error);
if (error.code === 'NOT_FOUND') {
toast.add({
severity: 'warn',
summary: 'Не найдено',
detail: `Credentials для ${platformToDelete.value} не найдены`,
life: 5000
});
} else if (error.code === 'UNAUTHORIZED') {
toast.add({
severity: 'error',
summary: 'Ошибка авторизации',
detail: 'Необходимо войти в систему',
life: 5000
});
} else {
toast.add({
severity: 'error',
summary: 'Ошибка',
detail: error.message || 'Не удалось удалить credentials',
life: 5000
});
}
} finally {
deleting.value = false;
}
};
// Format date time
const formatDateTime = (dateString) => {
if (!dateString) return '—';
const date = new Date(dateString);
return date.toLocaleString('ru-RU', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
// Load on mount
onMounted(() => {
loadCredentials();
});
</script>
<style scoped>
.marketing-credentials-page {
font-family: var(--font-family);
}
.card {
background: var(--surface-card);
border-radius: 12px;
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.12),
0 1px 2px rgba(0, 0, 0, 0.24);
padding: 1.5rem;
}
.card-header {
border-bottom: 1px solid var(--surface-border);
padding-bottom: 1rem;
margin-bottom: 1rem;
}
.card-content {
padding-top: 0;
}
.confirmation-content {
display: flex;
align-items: center;
gap: 1rem;
}
</style>
@@ -72,6 +72,13 @@ const menuItems = [
iconColor: 'text-primary-500', iconColor: 'text-primary-500',
route: '/marketing-analysis/strategies' route: '/marketing-analysis/strategies'
}, },
{
id: 'credentials',
label: 'Credentials социальных сетей',
icon: 'pi pi-key',
iconColor: 'text-primary-500',
route: '/marketing-analysis/credentials'
},
{ {
id: 'results', id: 'results',
label: 'Результаты', label: 'Результаты',
@@ -77,7 +77,17 @@
<h2 class="text-2xl font-bold text-surface-900 dark:text-surface-0">Стратегия продвижения</h2> <h2 class="text-2xl font-bold text-surface-900 dark:text-surface-0">Стратегия продвижения</h2>
<p class="text-sm text-surface-500 dark:text-surface-400 mt-1">Длительность: {{ strategyData.durationWeeks }} {{ pluralize(strategyData.durationWeeks, 'неделя', 'недели', 'недель') }}</p> <p class="text-sm text-surface-500 dark:text-surface-400 mt-1">Длительность: {{ strategyData.durationWeeks }} {{ pluralize(strategyData.durationWeeks, 'неделя', 'недели', 'недель') }}</p>
</div> </div>
<Button label="Создать новую стратегию" icon="pi pi-plus" severity="secondary" @click="resetStrategy" /> <div class="flex gap-2">
<Button
label="Запустить стратегию"
icon="pi pi-play"
severity="success"
@click="handleStartStrategy"
:loading="startingStrategy"
:disabled="startingStrategy"
/>
<Button label="Создать новую стратегию" icon="pi pi-plus" severity="secondary" @click="resetStrategy" />
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -215,12 +225,48 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Диалог для настройки credentials -->
<Dialog
v-model:visible="credentialsDialogVisible"
:header="'Настройка credentials'"
:style="{ width: '600px' }"
:modal="true"
>
<div class="credentials-dialog-content">
<p class="text-surface-700 dark:text-surface-300 mb-4">
Для запуска стратегии необходимо настроить credentials для следующих платформ:
</p>
<div class="mb-4">
<Tag
v-for="platform in missingPlatforms"
:key="platform"
:value="platform"
severity="warning"
class="mr-2 mb-2"
/>
</div>
<p class="text-surface-600 dark:text-surface-400 text-sm">
Перейдите на страницу управления credentials для настройки необходимых платформ.
</p>
</div>
<template #footer>
<Button label="Отмена" icon="pi pi-times" @click="credentialsDialogVisible = false" severity="secondary" />
<Button
label="Настроить credentials"
icon="pi pi-cog"
@click="goToCredentials"
severity="info"
/>
</template>
</Dialog>
</div> </div>
</template> </template>
<script setup> <script setup>
import MarketingService from '@/service/MarketingService'; import MarketingService from '@/service/MarketingService';
import Button from 'primevue/button'; import Button from 'primevue/button';
import Dialog from 'primevue/dialog';
import InputNumber from 'primevue/inputnumber'; import InputNumber from 'primevue/inputnumber';
import InputText from 'primevue/inputtext'; import InputText from 'primevue/inputtext';
import MultiSelect from 'primevue/multiselect'; import MultiSelect from 'primevue/multiselect';
@@ -229,10 +275,11 @@ import Tag from 'primevue/tag';
import Toast from 'primevue/toast'; import Toast from 'primevue/toast';
import { useToast } from 'primevue/usetoast'; import { useToast } from 'primevue/usetoast';
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'; import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
const toast = useToast(); const toast = useToast();
const route = useRoute(); const route = useRoute();
const router = useRouter();
// Form data // Form data
const formData = ref({ const formData = ref({
@@ -255,6 +302,9 @@ const pollingAttempts = ref(0);
const maxPollingAttempts = 60; // 10 minutes with 10 second intervals const maxPollingAttempts = 60; // 10 minutes with 10 second intervals
const pollingIntervalMs = 10000; // 10 seconds const pollingIntervalMs = 10000; // 10 seconds
const groupByDate = ref(true); const groupByDate = ref(true);
const startingStrategy = ref(false);
const credentialsDialogVisible = ref(false);
const missingPlatforms = ref([]);
// Computed // Computed
const postsByDate = computed(() => { const postsByDate = computed(() => {
@@ -462,6 +512,98 @@ const toggleGroupByDate = () => {
groupByDate.value = !groupByDate.value; groupByDate.value = !groupByDate.value;
}; };
// Handle start strategy
const handleStartStrategy = async () => {
if (!strategyId.value) {
toast.add({
severity: 'error',
summary: 'Ошибка',
detail: 'ID стратегии не найден',
life: 3000
});
return;
}
startingStrategy.value = true;
try {
const result = await MarketingService.startStrategy(strategyId.value);
toast.add({
severity: 'success',
summary: 'Стратегия запущена',
detail: `Стратегия успешно запущена. Создано задач: ${result.tasksCreated}. Платформы: ${result.platforms?.join(', ') || 'N/A'}`,
life: 5000
});
} catch (error) {
console.error('Ошибка при запуске стратегии:', error);
// Handle specific error codes
if (error.code === 'MISSING_CREDENTIALS') {
// Extract platform from error message (format: "Credentials not found for platform: facebook")
const platformMatch = error.message?.match(/platform:\s*(\w+)/i) || error.message?.match(/for platform\s+(\w+)/i);
if (platformMatch) {
missingPlatforms.value = [platformMatch[1].toLowerCase()];
} else {
// Try to get platforms from strategy data
const platforms = strategyData.value?.priorityPlatforms || strategyData.value?.strategy?.postCalendar?.map(p => p.platform).filter((v, i, a) => a.indexOf(v) === i) || [];
missingPlatforms.value = platforms.length > 0 ? platforms : ['facebook']; // Default to facebook if unknown
}
credentialsDialogVisible.value = true;
} else if (error.code === 'INVALID_STATUS') {
toast.add({
severity: 'warn',
summary: 'Стратегия не готова',
detail: error.message || 'Стратегия еще не завершена. Дождитесь завершения генерации.',
life: 5000
});
} else if (error.code === 'UNAUTHORIZED') {
toast.add({
severity: 'error',
summary: 'Ошибка авторизации',
detail: 'Необходимо войти в систему',
life: 5000
});
} else if (error.code === 'NOT_FOUND') {
toast.add({
severity: 'error',
summary: 'Не найдено',
detail: 'Стратегия не найдена',
life: 5000
});
} else if (error.code === 'FORBIDDEN') {
toast.add({
severity: 'error',
summary: 'Доступ запрещен',
detail: 'У вас нет доступа к этой стратегии',
life: 5000
});
} else if (error.code === 'VALIDATION_ERROR') {
toast.add({
severity: 'error',
summary: 'Ошибка валидации',
detail: error.message || 'Проверьте правильность данных',
life: 5000
});
} else {
toast.add({
severity: 'error',
summary: 'Ошибка',
detail: error.message || 'Не удалось запустить стратегию. Попробуйте позже.',
life: 5000
});
}
} finally {
startingStrategy.value = false;
}
};
// Go to credentials page
const goToCredentials = () => {
credentialsDialogVisible.value = false;
router.push({ name: 'marketing-credentials' });
};
// Format date // Format date
const formatDate = (dateString) => { const formatDate = (dateString) => {
const date = new Date(dateString); const date = new Date(dateString);