This commit is contained in:
arys
2026-01-24 00:38:51 +05:00
parent 473edafc58
commit 155ed629cf
2 changed files with 109 additions and 216 deletions
@@ -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<String, Object> getTargetAudience() {
return targetAudience;
}
public void setTargetAudience(Map<String, Object> targetAudience) {
this.targetAudience = targetAudience;
}
public List<String> getRegion() {
return region;
}
public void setRegion(List<String> 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<String> getAnalysisType() {
return analysisType;
}
public void setAnalysisType(List<String> 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;
}
}
@@ -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<MarketingAnalysisV2Document> 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<MarketingStrategy> 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<MarketingStrategy> 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<MarketingStrategy.WeeklyPlan> weeklyPlans = generateWeeklyPlans(
context, strategy.getDurationWeeks(), strategy.getPriorityPlatforms());
// Generate post calendar
List<MarketingStrategy.PostCalendarItem> 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<String, Object> 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<String, Object> jsonMap = objectMapper.readValue(jsonStr, new TypeReference<Map<String, Object>>() {
});
@SuppressWarnings("unchecked")
@@ -263,7 +279,6 @@ public class MarketingStrategyService {
@SuppressWarnings("unchecked")
List<String> platforms = (List<String>) 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<MarketingAnalysis> 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<String, Object> 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<MarketingAnalysisV2Document> 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<String, Object> 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<String, Object> ta = (Map<String, Object>) 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<String, Object> 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<MarketingStrategy.PostCalendarItem> generatePostCalendar(
String context, Integer durationWeeks, List<String> priorityPlatforms,
List<MarketingStrategy.WeeklyPlan> 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<String, Object> jsonMap = objectMapper.readValue(jsonStr, new TypeReference<Map<String, Object>>() {
});
@SuppressWarnings("unchecked")
@@ -408,7 +449,6 @@ public class MarketingStrategyService {
for (Map<String, Object> 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<String> contentTypes = Arrays.asList("пост", "сторис", "видео");
List<String> 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<MarketingStrategyResponse.WeeklyPlan> 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<MarketingStrategyResponse.PostCalendarItem> postCalendar = new ArrayList<>();
// Получаем все задачи для этой стратегии
List<PostingTask> 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<PostingTask> 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<MarketingStrategy> 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;
}
}
}