From f61de9acd1a1f8f9a8c639e979abd3dec21713be Mon Sep 17 00:00:00 2001 From: root Date: Sat, 3 Jan 2026 12:02:28 +0500 Subject: [PATCH] . --- FRONTEND_API_CHANGES_V2.md | 164 ++++++++++++++++ .../controller/MarketingController.java | 97 ++++++++- .../parser/dto/AnalysisHistoryResponse.java | 9 + .../parser/model/MarketingAnalysis.java | 11 ++ .../model/MarketingAnalysisV2Document.java | 185 ++++++++++++++++++ .../MarketingAnalysisV2Repository.java | 19 ++ .../service/MarketingAnalysisService.java | 87 ++++++++ 7 files changed, 570 insertions(+), 2 deletions(-) create mode 100644 FRONTEND_API_CHANGES_V2.md create mode 100644 src/main/java/kz/konturai/parser/model/MarketingAnalysisV2Document.java create mode 100644 src/main/java/kz/konturai/parser/repository/MarketingAnalysisV2Repository.java diff --git a/FRONTEND_API_CHANGES_V2.md b/FRONTEND_API_CHANGES_V2.md new file mode 100644 index 0000000..20db2f5 --- /dev/null +++ b/FRONTEND_API_CHANGES_V2.md @@ -0,0 +1,164 @@ +# Изменения в API: Интеграция v2 анализа + +## Обзор изменений + +Добавлена возможность опционального создания и сохранения v2 анализа вместе с обычным анализом. Все v2 анализы сохраняются в MongoDB и доступны через новые эндпойнты. + +--- + +## 1. POST `/api/marketing/analysis/start` - Обновлен + +### Новый query параметр: + +- **`generateV2`** (boolean, опционально, по умолчанию `false`) + - Если `true`, вместе с обычным анализом создается и сохраняется v2 анализ + - Оба анализа автоматически связываются между собой + +### Изменения в ответе: + +**Если `generateV2=false` (по умолчанию):** + +```json +{ + "success": true, + "message": "Анализ запущен успешно", + "data": { + "analysisId": "string", + "status": "processing", + "estimatedCompletionTime": "2024-01-01T12:00:00", + "message": "Комплексный анализ (...) запущен успешно..." + } +} +``` + +**Если `generateV2=true`:** + +```json +{ + "success": true, + "message": "Анализ запущен успешно", + "data": { + "analysisId": "string", + "status": "processing", + "estimatedCompletionTime": "2024-01-01T12:00:00", + "message": "Комплексный анализ (...) запущен успешно...", + "v2AnalysisId": "string" // ← НОВОЕ ПОЛЕ + } +} +``` + +### Пример использования: + +```javascript +// Создание обычного анализа +POST /api/marketing/analysis/start +Body: { ... } + +// Создание обычного анализа + v2 анализа +POST /api/marketing/analysis/start?generateV2=true +Body: { ... } +``` + +--- + +## 2. GET `/api/marketing/analysis/my` - Обновлен + +### Новое поле в ответе: + +Каждый элемент в списке теперь содержит опциональное поле `v2AnalysisId`: + +```json +{ + "success": true, + "data": [ + { + "analysisId": "string", + "businessNiche": "string", + "product": "string", + // ... другие поля ... + "v2AnalysisId": "string" // ← НОВОЕ ПОЛЕ (может быть null) + } + ] +} +``` + +**Примечание:** Поле `v2AnalysisId` будет присутствовать только если для данного анализа был создан связанный v2 анализ. + +--- + +## 3. GET `/api/marketing/analysis/v2/{id}` - Новый эндпойнт + +### Описание: + +Получение сохраненного v2 анализа по ID. + +### Параметры: + +- **Path:** `id` (string) - ID v2 анализа +- **Header:** `Authorization` (JWT токен) + +### Ответ: + +**Успешный ответ (200):** + +```json +{ + "success": true, + "data": { + "reportTitle": "string", + "sections": { + "marketOverview": { ... }, + "targetAudience": { ... }, + "competitiveEnvironment": { ... }, + // ... другие секции ... + } + } +} +``` + +**Ошибки:** + +- `401` - Не авторизован +- `404` - Анализ не найден +- `403` - Нет доступа к этому анализу + +### Пример использования: + +```javascript +GET /api/marketing/analysis/v2/507f1f77bcf86cd799439011 +Authorization: Bearer +``` + +--- + +## 4. POST `/api/marketing/analysis/start/v2` - Без изменений + +Эндпойнт работает как прежде - создает только v2 анализ без связи с обычным анализом. + +--- + +## Рекомендации для фронтенда + +1. **При создании анализа:** + + - Используйте `generateV2=true` если нужен v2 анализ сразу + - Сохраняйте `v2AnalysisId` из ответа для последующего доступа + +2. **При отображении списка анализов:** + + - Проверяйте наличие поля `v2AnalysisId` + - Если поле присутствует, можно показать кнопку/ссылку для просмотра v2 анализа + +3. **При получении v2 анализа:** + - Используйте `GET /api/marketing/analysis/v2/{id}` для получения полных данных + - Обрабатывайте случаи, когда v2 анализ может быть не найден (404) + +--- + +## Обратная совместимость + +✅ Все изменения обратно совместимы: + +- Существующие запросы без `generateV2` работают как прежде +- Поле `v2AnalysisId` опционально и не ломает существующий код +- Новые эндпойнты не влияют на старую функциональность diff --git a/src/main/java/kz/konturai/parser/controller/MarketingController.java b/src/main/java/kz/konturai/parser/controller/MarketingController.java index 6afb193..3fad309 100644 --- a/src/main/java/kz/konturai/parser/controller/MarketingController.java +++ b/src/main/java/kz/konturai/parser/controller/MarketingController.java @@ -2,6 +2,7 @@ package kz.konturai.parser.controller; import kz.konturai.parser.dto.*; import kz.konturai.parser.model.MarketingAnalysis; +import kz.konturai.parser.model.MarketingAnalysisV2Document; import kz.konturai.parser.model.MarketingStrategy; import kz.konturai.parser.model.PostingTask; import kz.konturai.parser.service.JwtService; @@ -78,6 +79,7 @@ public class MarketingController { @PostMapping("/start") public ResponseEntity startAnalysis( @RequestHeader(value = "Authorization", required = false) String authHeader, + @RequestParam(value = "generateV2", defaultValue = "false") boolean generateV2, @Valid @RequestBody MarketingAnalysisRequest request) { String userId = extractUserIdFromHeader(authHeader); @@ -89,6 +91,24 @@ public class MarketingController { List analyses = marketingAnalysisService.startAnalysis(request, userId); MarketingAnalysis analysis = analyses.get(0); + // Опционально создаем v2 анализ и связываем его + String v2AnalysisId = null; + if (generateV2) { + try { + v2AnalysisId = marketingAnalysisService.generateAndSaveAnalysisV2(request, userId); + // Связываем обычный анализ с v2 + analysis.setV2AnalysisId(v2AnalysisId); + marketingAnalysisService.updateAnalysis(analysis); + // Связываем v2 анализ с обычным + marketingAnalysisService.linkV2AnalysisToAnalysis(v2AnalysisId, analysis.getId()); + logger.info("Created and linked v2 analysis {} to analysis {}", v2AnalysisId, + analysis.getId()); + } catch (Exception e) { + logger.error("Error creating v2 analysis: {}", e.getMessage(), e); + // Продолжаем выполнение даже если v2 анализ не создался + } + } + // Start async processing marketingAnalysisService.processAnalysis(analysis.getId(), request); @@ -107,6 +127,18 @@ public class MarketingController { estimatedCompletion, "Комплексный анализ (" + analysisTypesStr + ") запущен успешно. Результаты будут готовы в течение 5-10 минут."); + + // Добавляем v2 id в ответ, если он был создан + if (v2AnalysisId != null) { + Map responseData = new HashMap<>(); + responseData.put("analysisId", response.getAnalysisId()); + responseData.put("status", response.getStatus()); + responseData.put("estimatedCompletionTime", response.getEstimatedCompletionTime()); + responseData.put("message", response.getMessage()); + responseData.put("v2AnalysisId", v2AnalysisId); + return ResponseEntity.ok(ApiResponse.success("Анализ запущен успешно", responseData)); + } + return ResponseEntity.ok(ApiResponse.success("Анализ запущен успешно", response)); } @@ -120,8 +152,53 @@ public class MarketingController { return unauthorizedResponse(); } - MarketingAnalysisResponseV2 response = marketingAnalysisService.generateAnalysisV2(request); - return ResponseEntity.ok(response); + try { + String analysisId = marketingAnalysisService.generateAndSaveAnalysisV2(request, userId); + Map response = new HashMap<>(); + response.put("id", analysisId); + response.put("message", "Анализ успешно создан и сохранен"); + return ResponseEntity.ok(ApiResponse.success("Анализ создан", response)); + } catch (Exception e) { + logger.error("Error creating analysis v2: {}", e.getMessage(), e); + ErrorResponse error = new ErrorResponse( + "INTERNAL_SERVER_ERROR", + "Произошла ошибка при создании анализа"); + return ResponseEntity.status(500) + .body(ApiResponse.error("Внутренняя ошибка сервера", error)); + } + } + + @GetMapping("/v2/{id}") + public ResponseEntity getAnalysisV2( + @RequestHeader(value = "Authorization", required = false) String authHeader, + @PathVariable String id) { + + String userId = extractUserIdFromHeader(authHeader); + if (userId == null) { + return unauthorizedResponse(); + } + + Optional optAnalysis = marketingAnalysisService + .getAnalysisV2ById(id); + + if (optAnalysis.isEmpty()) { + ErrorResponse error = new ErrorResponse( + "NOT_FOUND", + "Анализ с указанным ID не найден"); + return ResponseEntity.status(404) + .body(ApiResponse.error("Анализ не найден", error)); + } + + MarketingAnalysisV2Document analysis = optAnalysis.get(); + if (!userId.equals(analysis.getUserId())) { + ErrorResponse error = new ErrorResponse( + "FORBIDDEN", + "У вас нет доступа к этому анализу"); + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(ApiResponse.error("Доступ запрещен", error)); + } + + return ResponseEntity.ok(ApiResponse.success(analysis.getAnalysisData())); } @GetMapping("/{analysisId}") @@ -561,6 +638,22 @@ public class MarketingController { response.setCreatedAt(analysis.getCreatedAt()); response.setCompletedAt(analysis.getCompletedAt()); response.setStatusHistory(analysis.getStatusHistory()); + + // Ищем связанный v2 анализ + String v2AnalysisId = analysis.getV2AnalysisId(); + if (v2AnalysisId == null || v2AnalysisId.isEmpty()) { + // Если v2AnalysisId не установлен, пытаемся найти по analysisId + Optional optV2 = marketingAnalysisService + .getV2AnalysisByAnalysisId(analysis.getId()); + if (optV2.isPresent()) { + v2AnalysisId = optV2.get().getId(); + // Обновляем анализ для сохранения связи + analysis.setV2AnalysisId(v2AnalysisId); + marketingAnalysisService.updateAnalysis(analysis); + } + } + response.setV2AnalysisId(v2AnalysisId); + return response; } diff --git a/src/main/java/kz/konturai/parser/dto/AnalysisHistoryResponse.java b/src/main/java/kz/konturai/parser/dto/AnalysisHistoryResponse.java index 80bbc40..140b1f9 100644 --- a/src/main/java/kz/konturai/parser/dto/AnalysisHistoryResponse.java +++ b/src/main/java/kz/konturai/parser/dto/AnalysisHistoryResponse.java @@ -19,6 +19,7 @@ public class AnalysisHistoryResponse { private LocalDateTime createdAt; private LocalDateTime completedAt; private List statusHistory; + private String v2AnalysisId; // ID связанного v2 анализа public AnalysisHistoryResponse() { } @@ -142,4 +143,12 @@ public class AnalysisHistoryResponse { public void setAnalysisType(String analysisType) { this.analysisType = analysisType; } + + public String getV2AnalysisId() { + return v2AnalysisId; + } + + public void setV2AnalysisId(String v2AnalysisId) { + this.v2AnalysisId = v2AnalysisId; + } } diff --git a/src/main/java/kz/konturai/parser/model/MarketingAnalysis.java b/src/main/java/kz/konturai/parser/model/MarketingAnalysis.java index 236a700..d487497 100644 --- a/src/main/java/kz/konturai/parser/model/MarketingAnalysis.java +++ b/src/main/java/kz/konturai/parser/model/MarketingAnalysis.java @@ -67,6 +67,9 @@ public class MarketingAnalysis { @Field("status_history") private List statusHistory; + @Field("v2_analysis_id") + private String v2AnalysisId; // ID связанного v2 анализа + public MarketingAnalysis() { this.createdAt = LocalDateTime.now(); this.status = "queued"; @@ -231,4 +234,12 @@ public class MarketingAnalysis { public void setAnalysisType(List analysisType) { this.analysisType = analysisType; } + + public String getV2AnalysisId() { + return v2AnalysisId; + } + + public void setV2AnalysisId(String v2AnalysisId) { + this.v2AnalysisId = v2AnalysisId; + } } diff --git a/src/main/java/kz/konturai/parser/model/MarketingAnalysisV2Document.java b/src/main/java/kz/konturai/parser/model/MarketingAnalysisV2Document.java new file mode 100644 index 0000000..5f45713 --- /dev/null +++ b/src/main/java/kz/konturai/parser/model/MarketingAnalysisV2Document.java @@ -0,0 +1,185 @@ +package kz.konturai.parser.model; + +import kz.konturai.parser.dto.MarketingAnalysisResponseV2; +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.List; +import java.util.Map; + +@Document(collection = "marketing_analysis_v2") +public class MarketingAnalysisV2Document { + + @Id + private String id; + + @Field("user_id") + private String userId; + + @Field("business_niche") + private String businessNiche; + + @Field("product") + private String product; + + @Field("target_audience") + private Map targetAudience; + + @Field("region") + private List region; + + @Field("goal") + private String goal; + + @Field("detail_level") + private String detailLevel; + + @Field("strong_side") + private String strongSide; + + @Field("weak_side") + private String weakSide; + + @Field("analysis_type") + private List analysisType; + + @Field("analysis_data") + private MarketingAnalysisResponseV2 analysisData; + + @Field("created_at") + private LocalDateTime createdAt; + + @Field("updated_at") + private LocalDateTime updatedAt; + + @Field("analysis_id") + private String analysisId; // ID связанного обычного анализа + + public MarketingAnalysisV2Document() { + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getBusinessNiche() { + return businessNiche; + } + + public void setBusinessNiche(String businessNiche) { + this.businessNiche = businessNiche; + } + + public String getProduct() { + return product; + } + + public void setProduct(String product) { + this.product = product; + } + + public Map getTargetAudience() { + return targetAudience; + } + + public void setTargetAudience(Map targetAudience) { + this.targetAudience = targetAudience; + } + + public List getRegion() { + return region; + } + + public void setRegion(List region) { + this.region = region; + } + + public String getGoal() { + return goal; + } + + public void setGoal(String goal) { + this.goal = goal; + } + + public String getDetailLevel() { + return detailLevel; + } + + public void setDetailLevel(String detailLevel) { + this.detailLevel = detailLevel; + } + + public String getStrongSide() { + return strongSide; + } + + public void setStrongSide(String strongSide) { + this.strongSide = strongSide; + } + + public String getWeakSide() { + return weakSide; + } + + public void setWeakSide(String weakSide) { + this.weakSide = weakSide; + } + + public List getAnalysisType() { + return analysisType; + } + + public void setAnalysisType(List analysisType) { + this.analysisType = analysisType; + } + + public MarketingAnalysisResponseV2 getAnalysisData() { + return analysisData; + } + + public void setAnalysisData(MarketingAnalysisResponseV2 analysisData) { + this.analysisData = analysisData; + this.updatedAt = LocalDateTime.now(); + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + public String getAnalysisId() { + return analysisId; + } + + public void setAnalysisId(String analysisId) { + this.analysisId = analysisId; + } +} diff --git a/src/main/java/kz/konturai/parser/repository/MarketingAnalysisV2Repository.java b/src/main/java/kz/konturai/parser/repository/MarketingAnalysisV2Repository.java new file mode 100644 index 0000000..0050124 --- /dev/null +++ b/src/main/java/kz/konturai/parser/repository/MarketingAnalysisV2Repository.java @@ -0,0 +1,19 @@ +package kz.konturai.parser.repository; + +import kz.konturai.parser.model.MarketingAnalysisV2Document; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface MarketingAnalysisV2Repository extends MongoRepository { + Optional findById(String id); + + List findByUserIdOrderByCreatedAtDesc(String userId); + + Optional findByIdAndUserId(String id, String userId); + + Optional findByAnalysisId(String analysisId); +} diff --git a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java index 9a2cedf..c366522 100644 --- a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java +++ b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java @@ -9,7 +9,9 @@ import kz.konturai.parser.dto.StatusHistoryEntry; import kz.konturai.parser.model.AnalysisType; import kz.konturai.parser.model.DetailLevel; import kz.konturai.parser.model.MarketingAnalysis; +import kz.konturai.parser.model.MarketingAnalysisV2Document; import kz.konturai.parser.repository.MarketingAnalysisRepository; +import kz.konturai.parser.repository.MarketingAnalysisV2Repository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; @@ -392,6 +394,7 @@ public class MarketingAnalysisService { private static final long RETRY_DELAY_MS = 1000; private final MarketingAnalysisRepository repository; + private final MarketingAnalysisV2Repository v2Repository; private final OpenAIAnalyticsService openAIAnalyticsService; private final MinIOService minIOService; private final ResearchPdfService researchPdfService; @@ -411,11 +414,13 @@ public class MarketingAnalysisService { public MarketingAnalysisService( MarketingAnalysisRepository repository, + MarketingAnalysisV2Repository v2Repository, OpenAIAnalyticsService openAIAnalyticsService, MinIOService minIOService, ResearchPdfService researchPdfService, SerperSearchService serperSearchService) { this.repository = repository; + this.v2Repository = v2Repository; this.openAIAnalyticsService = openAIAnalyticsService; this.minIOService = minIOService; this.researchPdfService = researchPdfService; @@ -703,6 +708,88 @@ public class MarketingAnalysisService { } } + /** + * Генерирует анализ v2 и сохраняет его в MongoDB + * + * @param request Запрос на анализ + * @param userId ID пользователя + * @return ID сохраненного анализа + */ + public String generateAndSaveAnalysisV2(MarketingAnalysisRequest request, String userId) { + try { + // Генерируем анализ + MarketingAnalysisResponseV2 analysisData = generateAnalysisV2(request); + + // Создаем документ для сохранения + MarketingAnalysisV2Document document = new MarketingAnalysisV2Document(); + document.setUserId(userId); + document.setBusinessNiche(request.getBusinessNiche()); + document.setProduct(request.getProduct()); + document.setTargetAudience(request.getTargetAudience()); + document.setRegion(request.getRegion()); + document.setGoal(request.getGoal()); + document.setDetailLevel(request.getDetailLevel()); + document.setStrongSide(request.getStrongSide()); + document.setWeakSide(request.getWeakSide()); + document.setAnalysisType(request.getAnalysisType()); + document.setAnalysisData(analysisData); + + // Сохраняем в MongoDB + MarketingAnalysisV2Document saved = v2Repository.save(document); + logger.info("Saved marketing analysis v2 with id: {}", saved.getId()); + return saved.getId(); + } catch (Exception e) { + logger.error("generateAndSaveAnalysisV2: Failed to generate and save analysis: {}", e.getMessage(), e); + throw new RuntimeException("Не удалось сохранить marketing analysis v2", e); + } + } + + /** + * Получает сохраненный анализ v2 по ID + * + * @param id ID анализа + * @return Документ анализа или null, если не найден + */ + public Optional getAnalysisV2ById(String id) { + return v2Repository.findById(id); + } + + /** + * Обновляет анализ в базе данных + * + * @param analysis Анализ для обновления + * @return Обновленный анализ + */ + public MarketingAnalysis updateAnalysis(MarketingAnalysis analysis) { + return repository.save(analysis); + } + + /** + * Связывает v2 анализ с обычным анализом + * + * @param v2AnalysisId ID v2 анализа + * @param analysisId ID обычного анализа + */ + public void linkV2AnalysisToAnalysis(String v2AnalysisId, String analysisId) { + Optional optV2 = v2Repository.findById(v2AnalysisId); + if (optV2.isPresent()) { + MarketingAnalysisV2Document v2Doc = optV2.get(); + v2Doc.setAnalysisId(analysisId); + v2Repository.save(v2Doc); + logger.info("Linked v2 analysis {} to analysis {}", v2AnalysisId, analysisId); + } + } + + /** + * Получает v2 анализ по ID обычного анализа + * + * @param analysisId ID обычного анализа + * @return Документ v2 анализа или null, если не найден + */ + public Optional getV2AnalysisByAnalysisId(String analysisId) { + return v2Repository.findByAnalysisId(analysisId); + } + /** * Example payload used ONLY for prompting the AI with the expected JSON * schema/shape.