diff --git a/src/main/java/kz/konturai/parser/controller/MarketingAnalysisV3Controller.java b/src/main/java/kz/konturai/parser/controller/MarketingAnalysisV3Controller.java index a0d6206..49e1b27 100644 --- a/src/main/java/kz/konturai/parser/controller/MarketingAnalysisV3Controller.java +++ b/src/main/java/kz/konturai/parser/controller/MarketingAnalysisV3Controller.java @@ -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 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 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 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 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 optAnalysis = analysisService.getAnalysisById(analysisId); + if (optAnalysis.isEmpty()) return notFoundResponse("Анализ не найден"); + if (!optAnalysis.get().getUserId().equals(userId)) return forbiddenResponse(); + + Optional 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 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 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 tasks = postingTaskService.createTasksFromStrategy(strategyId); + List 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 optTask = postingTaskService.getTaskById(taskId); + if (optTask.isEmpty()) return notFoundResponse("Задача не найдена"); + + PostingTask task = optTask.get(); + if (!userId.equals(task.getUserId())) return forbiddenResponse(); + + postingTaskService.executeTaskManually(taskId); + + Optional updatedTask = postingTaskService.getTaskById(taskId); + if (updatedTask.isPresent()) { + PostingTask taskData = updatedTask.get(); + Map 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 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 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); } } diff --git a/src/main/java/kz/konturai/parser/dto/StatusHistoryEntry.java b/src/main/java/kz/konturai/parser/dto/StatusHistoryEntry.java index 1cc47b1..72541d2 100644 --- a/src/main/java/kz/konturai/parser/dto/StatusHistoryEntry.java +++ b/src/main/java/kz/konturai/parser/dto/StatusHistoryEntry.java @@ -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; - } } diff --git a/src/main/java/kz/konturai/parser/service/MarketingStrategyV3Service.java b/src/main/java/kz/konturai/parser/service/MarketingStrategyV3Service.java index f40c47e..4774705 100644 --- a/src/main/java/kz/konturai/parser/service/MarketingStrategyV3Service.java +++ b/src/main/java/kz/konturai/parser/service/MarketingStrategyV3Service.java @@ -51,13 +51,17 @@ public class MarketingStrategyV3Service { return repository.findById(id); } + public Optional getStrategyByAnalysisId(String analysisId) { + return repository.findByAnalysisId(analysisId); + } + public List 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 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 optStrategy = repository.findById(strategyId); + if (optStrategy.isEmpty()) { + return null; + } + + MarketingStrategy strategy = optStrategy.get(); + List postCalendar = strategy.getPostCalendar(); + + if (postCalendar == null || postIndex < 0 || postIndex >= postCalendar.size()) { + return null; + } + + MarketingStrategy.PostCalendarItem item = postCalendar.get(postIndex); + + Optional 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()); } }