diff --git a/src/main/java/kz/konturai/parser/model/MarketingAnalysisV2Document.java b/src/main/java/kz/konturai/parser/model/MarketingAnalysisV2Document.java index 5f45713..1ee4e37 100644 --- a/src/main/java/kz/konturai/parser/model/MarketingAnalysisV2Document.java +++ b/src/main/java/kz/konturai/parser/model/MarketingAnalysisV2Document.java @@ -1,6 +1,7 @@ package kz.konturai.parser.model; import kz.konturai.parser.dto.MarketingAnalysisResponseV2; +import lombok.Data; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.Field; @@ -9,6 +10,7 @@ import java.time.LocalDateTime; import java.util.List; import java.util.Map; +@Data @Document(collection = "marketing_analysis_v2") public class MarketingAnalysisV2Document { @@ -62,124 +64,5 @@ public class MarketingAnalysisV2Document { this.updatedAt = LocalDateTime.now(); } - public String getId() { - return id; - } - public void setId(String id) { - this.id = id; - } - - public String getUserId() { - return userId; - } - - public void setUserId(String userId) { - this.userId = userId; - } - - public String getBusinessNiche() { - return businessNiche; - } - - public void setBusinessNiche(String businessNiche) { - this.businessNiche = businessNiche; - } - - public String getProduct() { - return product; - } - - public void setProduct(String product) { - this.product = product; - } - - public Map getTargetAudience() { - return targetAudience; - } - - public void setTargetAudience(Map targetAudience) { - this.targetAudience = targetAudience; - } - - public List getRegion() { - return region; - } - - public void setRegion(List region) { - this.region = region; - } - - public String getGoal() { - return goal; - } - - public void setGoal(String goal) { - this.goal = goal; - } - - public String getDetailLevel() { - return detailLevel; - } - - public void setDetailLevel(String detailLevel) { - this.detailLevel = detailLevel; - } - - public String getStrongSide() { - return strongSide; - } - - public void setStrongSide(String strongSide) { - this.strongSide = strongSide; - } - - public String getWeakSide() { - return weakSide; - } - - public void setWeakSide(String weakSide) { - this.weakSide = weakSide; - } - - public List getAnalysisType() { - return analysisType; - } - - public void setAnalysisType(List analysisType) { - this.analysisType = analysisType; - } - - public MarketingAnalysisResponseV2 getAnalysisData() { - return analysisData; - } - - public void setAnalysisData(MarketingAnalysisResponseV2 analysisData) { - this.analysisData = analysisData; - this.updatedAt = LocalDateTime.now(); - } - - public LocalDateTime getCreatedAt() { - return createdAt; - } - - public void setCreatedAt(LocalDateTime createdAt) { - this.createdAt = createdAt; - } - - public LocalDateTime getUpdatedAt() { - return updatedAt; - } - - public void setUpdatedAt(LocalDateTime updatedAt) { - this.updatedAt = updatedAt; - } - - public String getAnalysisId() { - return analysisId; - } - - public void setAnalysisId(String analysisId) { - this.analysisId = analysisId; - } } diff --git a/src/main/java/kz/konturai/parser/service/MarketingStrategyService.java b/src/main/java/kz/konturai/parser/service/MarketingStrategyService.java index aa94e00..35bd35b 100644 --- a/src/main/java/kz/konturai/parser/service/MarketingStrategyService.java +++ b/src/main/java/kz/konturai/parser/service/MarketingStrategyService.java @@ -7,10 +7,13 @@ 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.MarketingAnalysisV2Document; import kz.konturai.parser.model.MarketingStrategy; import kz.konturai.parser.model.PostingTask; import kz.konturai.parser.repository.MarketingAnalysisRepository; +import kz.konturai.parser.repository.MarketingAnalysisV2Repository; import kz.konturai.parser.repository.MarketingStrategyRepository; +import lombok.RequiredArgsConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; @@ -24,6 +27,7 @@ import java.util.*; import java.util.stream.Collectors; @Service +@RequiredArgsConstructor public class MarketingStrategyService { private static final Logger logger = LoggerFactory.getLogger(MarketingStrategyService.class); @@ -31,6 +35,7 @@ public class MarketingStrategyService { private final MarketingStrategyRepository repository; private final MarketingAnalysisService marketingAnalysisService; private final MarketingAnalysisRepository analysisRepository; + private final MarketingAnalysisV2Repository v2Repository; private final OpenAIAnalyticsService openAIAnalyticsService; private final ImageGenerationService imageGenerationService; private final MinIOService minIOService; @@ -40,41 +45,31 @@ public class MarketingStrategyService { @Value("${image.generation.delayBetweenRequestsMs:3000}") private long delayBetweenRequestsMs; - public MarketingStrategyService( - MarketingStrategyRepository repository, - MarketingAnalysisService marketingAnalysisService, - MarketingAnalysisRepository analysisRepository, - OpenAIAnalyticsService openAIAnalyticsService, - ImageGenerationService imageGenerationService, - 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) { - // Check if analysis exists and is completed MarketingAnalysisResult analysisResult = marketingAnalysisService.getAnalysisResult(analysisId); + + if (analysisResult == null) { + Optional v2Opt = v2Repository.findById(analysisId); + if (v2Opt.isPresent()) { + analysisResult = convertV2ToResult(v2Opt.get()); + } + } + if (analysisResult == null) { throw new IllegalArgumentException("Анализ с ID " + analysisId + " не найден"); } + if (!"completed".equals(analysisResult.getStatus())) { throw new IllegalStateException("Анализ еще не завершен. Статус: " + analysisResult.getStatus()); } - // Check if strategy already exists Optional existingStrategy = repository.findByAnalysisId(analysisId); if (existingStrategy.isPresent()) { logger.info("Стратегия для анализа {} уже существует: {}", analysisId, existingStrategy.get().getId()); return existingStrategy.get(); } - // Create new strategy MarketingStrategy strategy = new MarketingStrategy(analysisId); strategy.setUserId(userId); strategy.setDurationWeeks(request.getDurationWeeks() != null ? request.getDurationWeeks() : 4); @@ -85,12 +80,43 @@ public class MarketingStrategyService { logger.info("Marketing strategy created with ID: {} for user: {}", strategy.getId(), userId); - // Start async processing processStrategyGeneration(strategy.getId(), analysisId, analysisResult); return strategy; } + private MarketingAnalysisResult convertV2ToResult(MarketingAnalysisV2Document v2Doc) { + MarketingAnalysisResult result = new MarketingAnalysisResult(); + result.setAnalysisId(v2Doc.getId()); + result.setStatus("completed"); + + MarketingAnalysisResult.MarketingReport report = new MarketingAnalysisResult.MarketingReport(); + + StringBuilder fullAnalysisBuilder = new StringBuilder(); + fullAnalysisBuilder.append("# Маркетинговый анализ (V2)\n\n"); + + if (v2Doc.getProduct() != null) fullAnalysisBuilder.append("**Продукт:** ").append(v2Doc.getProduct()).append("\n"); + if (v2Doc.getBusinessNiche() != null) fullAnalysisBuilder.append("**Ниша:** ").append(v2Doc.getBusinessNiche()).append("\n"); + if (v2Doc.getGoal() != null) fullAnalysisBuilder.append("**Цель:** ").append(v2Doc.getGoal()).append("\n\n"); + + if (v2Doc.getAnalysisData() != null) { + try { + String jsonData = objectMapper.writeValueAsString(v2Doc.getAnalysisData()); + fullAnalysisBuilder.append("**Детальные данные анализа:**\n").append(jsonData); + } catch (Exception e) { + fullAnalysisBuilder.append("Ошибка обработки данных анализа V2."); + } + } else { + fullAnalysisBuilder.append("Детальный анализ недоступен."); + } + + report.setFullAnalysis(fullAnalysisBuilder.toString()); + report.setSummary("Анализ V2 для продукта: " + v2Doc.getProduct()); + + result.setReport(report); + return result; + } + private void addStatusHistoryEntry(MarketingStrategy strategy, String status, String message) { if (strategy.getStatusHistory() == null) { strategy.setStatusHistory(new ArrayList<>()); @@ -101,7 +127,7 @@ public class MarketingStrategyService { @Async("reportGenerationExecutor") public void processStrategyGeneration(String strategyId, String analysisId, - MarketingAnalysisResult analysisResult) { + MarketingAnalysisResult analysisResult) { try { Optional optStrategy = repository.findById(strategyId); if (optStrategy.isEmpty()) { @@ -116,28 +142,22 @@ public class MarketingStrategyService { logger.info("Starting marketing strategy generation for ID: {}", strategyId); - // 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, businessContext); - // Update strategy strategy.setStatus("completed"); strategy.setCompletedAt(LocalDateTime.now()); strategy.setWeeklyPlans(weeklyPlans); strategy.setPostCalendar(postCalendar); - // Store full strategy data as JSON Map strategyData = new HashMap<>(); strategyData.put("weeklyPlans", weeklyPlans); strategyData.put("postCalendar", postCalendar); @@ -174,10 +194,8 @@ public class MarketingStrategyService { context.append("Резюме анализа: ").append(report.getSummary()).append("\n\n"); } - // Since the system migrated to fullAnalysis, use it as the primary context. if (report.getFullAnalysis() != null && !report.getFullAnalysis().isBlank()) { String full = report.getFullAnalysis().trim(); - // Guard against excessively large prompts int maxChars = 12000; if (full.length() > maxChars) { full = full.substring(0, maxChars) + "\n\n(…обрезано для лимита контекста…)"; @@ -232,14 +250,12 @@ public class MarketingStrategyService { return generateDefaultWeeklyPlans(durationWeeks, priorityPlatforms); } - // Extract JSON from response String jsonStr = extractJsonFromResponse(response); if (jsonStr == null) { logger.warn("Could not extract JSON from weekly plans response, using default"); return generateDefaultWeeklyPlans(durationWeeks, priorityPlatforms); } - // Parse JSON Map jsonMap = objectMapper.readValue(jsonStr, new TypeReference>() { }); @SuppressWarnings("unchecked") @@ -263,7 +279,6 @@ public class MarketingStrategyService { @SuppressWarnings("unchecked") List platforms = (List) planMap.get("priorityPlatforms"); - // Filter platforms to only include those from the original request if (platforms != null && priorityPlatforms != null && !priorityPlatforms.isEmpty()) { platforms = platforms.stream() .filter(p -> priorityPlatforms.stream() @@ -287,39 +302,68 @@ public class MarketingStrategyService { try { Optional optAnalysis = analysisRepository.findById(analysisId); if (optAnalysis.isPresent()) { - MarketingAnalysis analysis = optAnalysis.get(); - StringBuilder businessContext = new StringBuilder(); - - if (analysis.getProduct() != null && !analysis.getProduct().isEmpty()) { - businessContext.append("Главный объект изображения (продукт): ").append(analysis.getProduct()).append(". "); - } - - if (analysis.getBusinessNiche() != null && !analysis.getBusinessNiche().isEmpty()) { - businessContext.append("Ниша: ").append(analysis.getBusinessNiche()).append(". "); - } - - if (analysis.getTargetAudience() != null) { - Map ta = analysis.getTargetAudience(); - if (ta.containsKey("ageRanges") || ta.containsKey("types")) { - businessContext.append("Целевая аудитория (люди на фото): "); - if (ta.containsKey("types")) { - businessContext.append(ta.get("types")).append(" "); - } - if (ta.containsKey("ageRanges")) { - businessContext.append("возраст ").append(ta.get("ageRanges")); - } - businessContext.append(". "); - } - } - - return businessContext.toString(); + return extractContextFromV1(optAnalysis.get()); } + + Optional optV2 = v2Repository.findById(analysisId); + if (optV2.isPresent()) { + return extractContextFromV2(optV2.get()); + } + } catch (Exception e) { logger.warn("Failed to get business context from analysis: {}", e.getMessage()); } return ""; } + private String extractContextFromV1(MarketingAnalysis analysis) { + StringBuilder businessContext = new StringBuilder(); + if (analysis.getProduct() != null && !analysis.getProduct().isEmpty()) { + businessContext.append("Главный объект изображения (продукт): ").append(analysis.getProduct()).append(". "); + } + if (analysis.getBusinessNiche() != null && !analysis.getBusinessNiche().isEmpty()) { + businessContext.append("Ниша: ").append(analysis.getBusinessNiche()).append(". "); + } + if (analysis.getTargetAudience() != null) { + Map ta = analysis.getTargetAudience(); + appendTargetAudience(businessContext, ta); + } + return businessContext.toString(); + } + + private String extractContextFromV2(MarketingAnalysisV2Document v2) { + StringBuilder businessContext = new StringBuilder(); + if (v2.getProduct() != null && !v2.getProduct().isEmpty()) { + businessContext.append("Главный объект изображения (продукт): ").append(v2.getProduct()).append(". "); + } + if (v2.getBusinessNiche() != null && !v2.getBusinessNiche().isEmpty()) { + businessContext.append("Ниша: ").append(v2.getBusinessNiche()).append(". "); + } + if (v2.getTargetAudience() != null) { + if (v2.getTargetAudience() instanceof Map) { + @SuppressWarnings("unchecked") + Map ta = (Map) v2.getTargetAudience(); + appendTargetAudience(businessContext, ta); + } else if (v2.getTargetAudience() != null) { + businessContext.append("Целевая аудитория: ").append(v2.getTargetAudience().toString()).append(". "); + } + } + return businessContext.toString(); + } + + private void appendTargetAudience(StringBuilder sb, Map ta) { + if (ta.containsKey("ageRanges") || ta.containsKey("types")) { + sb.append("Целевая аудитория (люди на фото): "); + if (ta.containsKey("types")) { + sb.append(ta.get("types")).append(" "); + } + if (ta.containsKey("ageRanges")) { + sb.append("возраст ").append(ta.get("ageRanges")); + } + sb.append(". "); + } + } + private List generatePostCalendar( String context, Integer durationWeeks, List priorityPlatforms, List weeklyPlans, String businessContext) { @@ -328,7 +372,6 @@ public class MarketingStrategyService { ? String.join(", ", priorityPlatforms) : "Instagram, Facebook, LinkedIn, Telegram, TikTok, YouTube"; - // Build weekly plans summary for context String weeklyPlansSummary = ""; if (weeklyPlans != null && !weeklyPlans.isEmpty()) { weeklyPlansSummary = weeklyPlans.stream() @@ -337,7 +380,7 @@ public class MarketingStrategyService { .collect(Collectors.joining("\n")); } - LocalDateTime startDate = LocalDateTime.now().plusDays(1); // Start from tomorrow + LocalDateTime startDate = LocalDateTime.now().plusDays(1); String prompt = String.format( "На основе следующего маркетингового анализа и недельного плана создай детальный календарь постов на %d недель.\n\n" @@ -386,14 +429,12 @@ public class MarketingStrategyService { return generateDefaultPostCalendar(durationWeeks, priorityPlatforms, startDate); } - // Extract JSON from response String jsonStr = extractJsonFromResponse(response); if (jsonStr == null) { logger.warn("Could not extract JSON from post calendar response, using default"); return generateDefaultPostCalendar(durationWeeks, priorityPlatforms, startDate); } - // Parse JSON Map jsonMap = objectMapper.readValue(jsonStr, new TypeReference>() { }); @SuppressWarnings("unchecked") @@ -408,7 +449,6 @@ public class MarketingStrategyService { for (Map postMap : postCalendarList) { MarketingStrategy.PostCalendarItem item = new MarketingStrategy.PostCalendarItem(); - // Parse date String dateStr = (String) postMap.get("publishDate"); if (dateStr != null) { try { @@ -423,13 +463,11 @@ public class MarketingStrategyService { } String platformRaw = (String) postMap.get("platform"); - // Validate platform - only use if it's in the requested platforms final String platform; if (platformRaw != null && priorityPlatforms != null && !priorityPlatforms.isEmpty()) { boolean isValidPlatform = priorityPlatforms.stream() .anyMatch(pp -> pp.equalsIgnoreCase(platformRaw)); if (!isValidPlatform) { - // If platform is not in the list, use the first valid platform instead logger.warn("Platform '{}' not in requested platforms {}, using '{}' instead", platformRaw, priorityPlatforms, priorityPlatforms.get(0)); platform = priorityPlatforms.get(0); @@ -450,11 +488,8 @@ public class MarketingStrategyService { item.setPublishTime((String) postMap.get("publishTime")); - // Generate image for the post generateAndSaveImageForPost(item, businessContext); - // Add delay between image generation requests to avoid rate limiting - // This helps prevent 429 errors from Gemini API if (delayBetweenRequestsMs > 0) { try { Thread.sleep(delayBetweenRequestsMs); @@ -480,7 +515,6 @@ public class MarketingStrategyService { return null; } - // Try to find JSON object in response int startIdx = response.indexOf("{"); int endIdx = response.lastIndexOf("}"); @@ -488,7 +522,6 @@ public class MarketingStrategyService { return response.substring(startIdx, endIdx + 1); } - // Try to find JSON array startIdx = response.indexOf("["); endIdx = response.lastIndexOf("]"); @@ -531,9 +564,8 @@ public class MarketingStrategyService { 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 + item.setImageUrl(filename); logger.info("Successfully generated and saved image for post: {}", filename); } else { @@ -541,7 +573,6 @@ public class MarketingStrategyService { } } catch (Exception e) { logger.error("Error generating image for post: {}", e.getMessage(), e); - // Continue without image - post will be published without image } } @@ -562,12 +593,10 @@ public class MarketingStrategyService { prompt.append("Фотореалистичное изображение. "); - // Добавляем контекст продукта (это самое важное!) if (businessContext != null && !businessContext.isEmpty()) { prompt.append(businessContext).append(" "); } - // 3. Контекст поста if (item.getTheme() != null && !item.getTheme().isEmpty()) { prompt.append("Сюжет изображения: ").append(item.getTheme()).append(". "); } @@ -590,10 +619,10 @@ public class MarketingStrategyService { List contentTypes = Arrays.asList("пост", "сторис", "видео"); List times = Arrays.asList("10:00", "14:00", "18:00"); - int postCount = durationWeeks * 3; // 3 posts per week + int postCount = durationWeeks * 3; for (int i = 0; i < postCount; i++) { MarketingStrategy.PostCalendarItem item = new MarketingStrategy.PostCalendarItem(); - item.setPublishDate(startDate.plusDays(i * 2)); // Every 2 days + item.setPublishDate(startDate.plusDays(i * 2)); item.setPlatform(platforms.get(i % platforms.size())); item.setContentType(contentTypes.get(i % contentTypes.size())); item.setTheme("Тема поста " + (i + 1)); @@ -626,7 +655,6 @@ public class MarketingStrategyService { && "completed".equals(strategy.getStatus())) { MarketingStrategyResponse.StrategyContent strategyContent = new MarketingStrategyResponse.StrategyContent(); - // Convert WeeklyPlans List weeklyPlans = new ArrayList<>(); for (MarketingStrategy.WeeklyPlan plan : strategy.getWeeklyPlans()) { MarketingStrategyResponse.WeeklyPlan dtoPlan = new MarketingStrategyResponse.WeeklyPlan(); @@ -638,9 +666,7 @@ public class MarketingStrategyService { } strategyContent.setWeeklyPlans(weeklyPlans); - // Convert PostCalendarItems and add taskId if tasks exist List postCalendar = new ArrayList<>(); - // Получаем все задачи для этой стратегии List tasks = postingTaskService.getStrategyTasks(strategyId); for (MarketingStrategy.PostCalendarItem item : strategy.getPostCalendar()) { @@ -655,7 +681,6 @@ public class MarketingStrategyService { dtoItem.setImageUrl(item.getImageUrl()); dtoItem.setImageFilename(item.getImageFilename()); - // Находим соответствующую задачу по дате публикации, платформе и тексту поста Optional matchingTask = tasks.stream() .filter(task -> task.getPublishDate() != null && item.getPublishDate() != null && task.getPublishDate().equals(item.getPublishDate()) @@ -696,14 +721,6 @@ public class MarketingStrategyService { return repository.findById(strategyId); } - /** - * Регенерирует изображение для указанного поста в стратегии - * - * @param strategyId ID стратегии - * @param postIndex Индекс поста в списке postCalendar (начиная с 0) - * @return Обновленный элемент календаря постов с новым изображением, или null - * если пост не найден - */ public MarketingStrategy.PostCalendarItem regeneratePostImage(String strategyId, int postIndex) { Optional optStrategy = repository.findById(strategyId); if (optStrategy.isEmpty()) { @@ -722,20 +739,13 @@ public class MarketingStrategyService { MarketingStrategy.PostCalendarItem item = postCalendar.get(postIndex); - // Get business context for image generation String businessContext = getBusinessContextFromAnalysis(strategy.getAnalysisId()); - // Note: Old image file is not deleted (MinIO doesn't have deleteFile method) - // New image will have a different filename, so old file will remain but won't - // be used - - // Generate new image generateAndSaveImageForPost(item, businessContext); - // Save updated strategy repository.save(strategy); logger.info("Successfully regenerated image for post at index {} in strategy {}", postIndex, strategyId); return item; } -} +} \ No newline at end of file