target fix

This commit is contained in:
arys
2026-04-01 18:59:48 +05:00
parent 2fad79e009
commit a47fe1cce9
11 changed files with 1122 additions and 443 deletions
+12
View File
@@ -0,0 +1,12 @@
{
"permissions": {
"allow": [
"Bash(curl -s http://localhost:8080/api/targeting/analysis -X POST -H \"Content-Type: application/json\" -d '{\"topic\":\"test\"}')",
"Bash(curl -s -X POST http://localhost:8080/api/targeting/analysis -H 'Content-Type: application/json' -d '{\"topic\":\"\"}')",
"Bash(curl -s -X POST http://localhost:8080/api/targeting/analysis -H 'Content-Type: application/json' -d '{\"topic\":\"ab\"}')",
"Bash(curl -s -X POST http://localhost:8080/api/targeting/analysis -H 'Content-Type: application/json' -d '{}')",
"Bash(curl -s -X POST http://localhost:8080/api/targeting/strategy -H 'Content-Type: application/json' -d '{\"analysis\": null}')",
"Bash(curl -s -X POST http://localhost:8080/api/targeting/creative -H 'Content-Type: application/json' -d '{\"captionIdea\": \"\"}')"
]
}
}
@@ -1,5 +1,6 @@
package kz.konturai.parser.dto;
import kz.konturai.parser.dto.targeting.TargetingRecommendationDto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@@ -12,8 +13,20 @@ import java.util.List;
@NoArgsConstructor
@AllArgsConstructor
public class TargetingAIAnalysisResult {
private String strategyId;
private String analysisId;
private String businessNiche;
private String productName;
private String goal;
private String recommendedTargetingType;
private String recommendedTargetingTypeLabel;
private String recommendedCampaignObjective;
private List<String> questionnaireSignals;
private List<AudienceSegmentDto> audienceSegments;
private List<AudienceSegmentDto> competitorInsights;
private BudgetOptimizationDto budgetOptimization;
private String aiRecommendationsRationale;
private List<String> quickWins;
private List<String> warnings;
private TargetingRecommendationDto targetingRecommendation;
}
@@ -13,7 +13,6 @@ import java.util.List;
@NoArgsConstructor
@AllArgsConstructor
public class TargetingCampaignRequest {
private String analysisId;
private String strategyId;
private String campaignName;
private String objective;
@@ -0,0 +1,44 @@
package kz.konturai.parser.dto.targeting;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class AudienceProfileDto {
/** Основной сегмент — кто это люди */
private String primarySegment;
/** Дополнительный сегмент */
private String secondarySegment;
/** Возрастной диапазон, например "28-45" */
private String ageRange;
/** Пол, например "Женщины 70%, Мужчины 30%" */
private String gender;
/** Интересы для таргетинга */
private List<String> interests;
/** Поведенческие паттерны для таргетинга */
private List<String> behaviors;
/** Города */
private List<String> geography;
/** Исключения из аудитории */
private List<String> exclusions;
/** Язык аудитории */
private String language;
}
@@ -0,0 +1,28 @@
package kz.konturai.parser.dto.targeting;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PublishedPostDto {
private int postIndex;
private String platform;
private String contentType;
private String theme;
private String postText;
private List<String> hashtags;
private String imageUrl;
private boolean imagePosted;
private String facebookPostId;
/** PUBLISHED | TEXT_ONLY | FAILED */
private String facebookStatus;
}
@@ -0,0 +1,36 @@
package kz.konturai.parser.dto.targeting;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class TargetingBudgetDto {
/** Рекомендуемый дневной бюджет */
private String dailyBudget;
/** Рекомендуемый месячный бюджет */
private String monthlyBudget;
/** Стратегия ставок */
private String bidStrategy;
/** Обоснование бюджета */
private String rationale;
/** Ожидаемая стоимость клика */
private String estimatedCpc;
/** Ожидаемая стоимость лида */
private String estimatedCpl;
/** Ожидаемый охват в день */
private String estimatedDailyReach;
}
@@ -5,8 +5,6 @@ import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@@ -18,24 +16,15 @@ public class TargetingLaunchResultDto {
private String analysisId;
private String scoringModelName;
private String scoringModelTitle;
private String businessNiche;
private String goal;
// ── Первый опубликованный пост ────────────────────────────────────────────
private int postIndex;
private String platform;
private String contentType; // "фото" / "видео"
private String theme;
private String postText;
private List<String> hashtags;
private String imageUrl;
private boolean imagePosted;
// ── AI-рекомендация таргетинга (главный объект) ───────────────────────────
private TargetingRecommendationDto targetingRecommendation;
// ── Facebook ──────────────────────────────────────────────────────────────
private String facebookPostId;
private String facebookStatus; // "PUBLISHED" | "FAILED" | "TEXT_ONLY"
// ── Опубликованный первый пост ────────────────────────────────────────────
private PublishedPostDto publishedPost;
// ── Таргетинг (генерируется AI из V3 анализа) ────────────────────────────
private TargetingSettingsDto targetingSettings;
// ── Сводка ───────────────────────────────────────────────────────────────
// ── Итоговое сообщение ────────────────────────────────────────────────────
private String message;
}
@@ -0,0 +1,46 @@
package kz.konturai.parser.dto.targeting;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class TargetingPhaseDto {
private int phase;
/** Название фазы */
private String name;
/** Продолжительность */
private String duration;
/** Тип таргетинга в этой фазе */
private String targetingType;
/** Цель фазы */
private String objective;
/** Аудитория для этой фазы */
private String audience;
/** Рекламные форматы */
private List<String> adFormats;
/** Бюджет на фазу */
private String budget;
/** KPI */
private String kpi;
/** Тактики */
private List<String> tactics;
}
@@ -0,0 +1,84 @@
package kz.konturai.parser.dto.targeting;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class TargetingRecommendationDto {
// ── Тип таргетинга ────────────────────────────────────────────────────────
/**
* Рекомендуемый тип: COLD_INTEREST / BEHAVIORAL / LOOKALIKE /
* RETARGETING / LEAD_GEN / AWARENESS / MIXED
*/
private String recommendedType;
/** Читаемое название типа */
private String recommendedTypeLabel;
/** Почему AI выбрал именно этот тип — на основе данных опросника */
private String typeRationale;
// ── Цель кампании ─────────────────────────────────────────────────────────
/** AWARENESS / TRAFFIC / LEADS / ENGAGEMENT / SALES */
private String campaignObjective;
/** Читаемая цель */
private String campaignObjectiveLabel;
/** Обоснование цели */
private String campaignObjectiveRationale;
// ── Аудитория ─────────────────────────────────────────────────────────────
private AudienceProfileDto audienceProfile;
// ── Платформы и форматы ───────────────────────────────────────────────────
private List<String> recommendedPlatforms;
private List<String> recommendedAdFormats;
private List<String> recommendedPlacements;
// ── Бюджет ────────────────────────────────────────────────────────────────
private TargetingBudgetDto budgetRecommendation;
// ── Фазы таргетинга ───────────────────────────────────────────────────────
private List<TargetingPhaseDto> phases;
// ── KPI ───────────────────────────────────────────────────────────────────
private String estimatedCtr;
private String estimatedCpl;
private String estimatedReach;
private String estimatedFrequency;
private String estimatedConversions;
// ── Ключевые выводы из опросника ──────────────────────────────────────────
/** Сигналы из опросника, которые повлияли на рекомендацию */
private List<String> questionnaireSinals;
// ── Конкурентный контекст ─────────────────────────────────────────────────
private String competitiveContext;
private String ciiLevel;
private String differentiationAdvice;
// ── Быстрые победы и предупреждения ──────────────────────────────────────
private List<String> quickWins;
private List<String> warnings;
// ── Контент-стратегия ─────────────────────────────────────────────────────
private List<String> contentMix;
private String postingFrequency;
private List<String> bestPostingTimes;
// ── CTA и воронка ─────────────────────────────────────────────────────────
private String primaryCta;
private String funnelStage;
private String conversionMechanism;
}
File diff suppressed because it is too large Load Diff
@@ -4,6 +4,8 @@ import kz.konturai.parser.dto.StatusHistoryEntry;
import kz.konturai.parser.dto.TargetingAIAnalysisResult;
import kz.konturai.parser.dto.TargetingCampaignRequest;
import kz.konturai.parser.dto.TargetingPerformanceDto;
import kz.konturai.parser.dto.targeting.AudienceProfileDto;
import kz.konturai.parser.dto.targeting.TargetingRecommendationDto;
import kz.konturai.parser.exception.TargetingCampaignNotFoundException;
import kz.konturai.parser.model.*;
import kz.konturai.parser.repository.MarketingAnalysisV3Repository;
@@ -22,9 +24,13 @@ import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Locale;
import java.util.Objects;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@@ -34,6 +40,7 @@ public class TargetingCampaignService {
private final TargetingCampaignRepository repository;
private final MarketingAnalysisV3Repository analysisRepository;
private final AiTargetologistService aiService;
private final AiTargetingSystemService aiTargetingSystemService;
private final ABTestingService abTestingService;
private final FacebookAdsService facebookAdsService;
private final TikTokAdsService tikTokAdsService;
@@ -42,21 +49,30 @@ public class TargetingCampaignService {
private final MarketingStrategyRepository strategyRepository;
public TargetingCampaign createCampaign(TargetingCampaignRequest req, String userId) {
if ((req.getAnalysisId() == null || req.getAnalysisId().isBlank()) && (req.getStrategyId() == null || req.getStrategyId().isBlank())) {
throw new IllegalArgumentException("analysisId or strategyId is required");
if (req.getStrategyId() == null || req.getStrategyId().isBlank()) {
throw new IllegalArgumentException("strategyId is required");
}
if (req.getStrategyId() != null && !req.getStrategyId().isBlank()) {
MarketingStrategy strategy = strategyRepository.findById(req.getStrategyId())
.orElseThrow(() -> new IllegalArgumentException("Strategy not found: " + req.getStrategyId()));
if (req.getAnalysisId() == null || req.getAnalysisId().isBlank()) {
req.setAnalysisId(strategy.getAnalysisId());
}
MarketingStrategy strategy = strategyRepository.findById(req.getStrategyId())
.orElseThrow(() -> new IllegalArgumentException("Strategy not found: " + req.getStrategyId()));
if (strategy.getAnalysisId() == null || strategy.getAnalysisId().isBlank()) {
throw new IllegalArgumentException("Strategy does not contain analysisId: " + req.getStrategyId());
}
MarketingAnalysisV3Document analysis = analysisRepository.findById(strategy.getAnalysisId())
.orElseThrow(() -> new IllegalArgumentException("Analysis not found: " + strategy.getAnalysisId()));
if (req.getTotalBudgetKzt() == null || req.getTotalBudgetKzt() <= 0) {
throw new IllegalArgumentException("Valid budget is required");
}
TargetingRecommendationDto recommendation =
aiTargetingSystemService.generateLinkedRecommendation(strategy, analysis);
TargetingAudienceProfile audienceProfile =
buildAudienceProfile(recommendation, req.getAudienceOverride());
TargetingAIAnalysisResult aiAnalysisResult =
buildAiAnalysisResult(strategy, analysis, recommendation);
BudgetConfig budget = BudgetConfig.builder()
.totalBudget(req.getTotalBudgetKzt())
.dailyBudget(req.getDailyBudgetKzt())
@@ -66,12 +82,14 @@ public class TargetingCampaignService {
TargetingCampaign campaign = TargetingCampaign.builder()
.userId(userId)
.analysisId(req.getAnalysisId())
.analysisId(strategy.getAnalysisId())
.strategyId(req.getStrategyId())
.name(req.getCampaignName())
.objective(CampaignObjective.valueOf(req.getObjective()))
.name(resolveCampaignName(req, strategy, recommendation))
.objective(resolveObjective(req, recommendation))
.status("draft")
.budget(budget)
.audience(audienceProfile)
.aiRecommendations(aiAnalysisResult)
.createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
.statusHistory(new ArrayList<>())
@@ -80,13 +98,9 @@ public class TargetingCampaignService {
.build();
// Map platforms
if (req.getPlatforms() != null) {
List<kz.konturai.parser.enums.TargetingPlatform> platforms = new ArrayList<>();
req.getPlatforms().forEach(p -> platforms.add(kz.konturai.parser.enums.TargetingPlatform.valueOf(p)));
campaign.setPlatforms(platforms);
}
campaign.setPlatforms(resolvePlatforms(req, recommendation));
addStatusHistory(campaign, "CREATED", "Campaign draft created, initiating AI pipeline");
addStatusHistory(campaign, "CREATED", "Campaign draft created from strategy with AI targeting recommendation");
TargetingCampaign saved = repository.save(campaign);
// Process Async
@@ -108,12 +122,7 @@ public class TargetingCampaignService {
return;
}
MarketingStrategy strategy = null;
if (req.getStrategyId() != null) {
strategy = strategyRepository.findById(req.getStrategyId()).orElse(null);
} else {
strategy = strategyRepository.findFirstByAnalysisIdOrderByCreatedAtDesc(campaign.getAnalysisId()).orElse(null);
}
MarketingStrategy strategy = strategyRepository.findById(campaign.getStrategyId()).orElse(null);
if (strategy == null) {
failCampaign(campaign, "Marketing Strategy containing media assets not found.");
return;
@@ -346,4 +355,202 @@ public class TargetingCampaignService {
.message(message)
.build());
}
private CampaignObjective resolveObjective(
TargetingCampaignRequest req,
TargetingRecommendationDto recommendation) {
String objective = req.getObjective();
if (objective == null || objective.isBlank()) {
objective = recommendation != null ? recommendation.getCampaignObjective() : null;
}
if (objective == null || objective.isBlank()) {
return CampaignObjective.LEADS;
}
try {
return CampaignObjective.valueOf(objective.trim().toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException ex) {
log.warn("Unknown campaign objective '{}', fallback to LEADS", objective);
return CampaignObjective.LEADS;
}
}
private String resolveCampaignName(
TargetingCampaignRequest req,
MarketingStrategy strategy,
TargetingRecommendationDto recommendation) {
if (req.getCampaignName() != null && !req.getCampaignName().isBlank()) {
return req.getCampaignName().trim();
}
List<String> nameParts = new ArrayList<>();
nameParts.add("Targeting");
if (strategy.getScoringModelTitle() != null && !strategy.getScoringModelTitle().isBlank()) {
nameParts.add(strategy.getScoringModelTitle().trim());
}
if (recommendation != null
&& recommendation.getRecommendedTypeLabel() != null
&& !recommendation.getRecommendedTypeLabel().isBlank()) {
nameParts.add(recommendation.getRecommendedTypeLabel().trim());
}
return String.join(" | ", nameParts);
}
private List<kz.konturai.parser.enums.TargetingPlatform> resolvePlatforms(
TargetingCampaignRequest req,
TargetingRecommendationDto recommendation) {
List<String> rawPlatforms = req.getPlatforms();
if (rawPlatforms == null || rawPlatforms.isEmpty()) {
rawPlatforms = recommendation != null ? recommendation.getRecommendedPlatforms() : Collections.emptyList();
}
if (rawPlatforms == null || rawPlatforms.isEmpty()) {
return List.of(
kz.konturai.parser.enums.TargetingPlatform.INSTAGRAM,
kz.konturai.parser.enums.TargetingPlatform.FACEBOOK
);
}
return rawPlatforms.stream()
.map(this::mapPlatform)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
}
private kz.konturai.parser.enums.TargetingPlatform mapPlatform(String platform) {
if (platform == null || platform.isBlank()) {
return null;
}
String normalized = platform.trim().toUpperCase(Locale.ROOT);
if ("META".equals(normalized)) {
return kz.konturai.parser.enums.TargetingPlatform.FACEBOOK;
}
try {
return kz.konturai.parser.enums.TargetingPlatform.valueOf(normalized);
} catch (IllegalArgumentException ex) {
log.warn("Unknown targeting platform '{}', skipping", platform);
return null;
}
}
private TargetingAIAnalysisResult buildAiAnalysisResult(
MarketingStrategy strategy,
MarketingAnalysisV3Document analysis,
TargetingRecommendationDto recommendation) {
return TargetingAIAnalysisResult.builder()
.strategyId(strategy.getId())
.analysisId(strategy.getAnalysisId())
.businessNiche(analysis.getRequestData() != null ? analysis.getRequestData().getBusinessNiche() : null)
.productName(analysis.getRequestData() != null ? analysis.getRequestData().getProductName() : null)
.goal(analysis.getRequestData() != null ? analysis.getRequestData().getGoal() : null)
.recommendedTargetingType(recommendation != null ? recommendation.getRecommendedType() : null)
.recommendedTargetingTypeLabel(recommendation != null ? recommendation.getRecommendedTypeLabel() : null)
.recommendedCampaignObjective(recommendation != null ? recommendation.getCampaignObjective() : null)
.questionnaireSignals(recommendation != null ? recommendation.getQuestionnaireSinals() : List.of())
.aiRecommendationsRationale(recommendation != null ? recommendation.getTypeRationale() : null)
.quickWins(recommendation != null ? recommendation.getQuickWins() : List.of())
.warnings(recommendation != null ? recommendation.getWarnings() : List.of())
.targetingRecommendation(recommendation)
.build();
}
private TargetingAudienceProfile buildAudienceProfile(
TargetingRecommendationDto recommendation,
kz.konturai.parser.dto.AudienceOverrideDto overrideDto) {
AudienceProfileDto aiAudience = recommendation != null ? recommendation.getAudienceProfile() : null;
int[] ageRange = parseAgeRange(aiAudience != null ? aiAudience.getAgeRange() : null);
int ageMin = overrideDto != null && overrideDto.getAgeMin() > 0 ? overrideDto.getAgeMin() : ageRange[0];
int ageMax = overrideDto != null && overrideDto.getAgeMax() > 0 ? overrideDto.getAgeMax() : ageRange[1];
List<String> interests = new ArrayList<>();
if (aiAudience != null && aiAudience.getInterests() != null) {
interests.addAll(aiAudience.getInterests());
}
if (overrideDto != null && overrideDto.getAdditionalInterests() != null) {
interests.addAll(overrideDto.getAdditionalInterests());
}
List<String> cities = new ArrayList<>();
if (overrideDto != null && overrideDto.getCities() != null && !overrideDto.getCities().isEmpty()) {
cities.addAll(overrideDto.getCities());
} else if (aiAudience != null && aiAudience.getGeography() != null) {
cities.addAll(aiAudience.getGeography());
}
List<String> genders = overrideDto != null && overrideDto.getGenders() != null && !overrideDto.getGenders().isEmpty()
? overrideDto.getGenders()
: inferGenders(aiAudience != null ? aiAudience.getGender() : null);
List<LocationTarget> locations = cities.stream()
.filter(Objects::nonNull)
.map(String::trim)
.filter(city -> !city.isBlank())
.distinct()
.map(city -> LocationTarget.builder()
.cityName(city)
.country("KZ")
.radius(25.0)
.build())
.collect(Collectors.toList());
return TargetingAudienceProfile.builder()
.ageMin(ageMin)
.ageMax(ageMax)
.genders(genders)
.locations(locations)
.interests(interests.stream().filter(Objects::nonNull).map(String::trim).filter(s -> !s.isBlank()).distinct().collect(Collectors.toList()))
.behaviors(aiAudience != null && aiAudience.getBehaviors() != null ? aiAudience.getBehaviors() : List.of())
.languages(splitLanguages(aiAudience != null ? aiAudience.getLanguage() : null))
.excludedAudiences(aiAudience != null && aiAudience.getExclusions() != null ? aiAudience.getExclusions() : List.of())
.deviceTypes(List.of("mobile", "desktop"))
.connectionType("ALL")
.audienceScore(85)
.build();
}
private int[] parseAgeRange(String rawAgeRange) {
if (rawAgeRange == null || rawAgeRange.isBlank()) {
return new int[]{25, 45};
}
String[] parts = rawAgeRange.replaceAll("[^0-9-]", "").split("-");
if (parts.length == 2) {
try {
return new int[]{Integer.parseInt(parts[0]), Integer.parseInt(parts[1])};
} catch (NumberFormatException ignored) {
return new int[]{25, 45};
}
}
return new int[]{25, 45};
}
private List<String> inferGenders(String genderText) {
if (genderText == null || genderText.isBlank()) {
return List.of("ALL");
}
String normalized = genderText.toLowerCase(Locale.ROOT);
boolean hasWomen = normalized.contains("жен");
boolean hasMen = normalized.contains("муж");
if (hasWomen && hasMen) {
return List.of("FEMALE", "MALE");
}
if (hasWomen) {
return List.of("FEMALE");
}
if (hasMen) {
return List.of("MALE");
}
return List.of("ALL");
}
private List<String> splitLanguages(String languagesText) {
if (languagesText == null || languagesText.isBlank()) {
return List.of("Русский");
}
return List.of(languagesText.split(","))
.stream()
.map(String::trim)
.filter(s -> !s.isBlank())
.distinct()
.collect(Collectors.toList());
}
}