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

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