This commit is contained in:
root
2025-11-23 18:50:59 +05:00
parent 3941980337
commit b56dc44530
12 changed files with 2018 additions and 0 deletions
@@ -0,0 +1,127 @@
package kz.konturai.parser.controller;
import kz.konturai.parser.dto.*;
import kz.konturai.parser.model.MarketingAnalysis;
import kz.konturai.parser.service.MarketingAnalysisService;
import kz.konturai.parser.service.MinIOService;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@RestController
@RequestMapping("/api/marketing/analysis")
public class MarketingController {
private final MarketingAnalysisService marketingAnalysisService;
private final MinIOService minIOService;
public MarketingController(MarketingAnalysisService marketingAnalysisService, MinIOService minIOService) {
this.marketingAnalysisService = marketingAnalysisService;
this.minIOService = minIOService;
}
@PostMapping("/start")
public ResponseEntity<ApiResponse<MarketingAnalysisResponse>> startAnalysis(
@Valid @RequestBody MarketingAnalysisRequest request) {
// Create analysis record
MarketingAnalysis analysis = marketingAnalysisService.startAnalysis(request);
// Start async processing
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));
}
@GetMapping("/{analysisId}")
public ResponseEntity<?> getAnalysis(
@PathVariable String analysisId) {
MarketingAnalysisResult result = marketingAnalysisService.getAnalysisResult(analysisId);
if (result == null) {
ErrorResponse error = new ErrorResponse(
"NOT_FOUND",
"Анализ с указанным ID не найден");
return ResponseEntity.status(404)
.body(ApiResponse.error("Анализ не найден", error));
}
return ResponseEntity.ok(ApiResponse.success(result));
}
@GetMapping("/{analysisId}/download")
public ResponseEntity<byte[]> downloadPdf(@PathVariable String analysisId) {
MarketingAnalysisResult result = marketingAnalysisService.getAnalysisResult(analysisId);
if (result == null || result.getReport() == null || result.getReport().getPdfUrl() == null) {
return ResponseEntity.notFound().build();
}
// Get the analysis to find PDF filename
Optional<MarketingAnalysis> optAnalysis = marketingAnalysisService.getAnalysisById(analysisId);
if (optAnalysis.isEmpty() || optAnalysis.get().getPdfFilePath() == null) {
return ResponseEntity.notFound().build();
}
try {
InputStream inputStream = minIOService.downloadFile(optAnalysis.get().getPdfFilePath());
byte[] bytes = inputStream.readAllBytes();
inputStream.close();
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + optAnalysis.get().getPdfFilename() + "\"")
.contentType(MediaType.APPLICATION_PDF)
.body(bytes);
} catch (Exception e) {
return ResponseEntity.internalServerError().build();
}
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse<ErrorResponse>> handleValidationException(
MethodArgumentNotValidException ex) {
Map<String, String> details = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error -> {
details.put(error.getField(), error.getDefaultMessage());
});
ErrorResponse errorResponse = new ErrorResponse(
"VALIDATION_ERROR",
"Ошибка валидации входных данных",
details);
return ResponseEntity.badRequest()
.body(ApiResponse.error("Ошибка валидации", errorResponse));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<ErrorResponse>> handleGenericException(Exception e) {
ErrorResponse errorResponse = new ErrorResponse(
"INTERNAL_SERVER_ERROR",
"Произошла внутренняя ошибка сервера. Попробуйте позже.");
return ResponseEntity.status(500)
.body(ApiResponse.error("Внутренняя ошибка сервера", errorResponse));
}
}
@@ -0,0 +1,47 @@
package kz.konturai.parser.dto;
import java.util.Map;
public class ErrorResponse {
private String code;
private String message;
private Map<String, String> details;
public ErrorResponse() {
}
public ErrorResponse(String code, String message) {
this.code = code;
this.message = message;
}
public ErrorResponse(String code, String message, Map<String, String> details) {
this.code = code;
this.message = message;
this.details = details;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Map<String, String> getDetails() {
return details;
}
public void setDetails(Map<String, String> details) {
this.details = details;
}
}
@@ -0,0 +1,69 @@
package kz.konturai.parser.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import kz.konturai.parser.validator.ValidClientType;
public class MarketingAnalysisRequest {
@NotBlank(message = "Поле 'product' обязательно для заполнения")
@Size(min = 3, max = 200, message = "Поле 'product' должно содержать от 3 до 200 символов")
@Pattern(regexp = "^[\\p{L}\\p{N}\\s\\-,]+$", message = "Поле 'product' содержит недопустимые символы")
private String product;
@NotBlank(message = "Поле 'location' обязательно для заполнения")
@Size(min = 2, max = 150, message = "Поле 'location' должно содержать от 2 до 150 символов")
@Pattern(regexp = "^[\\p{L}\\p{N}\\s\\-,]+$", message = "Поле 'location' содержит недопустимые символы")
private String location;
@NotBlank(message = "Поле 'client' обязательно для заполнения")
@ValidClientType(message = "Поле 'client' должно быть одним из: B2B клиенты, B2C клиенты, Частные лица, Корпорации, Малый бизнес")
private String client;
@NotBlank(message = "Поле 'differentiator' обязательно для заполнения")
@Size(min = 10, max = 500, message = "Поле 'differentiator' должно содержать от 10 до 500 символов")
private String differentiator;
public MarketingAnalysisRequest() {
}
public MarketingAnalysisRequest(String product, String location, String client, String differentiator) {
this.product = product;
this.location = location;
this.client = client;
this.differentiator = differentiator;
}
public String getProduct() {
return product;
}
public void setProduct(String product) {
this.product = product;
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
public String getClient() {
return client;
}
public void setClient(String client) {
this.client = client;
}
public String getDifferentiator() {
return differentiator;
}
public void setDifferentiator(String differentiator) {
this.differentiator = differentiator;
}
}
@@ -0,0 +1,71 @@
package kz.konturai.parser.dto;
import java.time.LocalDateTime;
public class MarketingAnalysisResponse {
private String analysisId;
private String status;
private LocalDateTime estimatedCompletionTime;
private String message;
private Integer queuePosition;
private Integer estimatedWaitTime;
public MarketingAnalysisResponse() {
}
public MarketingAnalysisResponse(String analysisId, String status, LocalDateTime estimatedCompletionTime,
String message) {
this.analysisId = analysisId;
this.status = status;
this.estimatedCompletionTime = estimatedCompletionTime;
this.message = message;
}
public String getAnalysisId() {
return analysisId;
}
public void setAnalysisId(String analysisId) {
this.analysisId = analysisId;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public LocalDateTime getEstimatedCompletionTime() {
return estimatedCompletionTime;
}
public void setEstimatedCompletionTime(LocalDateTime estimatedCompletionTime) {
this.estimatedCompletionTime = estimatedCompletionTime;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Integer getQueuePosition() {
return queuePosition;
}
public void setQueuePosition(Integer queuePosition) {
this.queuePosition = queuePosition;
}
public Integer getEstimatedWaitTime() {
return estimatedWaitTime;
}
public void setEstimatedWaitTime(Integer estimatedWaitTime) {
this.estimatedWaitTime = estimatedWaitTime;
}
}
@@ -0,0 +1,183 @@
package kz.konturai.parser.dto;
import java.time.LocalDateTime;
import java.util.List;
public class MarketingAnalysisResult {
private String analysisId;
private String status;
private LocalDateTime createdAt;
private LocalDateTime completedAt;
private MarketingReport report;
public MarketingAnalysisResult() {
}
public MarketingAnalysisResult(String analysisId, String status, LocalDateTime createdAt, LocalDateTime completedAt,
MarketingReport report) {
this.analysisId = analysisId;
this.status = status;
this.createdAt = createdAt;
this.completedAt = completedAt;
this.report = report;
}
public String getAnalysisId() {
return analysisId;
}
public void setAnalysisId(String analysisId) {
this.analysisId = analysisId;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getCompletedAt() {
return completedAt;
}
public void setCompletedAt(LocalDateTime completedAt) {
this.completedAt = completedAt;
}
public MarketingReport getReport() {
return report;
}
public void setReport(MarketingReport report) {
this.report = report;
}
public static class MarketingReport {
private String summary;
private TargetAudience targetAudience;
private List<String> recommendations;
private Strategy strategy;
private String pdfUrl;
public MarketingReport() {
}
public String getSummary() {
return summary;
}
public void setSummary(String summary) {
this.summary = summary;
}
public TargetAudience getTargetAudience() {
return targetAudience;
}
public void setTargetAudience(TargetAudience targetAudience) {
this.targetAudience = targetAudience;
}
public List<String> getRecommendations() {
return recommendations;
}
public void setRecommendations(List<String> recommendations) {
this.recommendations = recommendations;
}
public Strategy getStrategy() {
return strategy;
}
public void setStrategy(Strategy strategy) {
this.strategy = strategy;
}
public String getPdfUrl() {
return pdfUrl;
}
public void setPdfUrl(String pdfUrl) {
this.pdfUrl = pdfUrl;
}
}
public static class TargetAudience {
private String description;
private List<String> channels;
public TargetAudience() {
}
public TargetAudience(String description, List<String> channels) {
this.description = description;
this.channels = channels;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public List<String> getChannels() {
return channels;
}
public void setChannels(List<String> channels) {
this.channels = channels;
}
}
public static class Strategy {
private String duration;
private List<String> channels;
private List<String> contentTypes;
public Strategy() {
}
public Strategy(String duration, List<String> channels, List<String> contentTypes) {
this.duration = duration;
this.channels = channels;
this.contentTypes = contentTypes;
}
public String getDuration() {
return duration;
}
public void setDuration(String duration) {
this.duration = duration;
}
public List<String> getChannels() {
return channels;
}
public void setChannels(List<String> channels) {
this.channels = channels;
}
public List<String> getContentTypes() {
return contentTypes;
}
public void setContentTypes(List<String> contentTypes) {
this.contentTypes = contentTypes;
}
}
}
@@ -0,0 +1,157 @@
package kz.konturai.parser.model;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
import java.time.LocalDateTime;
import java.util.Map;
@Document(collection = "marketing_analysis")
public class MarketingAnalysis {
@Id
private String id;
@Field("product")
private String product;
@Field("location")
private String location;
@Field("client_type")
private String clientType;
@Field("differentiator")
private String differentiator;
@Field("status")
private String status; // queued, processing, completed, failed
@Field("created_at")
private LocalDateTime createdAt;
@Field("completed_at")
private LocalDateTime completedAt;
@Field("report_data")
private Map<String, Object> reportData; // JSON data with report content
@Field("pdf_filename")
private String pdfFilename;
@Field("pdf_file_path")
private String pdfFilePath;
@Field("user_id")
private String userId; // Optional, for future authentication
public MarketingAnalysis() {
this.createdAt = LocalDateTime.now();
this.status = "queued";
}
public MarketingAnalysis(String product, String location, String clientType, String differentiator) {
this();
this.product = product;
this.location = location;
this.clientType = clientType;
this.differentiator = differentiator;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getProduct() {
return product;
}
public void setProduct(String product) {
this.product = product;
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
public String getClientType() {
return clientType;
}
public void setClientType(String clientType) {
this.clientType = clientType;
}
public String getDifferentiator() {
return differentiator;
}
public void setDifferentiator(String differentiator) {
this.differentiator = differentiator;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getCompletedAt() {
return completedAt;
}
public void setCompletedAt(LocalDateTime completedAt) {
this.completedAt = completedAt;
}
public Map<String, Object> getReportData() {
return reportData;
}
public void setReportData(Map<String, Object> reportData) {
this.reportData = reportData;
}
public String getPdfFilename() {
return pdfFilename;
}
public void setPdfFilename(String pdfFilename) {
this.pdfFilename = pdfFilename;
}
public String getPdfFilePath() {
return pdfFilePath;
}
public void setPdfFilePath(String pdfFilePath) {
this.pdfFilePath = pdfFilePath;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
}
@@ -0,0 +1,12 @@
package kz.konturai.parser.repository;
import kz.konturai.parser.model.MarketingAnalysis;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface MarketingAnalysisRepository extends MongoRepository<MarketingAnalysis, String> {
Optional<MarketingAnalysis> findById(String id);
}
@@ -0,0 +1,394 @@
package kz.konturai.parser.service;
import kz.konturai.parser.dto.MarketingAnalysisRequest;
import kz.konturai.parser.dto.MarketingAnalysisResult;
import kz.konturai.parser.model.MarketingAnalysis;
import kz.konturai.parser.repository.MarketingAnalysisRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.*;
@Service
public class MarketingAnalysisService {
private static final Logger logger = LoggerFactory.getLogger(MarketingAnalysisService.class);
private final MarketingAnalysisRepository repository;
private final OpenAIAnalyticsService openAIAnalyticsService;
private final MinIOService minIOService;
private final ResearchPdfService researchPdfService;
public MarketingAnalysisService(
MarketingAnalysisRepository repository,
OpenAIAnalyticsService openAIAnalyticsService,
MinIOService minIOService,
ResearchPdfService researchPdfService) {
this.repository = repository;
this.openAIAnalyticsService = openAIAnalyticsService;
this.minIOService = minIOService;
this.researchPdfService = researchPdfService;
}
public MarketingAnalysis startAnalysis(MarketingAnalysisRequest request) {
MarketingAnalysis analysis = new MarketingAnalysis(
request.getProduct(),
request.getLocation(),
request.getClient(),
request.getDifferentiator());
analysis.setStatus("queued");
analysis = repository.save(analysis);
logger.info("Marketing analysis created with ID: {}", analysis.getId());
return analysis;
}
@Async("reportGenerationExecutor")
public void processAnalysis(String analysisId, MarketingAnalysisRequest request) {
try {
Optional<MarketingAnalysis> optAnalysis = repository.findById(analysisId);
if (optAnalysis.isEmpty()) {
logger.error("Marketing analysis not found: {}", analysisId);
return;
}
MarketingAnalysis analysis = optAnalysis.get();
analysis.setStatus("processing");
repository.save(analysis);
logger.info("Starting marketing analysis processing for ID: {}", analysisId);
// Generate marketing report
Map<String, Object> reportData = generateMarketingReport(request);
// Generate markdown content for PDF
String markdownContent = buildMarkdownFromReport(reportData, request);
// Generate PDF
byte[] pdfBytes = researchPdfService.generatePdfReport(
"Маркетинговый анализ: " + request.getProduct(),
markdownContent,
new ArrayList<>());
// Save PDF to MinIO
String filename = "marketing_analysis_" + analysisId + "_" + System.currentTimeMillis() + ".pdf";
minIOService.uploadFile(filename, pdfBytes, MediaType.APPLICATION_PDF.toString());
// Update analysis with results
analysis.setStatus("completed");
analysis.setCompletedAt(LocalDateTime.now());
analysis.setReportData(reportData);
analysis.setPdfFilename(filename);
analysis.setPdfFilePath(filename);
repository.save(analysis);
logger.info("Marketing analysis completed successfully for ID: {}", analysisId);
} catch (Exception e) {
logger.error("Error processing marketing analysis {}: {}", analysisId, e.getMessage(), e);
try {
Optional<MarketingAnalysis> optAnalysis = repository.findById(analysisId);
if (optAnalysis.isPresent()) {
MarketingAnalysis analysis = optAnalysis.get();
analysis.setStatus("failed");
repository.save(analysis);
}
} catch (Exception saveError) {
logger.error("Failed to update analysis status to failed: {}", saveError.getMessage());
}
}
}
private Map<String, Object> generateMarketingReport(MarketingAnalysisRequest request) {
Map<String, Object> report = new HashMap<>();
// Build context for AI
String context = String.format(
"Продукт/услуга: %s\n" +
"Локация: %s\n" +
"Тип клиентов: %s\n" +
"Уникальные особенности: %s\n",
request.getProduct(),
request.getLocation(),
request.getClient(),
request.getDifferentiator());
// Generate summary
String summaryPrompt = "На основе следующей информации о бизнесе создай краткое резюме маркетингового анализа (2-3 абзаца). "
+
"Выдели ключевые возможности и особенности бизнеса. " +
"Ответ должен быть на русском языке, деловым стилем.\n\n" + context;
String summary = openAIAnalyticsService.generateWithInstruction(context, summaryPrompt, "ru");
report.put("summary", summary != null ? summary : "Резюме не удалось сгенерировать.");
// Generate target audience description
String audiencePrompt = "На основе информации о бизнесе опиши целевую аудиторию (1-2 абзаца). " +
"Укажи, какие каналы коммуникации наиболее подходят для этой аудитории. " +
"Ответ должен быть на русском языке.\n\n" + context;
String audienceDescription = openAIAnalyticsService.generateWithInstruction(context, audiencePrompt, "ru");
// Extract channels from description or generate separately
String channelsPrompt = "На основе описания бизнеса перечисли 3-5 наиболее подходящих маркетинговых каналов. " +
"Ответ должен быть простым списком через запятую, без нумерации. Пример: Instagram, LinkedIn, Telegram\n\n"
+ context;
String channelsStr = openAIAnalyticsService.generateWithInstruction(context, channelsPrompt, "ru");
List<String> channels = parseChannels(channelsStr);
Map<String, Object> targetAudience = new HashMap<>();
targetAudience.put("description", audienceDescription != null ? audienceDescription
: "Описание целевой аудитории не удалось сгенерировать.");
targetAudience.put("channels",
channels.isEmpty() ? Arrays.asList("Instagram", "LinkedIn", "Telegram") : channels);
report.put("targetAudience", targetAudience);
// Generate recommendations
String recommendationsPrompt = "На основе информации о бизнесе сформулируй 4-6 практических рекомендаций для маркетинговой стратегии. "
+
"Каждая рекомендация должна быть конкретной и применимой. " +
"Ответ должен быть списком рекомендаций, каждая с новой строки, без нумерации.\n\n" + context;
String recommendationsStr = openAIAnalyticsService.generateWithInstruction(context, recommendationsPrompt,
"ru");
List<String> recommendations = parseRecommendations(recommendationsStr);
report.put("recommendations",
recommendations.isEmpty() ? Arrays.asList("Рекомендации не удалось сгенерировать.") : recommendations);
// Generate strategy
String strategyPrompt = "На основе информации о бизнесе создай краткую маркетинговую стратегию. " +
"Укажи рекомендуемую длительность кампании (например, '2 недели', '1 месяц'), " +
"3-5 каналов коммуникации и типы контента (например, 'посты', 'сторис', 'баннеры'). " +
"Ответ должен быть структурированным текстом на русском языке.\n\n" + context;
String strategyText = openAIAnalyticsService.generateWithInstruction(context, strategyPrompt, "ru");
Map<String, Object> strategy = parseStrategy(strategyText, channels);
report.put("strategy", strategy);
return report;
}
private List<String> parseChannels(String channelsStr) {
if (channelsStr == null || channelsStr.trim().isEmpty()) {
return new ArrayList<>();
}
List<String> channels = new ArrayList<>();
String[] parts = channelsStr.split("[,;\\n]");
for (String part : parts) {
String trimmed = part.trim();
if (!trimmed.isEmpty() && trimmed.length() > 1) {
channels.add(trimmed);
}
}
return channels;
}
private List<String> parseRecommendations(String recommendationsStr) {
if (recommendationsStr == null || recommendationsStr.trim().isEmpty()) {
return new ArrayList<>();
}
List<String> recommendations = new ArrayList<>();
String[] lines = recommendationsStr.split("\\n");
for (String line : lines) {
String trimmed = line.trim();
// Remove numbering and bullets
trimmed = trimmed.replaceAll("^[\\d\\-\\.\\*]+\\s*", "");
if (!trimmed.isEmpty() && trimmed.length() > 10) {
recommendations.add(trimmed);
}
}
return recommendations;
}
private Map<String, Object> parseStrategy(String strategyText, List<String> defaultChannels) {
Map<String, Object> strategy = new HashMap<>();
// Try to extract duration
String duration = "2 недели"; // default
if (strategyText != null) {
if (strategyText.contains("неделя") || strategyText.contains("недели") || strategyText.contains("недель")) {
// Extract duration pattern
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("(\\d+)\\s*(неделя|недели|недель)");
java.util.regex.Matcher matcher = pattern.matcher(strategyText);
if (matcher.find()) {
duration = matcher.group(1) + " " + matcher.group(2);
}
} else if (strategyText.contains("месяц") || strategyText.contains("месяца")
|| strategyText.contains("месяцев")) {
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("(\\d+)\\s*(месяц|месяца|месяцев)");
java.util.regex.Matcher matcher = pattern.matcher(strategyText);
if (matcher.find()) {
duration = matcher.group(1) + " " + matcher.group(2);
}
}
}
strategy.put("duration", duration);
// Extract channels from strategy text or use default
List<String> channels = parseChannels(strategyText);
if (channels.isEmpty()) {
channels = defaultChannels.isEmpty() ? Arrays.asList("Instagram", "Telegram", "21MC") : defaultChannels;
}
strategy.put("channels", channels);
// Extract content types
List<String> contentTypes = new ArrayList<>();
if (strategyText != null) {
String lowerText = strategyText.toLowerCase();
if (lowerText.contains("пост") || lowerText.contains("посты")) {
contentTypes.add("посты");
}
if (lowerText.contains("сторис") || lowerText.contains("stories")) {
contentTypes.add("сторис");
}
if (lowerText.contains("баннер") || lowerText.contains("banner")) {
contentTypes.add("баннеры");
}
if (lowerText.contains("видео") || lowerText.contains("video")) {
contentTypes.add("видео");
}
}
if (contentTypes.isEmpty()) {
contentTypes = Arrays.asList("посты", "сторис", "баннеры");
}
strategy.put("contentTypes", contentTypes);
return strategy;
}
private String buildMarkdownFromReport(Map<String, Object> reportData, MarketingAnalysisRequest request) {
StringBuilder markdown = new StringBuilder();
markdown.append("# Маркетинговый анализ\n\n");
markdown.append("## Информация о бизнесе\n\n");
markdown.append("- **Продукт/услуга:** ").append(request.getProduct()).append("\n");
markdown.append("- **Локация:** ").append(request.getLocation()).append("\n");
markdown.append("- **Тип клиентов:** ").append(request.getClient()).append("\n");
markdown.append("- **Уникальные особенности:** ").append(request.getDifferentiator()).append("\n\n");
markdown.append("## Резюме анализа\n\n");
markdown.append(reportData.get("summary")).append("\n\n");
@SuppressWarnings("unchecked")
Map<String, Object> targetAudience = (Map<String, Object>) reportData.get("targetAudience");
if (targetAudience != null) {
markdown.append("## Целевая аудитория\n\n");
markdown.append(targetAudience.get("description")).append("\n\n");
@SuppressWarnings("unchecked")
List<String> channels = (List<String>) targetAudience.get("channels");
if (channels != null && !channels.isEmpty()) {
markdown.append("### Рекомендуемые каналы:\n\n");
for (String channel : channels) {
markdown.append("- ").append(channel).append("\n");
}
markdown.append("\n");
}
}
@SuppressWarnings("unchecked")
List<String> recommendations = (List<String>) reportData.get("recommendations");
if (recommendations != null && !recommendations.isEmpty()) {
markdown.append("## Рекомендации\n\n");
for (String rec : recommendations) {
markdown.append("- ").append(rec).append("\n");
}
markdown.append("\n");
}
@SuppressWarnings("unchecked")
Map<String, Object> strategy = (Map<String, Object>) reportData.get("strategy");
if (strategy != null) {
markdown.append("## Маркетинговая стратегия\n\n");
markdown.append("### Длительность кампании: ").append(strategy.get("duration")).append("\n\n");
@SuppressWarnings("unchecked")
List<String> strategyChannels = (List<String>) strategy.get("channels");
if (strategyChannels != null && !strategyChannels.isEmpty()) {
markdown.append("### Каналы коммуникации:\n\n");
for (String channel : strategyChannels) {
markdown.append("- ").append(channel).append("\n");
}
markdown.append("\n");
}
@SuppressWarnings("unchecked")
List<String> contentTypes = (List<String>) strategy.get("contentTypes");
if (contentTypes != null && !contentTypes.isEmpty()) {
markdown.append("### Типы контента:\n\n");
for (String type : contentTypes) {
markdown.append("- ").append(type).append("\n");
}
markdown.append("\n");
}
}
return markdown.toString();
}
public MarketingAnalysisResult getAnalysisResult(String analysisId) {
Optional<MarketingAnalysis> optAnalysis = repository.findById(analysisId);
if (optAnalysis.isEmpty()) {
return null;
}
MarketingAnalysis analysis = optAnalysis.get();
MarketingAnalysisResult result = new MarketingAnalysisResult();
result.setAnalysisId(analysis.getId());
result.setStatus(analysis.getStatus());
result.setCreatedAt(analysis.getCreatedAt());
result.setCompletedAt(analysis.getCompletedAt());
if (analysis.getReportData() != null && "completed".equals(analysis.getStatus())) {
MarketingAnalysisResult.MarketingReport report = buildReportFromData(analysis.getReportData(), analysis);
result.setReport(report);
}
return result;
}
public Optional<MarketingAnalysis> getAnalysisById(String analysisId) {
return repository.findById(analysisId);
}
private MarketingAnalysisResult.MarketingReport buildReportFromData(Map<String, Object> reportData,
MarketingAnalysis analysis) {
MarketingAnalysisResult.MarketingReport report = new MarketingAnalysisResult.MarketingReport();
report.setSummary((String) reportData.get("summary"));
@SuppressWarnings("unchecked")
Map<String, Object> targetAudienceData = (Map<String, Object>) reportData.get("targetAudience");
if (targetAudienceData != null) {
MarketingAnalysisResult.TargetAudience targetAudience = new MarketingAnalysisResult.TargetAudience();
targetAudience.setDescription((String) targetAudienceData.get("description"));
@SuppressWarnings("unchecked")
List<String> channels = (List<String>) targetAudienceData.get("channels");
targetAudience.setChannels(channels);
report.setTargetAudience(targetAudience);
}
@SuppressWarnings("unchecked")
List<String> recommendations = (List<String>) reportData.get("recommendations");
report.setRecommendations(recommendations);
@SuppressWarnings("unchecked")
Map<String, Object> strategyData = (Map<String, Object>) reportData.get("strategy");
if (strategyData != null) {
MarketingAnalysisResult.Strategy strategy = new MarketingAnalysisResult.Strategy();
strategy.setDuration((String) strategyData.get("duration"));
@SuppressWarnings("unchecked")
List<String> channels = (List<String>) strategyData.get("channels");
strategy.setChannels(channels);
@SuppressWarnings("unchecked")
List<String> contentTypes = (List<String>) strategyData.get("contentTypes");
strategy.setContentTypes(contentTypes);
report.setStrategy(strategy);
}
if (analysis.getPdfFilename() != null) {
report.setPdfUrl("/api/marketing/analysis/" + analysis.getId() + "/download");
}
return report;
}
}
@@ -0,0 +1,28 @@
package kz.konturai.parser.validator;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import java.util.Arrays;
import java.util.List;
public class ClientTypeValidator implements ConstraintValidator<ValidClientType, String> {
private static final List<String> VALID_CLIENT_TYPES = Arrays.asList(
"B2B клиенты",
"B2C клиенты",
"Частные лица",
"Корпорации",
"Малый бизнес");
@Override
public void initialize(ValidClientType constraintAnnotation) {
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null || value.trim().isEmpty()) {
return true; // @NotBlank will handle null/empty
}
return VALID_CLIENT_TYPES.contains(value);
}
}
@@ -0,0 +1,21 @@
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 = ClientTypeValidator.class)
@Target({ ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidClientType {
String message() default "Поле 'client' должно быть одним из: B2B клиенты, B2C клиенты, Частные лица, Корпорации, Малый бизнес";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}