new endpoints for analys history
This commit is contained in:
@@ -0,0 +1,309 @@
|
|||||||
|
# Извлечение информации о пользователе из JWT токена
|
||||||
|
|
||||||
|
## Обзор
|
||||||
|
|
||||||
|
Данная документация описывает, как извлечь информацию о пользователе из JWT токена в микросервисе на Spring Boot.
|
||||||
|
|
||||||
|
## Структура JWT токена
|
||||||
|
|
||||||
|
JWT токен содержит следующую информацию:
|
||||||
|
|
||||||
|
- **Subject (sub)**: Email пользователя
|
||||||
|
- **Custom Claims**:
|
||||||
|
- `uid`: ID пользователя (Long)
|
||||||
|
- `roles`: Роли пользователя (String, разделённые запятыми, например: "ROLE_USER,ROLE_ADMIN")
|
||||||
|
- **Стандартные поля**: `iat` (issued at), `exp` (expiration)
|
||||||
|
|
||||||
|
## Зависимости
|
||||||
|
|
||||||
|
Убедитесь, что в `pom.xml` добавлена зависимость:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-api</artifactId>
|
||||||
|
<version>0.12.3</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-impl</artifactId>
|
||||||
|
<version>0.12.3</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-jackson</artifactId>
|
||||||
|
<version>0.12.3</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Конфигурация
|
||||||
|
|
||||||
|
В `application.properties` или `application.yml`:
|
||||||
|
|
||||||
|
```properties
|
||||||
|
security.jwt.secret-base64=<base64-encoded-secret-key>
|
||||||
|
security.jwt.access-ttl-seconds=3600
|
||||||
|
```
|
||||||
|
|
||||||
|
**Важно**: Используйте тот же `secret-base64`, что и в сервисе, выдающем токены.
|
||||||
|
|
||||||
|
## Создание JwtService
|
||||||
|
|
||||||
|
```java
|
||||||
|
package com.example.service;
|
||||||
|
|
||||||
|
import io.jsonwebtoken.Claims;
|
||||||
|
import io.jsonwebtoken.Jwts;
|
||||||
|
import io.jsonwebtoken.io.Decoders;
|
||||||
|
import io.jsonwebtoken.security.Keys;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.security.Key;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class JwtService {
|
||||||
|
|
||||||
|
private final Key signingKey;
|
||||||
|
|
||||||
|
public JwtService(
|
||||||
|
@Value("${security.jwt.secret-base64}") String base64Secret) {
|
||||||
|
this.signingKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(base64Secret));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Claims parseAndValidate(String token) {
|
||||||
|
return Jwts.parserBuilder()
|
||||||
|
.setSigningKey(signingKey)
|
||||||
|
.build()
|
||||||
|
.parseClaimsJws(token)
|
||||||
|
.getBody();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Извлечение информации о пользователе
|
||||||
|
|
||||||
|
### Вариант 1: Из заголовка Authorization
|
||||||
|
|
||||||
|
```java
|
||||||
|
import io.jsonwebtoken.Claims;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api")
|
||||||
|
public class UserController {
|
||||||
|
|
||||||
|
private final JwtService jwtService;
|
||||||
|
|
||||||
|
public UserController(JwtService jwtService) {
|
||||||
|
this.jwtService = jwtService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/user-info")
|
||||||
|
public ResponseEntity<UserInfo> getUserInfo(
|
||||||
|
@RequestHeader(HttpHeaders.AUTHORIZATION) String authHeader) {
|
||||||
|
|
||||||
|
// Извлекаем токен из заголовка "Bearer <token>"
|
||||||
|
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
||||||
|
return ResponseEntity.status(401).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
String token = authHeader.substring(7);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Claims claims = jwtService.parseAndValidate(token);
|
||||||
|
|
||||||
|
// Извлекаем информацию
|
||||||
|
String email = claims.getSubject();
|
||||||
|
Long userId = claims.get("uid", Long.class);
|
||||||
|
String rolesString = claims.get("roles", String.class);
|
||||||
|
|
||||||
|
// Парсим роли
|
||||||
|
List<String> roles = rolesString == null || rolesString.isBlank()
|
||||||
|
? List.of()
|
||||||
|
: Arrays.stream(rolesString.split(","))
|
||||||
|
.map(String::trim)
|
||||||
|
.filter(s -> !s.isEmpty())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
UserInfo userInfo = new UserInfo(userId, email, roles);
|
||||||
|
return ResponseEntity.ok(userInfo);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Токен невалиден или истёк
|
||||||
|
return ResponseEntity.status(401).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Вариант 2: Использование Spring Security (рекомендуется)
|
||||||
|
|
||||||
|
Если в вашем микросервисе настроен Spring Security с JWT фильтром, используйте `Principal`:
|
||||||
|
|
||||||
|
```java
|
||||||
|
import java.security.Principal;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api")
|
||||||
|
public class UserController {
|
||||||
|
|
||||||
|
private final JwtService jwtService;
|
||||||
|
|
||||||
|
public UserController(JwtService jwtService) {
|
||||||
|
this.jwtService = jwtService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/me")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ResponseEntity<UserInfo> getCurrentUser(Principal principal) {
|
||||||
|
// Principal.getName() возвращает subject (email) из JWT
|
||||||
|
String email = principal.getName();
|
||||||
|
|
||||||
|
// Если нужны дополнительные данные (uid, roles),
|
||||||
|
// можно извлечь их из SecurityContext или извлечь токен из запроса
|
||||||
|
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
|
||||||
|
// Или получить токен из запроса и распарсить
|
||||||
|
// (см. Вариант 1 для полного извлечения всех claims)
|
||||||
|
|
||||||
|
return ResponseEntity.ok(new UserInfo(null, email, List.of()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Вариант 3: Полное извлечение через HttpServletRequest
|
||||||
|
|
||||||
|
```java
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api")
|
||||||
|
public class UserController {
|
||||||
|
|
||||||
|
private final JwtService jwtService;
|
||||||
|
|
||||||
|
@GetMapping("/profile")
|
||||||
|
public ResponseEntity<UserInfo> getProfile(HttpServletRequest request) {
|
||||||
|
String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||||
|
|
||||||
|
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
||||||
|
return ResponseEntity.status(401).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
String token = authHeader.substring(7);
|
||||||
|
Claims claims = jwtService.parseAndValidate(token);
|
||||||
|
|
||||||
|
String email = claims.getSubject();
|
||||||
|
Long userId = claims.get("uid", Long.class);
|
||||||
|
String rolesString = claims.get("roles", String.class);
|
||||||
|
List<String> roles = parseRoles(rolesString);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(new UserInfo(userId, email, roles));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> parseRoles(String rolesString) {
|
||||||
|
if (rolesString == null || rolesString.isBlank()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
return Arrays.stream(rolesString.split(","))
|
||||||
|
.map(String::trim)
|
||||||
|
.filter(s -> !s.isEmpty())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## DTO для пользователя
|
||||||
|
|
||||||
|
```java
|
||||||
|
public record UserInfo(
|
||||||
|
Long userId,
|
||||||
|
String email,
|
||||||
|
List<String> roles
|
||||||
|
) {}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Обработка ошибок
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ControllerAdvice
|
||||||
|
public class JwtExceptionHandler {
|
||||||
|
|
||||||
|
@ExceptionHandler(JwtException.class)
|
||||||
|
public ResponseEntity<ErrorResponse> handleJwtException(JwtException e) {
|
||||||
|
return ResponseEntity.status(401)
|
||||||
|
.body(new ErrorResponse("Invalid or expired token", 401));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Пример использования в сервисном слое
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Service
|
||||||
|
public class BusinessService {
|
||||||
|
|
||||||
|
private final JwtService jwtService;
|
||||||
|
|
||||||
|
public BusinessService(JwtService jwtService) {
|
||||||
|
this.jwtService = jwtService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processRequest(String token) {
|
||||||
|
Claims claims = jwtService.parseAndValidate(token);
|
||||||
|
Long userId = claims.get("uid", Long.class);
|
||||||
|
String email = claims.getSubject();
|
||||||
|
|
||||||
|
// Используйте userId и email для бизнес-логики
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Важные замечания
|
||||||
|
|
||||||
|
1. **Валидация токена**: Метод `parseAndValidate` автоматически проверяет:
|
||||||
|
|
||||||
|
- Подпись токена
|
||||||
|
- Срок действия (expiration)
|
||||||
|
- Формат токена
|
||||||
|
|
||||||
|
2. **Безопасность**: Никогда не логируйте полный JWT токен или секретный ключ.
|
||||||
|
|
||||||
|
3. **Секретный ключ**: Должен совпадать с ключом в сервисе, выдающем токены.
|
||||||
|
|
||||||
|
4. **Обработка исключений**: `JwtException` и его подклассы (`ExpiredJwtException`, `MalformedJwtException`, и т.д.) должны обрабатываться корректно.
|
||||||
|
|
||||||
|
## Примеры исключений
|
||||||
|
|
||||||
|
- `ExpiredJwtException`: Токен истёк
|
||||||
|
- `MalformedJwtException`: Неверный формат токена
|
||||||
|
- `SignatureException`: Неверная подпись
|
||||||
|
- `UnsupportedJwtException`: Неподдерживаемый тип токена
|
||||||
|
|
||||||
|
## Тестирование
|
||||||
|
|
||||||
|
```java
|
||||||
|
@SpringBootTest
|
||||||
|
class JwtServiceTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private JwtService jwtService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testParseToken() {
|
||||||
|
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
|
||||||
|
Claims claims = jwtService.parseAndValidate(token);
|
||||||
|
|
||||||
|
assertEquals("user@example.com", claims.getSubject());
|
||||||
|
assertEquals(123L, claims.get("uid", Long.class));
|
||||||
|
assertEquals("ROLE_USER", claims.get("roles", String.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,987 @@
|
|||||||
|
# API Документация: Маркетинговый анализ и стратегии (Frontend/AI Agent)
|
||||||
|
|
||||||
|
## Базовый URL
|
||||||
|
|
||||||
|
```
|
||||||
|
https://api.konturai.kz
|
||||||
|
```
|
||||||
|
|
||||||
|
## Обзор
|
||||||
|
|
||||||
|
API для генерации маркетингового анализа и стратегий на основе данных о бизнесе. Все эндпоинты требуют JWT аутентификации для связи данных с пользователем.
|
||||||
|
|
||||||
|
**Важные изменения:**
|
||||||
|
|
||||||
|
- ✅ Все эндпоинты теперь требуют JWT токен в заголовке `Authorization`
|
||||||
|
- ✅ Пользователи могут видеть только свои анализы и стратегии
|
||||||
|
- ✅ Добавлена детальная история статусов для каждого анализа и стратегии
|
||||||
|
- ✅ Новые эндпоинты для получения списка всех анализов/стратегий пользователя
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Аутентификация
|
||||||
|
|
||||||
|
Все запросы должны включать JWT токен в заголовке `Authorization`:
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <your_jwt_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Структура JWT токена
|
||||||
|
|
||||||
|
JWT токен содержит следующую информацию:
|
||||||
|
|
||||||
|
- **sub**: Email пользователя
|
||||||
|
- **uid**: ID пользователя (Long) - используется для связи данных
|
||||||
|
- **roles**: Роли пользователя (String, разделённые запятыми)
|
||||||
|
|
||||||
|
### Ошибки аутентификации
|
||||||
|
|
||||||
|
Если токен отсутствует или невалиден, API вернет:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Не авторизован",
|
||||||
|
"error": {
|
||||||
|
"code": "UNAUTHORIZED",
|
||||||
|
"message": "Требуется аутентификация. Пожалуйста, предоставьте валидный JWT токен."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**HTTP статус**: `401 Unauthorized`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Эндпоинты
|
||||||
|
|
||||||
|
### 1. Запуск маркетингового анализа
|
||||||
|
|
||||||
|
**POST** `/api/marketing/analysis/start`
|
||||||
|
|
||||||
|
Создает новую задачу на генерацию маркетингового анализа и запускает асинхронную обработку. Анализ автоматически связывается с пользователем из JWT токена.
|
||||||
|
|
||||||
|
#### Заголовки запроса
|
||||||
|
|
||||||
|
```
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer <your_jwt_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Тело запроса (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 символов
|
||||||
|
|
||||||
|
#### Пример запроса
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const response = await fetch(
|
||||||
|
'https://api.konturai.kz/api/marketing/analysis/start',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
product: 'Разработка мобильных приложений',
|
||||||
|
location: 'Нур-Султан, Казахстан',
|
||||||
|
client: 'B2B клиенты',
|
||||||
|
differentiator: 'Специализируемся на быстрой разработке MVP за 4 недели',
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Пример успешного ответа (200 OK)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Анализ запущен успешно",
|
||||||
|
"data": {
|
||||||
|
"analysisId": "507f1f77bcf86cd799439011",
|
||||||
|
"status": "processing",
|
||||||
|
"estimatedCompletionTime": "2025-01-20T15:38:00",
|
||||||
|
"message": "Анализ запущен успешно. Результаты будут готовы в течение 5-10 минут."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Получение результата анализа
|
||||||
|
|
||||||
|
**GET** `/api/marketing/analysis/{analysisId}`
|
||||||
|
|
||||||
|
Возвращает статус и результаты анализа по идентификатору. Пользователь может получить только свои анализы.
|
||||||
|
|
||||||
|
#### Заголовки запроса
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <your_jwt_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Параметры пути
|
||||||
|
|
||||||
|
| Параметр | Тип | Описание |
|
||||||
|
| ------------ | ------ | --------------------- |
|
||||||
|
| `analysisId` | string | Идентификатор анализа |
|
||||||
|
|
||||||
|
#### Пример запроса
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.konturai.kz/api/marketing/analysis/${analysisId}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Пример ответа (когда анализ завершен - 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"],
|
||||||
|
"strategy": {
|
||||||
|
"duration": "2 недели",
|
||||||
|
"channels": ["Instagram", "Telegram", "21MC"],
|
||||||
|
"contentTypes": ["посты", "сторис", "баннеры"]
|
||||||
|
},
|
||||||
|
"pdfUrl": "/api/marketing/analysis/507f1f77bcf86cd799439011/download"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Статусы анализа
|
||||||
|
|
||||||
|
| Статус | Описание |
|
||||||
|
| ------------ | ----------------------------- |
|
||||||
|
| `queued` | Запрос в очереди на обработку |
|
||||||
|
| `processing` | Анализ выполняется |
|
||||||
|
| `completed` | Анализ завершен успешно |
|
||||||
|
| `failed` | Анализ завершился с ошибкой |
|
||||||
|
|
||||||
|
#### Ошибки доступа
|
||||||
|
|
||||||
|
Если пользователь пытается получить доступ к анализу другого пользователя:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Доступ запрещен",
|
||||||
|
"error": {
|
||||||
|
"code": "FORBIDDEN",
|
||||||
|
"message": "У вас нет доступа к этому анализу"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**HTTP статус**: `403 Forbidden`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Получение списка всех анализов пользователя
|
||||||
|
|
||||||
|
**GET** `/api/marketing/analysis/my`
|
||||||
|
|
||||||
|
Возвращает список всех анализов текущего пользователя, отсортированных по дате создания (новые первыми).
|
||||||
|
|
||||||
|
#### Заголовки запроса
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <your_jwt_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Пример запроса
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const response = await fetch(
|
||||||
|
'https://api.konturai.kz/api/marketing/analysis/my',
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Пример успешного ответа (200 OK)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Операция выполнена успешно",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"analysisId": "507f1f77bcf86cd799439011",
|
||||||
|
"product": "Разработка мобильных приложений",
|
||||||
|
"location": "Нур-Султан, Казахстан",
|
||||||
|
"clientType": "B2B клиенты",
|
||||||
|
"differentiator": "Быстрая разработка MVP за 4 недели",
|
||||||
|
"status": "completed",
|
||||||
|
"userId": "12345",
|
||||||
|
"createdAt": "2025-01-20T15:30:00",
|
||||||
|
"completedAt": "2025-01-20T15:38:00",
|
||||||
|
"statusHistory": [
|
||||||
|
{
|
||||||
|
"status": "queued",
|
||||||
|
"timestamp": "2025-01-20T15:30:00",
|
||||||
|
"message": "Анализ создан и добавлен в очередь"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"status": "processing",
|
||||||
|
"timestamp": "2025-01-20T15:30:05",
|
||||||
|
"message": "Начата обработка анализа"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"status": "completed",
|
||||||
|
"timestamp": "2025-01-20T15:38:00",
|
||||||
|
"message": "Анализ успешно завершен"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"analysisId": "507f1f77bcf86cd799439012",
|
||||||
|
"product": "Веб-разработка",
|
||||||
|
"location": "Алматы, Казахстан",
|
||||||
|
"clientType": "B2C клиенты",
|
||||||
|
"differentiator": "Современные технологии",
|
||||||
|
"status": "processing",
|
||||||
|
"userId": "12345",
|
||||||
|
"createdAt": "2025-01-20T14:00:00",
|
||||||
|
"completedAt": null,
|
||||||
|
"statusHistory": [
|
||||||
|
{
|
||||||
|
"status": "queued",
|
||||||
|
"timestamp": "2025-01-20T14:00:00",
|
||||||
|
"message": "Анализ создан и добавлен в очередь"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"status": "processing",
|
||||||
|
"timestamp": "2025-01-20T14:00:05",
|
||||||
|
"message": "Начата обработка анализа"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Структура ответа
|
||||||
|
|
||||||
|
| Поле | Тип | Описание |
|
||||||
|
| ---------------------------------- | ------ | ------------------------------------------------------ |
|
||||||
|
| `data[].analysisId` | string | Уникальный идентификатор анализа |
|
||||||
|
| `data[].product` | string | Название продукта или услуги |
|
||||||
|
| `data[].location` | string | Географическая локация |
|
||||||
|
| `data[].clientType` | string | Тип целевой аудитории |
|
||||||
|
| `data[].differentiator` | string | Уникальные особенности бизнеса |
|
||||||
|
| `data[].status` | string | Текущий статус анализа |
|
||||||
|
| `data[].userId` | string | ID пользователя (из JWT) |
|
||||||
|
| `data[].createdAt` | string | ISO 8601 дата/время создания |
|
||||||
|
| `data[].completedAt` | string | ISO 8601 дата/время завершения (null если не завершен) |
|
||||||
|
| `data[].statusHistory` | array | Детальная история изменений статуса |
|
||||||
|
| `data[].statusHistory[].status` | string | Статус на момент изменения |
|
||||||
|
| `data[].statusHistory[].timestamp` | string | ISO 8601 дата/время изменения статуса |
|
||||||
|
| `data[].statusHistory[].message` | string | Описание изменения статуса |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Получение детальной истории анализа
|
||||||
|
|
||||||
|
**GET** `/api/marketing/analysis/{analysisId}/history`
|
||||||
|
|
||||||
|
Возвращает детальную информацию об анализе, включая полную историю изменений статуса.
|
||||||
|
|
||||||
|
#### Заголовки запроса
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <your_jwt_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Параметры пути
|
||||||
|
|
||||||
|
| Параметр | Тип | Описание |
|
||||||
|
| ------------ | ------ | --------------------- |
|
||||||
|
| `analysisId` | string | Идентификатор анализа |
|
||||||
|
|
||||||
|
#### Пример запроса
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.konturai.kz/api/marketing/analysis/${analysisId}/history`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Пример успешного ответа (200 OK)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Операция выполнена успешно",
|
||||||
|
"data": {
|
||||||
|
"analysisId": "507f1f77bcf86cd799439011",
|
||||||
|
"product": "Разработка мобильных приложений",
|
||||||
|
"location": "Нур-Султан, Казахстан",
|
||||||
|
"clientType": "B2B клиенты",
|
||||||
|
"differentiator": "Быстрая разработка MVP за 4 недели",
|
||||||
|
"status": "completed",
|
||||||
|
"userId": "12345",
|
||||||
|
"createdAt": "2025-01-20T15:30:00",
|
||||||
|
"completedAt": "2025-01-20T15:38:00",
|
||||||
|
"statusHistory": [
|
||||||
|
{
|
||||||
|
"status": "queued",
|
||||||
|
"timestamp": "2025-01-20T15:30:00",
|
||||||
|
"message": "Анализ создан и добавлен в очередь"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"status": "processing",
|
||||||
|
"timestamp": "2025-01-20T15:30:05",
|
||||||
|
"message": "Начата обработка анализа"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"status": "completed",
|
||||||
|
"timestamp": "2025-01-20T15:38:00",
|
||||||
|
"message": "Анализ успешно завершен"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Скачивание PDF отчета
|
||||||
|
|
||||||
|
**GET** `/api/marketing/analysis/{analysisId}/download`
|
||||||
|
|
||||||
|
Возвращает PDF файл с полным маркетинговым отчетом. Пользователь может скачать только свои отчеты.
|
||||||
|
|
||||||
|
#### Заголовки запроса
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <your_jwt_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Параметры пути
|
||||||
|
|
||||||
|
| Параметр | Тип | Описание |
|
||||||
|
| ------------ | ------ | --------------------- |
|
||||||
|
| `analysisId` | string | Идентификатор анализа |
|
||||||
|
|
||||||
|
#### Пример запроса
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.konturai.kz/api/marketing/analysis/${analysisId}/download`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
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`;
|
||||||
|
a.click();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Успешный ответ (200 OK)
|
||||||
|
|
||||||
|
- **Content-Type**: `application/pdf`
|
||||||
|
- **Content-Disposition**: `attachment; filename="marketing_analysis_{analysisId}_{timestamp}.pdf"`
|
||||||
|
- **Body**: Бинарные данные PDF файла
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Генерация маркетинговой стратегии
|
||||||
|
|
||||||
|
**POST** `/api/marketing/analysis/strategy/generate`
|
||||||
|
|
||||||
|
Создает маркетинговую стратегию на основе завершенного анализа. Стратегия автоматически связывается с пользователем из JWT токена.
|
||||||
|
|
||||||
|
#### Заголовки запроса
|
||||||
|
|
||||||
|
```
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer <your_jwt_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Параметры запроса
|
||||||
|
|
||||||
|
| Параметр | Тип | Обязательный | Описание |
|
||||||
|
| ------------------- | ------- | ------------ | --------------------------------------------- |
|
||||||
|
| `analysisId` | string | ✅ | ID завершенного анализа (query parameter) |
|
||||||
|
| `durationWeeks` | integer | ❌ | Длительность стратегии в неделях (default: 4) |
|
||||||
|
| `priorityPlatforms` | array | ❌ | Приоритетные платформы (массив строк) |
|
||||||
|
|
||||||
|
#### Тело запроса (JSON, опционально)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"durationWeeks": 4,
|
||||||
|
"priorityPlatforms": ["Instagram", "LinkedIn", "Telegram"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Пример запроса
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.konturai.kz/api/marketing/analysis/strategy/generate?analysisId=${analysisId}`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
durationWeeks: 4,
|
||||||
|
priorityPlatforms: ['Instagram', 'LinkedIn', 'Telegram'],
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Пример успешного ответа (200 OK)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Генерация стратегии запущена успешно. Результаты будут готовы в течение 3-5 минут.",
|
||||||
|
"data": {
|
||||||
|
"strategyId": "507f1f77bcf86cd799439020",
|
||||||
|
"analysisId": "507f1f77bcf86cd799439011",
|
||||||
|
"status": "queued",
|
||||||
|
"createdAt": "2025-01-20T15:40:00",
|
||||||
|
"completedAt": null,
|
||||||
|
"durationWeeks": 4,
|
||||||
|
"priorityPlatforms": ["Instagram", "LinkedIn", "Telegram"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. Получение результата стратегии
|
||||||
|
|
||||||
|
**GET** `/api/marketing/analysis/strategy/{strategyId}`
|
||||||
|
|
||||||
|
Возвращает статус и результаты стратегии. Пользователь может получить только свои стратегии.
|
||||||
|
|
||||||
|
#### Заголовки запроса
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <your_jwt_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Параметры пути
|
||||||
|
|
||||||
|
| Параметр | Тип | Описание |
|
||||||
|
| ------------ | ------ | ----------------------- |
|
||||||
|
| `strategyId` | string | Идентификатор стратегии |
|
||||||
|
|
||||||
|
#### Пример ответа (когда стратегия завершена - 200 OK)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Операция выполнена успешно",
|
||||||
|
"data": {
|
||||||
|
"strategyId": "507f1f77bcf86cd799439020",
|
||||||
|
"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"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"postCalendar": [
|
||||||
|
{
|
||||||
|
"publishDate": "2025-01-21T10:00:00",
|
||||||
|
"platform": "Instagram",
|
||||||
|
"contentType": "пост",
|
||||||
|
"theme": "Презентация продукта",
|
||||||
|
"postText": "Полный текст поста для публикации...",
|
||||||
|
"hashtags": ["#маркетинг", "#бизнес"],
|
||||||
|
"publishTime": "10:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. Получение стратегии по ID анализа
|
||||||
|
|
||||||
|
**GET** `/api/marketing/analysis/{analysisId}/strategy`
|
||||||
|
|
||||||
|
Возвращает стратегию, связанную с указанным анализом. Пользователь может получить только стратегии для своих анализов.
|
||||||
|
|
||||||
|
#### Заголовки запроса
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <your_jwt_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Параметры пути
|
||||||
|
|
||||||
|
| Параметр | Тип | Описание |
|
||||||
|
| ------------ | ------ | --------------------- |
|
||||||
|
| `analysisId` | string | Идентификатор анализа |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. Получение списка всех стратегий пользователя
|
||||||
|
|
||||||
|
**GET** `/api/marketing/analysis/strategy/my`
|
||||||
|
|
||||||
|
Возвращает список всех стратегий текущего пользователя, отсортированных по дате создания (новые первыми).
|
||||||
|
|
||||||
|
#### Заголовки запроса
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <your_jwt_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Пример успешного ответа (200 OK)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Операция выполнена успешно",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"strategyId": "507f1f77bcf86cd799439020",
|
||||||
|
"analysisId": "507f1f77bcf86cd799439011",
|
||||||
|
"status": "completed",
|
||||||
|
"userId": "12345",
|
||||||
|
"durationWeeks": 4,
|
||||||
|
"priorityPlatforms": ["Instagram", "LinkedIn", "Telegram"],
|
||||||
|
"createdAt": "2025-01-20T15:40:00",
|
||||||
|
"completedAt": "2025-01-20T15:43:00",
|
||||||
|
"statusHistory": [
|
||||||
|
{
|
||||||
|
"status": "queued",
|
||||||
|
"timestamp": "2025-01-20T15:40:00",
|
||||||
|
"message": "Стратегия создана и добавлена в очередь"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"status": "processing",
|
||||||
|
"timestamp": "2025-01-20T15:40:05",
|
||||||
|
"message": "Начата генерация стратегии"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"status": "completed",
|
||||||
|
"timestamp": "2025-01-20T15:43:00",
|
||||||
|
"message": "Стратегия успешно сгенерирована"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10. Получение детальной истории стратегии
|
||||||
|
|
||||||
|
**GET** `/api/marketing/analysis/strategy/{strategyId}/history`
|
||||||
|
|
||||||
|
Возвращает детальную информацию о стратегии, включая полную историю изменений статуса.
|
||||||
|
|
||||||
|
#### Заголовки запроса
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <your_jwt_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Параметры пути
|
||||||
|
|
||||||
|
| Параметр | Тип | Описание |
|
||||||
|
| ------------ | ------ | ----------------------- |
|
||||||
|
| `strategyId` | string | Идентификатор стратегии |
|
||||||
|
|
||||||
|
#### Пример успешного ответа (200 OK)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Операция выполнена успешно",
|
||||||
|
"data": {
|
||||||
|
"strategyId": "507f1f77bcf86cd799439020",
|
||||||
|
"analysisId": "507f1f77bcf86cd799439011",
|
||||||
|
"status": "completed",
|
||||||
|
"userId": "12345",
|
||||||
|
"durationWeeks": 4,
|
||||||
|
"priorityPlatforms": ["Instagram", "LinkedIn", "Telegram"],
|
||||||
|
"createdAt": "2025-01-20T15:40:00",
|
||||||
|
"completedAt": "2025-01-20T15:43:00",
|
||||||
|
"statusHistory": [
|
||||||
|
{
|
||||||
|
"status": "queued",
|
||||||
|
"timestamp": "2025-01-20T15:40:00",
|
||||||
|
"message": "Стратегия создана и добавлена в очередь"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"status": "processing",
|
||||||
|
"timestamp": "2025-01-20T15:40:05",
|
||||||
|
"message": "Начата генерация стратегии"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"status": "completed",
|
||||||
|
"timestamp": "2025-01-20T15:43:00",
|
||||||
|
"message": "Стратегия успешно сгенерирована"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Обработка ошибок
|
||||||
|
|
||||||
|
### Коды ошибок
|
||||||
|
|
||||||
|
| Код | HTTP статус | Описание |
|
||||||
|
| ------------------------ | ----------- | ------------------------------- |
|
||||||
|
| `UNAUTHORIZED` | 401 | Требуется аутентификация |
|
||||||
|
| `FORBIDDEN` | 403 | Нет доступа к ресурсу |
|
||||||
|
| `VALIDATION_ERROR` | 400 | Ошибка валидации входных данных |
|
||||||
|
| `NOT_FOUND` | 404 | Ресурс не найден |
|
||||||
|
| `INVALID_ANALYSIS` | 404 | Анализ не найден |
|
||||||
|
| `ANALYSIS_NOT_COMPLETED` | 400 | Анализ еще не завершен |
|
||||||
|
| `INTERNAL_SERVER_ERROR` | 500 | Внутренняя ошибка сервера |
|
||||||
|
|
||||||
|
### Формат ошибки
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Описание ошибки",
|
||||||
|
"error": {
|
||||||
|
"code": "ERROR_CODE",
|
||||||
|
"message": "Детальное сообщение об ошибке",
|
||||||
|
"details": {
|
||||||
|
"field1": "Сообщение об ошибке для поля 1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Примеры использования
|
||||||
|
|
||||||
|
### JavaScript/TypeScript (Fetch API)
|
||||||
|
|
||||||
|
#### Получение JWT токена
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Предполагается, что токен получен при логине
|
||||||
|
const jwtToken = localStorage.getItem('jwtToken');
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Запуск анализа с JWT
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function startMarketingAnalysis(data, jwtToken) {
|
||||||
|
const response = await fetch(
|
||||||
|
'https://api.konturai.kz/api/marketing/analysis/start',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 401) {
|
||||||
|
throw new Error('Требуется аутентификация');
|
||||||
|
}
|
||||||
|
const error = await response.json();
|
||||||
|
throw new Error(error.error?.message || 'Ошибка при запуске анализа');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
return result.data.analysisId;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Получение списка всех анализов пользователя
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function getMyAnalyses(jwtToken) {
|
||||||
|
const response = await fetch(
|
||||||
|
'https://api.konturai.kz/api/marketing/analysis/my',
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Ошибка при получении списка анализов');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
return result.data; // Массив анализов
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Получение детальной истории анализа
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function getAnalysisHistory(analysisId, jwtToken) {
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.konturai.kz/api/marketing/analysis/${analysisId}/history`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 403) {
|
||||||
|
throw new Error('Нет доступа к этому анализу');
|
||||||
|
}
|
||||||
|
throw new Error('Ошибка при получении истории');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Полный пример: создание анализа и отслеживание статуса
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function createAndTrackAnalysis(
|
||||||
|
product,
|
||||||
|
location,
|
||||||
|
client,
|
||||||
|
differentiator,
|
||||||
|
jwtToken
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// 1. Запускаем анализ
|
||||||
|
const analysisId = await startMarketingAnalysis(
|
||||||
|
{
|
||||||
|
product,
|
||||||
|
location,
|
||||||
|
client,
|
||||||
|
differentiator,
|
||||||
|
},
|
||||||
|
jwtToken
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`Анализ запущен: ${analysisId}`);
|
||||||
|
|
||||||
|
// 2. Получаем историю для отслеживания статуса
|
||||||
|
const checkStatus = async () => {
|
||||||
|
const history = await getAnalysisHistory(analysisId, jwtToken);
|
||||||
|
|
||||||
|
// Показываем последний статус
|
||||||
|
const lastStatus =
|
||||||
|
history.statusHistory[history.statusHistory.length - 1];
|
||||||
|
console.log(`Статус: ${lastStatus.status} - ${lastStatus.message}`);
|
||||||
|
|
||||||
|
return history.status;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. Polling: проверяем статус каждые 10 секунд
|
||||||
|
const pollInterval = setInterval(async () => {
|
||||||
|
const status = await checkStatus();
|
||||||
|
|
||||||
|
if (status === 'completed') {
|
||||||
|
clearInterval(pollInterval);
|
||||||
|
console.log('Анализ завершен!');
|
||||||
|
// Получаем полный результат
|
||||||
|
const fullResult = await getAnalysisResult(analysisId, jwtToken);
|
||||||
|
return fullResult;
|
||||||
|
} else if (status === 'failed') {
|
||||||
|
clearInterval(pollInterval);
|
||||||
|
throw new Error('Анализ завершился с ошибкой');
|
||||||
|
}
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
// Останавливаем polling через 15 минут
|
||||||
|
setTimeout(() => {
|
||||||
|
clearInterval(pollInterval);
|
||||||
|
console.log('Превышено время ожидания');
|
||||||
|
}, 15 * 60 * 1000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Получение списка стратегий пользователя
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function getMyStrategies(jwtToken) {
|
||||||
|
const response = await fetch(
|
||||||
|
'https://api.konturai.kz/api/marketing/analysis/strategy/my',
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Ошибка при получении списка стратегий');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
return result.data; // Массив стратегий
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Рекомендации по интеграции
|
||||||
|
|
||||||
|
### 1. Обработка JWT токена
|
||||||
|
|
||||||
|
- Сохраняйте токен в безопасном месте (например, `localStorage` или `sessionStorage`)
|
||||||
|
- Проверяйте срок действия токена перед запросами
|
||||||
|
- Реализуйте механизм обновления токена при истечении
|
||||||
|
|
||||||
|
### 2. Обработка ошибок аутентификации
|
||||||
|
|
||||||
|
При получении `401 Unauthorized`:
|
||||||
|
|
||||||
|
- Перенаправляйте пользователя на страницу входа
|
||||||
|
- Очищайте сохраненный токен
|
||||||
|
- Показывайте понятное сообщение пользователю
|
||||||
|
|
||||||
|
### 3. Обработка ошибок доступа
|
||||||
|
|
||||||
|
При получении `403 Forbidden`:
|
||||||
|
|
||||||
|
- Показывайте сообщение о том, что ресурс недоступен
|
||||||
|
- Не пытайтесь повторять запрос с теми же параметрами
|
||||||
|
|
||||||
|
### 4. Polling стратегия
|
||||||
|
|
||||||
|
Для отслеживания статуса анализа/стратегии:
|
||||||
|
|
||||||
|
- Используйте интервал 10-15 секунд
|
||||||
|
- Максимальное время ожидания: 15 минут
|
||||||
|
- Показывайте прогресс пользователю на основе `statusHistory`
|
||||||
|
|
||||||
|
### 5. Отображение истории статусов
|
||||||
|
|
||||||
|
Используйте `statusHistory` для:
|
||||||
|
|
||||||
|
- Показывать временную шкалу изменений статуса
|
||||||
|
- Отображать детальную информацию о каждом этапе
|
||||||
|
- Информировать пользователя о прогрессе
|
||||||
|
|
||||||
|
### 6. Кэширование
|
||||||
|
|
||||||
|
- Кэшируйте список анализов/стратегий пользователя
|
||||||
|
- Обновляйте кэш при создании новых записей
|
||||||
|
- Используйте `analysisId`/`strategyId` как ключи кэша
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Примечания
|
||||||
|
|
||||||
|
1. **Формат даты**: Все даты возвращаются в формате ISO 8601 без timezone (LocalDateTime)
|
||||||
|
2. **Идентификаторы**: Используются MongoDB ObjectId (24 символа hex)
|
||||||
|
3. **Асинхронность**: Анализ и стратегия выполняются асинхронно
|
||||||
|
4. **Безопасность**: Все эндпоинты требуют валидный JWT токен
|
||||||
|
5. **Изоляция данных**: Пользователи видят только свои данные
|
||||||
|
6. **История статусов**: Детальная история доступна для всех анализов и стратегий
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Поддержка
|
||||||
|
|
||||||
|
При возникновении проблем с API обращайтесь в техническую поддержку с указанием:
|
||||||
|
|
||||||
|
- `analysisId` или `strategyId` (если есть)
|
||||||
|
- Время запроса
|
||||||
|
- Описание проблемы
|
||||||
|
- Код ошибки (если есть)
|
||||||
|
- JWT токен (только для отладки, не в продакшене!)
|
||||||
@@ -126,6 +126,25 @@
|
|||||||
<artifactId>batik-transcoder</artifactId>
|
<artifactId>batik-transcoder</artifactId>
|
||||||
<version>1.17</version>
|
<version>1.17</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- JWT library for token parsing and validation -->
|
||||||
|
<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>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ package kz.konturai.parser.controller;
|
|||||||
import kz.konturai.parser.dto.*;
|
import kz.konturai.parser.dto.*;
|
||||||
import kz.konturai.parser.model.MarketingAnalysis;
|
import kz.konturai.parser.model.MarketingAnalysis;
|
||||||
import kz.konturai.parser.model.MarketingStrategy;
|
import kz.konturai.parser.model.MarketingStrategy;
|
||||||
|
import kz.konturai.parser.service.JwtService;
|
||||||
import kz.konturai.parser.service.MarketingAnalysisService;
|
import kz.konturai.parser.service.MarketingAnalysisService;
|
||||||
import kz.konturai.parser.service.MarketingStrategyService;
|
import kz.konturai.parser.service.MarketingStrategyService;
|
||||||
import kz.konturai.parser.service.MinIOService;
|
import kz.konturai.parser.service.MinIOService;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
@@ -16,8 +18,10 @@ import jakarta.validation.Valid;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/marketing/analysis")
|
@RequestMapping("/api/marketing/analysis")
|
||||||
@@ -26,22 +30,46 @@ public class MarketingController {
|
|||||||
private final MarketingAnalysisService marketingAnalysisService;
|
private final MarketingAnalysisService marketingAnalysisService;
|
||||||
private final MarketingStrategyService marketingStrategyService;
|
private final MarketingStrategyService marketingStrategyService;
|
||||||
private final MinIOService minIOService;
|
private final MinIOService minIOService;
|
||||||
|
private final JwtService jwtService;
|
||||||
|
|
||||||
public MarketingController(
|
public MarketingController(
|
||||||
MarketingAnalysisService marketingAnalysisService,
|
MarketingAnalysisService marketingAnalysisService,
|
||||||
MarketingStrategyService marketingStrategyService,
|
MarketingStrategyService marketingStrategyService,
|
||||||
MinIOService minIOService) {
|
MinIOService minIOService,
|
||||||
|
JwtService jwtService) {
|
||||||
this.marketingAnalysisService = marketingAnalysisService;
|
this.marketingAnalysisService = marketingAnalysisService;
|
||||||
this.marketingStrategyService = marketingStrategyService;
|
this.marketingStrategyService = marketingStrategyService;
|
||||||
this.minIOService = minIOService;
|
this.minIOService = minIOService;
|
||||||
|
this.jwtService = jwtService;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractUserIdFromHeader(String authHeader) {
|
||||||
|
if (authHeader == null || authHeader.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return jwtService.extractUserIdFromHeader(authHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResponseEntity<?> unauthorizedResponse() {
|
||||||
|
ErrorResponse error = new ErrorResponse(
|
||||||
|
"UNAUTHORIZED",
|
||||||
|
"Требуется аутентификация. Пожалуйста, предоставьте валидный JWT токен.");
|
||||||
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||||
|
.body(ApiResponse.error("Не авторизован", error));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/start")
|
@PostMapping("/start")
|
||||||
public ResponseEntity<ApiResponse<MarketingAnalysisResponse>> startAnalysis(
|
public ResponseEntity<?> startAnalysis(
|
||||||
|
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||||
@Valid @RequestBody MarketingAnalysisRequest request) {
|
@Valid @RequestBody MarketingAnalysisRequest request) {
|
||||||
|
|
||||||
|
String userId = extractUserIdFromHeader(authHeader);
|
||||||
|
if (userId == null) {
|
||||||
|
return unauthorizedResponse();
|
||||||
|
}
|
||||||
|
|
||||||
// Create analysis record
|
// Create analysis record
|
||||||
MarketingAnalysis analysis = marketingAnalysisService.startAnalysis(request);
|
MarketingAnalysis analysis = marketingAnalysisService.startAnalysis(request, userId);
|
||||||
|
|
||||||
// Start async processing
|
// Start async processing
|
||||||
marketingAnalysisService.processAnalysis(analysis.getId(), request);
|
marketingAnalysisService.processAnalysis(analysis.getId(), request);
|
||||||
@@ -60,11 +88,16 @@ public class MarketingController {
|
|||||||
|
|
||||||
@GetMapping("/{analysisId}")
|
@GetMapping("/{analysisId}")
|
||||||
public ResponseEntity<?> getAnalysis(
|
public ResponseEntity<?> getAnalysis(
|
||||||
|
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||||
@PathVariable String analysisId) {
|
@PathVariable String analysisId) {
|
||||||
|
|
||||||
MarketingAnalysisResult result = marketingAnalysisService.getAnalysisResult(analysisId);
|
String userId = extractUserIdFromHeader(authHeader);
|
||||||
|
if (userId == null) {
|
||||||
|
return unauthorizedResponse();
|
||||||
|
}
|
||||||
|
|
||||||
if (result == null) {
|
Optional<MarketingAnalysis> optAnalysis = marketingAnalysisService.getAnalysisById(analysisId);
|
||||||
|
if (optAnalysis.isEmpty()) {
|
||||||
ErrorResponse error = new ErrorResponse(
|
ErrorResponse error = new ErrorResponse(
|
||||||
"NOT_FOUND",
|
"NOT_FOUND",
|
||||||
"Анализ с указанным ID не найден");
|
"Анализ с указанным ID не найден");
|
||||||
@@ -72,31 +105,55 @@ public class MarketingController {
|
|||||||
.body(ApiResponse.error("Анализ не найден", error));
|
.body(ApiResponse.error("Анализ не найден", error));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MarketingAnalysis analysis = optAnalysis.get();
|
||||||
|
if (!userId.equals(analysis.getUserId())) {
|
||||||
|
ErrorResponse error = new ErrorResponse(
|
||||||
|
"FORBIDDEN",
|
||||||
|
"У вас нет доступа к этому анализу");
|
||||||
|
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||||
|
.body(ApiResponse.error("Доступ запрещен", error));
|
||||||
|
}
|
||||||
|
|
||||||
|
MarketingAnalysisResult result = marketingAnalysisService.getAnalysisResult(analysisId);
|
||||||
return ResponseEntity.ok(ApiResponse.success(result));
|
return ResponseEntity.ok(ApiResponse.success(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{analysisId}/download")
|
@GetMapping("/{analysisId}/download")
|
||||||
public ResponseEntity<byte[]> downloadPdf(@PathVariable String analysisId) {
|
public ResponseEntity<?> downloadPdf(
|
||||||
MarketingAnalysisResult result = marketingAnalysisService.getAnalysisResult(analysisId);
|
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||||
|
@PathVariable String analysisId) {
|
||||||
|
|
||||||
if (result == null || result.getReport() == null || result.getReport().getPdfUrl() == null) {
|
String userId = extractUserIdFromHeader(authHeader);
|
||||||
|
if (userId == null) {
|
||||||
|
return unauthorizedResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<MarketingAnalysis> optAnalysis = marketingAnalysisService.getAnalysisById(analysisId);
|
||||||
|
if (optAnalysis.isEmpty()) {
|
||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.notFound().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the analysis to find PDF filename
|
MarketingAnalysis analysis = optAnalysis.get();
|
||||||
Optional<MarketingAnalysis> optAnalysis = marketingAnalysisService.getAnalysisById(analysisId);
|
if (!userId.equals(analysis.getUserId())) {
|
||||||
if (optAnalysis.isEmpty() || optAnalysis.get().getPdfFilePath() == null) {
|
ErrorResponse error = new ErrorResponse(
|
||||||
|
"FORBIDDEN",
|
||||||
|
"У вас нет доступа к этому анализу");
|
||||||
|
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||||
|
.body(ApiResponse.error("Доступ запрещен", error));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (analysis.getPdfFilePath() == null) {
|
||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.notFound().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
InputStream inputStream = minIOService.downloadFile(optAnalysis.get().getPdfFilePath());
|
InputStream inputStream = minIOService.downloadFile(analysis.getPdfFilePath());
|
||||||
byte[] bytes = inputStream.readAllBytes();
|
byte[] bytes = inputStream.readAllBytes();
|
||||||
inputStream.close();
|
inputStream.close();
|
||||||
|
|
||||||
return ResponseEntity.ok()
|
return ResponseEntity.ok()
|
||||||
.header(HttpHeaders.CONTENT_DISPOSITION,
|
.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||||
"attachment; filename=\"" + optAnalysis.get().getPdfFilename() + "\"")
|
"attachment; filename=\"" + analysis.getPdfFilename() + "\"")
|
||||||
.contentType(MediaType.APPLICATION_PDF)
|
.contentType(MediaType.APPLICATION_PDF)
|
||||||
.body(bytes);
|
.body(bytes);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -106,15 +163,38 @@ public class MarketingController {
|
|||||||
|
|
||||||
@PostMapping("/strategy/generate")
|
@PostMapping("/strategy/generate")
|
||||||
public ResponseEntity<?> generateStrategy(
|
public ResponseEntity<?> generateStrategy(
|
||||||
|
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||||
@RequestParam String analysisId,
|
@RequestParam String analysisId,
|
||||||
@Valid @RequestBody(required = false) MarketingStrategyRequest request) {
|
@Valid @RequestBody(required = false) MarketingStrategyRequest request) {
|
||||||
|
|
||||||
|
String userId = extractUserIdFromHeader(authHeader);
|
||||||
|
if (userId == null) {
|
||||||
|
return unauthorizedResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user owns the analysis
|
||||||
|
Optional<MarketingAnalysis> optAnalysis = marketingAnalysisService.getAnalysisById(analysisId);
|
||||||
|
if (optAnalysis.isEmpty()) {
|
||||||
|
ErrorResponse error = new ErrorResponse("INVALID_ANALYSIS", "Анализ не найден");
|
||||||
|
return ResponseEntity.status(404)
|
||||||
|
.body(ApiResponse.error("Анализ не найден", error));
|
||||||
|
}
|
||||||
|
|
||||||
|
MarketingAnalysis analysis = optAnalysis.get();
|
||||||
|
if (!userId.equals(analysis.getUserId())) {
|
||||||
|
ErrorResponse error = new ErrorResponse(
|
||||||
|
"FORBIDDEN",
|
||||||
|
"У вас нет доступа к этому анализу");
|
||||||
|
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||||
|
.body(ApiResponse.error("Доступ запрещен", error));
|
||||||
|
}
|
||||||
|
|
||||||
if (request == null) {
|
if (request == null) {
|
||||||
request = new MarketingStrategyRequest();
|
request = new MarketingStrategyRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
MarketingStrategy strategy = marketingStrategyService.generateStrategy(analysisId, request);
|
MarketingStrategy strategy = marketingStrategyService.generateStrategy(analysisId, request, userId);
|
||||||
|
|
||||||
MarketingStrategyResponse response = new MarketingStrategyResponse();
|
MarketingStrategyResponse response = new MarketingStrategyResponse();
|
||||||
response.setStrategyId(strategy.getId());
|
response.setStrategyId(strategy.getId());
|
||||||
@@ -145,10 +225,17 @@ public class MarketingController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/strategy/{strategyId}")
|
@GetMapping("/strategy/{strategyId}")
|
||||||
public ResponseEntity<?> getStrategy(@PathVariable String strategyId) {
|
public ResponseEntity<?> getStrategy(
|
||||||
MarketingStrategyResponse result = marketingStrategyService.getStrategyResult(strategyId);
|
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||||
|
@PathVariable String strategyId) {
|
||||||
|
|
||||||
if (result == null) {
|
String userId = extractUserIdFromHeader(authHeader);
|
||||||
|
if (userId == null) {
|
||||||
|
return unauthorizedResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<MarketingStrategy> optStrategy = marketingStrategyService.getStrategyById(strategyId);
|
||||||
|
if (optStrategy.isEmpty()) {
|
||||||
ErrorResponse error = new ErrorResponse(
|
ErrorResponse error = new ErrorResponse(
|
||||||
"NOT_FOUND",
|
"NOT_FOUND",
|
||||||
"Стратегия с указанным ID не найдена");
|
"Стратегия с указанным ID не найдена");
|
||||||
@@ -156,11 +243,48 @@ public class MarketingController {
|
|||||||
.body(ApiResponse.error("Стратегия не найдена", error));
|
.body(ApiResponse.error("Стратегия не найдена", error));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MarketingStrategy strategy = optStrategy.get();
|
||||||
|
if (!userId.equals(strategy.getUserId())) {
|
||||||
|
ErrorResponse error = new ErrorResponse(
|
||||||
|
"FORBIDDEN",
|
||||||
|
"У вас нет доступа к этой стратегии");
|
||||||
|
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||||
|
.body(ApiResponse.error("Доступ запрещен", error));
|
||||||
|
}
|
||||||
|
|
||||||
|
MarketingStrategyResponse result = marketingStrategyService.getStrategyResult(strategyId);
|
||||||
return ResponseEntity.ok(ApiResponse.success(result));
|
return ResponseEntity.ok(ApiResponse.success(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{analysisId}/strategy")
|
@GetMapping("/{analysisId}/strategy")
|
||||||
public ResponseEntity<?> getStrategyByAnalysis(@PathVariable String analysisId) {
|
public ResponseEntity<?> getStrategyByAnalysis(
|
||||||
|
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||||
|
@PathVariable String analysisId) {
|
||||||
|
|
||||||
|
String userId = extractUserIdFromHeader(authHeader);
|
||||||
|
if (userId == null) {
|
||||||
|
return unauthorizedResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user owns the analysis
|
||||||
|
Optional<MarketingAnalysis> optAnalysis = marketingAnalysisService.getAnalysisById(analysisId);
|
||||||
|
if (optAnalysis.isEmpty()) {
|
||||||
|
ErrorResponse error = new ErrorResponse(
|
||||||
|
"NOT_FOUND",
|
||||||
|
"Анализ с указанным ID не найден");
|
||||||
|
return ResponseEntity.status(404)
|
||||||
|
.body(ApiResponse.error("Анализ не найден", error));
|
||||||
|
}
|
||||||
|
|
||||||
|
MarketingAnalysis analysis = optAnalysis.get();
|
||||||
|
if (!userId.equals(analysis.getUserId())) {
|
||||||
|
ErrorResponse error = new ErrorResponse(
|
||||||
|
"FORBIDDEN",
|
||||||
|
"У вас нет доступа к этому анализу");
|
||||||
|
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||||
|
.body(ApiResponse.error("Доступ запрещен", error));
|
||||||
|
}
|
||||||
|
|
||||||
MarketingStrategyResponse result = marketingStrategyService.getStrategyByAnalysisId(analysisId);
|
MarketingStrategyResponse result = marketingStrategyService.getStrategyByAnalysisId(analysisId);
|
||||||
|
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
@@ -174,6 +298,133 @@ public class MarketingController {
|
|||||||
return ResponseEntity.ok(ApiResponse.success(result));
|
return ResponseEntity.ok(ApiResponse.success(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/my")
|
||||||
|
public ResponseEntity<?> getMyAnalyses(
|
||||||
|
@RequestHeader(value = "Authorization", required = false) String authHeader) {
|
||||||
|
|
||||||
|
String userId = extractUserIdFromHeader(authHeader);
|
||||||
|
if (userId == null) {
|
||||||
|
return unauthorizedResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MarketingAnalysis> analyses = marketingAnalysisService.getUserAnalyses(userId);
|
||||||
|
List<AnalysisHistoryResponse> responseList = analyses.stream()
|
||||||
|
.map(this::convertToHistoryResponse)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(responseList));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/strategy/my")
|
||||||
|
public ResponseEntity<?> getMyStrategies(
|
||||||
|
@RequestHeader(value = "Authorization", required = false) String authHeader) {
|
||||||
|
|
||||||
|
String userId = extractUserIdFromHeader(authHeader);
|
||||||
|
if (userId == null) {
|
||||||
|
return unauthorizedResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MarketingStrategy> strategies = marketingStrategyService.getUserStrategies(userId);
|
||||||
|
List<StrategyHistoryResponse> responseList = strategies.stream()
|
||||||
|
.map(this::convertToStrategyHistoryResponse)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(responseList));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{analysisId}/history")
|
||||||
|
public ResponseEntity<?> getAnalysisHistory(
|
||||||
|
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||||
|
@PathVariable String analysisId) {
|
||||||
|
|
||||||
|
String userId = extractUserIdFromHeader(authHeader);
|
||||||
|
if (userId == null) {
|
||||||
|
return unauthorizedResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<MarketingAnalysis> optAnalysis = marketingAnalysisService.getAnalysisById(analysisId);
|
||||||
|
if (optAnalysis.isEmpty()) {
|
||||||
|
ErrorResponse error = new ErrorResponse(
|
||||||
|
"NOT_FOUND",
|
||||||
|
"Анализ с указанным ID не найден");
|
||||||
|
return ResponseEntity.status(404)
|
||||||
|
.body(ApiResponse.error("Анализ не найден", error));
|
||||||
|
}
|
||||||
|
|
||||||
|
MarketingAnalysis analysis = optAnalysis.get();
|
||||||
|
if (!userId.equals(analysis.getUserId())) {
|
||||||
|
ErrorResponse error = new ErrorResponse(
|
||||||
|
"FORBIDDEN",
|
||||||
|
"У вас нет доступа к этому анализу");
|
||||||
|
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||||
|
.body(ApiResponse.error("Доступ запрещен", error));
|
||||||
|
}
|
||||||
|
|
||||||
|
AnalysisHistoryResponse response = convertToHistoryResponse(analysis);
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/strategy/{strategyId}/history")
|
||||||
|
public ResponseEntity<?> getStrategyHistory(
|
||||||
|
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||||
|
@PathVariable String strategyId) {
|
||||||
|
|
||||||
|
String userId = extractUserIdFromHeader(authHeader);
|
||||||
|
if (userId == null) {
|
||||||
|
return unauthorizedResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<MarketingStrategy> optStrategy = marketingStrategyService.getStrategyById(strategyId);
|
||||||
|
if (optStrategy.isEmpty()) {
|
||||||
|
ErrorResponse error = new ErrorResponse(
|
||||||
|
"NOT_FOUND",
|
||||||
|
"Стратегия с указанным ID не найдена");
|
||||||
|
return ResponseEntity.status(404)
|
||||||
|
.body(ApiResponse.error("Стратегия не найдена", error));
|
||||||
|
}
|
||||||
|
|
||||||
|
MarketingStrategy strategy = optStrategy.get();
|
||||||
|
if (!userId.equals(strategy.getUserId())) {
|
||||||
|
ErrorResponse error = new ErrorResponse(
|
||||||
|
"FORBIDDEN",
|
||||||
|
"У вас нет доступа к этой стратегии");
|
||||||
|
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||||
|
.body(ApiResponse.error("Доступ запрещен", error));
|
||||||
|
}
|
||||||
|
|
||||||
|
StrategyHistoryResponse response = convertToStrategyHistoryResponse(strategy);
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
private AnalysisHistoryResponse convertToHistoryResponse(MarketingAnalysis analysis) {
|
||||||
|
AnalysisHistoryResponse response = new AnalysisHistoryResponse();
|
||||||
|
response.setAnalysisId(analysis.getId());
|
||||||
|
response.setProduct(analysis.getProduct());
|
||||||
|
response.setLocation(analysis.getLocation());
|
||||||
|
response.setClientType(analysis.getClientType());
|
||||||
|
response.setDifferentiator(analysis.getDifferentiator());
|
||||||
|
response.setStatus(analysis.getStatus());
|
||||||
|
response.setUserId(analysis.getUserId());
|
||||||
|
response.setCreatedAt(analysis.getCreatedAt());
|
||||||
|
response.setCompletedAt(analysis.getCompletedAt());
|
||||||
|
response.setStatusHistory(analysis.getStatusHistory());
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private StrategyHistoryResponse convertToStrategyHistoryResponse(MarketingStrategy strategy) {
|
||||||
|
StrategyHistoryResponse response = new StrategyHistoryResponse();
|
||||||
|
response.setStrategyId(strategy.getId());
|
||||||
|
response.setAnalysisId(strategy.getAnalysisId());
|
||||||
|
response.setStatus(strategy.getStatus());
|
||||||
|
response.setUserId(strategy.getUserId());
|
||||||
|
response.setDurationWeeks(strategy.getDurationWeeks());
|
||||||
|
response.setPriorityPlatforms(strategy.getPriorityPlatforms());
|
||||||
|
response.setCreatedAt(strategy.getCreatedAt());
|
||||||
|
response.setCompletedAt(strategy.getCompletedAt());
|
||||||
|
response.setStatusHistory(strategy.getStatusHistory());
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
public ResponseEntity<ApiResponse<ErrorResponse>> handleValidationException(
|
public ResponseEntity<ApiResponse<ErrorResponse>> handleValidationException(
|
||||||
MethodArgumentNotValidException ex) {
|
MethodArgumentNotValidException ex) {
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package kz.konturai.parser.dto;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class AnalysisHistoryResponse {
|
||||||
|
private String analysisId;
|
||||||
|
private String product;
|
||||||
|
private String location;
|
||||||
|
private String clientType;
|
||||||
|
private String differentiator;
|
||||||
|
private String status;
|
||||||
|
private String userId;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime completedAt;
|
||||||
|
private List<StatusHistoryEntry> statusHistory;
|
||||||
|
|
||||||
|
public AnalysisHistoryResponse() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAnalysisId() {
|
||||||
|
return analysisId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAnalysisId(String analysisId) {
|
||||||
|
this.analysisId = analysisId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getProduct() {
|
||||||
|
return product;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProduct(String product) {
|
||||||
|
this.product = product;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLocation() {
|
||||||
|
return location;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLocation(String location) {
|
||||||
|
this.location = location;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClientType() {
|
||||||
|
return clientType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClientType(String clientType) {
|
||||||
|
this.clientType = clientType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDifferentiator() {
|
||||||
|
return differentiator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDifferentiator(String differentiator) {
|
||||||
|
this.differentiator = differentiator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(String status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserId() {
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserId(String userId) {
|
||||||
|
this.userId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCompletedAt() {
|
||||||
|
return completedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCompletedAt(LocalDateTime completedAt) {
|
||||||
|
this.completedAt = completedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<StatusHistoryEntry> getStatusHistory() {
|
||||||
|
return statusHistory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatusHistory(List<StatusHistoryEntry> statusHistory) {
|
||||||
|
this.statusHistory = statusHistory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package kz.konturai.parser.dto;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
public class StatusHistoryEntry {
|
||||||
|
private String status;
|
||||||
|
private LocalDateTime timestamp;
|
||||||
|
private String message;
|
||||||
|
|
||||||
|
public StatusHistoryEntry() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public StatusHistoryEntry(String status, LocalDateTime timestamp) {
|
||||||
|
this.status = status;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StatusHistoryEntry(String status, LocalDateTime timestamp, String message) {
|
||||||
|
this.status = status;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(String status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getTimestamp() {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTimestamp(LocalDateTime timestamp) {
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMessage(String message) {
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package kz.konturai.parser.dto;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class StrategyHistoryResponse {
|
||||||
|
private String strategyId;
|
||||||
|
private String analysisId;
|
||||||
|
private String status;
|
||||||
|
private String userId;
|
||||||
|
private Integer durationWeeks;
|
||||||
|
private List<String> priorityPlatforms;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime completedAt;
|
||||||
|
private List<StatusHistoryEntry> statusHistory;
|
||||||
|
|
||||||
|
public StrategyHistoryResponse() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStrategyId() {
|
||||||
|
return strategyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStrategyId(String strategyId) {
|
||||||
|
this.strategyId = strategyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAnalysisId() {
|
||||||
|
return analysisId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAnalysisId(String analysisId) {
|
||||||
|
this.analysisId = analysisId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(String status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserId() {
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserId(String userId) {
|
||||||
|
this.userId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getDurationWeeks() {
|
||||||
|
return durationWeeks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDurationWeeks(Integer durationWeeks) {
|
||||||
|
this.durationWeeks = durationWeeks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getPriorityPlatforms() {
|
||||||
|
return priorityPlatforms;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPriorityPlatforms(List<String> priorityPlatforms) {
|
||||||
|
this.priorityPlatforms = priorityPlatforms;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCompletedAt() {
|
||||||
|
return completedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCompletedAt(LocalDateTime completedAt) {
|
||||||
|
this.completedAt = completedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<StatusHistoryEntry> getStatusHistory() {
|
||||||
|
return statusHistory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatusHistory(List<StatusHistoryEntry> statusHistory) {
|
||||||
|
this.statusHistory = statusHistory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
package kz.konturai.parser.model;
|
package kz.konturai.parser.model;
|
||||||
|
|
||||||
|
import kz.konturai.parser.dto.StatusHistoryEntry;
|
||||||
import org.springframework.data.annotation.Id;
|
import org.springframework.data.annotation.Id;
|
||||||
import org.springframework.data.mongodb.core.mapping.Document;
|
import org.springframework.data.mongodb.core.mapping.Document;
|
||||||
import org.springframework.data.mongodb.core.mapping.Field;
|
import org.springframework.data.mongodb.core.mapping.Field;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@Document(collection = "marketing_analysis")
|
@Document(collection = "marketing_analysis")
|
||||||
@@ -46,9 +49,13 @@ public class MarketingAnalysis {
|
|||||||
@Field("user_id")
|
@Field("user_id")
|
||||||
private String userId; // Optional, for future authentication
|
private String userId; // Optional, for future authentication
|
||||||
|
|
||||||
|
@Field("status_history")
|
||||||
|
private List<StatusHistoryEntry> statusHistory;
|
||||||
|
|
||||||
public MarketingAnalysis() {
|
public MarketingAnalysis() {
|
||||||
this.createdAt = LocalDateTime.now();
|
this.createdAt = LocalDateTime.now();
|
||||||
this.status = "queued";
|
this.status = "queued";
|
||||||
|
this.statusHistory = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public MarketingAnalysis(String product, String location, String clientType, String differentiator) {
|
public MarketingAnalysis(String product, String location, String clientType, String differentiator) {
|
||||||
@@ -154,4 +161,12 @@ public class MarketingAnalysis {
|
|||||||
public void setUserId(String userId) {
|
public void setUserId(String userId) {
|
||||||
this.userId = userId;
|
this.userId = userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<StatusHistoryEntry> getStatusHistory() {
|
||||||
|
return statusHistory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatusHistory(List<StatusHistoryEntry> statusHistory) {
|
||||||
|
this.statusHistory = statusHistory;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package kz.konturai.parser.model;
|
package kz.konturai.parser.model;
|
||||||
|
|
||||||
|
import kz.konturai.parser.dto.StatusHistoryEntry;
|
||||||
import org.springframework.data.annotation.Id;
|
import org.springframework.data.annotation.Id;
|
||||||
import org.springframework.data.mongodb.core.mapping.Document;
|
import org.springframework.data.mongodb.core.mapping.Document;
|
||||||
import org.springframework.data.mongodb.core.mapping.Field;
|
import org.springframework.data.mongodb.core.mapping.Field;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -41,9 +43,16 @@ public class MarketingStrategy {
|
|||||||
@Field("strategy_data")
|
@Field("strategy_data")
|
||||||
private Map<String, Object> strategyData; // JSON data with full strategy content
|
private Map<String, Object> strategyData; // JSON data with full strategy content
|
||||||
|
|
||||||
|
@Field("user_id")
|
||||||
|
private String userId;
|
||||||
|
|
||||||
|
@Field("status_history")
|
||||||
|
private List<StatusHistoryEntry> statusHistory;
|
||||||
|
|
||||||
public MarketingStrategy() {
|
public MarketingStrategy() {
|
||||||
this.createdAt = LocalDateTime.now();
|
this.createdAt = LocalDateTime.now();
|
||||||
this.status = "queued";
|
this.status = "queued";
|
||||||
|
this.statusHistory = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public MarketingStrategy(String analysisId) {
|
public MarketingStrategy(String analysisId) {
|
||||||
@@ -131,6 +140,22 @@ public class MarketingStrategy {
|
|||||||
this.strategyData = strategyData;
|
this.strategyData = strategyData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getUserId() {
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserId(String userId) {
|
||||||
|
this.userId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<StatusHistoryEntry> getStatusHistory() {
|
||||||
|
return statusHistory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatusHistory(List<StatusHistoryEntry> statusHistory) {
|
||||||
|
this.statusHistory = statusHistory;
|
||||||
|
}
|
||||||
|
|
||||||
public static class WeeklyPlan {
|
public static class WeeklyPlan {
|
||||||
@Field("week_number")
|
@Field("week_number")
|
||||||
private Integer weekNumber;
|
private Integer weekNumber;
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import kz.konturai.parser.model.MarketingAnalysis;
|
|||||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface MarketingAnalysisRepository extends MongoRepository<MarketingAnalysis, String> {
|
public interface MarketingAnalysisRepository extends MongoRepository<MarketingAnalysis, String> {
|
||||||
Optional<MarketingAnalysis> findById(String id);
|
Optional<MarketingAnalysis> findById(String id);
|
||||||
|
List<MarketingAnalysis> findByUserIdOrderByCreatedAtDesc(String userId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,14 @@ import kz.konturai.parser.model.MarketingStrategy;
|
|||||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface MarketingStrategyRepository extends MongoRepository<MarketingStrategy, String> {
|
public interface MarketingStrategyRepository extends MongoRepository<MarketingStrategy, String> {
|
||||||
Optional<MarketingStrategy> findById(String id);
|
Optional<MarketingStrategy> findById(String id);
|
||||||
Optional<MarketingStrategy> findByAnalysisId(String analysisId);
|
Optional<MarketingStrategy> findByAnalysisId(String analysisId);
|
||||||
|
List<MarketingStrategy> findByUserIdOrderByCreatedAtDesc(String userId);
|
||||||
|
List<MarketingStrategy> findByUserIdAndAnalysisId(String userId, String analysisId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
package kz.konturai.parser.service;
|
||||||
|
|
||||||
|
import io.jsonwebtoken.Claims;
|
||||||
|
import io.jsonwebtoken.JwtException;
|
||||||
|
import io.jsonwebtoken.Jwts;
|
||||||
|
import io.jsonwebtoken.io.Decoders;
|
||||||
|
import io.jsonwebtoken.security.Keys;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class JwtService {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(JwtService.class);
|
||||||
|
private final SecretKey signingKey;
|
||||||
|
|
||||||
|
public JwtService(
|
||||||
|
@Value("${security.jwt.secret-base64:}") String base64Secret) {
|
||||||
|
if (StringUtils.hasText(base64Secret)) {
|
||||||
|
this.signingKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(base64Secret));
|
||||||
|
} else {
|
||||||
|
// Если секрет не настроен, создаем временный ключ (для разработки)
|
||||||
|
// В продакшене это должно быть обязательно настроено
|
||||||
|
logger.warn("JWT secret not configured, using default key. This should be configured in production!");
|
||||||
|
String defaultSecret = "dGVzdC1zZWNyZXQta2V5LWZvci1kZXZlbG9wbWVudC1vbmx5LWRvLW5vdC11c2UtaW4tcHJvZHVjdGlvbg==";
|
||||||
|
this.signingKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(defaultSecret));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Парсит и валидирует JWT токен
|
||||||
|
*
|
||||||
|
* @param token JWT токен
|
||||||
|
* @return Claims из токена
|
||||||
|
* @throws JwtException если токен невалиден или истёк
|
||||||
|
*/
|
||||||
|
public Claims parseAndValidate(String token) throws JwtException {
|
||||||
|
return Jwts.parser()
|
||||||
|
.verifyWith(signingKey)
|
||||||
|
.build()
|
||||||
|
.parseSignedClaims(token)
|
||||||
|
.getPayload();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Извлекает JWT токен из заголовка Authorization
|
||||||
|
*
|
||||||
|
* @param authHeader значение заголовка Authorization (например, "Bearer
|
||||||
|
* <token>")
|
||||||
|
* @return JWT токен или null если не найден
|
||||||
|
*/
|
||||||
|
public String extractTokenFromHeader(String authHeader) {
|
||||||
|
if (!StringUtils.hasText(authHeader)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authHeader.startsWith("Bearer ")) {
|
||||||
|
return authHeader.substring(7);
|
||||||
|
}
|
||||||
|
|
||||||
|
return authHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Извлекает userId (uid) из JWT токена
|
||||||
|
* Согласно документации, userId находится в поле "uid" как Long
|
||||||
|
*
|
||||||
|
* Структура JWT токена:
|
||||||
|
* - sub: Email пользователя
|
||||||
|
* - uid: ID пользователя (Long)
|
||||||
|
* - roles: Роли пользователя (String, разделённые запятыми)
|
||||||
|
*
|
||||||
|
* @param token JWT токен
|
||||||
|
* @return userId как String или null если не найден
|
||||||
|
*/
|
||||||
|
public String extractUserIdFromToken(String token) {
|
||||||
|
if (!StringUtils.hasText(token)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Claims claims = parseAndValidate(token);
|
||||||
|
|
||||||
|
// Основной способ: извлекаем uid (Long) из claims согласно документации
|
||||||
|
Long uid = claims.get("uid", Long.class);
|
||||||
|
if (uid != null) {
|
||||||
|
return String.valueOf(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: пробуем другие поля, если uid не найден (для совместимости)
|
||||||
|
if (claims.get("userId") != null) {
|
||||||
|
logger.debug("Using 'userId' field as fallback");
|
||||||
|
return String.valueOf(claims.get("userId"));
|
||||||
|
}
|
||||||
|
if (claims.get("id") != null) {
|
||||||
|
logger.debug("Using 'id' field as fallback");
|
||||||
|
return String.valueOf(claims.get("id"));
|
||||||
|
}
|
||||||
|
if (claims.get("user_id") != null) {
|
||||||
|
logger.debug("Using 'user_id' field as fallback");
|
||||||
|
return String.valueOf(claims.get("user_id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если ничего не найдено, пробуем subject (email) как последний вариант
|
||||||
|
// (не рекомендуется, но может быть полезно для обратной совместимости)
|
||||||
|
String subject = claims.getSubject();
|
||||||
|
if (subject != null && !subject.isEmpty()) {
|
||||||
|
logger.warn("Using subject (email) as userId fallback. Token should contain 'uid' field: {}", subject);
|
||||||
|
return subject;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warn("No userId field (uid) found in JWT token");
|
||||||
|
return null;
|
||||||
|
|
||||||
|
} catch (io.jsonwebtoken.ExpiredJwtException e) {
|
||||||
|
logger.error("JWT token has expired: {}", e.getMessage());
|
||||||
|
return null;
|
||||||
|
} catch (io.jsonwebtoken.MalformedJwtException e) {
|
||||||
|
logger.error("Malformed JWT token: {}", e.getMessage());
|
||||||
|
return null;
|
||||||
|
} catch (io.jsonwebtoken.security.SignatureException e) {
|
||||||
|
logger.error("Invalid JWT signature: {}", e.getMessage());
|
||||||
|
return null;
|
||||||
|
} catch (JwtException e) {
|
||||||
|
logger.error("Error parsing/validating JWT token: {}", e.getMessage());
|
||||||
|
return null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Unexpected error parsing JWT token: {}", e.getMessage(), e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Извлекает userId из заголовка Authorization
|
||||||
|
*
|
||||||
|
* @param authHeader значение заголовка Authorization
|
||||||
|
* @return userId или null если не найден
|
||||||
|
*/
|
||||||
|
public String extractUserIdFromHeader(String authHeader) {
|
||||||
|
String token = extractTokenFromHeader(authHeader);
|
||||||
|
if (token == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return extractUserIdFromToken(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Извлекает email (subject) из JWT токена
|
||||||
|
*
|
||||||
|
* @param token JWT токен
|
||||||
|
* @return email или null если не найден
|
||||||
|
*/
|
||||||
|
public String extractEmailFromToken(String token) {
|
||||||
|
if (!StringUtils.hasText(token)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Claims claims = parseAndValidate(token);
|
||||||
|
return claims.getSubject();
|
||||||
|
} catch (JwtException e) {
|
||||||
|
logger.error("Error parsing/validating JWT token: {}", e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Извлекает роли из JWT токена
|
||||||
|
*
|
||||||
|
* @param token JWT токен
|
||||||
|
* @return список ролей или пустой список
|
||||||
|
*/
|
||||||
|
public java.util.List<String> extractRolesFromToken(String token) {
|
||||||
|
if (!StringUtils.hasText(token)) {
|
||||||
|
return java.util.List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Claims claims = parseAndValidate(token);
|
||||||
|
String rolesString = claims.get("roles", String.class);
|
||||||
|
|
||||||
|
if (rolesString == null || rolesString.isBlank()) {
|
||||||
|
return java.util.List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
return java.util.Arrays.stream(rolesString.split(","))
|
||||||
|
.map(String::trim)
|
||||||
|
.filter(s -> !s.isEmpty())
|
||||||
|
.collect(java.util.stream.Collectors.toList());
|
||||||
|
} catch (JwtException e) {
|
||||||
|
logger.error("Error parsing/validating JWT token: {}", e.getMessage());
|
||||||
|
return java.util.List.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package kz.konturai.parser.service;
|
|||||||
|
|
||||||
import kz.konturai.parser.dto.MarketingAnalysisRequest;
|
import kz.konturai.parser.dto.MarketingAnalysisRequest;
|
||||||
import kz.konturai.parser.dto.MarketingAnalysisResult;
|
import kz.konturai.parser.dto.MarketingAnalysisResult;
|
||||||
|
import kz.konturai.parser.dto.StatusHistoryEntry;
|
||||||
import kz.konturai.parser.model.MarketingAnalysis;
|
import kz.konturai.parser.model.MarketingAnalysis;
|
||||||
import kz.konturai.parser.repository.MarketingAnalysisRepository;
|
import kz.konturai.parser.repository.MarketingAnalysisRepository;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -34,18 +35,28 @@ public class MarketingAnalysisService {
|
|||||||
this.researchPdfService = researchPdfService;
|
this.researchPdfService = researchPdfService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MarketingAnalysis startAnalysis(MarketingAnalysisRequest request) {
|
public MarketingAnalysis startAnalysis(MarketingAnalysisRequest request, String userId) {
|
||||||
MarketingAnalysis analysis = new MarketingAnalysis(
|
MarketingAnalysis analysis = new MarketingAnalysis(
|
||||||
request.getProduct(),
|
request.getProduct(),
|
||||||
request.getLocation(),
|
request.getLocation(),
|
||||||
request.getClient(),
|
request.getClient(),
|
||||||
request.getDifferentiator());
|
request.getDifferentiator());
|
||||||
|
analysis.setUserId(userId);
|
||||||
analysis.setStatus("queued");
|
analysis.setStatus("queued");
|
||||||
|
addStatusHistoryEntry(analysis, "queued", "Анализ создан и добавлен в очередь");
|
||||||
analysis = repository.save(analysis);
|
analysis = repository.save(analysis);
|
||||||
logger.info("Marketing analysis created with ID: {}", analysis.getId());
|
logger.info("Marketing analysis created with ID: {} for user: {}", analysis.getId(), userId);
|
||||||
return analysis;
|
return analysis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void addStatusHistoryEntry(MarketingAnalysis analysis, String status, String message) {
|
||||||
|
if (analysis.getStatusHistory() == null) {
|
||||||
|
analysis.setStatusHistory(new ArrayList<>());
|
||||||
|
}
|
||||||
|
StatusHistoryEntry entry = new StatusHistoryEntry(status, LocalDateTime.now(), message);
|
||||||
|
analysis.getStatusHistory().add(entry);
|
||||||
|
}
|
||||||
|
|
||||||
@Async("reportGenerationExecutor")
|
@Async("reportGenerationExecutor")
|
||||||
public void processAnalysis(String analysisId, MarketingAnalysisRequest request) {
|
public void processAnalysis(String analysisId, MarketingAnalysisRequest request) {
|
||||||
try {
|
try {
|
||||||
@@ -57,6 +68,7 @@ public class MarketingAnalysisService {
|
|||||||
|
|
||||||
MarketingAnalysis analysis = optAnalysis.get();
|
MarketingAnalysis analysis = optAnalysis.get();
|
||||||
analysis.setStatus("processing");
|
analysis.setStatus("processing");
|
||||||
|
addStatusHistoryEntry(analysis, "processing", "Начата обработка анализа");
|
||||||
repository.save(analysis);
|
repository.save(analysis);
|
||||||
|
|
||||||
logger.info("Starting marketing analysis processing for ID: {}", analysisId);
|
logger.info("Starting marketing analysis processing for ID: {}", analysisId);
|
||||||
@@ -83,6 +95,7 @@ public class MarketingAnalysisService {
|
|||||||
analysis.setReportData(reportData);
|
analysis.setReportData(reportData);
|
||||||
analysis.setPdfFilename(filename);
|
analysis.setPdfFilename(filename);
|
||||||
analysis.setPdfFilePath(filename);
|
analysis.setPdfFilePath(filename);
|
||||||
|
addStatusHistoryEntry(analysis, "completed", "Анализ успешно завершен");
|
||||||
repository.save(analysis);
|
repository.save(analysis);
|
||||||
|
|
||||||
logger.info("Marketing analysis completed successfully for ID: {}", analysisId);
|
logger.info("Marketing analysis completed successfully for ID: {}", analysisId);
|
||||||
@@ -94,6 +107,7 @@ public class MarketingAnalysisService {
|
|||||||
if (optAnalysis.isPresent()) {
|
if (optAnalysis.isPresent()) {
|
||||||
MarketingAnalysis analysis = optAnalysis.get();
|
MarketingAnalysis analysis = optAnalysis.get();
|
||||||
analysis.setStatus("failed");
|
analysis.setStatus("failed");
|
||||||
|
addStatusHistoryEntry(analysis, "failed", "Ошибка при обработке: " + e.getMessage());
|
||||||
repository.save(analysis);
|
repository.save(analysis);
|
||||||
}
|
}
|
||||||
} catch (Exception saveError) {
|
} catch (Exception saveError) {
|
||||||
@@ -350,6 +364,10 @@ public class MarketingAnalysisService {
|
|||||||
return repository.findById(analysisId);
|
return repository.findById(analysisId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<MarketingAnalysis> getUserAnalyses(String userId) {
|
||||||
|
return repository.findByUserIdOrderByCreatedAtDesc(userId);
|
||||||
|
}
|
||||||
|
|
||||||
private MarketingAnalysisResult.MarketingReport buildReportFromData(Map<String, Object> reportData,
|
private MarketingAnalysisResult.MarketingReport buildReportFromData(Map<String, Object> reportData,
|
||||||
MarketingAnalysis analysis) {
|
MarketingAnalysis analysis) {
|
||||||
MarketingAnalysisResult.MarketingReport report = new MarketingAnalysisResult.MarketingReport();
|
MarketingAnalysisResult.MarketingReport report = new MarketingAnalysisResult.MarketingReport();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import kz.konturai.parser.dto.MarketingAnalysisResult;
|
import kz.konturai.parser.dto.MarketingAnalysisResult;
|
||||||
import kz.konturai.parser.dto.MarketingStrategyRequest;
|
import kz.konturai.parser.dto.MarketingStrategyRequest;
|
||||||
import kz.konturai.parser.dto.MarketingStrategyResponse;
|
import kz.konturai.parser.dto.MarketingStrategyResponse;
|
||||||
|
import kz.konturai.parser.dto.StatusHistoryEntry;
|
||||||
import kz.konturai.parser.model.MarketingStrategy;
|
import kz.konturai.parser.model.MarketingStrategy;
|
||||||
import kz.konturai.parser.repository.MarketingStrategyRepository;
|
import kz.konturai.parser.repository.MarketingStrategyRepository;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -36,7 +37,7 @@ public class MarketingStrategyService {
|
|||||||
this.openAIAnalyticsService = openAIAnalyticsService;
|
this.openAIAnalyticsService = openAIAnalyticsService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MarketingStrategy generateStrategy(String analysisId, MarketingStrategyRequest request) {
|
public MarketingStrategy generateStrategy(String analysisId, MarketingStrategyRequest request, String userId) {
|
||||||
// Check if analysis exists and is completed
|
// Check if analysis exists and is completed
|
||||||
MarketingAnalysisResult analysisResult = marketingAnalysisService.getAnalysisResult(analysisId);
|
MarketingAnalysisResult analysisResult = marketingAnalysisService.getAnalysisResult(analysisId);
|
||||||
if (analysisResult == null) {
|
if (analysisResult == null) {
|
||||||
@@ -55,12 +56,14 @@ public class MarketingStrategyService {
|
|||||||
|
|
||||||
// Create new strategy
|
// Create new strategy
|
||||||
MarketingStrategy strategy = new MarketingStrategy(analysisId);
|
MarketingStrategy strategy = new MarketingStrategy(analysisId);
|
||||||
|
strategy.setUserId(userId);
|
||||||
strategy.setDurationWeeks(request.getDurationWeeks() != null ? request.getDurationWeeks() : 4);
|
strategy.setDurationWeeks(request.getDurationWeeks() != null ? request.getDurationWeeks() : 4);
|
||||||
strategy.setPriorityPlatforms(request.getPriorityPlatforms());
|
strategy.setPriorityPlatforms(request.getPriorityPlatforms());
|
||||||
strategy.setStatus("queued");
|
strategy.setStatus("queued");
|
||||||
|
addStatusHistoryEntry(strategy, "queued", "Стратегия создана и добавлена в очередь");
|
||||||
strategy = repository.save(strategy);
|
strategy = repository.save(strategy);
|
||||||
|
|
||||||
logger.info("Marketing strategy created with ID: {}", strategy.getId());
|
logger.info("Marketing strategy created with ID: {} for user: {}", strategy.getId(), userId);
|
||||||
|
|
||||||
// Start async processing
|
// Start async processing
|
||||||
processStrategyGeneration(strategy.getId(), analysisId, analysisResult);
|
processStrategyGeneration(strategy.getId(), analysisId, analysisResult);
|
||||||
@@ -68,6 +71,14 @@ public class MarketingStrategyService {
|
|||||||
return strategy;
|
return strategy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void addStatusHistoryEntry(MarketingStrategy strategy, String status, String message) {
|
||||||
|
if (strategy.getStatusHistory() == null) {
|
||||||
|
strategy.setStatusHistory(new ArrayList<>());
|
||||||
|
}
|
||||||
|
StatusHistoryEntry entry = new StatusHistoryEntry(status, LocalDateTime.now(), message);
|
||||||
|
strategy.getStatusHistory().add(entry);
|
||||||
|
}
|
||||||
|
|
||||||
@Async("reportGenerationExecutor")
|
@Async("reportGenerationExecutor")
|
||||||
public void processStrategyGeneration(String strategyId, String analysisId, MarketingAnalysisResult analysisResult) {
|
public void processStrategyGeneration(String strategyId, String analysisId, MarketingAnalysisResult analysisResult) {
|
||||||
try {
|
try {
|
||||||
@@ -79,6 +90,7 @@ public class MarketingStrategyService {
|
|||||||
|
|
||||||
MarketingStrategy strategy = optStrategy.get();
|
MarketingStrategy strategy = optStrategy.get();
|
||||||
strategy.setStatus("processing");
|
strategy.setStatus("processing");
|
||||||
|
addStatusHistoryEntry(strategy, "processing", "Начата генерация стратегии");
|
||||||
repository.save(strategy);
|
repository.save(strategy);
|
||||||
|
|
||||||
logger.info("Starting marketing strategy generation for ID: {}", strategyId);
|
logger.info("Starting marketing strategy generation for ID: {}", strategyId);
|
||||||
@@ -106,6 +118,7 @@ public class MarketingStrategyService {
|
|||||||
strategyData.put("postCalendar", postCalendar);
|
strategyData.put("postCalendar", postCalendar);
|
||||||
strategy.setStrategyData(strategyData);
|
strategy.setStrategyData(strategyData);
|
||||||
|
|
||||||
|
addStatusHistoryEntry(strategy, "completed", "Стратегия успешно сгенерирована");
|
||||||
repository.save(strategy);
|
repository.save(strategy);
|
||||||
|
|
||||||
logger.info("Marketing strategy generation completed successfully for ID: {}", strategyId);
|
logger.info("Marketing strategy generation completed successfully for ID: {}", strategyId);
|
||||||
@@ -117,6 +130,7 @@ public class MarketingStrategyService {
|
|||||||
if (optStrategy.isPresent()) {
|
if (optStrategy.isPresent()) {
|
||||||
MarketingStrategy strategy = optStrategy.get();
|
MarketingStrategy strategy = optStrategy.get();
|
||||||
strategy.setStatus("failed");
|
strategy.setStatus("failed");
|
||||||
|
addStatusHistoryEntry(strategy, "failed", "Ошибка при генерации стратегии: " + e.getMessage());
|
||||||
repository.save(strategy);
|
repository.save(strategy);
|
||||||
}
|
}
|
||||||
} catch (Exception saveError) {
|
} catch (Exception saveError) {
|
||||||
@@ -497,5 +511,13 @@ public class MarketingStrategyService {
|
|||||||
|
|
||||||
return getStrategyResult(optStrategy.get().getId());
|
return getStrategyResult(optStrategy.get().getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<MarketingStrategy> getUserStrategies(String userId) {
|
||||||
|
return repository.findByUserIdOrderByCreatedAtDesc(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<MarketingStrategy> getStrategyById(String strategyId) {
|
||||||
|
return repository.findById(strategyId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -77,3 +77,8 @@ spring.mail.properties.mail.smtp.starttls.required=true
|
|||||||
# Deep Research API Configuration
|
# Deep Research API Configuration
|
||||||
deep-research.api.url=http://185.35.223.45:3051
|
deep-research.api.url=http://185.35.223.45:3051
|
||||||
deep-research.api.timeout=1800000
|
deep-research.api.timeout=1800000
|
||||||
|
|
||||||
|
# JWT Configuration
|
||||||
|
# Используйте тот же secret-base64, что и в сервисе, выдающем токены
|
||||||
|
# Формат: base64-encoded secret key
|
||||||
|
security.jwt.secret-base64=ZmFrZV9zZWNyZXRfMTIzNDU2Nzg5MGFiY2RlZmFrZV9zZWNyZXRfMTIzNDU2Nzg5MGFiY2Rl
|
||||||
|
|||||||
Reference in New Issue
Block a user