This commit is contained in:
root
2026-01-03 12:02:28 +05:00
parent 2c5d15fb33
commit f61de9acd1
7 changed files with 570 additions and 2 deletions
+164
View File
@@ -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 <token>
```
---
## 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` опционально и не ломает существующий код
- Новые эндпойнты не влияют на старую функциональность
@@ -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<MarketingAnalysis> 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<String, Object> 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<String, Object> 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<MarketingAnalysisV2Document> 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<MarketingAnalysisV2Document> optV2 = marketingAnalysisService
.getV2AnalysisByAnalysisId(analysis.getId());
if (optV2.isPresent()) {
v2AnalysisId = optV2.get().getId();
// Обновляем анализ для сохранения связи
analysis.setV2AnalysisId(v2AnalysisId);
marketingAnalysisService.updateAnalysis(analysis);
}
}
response.setV2AnalysisId(v2AnalysisId);
return response;
}
@@ -19,6 +19,7 @@ public class AnalysisHistoryResponse {
private LocalDateTime createdAt;
private LocalDateTime completedAt;
private List<StatusHistoryEntry> 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;
}
}
@@ -67,6 +67,9 @@ public class MarketingAnalysis {
@Field("status_history")
private List<StatusHistoryEntry> 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<String> analysisType) {
this.analysisType = analysisType;
}
public String getV2AnalysisId() {
return v2AnalysisId;
}
public void setV2AnalysisId(String v2AnalysisId) {
this.v2AnalysisId = v2AnalysisId;
}
}
@@ -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<String, Object> targetAudience;
@Field("region")
private List<String> 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<String> 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<String, Object> getTargetAudience() {
return targetAudience;
}
public void setTargetAudience(Map<String, Object> targetAudience) {
this.targetAudience = targetAudience;
}
public List<String> getRegion() {
return region;
}
public void setRegion(List<String> 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<String> getAnalysisType() {
return analysisType;
}
public void setAnalysisType(List<String> 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;
}
}
@@ -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<MarketingAnalysisV2Document, String> {
Optional<MarketingAnalysisV2Document> findById(String id);
List<MarketingAnalysisV2Document> findByUserIdOrderByCreatedAtDesc(String userId);
Optional<MarketingAnalysisV2Document> findByIdAndUserId(String id, String userId);
Optional<MarketingAnalysisV2Document> findByAnalysisId(String analysisId);
}
@@ -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<MarketingAnalysisV2Document> 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<MarketingAnalysisV2Document> 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<MarketingAnalysisV2Document> getV2AnalysisByAnalysisId(String analysisId) {
return v2Repository.findByAnalysisId(analysisId);
}
/**
* Example payload used ONLY for prompting the AI with the expected JSON
* schema/shape.