This commit is contained in:
root
2025-12-03 19:33:49 +05:00
parent 8b1a96efe5
commit 16609d3eb2
11 changed files with 1726 additions and 148 deletions
@@ -73,22 +73,41 @@ public class MarketingController {
return unauthorizedResponse();
}
// Create analysis record
MarketingAnalysis analysis = marketingAnalysisService.startAnalysis(request, userId);
// Create analysis records (one for each analysis type)
List<MarketingAnalysis> analyses = marketingAnalysisService.startAnalysis(request, userId);
// Start async processing
marketingAnalysisService.processAnalysis(analysis.getId(), request);
// Start async processing for each analysis
for (MarketingAnalysis analysis : analyses) {
marketingAnalysisService.processAnalysis(analysis.getId(), request);
}
// Calculate estimated completion time (5-10 minutes)
LocalDateTime estimatedCompletion = LocalDateTime.now().plusMinutes(8);
MarketingAnalysisResponse response = new MarketingAnalysisResponse(
analysis.getId(),
"processing",
estimatedCompletion,
"Анализ запущен успешно. Результаты будут готовы в течение 5-10 минут.");
return ResponseEntity.ok(ApiResponse.success("Анализ запущен успешно", response));
// If multiple analyses, return list of responses
if (analyses.size() > 1) {
List<MarketingAnalysisResponse> responses = analyses.stream()
.map(analysis -> new MarketingAnalysisResponse(
analysis.getId(),
"processing",
estimatedCompletion,
"Анализ типа '" +
(analysis.getAnalysisType() != null && !analysis.getAnalysisType().isEmpty()
? analysis.getAnalysisType().get(0) : "неизвестно") +
"' запущен успешно. Результаты будут готовы в течение 5-10 минут."))
.collect(Collectors.toList());
return ResponseEntity.ok(ApiResponse.success(
"Создано анализов: " + analyses.size() + ". Все анализы запущены успешно.",
responses));
} else {
MarketingAnalysis analysis = analyses.get(0);
MarketingAnalysisResponse response = new MarketingAnalysisResponse(
analysis.getId(),
"processing",
estimatedCompletion,
"Анализ запущен успешно. Результаты будут готовы в течение 5-10 минут.");
return ResponseEntity.ok(ApiResponse.success("Анализ запущен успешно", response));
}
}
@GetMapping("/{analysisId}")
@@ -407,13 +426,52 @@ public class MarketingController {
response.setAnalysisId(analysis.getId());
response.setBusinessNiche(analysis.getBusinessNiche());
response.setProduct(analysis.getProduct());
response.setTargetAudience(analysis.getTargetAudience());
response.setRegion(analysis.getRegion());
// Convert targetAudience Map to String representation
Map<String, Object> targetAudienceMap = analysis.getTargetAudience();
if (targetAudienceMap != null) {
StringBuilder audienceBuilder = new StringBuilder();
if (targetAudienceMap.containsKey("genders")) {
@SuppressWarnings("unchecked")
List<String> genders = (List<String>) targetAudienceMap.get("genders");
if (genders != null && !genders.isEmpty()) {
audienceBuilder.append("Гендер: ").append(String.join(", ", genders)).append(". ");
}
}
if (targetAudienceMap.containsKey("ageRanges")) {
@SuppressWarnings("unchecked")
List<String> ageRanges = (List<String>) targetAudienceMap.get("ageRanges");
if (ageRanges != null && !ageRanges.isEmpty()) {
audienceBuilder.append("Возраст: ").append(String.join(", ", ageRanges)).append(". ");
}
}
if (targetAudienceMap.containsKey("types")) {
@SuppressWarnings("unchecked")
List<String> types = (List<String>) targetAudienceMap.get("types");
if (types != null && !types.isEmpty()) {
audienceBuilder.append("Тип: ").append(String.join(", ", types)).append(". ");
}
}
response.setTargetAudience(audienceBuilder.toString().trim());
}
// Convert region List to String
List<String> regions = analysis.getRegion();
if (regions != null && !regions.isEmpty()) {
response.setRegion(String.join(", ", regions));
}
response.setGoal(analysis.getGoal());
response.setDetailLevel(analysis.getDetailLevel());
response.setStrongSide(analysis.getStrongSide());
response.setWeakSide(analysis.getWeakSide());
response.setAnalysisType(analysis.getAnalysisType());
// Convert analysisType List to String
List<String> analysisTypes = analysis.getAnalysisType();
if (analysisTypes != null && !analysisTypes.isEmpty()) {
response.setAnalysisType(String.join(", ", analysisTypes));
}
response.setStatus(analysis.getStatus());
response.setUserId(analysis.getUserId());
response.setCreatedAt(analysis.getCreatedAt());
@@ -1,6 +1,7 @@
package kz.konturai.parser.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import kz.konturai.parser.model.AnalysisType;
@@ -8,6 +9,10 @@ import kz.konturai.parser.model.DetailLevel;
import kz.konturai.parser.validator.ValidAnalysisType;
import kz.konturai.parser.validator.ValidDetailLevel;
import kz.konturai.parser.validator.ValidRegion;
import kz.konturai.parser.validator.ValidTargetAudience;
import java.util.List;
import java.util.Map;
public class MarketingAnalysisRequest {
@@ -21,13 +26,13 @@ public class MarketingAnalysisRequest {
@Pattern(regexp = "^[\\p{L}\\p{N}\\s\\-,]+$", message = "Поле 'product' содержит недопустимые символы")
private String product;
@NotBlank(message = "Поле 'targetAudience' обязательно для заполнения")
@Size(min = 3, max = 300, message = "Поле 'targetAudience' должно содержать от 3 до 300 символов")
private String targetAudience;
@NotEmpty(message = "Поле 'targetAudience' обязательно для заполнения")
@ValidTargetAudience(message = "Поле 'targetAudience' должно содержать валидную структуру с полями genders, ageRanges, types")
private Map<String, Object> targetAudience;
@NotBlank(message = "Поле 'region' обязательно для заполнения")
@ValidRegion(message = "Поле 'region' должно быть одним из допустимых городов Казахстана")
private String region;
@NotEmpty(message = "Поле 'region' обязательно для заполнения. Выберите хотя бы один регион")
@ValidRegion(message = "Поле 'region' должно содержать один или несколько допустимых городов Казахстана")
private List<String> region;
@NotBlank(message = "Поле 'goal' обязательно для заполнения")
@Size(min = 10, max = 500, message = "Поле 'goal' должно содержать от 10 до 500 символов")
@@ -43,15 +48,15 @@ public class MarketingAnalysisRequest {
@Size(max = 500, message = "Поле 'weakSide' должно содержать не более 500 символов")
private String weakSide;
@NotBlank(message = "Поле 'analysisType' обязательно для заполнения")
@ValidAnalysisType(message = "Поле 'analysisType' должно быть одним из: РЫНОК, КОНКУРЕНТЫ, ЦА, КАНАЛЫ, SWOT")
private String analysisType;
@NotEmpty(message = "Поле 'analysisType' обязательно для заполнения. Выберите хотя бы один тип анализа")
@ValidAnalysisType(message = "Поле 'analysisType' должно содержать один или несколько типов: РЫНОК, КОНКУРЕНТЫ, ЦА, КАНАЛЫ, SWOT")
private List<String> analysisType;
public MarketingAnalysisRequest() {
}
public MarketingAnalysisRequest(String businessNiche, String product, String targetAudience, String region,
String goal, String detailLevel, String strongSide, String weakSide, String analysisType) {
public MarketingAnalysisRequest(String businessNiche, String product, Map<String, Object> targetAudience, List<String> region,
String goal, String detailLevel, String strongSide, String weakSide, List<String> analysisType) {
this.businessNiche = businessNiche;
this.product = product;
this.targetAudience = targetAudience;
@@ -79,19 +84,19 @@ public class MarketingAnalysisRequest {
this.product = product;
}
public String getTargetAudience() {
public Map<String, Object> getTargetAudience() {
return targetAudience;
}
public void setTargetAudience(String targetAudience) {
public void setTargetAudience(Map<String, Object> targetAudience) {
this.targetAudience = targetAudience;
}
public String getRegion() {
public List<String> getRegion() {
return region;
}
public void setRegion(String region) {
public void setRegion(List<String> region) {
this.region = region;
}
@@ -131,15 +136,21 @@ public class MarketingAnalysisRequest {
this.weakSide = weakSide;
}
public String getAnalysisType() {
public List<String> getAnalysisType() {
return analysisType;
}
public void setAnalysisType(String analysisType) {
public void setAnalysisType(List<String> analysisType) {
this.analysisType = analysisType;
}
public AnalysisType getAnalysisTypeEnum() {
return AnalysisType.fromString(analysisType);
public List<AnalysisType> getAnalysisTypeEnums() {
if (analysisType == null) {
return List.of();
}
return analysisType.stream()
.map(AnalysisType::fromString)
.filter(type -> type != null)
.toList();
}
}
@@ -23,10 +23,10 @@ public class MarketingAnalysis {
private String product;
@Field("target_audience")
private String targetAudience;
private Map<String, Object> targetAudience; // JSON structure: {genders: [], ageRanges: [], types: []}
@Field("region")
private String region;
private List<String> region;
@Field("goal")
private String goal;
@@ -41,7 +41,7 @@ public class MarketingAnalysis {
private String weakSide;
@Field("analysis_type")
private String analysisType; // РЫНОК, КОНКУРЕНТЫ, ЦА, КАНАЛЫ, SWOT
private List<String> analysisType; // РЫНОК, КОНКУРЕНТЫ, ЦА, КАНАЛЫ, SWOT
@Field("status")
private String status; // queued, processing, completed, failed
@@ -73,8 +73,9 @@ public class MarketingAnalysis {
this.statusHistory = new ArrayList<>();
}
public MarketingAnalysis(String businessNiche, String product, String targetAudience, String region,
String goal, String detailLevel, String strongSide, String weakSide, String analysisType) {
public MarketingAnalysis(String businessNiche, String product, Map<String, Object> targetAudience,
List<String> region,
String goal, String detailLevel, String strongSide, String weakSide, List<String> analysisType) {
this();
this.businessNiche = businessNiche;
this.product = product;
@@ -111,19 +112,19 @@ public class MarketingAnalysis {
this.businessNiche = businessNiche;
}
public String getTargetAudience() {
public Map<String, Object> getTargetAudience() {
return targetAudience;
}
public void setTargetAudience(String targetAudience) {
public void setTargetAudience(Map<String, Object> targetAudience) {
this.targetAudience = targetAudience;
}
public String getRegion() {
public List<String> getRegion() {
return region;
}
public void setRegion(String region) {
public void setRegion(List<String> region) {
this.region = region;
}
@@ -223,11 +224,11 @@ public class MarketingAnalysis {
this.statusHistory = statusHistory;
}
public String getAnalysisType() {
public List<String> getAnalysisType() {
return analysisType;
}
public void setAnalysisType(String analysisType) {
public void setAnalysisType(List<String> analysisType) {
this.analysisType = analysisType;
}
}
@@ -37,24 +37,33 @@ public class MarketingAnalysisService {
this.researchPdfService = researchPdfService;
}
public MarketingAnalysis startAnalysis(MarketingAnalysisRequest request, String userId) {
MarketingAnalysis analysis = new MarketingAnalysis(
request.getBusinessNiche(),
request.getProduct(),
request.getTargetAudience(),
request.getRegion(),
request.getGoal(),
request.getDetailLevel(),
request.getStrongSide(),
request.getWeakSide(),
request.getAnalysisType());
analysis.setUserId(userId);
analysis.setStatus("queued");
addStatusHistoryEntry(analysis, "queued", "Анализ создан и добавлен в очередь");
analysis = repository.save(analysis);
logger.info("Marketing analysis created with ID: {} for user: {} with type: {}",
analysis.getId(), userId, request.getAnalysisType());
return analysis;
public List<MarketingAnalysis> startAnalysis(MarketingAnalysisRequest request, String userId) {
List<String> analysisTypes = request.getAnalysisType();
if (analysisTypes == null || analysisTypes.isEmpty()) {
throw new IllegalArgumentException("Необходимо выбрать хотя бы один тип анализа");
}
List<MarketingAnalysis> analyses = new ArrayList<>();
for (String analysisType : analysisTypes) {
MarketingAnalysis analysis = new MarketingAnalysis(
request.getBusinessNiche(),
request.getProduct(),
request.getTargetAudience(),
request.getRegion(),
request.getGoal(),
request.getDetailLevel(),
request.getStrongSide(),
request.getWeakSide(),
List.of(analysisType)); // Каждый анализ имеет один тип
analysis.setUserId(userId);
analysis.setStatus("queued");
addStatusHistoryEntry(analysis, "queued", "Анализ создан и добавлен в очередь");
analysis = repository.save(analysis);
analyses.add(analysis);
logger.info("Marketing analysis created with ID: {} for user: {} with type: {}",
analysis.getId(), userId, analysisType);
}
return analyses;
}
private void addStatusHistoryEntry(MarketingAnalysis analysis, String status, String message) {
@@ -81,15 +90,18 @@ public class MarketingAnalysisService {
logger.info("Starting marketing analysis processing for ID: {}", analysisId);
// Create a modified request with single analysis type for this analysis
MarketingAnalysisRequest singleTypeRequest = createSingleTypeRequest(request, analysis.getAnalysisType().get(0));
// Generate marketing report
Map<String, Object> reportData = generateMarketingReport(request);
Map<String, Object> reportData = generateMarketingReport(singleTypeRequest);
// Generate markdown content for PDF
String markdownContent = buildMarkdownFromReport(reportData, request);
String markdownContent = buildMarkdownFromReport(reportData, singleTypeRequest);
// Generate PDF
byte[] pdfBytes = researchPdfService.generatePdfReport(
"Маркетинговый анализ: " + request.getProduct(),
"Маркетинговый анализ: " + singleTypeRequest.getProduct(),
markdownContent,
new ArrayList<>());
@@ -124,11 +136,26 @@ public class MarketingAnalysisService {
}
}
private MarketingAnalysisRequest createSingleTypeRequest(MarketingAnalysisRequest originalRequest, String analysisType) {
MarketingAnalysisRequest singleTypeRequest = new MarketingAnalysisRequest();
singleTypeRequest.setBusinessNiche(originalRequest.getBusinessNiche());
singleTypeRequest.setProduct(originalRequest.getProduct());
singleTypeRequest.setTargetAudience(originalRequest.getTargetAudience());
singleTypeRequest.setRegion(originalRequest.getRegion());
singleTypeRequest.setGoal(originalRequest.getGoal());
singleTypeRequest.setDetailLevel(originalRequest.getDetailLevel());
singleTypeRequest.setStrongSide(originalRequest.getStrongSide());
singleTypeRequest.setWeakSide(originalRequest.getWeakSide());
singleTypeRequest.setAnalysisType(List.of(analysisType));
return singleTypeRequest;
}
private Map<String, Object> generateMarketingReport(MarketingAnalysisRequest request) {
Map<String, Object> report = new HashMap<>();
// Get analysis type and detail level
AnalysisType analysisType = request.getAnalysisTypeEnum();
// Get analysis type and detail level - теперь всегда один тип
List<AnalysisType> analysisTypes = request.getAnalysisTypeEnums();
AnalysisType analysisType = analysisTypes != null && !analysisTypes.isEmpty() ? analysisTypes.get(0) : null;
String analysisTypeName = analysisType != null ? analysisType.getDisplayName() : "Общий";
DetailLevel detailLevel = request.getDetailLevelEnum();
String detailLevelName = detailLevel != null ? detailLevel.getDisplayName() : "Стандартно";
@@ -137,8 +164,40 @@ public class MarketingAnalysisService {
StringBuilder contextBuilder = new StringBuilder();
contextBuilder.append("Ниша бизнеса: ").append(request.getBusinessNiche()).append("\n");
contextBuilder.append("Продукт/услуга: ").append(request.getProduct()).append("\n");
contextBuilder.append("Целевая аудитория: ").append(request.getTargetAudience()).append("\n");
contextBuilder.append("Регион: ").append(request.getRegion()).append("\n");
// Format target audience
Map<String, Object> targetAudienceData = request.getTargetAudience();
if (targetAudienceData != null) {
StringBuilder audienceBuilder = new StringBuilder();
if (targetAudienceData.containsKey("genders")) {
@SuppressWarnings("unchecked")
List<String> genders = (List<String>) targetAudienceData.get("genders");
if (genders != null && !genders.isEmpty()) {
audienceBuilder.append("Гендер: ").append(String.join(", ", genders)).append(". ");
}
}
if (targetAudienceData.containsKey("ageRanges")) {
@SuppressWarnings("unchecked")
List<String> ageRanges = (List<String>) targetAudienceData.get("ageRanges");
if (ageRanges != null && !ageRanges.isEmpty()) {
audienceBuilder.append("Возраст: ").append(String.join(", ", ageRanges)).append(". ");
}
}
if (targetAudienceData.containsKey("types")) {
@SuppressWarnings("unchecked")
List<String> types = (List<String>) targetAudienceData.get("types");
if (types != null && !types.isEmpty()) {
audienceBuilder.append("Тип: ").append(String.join(", ", types)).append(". ");
}
}
contextBuilder.append("Целевая аудитория: ").append(audienceBuilder.toString().trim()).append("\n");
}
// Format regions
List<String> regions = request.getRegion();
if (regions != null && !regions.isEmpty()) {
contextBuilder.append("Регион: ").append(String.join(", ", regions)).append("\n");
}
contextBuilder.append("Цель на 6-12 месяцев: ").append(request.getGoal()).append("\n");
contextBuilder.append("Уровень детализации: ").append(detailLevelName).append("\n");
if (request.getStrongSide() != null && !request.getStrongSide().trim().isEmpty()) {
@@ -395,8 +454,41 @@ public class MarketingAnalysisService {
markdown.append("## Информация о бизнесе\n\n");
markdown.append("- **Ниша бизнеса:** ").append(request.getBusinessNiche()).append("\n");
markdown.append("- **Продукт/услуга:** ").append(request.getProduct()).append("\n");
markdown.append("- **Целевая аудитория:** ").append(request.getTargetAudience()).append("\n");
markdown.append("- **Регион:** ").append(request.getRegion()).append("\n");
// Format target audience
Map<String, Object> targetAudienceData = request.getTargetAudience();
if (targetAudienceData != null) {
StringBuilder audienceBuilder = new StringBuilder();
if (targetAudienceData.containsKey("genders")) {
@SuppressWarnings("unchecked")
List<String> genders = (List<String>) targetAudienceData.get("genders");
if (genders != null && !genders.isEmpty()) {
audienceBuilder.append("Гендер: ").append(String.join(", ", genders)).append(". ");
}
}
if (targetAudienceData.containsKey("ageRanges")) {
@SuppressWarnings("unchecked")
List<String> ageRanges = (List<String>) targetAudienceData.get("ageRanges");
if (ageRanges != null && !ageRanges.isEmpty()) {
audienceBuilder.append("Возраст: ").append(String.join(", ", ageRanges)).append(". ");
}
}
if (targetAudienceData.containsKey("types")) {
@SuppressWarnings("unchecked")
List<String> types = (List<String>) targetAudienceData.get("types");
if (types != null && !types.isEmpty()) {
audienceBuilder.append("Тип: ").append(String.join(", ", types)).append(". ");
}
}
markdown.append("- **Целевая аудитория:** ").append(audienceBuilder.toString().trim()).append("\n");
}
// Format regions
List<String> regions = request.getRegion();
if (regions != null && !regions.isEmpty()) {
markdown.append("- **Регион:** ").append(String.join(", ", regions)).append("\n");
}
markdown.append("- **Цель на 6-12 месяцев:** ").append(request.getGoal()).append("\n");
String detailLevelName = (String) reportData.get("detailLevel");
@@ -3,18 +3,25 @@ package kz.konturai.parser.validator;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import kz.konturai.parser.model.AnalysisType;
import java.util.List;
public class AnalysisTypeValidator implements ConstraintValidator<ValidAnalysisType, String> {
public class AnalysisTypeValidator implements ConstraintValidator<ValidAnalysisType, List<String>> {
@Override
public void initialize(ValidAnalysisType constraintAnnotation) {
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null || value.trim().isEmpty()) {
return true; // @NotBlank will handle null/empty
public boolean isValid(List<String> value, ConstraintValidatorContext context) {
if (value == null || value.isEmpty()) {
return true; // @NotEmpty will handle null/empty
}
return AnalysisType.fromString(value) != null;
// Проверяем, что все типы анализа в списке валидны
for (String type : value) {
if (type == null || type.trim().isEmpty() || AnalysisType.fromString(type) == null) {
return false;
}
}
return true;
}
}
@@ -5,7 +5,7 @@ import jakarta.validation.ConstraintValidatorContext;
import java.util.Arrays;
import java.util.List;
public class RegionValidator implements ConstraintValidator<ValidRegion, String> {
public class RegionValidator implements ConstraintValidator<ValidRegion, List<String>> {
private static final List<String> VALID_REGIONS = Arrays.asList(
"Алматы",
@@ -34,11 +34,17 @@ public class RegionValidator implements ConstraintValidator<ValidRegion, String>
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null || value.trim().isEmpty()) {
return true; // @NotBlank will handle null/empty
public boolean isValid(List<String> value, ConstraintValidatorContext context) {
if (value == null || value.isEmpty()) {
return true; // @NotEmpty will handle null/empty
}
return VALID_REGIONS.contains(value);
// Проверяем, что все регионы в списке валидны
for (String region : value) {
if (region == null || region.trim().isEmpty() || !VALID_REGIONS.contains(region)) {
return false;
}
}
return true;
}
}
@@ -0,0 +1,90 @@
package kz.konturai.parser.validator;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import java.util.List;
import java.util.Map;
public class TargetAudienceValidator implements ConstraintValidator<ValidTargetAudience, Map<String, Object>> {
private static final List<String> VALID_GENDERS = List.of("Женщины", "Мужчины");
private static final List<String> VALID_AGE_RANGES = List.of("20-40", "25-45", "18-25", "40-60", "60+");
private static final List<String> VALID_TYPES = List.of("Семьи", "Молодёжь", "Все подряд");
@Override
public void initialize(ValidTargetAudience constraintAnnotation) {
}
@Override
public boolean isValid(Map<String, Object> value, ConstraintValidatorContext context) {
if (value == null || value.isEmpty()) {
return false;
}
// Проверяем наличие хотя бы одного поля
boolean hasGenders = value.containsKey("genders") && value.get("genders") != null;
boolean hasAgeRanges = value.containsKey("ageRanges") && value.get("ageRanges") != null;
boolean hasTypes = value.containsKey("types") && value.get("types") != null;
if (!hasGenders && !hasAgeRanges && !hasTypes) {
return false;
}
// Валидация genders
if (hasGenders) {
Object gendersObj = value.get("genders");
if (!(gendersObj instanceof List)) {
return false;
}
@SuppressWarnings("unchecked")
List<String> genders = (List<String>) gendersObj;
if (genders.isEmpty()) {
return false;
}
for (String gender : genders) {
if (!VALID_GENDERS.contains(gender)) {
return false;
}
}
}
// Валидация ageRanges
if (hasAgeRanges) {
Object ageRangesObj = value.get("ageRanges");
if (!(ageRangesObj instanceof List)) {
return false;
}
@SuppressWarnings("unchecked")
List<String> ageRanges = (List<String>) ageRangesObj;
if (ageRanges.isEmpty()) {
return false;
}
for (String ageRange : ageRanges) {
if (!VALID_AGE_RANGES.contains(ageRange)) {
return false;
}
}
}
// Валидация types
if (hasTypes) {
Object typesObj = value.get("types");
if (!(typesObj instanceof List)) {
return false;
}
@SuppressWarnings("unchecked")
List<String> types = (List<String>) typesObj;
if (types.isEmpty()) {
return false;
}
for (String type : types) {
if (!VALID_TYPES.contains(type)) {
return false;
}
}
}
return true;
}
}
@@ -13,7 +13,7 @@ import java.lang.annotation.Target;
@Target({ ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidRegion {
String message() default "Поле 'region' должно быть одним из допустимых городов Казахстана";
String message() default "Поле 'region' должно содержать один или несколько допустимых городов Казахстана";
Class<?>[] groups() default {};
@@ -0,0 +1,22 @@
package kz.konturai.parser.validator;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Documented
@Constraint(validatedBy = TargetAudienceValidator.class)
@Target({ ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidTargetAudience {
String message() default "Поле 'targetAudience' должно содержать валидную структуру с полями genders, ageRanges, types";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}