diff --git a/src/main/java/kz/konturai/parser/config/MongoConfig.java b/src/main/java/kz/konturai/parser/config/MongoConfig.java new file mode 100644 index 0000000..69c0b6d --- /dev/null +++ b/src/main/java/kz/konturai/parser/config/MongoConfig.java @@ -0,0 +1,115 @@ +package kz.konturai.parser.config; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.bson.Document; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.ReadingConverter; +import org.springframework.data.convert.WritingConverter; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Configuration +public class MongoConfig { + + private static final Logger logger = LoggerFactory.getLogger(MongoConfig.class); + private static final ObjectMapper objectMapper = new ObjectMapper(); + + @Bean + public MongoCustomConversions customConversions() { + List> converters = new ArrayList<>(); + converters.add(new StringToMapConverter()); + converters.add(new DocumentToMapConverter()); + converters.add(new MapToStringConverter()); + return new MongoCustomConversions(converters); + } + + /** + * Конвертер для чтения: String -> Map + * Обрабатывает случай, когда target_audience хранится как строка в MongoDB + */ + @ReadingConverter + public static class StringToMapConverter implements Converter> { + @Override + public Map convert(String source) { + logger.debug("StringToMapConverter: Converting string to Map: {}", source); + if (source == null || source.trim().isEmpty()) { + logger.debug("StringToMapConverter: Source is null or empty, returning empty Map"); + return new HashMap<>(); + } + + try { + // Если это уже JSON строка, парсим её + if (source.trim().startsWith("{") || source.trim().startsWith("[")) { + Map result = objectMapper.readValue(source, + new TypeReference>() { + }); + logger.debug("StringToMapConverter: Successfully parsed JSON string to Map"); + return result; + } + // Если это простая строка, создаём пустой Map + logger.warn("StringToMapConverter: target_audience is a plain string, not JSON: {}", source); + return new HashMap<>(); + } catch (Exception e) { + logger.error("StringToMapConverter: Error converting string to Map for target_audience: {}", source, e); + return new HashMap<>(); + } + } + } + + /** + * Конвертер для чтения: Document -> Map + * Обрабатывает случай, когда MongoDB возвращает Document вместо Map + */ + @ReadingConverter + public static class DocumentToMapConverter implements Converter> { + @Override + public Map convert(Document source) { + logger.debug("DocumentToMapConverter: Converting Document to Map"); + if (source == null) { + logger.debug("DocumentToMapConverter: Source is null, returning empty Map"); + return new HashMap<>(); + } + Map result = new HashMap<>(); + for (String key : source.keySet()) { + Object value = source.get(key); + // Рекурсивно конвертируем вложенные Document + if (value instanceof Document) { + result.put(key, convert((Document) value)); + } else { + result.put(key, value); + } + } + logger.debug("DocumentToMapConverter: Successfully converted Document to Map with {} keys", result.size()); + return result; + } + } + + /** + * Конвертер для записи: Map -> String + * Не используется, но может быть полезен для обратной совместимости + */ + @WritingConverter + public static class MapToStringConverter implements Converter, String> { + @Override + public String convert(Map source) { + if (source == null || source.isEmpty()) { + return null; + } + try { + return objectMapper.writeValueAsString(source); + } catch (Exception e) { + logger.error("Error converting Map to String: {}", e.getMessage(), e); + return null; + } + } + } +} diff --git a/src/main/java/kz/konturai/parser/controller/MarketingController.java b/src/main/java/kz/konturai/parser/controller/MarketingController.java index 509982c..bcc39bb 100644 --- a/src/main/java/kz/konturai/parser/controller/MarketingController.java +++ b/src/main/java/kz/konturai/parser/controller/MarketingController.java @@ -15,6 +15,8 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import jakarta.validation.Valid; import java.io.InputStream; @@ -29,6 +31,8 @@ import java.util.stream.Collectors; @RequestMapping("/api/marketing/analysis") public class MarketingController { + private static final Logger logger = LoggerFactory.getLogger(MarketingController.class); + private final MarketingAnalysisService marketingAnalysisService; private final MarketingStrategyService marketingStrategyService; private final MinIOService minIOService; @@ -50,9 +54,17 @@ public class MarketingController { private String extractUserIdFromHeader(String authHeader) { if (authHeader == null || authHeader.isEmpty()) { + logger.debug("Authorization header is null or empty"); + return null; + } + try { + String userId = jwtService.extractUserIdFromHeader(authHeader); + logger.debug("Extracted userId from header: {}", userId); + return userId; + } catch (Exception e) { + logger.error("Error extracting userId from header: {}", e.getMessage(), e); return null; } - return jwtService.extractUserIdFromHeader(authHeader); } private ResponseEntity unauthorizedResponse() { @@ -91,10 +103,14 @@ public class MarketingController { analysis.getId(), "processing", estimatedCompletion, - "Анализ типа '" + - (analysis.getAnalysisType() != null && !analysis.getAnalysisType().isEmpty() - ? analysis.getAnalysisType().get(0) : "неизвестно") + - "' запущен успешно. Результаты будут готовы в течение 5-10 минут.")) + "Анализ типа '" + + (analysis.getAnalysisType() != null && !analysis + .getAnalysisType().isEmpty() + ? analysis.getAnalysisType() + .get(0) + : "неизвестно") + + + "' запущен успешно. Результаты будут готовы в течение 5-10 минут.")) .collect(Collectors.toList()); return ResponseEntity.ok(ApiResponse.success( "Создано анализов: " + analyses.size() + ". Все анализы запущены успешно.", @@ -327,17 +343,41 @@ public class MarketingController { public ResponseEntity getMyAnalyses( @RequestHeader(value = "Authorization", required = false) String authHeader) { - String userId = extractUserIdFromHeader(authHeader); - if (userId == null) { - return unauthorizedResponse(); + try { + logger.info("Getting analyses for user"); + String userId = extractUserIdFromHeader(authHeader); + if (userId == null) { + logger.warn("Unauthorized request to /my endpoint"); + return unauthorizedResponse(); + } + + logger.debug("Fetching analyses for userId: {}", userId); + List analyses = marketingAnalysisService.getUserAnalyses(userId); + logger.debug("Found {} analyses for userId: {}", analyses.size(), userId); + + List responseList = analyses.stream() + .map(analysis -> { + try { + return convertToHistoryResponse(analysis); + } catch (Exception e) { + logger.error("Error converting analysis {} to history response: {}", + analysis != null ? analysis.getId() : "null", + e.getMessage(), e); + throw e; + } + }) + .collect(Collectors.toList()); + + logger.info("Successfully retrieved {} analyses for userId: {}", responseList.size(), userId); + return ResponseEntity.ok(ApiResponse.success(responseList)); + } catch (Exception e) { + logger.error("Error in getMyAnalyses endpoint: {}", e.getMessage(), e); + ErrorResponse error = new ErrorResponse( + "INTERNAL_SERVER_ERROR", + "Произошла внутренняя ошибка сервера. Попробуйте позже."); + return ResponseEntity.status(500) + .body(ApiResponse.error("Внутренняя ошибка сервера", error)); } - - List analyses = marketingAnalysisService.getUserAnalyses(userId); - List responseList = analyses.stream() - .map(this::convertToHistoryResponse) - .collect(Collectors.toList()); - - return ResponseEntity.ok(ApiResponse.success(responseList)); } @GetMapping("/strategy/my") @@ -422,11 +462,16 @@ public class MarketingController { } private AnalysisHistoryResponse convertToHistoryResponse(MarketingAnalysis analysis) { + if (analysis == null) { + logger.error("Attempted to convert null analysis to history response"); + throw new IllegalArgumentException("Analysis cannot be null"); + } + AnalysisHistoryResponse response = new AnalysisHistoryResponse(); response.setAnalysisId(analysis.getId()); response.setBusinessNiche(analysis.getBusinessNiche()); response.setProduct(analysis.getProduct()); - + // Convert targetAudience Map to String representation Map targetAudienceMap = analysis.getTargetAudience(); if (targetAudienceMap != null) { @@ -435,14 +480,16 @@ public class MarketingController { @SuppressWarnings("unchecked") List genders = (List) targetAudienceMap.get("genders"); if (genders != null && !genders.isEmpty()) { - audienceBuilder.append("Гендер: ").append(String.join(", ", genders)).append(". "); + audienceBuilder.append("Гендер: ").append(String.join(", ", genders)) + .append(". "); } } if (targetAudienceMap.containsKey("ageRanges")) { @SuppressWarnings("unchecked") List ageRanges = (List) targetAudienceMap.get("ageRanges"); if (ageRanges != null && !ageRanges.isEmpty()) { - audienceBuilder.append("Возраст: ").append(String.join(", ", ageRanges)).append(". "); + audienceBuilder.append("Возраст: ").append(String.join(", ", ageRanges)) + .append(". "); } } if (targetAudienceMap.containsKey("types")) { @@ -454,24 +501,24 @@ public class MarketingController { } response.setTargetAudience(audienceBuilder.toString().trim()); } - + // Convert region List to String List 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()); - + // Convert analysisType List to String List 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()); @@ -593,6 +640,7 @@ public class MarketingController { @ExceptionHandler(Exception.class) public ResponseEntity> handleGenericException(Exception e) { + logger.error("Unhandled exception in MarketingController: {}", e.getMessage(), e); ErrorResponse errorResponse = new ErrorResponse( "INTERNAL_SERVER_ERROR", "Произошла внутренняя ошибка сервера. Попробуйте позже."); diff --git a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java index b56d8a5..d020fca 100644 --- a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java +++ b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java @@ -91,7 +91,8 @@ 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)); + MarketingAnalysisRequest singleTypeRequest = createSingleTypeRequest(request, + analysis.getAnalysisType().get(0)); // Generate marketing report Map reportData = generateMarketingReport(singleTypeRequest); @@ -136,7 +137,8 @@ public class MarketingAnalysisService { } } - private MarketingAnalysisRequest createSingleTypeRequest(MarketingAnalysisRequest originalRequest, String analysisType) { + private MarketingAnalysisRequest createSingleTypeRequest(MarketingAnalysisRequest originalRequest, + String analysisType) { MarketingAnalysisRequest singleTypeRequest = new MarketingAnalysisRequest(); singleTypeRequest.setBusinessNiche(originalRequest.getBusinessNiche()); singleTypeRequest.setProduct(originalRequest.getProduct()); @@ -164,7 +166,7 @@ public class MarketingAnalysisService { StringBuilder contextBuilder = new StringBuilder(); contextBuilder.append("Ниша бизнеса: ").append(request.getBusinessNiche()).append("\n"); contextBuilder.append("Продукт/услуга: ").append(request.getProduct()).append("\n"); - + // Format target audience Map targetAudienceData = request.getTargetAudience(); if (targetAudienceData != null) { @@ -192,7 +194,7 @@ public class MarketingAnalysisService { } contextBuilder.append("Целевая аудитория: ").append(audienceBuilder.toString().trim()).append("\n"); } - + // Format regions List regions = request.getRegion(); if (regions != null && !regions.isEmpty()) { @@ -454,7 +456,7 @@ public class MarketingAnalysisService { markdown.append("## Информация о бизнесе\n\n"); markdown.append("- **Ниша бизнеса:** ").append(request.getBusinessNiche()).append("\n"); markdown.append("- **Продукт/услуга:** ").append(request.getProduct()).append("\n"); - + // Format target audience Map targetAudienceData = request.getTargetAudience(); if (targetAudienceData != null) { @@ -482,13 +484,13 @@ public class MarketingAnalysisService { } markdown.append("- **Целевая аудитория:** ").append(audienceBuilder.toString().trim()).append("\n"); } - + // Format regions List 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"); @@ -595,7 +597,15 @@ public class MarketingAnalysisService { } public List getUserAnalyses(String userId) { - return repository.findByUserIdOrderByCreatedAtDesc(userId); + logger.debug("Fetching analyses for userId: {}", userId); + try { + List analyses = repository.findByUserIdOrderByCreatedAtDesc(userId); + logger.debug("Found {} analyses for userId: {}", analyses.size(), userId); + return analyses; + } catch (Exception e) { + logger.error("Error fetching analyses for userId {}: {}", userId, e.getMessage(), e); + throw e; + } } private MarketingAnalysisResult.MarketingReport buildReportFromData(Map reportData, diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 992c688..733a5be 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -53,6 +53,10 @@ logging.level.org.springframework.data.mongodb=INFO logging.level.com.mongodb=WARN logging.level.kz.konturai.parser.service.OllamaAnalyticsService=INFO +logging.level.kz.konturai.parser.controller.MarketingController=DEBUG +logging.level.kz.konturai.parser.service.MarketingAnalysisService=DEBUG +logging.level.kz.konturai.parser.config.MongoConfig=DEBUG +logging.level.org.springframework.data.mongodb.core.convert=DEBUG # OpenAI Configuration openai.api.key=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