diff --git a/src/main/java/kz/konturai/parser/dto/MarketingStrategyResponse.java b/src/main/java/kz/konturai/parser/dto/MarketingStrategyResponse.java index 1fef964..f6f4d0b 100644 --- a/src/main/java/kz/konturai/parser/dto/MarketingStrategyResponse.java +++ b/src/main/java/kz/konturai/parser/dto/MarketingStrategyResponse.java @@ -177,6 +177,8 @@ public class MarketingStrategyResponse { private String postText; private List hashtags; private String publishTime; + private String imageUrl; + private String imageFilename; public PostCalendarItem() { } @@ -246,6 +248,22 @@ public class MarketingStrategyResponse { public void setPublishTime(String publishTime) { this.publishTime = publishTime; } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public String getImageFilename() { + return imageFilename; + } + + public void setImageFilename(String imageFilename) { + this.imageFilename = imageFilename; + } } } diff --git a/src/main/java/kz/konturai/parser/model/MarketingStrategy.java b/src/main/java/kz/konturai/parser/model/MarketingStrategy.java index 071f76e..6a0b954 100644 --- a/src/main/java/kz/konturai/parser/model/MarketingStrategy.java +++ b/src/main/java/kz/konturai/parser/model/MarketingStrategy.java @@ -234,6 +234,12 @@ public class MarketingStrategy { @Field("publish_time") private String publishTime; // время публикации в формате HH:mm + @Field("image_url") + private String imageUrl; + + @Field("image_filename") + private String imageFilename; + public PostCalendarItem() { } @@ -302,6 +308,22 @@ public class MarketingStrategy { public void setPublishTime(String publishTime) { this.publishTime = publishTime; } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public String getImageFilename() { + return imageFilename; + } + + public void setImageFilename(String imageFilename) { + this.imageFilename = imageFilename; + } } } diff --git a/src/main/java/kz/konturai/parser/model/PostingTask.java b/src/main/java/kz/konturai/parser/model/PostingTask.java index 7e1620e..2e067c4 100644 --- a/src/main/java/kz/konturai/parser/model/PostingTask.java +++ b/src/main/java/kz/konturai/parser/model/PostingTask.java @@ -44,6 +44,12 @@ public class PostingTask { @Field("error_message") private String errorMessage; + @Field("image_url") + private String imageUrl; + + @Field("image_filename") + private String imageFilename; + public PostingTask() { this.createdAt = LocalDateTime.now(); this.status = "pending"; @@ -148,5 +154,21 @@ public class PostingTask { public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public String getImageFilename() { + return imageFilename; + } + + public void setImageFilename(String imageFilename) { + this.imageFilename = imageFilename; + } } diff --git a/src/main/java/kz/konturai/parser/service/FacebookPostingService.java b/src/main/java/kz/konturai/parser/service/FacebookPostingService.java index 92ed7d3..a60af95 100644 --- a/src/main/java/kz/konturai/parser/service/FacebookPostingService.java +++ b/src/main/java/kz/konturai/parser/service/FacebookPostingService.java @@ -6,8 +6,15 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.http.MediaType; import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClientResponseException; import reactor.netty.http.client.HttpClient; @@ -21,7 +28,7 @@ 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; @@ -31,12 +38,12 @@ public class FacebookPostingService { 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(); } @@ -44,8 +51,8 @@ public class FacebookPostingService { * Публикует пост в Facebook через Graph API * * @param accessToken Access Token пользователя - * @param postText Текст поста - * @param hashtags Список хештегов + * @param postText Текст поста + * @param hashtags Список хештегов * @return ID опубликованного поста * @throws RuntimeException если публикация не удалась */ @@ -53,16 +60,16 @@ public class FacebookPostingService { 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); @@ -91,11 +98,13 @@ public class FacebookPostingService { return jsonNode.get("id").asText(); } catch (JsonProcessingException e) { logger.error("Failed to parse Facebook page ID response", e); - // Если не удалось получить ID страницы, используем "me" для публикации на стене пользователя + // Если не удалось получить ID страницы, используем "me" для публикации на стене + // пользователя return "me"; } catch (Exception e) { logger.error("Failed to get Facebook page ID", e); - // Если не удалось получить ID страницы, используем "me" для публикации на стене пользователя + // Если не удалось получить ID страницы, используем "me" для публикации на стене + // пользователя return "me"; } } @@ -103,7 +112,7 @@ public class FacebookPostingService { /** * Публикует пост на странице Facebook */ - private String publishPost(String accessToken, String pageId, String message) + private String publishPost(String accessToken, String pageId, String message) throws JsonProcessingException { String response = webClient.post() .uri(uriBuilder -> uriBuilder @@ -119,26 +128,93 @@ public class FacebookPostingService { return jsonNode.get("id").asText(); } + /** + * Публикует пост в Facebook с изображением через Graph API + * + * @param accessToken Access Token пользователя + * @param postText Текст поста + * @param hashtags Список хештегов + * @param imageData Данные изображения + * @param imageContentType MIME тип изображения (например, "image/png") + * @return ID опубликованного поста + * @throws RuntimeException если публикация не удалась + */ + public String postToFacebookWithImage(String accessToken, String postText, List hashtags, + byte[] imageData, String imageContentType) { + try { + // Формируем полный текст поста с хештегами + String fullPostText = buildPostText(postText, hashtags); + + // Получаем ID страницы пользователя (me) + String pageId = getPageId(accessToken); + + // Загружаем изображение и публикуем пост + String postId = publishPostWithImage(accessToken, pageId, fullPostText, imageData, imageContentType); + + logger.info("Successfully posted to Facebook with image. 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); + } + } + + /** + * Публикует пост с изображением на странице Facebook + * Использует endpoint /photos для загрузки изображения с текстом + */ + private String publishPostWithImage(String accessToken, String pageId, String message, + byte[] imageData, String imageContentType) + throws JsonProcessingException { + // Создаем multipart form data для загрузки изображения + DataBufferFactory bufferFactory = new DefaultDataBufferFactory(); + DataBuffer imageBuffer = bufferFactory.wrap(imageData); + + MultiValueMap formData = new LinkedMultiValueMap<>(); + formData.add("message", message); + formData.add("source", imageBuffer); + + String response = webClient.post() + .uri(uriBuilder -> uriBuilder + .path("/{pageId}/photos") + .queryParam("access_token", accessToken) + .build(pageId)) + .contentType(MediaType.MULTIPART_FORM_DATA) + .body(BodyInserters.fromMultipartData(formData)) + .retrieve() + .bodyToMono(String.class) + .block(Duration.ofMillis(timeoutMs * 2)); // Увеличиваем таймаут для загрузки изображения + + JsonNode jsonNode = objectMapper.readTree(response); + String photoId = jsonNode.get("id").asText(); + + // Возвращаем ID фото, который также является ID поста + return photoId; + } + /** * Формирует полный текст поста с хештегами */ private String buildPostText(String postText, List 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(); } } - diff --git a/src/main/java/kz/konturai/parser/service/MarketingStrategyService.java b/src/main/java/kz/konturai/parser/service/MarketingStrategyService.java index 62ff2bb..a7eef7d 100644 --- a/src/main/java/kz/konturai/parser/service/MarketingStrategyService.java +++ b/src/main/java/kz/konturai/parser/service/MarketingStrategyService.java @@ -6,10 +6,13 @@ import kz.konturai.parser.dto.MarketingAnalysisResult; import kz.konturai.parser.dto.MarketingStrategyRequest; 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.repository.MarketingAnalysisRepository; import kz.konturai.parser.repository.MarketingStrategyRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.http.MediaType; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; @@ -25,16 +28,25 @@ public class MarketingStrategyService { private final MarketingStrategyRepository repository; private final MarketingAnalysisService marketingAnalysisService; + private final MarketingAnalysisRepository analysisRepository; private final OpenAIAnalyticsService openAIAnalyticsService; + private final OpenAIImageGenerationService imageGenerationService; + private final MinIOService minIOService; private final ObjectMapper objectMapper = new ObjectMapper(); public MarketingStrategyService( MarketingStrategyRepository repository, MarketingAnalysisService marketingAnalysisService, - OpenAIAnalyticsService openAIAnalyticsService) { + MarketingAnalysisRepository analysisRepository, + OpenAIAnalyticsService openAIAnalyticsService, + OpenAIImageGenerationService imageGenerationService, + MinIOService minIOService) { this.repository = repository; this.marketingAnalysisService = marketingAnalysisService; + this.analysisRepository = analysisRepository; this.openAIAnalyticsService = openAIAnalyticsService; + this.imageGenerationService = imageGenerationService; + this.minIOService = minIOService; } public MarketingStrategy generateStrategy(String analysisId, MarketingStrategyRequest request, String userId) { @@ -98,13 +110,16 @@ public class MarketingStrategyService { // Build context from analysis String context = buildContextFromAnalysis(analysisResult); + // Get business context for image generation + String businessContext = getBusinessContextFromAnalysis(analysisId); + // Generate weekly plans List weeklyPlans = generateWeeklyPlans( context, strategy.getDurationWeeks(), strategy.getPriorityPlatforms()); // Generate post calendar List postCalendar = generatePostCalendar( - context, strategy.getDurationWeeks(), strategy.getPriorityPlatforms(), weeklyPlans); + context, strategy.getDurationWeeks(), strategy.getPriorityPlatforms(), weeklyPlans, businessContext); // Update strategy strategy.setStatus("completed"); @@ -265,9 +280,29 @@ public class MarketingStrategyService { } } + private String getBusinessContextFromAnalysis(String analysisId) { + try { + Optional optAnalysis = analysisRepository.findById(analysisId); + if (optAnalysis.isPresent()) { + MarketingAnalysis analysis = optAnalysis.get(); + StringBuilder businessContext = new StringBuilder(); + if (analysis.getBusinessNiche() != null) { + businessContext.append("Ниша бизнеса: ").append(analysis.getBusinessNiche()).append(". "); + } + if (analysis.getProduct() != null) { + businessContext.append("Продукт: ").append(analysis.getProduct()).append("."); + } + return businessContext.toString(); + } + } catch (Exception e) { + logger.warn("Failed to get business context from analysis: {}", e.getMessage()); + } + return ""; + } + private List generatePostCalendar( String context, Integer durationWeeks, List priorityPlatforms, - List weeklyPlans) { + List weeklyPlans, String businessContext) { try { String platformsStr = priorityPlatforms != null && !priorityPlatforms.isEmpty() ? String.join(", ", priorityPlatforms) @@ -371,6 +406,9 @@ public class MarketingStrategyService { item.setPublishTime((String) postMap.get("publishTime")); + // Generate image for the post + generateAndSaveImageForPost(item, businessContext); + calendar.add(item); } @@ -425,6 +463,59 @@ public class MarketingStrategyService { return plans; } + private void generateAndSaveImageForPost(MarketingStrategy.PostCalendarItem item, String businessContext) { + try { + // Create prompt for image generation + String imagePrompt = buildImagePrompt(item, businessContext); + + // Generate image + byte[] imageBytes = imageGenerationService.generateImage(imagePrompt); + + if (imageBytes != null && imageBytes.length > 0) { + // Save image to MinIO + String filename = "post_image_" + System.currentTimeMillis() + "_" + item.hashCode() + ".png"; + minIOService.uploadFile(filename, imageBytes, MediaType.IMAGE_PNG.toString()); + + // Save filename and URL in item + item.setImageFilename(filename); + item.setImageUrl(filename); // In MinIO, filename is also the URL/path + + logger.info("Successfully generated and saved image for post: {}", filename); + } else { + logger.warn("Failed to generate image for post with theme: {}", item.getTheme()); + } + } catch (Exception e) { + logger.error("Error generating image for post: {}", e.getMessage(), e); + // Continue without image - post will be published without image + } + } + + private String buildImagePrompt(MarketingStrategy.PostCalendarItem item, String businessContext) { + StringBuilder prompt = new StringBuilder(); + prompt.append("Создай привлекательное маркетинговое изображение для поста в социальных сетях. "); + + if (businessContext != null && !businessContext.isEmpty()) { + prompt.append(businessContext).append(" "); + } + + if (item.getTheme() != null && !item.getTheme().isEmpty()) { + prompt.append("Тема поста: ").append(item.getTheme()).append(". "); + } + + if (item.getPostText() != null && !item.getPostText().isEmpty()) { + // Use first 100 characters of post text for context + String textPreview = item.getPostText().length() > 100 + ? item.getPostText().substring(0, 100) + "..." + : item.getPostText(); + prompt.append("Текст поста: ").append(textPreview).append(". "); + } + + prompt.append("Стиль: современный, профессиональный, привлекающий внимание, подходящий для социальных сетей. "); + prompt.append("Изображение должно быть ярким, но не перегруженным, с акцентом на главную идею поста."); + + return prompt.toString(); + } + private List generateDefaultPostCalendar( Integer durationWeeks, List priorityPlatforms, LocalDateTime startDate) { List calendar = new ArrayList<>(); @@ -493,6 +584,8 @@ public class MarketingStrategyService { dtoItem.setPostText(item.getPostText()); dtoItem.setHashtags(item.getHashtags()); dtoItem.setPublishTime(item.getPublishTime()); + dtoItem.setImageUrl(item.getImageUrl()); + dtoItem.setImageFilename(item.getImageFilename()); postCalendar.add(dtoItem); } strategyContent.setPostCalendar(postCalendar); diff --git a/src/main/java/kz/konturai/parser/service/OpenAIImageGenerationService.java b/src/main/java/kz/konturai/parser/service/OpenAIImageGenerationService.java new file mode 100644 index 0000000..36fd679 --- /dev/null +++ b/src/main/java/kz/konturai/parser/service/OpenAIImageGenerationService.java @@ -0,0 +1,213 @@ +package kz.konturai.parser.service; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientResponseException; +import reactor.core.publisher.Mono; +import reactor.netty.http.client.HttpClient; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import reactor.util.retry.Retry; +import reactor.util.retry.RetryBackoffSpec; + +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +public class OpenAIImageGenerationService { + + private static final Logger logger = LoggerFactory.getLogger(OpenAIImageGenerationService.class); + private static final String DALL_E_API_URL = "https://api.openai.com/v1/images/generations"; + + private final WebClient webClient; + + @Value("${openai.api.key}") + private String apiKey; + + @Value("${openai.image.model:dall-e-3}") + private String model; + + @Value("${openai.image.size:1024x1024}") + private String imageSize; + + @Value("${openai.image.quality:standard}") + private String imageQuality; + + @Value("${openai.timeoutMs:90000}") + private long timeoutMs; + + @Value("${openai.retry.maxAttempts:3}") + private int maxRetryAttempts; + + @Value("${openai.retry.initialDelayMs:2000}") + private long initialRetryDelayMs; + + @Value("${openai.retry.maxDelayMs:60000}") + private long maxRetryDelayMs; + + @Value("${openai.retry.multiplier:2.0}") + private double retryMultiplier; + + public OpenAIImageGenerationService() { + HttpClient httpClient = HttpClient.create() + .responseTimeout(Duration.ofMillis(90000)); + + this.webClient = WebClient.builder() + .baseUrl(DALL_E_API_URL) + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .build(); + } + + /** + * Генерирует изображение через OpenAI DALL-E API + * + * @param prompt Промпт для генерации изображения + * @return Массив байтов изображения в формате PNG + */ + public byte[] generateImage(String prompt) { + return generateImage(prompt, imageSize); + } + + /** + * Генерирует изображение через OpenAI DALL-E API с указанным размером + * + * @param prompt Промпт для генерации изображения + * @param size Размер изображения (1024x1024, 1792x1024, 1024x1792) + * @return Массив байтов изображения в формате PNG + */ + public byte[] generateImage(String prompt, String size) { + if (apiKey == null || apiKey.trim().isEmpty()) { + logger.error("OpenAI API key is not configured. Cannot generate image."); + return null; + } + + if (prompt == null || prompt.trim().isEmpty()) { + logger.warn("Empty prompt provided for image generation"); + return null; + } + + try { + Map requestBody = new HashMap<>(); + requestBody.put("model", model); + requestBody.put("prompt", prompt); + requestBody.put("n", 1); + requestBody.put("size", size); + requestBody.put("quality", imageQuality); + requestBody.put("response_format", "b64_json"); // Получаем изображение в base64 + + logger.info("Requesting image generation from DALL-E with prompt: {}", prompt.substring(0, Math.min(100, prompt.length()))); + + Map response = webClient.post() + .header("Authorization", "Bearer " + apiKey) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .bodyValue(requestBody) + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() {}) + .retryWhen(createRetrySpec("generateImage")) + .onErrorResume(err -> { + logger.error("DALL-E API request failed after retries: {} - {}", err.getMessage(), + err.getClass().getSimpleName()); + if (err instanceof WebClientResponseException) { + WebClientResponseException wcre = (WebClientResponseException) err; + if (wcre.getStatusCode().value() == 401) { + logger.error("OpenAI API key is invalid or expired. Please check your openai.api.key configuration."); + } else if (wcre.getStatusCode().value() == 429) { + logger.error("OpenAI API rate limit exceeded after all retry attempts."); + } else if (wcre.getStatusCode().value() >= 500) { + logger.error("OpenAI API server error after all retry attempts."); + } + } + return Mono.empty(); + }) + .block(Duration.ofMillis(timeoutMs)); + + if (response == null) { + logger.error("Failed to generate image: empty response from DALL-E API"); + return null; + } + + // Извлекаем изображение из ответа + @SuppressWarnings("unchecked") + List> data = (List>) response.get("data"); + if (data == null || data.isEmpty()) { + logger.error("No image data in DALL-E API response"); + return null; + } + + Map imageData = data.get(0); + String b64Json = (String) imageData.get("b64_json"); + if (b64Json == null) { + logger.error("No b64_json in DALL-E API response"); + return null; + } + + // Декодируем base64 в массив байтов + byte[] imageBytes = java.util.Base64.getDecoder().decode(b64Json); + logger.info("Successfully generated image. Size: {} bytes", imageBytes.length); + return imageBytes; + + } catch (Exception e) { + logger.error("Error generating image with DALL-E: {}", e.getMessage(), e); + return null; + } + } + + private RetryBackoffSpec createRetrySpec(String operation) { + return Retry.backoff(maxRetryAttempts, Duration.ofMillis(initialRetryDelayMs)) + .maxBackoff(Duration.ofMillis(maxRetryDelayMs)) + .multiplier(retryMultiplier) + .filter(throwable -> { + if (throwable instanceof WebClientResponseException) { + WebClientResponseException wcre = (WebClientResponseException) throwable; + int statusCode = wcre.getStatusCode().value(); + // Only retry on 429 (rate limit) and 5xx (server errors) + return statusCode == 429 || statusCode >= 500; + } + // Retry on network errors + return throwable instanceof java.util.concurrent.TimeoutException + || throwable instanceof java.net.ConnectException + || throwable instanceof java.io.IOException; + }) + .doBeforeRetry(retrySignal -> { + long attempt = retrySignal.totalRetries() + 1; + Throwable failure = retrySignal.failure(); + + Duration retryAfter = null; + if (failure instanceof WebClientResponseException) { + WebClientResponseException wcre = (WebClientResponseException) failure; + if (wcre.getStatusCode().value() == 429) { + String retryAfterHeader = wcre.getHeaders().getFirst("Retry-After"); + if (retryAfterHeader != null) { + try { + int seconds = Integer.parseInt(retryAfterHeader); + retryAfter = Duration.ofSeconds(seconds); + logger.warn( + "DALL-E API returned 429 for operation '{}'. Retry-After: {} seconds. Will retry in {}ms (attempt {}/{})", + operation, seconds, retryAfter.toMillis(), attempt, maxRetryAttempts); + } catch (NumberFormatException e) { + // Ignore if header is not a number + } + } + } + } + + if (retryAfter == null) { + logger.warn("DALL-E API returned error for operation '{}'. Will retry (attempt {}/{})", + operation, attempt, maxRetryAttempts); + } + }) + .onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> { + logger.error("DALL-E request for operation '{}' exhausted all {} retry attempts. Giving up.", + operation, maxRetryAttempts); + return retrySignal.failure(); + }); + } +} + diff --git a/src/main/java/kz/konturai/parser/service/PostingTaskService.java b/src/main/java/kz/konturai/parser/service/PostingTaskService.java index 7e566e5..42f4237 100644 --- a/src/main/java/kz/konturai/parser/service/PostingTaskService.java +++ b/src/main/java/kz/konturai/parser/service/PostingTaskService.java @@ -23,16 +23,19 @@ public class PostingTaskService { private final MarketingStrategyRepository strategyRepository; private final SocialMediaCredentialsService credentialsService; private final FacebookPostingService facebookPostingService; + private final MinIOService minIOService; public PostingTaskService( PostingTaskRepository taskRepository, MarketingStrategyRepository strategyRepository, SocialMediaCredentialsService credentialsService, - FacebookPostingService facebookPostingService) { + FacebookPostingService facebookPostingService, + MinIOService minIOService) { this.taskRepository = taskRepository; this.strategyRepository = strategyRepository; this.credentialsService = credentialsService; this.facebookPostingService = facebookPostingService; + this.minIOService = minIOService; } /** @@ -49,7 +52,7 @@ public class PostingTaskService { } MarketingStrategy strategy = optStrategy.get(); - + if (!"completed".equals(strategy.getStatus())) { throw new IllegalStateException("Strategy is not completed yet. Status: " + strategy.getStatus()); } @@ -90,8 +93,11 @@ public class PostingTaskService { calendarItem.getPlatform(), calendarItem.getPostText(), calendarItem.getHashtags(), - calendarItem.getPublishDate() - ); + calendarItem.getPublishDate()); + + // Копируем данные об изображении + task.setImageUrl(calendarItem.getImageUrl()); + task.setImageFilename(calendarItem.getImageFilename()); // Если дата публикации в прошлом, выполняем сразу if (task.getPublishDate().isBefore(now) || task.getPublishDate().isEqual(now)) { @@ -159,14 +165,35 @@ public class PostingTaskService { throw new IllegalStateException("Credentials not found for platform: " + task.getPlatform()); } + // Загружаем изображение, если оно есть + byte[] imageData = null; + if (task.getImageFilename() != null && !task.getImageFilename().isEmpty()) { + try { + java.io.InputStream imageStream = minIOService.downloadFile(task.getImageFilename()); + imageData = imageStream.readAllBytes(); + logger.info("Loaded image for task {}: {} bytes", taskId, imageData.length); + } catch (Exception e) { + logger.warn("Failed to load image for task {}: {}", taskId, e.getMessage()); + // Продолжаем без изображения + } + } + // Выполняем публикацию в зависимости от платформы String postId = null; if ("facebook".equalsIgnoreCase(task.getPlatform())) { - postId = facebookPostingService.postToFacebook( - credentials, - task.getPostText(), - task.getHashtags() - ); + if (imageData != null && imageData.length > 0) { + postId = facebookPostingService.postToFacebookWithImage( + credentials, + task.getPostText(), + task.getHashtags(), + imageData, + "image/png"); + } else { + postId = facebookPostingService.postToFacebook( + credentials, + task.getPostText(), + task.getHashtags()); + } } else { throw new UnsupportedOperationException("Platform not supported: " + task.getPlatform()); } @@ -181,7 +208,7 @@ public class PostingTaskService { } catch (Exception e) { logger.error("Failed to execute task {}", taskId, e); - + // Обновляем статус на failed task.setStatus("failed"); task.setExecutedAt(LocalDateTime.now()); @@ -218,4 +245,3 @@ public class PostingTaskService { return taskRepository.findByStrategyId(strategyId); } } - diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 733a5be..6b36448 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -69,6 +69,11 @@ openai.retry.maxDelayMs=60000 openai.retry.multiplier=2.0 openai.rateLimit.maxConcurrentRequests=2 +# OpenAI DALL-E Image Generation Configuration +openai.image.model=dall-e-3 +openai.image.size=1024x1024 +openai.image.quality=standard + # Email Configuration spring.mail.host=smtp.gmail.com spring.mail.port=587