Refactor code structure and optimize performance across multiple modules
deploy / deploy (push) Has been cancelled
deploy / deploy (push) Has been cancelled
This commit is contained in:
@@ -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`.
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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` (если есть)
|
||||
- Время запроса
|
||||
- Описание проблемы
|
||||
- Код ошибки (если есть)
|
||||
- Пример запроса (без чувствительных данных)
|
||||
@@ -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`
|
||||
- Время запроса
|
||||
- Описание проблемы
|
||||
- Код ошибки (если есть)
|
||||
@@ -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` в элементы календаря постов при получении стратегии
|
||||
@@ -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` в ответе не пустое
|
||||
@@ -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();
|
||||
};
|
||||
```
|
||||
@@ -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. Обратитесь к разработчикам с описанием проблемы
|
||||
Reference in New Issue
Block a user