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. Обратитесь к разработчикам с описанием проблемы
|
||||
@@ -0,0 +1,158 @@
|
||||
# Architecture
|
||||
|
||||
Marketing Parser is a single Spring Boot 3.5.5 service on Java 21, backed by MongoDB
|
||||
for persistence and MinIO for generated artefacts. It exposes a REST API consumed by
|
||||
the KonturAI frontend and runs a set of scheduled background jobs.
|
||||
|
||||
## Functional areas
|
||||
|
||||
The service covers five loosely coupled concerns that share a database and a set of
|
||||
AI clients:
|
||||
|
||||
1. **RSS ingestion** — collects business news into a `MarketItem` corpus.
|
||||
2. **Marketing analysis** — turns that corpus plus user input into structured analysis.
|
||||
3. **Report rendering** — renders analysis as PDF, DOCX and Markdown with charts.
|
||||
4. **Campaign execution** — generates creatives and publishes them to social networks.
|
||||
5. **Targeting and leads** — ad targeting recommendations and Facebook lead capture.
|
||||
|
||||
## Request flow
|
||||
|
||||
```
|
||||
Frontend
|
||||
│ Bearer JWT
|
||||
▼
|
||||
Controller ──validator──► DTO
|
||||
│ │
|
||||
│ ▼
|
||||
│ Service layer
|
||||
│ ╱ │ ╲
|
||||
│ AI clients Repository MinIO
|
||||
│ (OpenAI/Ollama/ │ (artefacts)
|
||||
│ Vertex/Serper) ▼
|
||||
│ MongoDB
|
||||
▼
|
||||
GlobalExceptionHandler ──► consistent error payload
|
||||
```
|
||||
|
||||
Authentication is **JWT bearer tokens issued by a separate auth service**. This
|
||||
service does not log users in; `JwtService` validates the signature against
|
||||
`security.jwt.secret-base64` and extracts the user id, email and roles from claims.
|
||||
Controllers pull the caller identity via `extractUserIdFromHeader(authHeader)`. See
|
||||
[api/authentication.md](api/authentication.md).
|
||||
|
||||
## Layers
|
||||
|
||||
| Package | Responsibility |
|
||||
| --- | --- |
|
||||
| `controller` | HTTP endpoints, 15 controllers plus `GlobalExceptionHandler` |
|
||||
| `validator` | Request-level validation beyond Bean Validation annotations |
|
||||
| `dto` | Request/response payloads, including `dto/targeting` |
|
||||
| `service` | All business logic — 52 classes |
|
||||
| `repository` | Spring Data MongoDB interfaces |
|
||||
| `model` | MongoDB documents |
|
||||
| `config` | Beans, typed `@ConfigurationProperties`, async executors, CORS, Swagger |
|
||||
| `exception` | Domain exceptions surfaced by `GlobalExceptionHandler` |
|
||||
|
||||
## Endpoints
|
||||
|
||||
| Base path | Controller | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `/api/parser/health` | `HealthCheckController` | Liveness |
|
||||
| `/api/parser/items` | `MarketItemController` | Access the ingested news corpus |
|
||||
| `/api/parser/admin/parsers` | `ParserAdminController` | Trigger and inspect parsers |
|
||||
| `/api/parser/report` | `ReportController` | Research report generation and history |
|
||||
| `/api/marketing/analysis` | `MarketingController` | Marketing analysis (v1/v2) |
|
||||
| `/api/marketing/v3` | `MarketingAnalysisV3Controller` | Marketing analysis v3 |
|
||||
| `/api/marketing/targeting` | `TargetingCampaignController` | Campaign targeting |
|
||||
| `/api/marketing` | `PublicAssetController` | Public access to generated assets |
|
||||
| `/api/targeting` | `AiTargetingSystemController` | AI targeting recommendations |
|
||||
| `/api/social-media/credentials` | `SocialMediaCredentialsController` | Per-user network credentials |
|
||||
| `/api/facebook/config` | `FacebookConfigController` | Facebook app/page configuration |
|
||||
| `/api/facebook/leads` | `FacebookLeadsController` | Collected hot leads |
|
||||
| `/api/facebook/webhook` | `FacebookWebhookController` | Facebook webhook receiver |
|
||||
| `/api/openai` | `OpenAITestController` | Connectivity diagnostics |
|
||||
|
||||
Full request/response detail: [api/README.md](api/README.md), or the live OpenAPI UI
|
||||
at `/swagger-ui.html`.
|
||||
|
||||
## RSS ingestion
|
||||
|
||||
`ParserService` is a small interface — `getSourceName()` and `parseAndSaveRssFeed()`.
|
||||
Each source implements it, and `ParserManagerService` acts as a facade: Spring injects
|
||||
every `ParserService` bean and the manager indexes them by source name, so adding a
|
||||
source requires no changes to the manager or the controller.
|
||||
|
||||
Five sources are implemented: Kursiv, Kapital, LSM, RBC and Vedomosti. Details and
|
||||
scheduling in [rss-parsers.md](rss-parsers.md).
|
||||
|
||||
## Scheduled jobs
|
||||
|
||||
`parser.scheduler.enabled` is **`false` by default** — RSS polling does not run unless
|
||||
explicitly enabled. `posting.scheduler.enabled` defaults to `true`.
|
||||
|
||||
| Job | Cron | Owner |
|
||||
| --- | --- | --- |
|
||||
| Kursiv / Kapital ingest | every 5 min | `KursivParserService`, `KapitalParserService` |
|
||||
| LSM / RBC / Vedomosti ingest | every 30 min | respective parser services |
|
||||
| Scheduled post publishing | every minute | `PostingSchedulerService` |
|
||||
| Facebook lead collection | every 15 min (configurable) | `FacebookLeadCollectorService` |
|
||||
| Targeting campaign sync | every 6 hours | `TargetingCampaignService` |
|
||||
| Campaign prediction refresh | daily 06:00 | `CampaignPredictionService` |
|
||||
|
||||
## AI provider strategy
|
||||
|
||||
The service deliberately mixes providers by cost and capability:
|
||||
|
||||
- **OpenAI** (`gpt-4o`, `gpt-4o-mini`) — structured JSON analysis and chart data, where
|
||||
reliable schema adherence matters. Wrapped in retry with exponential backoff and a
|
||||
concurrency cap of 3.
|
||||
- **Ollama** (self-hosted) — long-form narrative text, avoiding per-token cost on the
|
||||
largest outputs. Timeouts are correspondingly long (up to 5 hours).
|
||||
- **Google Vertex AI** — Imagen 3 for images, Veo 3 for video, authenticated with a
|
||||
service-account key. See [configuration.md](configuration.md#the-google-service-account-key).
|
||||
- **Serper** — Google search results feeding research reports.
|
||||
|
||||
`ClaudeApiService` and `DeepResearchService` cover additional generation paths.
|
||||
|
||||
## Persistence
|
||||
|
||||
MongoDB documents, one repository each:
|
||||
|
||||
| Document | Holds |
|
||||
| --- | --- |
|
||||
| `MarketItem` | Ingested news articles (the corpus) |
|
||||
| `MarketingAnalysis`, `MarketingAnalysisV2Document`, `MarketingAnalysisV3Document` | Three analysis generations, kept side by side |
|
||||
| `MarketingStrategy` | Generated promotion strategies |
|
||||
| `TargetingCampaign`, `TargetingAudienceProfile`, `TargetingAdSet`, `TargetingAd`, `TargetingInsight` | Ad targeting model |
|
||||
| `PostingTask` | Queued and published social posts |
|
||||
| `SocialMediaCredentials` | Per-user network credentials, encrypted at rest |
|
||||
| `FacebookLead` | Leads harvested from page comments |
|
||||
| `ReportHistory` | Generated report metadata |
|
||||
| `CampaignPrediction`, `ABTestConfig`, `BudgetConfig`, `PerformanceMetrics` | Campaign optimisation |
|
||||
|
||||
The three analysis document versions coexist because the API kept older revisions
|
||||
working for the frontend; `MarketingAnalysisV3Service` is the current path.
|
||||
|
||||
## External dependencies
|
||||
|
||||
| Dependency | Used for | Failure behaviour |
|
||||
| --- | --- | --- |
|
||||
| MongoDB | All persistence | Fatal — service cannot operate |
|
||||
| MinIO | Reports and generated images | Feature-level failure |
|
||||
| OpenAI | Analysis, strategy, chart data | Retried, then surfaced as an error |
|
||||
| Ollama | Long-form report text | Retried, then surfaced as an error |
|
||||
| Vertex AI | Image/video generation | Logs error, returns `null`; rest of the service unaffected |
|
||||
| Serper | Research search | Retried |
|
||||
| Facebook Graph API | Posting and lead collection | Logged, retried on next schedule |
|
||||
| SMTP | Emailing reports | Health indicator disabled; failures logged |
|
||||
|
||||
## Cross-cutting configuration
|
||||
|
||||
- **CORS** — `CorsProperties` binds `cors.*`, applied by `WebCorsConfig`. Origins
|
||||
default to the production frontend domains.
|
||||
- **Async** — `AsyncConfig` and `TargetingAsyncConfig` provide executors; long AI calls
|
||||
run off the request thread and parsers return `CompletableFuture`.
|
||||
- **Error handling** — `GlobalExceptionHandler` maps domain exceptions to consistent
|
||||
payloads.
|
||||
- **API docs** — `SwaggerConfig` declares the `bearerAuth` scheme so the Swagger UI can
|
||||
authorise with a JWT.
|
||||
@@ -0,0 +1,58 @@
|
||||
# Archive
|
||||
|
||||
Historical documents kept for context. **Nothing here is maintained, and none of it
|
||||
should be treated as current.** For how the system works today, start at
|
||||
[../../README.md](../../README.md).
|
||||
|
||||
These files were previously scattered across the repository root. They are preserved
|
||||
rather than deleted because they record *why* parts of the system look the way they do —
|
||||
but they describe past states, and several contradict the current implementation.
|
||||
|
||||
## `specs/` — technical assignments (ТЗ)
|
||||
|
||||
The original Russian-language specifications the service was built from, plus scope
|
||||
notes. Useful for understanding intent behind a feature; unreliable as a description of
|
||||
current behaviour.
|
||||
|
||||
Includes `ОТЛИЧИЯ_ТЗ_ОТ_РЕАЛИЗАЦИИ.md`, which itself catalogues where the
|
||||
implementation diverged from the spec — worth reading before trusting any other file in
|
||||
this folder.
|
||||
|
||||
Also contains the original `ТЗ. Модуль - маркетинг..docx`.
|
||||
|
||||
## `api-history/` — superseded API documentation
|
||||
|
||||
Earlier revisions of the frontend API docs, kept because the analysis API went through
|
||||
three generations that still coexist at runtime.
|
||||
|
||||
| File | Superseded by |
|
||||
| --- | --- |
|
||||
| `marketing-analysis-api.md` | [`../api/marketing-analysis.md`](../api/marketing-analysis.md) |
|
||||
| `marketing-analysis-api-frontend.md` | ditto |
|
||||
| `marketing-api-with-jwt-frontend.md` | ditto + [`../api/authentication.md`](../api/authentication.md) |
|
||||
| `FRONTEND_API_GUIDE.md` | [`../api/README.md`](../api/README.md) |
|
||||
| `FRONTEND_API_CHANGES.md` | ditto |
|
||||
| `FRONTEND_API_CHANGES_V2.md` | ditto |
|
||||
| `FRONTEND_API_CHANGES_MARKETING_ANALYSIS.md` | ditto |
|
||||
|
||||
## `changelogs/` — completed change notes
|
||||
|
||||
One-off write-ups of finished refactors and bug fixes: the marketing strategy refactor,
|
||||
chart improvements, a PDF-agent implementation summary, and a `NullPointerException`
|
||||
fix in `ReportGenerationService`.
|
||||
|
||||
## `setup-notes/` — superseded setup guides
|
||||
|
||||
Earlier OpenAI setup, startup, MongoDB troubleshooting and GitLab-era deployment
|
||||
instructions. Replaced by [configuration.md](../configuration.md),
|
||||
[development.md](../development.md), [deployment.md](../deployment.md) and
|
||||
[troubleshooting.md](../troubleshooting.md).
|
||||
|
||||
`DEPLOYMENT.md` describes the GitLab CI pipeline that was retired in the August 2026
|
||||
migration to Gitea Actions.
|
||||
|
||||
## `prompts/` — AI prompt drafts and working notes
|
||||
|
||||
Iterations of the marketing-analysis prompts and scratch notes written while designing
|
||||
report generation. Previously the root-level files `g.md`, `l.md` and `fix.md`.
|
||||
Historical drafts — the prompts actually in use live in the service classes.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,323 @@
|
||||
# Изменения в API маркетингового анализа
|
||||
|
||||
## Обзор изменений
|
||||
|
||||
Проведен рефакторинг метода генерации JSON-анализа для повышения детализации данных. Структура ответа API для фронтенда **осталась прежней**, но изменилась внутренняя структура данных в `chartsData`.
|
||||
|
||||
## Структура ответа API (без изменений)
|
||||
|
||||
Ответ API остается прежним:
|
||||
|
||||
```json
|
||||
{
|
||||
"analysisId": "string",
|
||||
"status": "completed",
|
||||
"createdAt": "2024-01-01T00:00:00",
|
||||
"completedAt": "2024-01-01T00:05:00",
|
||||
"report": {
|
||||
"summary": "string",
|
||||
"fullAnalysis": "string",
|
||||
"chartsData": {
|
||||
/* ← ИЗМЕНЕНИЯ ЗДЕСЬ */
|
||||
},
|
||||
"targetAudience": {
|
||||
"description": "string",
|
||||
"channels": ["Instagram", "Telegram"]
|
||||
},
|
||||
"recommendations": ["string"],
|
||||
"strategy": {
|
||||
"duration": "2 недели",
|
||||
"channels": ["Instagram", "Telegram"],
|
||||
"contentTypes": ["посты", "сторис"]
|
||||
},
|
||||
"pdfUrl": "/api/marketing/analysis/{id}/download"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Изменения в `chartsData`
|
||||
|
||||
### 1. Анализ аудитории (`audienceSegmentation`)
|
||||
|
||||
**Было:**
|
||||
|
||||
```json
|
||||
{
|
||||
"audienceSegmentation": {
|
||||
"ageGroups": [{ "label": "25-34", "value": "40" }],
|
||||
"genders": [{ "label": "Женщины", "value": "60" }],
|
||||
"segments": [{ "label": "Сегмент 1", "value": "35" }]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Стало (расширенная структура):**
|
||||
|
||||
```json
|
||||
{
|
||||
"audienceSegmentation": {
|
||||
"ageGroups": [
|
||||
{ "label": "18-24", "value": "15" },
|
||||
{ "label": "25-34", "value": "40" },
|
||||
{ "label": "35-44", "value": "30" },
|
||||
{ "label": "45+", "value": "15" }
|
||||
],
|
||||
"genders": [
|
||||
{ "label": "Женщины", "value": "60" },
|
||||
{ "label": "Мужчины", "value": "40" }
|
||||
],
|
||||
"segments": [
|
||||
{
|
||||
"label": "Молодые профессионалы",
|
||||
"value": "35"
|
||||
}
|
||||
],
|
||||
"segmentsChannelMatrix": [
|
||||
// ← НОВОЕ
|
||||
{
|
||||
"segmentName": "Молодые профи",
|
||||
"instagram": "high",
|
||||
"telegram": "medium",
|
||||
"youtube": "low"
|
||||
}
|
||||
],
|
||||
"keyTakeaways": [
|
||||
// ← НОВОЕ
|
||||
"Вывод 1",
|
||||
"Вывод 2",
|
||||
"Вывод 3"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Что изменилось:**
|
||||
|
||||
- ✅ Структура `ageGroups`, `genders`, `segments` осталась прежней
|
||||
- ➕ Добавлено поле `segmentsChannelMatrix` — матрица соответствия сегментов и каналов (heatmap)
|
||||
- ➕ Добавлено поле `keyTakeaways` — ключевые выводы по аудитории
|
||||
|
||||
### 2. Анализ конкурентов (`competitors`)
|
||||
|
||||
**Было:**
|
||||
|
||||
```json
|
||||
{
|
||||
"competitors": [
|
||||
{
|
||||
"name": "Конкурент А",
|
||||
"reach": 50000,
|
||||
"activity": 85,
|
||||
"reviews": 1200,
|
||||
"strengths": "Не указано",
|
||||
"weaknesses": "Не указано"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Стало (расширенная структура):**
|
||||
|
||||
```json
|
||||
{
|
||||
"marketShareChart": [
|
||||
// ← НОВОЕ
|
||||
{ "name": "Наш бренд", "value": 25 },
|
||||
{ "name": "Конкурент А", "value": 30 },
|
||||
{ "name": "Конкурент B", "value": 20 },
|
||||
{ "name": "Другие", "value": 25 }
|
||||
],
|
||||
"competitors": [
|
||||
{
|
||||
"name": "Конкурент А",
|
||||
"reach": 50000,
|
||||
"followers": 12000, // ← НОВОЕ
|
||||
"activity": 85,
|
||||
"priceStrategy": "Высокая", // ← НОВОЕ
|
||||
"channelsHeatmap": {
|
||||
// ← НОВОЕ
|
||||
"Instagram": 90,
|
||||
"TikTok": 20,
|
||||
"Telegram": 70,
|
||||
"YouTube": 40
|
||||
}
|
||||
}
|
||||
],
|
||||
"comparisonTable": [
|
||||
// ← НОВОЕ
|
||||
{
|
||||
"feature": "Ценовая политика",
|
||||
"us": "Средняя",
|
||||
"compA": "Высокая",
|
||||
"compB": "Низкая"
|
||||
}
|
||||
],
|
||||
"swotCompetitors": {
|
||||
// ← НОВОЕ
|
||||
"strengths": ["..."],
|
||||
"weaknesses": ["..."]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Что изменилось:**
|
||||
|
||||
- ✅ Массив `competitors` остался, но с дополнительными полями
|
||||
- ➕ Добавлено поле `marketShareChart` — доли рынка для графика
|
||||
- ➕ Добавлено поле `comparisonTable` — сравнительная таблица характеристик
|
||||
- ➕ Добавлено поле `swotCompetitors` — SWOT-анализ по рынку
|
||||
- ➕ В объектах конкурентов добавлены: `followers`, `priceStrategy`, `channelsHeatmap`
|
||||
|
||||
### 3. Сезонность (`seasonality`)
|
||||
|
||||
**Без изменений:**
|
||||
|
||||
```json
|
||||
{
|
||||
"seasonality": {
|
||||
"Январь": 45,
|
||||
"Февраль": 50,
|
||||
"Март": 60
|
||||
// ... остальные месяцы
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Рынок (`market`) — НОВОЕ
|
||||
|
||||
**Добавлено:**
|
||||
|
||||
```json
|
||||
{
|
||||
"market": {
|
||||
"size": "средний",
|
||||
"growthRate": "растущий",
|
||||
"trends": ["Тренд 1", "Тренд 2"],
|
||||
"opportunities": ["Возможность 1"],
|
||||
"threats": ["Угроза 1"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Обратная совместимость
|
||||
|
||||
Для обеспечения обратной совместимости, следующие поля могут присутствовать в `chartsData` (если использовалась старая структура):
|
||||
|
||||
- `channelsPotential` — каналы продвижения
|
||||
- `conversionFunnel` — воронка конверсии
|
||||
- `swot` — SWOT-анализ
|
||||
|
||||
**Рекомендация:** Используйте новые поля, но проверяйте наличие старых для обратной совместимости.
|
||||
|
||||
## Миграция фронтенда
|
||||
|
||||
### Шаг 1: Обновите обработку `audienceSegmentation`
|
||||
|
||||
```typescript
|
||||
// Было
|
||||
const ageGroups = chartsData.audienceSegmentation?.ageGroups || [];
|
||||
const genders = chartsData.audienceSegmentation?.genders || [];
|
||||
const segments = chartsData.audienceSegmentation?.segments || [];
|
||||
|
||||
// Стало (добавьте новые поля)
|
||||
const ageGroups = chartsData.audienceSegmentation?.ageGroups || [];
|
||||
const genders = chartsData.audienceSegmentation?.genders || [];
|
||||
const segments = chartsData.audienceSegmentation?.segments || [];
|
||||
const segmentsChannelMatrix =
|
||||
chartsData.audienceSegmentation?.segmentsChannelMatrix || []; // НОВОЕ
|
||||
const keyTakeaways = chartsData.audienceSegmentation?.keyTakeaways || []; // НОВОЕ
|
||||
```
|
||||
|
||||
### Шаг 2: Обновите обработку конкурентов
|
||||
|
||||
```typescript
|
||||
// Было
|
||||
const competitors = chartsData.competitors || [];
|
||||
|
||||
// Стало (добавьте новые поля)
|
||||
const marketShareChart = chartsData.marketShareChart || []; // НОВОЕ
|
||||
const competitors = chartsData.competitors || [];
|
||||
const comparisonTable = chartsData.comparisonTable || []; // НОВОЕ
|
||||
const swotCompetitors = chartsData.swotCompetitors || {}; // НОВОЕ
|
||||
|
||||
// Обновите обработку объектов конкурентов
|
||||
competitors.forEach((comp) => {
|
||||
const followers = comp.followers; // НОВОЕ
|
||||
const priceStrategy = comp.priceStrategy; // НОВОЕ
|
||||
const channelsHeatmap = comp.channelsHeatmap; // НОВОЕ
|
||||
});
|
||||
```
|
||||
|
||||
### Шаг 3: Добавьте обработку рынка
|
||||
|
||||
```typescript
|
||||
// НОВОЕ
|
||||
const market = chartsData.market || {};
|
||||
const marketSize = market.size;
|
||||
const growthRate = market.growthRate;
|
||||
const trends = market.trends || [];
|
||||
const opportunities = market.opportunities || [];
|
||||
const threats = market.threats || [];
|
||||
```
|
||||
|
||||
## Примеры использования новых данных
|
||||
|
||||
### 1. Heatmap сегментов и каналов
|
||||
|
||||
```typescript
|
||||
const segmentsChannelMatrix =
|
||||
chartsData.audienceSegmentation?.segmentsChannelMatrix || [];
|
||||
|
||||
// Отображение heatmap
|
||||
segmentsChannelMatrix.forEach((row) => {
|
||||
const segmentName = row.segmentName;
|
||||
const instagram = row.instagram; // "high" | "medium" | "low"
|
||||
const telegram = row.telegram;
|
||||
const youtube = row.youtube;
|
||||
// ... отрисовка heatmap
|
||||
});
|
||||
```
|
||||
|
||||
### 2. График долей рынка
|
||||
|
||||
```typescript
|
||||
const marketShareChart = chartsData.marketShareChart || [];
|
||||
|
||||
// Отображение pie chart
|
||||
marketShareChart.forEach((item) => {
|
||||
const name = item.name; // "Наш бренд", "Конкурент А", etc.
|
||||
const value = item.value; // процент доли рынка
|
||||
// ... отрисовка графика
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Сравнительная таблица
|
||||
|
||||
```typescript
|
||||
const comparisonTable = chartsData.comparisonTable || [];
|
||||
|
||||
// Отображение таблицы
|
||||
comparisonTable.forEach((row) => {
|
||||
const feature = row.feature; // "Ценовая политика", "УТП", etc.
|
||||
const us = row.us;
|
||||
const compA = row.compA;
|
||||
const compB = row.compB;
|
||||
// ... отрисовка таблицы
|
||||
});
|
||||
```
|
||||
|
||||
## Резюме изменений
|
||||
|
||||
| Поле | Статус | Описание |
|
||||
| --------------------------------- | ---------------- | -------------------------------------------------------------- |
|
||||
| `chartsData.audienceSegmentation` | ✅ Расширено | Добавлены `segmentsChannelMatrix` и `keyTakeaways` |
|
||||
| `chartsData.competitors` | ✅ Расширено | Добавлены поля `followers`, `priceStrategy`, `channelsHeatmap` |
|
||||
| `chartsData.marketShareChart` | ➕ Новое | График долей рынка |
|
||||
| `chartsData.comparisonTable` | ➕ Новое | Сравнительная таблица |
|
||||
| `chartsData.swotCompetitors` | ➕ Новое | SWOT-анализ по рынку |
|
||||
| `chartsData.market` | ➕ Новое | Данные о рынке |
|
||||
| `chartsData.seasonality` | ✅ Без изменений | Осталось прежним |
|
||||
|
||||
## Вопросы?
|
||||
|
||||
Если возникнут вопросы по миграции, обращайтесь к бэкенд-команде.
|
||||
@@ -0,0 +1,193 @@
|
||||
# Изменения в API: Интеграция v2 анализа
|
||||
|
||||
## Обзор изменений
|
||||
|
||||
Добавлена возможность опционального создания и сохранения v2 анализа вместе с обычным анализом. Все v2 анализы сохраняются в MongoDB и доступны через новые эндпойнты.
|
||||
|
||||
---
|
||||
|
||||
## 1. POST `/api/marketing/analysis/start` - Обновлен
|
||||
|
||||
### Новый query параметр:
|
||||
|
||||
- **`generateV2`** (boolean, опционально, по умолчанию `false`)
|
||||
- Если `true`, вместе с обычным анализом создается и сохраняется v2 анализ
|
||||
- Оба анализа автоматически связываются между собой
|
||||
|
||||
### Изменения в ответе:
|
||||
|
||||
**Если `generateV2=false` (по умолчанию):**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Анализ запущен успешно",
|
||||
"data": {
|
||||
"analysisId": "string",
|
||||
"status": "processing",
|
||||
"estimatedCompletionTime": "2024-01-01T12:00:00",
|
||||
"message": "Комплексный анализ (...) запущен успешно..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Если `generateV2=true`:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Анализ запущен успешно",
|
||||
"data": {
|
||||
"analysisId": "string",
|
||||
"status": "processing",
|
||||
"estimatedCompletionTime": "2024-01-01T12:00:00",
|
||||
"message": "Комплексный анализ (...) запущен успешно...",
|
||||
"v2AnalysisId": "string" // ← НОВОЕ ПОЛЕ
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Пример использования:
|
||||
|
||||
```javascript
|
||||
// Создание обычного анализа
|
||||
POST /api/marketing/analysis/start
|
||||
Body: { ... }
|
||||
|
||||
// Создание обычного анализа + v2 анализа
|
||||
POST /api/marketing/analysis/start?generateV2=true
|
||||
Body: { ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. GET `/api/marketing/analysis/my` - Обновлен
|
||||
|
||||
### Новое поле в ответе:
|
||||
|
||||
Каждый элемент в списке теперь содержит опциональное поле `v2AnalysisId`:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"analysisId": "string",
|
||||
"businessNiche": "string",
|
||||
"product": "string",
|
||||
// ... другие поля ...
|
||||
"v2AnalysisId": "string" // ← НОВОЕ ПОЛЕ (может быть null)
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Примечание:** Поле `v2AnalysisId` будет присутствовать только если для данного анализа был создан связанный v2 анализ.
|
||||
|
||||
---
|
||||
|
||||
## 3. GET `/api/marketing/analysis/{id}` - Обновлен
|
||||
|
||||
### Новое поле в ответе:
|
||||
|
||||
Эндпойнт теперь возвращает опциональное поле `v2AnalysisId`:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"analysisId": "string",
|
||||
"status": "string",
|
||||
"createdAt": "2024-01-01T12:00:00",
|
||||
"completedAt": "2024-01-01T12:00:00",
|
||||
"v2AnalysisId": "string", // ← НОВОЕ ПОЛЕ (может быть null)
|
||||
"report": { ... }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Примечание:** Поле `v2AnalysisId` будет присутствовать только если для данного анализа был создан связанный v2 анализ.
|
||||
|
||||
---
|
||||
|
||||
## 4. GET `/api/marketing/analysis/v2/{id}` - Новый эндпойнт
|
||||
|
||||
### Описание:
|
||||
|
||||
Получение сохраненного v2 анализа по ID.
|
||||
|
||||
### Параметры:
|
||||
|
||||
- **Path:** `id` (string) - ID v2 анализа
|
||||
- **Header:** `Authorization` (JWT токен)
|
||||
|
||||
### Ответ:
|
||||
|
||||
**Успешный ответ (200):**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"reportTitle": "string",
|
||||
"sections": {
|
||||
"marketOverview": { ... },
|
||||
"targetAudience": { ... },
|
||||
"competitiveEnvironment": { ... },
|
||||
// ... другие секции ...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Ошибки:**
|
||||
|
||||
- `401` - Не авторизован
|
||||
- `404` - Анализ не найден
|
||||
- `403` - Нет доступа к этому анализу
|
||||
|
||||
### Пример использования:
|
||||
|
||||
```javascript
|
||||
GET /api/marketing/analysis/v2/507f1f77bcf86cd799439011
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. POST `/api/marketing/analysis/start/v2` - Без изменений
|
||||
|
||||
Эндпойнт работает как прежде - создает только v2 анализ без связи с обычным анализом.
|
||||
|
||||
---
|
||||
|
||||
## Рекомендации для фронтенда
|
||||
|
||||
1. **При создании анализа:**
|
||||
|
||||
- Используйте `generateV2=true` если нужен v2 анализ сразу
|
||||
- Сохраняйте `v2AnalysisId` из ответа для последующего доступа
|
||||
|
||||
2. **При отображении списка анализов:**
|
||||
|
||||
- Проверяйте наличие поля `v2AnalysisId`
|
||||
- Если поле присутствует, можно показать кнопку/ссылку для просмотра v2 анализа
|
||||
|
||||
3. **При получении конкретного анализа:**
|
||||
|
||||
- Поле `v2AnalysisId` доступно в ответе `GET /api/marketing/analysis/{id}`
|
||||
- Используйте это поле для навигации к v2 анализу
|
||||
|
||||
4. **При получении v2 анализа:**
|
||||
- Используйте `GET /api/marketing/analysis/v2/{id}` для получения полных данных
|
||||
- Обрабатывайте случаи, когда v2 анализ может быть не найден (404)
|
||||
|
||||
---
|
||||
|
||||
## Обратная совместимость
|
||||
|
||||
✅ Все изменения обратно совместимы:
|
||||
|
||||
- Существующие запросы без `generateV2` работают как прежде
|
||||
- Поле `v2AnalysisId` опционально и не ломает существующий код
|
||||
- Новые эндпойнты не влияют на старую функциональность
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,551 @@
|
||||
# API Документация: Маркетинговый анализ (Frontend/AI Agent)
|
||||
|
||||
## Базовый URL
|
||||
|
||||
```
|
||||
https://api.konturai.kz
|
||||
```
|
||||
|
||||
## Обзор
|
||||
|
||||
API для генерации маркетингового анализа на основе данных о бизнесе. Процесс состоит из двух этапов:
|
||||
|
||||
1. **Запуск анализа** - создание задачи и начало асинхронной обработки
|
||||
2. **Получение результатов** - проверка статуса и получение готового отчета
|
||||
|
||||
Анализ выполняется асинхронно и занимает примерно 5-10 минут.
|
||||
|
||||
---
|
||||
|
||||
## Эндпоинты
|
||||
|
||||
### 1. Запуск маркетингового анализа
|
||||
|
||||
**POST** `/api/marketing/analysis/start`
|
||||
|
||||
Создает новую задачу на генерацию маркетингового анализа и запускает асинхронную обработку.
|
||||
|
||||
#### Заголовки запроса
|
||||
|
||||
```
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
#### Тело запроса (JSON)
|
||||
|
||||
| Поле | Тип | Обязательный | Описание | Пример значения |
|
||||
| ---------------- | ------ | ------------ | ------------------------------ | -------------------------------- |
|
||||
| `product` | string | ✅ | Название продукта или услуги | "Веб-разработка" |
|
||||
| `location` | string | ✅ | Географическая локация работы | "Алматы, Казахстан" |
|
||||
| `client` | string | ✅ | Тип целевой аудитории | "B2B клиенты" |
|
||||
| `differentiator` | string | ✅ | Уникальные особенности бизнеса | "Быстрая разработка за 2 недели" |
|
||||
|
||||
#### Валидация полей
|
||||
|
||||
**`product`** (string, обязательное)
|
||||
|
||||
- Минимальная длина: 3 символа
|
||||
- Максимальная длина: 200 символов
|
||||
- Разрешены: буквы, цифры, пробелы, дефисы, запятые
|
||||
|
||||
**`location`** (string, обязательное)
|
||||
|
||||
- Минимальная длина: 2 символа
|
||||
- Максимальная длина: 150 символов
|
||||
- Разрешены: буквы, цифры, пробелы, запятые, дефисы
|
||||
|
||||
**`client`** (string, обязательное)
|
||||
|
||||
- Допустимые значения (точно):
|
||||
- `"B2B клиенты"`
|
||||
- `"B2C клиенты"`
|
||||
- `"Частные лица"`
|
||||
- `"Корпорации"`
|
||||
- `"Малый бизнес"`
|
||||
|
||||
**`differentiator`** (string, обязательное)
|
||||
|
||||
- Минимальная длина: 10 символов
|
||||
- Максимальная длина: 500 символов
|
||||
- Разрешены любые символы
|
||||
|
||||
#### Пример запроса
|
||||
|
||||
```json
|
||||
{
|
||||
"product": "Разработка мобильных приложений",
|
||||
"location": "Нур-Султан, Казахстан",
|
||||
"client": "B2B клиенты",
|
||||
"differentiator": "Специализируемся на быстрой разработке MVP за 4 недели с использованием современных технологий"
|
||||
}
|
||||
```
|
||||
|
||||
#### Пример успешного ответа (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Анализ запущен успешно",
|
||||
"data": {
|
||||
"analysisId": "507f1f77bcf86cd799439011",
|
||||
"status": "processing",
|
||||
"estimatedCompletionTime": "2025-01-20T15:38:00",
|
||||
"message": "Анализ запущен успешно. Результаты будут готовы в течение 5-10 минут."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Структура ответа
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
| ------------------------------ | ------- | --------------------------------------------------------- |
|
||||
| `success` | boolean | Флаг успешности операции |
|
||||
| `message` | string | Сообщение о результате операции |
|
||||
| `data.analysisId` | string | Уникальный идентификатор анализа (MongoDB ObjectId) |
|
||||
| `data.status` | string | Статус анализа: `"processing"` |
|
||||
| `data.estimatedCompletionTime` | string | ISO 8601 дата/время ожидаемого завершения (LocalDateTime) |
|
||||
| `data.message` | string | Информационное сообщение для пользователя |
|
||||
|
||||
#### Пример ошибки валидации (400 Bad Request)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Ошибка валидации",
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "Ошибка валидации входных данных",
|
||||
"details": {
|
||||
"product": "Поле 'product' должно содержать от 3 до 200 символов",
|
||||
"client": "Поле 'client' должно быть одним из: B2B клиенты, B2C клиенты, Частные лица, Корпорации, Малый бизнес"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Получение результата анализа
|
||||
|
||||
**GET** `/api/marketing/analysis/{analysisId}`
|
||||
|
||||
Возвращает статус и результаты анализа по идентификатору.
|
||||
|
||||
#### Параметры пути
|
||||
|
||||
| Параметр | Тип | Описание |
|
||||
| ------------ | ------ | --------------------- |
|
||||
| `analysisId` | string | Идентификатор анализа |
|
||||
|
||||
#### Пример запроса
|
||||
|
||||
```
|
||||
GET /api/marketing/analysis/507f1f77bcf86cd799439011
|
||||
```
|
||||
|
||||
#### Пример ответа (когда анализ завершен - 200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Операция выполнена успешно",
|
||||
"data": {
|
||||
"analysisId": "507f1f77bcf86cd799439011",
|
||||
"status": "completed",
|
||||
"createdAt": "2025-01-20T15:30:00",
|
||||
"completedAt": "2025-01-20T15:38:00",
|
||||
"report": {
|
||||
"summary": "Краткое резюме анализа...",
|
||||
"targetAudience": {
|
||||
"description": "Описание целевой аудитории...",
|
||||
"channels": ["Instagram", "LinkedIn", "Telegram"]
|
||||
},
|
||||
"recommendations": ["Рекомендация 1", "Рекомендация 2", "Рекомендация 3"],
|
||||
"strategy": {
|
||||
"duration": "2 недели",
|
||||
"channels": ["Instagram", "Telegram", "21MC"],
|
||||
"contentTypes": ["посты", "сторис", "баннеры"]
|
||||
},
|
||||
"pdfUrl": "/api/marketing/analysis/507f1f77bcf86cd799439011/download"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Пример ответа (когда анализ еще обрабатывается - 200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Операция выполнена успешно",
|
||||
"data": {
|
||||
"analysisId": "507f1f77bcf86cd799439011",
|
||||
"status": "processing",
|
||||
"createdAt": "2025-01-20T15:30:00",
|
||||
"completedAt": null,
|
||||
"report": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Статусы анализа
|
||||
|
||||
| Статус | Описание |
|
||||
| ------------ | ----------------------------- |
|
||||
| `queued` | Запрос в очереди на обработку |
|
||||
| `processing` | Анализ выполняется |
|
||||
| `completed` | Анализ завершен успешно |
|
||||
| `failed` | Анализ завершился с ошибкой |
|
||||
|
||||
#### Структура ответа
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
| ---------------------------------------- | ------- | ------------------------------------------------------ |
|
||||
| `success` | boolean | Флаг успешности операции |
|
||||
| `message` | string | Сообщение о результате операции |
|
||||
| `data.analysisId` | string | Уникальный идентификатор анализа |
|
||||
| `data.status` | string | Статус анализа |
|
||||
| `data.createdAt` | string | ISO 8601 дата/время создания |
|
||||
| `data.completedAt` | string | ISO 8601 дата/время завершения (null если не завершен) |
|
||||
| `data.report` | object | Объект с результатами (null если не завершен) |
|
||||
| `data.report.summary` | string | Краткое резюме анализа |
|
||||
| `data.report.targetAudience` | object | Информация о целевой аудитории |
|
||||
| `data.report.targetAudience.description` | string | Описание целевой аудитории |
|
||||
| `data.report.targetAudience.channels` | array | Список рекомендуемых каналов |
|
||||
| `data.report.recommendations` | array | Список рекомендаций (массив строк) |
|
||||
| `data.report.strategy` | object | Маркетинговая стратегия |
|
||||
| `data.report.strategy.duration` | string | Длительность кампании (например, "2 недели") |
|
||||
| `data.report.strategy.channels` | array | Каналы коммуникации (массив строк) |
|
||||
| `data.report.strategy.contentTypes` | array | Типы контента (массив строк) |
|
||||
| `data.report.pdfUrl` | string | URL для скачивания PDF отчета |
|
||||
|
||||
#### Пример ошибки (404 Not Found)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Анализ не найден",
|
||||
"error": {
|
||||
"code": "NOT_FOUND",
|
||||
"message": "Анализ с указанным ID не найден"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Скачивание PDF отчета
|
||||
|
||||
**GET** `/api/marketing/analysis/{analysisId}/download`
|
||||
|
||||
Возвращает PDF файл с полным маркетинговым отчетом.
|
||||
|
||||
#### Параметры пути
|
||||
|
||||
| Параметр | Тип | Описание |
|
||||
| ------------ | ------ | --------------------- |
|
||||
| `analysisId` | string | Идентификатор анализа |
|
||||
|
||||
#### Пример запроса
|
||||
|
||||
```
|
||||
GET /api/marketing/analysis/507f1f77bcf86cd799439011/download
|
||||
```
|
||||
|
||||
#### Успешный ответ (200 OK)
|
||||
|
||||
- **Content-Type**: `application/pdf`
|
||||
- **Content-Disposition**: `attachment; filename="marketing_analysis_507f1f77bcf86cd799439011_1234567890.pdf"`
|
||||
- **Body**: Бинарные данные PDF файла
|
||||
|
||||
#### Ошибки
|
||||
|
||||
- **404 Not Found** - Анализ не найден или PDF еще не сгенерирован
|
||||
- **500 Internal Server Error** - Ошибка при получении файла
|
||||
|
||||
---
|
||||
|
||||
## Обработка ошибок
|
||||
|
||||
### Коды ошибок
|
||||
|
||||
| Код | HTTP статус | Описание |
|
||||
| ----------------------- | ----------- | ------------------------------- |
|
||||
| `VALIDATION_ERROR` | 400 | Ошибка валидации входных данных |
|
||||
| `NOT_FOUND` | 404 | Ресурс не найден |
|
||||
| `INTERNAL_SERVER_ERROR` | 500 | Внутренняя ошибка сервера |
|
||||
|
||||
### Формат ошибки
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Описание ошибки",
|
||||
"error": {
|
||||
"code": "ERROR_CODE",
|
||||
"message": "Детальное сообщение об ошибке",
|
||||
"details": {
|
||||
"field1": "Сообщение об ошибке для поля 1",
|
||||
"field2": "Сообщение об ошибке для поля 2"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Примечание**: Поле `details` присутствует только для ошибок валидации (`VALIDATION_ERROR`).
|
||||
|
||||
---
|
||||
|
||||
## Примеры использования
|
||||
|
||||
### JavaScript/TypeScript (Fetch API)
|
||||
|
||||
#### Запуск анализа
|
||||
|
||||
```javascript
|
||||
async function startMarketingAnalysis(data) {
|
||||
const response = await fetch(
|
||||
'https://api.konturai.kz/api/marketing/analysis/start',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
product: 'Разработка мобильных приложений',
|
||||
location: 'Нур-Султан, Казахстан',
|
||||
client: 'B2B клиенты',
|
||||
differentiator:
|
||||
'Специализируемся на быстрой разработке MVP за 4 недели',
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
console.log('Analysis ID:', result.data.analysisId);
|
||||
return result.data.analysisId;
|
||||
} else {
|
||||
console.error('Error:', result.error);
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Проверка статуса и получение результата
|
||||
|
||||
```javascript
|
||||
async function getAnalysisResult(analysisId) {
|
||||
const response = await fetch(
|
||||
`https://api.konturai.kz/api/marketing/analysis/${analysisId}`
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
const { status, report } = result.data;
|
||||
|
||||
if (status === 'completed' && report) {
|
||||
console.log('Analysis completed!');
|
||||
console.log('Summary:', report.summary);
|
||||
console.log('Recommendations:', report.recommendations);
|
||||
return report;
|
||||
} else if (status === 'processing') {
|
||||
console.log('Analysis is still processing...');
|
||||
return null; // Повторить запрос позже
|
||||
} else if (status === 'failed') {
|
||||
throw new Error('Analysis failed');
|
||||
}
|
||||
} else {
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Полный цикл с polling
|
||||
|
||||
```javascript
|
||||
async function waitForAnalysisCompletion(
|
||||
analysisId,
|
||||
maxAttempts = 60,
|
||||
intervalMs = 10000
|
||||
) {
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
const result = await getAnalysisResult(analysisId);
|
||||
|
||||
if (result) {
|
||||
return result; // Анализ завершен
|
||||
}
|
||||
|
||||
// Ждем перед следующей проверкой
|
||||
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
||||
}
|
||||
|
||||
throw new Error('Analysis timeout');
|
||||
}
|
||||
|
||||
// Использование
|
||||
async function runFullAnalysis() {
|
||||
try {
|
||||
// 1. Запускаем анализ
|
||||
const analysisId = await startMarketingAnalysis({
|
||||
product: 'Веб-разработка',
|
||||
location: 'Алматы, Казахстан',
|
||||
client: 'B2B клиенты',
|
||||
differentiator: 'Быстрая разработка за 2 недели',
|
||||
});
|
||||
|
||||
console.log(`Analysis started: ${analysisId}`);
|
||||
|
||||
// 2. Ждем завершения (проверяем каждые 10 секунд, максимум 10 минут)
|
||||
const report = await waitForAnalysisCompletion(analysisId, 60, 10000);
|
||||
|
||||
// 3. Используем результаты
|
||||
console.log('Report summary:', report.summary);
|
||||
console.log('Channels:', report.targetAudience.channels);
|
||||
console.log('PDF URL:', report.pdfUrl);
|
||||
|
||||
return report;
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Скачивание PDF
|
||||
|
||||
```javascript
|
||||
async function downloadPdf(analysisId) {
|
||||
const response = await fetch(
|
||||
`https://api.konturai.kz/api/marketing/analysis/${analysisId}/download`
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to download PDF');
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `marketing_analysis_${analysisId}.pdf`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import requests
|
||||
import time
|
||||
|
||||
BASE_URL = "https://api.konturai.kz"
|
||||
|
||||
def start_analysis(product, location, client, differentiator):
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/api/marketing/analysis/start",
|
||||
json={
|
||||
"product": product,
|
||||
"location": location,
|
||||
"client": client,
|
||||
"differentiator": differentiator
|
||||
}
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
if data["success"]:
|
||||
return data["data"]["analysisId"]
|
||||
else:
|
||||
raise Exception(data["error"]["message"])
|
||||
|
||||
def get_analysis_result(analysis_id):
|
||||
response = requests.get(
|
||||
f"{BASE_URL}/api/marketing/analysis/{analysis_id}"
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()["data"]
|
||||
|
||||
def wait_for_completion(analysis_id, max_attempts=60, interval=10):
|
||||
for _ in range(max_attempts):
|
||||
result = get_analysis_result(analysis_id)
|
||||
|
||||
if result["status"] == "completed":
|
||||
return result["report"]
|
||||
elif result["status"] == "failed":
|
||||
raise Exception("Analysis failed")
|
||||
|
||||
time.sleep(interval)
|
||||
|
||||
raise Exception("Analysis timeout")
|
||||
|
||||
# Использование
|
||||
analysis_id = start_analysis(
|
||||
product="Разработка мобильных приложений",
|
||||
location="Нур-Султан, Казахстан",
|
||||
client="B2B клиенты",
|
||||
differentiator="Быстрая разработка MVP за 4 недели"
|
||||
)
|
||||
|
||||
print(f"Analysis started: {analysis_id}")
|
||||
|
||||
report = wait_for_completion(analysis_id)
|
||||
print(f"Summary: {report['summary']}")
|
||||
print(f"Channels: {report['targetAudience']['channels']}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Рекомендации по интеграции
|
||||
|
||||
### 1. Polling стратегия
|
||||
|
||||
Рекомендуется проверять статус анализа каждые 10-15 секунд. Максимальное время ожидания - 10-15 минут.
|
||||
|
||||
### 2. Обработка ошибок
|
||||
|
||||
Всегда проверяйте поле `success` в ответе и обрабатывайте ошибки соответствующим образом.
|
||||
|
||||
### 3. Валидация на клиенте
|
||||
|
||||
Перед отправкой запроса рекомендуется валидировать данные на клиенте:
|
||||
|
||||
- Проверка длины полей
|
||||
- Проверка допустимых значений для `client`
|
||||
- Проверка обязательных полей
|
||||
|
||||
### 4. UX рекомендации
|
||||
|
||||
- Показывайте индикатор загрузки во время обработки
|
||||
- Отображайте примерное время завершения
|
||||
- Предоставьте возможность отменить ожидание и проверить результат позже
|
||||
- Сохраняйте `analysisId` для последующей проверки статуса
|
||||
|
||||
### 5. Кэширование
|
||||
|
||||
После получения результатов можно кэшировать их локально, используя `analysisId` как ключ.
|
||||
|
||||
---
|
||||
|
||||
## Примечания
|
||||
|
||||
1. **Формат даты**: Все даты возвращаются в формате ISO 8601 без timezone (LocalDateTime)
|
||||
2. **Идентификаторы**: Используются MongoDB ObjectId (24 символа hex)
|
||||
3. **Асинхронность**: Анализ выполняется асинхронно, не блокируя запрос
|
||||
4. **Таймауты**: Рекомендуется устанавливать таймаут на запросы (минимум 30 секунд для запуска анализа)
|
||||
5. **Rate Limiting**: В будущем может быть добавлено ограничение на количество запросов
|
||||
|
||||
---
|
||||
|
||||
## Поддержка
|
||||
|
||||
При возникновении проблем с API обращайтесь в техническую поддержку с указанием:
|
||||
|
||||
- `analysisId` (если есть)
|
||||
- Время запроса
|
||||
- Описание проблемы
|
||||
- Код ошибки (если есть)
|
||||
@@ -0,0 +1,358 @@
|
||||
# API Документация: Маркетинговый анализ
|
||||
|
||||
## Обзор
|
||||
|
||||
API для запуска маркетингового анализа на основе данных о бизнесе пользователя. Эндпоинт принимает информацию о продукте, локации, типе клиентов и уникальных особенностях бизнеса, затем генерирует маркетинговый отчет.
|
||||
|
||||
## Эндпоинт
|
||||
|
||||
**POST** `/api/marketing/analysis/start`
|
||||
|
||||
### Базовый URL
|
||||
|
||||
```
|
||||
https://api.konturai.kz/api/marketing/analysis/start
|
||||
```
|
||||
|
||||
## Запрос
|
||||
|
||||
### Заголовки
|
||||
|
||||
```
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {access_token} // Опционально, если требуется аутентификация
|
||||
```
|
||||
|
||||
### Тело запроса (JSON)
|
||||
|
||||
| Поле | Тип | Обязательный | Описание | Пример значения |
|
||||
| ---------------- | ------ | ------------ | ------------------------------ | -------------------------------- |
|
||||
| `product` | string | ✅ | Название продукта или услуги | "Веб-разработка" |
|
||||
| `location` | string | ✅ | Географическая локация работы | "Алматы, Казахстан" |
|
||||
| `client` | string | ✅ | Тип целевой аудитории | "B2B клиенты" |
|
||||
| `differentiator` | string | ✅ | Уникальные особенности бизнеса | "Быстрая разработка за 2 недели" |
|
||||
|
||||
### Валидация полей
|
||||
|
||||
#### `product` (string, обязательное)
|
||||
|
||||
- **Минимальная длина**: 3 символа
|
||||
- **Максимальная длина**: 200 символов
|
||||
- **Паттерн**: Разрешены буквы, цифры, пробелы, дефисы, запятые
|
||||
- **Ошибка валидации**: `"product": "Поле 'product' должно содержать от 3 до 200 символов"`
|
||||
|
||||
#### `location` (string, обязательное)
|
||||
|
||||
- **Минимальная длина**: 2 символа
|
||||
- **Максимальная длина**: 150 символов
|
||||
- **Паттерн**: Разрешены буквы, цифры, пробелы, запятые, дефисы
|
||||
- **Ошибка валидации**: `"location": "Поле 'location' должно содержать от 2 до 150 символов"`
|
||||
|
||||
#### `client` (string, обязательное)
|
||||
|
||||
- **Допустимые значения**:
|
||||
- `"B2B клиенты"`
|
||||
- `"B2C клиенты"`
|
||||
- `"Частные лица"`
|
||||
- `"Корпорации"`
|
||||
- `"Малый бизнес"`
|
||||
- **Ошибка валидации**: `"client": "Поле 'client' должно быть одним из: B2B клиенты, B2C клиенты, Частные лица, Корпорации, Малый бизнес"`
|
||||
|
||||
#### `differentiator` (string, обязательное)
|
||||
|
||||
- **Минимальная длина**: 10 символов
|
||||
- **Максимальная длина**: 500 символов
|
||||
- **Паттерн**: Разрешены любые символы
|
||||
- **Ошибка валидации**: `"differentiator": "Поле 'differentiator' должно содержать от 10 до 500 символов"`
|
||||
|
||||
### Пример запроса
|
||||
|
||||
```json
|
||||
{
|
||||
"product": "Разработка мобильных приложений",
|
||||
"location": "Нур-Султан, Казахстан",
|
||||
"client": "B2B клиенты",
|
||||
"differentiator": "Специализируемся на быстрой разработке MVP за 4 недели с использованием современных технологий"
|
||||
}
|
||||
```
|
||||
|
||||
## Ответ
|
||||
|
||||
### Успешный ответ (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"analysisId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"status": "processing",
|
||||
"estimatedCompletionTime": "2025-01-20T15:30:00Z",
|
||||
"message": "Анализ запущен успешно. Результаты будут готовы в течение 5-10 минут."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Поля ответа
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
| ------------------------------ | ------- | --------------------------------------------------------- |
|
||||
| `success` | boolean | Флаг успешности операции |
|
||||
| `data.analysisId` | string | Уникальный идентификатор анализа (UUID) |
|
||||
| `data.status` | string | Статус анализа: `"processing"`, `"completed"`, `"failed"` |
|
||||
| `data.estimatedCompletionTime` | string | ISO 8601 дата/время ожидаемого завершения анализа |
|
||||
| `data.message` | string | Информационное сообщение для пользователя |
|
||||
|
||||
### Асинхронная обработка (202 Accepted)
|
||||
|
||||
Если анализ требует длительной обработки, сервер может вернуть статус 202:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"analysisId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"status": "queued",
|
||||
"queuePosition": 3,
|
||||
"estimatedWaitTime": 300,
|
||||
"message": "Запрос добавлен в очередь. Примерное время ожидания: 5 минут."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Обработка ошибок
|
||||
|
||||
### Ошибки валидации (400 Bad Request)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "Ошибка валидации входных данных",
|
||||
"details": {
|
||||
"product": "Поле 'product' обязательно для заполнения",
|
||||
"client": "Недопустимое значение поля 'client'"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Ошибка аутентификации (401 Unauthorized)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "UNAUTHORIZED",
|
||||
"message": "Требуется аутентификация"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Ошибка сервера (500 Internal Server Error)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "INTERNAL_SERVER_ERROR",
|
||||
"message": "Произошла внутренняя ошибка сервера. Попробуйте позже."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Ошибка таймаута (504 Gateway Timeout)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "TIMEOUT",
|
||||
"message": "Превышено время ожидания ответа от сервиса анализа"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Получение результатов анализа
|
||||
|
||||
После успешного запуска анализа, результаты можно получить по идентификатору:
|
||||
|
||||
**GET** `/api/marketing/analysis/{analysisId}`
|
||||
|
||||
### Пример запроса
|
||||
|
||||
```
|
||||
GET /api/marketing/analysis/550e8400-e29b-41d4-a716-446655440000
|
||||
```
|
||||
|
||||
### Пример ответа (когда анализ завершен)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"analysisId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"status": "completed",
|
||||
"createdAt": "2025-01-20T15:00:00Z",
|
||||
"completedAt": "2025-01-20T15:08:00Z",
|
||||
"report": {
|
||||
"summary": "Краткое резюме анализа...",
|
||||
"targetAudience": {
|
||||
"description": "Описание целевой аудитории...",
|
||||
"channels": ["Instagram", "LinkedIn", "Telegram"]
|
||||
},
|
||||
"recommendations": ["Рекомендация 1", "Рекомендация 2"],
|
||||
"strategy": {
|
||||
"duration": "2 недели",
|
||||
"channels": ["Instagram", "Telegram", "21MC"],
|
||||
"contentTypes": ["посты", "сторис", "баннеры"]
|
||||
},
|
||||
"pdfUrl": "/api/marketing/analysis/550e8400-e29b-41d4-a716-446655440000/download"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Статусы анализа
|
||||
|
||||
- `queued` - Запрос в очереди на обработку
|
||||
- `processing` - Анализ выполняется
|
||||
- `completed` - Анализ завершен успешно
|
||||
- `failed` - Анализ завершился с ошибкой
|
||||
|
||||
## Рекомендации по реализации
|
||||
|
||||
### 1. Валидация на бэкенде
|
||||
|
||||
```java
|
||||
// Пример валидации (Java/Spring Boot)
|
||||
@PostMapping("/api/marketing/analysis/start")
|
||||
public ResponseEntity<?> startAnalysis(@Valid @RequestBody MarketingAnalysisRequest request) {
|
||||
// Валидация выполняется автоматически через @Valid
|
||||
// Дополнительная бизнес-логика валидации
|
||||
if (!isValidClientType(request.getClient())) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(new ErrorResponse("VALIDATION_ERROR", "Недопустимый тип клиента"));
|
||||
}
|
||||
// Обработка запроса
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Асинхронная обработка
|
||||
|
||||
Рекомендуется использовать асинхронную обработку для длительных операций:
|
||||
|
||||
```java
|
||||
@Async
|
||||
public CompletableFuture<AnalysisResult> processAnalysis(MarketingAnalysisRequest request) {
|
||||
// Длительная обработка
|
||||
// Генерация отчета
|
||||
// Сохранение результатов
|
||||
return CompletableFuture.completedFuture(result);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Хранение данных
|
||||
|
||||
Рекомендуемая структура таблицы в БД:
|
||||
|
||||
```sql
|
||||
CREATE TABLE marketing_analysis (
|
||||
id UUID PRIMARY KEY,
|
||||
product VARCHAR(200) NOT NULL,
|
||||
location VARCHAR(150) NOT NULL,
|
||||
client_type VARCHAR(50) NOT NULL,
|
||||
differentiator TEXT NOT NULL,
|
||||
status VARCHAR(20) NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
completed_at TIMESTAMP,
|
||||
user_id UUID, -- Если требуется аутентификация
|
||||
report_data JSONB, -- JSON с результатами анализа
|
||||
CONSTRAINT valid_client_type CHECK (client_type IN (
|
||||
'B2B клиенты', 'B2C клиенты', 'Частные лица', 'Корпорации', 'Малый бизнес'
|
||||
)),
|
||||
CONSTRAINT valid_status CHECK (status IN (
|
||||
'queued', 'processing', 'completed', 'failed'
|
||||
))
|
||||
);
|
||||
```
|
||||
|
||||
### 4. Интеграция с AI сервисом
|
||||
|
||||
Если используется внешний AI сервис для генерации анализа:
|
||||
|
||||
```java
|
||||
public AnalysisResult generateAnalysis(MarketingAnalysisRequest request) {
|
||||
// Подготовка промпта для AI
|
||||
String prompt = String.format(
|
||||
"Проанализируй бизнес:\n" +
|
||||
"Продукт: %s\n" +
|
||||
"Локация: %s\n" +
|
||||
"Клиенты: %s\n" +
|
||||
"Уникальность: %s\n" +
|
||||
"Создай маркетинговую стратегию...",
|
||||
request.getProduct(),
|
||||
request.getLocation(),
|
||||
request.getClient(),
|
||||
request.getDifferentiator()
|
||||
);
|
||||
|
||||
// Вызов AI API
|
||||
return aiService.generateReport(prompt);
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Обработка ошибок
|
||||
|
||||
```java
|
||||
@ExceptionHandler(ValidationException.class)
|
||||
public ResponseEntity<ErrorResponse> handleValidationException(ValidationException e) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(new ErrorResponse("VALIDATION_ERROR", e.getMessage(), e.getDetails()));
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ErrorResponse> handleGenericException(Exception e) {
|
||||
log.error("Unexpected error", e);
|
||||
return ResponseEntity.status(500)
|
||||
.body(new ErrorResponse("INTERNAL_SERVER_ERROR", "Внутренняя ошибка сервера"));
|
||||
}
|
||||
```
|
||||
|
||||
## Тестирование
|
||||
|
||||
### Примеры тестовых запросов
|
||||
|
||||
#### Успешный запрос
|
||||
|
||||
```bash
|
||||
curl -X POST https://api.konturai.kz/api/marketing/analysis/start \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"product": "Разработка мобильных приложений",
|
||||
"location": "Алматы, Казахстан",
|
||||
"client": "B2B клиенты",
|
||||
"differentiator": "Специализируемся на быстрой разработке MVP за 4 недели"
|
||||
}'
|
||||
```
|
||||
|
||||
#### Запрос с ошибкой валидации
|
||||
|
||||
```bash
|
||||
curl -X POST https://api.konturai.kz/api/marketing/analysis/start \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"product": "AB",
|
||||
"location": "Алматы",
|
||||
"client": "Неверный тип",
|
||||
"differentiator": "Коротко"
|
||||
}'
|
||||
```
|
||||
|
||||
## Примечания
|
||||
|
||||
1. **Аутентификация**: Если требуется аутентификация, используйте JWT токен в заголовке `Authorization`
|
||||
2. **Rate Limiting**: Рекомендуется ограничить количество запросов на пользователя (например, 10 запросов в час)
|
||||
3. **Кэширование**: Можно кэшировать результаты для одинаковых запросов
|
||||
4. **Логирование**: Все запросы должны логироваться для отладки и аналитики
|
||||
5. **Мониторинг**: Отслеживайте время выполнения анализа и процент успешных завершений
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,68 @@
|
||||
# Улучшения диаграмм в PDF-отчетах
|
||||
|
||||
## Проблемы, которые были решены
|
||||
|
||||
1. **Обрезание диаграмм** - диаграммы выходили за границы страницы PDF
|
||||
2. **Некрасивый внешний вид** - диаграммы выглядели неаккуратно и нечитаемо
|
||||
3. **Плохое качество** - низкое разрешение и размытость
|
||||
|
||||
## Внесенные улучшения
|
||||
|
||||
### 1. Улучшенная логика масштабирования (`ResearchPdfService.java`)
|
||||
|
||||
- **Точный расчет доступного пространства** с учетом всех отступов
|
||||
- **Пропорциональное масштабирование** для сохранения соотношения сторон
|
||||
- **Ограничения масштабирования** (30%-100%) для читаемости
|
||||
- **Дополнительная проверка** размера после масштабирования
|
||||
- **Принудительное уменьшение** если диаграмма все еще не помещается
|
||||
|
||||
### 2. Оптимизация генерации SVG (`OpenAiChartService.java`)
|
||||
|
||||
- **Стандартные размеры** 800x600 для оптимального качества в PDF
|
||||
- **Увеличенные отступы** (минимум 60px) вокруг диаграммы
|
||||
- **Улучшенная стилизация** с градиентами, тенями и рамками
|
||||
- **Контрастные цвета** и размер шрифта не менее 14px
|
||||
- **Сетка и оси** для лучшей читаемости
|
||||
|
||||
### 3. Улучшенная конвертация SVG в PNG (`SvgToPngConverter.java`)
|
||||
|
||||
- **Высокое качество** конвертации для PDF
|
||||
- **Автоматические размеры** если SVG не содержит явных размеров
|
||||
- **Оптимизация** для печати и отображения
|
||||
|
||||
### 4. Адаптивное размещение диаграмм
|
||||
|
||||
- **Автоматическое разбиение** больших диаграмм на отдельные страницы
|
||||
- **Подписи к диаграммам** при наличии нескольких диаграмм
|
||||
- **Улучшенные отступы** для лучшего внешнего вида
|
||||
- **Центрирование** всех диаграмм
|
||||
|
||||
### 5. Улучшения в ReportGenerationService
|
||||
|
||||
- **Улучшенное масштабирование** для диаграмм тональности
|
||||
- **Пропорциональное изменение размера** с сохранением качества
|
||||
- **Лучшее позиционирование** и отступы
|
||||
|
||||
## Результат
|
||||
|
||||
- ✅ Диаграммы больше не обрезаются
|
||||
- ✅ Улучшенное качество и читаемость
|
||||
- ✅ Профессиональный внешний вид
|
||||
- ✅ Адаптивное размещение на страницах
|
||||
- ✅ Оптимизация для PDF-формата
|
||||
|
||||
## Технические детали
|
||||
|
||||
### Ключевые методы:
|
||||
|
||||
- `addChartsSection()` - основная логика добавления диаграмм
|
||||
- `shouldSplitChart()` - проверка необходимости разбиения на страницы
|
||||
- `addChartWithPagination()` - добавление с разбиением
|
||||
- `buildSvgPrompt()` - улучшенные промпты для генерации SVG
|
||||
|
||||
### Параметры масштабирования:
|
||||
|
||||
- Минимальный масштаб: 30%
|
||||
- Максимальный масштаб: 100%
|
||||
- Безопасный отступ: 90% от доступного пространства
|
||||
- Стандартные размеры SVG: 800x600px
|
||||
@@ -0,0 +1,119 @@
|
||||
# Резюме реализации AI-агента для генерации PDF-отчётов
|
||||
|
||||
## Выполненные задачи
|
||||
|
||||
### ✅ 1. Создание DTO классов
|
||||
|
||||
- `DeepResearchRequest.java` - для запросов к deep-research API
|
||||
- `DeepResearchResponse.java` - для ответов от deep-research API
|
||||
- `ResearchReportRequest.java` - для валидации входящих запросов
|
||||
|
||||
### ✅ 2. Реализация HTTP клиента
|
||||
|
||||
- `DeepResearchService.java` - сервис для взаимодействия с deep-research API
|
||||
- Поддержка синхронных и асинхронных вызовов
|
||||
- Обработка ошибок и таймаутов
|
||||
|
||||
### ✅ 3. Генерация PDF отчётов
|
||||
|
||||
- `ResearchPdfService.java` - сервис для создания PDF с профессиональным оформлением
|
||||
- Титульная страница с названием исследования
|
||||
- Автоматически генерируемое содержание
|
||||
- Структурированный основной текст
|
||||
- Список источников из visitedUrls
|
||||
|
||||
### ✅ 4. Модификация ReportController
|
||||
|
||||
- Добавлен новый эндпоинт `POST /api/parser/report`
|
||||
- Валидация входных параметров
|
||||
- Обработка ошибок (400, 500, 504)
|
||||
- Сохранение отчётов в историю и MinIO
|
||||
|
||||
### ✅ 5. Обновление конфигурации
|
||||
|
||||
- Добавлены настройки deep-research API в `application.properties`
|
||||
- Добавлена зависимость Jackson в `pom.xml`
|
||||
- Настроены таймауты и URL для внешнего API
|
||||
|
||||
## API Спецификация
|
||||
|
||||
### Эндпоинт
|
||||
|
||||
```
|
||||
POST /api/parser/report
|
||||
```
|
||||
|
||||
### Параметры запроса
|
||||
|
||||
```json
|
||||
{
|
||||
"query": "string (обязательный)",
|
||||
"lang": "string (по умолчанию: ru)",
|
||||
"depth": "integer 1-5 (по умолчанию: 3)",
|
||||
"breadth": "integer 2-10 (по умолчанию: 5)",
|
||||
"report_type": "string (по умолчанию: report)"
|
||||
}
|
||||
```
|
||||
|
||||
### Ответы
|
||||
|
||||
- **200 OK**: PDF файл с отчётом
|
||||
- **400 Bad Request**: Некорректные параметры
|
||||
- **500 Internal Server Error**: Внутренняя ошибка
|
||||
- **504 Gateway Timeout**: Таймаут внешнего API
|
||||
|
||||
## Структура PDF-отчёта
|
||||
|
||||
1. **Титульная страница**
|
||||
|
||||
- Название исследования
|
||||
- Дата создания
|
||||
- Подзаголовок
|
||||
|
||||
2. **Содержание**
|
||||
|
||||
- Автоматически генерируемое оглавление
|
||||
|
||||
3. **Основная часть**
|
||||
|
||||
- Введение
|
||||
- Текст отчёта от deep-research API
|
||||
- Заключение
|
||||
|
||||
4. **Источники**
|
||||
- Список URL из visitedUrls
|
||||
|
||||
## Конфигурация
|
||||
|
||||
```properties
|
||||
# Deep Research API Configuration
|
||||
deep-research.api.url=http://185.35.223.45:3051
|
||||
deep-research.api.timeout=300000
|
||||
```
|
||||
|
||||
## Тестирование
|
||||
|
||||
Создан тестовый скрипт `test_research_api.sh` для проверки функциональности.
|
||||
|
||||
## Документация
|
||||
|
||||
Создано подробное руководство `RESEARCH_API_GUIDE.md` с примерами использования.
|
||||
|
||||
## Соответствие техническому заданию
|
||||
|
||||
✅ Все требования из ТЗ выполнены:
|
||||
|
||||
- Модифицирован эндпоинт `/api/parser/report`
|
||||
- Реализована интеграция с deep-research API
|
||||
- Добавлена генерация PDF с требуемой структурой
|
||||
- Настроена обработка ошибок
|
||||
- Добавлена валидация параметров
|
||||
- Реализовано сохранение в историю отчётов
|
||||
|
||||
## Готовность к использованию
|
||||
|
||||
Система готова к тестированию и использованию. Для запуска необходимо:
|
||||
|
||||
1. Убедиться, что deep-research API доступен
|
||||
2. Запустить приложение
|
||||
3. Отправить POST запрос на `/api/parser/report`
|
||||
@@ -0,0 +1,184 @@
|
||||
# Рефакторинг Маркетинговой Стратегии - Changelog
|
||||
|
||||
## Дата: 11 апреля 2026
|
||||
|
||||
### 📋 Описание изменений
|
||||
|
||||
Полностью переработана логика генерации маркетинговой стратегии для решения проблем:
|
||||
- Недостаточное количество постов (было 8, стало 16)
|
||||
- Неравномерное распределение постов (пропуски 2-3 дня)
|
||||
- Отсутствие связи между постами и неделями
|
||||
- Непрофессиональное отображение на фронте
|
||||
|
||||
---
|
||||
|
||||
### ✅ Что изменено
|
||||
|
||||
#### 1. **Увеличено количество постов: 8 → 16**
|
||||
- **Было:** 8 постов за 4 недели (2 поста/неделю)
|
||||
- **Стало:** 16 постов за 4 недели (4 поста/неделю)
|
||||
- **Распределение:** Пн 09:00, Ср 12:00, Пт 18:00, Сб 20:00
|
||||
|
||||
#### 2. **Добавлено поле `weekNumber`**
|
||||
- **Model:** `MarketingStrategy.PostCalendarItem.weekNumber`
|
||||
- **DTO:** `MarketingStrategyResponse.PostCalendarItem.weekNumber`
|
||||
- **Цель:** Чёткая связь каждого поста с конкретной неделей стратегии
|
||||
|
||||
#### 3. **Нормализация расписания**
|
||||
- Новый метод: `normalizePostSchedule()`
|
||||
- Автоматически выравнивает посты по дням недели
|
||||
- Гарантирует равномерный ритм публикаций
|
||||
- Сохраняет валидные даты от AI (если в пределах ±2 дней)
|
||||
|
||||
#### 4. **Усиленный промпт для AI**
|
||||
- Чёткие инструкции по распределению 16 постов
|
||||
- Примеры правильного JSON с `weekNumber`
|
||||
- Явные требования: 4 поста/неделю, Пн-Ср-Пт-Сб
|
||||
- Увеличено количество видео: 2 → 4 (по 1 в неделю)
|
||||
|
||||
#### 5. **Улучшенная валидация**
|
||||
- Логирование при < 12 постах от AI
|
||||
- Авто-расчёт дат если AI вернул некорректные
|
||||
- Нормализация `weekNumber` для всех постов
|
||||
|
||||
---
|
||||
|
||||
### 📁 Изменённые файлы
|
||||
|
||||
1. **`MarketingStrategy.java`** (Model)
|
||||
- Добавлено поле `weekNumber` в `PostCalendarItem`
|
||||
|
||||
2. **`MarketingStrategyResponse.java`** (DTO)
|
||||
- Добавлено поле `weekNumber` в `PostCalendarItem`
|
||||
- Добавлены геттеры/сеттеры + поля `videoUrl`, `videoFilename`
|
||||
|
||||
3. **`MarketingStrategyV3Service.java`** (Service)
|
||||
- Усилен `buildUserPrompt()` — детальные инструкции по распределению
|
||||
- Переписан `populateStrategyEntity()` — обработка `weekNumber`
|
||||
- Новый метод `normalizePostSchedule()` — выравнивание расписания
|
||||
- Новый метод `getDayOffset()` — расчёт дней недели
|
||||
- Обновлён `buildPostCountPlan()` — 16 постов вместо 12
|
||||
- Увеличен лимит видео: 2 → 4
|
||||
|
||||
4. **`getStrategyResult()`**
|
||||
- Добавлено поле `weekNumber` в ответ API
|
||||
|
||||
---
|
||||
|
||||
### 🎯 Результат
|
||||
|
||||
#### **До рефакторинга:**
|
||||
```json
|
||||
{
|
||||
"postCalendar": [
|
||||
{ "publishDate": "2026-04-10T09:00:00", "theme": "Кейс: ..." },
|
||||
{ "publishDate": "2026-04-12T18:00:00", "theme": "Процесс: ..." },
|
||||
{ "publishDate": "2026-04-15T12:00:00", "theme": "Отзыв: ..." }
|
||||
// ... всего 8 постов, пропуски 2-3 дня
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### **После рефакторинга:**
|
||||
```json
|
||||
{
|
||||
"postCalendar": [
|
||||
{
|
||||
"postIndex": 0,
|
||||
"publishDate": "2026-04-13T09:00:00",
|
||||
"weekNumber": 1,
|
||||
"platform": "Instagram",
|
||||
"contentType": "видео",
|
||||
"theme": "Кейс: Как мы увеличили продажи на 40%",
|
||||
"publishTime": "09:00"
|
||||
},
|
||||
{
|
||||
"postIndex": 1,
|
||||
"publishDate": "2026-04-15T12:00:00",
|
||||
"weekNumber": 1,
|
||||
"platform": "Instagram",
|
||||
"contentType": "фото",
|
||||
"theme": "Процесс: За кадром нашей работы",
|
||||
"publishTime": "12:00"
|
||||
},
|
||||
// ... ещё 14 постов, ровно 4 в неделю
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 📊 Расписание постов (пример)
|
||||
|
||||
| Неделя | День | Время | Тип контента |
|
||||
|--------|------|-------|--------------|
|
||||
| 1 | Понедельник (день 1) | 09:00 | 🎥 Видео (Кейс) |
|
||||
| 1 | Среда (день 3) | 12:00 | 📸 Фото (Процесс) |
|
||||
| 1 | Пятница (день 5) | 18:00 | 📸 Фото (Отзыв) |
|
||||
| 1 | Суббота (день 6) | 20:00 | 📸 Фото (Экспертный) |
|
||||
| 2 | Понедельник (день 8) | 09:00 | 🎥 Видео (Кейс) |
|
||||
| 2 | Среда (день 10) | 12:00 | 📸 Фото (Процесс) |
|
||||
| ... | ... | ... | ... |
|
||||
|
||||
---
|
||||
|
||||
### 🚀 Преимущества
|
||||
|
||||
✅ **Профессиональный вид** — равномерное расписание, как у SMM-агентств
|
||||
✅ **Предсказуемость** — клиент знает когда будет каждый пост
|
||||
✅ **Связность** — каждый пост привязан к неделе и теме
|
||||
✅ **Гибкость** — AI может предлагать даты, но система нормализует
|
||||
✅ **Масштабируемость** — легко изменить на 3 или 5 постов/неделю
|
||||
|
||||
---
|
||||
|
||||
### ⚠️ Важно для фронтенда
|
||||
|
||||
Теперь в ответе API появляется новое поле `weekNumber` у каждого поста:
|
||||
|
||||
```typescript
|
||||
interface PostCalendarItem {
|
||||
postIndex: number;
|
||||
publishDate: string;
|
||||
weekNumber: number; // ← НОВОЕ ПОЛЕ
|
||||
platform: string;
|
||||
contentType: string;
|
||||
theme: string;
|
||||
postText: string;
|
||||
hashtags: string[];
|
||||
publishTime: string;
|
||||
imageUrl?: string;
|
||||
videoUrl?: string;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Рекомендуется использовать `weekNumber` для группировки постов на фронте!
|
||||
|
||||
---
|
||||
|
||||
### 🧪 Тестирование
|
||||
|
||||
После деплоя проверьте:
|
||||
1. Генерацию новой стратегии — должно быть 16 постов
|
||||
2. Распределение по неделям — по 4 поста в каждой
|
||||
3. Даты публикаций — Пн-Ср-Пт-Сб
|
||||
4. Поле `weekNumber` — должно присутствовать у всех постов
|
||||
5. Видео-посты — ровно 4 (по 1 в неделю)
|
||||
|
||||
---
|
||||
|
||||
### 📝 Компиляция
|
||||
|
||||
```bash
|
||||
cd c:\Users\777\IdeaProjects\telecomBackend
|
||||
mvnw.cmd compile -DskipTests
|
||||
```
|
||||
|
||||
✅ **BUILD SUCCESS** — все изменения компилируются без ошибок!
|
||||
|
||||
---
|
||||
|
||||
**Автор:** AI Assistant
|
||||
**Дата:** 11.04.2026
|
||||
**Версия:** 2.0
|
||||
@@ -0,0 +1,62 @@
|
||||
# Исправление NullPointerException в ReportGenerationService
|
||||
|
||||
## Проблема
|
||||
|
||||
Ошибка: `Cannot invoke "java.time.LocalDateTime.toLocalDate()" because "end" is null`
|
||||
|
||||
## Причина
|
||||
|
||||
В методах `generate()` и `simpleGenerate()` класса `ReportGenerationService` код пытался вызвать `end.toLocalDate()` без проверки на null, когда поле `endDate` в `ReportGenerateRequest` было null.
|
||||
|
||||
## Исправления
|
||||
|
||||
### 1. Добавлены значения по умолчанию для дат
|
||||
|
||||
```java
|
||||
// Устанавливаем значения по умолчанию, если даты не указаны
|
||||
if (start == null) {
|
||||
start = LocalDateTime.now().minusDays(30); // 30 дней назад
|
||||
}
|
||||
if (end == null) {
|
||||
end = LocalDateTime.now(); // сейчас
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Исправлены вызовы format() в buildDocx и buildPdf
|
||||
|
||||
```java
|
||||
// Было:
|
||||
req.getStartDate().format(dt), req.getEndDate().format(dt)
|
||||
|
||||
// Стало:
|
||||
req.getStartDate() != null ? req.getStartDate().format(dt) : "не указано",
|
||||
req.getEndDate() != null ? req.getEndDate().format(dt) : "не указано"
|
||||
```
|
||||
|
||||
### 3. Упрощено создание имени файла
|
||||
|
||||
```java
|
||||
// Было:
|
||||
String dateSuffix = end != null ? end.toLocalDate().toString() : "unknown_date";
|
||||
|
||||
// Стало:
|
||||
String dateSuffix = end.toLocalDate().toString(); // end теперь всегда не null
|
||||
```
|
||||
|
||||
## Затронутые методы
|
||||
|
||||
- `generate(ReportGenerateRequest req)`
|
||||
- `simpleGenerate(ReportGenerateRequest req)`
|
||||
- `buildDocx(ReportGenerateRequest req, List<MarketItem> items, Sections sections)`
|
||||
- `buildPdf(ReportGenerateRequest req, List<MarketItem> items, Sections sections)`
|
||||
|
||||
## Результат
|
||||
|
||||
- ✅ Устранена ошибка NullPointerException
|
||||
- ✅ Добавлена обработка случаев, когда даты не указаны
|
||||
- ✅ Установлены разумные значения по умолчанию (последние 30 дней)
|
||||
- ✅ Сохранена обратная совместимость
|
||||
|
||||
## Тестирование
|
||||
|
||||
После исправлений асинхронная генерация отчётов должна работать корректно даже при отсутствии дат в запросе.
|
||||
@@ -0,0 +1,68 @@
|
||||
Я внимательно изучил поле `fullAnalysis` в присланном JSON. Там всё ещё обычный текст и списки, а не Markdown-блоки с JSON.
|
||||
|
||||
**Вот пример того, что мы видим сейчас (в `fullAnalysis`):**
|
||||
|
||||
```markdown
|
||||
## VIII. Воронка спроса
|
||||
|
||||
Воронка продаж показывает следующие этапы:
|
||||
|
||||
- Охват: 1000
|
||||
- Вовлечённость: 300 (30%)
|
||||
...
|
||||
```
|
||||
|
||||
**А должны были увидеть (согласно плану):**
|
||||
|
||||
````markdown
|
||||
## VIII. Воронка спроса
|
||||
|
||||
```json:conversionFunnel
|
||||
[
|
||||
{ "stage": "Охват", "value": 1000, "conversion": 0 },
|
||||
...
|
||||
]
|
||||
```
|
||||
````
|
||||
|
||||
````
|
||||
|
||||
---
|
||||
|
||||
### Почему так произошло?
|
||||
|
||||
Проблема в архитектуре метода `processAnalysis` в вашем Java-коде.
|
||||
|
||||
1. Вы изменили метод `buildMarkdownFromReport`, и он, скорее всего, работает правильно и генерирует строку с JSON-блоками.
|
||||
2. **НО** эта строка (`markdownContent`) используется **только для генерации PDF**.
|
||||
3. В базу данных и во фронтенд (в поле `reportData`) сохраняется старая версия текста, сгенерированная AI (Промпт №2), которая не знает о нашей новой логике встраивания графиков.
|
||||
|
||||
### Как исправить?
|
||||
|
||||
Нам нужно сказать Cursor'у, чтобы он **перезаписал** поле `fullAnalysis` внутри `reportData` тем Markdown-контентом, который генерирует метод `buildMarkdownFromReport`.
|
||||
|
||||
**Промпт для Cursor (отправь это, чтобы починить):**
|
||||
|
||||
```text
|
||||
Я не вижу изменений в JSON-ответе для фронтенда. Проблема в методе `processAnalysis` класса `MarketingAnalysisService`.
|
||||
|
||||
Сейчас переменная `markdownContent` (результат метода `buildMarkdownFromReport`) используется только для генерации PDF.
|
||||
А в `analysis.setReportData(reportData)` попадает старый текст, сгенерированный AI.
|
||||
|
||||
Пожалуйста, сделай следующее изменение в методе `processAnalysis`:
|
||||
|
||||
1. После вызова:
|
||||
String markdownContent = buildMarkdownFromReport(reportData, request);
|
||||
|
||||
2. Добавь строку, которая обновляет reportData:
|
||||
reportData.put("fullAnalysis", markdownContent);
|
||||
|
||||
Это нужно, чтобы фронтенд получал именно сформированный Markdown с JSON-блоками (charts), а не просто текст от AI.
|
||||
````
|
||||
|
||||
### Что произойдет после этого исправления:
|
||||
|
||||
1. Backend сгенерирует Markdown с JSON-блоками.
|
||||
2. Этот Markdown сохранится в базу данных как `fullAnalysis`.
|
||||
3. Фронтенд получит JSON, где внутри поля `report.fullAnalysis` будут блоки кода ` ```json:seasonality... `.
|
||||
4. Твой кастомный рендерер на фронтенде (о котором я писал выше) увидит эти блоки и нарисует графики.
|
||||
@@ -0,0 +1,213 @@
|
||||
Промт №1 — для JSON-аналитики (цифры + структура для графиков).
|
||||
Промт №2 — для текстового отчёта на основе JSON.
|
||||
Краткое ТЗ для backend: как из этого собирать графики и отчёт.
|
||||
|
||||
🔷 ПРОМТ №1 — МАРКЕТИНГОВЫЙ АНАЛИЗ С ВЫВОДОМ В JSON (ДЛЯ ГРАФИКОВ)
|
||||
Этот промт модель должна выполнять в режиме:
отдаёт ТОЛЬКО JSON, без текста, без комментариев.
|
||||
Ты — профессиональный маркетинговый аналитик, работающий с малым и средним бизнесом в Казахстане.
|
||||
|
||||
Твоя задача — на основе входных данных о бизнесе пользователя выполнить маркетинговый анализ и выдать результат ТОЛЬКО в виде корректного JSON-объекта строго по указанной структуре.
|
||||
|
||||
Не добавляй пояснений, комментариев, текста до или после JSON.
|
||||
Ответ должен быть валидным JSON.
|
||||
|
||||
---
|
||||
|
||||
ВХОДНЫЕ ДАННЫЕ ПОЛЬЗОВАТЕЛЯ:
|
||||
|
||||
- Название компании/продукта: {{company_name}}
|
||||
- Описание продукта/услуг: {{product_description}}
|
||||
- Целевая аудитория (если указана): {{target_audience}}
|
||||
- Конкуренты (если указаны): {{competitors}}
|
||||
- Цель продвижения: {{goals}}
|
||||
- Регион/город: {{region}}
|
||||
|
||||
## Если каких-то данных не хватает, ты можешь логично дополнить предположениями, НО в таких полях добавь флаг "estimated": true.
|
||||
|
||||
СФОРМИРУЙ ОДИН JSON СЛЕДУЮЩЕЙ СТРУКТУРЫ:
|
||||
|
||||
{
|
||||
"meta": {
|
||||
"companyName": string,
|
||||
"region": string,
|
||||
"primaryGoal": string,
|
||||
"notes": string
|
||||
},
|
||||
|
||||
"seasonality": {
|
||||
"description": string,
|
||||
"months": [ "Январь", "Февраль", ... 12 месяцев ],
|
||||
"demandIndex": [12 чисел от 0 до 100],
|
||||
"estimated": boolean
|
||||
},
|
||||
|
||||
"competitors": [
|
||||
{
|
||||
"name": string,
|
||||
"reach": integer, // 1000–50000
|
||||
"socialActivityIndex": integer, // 0–100
|
||||
"reviewsCount": integer, // 0–5000
|
||||
"strengths": [string],
|
||||
"weaknesses": [string],
|
||||
"estimated": boolean
|
||||
}
|
||||
],
|
||||
|
||||
"audienceSegments": {
|
||||
"description": string,
|
||||
"segments": [
|
||||
{
|
||||
"name": string,
|
||||
"sharePercent": integer, // сумма всех sharePercent = 100
|
||||
"ageRange": string,
|
||||
"gender": string, // "мужчины", "женщины", "смешанная"
|
||||
"incomeLevel": string, // например: "низкий", "средний", "выше среднего"
|
||||
"keyNeeds": [string],
|
||||
"estimated": boolean
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"channels": [
|
||||
{
|
||||
"name": string, // "Instagram", "TikTok", "Telegram", "2GIS", "Google Ads" и т.п.
|
||||
"potentialIndex": integer, // 0–100
|
||||
"priorityLevel": string, // "высокий", "средний", "низкий"
|
||||
"roleInStrategy": string, // коротко: для чего этот канал
|
||||
"justification": string, // почему этот канал важен
|
||||
"estimated": boolean
|
||||
}
|
||||
],
|
||||
|
||||
"funnel": {
|
||||
"description": string,
|
||||
"stages": [
|
||||
{
|
||||
"stageName": string, // "Охват", "Вовлечённость", "Клики", "Переходы", "Заявки", "Покупки"
|
||||
"value": integer, // количество (должно логично уменьшаться вниз по воронке)
|
||||
"conversionPercent": number // от предыдущего этапа, с одним знаком после запятой
|
||||
}
|
||||
],
|
||||
"estimated": boolean
|
||||
},
|
||||
|
||||
"swot": {
|
||||
"strengths": [string],
|
||||
"weaknesses": [string],
|
||||
"opportunities": [string],
|
||||
"threats": [string]
|
||||
},
|
||||
|
||||
"positioning": {
|
||||
"brandImage": string,
|
||||
"keyMessage": string,
|
||||
"toneOfVoice": string,
|
||||
"trustFactors": [string]
|
||||
},
|
||||
|
||||
"valueProposition": {
|
||||
"utpVariants": [string], // 1–3 варианта УТП
|
||||
"mainBenefits": [string]
|
||||
},
|
||||
|
||||
"contentRecommendations": {
|
||||
"salesContent": [string],
|
||||
"expertContent": [string],
|
||||
"trustContent": [string],
|
||||
"entertainmentContent": [string]
|
||||
}
|
||||
}
|
||||
|
||||
ТРЕБОВАНИЯ:
|
||||
|
||||
- Верни ТОЛЬКО JSON, без пояснений.
|
||||
- Следи, чтобы JSON был синтаксически корректным.
|
||||
- Сумма "sharePercent" по audienceSegments.segments должна быть = 100.
|
||||
- Значения "demandIndex" должны быть от 0 до 100.
|
||||
- Значения "potentialIndex" должны быть от 0 до 100.
|
||||
- В "funnel.stages" значения "value" должны уменьшаться от этапа к этапу.
|
||||
- Не используй null, если можно задать разумное значение.
|
||||
- Все строки — на русском языке.
|
||||
Этот промт:
|
||||
даёт тебе цифры,
|
||||
структурирует данные,
|
||||
обеспечивает основу для построения графиков и диаграмм,
|
||||
даёт SWOT в структурированном виде, а не “простынёй текста”.
|
||||
|
||||
ПРОМТ №2 — ТЕКСТОВЫЙ ОТЧЁТ НА ОСНОВЕ JSON (КРАСИВЫЙ ЧЕЛОВЕЧЕСКИЙ ОТЧЁТ)
|
||||
Этот промт вызывается вторым шагом.
Ему на вход даётся JSON, который вернул промт №1.
|
||||
|
||||
Ты — профессиональный маркетолог и аналитик.
|
||||
|
||||
Тебе передан JSON с результатами маркетингового анализа в следующей структуре:
|
||||
{{analysis_json}}
|
||||
|
||||
На основе этих данных подготовь ПОНЯТНЫЙ для предпринимателя малого/среднего бизнеса текстовый отчёт.
|
||||
|
||||
ТРЕБОВАНИЯ К ОТЧЁТУ:
|
||||
|
||||
1. Структура разделов:
|
||||
I. Краткое резюме (что за бизнес, какая цель, общий вывод)
|
||||
II. Анализ продукта
|
||||
III. Анализ рынка и сезонности
|
||||
IV. Анализ целевой аудитории
|
||||
V. Анализ конкурентов
|
||||
VI. SWOT-анализ (в виде 4 списков)
|
||||
VII. Каналы продвижения и их потенциал
|
||||
VIII. Воронка спроса (с пояснением, где “узкое место”)
|
||||
IX. Рекомендации по позиционированию и УТП
|
||||
X. Рекомендации по контенту
|
||||
XI. Итоговая стратегия: что делать в первую очередь
|
||||
|
||||
2. Используй данные ТОЛЬКО из JSON:
|
||||
|
||||
- seasonality → для описания сезонности
|
||||
- audienceSegments → для портрета ЦА
|
||||
- competitors → для анализа конкурентов
|
||||
- channels → для описания каналов
|
||||
- funnel → для объяснения этапов воронки
|
||||
- swot → для SWOT-раздела
|
||||
- valueProposition, positioning, contentRecommendations → для рекомендаций.
|
||||
|
||||
3. Пиши простым, деловым, понятным языком:
|
||||
|
||||
- без академического жаргона,
|
||||
- без лишней теории маркетинга,
|
||||
- с акцентом на практическую пользу.
|
||||
|
||||
4. Не повторяй сам JSON.
|
||||
Отчёт — это человеческое объяснение на основе уже рассчитанных данных.
|
||||
|
||||
5. Не выдумывай новые цифры — используй только те, что есть в JSON.
|
||||
|
||||
ТЗ ДЛЯ BACKEND: КАК ИЗ ЭТОГО СДЕЛАТЬ ТАБЛИЦЫ, ГРАФИКИ И ОТЧЁТ
|
||||
Кратко, по шагам.
|
||||
|
||||
1. Вызов промта №1 (JSON-анализ)
|
||||
Backend берёт входные данные пользователя.
|
||||
Отправляет их в LLM с промтом №1.
|
||||
Получает строку JSON.
|
||||
Валидирует JSON:
|
||||
парсится/не парсится,
|
||||
есть ли все ключи,
|
||||
сумма процентов = 100 и т.д.
|
||||
Сохраняет JSON в БД (например, в колонке analysis_json).
|
||||
2. Построение графиков и диаграмм
|
||||
Используем JSON-поля:
|
||||
seasonality.months + seasonality.demandIndex
→ линейный график (Line chart).
|
||||
competitors
→ столбчатая диаграмма (Bar chart) по reach или socialActivityIndex.
|
||||
audienceSegments.segments
→ круговая диаграмма (Pie chart) по sharePercent.
|
||||
channels
→ горизонтальный bar chart по potentialIndex.
|
||||
funnel.stages
→ воронка (Funnel chart) и/или bar chart по value.
|
||||
Технически:
|
||||
Python (matplotlib / plotly) или JS (ECharts / Chart.js) — на твой выбор.
|
||||
На выходе: PNG/SVG для отчёта или интерактив для веб-панели.
|
||||
3. Вызов промта №2 (текстовый отчёт)
|
||||
Backend берёт сохранённый analysis_json.
|
||||
Подставляет его в промт №2 (как {{analysis_json}}).
|
||||
LLM возвращает текстовый отчёт.
|
||||
Отчёт сохраняется в БД и/или показывается пользователю.
|
||||
4. SWOT и таблицы
|
||||
SWOT уже структурирован в swot.
|
||||
На фронтенде его легко отобразить как таблицу 2×2.
|
||||
Остальные таблицы (конкуренты, сегменты ЦА, каналы, воронка) строятся напрямую из JSON.
|
||||
@@ -0,0 +1,109 @@
|
||||
Я хочу изменить логику формирования Markdown-отчета в методе `buildMarkdownFromReport` класса `MarketingAnalysisService`.
|
||||
|
||||
Сейчас метод создает Markdown-таблицы (ASCII tables) для отображения данных (сезонность, конкуренты и т.д.).
|
||||
Мне нужно изменить это поведение: вместо таблиц я хочу встраивать JSON-данные для графиков прямо в текст отчета, используя Markdown code blocks.
|
||||
|
||||
**Требования:**
|
||||
|
||||
1. Найди метод `buildMarkdownFromReport`.
|
||||
2. В местах, где сейчас формируются таблицы (например, для "seasonality", "competitors", "audienceSegmentation", "channelsPotential", "conversionFunnel"), замени генерацию таблиц на вставку JSON-блока.
|
||||
3. Формат блока должен быть таким:
|
||||
```json:chart-type
|
||||
{ ... json data ... }
|
||||
```
|
||||
|
||||
````
|
||||
|
||||
Где `chart-type` — это идентификатор типа графика (например, `json:seasonality`, `json:competitors`, `json:funnel`). Это поможет фронтенду понять, какой компонент графика рисовать.
|
||||
4\. Используй поле `objectMapper` для сериализации соответствующих карт/списков (chartsData) в JSON-строку перед вставкой.
|
||||
5\. Убедись, что JSON отформатирован красиво (pretty print), чтобы он был читаемым, если смотреть на raw text.
|
||||
|
||||
**Пример желаемого результата в Markdown:**
|
||||
Вместо таблицы с месяцами, должно получиться:
|
||||
|
||||
### 1️⃣ Сезонность спроса (12 месяцев)
|
||||
|
||||
```json:seasonality
|
||||
{
|
||||
"Январь": 70,
|
||||
"Февраль": 60,
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Пожалуйста, рефактори метод `buildMarkdownFromReport` соответствующим образом.
|
||||
|
||||
````
|
||||
|
||||
---
|
||||
|
||||
### Как это будет работать (для твоего понимания)
|
||||
|
||||
После того как Cursor применит изменения, твой метод `buildMarkdownFromReport` будет выглядеть примерно так (псевдокод):
|
||||
|
||||
````java
|
||||
// Внутри buildMarkdownFromReport
|
||||
|
||||
// ... заголовки ...
|
||||
|
||||
if (chartsData.containsKey("seasonality")) {
|
||||
markdown.append("### 1️⃣ Сезонность спроса (12 месяцев)\n\n");
|
||||
|
||||
// Сериализуем данные в JSON
|
||||
String jsonStr = objectMapper.writerWithDefaultPrettyPrinter()
|
||||
.writeValueAsString(chartsData.get("seasonality"));
|
||||
|
||||
// Вставляем как блок кода с уникальным идентификатором языка
|
||||
markdown.append("```json:seasonality\n");
|
||||
markdown.append(jsonStr);
|
||||
markdown.append("\n```\n\n");
|
||||
}
|
||||
````
|
||||
|
||||
### Что нужно сделать на Фронтенде?
|
||||
|
||||
Так как мы изменили подход, фронтенд (React/Vue) должен использовать библиотеку для рендеринга Markdown (например, `react-markdown`).
|
||||
|
||||
Тебе нужно будет написать кастомный рендерер для элементов `code`.
|
||||
|
||||
**Пример логики для фронтенда (React + react-markdown):**
|
||||
|
||||
```javascript
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { SeasonalityChart, FunnelChart } from './MyCharts'; // Твои компоненты графиков
|
||||
|
||||
const MarkdownRender = ({ content }) => {
|
||||
return (
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
code({ node, inline, className, children, ...props }) {
|
||||
const match = /language-(\w+):(\w+)/.exec(className || '');
|
||||
// match[1] будет "json", match[2] будет тип графика (seasonality, funnel и т.д.)
|
||||
|
||||
if (!inline && match && match[1] === 'json') {
|
||||
const chartType = match[2];
|
||||
const data = JSON.parse(String(children).replace(/\n$/, ''));
|
||||
|
||||
switch (chartType) {
|
||||
case 'seasonality':
|
||||
return <SeasonalityChart data={data} />;
|
||||
case 'funnel':
|
||||
return <FunnelChart data={data} />;
|
||||
default:
|
||||
return <pre>{children}</pre>; // Фоллбек
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
};
|
||||
```
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
ФИНАЛЬНЫЙ ПРОМТ ДЛЯ ГЕНЕРАЦИИ МАРКЕТИНГОВОГО АНАЛИЗА С ГРАФИКАМИ И ЧИСЛОВЫМИ ПОКАЗАТЕЛЯМИ
|
||||
(готов к использованию в backend или системе агентов)
|
||||
|
||||
Ты — профессиональный маркетолог и аналитик, работающий с малым и средним бизнесом Казахстана.
Твоя задача — выполнить глубокий маркетинговый анализ компании пользователя с генерацией всех необходимых количественных данных для построения графиков и диаграмм.
Пиши структурированно, лаконично, без воды, понятным языком, ориентируясь на предпринимателя МСБ.
|
||||
|
||||
📥 Входные данные пользователя:
|
||||
Название компании/продукта: {{company_name}}
|
||||
Описание продукта: {{product_description}}
|
||||
Целевая аудитория: {{target_audience}}
|
||||
Конкуренты: {{competitors}}
|
||||
Цель продвижения: {{goals}}
|
||||
Регион/город: {{region}}
|
||||
Если данных недостаточно — логично дополни, отметив:
«Данные уточнены системой автоматически».
|
||||
|
||||
📊 Требования к числовым данным
|
||||
Ты обязан сгенерировать реалистичные числовые значения, подходящие для автоматической визуализации.
|
||||
|
||||
1️⃣ Сезонность спроса (линейный график, 12 месяцев)
|
||||
Выведи таблицу:
|
||||
| Месяц | Уровень спроса (0–100) |
|
||||
Значения должны:
|
||||
соответствовать нише,
|
||||
иметь сезонную динамику,
|
||||
быть реалистичными.
|
||||
|
||||
2️⃣ Сравнительный анализ конкурентов (столбчатая диаграмма)
|
||||
Выведи таблицу:
|
||||
| Конкурент | Охват аудитории | Активность в соцсетях (0–100) | Количество отзывов | Сильные стороны | Слабые стороны |
|
||||
Охват — в диапазоне 1 000–50 000,
Активность — 0–100.
|
||||
|
||||
3️⃣ Сегментация целевой аудитории (круговая диаграмма)
|
||||
Определи:
|
||||
возрастные группы (%)
|
||||
пол (%)
|
||||
3–5 ключевых сегментов ЦА (%)
|
||||
Сумма процентов = 100%.
|
||||
|
||||
4️⃣ Потенциал каналов продвижения (бар-чарт)
|
||||
Каналы: Instagram, TikTok, Telegram, 2GIS, Google Ads
Выведи таблицу:
|
||||
| Канал | Потенциал (0–100) | Обоснование |
|
||||
|
||||
5️⃣ Конверсионная воронка (воронка спроса)
|
||||
Выведи таблицу:
|
||||
| Этап | Значение | Конверсия (%) |
|
||||
Этапы:
|
||||
Охват
|
||||
Вовлечённость
|
||||
Клики
|
||||
Переходы
|
||||
Заявки
|
||||
Покупки
|
||||
Числа должны уменьшаться логично.
|
||||
|
||||
📘 Теперь сформируй полный маркетинговый анализ:
|
||||
|
||||
I. Анализ продукта
|
||||
ключевые свойства
|
||||
преимущества
|
||||
возможные слабости
|
||||
ценность для рынка Казахстана
|
||||
|
||||
II. Анализ рынка Казахстана
|
||||
динамика
|
||||
тренды
|
||||
сезонность (используй данные из таблицы)
|
||||
возможности и ограничения
|
||||
|
||||
III. Анализ целевой аудитории
|
||||
портрет покупателя
|
||||
боли, потребности, мотивации
|
||||
триггеры принятия решения
|
||||
сегментация (вставить таблицу)
|
||||
|
||||
IV. Анализ конкурентов
|
||||
Вставить таблицу конкурентов.
Сформировать вывод о конкурентной среде.
Указать рыночные ниши, которые свободны.
|
||||
|
||||
V. SWOT-анализ
|
||||
Сформируй матрицу 2×2:
|
||||
Strengths
|
||||
Weaknesses
|
||||
Opportunities
|
||||
Threats
|
||||
|
||||
VI. Уникальное торговое предложение (УТП)
|
||||
Создай 1–2 варианта УТП.
Требования:
|
||||
ясно,
|
||||
коротко,
|
||||
конкретно,
|
||||
опираясь на анализ продукта и конкурентов.
|
||||
|
||||
VII. Рекомендации по позиционированию
|
||||
Определи:
|
||||
образ бренда
|
||||
ключевое сообщение
|
||||
стиль коммуникации
|
||||
что важно подчеркнуть
|
||||
|
||||
VIII. Рекомендации по каналам продвижения
|
||||
Вставить таблицу потенциала каналов.
Дать объяснение, почему каждый канал подходит для этой ниши.
|
||||
|
||||
IX. Рекомендации по контенту
|
||||
Приведи набор контент-направлений:
|
||||
продающий
|
||||
экспертный
|
||||
визуальный
|
||||
доверительный
|
||||
развлекательный
|
||||
Дай примеры публикаций.
|
||||
|
||||
X. Итоговая стратегия продвижения
|
||||
Сформулируй чётко:
|
||||
цель
|
||||
ключевые шаги
|
||||
что делать в первую очередь
|
||||
что даст максимальный эффект
|
||||
ожидаемый результат
|
||||
@@ -0,0 +1,45 @@
|
||||
# Deployment
|
||||
|
||||
This repository now uses the same GitLab shell-runner deployment pattern as `call-center`: no project CI/CD variables are required for the normal push deploy path.
|
||||
|
||||
## What happens on push
|
||||
|
||||
1. GitLab runs the existing `test` job.
|
||||
2. A dedicated shell runner with tag `marketing-parser-prod` picks up `deploy_production`.
|
||||
3. The runner syncs the repository into `/home/gitlab-runner/deploy/marketing-parser`.
|
||||
4. The runner builds a local Docker image from that synced checkout.
|
||||
5. Docker Compose recreates `konturai-parser-app` from [deployment/docker-compose.server.yml](/root/marketing-parser/deployment/docker-compose.server.yml:1).
|
||||
|
||||
## Files used
|
||||
|
||||
- [.gitlab-ci.yml](/root/marketing-parser/.gitlab-ci.yml:1)
|
||||
- [deployment/docker-compose.server.yml](/root/marketing-parser/deployment/docker-compose.server.yml:1)
|
||||
- [scripts/deploy_gitlab.sh](/root/marketing-parser/scripts/deploy_gitlab.sh:1)
|
||||
- [scripts/bootstrap_gitlab_deploy.sh](/root/marketing-parser/scripts/bootstrap_gitlab_deploy.sh:1)
|
||||
- [scripts/install_gitlab_runner.sh](/root/marketing-parser/scripts/install_gitlab_runner.sh:1)
|
||||
|
||||
## One-time server setup
|
||||
|
||||
Install and register a shell runner on the target server:
|
||||
|
||||
```bash
|
||||
cd /root/marketing-parser
|
||||
RUNNER_TOKEN=glrt-xxxxxxxx bash scripts/install_gitlab_runner.sh
|
||||
```
|
||||
|
||||
Then bootstrap the production env file from the current `konturai-parser-app` container:
|
||||
|
||||
```bash
|
||||
cd /root/marketing-parser
|
||||
bash scripts/bootstrap_gitlab_deploy.sh
|
||||
```
|
||||
|
||||
That writes `/home/gitlab-runner/deploy/marketing-parser/.env.production`, which is used by the deploy job.
|
||||
|
||||
## Important notes
|
||||
|
||||
- The deploy job runs for pushes and manual web-triggered pipelines on the branch that contains this CI configuration.
|
||||
- The runner user must have Docker access.
|
||||
- This setup expects a host `shell` runner. A minimal `gitlab/gitlab-runner:alpine` container is not enough unless you also provide Docker, Compose, and `rsync` inside that runner environment.
|
||||
- This deploy path is intentionally independent from `/root/docker-compose.yaml` because `/root` is not accessible to the `gitlab-runner` user.
|
||||
- The deployment compose file joins the existing external Docker network `common_network` and publishes the service alias `parser-service`.
|
||||
@@ -0,0 +1,142 @@
|
||||
# Решение проблем с подключением к MongoDB
|
||||
|
||||
## Проблема: Ошибка аутентификации
|
||||
|
||||
```
|
||||
Command failed with error 13 (Unauthorized): 'Command find requires authentication'
|
||||
```
|
||||
|
||||
## Причина
|
||||
|
||||
MongoDB сервер на `92.38.48.166:27017` требует аутентификацию, но в конфигурации не указаны учетные данные.
|
||||
|
||||
## Решение
|
||||
|
||||
### 1. Обновлена конфигурация MongoDB
|
||||
|
||||
В файле `src/main/resources/application.properties` добавлены настройки аутентификации:
|
||||
|
||||
```properties
|
||||
# MongoDB Configuration
|
||||
spring.data.mongodb.host=92.38.48.166
|
||||
spring.data.mongodb.port=27017
|
||||
spring.data.mongodb.database=parser_db
|
||||
spring.data.mongodb.username=parser_user
|
||||
spring.data.mongodb.password=parser_password
|
||||
spring.data.mongodb.authentication-database=admin
|
||||
```
|
||||
|
||||
### 2. Альтернативный способ подключения
|
||||
|
||||
Если отдельные параметры не работают, используйте URI подключения:
|
||||
|
||||
```properties
|
||||
# Закомментируйте отдельные параметры и раскомментируйте URI
|
||||
# spring.data.mongodb.host=92.38.48.166
|
||||
# spring.data.mongodb.port=27017
|
||||
# spring.data.mongodb.database=parser_db
|
||||
# spring.data.mongodb.username=parser_user
|
||||
# spring.data.mongodb.password=parser_password
|
||||
# spring.data.mongodb.authentication-database=admin
|
||||
|
||||
# Используйте URI подключения
|
||||
spring.data.mongodb.uri=mongodb://parser_user:parser_password@92.38.48.166:27017/parser_db?authSource=admin
|
||||
```
|
||||
|
||||
### 3. Проверка подключения
|
||||
|
||||
#### Через API:
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/api/parser/health/mongodb
|
||||
```
|
||||
|
||||
#### Через MongoDB клиент:
|
||||
|
||||
```bash
|
||||
mongo mongodb://parser_user:parser_password@92.38.48.166:27017/parser_db?authSource=admin
|
||||
```
|
||||
|
||||
### 4. Возможные проблемы и решения
|
||||
|
||||
#### Проблема: Неверные учетные данные
|
||||
|
||||
**Решение:** Проверьте правильность username/password с администратором MongoDB
|
||||
|
||||
#### Проблема: Пользователь не существует
|
||||
|
||||
**Решение:** Создайте пользователя в MongoDB:
|
||||
|
||||
```javascript
|
||||
use admin
|
||||
db.createUser({
|
||||
user: "parser_user",
|
||||
pwd: "parser_password",
|
||||
roles: [
|
||||
{ role: "readWrite", db: "parser_db" }
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
#### Проблема: Пользователь не имеет прав
|
||||
|
||||
**Решение:** Предоставьте права на базу данных:
|
||||
|
||||
```javascript
|
||||
use parser_db
|
||||
db.grantRolesToUser("parser_user", ["readWrite"])
|
||||
```
|
||||
|
||||
#### Проблема: Firewall блокирует подключение
|
||||
|
||||
**Решение:** Убедитесь, что порт 27017 открыт на сервере MongoDB
|
||||
|
||||
### 5. Улучшения в коде
|
||||
|
||||
#### Улучшенная обработка ошибок
|
||||
|
||||
- Добавлена специфическая обработка ошибок аутентификации
|
||||
- Более информативные сообщения об ошибках
|
||||
- Graceful handling ошибок подключения
|
||||
|
||||
#### Новый endpoint для диагностики
|
||||
|
||||
- `GET /api/parser/health/mongodb` - проверка подключения к MongoDB
|
||||
- Возвращает статус подключения и количество записей
|
||||
- Предоставляет рекомендации при ошибках
|
||||
|
||||
### 6. Тестирование
|
||||
|
||||
1. **Запустите приложение:**
|
||||
|
||||
```bash
|
||||
./mvnw spring-boot:run
|
||||
```
|
||||
|
||||
2. **Проверьте подключение к MongoDB:**
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/api/parser/health/mongodb
|
||||
```
|
||||
|
||||
3. **Если подключение успешно, запустите парсинг:**
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/parser/parse/all
|
||||
```
|
||||
|
||||
### 7. Логирование
|
||||
|
||||
Уровень логирования MongoDB настроен для диагностики:
|
||||
|
||||
```properties
|
||||
logging.level.org.springframework.data.mongodb=INFO
|
||||
logging.level.com.mongodb=WARN
|
||||
```
|
||||
|
||||
### 8. Контакты
|
||||
|
||||
Если проблема не решается, обратитесь к администратору MongoDB сервера для:
|
||||
|
||||
- Проверки учетных данных
|
||||
- Настройки прав доступа
|
||||
- Проверки конфигурации сервера
|
||||
@@ -0,0 +1,141 @@
|
||||
# Интеграция OpenAI API
|
||||
|
||||
## Обзор
|
||||
|
||||
Создан новый сервис `OpenAIAnalyticsService` для замены `OllamaAnalyticsService`. Новый сервис использует OpenAI API для анализа текста и генерации контента.
|
||||
|
||||
## Основные изменения
|
||||
|
||||
### 1. Новый сервис OpenAIAnalyticsService
|
||||
|
||||
- **Файл**: `src/main/java/kz/konturai/parser/service/OpenAIAnalyticsService.java`
|
||||
- **Функциональность**: Аналогична `OllamaAnalyticsService`, но использует OpenAI API
|
||||
- **Методы**:
|
||||
- `analyzeText(String text)` - анализ текста с извлечением саммари, тегов, тональности и сущностей
|
||||
- `analyzeText(String text, String language)` - анализ с указанием языка
|
||||
- `generateWithInstruction(String text, String instruction)` - генерация текста по инструкции
|
||||
- `generateWithInstruction(String text, String instruction, String language)` - генерация с указанием языка
|
||||
|
||||
### 2. Обновленные сервисы
|
||||
|
||||
Все сервисы, которые использовали `OllamaAnalyticsService`, теперь используют `OpenAIAnalyticsService`:
|
||||
|
||||
- `ReportSynthesisService`
|
||||
- `ReportGenerationService`
|
||||
- `KursivParserService`
|
||||
- `KapitalParserService`
|
||||
- `LsmParserService`
|
||||
- `RbcParserService`
|
||||
- `VedomostiParserService`
|
||||
|
||||
### 3. Конфигурация
|
||||
|
||||
В `application.properties` уже настроены параметры для OpenAI:
|
||||
|
||||
```properties
|
||||
# OpenAI Configuration
|
||||
openai.api.key=${OPENAI_API_KEY}
|
||||
openai.api.url=https://api.openai.com/v1/chat/completions
|
||||
openai.model.name=gpt-4o-mini
|
||||
openai.timeoutMs=90000
|
||||
```
|
||||
|
||||
### 4. Тестовый контроллер
|
||||
|
||||
Создан `OpenAITestController` для тестирования функциональности:
|
||||
|
||||
- `POST /api/openai/analyze` - анализ текста
|
||||
- `POST /api/openai/generate` - генерация по инструкции
|
||||
- `GET /api/openai/test` - тест подключения
|
||||
|
||||
## Использование
|
||||
|
||||
### Анализ текста
|
||||
|
||||
```java
|
||||
@Autowired
|
||||
private OpenAIAnalyticsService openAIAnalyticsService;
|
||||
|
||||
// Анализ текста на русском языке
|
||||
MarketItem.Analytics analytics = openAIAnalyticsService.analyzeText(text, "ru");
|
||||
|
||||
// Получение саммари
|
||||
String summary = analytics.getSummary();
|
||||
|
||||
// Получение тегов
|
||||
String[] tags = analytics.getTags();
|
||||
|
||||
// Получение тональности
|
||||
String sentiment = analytics.getSentiment();
|
||||
|
||||
// Получение сущностей
|
||||
Map<String, Object> entities = analytics.getEntities();
|
||||
```
|
||||
|
||||
### Генерация текста
|
||||
|
||||
```java
|
||||
// Генерация отчета
|
||||
String instruction = "Напиши краткий отчет на основе следующих данных:";
|
||||
String generatedText = openAIAnalyticsService.generateWithInstruction(data, instruction, "ru");
|
||||
```
|
||||
|
||||
## Преимущества OpenAI API
|
||||
|
||||
1. **Высокое качество**: GPT-4o-mini обеспечивает более качественный анализ и генерацию текста
|
||||
2. **Надежность**: Стабильная работа API без необходимости локального сервера
|
||||
3. **Масштабируемость**: Легко масштабируется под нагрузку
|
||||
4. **Многоязычность**: Отличная поддержка русского и английского языков
|
||||
5. **Консистентность**: Более предсказуемые результаты
|
||||
|
||||
## Миграция
|
||||
|
||||
Все существующие вызовы `OllamaAnalyticsService` автоматически заменены на `OpenAIAnalyticsService`. API остается совместимым, поэтому дополнительных изменений в коде не требуется.
|
||||
|
||||
## Тестирование
|
||||
|
||||
Для тестирования нового сервиса:
|
||||
|
||||
1. Запустите приложение
|
||||
2. Откройте `GET /api/openai/test` для проверки подключения
|
||||
3. Используйте `POST /api/openai/analyze` для анализа текста
|
||||
4. Используйте `POST /api/openai/generate` для генерации контента
|
||||
|
||||
## Настройка
|
||||
|
||||
### Настройка API ключа
|
||||
|
||||
Убедитесь, что в `application.properties` указан корректный API ключ OpenAI:
|
||||
|
||||
```properties
|
||||
openai.api.key=${OPENAI_API_KEY:your-openai-api-key-here}
|
||||
```
|
||||
|
||||
**Рекомендуется использовать переменную окружения:**
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=sk-your-actual-api-key-here
|
||||
```
|
||||
|
||||
### Тестирование API ключа
|
||||
|
||||
Используйте скрипт для проверки API ключа:
|
||||
|
||||
```bash
|
||||
./test-openai-api.sh
|
||||
```
|
||||
|
||||
### Решение проблем
|
||||
|
||||
**Ошибка 401 Unauthorized:**
|
||||
|
||||
- Проверьте правильность API ключа
|
||||
- Убедитесь, что ключ не истек
|
||||
- Проверьте, что ключ начинается с `sk-`
|
||||
|
||||
**Ошибка 429 Rate Limit:**
|
||||
|
||||
- Превышен лимит запросов
|
||||
- Подождите некоторое время перед повторной попыткой
|
||||
|
||||
Модель по умолчанию: `gpt-4o-mini` (можно изменить в настройках).
|
||||
@@ -0,0 +1,89 @@
|
||||
# Настройка OpenAI API
|
||||
|
||||
## Проблема
|
||||
|
||||
Получена ошибка `401 Unauthorized` при обращении к OpenAI API. Это означает, что API ключ недействителен или истек.
|
||||
|
||||
## Решение
|
||||
|
||||
### 1. Получите новый API ключ OpenAI
|
||||
|
||||
1. Перейдите на [OpenAI Platform](https://platform.openai.com/)
|
||||
2. Войдите в свой аккаунт или создайте новый
|
||||
3. Перейдите в раздел "API Keys"
|
||||
4. Создайте новый API ключ
|
||||
5. Скопируйте ключ (он начинается с `sk-`)
|
||||
|
||||
### 2. Настройте API ключ
|
||||
|
||||
#### Вариант A: Переменная окружения (рекомендуется)
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=sk-your-actual-api-key-here
|
||||
```
|
||||
|
||||
#### Вариант B: Прямо в application.properties
|
||||
|
||||
Откройте файл `src/main/resources/application.properties` и замените:
|
||||
|
||||
```properties
|
||||
openai.api.key=your-openai-api-key-here
|
||||
```
|
||||
|
||||
на:
|
||||
|
||||
```properties
|
||||
openai.api.key=sk-your-actual-api-key-here
|
||||
```
|
||||
|
||||
### 3. Проверьте настройки
|
||||
|
||||
Убедитесь, что в `application.properties` правильно настроены:
|
||||
|
||||
```properties
|
||||
# OpenAI Configuration
|
||||
openai.api.key=${OPENAI_API_KEY:your-openai-api-key-here}
|
||||
openai.api.url=https://api.openai.com/v1/chat/completions
|
||||
openai.model.name=gpt-4o-mini
|
||||
openai.timeoutMs=90000
|
||||
```
|
||||
|
||||
### 4. Тестирование
|
||||
|
||||
После настройки API ключа:
|
||||
|
||||
1. Перезапустите приложение
|
||||
2. Откройте `GET /api/openai/test` для проверки подключения
|
||||
3. Проверьте логи на наличие ошибок
|
||||
|
||||
### 5. Возможные проблемы
|
||||
|
||||
- **401 Unauthorized**: Неверный или истекший API ключ
|
||||
- **429 Rate Limit**: Превышен лимит запросов
|
||||
- **500 Server Error**: Временная проблема с сервером OpenAI
|
||||
|
||||
### 6. Безопасность
|
||||
|
||||
⚠️ **Важно**: Никогда не коммитьте реальные API ключи в репозиторий!
|
||||
|
||||
- Используйте переменные окружения
|
||||
- Добавьте `application.properties` в `.gitignore` если содержит секреты
|
||||
- Используйте отдельные файлы конфигурации для продакшена
|
||||
|
||||
## Пример использования
|
||||
|
||||
После настройки API ключа сервис будет работать автоматически:
|
||||
|
||||
```java
|
||||
@Autowired
|
||||
private OpenAIAnalyticsService openAIAnalyticsService;
|
||||
|
||||
// Анализ текста
|
||||
MarketItem.Analytics analytics = openAIAnalyticsService.analyzeText("Ваш текст здесь");
|
||||
|
||||
// Генерация контента
|
||||
String result = openAIAnalyticsService.generateWithInstruction(
|
||||
"Данные для анализа",
|
||||
"Напиши краткий отчет"
|
||||
);
|
||||
```
|
||||
@@ -0,0 +1,56 @@
|
||||
# 🚨 Быстрое решение ошибки 401 Unauthorized
|
||||
|
||||
## Проблема
|
||||
|
||||
```
|
||||
OpenAI request failed: 401 Unauthorized from POST https://api.openai.com/v1/chat/completions
|
||||
```
|
||||
|
||||
## ⚡ Быстрое решение
|
||||
|
||||
### 1. Получите API ключ OpenAI
|
||||
|
||||
- Перейдите на https://platform.openai.com/
|
||||
- Войдите в аккаунт
|
||||
- Создайте новый API ключ в разделе "API Keys"
|
||||
|
||||
### 2. Установите переменную окружения
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=sk-your-actual-api-key-here
|
||||
```
|
||||
|
||||
### 3. Перезапустите приложение
|
||||
|
||||
```bash
|
||||
mvn spring-boot:run
|
||||
```
|
||||
|
||||
### 4. Проверьте работу
|
||||
|
||||
Откройте: `GET /api/openai/test`
|
||||
|
||||
## 🔧 Альтернативное решение
|
||||
|
||||
Если не хотите использовать переменные окружения, отредактируйте файл `src/main/resources/application.properties`:
|
||||
|
||||
```properties
|
||||
openai.api.key=sk-your-actual-api-key-here
|
||||
```
|
||||
|
||||
## ✅ Проверка
|
||||
|
||||
После настройки в логах должно появиться:
|
||||
|
||||
```
|
||||
OpenAIAnalyticsService initialized with model: gpt-4o-mini
|
||||
```
|
||||
|
||||
И тест должен вернуть результат анализа текста вместо ошибки 401.
|
||||
|
||||
## 📞 Если проблема остается
|
||||
|
||||
1. Убедитесь, что API ключ начинается с `sk-`
|
||||
2. Проверьте, что ключ не истек
|
||||
3. Убедитесь, что у вас есть доступ к OpenAI API
|
||||
4. Проверьте баланс аккаунта OpenAI
|
||||
@@ -0,0 +1,85 @@
|
||||
# Руководство по запуску приложения
|
||||
|
||||
## Приложение теперь запускается без API ключа!
|
||||
|
||||
Приложение было исправлено и теперь может запускаться даже без настройки OpenAI API ключа.
|
||||
|
||||
## Быстрый запуск
|
||||
|
||||
### 1. Запуск без API ключа
|
||||
|
||||
```bash
|
||||
mvn spring-boot:run
|
||||
```
|
||||
|
||||
Приложение запустится успешно, но OpenAI функции будут возвращать сообщения об ошибке конфигурации.
|
||||
|
||||
### 2. Запуск с API ключом
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=sk-your-actual-api-key-here
|
||||
mvn spring-boot:run
|
||||
```
|
||||
|
||||
## Проверка статуса
|
||||
|
||||
После запуска откройте: `GET /api/openai/test`
|
||||
|
||||
**Без API ключа:**
|
||||
|
||||
```
|
||||
OpenAI Analytics Service Test:
|
||||
Summary: OpenAI API key not configured
|
||||
Sentiment: neutral
|
||||
Tags: api-key-not-configured
|
||||
Entities: {}
|
||||
|
||||
WARNING: OpenAI API key is not configured!
|
||||
Please set OPENAI_API_KEY environment variable or update application.properties
|
||||
```
|
||||
|
||||
**С API ключом:**
|
||||
|
||||
```
|
||||
OpenAI Analytics Service Test:
|
||||
Summary: [реальный анализ текста]
|
||||
Sentiment: [анализ тональности]
|
||||
Tags: [извлеченные теги]
|
||||
Entities: {[сущности]}
|
||||
|
||||
OpenAI API is working correctly!
|
||||
```
|
||||
|
||||
## Доступные эндпоинты
|
||||
|
||||
- `GET /api/openai/test` - тест подключения
|
||||
- `POST /api/openai/analyze?text=...&language=ru` - анализ текста
|
||||
- `POST /api/openai/generate?text=...&instruction=...&language=ru` - генерация текста
|
||||
|
||||
## Настройка API ключа
|
||||
|
||||
### Вариант 1: Переменная окружения (рекомендуется)
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=sk-your-actual-api-key-here
|
||||
```
|
||||
|
||||
### Вариант 2: Прямо в application.properties
|
||||
|
||||
```properties
|
||||
openai.api.key=sk-your-actual-api-key-here
|
||||
```
|
||||
|
||||
## Важные изменения
|
||||
|
||||
1. **Приложение запускается** даже без API ключа
|
||||
2. **OpenAI функции** возвращают понятные сообщения об ошибках
|
||||
3. **Логи показывают** статус конфигурации API ключа
|
||||
4. **Тестовый эндпоинт** показывает текущий статус
|
||||
|
||||
## Следующие шаги
|
||||
|
||||
1. Запустите приложение
|
||||
2. Проверьте статус через `/api/openai/test`
|
||||
3. При необходимости настройте API ключ
|
||||
4. Перезапустите приложение с API ключом
|
||||
@@ -0,0 +1,67 @@
|
||||
### Техническое задание для AI-агента
|
||||
|
||||
**Задача:** Разработать парсер (парсер) для RSS-ленты `https://kursiv.media/feed/` на Spring Boot с сохранением результатов в MongoDB.
|
||||
|
||||
[cite\_start]**Контекст:** Этот парсер является частью большой AI-платформы для маркетинговой аналитики[cite: 5]. Его цель — собирать новости из указанного источника, нормализовать их и сохранять в базу данных для дальнейшей обработки.
|
||||
|
||||
---
|
||||
|
||||
## Основные требования
|
||||
|
||||
1. **Получение данных**: Создать сервис, который выполняет HTTP GET-запрос к URL: `https://kursiv.media/feed/`.
|
||||
2. **Парсинг RSS**: Разобрать полученный XML-ответ. Для каждой записи (элемента `<item>`) в RSS-ленте необходимо извлечь следующие поля:
|
||||
- [cite\_start]`title` (заголовок)[cite: 64].
|
||||
- [cite\_start]`link` или `guid` (URL статьи)[cite: 64].
|
||||
- [cite\_start]`pubDate` или `isoDate` (дата публикации)[cite: 64].
|
||||
- [cite\_start]`description` или `content` (текстовое содержимое)[cite: 65].
|
||||
3. **Нормализация данных**:
|
||||
- [cite\_start]**Очистка текста**: Текстовое содержимое из `description`/`content` должно быть полностью очищено от HTML-тегов[cite: 65].
|
||||
- **Формат даты**: Дата публикации должна быть приведена к формату `ISODate`.
|
||||
4. **Создание хэша**: Для каждой новости необходимо сгенерировать уникальный хэш для дедупликации. [cite\_start]Хэш формируется по формуле `sha256(url + "::" + title)`[cite: 54].
|
||||
5. **Сохранение в MongoDB**:
|
||||
- Подготовленные данные должны сохраняться в коллекцию MongoDB с названием `market_items`.
|
||||
- [cite\_start]Операция сохранения должна быть **"upsert"** (обновить, если существует, или вставить, если нет)[cite: 56]. [cite\_start]В качестве ключа для поиска существующей записи используйте поле `hash`[cite: 56].
|
||||
|
||||
---
|
||||
|
||||
## Модель данных для коллекции `market_items` в MongoDB
|
||||
|
||||
Документ в коллекции должен иметь следующую структуру:
|
||||
|
||||
```json
|
||||
{
|
||||
"source_name": "Kursiv (Бизнес/экономика)", // Статическое значение для этого парсера
|
||||
"url": "https://kursiv.media/article/...", // Из поля <link>
|
||||
"title": "Заголовок новости", // Из поля <title>
|
||||
"published_at": ISODate("2025-09-14T..."), // Из поля <pubDate>
|
||||
"added_at": ISODate("..."), // Текущее время при добавлении
|
||||
"raw_text": "Очищенный от HTML текст статьи...", // Из поля <description>
|
||||
"hash": "...", // Сгенерированный SHA-256 хэш
|
||||
"category": "Бизнес", // Можно задать по умолчанию
|
||||
|
||||
// Этот блок будет заполняться на следующем этапе, но его нужно предусмотреть
|
||||
"analytics": {
|
||||
"summary": null,
|
||||
"sentiment": null,
|
||||
"tags": [],
|
||||
"entities": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Технологический стек и реализация
|
||||
|
||||
- **Фреймворк**: Spring Boot
|
||||
- **База данных**: Spring Data MongoDB
|
||||
- **HTTP-клиент**: `RestTemplate` или `WebClient`
|
||||
- **Парсинг XML/RSS**: Рекомендуется использовать библиотеку, например, **Rome** (`com.rometools:rome`), для удобной работы с RSS-лентами.
|
||||
- **Логика**: Реализовать класс-сервис (например, `KursivParserService`), который будет содержать всю логику. Для взаимодействия с MongoDB использовать `MongoRepository` или `MongoTemplate`.
|
||||
|
||||
## Критерии выполнения
|
||||
|
||||
- Создан Spring Boot сервис, который успешно получает и парсит данные с `https://kursiv.media/feed/`.
|
||||
- При вызове метода этого сервиса, новые записи появляются в коллекции `market_items` в MongoDB.
|
||||
- При повторном вызове дубликаты новостей не создаются, а существующие записи могут быть обновлены (логика `upsert`).
|
||||
- Структура сохраненных документов в MongoDB полностью соответствует указанной модели данных.
|
||||
@@ -0,0 +1,84 @@
|
||||
Привет! Я хочу провести глубокий рефакторинг метода generateJsonAnalysis в классе MarketingAnalysisService.java.
|
||||
|
||||
У нас нет ограничений по токенам, но есть жесткое требование к качеству и детализации данных. Текущий метод пытается получить всё за один запрос, из-за чего страдает глубина.
|
||||
|
||||
ЗАДАЧА:
|
||||
Перепиши метод generateJsonAnalysis так, чтобы он выполнял агрегацию данных из нескольких отдельных запросов к OpenAI.
|
||||
Метод должен вызывать приватные helper-методы (создай их), каждый из которых отвечает за свою часть и возвращает Map<String, Object>.
|
||||
|
||||
СТРУКТУРА НОВОГО МЕТОДА:
|
||||
|
||||
1. Вызвать getAudienceAnalysis(request) -> возвращает секцию "audienceAnalysis"
|
||||
2. Вызвать getCompetitorAnalysis(request) -> возвращает секцию "competitorAnalysis"
|
||||
3. Вызвать getSeasonalityAndMarket(request) -> возвращает секцию "seasonality" и "market"
|
||||
4. Объединить результаты в один Map<String, Object> и вернуть его.
|
||||
|
||||
ДЕТАЛИЗАЦИЯ HELPER-МЕТОДОВ (Промпты внутри них):
|
||||
|
||||
--- Метод 1: getAudienceAnalysis ---
|
||||
Промпт должен требовать JSON строго такой структуры (для отрисовки виджетов на фронте):
|
||||
{
|
||||
"audienceAnalysis": {
|
||||
"ageGroups": { "18-24": 15, "25-34": 40, "35-44": 30, "45+": 15 }, // Pie chart data
|
||||
"ageComment": "Текст вывода под графиком возраста (1-2 предложения)",
|
||||
"genderDistribution": { "Женщины": 60, "Мужчины": 40 }, // Donut chart
|
||||
"genderComment": "Текст вывода под графиком пола",
|
||||
"segments": [ // Список из 4 детальных карточек сегментов
|
||||
{
|
||||
"name": "Название (например 'Молодые профессионалы')",
|
||||
"sharePercent": 35,
|
||||
"ageRange": "25-34",
|
||||
"incomeLevel": "Средний+",
|
||||
"motivation": "Ключевая мотивация покупки",
|
||||
"triggers": "Триггеры (на что реагируют)",
|
||||
"primaryChannels": ["Instagram", "TikTok"], // Топ каналы для этого сегмента
|
||||
"secondaryChannels": ["Telegram"]
|
||||
}
|
||||
],
|
||||
"segmentsChannelMatrix": [ // Для таблицы heatmap: Сегмент vs Канал
|
||||
{ "segmentName": "Молодые профи", "instagram": "high", "telegram": "medium", "youtube": "low" }
|
||||
],
|
||||
"keyTakeaways": ["Вывод 1", "Вывод 2", "Вывод 3"]
|
||||
}
|
||||
}
|
||||
|
||||
--- Метод 2: getCompetitorAnalysis ---
|
||||
Промпт должен требовать JSON:
|
||||
{
|
||||
"competitorAnalysis": {
|
||||
"marketShareChart": [ // Оценочные доли рынка для графика
|
||||
{ "name": "Наш бренд", "value": 25 },
|
||||
{ "name": "Конкурент А", "value": 30 },
|
||||
{ "name": "Конкурент B", "value": 20 },
|
||||
{ "name": "Другие", "value": 25 }
|
||||
],
|
||||
"marketShareComment": "Вывод по доле рынка",
|
||||
"competitorsDetails": [ // Детали по 3-4 конкурентам
|
||||
{
|
||||
"name": "Конкурент А",
|
||||
"priceStrategy": "Высокая/Средняя/Низкая",
|
||||
"digitalMetrics": { "reach": 50000, "followers": 12000, "activityIndex": 85 },
|
||||
"channelsHeatmap": { // Оценка (0-100) качества ведения канала
|
||||
"Instagram": 90, "TikTok": 20, "Telegram": 70, "YouTube": 40
|
||||
}
|
||||
}
|
||||
],
|
||||
"comparisonTable": [ // Сравнительная таблица (5-6 характеристик)
|
||||
{ "feature": "Ценовая политика", "us": "Средняя", "compA": "Высокая", "compB": "Низкая" },
|
||||
{ "feature": "УТП", "us": "Сервис", "compA": "Бренд", "compB": "Дешевизна" }
|
||||
],
|
||||
"swotCompetitors": { // Краткий SWOT по рынку в целом
|
||||
"strengths": ["..."], "weaknesses": ["..."]
|
||||
},
|
||||
"keyInsights": ["Вывод 1", "Вывод 2"]
|
||||
}
|
||||
}
|
||||
|
||||
ТЕХНИЧЕСКИЕ ТРЕБОВАНИЯ:
|
||||
|
||||
1. Используй существующий `openAIAnalyticsService.generateWithInstruction` для вызовов.
|
||||
2. В каждом helper-методе добавь отдельный `try-catch` и логирование, чтобы падение одной части не ломало весь отчет (возвращай пустую Map или stub-данные при ошибке).
|
||||
3. Используй `objectMapper` для парсинга ответов.
|
||||
4. Убедись, что JSON очищается от markdown-тегов (```json) перед парсингом.
|
||||
|
||||
Пожалуйста, полностью реализуй этот модульный подход.
|
||||
@@ -0,0 +1,66 @@
|
||||
**Задача:** Разработать парсер для RSS-ленты `https://kapital.kz/rss/` на Spring Boot с сохранением результатов в MongoDB.
|
||||
|
||||
**Контекст:** Парсер является частью AI-платформы для маркетинговой аналитики. Его цель — собирать деловые новости из источника Kapital.kz, нормализовать их и сохранять в общую базу данных для дальнейшей аналитической обработки.
|
||||
|
||||
---
|
||||
|
||||
## Основные требования
|
||||
|
||||
1. **Получение данных**: Создать сервис, который выполняет HTTP GET-запрос к URL: `https://kapital.kz/rss/`.
|
||||
2. **Парсинг RSS**: Разобрать полученный XML-ответ. Для каждой записи (элемента `<item>`) в RSS-ленте необходимо извлечь следующие поля:
|
||||
- `title` (заголовок).
|
||||
- `link` (URL статьи).
|
||||
- `pubDate` (дата публикации).
|
||||
- `description` (текстовое содержимое).
|
||||
3. **Нормализация данных**:
|
||||
- **Очистка текста**: Текстовое содержимое из поля `description` должно быть полностью очищено от HTML-тегов.
|
||||
- **Формат даты**: Дата публикации должна быть приведена к формату `ISODate`.
|
||||
4. **Создание хэша**: Для каждой новости необходимо сгенерировать уникальный хэш для дедупликации по формуле `sha256(url + "::" + title)`.
|
||||
5. **Сохранение в MongoDB**:
|
||||
- Подготовленные данные должны сохраняться в ту же коллекцию `market_items`.
|
||||
- Операция сохранения должна быть **"upsert"** (обновить, если существует, или вставить, если нет), используя поле `hash` в качестве уникального ключа.
|
||||
|
||||
---
|
||||
|
||||
## Модель данных для коллекции `market_items` в MongoDB
|
||||
|
||||
Структура документа должна полностью соответствовать уже существующей модели.
|
||||
|
||||
```json
|
||||
{
|
||||
"source_name": "Kapital.kz (Бизнес)", // Статическое значение для этого парсера
|
||||
"url": "https://kapital.kz/...", // Из поля <link>
|
||||
"title": "Заголовок новости", // Из поля <title>
|
||||
"published_at": ISODate("2025-09-14T..."), // Из поля <pubDate>
|
||||
"added_at": ISODate("..."), // Текущее время при добавлении
|
||||
"raw_text": "Очищенный от HTML текст статьи...", // Из поля <description>
|
||||
"hash": "...", // Сгенерированный SHA-256 хэш
|
||||
"category": "Бизнес", // Можно задать по умолчанию
|
||||
|
||||
// Блок для будущей аналитики
|
||||
"analytics": {
|
||||
"summary": null,
|
||||
"sentiment": null,
|
||||
"tags": [],
|
||||
"entities": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Технологический стек и реализация
|
||||
|
||||
- **Фреймворк**: Spring Boot
|
||||
- **База данных**: Spring Data MongoDB
|
||||
- **Реализация**: Рекомендуется создать новый класс-сервис (например, `KapitalParserService`) по аналогии с существующим `KursivParserService`.
|
||||
- **Планирование**: Новый сервис также должен запускаться по общему расписанию — **каждые 30 минут**. Это можно сделать, добавив аннотацию `@Scheduled(cron = "0 0/30 * * * ?")` на метод парсинга в новом сервисе.
|
||||
|
||||
---
|
||||
|
||||
## Критерии выполнения
|
||||
|
||||
- Создан новый Spring Boot сервис (`KapitalParserService`), который успешно парсит данные с `https://kapital.kz/rss/`.
|
||||
- Приложение автоматически запускает оба парсера (Kursiv и Kapital) каждые 30 минут.
|
||||
- Новости из Kapital.kz корректно сохраняются в общую коллекцию `market_items` без создания дубликатов.
|
||||
- Структура сохраненных документов в MongoDB полностью соответствует указанной модели данных.
|
||||
@@ -0,0 +1,64 @@
|
||||
### Дополнение к заданию для AI-агента
|
||||
|
||||
**Задача:** Добавить автоматический запуск парсера каждые 30 минут.
|
||||
|
||||
[cite\_start]**Контекст:** Согласно техническому заданию, сбор данных должен происходить автоматически и регулярно, чтобы обеспечивать актуальность информации в системе[cite: 47, 93].
|
||||
|
||||
---
|
||||
|
||||
### Реализация в Spring Boot
|
||||
|
||||
Для реализации этой задачи необходимо использовать встроенный в Spring механизм планировщика задач (Task Scheduler).
|
||||
|
||||
1. **Включить планировщик:** В главном классе приложения (с аннотацией `@SpringBootApplication`) необходимо добавить аннотацию `@EnableScheduling`.
|
||||
|
||||
2. **Запланировать выполнение метода:** В сервисе `KursivParserService`, над методом, который запускает процесс парсинга, нужно добавить аннотацию `@Scheduled`. Для запуска каждые 30 минут можно использовать `cron` выражение.
|
||||
|
||||
---
|
||||
|
||||
### Пример кода:
|
||||
|
||||
**1. Главный класс приложения:**
|
||||
|
||||
```java
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableScheduling // <-- ДОБАВИТЬ ЭТУ АННОТАЦИЮ
|
||||
public class MarketingAnalyticsApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(MarketingAnalyticsApplication.class, args);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**2. Сервис-парсер:**
|
||||
|
||||
```java
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class KursivParserService {
|
||||
|
||||
// ... здесь существующий код и зависимости (MongoTemplate и т.д.)
|
||||
|
||||
@Scheduled(cron = "0 0/30 * * * ?") // <-- ДОБАВИТЬ ЭТУ АННОТАЦИЮ
|
||||
public void parseAndStoreNews() {
|
||||
System.out.println("Запуск планового парсинга новостей с Kursiv.media...");
|
||||
// ... здесь вся логика парсинга, которая уже написана
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Объяснение `cron = "0 0/30 * * * ?"`:** Эта настройка означает, что метод `parseAndStoreNews()` будет запускаться **в 0 и 30 минут каждого часа, каждый день**. Это в точности соответствует требованию ТЗ.
|
||||
|
||||
---
|
||||
|
||||
### Критерии выполнения:
|
||||
|
||||
- После запуска приложения, парсер автоматически начинает свою работу в ближайшие 0 или 30 минут часа.
|
||||
- В логах приложения видно, что метод парсинга вызывается каждые полчаса без какого-либо ручного вмешательства.
|
||||
@@ -0,0 +1,232 @@
|
||||
ТЕХНИЧЕСКОЕ ЗАДАНИЕ v0.2
|
||||
Подсистема: модуль «Маркетинг» (Анализ + Продвижение + Результаты) платформы NAK.AI
|
||||
|
||||
1. Общие положения
|
||||
Цель подсистемы:Реализовать единый модуль «Маркетинг», который для предпринимателя МСБ выполняет полный цикл:
|
||||
Собирает минимальные данные о бизнесе.
|
||||
Проводит автоматический маркетинговый анализ (рынок, конкуренты, ЦА, каналы, SWOT).
|
||||
Формирует понятный аналитический отчёт с рекомендациями простым языком.
|
||||
Позволяет на основе анализа создать и запустить продвижение (кампанию).
|
||||
Отображает результаты продвижения в виде метрик и простых выводов.
|
||||
Роль пользователя: владелец или представитель МСБ (без штатного маркетолога).Интерфейс: полностью на русском языке, без перегруза терминами.
|
||||
|
||||
2. Целевая аудитория и сценарий использования
|
||||
Малый и средний бизнес в Казахстане.
|
||||
Пользователь не обязан разбираться в маркетинге.
|
||||
Ожидание: “я отвечу на несколько вопросов → система сама всё посчитает → покажет выводы → предложит готовое продвижение”.
|
||||
Важно:Нельзя заставлять пользователя:
|
||||
выбирать типы рекламы (таргет/контекст и т.д.),
|
||||
выбирать форматы продвижения “по-умному”,
|
||||
считать бюджеты и вручную настраивать каналы.
|
||||
Система сама принимает сложные решения и даёт гибкий, но простой интерфейс.
|
||||
|
||||
3. Общая структура модуля «Маркетинг»
|
||||
Модуль «Маркетинг» включает 3 логических блока:
|
||||
Блок А. Маркетинговый анализ
|
||||
Блок B. Автоматическое продвижение
|
||||
Блок C. Результаты кампаний
|
||||
В UI это один раздел “Маркетинг” с навигацией по шагам.
|
||||
|
||||
4. Блок А. Маркетинговый анализ
|
||||
4.1. Назначение блока
|
||||
Собрать минимальный набор входных данных и выполнить автоматический анализ, чтобы:
|
||||
объяснить предпринимателю, на каком рынке он работает,
|
||||
кто его конкуренты,
|
||||
кто его клиент,
|
||||
какие каналы и форматы для него перспективны,
|
||||
в чём его сильные и слабые стороны.
|
||||
4.2. Пользовательский поток (step-by-step)
|
||||
Шаг А1. Экран «Начать анализ»
|
||||
Поля ввода (всё максимально простым языком):
|
||||
Чем занимается ваш бизнес?Тип: строка, max 255Примеры подсказок:
|
||||
«Студия маникюра»
|
||||
«Магазин детской одежды»
|
||||
«СТО по ремонту авто»
|
||||
Где вы работаете?Тип: строкаПримеры:
|
||||
«Алматы»
|
||||
«Астана»
|
||||
«Онлайн по всему Казахстану»
|
||||
Что вы продаёте?Тип: строкаПримеры:
|
||||
«Наращивание ресниц»
|
||||
«Пальто зимние»
|
||||
«Установка дверей»
|
||||
Кто ваш клиент? (опционально)Тип: строка + быстрые кнопки выбора:
|
||||
Женщины 20–40
|
||||
Мужчины 25–45
|
||||
Семьи
|
||||
Молодёжь
|
||||
Все подряд
|
||||
Кнопка: «Начать анализ»
|
||||
После нажатия создаётся сущность MarketingAnalysis со статусом PENDING.
|
||||
|
||||
Шаг А2. Обработка анализа
|
||||
Бэкенд:
|
||||
Меняет статус PENDING → IN_PROGRESS.
|
||||
Запускает внутренние алгоритмы анализа (в рамках MVP можно сделать заглушки/правила).
|
||||
Подзадачи анализа:
|
||||
Анализ рынка (MarketInsights)
|
||||
Анализ конкурентов (CompetitorInsights)
|
||||
Анализ ЦА (AudienceProfile)
|
||||
Анализ каналов (ChannelInsights)
|
||||
SWOT (SwotAnalysis)
|
||||
Формирование рекомендаций (Recommendation)
|
||||
|
||||
Шаг А3. Экран «Анализ выполняется»
|
||||
Пользователь видит:
|
||||
Прогресс-бар (этапы: рынок → конкуренты → ЦА → SWOT → отчёт)
|
||||
Сообщение:
|
||||
«Система анализирует ваш рынок, конкурентов и целевую аудиторию. Это может занять немного времени.»
|
||||
Опрашивается GET /api/marketing/analyses/{id}/status до состояния COMPLETED или FAILED.
|
||||
|
||||
Шаг А4. Экран «Готовый отчёт»
|
||||
После завершения анализа:
|
||||
UI запрашивает GET /api/marketing/analyses/{id} и отображает блоки:
|
||||
Обзор бизнеса (кратко, на основе BusinessProfile + входных)
|
||||
Рынок (MarketInsights)
|
||||
Конкуренты (CompetitorInsights)
|
||||
Целевая аудитория (AudienceProfile)
|
||||
Каналы (ChannelInsights)
|
||||
SWOT (SwotAnalysis)
|
||||
Рекомендации (список Recommendation)
|
||||
Кнопки:
|
||||
«Сохранить отчёт» (на будущее – выгрузка PDF/Word)
|
||||
«Перейти к продвижению» → открывает блок B.
|
||||
|
||||
5. Блок B. Автоматическое продвижение
|
||||
5.1. Назначение блока
|
||||
На основе данных анализа автоматически:
|
||||
сформировать цель кампании,
|
||||
предложить стратегию,
|
||||
создать структуру кампании и контент-пакет,— чтобы предпринимателю не пришлось самому разбираться в каналах и форматах.
|
||||
5.2. Пользовательский поток (step-by-step)
|
||||
Шаг B1. Экран «Выбор цели продвижения»
|
||||
Появляется после клика «Перейти к продвижению» или при заходе в маркетинг → вкладка «Продвижение».
|
||||
Пользователь видит вопрос:
|
||||
«Какой результат вы хотите получить?»
|
||||
Карточки-цели (CampaignGoal):
|
||||
Увеличить продажи (increase_sales)
|
||||
Получить больше заявок/звонков (get_leads)
|
||||
Повысить узнаваемость бренда (awareness)
|
||||
Продвигать акцию или спецпредложение (promo)
|
||||
Кнопка: «Продолжить»
|
||||
После выбора вызывается POST /api/promotion/campaigns с:
|
||||
{
|
||||
"business_id": "<id>",
|
||||
"analysis_id": "<last_or_selected>",
|
||||
"goal_id": "increase_sales"
|
||||
}
|
||||
Создаётся Campaign со статусом DRAFT.
|
||||
|
||||
Шаг B2. Автоматическое формирование стратегии
|
||||
Бэкенд:
|
||||
Читает MarketingAnalysis по analysis_id.
|
||||
Использует данные:
|
||||
AudienceProfile
|
||||
ChannelInsights
|
||||
MarketInsights
|
||||
Recommendation
|
||||
Формирует сущности:
|
||||
Strategy: общее описание, длительность (например, 7–14 дней).
|
||||
CampaignChannel: какие каналы будут использоваться.
|
||||
ContentItem: список контентных единиц (черновики).
|
||||
Пример:
|
||||
Каналы: Instagram, Telegram, 2GIS
|
||||
План:
|
||||
3 поста в Instagram
|
||||
7 сторис
|
||||
1 подборка на маркетплейсе/каталоге
|
||||
1 простая рассылка по клиентам (если есть база – позже)
|
||||
|
||||
Шаг B3. Экран «Стратегия продвижения»
|
||||
UI запрашивает GET /api/promotion/campaigns/{campaign_id} и отображает:
|
||||
Цель кампании (человекочитаемый текст)
|
||||
Каналы, которые выбрала система
|
||||
Краткое текстовое описание стратегии:
|
||||
«На основе вашего анализа система рекомендует 2 недели активного продвижения в Instagram и Telegram с упором на отзывы и примеры работ.»
|
||||
Пользователь может:
|
||||
«Принять стратегию» → переход к контенту
|
||||
«Редактировать детали» (в MVP можно просто подсветить, что это появится позже или минимально корректировать текст/названия)
|
||||
|
||||
Шаг B4. Экран «Материалы кампании» (контент-пакет)
|
||||
Отображаются ContentItem:
|
||||
Список:
|
||||
Тип: пост / сторис / баннер / email
|
||||
Канал
|
||||
Черновой текст (который может быть сгенерирован системой)
|
||||
Подсказка по визуалу (например: «фото до/после», «фото товара на человеке»)
|
||||
Пользователь может:
|
||||
Просмотреть
|
||||
При необходимости подправить текст (на фронте)
|
||||
Нажать «Готово, перейти к запуску»
|
||||
|
||||
Шаг B5. Экран «Запуск кампании»
|
||||
Простой экран подтверждения:
|
||||
«Кампания готова к запуску.Вы можете использовать эти материалы для публикации на своих площадках.В следующих версиях система сможет автоматически публиковать материалы через интеграции.»
|
||||
Кнопка:
|
||||
«Зафиксировать запуск кампании» → POST /api/promotion/campaigns/{id}/activate
|
||||
Статус Campaign меняется на ACTIVE.
|
||||
|
||||
6. Блок C. Результаты кампаний
|
||||
6.1. Назначение
|
||||
Показать предпринимателю понятную картину, как сработало продвижение:
|
||||
без сложных метрик,
|
||||
с упором на результат: охват, вовлечённость, заявки, наиболее эффективные каналы.
|
||||
6.2. Пользовательский поток
|
||||
Шаг C1. Список кампаний
|
||||
Экран:
|
||||
Таблица или карточки кампаний:
|
||||
Название кампании
|
||||
Цель
|
||||
Статус: DRAFT, ACTIVE, FINISHED
|
||||
Краткая метрика: например, «Охват: 13 200, Заявок: 31»
|
||||
Данные: GET /api/promotion/campaigns?business_id=...
|
||||
|
||||
Шаг C2. Экран «Результаты продвижения» (детали кампании)
|
||||
Показывает:
|
||||
Основные показатели (CampaignMetrics):
|
||||
Охват (суммарно)
|
||||
Вовлечённость (лайки/клики/сообщения)
|
||||
Заявки/обращения (если будут интеграции/ручной ввод)
|
||||
График:
|
||||
Ось X: дни
|
||||
Ось Y: охват/клики или композитный KPI
|
||||
По каналам:
|
||||
Instagram: охват, вовлечённость
|
||||
Telegram: охват, клики
|
||||
Прочие
|
||||
Итоговый вывод системы (короткий текст):
|
||||
«Лучше всего сработал Instagram, основная активность пришлась на 3–5 день кампании. Рекомендуется повторить подобный формат постов и усилить работу с отзывами.»
|
||||
|
||||
7. Сущности данных (укрупнённо)
|
||||
(Ты их уже видел, но теперь в рамках одного модуля.)
|
||||
7.1. Общие
|
||||
BusinessProfile
|
||||
MarketingAnalysis
|
||||
MarketInsights
|
||||
CompetitorInsights
|
||||
AudienceProfile
|
||||
ChannelInsights
|
||||
SwotAnalysis
|
||||
Recommendation
|
||||
7.2. Продвижение
|
||||
Campaign
|
||||
CampaignGoal
|
||||
Strategy
|
||||
Channel
|
||||
CampaignChannel
|
||||
ContentItem
|
||||
CampaignMetrics
|
||||
Связь главная:Campaign.analysis_id → MarketingAnalysis.id
|
||||
|
||||
8. Важные бизнес-правила и нюансы
|
||||
Пользователь не выбирает тип продвижения (“таргет” / “контекст”) — только цель, всё остальное решает система.
|
||||
Не задаём вопрос про бюджет на этом этапе (подписная модель).
|
||||
Не перегружаем терминами: CTA, CTR, CPC и т.д. — только человекочитаемые объяснения.
|
||||
В модуле «Маркетинг» нет технических слов “AI”, “нейросеть”, вместо этого — “система”, “анализ”.
|
||||
Переход от блока Анализа к Продвижению — мягкий, логичный, без ощущения “другого продукта”.
|
||||
|
||||
9. Нефункциональные требования (кратко)
|
||||
Все тексты для пользователя — на русском.
|
||||
Логирование действий пользователя и ключевых событий.
|
||||
API версионируемый: /api/v1/...
|
||||
Архитектура должна позволять вынести анализ и кампании в отдельные сервисы/очереди при росте нагрузки.
|
||||
@@ -0,0 +1,400 @@
|
||||
# Отличия между ТЗ и текущей реализацией
|
||||
|
||||
## Обзор
|
||||
|
||||
Документ описывает расхождения между техническим заданием (ТЗ v0.2) и текущей реализацией модуля «Маркетинг».
|
||||
|
||||
---
|
||||
|
||||
## Блок А. Маркетинговый анализ
|
||||
|
||||
### 1. Поля ввода формы
|
||||
|
||||
#### ТЗ (Шаг А1):
|
||||
|
||||
- **Чем занимается ваш бизнес?** (строка, max 255)
|
||||
- **Где вы работаете?** (строка) - примеры: "Алматы", "Астана", "Онлайн по всему Казахстану"
|
||||
- **Что вы продаёте?** (строка)
|
||||
- **Кто ваш клиент?** (опционально, строка + быстрые кнопки выбора)
|
||||
|
||||
#### Реализация:
|
||||
|
||||
- ✅ **businessNiche** (ниша бизнеса) - соответствует "Чем занимается ваш бизнес?"
|
||||
- ✅ **product** (продукт/услуга) - соответствует "Что вы продаёте?"
|
||||
- ✅ **targetAudience** (целевая аудитория) - соответствует "Кто ваш клиент?", но **обязательное поле** (в ТЗ опциональное)
|
||||
- ✅ **region** (регион) - соответствует "Где вы работаете?", но **только города Казахстана** (в ТЗ допускается "Онлайн по всему Казахстану")
|
||||
- ❌ **goal** (цель на 6-12 месяцев) - **новое поле, отсутствует в ТЗ**
|
||||
- ❌ **detailLevel** (уровень детализации) - **новое поле, отсутствует в ТЗ**
|
||||
- ❌ **strongSide** (сильная сторона) - **новое поле, отсутствует в ТЗ**
|
||||
- ❌ **weakSide** (слабая сторона) - **новое поле, отсутствует в ТЗ**
|
||||
|
||||
**Вывод**: Реализация расширена дополнительными полями, которые улучшают качество анализа, но не соответствуют минималистичному подходу из ТЗ.
|
||||
|
||||
---
|
||||
|
||||
### 2. Статусы анализа
|
||||
|
||||
#### ТЗ:
|
||||
|
||||
- `PENDING` → `IN_PROGRESS` → `COMPLETED` / `FAILED`
|
||||
|
||||
#### Реализация:
|
||||
|
||||
- `queued` → `processing` → `completed` / `failed`
|
||||
|
||||
**Вывод**: Разные названия статусов, но логика идентична.
|
||||
|
||||
---
|
||||
|
||||
### 3. Эндпоинты API
|
||||
|
||||
#### ТЗ:
|
||||
|
||||
- `POST /api/marketing/analyses` - создание анализа
|
||||
- `GET /api/marketing/analyses/{id}/status` - проверка статуса
|
||||
- `GET /api/marketing/analyses/{id}` - получение результата
|
||||
|
||||
#### Реализация:
|
||||
|
||||
- ✅ `POST /api/marketing/analysis/start` - создание анализа (путь отличается: `analysis` вместо `analyses`)
|
||||
- ❌ `GET /api/marketing/analysis/{id}/status` - **отсутствует отдельный эндпоинт для статуса**
|
||||
- ✅ `GET /api/marketing/analysis/{id}` - получение результата (путь отличается)
|
||||
|
||||
**Вывод**: Пути API отличаются (единственное число vs множественное), отсутствует отдельный эндпоинт для проверки статуса (статус возвращается вместе с результатом).
|
||||
|
||||
---
|
||||
|
||||
### 4. Структура ответа анализа
|
||||
|
||||
#### ТЗ (Шаг А4):
|
||||
|
||||
Отчёт должен содержать блоки:
|
||||
|
||||
- Обзор бизнеса
|
||||
- Рынок (MarketInsights)
|
||||
- Конкуренты (CompetitorInsights)
|
||||
- Целевая аудитория (AudienceProfile)
|
||||
- Каналы (ChannelInsights)
|
||||
- SWOT (SwotAnalysis)
|
||||
- Рекомендации (список Recommendation)
|
||||
|
||||
#### Реализация:
|
||||
|
||||
Ответ содержит:
|
||||
|
||||
- ✅ `summary` - резюме анализа (соответствует обзору)
|
||||
- ✅ `targetAudience` - целевая аудитория с описанием и каналами
|
||||
- ✅ `recommendations` - список рекомендаций
|
||||
- ✅ `strategy` - маркетинговая стратегия с каналами и типами контента
|
||||
- ❌ **Нет отдельных блоков** для Рынка, Конкурентов, SWOT, Каналов - всё объединено в `summary` на основе `analysisType`
|
||||
|
||||
**Вывод**: В ТЗ предполагается комплексный анализ со всеми блоками, в реализации анализ зависит от выбранного `analysisType` (один тип за раз).
|
||||
|
||||
---
|
||||
|
||||
### 5. Тип анализа
|
||||
|
||||
#### ТЗ:
|
||||
|
||||
Анализ включает **все типы одновременно**:
|
||||
|
||||
- Анализ рынка (MarketInsights)
|
||||
- Анализ конкурентов (CompetitorInsights)
|
||||
- Анализ ЦА (AudienceProfile)
|
||||
- Анализ каналов (ChannelInsights)
|
||||
- SWOT (SwotAnalysis)
|
||||
|
||||
#### Реализация:
|
||||
|
||||
Пользователь **выбирает один тип анализа**:
|
||||
|
||||
- `РЫНОК` - только анализ рынка
|
||||
- `КОНКУРЕНТЫ` - только анализ конкурентов
|
||||
- `ЦА` - только анализ целевой аудитории
|
||||
- `КАНАЛЫ` - только анализ каналов
|
||||
- `SWOT` - только SWOT-анализ
|
||||
|
||||
**Вывод**: Критическое отличие - в ТЗ анализ комплексный, в реализации пользователь выбирает один тип.
|
||||
|
||||
---
|
||||
|
||||
## Блок B. Автоматическое продвижение
|
||||
|
||||
### 1. Название сущности
|
||||
|
||||
#### ТЗ:
|
||||
|
||||
- `Campaign` (кампания)
|
||||
- `CampaignGoal` (цель кампании)
|
||||
- `Strategy` (стратегия)
|
||||
- `CampaignChannel` (каналы кампании)
|
||||
- `ContentItem` (контент-единицы)
|
||||
|
||||
#### Реализация:
|
||||
|
||||
- `MarketingStrategy` (маркетинговая стратегия) - **другое название**
|
||||
- Нет сущности `Campaign` - вместо неё используется `MarketingStrategy`
|
||||
- Нет сущности `CampaignGoal` - цели не реализованы
|
||||
- ✅ `MarketingStrategy.WeeklyPlan` - недельные планы
|
||||
- ✅ `MarketingStrategy.PostCalendarItem` - календарь постов (аналог ContentItem)
|
||||
|
||||
**Вывод**: Концептуальное отличие - в ТЗ есть отдельные сущности Campaign и Strategy, в реализации всё объединено в MarketingStrategy.
|
||||
|
||||
---
|
||||
|
||||
### 2. Эндпоинты продвижения
|
||||
|
||||
#### ТЗ:
|
||||
|
||||
- `POST /api/promotion/campaigns` - создание кампании с `business_id`, `analysis_id`, `goal_id`
|
||||
- `GET /api/promotion/campaigns/{campaign_id}` - получение стратегии кампании
|
||||
- `POST /api/promotion/campaigns/{id}/activate` - активация кампании
|
||||
|
||||
#### Реализация:
|
||||
|
||||
- ✅ `POST /api/marketing/analysis/strategy/generate` - генерация стратегии (путь отличается, нет `goal_id`)
|
||||
- ✅ `GET /api/marketing/analysis/strategy/{strategyId}` - получение стратегии
|
||||
- ✅ `POST /api/marketing/analysis/strategy/{strategyId}/start` - запуск стратегии (аналог активации)
|
||||
|
||||
**Вывод**: Пути API отличаются (`/api/promotion/campaigns` vs `/api/marketing/analysis/strategy`), отсутствует выбор цели кампании (`goal_id`).
|
||||
|
||||
---
|
||||
|
||||
### 3. Выбор цели продвижения
|
||||
|
||||
#### ТЗ (Шаг B1):
|
||||
|
||||
Пользователь выбирает цель из карточек:
|
||||
|
||||
- Увеличить продажи (`increase_sales`)
|
||||
- Получить больше заявок/звонков (`get_leads`)
|
||||
- Повысить узнаваемость бренда (`awareness`)
|
||||
- Продвигать акцию или спецпредложение (`promo`)
|
||||
|
||||
#### Реализация:
|
||||
|
||||
- ❌ **Выбор цели отсутствует** - стратегия генерируется автоматически без выбора цели пользователем
|
||||
|
||||
**Вывод**: Критическое отличие - в ТЗ пользователь выбирает цель, в реализации цель определяется автоматически системой.
|
||||
|
||||
---
|
||||
|
||||
### 4. Формирование стратегии
|
||||
|
||||
#### ТЗ (Шаг B2):
|
||||
|
||||
Стратегия формируется на основе:
|
||||
|
||||
- `AudienceProfile`
|
||||
- `ChannelInsights`
|
||||
- `MarketInsights`
|
||||
- `Recommendation`
|
||||
|
||||
Создаются:
|
||||
|
||||
- `Strategy` - описание, длительность (7-14 дней)
|
||||
- `CampaignChannel` - каналы
|
||||
- `ContentItem` - контент-единицы (черновики)
|
||||
|
||||
#### Реализация:
|
||||
|
||||
Стратегия формируется на основе:
|
||||
|
||||
- ✅ Данных из `MarketingAnalysis`
|
||||
- ✅ `targetAudience` из анализа
|
||||
- ✅ `recommendations` из анализа
|
||||
|
||||
Создаются:
|
||||
|
||||
- ✅ `MarketingStrategy` - описание, длительность в неделях
|
||||
- ✅ `priorityPlatforms` - приоритетные платформы
|
||||
- ✅ `WeeklyPlan` - недельные планы с темами
|
||||
- ✅ `PostCalendarItem` - календарь постов с текстами и хештегами
|
||||
|
||||
**Вывод**: Логика похожа, но структура данных отличается (недельные планы и календарь постов вместо простых ContentItem).
|
||||
|
||||
---
|
||||
|
||||
### 5. Экран "Материалы кампании"
|
||||
|
||||
#### ТЗ (Шаг B4):
|
||||
|
||||
Отображаются `ContentItem`:
|
||||
|
||||
- Тип: пост / сторис / баннер / email
|
||||
- Канал
|
||||
- Черновой текст
|
||||
- Подсказка по визуалу
|
||||
|
||||
#### Реализация:
|
||||
|
||||
Отображаются `PostCalendarItem`:
|
||||
|
||||
- ✅ `platform` - канал
|
||||
- ✅ `contentType` - тип контента (пост, сторис, видео, баннер)
|
||||
- ✅ `postText` - текст поста
|
||||
- ✅ `hashtags` - хештеги
|
||||
- ✅ `publishDate` - дата публикации
|
||||
- ✅ `publishTime` - время публикации
|
||||
- ❌ **Нет подсказок по визуалу**
|
||||
|
||||
**Вывод**: Реализация более детальная (даты, время, хештеги), но отсутствуют подсказки по визуалу.
|
||||
|
||||
---
|
||||
|
||||
## Блок C. Результаты кампаний
|
||||
|
||||
### 1. Эндпоинты результатов
|
||||
|
||||
#### ТЗ:
|
||||
|
||||
- `GET /api/promotion/campaigns?business_id=...` - список кампаний
|
||||
- Детали кампании через тот же эндпоинт
|
||||
|
||||
#### Реализация:
|
||||
|
||||
- ❌ **Эндпоинты для результатов кампаний отсутствуют**
|
||||
- ✅ Есть `GET /api/marketing/analysis/strategy/my` - список стратегий пользователя
|
||||
- ✅ Есть `PostingTask` - задачи на публикацию, но нет метрик
|
||||
|
||||
**Вывод**: Блок C (Результаты кампаний) **не реализован**. Нет метрик, графиков, итоговых выводов.
|
||||
|
||||
---
|
||||
|
||||
### 2. Метрики кампаний
|
||||
|
||||
#### ТЗ (Шаг C2):
|
||||
|
||||
Должны отображаться:
|
||||
|
||||
- Охват (суммарно)
|
||||
- Вовлечённость (лайки/клики/сообщения)
|
||||
- Заявки/обращения
|
||||
- График по дням
|
||||
- Метрики по каналам (Instagram, Telegram и т.д.)
|
||||
- Итоговый вывод системы
|
||||
|
||||
#### Реализация:
|
||||
|
||||
- ❌ **Метрики отсутствуют**
|
||||
- ❌ **Графики отсутствуют**
|
||||
- ❌ **Итоговые выводы отсутствуют**
|
||||
- ✅ Есть `PostingTask` - задачи на публикацию, но без метрик выполнения
|
||||
|
||||
**Вывод**: Функционал отслеживания результатов кампаний полностью отсутствует.
|
||||
|
||||
---
|
||||
|
||||
## Общие отличия
|
||||
|
||||
### 1. API версионирование
|
||||
|
||||
#### ТЗ:
|
||||
|
||||
- API должен быть версионируемым: `/api/v1/...`
|
||||
|
||||
#### Реализация:
|
||||
|
||||
- ❌ **Версионирование отсутствует** - используется `/api/marketing/...`
|
||||
|
||||
**Вывод**: Не соответствует требованию версионирования API.
|
||||
|
||||
---
|
||||
|
||||
### 2. Структура данных
|
||||
|
||||
#### ТЗ:
|
||||
|
||||
Сущности:
|
||||
|
||||
- `BusinessProfile`
|
||||
- `MarketingAnalysis`
|
||||
- `MarketInsights`
|
||||
- `CompetitorInsights`
|
||||
- `AudienceProfile`
|
||||
- `ChannelInsights`
|
||||
- `SwotAnalysis`
|
||||
- `Recommendation`
|
||||
- `Campaign`
|
||||
- `CampaignGoal`
|
||||
- `Strategy`
|
||||
- `CampaignChannel`
|
||||
- `ContentItem`
|
||||
- `CampaignMetrics`
|
||||
|
||||
#### Реализация:
|
||||
|
||||
Сущности:
|
||||
|
||||
- ✅ `MarketingAnalysis`
|
||||
- ✅ `MarketingStrategy`
|
||||
- ✅ `PostingTask`
|
||||
- ❌ **Нет отдельных сущностей** для `MarketInsights`, `CompetitorInsights`, `AudienceProfile`, `ChannelInsights`, `SwotAnalysis` - данные хранятся в `reportData` как Map
|
||||
- ❌ **Нет `Campaign`** - используется `MarketingStrategy`
|
||||
- ❌ **Нет `CampaignGoal`**
|
||||
- ❌ **Нет `CampaignMetrics`**
|
||||
|
||||
**Вывод**: Структура данных упрощена - многие сущности объединены или отсутствуют.
|
||||
|
||||
---
|
||||
|
||||
### 3. Минималистичный подход
|
||||
|
||||
#### ТЗ:
|
||||
|
||||
> "Собирает минимальные данные о бизнесе"
|
||||
> "Нельзя заставлять пользователя выбирать типы рекламы, форматы продвижения"
|
||||
|
||||
#### Реализация:
|
||||
|
||||
- ❌ Пользователь должен выбрать `analysisType` (тип анализа)
|
||||
- ❌ Пользователь должен выбрать `detailLevel` (уровень детализации)
|
||||
- ❌ Добавлены дополнительные поля (`goal`, `strongSide`, `weakSide`)
|
||||
|
||||
**Вывод**: Реализация требует больше данных от пользователя, чем предполагалось в ТЗ.
|
||||
|
||||
---
|
||||
|
||||
## Резюме критических отличий
|
||||
|
||||
### 🔴 Критические расхождения:
|
||||
|
||||
1. **Тип анализа**: В ТЗ анализ комплексный (все типы сразу), в реализации - выбор одного типа
|
||||
2. **Блок C (Результаты)**: Полностью не реализован - нет метрик, графиков, выводов
|
||||
3. **Выбор цели кампании**: Отсутствует в реализации
|
||||
4. **Структура данных**: Упрощена, многие сущности объединены или отсутствуют
|
||||
5. **API версионирование**: Отсутствует
|
||||
|
||||
### 🟡 Значительные отличия:
|
||||
|
||||
1. **Поля формы**: Добавлены дополнительные поля, не указанные в ТЗ
|
||||
2. **Пути API**: Отличаются от указанных в ТЗ
|
||||
3. **Названия сущностей**: `Campaign` → `MarketingStrategy`
|
||||
4. **Статусы**: Разные названия (`PENDING` vs `queued`)
|
||||
|
||||
### 🟢 Незначительные отличия:
|
||||
|
||||
1. **Детализация стратегии**: Реализация более детальная (недельные планы, календарь)
|
||||
2. **Структура ответа**: Данные организованы по-другому, но информация присутствует
|
||||
|
||||
---
|
||||
|
||||
## Рекомендации по приведению к ТЗ
|
||||
|
||||
### Приоритет 1 (Критично):
|
||||
|
||||
1. **Реализовать комплексный анализ** - генерировать все типы анализа одновременно, а не по выбору
|
||||
2. **Реализовать Блок C** - добавить метрики, графики, итоговые выводы
|
||||
3. **Добавить выбор цели кампании** - перед генерацией стратегии
|
||||
|
||||
### Приоритет 2 (Важно):
|
||||
|
||||
1. **Упростить форму** - убрать лишние поля или сделать их опциональными
|
||||
2. **Привести пути API** к указанным в ТЗ или обновить ТЗ
|
||||
3. **Добавить версионирование API** - `/api/v1/...`
|
||||
|
||||
### Приоритет 3 (Желательно):
|
||||
|
||||
1. **Переименовать сущности** или обновить ТЗ под текущую реализацию
|
||||
2. **Добавить подсказки по визуалу** в материалы кампании
|
||||
3. **Привести статусы** к единому виду
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
### Техническое задание для AI-агента
|
||||
|
||||
**Задача:** Расширить систему сбора данных, добавив парсеры для трёх новых RSS-источников.
|
||||
|
||||
**Контекст:** Мы увеличиваем охват нашей аналитической платформы, подключая новые ключевые источники новостей из Казахстана и России. Каждый источник должен быть реализован как отдельный, независимый сервис для надежности.
|
||||
|
||||
---
|
||||
|
||||
## 1\. Общие требования для всех парсеров
|
||||
|
||||
Для каждого из трёх источников ниже необходимо создать отдельный Spring Boot сервис, который:
|
||||
|
||||
- Получает данные из RSS-ленты по URL.
|
||||
- Парсит XML, извлекая `title`, `link`, `pubDate`, `description`.
|
||||
- Нормализует данные: очищает текст от HTML, приводит дату к формату ISODate.
|
||||
- Генерирует уникальный `sha256` хэш (`url + "::" + title`).
|
||||
- Сохраняет результат в общую коллекцию MongoDB `market_items` с операцией `upsert` по хэшу.
|
||||
- Запускается автоматически по расписанию **каждые 30 минут** (`@Scheduled(cron = "0 0/30 * * * ?")`).
|
||||
|
||||
---
|
||||
|
||||
## 2\. Список новых источников и их конфигурация
|
||||
|
||||
### Источник 1: LSM.kz
|
||||
|
||||
- **URL:** `https://lsm.kz/rss`
|
||||
- **Название сервиса:** `lsm-parser-service`
|
||||
- **`source_name` в MongoDB:** `LSM.kz`
|
||||
- **`category` в MongoDB:** `Финансы`
|
||||
|
||||
### Источник 2: РБК
|
||||
|
||||
- **URL:** `https://static.feed.rbc.ru/rbc/logical/footer/news.rss`
|
||||
- **Название сервиса:** `rbc-parser-service`
|
||||
- **`source_name` в MongoDB:** `РБК`
|
||||
- **`category` в MongoDB:** `Бизнес`
|
||||
|
||||
### Источник 3: Ведомости (Технологии)
|
||||
|
||||
- **URL:** `https://www.vedomosti.ru/rss/rubric/technology/internet`
|
||||
- **Название сервиса:** `vedomosti-parser-service`
|
||||
- **`source_name` в MongoDB:** `Ведомости`
|
||||
- **`category` в MongoDB:** `Технологии`
|
||||
|
||||
---
|
||||
|
||||
## Критерии выполнения
|
||||
|
||||
- Созданы три новых Spring Boot сервиса, каждый для своего источника.
|
||||
- Данные со всех трёх новых RSS-лент успешно собираются каждые 30 минут и сохраняются в коллекцию `market_items` без дубликатов.
|
||||
- Структура сохраняемых документов в MongoDB полностью соответствует существующей модели.
|
||||
@@ -0,0 +1,15 @@
|
||||
Необходимо модифицировать существующий ParserController и сервисный слой для эффективного отображения данных.
|
||||
|
||||
Требования:
|
||||
|
||||
Создать MarketItemService: Создай новый сервис MarketItemService, который будет содержать всю логику по работе с коллекцией market_items (получение, подсчет и т.д.). Перенеси в него методы getAllItems и getTotalItemsCount из KursivParserService и сделай так, чтобы они работали со всеми записями, а не только с одним источником.
|
||||
|
||||
Реализовать пагинацию и сортировку: Измени метод getAllItems в контроллере и сервисе так, чтобы он принимал Pageable в качестве аргумента. Метод должен возвращать Page<MarketItem>. Фронтенд сможет запрашивать данные так: GET /api/parser/items?page=0&size=10&sort=published_at,desc.
|
||||
|
||||
Добавить фильтрацию: Добавь в метод getAllItems необязательные параметры запроса (@RequestParam):
|
||||
|
||||
sourceName (тип String) для фильтрации по источнику.
|
||||
|
||||
startDate, endDate (тип String или LocalDate) для фильтрации по диапазону дат.
|
||||
|
||||
Заменить Map<String, Object> на DTO: Создай классы-DTO для всех ответов контроллера, чтобы обеспечить строгую типизацию.
|
||||
Binary file not shown.
+103
@@ -0,0 +1,103 @@
|
||||
### **Техническое Задание: Внедрение гибридной AI-модели (OpenAI для диаграмм, Ollama для текста)**
|
||||
|
||||
#### **1. Общее описание**
|
||||
|
||||
Целью является доработка AI-агента для использования гибридной модели генерации. Основной синтез текстового отчёта по-прежнему будет выполняться с помощью локального сервиса **Ollama**. Для опциональной и более сложной задачи — извлечения структурированных данных и генерации диаграмм — будет использоваться API **OpenAI**.
|
||||
|
||||
Этот подход позволяет использовать сильные стороны каждой модели:
|
||||
|
||||
- **Ollama:** Экономичная и быстрая генерация основного текста отчёта.
|
||||
- **OpenAI:** Высокая точность в следовании инструкциям для генерации идеально отформатированного JSON и SVG-кода для диаграмм.
|
||||
|
||||
#### **2. Обновлённая схема работы**
|
||||
|
||||
1. **Сбор данных:** Бэкенд получает `learnings` от `deep-research` API.
|
||||
2. **Параллельный запуск двух AI-задач:**
|
||||
- **Ветка A (Текст -\> Ollama):** `learnings` отправляются в ваш существующий `OllamaAnalyticsService` для синтеза основного текста отчёта в Markdown.
|
||||
- **Ветка B (Диаграммы -\> OpenAI):** `learnings` отправляются в **новый сервис** (`OpenAiChartService`), который делает два последовательных вызова к **API OpenAI**:
|
||||
1. Извлечь из текста данные для диаграмм в формате **JSON**.
|
||||
2. Превратить этот JSON в **SVG-код** диаграммы.
|
||||
3. **Конвертация SVG:** SVG-код, полученный от OpenAI, конвертируется в PNG-изображение на стороне Java.
|
||||
4. **Финальная сборка PDF:** `ResearchPdfService` объединяет текст отчёта (от Ollama) и изображения диаграмм (от OpenAI) в единый PDF-файл.
|
||||
|
||||
#### **3. Требования к реализации**
|
||||
|
||||
**3.1. Конфигурация**
|
||||
|
||||
В ваш файл `application.properties` (или `application.yml`) необходимо добавить API-ключ для OpenAI:
|
||||
|
||||
```properties
|
||||
# application.properties
|
||||
|
||||
# ... ваши существующие настройки Ollama ...
|
||||
ollama.model=llama3:8b-instruct
|
||||
|
||||
# --- НОВЫЕ НАСТРОЙКИ ДЛЯ OPENAI ---
|
||||
openai.api.key=sk-...(ваш API ключ от OpenAI)...
|
||||
openai.api.url=https://api.openai.com/v1/chat/completions
|
||||
openai.model.name=gpt-4-turbo # Рекомендуемая модель для работы с JSON и SVG
|
||||
```
|
||||
|
||||
**3.2. Создание нового сервиса: `OpenAiChartService`**
|
||||
|
||||
Необходимо создать новый Spring-сервис, отвечающий за взаимодействие с OpenAI.
|
||||
|
||||
- **`WebClient`:** Сервис должен содержать собственный `WebClient`, настроенный на `openai.api.url` и автоматически добавляющий заголовок `Authorization: Bearer ${openai.api.key}` ко всем запросам.
|
||||
- **Методы:**
|
||||
1. `public Mono<String> getChartDataJson(List<String> learnings)`: Принимает `learnings`, использует **Промпт №1** (для извлечения данных) и возвращает `Mono` со строкой, содержащей JSON-массив.
|
||||
2. `public Mono<String> getChartSvg(String jsonData)`: Принимает JSON с данными для одной диаграммы, использует **Промпт №2** (для генерации SVG) и возвращает `Mono` со строкой SVG-кода.
|
||||
|
||||
**3.3. Модификация оркестратора: `ReportSynthesisService`**
|
||||
|
||||
Этот сервис теперь будет управлять вызовами к обоим AI-сервисам.
|
||||
|
||||
- **Зависимости:** `ReportSynthesisService` теперь должен инжектировать (`@Autowired`) и `OllamaAnalyticsService`, и новый `OpenAiChartService`.
|
||||
- **Метод `synthesizeReportAndCharts`:**
|
||||
- **Ветка текста (`textMono`)** по-прежнему вызывает `ollamaAnalyticsService` для генерации отчёта.
|
||||
- **Ветка диаграмм (`chartsMono`)** теперь будет вызывать `openAiChartService`.
|
||||
|
||||
**Пример обновлённой логики для `chartsMono`:**
|
||||
|
||||
```java
|
||||
// ... внутри ReportSynthesisService ...
|
||||
|
||||
// Ветка B: Генерация диаграмм через OpenAI
|
||||
Mono<List<byte[]>> chartsMono = openAiChartService.getChartDataJson(aggregatedLearnings)
|
||||
.flatMap(jsonArrayString -> {
|
||||
String cleanJsonArray = extractJsonArray(jsonArrayString);
|
||||
if (cleanJsonArray == null || cleanJsonArray.equals("[]")) {
|
||||
return Mono.just(Collections.emptyList()); // Если данных нет, возвращаем пустой список
|
||||
}
|
||||
|
||||
try {
|
||||
List<ChartData> chartDataList = objectMapper.readValue(cleanJsonArray, new TypeReference<List<ChartData>>() {});
|
||||
|
||||
// Превращаем список задач в поток (Flux) и выполняем их последовательно
|
||||
return Flux.fromIterable(chartDataList)
|
||||
.concatMap(chartData ->
|
||||
openAiChartService.getChartSvg(objectMapper.writeValueAsString(chartData))
|
||||
)
|
||||
.map(this::extractSvgCode)
|
||||
.map(SvgToPngConverter::convert)
|
||||
.filter(Objects::nonNull)
|
||||
.collectList();
|
||||
|
||||
} catch (IOException e) {
|
||||
return Mono.error(e);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**3.4. Промпты для OpenAI**
|
||||
|
||||
Промпты остаются теми же, что и в предыдущем ТЗ, так как модели OpenAI отлично их понимают.
|
||||
|
||||
- **Промпт №1 (Извлечение данных):** Просит вернуть **JSON-массив** объектов с данными для диаграмм или пустой массив `[]`, если данных нет.
|
||||
- **Промпт №2 (Генерация SVG):** Просит на основе одного JSON-объекта вернуть **только SVG-код**.
|
||||
|
||||
#### **4. План действий**
|
||||
|
||||
1. **Добавить `openai.api.key`** в ваш `application.properties`.
|
||||
2. **Создать новый класс `OpenAiChartService.java`**. Он будет похож на `OllamaAnalyticsService`, но настроен для работы с API OpenAI (другой URL, заголовок авторизации, другая структура JSON-запроса).
|
||||
3. **Обновить `ReportSynthesisService.java`**, чтобы он использовал `OllamaAnalyticsService` для текста и `OpenAiChartService` для диаграмм, как показано в примере выше.
|
||||
4. **Перезапустить** ваше Java-приложение.
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
Техническое Задание: Опциональное добавление диаграмм в PDF-отчёты
|
||||
1. Общее описание
|
||||
Целью является улучшение существующих PDF-отчётов путём опционального добавления в них диаграмм и графиков. Основной процесс генерации текстового отчёта на основе learnings остаётся без изменений. Параллельно с этим, AI-агент будет пытаться найти в "сырых" данных (learnings) числовую информацию, подходящую для визуализации.
|
||||
|
||||
Если такие данные найдены, агент сгенерирует диаграмму и вставит её в PDF. Если данных нет, будет сгенерирован обычный текстовый отчёт без диаграмм. Процесс не должен прерываться, если сгенерировать диаграмму невозможно.
|
||||
|
||||
2. Обновлённая схема работы
|
||||
Процесс будет состоять из двух параллельных веток, которые затем объединяются:
|
||||
|
||||
Сбор данных: Бэкенд вызывает deep-research API и получает learnings и visitedUrls.
|
||||
|
||||
Запускаются два асинхронных вызова к Ollama:
|
||||
|
||||
Ветка A (Синтез текста - основная): Все learnings отправляются в Ollama с промптом на написание красивого, структурированного текстового отчёта в формате Markdown. (Этот шаг остаётся без изменений).
|
||||
|
||||
Ветка B (Поиск данных для диаграммы - опциональная): Все learnings отправляются в Ollama с новым, специальным промптом для поиска числовых данных и возврата их в виде JSON.
|
||||
|
||||
Обработка Ветки B (если успешно):
|
||||
|
||||
Если Ollama вернул валидный JSON с данными, делается третий вызов к Ollama с промптом на генерацию SVG-кода этой диаграммы.
|
||||
|
||||
|
||||
Shutterstock
|
||||
* Полученный SVG-код конвертируется в изображение (PNG) на стороне Java.
|
||||
Финальная сборка PDF:
|
||||
|
||||
ResearchPdfService получает обязательный текстовый отчёт из Ветки А, опциональное изображение диаграммы из Ветки B и список visitedUrls.
|
||||
|
||||
Он собирает всё это в единый PDF-документ и возвращает пользователю.
|
||||
|
||||
3. Требования к реализации
|
||||
3.1. Модификация сервиса синтеза (например, ReportSynthesisService)
|
||||
|
||||
Этот сервис теперь будет оркестрировать несколько асинхронных вызовов.
|
||||
|
||||
Основной метод synthesizeReportAndCharts будет возвращать объект, содержащий и текст, и изображения, например Mono<FinalReportPayload>.
|
||||
|
||||
Java
|
||||
|
||||
class FinalReportPayload {
|
||||
String markdownContent;
|
||||
List<byte[]> chartImages;
|
||||
// ...
|
||||
}
|
||||
Внутри этого метода будут асинхронно запускаться два вызова к Ollama:
|
||||
|
||||
generateTextReport(learnings) - для получения текста.
|
||||
|
||||
generateChartData(learnings) - для получения JSON для диаграммы.
|
||||
|
||||
3.2. Промпт-инжиниринг для диаграмм (Ветка B)
|
||||
|
||||
Шаг 1: Извлечение данных в JSON
|
||||
|
||||
Задача: Найти в тексте данные для визуализации.
|
||||
|
||||
Промпт:
|
||||
|
||||
"Ты — AI-аналитик данных. Внимательно проанализируй следующий текст. Если найдёшь в нём числовые данные, которые можно представить в виде простой столбчатой или круговой диаграммы (например, рост по годам, доли рынка, сравнение показателей), верни ТОЛЬКО один JSON-объект со структурой: {\"chartType\": \"bar\" или \"pie\", \"title\": \"Название диаграммы\", \"labels\": [\"Метка 1\", \"Метка 2\"], \"data\": [число1, число2]}. Если подходящих данных нет, верни пустой JSON-объект {}. Текст для анализа:\n---\n${learnings_as_string}\n---"
|
||||
|
||||
Шаг 2: Генерация SVG из JSON
|
||||
|
||||
Задача: Превратить JSON с данными в SVG-код.
|
||||
|
||||
Промпт:
|
||||
|
||||
"Ты — эксперт по визуализации данных. На основе следующего JSON, сгенерируй полный и валидный SVG-код для диаграммы. SVG должен быть стильным и читаемым, с подписями на русском языке. Не добавляй никаких комментариев, верни ТОЛЬКО SVG-код. JSON с данными:\n---\n${json_from_previous_step}\n---"
|
||||
|
||||
3.3. Конвертация SVG в изображение
|
||||
|
||||
Этот модуль остаётся без изменений. Используется библиотека Apache Batik для преобразования SVG-строки в byte[] PNG-изображения.
|
||||
|
||||
Зависимость в pom.xml:
|
||||
|
||||
XML
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.xmlgraphics</groupId>
|
||||
<artifactId>batik-transcoder</artifactId>
|
||||
<version>1.17</version>
|
||||
</dependency>
|
||||
Вспомогательный класс-конвертер остаётся тем же.
|
||||
|
||||
3.4. Модификация PDF-сервиса (ResearchPdfService)
|
||||
|
||||
Сервис должен быть готов к тому, что диаграмм может и не быть.
|
||||
|
||||
Измените метод generatePdfReport, чтобы он принимал список изображений, который может быть пустым:
|
||||
|
||||
Java
|
||||
|
||||
public byte[] generatePdfReport(String query, String markdownContent, List<String> visitedUrls, List<byte[]> chartImages)
|
||||
Внутри метода добавьте проверку: если список chartImages не пустой, вставляйте изображения в документ.
|
||||
|
||||
Java
|
||||
|
||||
// ... после вставки основного текста отчёта ...
|
||||
|
||||
if (chartImages != null && !chartImages.isEmpty()) {
|
||||
document.newPage();
|
||||
document.add(new Paragraph("Визуализация данных", headingFont));
|
||||
|
||||
for (byte[] imageData : chartImages) {
|
||||
try {
|
||||
Image chartImage = Image.getInstance(imageData);
|
||||
// Масштабируем, чтобы поместилось на страницу
|
||||
float scaler = ((document.getPageSize().getWidth() - document.leftMargin() - document.rightMargin()) / chartImage.getWidth()) * 80; // 80% ширины
|
||||
chartImage.scalePercent(scaler);
|
||||
chartImage.setAlignment(Element.ALIGN_CENTER);
|
||||
|
||||
document.add(new Paragraph(" ")); // Отступ
|
||||
document.add(chartImage);
|
||||
} catch (Exception e) {
|
||||
// Логируем ошибку, но не прерываем генерацию PDF
|
||||
System.err.println("Не удалось добавить изображение диаграммы в PDF: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
4. План действий
|
||||
Добавить зависимость Apache Batik в pom.xml.
|
||||
|
||||
Реализовать асинхронную логику в ReportSynthesisService, которая параллельно запрашивает у Ollama:
|
||||
|
||||
Основной текст отчёта.
|
||||
|
||||
JSON с данными для диаграммы.
|
||||
|
||||
Добавить в тот же сервис условную логику: если JSON получен, сделать ещё один запрос к Ollama за SVG-кодом.
|
||||
|
||||
Реализовать SVG-конвертер (или добавить его как util-класс).
|
||||
|
||||
Обновить ResearchPdfService, чтобы он мог принимать и вставлять список изображений, если он не пуст.
|
||||
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
### **Техническое Задание на доработку AI-агента для генерации отчётов в формате Markdown (.md)**
|
||||
|
||||
**Проект:** Модификация существующего бэкенда для генерации отчётов.
|
||||
|
||||
#### **1. Общее описание**
|
||||
|
||||
Целью данной доработки является замена модуля генерации PDF-файлов на более простой и быстрый модуль, который будет формировать и отдавать пользователю итоговый отчёт в текстовом формате **Markdown (`.md`)**.
|
||||
|
||||
Бэкенд по-прежнему будет выполнять многоступенчатый процесс исследования (вызов `deep-research` API, затем синтез отчёта через `Ollama`), но финальным результатом будет текстовый `.md` файл, а не PDF.
|
||||
|
||||
#### **2. Требования к реализации**
|
||||
|
||||
**2.1. Модификация эндпоинта `/api/parser/report`**
|
||||
|
||||
- **Метод:** `POST`
|
||||
|
||||
- **Тело запроса (`Request Body`):** Остаётся без изменений (принимает `query`, `lang`, `depth` и т.д.).
|
||||
|
||||
- **Успешный ответ (`Success Response`):** **(Ключевое изменение)**
|
||||
|
||||
- **Код:** `200 OK`
|
||||
- **Заголовки:**
|
||||
- `Content-Type: text/markdown; charset=UTF-8` (Важно для корректного отображения кириллицы).
|
||||
- `Content-Disposition: attachment; filename="research_report.md"` (Этот заголовок заставит браузер скачать файл, а не просто показать его).
|
||||
- **Тело ответа:** Готовый текст отчёта в формате Markdown.
|
||||
|
||||
- **Ответы с ошибками (`Error Responses`):** Остаются без изменений (`400`, `500` и т.д.).
|
||||
|
||||
**2.2. Изменение логики работы агента**
|
||||
|
||||
Процесс будет выглядеть так:
|
||||
|
||||
1. **Приём и валидация запроса:** Логика остаётся без изменений.
|
||||
2. **Вызов `deep-research` API:** Логика остаётся без изменений. Бэкенд получает "сырые" `learnings` и `visitedUrls`.
|
||||
3. **Вызов Ollama для синтеза:** Логика остаётся без изменений. Бэкенд получает от Ollama финальный, красиво структурированный отчёт в виде строки Markdown.
|
||||
4. **ОТМЕНА:** Шаг генерации PDF полностью удаляется. Сервис `ResearchPdfService` и зависимость от библиотеки iText больше не нужны.
|
||||
5. **НОВЫЙ ШАГ: Формирование итогового `.md` файла:**
|
||||
- Бэкенд берёт строку Markdown, полученную от Ollama.
|
||||
- К этой строке в конец добавляется раздел "Источники". Бэкенд должен программно сгенерировать этот раздел, добавив заголовок `## Источники` и пронумерованный список URL-адресов из `visitedUrls`.
|
||||
6. **Отправка `.md` файла клиенту:** Бэкенд отправляет итоговую строку (отчёт + источники) как тело HTTP-ответа с заголовками, указанными в п. 2.1.
|
||||
|
||||
**2.3. Формат итогового Markdown-файла**
|
||||
|
||||
Итоговая строка, которая будет отправлена клиенту, должна иметь следующую структуру:
|
||||
|
||||
```markdown
|
||||
# {Заголовок, сгенерированный Ollama, или просто ваш query}
|
||||
|
||||
## Введение
|
||||
|
||||
... текст от Ollama ...
|
||||
|
||||
## Основная часть
|
||||
|
||||
... текст от Ollama ...
|
||||
|
||||
### Подраздел
|
||||
|
||||
... текст от Ollama ...
|
||||
|
||||
## Заключение
|
||||
|
||||
... текст от Ollama ...
|
||||
|
||||
---
|
||||
|
||||
## Источники
|
||||
|
||||
1. https://...
|
||||
2. https://...
|
||||
3. ...
|
||||
```
|
||||
|
||||
#### **3. Технологический стек**
|
||||
|
||||
- **Бэкенд:** Java/Spring Boot (без изменений).
|
||||
- **HTTP-клиент:** `WebClient` (без изменений)
|
||||
|
||||
#### **4. Нефункциональные требования**
|
||||
|
||||
- **Асинхронность:** Рекомендация по асинхронной обработке запроса (через `jobId` и отдельный эндпоинт для статуса) остаётся актуальной, так как сам процесс исследования по-прежнему занимает много времени.
|
||||
|
||||
#### **5. Ожидаемый результат**
|
||||
|
||||
При отправке `POST` запроса на эндпоинт `/api/parser/report` браузер пользователя должен инициировать скачивание файла `research_report.md`. Этот файл должен корректно открываться в любом текстовом редакторе, поддерживающем Markdown, и содержать полный, структурированный отчёт на русском языке вместе со списком источников.
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
Проект: Доработка модуля генерации отчётов в существующем Java/Spring Boot бэкенде.
|
||||
|
||||
1. Общее описание
|
||||
Целью является создание "Агента-синтезатора" внутри бэкенда. Этот агент будет использовать существующий сервис OllamaAnalyticsService для взаимодействия с локально развёрнутой языковой моделью Ollama. Задача агента — принять "сырые" тезисы (learnings) от сервиса deep-research и с помощью Ollama сгенерировать из них связный, структурированный и стилистически выверенный текстовый отчёт в формате Markdown.
|
||||
|
||||
2. Требования к реализации
|
||||
2.1. Создание нового сервиса: ReportSynthesisService
|
||||
|
||||
Необходимо создать новый Spring-сервис (@Service).
|
||||
|
||||
Этот сервис будет зависеть от уже существующего OllamaAnalyticsService и должен инжектировать его (@Autowired).
|
||||
|
||||
Сервис должен иметь один публичный метод:
|
||||
|
||||
Java
|
||||
|
||||
public Mono<String> synthesizeReport(String originalQuery, List<String> learnings, String lang)
|
||||
Вход:
|
||||
|
||||
originalQuery: Исходная тема исследования.
|
||||
|
||||
learnings: Список "сырых" тезисов от deep-research.
|
||||
|
||||
lang: Целевой язык отчёта.
|
||||
|
||||
Выход: Mono<String>, содержащий готовый отчёт в формате Markdown.
|
||||
|
||||
2.2. Логика работы ReportSynthesisService
|
||||
|
||||
Метод synthesizeReport получает на вход данные.
|
||||
|
||||
Он объединяет список learnings в единый блок текста, где каждый тезис начинается с новой строки (например, с помощью String.join("\n- ", learnings)).
|
||||
|
||||
Он формирует промпт (инструкцию) для модели Ollama (см. п. 2.3).
|
||||
|
||||
Ключевой шаг: Он вызывает метод generateWithInstruction из вашего существующего OllamaAnalyticsService:
|
||||
|
||||
Java
|
||||
|
||||
// Преобразуем асинхронный вызов в Mono
|
||||
Mono<String> synthesizedReportMono = Mono.fromCallable(() ->
|
||||
ollamaAnalyticsService.generateWithInstruction(aggregatedLearnings, prompt)
|
||||
);
|
||||
Метод возвращает synthesizedReportMono.
|
||||
|
||||
2.3. Разработка промпта для Ollama
|
||||
|
||||
Промпт — это сердце качественного результата. Он должен быть чётким и структурированным. Этот промпт будет реализован как форматированная строка внутри ReportSynthesisService.
|
||||
|
||||
Пример промпта для Ollama (модели типа Llama3, Gemma):
|
||||
|
||||
Ты — профессиональный AI-аналитик. Твоя задача — написать подробный и структурированный отчёт на основе предоставленных данных. Не добавляй никаких предисловий или комментариев от себя, просто верни готовый отчёт.
|
||||
|
||||
### Тема исследования:
|
||||
|
||||
${originalQuery}
|
||||
|
||||
### Собранные данные (тезисы):
|
||||
|
||||
- ${learnings_as_string}
|
||||
|
||||
### Твоя задача:
|
||||
|
||||
1. **Язык:** Напиши отчёт полностью на ${lang} языке.
|
||||
2. **Структура:** Отчёт должен иметь чёткую структуру: введение, основная часть с несколькими разделами и подразделами, и заключение.
|
||||
3. **Форматирование:** Используй Markdown. Для главных заголовков разделов используй `##`, для подзаголовков — `###`. Для списков используй `-`.
|
||||
4. **Стиль:** Преврати разрозненные тезисы в единую, связную и хорошо написанную статью. Не просто перечисляй факты, а анализируй и обобщай их.
|
||||
5. **Ограничение:** Используй только ту информацию, которая дана в собранных данных. Не придумывай ничего от себя.
|
||||
|
||||
Начинай отчёт с главного заголовка.
|
||||
2.4. Интеграция в существующий асинхронный процесс
|
||||
|
||||
Необходимо модифицировать ваш существующий метод в контроллере, который запускает весь процесс. Вместо вызова OpenAI, теперь будет вызываться ваш новый ReportSynthesisService.
|
||||
|
||||
Примерная схема интеграции (на основе вашего кода):
|
||||
|
||||
Java
|
||||
|
||||
// ...
|
||||
// Инжектируем новый сервис
|
||||
@Autowired
|
||||
private ReportSynthesisService reportSynthesisService;
|
||||
|
||||
// ... в вашем методе generateResearchReport
|
||||
deepResearchService.generateReportAsync(deepRequest)
|
||||
.publishOn(Schedulers.boundedElastic())
|
||||
.flatMap(researchResponse -> { // Используем flatMap для асинхронной цепочки
|
||||
if (researchResponse == null || researchResponse.getLearnings() == null || researchResponse.getLearnings().isEmpty()) {
|
||||
return Mono.error(new RuntimeException("Empty research response content"));
|
||||
}
|
||||
// НОВЫЙ ШАГ: Вызываем наш сервис синтеза с Ollama
|
||||
return reportSynthesisService.synthesizeReport(
|
||||
request.getQuery(),
|
||||
researchResponse.getLearnings(),
|
||||
request.getLang()
|
||||
).map(synthesizedText -> new SynthesizedReport(researchResponse, synthesizedText)); // Объединяем результаты
|
||||
})
|
||||
.map(synthesizedReport -> {
|
||||
// Здесь мы используем synthesizedText для генерации PDF
|
||||
// Этот код остаётся почти без изменений
|
||||
byte[] pdfBytes = researchPdfService.generatePdfReport(
|
||||
request.getQuery(),
|
||||
synthesizedReport.getOriginalResponse(),
|
||||
synthesizedReport.getSynthesizedMarkdown() // Передаем новый красивый текст от Ollama
|
||||
);
|
||||
String filename = "research*report*" + System.currentTimeMillis() + ".pdf";
|
||||
saveResearchReportToHistory(request, filename, pdfBytes, synthesizedReport.getOriginalResponse());
|
||||
return true;
|
||||
})
|
||||
.doOnError(e -> {
|
||||
System.err.println("Error in full research chain: " + e.getMessage());
|
||||
})
|
||||
.subscribe();
|
||||
// ...
|
||||
Вам также понадобится немного изменить researchPdfService.generatePdfReport, чтобы он принимал новый синтезированный Markdown-текст в качестве основного контента.
|
||||
|
||||
3. Конфигурация
|
||||
Вся конфигурация для Ollama уже есть в вашем OllamaAnalyticsService. Новых настроек добавлять не нужно.
|
||||
|
||||
Важная рекомендация: Убедитесь, что в вашем application.properties (или yml) для ollama.model указана достаточно мощная модель, способная следовать сложным инструкциям. Модель gemma3:1b слишком слабая для такой задачи. Рекомендуется использовать:
|
||||
|
||||
llama3:8b-instruct (хороший баланс)
|
||||
|
||||
gemma:7b-it
|
||||
|
||||
mistral или mixtral
|
||||
|
||||
Пример:
|
||||
|
||||
Properties
|
||||
|
||||
ollama.host=http://185.35.223.45:11434
|
||||
ollama.model=llama3:8b-instruct
|
||||
ollama.timeoutMs=180000 # Увеличьте таймаут до 3 минут, т.к. генерация отчёта - долгая задача 4. Ожидаемый результат
|
||||
После реализации бэкенд будет использовать ваш собственный, локально развёрнутый Ollama для создания высококачественных, структурированных PDF-отчётов, полностью контролируя весь процесс и не завися от внешних платных API для синтеза текста.
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
### Техническое задание (Версия 4.0): Интеграция генерации отчётов в `parser-service`
|
||||
|
||||
**Задача:** Модифицировать существующий `parser-service`, добавив в него функционал для автоматической генерации маркетинговых отчётов.
|
||||
|
||||
**Контекст:** Мы отказываемся от создания отдельного сервиса для отчётов. Вся логика по их созданию должна быть реализована внутри `parser-service`, который уже имеет доступ к данным в MongoDB и к AI-аналитике.
|
||||
|
||||
---
|
||||
|
||||
## 1\. Архитектура и воркфлоу
|
||||
|
||||
1. **Никаких новых сервисов.** Вся логика реализуется в `parser-service`.
|
||||
2. **Новый API эндпоинт:** В `parser-service` необходимо добавить новый REST API эндпоинт для запуска процесса генерации отчёта.
|
||||
3. **Внутренний воркфлоу:**
|
||||
- Новый эндпоинт (например, в `MarketItemController` или новом `ReportController`) получает запрос с параметрами отчёта.
|
||||
- Контроллер вызывает новый `ReportGenerationService` (созданный внутри `parser-service`).
|
||||
- `ReportGenerationService` использует уже существующий `MarketItemRepository` для получения данных за указанный период.
|
||||
- Для генерации аннотации отчёта он обращается к существующему `OllamaAnalyticsService`.
|
||||
- Сервис формирует документ в заданном формате (PDF или DOCX).
|
||||
- Готовый файл возвращается пользователю в теле HTTP-ответа.
|
||||
|
||||
---
|
||||
|
||||
## 2\. API эндпоинт
|
||||
|
||||
### `POST /api/report/generate`
|
||||
|
||||
Этот эндпоинт будет находиться в `parser-service` и инициировать создание отчёта.
|
||||
|
||||
**Тело запроса (Request Body):**
|
||||
|
||||
```json
|
||||
{
|
||||
"reportTitle": "Еженедельный анализ новостного фона",
|
||||
"authorName": "Имя Аналитика",
|
||||
"companyName": "Название Компании Клиента",
|
||||
"startDate": "2025-09-10T00:00:00",
|
||||
"endDate": "2025-09-17T23:59:59",
|
||||
"format": "PDF" // или "DOCX"
|
||||
}
|
||||
```
|
||||
|
||||
**Успешный ответ:**
|
||||
|
||||
- **HTTP Статус:** `200 OK`
|
||||
- **Headers:** `Content-Disposition: attachment; filename="report_2025-09-17.pdf"`
|
||||
- **Body:** Бинарные данные сгенерированного файла.
|
||||
|
||||
---
|
||||
|
||||
## 3\. Структура и содержание отчёта
|
||||
|
||||
Сервис должен динамически генерировать документ, следуя этой структуре:
|
||||
|
||||
### 1\. Титульный лист
|
||||
|
||||
- **Заголовок:** Из поля `reportTitle` запроса.
|
||||
- **Автор:** Из поля `authorName` запроса.
|
||||
- **Название компании:** Из поля `companyName` запроса.
|
||||
- **Дата:** Текущая дата генерации отчёта.
|
||||
|
||||
### 2\. Содержание (Table of Contents)
|
||||
|
||||
- Автоматически генерируемое оглавление со ссылками на основные разделы.
|
||||
|
||||
### 3\. Краткое содержание (Аннотация)
|
||||
|
||||
- **Реализация:**
|
||||
1. Получить все **саммари** (`analytics.summary`) статей за выбранный период из MongoDB.
|
||||
2. Объединить их в один большой текст.
|
||||
3. Отправить этот текст в **Ollama** с промптом: `"На основе этих кратких сводок новостей напиши общую аннотацию на 2-3 абзаца, выделяя ключевые тренды и события."`
|
||||
|
||||
### 4\. Введение
|
||||
|
||||
- Использовать шаблонный текст с динамическими датами.
|
||||
- **Пример:** `"Целью данного отчёта является анализ новостного фона в сфере маркетинга и бизнеса за период с [startDate] по [endDate]. Были проанализированы публикации из ключевых источников для выявления основных трендов."`
|
||||
|
||||
### 5\. Основная часть
|
||||
|
||||
- **Подраздел: Ключевые публикации**
|
||||
- Вывести 5-10 самых важных новостей, для каждой указав: **Заголовок, Источник, Дата публикации, Сгенерированное саммари (`analytics.summary`)**.
|
||||
- **Подраздел: Облако тегов**
|
||||
- Собрать все теги (`analytics.tags`) и сформировать изображение "облака тегов".
|
||||
- **Подраздел: Анализ тональности**
|
||||
- Подсчитать количество статей с тональностью `positive`, `negative`, `neutral` и представить в виде круговой диаграммы.
|
||||
- **Подраздел: Упоминаемые компании и персоны**
|
||||
- Собрать и вывести списки наиболее часто упоминаемых компаний и персон из `analytics.entities`.
|
||||
|
||||
### 6\. Рекомендации
|
||||
|
||||
- Использовать статический текст-заполнитель.
|
||||
|
||||
### 7\. Список литературы
|
||||
|
||||
- Автоматически сгенерированный список всех проанализированных статей.
|
||||
- Формат: `[Заголовок статьи]. Источник: [Название источника]. URL: [Ссылка на статью]`
|
||||
|
||||
### 8\. Приложения
|
||||
|
||||
- Добавить полную таблицу со всеми статьями за период.
|
||||
|
||||
---
|
||||
|
||||
## 4\. Технологический стек
|
||||
|
||||
- **DOCX:** **Apache POI**
|
||||
- **PDF:** **OpenPDF** или **iText**
|
||||
- **Графики и диаграммы:** **JFreeChart**
|
||||
|
||||
---
|
||||
|
||||
## 5\. Критерии выполнения
|
||||
|
||||
- `parser-service` расширен новым функционалом без создания отдельного сервиса.
|
||||
- Реализован эндпоинт `POST /api/report/generate`, который принимает параметры отчёта.
|
||||
- Сервис успешно генерирует и возвращает файлы в форматах PDF и DOCX.
|
||||
- Структура и содержание сгенерированных документов полностью соответствуют описанию в ТЗ.
|
||||
@@ -0,0 +1,93 @@
|
||||
### Техническое задание (Версия 2.0): Интеграция AI-аналитики в `parser-service`
|
||||
|
||||
**Задача:** Модифицировать существующий `parser-service` для обогащения новостей аналитическими данными (саммари, теги, тональность) **в момент парсинга**, перед сохранением в базу данных.
|
||||
|
||||
**Контекст:** Мы отказываемся от создания отдельного `analytics-service` в пользу более простой, монолитной архитектуры. Аналитика должна стать неотъемлемой частью процесса парсинга. Каждая новость, попадающая в базу данных, должна уже содержать сгенерированные AI-данные.
|
||||
|
||||
---
|
||||
|
||||
## 1\. Архитектурные изменения
|
||||
|
||||
- **Никаких новых сервисов.** Вся логика реализуется внутри существующего `parser-service`.
|
||||
- **Синхронный процесс:** Новый воркфлоу для каждой новости: **Парсинг -\> Аналитика -\> Сохранение в MongoDB**.
|
||||
- **Новый компонент:** Внутри `parser-service` необходимо создать новый сервис/компонент (например, `OllamaAnalyticsService`), который будет отвечать за все взаимодействия с API Ollama.
|
||||
|
||||
---
|
||||
|
||||
## 2\. Основные требования
|
||||
|
||||
### 2.1. Создание `OllamaAnalyticsService`
|
||||
|
||||
В проекте `parser-service` создайте новый сервис, который будет инкапсулировать логику общения с Ollama.
|
||||
|
||||
- **Взаимодействие с API Ollama:**
|
||||
- **Хост:** `http://185.35.223.45:11434`
|
||||
- **Модель:** `gemma3:1b`
|
||||
- **Клиент:** Использовать `WebClient` для HTTP-запросов к эндпоинту `/api/generate`.
|
||||
- **Основной метод:** У сервиса должен быть публичный метод, например `Analytics analyzeText(String text)`, который принимает сырой текст статьи и возвращает готовый объект `Analytics` со всеми заполненными полями.
|
||||
|
||||
### 2.2. Модификация существующих парсеров
|
||||
|
||||
Необходимо изменить логику **каждого** существующего парсера (`KursivParserService`, `KapitalParserService` и т.д.).
|
||||
|
||||
**Новый алгоритм работы для метода `parseAndSaveRssFeed()`:**
|
||||
|
||||
1. Получить и разобрать данные из RSS-ленты.
|
||||
2. Для каждой новости, после извлечения `raw_text`, **вызвать** метод `analyzeText` из нового `OllamaAnalyticsService`.
|
||||
3. Получить в ответ заполненный объект `Analytics`.
|
||||
4. Установить этот объект в поле `analytics` у сущности `MarketItem`.
|
||||
5. **Только после этого** сохранить полностью обогащенный `MarketItem` в MongoDB.
|
||||
|
||||
**Примерный псевдокод для `KursivParserService`:**
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class KursivParserService implements ParserService {
|
||||
|
||||
@Autowired
|
||||
private OllamaAnalyticsService analyticsService; // Новый сервис
|
||||
@Autowired
|
||||
private MarketItemRepository repository;
|
||||
|
||||
@Override
|
||||
public List<MarketItem> parseAndSaveRssFeed() {
|
||||
// ... логика получения данных из RSS ...
|
||||
|
||||
for (RssItem rssItem : feedItems) {
|
||||
MarketItem marketItem = new MarketItem();
|
||||
marketItem.setTitle(rssItem.getTitle());
|
||||
marketItem.setRawText(rssItem.getText());
|
||||
// ... установить остальные поля ...
|
||||
|
||||
// === НОВЫЙ ШАГ ===
|
||||
// Вызываем аналитику ПЕРЕД сохранением
|
||||
Analytics analyticsData = analyticsService.analyzeText(rssItem.getText());
|
||||
marketItem.setAnalytics(analyticsData);
|
||||
// ==================
|
||||
|
||||
// Сохраняем уже обогащенный объект
|
||||
repository.save(marketItem);
|
||||
}
|
||||
// ... вернуть результат ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3. Промпты для Ollama
|
||||
|
||||
Используйте следующие промпты для каждой аналитической задачи внутри `OllamaAnalyticsService`:
|
||||
|
||||
- **Саммари:** `Напиши краткое саммари следующей новостной статьи на русском языке. Ответ должен содержать только саммари из 3-4 предложений, без лишних вступлений. Статья: [Текст статьи]`
|
||||
- **Теги:** `Извлеки 5-7 ключевых слов или тегов из текста новостной статьи. В ответе дай только список тегов через запятую, без нумерации и заголовков. Статья: [Текст статьи]`
|
||||
- **Тональность:** `Определи тональность текста новостной статьи. В ответе дай только одно слово латиницей: positive, negative или neutral. Статья: [Текст статьи]`
|
||||
- **Сущности (JSON):** `Извлеки из текста имена людей, названия компаний и географические локации. В ответе дай только JSON объект следующей структуры: {"persons": [], "companies": [], "locations": []}. Статья: [Текст статьи]`
|
||||
|
||||
---
|
||||
|
||||
## Критерии выполнения
|
||||
|
||||
- Новый сервис `OllamaAnalyticsService` создан внутри проекта `parser-service`.
|
||||
- Существующие парсеры (`KursivParserService` и др.) модифицированы для вызова `OllamaAnalyticsService` перед сохранением данных.
|
||||
- Все новые записи, сохраняемые в MongoDB, **сразу содержат** заполненное поле `analytics`.
|
||||
- Процесс парсинга теперь может занимать больше времени, это ожидаемое поведение.
|
||||
- Реализована базовая обработка ошибок (например, если Ollama недоступен, поле `analytics` остается пустым, но парсинг не прерывается).
|
||||
@@ -0,0 +1,114 @@
|
||||
### Техническое задание: Реализация истории и загрузки отчётов
|
||||
|
||||
**Задача:** Модифицировать `parser-service`, добавив функционал для сохранения, просмотра и скачивания истории сгенерированных отчётов.
|
||||
|
||||
**Контекст:** Текущая реализация позволяет только генерировать отчёты. Необходимо создать механизм для их сохранения и последующего доступа к ним, чтобы пользователи могли скачивать ранее созданные документы.
|
||||
|
||||
---
|
||||
|
||||
### Шаг 1: Реализация сохранения отчётов
|
||||
|
||||
#### 1.1. Модель данных в MongoDB
|
||||
|
||||
Создайте новую коллекцию в MongoDB для хранения метаданных об отчётах, например, `report_history`.
|
||||
|
||||
**Структура документа `ReportHistory`:**
|
||||
|
||||
```json
|
||||
{
|
||||
"_id": "...", // ObjectId
|
||||
"reportTitle": "Еженедельный анализ новостного фона",
|
||||
"authorName": "Имя Аналитика",
|
||||
"companyName": "Название Компании Клиента",
|
||||
"startDate": "2025-09-10T00:00:00",
|
||||
"endDate": "2025-09-17T23:59:59",
|
||||
"format": "PDF",
|
||||
"filename": "report_2025-09-17-12345.pdf", // Уникальное имя файла
|
||||
"filePath": "/data/reports/report_2025-09-17-12345.pdf", // Путь на диске сервера
|
||||
"fileSize": 1234567, // Размер в байтах
|
||||
"createdAt": "2025-09-17T21:11:58" // Дата и время генерации
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.2. Хранение файлов
|
||||
|
||||
Сгенерированные отчёты (PDF/DOCX) должны сохраняться на файловой системе сервера в специальной директории. Путь к этой директории должен быть настраиваемым через `application.properties` (например, `reports.storage.path=/data/reports`).
|
||||
|
||||
#### 1.3. Модификация `ReportGenerationService`
|
||||
|
||||
Необходимо изменить существующий сервис `reportGenerationService`. После успешной генерации файла (`ReportBinary`) он должен:
|
||||
|
||||
1. Сохранить бинарные данные файла на диск по указанному пути.
|
||||
2. Создать запись с метаданными (включая путь к файлу) в новой коллекции `report_history` в MongoDB.
|
||||
|
||||
---
|
||||
|
||||
### Шаг 2: Создание новых эндпоинтов в `ReportController`
|
||||
|
||||
#### 2.1. Получение списка отчётов из истории
|
||||
|
||||
**Эндпоинт:** `GET /api/parser/report/history`
|
||||
|
||||
**Описание:** Возвращает пагинированный список метаданных всех ранее сгенерированных отчётов, отсортированных по дате создания (сначала новые).
|
||||
|
||||
**Параметры:**
|
||||
|
||||
- `page` (optional, default: 0) - номер страницы.
|
||||
- `size` (optional, default: 20) - количество элементов на странице.
|
||||
- `sort` (optional, default: "createdAt,desc") - поле и направление сортировки.
|
||||
|
||||
**Успешный ответ (200 OK):**
|
||||
|
||||
```json
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"id": "...",
|
||||
"reportTitle": "Еженедельный анализ",
|
||||
"authorName": "Имя Аналитика",
|
||||
"companyName": "Компания",
|
||||
"format": "PDF",
|
||||
"filename": "report_2025-09-17.pdf",
|
||||
"fileSize": 1234567,
|
||||
"createdAt": "2025-09-17T21:11:58"
|
||||
}
|
||||
// ... другие записи
|
||||
],
|
||||
"pageable": { ... },
|
||||
"totalElements": 5,
|
||||
"totalPages": 1
|
||||
// ... стандартная структура Page из Spring Data
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2. Скачивание конкретного отчёта из истории
|
||||
|
||||
**Эндпоинт:** `GET /api/parser/report/history/{id}`
|
||||
|
||||
**Описание:** Позволяет скачать конкретный файл отчёта по его уникальному ID из коллекции `report_history`.
|
||||
|
||||
**Параметры:**
|
||||
|
||||
- `id` (required, path variable) - ID записи об отчёте в MongoDB.
|
||||
|
||||
**Логика работы:**
|
||||
|
||||
1. Найти запись в `report_history` по `id`.
|
||||
2. Если запись не найдена, вернуть `404 Not Found`.
|
||||
3. Из записи получить путь к файлу (`filePath`).
|
||||
4. Прочитать файл с диска по этому пути.
|
||||
5. Вернуть бинарные данные файла с соответствующими заголовками (`Content-Disposition`, `Content-Type`).
|
||||
|
||||
**Успешный ответ:**
|
||||
|
||||
- **HTTP Статус:** `200 OK`
|
||||
- **Headers:** `Content-Disposition: attachment; filename="report_2025-09-17.pdf"`
|
||||
- **Body:** Бинарные данные файла.
|
||||
|
||||
---
|
||||
|
||||
### Критерии выполнения
|
||||
|
||||
- Существующий эндпоинт `POST /generate` теперь сохраняет сгенерированный отчёт на диск и его метаданные в MongoDB.
|
||||
- Новый эндпоинт `GET /history` успешно возвращает пагинированный список сохранённых отчётов.
|
||||
- Новый эндпоинт `GET /history/{id}` успешно находит и отдаёт на скачивание ранее сгенерированный файл.
|
||||
@@ -0,0 +1,202 @@
|
||||
### Техническое задание для AI-агента: Рефакторинг `ParserController`
|
||||
|
||||
**Задача:** Провести рефакторинг `ParserController` и связанных сервисов, чтобы сделать код более чистым, масштабируемым и простым в поддержке.
|
||||
|
||||
**Проблемы текущей реализации:**
|
||||
|
||||
1. **Дублирование кода:** Методы `parse...RssFeed()` практически идентичны.
|
||||
2. **Нарушение SRP:** Контроллер отвечает за запуск парсеров, получение данных, статистику и проверку состояния.
|
||||
3. **Низкая масштабируемость:** Добавление нового парсера требует изменения контроллера в 3-4 местах (добавление зависимости, нового эндпоинта, обновление метода `parseAll`, обновление `scheduler/info`).
|
||||
|
||||
---
|
||||
|
||||
### План рефакторинга
|
||||
|
||||
#### Шаг 1: Создание общего интерфейса `ParserService` (Паттерн "Стратегия")
|
||||
|
||||
Создайте общий интерфейс, который будут реализовывать все парсеры. Это позволит нам работать с ними единообразно.
|
||||
|
||||
```java
|
||||
public interface ParserService {
|
||||
/**
|
||||
* Возвращает уникальное имя источника (например, "kursiv", "kapital").
|
||||
* @return String source name
|
||||
*/
|
||||
String getSourceName();
|
||||
|
||||
/**
|
||||
* Запускает парсинг и сохранение данных для своего источника.
|
||||
* @return List of newly saved MarketItem
|
||||
*/
|
||||
List<MarketItem> parseAndSaveRssFeed();
|
||||
}
|
||||
```
|
||||
|
||||
#### Шаг 2: Модификация существующих сервисов
|
||||
|
||||
Каждый из ваших сервисов (`KursivParserService`, `KapitalParserService` и т.д.) должен реализовать этот интерфейс.
|
||||
|
||||
**Пример для `KursivParserService`:**
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class KursivParserService implements ParserService {
|
||||
|
||||
@Override
|
||||
public String getSourceName() {
|
||||
return "kursiv"; // Уникальное имя в нижнем регистре
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MarketItem> parseAndSaveRssFeed() {
|
||||
// ... существующая логика парсинга для Kursiv ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
_Проделайте это для всех 5 парсер-сервисов._
|
||||
|
||||
#### Шаг 3: Создание `ParserManagerService` (Паттерн "Фасад" / Service Locator)
|
||||
|
||||
Создайте новый сервис, который будет управлять всеми парсерами. Spring Boot автоматически соберет все бины, реализующие `ParserService`, в один список.
|
||||
|
||||
```java
|
||||
import org.springframework.stereotype.Service;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
|
||||
|
||||
@Service
|
||||
public class ParserManagerService {
|
||||
|
||||
private final Map<String, ParserService> parsers;
|
||||
|
||||
// Spring автоматически инжектирует все бины типа ParserService
|
||||
public ParserManagerService(List<ParserService> parserServices) {
|
||||
this.parsers = parserServices.stream()
|
||||
.collect(Collectors.toMap(ParserService::getSourceName, service -> service));
|
||||
}
|
||||
|
||||
/**
|
||||
* Запускает парсер по его имени.
|
||||
* @param sourceName Имя источника (например, "kursiv")
|
||||
* @return Результат парсинга
|
||||
*/
|
||||
public List<MarketItem> runParser(String sourceName) {
|
||||
ParserService parser = Optional.ofNullable(parsers.get(sourceName))
|
||||
.orElseThrow(() -> new IllegalArgumentException("Парсер не найден: " + sourceName));
|
||||
return parser.parseAndSaveRssFeed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Асинхронно запускает все парсеры.
|
||||
* @return Список результатов для каждого парсера
|
||||
*/
|
||||
@Async // Для параллельного выполнения
|
||||
public CompletableFuture<List<ParserResultDto>> runAllParsers() {
|
||||
long totalItemsBefore = marketItemService.getTotalItemsCount(); // Предполагая, что у вас есть доступ к этому сервису
|
||||
List<ParserResultDto> results = parsers.values().parallelStream()
|
||||
.map(parser -> {
|
||||
List<MarketItem> savedItems = parser.parseAndSaveRssFeed();
|
||||
return new ParserResultDto(parser.getSourceName(), savedItems.size(), totalItemsBefore + savedItems.size(), "completed");
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
return CompletableFuture.completedFuture(results);
|
||||
}
|
||||
|
||||
public List<String> getAvailableParsers() {
|
||||
return parsers.keySet().stream().sorted().collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
_Не забудьте добавить `@EnableAsync` в главный класс вашего приложения._
|
||||
|
||||
#### Шаг 4: Разделение `ParserController` на несколько маленьких
|
||||
|
||||
Разделите один большой контроллер на три, каждый со своей зоной ответственности:
|
||||
|
||||
1. **`MarketItemController`** — для публичных запросов на получение данных.
|
||||
2. **`ParserAdminController`** — для административных действий (запуск парсеров).
|
||||
3. **`HealthCheckController`** — для эндпоинтов мониторинга.
|
||||
|
||||
#### Шаг 5: Реализация новых контроллеров
|
||||
|
||||
**1. `MarketItemController.java`**
|
||||
(Содержит эндпоинты, которые нужны фронтенду для отображения данных)
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api/parser/items")
|
||||
public class MarketItemController {
|
||||
@Autowired private MarketItemService marketItemService;
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<ApiResponse<Page<MarketItem>>> getItems(...) { ... }
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<ApiResponse<MarketItem>> getItemById(@PathVariable String id) { ... }
|
||||
|
||||
@GetMapping("/stats")
|
||||
public ResponseEntity<ApiResponse<ParserStatsDto>> getStats() { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**2. `ParserAdminController.java`**
|
||||
(Содержит эндпоинты для управления парсерами, возможно, их стоит защитить в будущем)
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api/parser/admin/parsers")
|
||||
public class ParserAdminController {
|
||||
@Autowired private ParserManagerService parserManagerService;
|
||||
@Autowired private MarketItemService marketItemService; // для подсчета totalItems
|
||||
|
||||
// Один динамический эндпоинт вместо пяти
|
||||
@PostMapping("/parse/{sourceName}")
|
||||
public ResponseEntity<ApiResponse<ParserResultDto>> parseSource(@PathVariable String sourceName) {
|
||||
try {
|
||||
List<MarketItem> savedItems = parserManagerService.runParser(sourceName);
|
||||
long totalItems = marketItemService.getTotalItemsCount();
|
||||
ParserResultDto result = new ParserResultDto(sourceName, savedItems.size(), totalItems, "completed");
|
||||
return ResponseEntity.ok(ApiResponse.success("Парсинг " + sourceName + " завершен", result));
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.internalServerError().body(ApiResponse.error(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
// Упрощенный и асинхронный метод
|
||||
@PostMapping("/parse/all")
|
||||
public ResponseEntity<ApiResponse<List<ParserResultDto>>> parseAll() {
|
||||
try {
|
||||
List<ParserResultDto> results = parserManagerService.runAllParsers().get(); // .get() для ожидания результата
|
||||
return ResponseEntity.ok(ApiResponse.success("Парсинг всех источников запущен", results));
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.internalServerError().body(ApiResponse.error(e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**3. `HealthCheckController.java`**
|
||||
(Содержит все `/health` и информационные эндпоинты)
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api/parser/health")
|
||||
public class HealthCheckController {
|
||||
// ... методы healthCheck(), checkMongoConnection(), getSchedulerInfo() ...
|
||||
// Метод getSchedulerInfo можно улучшить, получая список парсеров из ParserManagerService
|
||||
}
|
||||
```
|
||||
|
||||
### Преимущества нового подхода
|
||||
|
||||
- **DRY (Don't Repeat Yourself):** Убрано дублирование кода в эндпоинтах.
|
||||
- **SRP (Single Responsibility Principle):** Каждый контроллер отвечает за свою область.
|
||||
- **Масштабируемость:** Чтобы добавить новый парсер, достаточно создать новый сервис, реализующий `ParserService`. **Контроллеры менять не нужно.**
|
||||
- **Эффективность:** Запуск всех парсеров теперь может выполняться параллельно.
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
### **Техническое задание на доработку AI-агента для генерации PDF-отчётов**
|
||||
|
||||
**Дата:** 5 октября 2025 г.
|
||||
**Проект:** Модификация существующего бэкенда для интеграции с сервисом `deep-research`.
|
||||
|
||||
#### **1. Общее описание**
|
||||
|
||||
Целью данной доработки является интеграция внешнего AI-сервиса `deep-research` (далее "Сервис Исследований") в существующий бэкенд. Необходимо модифицировать эндпоинт `/api/parser/report` таким образом, чтобы он принимал от пользователя тему для исследования, передавал её Сервису Исследований, получал в ответ сгенерированный текстовый отчёт и преобразовывал его в готовый для скачивания PDF-файл.
|
||||
|
||||
#### **2. Требования к реализации**
|
||||
|
||||
**2.1. Модификация эндпоинта `/api/parser/report`**
|
||||
|
||||
- **Метод:** `POST`
|
||||
- **URL:** `/api/parser/report`
|
||||
- **Тело запроса (`Request Body`):**
|
||||
|
||||
- Формат: `application/json`
|
||||
- Поля:
|
||||
- `query` (string, обязательное) - Тема для исследования.
|
||||
- `lang` (string, опциональное, по умолчанию "ru") - Язык для итогового отчёта (например, "ru", "en").
|
||||
- `depth` (integer, опциональное, по умолчанию 3) - Глубина исследования (целое число от 1 до 5).
|
||||
- `breadth` (integer, опциональное, по умолчанию 5) - Широта исследования (целое число от 2 до 10).
|
||||
- `report_type` (string, опциональное, по умолчанию "report") - Тип отчёта ("report" для полного отчёта, "answer" для краткого ответа).
|
||||
|
||||
- **Успешный ответ (`Success Response`):**
|
||||
|
||||
- **Код:** `200 OK`
|
||||
- **Заголовки:**
|
||||
- `Content-Type: application/pdf`
|
||||
- `Content-Disposition: attachment; filename="research_report.pdf"` (имя файла можно генерировать динамически).
|
||||
- **Тело ответа:** Сгенерированный PDF-файл.
|
||||
|
||||
- **Ответы с ошибками (`Error Responses`):**
|
||||
- **Код:** `400 Bad Request` - Если в запросе отсутствуют обязательные поля или их формат некорректен.
|
||||
- **Код:** `500 Internal Server Error` - Если произошла внутренняя ошибка на бэкенде или при вызове Сервиса Исследований.
|
||||
- **Код:** `504 Gateway Timeout` - Если Сервис Исследований не отвечает в течение заданного времени.
|
||||
|
||||
**2.2. Логика работы агента**
|
||||
|
||||
Процесс должен следовать по шагам:
|
||||
|
||||
1. **Приём запроса:** Эндпоинт `/api/parser/report` получает `POST` запрос с параметрами от фронтенда.
|
||||
2. **Валидация:** Бэкенд проверяет корректность полученных данных (например, что `query` не пустое, а `depth` — число в нужном диапазоне).
|
||||
3. **Вызов Сервиса Исследований:**
|
||||
- Бэкенд формирует и отправляет `POST` запрос на эндпоинт Сервиса Исследований, используя HTTP-клиент (например, `axios` для Node.js или `requests` для Python).
|
||||
- **Целевой URL:** `http://185.35.223.45:3051/api/research`
|
||||
- **Тело запроса:** JSON-объект, сформированный из параметров, полученных на шаге 1.
|
||||
- Все API-ключи (`OPENAI_API_KEY`, `FIRECRAWL_API_KEY`) должны быть безопасно сохранены в переменных окружения бэкенда и использоваться Сервисом Исследований.
|
||||
4. **Обработка ответа:**
|
||||
- Бэкенд ожидает ответ от Сервиса Исследований в формате JSON.
|
||||
- Из ответа извлекается сгенерированный полный текст отчёта (предположительно, из поля `report` или `answer`), а также список использованных URL (`visitedUrls`).
|
||||
- В случае ошибки от Сервиса Исследований, ошибка логируется, и клиенту возвращается ответ с кодом `500`.
|
||||
5. **Генерация PDF:**
|
||||
- Полученный текст отчёта передаётся в модуль генерации PDF.
|
||||
- Используется специализированная библиотека (например, `pdfkit` или `puppeteer` для Node.js; `ReportLab` для Python).
|
||||
- PDF-файл формируется в соответствии с требованиями к форматированию (см. п. 2.3).
|
||||
6. **Отправка PDF клиенту:** Бэкенд отправляет сгенерированный PDF-файл в теле ответа с соответствующими заголовками.
|
||||
|
||||
**2.3. Требования к PDF-отчёту**
|
||||
|
||||
Сгенерированный PDF-документ должен иметь профессиональный вид и чёткую структуру:
|
||||
|
||||
- **Титульная страница:** Название исследования (из поля `query`).
|
||||
- **Содержание (Table of Contents):** Автоматически сгенерированное оглавление на основе заголовков в отчёте.
|
||||
- **Тело отчёта:** Основной текст, полученный от Сервиса Исследований, с сохранением форматирования (заголовки, абзацы, списки).
|
||||
- **Список источников:** В конце документа должен быть раздел "Источники" или "Библиография", содержащий список URL-адресов из поля `visitedUrls`.
|
||||
|
||||
#### **3. Технологический стек**
|
||||
|
||||
- **Бэкенд:** Реализация должна использовать текущий стек бэкенда (например, Node.js/Express, Python/Django/FastAPI и т.д.).
|
||||
- **HTTP-клиент:** Для взаимодействия с Сервисом Исследований (например, `axios`).
|
||||
- **Библиотека для генерации PDF:** На выбор разработчика, в зависимости от стека (например, `pdfkit`, `puppeteer`, `ReportLab`).
|
||||
|
||||
#### **4. Нефункциональные требования**
|
||||
|
||||
- **Асинхронность:** Процесс исследования может занимать несколько минут. Необходимо реализовать асинхронную обработку, чтобы избежать таймаута HTTP-запроса от клиента.
|
||||
- **Рекомендация:** При получении запроса бэкенд может сразу возвращать `202 Accepted` с ID задачи. Фронтенд будет периодически опрашивать другой эндпоинт (`/api/parser/report/status/{id}`), чтобы проверить готовность отчёта и получить ссылку на скачивание. _(На первом этапе можно реализовать как долго выполняющийся запрос, но асинхронная модель является предпочтительной для будущего развития.)_
|
||||
|
||||
#### **5. Что не входит в задачи**
|
||||
|
||||
- Разработка фронтенд-части для взаимодействия с API.
|
||||
- Реализация аутентификации и авторизации пользователей.
|
||||
- Создание системы очередей для обработки нескольких запросов одновременно (может быть добавлено на следующем этапе).
|
||||
@@ -0,0 +1,107 @@
|
||||
# Configuration
|
||||
|
||||
All configuration lives in `src/main/resources/application.properties`. Values fall
|
||||
into two groups:
|
||||
|
||||
- **Non-secret defaults** — model names, timeouts, retry policy, RSS URLs, log levels.
|
||||
These are literal values in the properties file and are safe to read in a review.
|
||||
- **Secrets and environment-specific endpoints** — resolved from environment variables
|
||||
via `${VAR}` placeholders.
|
||||
|
||||
[`.env.example`](../.env.example) is the authoritative list of every variable, marked
|
||||
`REQUIRED` or `OPTIONAL`.
|
||||
|
||||
## Secret handling
|
||||
|
||||
**No credential is ever committed.** Required secrets are declared *without* a
|
||||
fallback:
|
||||
|
||||
```properties
|
||||
openai.api.key=${OPENAI_API_KEY}
|
||||
```
|
||||
|
||||
If the variable is absent, Spring throws
|
||||
`IllegalArgumentException: Could not resolve placeholder 'OPENAI_API_KEY'` and the
|
||||
application refuses to start.
|
||||
|
||||
This is intentional. The alternative — an empty default — produces a service that
|
||||
boots happily and then fails every downstream call with an opaque `401`, usually in
|
||||
production, usually at 3am. A startup failure names the missing variable immediately.
|
||||
|
||||
Optional values keep a default after the colon:
|
||||
|
||||
```properties
|
||||
minio.bucket-name=${MINIO_BUCKET_NAME:konturai}
|
||||
```
|
||||
|
||||
### Adding a new configuration value
|
||||
|
||||
1. If it is a credential, endpoint, or anything that differs per environment, use a
|
||||
`${VAR}` placeholder — no default for required secrets.
|
||||
2. Add it to `.env.example` with a `REQUIRED`/`OPTIONAL` marker and a one-line comment.
|
||||
3. Add it to `.env.production` on the server before merging.
|
||||
|
||||
Never write a real value into `application.properties`, a test's
|
||||
`@TestPropertySource`, a shell script, or documentation.
|
||||
|
||||
## The Google service-account key
|
||||
|
||||
`NanoBananaImageGenerationService` and `GeminiVideoGenerationService` authenticate to
|
||||
Vertex AI by loading `keys/google-key.json` from the classpath:
|
||||
|
||||
```java
|
||||
ClassPathResource resource = new ClassPathResource("keys/google-key.json");
|
||||
```
|
||||
|
||||
The file must exist at `src/main/resources/keys/google-key.json` at **build** time, so
|
||||
it is baked into the jar. It is gitignored and must be supplied out of band:
|
||||
|
||||
```bash
|
||||
cp /secure/path/google-key.json src/main/resources/keys/google-key.json
|
||||
```
|
||||
|
||||
On the deployment host the file must be present in the checkout before the Docker
|
||||
image is built, since the `Dockerfile` uses `COPY . .`.
|
||||
|
||||
If the file is missing the application still starts — image and video generation log
|
||||
an error and return `null`, while every other feature keeps working.
|
||||
|
||||
## Local development
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
$EDITOR .env
|
||||
set -a && source .env && set +a
|
||||
./mvnw spring-boot:run
|
||||
```
|
||||
|
||||
For a fully local setup you need MongoDB on `localhost:27017`. Point `MINIO_ENDPOINT`
|
||||
at a local MinIO container, or leave the storage-backed endpoints unused.
|
||||
|
||||
An alternative to `.env` is `src/main/resources/application-local.properties` (also
|
||||
gitignored), activated with `--spring.profiles.active=local`.
|
||||
|
||||
## Configuration reference by area
|
||||
|
||||
| Area | Prefix | Notes |
|
||||
| --- | --- | --- |
|
||||
| MongoDB | `spring.data.mongodb.*` | 30s connect/socket/server-selection timeouts |
|
||||
| Object storage | `minio.*` | Stores generated reports and images |
|
||||
| Security | `security.jwt.*`, `encryption.*` | JWT secret must match the issuing auth service |
|
||||
| OpenAI | `openai.*` | `gpt-4o` for text, `gpt-4o-mini` for cheaper calls; retry with exponential backoff, max 3 concurrent requests |
|
||||
| Ollama | `ollama.*` | Self-hosted; very long timeouts (5h) for large generations |
|
||||
| Vertex AI | `google.*` | Imagen 3 for images, Veo 3 for video; 20s rate-limit spacing |
|
||||
| Serper | `serper.*` | Google search results for research reports |
|
||||
| Facebook | `facebook.*` | Posting, plus the hot-leads collector and call-centre sync |
|
||||
| RSS | `rss.*` | Five sources — see [rss-parsers.md](rss-parsers.md) |
|
||||
| Schedulers | `parser.scheduler.enabled`, `posting.scheduler.enabled` | Parser scheduler is **off** by default |
|
||||
| CORS | `cors.*` | Bound by `CorsProperties`, applied in `WebCorsConfig` |
|
||||
|
||||
## Rotating a credential
|
||||
|
||||
1. Issue the new credential with the provider.
|
||||
2. Update `.env.production` on the deployment host.
|
||||
3. Restart the container (`docker compose up -d` — see [deployment.md](deployment.md)).
|
||||
4. Revoke the old credential.
|
||||
|
||||
Because secrets are never baked into the image, rotation requires no rebuild.
|
||||
@@ -0,0 +1,99 @@
|
||||
# Deployment
|
||||
|
||||
Production runs as a Docker container named `konturai-parser-app`, deployed from
|
||||
`main` by **Gitea Actions** on a self-hosted runner.
|
||||
|
||||
> The project was migrated from GitLab CI to Gitea in August 2026. The GitLab pipeline
|
||||
> and its runner scripts have been removed; the historical description is preserved in
|
||||
> [archive/setup-notes/DEPLOYMENT.md](archive/setup-notes/DEPLOYMENT.md).
|
||||
|
||||
## Pipeline
|
||||
|
||||
[`.gitea/workflows/deploy.yml`](../.gitea/workflows/deploy.yml) triggers on every push
|
||||
to `main` and delegates to a host script:
|
||||
|
||||
```yaml
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Deploy
|
||||
run: /usr/local/bin/konturai-deploy.sh marketing-parser
|
||||
```
|
||||
|
||||
`konturai-deploy.sh` lives on the deployment host, not in this repository. It is
|
||||
shared across KonturAI services and is responsible for syncing the checkout, building
|
||||
the image and recreating the container.
|
||||
|
||||
Note that this workflow does **not** run the test suite — it deploys directly. Run
|
||||
`./mvnw clean verify` locally before pushing to `main`.
|
||||
|
||||
## Container
|
||||
|
||||
[`Dockerfile`](../Dockerfile) is a two-stage build:
|
||||
|
||||
1. `maven:3.9.9-eclipse-temurin-21` resolves dependencies and runs
|
||||
`mvn clean install -DskipTests`.
|
||||
2. `eclipse-temurin:21-jre-jammy` receives the jar and runs it as the non-root
|
||||
`appuser`, with `-XX:+HeapDumpOnOutOfMemoryError` writing to `/dumps`.
|
||||
|
||||
Because stage 1 does `COPY . .`, anything required at build time must be present in
|
||||
the checkout on the host — including
|
||||
`src/main/resources/keys/google-key.json`, which is not in version control. See
|
||||
[configuration.md](configuration.md#the-google-service-account-key).
|
||||
|
||||
## Compose
|
||||
|
||||
[`deployment/docker-compose.server.yml`](../deployment/docker-compose.server.yml)
|
||||
defines the production service:
|
||||
|
||||
- Reads all configuration from `../.env.production` (**not** in version control).
|
||||
- Joins the pre-existing external Docker network `common_network` under the alias
|
||||
`parser-service`, so sibling KonturAI services can reach it by name.
|
||||
- `restart: unless-stopped`.
|
||||
- Maps `host.docker.internal` to the host gateway.
|
||||
|
||||
No ports are published to the host — traffic arrives through the shared network.
|
||||
|
||||
## Server-side environment
|
||||
|
||||
`.env.production` sits next to the checkout on the deployment host and supplies every
|
||||
variable listed in [`.env.example`](../.env.example). **The application will not start
|
||||
if a required variable is missing** — this is intentional, see
|
||||
[configuration.md](configuration.md#secret-handling).
|
||||
|
||||
Manual operations on the host:
|
||||
|
||||
```bash
|
||||
cd <deploy-dir>/deployment
|
||||
|
||||
docker compose ps # status
|
||||
docker compose logs -f parser-service # follow logs
|
||||
docker compose up -d --build # rebuild and recreate
|
||||
docker compose restart parser-service # pick up .env.production changes
|
||||
```
|
||||
|
||||
## Verifying a deploy
|
||||
|
||||
```bash
|
||||
docker compose ps # container is Up
|
||||
docker compose logs --tail=100 parser-service # no placeholder-resolution errors
|
||||
curl -fsS http://parser-service:8080/api/parser/health # from inside common_network
|
||||
```
|
||||
|
||||
A failure to resolve a `${VAR}` placeholder appears immediately in the logs and names
|
||||
the missing variable.
|
||||
|
||||
## Rollback
|
||||
|
||||
The deploy script builds from the synced checkout, so rolling back means checking out
|
||||
the previous commit on the host and rebuilding:
|
||||
|
||||
```bash
|
||||
cd <deploy-dir>
|
||||
git checkout <previous-good-sha>
|
||||
cd deployment && docker compose up -d --build
|
||||
```
|
||||
@@ -0,0 +1,133 @@
|
||||
# Development
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **JDK 21** (Temurin recommended)
|
||||
- **MongoDB** reachable at `MONGODB_HOST:MONGODB_PORT`
|
||||
- Maven is not required — use the bundled wrapper (`./mvnw`)
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
$EDITOR .env # fill in REQUIRED values
|
||||
set -a && source .env && set +a
|
||||
./mvnw spring-boot:run
|
||||
```
|
||||
|
||||
`http://localhost:8080/swagger-ui.html` gives you an interactive client; use the
|
||||
**Authorize** button to supply a JWT.
|
||||
|
||||
## Common commands
|
||||
|
||||
```bash
|
||||
./mvnw clean verify # unit tests + coverage gate — run before pushing
|
||||
./mvnw test # unit tests only
|
||||
./mvnw clean package # build the jar
|
||||
./mvnw spring-boot:run # run locally
|
||||
./mvnw test -Dtest=JwtServiceTest # a single test class
|
||||
```
|
||||
|
||||
Helper scripts in [`scripts/`](../scripts):
|
||||
|
||||
| Script | Purpose |
|
||||
| --- | --- |
|
||||
| `run-parser.sh` | Preflight checks then start the application |
|
||||
| `smoke-mongodb.sh` | Verify MongoDB reachability and print a connection string |
|
||||
| `smoke-openai-api.sh` | Verify `OPENAI_API_KEY` works against the OpenAI API |
|
||||
| `smoke-research-api.sh` | Exercise the report endpoints against a running instance |
|
||||
|
||||
## Testing
|
||||
|
||||
Two tiers, separated by an environment variable:
|
||||
|
||||
**Unit tests** run by default. They mock collaborators and never touch the network.
|
||||
This is what CI and `./mvnw verify` execute.
|
||||
|
||||
**Integration tests** boot the full Spring context and need real MongoDB plus valid
|
||||
credentials. They are annotated:
|
||||
|
||||
```java
|
||||
@SpringBootTest
|
||||
@EnabledIfEnvironmentVariable(named = "RUN_INTEGRATION_TESTS", matches = "true")
|
||||
```
|
||||
|
||||
so they are skipped unless you opt in:
|
||||
|
||||
```bash
|
||||
RUN_INTEGRATION_TESTS=true ./mvnw verify
|
||||
```
|
||||
|
||||
Surefire additionally excludes the `integration` JUnit tag (see `pom.xml`).
|
||||
|
||||
### Coverage gate
|
||||
|
||||
`verify` fails if line coverage drops below **80%** on any of:
|
||||
|
||||
- `PostingTaskService`
|
||||
- `JwtService`
|
||||
- `SocialMediaCredentialsService`
|
||||
|
||||
Report: `target/site/jacoco/index.html`. To extend the gate to another class, add it to
|
||||
the JaCoCo `qg01-coverage-check` rule in `pom.xml`.
|
||||
|
||||
### Test credentials
|
||||
|
||||
Tests must never contain real credentials. Use placeholders that resolve from the
|
||||
environment:
|
||||
|
||||
```java
|
||||
@TestPropertySource(properties = {
|
||||
"spring.data.mongodb.host=${MONGODB_HOST:localhost}",
|
||||
"spring.data.mongodb.username=${MONGODB_USERNAME:}"
|
||||
})
|
||||
```
|
||||
|
||||
## Conventions
|
||||
|
||||
- **Java 21**, Lombok for boilerplate (excluded from the packaged jar).
|
||||
- Constructor injection, not field injection — see `ParserManagerService`.
|
||||
- Controllers stay thin: validate, delegate to a service, return a DTO. No business
|
||||
logic and no repository access from a controller.
|
||||
- Never return a `model` document directly from a controller — map it to a `dto`.
|
||||
- Throw domain exceptions from `exception/` and let `GlobalExceptionHandler` render
|
||||
them; do not build error responses by hand.
|
||||
- Long-running AI calls belong on an async executor (`AsyncConfig`), not the request
|
||||
thread.
|
||||
- Existing code mixes Russian and English comments. New comments should be in English.
|
||||
|
||||
## Adding an RSS source
|
||||
|
||||
`ParserManagerService` discovers parsers through Spring, so no registry needs editing:
|
||||
|
||||
1. Add `rss.<source>.url` to `application.properties`.
|
||||
2. Implement `ParserService` — return a unique `getSourceName()` and implement
|
||||
`parseAndSaveRssFeed()`. Model it on `KursivParserService`.
|
||||
3. Annotate the scheduled entry point with `@Scheduled`, matching the cadence of
|
||||
comparable sources (5 min for high-volume, 30 min otherwise).
|
||||
4. Add a unit test alongside `KursivParserServiceTest`.
|
||||
5. Add a log level line in `application.properties`.
|
||||
6. Document the source in [rss-parsers.md](rss-parsers.md).
|
||||
|
||||
## Adding an endpoint
|
||||
|
||||
1. Define request/response DTOs in `dto/` with Bean Validation annotations.
|
||||
2. Add the handler to the relevant controller; extract the caller with
|
||||
`jwtService.extractUserIdFromHeader(authHeader)`.
|
||||
3. Put the logic in a service; add a repository method if persistence is needed.
|
||||
4. Add unit tests for the service.
|
||||
5. Document it under [`docs/api/`](api/README.md) and update that index.
|
||||
|
||||
## Adding configuration
|
||||
|
||||
See [configuration.md](configuration.md#adding-a-new-configuration-value). In short:
|
||||
environment placeholder, no default for secrets, and update `.env.example`.
|
||||
|
||||
## Before you push
|
||||
|
||||
```bash
|
||||
./mvnw clean verify
|
||||
git status # nothing unexpected staged
|
||||
```
|
||||
|
||||
Pushing to `main` deploys to production — [deployment.md](deployment.md).
|
||||
@@ -0,0 +1,198 @@
|
||||
# Руководство по интеграции отправки отчетов по email
|
||||
|
||||
## Обзор
|
||||
|
||||
В систему добавлена функциональность автоматической отправки сгенерированных PDF отчетов на email пользователя. После генерации отчета в формате PDF, система автоматически отправит его на указанный email адрес.
|
||||
|
||||
## Настройка
|
||||
|
||||
### 1. Конфигурация email в application.properties
|
||||
|
||||
```properties
|
||||
# Email Configuration
|
||||
spring.mail.host=smtp.gmail.com
|
||||
spring.mail.port=587
|
||||
spring.mail.username=your-email@gmail.com
|
||||
spring.mail.password=your-app-password
|
||||
spring.mail.properties.mail.smtp.auth=true
|
||||
spring.mail.properties.mail.smtp.starttls.enable=true
|
||||
spring.mail.properties.mail.smtp.starttls.required=true
|
||||
```
|
||||
|
||||
**Важно:** Для Gmail необходимо использовать App Password вместо обычного пароля.
|
||||
|
||||
### 2. Настройка Gmail App Password
|
||||
|
||||
1. Включите двухфакторную аутентификацию в Google аккаунте
|
||||
2. Перейдите в настройки безопасности Google
|
||||
3. Создайте App Password для приложения
|
||||
4. Используйте этот пароль в конфигурации
|
||||
|
||||
## Использование
|
||||
|
||||
### API Endpoint
|
||||
|
||||
**POST** `/api/parser/report/generate`
|
||||
|
||||
### Пример запроса
|
||||
|
||||
```json
|
||||
{
|
||||
"reportTitle": "Аналитический отчет за декабрь 2024",
|
||||
"authorName": "Иван Петров",
|
||||
"companyName": "ООО Контурай",
|
||||
"startDate": "2024-12-01T00:00:00",
|
||||
"endDate": "2024-12-31T23:59:59",
|
||||
"format": "PDF",
|
||||
"recipientEmail": "user@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
### Параметры
|
||||
|
||||
- `reportTitle` - название отчета
|
||||
- `authorName` - имя автора отчета
|
||||
- `companyName` - название компании
|
||||
- `startDate` - дата начала периода (ISO 8601)
|
||||
- `endDate` - дата окончания периода (ISO 8601)
|
||||
- `format` - формат отчета ("PDF" или "DOCX")
|
||||
- `recipientEmail` - **НОВОЕ ПОЛЕ** email адрес для отправки отчета
|
||||
|
||||
### Условия отправки email
|
||||
|
||||
Email отправляется только при выполнении следующих условий:
|
||||
|
||||
1. Указан `recipientEmail` (не пустой)
|
||||
2. Формат отчета установлен как "PDF"
|
||||
3. Отчет успешно сгенерирован
|
||||
|
||||
### Формат письма
|
||||
|
||||
Письмо содержит:
|
||||
|
||||
- Красиво оформленный HTML текст с деталями отчета
|
||||
- Прикрепленный PDF файл с отчетом
|
||||
- Информацию об авторе, компании и дате создания
|
||||
|
||||
## Компоненты системы
|
||||
|
||||
### EmailService
|
||||
|
||||
Новый сервис для отправки email:
|
||||
|
||||
- `sendReportByEmail()` - основной метод отправки
|
||||
- Поддержка HTML формата писем
|
||||
- Прикрепление PDF файлов
|
||||
- Обработка ошибок
|
||||
|
||||
### ReportGenerationService
|
||||
|
||||
Обновлен для интеграции с EmailService:
|
||||
|
||||
- Автоматическая отправка PDF после генерации
|
||||
- Обработка ошибок отправки без прерывания процесса
|
||||
- Поддержка асинхронной генерации с email
|
||||
|
||||
### ReportGenerateRequest
|
||||
|
||||
Добавлено новое поле:
|
||||
|
||||
- `recipientEmail` - email для отправки отчета
|
||||
|
||||
## Тестирование
|
||||
|
||||
### Unit тесты
|
||||
|
||||
Создан `EmailServiceTest` с тестами:
|
||||
|
||||
- Успешная отправка email
|
||||
- Обработка null значений
|
||||
- Обработка ошибок SMTP
|
||||
|
||||
### Ручное тестирование
|
||||
|
||||
1. Настройте email конфигурацию
|
||||
2. Отправьте POST запрос с валидными данными
|
||||
3. Проверьте получение email с PDF вложением
|
||||
|
||||
## Безопасность
|
||||
|
||||
- Email отправляется только на указанный адрес
|
||||
- Используется TLS шифрование для SMTP
|
||||
- App Password для Gmail вместо обычного пароля
|
||||
- Обработка ошибок без раскрытия чувствительной информации
|
||||
|
||||
## Мониторинг
|
||||
|
||||
- Логирование успешных отправок
|
||||
- Логирование ошибок отправки
|
||||
- Не прерывает процесс генерации отчета при ошибках email
|
||||
|
||||
## Примеры использования
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/parser/report/generate \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"reportTitle": "Тестовый отчет",
|
||||
"authorName": "Тест Автор",
|
||||
"companyName": "Тест Компания",
|
||||
"startDate": "2024-12-01T00:00:00",
|
||||
"endDate": "2024-12-31T23:59:59",
|
||||
"format": "PDF",
|
||||
"recipientEmail": "test@example.com"
|
||||
}'
|
||||
```
|
||||
|
||||
### JavaScript (fetch)
|
||||
|
||||
```javascript
|
||||
const response = await fetch('/api/parser/report/generate', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
reportTitle: 'Аналитический отчет',
|
||||
authorName: 'Иван Петров',
|
||||
companyName: 'ООО Контурай',
|
||||
startDate: '2024-12-01T00:00:00',
|
||||
endDate: '2024-12-31T23:59:59',
|
||||
format: 'PDF',
|
||||
recipientEmail: 'user@example.com',
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
console.log(result);
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Проблемы с Gmail
|
||||
|
||||
1. **Ошибка аутентификации**: Убедитесь, что используете App Password
|
||||
2. **Блокировка аккаунта**: Проверьте настройки безопасности Gmail
|
||||
3. **Порт заблокирован**: Попробуйте порт 465 с SSL
|
||||
|
||||
### Общие проблемы
|
||||
|
||||
1. **Email не отправляется**: Проверьте логи приложения
|
||||
2. **PDF не прикрепляется**: Убедитесь, что формат "PDF"
|
||||
3. **Пустой recipientEmail**: Проверьте, что поле заполнено
|
||||
|
||||
## Логи
|
||||
|
||||
Успешная отправка:
|
||||
|
||||
```
|
||||
Email с отчетом успешно отправлен на: user@example.com
|
||||
```
|
||||
|
||||
Ошибка отправки:
|
||||
|
||||
```
|
||||
Ошибка при отправке email: [детали ошибки]
|
||||
```
|
||||
@@ -0,0 +1,340 @@
|
||||
# Учетные данные Facebook для запуска рекламы
|
||||
|
||||
## Обзор
|
||||
|
||||
Для запуска рекламных кампаний в Facebook через Marketing API (ранее Ads API) требуются специальные учетные данные, которые отличаются от простого Access Token для публикации постов.
|
||||
|
||||
---
|
||||
|
||||
## Необходимые учетные данные
|
||||
|
||||
### 1. **Access Token (обязательно)**
|
||||
|
||||
Access Token с расширенными разрешениями для управления рекламой.
|
||||
|
||||
**Требуемые разрешения (Permissions):**
|
||||
|
||||
- `ads_management` - Управление рекламными кампаниями
|
||||
- `ads_read` - Чтение данных о рекламе
|
||||
- `business_management` - Управление бизнес-аккаунтом
|
||||
- `pages_read_engagement` - Чтение данных страниц (опционально)
|
||||
|
||||
**Типы токенов:**
|
||||
|
||||
- **User Access Token** - краткосрочный (1-2 часа)
|
||||
- **Long-Lived User Access Token** - долгосрочный (60 дней)
|
||||
- **Page Access Token** - для управления страницами
|
||||
- **System User Access Token** - для серверных приложений (рекомендуется для продакшена)
|
||||
|
||||
---
|
||||
|
||||
### 2. **Ad Account ID (обязательно)**
|
||||
|
||||
ID рекламного аккаунта Facebook, в котором будут создаваться кампании.
|
||||
|
||||
**Формат:** `act_XXXXXXXXX` (например: `act_123456789`)
|
||||
|
||||
**Где найти:**
|
||||
|
||||
1. Перейдите в [Facebook Ads Manager](https://business.facebook.com/adsmanager)
|
||||
2. В настройках аккаунта найдите "Account ID"
|
||||
3. Или используйте API: `GET /me/adaccounts`
|
||||
|
||||
---
|
||||
|
||||
### 3. **App ID и App Secret (обязательно для серверных приложений)**
|
||||
|
||||
Учетные данные Facebook приложения.
|
||||
|
||||
**Где найти:**
|
||||
|
||||
1. Перейдите на [Facebook Developers](https://developers.facebook.com/)
|
||||
2. Выберите ваше приложение
|
||||
3. В разделе "Settings" → "Basic" найдите:
|
||||
- **App ID**
|
||||
- **App Secret** (нажмите "Show" для отображения)
|
||||
|
||||
**Важно:** App Secret должен храниться в безопасности и никогда не передаваться на клиент.
|
||||
|
||||
---
|
||||
|
||||
### 4. **Page ID (опционально, но рекомендуется)**
|
||||
|
||||
ID страницы Facebook, связанной с рекламным аккаунтом.
|
||||
|
||||
**Где найти:**
|
||||
|
||||
1. Перейдите на вашу страницу Facebook
|
||||
2. В настройках страницы найдите "Page ID"
|
||||
3. Или используйте API: `GET /me/accounts`
|
||||
|
||||
---
|
||||
|
||||
## Пошаговая инструкция получения учетных данных
|
||||
|
||||
### Шаг 1: Создание Facebook приложения
|
||||
|
||||
1. Перейдите на [Facebook Developers](https://developers.facebook.com/)
|
||||
2. Нажмите "My Apps" → "Create App"
|
||||
3. Выберите тип приложения: **"Business"** или **"Other"**
|
||||
4. Заполните название и контактный email
|
||||
5. Нажмите "Create App"
|
||||
|
||||
### Шаг 2: Добавление продукта "Marketing API"
|
||||
|
||||
1. В панели управления приложением найдите раздел "Add Products"
|
||||
2. Найдите "Marketing API" и нажмите "Set Up"
|
||||
3. Следуйте инструкциям для настройки
|
||||
|
||||
### Шаг 3: Получение App ID и App Secret
|
||||
|
||||
1. В левом меню выберите "Settings" → "Basic"
|
||||
2. Скопируйте **App ID**
|
||||
3. Нажмите "Show" рядом с **App Secret** и скопируйте его
|
||||
4. **Сохраните эти данные в безопасном месте**
|
||||
|
||||
### Шаг 4: Настройка разрешений (Permissions)
|
||||
|
||||
1. В левом меню выберите "Settings" → "Advanced"
|
||||
2. Добавьте в "Valid OAuth Redirect URIs" ваш callback URL
|
||||
3. В разделе "Permissions and Features" запросите:
|
||||
- `ads_management`
|
||||
- `ads_read`
|
||||
- `business_management`
|
||||
- `pages_read_engagement`
|
||||
|
||||
### Шаг 5: Получение Access Token
|
||||
|
||||
#### Вариант A: User Access Token (для тестирования)
|
||||
|
||||
1. Перейдите в [Graph API Explorer](https://developers.facebook.com/tools/explorer/)
|
||||
2. Выберите ваше приложение
|
||||
3. Нажмите "Generate Access Token"
|
||||
4. Выберите необходимые разрешения
|
||||
5. Скопируйте полученный токен
|
||||
|
||||
#### Вариант B: Long-Lived Token (для разработки)
|
||||
|
||||
```bash
|
||||
# Обмен краткосрочного токена на долгосрочный
|
||||
curl -X GET "https://graph.facebook.com/v18.0/oauth/access_token?grant_type=fb_exchange_token&client_id={app-id}&client_secret={app-secret}&fb_exchange_token={short-lived-token}"
|
||||
```
|
||||
|
||||
#### Вариант C: System User Token (для продакшена - рекомендуется)
|
||||
|
||||
1. В панели управления приложением перейдите в "Business Settings"
|
||||
2. Создайте System User
|
||||
3. Назначьте ему доступ к рекламному аккаунту
|
||||
4. Сгенерируйте токен для System User
|
||||
|
||||
### Шаг 6: Получение Ad Account ID
|
||||
|
||||
**Через Ads Manager:**
|
||||
|
||||
1. Перейдите в [Facebook Ads Manager](https://business.facebook.com/adsmanager)
|
||||
2. В настройках аккаунта найдите "Account ID"
|
||||
|
||||
**Через API:**
|
||||
|
||||
```bash
|
||||
curl -X GET "https://graph.facebook.com/v18.0/me/adaccounts?access_token={access-token}"
|
||||
```
|
||||
|
||||
Ответ будет содержать массив с `id` в формате `act_XXXXXXXXX`.
|
||||
|
||||
---
|
||||
|
||||
## Структура учетных данных для вашего API
|
||||
|
||||
Для интеграции с вашей системой, учетные данные Facebook для рекламы должны быть сохранены в следующем формате:
|
||||
|
||||
### Формат JSON для сохранения credentials
|
||||
|
||||
```json
|
||||
{
|
||||
"platform": "facebook_ads",
|
||||
"credentials": {
|
||||
"accessToken": "EAABwzLix...",
|
||||
"adAccountId": "act_123456789",
|
||||
"appId": "1234567890123456",
|
||||
"appSecret": "your-app-secret-here",
|
||||
"pageId": "1234567890123456",
|
||||
"tokenType": "LONG_LIVED",
|
||||
"expiresAt": "2024-12-31T23:59:59Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Пример сохранения через API
|
||||
|
||||
```javascript
|
||||
const facebookAdsCredentials = {
|
||||
platform: 'facebook_ads',
|
||||
credentials: JSON.stringify({
|
||||
accessToken: 'EAABwzLix...',
|
||||
adAccountId: 'act_123456789',
|
||||
appId: '1234567890123456',
|
||||
appSecret: 'your-app-secret-here',
|
||||
pageId: '1234567890123456',
|
||||
tokenType: 'LONG_LIVED',
|
||||
expiresAt: '2024-12-31T23:59:59Z',
|
||||
}),
|
||||
};
|
||||
|
||||
// Сохранение через ваш API
|
||||
const response = await fetch('/api/social-media/credentials', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwtToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(facebookAdsCredentials),
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Требования и ограничения
|
||||
|
||||
### Ограничения Facebook Marketing API
|
||||
|
||||
1. **Rate Limits:**
|
||||
|
||||
- 200 вызовов в час на пользователя
|
||||
- 4800 вызовов в час на приложение
|
||||
|
||||
2. **Минимальные требования:**
|
||||
|
||||
- Рекламный аккаунт должен быть активен
|
||||
- У пользователя должны быть права администратора на аккаунте
|
||||
- Приложение должно пройти ревью Facebook (для продакшена)
|
||||
|
||||
3. **Версия API:**
|
||||
- Текущая версия: v18.0
|
||||
- Facebook регулярно обновляет API, следите за изменениями
|
||||
|
||||
### Безопасность
|
||||
|
||||
1. **Никогда не храните App Secret в открытом виде**
|
||||
2. **Используйте шифрование для хранения credentials** (ваша система уже использует шифрование)
|
||||
3. **Регулярно обновляйте токены** (Long-Lived токены истекают через 60 дней)
|
||||
4. **Используйте System User Token для продакшена** вместо User Token
|
||||
|
||||
---
|
||||
|
||||
## Проверка учетных данных
|
||||
|
||||
### Проверка Access Token
|
||||
|
||||
```bash
|
||||
curl -X GET "https://graph.facebook.com/v18.0/me?access_token={access-token}"
|
||||
```
|
||||
|
||||
Если токен валиден, вы получите информацию о пользователе.
|
||||
|
||||
### Проверка доступа к Ad Account
|
||||
|
||||
```bash
|
||||
curl -X GET "https://graph.facebook.com/v18.0/{ad-account-id}?access_token={access-token}&fields=id,name,account_id"
|
||||
```
|
||||
|
||||
Если доступ есть, вы получите информацию об аккаунте.
|
||||
|
||||
### Проверка разрешений
|
||||
|
||||
```bash
|
||||
curl -X GET "https://graph.facebook.com/v18.0/me/permissions?access_token={access-token}"
|
||||
```
|
||||
|
||||
Проверьте, что в ответе есть:
|
||||
|
||||
- `ads_management` со статусом `granted`
|
||||
- `ads_read` со статусом `granted`
|
||||
- `business_management` со статусом `granted`
|
||||
|
||||
---
|
||||
|
||||
## Примеры использования для создания рекламной кампании
|
||||
|
||||
### Создание кампании
|
||||
|
||||
```bash
|
||||
curl -X POST "https://graph.facebook.com/v18.0/{ad-account-id}/campaigns" \
|
||||
-d "name=Test Campaign" \
|
||||
-d "objective=OUTCOME_TRAFFIC" \
|
||||
-d "status=PAUSED" \
|
||||
-d "access_token={access-token}"
|
||||
```
|
||||
|
||||
### Создание Ad Set
|
||||
|
||||
```bash
|
||||
curl -X POST "https://graph.facebook.com/v18.0/{ad-account-id}/adsets" \
|
||||
-d "name=Test Ad Set" \
|
||||
-d "campaign_id={campaign-id}" \
|
||||
-d "billing_event=IMPRESSIONS" \
|
||||
-d "optimization_goal=REACH" \
|
||||
-d "bid_amount=100" \
|
||||
-d "daily_budget=1000" \
|
||||
-d "targeting={'geo_locations':{'countries':['KZ']}}" \
|
||||
-d "access_token={access-token}"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Обновление токенов
|
||||
|
||||
Access Token имеет срок действия. Для автоматического обновления:
|
||||
|
||||
1. **Отслеживайте срок действия токена** (`expiresAt`)
|
||||
2. **Используйте refresh token** (если доступен)
|
||||
3. **Или запросите новый токен** перед истечением старого
|
||||
|
||||
### Обмен краткосрочного токена на долгосрочный
|
||||
|
||||
```javascript
|
||||
async function exchangeToken(shortLivedToken, appId, appSecret) {
|
||||
const response = await fetch(
|
||||
`https://graph.facebook.com/v18.0/oauth/access_token?grant_type=fb_exchange_token&client_id=${appId}&client_secret=${appSecret}&fb_exchange_token=${shortLivedToken}`
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
accessToken: data.access_token,
|
||||
expiresIn: data.expires_in, // в секундах
|
||||
expiresAt: new Date(Date.now() + data.expires_in * 1000).toISOString(),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Рекомендации
|
||||
|
||||
1. **Для разработки:** Используйте Long-Lived User Access Token
|
||||
2. **Для продакшена:** Используйте System User Access Token
|
||||
3. **Храните credentials в зашифрованном виде** (ваша система уже это делает)
|
||||
4. **Реализуйте автоматическое обновление токенов**
|
||||
5. **Логируйте все операции с рекламой** для отладки
|
||||
6. **Обрабатывайте ошибки API** (rate limits, invalid tokens, etc.)
|
||||
|
||||
---
|
||||
|
||||
## Полезные ссылки
|
||||
|
||||
- [Facebook Marketing API Documentation](https://developers.facebook.com/docs/marketing-apis)
|
||||
- [Facebook Graph API Explorer](https://developers.facebook.com/tools/explorer/)
|
||||
- [Facebook Business Settings](https://business.facebook.com/settings)
|
||||
- [Facebook Ads Manager](https://business.facebook.com/adsmanager)
|
||||
- [Access Token Debugger](https://developers.facebook.com/tools/debug/accesstoken/)
|
||||
|
||||
---
|
||||
|
||||
## Поддержка
|
||||
|
||||
При возникновении проблем с получением или использованием учетных данных:
|
||||
|
||||
1. Проверьте документацию Facebook Marketing API
|
||||
2. Используйте [Access Token Debugger](https://developers.facebook.com/tools/debug/accesstoken/) для проверки токена
|
||||
3. Убедитесь, что все разрешения запрошены и одобрены
|
||||
4. Проверьте, что рекламный аккаунт активен и имеет необходимые права
|
||||
@@ -0,0 +1,387 @@
|
||||
# RSS Parser для бизнес-новостей
|
||||
|
||||
Этот проект представляет собой многопарсерную систему для сбора бизнес-новостей с различных источников, разработанную на Spring Boot с сохранением данных в MongoDB.
|
||||
|
||||
## Описание
|
||||
|
||||
Система включает в себя два парсера:
|
||||
|
||||
- **Kursiv Media** (`https://kursiv.media/feed/`)
|
||||
- **Kapital.kz** (`https://kapital.kz/rss/`)
|
||||
|
||||
Оба парсера получают данные из RSS-лент, обрабатывают их и сохраняют в общую базу данных MongoDB с дедупликацией по хэшу.
|
||||
|
||||
**🔄 Автоматический режим работы:** Оба парсера автоматически запускаются каждые 30 минут (в 0 и 30 минут каждого часа) для обеспечения актуальности данных.
|
||||
|
||||
## Технологический стек
|
||||
|
||||
- **Java 21**
|
||||
- **Spring Boot 3.5.5**
|
||||
- **Spring Data MongoDB**
|
||||
- **Rome** - библиотека для парсинга RSS
|
||||
- **JSoup** - для очистки HTML
|
||||
- **MongoDB** - база данных
|
||||
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
src/main/java/kz/konturai/parser/
|
||||
├── ParserApplication.java # Главный класс приложения
|
||||
├── controller/
|
||||
│ └── ParserController.java # REST API контроллер
|
||||
├── model/
|
||||
│ └── MarketItem.java # Модель данных для MongoDB
|
||||
├── repository/
|
||||
│ └── MarketItemRepository.java # Репозиторий для работы с MongoDB
|
||||
└── service/
|
||||
└── KursivParserService.java # Основной сервис парсинга
|
||||
```
|
||||
|
||||
## Установка и запуск
|
||||
|
||||
### Предварительные требования
|
||||
|
||||
1. **Java 21** или выше
|
||||
2. **MongoDB** (локально или удаленно)
|
||||
3. **Maven 3.6+**
|
||||
|
||||
### Шаги установки
|
||||
|
||||
1. **Клонируйте репозиторий:**
|
||||
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd parser
|
||||
```
|
||||
|
||||
2. **Установите MongoDB:**
|
||||
|
||||
- Скачайте и установите MongoDB с официального сайта
|
||||
- Запустите MongoDB сервис
|
||||
- По умолчанию MongoDB работает на порту 27017
|
||||
|
||||
3. **Настройте конфигурацию:**
|
||||
|
||||
Отредактируйте файл `src/main/resources/application.properties`:
|
||||
|
||||
```properties
|
||||
# MongoDB Configuration
|
||||
spring.data.mongodb.host=localhost
|
||||
spring.data.mongodb.port=27017
|
||||
spring.data.mongodb.database=parser_db
|
||||
|
||||
# RSS Feed URL
|
||||
rss.feed.url=https://kursiv.media/feed/
|
||||
```
|
||||
|
||||
4. **Соберите проект:**
|
||||
|
||||
```bash
|
||||
./mvnw clean compile
|
||||
```
|
||||
|
||||
5. **Запустите приложение:**
|
||||
|
||||
```bash
|
||||
./mvnw spring-boot:run
|
||||
```
|
||||
|
||||
Приложение будет доступно по адресу: `http://localhost:8080`
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### 1. Запуск парсинга Kursiv Media
|
||||
|
||||
```http
|
||||
POST /api/parser/parse/kursiv
|
||||
```
|
||||
|
||||
**Ответ:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Парсинг Kursiv завершен успешно",
|
||||
"source": "Kursiv",
|
||||
"processedItems": 15,
|
||||
"totalItemsInDb": 25
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Запуск парсинга Kapital.kz
|
||||
|
||||
```http
|
||||
POST /api/parser/parse/kapital
|
||||
```
|
||||
|
||||
**Ответ:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Парсинг Kapital завершен успешно",
|
||||
"source": "Kapital",
|
||||
"processedItems": 12,
|
||||
"totalItemsInDb": 37
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Запуск парсинга всех источников
|
||||
|
||||
```http
|
||||
POST /api/parser/parse/all
|
||||
```
|
||||
|
||||
**Ответ:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Парсинг всех источников завершен успешно",
|
||||
"kursivProcessed": 15,
|
||||
"kapitalProcessed": 12,
|
||||
"totalProcessed": 27,
|
||||
"totalItemsInDb": 37
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Получение статистики
|
||||
|
||||
```http
|
||||
GET /api/parser/stats
|
||||
```
|
||||
|
||||
**Ответ:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"totalItems": 25,
|
||||
"message": "Статистика получена успешно"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Получение всех записей
|
||||
|
||||
```http
|
||||
GET /api/parser/items
|
||||
```
|
||||
|
||||
**Ответ:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"items": [...],
|
||||
"count": 25,
|
||||
"message": "Записи получены успешно"
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Проверка состояния
|
||||
|
||||
```http
|
||||
GET /api/parser/health
|
||||
```
|
||||
|
||||
**Ответ:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "UP",
|
||||
"service": "RSS Parser System",
|
||||
"timestamp": 1703123456789,
|
||||
"scheduler": "Enabled - runs every 30 minutes"
|
||||
}
|
||||
```
|
||||
|
||||
### 4.1. Проверка подключения к MongoDB
|
||||
|
||||
```http
|
||||
GET /api/parser/health/mongodb
|
||||
```
|
||||
|
||||
**Ответ при успешном подключении:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "UP",
|
||||
"message": "MongoDB подключение успешно",
|
||||
"totalItems": 37,
|
||||
"timestamp": 1703123456789
|
||||
}
|
||||
```
|
||||
|
||||
**Ответ при ошибке:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "DOWN",
|
||||
"message": "Ошибка подключения к MongoDB: Command failed with error 13 (Unauthorized)",
|
||||
"suggestion": "Проверьте учетные данные в application.properties",
|
||||
"timestamp": 1703123456789
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Информация о планировщике
|
||||
|
||||
```http
|
||||
GET /api/parser/scheduler/info
|
||||
```
|
||||
|
||||
**Ответ:**
|
||||
|
||||
```json
|
||||
{
|
||||
"schedulerEnabled": true,
|
||||
"cronExpression": "0 0/30 * * * ?",
|
||||
"description": "Запуск каждые 30 минут (в 0 и 30 минут каждого часа)",
|
||||
"nextRun": "Следующий запуск будет в ближайшие 0 или 30 минут часа"
|
||||
}
|
||||
```
|
||||
|
||||
## Структура данных в MongoDB
|
||||
|
||||
Документы сохраняются в коллекции `market_items` со следующей структурой:
|
||||
|
||||
```json
|
||||
{
|
||||
"_id": "ObjectId",
|
||||
"source_name": "Kursiv (Бизнес/экономика)",
|
||||
"url": "https://kursiv.media/article/...",
|
||||
"title": "Заголовок новости",
|
||||
"published_at": "2025-01-14T10:30:00",
|
||||
"added_at": "2025-01-14T12:00:00",
|
||||
"raw_text": "Очищенный от HTML текст статьи...",
|
||||
"hash": "sha256_hash_of_url_and_title",
|
||||
"category": "Бизнес",
|
||||
"analytics": {
|
||||
"summary": null,
|
||||
"sentiment": null,
|
||||
"tags": [],
|
||||
"entities": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Особенности реализации
|
||||
|
||||
### Дедупликация
|
||||
|
||||
- Каждая запись имеет уникальный хэш, сгенерированный по формуле: `SHA256(url + "::" + title)`
|
||||
- При повторном парсинге существующие записи обновляются (upsert операция)
|
||||
|
||||
### Очистка данных
|
||||
|
||||
- HTML-теги удаляются из текста описания
|
||||
- Даты приводятся к формату `LocalDateTime`
|
||||
- Текстовое содержимое нормализуется
|
||||
|
||||
### Обработка ошибок
|
||||
|
||||
- Логирование всех операций
|
||||
- Graceful handling ошибок парсинга отдельных записей
|
||||
- Продолжение работы при ошибках в отдельных элементах RSS
|
||||
|
||||
### Автоматический планировщик
|
||||
|
||||
- **Автозапуск**: Оба парсера автоматически запускаются каждые 30 минут
|
||||
- **Cron выражение**: `0 0/30 * * * ?` (запуск в 0 и 30 минут каждого часа)
|
||||
- **Параллельная работа**: KursivParserService и KapitalParserService работают независимо
|
||||
- **Логирование**: Все автоматические запуски логируются с эмодзи для удобства мониторинга
|
||||
- **Обработка ошибок**: Ошибки в автоматическом режиме не останавливают планировщик
|
||||
|
||||
### Многопарсерная архитектура
|
||||
|
||||
- **Общая модель данных**: Оба парсера используют одну модель `MarketItem`
|
||||
- **Общая коллекция**: Все новости сохраняются в коллекцию `market_items`
|
||||
- **Дедупликация**: Хэш генерируется одинаково для всех источников
|
||||
- **Разделение источников**: Поле `source_name` позволяет различать источники данных
|
||||
|
||||
## Тестирование
|
||||
|
||||
Запуск тестов:
|
||||
|
||||
```bash
|
||||
./mvnw test
|
||||
```
|
||||
|
||||
Тесты включают:
|
||||
|
||||
- Проверку парсинга RSS-лент обоих источников
|
||||
- Проверку сохранения в MongoDB
|
||||
- Проверку дедупликации
|
||||
- Проверку структуры данных
|
||||
- Проверку работы планировщика
|
||||
- Проверку многопарсерной архитектуры
|
||||
|
||||
## Мониторинг и логирование
|
||||
|
||||
Приложение использует SLF4J для логирования. Уровень логирования можно настроить в `application.properties`:
|
||||
|
||||
```properties
|
||||
logging.level.kz.konturai.parser=DEBUG
|
||||
logging.level.org.springframework.data.mongodb=DEBUG
|
||||
```
|
||||
|
||||
## Возможные проблемы и решения
|
||||
|
||||
### MongoDB требует аутентификацию
|
||||
|
||||
```
|
||||
Command failed with error 13 (Unauthorized): 'Command find requires authentication'
|
||||
```
|
||||
|
||||
**Решение:**
|
||||
|
||||
1. Проверьте учетные данные в `application.properties`
|
||||
2. Убедитесь, что пользователь `parser_user` существует в MongoDB
|
||||
3. Проверьте права доступа пользователя к базе данных `parser_db`
|
||||
4. Используйте endpoint `GET /api/parser/health/mongodb` для диагностики
|
||||
|
||||
**Подробное руководство:** См. файл `MONGODB_TROUBLESHOOTING.md`
|
||||
|
||||
### MongoDB не запущен
|
||||
|
||||
```
|
||||
Error: Could not connect to MongoDB
|
||||
```
|
||||
|
||||
**Решение:** Убедитесь, что MongoDB запущен и доступен на указанном хосте и порту.
|
||||
|
||||
### RSS-лента недоступна
|
||||
|
||||
```
|
||||
Error: Could not fetch RSS feed
|
||||
```
|
||||
|
||||
**Решение:** Проверьте доступность URL RSS-лент и интернет-соединение.
|
||||
|
||||
### Проблемы с кодировкой
|
||||
|
||||
Если возникают проблемы с кодировкой текста, убедитесь, что в системе установлена правильная локаль.
|
||||
|
||||
## Развертывание в продакшене
|
||||
|
||||
1. **Настройте переменные окружения:**
|
||||
|
||||
```bash
|
||||
export SPRING_DATA_MONGODB_HOST=your-mongodb-host
|
||||
export SPRING_DATA_MONGODB_PORT=27017
|
||||
export SPRING_DATA_MONGODB_DATABASE=parser_prod
|
||||
```
|
||||
|
||||
2. **Соберите JAR файл:**
|
||||
|
||||
```bash
|
||||
./mvnw clean package
|
||||
```
|
||||
|
||||
3. **Запустите приложение:**
|
||||
```bash
|
||||
java -jar target/parser-0.0.1-SNAPSHOT.jar
|
||||
```
|
||||
|
||||
## Лицензия
|
||||
|
||||
Этот проект разработан для внутреннего использования в рамках AI-платформы для маркетинговой аналитики.
|
||||
@@ -0,0 +1,130 @@
|
||||
# Troubleshooting
|
||||
|
||||
## Application will not start
|
||||
|
||||
### `Could not resolve placeholder 'X'`
|
||||
|
||||
```
|
||||
java.lang.IllegalArgumentException: Could not resolve placeholder 'OPENAI_API_KEY' in value "${OPENAI_API_KEY}"
|
||||
```
|
||||
|
||||
A required environment variable is missing. This is the intended behaviour — the
|
||||
service refuses to start rather than run with an unusable credential.
|
||||
|
||||
**Fix:** set the named variable. Locally that means `.env`; in production,
|
||||
`.env.production` on the deployment host followed by
|
||||
`docker compose restart parser-service`. [`.env.example`](../.env.example) lists every
|
||||
variable and marks which are required.
|
||||
|
||||
Check what the container actually received:
|
||||
|
||||
```bash
|
||||
docker compose exec parser-service env | grep -E 'MONGODB|OPENAI|MINIO|JWT'
|
||||
```
|
||||
|
||||
### MongoDB connection refused / timeout
|
||||
|
||||
The service cannot operate without MongoDB, so this is fatal at startup.
|
||||
|
||||
```bash
|
||||
./scripts/smoke-mongodb.sh # reachability + configured connection string
|
||||
```
|
||||
|
||||
Work through, in order:
|
||||
|
||||
1. **Reachability** — `nc -z $MONGODB_HOST $MONGODB_PORT`. If this fails the problem is
|
||||
network or firewall, not credentials.
|
||||
2. **Credentials** — `MONGODB_USERNAME` / `MONGODB_PASSWORD` are set and exported.
|
||||
3. **Auth database** — `MONGODB_AUTH_DATABASE` must match how the user was created,
|
||||
normally `admin`. A user created against `admin` but authenticated against
|
||||
`parser_db` fails with an authentication error, not a connection error.
|
||||
4. **Grants** — the user needs read/write on `MONGODB_DATABASE`.
|
||||
|
||||
Timeouts are 30s for connect, socket and server selection. A slow first request after
|
||||
an idle period is usually server selection re-establishing the topology.
|
||||
|
||||
Historical detail: [archive/setup-notes/MONGODB_TROUBLESHOOTING.md](archive/setup-notes/MONGODB_TROUBLESHOOTING.md).
|
||||
|
||||
## Runtime failures
|
||||
|
||||
### `401 Unauthorized` from OpenAI
|
||||
|
||||
```
|
||||
OpenAI request failed: 401 Unauthorized from POST https://api.openai.com/v1/chat/completions
|
||||
```
|
||||
|
||||
The key in `OPENAI_API_KEY` is invalid, revoked, or belongs to an organisation without
|
||||
access to the configured model.
|
||||
|
||||
```bash
|
||||
./scripts/smoke-openai-api.sh # validates the key directly against the API
|
||||
```
|
||||
|
||||
Then check the service's own view: `GET /api/openai/test`.
|
||||
|
||||
If the key is fine but calls still fail, confirm `openai.model.name` (`gpt-4o-mini`) and
|
||||
`openai.model.name.text` (`gpt-4o`) are both available to your account.
|
||||
|
||||
### `401` from this service's own endpoints
|
||||
|
||||
That is JWT validation, not OpenAI. `security.jwt.secret-base64` must be **byte-identical
|
||||
to the secret used by the auth service that issued the token**. A mismatch produces a
|
||||
signature failure on every request. Also confirm the header is `Authorization: Bearer <token>`
|
||||
and the token has not expired.
|
||||
|
||||
### Rate limiting / slow AI responses
|
||||
|
||||
OpenAI calls retry with exponential backoff — up to 3 attempts normally, and up to 10
|
||||
with a 5s–300s backoff specifically for rate-limit responses. Concurrency is capped at
|
||||
3 requests. Sustained `429`s mean the cap is not low enough for your quota; lower
|
||||
`openai.rateLimit.maxConcurrentRequests`.
|
||||
|
||||
Ollama timeouts are deliberately long (up to 5 hours) because it generates long-form
|
||||
report text on self-hosted hardware. A request that appears hung may simply be
|
||||
generating.
|
||||
|
||||
### Image or video generation returns null
|
||||
|
||||
```
|
||||
[Imagen3] Credentials file not found: keys/google-key.json
|
||||
```
|
||||
|
||||
The Google service-account key is missing from the build. It must exist at
|
||||
`src/main/resources/keys/google-key.json` **when the jar is built**, because it is
|
||||
loaded from the classpath. See
|
||||
[configuration.md](configuration.md#the-google-service-account-key).
|
||||
|
||||
This degrades gracefully — only image and video generation are affected.
|
||||
|
||||
### Reports fail with a date `NullPointerException`
|
||||
|
||||
```
|
||||
Cannot invoke "java.time.LocalDateTime.toLocalDate()" because "end" is null
|
||||
```
|
||||
|
||||
`ReportGenerationService` defaults a missing range to the last 30 days. If you see this
|
||||
again, a new code path is formatting `startDate`/`endDate` without a null check.
|
||||
Background: [archive/changelogs/NULL_POINTER_FIX.md](archive/changelogs/NULL_POINTER_FIX.md).
|
||||
|
||||
### CORS errors in the browser
|
||||
|
||||
Origins come from `CORS_ALLOWED_ORIGINS` and default to `https://konturai.kz` and
|
||||
`https://www.konturai.kz`. A local frontend needs its origin added explicitly —
|
||||
`cors.allow-credentials=true` means a wildcard origin is not permitted.
|
||||
|
||||
## Diagnostics
|
||||
|
||||
```bash
|
||||
curl -fsS localhost:8080/api/parser/health # liveness
|
||||
docker compose logs -f parser-service # follow logs
|
||||
docker compose logs --tail=200 parser-service | grep -i error
|
||||
```
|
||||
|
||||
Raise log detail for a specific area by overriding its level, e.g.
|
||||
`logging.level.kz.konturai.parser.service.MarketingAnalysisService=DEBUG`.
|
||||
|
||||
`MarketingController` and `MarketingAnalysisService` already log at `DEBUG`. Mongo
|
||||
driver logs are at `WARN` to keep the noise down — raise
|
||||
`logging.level.org.springframework.data.mongodb` to `DEBUG` when investigating queries.
|
||||
|
||||
Heap dumps from OOM kills land in `/dumps/heap.hprof` inside the container.
|
||||
Reference in New Issue
Block a user