.
This commit is contained in:
@@ -16,6 +16,7 @@ API для генерации маркетингового анализа и с
|
||||
- ✅ Пользователи могут видеть только свои анализы и стратегии
|
||||
- ✅ Добавлена детальная история статусов для каждого анализа и стратегии
|
||||
- ✅ Новые эндпоинты для получения списка всех анализов/стратегий пользователя
|
||||
- ✅ **НОВОЕ**: Добавлена поддержка типов анализов (РЫНОК, КОНКУРЕНТЫ, ЦА, КАНАЛЫ, SWOT)
|
||||
|
||||
---
|
||||
|
||||
@@ -77,6 +78,7 @@ Authorization: Bearer <your_jwt_token>
|
||||
| `location` | string | ✅ | Географическая локация работы | "Алматы, Казахстан" |
|
||||
| `client` | string | ✅ | Тип целевой аудитории | "B2B клиенты" |
|
||||
| `differentiator` | string | ✅ | Уникальные особенности бизнеса | "Быстрая разработка за 2 недели" |
|
||||
| `analysisType` | string | ✅ | Тип анализа для генерации | "РЫНОК" |
|
||||
|
||||
#### Валидация полей
|
||||
|
||||
@@ -104,6 +106,15 @@ Authorization: Bearer <your_jwt_token>
|
||||
- Минимальная длина: 10 символов
|
||||
- Максимальная длина: 500 символов
|
||||
|
||||
**`analysisType`** (string, обязательное)
|
||||
|
||||
- Допустимые значения (регистр не важен, но рекомендуется использовать заглавные буквы):
|
||||
- `"РЫНОК"` - Анализ рынка (размер рынка, динамика роста, сегменты, тренды)
|
||||
- `"КОНКУРЕНТЫ"` - Анализ конкурентов (основные конкуренты, их сильные/слабые стороны, позиционирование)
|
||||
- `"ЦА"` - Анализ целевой аудитории (демография, психография, потребности, поведение)
|
||||
- `"КАНАЛЫ"` - Анализ маркетинговых каналов (эффективность каналов, рекомендации по выбору)
|
||||
- `"SWOT"` - SWOT-анализ (сильные стороны, слабые стороны, возможности, угрозы)
|
||||
|
||||
#### Пример запроса
|
||||
|
||||
```javascript
|
||||
@@ -120,6 +131,7 @@ const response = await fetch(
|
||||
location: 'Нур-Султан, Казахстан',
|
||||
client: 'B2B клиенты',
|
||||
differentiator: 'Специализируемся на быстрой разработке MVP за 4 недели',
|
||||
analysisType: 'РЫНОК',
|
||||
}),
|
||||
}
|
||||
);
|
||||
@@ -976,6 +988,122 @@ async function getMyStrategies(jwtToken) {
|
||||
|
||||
---
|
||||
|
||||
## Изменения: Поддержка типов анализов
|
||||
|
||||
### Обзор изменений
|
||||
|
||||
В API добавлена поддержка различных типов маркетинговых анализов. Теперь при создании анализа можно указать тип анализа, который определяет фокус и содержание генерируемого отчета.
|
||||
|
||||
### Новое поле в запросе
|
||||
|
||||
**Поле**: `analysisType` (обязательное)
|
||||
|
||||
**Тип**: `string`
|
||||
|
||||
**Допустимые значения**:
|
||||
|
||||
- `"РЫНОК"` - Анализ рынка
|
||||
- `"КОНКУРЕНТЫ"` - Анализ конкурентов
|
||||
- `"ЦА"` - Анализ целевой аудитории
|
||||
- `"КАНАЛЫ"` - Анализ маркетинговых каналов
|
||||
- `"SWOT"` - SWOT-анализ
|
||||
|
||||
### Описание типов анализов
|
||||
|
||||
#### 1. РЫНОК
|
||||
|
||||
Генерирует анализ рынка, включающий:
|
||||
|
||||
- Размер рынка
|
||||
- Динамику роста
|
||||
- Основные сегменты
|
||||
- Тренды и перспективы развития
|
||||
|
||||
#### 2. КОНКУРЕНТЫ
|
||||
|
||||
Генерирует анализ конкурентов, включающий:
|
||||
|
||||
- Основных конкурентов
|
||||
- Их сильные и слабые стороны
|
||||
- Позиционирование
|
||||
- Ценовую политику
|
||||
- Маркетинговые стратегии
|
||||
|
||||
#### 3. ЦА (Целевая аудитория)
|
||||
|
||||
Генерирует анализ целевой аудитории, включающий:
|
||||
|
||||
- Демографические характеристики
|
||||
- Психографический профиль
|
||||
- Потребности и боли
|
||||
- Поведенческие паттерны
|
||||
- Предпочтения
|
||||
|
||||
#### 4. КАНАЛЫ
|
||||
|
||||
Генерирует анализ маркетинговых каналов, включающий:
|
||||
|
||||
- Оценку эффективности различных каналов коммуникации
|
||||
- Рекомендации по выбору каналов
|
||||
- Особенности использования каждого канала
|
||||
- Бюджетные рекомендации
|
||||
|
||||
#### 5. SWOT
|
||||
|
||||
Генерирует SWOT-анализ, включающий:
|
||||
|
||||
- Сильные стороны (Strengths)
|
||||
- Слабые стороны (Weaknesses)
|
||||
- Возможности (Opportunities)
|
||||
- Угрозы (Threats)
|
||||
|
||||
### Изменения в ответе
|
||||
|
||||
В ответе анализа теперь присутствует поле `analysisType` в объекте `reportData`, которое содержит название типа анализа.
|
||||
|
||||
### Пример использования
|
||||
|
||||
```javascript
|
||||
// Запрос на создание SWOT-анализа
|
||||
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 недели',
|
||||
analysisType: 'SWOT', // Указываем тип анализа
|
||||
}),
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### Рекомендации для фронтенда
|
||||
|
||||
1. **UI для выбора типа анализа**: Добавьте выпадающий список или радиокнопки для выбора типа анализа перед отправкой запроса.
|
||||
|
||||
2. **Валидация на фронтенде**: Убедитесь, что отправляется только один из допустимых значений.
|
||||
|
||||
3. **Отображение типа анализа**: При показе результатов анализа отображайте тип анализа для контекста.
|
||||
|
||||
4. **Обратная совместимость**: Если поле `analysisType` не указано, API вернет ошибку валидации. Это обязательное поле.
|
||||
|
||||
### Миграция существующих интеграций
|
||||
|
||||
Если у вас есть существующие интеграции, которые не используют поле `analysisType`, необходимо:
|
||||
|
||||
1. Добавить поле `analysisType` в запрос
|
||||
2. Выбрать подходящий тип анализа по умолчанию или позволить пользователю выбрать
|
||||
3. Обновить UI для отображения выбора типа анализа
|
||||
|
||||
---
|
||||
|
||||
## Поддержка
|
||||
|
||||
При возникновении проблем с API обращайтесь в техническую поддержку с указанием:
|
||||
|
||||
@@ -3,10 +3,12 @@ package kz.konturai.parser.controller;
|
||||
import kz.konturai.parser.dto.*;
|
||||
import kz.konturai.parser.model.MarketingAnalysis;
|
||||
import kz.konturai.parser.model.MarketingStrategy;
|
||||
import kz.konturai.parser.model.PostingTask;
|
||||
import kz.konturai.parser.service.JwtService;
|
||||
import kz.konturai.parser.service.MarketingAnalysisService;
|
||||
import kz.konturai.parser.service.MarketingStrategyService;
|
||||
import kz.konturai.parser.service.MinIOService;
|
||||
import kz.konturai.parser.service.PostingTaskService;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -31,16 +33,19 @@ public class MarketingController {
|
||||
private final MarketingStrategyService marketingStrategyService;
|
||||
private final MinIOService minIOService;
|
||||
private final JwtService jwtService;
|
||||
private final PostingTaskService postingTaskService;
|
||||
|
||||
public MarketingController(
|
||||
MarketingAnalysisService marketingAnalysisService,
|
||||
MarketingStrategyService marketingStrategyService,
|
||||
MinIOService minIOService,
|
||||
JwtService jwtService) {
|
||||
JwtService jwtService,
|
||||
PostingTaskService postingTaskService) {
|
||||
this.marketingAnalysisService = marketingAnalysisService;
|
||||
this.marketingStrategyService = marketingStrategyService;
|
||||
this.minIOService = minIOService;
|
||||
this.jwtService = jwtService;
|
||||
this.postingTaskService = postingTaskService;
|
||||
}
|
||||
|
||||
private String extractUserIdFromHeader(String authHeader) {
|
||||
@@ -403,6 +408,7 @@ public class MarketingController {
|
||||
response.setLocation(analysis.getLocation());
|
||||
response.setClientType(analysis.getClientType());
|
||||
response.setDifferentiator(analysis.getDifferentiator());
|
||||
response.setAnalysisType(analysis.getAnalysisType());
|
||||
response.setStatus(analysis.getStatus());
|
||||
response.setUserId(analysis.getUserId());
|
||||
response.setCreatedAt(analysis.getCreatedAt());
|
||||
@@ -425,6 +431,85 @@ public class MarketingController {
|
||||
return response;
|
||||
}
|
||||
|
||||
@PostMapping("/strategy/{strategyId}/start")
|
||||
public ResponseEntity<?> startStrategy(
|
||||
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||
@PathVariable String strategyId,
|
||||
@RequestBody(required = false) StartStrategyRequest request) {
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
if (!"completed".equals(strategy.getStatus())) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"INVALID_STATUS",
|
||||
"Стратегия еще не завершена. Статус: " + strategy.getStatus());
|
||||
return ResponseEntity.status(400)
|
||||
.body(ApiResponse.error("Стратегия не готова к запуску", error));
|
||||
}
|
||||
|
||||
try {
|
||||
// Создаем задачи из стратегии
|
||||
List<PostingTask> tasks = postingTaskService.createTasksFromStrategy(strategyId);
|
||||
|
||||
// Получаем список платформ
|
||||
List<String> platforms = tasks.stream()
|
||||
.map(PostingTask::getPlatform)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
StartStrategyResponse response = new StartStrategyResponse(
|
||||
strategyId,
|
||||
tasks.size(),
|
||||
platforms,
|
||||
"Стратегия успешно запущена. Создано задач: " + tasks.size());
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(
|
||||
"Стратегия успешно запущена",
|
||||
response));
|
||||
|
||||
} catch (IllegalStateException e) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"MISSING_CREDENTIALS",
|
||||
e.getMessage());
|
||||
return ResponseEntity.status(400)
|
||||
.body(ApiResponse.error("Не удалось запустить стратегию", error));
|
||||
} catch (IllegalArgumentException e) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"INVALID_STRATEGY",
|
||||
e.getMessage());
|
||||
return ResponseEntity.status(404)
|
||||
.body(ApiResponse.error("Стратегия не найдена", error));
|
||||
} catch (Exception e) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"INTERNAL_SERVER_ERROR",
|
||||
"Произошла ошибка при запуске стратегии");
|
||||
return ResponseEntity.status(500)
|
||||
.body(ApiResponse.error("Внутренняя ошибка сервера", error));
|
||||
}
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public ResponseEntity<ApiResponse<ErrorResponse>> handleValidationException(
|
||||
MethodArgumentNotValidException ex) {
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
package kz.konturai.parser.controller;
|
||||
|
||||
import kz.konturai.parser.dto.*;
|
||||
import kz.konturai.parser.model.SocialMediaCredentials;
|
||||
import kz.konturai.parser.service.JwtService;
|
||||
import kz.konturai.parser.service.SocialMediaCredentialsService;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/social-media/credentials")
|
||||
public class SocialMediaCredentialsController {
|
||||
|
||||
private final SocialMediaCredentialsService credentialsService;
|
||||
private final JwtService jwtService;
|
||||
|
||||
public SocialMediaCredentialsController(
|
||||
SocialMediaCredentialsService credentialsService,
|
||||
JwtService jwtService) {
|
||||
this.credentialsService = credentialsService;
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохранение или обновление credentials для платформы
|
||||
* POST /api/social-media/credentials
|
||||
*/
|
||||
@PostMapping
|
||||
public ResponseEntity<?> saveCredentials(
|
||||
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||
@Valid @RequestBody SocialMediaCredentialsRequest request) {
|
||||
|
||||
String userId = extractUserIdFromHeader(authHeader);
|
||||
if (userId == null) {
|
||||
return unauthorizedResponse();
|
||||
}
|
||||
|
||||
try {
|
||||
SocialMediaCredentials credentials = credentialsService.saveCredentials(
|
||||
userId,
|
||||
request.getPlatform(),
|
||||
request.getCredentials()
|
||||
);
|
||||
|
||||
SocialMediaCredentialsResponse response = new SocialMediaCredentialsResponse(
|
||||
credentials.getPlatform(),
|
||||
true,
|
||||
credentials.getCreatedAt(),
|
||||
credentials.getUpdatedAt()
|
||||
);
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(
|
||||
"Credentials успешно сохранены для платформы " + request.getPlatform(),
|
||||
response));
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
ErrorResponse error = new ErrorResponse("VALIDATION_ERROR", e.getMessage());
|
||||
return ResponseEntity.badRequest()
|
||||
.body(ApiResponse.error("Ошибка валидации", error));
|
||||
} catch (Exception e) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"INTERNAL_SERVER_ERROR",
|
||||
"Произошла ошибка при сохранении credentials");
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(ApiResponse.error("Внутренняя ошибка сервера", error));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение информации о credentials для платформы
|
||||
* GET /api/social-media/credentials/{platform}
|
||||
*/
|
||||
@GetMapping("/{platform}")
|
||||
public ResponseEntity<?> getCredentials(
|
||||
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||
@PathVariable String platform) {
|
||||
|
||||
String userId = extractUserIdFromHeader(authHeader);
|
||||
if (userId == null) {
|
||||
return unauthorizedResponse();
|
||||
}
|
||||
|
||||
boolean hasCredentials = credentialsService.hasCredentials(userId, platform);
|
||||
SocialMediaCredentialsResponse response = new SocialMediaCredentialsResponse(
|
||||
platform,
|
||||
hasCredentials
|
||||
);
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение списка всех платформ с credentials
|
||||
* GET /api/social-media/credentials
|
||||
*/
|
||||
@GetMapping
|
||||
public ResponseEntity<?> getAllCredentials(
|
||||
@RequestHeader(value = "Authorization", required = false) String authHeader) {
|
||||
|
||||
String userId = extractUserIdFromHeader(authHeader);
|
||||
if (userId == null) {
|
||||
return unauthorizedResponse();
|
||||
}
|
||||
|
||||
List<SocialMediaCredentials> credentialsList = credentialsService.getUserCredentials(userId);
|
||||
List<SocialMediaCredentialsResponse> responseList = credentialsList.stream()
|
||||
.map(cred -> new SocialMediaCredentialsResponse(
|
||||
cred.getPlatform(),
|
||||
true,
|
||||
cred.getCreatedAt(),
|
||||
cred.getUpdatedAt()
|
||||
))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(responseList));
|
||||
}
|
||||
|
||||
/**
|
||||
* Удаление credentials для платформы
|
||||
* DELETE /api/social-media/credentials/{platform}
|
||||
*/
|
||||
@DeleteMapping("/{platform}")
|
||||
public ResponseEntity<?> deleteCredentials(
|
||||
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||
@PathVariable String platform) {
|
||||
|
||||
String userId = extractUserIdFromHeader(authHeader);
|
||||
if (userId == null) {
|
||||
return unauthorizedResponse();
|
||||
}
|
||||
|
||||
try {
|
||||
if (!credentialsService.hasCredentials(userId, platform)) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"NOT_FOUND",
|
||||
"Credentials для платформы " + platform + " не найдены");
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.body(ApiResponse.error("Credentials не найдены", error));
|
||||
}
|
||||
|
||||
credentialsService.deleteCredentials(userId, platform);
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(
|
||||
"Credentials для платформы " + platform + " успешно удалены",
|
||||
null));
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
ErrorResponse error = new ErrorResponse("VALIDATION_ERROR", e.getMessage());
|
||||
return ResponseEntity.badRequest()
|
||||
.body(ApiResponse.error("Ошибка валидации", error));
|
||||
} catch (Exception e) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"INTERNAL_SERVER_ERROR",
|
||||
"Произошла ошибка при удалении credentials");
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(ApiResponse.error("Внутренняя ошибка сервера", error));
|
||||
}
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public ResponseEntity<ApiResponse<ErrorResponse>> handleValidationException(
|
||||
MethodArgumentNotValidException ex) {
|
||||
|
||||
Map<String, String> details = new HashMap<>();
|
||||
ex.getBindingResult().getFieldErrors().forEach(error -> {
|
||||
details.put(error.getField(), error.getDefaultMessage());
|
||||
});
|
||||
|
||||
ErrorResponse errorResponse = new ErrorResponse(
|
||||
"VALIDATION_ERROR",
|
||||
"Ошибка валидации входных данных",
|
||||
details);
|
||||
|
||||
return ResponseEntity.badRequest()
|
||||
.body(ApiResponse.error("Ошибка валидации", errorResponse));
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ApiResponse<ErrorResponse>> handleGenericException(Exception e) {
|
||||
ErrorResponse errorResponse = new ErrorResponse(
|
||||
"INTERNAL_SERVER_ERROR",
|
||||
"Произошла внутренняя ошибка сервера. Попробуйте позже.");
|
||||
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(ApiResponse.error("Внутренняя ошибка сервера", errorResponse));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ public class AnalysisHistoryResponse {
|
||||
private String location;
|
||||
private String clientType;
|
||||
private String differentiator;
|
||||
private String analysisType;
|
||||
private String status;
|
||||
private String userId;
|
||||
private LocalDateTime createdAt;
|
||||
@@ -97,5 +98,12 @@ public class AnalysisHistoryResponse {
|
||||
public void setStatusHistory(List<StatusHistoryEntry> statusHistory) {
|
||||
this.statusHistory = statusHistory;
|
||||
}
|
||||
}
|
||||
|
||||
public String getAnalysisType() {
|
||||
return analysisType;
|
||||
}
|
||||
|
||||
public void setAnalysisType(String analysisType) {
|
||||
this.analysisType = analysisType;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package kz.konturai.parser.dto;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import kz.konturai.parser.model.AnalysisType;
|
||||
import kz.konturai.parser.validator.ValidAnalysisType;
|
||||
import kz.konturai.parser.validator.ValidClientType;
|
||||
|
||||
public class MarketingAnalysisRequest {
|
||||
@@ -25,14 +27,20 @@ public class MarketingAnalysisRequest {
|
||||
@Size(min = 10, max = 500, message = "Поле 'differentiator' должно содержать от 10 до 500 символов")
|
||||
private String differentiator;
|
||||
|
||||
@NotBlank(message = "Поле 'analysisType' обязательно для заполнения")
|
||||
@ValidAnalysisType(message = "Поле 'analysisType' должно быть одним из: РЫНОК, КОНКУРЕНТЫ, ЦА, КАНАЛЫ, SWOT")
|
||||
private String analysisType;
|
||||
|
||||
public MarketingAnalysisRequest() {
|
||||
}
|
||||
|
||||
public MarketingAnalysisRequest(String product, String location, String client, String differentiator) {
|
||||
public MarketingAnalysisRequest(String product, String location, String client, String differentiator,
|
||||
String analysisType) {
|
||||
this.product = product;
|
||||
this.location = location;
|
||||
this.client = client;
|
||||
this.differentiator = differentiator;
|
||||
this.analysisType = analysisType;
|
||||
}
|
||||
|
||||
public String getProduct() {
|
||||
@@ -66,4 +74,16 @@ public class MarketingAnalysisRequest {
|
||||
public void setDifferentiator(String differentiator) {
|
||||
this.differentiator = differentiator;
|
||||
}
|
||||
|
||||
public String getAnalysisType() {
|
||||
return analysisType;
|
||||
}
|
||||
|
||||
public void setAnalysisType(String analysisType) {
|
||||
this.analysisType = analysisType;
|
||||
}
|
||||
|
||||
public AnalysisType getAnalysisTypeEnum() {
|
||||
return AnalysisType.fromString(analysisType);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package kz.konturai.parser.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public class SocialMediaCredentialsRequest {
|
||||
|
||||
@NotBlank(message = "Platform is required")
|
||||
private String platform;
|
||||
|
||||
@NotBlank(message = "Credentials are required")
|
||||
private String credentials;
|
||||
|
||||
public SocialMediaCredentialsRequest() {
|
||||
}
|
||||
|
||||
public SocialMediaCredentialsRequest(String platform, String credentials) {
|
||||
this.platform = platform;
|
||||
this.credentials = credentials;
|
||||
}
|
||||
|
||||
public String getPlatform() {
|
||||
return platform;
|
||||
}
|
||||
|
||||
public void setPlatform(String platform) {
|
||||
this.platform = platform;
|
||||
}
|
||||
|
||||
public String getCredentials() {
|
||||
return credentials;
|
||||
}
|
||||
|
||||
public void setCredentials(String credentials) {
|
||||
this.credentials = credentials;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package kz.konturai.parser.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class SocialMediaCredentialsResponse {
|
||||
|
||||
private String platform;
|
||||
private boolean hasCredentials;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public SocialMediaCredentialsResponse() {
|
||||
}
|
||||
|
||||
public SocialMediaCredentialsResponse(String platform, boolean hasCredentials) {
|
||||
this.platform = platform;
|
||||
this.hasCredentials = hasCredentials;
|
||||
}
|
||||
|
||||
public SocialMediaCredentialsResponse(String platform, boolean hasCredentials,
|
||||
LocalDateTime createdAt, LocalDateTime updatedAt) {
|
||||
this.platform = platform;
|
||||
this.hasCredentials = hasCredentials;
|
||||
this.createdAt = createdAt;
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
public String getPlatform() {
|
||||
return platform;
|
||||
}
|
||||
|
||||
public void setPlatform(String platform) {
|
||||
this.platform = platform;
|
||||
}
|
||||
|
||||
public boolean isHasCredentials() {
|
||||
return hasCredentials;
|
||||
}
|
||||
|
||||
public void setHasCredentials(boolean hasCredentials) {
|
||||
this.hasCredentials = hasCredentials;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package kz.konturai.parser.dto;
|
||||
|
||||
public class StartStrategyRequest {
|
||||
|
||||
// Пока что пустой класс, можно расширить в будущем
|
||||
// Например, добавить опции для фильтрации задач или настройки выполнения
|
||||
|
||||
public StartStrategyRequest() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package kz.konturai.parser.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class StartStrategyResponse {
|
||||
|
||||
private String strategyId;
|
||||
private int tasksCreated;
|
||||
private List<String> platforms;
|
||||
private String message;
|
||||
|
||||
public StartStrategyResponse() {
|
||||
}
|
||||
|
||||
public StartStrategyResponse(String strategyId, int tasksCreated, List<String> platforms, String message) {
|
||||
this.strategyId = strategyId;
|
||||
this.tasksCreated = tasksCreated;
|
||||
this.platforms = platforms;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getStrategyId() {
|
||||
return strategyId;
|
||||
}
|
||||
|
||||
public void setStrategyId(String strategyId) {
|
||||
this.strategyId = strategyId;
|
||||
}
|
||||
|
||||
public int getTasksCreated() {
|
||||
return tasksCreated;
|
||||
}
|
||||
|
||||
public void setTasksCreated(int tasksCreated) {
|
||||
this.tasksCreated = tasksCreated;
|
||||
}
|
||||
|
||||
public List<String> getPlatforms() {
|
||||
return platforms;
|
||||
}
|
||||
|
||||
public void setPlatforms(List<String> platforms) {
|
||||
this.platforms = platforms;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package kz.konturai.parser.model;
|
||||
|
||||
public enum AnalysisType {
|
||||
РЫНОК("Рынок"),
|
||||
КОНКУРЕНТЫ("Конкуренты"),
|
||||
ЦА("Целевая аудитория"),
|
||||
КАНАЛЫ("Каналы"),
|
||||
SWOT("SWOT");
|
||||
|
||||
private final String displayName;
|
||||
|
||||
AnalysisType(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public static AnalysisType fromString(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return AnalysisType.valueOf(value.toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Try to match by display name
|
||||
for (AnalysisType type : AnalysisType.values()) {
|
||||
if (type.getDisplayName().equalsIgnoreCase(value) ||
|
||||
type.name().equalsIgnoreCase(value)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,9 @@ public class MarketingAnalysis {
|
||||
@Field("differentiator")
|
||||
private String differentiator;
|
||||
|
||||
@Field("analysis_type")
|
||||
private String analysisType; // РЫНОК, КОНКУРЕНТЫ, ЦА, КАНАЛЫ, SWOT
|
||||
|
||||
@Field("status")
|
||||
private String status; // queued, processing, completed, failed
|
||||
|
||||
@@ -66,6 +69,16 @@ public class MarketingAnalysis {
|
||||
this.differentiator = differentiator;
|
||||
}
|
||||
|
||||
public MarketingAnalysis(String product, String location, String clientType, String differentiator,
|
||||
String analysisType) {
|
||||
this();
|
||||
this.product = product;
|
||||
this.location = location;
|
||||
this.clientType = clientType;
|
||||
this.differentiator = differentiator;
|
||||
this.analysisType = analysisType;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
@@ -169,4 +182,12 @@ public class MarketingAnalysis {
|
||||
public void setStatusHistory(List<StatusHistoryEntry> statusHistory) {
|
||||
this.statusHistory = statusHistory;
|
||||
}
|
||||
|
||||
public String getAnalysisType() {
|
||||
return analysisType;
|
||||
}
|
||||
|
||||
public void setAnalysisType(String analysisType) {
|
||||
this.analysisType = analysisType;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
package kz.konturai.parser.model;
|
||||
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
import org.springframework.data.mongodb.core.mapping.Field;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Document(collection = "posting_tasks")
|
||||
public class PostingTask {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Field("strategy_id")
|
||||
private String strategyId;
|
||||
|
||||
@Field("user_id")
|
||||
private String userId;
|
||||
|
||||
@Field("platform")
|
||||
private String platform;
|
||||
|
||||
@Field("post_text")
|
||||
private String postText;
|
||||
|
||||
@Field("hashtags")
|
||||
private List<String> hashtags;
|
||||
|
||||
@Field("publish_date")
|
||||
private LocalDateTime publishDate;
|
||||
|
||||
@Field("status")
|
||||
private String status; // "pending", "processing", "completed", "failed"
|
||||
|
||||
@Field("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Field("executed_at")
|
||||
private LocalDateTime executedAt;
|
||||
|
||||
@Field("error_message")
|
||||
private String errorMessage;
|
||||
|
||||
public PostingTask() {
|
||||
this.createdAt = LocalDateTime.now();
|
||||
this.status = "pending";
|
||||
this.hashtags = new ArrayList<>();
|
||||
}
|
||||
|
||||
public PostingTask(String strategyId, String userId, String platform, String postText,
|
||||
List<String> hashtags, LocalDateTime publishDate) {
|
||||
this();
|
||||
this.strategyId = strategyId;
|
||||
this.userId = userId;
|
||||
this.platform = platform;
|
||||
this.postText = postText;
|
||||
this.hashtags = hashtags != null ? hashtags : new ArrayList<>();
|
||||
this.publishDate = publishDate;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getStrategyId() {
|
||||
return strategyId;
|
||||
}
|
||||
|
||||
public void setStrategyId(String strategyId) {
|
||||
this.strategyId = strategyId;
|
||||
}
|
||||
|
||||
public String getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(String userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getPlatform() {
|
||||
return platform;
|
||||
}
|
||||
|
||||
public void setPlatform(String platform) {
|
||||
this.platform = platform;
|
||||
}
|
||||
|
||||
public String getPostText() {
|
||||
return postText;
|
||||
}
|
||||
|
||||
public void setPostText(String postText) {
|
||||
this.postText = postText;
|
||||
}
|
||||
|
||||
public List<String> getHashtags() {
|
||||
return hashtags;
|
||||
}
|
||||
|
||||
public void setHashtags(List<String> hashtags) {
|
||||
this.hashtags = hashtags != null ? hashtags : new ArrayList<>();
|
||||
}
|
||||
|
||||
public LocalDateTime getPublishDate() {
|
||||
return publishDate;
|
||||
}
|
||||
|
||||
public void setPublishDate(LocalDateTime publishDate) {
|
||||
this.publishDate = publishDate;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getExecutedAt() {
|
||||
return executedAt;
|
||||
}
|
||||
|
||||
public void setExecutedAt(LocalDateTime executedAt) {
|
||||
this.executedAt = executedAt;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
public void setErrorMessage(String errorMessage) {
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package kz.konturai.parser.model;
|
||||
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
import org.springframework.data.mongodb.core.mapping.Field;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Document(collection = "social_media_credentials")
|
||||
public class SocialMediaCredentials {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Field("user_id")
|
||||
private String userId;
|
||||
|
||||
@Field("platform")
|
||||
private String platform; // "facebook", "instagram", etc.
|
||||
|
||||
@Field("encrypted_credentials")
|
||||
private String encryptedCredentials;
|
||||
|
||||
@Field("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Field("updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public SocialMediaCredentials() {
|
||||
this.createdAt = LocalDateTime.now();
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public SocialMediaCredentials(String userId, String platform, String encryptedCredentials) {
|
||||
this();
|
||||
this.userId = userId;
|
||||
this.platform = platform;
|
||||
this.encryptedCredentials = encryptedCredentials;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(String userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getPlatform() {
|
||||
return platform;
|
||||
}
|
||||
|
||||
public void setPlatform(String platform) {
|
||||
this.platform = platform;
|
||||
}
|
||||
|
||||
public String getEncryptedCredentials() {
|
||||
return encryptedCredentials;
|
||||
}
|
||||
|
||||
public void setEncryptedCredentials(String encryptedCredentials) {
|
||||
this.encryptedCredentials = encryptedCredentials;
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package kz.konturai.parser.repository;
|
||||
|
||||
import kz.konturai.parser.model.PostingTask;
|
||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface PostingTaskRepository extends MongoRepository<PostingTask, String> {
|
||||
List<PostingTask> findByStatusAndPublishDateLessThanEqual(String status, LocalDateTime date);
|
||||
List<PostingTask> findByStrategyId(String strategyId);
|
||||
List<PostingTask> findByUserId(String userId);
|
||||
Optional<PostingTask> findById(String id);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package kz.konturai.parser.repository;
|
||||
|
||||
import kz.konturai.parser.model.SocialMediaCredentials;
|
||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface SocialMediaCredentialsRepository extends MongoRepository<SocialMediaCredentials, String> {
|
||||
Optional<SocialMediaCredentials> findByUserIdAndPlatform(String userId, String platform);
|
||||
List<SocialMediaCredentials> findByUserId(String userId);
|
||||
void deleteByUserIdAndPlatform(String userId, String platform);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package kz.konturai.parser.service;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
|
||||
@Service
|
||||
public class EncryptionService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(EncryptionService.class);
|
||||
private static final String ALGORITHM = "AES";
|
||||
private static final int KEY_SIZE = 256;
|
||||
|
||||
private final SecretKey secretKey;
|
||||
|
||||
public EncryptionService(@Value("${encryption.secret-key:}") String secretKeyBase64) {
|
||||
SecretKey key;
|
||||
if (secretKeyBase64 != null && !secretKeyBase64.isEmpty()) {
|
||||
try {
|
||||
byte[] keyBytes = Base64.getDecoder().decode(secretKeyBase64);
|
||||
key = new SecretKeySpec(keyBytes, ALGORITHM);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to decode encryption key from config, generating new one", e);
|
||||
key = generateKey();
|
||||
}
|
||||
} else {
|
||||
logger.warn("Encryption secret key not configured, generating temporary key. This should be configured in production!");
|
||||
key = generateKey();
|
||||
}
|
||||
this.secretKey = key;
|
||||
}
|
||||
|
||||
private SecretKey generateKey() {
|
||||
try {
|
||||
KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM);
|
||||
keyGenerator.init(KEY_SIZE, new SecureRandom());
|
||||
return keyGenerator.generateKey();
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to generate encryption key", e);
|
||||
throw new RuntimeException("Failed to initialize encryption service", e);
|
||||
}
|
||||
}
|
||||
|
||||
public String encrypt(String plainText) {
|
||||
if (plainText == null || plainText.isEmpty()) {
|
||||
return plainText;
|
||||
}
|
||||
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(ALGORITHM);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
|
||||
byte[] encryptedBytes = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getEncoder().encodeToString(encryptedBytes);
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to encrypt data", e);
|
||||
throw new RuntimeException("Encryption failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
public String decrypt(String encryptedText) {
|
||||
if (encryptedText == null || encryptedText.isEmpty()) {
|
||||
return encryptedText;
|
||||
}
|
||||
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(ALGORITHM);
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey);
|
||||
byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedText));
|
||||
return new String(decryptedBytes, StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to decrypt data", e);
|
||||
throw new RuntimeException("Decryption failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
package kz.konturai.parser.service;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.reactive.function.client.WebClientResponseException;
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class FacebookPostingService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(FacebookPostingService.class);
|
||||
private static final String FACEBOOK_GRAPH_API_BASE = "https://graph.facebook.com/v18.0";
|
||||
|
||||
private final WebClient webClient;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Value("${facebook.api.timeout:30000}")
|
||||
private int timeoutMs;
|
||||
|
||||
public FacebookPostingService() {
|
||||
HttpClient httpClient = HttpClient.create()
|
||||
.responseTimeout(Duration.ofMillis(30000));
|
||||
|
||||
this.webClient = WebClient.builder()
|
||||
.baseUrl(FACEBOOK_GRAPH_API_BASE)
|
||||
.clientConnector(new ReactorClientHttpConnector(httpClient))
|
||||
.build();
|
||||
|
||||
this.objectMapper = new ObjectMapper();
|
||||
}
|
||||
|
||||
/**
|
||||
* Публикует пост в Facebook через Graph API
|
||||
*
|
||||
* @param accessToken Access Token пользователя
|
||||
* @param postText Текст поста
|
||||
* @param hashtags Список хештегов
|
||||
* @return ID опубликованного поста
|
||||
* @throws RuntimeException если публикация не удалась
|
||||
*/
|
||||
public String postToFacebook(String accessToken, String postText, List<String> hashtags) {
|
||||
try {
|
||||
// Формируем полный текст поста с хештегами
|
||||
String fullPostText = buildPostText(postText, hashtags);
|
||||
|
||||
// Получаем ID страницы пользователя (me)
|
||||
String pageId = getPageId(accessToken);
|
||||
|
||||
// Публикуем пост на странице
|
||||
String postId = publishPost(accessToken, pageId, fullPostText);
|
||||
|
||||
logger.info("Successfully posted to Facebook. Post ID: {}", postId);
|
||||
return postId;
|
||||
|
||||
} catch (WebClientResponseException e) {
|
||||
logger.error("Facebook API error: {} - {}", e.getStatusCode(), e.getResponseBodyAsString());
|
||||
throw new RuntimeException("Failed to post to Facebook: " + e.getMessage(), e);
|
||||
} catch (Exception e) {
|
||||
logger.error("Unexpected error posting to Facebook", e);
|
||||
throw new RuntimeException("Failed to post to Facebook", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает ID страницы пользователя
|
||||
*/
|
||||
private String getPageId(String accessToken) {
|
||||
try {
|
||||
String response = webClient.get()
|
||||
.uri(uriBuilder -> uriBuilder
|
||||
.path("/me")
|
||||
.queryParam("access_token", accessToken)
|
||||
.queryParam("fields", "id")
|
||||
.build())
|
||||
.retrieve()
|
||||
.bodyToMono(String.class)
|
||||
.block(Duration.ofMillis(timeoutMs));
|
||||
|
||||
JsonNode jsonNode = objectMapper.readTree(response);
|
||||
return jsonNode.get("id").asText();
|
||||
} catch (JsonProcessingException e) {
|
||||
logger.error("Failed to parse Facebook page ID response", e);
|
||||
// Если не удалось получить ID страницы, используем "me" для публикации на стене пользователя
|
||||
return "me";
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to get Facebook page ID", e);
|
||||
// Если не удалось получить ID страницы, используем "me" для публикации на стене пользователя
|
||||
return "me";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Публикует пост на странице Facebook
|
||||
*/
|
||||
private String publishPost(String accessToken, String pageId, String message)
|
||||
throws JsonProcessingException {
|
||||
String response = webClient.post()
|
||||
.uri(uriBuilder -> uriBuilder
|
||||
.path("/{pageId}/feed")
|
||||
.queryParam("access_token", accessToken)
|
||||
.queryParam("message", message)
|
||||
.build(pageId))
|
||||
.retrieve()
|
||||
.bodyToMono(String.class)
|
||||
.block(Duration.ofMillis(timeoutMs));
|
||||
|
||||
JsonNode jsonNode = objectMapper.readTree(response);
|
||||
return jsonNode.get("id").asText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Формирует полный текст поста с хештегами
|
||||
*/
|
||||
private String buildPostText(String postText, List<String> hashtags) {
|
||||
StringBuilder fullText = new StringBuilder(postText != null ? postText : "");
|
||||
|
||||
if (hashtags != null && !hashtags.isEmpty()) {
|
||||
if (fullText.length() > 0) {
|
||||
fullText.append("\n\n");
|
||||
}
|
||||
|
||||
// Добавляем хештеги, убеждаясь что они начинаются с #
|
||||
String hashtagsText = hashtags.stream()
|
||||
.map(tag -> tag.startsWith("#") ? tag : "#" + tag)
|
||||
.collect(Collectors.joining(" "));
|
||||
|
||||
fullText.append(hashtagsText);
|
||||
}
|
||||
|
||||
return fullText.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package kz.konturai.parser.service;
|
||||
import kz.konturai.parser.dto.MarketingAnalysisRequest;
|
||||
import kz.konturai.parser.dto.MarketingAnalysisResult;
|
||||
import kz.konturai.parser.dto.StatusHistoryEntry;
|
||||
import kz.konturai.parser.model.AnalysisType;
|
||||
import kz.konturai.parser.model.MarketingAnalysis;
|
||||
import kz.konturai.parser.repository.MarketingAnalysisRepository;
|
||||
import org.slf4j.Logger;
|
||||
@@ -40,12 +41,14 @@ public class MarketingAnalysisService {
|
||||
request.getProduct(),
|
||||
request.getLocation(),
|
||||
request.getClient(),
|
||||
request.getDifferentiator());
|
||||
request.getDifferentiator(),
|
||||
request.getAnalysisType());
|
||||
analysis.setUserId(userId);
|
||||
analysis.setStatus("queued");
|
||||
addStatusHistoryEntry(analysis, "queued", "Анализ создан и добавлен в очередь");
|
||||
analysis = repository.save(analysis);
|
||||
logger.info("Marketing analysis created with ID: {} for user: {}", analysis.getId(), userId);
|
||||
logger.info("Marketing analysis created with ID: {} for user: {} with type: {}",
|
||||
analysis.getId(), userId, request.getAnalysisType());
|
||||
return analysis;
|
||||
}
|
||||
|
||||
@@ -119,24 +122,27 @@ public class MarketingAnalysisService {
|
||||
private Map<String, Object> generateMarketingReport(MarketingAnalysisRequest request) {
|
||||
Map<String, Object> report = new HashMap<>();
|
||||
|
||||
// Get analysis type
|
||||
AnalysisType analysisType = request.getAnalysisTypeEnum();
|
||||
String analysisTypeName = analysisType != null ? analysisType.getDisplayName() : "Общий";
|
||||
|
||||
// Build context for AI
|
||||
String context = String.format(
|
||||
"Продукт/услуга: %s\n" +
|
||||
"Локация: %s\n" +
|
||||
"Тип клиентов: %s\n" +
|
||||
"Уникальные особенности: %s\n",
|
||||
"Уникальные особенности: %s\n" +
|
||||
"Тип анализа: %s\n",
|
||||
request.getProduct(),
|
||||
request.getLocation(),
|
||||
request.getClient(),
|
||||
request.getDifferentiator());
|
||||
request.getDifferentiator(),
|
||||
analysisTypeName);
|
||||
|
||||
// Generate summary
|
||||
String summaryPrompt = "На основе следующей информации о бизнесе создай краткое резюме маркетингового анализа (2-3 абзаца). "
|
||||
+
|
||||
"Выдели ключевые возможности и особенности бизнеса. " +
|
||||
"Ответ должен быть на русском языке, деловым стилем.\n\n" + context;
|
||||
String summary = openAIAnalyticsService.generateWithInstruction(context, summaryPrompt, "ru");
|
||||
report.put("summary", summary != null ? summary : "Резюме не удалось сгенерировать.");
|
||||
// Generate analysis based on type
|
||||
String analysisContent = generateAnalysisByType(context, analysisType);
|
||||
report.put("analysisType", analysisTypeName);
|
||||
report.put("summary", analysisContent != null ? analysisContent : "Анализ не удалось сгенерировать.");
|
||||
|
||||
// Generate target audience description
|
||||
String audiencePrompt = "На основе информации о бизнесе опиши целевую аудиторию (1-2 абзаца). " +
|
||||
@@ -182,6 +188,54 @@ public class MarketingAnalysisService {
|
||||
return report;
|
||||
}
|
||||
|
||||
private String generateAnalysisByType(String context, AnalysisType analysisType) {
|
||||
if (analysisType == null) {
|
||||
analysisType = AnalysisType.РЫНОК; // Default
|
||||
}
|
||||
|
||||
String prompt;
|
||||
switch (analysisType) {
|
||||
case РЫНОК:
|
||||
prompt = "Проведи детальный анализ рынка на основе предоставленной информации о бизнесе. " +
|
||||
"Включи: размер рынка, динамику роста, основные сегменты, тренды и перспективы развития. " +
|
||||
"Ответ должен быть структурированным текстом на русском языке (3-5 абзацев).\n\n" + context;
|
||||
break;
|
||||
case КОНКУРЕНТЫ:
|
||||
prompt = "Проведи анализ конкурентов на основе предоставленной информации о бизнесе. " +
|
||||
"Включи: основных конкурентов, их сильные и слабые стороны, позиционирование, " +
|
||||
"ценовую политику и маркетинговые стратегии. Ответ должен быть структурированным текстом на русском языке (3-5 абзацев).\n\n"
|
||||
+ context;
|
||||
break;
|
||||
case ЦА:
|
||||
prompt = "Проведи детальный анализ целевой аудитории на основе предоставленной информации о бизнесе. " +
|
||||
"Включи: демографические характеристики, психографический профиль, потребности и боли, " +
|
||||
"поведенческие паттерны и предпочтения. Ответ должен быть структурированным текстом на русском языке (3-5 абзацев).\n\n"
|
||||
+ context;
|
||||
break;
|
||||
case КАНАЛЫ:
|
||||
prompt = "Проведи анализ маркетинговых каналов на основе предоставленной информации о бизнесе. " +
|
||||
"Включи: оценку эффективности различных каналов коммуникации, рекомендации по выбору каналов, "
|
||||
+
|
||||
"особенности использования каждого канала и бюджетные рекомендации. Ответ должен быть структурированным текстом на русском языке (3-5 абзацев).\n\n"
|
||||
+ context;
|
||||
break;
|
||||
case SWOT:
|
||||
prompt = "Проведи SWOT-анализ на основе предоставленной информации о бизнесе. " +
|
||||
"Включи детальный анализ: Сильных сторон (Strengths), Слабых сторон (Weaknesses), " +
|
||||
"Возможностей (Opportunities) и Угроз (Threats). Ответ должен быть структурированным текстом на русском языке, "
|
||||
+
|
||||
"с четким разделением по каждому разделу SWOT (3-5 абзацев).\n\n" + context;
|
||||
break;
|
||||
default:
|
||||
prompt = "На основе следующей информации о бизнесе создай краткое резюме маркетингового анализа (2-3 абзаца). "
|
||||
+
|
||||
"Выдели ключевые возможности и особенности бизнеса. " +
|
||||
"Ответ должен быть на русском языке, деловым стилем.\n\n" + context;
|
||||
}
|
||||
|
||||
return openAIAnalyticsService.generateWithInstruction(context, prompt, "ru");
|
||||
}
|
||||
|
||||
private List<String> parseChannels(String channelsStr) {
|
||||
if (channelsStr == null || channelsStr.trim().isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
@@ -278,9 +332,16 @@ public class MarketingAnalysisService {
|
||||
markdown.append("- **Продукт/услуга:** ").append(request.getProduct()).append("\n");
|
||||
markdown.append("- **Локация:** ").append(request.getLocation()).append("\n");
|
||||
markdown.append("- **Тип клиентов:** ").append(request.getClient()).append("\n");
|
||||
markdown.append("- **Уникальные особенности:** ").append(request.getDifferentiator()).append("\n\n");
|
||||
markdown.append("- **Уникальные особенности:** ").append(request.getDifferentiator()).append("\n");
|
||||
|
||||
markdown.append("## Резюме анализа\n\n");
|
||||
String analysisTypeName = (String) reportData.get("analysisType");
|
||||
if (analysisTypeName != null) {
|
||||
markdown.append("- **Тип анализа:** ").append(analysisTypeName).append("\n");
|
||||
}
|
||||
markdown.append("\n");
|
||||
|
||||
String analysisTitle = analysisTypeName != null ? analysisTypeName : "Резюме анализа";
|
||||
markdown.append("## ").append(analysisTitle).append("\n\n");
|
||||
markdown.append(reportData.get("summary")).append("\n\n");
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package kz.konturai.parser.service;
|
||||
|
||||
import kz.konturai.parser.model.PostingTask;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class PostingSchedulerService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(PostingSchedulerService.class);
|
||||
|
||||
private final PostingTaskService postingTaskService;
|
||||
|
||||
@Value("${posting.scheduler.enabled:true}")
|
||||
private boolean schedulerEnabled;
|
||||
|
||||
public PostingSchedulerService(PostingTaskService postingTaskService) {
|
||||
this.postingTaskService = postingTaskService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Планировщик, который запускается каждую минуту
|
||||
* Проверяет очередь задач и выполняет задачи, у которых наступило время публикации
|
||||
*/
|
||||
@Scheduled(cron = "0 * * * * ?")
|
||||
public void checkAndExecutePendingTasks() {
|
||||
if (!schedulerEnabled) {
|
||||
logger.debug("Posting scheduler is disabled. Skipping task check.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
logger.debug("Checking for pending posting tasks at {}", now);
|
||||
|
||||
// Получаем задачи со статусом pending, у которых publishDate <= текущее время
|
||||
List<PostingTask> pendingTasks = postingTaskService.getPendingTasks(now);
|
||||
|
||||
if (pendingTasks.isEmpty()) {
|
||||
logger.debug("No pending tasks found for execution");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("Found {} pending task(s) to execute", pendingTasks.size());
|
||||
|
||||
// Выполняем каждую задачу
|
||||
for (PostingTask task : pendingTasks) {
|
||||
try {
|
||||
logger.info("Executing task {} for platform {} (publish date: {})",
|
||||
task.getId(), task.getPlatform(), task.getPublishDate());
|
||||
postingTaskService.executeTaskAsync(task.getId());
|
||||
} catch (Exception e) {
|
||||
logger.error("Error executing task {}: {}", task.getId(), e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Error in posting scheduler", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
package kz.konturai.parser.service;
|
||||
|
||||
import kz.konturai.parser.model.MarketingStrategy;
|
||||
import kz.konturai.parser.model.PostingTask;
|
||||
import kz.konturai.parser.repository.MarketingStrategyRepository;
|
||||
import kz.konturai.parser.repository.PostingTaskRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
public class PostingTaskService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(PostingTaskService.class);
|
||||
|
||||
private final PostingTaskRepository taskRepository;
|
||||
private final MarketingStrategyRepository strategyRepository;
|
||||
private final SocialMediaCredentialsService credentialsService;
|
||||
private final FacebookPostingService facebookPostingService;
|
||||
|
||||
public PostingTaskService(
|
||||
PostingTaskRepository taskRepository,
|
||||
MarketingStrategyRepository strategyRepository,
|
||||
SocialMediaCredentialsService credentialsService,
|
||||
FacebookPostingService facebookPostingService) {
|
||||
this.taskRepository = taskRepository;
|
||||
this.strategyRepository = strategyRepository;
|
||||
this.credentialsService = credentialsService;
|
||||
this.facebookPostingService = facebookPostingService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Создает задачи публикации из стратегии
|
||||
* Конвертирует PostCalendarItem в PostingTask
|
||||
*
|
||||
* @param strategyId ID стратегии
|
||||
* @return Список созданных задач
|
||||
*/
|
||||
public List<PostingTask> createTasksFromStrategy(String strategyId) {
|
||||
Optional<MarketingStrategy> optStrategy = strategyRepository.findById(strategyId);
|
||||
if (optStrategy.isEmpty()) {
|
||||
throw new IllegalArgumentException("Strategy not found: " + strategyId);
|
||||
}
|
||||
|
||||
MarketingStrategy strategy = optStrategy.get();
|
||||
|
||||
if (!"completed".equals(strategy.getStatus())) {
|
||||
throw new IllegalStateException("Strategy is not completed yet. Status: " + strategy.getStatus());
|
||||
}
|
||||
|
||||
if (strategy.getPostCalendar() == null || strategy.getPostCalendar().isEmpty()) {
|
||||
logger.warn("Strategy {} has no post calendar items", strategyId);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// Проверяем наличие credentials для платформ
|
||||
List<String> platforms = strategy.getPostCalendar().stream()
|
||||
.map(MarketingStrategy.PostCalendarItem::getPlatform)
|
||||
.distinct()
|
||||
.toList();
|
||||
|
||||
for (String platform : platforms) {
|
||||
if (!credentialsService.hasCredentials(strategy.getUserId(), platform)) {
|
||||
throw new IllegalStateException(
|
||||
"Credentials not found for platform: " + platform + ". Please configure credentials first.");
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем, не созданы ли уже задачи для этой стратегии
|
||||
List<PostingTask> existingTasks = taskRepository.findByStrategyId(strategyId);
|
||||
if (!existingTasks.isEmpty()) {
|
||||
logger.info("Tasks already exist for strategy {}. Returning existing tasks.", strategyId);
|
||||
return existingTasks;
|
||||
}
|
||||
|
||||
// Создаем задачи из календаря постов
|
||||
List<PostingTask> tasks = new ArrayList<>();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
for (MarketingStrategy.PostCalendarItem calendarItem : strategy.getPostCalendar()) {
|
||||
PostingTask task = new PostingTask(
|
||||
strategyId,
|
||||
strategy.getUserId(),
|
||||
calendarItem.getPlatform(),
|
||||
calendarItem.getPostText(),
|
||||
calendarItem.getHashtags(),
|
||||
calendarItem.getPublishDate()
|
||||
);
|
||||
|
||||
// Если дата публикации в прошлом, выполняем сразу
|
||||
if (task.getPublishDate().isBefore(now) || task.getPublishDate().isEqual(now)) {
|
||||
task.setStatus("pending");
|
||||
logger.info("Task created with past date, will be executed immediately: {}", task.getPublishDate());
|
||||
}
|
||||
|
||||
tasks.add(task);
|
||||
}
|
||||
|
||||
// Сохраняем все задачи
|
||||
List<PostingTask> savedTasks = taskRepository.saveAll(tasks);
|
||||
logger.info("Created {} posting tasks from strategy {}", savedTasks.size(), strategyId);
|
||||
|
||||
// Выполняем задачи с прошедшей датой сразу
|
||||
for (PostingTask task : savedTasks) {
|
||||
if (task.getPublishDate().isBefore(now) || task.getPublishDate().isEqual(now)) {
|
||||
if ("pending".equals(task.getStatus())) {
|
||||
executeTaskAsync(task.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return savedTasks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает задачи для выполнения (pending с датой <= указанной)
|
||||
*
|
||||
* @param beforeDate Максимальная дата публикации
|
||||
* @return Список задач для выполнения
|
||||
*/
|
||||
public List<PostingTask> getPendingTasks(LocalDateTime beforeDate) {
|
||||
return taskRepository.findByStatusAndPublishDateLessThanEqual("pending", beforeDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Выполняет задачу публикации
|
||||
*
|
||||
* @param taskId ID задачи
|
||||
*/
|
||||
public void executeTask(String taskId) {
|
||||
Optional<PostingTask> optTask = taskRepository.findById(taskId);
|
||||
if (optTask.isEmpty()) {
|
||||
logger.error("Task not found: {}", taskId);
|
||||
return;
|
||||
}
|
||||
|
||||
PostingTask task = optTask.get();
|
||||
|
||||
// Проверяем статус
|
||||
if (!"pending".equals(task.getStatus())) {
|
||||
logger.warn("Task {} is not in pending status. Current status: {}", taskId, task.getStatus());
|
||||
return;
|
||||
}
|
||||
|
||||
// Обновляем статус на processing
|
||||
task.setStatus("processing");
|
||||
taskRepository.save(task);
|
||||
|
||||
try {
|
||||
// Получаем credentials
|
||||
String credentials = credentialsService.getCredentials(task.getUserId(), task.getPlatform());
|
||||
if (credentials == null || credentials.isEmpty()) {
|
||||
throw new IllegalStateException("Credentials not found for platform: " + task.getPlatform());
|
||||
}
|
||||
|
||||
// Выполняем публикацию в зависимости от платформы
|
||||
String postId = null;
|
||||
if ("facebook".equalsIgnoreCase(task.getPlatform())) {
|
||||
postId = facebookPostingService.postToFacebook(
|
||||
credentials,
|
||||
task.getPostText(),
|
||||
task.getHashtags()
|
||||
);
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Platform not supported: " + task.getPlatform());
|
||||
}
|
||||
|
||||
// Обновляем статус на completed
|
||||
task.setStatus("completed");
|
||||
task.setExecutedAt(LocalDateTime.now());
|
||||
task.setErrorMessage(null);
|
||||
taskRepository.save(task);
|
||||
|
||||
logger.info("Task {} executed successfully. Post ID: {}", taskId, postId);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to execute task {}", taskId, e);
|
||||
|
||||
// Обновляем статус на failed
|
||||
task.setStatus("failed");
|
||||
task.setExecutedAt(LocalDateTime.now());
|
||||
task.setErrorMessage(e.getMessage());
|
||||
taskRepository.save(task);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Асинхронное выполнение задачи
|
||||
*/
|
||||
@Async("reportGenerationExecutor")
|
||||
public void executeTaskAsync(String taskId) {
|
||||
executeTask(taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает задачи пользователя
|
||||
*
|
||||
* @param userId ID пользователя
|
||||
* @return Список задач
|
||||
*/
|
||||
public List<PostingTask> getUserTasks(String userId) {
|
||||
return taskRepository.findByUserId(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает задачи стратегии
|
||||
*
|
||||
* @param strategyId ID стратегии
|
||||
* @return Список задач
|
||||
*/
|
||||
public List<PostingTask> getStrategyTasks(String strategyId) {
|
||||
return taskRepository.findByStrategyId(strategyId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
package kz.konturai.parser.service;
|
||||
|
||||
import kz.konturai.parser.model.SocialMediaCredentials;
|
||||
import kz.konturai.parser.repository.SocialMediaCredentialsRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
public class SocialMediaCredentialsService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SocialMediaCredentialsService.class);
|
||||
|
||||
private final SocialMediaCredentialsRepository repository;
|
||||
private final EncryptionService encryptionService;
|
||||
|
||||
public SocialMediaCredentialsService(
|
||||
SocialMediaCredentialsRepository repository,
|
||||
EncryptionService encryptionService) {
|
||||
this.repository = repository;
|
||||
this.encryptionService = encryptionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохраняет или обновляет credentials пользователя для платформы
|
||||
*
|
||||
* @param userId ID пользователя
|
||||
* @param platform Платформа (facebook, instagram, etc.)
|
||||
* @param credentials Открытые credentials (будут зашифрованы)
|
||||
* @return Сохраненные credentials
|
||||
*/
|
||||
public SocialMediaCredentials saveCredentials(String userId, String platform, String credentials) {
|
||||
if (userId == null || platform == null || credentials == null || credentials.isEmpty()) {
|
||||
throw new IllegalArgumentException("UserId, platform and credentials are required");
|
||||
}
|
||||
|
||||
// Проверяем существующие credentials
|
||||
Optional<SocialMediaCredentials> existing = repository.findByUserIdAndPlatform(userId, platform);
|
||||
|
||||
SocialMediaCredentials credentialsEntity;
|
||||
if (existing.isPresent()) {
|
||||
credentialsEntity = existing.get();
|
||||
logger.info("Updating existing credentials for user {} and platform {}", userId, platform);
|
||||
} else {
|
||||
credentialsEntity = new SocialMediaCredentials();
|
||||
credentialsEntity.setUserId(userId);
|
||||
credentialsEntity.setPlatform(platform.toLowerCase());
|
||||
logger.info("Creating new credentials for user {} and platform {}", userId, platform);
|
||||
}
|
||||
|
||||
// Шифруем credentials перед сохранением
|
||||
String encryptedCredentials = encryptionService.encrypt(credentials);
|
||||
credentialsEntity.setEncryptedCredentials(encryptedCredentials);
|
||||
|
||||
return repository.save(credentialsEntity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает и расшифровывает credentials пользователя для платформы
|
||||
*
|
||||
* @param userId ID пользователя
|
||||
* @param platform Платформа
|
||||
* @return Расшифрованные credentials или null если не найдены
|
||||
*/
|
||||
public String getCredentials(String userId, String platform) {
|
||||
if (userId == null || platform == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Optional<SocialMediaCredentials> optCredentials = repository.findByUserIdAndPlatform(
|
||||
userId, platform.toLowerCase());
|
||||
|
||||
if (optCredentials.isEmpty()) {
|
||||
logger.debug("Credentials not found for user {} and platform {}", userId, platform);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
SocialMediaCredentials credentialsEntity = optCredentials.get();
|
||||
return encryptionService.decrypt(credentialsEntity.getEncryptedCredentials());
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to decrypt credentials for user {} and platform {}", userId, platform, e);
|
||||
throw new RuntimeException("Failed to decrypt credentials", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет наличие credentials для пользователя и платформы
|
||||
*
|
||||
* @param userId ID пользователя
|
||||
* @param platform Платформа
|
||||
* @return true если credentials существуют
|
||||
*/
|
||||
public boolean hasCredentials(String userId, String platform) {
|
||||
if (userId == null || platform == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return repository.findByUserIdAndPlatform(userId, platform.toLowerCase()).isPresent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Удаляет credentials пользователя для платформы
|
||||
*
|
||||
* @param userId ID пользователя
|
||||
* @param platform Платформа
|
||||
*/
|
||||
public void deleteCredentials(String userId, String platform) {
|
||||
if (userId == null || platform == null) {
|
||||
throw new IllegalArgumentException("UserId and platform are required");
|
||||
}
|
||||
|
||||
repository.deleteByUserIdAndPlatform(userId, platform.toLowerCase());
|
||||
logger.info("Deleted credentials for user {} and platform {}", userId, platform);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает список всех платформ, для которых у пользователя есть credentials
|
||||
*
|
||||
* @param userId ID пользователя
|
||||
* @return Список платформ
|
||||
*/
|
||||
public List<String> getUserPlatforms(String userId) {
|
||||
if (userId == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
return repository.findByUserId(userId).stream()
|
||||
.map(SocialMediaCredentials::getPlatform)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает все credentials пользователя (без расшифровки)
|
||||
*
|
||||
* @param userId ID пользователя
|
||||
* @return Список credentials entities
|
||||
*/
|
||||
public List<SocialMediaCredentials> getUserCredentials(String userId) {
|
||||
if (userId == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
return repository.findByUserId(userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package kz.konturai.parser.validator;
|
||||
|
||||
import jakarta.validation.ConstraintValidator;
|
||||
import jakarta.validation.ConstraintValidatorContext;
|
||||
import kz.konturai.parser.model.AnalysisType;
|
||||
|
||||
public class AnalysisTypeValidator implements ConstraintValidator<ValidAnalysisType, String> {
|
||||
|
||||
@Override
|
||||
public void initialize(ValidAnalysisType constraintAnnotation) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid(String value, ConstraintValidatorContext context) {
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
return true; // @NotBlank will handle null/empty
|
||||
}
|
||||
return AnalysisType.fromString(value) != null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package kz.konturai.parser.validator;
|
||||
|
||||
import jakarta.validation.Constraint;
|
||||
import jakarta.validation.Payload;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Documented
|
||||
@Constraint(validatedBy = AnalysisTypeValidator.class)
|
||||
@Target({ ElementType.FIELD, ElementType.PARAMETER })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ValidAnalysisType {
|
||||
String message() default "Поле 'analysisType' должно быть одним из: РЫНОК, КОНКУРЕНТЫ, ЦА, КАНАЛЫ, SWOT";
|
||||
|
||||
Class<?>[] groups() default {};
|
||||
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
@@ -82,3 +82,15 @@ deep-research.api.timeout=1800000
|
||||
# Используйте тот же secret-base64, что и в сервисе, выдающем токены
|
||||
# Формат: base64-encoded secret key
|
||||
security.jwt.secret-base64=ZmFrZV9zZWNyZXRfMTIzNDU2Nzg5MGFiY2RlZmFrZV9zZWNyZXRfMTIzNDU2Nzg5MGFiY2Rl
|
||||
|
||||
# Encryption Configuration for Social Media Credentials
|
||||
# Base64-encoded AES-256 key (32 bytes = 256 bits)
|
||||
# Generate a key: openssl rand -base64 32
|
||||
# IMPORTANT: Change this in production!
|
||||
encryption.secret-key=ZmFrZV9zZWNyZXRfMTIzNDU2Nzg5MGFiY2RlZmFrZV9zZWNyZXRfMTIzNDU2Nzg5MGFiY2Rl
|
||||
|
||||
# Posting Scheduler Configuration
|
||||
posting.scheduler.enabled=true
|
||||
|
||||
# Facebook API Configuration
|
||||
facebook.api.timeout=30000
|
||||
|
||||
Reference in New Issue
Block a user