.
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
# API Документация: Управление задачами публикации (Frontend/AI Agent)
|
||||
|
||||
## Базовый URL
|
||||
|
||||
```
|
||||
https://api.konturai.kz
|
||||
```
|
||||
|
||||
## Обзор
|
||||
|
||||
API для управления задачами публикации постов в социальных сетях. Позволяет запускать задачи публикации вручную, не дожидаясь времени выполнения через шедулер.
|
||||
|
||||
**Важно**:
|
||||
|
||||
- Для выполнения задачи требуются настроенные credentials для соответствующей платформы
|
||||
- Задачи создаются автоматически при запуске стратегии через `/api/marketing/analysis/strategy/{strategyId}/start`
|
||||
- Каждая задача связана с элементом календаря постов в стратегии
|
||||
|
||||
---
|
||||
|
||||
## Изменения в существующих эндпоинтах
|
||||
|
||||
### Обновление: Получение стратегии по ID анализа
|
||||
|
||||
**GET** `/api/marketing/analysis/{analysisId}/strategy`
|
||||
|
||||
Теперь каждый элемент в `postCalendar` содержит поле `taskId`, если задача была создана для этого элемента.
|
||||
|
||||
#### Пример ответа (обновленный формат)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"strategyId": "507f1f77bcf86cd799439012",
|
||||
"analysisId": "507f1f77bcf86cd799439011",
|
||||
"status": "completed",
|
||||
"strategy": {
|
||||
"postCalendar": [
|
||||
{
|
||||
"publishDate": "2025-01-25T10:00:00",
|
||||
"platform": "Facebook",
|
||||
"contentType": "пост",
|
||||
"theme": "Презентация продукта",
|
||||
"postText": "Добро пожаловать в наш новый продукт!",
|
||||
"hashtags": ["#маркетинг", "#бизнес"],
|
||||
"publishTime": "10:00",
|
||||
"imageUrl": "post_image_1234567890.png",
|
||||
"imageFilename": "post_image_1234567890.png",
|
||||
"taskId": "507f1f77bcf86cd799439013"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Новое поле:**
|
||||
|
||||
- `taskId` (string, опциональное) - ID задачи публикации, если задача была создана. Может быть `null`, если стратегия еще не была запущена.
|
||||
|
||||
---
|
||||
|
||||
## Новые эндпоинты
|
||||
|
||||
### 1. Ручной запуск задачи публикации
|
||||
|
||||
**POST** `/api/marketing/analysis/tasks/{taskId}/execute`
|
||||
|
||||
Запускает задачу публикации немедленно, не дожидаясь времени публикации через шедулер. Позволяет выполнить задачу вручную или повторить выполнение неудачной задачи.
|
||||
|
||||
#### Параметры запроса
|
||||
|
||||
| Параметр | Тип | Расположение | Обязательный | Описание |
|
||||
| -------- | ------ | ------------ | ------------ | -------------------- |
|
||||
| `taskId` | string | Path | ✅ | ID задачи публикации |
|
||||
|
||||
#### Заголовки запроса
|
||||
|
||||
```
|
||||
Authorization: Bearer <your-jwt-token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
#### Пример запроса
|
||||
|
||||
```http
|
||||
POST /api/marketing/analysis/tasks/507f1f77bcf86cd799439013/execute
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
#### Пример успешного ответа (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Задача успешно запущена",
|
||||
"data": {
|
||||
"taskId": "507f1f77bcf86cd799439013",
|
||||
"status": "processing",
|
||||
"platform": "Facebook",
|
||||
"publishDate": "2025-01-25T10:00:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Описание полей ответа
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
| ------------- | ------ | ----------------------------------------------------- |
|
||||
| `taskId` | string | ID задачи публикации |
|
||||
| `status` | string | Статус задачи: `processing`, `completed` или `failed` |
|
||||
| `platform` | string | Платформа для публикации (например, `Facebook`) |
|
||||
| `publishDate` | string | Дата и время публикации в формате ISO 8601 |
|
||||
|
||||
#### Ошибки
|
||||
|
||||
**401 Unauthorized** - Требуется аутентификация
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Не авторизован",
|
||||
"error": {
|
||||
"code": "UNAUTHORIZED",
|
||||
"message": "Требуется аутентификация. Пожалуйста, предоставьте валидный JWT токен."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**403 Forbidden** - Пользователь не является владельцем задачи
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Доступ запрещен",
|
||||
"error": {
|
||||
"code": "FORBIDDEN",
|
||||
"message": "У вас нет доступа к этой задаче"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**404 Not Found** - Задача не найдена
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Задача не найдена",
|
||||
"error": {
|
||||
"code": "NOT_FOUND",
|
||||
"message": "Задача с указанным ID не найдена"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**400 Bad Request** - Задача не может быть запущена
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Задача не может быть запущена",
|
||||
"error": {
|
||||
"code": "INVALID_STATUS",
|
||||
"message": "Task cannot be executed manually. Current status: completed. Only tasks with status 'pending' or 'failed' can be executed manually."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Правила выполнения
|
||||
|
||||
1. **Статусы задач:**
|
||||
|
||||
- `pending` - задача ожидает выполнения (может быть запущена вручную)
|
||||
- `failed` - задача завершилась с ошибкой (может быть запущена повторно вручную)
|
||||
- `processing` - задача выполняется (не может быть запущена повторно)
|
||||
- `completed` - задача успешно выполнена (не может быть запущена повторно)
|
||||
|
||||
2. **Повторное выполнение:**
|
||||
|
||||
- Задачи со статусом `failed` автоматически сбрасываются на `pending` перед повторным выполнением
|
||||
- Ошибка из предыдущего выполнения очищается
|
||||
|
||||
3. **Асинхронное выполнение:**
|
||||
- Задача запускается асинхронно
|
||||
- Ответ возвращается сразу после начала выполнения
|
||||
- Для проверки статуса задачи используйте соответствующие эндпоинты (если доступны)
|
||||
|
||||
#### Примеры использования
|
||||
|
||||
**Пример 1: Запуск задачи, которая еще не была выполнена**
|
||||
|
||||
```javascript
|
||||
// Получаем стратегию
|
||||
const strategyResponse = await fetch(
|
||||
`/api/marketing/analysis/${analysisId}/strategy`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const strategy = await strategyResponse.json();
|
||||
const taskId = strategy.data.strategy.postCalendar[0].taskId;
|
||||
|
||||
// Запускаем задачу вручную
|
||||
const executeResponse = await fetch(
|
||||
`/api/marketing/analysis/tasks/${taskId}/execute`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const result = await executeResponse.json();
|
||||
console.log('Задача запущена:', result.data);
|
||||
```
|
||||
|
||||
**Пример 2: Повторное выполнение неудачной задачи**
|
||||
|
||||
```javascript
|
||||
// Если задача завершилась с ошибкой (status: "failed")
|
||||
// можно повторить её выполнение
|
||||
const retryResponse = await fetch(
|
||||
`/api/marketing/analysis/tasks/${failedTaskId}/execute`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const retryResult = await retryResponse.json();
|
||||
if (retryResult.success) {
|
||||
console.log('Повторная попытка запущена:', retryResult.data);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Интеграция с существующим workflow
|
||||
|
||||
### Типичный сценарий использования
|
||||
|
||||
1. **Создание анализа** → `POST /api/marketing/analysis/start`
|
||||
2. **Генерация стратегии** → `POST /api/marketing/analysis/strategy/generate`
|
||||
3. **Получение стратегии** → `GET /api/marketing/analysis/{analysisId}/strategy`
|
||||
- Теперь содержит `taskId` для каждого элемента календаря
|
||||
4. **Запуск стратегии** → `POST /api/marketing/analysis/strategy/{strategyId}/start`
|
||||
- Создает задачи публикации для всех элементов календаря
|
||||
5. **Ручной запуск задачи** (опционально) → `POST /api/marketing/analysis/tasks/{taskId}/execute`
|
||||
- Запускает задачу немедленно, не дожидаясь времени публикации
|
||||
|
||||
### Когда использовать ручной запуск
|
||||
|
||||
- **Тестирование**: Проверить публикацию поста перед запланированным временем
|
||||
- **Повторная попытка**: Повторить выполнение задачи, которая завершилась с ошибкой
|
||||
- **Срочная публикация**: Опубликовать пост раньше запланированного времени
|
||||
- **Отладка**: Проверить работу системы публикации
|
||||
|
||||
---
|
||||
|
||||
## Примечания
|
||||
|
||||
1. **Аутентификация**: Все эндпоинты требуют валидный JWT токен в заголовке `Authorization`
|
||||
2. **Права доступа**: Пользователь может запускать только свои собственные задачи
|
||||
3. **Статусы задач**: Проверяйте статус задачи перед попыткой ручного запуска
|
||||
4. **Асинхронность**: Выполнение задачи происходит асинхронно, ответ возвращается сразу
|
||||
5. **Ошибки выполнения**: Если задача завершится с ошибкой, её можно запустить повторно
|
||||
|
||||
---
|
||||
|
||||
## Версия API
|
||||
|
||||
- **Версия документа**: 1.0
|
||||
- **Дата обновления**: 2025-01-20
|
||||
- **Изменения**:
|
||||
- Добавлен эндпоинт для ручного запуска задач публикации
|
||||
- Добавлено поле `taskId` в элементы календаря постов при получении стратегии
|
||||
@@ -659,6 +659,85 @@ public class MarketingController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/tasks/{taskId}/execute")
|
||||
public ResponseEntity<?> executeTaskManually(
|
||||
@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||
@PathVariable String taskId) {
|
||||
|
||||
String userId = extractUserIdFromHeader(authHeader);
|
||||
if (userId == null) {
|
||||
return unauthorizedResponse();
|
||||
}
|
||||
|
||||
// Проверяем существование задачи и права доступа
|
||||
Optional<PostingTask> optTask = postingTaskService.getTaskById(taskId);
|
||||
if (optTask.isEmpty()) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"NOT_FOUND",
|
||||
"Задача с указанным ID не найдена");
|
||||
return ResponseEntity.status(404)
|
||||
.body(ApiResponse.error("Задача не найдена", error));
|
||||
}
|
||||
|
||||
PostingTask task = optTask.get();
|
||||
if (!userId.equals(task.getUserId())) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"FORBIDDEN",
|
||||
"У вас нет доступа к этой задаче");
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body(ApiResponse.error("Доступ запрещен", error));
|
||||
}
|
||||
|
||||
try {
|
||||
// Запускаем задачу вручную
|
||||
postingTaskService.executeTaskManually(taskId);
|
||||
|
||||
// Получаем обновленную задачу для ответа
|
||||
Optional<PostingTask> updatedTask = postingTaskService.getTaskById(taskId);
|
||||
if (updatedTask.isPresent()) {
|
||||
PostingTask taskData = updatedTask.get();
|
||||
Map<String, Object> responseData = new HashMap<>();
|
||||
responseData.put("taskId", taskData.getId());
|
||||
responseData.put("status", taskData.getStatus());
|
||||
responseData.put("platform", taskData.getPlatform());
|
||||
responseData.put("publishDate", taskData.getPublishDate());
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(
|
||||
"Задача успешно запущена",
|
||||
responseData));
|
||||
} else {
|
||||
// Если задача не найдена после выполнения (маловероятно)
|
||||
Map<String, Object> responseData = new HashMap<>();
|
||||
responseData.put("taskId", taskId);
|
||||
responseData.put("status", "processing");
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(
|
||||
"Задача успешно запущена",
|
||||
responseData));
|
||||
}
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"NOT_FOUND",
|
||||
e.getMessage());
|
||||
return ResponseEntity.status(404)
|
||||
.body(ApiResponse.error("Задача не найдена", error));
|
||||
} catch (IllegalStateException e) {
|
||||
ErrorResponse error = new ErrorResponse(
|
||||
"INVALID_STATUS",
|
||||
e.getMessage());
|
||||
return ResponseEntity.status(400)
|
||||
.body(ApiResponse.error("Задача не может быть запущена", error));
|
||||
} catch (Exception e) {
|
||||
logger.error("Error executing task {} manually: {}", taskId, e.getMessage(), 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) {
|
||||
|
||||
@@ -16,7 +16,9 @@ public class MarketingStrategyResponse {
|
||||
public MarketingStrategyResponse() {
|
||||
}
|
||||
|
||||
public MarketingStrategyResponse(String strategyId, String analysisId, String status, LocalDateTime createdAt, LocalDateTime completedAt, Integer durationWeeks, List<String> priorityPlatforms, StrategyContent strategy) {
|
||||
public MarketingStrategyResponse(String strategyId, String analysisId, String status, LocalDateTime createdAt,
|
||||
LocalDateTime completedAt, Integer durationWeeks, List<String> priorityPlatforms,
|
||||
StrategyContent strategy) {
|
||||
this.strategyId = strategyId;
|
||||
this.analysisId = analysisId;
|
||||
this.status = status;
|
||||
@@ -129,7 +131,8 @@ public class MarketingStrategyResponse {
|
||||
public WeeklyPlan() {
|
||||
}
|
||||
|
||||
public WeeklyPlan(Integer weekNumber, List<String> mainThemes, String contentRecommendations, List<String> priorityPlatforms) {
|
||||
public WeeklyPlan(Integer weekNumber, List<String> mainThemes, String contentRecommendations,
|
||||
List<String> priorityPlatforms) {
|
||||
this.weekNumber = weekNumber;
|
||||
this.mainThemes = mainThemes;
|
||||
this.contentRecommendations = contentRecommendations;
|
||||
@@ -179,11 +182,13 @@ public class MarketingStrategyResponse {
|
||||
private String publishTime;
|
||||
private String imageUrl;
|
||||
private String imageFilename;
|
||||
private String taskId;
|
||||
|
||||
public PostCalendarItem() {
|
||||
}
|
||||
|
||||
public PostCalendarItem(LocalDateTime publishDate, String platform, String contentType, String theme, String postText, List<String> hashtags, String publishTime) {
|
||||
public PostCalendarItem(LocalDateTime publishDate, String platform, String contentType, String theme,
|
||||
String postText, List<String> hashtags, String publishTime) {
|
||||
this.publishDate = publishDate;
|
||||
this.platform = platform;
|
||||
this.contentType = contentType;
|
||||
@@ -264,6 +269,13 @@ public class MarketingStrategyResponse {
|
||||
public void setImageFilename(String imageFilename) {
|
||||
this.imageFilename = imageFilename;
|
||||
}
|
||||
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import kz.konturai.parser.dto.MarketingStrategyResponse;
|
||||
import kz.konturai.parser.dto.StatusHistoryEntry;
|
||||
import kz.konturai.parser.model.MarketingAnalysis;
|
||||
import kz.konturai.parser.model.MarketingStrategy;
|
||||
import kz.konturai.parser.model.PostingTask;
|
||||
import kz.konturai.parser.repository.MarketingAnalysisRepository;
|
||||
import kz.konturai.parser.repository.MarketingStrategyRepository;
|
||||
import org.slf4j.Logger;
|
||||
@@ -32,6 +33,7 @@ public class MarketingStrategyService {
|
||||
private final OpenAIAnalyticsService openAIAnalyticsService;
|
||||
private final OpenAIImageGenerationService imageGenerationService;
|
||||
private final MinIOService minIOService;
|
||||
private final PostingTaskService postingTaskService;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
public MarketingStrategyService(
|
||||
@@ -40,13 +42,15 @@ public class MarketingStrategyService {
|
||||
MarketingAnalysisRepository analysisRepository,
|
||||
OpenAIAnalyticsService openAIAnalyticsService,
|
||||
OpenAIImageGenerationService imageGenerationService,
|
||||
MinIOService minIOService) {
|
||||
MinIOService minIOService,
|
||||
PostingTaskService postingTaskService) {
|
||||
this.repository = repository;
|
||||
this.marketingAnalysisService = marketingAnalysisService;
|
||||
this.analysisRepository = analysisRepository;
|
||||
this.openAIAnalyticsService = openAIAnalyticsService;
|
||||
this.imageGenerationService = imageGenerationService;
|
||||
this.minIOService = minIOService;
|
||||
this.postingTaskService = postingTaskService;
|
||||
}
|
||||
|
||||
public MarketingStrategy generateStrategy(String analysisId, MarketingStrategyRequest request, String userId) {
|
||||
@@ -615,8 +619,11 @@ public class MarketingStrategyService {
|
||||
}
|
||||
strategyContent.setWeeklyPlans(weeklyPlans);
|
||||
|
||||
// Convert PostCalendarItems
|
||||
// Convert PostCalendarItems and add taskId if tasks exist
|
||||
List<MarketingStrategyResponse.PostCalendarItem> postCalendar = new ArrayList<>();
|
||||
// Получаем все задачи для этой стратегии
|
||||
List<PostingTask> tasks = postingTaskService.getStrategyTasks(strategyId);
|
||||
|
||||
for (MarketingStrategy.PostCalendarItem item : strategy.getPostCalendar()) {
|
||||
MarketingStrategyResponse.PostCalendarItem dtoItem = new MarketingStrategyResponse.PostCalendarItem();
|
||||
dtoItem.setPublishDate(item.getPublishDate());
|
||||
@@ -628,6 +635,21 @@ public class MarketingStrategyService {
|
||||
dtoItem.setPublishTime(item.getPublishTime());
|
||||
dtoItem.setImageUrl(item.getImageUrl());
|
||||
dtoItem.setImageFilename(item.getImageFilename());
|
||||
|
||||
// Находим соответствующую задачу по дате публикации, платформе и тексту поста
|
||||
Optional<PostingTask> matchingTask = tasks.stream()
|
||||
.filter(task -> task.getPublishDate() != null && item.getPublishDate() != null
|
||||
&& task.getPublishDate().equals(item.getPublishDate())
|
||||
&& task.getPlatform() != null && item.getPlatform() != null
|
||||
&& task.getPlatform().equalsIgnoreCase(item.getPlatform())
|
||||
&& task.getPostText() != null && item.getPostText() != null
|
||||
&& task.getPostText().equals(item.getPostText()))
|
||||
.findFirst();
|
||||
|
||||
if (matchingTask.isPresent()) {
|
||||
dtoItem.setTaskId(matchingTask.get().getId());
|
||||
}
|
||||
|
||||
postCalendar.add(dtoItem);
|
||||
}
|
||||
strategyContent.setPostCalendar(postCalendar);
|
||||
|
||||
@@ -225,6 +225,44 @@ public class PostingTaskService {
|
||||
executeTask(taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ручное выполнение задачи публикации (независимо от времени публикации)
|
||||
* Разрешает выполнение только для задач со статусом "pending" или "failed"
|
||||
*
|
||||
* @param taskId ID задачи
|
||||
* @throws IllegalArgumentException если задача не найдена
|
||||
* @throws IllegalStateException если задача уже выполнена или имеет
|
||||
* недопустимый статус
|
||||
*/
|
||||
public void executeTaskManually(String taskId) {
|
||||
Optional<PostingTask> optTask = taskRepository.findById(taskId);
|
||||
if (optTask.isEmpty()) {
|
||||
throw new IllegalArgumentException("Task not found: " + taskId);
|
||||
}
|
||||
|
||||
PostingTask task = optTask.get();
|
||||
|
||||
// Проверяем статус - разрешаем только pending или failed
|
||||
String status = task.getStatus();
|
||||
if (!"pending".equals(status) && !"failed".equals(status)) {
|
||||
throw new IllegalStateException(
|
||||
"Task cannot be executed manually. Current status: " + status +
|
||||
". Only tasks with status 'pending' or 'failed' can be executed manually.");
|
||||
}
|
||||
|
||||
// Если задача в статусе failed, сбрасываем статус на pending для повторной
|
||||
// попытки
|
||||
if ("failed".equals(status)) {
|
||||
task.setStatus("pending");
|
||||
task.setErrorMessage(null);
|
||||
taskRepository.save(task);
|
||||
logger.info("Task {} status reset from 'failed' to 'pending' for manual execution", taskId);
|
||||
}
|
||||
|
||||
// Выполняем задачу (игнорируя время публикации)
|
||||
executeTask(taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает задачи пользователя
|
||||
*
|
||||
@@ -244,4 +282,14 @@ public class PostingTaskService {
|
||||
public List<PostingTask> getStrategyTasks(String strategyId) {
|
||||
return taskRepository.findByStrategyId(strategyId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает задачу по ID
|
||||
*
|
||||
* @param taskId ID задачи
|
||||
* @return Optional с задачей, если найдена
|
||||
*/
|
||||
public Optional<PostingTask> getTaskById(String taskId) {
|
||||
return taskRepository.findById(taskId);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user