Refactor code structure and optimize performance across multiple modules
deploy / deploy (push) Has been cancelled

This commit is contained in:
didar
2026-08-14 16:42:12 +05:00
parent 503eccfb87
commit 8288f9eeb7
2308 changed files with 1185 additions and 180079 deletions
+67
View File
@@ -0,0 +1,67 @@
# API reference
The **authoritative, always-current** reference is the generated OpenAPI spec:
- Swagger UI — `http://localhost:8080/swagger-ui.html`
- Raw spec — `http://localhost:8080/v3/api-docs`
The documents here add what a generated spec cannot: payload semantics, field meanings,
worked examples and frontend integration notes. Where a document and the running
service disagree, **the service is correct** — please fix the document.
## Authentication
Every endpoint except health and public assets expects a JWT issued by the KonturAI
auth service:
```
Authorization: Bearer <token>
```
This service validates the signature and reads the user id, email and roles from the
claims; it does not issue tokens. See [authentication.md](authentication.md).
## Documents
| Document | Endpoints | Covers |
| --- | --- | --- |
| [authentication.md](authentication.md) | — | Extracting user identity from the JWT |
| [marketing-analysis.md](marketing-analysis.md) | `/api/marketing/analysis`, `/api/marketing/v3` | Analysis generation and the full field reference |
| [marketing-strategy.md](marketing-strategy.md) | `/api/marketing/analysis` | Promotion strategy generation |
| [strategy-execution.md](strategy-execution.md) | `/api/marketing/analysis` | Launching and tracking a strategy |
| [posting-tasks.md](posting-tasks.md) | `/api/marketing/analysis` | Scheduling and managing social posts |
| [social-media-credentials.md](social-media-credentials.md) | `/api/social-media/credentials` | Storing per-user network credentials |
| [image-generation.md](image-generation.md) | `/api/marketing` | Generating post imagery |
| [research-reports.md](research-reports.md) | `/api/parser/report` | Research report generation and history |
| [chart-rendering.md](chart-rendering.md) | — | Rendering chart JSON embedded in reports |
## Endpoints without a dedicated document
Use Swagger UI for these:
| Base path | Controller | Purpose |
| --- | --- | --- |
| `/api/parser/health` | `HealthCheckController` | Liveness |
| `/api/parser/items` | `MarketItemController` | Ingested news corpus |
| `/api/parser/admin/parsers` | `ParserAdminController` | Trigger/inspect RSS parsers |
| `/api/targeting` | `AiTargetingSystemController` | AI targeting recommendations |
| `/api/marketing/targeting` | `TargetingCampaignController` | Campaign targeting |
| `/api/facebook/config` | `FacebookConfigController` | Facebook app/page config |
| `/api/facebook/leads` | `FacebookLeadsController` | Collected hot leads |
| `/api/facebook/webhook` | `FacebookWebhookController` | Facebook webhook receiver |
| `/api/openai` | `OpenAITestController` | OpenAI connectivity diagnostics |
## Versioning
Marketing analysis exists in three generations — v1/v2 under `/api/marketing/analysis`
and v3 under `/api/marketing/v3` — kept side by side so older frontend builds keep
working. **v3 is the current path for new integrations.**
Superseded revisions of these documents are preserved in
[../archive/api-history/](../archive/api-history/).
## Errors
All errors are rendered by `GlobalExceptionHandler`, so the payload shape is consistent
across endpoints. Validation failures return `400` with per-field detail; a missing or
invalid token returns `401`.
+309
View File
@@ -0,0 +1,309 @@
# Извлечение информации о пользователе из JWT токена
## Обзор
Данная документация описывает, как извлечь информацию о пользователе из JWT токена в микросервисе на Spring Boot.
## Структура JWT токена
JWT токен содержит следующую информацию:
- **Subject (sub)**: Email пользователя
- **Custom Claims**:
- `uid`: ID пользователя (Long)
- `roles`: Роли пользователя (String, разделённые запятыми, например: "ROLE_USER,ROLE_ADMIN")
- **Стандартные поля**: `iat` (issued at), `exp` (expiration)
## Зависимости
Убедитесь, что в `pom.xml` добавлена зависимость:
```xml
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
```
## Конфигурация
В `application.properties` или `application.yml`:
```properties
security.jwt.secret-base64=<base64-encoded-secret-key>
security.jwt.access-ttl-seconds=3600
```
**Важно**: Используйте тот же `secret-base64`, что и в сервисе, выдающем токены.
## Создание JwtService
```java
package com.example.service;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.security.Key;
@Service
public class JwtService {
private final Key signingKey;
public JwtService(
@Value("${security.jwt.secret-base64}") String base64Secret) {
this.signingKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(base64Secret));
}
public Claims parseAndValidate(String token) {
return Jwts.parserBuilder()
.setSigningKey(signingKey)
.build()
.parseClaimsJws(token)
.getBody();
}
}
```
## Извлечение информации о пользователе
### Вариант 1: Из заголовка Authorization
```java
import io.jsonwebtoken.Claims;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
public class UserController {
private final JwtService jwtService;
public UserController(JwtService jwtService) {
this.jwtService = jwtService;
}
@GetMapping("/user-info")
public ResponseEntity<UserInfo> getUserInfo(
@RequestHeader(HttpHeaders.AUTHORIZATION) String authHeader) {
// Извлекаем токен из заголовка "Bearer <token>"
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return ResponseEntity.status(401).build();
}
String token = authHeader.substring(7);
try {
Claims claims = jwtService.parseAndValidate(token);
// Извлекаем информацию
String email = claims.getSubject();
Long userId = claims.get("uid", Long.class);
String rolesString = claims.get("roles", String.class);
// Парсим роли
List<String> roles = rolesString == null || rolesString.isBlank()
? List.of()
: Arrays.stream(rolesString.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.collect(Collectors.toList());
UserInfo userInfo = new UserInfo(userId, email, roles);
return ResponseEntity.ok(userInfo);
} catch (Exception e) {
// Токен невалиден или истёк
return ResponseEntity.status(401).build();
}
}
}
```
### Вариант 2: Использование Spring Security (рекомендуется)
Если в вашем микросервисе настроен Spring Security с JWT фильтром, используйте `Principal`:
```java
import java.security.Principal;
import org.springframework.security.access.prepost.PreAuthorize;
@RestController
@RequestMapping("/api")
public class UserController {
private final JwtService jwtService;
public UserController(JwtService jwtService) {
this.jwtService = jwtService;
}
@GetMapping("/me")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<UserInfo> getCurrentUser(Principal principal) {
// Principal.getName() возвращает subject (email) из JWT
String email = principal.getName();
// Если нужны дополнительные данные (uid, roles),
// можно извлечь их из SecurityContext или извлечь токен из запроса
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
// Или получить токен из запроса и распарсить
// (см. Вариант 1 для полного извлечения всех claims)
return ResponseEntity.ok(new UserInfo(null, email, List.of()));
}
}
```
### Вариант 3: Полное извлечение через HttpServletRequest
```java
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
public class UserController {
private final JwtService jwtService;
@GetMapping("/profile")
public ResponseEntity<UserInfo> getProfile(HttpServletRequest request) {
String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return ResponseEntity.status(401).build();
}
String token = authHeader.substring(7);
Claims claims = jwtService.parseAndValidate(token);
String email = claims.getSubject();
Long userId = claims.get("uid", Long.class);
String rolesString = claims.get("roles", String.class);
List<String> roles = parseRoles(rolesString);
return ResponseEntity.ok(new UserInfo(userId, email, roles));
}
private List<String> parseRoles(String rolesString) {
if (rolesString == null || rolesString.isBlank()) {
return List.of();
}
return Arrays.stream(rolesString.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.collect(Collectors.toList());
}
}
```
## DTO для пользователя
```java
public record UserInfo(
Long userId,
String email,
List<String> roles
) {}
```
## Обработка ошибок
```java
@ControllerAdvice
public class JwtExceptionHandler {
@ExceptionHandler(JwtException.class)
public ResponseEntity<ErrorResponse> handleJwtException(JwtException e) {
return ResponseEntity.status(401)
.body(new ErrorResponse("Invalid or expired token", 401));
}
}
```
## Пример использования в сервисном слое
```java
@Service
public class BusinessService {
private final JwtService jwtService;
public BusinessService(JwtService jwtService) {
this.jwtService = jwtService;
}
public void processRequest(String token) {
Claims claims = jwtService.parseAndValidate(token);
Long userId = claims.get("uid", Long.class);
String email = claims.getSubject();
// Используйте userId и email для бизнес-логики
// ...
}
}
```
## Важные замечания
1. **Валидация токена**: Метод `parseAndValidate` автоматически проверяет:
- Подпись токена
- Срок действия (expiration)
- Формат токена
2. **Безопасность**: Никогда не логируйте полный JWT токен или секретный ключ.
3. **Секретный ключ**: Должен совпадать с ключом в сервисе, выдающем токены.
4. **Обработка исключений**: `JwtException` и его подклассы (`ExpiredJwtException`, `MalformedJwtException`, и т.д.) должны обрабатываться корректно.
## Примеры исключений
- `ExpiredJwtException`: Токен истёк
- `MalformedJwtException`: Неверный формат токена
- `SignatureException`: Неверная подпись
- `UnsupportedJwtException`: Неподдерживаемый тип токена
## Тестирование
```java
@SpringBootTest
class JwtServiceTest {
@Autowired
private JwtService jwtService;
@Test
void testParseToken() {
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
Claims claims = jwtService.parseAndValidate(token);
assertEquals("user@example.com", claims.getSubject());
assertEquals(123L, claims.get("uid", Long.class));
assertEquals("ROLE_USER", claims.get("roles", String.class));
}
}
```
+124
View File
@@ -0,0 +1,124 @@
## Документация для фронтенда: как отображать графики из маркетингового анализа
Сервис возвращает данные для графиков в двух местах:
- **`report.chartsData`** (объект-словарь ключ → данные графика/таблицы)
- **`report.fullAnalysis`** (markdown-текст), где данные вставлены **инлайн** как fenced-блоки:
- формат: ```json:<key> … ```
Фронтенд может рендерить графики либо **по `chartsData` (проще)**, либо **инлайн** — парся `fullAnalysis` и заменяя ` ```json:<key>` на React-компоненты.
---
### Ключи `json:<key>`, которые нужно поддержать
#### Chart.js config (рендерить через Chart.js / react-chartjs-2)
- **`seasonality`**: сезонность (Chart.js config)
- **`audienceAge`**: возрастное распределение (Chart.js config)
- **`audienceGender`**: гендерное распределение (Chart.js config)
- **`marketShareChart`**: доли рынка (Chart.js config)
- **`channelsPotential`**: потенциал каналов (Chart.js config)
- **`funnel`**: воронка (Chart.js config)
Примечание: в `chartsData` ключ может быть `conversionFunnel`, но в тексте `fullAnalysis` блок идёт как `json:funnel`.
#### Таблица (рендерить табличным компонентом)
- **`comparisonTable`**: сравнительная таблица конкурентов (обычно `Array<Object>`)
---
### Формат данных для графиков (Chart.js config)
Ваш пример — это **валидный Chart.js config** (минимально нужные поля: `labels`, `datasets[]`):
```json
{
"datasets": [
{
"data": [25, 30, 20, 25],
"label": "Возрастные группы"
}
],
"labels": ["18-24", "25-34", "35-44", "45+"]
}
```
Рендеринг (пример для Bar):
```ts
<Bar data={chartJson} />
```
---
### Инлайн-рендеринг из `fullAnalysis` (React + react-markdown)
Идея: перехватить `code`-блоки, найти `json:<key>`, распарсить JSON и заменить на компонент.
```tsx
import React from "react";
import ReactMarkdown from "react-markdown";
function parseInfoString(className?: string) {
// react-markdown обычно кладёт info string как className вида:
// "language-json:audienceAge" или "language-json:marketShareChart"
const m = /language-([^ ]+)/.exec(className || "");
if (!m) return null;
const raw = m[1]; // "json:audienceAge"
const idx = raw.indexOf(":");
if (idx === -1) return { lang: raw, key: null };
return { lang: raw.slice(0, idx), key: raw.slice(idx + 1) };
}
export function ReportMarkdown({ markdown }: { markdown: string }) {
return (
<ReactMarkdown
components={{
code({ inline, className, children, ...props }) {
if (inline) return <code className={className} {...props}>{children}</code>;
const info = parseInfoString(className);
if (!info || info.lang !== "json" || !info.key) {
return <pre><code className={className} {...props}>{children}</code></pre>;
}
const raw = String(children).replace(/\n$/, "");
let data: any;
try {
data = JSON.parse(raw);
} catch {
return <pre><code className={className} {...props}>{children}</code></pre>;
}
switch (info.key) {
case "seasonality":
return <SeasonalityChart data={data} />;
case "audienceAge":
return <AudienceAgeChart data={data} />;
case "audienceGender":
return <AudienceGenderChart data={data} />;
case "marketShareChart":
return <MarketShareChart data={data} />;
case "channelsPotential":
return <ChannelsPotentialChart data={data} />;
case "funnel":
return <FunnelChart data={data} />;
case "comparisonTable":
return <ComparisonTable data={data} />;
default:
return <pre><code className={className} {...props}>{children}</code></pre>;
}
},
}}
>
{markdown}
</ReactMarkdown>
);
}
```
---
### Рекомендации по устойчивости
- **JSON.parse**: всегда `trim`/убирайте trailing newline (`replace(/\n$/, "")`).
- **Fallback**: для неизвестных ключей оставляйте `<pre>` (чтобы ничего не “ломалось”).
- **Таблицы**: `comparisonTable` лучше рендерить как таблицу, а не Chart.js.
+574
View File
@@ -0,0 +1,574 @@
# Документация для фронтенда: Генерация изображений для постов
## Обзор изменений
В систему добавлена автоматическая генерация изображений для постов в социальных сетях. Теперь при создании маркетинговой стратегии для каждого поста автоматически генерируется уникальное изображение с помощью OpenAI DALL-E API. Изображения сохраняются в MinIO и прикрепляются к постам при публикации в Facebook.
---
## Новые поля в API
### 1. PostCalendarItem (Календарь постов)
В объекте `PostCalendarItem` добавлены два новых поля для работы с изображениями:
| Поле | Тип | Описание | Обязательное |
| --------------- | ------ | ------------------------------------------------------------ | ------------ |
| `imageUrl` | string | Имя файла изображения (используется для получения через API) | Нет |
| `imageFilename` | string | Имя файла изображения | Нет |
**Важно:**
- Поля могут быть `null`, если генерация изображения не удалась
- В этом случае пост публикуется без изображения
- Оба поля содержат одинаковое значение (имя файла)
- Для получения изображения используйте API эндпоинт `/api/marketing/analysis/images/{imageFilename}`
### 2. PostingTask (Задачи публикации)
В объекте `PostingTask` также добавлены поля для изображений:
| Поле | Тип | Описание | Обязательное |
| --------------- | ------ | ------------------------------------------------------------ | ------------ |
| `imageUrl` | string | Имя файла изображения (используется для получения через API) | Нет |
| `imageFilename` | string | Имя файла изображения | Нет |
---
## Изменения в API эндпоинтах
### GET `/api/marketing/strategy/{strategyId}`
Ответ теперь включает поля изображений в каждом элементе календаря постов.
#### Пример ответа
```json
{
"success": true,
"data": {
"strategyId": "507f1f77bcf86cd799439011",
"analysisId": "507f1f77bcf86cd799439012",
"status": "completed",
"strategy": {
"postCalendar": [
{
"publishDate": "2024-01-15T10:00:00",
"platform": "Facebook",
"contentType": "пост",
"theme": "Презентация нового продукта",
"postText": "Мы рады представить наш новый продукт...",
"hashtags": ["#новинка", "#продукт", "#маркетинг"],
"publishTime": "10:00",
"imageUrl": "post_image_1705312800000_1234567890.png",
"imageFilename": "post_image_1705312800000_1234567890.png"
}
]
}
}
}
```
### GET `/api/marketing/analysis/{analysisId}/strategy`
Аналогично, ответ включает поля изображений в календаре постов.
---
## Получение изображений
### Через бэкенд API
Все изображения должны получаться через бэкенд API. Прямой доступ к MinIO с фронтенда не предусмотрен.
#### Эндпоинт для получения изображения
**GET** `/api/marketing/analysis/images/{imageFilename}`
Возвращает изображение поста в формате PNG.
#### Параметры пути
| Параметр | Тип | Описание |
| --------------- | ------ | --------------------- |
| `imageFilename` | string | Имя файла изображения |
#### Заголовки запроса
| Заголовок | Тип | Обязательный | Описание |
| --------------- | ------ | ------------ | ------------------------------------ |
| `Authorization` | string | Да | JWT токен в формате `Bearer {token}` |
#### Пример запроса
```javascript
const imageUrl = `/api/marketing/analysis/images/${post.imageFilename}`;
fetch(imageUrl, {
headers: {
Authorization: `Bearer ${token}`,
},
})
.then((response) => {
if (response.ok) {
return response.blob();
}
throw new Error('Failed to load image');
})
.then((blob) => {
const imageObjectUrl = URL.createObjectURL(blob);
// Используйте imageObjectUrl для отображения
});
```
#### Пример ответа
- **Успешный ответ (200 OK):**
- Content-Type: `image/png`
- Body: бинарные данные изображения PNG
- **Ошибка 401 Unauthorized:**
- Токен отсутствует или невалиден
- **Ошибка 404 Not Found:**
- Изображение не найдено на сервере
- **Ошибка 500 Internal Server Error:**
- Внутренняя ошибка сервера при загрузке изображения
#### Кэширование
Сервер возвращает заголовок `Cache-Control: public, max-age=3600`, что позволяет браузеру кэшировать изображения на 1 час.
---
## Рекомендации по отображению
### 1. Проверка наличия изображения
Всегда проверяйте наличие изображения перед отображением:
```javascript
// Пример на JavaScript/TypeScript
const PostCard = ({ post, apiBaseUrl, authToken }) => {
const hasImage = post.imageUrl && post.imageFilename;
const imageUrl = hasImage
? `${apiBaseUrl}/api/marketing/analysis/images/${post.imageFilename}`
: null;
return (
<div className='post-card'>
<h3>{post.theme}</h3>
<p>{post.postText}</p>
{imageUrl ? (
<img
src={imageUrl}
alt={post.theme}
onError={(e) => {
// Fallback если изображение не загрузилось
e.target.style.display = 'none';
}}
// Если требуется авторизация, используйте fetch с заголовками
/>
) : (
<div className='no-image-placeholder'>Изображение не сгенерировано</div>
)}
<div className='hashtags'>
{post.hashtags.map((tag) => (
<span key={tag}>#{tag}</span>
))}
</div>
</div>
);
};
```
### 2. Обработка ошибок загрузки
Всегда предусматривайте fallback для случаев, когда:
- Изображение не сгенерировано (`imageUrl` = `null`)
- Изображение не найдено на сервере
- Ошибка при загрузке изображения
```javascript
const [imageError, setImageError] = useState(false);
const getImageUrl = (imageFilename) => {
if (!imageFilename) return null;
return `${API_BASE_URL}/api/marketing/analysis/images/${imageFilename}`;
};
const handleImageError = () => {
setImageError(true);
};
return (
<>
{post.imageUrl && !imageError ? (
<img
src={getImageUrl(post.imageFilename)}
alt={post.theme}
onError={handleImageError}
/>
) : (
<div className='image-placeholder'>
<Icon name='image' />
<span>Изображение недоступно</span>
</div>
)}
</>
);
```
### 3. Оптимизация загрузки
Рекомендуется использовать lazy loading для изображений:
```javascript
<img
src={
post.imageUrl
? `${API_BASE_URL}/api/marketing/analysis/images/${post.imageFilename}`
: null
}
alt={post.theme}
loading='lazy'
className='post-image'
/>
```
### 4. Размеры изображений
Изображения генерируются в размере **1024x1024 пикселей** (квадратные). При отображении учитывайте это при настройке CSS:
```css
.post-image {
width: 100%;
max-width: 512px;
height: auto;
border-radius: 8px;
object-fit: cover;
}
```
---
## Примеры использования
### React компонент для отображения поста
```typescript
import React, { useState } from 'react';
interface PostCalendarItem {
publishDate: string;
platform: string;
contentType: string;
theme: string;
postText: string;
hashtags: string[];
publishTime: string;
imageUrl?: string | null;
imageFilename?: string | null;
}
interface PostCardProps {
post: PostCalendarItem;
apiBaseUrl: string;
}
const PostCard: React.FC<PostCardProps> = ({ post, apiBaseUrl }) => {
const [imageError, setImageError] = useState(false);
const getImageUrl = (): string | null => {
if (!post.imageFilename) return null;
return `${apiBaseUrl}/api/marketing/analysis/images/${post.imageFilename}`;
};
const imageUrl = getImageUrl();
return (
<div className='post-card'>
<div className='post-header'>
<span className='platform-badge'>{post.platform}</span>
<span className='content-type'>{post.contentType}</span>
<span className='publish-time'>{post.publishTime}</span>
</div>
<h3 className='post-theme'>{post.theme}</h3>
{imageUrl && !imageError ? (
<div className='post-image-container'>
<img
src={imageUrl}
alt={post.theme}
className='post-image'
loading='lazy'
onError={() => setImageError(true)}
/>
</div>
) : (
<div className='no-image-placeholder'>
<svg width='64' height='64' viewBox='0 0 24 24' fill='none'>
<path
d='M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z'
fill='currentColor'
/>
</svg>
<p>Изображение не сгенерировано</p>
</div>
)}
<p className='post-text'>{post.postText}</p>
<div className='post-hashtags'>
{post.hashtags.map((tag, index) => (
<span key={index} className='hashtag'>
{tag.startsWith('#') ? tag : `#${tag}`}
</span>
))}
</div>
<div className='post-footer'>
<span className='publish-date'>
{new Date(post.publishDate).toLocaleDateString('ru-RU')}
</span>
</div>
</div>
);
};
export default PostCard;
```
### Vue компонент
```vue
<template>
<div class="post-card">
<div class="post-header">
<span class="platform-badge">{{ post.platform }}</span>
<span class="content-type">{{ post.contentType }}</span>
<span class="publish-time">{{ post.publishTime }}</span>
</div>
<h3 class="post-theme">{{ post.theme }}</h3>
<div v-if="imageUrl && !imageError" class="post-image-container">
<img
:src="imageUrl"
:alt="post.theme"
class="post-image"
loading="lazy"
@error="imageError = true"
/>
</div>
<div v-else class="no-image-placeholder">
<Icon name="image" />
<p>Изображение не сгенерировано</p>
</div>
<p class="post-text">{{ post.postText }}</p>
<div class="post-hashtags">
<span v-for="(tag, index) in post.hashtags" :key="index" class="hashtag">
{{ tag.startsWith('#') ? tag : `#${tag}` }}
</span>
</div>
<div class="post-footer">
<span class="publish-date">
{{ formatDate(post.publishDate) }}
</span>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
interface PostCalendarItem {
publishDate: string;
platform: string;
contentType: string;
theme: string;
postText: string;
hashtags: string[];
publishTime: string;
imageUrl?: string | null;
imageFilename?: string | null;
}
const props = defineProps<{
post: PostCalendarItem;
apiBaseUrl: string;
}>();
const imageError = ref(false);
const imageUrl = computed(() => {
if (!props.post.imageFilename) return null;
return `${props.apiBaseUrl}/api/marketing/analysis/images/${props.post.imageFilename}`;
});
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('ru-RU');
};
</script>
```
---
## Важные замечания
### 1. Генерация изображений
- Изображения генерируются **автоматически** при создании стратегии
- Процесс генерации может занять время (обычно 10-30 секунд на изображение)
- Если генерация не удалась, пост все равно будет создан, но без изображения
### 2. Хранение изображений
- Все изображения хранятся на бэкенде
- Формат изображений: **PNG**
- Размер изображений: **1024x1024 пикселей**
- Имя файла уникально для каждого поста
- Доступ к изображениям только через API эндпоинт
### 3. Публикация постов
- При публикации поста в Facebook изображение автоматически прикрепляется
- Если изображение отсутствует, пост публикуется только с текстом
- Это не влияет на успешность публикации
### 4. Обратная совместимость
- Старые стратегии, созданные до добавления этой функции, не будут иметь изображений
- Поля `imageUrl` и `imageFilename` будут `null` для таких постов
- Фронтенд должен корректно обрабатывать `null` значения
---
## Конфигурация
Для работы с изображениями вам понадобится базовый URL вашего API:
```typescript
// config.ts
export const API_CONFIG = {
baseUrl: 'http://your-backend-url', // URL вашего бэкенда
// Например: 'http://localhost:8080' или 'https://api.example.com'
};
```
---
## Миграция существующего кода
Если у вас уже есть компоненты для отображения постов, обновите их следующим образом:
1. **Добавьте проверку наличия изображения:**
```typescript
const hasImage = post.imageUrl && post.imageFilename;
```
2. **Добавьте отображение изображения:**
```jsx
{
hasImage && (
<img
src={`${API_BASE_URL}/api/marketing/analysis/images/${post.imageFilename}`}
alt={post.theme}
/>
);
}
```
3. **Обновите типы/интерфейсы:**
```typescript
interface PostCalendarItem {
// ... существующие поля
imageUrl?: string | null;
imageFilename?: string | null;
}
```
---
## Поддержка
При возникновении проблем:
1. Проверьте, что `imageUrl` и `imageFilename` не `null`
2. Убедитесь, что используете правильный API эндпоинт: `/api/marketing/analysis/images/{imageFilename}`
3. Проверьте, что JWT токен валиден и передается в заголовке `Authorization`
4. Проверьте консоль браузера на наличие ошибок сети или авторизации
5. Убедитесь, что используете правильный базовый URL API
---
## Пример полного ответа API
```json
{
"success": true,
"data": {
"strategyId": "507f1f77bcf86cd799439011",
"analysisId": "507f1f77bcf86cd799439012",
"status": "completed",
"createdAt": "2024-01-15T10:00:00",
"completedAt": "2024-01-15T10:05:00",
"durationWeeks": 4,
"priorityPlatforms": ["Facebook", "Instagram"],
"strategy": {
"weeklyPlans": [
{
"weekNumber": 1,
"mainThemes": ["Презентация продукта", "Преимущества"],
"contentRecommendations": "Создавайте контент...",
"priorityPlatforms": ["Facebook"]
}
],
"postCalendar": [
{
"publishDate": "2024-01-16T10:00:00",
"platform": "Facebook",
"contentType": "пост",
"theme": "Презентация нового продукта",
"postText": "Мы рады представить наш новый продукт, который поможет вам...",
"hashtags": ["#новинка", "#продукт", "#маркетинг"],
"publishTime": "10:00",
"imageUrl": "post_image_1705312800000_1234567890.png",
"imageFilename": "post_image_1705312800000_1234567890.png"
},
{
"publishDate": "2024-01-18T14:00:00",
"platform": "Instagram",
"contentType": "сторис",
"theme": "Преимущества продукта",
"postText": "Узнайте о главных преимуществах нашего продукта...",
"hashtags": ["#преимущества", "#качество"],
"publishTime": "14:00",
"imageUrl": null,
"imageFilename": null
}
]
}
}
}
```
Обратите внимание, что второй пост не имеет изображения (`imageUrl` и `imageFilename` равны `null`). Это нормальная ситуация, если генерация изображения не удалась.
---
**Дата обновления:** 2024-01-15
**Версия API:** 1.0
+963
View File
@@ -0,0 +1,963 @@
# API Документация: Обновленные поля для генерации бизнес-анализа (Frontend/AI Agent)
## Обзор изменений
API для генерации маркетингового анализа был обновлен с новыми полями, которые более точно отражают требования бизнес-анализа. Все старые поля были заменены новыми для улучшения качества анализа.
---
## Изменения в структуре запроса
### Удаленные поля (больше не используются)
Следующие поля были **удалены** из API и больше не принимаются:
-`location` - заменено на `region`
-`client` - заменено на `targetAudience`
-`differentiator` - заменено на комбинацию `businessNiche`, `strongSide`, `weakSide`
### Новые обязательные поля
| Поле | Тип | Обязательный | Описание | Пример значения |
| ---------------- | ------ | ------------ | -------------------------------------- | ------------------------------------------------------ |
| `businessNiche` | string | ✅ | Ниша бизнеса | "E-commerce платформы" |
| `product` | string | ✅ | Продукт или услуга | "Разработка мобильных приложений" |
| `targetAudience` | string | ✅ | Целевая аудитория (детальное описание) | "Малый и средний бизнес, владельцы интернет-магазинов" |
| `region` | string | ✅ | Регион (город Казахстана) | "Алматы" |
| `goal` | string | ✅ | Цель на 6-12 месяцев | "Увеличить количество клиентов на 50%" |
| `detailLevel` | string | ✅ | Уровень детализации анализа | "СТАНДАРТНО" |
| `analysisType` | string | ✅ | Тип анализа | "РЫНОК" |
### Новые опциональные поля
| Поле | Тип | Обязательный | Описание | Пример значения |
| ------------ | ------ | ------------ | ----------------------- | ------------------------------------------------- |
| `strongSide` | string | ❌ | Сильная сторона бизнеса | "Опытная команда разработчиков, быстрая доставка" |
| `weakSide` | string | ❌ | Слабая сторона бизнеса | "Ограниченный маркетинговый бюджет" |
---
## Детальное описание полей
### 1. `businessNiche` (обязательное)
**Описание**: Ниша бизнеса, в которой работает компания.
**Валидация**:
- Минимальная длина: 3 символа
- Максимальная длина: 200 символов
- Разрешены: буквы, цифры, пробелы, дефисы, запятые
- Паттерн: `^[\p{L}\p{N}\s\-,]+$`
**Примеры**:
```json
"businessNiche": "E-commerce платформы"
"businessNiche": "Образовательные технологии"
"businessNiche": "Финансовые услуги для малого бизнеса"
```
---
### 2. `product` (обязательное)
**Описание**: Конкретный продукт или услуга, которую предоставляет компания.
**Валидация**:
- Минимальная длина: 3 символа
- Максимальная длина: 200 символов
- Разрешены: буквы, цифры, пробелы, дефисы, запятые
- Паттерн: `^[\p{L}\p{N}\s\-,]+$`
**Примеры**:
```json
"product": "Разработка мобильных приложений"
"product": "Консультации по маркетингу"
"product": "Веб-разработка и дизайн"
```
---
### 3. `targetAudience` (обязательное)
**Описание**: Структурированное описание целевой аудитории в формате JSON. Позволяет выбрать гендер, возрастные диапазоны и типы аудитории. Можно выбрать несколько вариантов в каждой категории.
**Валидация**:
- Должно быть объектом JSON с полями `genders`, `ageRanges`, `types`
- Хотя бы одно поле должно быть заполнено и содержать непустой массив
- Каждое поле должно содержать только допустимые значения
**Структура**:
```json
{
"genders": ["Женщины", "Мужчины"],
"ageRanges": ["20-40", "25-45"],
"types": ["Семьи", "Молодёжь"]
}
```
**Допустимые значения**:
- `genders`: `["Женщины", "Мужчины"]` - можно выбрать один или оба
- `ageRanges`: `["20-40", "25-45", "18-25", "40-60", "60+"]` - можно выбрать один или несколько диапазонов
- `types`: `["Семьи", "Молодёжь", "Все подряд"]` - можно выбрать один или несколько типов
**Примеры**:
```json
"targetAudience": {
"genders": ["Женщины"],
"ageRanges": ["20-40"],
"types": ["Молодёжь"]
}
```
```json
"targetAudience": {
"genders": ["Женщины", "Мужчины"],
"ageRanges": ["25-45", "40-60"],
"types": ["Семьи"]
}
```
```json
"targetAudience": {
"genders": ["Мужчины"],
"ageRanges": ["18-25"],
"types": ["Молодёжь", "Все подряд"]
}
```
**Примечание**: Можно выбрать несколько вариантов в каждой категории. Все выбранные значения будут отражены в анализе.
---
### 4. `region` (обязательное)
**Описание**: Регионы (города Казахстана), в которых работает бизнес. Можно выбрать один или несколько регионов. Это поле заменяет старое поле `location`.
**Валидация**:
- Должно быть массивом строк
- Минимум один регион должен быть выбран
- Каждый регион должен быть одним из допустимых городов Казахстана
- Проверка выполняется через валидатор `@ValidRegion`
**Допустимые значения** (точное совпадение):
- `"Алматы"`
- `"Астана"`
- `"Шымкент"`
- `"Караганда"`
- `"Актобе"`
- `"Тараз"`
- `"Павлодар"`
- `"Усть-Каменогорск"`
- `"Семей"`
- `"Костанай"`
- `"Кызылорда"`
- `"Уральск"`
- `"Петропавловск"`
- `"Атырау"`
- `"Актау"`
- `"Туркестан"`
- `"Кокшетау"`
- `"Талдыкорган"`
- `"Экибастуз"`
- `"Рудный"`
**Примеры**:
```json
"region": ["Алматы"]
```
```json
"region": ["Алматы", "Астана", "Шымкент"]
```
```json
"region": ["Астана", "Караганда"]
```
**Важно**:
- Значения должны точно совпадать с допустимыми городами (регистр важен)
- Можно выбрать все регионы, перечислив их в массиве
- Все выбранные регионы будут отражены в анализе
---
### 5. `goal` (обязательное)
**Описание**: Цель бизнеса на период 6-12 месяцев. Это новое поле, которое помогает AI лучше понять приоритеты бизнеса.
**Валидация**:
- Минимальная длина: 10 символов
- Максимальная длина: 500 символов
- Разрешены любые символы
**Примеры**:
```json
"goal": "Увеличить количество клиентов на 50% за следующие 6 месяцев"
"goal": "Выйти на рынок соседних регионов и открыть 3 новых филиала"
"goal": "Повысить узнаваемость бренда и увеличить продажи через онлайн-каналы на 30%"
```
---
### 6. `detailLevel` (обязательное)
**Описание**: Уровень детализации анализа. Влияет на объем и глубину генерируемого анализа.
**Валидация**:
- Должно быть одним из допустимых значений
- Проверка выполняется через валидатор `@ValidDetailLevel`
**Допустимые значения** (точное совпадение, регистр важен):
- `"КРАТКО"` - Краткий анализ (1-2 абзаца)
- `"СТАНДАРТНО"` - Стандартный анализ (3-5 абзацев) - **рекомендуется по умолчанию**
- `"ПОДРОБНО"` - Подробный анализ (5-8 абзацев)
**Влияние на анализ**:
| Уровень | Длина ответов | Количество рекомендаций | Детализация стратегии |
| ------------ | ------------- | ----------------------- | ------------------------ |
| `КРАТКО` | 1-2 абзаца | 3-4 рекомендации | Краткая |
| `СТАНДАРТНО` | 3-5 абзацев | 4-6 рекомендаций | Стандартная |
| `ПОДРОБНО` | 5-8 абзацев | 6-8 рекомендаций | Детальная с обоснованием |
**Примеры**:
```json
"detailLevel": "КРАТКО"
"detailLevel": "СТАНДАРТНО"
"detailLevel": "ПОДРОБНО"
```
---
### 7. `strongSide` (опциональное)
**Описание**: Сильная сторона бизнеса. Помогает AI лучше понять конкурентные преимущества.
**Валидация**:
- Максимальная длина: 500 символов
- Разрешены любые символы
- Может быть пустым или отсутствовать
**Примеры**:
```json
"strongSide": "Опытная команда разработчиков с 10+ летним опытом, быстрая доставка проектов"
"strongSide": "Уникальная технология, низкие цены, отличная поддержка клиентов"
```
**Примечание**: Если поле не указано, AI будет анализировать сильные стороны на основе других данных.
---
### 8. `weakSide` (опциональное)
**Описание**: Слабая сторона бизнеса. Помогает AI лучше понять области для улучшения.
**Валидация**:
- Максимальная длина: 500 символов
- Разрешены любые символы
- Может быть пустым или отсутствовать
**Примеры**:
```json
"weakSide": "Ограниченный маркетинговый бюджет, недостаточная узнаваемость бренда"
"weakSide": "Небольшая команда, ограниченные ресурсы для масштабирования"
```
**Примечание**: Если поле не указано, AI будет анализировать слабые стороны на основе других данных.
---
### 9. `analysisType` (обязательное)
**Описание**: Тип(ы) анализа, которые необходимо провести. Можно выбрать один или несколько типов анализа. При выборе нескольких типов будет создан отдельный анализ для каждого типа.
**Валидация**:
- Должно быть массивом строк
- Минимум один тип анализа должен быть выбран
- Каждый тип должен быть одним из допустимых значений
- Проверка выполняется через валидатор `@ValidAnalysisType`
**Допустимые значения**:
- `"РЫНОК"` - Анализ рынка (размер рынка, динамика роста, сегменты, тренды)
- `"КОНКУРЕНТЫ"` - Анализ конкурентов (основные конкуренты, их сильные/слабые стороны, позиционирование)
- `"ЦА"` - Анализ целевой аудитории (демография, психография, потребности, поведение)
- `"КАНАЛЫ"` - Анализ маркетинговых каналов (эффективность каналов, рекомендации по выбору)
- `"SWOT"` - SWOT-анализ (сильные стороны, слабые стороны, возможности, угрозы)
**Примеры**:
```json
"analysisType": ["РЫНОК"]
```
```json
"analysisType": ["РЫНОК", "КОНКУРЕНТЫ", "ЦА"]
```
```json
"analysisType": ["РЫНОК", "КОНКУРЕНТЫ", "ЦА", "КАНАЛЫ", "SWOT"]
```
**Важно**:
- При выборе нескольких типов анализа API создаст отдельные записи анализа для каждого типа
- В ответе будет возвращен массив с информацией о каждом созданном анализе
- Каждый анализ будет обрабатываться независимо и иметь свой статус
---
## Полный пример запроса
### POST `/api/marketing/analysis/start`
**Пример 1: Один тип анализа, один регион**
```json
{
"businessNiche": "E-commerce платформы",
"product": "Разработка мобильных приложений для интернет-магазинов",
"targetAudience": {
"genders": ["Женщины"],
"ageRanges": ["25-45"],
"types": ["Молодёжь"]
},
"region": ["Алматы"],
"goal": "Увеличить количество клиентов на 50% за следующие 6 месяцев и выйти на рынок соседних регионов",
"detailLevel": "СТАНДАРТНО",
"strongSide": "Опытная команда разработчиков с 10+ летним опытом, быстрая доставка проектов, использование современных технологий",
"weakSide": "Ограниченный маркетинговый бюджет, недостаточная узнаваемость бренда в новых регионах",
"analysisType": ["РЫНОК"]
}
```
**Пример 2: Несколько типов анализа, несколько регионов**
```json
{
"businessNiche": "E-commerce платформы",
"product": "Разработка мобильных приложений для интернет-магазинов",
"targetAudience": {
"genders": ["Женщины", "Мужчины"],
"ageRanges": ["25-45", "40-60"],
"types": ["Семьи", "Молодёжь"]
},
"region": ["Алматы", "Астана", "Шымкент"],
"goal": "Увеличить количество клиентов на 50% за следующие 6 месяцев и выйти на рынок соседних регионов",
"detailLevel": "СТАНДАРТНО",
"strongSide": "Опытная команда разработчиков с 10+ летним опытом, быстрая доставка проектов",
"weakSide": "Ограниченный маркетинговый бюджет",
"analysisType": ["РЫНОК", "КОНКУРЕНТЫ", "ЦА"]
}
```
**Пример 3: Все типы анализа**
```json
{
"businessNiche": "E-commerce платформы",
"product": "Разработка мобильных приложений для интернет-магазинов",
"targetAudience": {
"genders": ["Женщины", "Мужчины"],
"ageRanges": ["20-40", "25-45"],
"types": ["Семьи", "Молодёжь", "Все подряд"]
},
"region": ["Алматы", "Астана"],
"goal": "Увеличить количество клиентов на 50% за следующие 6 месяцев",
"detailLevel": "ПОДРОБНО",
"analysisType": ["РЫНОК", "КОНКУРЕНТЫ", "ЦА", "КАНАЛЫ", "SWOT"]
}
```
---
## Примеры использования на фронтенде
### JavaScript/TypeScript
```typescript
interface TargetAudience {
genders?: ('Женщины' | 'Мужчины')[];
ageRanges?: ('20-40' | '25-45' | '18-25' | '40-60' | '60+')[];
types?: ('Семьи' | 'Молодёжь' | 'Все подряд')[];
}
interface MarketingAnalysisRequest {
businessNiche: string;
product: string;
targetAudience: TargetAudience;
region: string[];
goal: string;
detailLevel: 'КРАТКО' | 'СТАНДАРТНО' | 'ПОДРОБНО';
strongSide?: string;
weakSide?: string;
analysisType: ('РЫНОК' | 'КОНКУРЕНТЫ' | 'ЦА' | 'КАНАЛЫ' | 'SWOT')[];
}
// Список допустимых регионов
const VALID_REGIONS = [
'Алматы',
'Астана',
'Шымкент',
'Караганда',
'Актобе',
'Тараз',
'Павлодар',
'Усть-Каменогорск',
'Семей',
'Костанай',
'Кызылорда',
'Уральск',
'Петропавловск',
'Атырау',
'Актау',
'Туркестан',
'Кокшетау',
'Талдыкорган',
'Экибастуз',
'Рудный',
];
// Список уровней детализации
const DETAIL_LEVELS = ['КРАТКО', 'СТАНДАРТНО', 'ПОДРОБНО'] as const;
// Функция для отправки запроса
async function startMarketingAnalysis(
data: MarketingAnalysisRequest
): Promise<MarketingAnalysisResponse | MarketingAnalysisResponse[]> {
const response = await fetch(
'https://api.konturai.kz/api/marketing/analysis/start',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer YOUR_JWT_TOKEN', // Если требуется
},
body: JSON.stringify(data),
}
);
const result = await response.json();
if (!result.success) {
throw new Error(result.error?.message || 'Failed to start analysis');
}
// Если выбрано несколько типов анализа, result.data будет массивом
return result.data;
}
interface MarketingAnalysisResponse {
analysisId: string;
status: string;
estimatedCompletion: string;
message: string;
}
// Пример использования
const result = await startMarketingAnalysis({
businessNiche: 'E-commerce платформы',
product: 'Разработка мобильных приложений',
targetAudience: {
genders: ['Женщины', 'Мужчины'],
ageRanges: ['25-45'],
types: ['Молодёжь']
},
region: ['Алматы', 'Астана'],
goal: 'Увеличить количество клиентов на 50% за следующие 6 месяцев',
detailLevel: 'СТАНДАРТНО',
strongSide: 'Опытная команда, быстрая доставка',
weakSide: 'Ограниченный маркетинговый бюджет',
analysisType: ['РЫНОК', 'КОНКУРЕНТЫ'],
});
// Если выбрано несколько типов анализа, result будет массивом
if (Array.isArray(result)) {
console.log(`Создано ${result.length} анализов`);
result.forEach((analysis, index) => {
console.log(`Анализ ${index + 1}: ${analysis.analysisId}`);
});
} else {
console.log('Анализ создан:', result.analysisId);
}
```
### React компонент с формой
```tsx
import React, { useState } from 'react';
const MarketingAnalysisForm: React.FC = () => {
const [formData, setFormData] = useState<MarketingAnalysisRequest>({
businessNiche: '',
product: '',
targetAudience: {
genders: [],
ageRanges: [],
types: []
},
region: [],
goal: '',
detailLevel: 'СТАНДАРТНО',
strongSide: '',
weakSide: '',
analysisType: [],
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
const analysisId = await startMarketingAnalysis(formData);
console.log('Analysis started:', analysisId);
// Перенаправление на страницу с результатами
} catch (error) {
console.error('Error:', error);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>Ниша бизнеса *</label>
<input
type='text'
value={formData.businessNiche}
onChange={(e) =>
setFormData({ ...formData, businessNiche: e.target.value })
}
required
minLength={3}
maxLength={200}
/>
</div>
<div>
<label>Продукт / услуга *</label>
<input
type='text'
value={formData.product}
onChange={(e) =>
setFormData({ ...formData, product: e.target.value })
}
required
minLength={3}
maxLength={200}
/>
</div>
<div>
<label>Целевая аудитория *</label>
<div>
<div>
<label>Гендер:</label>
{['Женщины', 'Мужчины'].map((gender) => (
<label key={gender}>
<input
type='checkbox'
checked={formData.targetAudience.genders?.includes(gender as any)}
onChange={(e) => {
const genders = formData.targetAudience.genders || [];
if (e.target.checked) {
setFormData({
...formData,
targetAudience: {
...formData.targetAudience,
genders: [...genders, gender as any]
}
});
} else {
setFormData({
...formData,
targetAudience: {
...formData.targetAudience,
genders: genders.filter((g) => g !== gender)
}
});
}
}}
/>
{gender}
</label>
))}
</div>
<div>
<label>Возраст:</label>
{['20-40', '25-45', '18-25', '40-60', '60+'].map((age) => (
<label key={age}>
<input
type='checkbox'
checked={formData.targetAudience.ageRanges?.includes(age as any)}
onChange={(e) => {
const ageRanges = formData.targetAudience.ageRanges || [];
if (e.target.checked) {
setFormData({
...formData,
targetAudience: {
...formData.targetAudience,
ageRanges: [...ageRanges, age as any]
}
});
} else {
setFormData({
...formData,
targetAudience: {
...formData.targetAudience,
ageRanges: ageRanges.filter((a) => a !== age)
}
});
}
}}
/>
{age}
</label>
))}
</div>
<div>
<label>Тип:</label>
{['Семьи', 'Молодёжь', 'Все подряд'].map((type) => (
<label key={type}>
<input
type='checkbox'
checked={formData.targetAudience.types?.includes(type as any)}
onChange={(e) => {
const types = formData.targetAudience.types || [];
if (e.target.checked) {
setFormData({
...formData,
targetAudience: {
...formData.targetAudience,
types: [...types, type as any]
}
});
} else {
setFormData({
...formData,
targetAudience: {
...formData.targetAudience,
types: types.filter((t) => t !== type)
}
});
}
}}
/>
{type}
</label>
))}
</div>
</div>
</div>
<div>
<label>Регион *</label>
<div>
{VALID_REGIONS.map((region) => (
<label key={region}>
<input
type='checkbox'
checked={formData.region.includes(region)}
onChange={(e) => {
if (e.target.checked) {
setFormData({
...formData,
region: [...formData.region, region]
});
} else {
setFormData({
...formData,
region: formData.region.filter((r) => r !== region)
});
}
}}
/>
{region}
</label>
))}
</div>
</div>
<div>
<label>Цель на 6-12 месяцев *</label>
<textarea
value={formData.goal}
onChange={(e) => setFormData({ ...formData, goal: e.target.value })}
required
minLength={10}
maxLength={500}
/>
</div>
<div>
<label>Уровень детализации анализа *</label>
<div>
{DETAIL_LEVELS.map((level) => (
<label key={level}>
<input
type='radio'
name='detailLevel'
value={level}
checked={formData.detailLevel === level}
onChange={(e) =>
setFormData({
...formData,
detailLevel: e.target.value as any,
})
}
/>
{level === 'КРАТКО' && ' Кратко'}
{level === 'СТАНДАРТНО' && ' Стандартно'}
{level === 'ПОДРОБНО' && ' Подробно'}
</label>
))}
</div>
</div>
<div>
<label>Сильная сторона</label>
<textarea
value={formData.strongSide}
onChange={(e) =>
setFormData({ ...formData, strongSide: e.target.value })
}
maxLength={500}
/>
</div>
<div>
<label>Слабая сторона</label>
<textarea
value={formData.weakSide}
onChange={(e) =>
setFormData({ ...formData, weakSide: e.target.value })
}
maxLength={500}
/>
</div>
<div>
<label>Тип анализа *</label>
<div>
{['РЫНОК', 'КОНКУРЕНТЫ', 'ЦА', 'КАНАЛЫ', 'SWOT'].map((type) => (
<label key={type}>
<input
type='checkbox'
checked={formData.analysisType.includes(type as any)}
onChange={(e) => {
if (e.target.checked) {
setFormData({
...formData,
analysisType: [...formData.analysisType, type as any]
});
} else {
setFormData({
...formData,
analysisType: formData.analysisType.filter((t) => t !== type)
});
}
}}
/>
{type === 'РЫНОК' && ' Рынок'}
{type === 'КОНКУРЕНТЫ' && ' Конкуренты'}
{type === 'ЦА' && ' Целевая аудитория'}
{type === 'КАНАЛЫ' && ' Каналы'}
{type === 'SWOT' && ' SWOT'}
</label>
))}
</div>
</div>
<button type='submit'>Сгенерировать анализ бизнеса</button>
</form>
);
};
```
---
## Валидация на клиенте
Рекомендуется выполнять валидацию на клиенте перед отправкой запроса:
```typescript
function validateMarketingAnalysisRequest(data: MarketingAnalysisRequest): {
valid: boolean;
errors: Record<string, string>;
} {
const errors: Record<string, string> = {};
// Валидация businessNiche
if (!data.businessNiche || data.businessNiche.trim().length < 3) {
errors.businessNiche = 'Ниша бизнеса должна содержать минимум 3 символа';
}
if (data.businessNiche && data.businessNiche.length > 200) {
errors.businessNiche = 'Ниша бизнеса не должна превышать 200 символов';
}
// Валидация product
if (!data.product || data.product.trim().length < 3) {
errors.product = 'Продукт должен содержать минимум 3 символа';
}
if (data.product && data.product.length > 200) {
errors.product = 'Продукт не должен превышать 200 символов';
}
// Валидация targetAudience
if (!data.targetAudience || data.targetAudience.trim().length < 3) {
errors.targetAudience =
'Целевая аудитория должна содержать минимум 3 символа';
}
if (data.targetAudience && data.targetAudience.length > 300) {
errors.targetAudience =
'Целевая аудитория не должна превышать 300 символов';
}
// Валидация region
if (!data.region || !VALID_REGIONS.includes(data.region)) {
errors.region = 'Выберите допустимый регион из списка';
}
// Валидация goal
if (!data.goal || data.goal.trim().length < 10) {
errors.goal = 'Цель должна содержать минимум 10 символов';
}
if (data.goal && data.goal.length > 500) {
errors.goal = 'Цель не должна превышать 500 символов';
}
// Валидация detailLevel
if (!data.detailLevel || !DETAIL_LEVELS.includes(data.detailLevel)) {
errors.detailLevel = 'Выберите допустимый уровень детализации';
}
// Валидация strongSide (опциональное)
if (data.strongSide && data.strongSide.length > 500) {
errors.strongSide = 'Сильная сторона не должна превышать 500 символов';
}
// Валидация weakSide (опциональное)
if (data.weakSide && data.weakSide.length > 500) {
errors.weakSide = 'Слабая сторона не должна превышать 500 символов';
}
// Валидация analysisType
const validAnalysisTypes = ['РЫНОК', 'КОНКУРЕНТЫ', 'ЦА', 'КАНАЛЫ', 'SWOT'];
if (!data.analysisType || !validAnalysisTypes.includes(data.analysisType)) {
errors.analysisType = 'Выберите допустимый тип анализа';
}
return {
valid: Object.keys(errors).length === 0,
errors,
};
}
```
---
## Миграция со старого API
Если у вас есть код, использующий старые поля, необходимо обновить его следующим образом:
### Старый формат (больше не работает):
```json
{
"product": "Разработка мобильных приложений",
"location": "Алматы, Казахстан",
"client": "B2B клиенты",
"differentiator": "Быстрая разработка за 2 недели",
"analysisType": "РЫНОК"
}
```
### Новый формат:
```json
{
"businessNiche": "Разработка программного обеспечения",
"product": "Разработка мобильных приложений",
"targetAudience": "B2B клиенты, технологические компании, стартапы",
"region": "Алматы",
"goal": "Увеличить количество клиентов на 50% за следующие 6 месяцев",
"detailLevel": "СТАНДАРТНО",
"strongSide": "Быстрая разработка за 2 недели, опытная команда",
"weakSide": "",
"analysisType": "РЫНОК"
}
```
### Маппинг старых полей на новые:
| Старое поле | Новое поле(я) | Примечание |
| ---------------- | ----------------------------------------- | ------------------------------------------------ |
| `location` | `region` | Только название города из списка |
| `client` | `targetAudience` | Более детальное описание (свободный текст) |
| `differentiator` | `businessNiche`, `strongSide`, `weakSide` | Разделено на несколько полей для лучшего анализа |
---
## Обработка ошибок валидации
При ошибках валидации API возвращает следующий формат:
```json
{
"success": false,
"message": "Ошибка валидации",
"error": {
"code": "VALIDATION_ERROR",
"message": "Ошибка валидации входных данных",
"details": {
"businessNiche": "Поле 'businessNiche' должно содержать от 3 до 200 символов",
"region": "Поле 'region' должно быть одним из допустимых городов Казахстана",
"detailLevel": "Поле 'detailLevel' должно быть одним из: КРАТКО, СТАНДАРТНО, ПОДРОБНО"
}
}
}
```
---
## Важные замечания
1. **Регистр важен**: Значения `region`, `detailLevel` и `analysisType` чувствительны к регистру. Используйте точные значения из списка допустимых.
2. **Все новые поля передаются в OpenAI**: Все указанные поля включаются в контекст для генерации анализа, что улучшает качество и релевантность результатов.
3. **Уровень детализации влияет на результат**: Выбор `detailLevel` напрямую влияет на объем и глубину генерируемого анализа.
4. **Опциональные поля улучшают анализ**: Хотя `strongSide` и `weakSide` опциональны, их указание помогает AI лучше понять бизнес и дать более точные рекомендации.
5. **Обратная совместимость**: Старые поля больше не поддерживаются. Необходимо обновить все клиентские приложения.
---
## Поддержка
При возникновении проблем с API обращайтесь в техническую поддержку с указанием:
- `analysisId` (если есть)
- Время запроса
- Описание проблемы
- Код ошибки (если есть)
- Пример запроса (без чувствительных данных)
+924
View File
@@ -0,0 +1,924 @@
# 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 |
| `data.strategy.postCalendar[].imageUrl` | string | Путь к изображению в MinIO (может быть null) |
| `data.strategy.postCalendar[].imageFilename` | string | Имя файла изображения в MinIO (может быть null) |
#### Пример ошибки (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 <div>Генерация стратегии...</div>;
}
if (error) {
return <div>Ошибка: {error}</div>;
}
if (!strategy || !strategy.strategy) {
return <div>Стратегия не найдена</div>;
}
return (
<div className='strategy-view'>
<h2>Стратегия продвижения</h2>
{/* Недельный план */}
<section className='weekly-plans'>
<h3>Недельный план</h3>
{strategy.strategy.weeklyPlans.map((plan) => (
<div key={plan.weekNumber} className='week-plan'>
<h4>Неделя {plan.weekNumber}</h4>
<div className='themes'>
<strong>Темы:</strong>
<ul>
{plan.mainThemes.map((theme, idx) => (
<li key={idx}>{theme}</li>
))}
</ul>
</div>
<div className='recommendations'>
<strong>Рекомендации:</strong>
<p>{plan.contentRecommendations}</p>
</div>
<div className='platforms'>
<strong>Платформы:</strong>
{plan.priorityPlatforms.join(', ')}
</div>
</div>
))}
</section>
{/* Календарь постов */}
<section className='post-calendar'>
<h3>Календарь постов</h3>
<div className='calendar-grid'>
{strategy.strategy.postCalendar.map((post, idx) => (
<div key={idx} className='post-item'>
<div className='post-header'>
<span className='date'>
{new Date(post.publishDate).toLocaleDateString('ru-RU')}
</span>
<span className='time'>{post.publishTime}</span>
<span className='platform'>{post.platform}</span>
<span className='content-type'>{post.contentType}</span>
</div>
<div className='post-theme'>
<strong>Тема:</strong> {post.theme}
</div>
<div className='post-text'>{post.postText}</div>
<div className='post-hashtags'>
{post.hashtags.map((tag, tagIdx) => (
<span key={tagIdx} className='hashtag'>
{tag}
</span>
))}
</div>
</div>
))}
</div>
</section>
</div>
);
}
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 (
<div className='post-calendar'>
<h3>Календарь публикаций</h3>
{Object.entries(postsByDate).map(([date, posts]) => (
<div key={date} className='date-group'>
<h4>{date}</h4>
{posts.map((post, idx) => (
<div key={idx} className='post-card'>
<div className='post-meta'>
<span className='platform-badge'>{post.platform}</span>
<span className='content-type-badge'>{post.contentType}</span>
<span className='time'>{post.publishTime}</span>
</div>
<div className='post-content'>
<h5>{post.theme}</h5>
<p>{post.postText}</p>
<div className='hashtags'>
{post.hashtags.map((tag, tagIdx) => (
<span key={tagIdx} className='hashtag'>
{tag}
</span>
))}
</div>
</div>
</div>
))}
</div>
))}
</div>
);
}
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`
- Время запроса
- Описание проблемы
- Код ошибки (если есть)
+286
View File
@@ -0,0 +1,286 @@
# API Документация: Управление задачами публикации (Frontend/AI Agent)
## Базовый URL
```
https://api.konturai.kz
```
## Обзор
API для управления задачами публикации постов в социальных сетях. Позволяет запускать задачи публикации вручную, не дожидаясь времени выполнения через шедулер.
**Важно**:
- Для выполнения задачи требуются настроенные credentials для соответствующей платформы
- Задачи создаются автоматически при запуске стратегии через `/api/marketing/analysis/strategy/{strategyId}/start`
- Каждая задача связана с элементом календаря постов в стратегии
---
## Изменения в существующих эндпоинтах
### Обновление: Получение стратегии по ID анализа
**GET** `/api/marketing/analysis/{analysisId}/strategy`
Теперь каждый элемент в `postCalendar` содержит поле `taskId`, если задача была создана для этого элемента.
#### Пример ответа (обновленный формат)
```json
{
"success": true,
"data": {
"strategyId": "507f1f77bcf86cd799439012",
"analysisId": "507f1f77bcf86cd799439011",
"status": "completed",
"strategy": {
"postCalendar": [
{
"publishDate": "2025-01-25T10:00:00",
"platform": "Facebook",
"contentType": "пост",
"theme": "Презентация продукта",
"postText": "Добро пожаловать в наш новый продукт!",
"hashtags": ["#маркетинг", "#бизнес"],
"publishTime": "10:00",
"imageUrl": "post_image_1234567890.png",
"imageFilename": "post_image_1234567890.png",
"taskId": "507f1f77bcf86cd799439013"
}
]
}
}
}
```
**Новое поле:**
- `taskId` (string, опциональное) - ID задачи публикации, если задача была создана. Может быть `null`, если стратегия еще не была запущена.
---
## Новые эндпоинты
### 1. Ручной запуск задачи публикации
**POST** `/api/marketing/analysis/tasks/{taskId}/execute`
Запускает задачу публикации немедленно, не дожидаясь времени публикации через шедулер. Позволяет выполнить задачу вручную или повторить выполнение неудачной задачи.
#### Параметры запроса
| Параметр | Тип | Расположение | Обязательный | Описание |
| -------- | ------ | ------------ | ------------ | -------------------- |
| `taskId` | string | Path | ✅ | ID задачи публикации |
#### Заголовки запроса
```
Authorization: Bearer <your-jwt-token>
Content-Type: application/json
```
#### Пример запроса
```http
POST /api/marketing/analysis/tasks/507f1f77bcf86cd799439013/execute
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
```
#### Пример успешного ответа (200 OK)
```json
{
"success": true,
"message": "Задача успешно запущена",
"data": {
"taskId": "507f1f77bcf86cd799439013",
"status": "processing",
"platform": "Facebook",
"publishDate": "2025-01-25T10:00:00"
}
}
```
#### Описание полей ответа
| Поле | Тип | Описание |
| ------------- | ------ | ----------------------------------------------------- |
| `taskId` | string | ID задачи публикации |
| `status` | string | Статус задачи: `processing`, `completed` или `failed` |
| `platform` | string | Платформа для публикации (например, `Facebook`) |
| `publishDate` | string | Дата и время публикации в формате ISO 8601 |
#### Ошибки
**401 Unauthorized** - Требуется аутентификация
```json
{
"success": false,
"message": "Не авторизован",
"error": {
"code": "UNAUTHORIZED",
"message": "Требуется аутентификация. Пожалуйста, предоставьте валидный JWT токен."
}
}
```
**403 Forbidden** - Пользователь не является владельцем задачи
```json
{
"success": false,
"message": "Доступ запрещен",
"error": {
"code": "FORBIDDEN",
"message": "У вас нет доступа к этой задаче"
}
}
```
**404 Not Found** - Задача не найдена
```json
{
"success": false,
"message": "Задача не найдена",
"error": {
"code": "NOT_FOUND",
"message": "Задача с указанным ID не найдена"
}
}
```
**400 Bad Request** - Задача не может быть запущена
```json
{
"success": false,
"message": "Задача не может быть запущена",
"error": {
"code": "INVALID_STATUS",
"message": "Task cannot be executed manually. Current status: completed. Only tasks with status 'pending' or 'failed' can be executed manually."
}
}
```
#### Правила выполнения
1. **Статусы задач:**
- `pending` - задача ожидает выполнения (может быть запущена вручную)
- `failed` - задача завершилась с ошибкой (может быть запущена повторно вручную)
- `processing` - задача выполняется (не может быть запущена повторно)
- `completed` - задача успешно выполнена (не может быть запущена повторно)
2. **Повторное выполнение:**
- Задачи со статусом `failed` автоматически сбрасываются на `pending` перед повторным выполнением
- Ошибка из предыдущего выполнения очищается
3. **Асинхронное выполнение:**
- Задача запускается асинхронно
- Ответ возвращается сразу после начала выполнения
- Для проверки статуса задачи используйте соответствующие эндпоинты (если доступны)
#### Примеры использования
**Пример 1: Запуск задачи, которая еще не была выполнена**
```javascript
// Получаем стратегию
const strategyResponse = await fetch(
`/api/marketing/analysis/${analysisId}/strategy`,
{
headers: {
Authorization: `Bearer ${token}`,
},
}
);
const strategy = await strategyResponse.json();
const taskId = strategy.data.strategy.postCalendar[0].taskId;
// Запускаем задачу вручную
const executeResponse = await fetch(
`/api/marketing/analysis/tasks/${taskId}/execute`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
}
);
const result = await executeResponse.json();
console.log('Задача запущена:', result.data);
```
**Пример 2: Повторное выполнение неудачной задачи**
```javascript
// Если задача завершилась с ошибкой (status: "failed")
// можно повторить её выполнение
const retryResponse = await fetch(
`/api/marketing/analysis/tasks/${failedTaskId}/execute`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
}
);
const retryResult = await retryResponse.json();
if (retryResult.success) {
console.log('Повторная попытка запущена:', retryResult.data);
}
```
---
## Интеграция с существующим workflow
### Типичный сценарий использования
1. **Создание анализа**`POST /api/marketing/analysis/start`
2. **Генерация стратегии**`POST /api/marketing/analysis/strategy/generate`
3. **Получение стратегии**`GET /api/marketing/analysis/{analysisId}/strategy`
- Теперь содержит `taskId` для каждого элемента календаря
4. **Запуск стратегии**`POST /api/marketing/analysis/strategy/{strategyId}/start`
- Создает задачи публикации для всех элементов календаря
5. **Ручной запуск задачи** (опционально) → `POST /api/marketing/analysis/tasks/{taskId}/execute`
- Запускает задачу немедленно, не дожидаясь времени публикации
### Когда использовать ручной запуск
- **Тестирование**: Проверить публикацию поста перед запланированным временем
- **Повторная попытка**: Повторить выполнение задачи, которая завершилась с ошибкой
- **Срочная публикация**: Опубликовать пост раньше запланированного времени
- **Отладка**: Проверить работу системы публикации
---
## Примечания
1. **Аутентификация**: Все эндпоинты требуют валидный JWT токен в заголовке `Authorization`
2. **Права доступа**: Пользователь может запускать только свои собственные задачи
3. **Статусы задач**: Проверяйте статус задачи перед попыткой ручного запуска
4. **Асинхронность**: Выполнение задачи происходит асинхронно, ответ возвращается сразу
5. **Ошибки выполнения**: Если задача завершится с ошибкой, её можно запустить повторно
---
## Версия API
- **Версия документа**: 1.0
- **Дата обновления**: 2025-01-20
- **Изменения**:
- Добавлен эндпоинт для ручного запуска задач публикации
- Добавлено поле `taskId` в элементы календаря постов при получении стратегии
+188
View File
@@ -0,0 +1,188 @@
# Руководство по API генерации исследовательских отчётов
## Обзор
Новый эндпоинт `/api/parser/report` интегрирован с сервисом `deep-research` для генерации PDF-отчётов на основе пользовательских запросов.
## Эндпоинт
**POST** `/api/parser/report`
### Параметры запроса
| Параметр | Тип | Обязательный | По умолчанию | Описание |
| ------------- | ------- | ------------ | ------------ | ------------------------------- |
| `query` | string | ✅ | - | Тема для исследования |
| `lang` | string | ❌ | "ru" | Язык отчёта ("ru", "en") |
| `depth` | integer | ❌ | 3 | Глубина исследования (1-5) |
| `breadth` | integer | ❌ | 5 | Широта исследования (2-10) |
| `report_type` | string | ❌ | "report" | Тип отчёта ("report", "answer") |
### Пример запроса
```json
{
"query": "Искусственный интеллект в здравоохранении",
"lang": "ru",
"depth": 3,
"breadth": 5,
"report_type": "report"
}
```
### Ответы
#### Успешный ответ (200 OK)
- **Content-Type**: `application/pdf`
- **Content-Disposition**: `attachment; filename="research_report_[timestamp].pdf"`
- **Тело**: PDF-файл с отчётом
#### Ошибки
| Код | Описание |
| --- | ----------------------------------------- |
| 400 | Некорректные параметры запроса |
| 500 | Внутренняя ошибка сервера |
| 504 | Таймаут при обращении к deep-research API |
## Структура PDF-отчёта
Сгенерированный PDF содержит:
1. **Титульная страница**
- Название исследования (из поля `query`)
- Дата создания
- Подзаголовок "Исследовательский отчёт"
2. **Содержание**
- Автоматически сгенерированное оглавление
3. **Основная часть**
- Введение
- Основной текст отчёта от deep-research API
- Заключение
4. **Источники**
- Список URL-адресов из поля `visitedUrls`
## Конфигурация
Настройки в `application.properties`:
```properties
# Deep Research API Configuration
deep-research.api.url=http://185.35.223.45:3051
deep-research.api.timeout=300000
```
## Примеры использования
### cURL
```bash
curl -X POST "http://localhost:8080/api/parser/report" \
-H "Content-Type: application/json" \
-d '{
"query": "Блокчейн технологии в финансах",
"lang": "ru",
"depth": 4,
"breadth": 6
}' \
--output "blockchain_report.pdf"
```
### JavaScript (fetch)
```javascript
const response = await fetch('/api/parser/report', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: 'Квантовые вычисления',
lang: 'ru',
depth: 3,
breadth: 5,
report_type: 'report',
}),
});
if (response.ok) {
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'quantum_computing_report.pdf';
a.click();
}
```
### Python (requests)
```python
import requests
response = requests.post(
'http://localhost:8080/api/parser/report',
json={
'query': 'Машинное обучение в медицине',
'lang': 'ru',
'depth': 3,
'breadth': 5,
'report_type': 'report'
}
)
if response.status_code == 200:
with open('ml_medicine_report.pdf', 'wb') as f:
f.write(response.content)
print("Отчёт сохранён как ml_medicine_report.pdf")
else:
print(f"Ошибка: {response.status_code}")
```
## Тестирование
Для тестирования API используйте скрипт `test_research_api.sh`:
```bash
./test_research_api.sh
```
## Логирование
Все операции логируются. Для отладки проверьте логи приложения:
```bash
tail -f logs/application.log | grep "DeepResearchService\|ResearchPdfService"
```
## Ограничения
- Максимальное время ожидания: 5 минут (300 секунд)
- Размер генерируемого PDF ограничен только ресурсами сервера
- Deep-research API должен быть доступен по указанному URL
## Устранение неполадок
### Ошибка 504 (Gateway Timeout)
- Проверьте доступность deep-research API
- Увеличьте timeout в конфигурации
- Проверьте сетевые настройки
### Ошибка 500 (Internal Server Error)
- Проверьте логи приложения
- Убедитесь, что все зависимости установлены
- Проверьте конфигурацию MinIO для сохранения файлов
### Пустой PDF
- Проверьте, что deep-research API возвращает корректные данные
- Убедитесь, что поле `report` или `answer` в ответе не пустое
+396
View File
@@ -0,0 +1,396 @@
# Social Media Credentials API - Frontend Guide
## Обзор изменений
API для управления credentials социальных сетей был обновлен для поддержки разных форматов данных в зависимости от платформы. Поле `credentials` теперь может принимать как простую строку (токен), так и JSON объект для платформ, требующих несколько параметров.
## Базовый URL
```
http://your-server:port/api/social-media/credentials
```
## Аутентификация
Все endpoints требуют JWT токен в заголовке `Authorization`:
```
Authorization: Bearer <your-jwt-token>
```
## Изменения в API
### Поле `credentials` теперь поддерживает разные типы
**До изменений:**
- `credentials` был только `String` (токен)
**После изменений:**
- `credentials` может быть `String` (для простых токенов) или `Object` (для сложных структур)
### Поддерживаемые форматы
#### 1. Простой токен (String) - для Facebook, LinkedIn
```json
{
"platform": "facebook",
"credentials": "EAABwzLix..."
}
```
#### 2. JSON объект (Object) - для Telegram и других платформ с несколькими параметрами
```json
{
"platform": "telegram",
"credentials": {
"botToken": "123456789:ABCdefGHIjklMNOpqrsTUVwxyz",
"chatId": "-1001234567890"
}
}
```
## Endpoints
### 1. Сохранение/обновление credentials
**Endpoint:** `POST /api/social-media/credentials`
**Headers:**
```
Authorization: Bearer <token>
Content-Type: application/json
```
**Request Body для простого токена (Facebook/LinkedIn):**
```json
{
"platform": "facebook",
"credentials": "EAABwzLix..."
}
```
**Request Body для JSON объекта (Telegram):**
```json
{
"platform": "telegram",
"credentials": {
"botToken": "123456789:ABCdefGHIjklMNOpqrsTUVwxyz",
"chatId": "-1001234567890"
}
}
```
**Response 200 OK:**
```json
{
"success": true,
"message": "Credentials успешно сохранены для платформы telegram",
"data": {
"platform": "telegram",
"hasCredentials": true,
"createdAt": "2024-01-15T10:30:00",
"updatedAt": "2024-01-15T10:30:00"
}
}
```
**Примеры использования (JavaScript):**
```javascript
// Сохранение простого токена (Facebook/LinkedIn)
const saveFacebookCredentials = async (accessToken) => {
const response = await fetch('/api/social-media/credentials', {
method: 'POST',
headers: {
Authorization: `Bearer ${jwtToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
platform: 'facebook',
credentials: accessToken, // Простая строка
}),
});
const data = await response.json();
return data;
};
// Сохранение JSON объекта (Telegram)
const saveTelegramCredentials = async (botToken, chatId) => {
const response = await fetch('/api/social-media/credentials', {
method: 'POST',
headers: {
Authorization: `Bearer ${jwtToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
platform: 'telegram',
credentials: {
// JSON объект
botToken: botToken,
chatId: chatId,
},
}),
});
const data = await response.json();
return data;
};
// Использование
await saveFacebookCredentials('EAABwzLix...');
await saveTelegramCredentials('123456789:ABC...', '-1001234567890');
```
### 2. Получение информации о 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": "telegram",
"hasCredentials": true
}
}
```
### 3. Получение списка всех 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"
},
{
"platform": "telegram",
"hasCredentials": true,
"createdAt": "2024-01-15T11:00:00",
"updatedAt": "2024-01-15T11:00:00"
}
]
}
```
### 4. Удаление credentials
**Endpoint:** `DELETE /api/social-media/credentials/{platform}`
**Headers:**
```
Authorization: Bearer <token>
```
**Path Parameters:**
- `platform` (string) - Название платформы
**Response 200 OK:**
```json
{
"success": true,
"message": "Credentials для платформы telegram успешно удалены",
"data": null
}
```
## Форматы credentials по платформам
### Facebook
```json
{
"platform": "facebook",
"credentials": "EAABwzLix..." // Access Token
}
```
### LinkedIn
```json
{
"platform": "linkedin",
"credentials": "AQV..." // Access Token
}
```
### Telegram
```json
{
"platform": "telegram",
"credentials": {
"botToken": "123456789:ABCdefGHIjklMNOpqrsTUVwxyz",
"chatId": "-1001234567890"
}
}
```
**Примечание для Telegram:**
- `botToken` - токен бота, полученный от @BotFather
- `chatId` - ID чата/канала (может быть отрицательным для групп и каналов)
- Формат: `-1001234567890` для супергрупп и каналов
## Обработка ошибок
### 400 Bad Request - Ошибка валидации
```json
{
"success": false,
"message": "Ошибка валидации",
"error": {
"code": "VALIDATION_ERROR",
"message": "Platform is required",
"details": {
"platform": "Platform is required"
}
}
}
```
### 401 Unauthorized
```json
{
"success": false,
"message": "Не авторизован",
"error": {
"code": "UNAUTHORIZED",
"message": "Требуется аутентификация. Пожалуйста, предоставьте валидный JWT токен."
}
}
```
### 500 Internal Server Error
```json
{
"success": false,
"message": "Внутренняя ошибка сервера",
"error": {
"code": "INTERNAL_SERVER_ERROR",
"message": "Произошла ошибка при сохранении credentials"
}
}
```
## Миграция с старого API
Если вы использовали старый API, где `credentials` был только строкой, изменения минимальны:
**Старый код:**
```javascript
body: JSON.stringify({
platform: 'facebook',
credentials: 'EAABwzLix...',
});
```
**Новый код (без изменений для простых токенов):**
```javascript
// Работает как раньше
body: JSON.stringify({
platform: 'facebook',
credentials: 'EAABwzLix...', // String по-прежнему поддерживается
});
```
**Для Telegram (новый формат):**
```javascript
body: JSON.stringify({
platform: 'telegram',
credentials: {
// Теперь можно передавать объект
botToken: '...',
chatId: '...',
},
});
```
## Важные замечания
1. **Обратная совместимость**: API полностью обратно совместим. Старый формат (String) продолжает работать.
2. **Автоматическая конвертация**: Если вы передаете объект, он автоматически сериализуется в JSON строку перед сохранением.
3. **Безопасность**: Все credentials автоматически шифруются перед сохранением в базе данных.
4. **Валидация**: Поле `credentials` обязательно (`@NotNull`), но может быть как строкой, так и объектом.
## Примеры для TypeScript
```typescript
interface SimpleCredentials {
platform: string;
credentials: string; // Для Facebook, LinkedIn
}
interface TelegramCredentials {
platform: 'telegram';
credentials: {
botToken: string;
chatId: string;
};
}
type CredentialsRequest = SimpleCredentials | TelegramCredentials;
// Использование
const saveCredentials = async (request: CredentialsRequest) => {
const response = await fetch('/api/social-media/credentials', {
method: 'POST',
headers: {
Authorization: `Bearer ${jwtToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(request),
});
return response.json();
};
```
+763
View File
@@ -0,0 +1,763 @@
# 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` для публикации
**Для запуска рекламных кампаний в Facebook требуется дополнительная настройка:**
📖 **Подробная инструкция:** См. [facebook-setup.md](../facebook-setup.md)
Для рекламы нужны:
- Access Token с разрешениями `ads_management`, `ads_read`, `business_management`
- Ad Account ID (формат: `act_XXXXXXXXX`)
- App ID и App Secret
- Page ID (опционально)
---
## Примеры 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. Обратитесь к разработчикам с описанием проблемы