This commit is contained in:
root
2025-12-05 19:22:44 +05:00
parent f2906d3eae
commit 894b226b27
5 changed files with 453 additions and 6 deletions
+286
View File
@@ -0,0 +1,286 @@
# API Документация: Управление задачами публикации (Frontend/AI Agent)
## Базовый URL
```
https://api.konturai.kz
```
## Обзор
API для управления задачами публикации постов в социальных сетях. Позволяет запускать задачи публикации вручную, не дожидаясь времени выполнения через шедулер.
**Важно**:
- Для выполнения задачи требуются настроенные credentials для соответствующей платформы
- Задачи создаются автоматически при запуске стратегии через `/api/marketing/analysis/strategy/{strategyId}/start`
- Каждая задача связана с элементом календаря постов в стратегии
---
## Изменения в существующих эндпоинтах
### Обновление: Получение стратегии по ID анализа
**GET** `/api/marketing/analysis/{analysisId}/strategy`
Теперь каждый элемент в `postCalendar` содержит поле `taskId`, если задача была создана для этого элемента.
#### Пример ответа (обновленный формат)
```json
{
"success": true,
"data": {
"strategyId": "507f1f77bcf86cd799439012",
"analysisId": "507f1f77bcf86cd799439011",
"status": "completed",
"strategy": {
"postCalendar": [
{
"publishDate": "2025-01-25T10:00:00",
"platform": "Facebook",
"contentType": "пост",
"theme": "Презентация продукта",
"postText": "Добро пожаловать в наш новый продукт!",
"hashtags": ["#маркетинг", "#бизнес"],
"publishTime": "10:00",
"imageUrl": "post_image_1234567890.png",
"imageFilename": "post_image_1234567890.png",
"taskId": "507f1f77bcf86cd799439013"
}
]
}
}
}
```
**Новое поле:**
- `taskId` (string, опциональное) - ID задачи публикации, если задача была создана. Может быть `null`, если стратегия еще не была запущена.
---
## Новые эндпоинты
### 1. Ручной запуск задачи публикации
**POST** `/api/marketing/analysis/tasks/{taskId}/execute`
Запускает задачу публикации немедленно, не дожидаясь времени публикации через шедулер. Позволяет выполнить задачу вручную или повторить выполнение неудачной задачи.
#### Параметры запроса
| Параметр | Тип | Расположение | Обязательный | Описание |
| -------- | ------ | ------------ | ------------ | -------------------- |
| `taskId` | string | Path | ✅ | ID задачи публикации |
#### Заголовки запроса
```
Authorization: Bearer <your-jwt-token>
Content-Type: application/json
```
#### Пример запроса
```http
POST /api/marketing/analysis/tasks/507f1f77bcf86cd799439013/execute
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
```
#### Пример успешного ответа (200 OK)
```json
{
"success": true,
"message": "Задача успешно запущена",
"data": {
"taskId": "507f1f77bcf86cd799439013",
"status": "processing",
"platform": "Facebook",
"publishDate": "2025-01-25T10:00:00"
}
}
```
#### Описание полей ответа
| Поле | Тип | Описание |
| ------------- | ------ | ----------------------------------------------------- |
| `taskId` | string | ID задачи публикации |
| `status` | string | Статус задачи: `processing`, `completed` или `failed` |
| `platform` | string | Платформа для публикации (например, `Facebook`) |
| `publishDate` | string | Дата и время публикации в формате ISO 8601 |
#### Ошибки
**401 Unauthorized** - Требуется аутентификация
```json
{
"success": false,
"message": "Не авторизован",
"error": {
"code": "UNAUTHORIZED",
"message": "Требуется аутентификация. Пожалуйста, предоставьте валидный JWT токен."
}
}
```
**403 Forbidden** - Пользователь не является владельцем задачи
```json
{
"success": false,
"message": "Доступ запрещен",
"error": {
"code": "FORBIDDEN",
"message": "У вас нет доступа к этой задаче"
}
}
```
**404 Not Found** - Задача не найдена
```json
{
"success": false,
"message": "Задача не найдена",
"error": {
"code": "NOT_FOUND",
"message": "Задача с указанным ID не найдена"
}
}
```
**400 Bad Request** - Задача не может быть запущена
```json
{
"success": false,
"message": "Задача не может быть запущена",
"error": {
"code": "INVALID_STATUS",
"message": "Task cannot be executed manually. Current status: completed. Only tasks with status 'pending' or 'failed' can be executed manually."
}
}
```
#### Правила выполнения
1. **Статусы задач:**
- `pending` - задача ожидает выполнения (может быть запущена вручную)
- `failed` - задача завершилась с ошибкой (может быть запущена повторно вручную)
- `processing` - задача выполняется (не может быть запущена повторно)
- `completed` - задача успешно выполнена (не может быть запущена повторно)
2. **Повторное выполнение:**
- Задачи со статусом `failed` автоматически сбрасываются на `pending` перед повторным выполнением
- Ошибка из предыдущего выполнения очищается
3. **Асинхронное выполнение:**
- Задача запускается асинхронно
- Ответ возвращается сразу после начала выполнения
- Для проверки статуса задачи используйте соответствующие эндпоинты (если доступны)
#### Примеры использования
**Пример 1: Запуск задачи, которая еще не была выполнена**
```javascript
// Получаем стратегию
const strategyResponse = await fetch(
`/api/marketing/analysis/${analysisId}/strategy`,
{
headers: {
Authorization: `Bearer ${token}`,
},
}
);
const strategy = await strategyResponse.json();
const taskId = strategy.data.strategy.postCalendar[0].taskId;
// Запускаем задачу вручную
const executeResponse = await fetch(
`/api/marketing/analysis/tasks/${taskId}/execute`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
}
);
const result = await executeResponse.json();
console.log('Задача запущена:', result.data);
```
**Пример 2: Повторное выполнение неудачной задачи**
```javascript
// Если задача завершилась с ошибкой (status: "failed")
// можно повторить её выполнение
const retryResponse = await fetch(
`/api/marketing/analysis/tasks/${failedTaskId}/execute`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
}
);
const retryResult = await retryResponse.json();
if (retryResult.success) {
console.log('Повторная попытка запущена:', retryResult.data);
}
```
---
## Интеграция с существующим workflow
### Типичный сценарий использования
1. **Создание анализа**`POST /api/marketing/analysis/start`
2. **Генерация стратегии**`POST /api/marketing/analysis/strategy/generate`
3. **Получение стратегии**`GET /api/marketing/analysis/{analysisId}/strategy`
- Теперь содержит `taskId` для каждого элемента календаря
4. **Запуск стратегии**`POST /api/marketing/analysis/strategy/{strategyId}/start`
- Создает задачи публикации для всех элементов календаря
5. **Ручной запуск задачи** (опционально) → `POST /api/marketing/analysis/tasks/{taskId}/execute`
- Запускает задачу немедленно, не дожидаясь времени публикации
### Когда использовать ручной запуск
- **Тестирование**: Проверить публикацию поста перед запланированным временем
- **Повторная попытка**: Повторить выполнение задачи, которая завершилась с ошибкой
- **Срочная публикация**: Опубликовать пост раньше запланированного времени
- **Отладка**: Проверить работу системы публикации
---
## Примечания
1. **Аутентификация**: Все эндпоинты требуют валидный JWT токен в заголовке `Authorization`
2. **Права доступа**: Пользователь может запускать только свои собственные задачи
3. **Статусы задач**: Проверяйте статус задачи перед попыткой ручного запуска
4. **Асинхронность**: Выполнение задачи происходит асинхронно, ответ возвращается сразу
5. **Ошибки выполнения**: Если задача завершится с ошибкой, её можно запустить повторно
---
## Версия API
- **Версия документа**: 1.0
- **Дата обновления**: 2025-01-20
- **Изменения**:
- Добавлен эндпоинт для ручного запуска задач публикации
- Добавлено поле `taskId` в элементы календаря постов при получении стратегии
@@ -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);
}
}