target fix
This commit is contained in:
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user