This commit is contained in:
arys
2026-03-01 11:56:24 +05:00
parent fa877c2e41
commit 6803106aae
3 changed files with 311 additions and 67 deletions
@@ -1,20 +1,19 @@
package kz.konturai.parser.controller;
import jakarta.validation.Valid;
import kz.konturai.parser.dto.ApiResponse;
import kz.konturai.parser.dto.ErrorResponse;
import kz.konturai.parser.dto.MarketingAnalysisV3Request;
import kz.konturai.parser.dto.MarketingStrategyRequest;
import kz.konturai.parser.dto.StrategyHistoryResponse;
import kz.konturai.parser.dto.*;
import kz.konturai.parser.enums.StrategyModel;
import kz.konturai.parser.model.MarketingAnalysisV3Document;
import kz.konturai.parser.model.MarketingStrategy;
import kz.konturai.parser.model.PostingTask;
import kz.konturai.parser.service.JwtService;
import kz.konturai.parser.service.MarketingAnalysisV3Service;
import kz.konturai.parser.service.MarketingStrategyV3Service;
import kz.konturai.parser.service.MinIOService;
import kz.konturai.parser.service.PostingTaskService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
@@ -22,6 +21,7 @@ import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -37,6 +37,7 @@ public class MarketingAnalysisV3Controller {
private final MarketingAnalysisV3Service analysisService;
private final MarketingStrategyV3Service strategyService;
private final PostingTaskService postingTaskService;
private final MinIOService minIOService;
private final JwtService jwtService;
@@ -47,7 +48,6 @@ public class MarketingAnalysisV3Controller {
try {
return jwtService.extractUserIdFromHeader(authHeader);
} catch (Exception e) {
log.error("Error extracting userId from JWT: {}", e.getMessage());
return null;
}
}
@@ -68,7 +68,6 @@ public class MarketingAnalysisV3Controller {
);
return ResponseEntity.accepted().body(ApiResponse.success("Анализ запущен", responseData));
} catch (Exception e) {
log.error("Failed to start analysis V3", e);
return internalErrorResponse(e);
}
}
@@ -90,7 +89,6 @@ public class MarketingAnalysisV3Controller {
return ResponseEntity.ok(ApiResponse.success(analysis));
} catch (Exception e) {
log.error("Error fetching analysis V3: {}", id, e);
return internalErrorResponse(e);
}
}
@@ -106,11 +104,102 @@ public class MarketingAnalysisV3Controller {
List<MarketingAnalysisV3Document> analyses = analysisService.getAllByUser(userId);
return ResponseEntity.ok(ApiResponse.success(analyses));
} catch (Exception e) {
log.error("Error fetching user analyses V3", e);
return internalErrorResponse(e);
}
}
@GetMapping("/{analysisId}/history")
public ResponseEntity<?> getAnalysisHistory(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@PathVariable String analysisId
) {
String userId = extractUserIdFromHeader(authHeader);
if (userId == null) return unauthorizedResponse();
try {
Optional<MarketingAnalysisV3Document> analysisOpt = analysisService.getAnalysisById(analysisId);
if (analysisOpt.isEmpty()) return notFoundResponse("Анализ не найден");
MarketingAnalysisV3Document analysis = analysisOpt.get();
if (!analysis.getUserId().equals(userId)) return forbiddenResponse();
AnalysisHistoryResponse response = new AnalysisHistoryResponse();
response.setAnalysisId(analysis.getId());
if (analysis.getRequestData() != null) {
response.setBusinessNiche(analysis.getRequestData().getBusinessNiche());
response.setProduct(analysis.getRequestData().getProductName());
response.setGoal(analysis.getRequestData().getGoal());
response.setDetailLevel(analysis.getRequestData().getDetailLevel());
if (analysis.getRequestData().getAnalysisType() != null) {
response.setAnalysisType(String.join(", ", analysis.getRequestData().getAnalysisType()));
}
if (analysis.getRequestData().getClientTarget() != null) {
response.setTargetAudience(analysis.getRequestData().getClientTarget().name());
}
if (analysis.getRequestData().getPromotionCities() != null) {
response.setRegion(String.join(", ", analysis.getRequestData().getPromotionCities()));
}
}
response.setStatus(analysis.getStatus());
response.setUserId(analysis.getUserId());
response.setCreatedAt(analysis.getCreatedAt());
response.setCompletedAt(analysis.getUpdatedAt());
return ResponseEntity.ok(ApiResponse.success(response));
} catch (Exception e) {
return internalErrorResponse(e);
}
}
@GetMapping("/{analysisId}/download")
public ResponseEntity<?> downloadPdf(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@PathVariable String analysisId
) {
String userId = extractUserIdFromHeader(authHeader);
if (userId == null) return unauthorizedResponse();
try {
Optional<MarketingAnalysisV3Document> analysisOpt = analysisService.getAnalysisById(analysisId);
if (analysisOpt.isEmpty()) return ResponseEntity.notFound().build();
MarketingAnalysisV3Document analysis = analysisOpt.get();
if (!analysis.getUserId().equals(userId)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
return ResponseEntity.notFound().build();
} catch (Exception e) {
return ResponseEntity.internalServerError().build();
}
}
@GetMapping("/images/{imageFilename}")
public ResponseEntity<byte[]> getPostImage(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@PathVariable String imageFilename
) {
String userId = extractUserIdFromHeader(authHeader);
if (userId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
try {
if (!minIOService.fileExists(imageFilename)) {
return ResponseEntity.notFound().build();
}
InputStream inputStream = minIOService.downloadFile(imageFilename);
byte[] bytes = inputStream.readAllBytes();
inputStream.close();
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE)
.header(HttpHeaders.CACHE_CONTROL, "public, max-age=3600")
.body(bytes);
} catch (Exception e) {
return ResponseEntity.internalServerError().build();
}
}
@GetMapping("/{analysisId}/strategy-preview")
public ResponseEntity<?> previewStrategy(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@@ -134,7 +223,6 @@ public class MarketingAnalysisV3Controller {
return ResponseEntity.ok(ApiResponse.success("Рекомендация сформирована", responseData));
} catch (Exception e) {
log.error("Error previewing strategy for analysis: {}", analysisId, e);
return internalErrorResponse(e);
}
}
@@ -160,7 +248,6 @@ public class MarketingAnalysisV3Controller {
logoFile.getOriginalFilename().substring(logoFile.getOriginalFilename().lastIndexOf(".")) : ".png";
logoFilename = "logo_" + UUID.randomUUID() + originalExt;
minIOService.uploadFile(logoFilename, logoFile.getBytes(), logoFile.getContentType());
log.info("Логотип клиента успешно загружен в MinIO: {}", logoFilename);
}
if (request == null) {
@@ -177,7 +264,6 @@ public class MarketingAnalysisV3Controller {
return ResponseEntity.accepted().body(ApiResponse.success("Автономная генерация запущена", responseData));
} catch (Exception e) {
log.error("Failed to generate strategy for analysis: {}", analysisId, e);
return internalErrorResponse(e);
}
}
@@ -199,7 +285,28 @@ public class MarketingAnalysisV3Controller {
return ResponseEntity.ok(ApiResponse.success(strategy));
} catch (Exception e) {
log.error("Error fetching strategy: {}", strategyId, e);
return internalErrorResponse(e);
}
}
@GetMapping("/{analysisId}/strategy")
public ResponseEntity<?> getStrategyByAnalysis(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@PathVariable String analysisId
) {
String userId = extractUserIdFromHeader(authHeader);
if (userId == null) return unauthorizedResponse();
try {
Optional<MarketingAnalysisV3Document> optAnalysis = analysisService.getAnalysisById(analysisId);
if (optAnalysis.isEmpty()) return notFoundResponse("Анализ не найден");
if (!optAnalysis.get().getUserId().equals(userId)) return forbiddenResponse();
Optional<MarketingStrategy> strategyOpt = strategyService.getStrategyByAnalysisId(analysisId);
if (strategyOpt.isEmpty()) return notFoundResponse("Стратегия не найдена");
return ResponseEntity.ok(ApiResponse.success(strategyOpt.get()));
} catch (Exception e) {
return internalErrorResponse(e);
}
}
@@ -219,7 +326,138 @@ public class MarketingAnalysisV3Controller {
return ResponseEntity.ok(ApiResponse.success(responseList));
} catch (Exception e) {
log.error("Error fetching user strategies", e);
return internalErrorResponse(e);
}
}
@GetMapping("/strategy/{strategyId}/history")
public ResponseEntity<?> getStrategyHistory(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@PathVariable String strategyId
) {
String userId = extractUserIdFromHeader(authHeader);
if (userId == null) return unauthorizedResponse();
try {
Optional<MarketingStrategy> optStrategy = strategyService.getStrategyById(strategyId);
if (optStrategy.isEmpty()) return notFoundResponse("Стратегия не найдена");
MarketingStrategy strategy = optStrategy.get();
if (!strategy.getUserId().equals(userId)) return forbiddenResponse();
StrategyHistoryResponse response = convertToStrategyHistoryResponse(strategy);
return ResponseEntity.ok(ApiResponse.success(response));
} catch (Exception e) {
return internalErrorResponse(e);
}
}
@PostMapping("/strategy/{strategyId}/start")
public ResponseEntity<?> startStrategy(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@PathVariable String strategyId
) {
String userId = extractUserIdFromHeader(authHeader);
if (userId == null) return unauthorizedResponse();
try {
Optional<MarketingStrategy> optStrategy = strategyService.getStrategyById(strategyId);
if (optStrategy.isEmpty()) return notFoundResponse("Стратегия не найдена");
MarketingStrategy strategy = optStrategy.get();
if (!strategy.getUserId().equals(userId)) return forbiddenResponse();
if (!"completed".equals(strategy.getStatus())) {
ErrorResponse error = new ErrorResponse("INVALID_STATUS", "Стратегия еще не завершена. Статус: " + strategy.getStatus());
return ResponseEntity.status(400).body(ApiResponse.error("Стратегия не готова к запуску", error));
}
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) {
return ResponseEntity.status(400).body(ApiResponse.error("Не удалось запустить стратегию", new ErrorResponse("ERROR", e.getMessage())));
} catch (Exception e) {
return internalErrorResponse(e);
}
}
@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();
try {
Optional<PostingTask> optTask = postingTaskService.getTaskById(taskId);
if (optTask.isEmpty()) return notFoundResponse("Задача не найдена");
PostingTask task = optTask.get();
if (!userId.equals(task.getUserId())) return forbiddenResponse();
postingTaskService.executeTaskManually(taskId);
Optional<PostingTask> updatedTask = postingTaskService.getTaskById(taskId);
if (updatedTask.isPresent()) {
PostingTask taskData = updatedTask.get();
Map<String, Object> responseData = Map.of(
"taskId", taskData.getId(),
"status", taskData.getStatus(),
"platform", taskData.getPlatform(),
"publishDate", taskData.getPublishDate()
);
return ResponseEntity.ok(ApiResponse.success("Задача успешно запущена", responseData));
}
return ResponseEntity.ok(ApiResponse.success("Задача отправлена в обработку", Map.of("taskId", taskId)));
} catch (IllegalStateException e) {
return ResponseEntity.status(400).body(ApiResponse.error("Задача не может быть запущена", new ErrorResponse("INVALID_STATUS", e.getMessage())));
} catch (Exception e) {
return internalErrorResponse(e);
}
}
@PostMapping("/strategy/{strategyId}/post/{postIndex}/regenerate-image")
public ResponseEntity<?> regeneratePostImage(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@PathVariable String strategyId,
@PathVariable int postIndex
) {
String userId = extractUserIdFromHeader(authHeader);
if (userId == null) return unauthorizedResponse();
try {
Optional<MarketingStrategy> optStrategy = strategyService.getStrategyById(strategyId);
if (optStrategy.isEmpty()) return notFoundResponse("Стратегия не найдена");
MarketingStrategy strategy = optStrategy.get();
if (!strategy.getUserId().equals(userId)) return forbiddenResponse();
MarketingStrategy.PostCalendarItem updatedItem = strategyService.regeneratePostImage(strategyId, postIndex);
if (updatedItem == null) return notFoundResponse("Пост не найден");
Map<String, Object> responseData = Map.of(
"strategyId", strategyId,
"postIndex", postIndex,
"imageUrl", updatedItem.getImageUrl() != null ? updatedItem.getImageUrl() : "",
"imageFilename", updatedItem.getImageFilename() != null ? updatedItem.getImageFilename() : "",
"theme", updatedItem.getTheme() != null ? updatedItem.getTheme() : "",
"platform", updatedItem.getPlatform() != null ? updatedItem.getPlatform() : "",
"publishDate", updatedItem.getPublishDate()
);
return ResponseEntity.ok(ApiResponse.success("Изображение для поста успешно регенерировано", responseData));
} catch (Exception e) {
return internalErrorResponse(e);
}
}
@@ -1,48 +1,17 @@
package kz.konturai.parser.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class StatusHistoryEntry {
private String status;
private LocalDateTime timestamp;
private String message;
public StatusHistoryEntry() {
}
public StatusHistoryEntry(String status, LocalDateTime timestamp) {
this.status = status;
this.timestamp = timestamp;
}
public StatusHistoryEntry(String status, LocalDateTime timestamp, String message) {
this.status = status;
this.timestamp = timestamp;
this.message = message;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public LocalDateTime getTimestamp() {
return timestamp;
}
public void setTimestamp(LocalDateTime timestamp) {
this.timestamp = timestamp;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
@@ -51,13 +51,17 @@ public class MarketingStrategyV3Service {
return repository.findById(id);
}
public Optional<MarketingStrategy> getStrategyByAnalysisId(String analysisId) {
return repository.findByAnalysisId(analysisId);
}
public List<MarketingStrategy> getUserStrategies(String userId) {
return repository.findByUserIdOrderByCreatedAtDesc(userId);
}
public MarketingStrategy generateStrategy(String analysisId, MarketingStrategyRequest request, String userId, String uploadedLogoFilename) {
MarketingAnalysisV3Document analysisDoc = analysisRepository.findById(analysisId)
.orElseThrow(() -> new IllegalArgumentException("Анализ V3 не найден"));
.orElseThrow(() -> new IllegalArgumentException("Analysis V3 not found"));
MarketingStrategy strategy = new MarketingStrategy(analysisId);
strategy.setUserId(userId);
@@ -87,7 +91,6 @@ public class MarketingStrategyV3Service {
repository.save(strategy);
StrategyModel bestModel = calculateBestScoringModel(analysis.getRequestData());
log.info("[Strategy ID: {}] Выбрана модель: {}", strategyId, bestModel.name());
String systemPrompt = buildSystemPrompt(bestModel);
String userPrompt = buildUserPrompt(analysis, bestModel);
@@ -96,7 +99,7 @@ public class MarketingStrategyV3Service {
String jsonResponse = extractCleanJson(rawResponse);
if (jsonResponse == null) {
throw new IllegalStateException("AI не вернул валидный JSON");
throw new IllegalStateException("AI failed to return valid JSON");
}
Map<String, Object> strategyData = objectMapper.readValue(jsonResponse, new TypeReference<>() {});
@@ -119,11 +122,52 @@ public class MarketingStrategyV3Service {
repository.save(strategy);
} catch (Exception e) {
log.error("Ошибка при генерации стратегии {}: {}", strategyId, e.getMessage(), e);
log.error("Strategy generation error {}: {}", strategyId, e.getMessage(), e);
markAsFailed(strategyId, "Ошибка при генерации стратегии: " + e.getMessage());
}
}
public MarketingStrategy.PostCalendarItem regeneratePostImage(String strategyId, int postIndex) {
Optional<MarketingStrategy> optStrategy = repository.findById(strategyId);
if (optStrategy.isEmpty()) {
return null;
}
MarketingStrategy strategy = optStrategy.get();
List<MarketingStrategy.PostCalendarItem> postCalendar = strategy.getPostCalendar();
if (postCalendar == null || postIndex < 0 || postIndex >= postCalendar.size()) {
return null;
}
MarketingStrategy.PostCalendarItem item = postCalendar.get(postIndex);
Optional<MarketingAnalysisV3Document> analysisOpt = analysisRepository.findById(strategy.getAnalysisId());
if (analysisOpt.isEmpty()) {
return null;
}
MarketingAnalysisV3Document analysis = analysisOpt.get();
String businessContext = getBusinessContext(analysis);
String brandName = analysis.getRequestData().getProductName();
byte[] clientLogoBytes = null;
if (strategy.getStrategyData() != null && strategy.getStrategyData().containsKey("clientLogoFilename")) {
String logoFilename = (String) strategy.getStrategyData().get("clientLogoFilename");
try {
InputStream logoStream = minIOService.downloadFile(logoFilename);
clientLogoBytes = logoStream.readAllBytes();
logoStream.close();
} catch (Exception e) {
log.error("Failed to download logo for regeneration: {}", e.getMessage());
}
}
doGenerateImage(item, businessContext, brandName, clientLogoBytes);
repository.save(strategy);
return item;
}
public StrategyModel calculateBestScoringModel(kz.konturai.parser.dto.MarketingAnalysisV3Request req) {
int entry = 0, authority = 0, trust = 0, conversion = 0;
@@ -238,9 +282,8 @@ public class MarketingStrategyV3Service {
InputStream logoStream = minIOService.downloadFile(logoFilename);
clientLogoBytes = logoStream.readAllBytes();
logoStream.close();
log.info("Загружен логотип для генерации: {}", logoFilename);
} catch (Exception e) {
log.error("Не удалось скачать логотип {}: {}", logoFilename, e.getMessage());
log.error("Failed to download logo: {}", e.getMessage());
}
}
@@ -250,13 +293,12 @@ public class MarketingStrategyV3Service {
try {
if (contentType.contains("видео") || contentType.contains("reels") || contentType.contains("tiktok")) {
String videoPrompt = "High quality cinematic commercial video. Business niche: " + businessContext + ". Scene: " + item.getTheme() + ". Photorealistic, dynamic motion, 4k.";
log.info("Попытка генерации ВИДЕО: {}", videoPrompt);
byte[] videoBytes = null;
try {
videoBytes = geminiVideoService.generateVideo(videoPrompt);
} catch (Exception e) {
log.warn("Сервис видео упал с ошибкой: {}", e.getMessage());
log.warn("Video service failed: {}", e.getMessage());
}
if (videoBytes != null && videoBytes.length > 0) {
@@ -264,9 +306,7 @@ public class MarketingStrategyV3Service {
minIOService.uploadFile(filename, videoBytes, "video/mp4");
item.setVideoUrl(filename);
item.setVideoFilename(filename);
log.info("Видео успешно сгенерировано: {}", filename);
} else {
log.warn("Видео не сгенерировалось (вернулся null). Включаю FALLBACK -> генерирую ФОТО для темы: {}", item.getTheme());
item.setContentType("фото");
doGenerateImage(item, businessContext, brandName, clientLogoBytes);
}
@@ -277,14 +317,13 @@ public class MarketingStrategyV3Service {
if (delayBetweenRequestsMs > 0) Thread.sleep(delayBetweenRequestsMs);
} catch (Exception e) {
log.error("Критическая ошибка создания медиа для поста '{}': {}", item.getTheme(), e.getMessage());
log.error("Error creating media for post '{}': {}", item.getTheme(), e.getMessage());
}
}
}
private void doGenerateImage(MarketingStrategy.PostCalendarItem item, String businessContext, String brandName, byte[] clientLogoBytes) {
String imagePrompt = buildImagePrompt(item, businessContext, brandName);
log.info("Генерация ФОТО: {}", imagePrompt);
try {
byte[] imageBytes = imageGenerationService.generateImageWithReference(imagePrompt, clientLogoBytes);
if (imageBytes != null && imageBytes.length > 0) {
@@ -292,11 +331,9 @@ public class MarketingStrategyV3Service {
minIOService.uploadFile(filename, imageBytes, MediaType.IMAGE_PNG_VALUE);
item.setImageUrl(filename);
item.setImageFilename(filename);
} else {
log.warn("NanoBanana вернул пустой массив байтов для темы: {}", item.getTheme());
}
} catch (Exception e) {
log.error("Генерация фото упала с ошибкой: {}", e.getMessage());
log.error("Image generation failed: {}", e.getMessage());
}
}