diff --git a/FRONTEND_API_GUIDE.md b/FRONTEND_API_GUIDE.md new file mode 100644 index 0000000..d8e0492 --- /dev/null +++ b/FRONTEND_API_GUIDE.md @@ -0,0 +1,340 @@ +# Руководство по API для фронтенда + +## Обзор + +После рефакторинга ParserController теперь поддерживает эффективную работу с фронтендом через строго типизированные DTO и расширенные возможности фильтрации и пагинации. + +## Основные изменения + +### 1. Строгая типизация ответов + +Все ответы API теперь используют DTO классы вместо `Map`: + +- `ApiResponse` - универсальный wrapper для всех ответов +- `ParserResultDto` - результат парсинга +- `ParserStatsDto` - статистика системы +- `HealthCheckDto` - состояние сервиса + +### 2. Пагинация и сортировка + +Поддержка Spring Data пагинации с параметрами: + +- `page` - номер страницы (начиная с 0) +- `size` - количество элементов на странице +- `sort` - поле для сортировки и направление + +### 3. Фильтрация данных + +Возможность фильтрации по: + +- `sourceName` - источнику новостей +- `startDate` - начальной дате +- `endDate` - конечной дате + +## API Endpoints + +### 1. Получение записей с фильтрацией и пагинацией + +```http +GET /api/parser/items?page=0&size=10&sort=publishedAt,desc&sourceName=Kursiv&startDate=2025-01-01&endDate=2025-01-31 +``` + +**Параметры:** + +- `page` (optional, default: 0) - номер страницы +- `size` (optional, default: 20) - размер страницы +- `sort` (optional, default: "publishedAt,desc") - сортировка +- `sourceName` (optional) - фильтр по источнику +- `startDate` (optional) - начальная дата (ISO format) +- `endDate` (optional) - конечная дата (ISO format) + +**Ответ:** + +```json +{ + "success": true, + "message": "Записи получены успешно", + "data": { + "content": [ + { + "id": "...", + "sourceName": "Kursiv (Бизнес/экономика)", + "title": "Заголовок новости", + "url": "https://kursiv.media/...", + "publishedAt": "2025-01-14T10:30:00", + "addedAt": "2025-01-14T12:00:00", + "rawText": "Текст статьи...", + "hash": "...", + "category": "Бизнес", + "analytics": { + "summary": null, + "sentiment": null, + "tags": [], + "entities": {} + } + } + ], + "pageable": { + "sort": { + "sorted": true, + "unsorted": false, + "empty": false + }, + "pageNumber": 0, + "pageSize": 10, + "offset": 0, + "paged": true, + "unpaged": false + }, + "totalElements": 25, + "totalPages": 3, + "last": false, + "first": true, + "numberOfElements": 10, + "size": 10, + "number": 0, + "sort": { + "sorted": true, + "unsorted": false, + "empty": false + }, + "empty": false + } +} +``` + +### 2. Статистика системы + +```http +GET /api/parser/stats +``` + +**Ответ:** + +```json +{ + "success": true, + "message": "Статистика получена успешно", + "data": { + "totalItems": 150, + "sourceStatistics": { + "Kursiv (Бизнес/экономика)": 75, + "Kapital.kz (Бизнес)": 75 + }, + "lastUpdate": "2025-01-14T12:00:00" + } +} +``` + +### 3. Запуск парсинга + +#### Парсинг всех источников + +```http +POST /api/parser/parse/all +``` + +**Ответ:** + +```json +{ + "success": true, + "message": "Парсинг всех источников завершен успешно", + "data": [ + { + "source": "Kursiv", + "processedItems": 15, + "totalItemsInDb": 90, + "status": "completed" + }, + { + "source": "Kapital", + "processedItems": 12, + "totalItemsInDb": 90, + "status": "completed" + } + ] +} +``` + +#### Парсинг конкретного источника + +```http +POST /api/parser/parse/kursiv +POST /api/parser/parse/kapital +``` + +### 4. Проверка состояния + +#### Общее состояние + +```http +GET /api/parser/health +``` + +**Ответ:** + +```json +{ + "success": true, + "message": "Сервис работает", + "data": { + "status": "UP", + "service": "RSS Parser System", + "timestamp": 1705123456789, + "scheduler": "Enabled - runs every 30 minutes" + } +} +``` + +#### Состояние MongoDB + +```http +GET /api/parser/health/mongodb +``` + +**Ответ при успехе:** + +```json +{ + "success": true, + "message": "MongoDB подключение успешно", + "data": { + "status": "UP", + "message": "MongoDB подключение успешно", + "timestamp": 1705123456789 + } +} +``` + +**Ответ при ошибке:** + +```json +{ + "success": false, + "message": "Ошибка подключения к MongoDB", + "data": { + "status": "DOWN", + "message": "Ошибка подключения к MongoDB: Command failed with error 13 (Unauthorized)", + "timestamp": 1705123456789, + "suggestion": "Проверьте учетные данные в application.properties" + } +} +``` + +## Примеры использования + +### 1. Получение последних новостей с пагинацией + +```javascript +// Получить первую страницу с 10 записями, отсортированными по дате публикации +fetch('/api/parser/items?page=0&size=10&sort=publishedAt,desc') + .then((response) => response.json()) + .then((data) => { + if (data.success) { + const items = data.data.content; + const totalPages = data.data.totalPages; + // Обработка данных + } + }); +``` + +### 2. Фильтрация по источнику + +```javascript +// Получить только новости от Kursiv +fetch('/api/parser/items?sourceName=Kursiv (Бизнес/экономика)&page=0&size=20') + .then((response) => response.json()) + .then((data) => { + if (data.success) { + const kursivNews = data.data.content; + // Обработка данных + } + }); +``` + +### 3. Фильтрация по датам + +```javascript +// Получить новости за последнюю неделю +const endDate = new Date().toISOString(); +const startDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(); + +fetch(`/api/parser/items?startDate=${startDate}&endDate=${endDate}`) + .then((response) => response.json()) + .then((data) => { + if (data.success) { + const recentNews = data.data.content; + // Обработка данных + } + }); +``` + +### 4. Комплексная фильтрация + +```javascript +// Получить новости Kapital за последний месяц, отсортированные по дате +const endDate = new Date().toISOString(); +const startDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(); + +fetch( + `/api/parser/items?sourceName=Kapital.kz (Бизнес)&startDate=${startDate}&endDate=${endDate}&sort=publishedAt,desc&page=0&size=50` +) + .then((response) => response.json()) + .then((data) => { + if (data.success) { + const kapitalNews = data.data.content; + const totalCount = data.data.totalElements; + // Обработка данных + } + }); +``` + +### 5. Получение статистики + +```javascript +fetch('/api/parser/stats') + .then((response) => response.json()) + .then((data) => { + if (data.success) { + const stats = data.data; + console.log(`Всего новостей: ${stats.totalItems}`); + console.log('По источникам:', stats.sourceStatistics); + } + }); +``` + +## Поддерживаемые форматы дат + +API поддерживает следующие форматы дат: + +- `yyyy-MM-dd` (например: 2025-01-14) +- `yyyy-MM-dd'T'HH:mm:ss` (например: 2025-01-14T10:30:00) +- `yyyy-MM-dd'T'HH:mm:ss.SSS` (например: 2025-01-14T10:30:00.000) +- `yyyy-MM-dd HH:mm:ss` (например: 2025-01-14 10:30:00) + +## Обработка ошибок + +Все ответы API следуют единому формату: + +```json +{ + "success": false, + "message": "Описание ошибки", + "error": "Детальная информация об ошибке" +} +``` + +HTTP статус коды: + +- `200` - Успешный запрос +- `500` - Внутренняя ошибка сервера +- `503` - Сервис недоступен (например, MongoDB) + +## Рекомендации для фронтенда + +1. **Кэширование**: Используйте кэширование для статистики и часто запрашиваемых данных +2. **Пагинация**: Реализуйте бесконечную прокрутку или традиционную пагинацию +3. **Фильтры**: Предоставьте пользователю удобные фильтры по источнику и датам +4. **Обновление**: Используйте WebSocket или polling для обновления данных в реальном времени +5. **Обработка ошибок**: Всегда проверяйте поле `success` в ответе diff --git a/MONGODB_TROUBLESHOOTING.md b/MONGODB_TROUBLESHOOTING.md new file mode 100644 index 0000000..0c76dbc --- /dev/null +++ b/MONGODB_TROUBLESHOOTING.md @@ -0,0 +1,142 @@ +# Решение проблем с подключением к MongoDB + +## Проблема: Ошибка аутентификации + +``` +Command failed with error 13 (Unauthorized): 'Command find requires authentication' +``` + +## Причина + +MongoDB сервер на `92.38.48.166:27017` требует аутентификацию, но в конфигурации не указаны учетные данные. + +## Решение + +### 1. Обновлена конфигурация MongoDB + +В файле `src/main/resources/application.properties` добавлены настройки аутентификации: + +```properties +# MongoDB Configuration +spring.data.mongodb.host=92.38.48.166 +spring.data.mongodb.port=27017 +spring.data.mongodb.database=parser_db +spring.data.mongodb.username=parser_user +spring.data.mongodb.password=parser_password +spring.data.mongodb.authentication-database=admin +``` + +### 2. Альтернативный способ подключения + +Если отдельные параметры не работают, используйте URI подключения: + +```properties +# Закомментируйте отдельные параметры и раскомментируйте URI +# spring.data.mongodb.host=92.38.48.166 +# spring.data.mongodb.port=27017 +# spring.data.mongodb.database=parser_db +# spring.data.mongodb.username=parser_user +# spring.data.mongodb.password=parser_password +# spring.data.mongodb.authentication-database=admin + +# Используйте URI подключения +spring.data.mongodb.uri=mongodb://parser_user:parser_password@92.38.48.166:27017/parser_db?authSource=admin +``` + +### 3. Проверка подключения + +#### Через API: + +```bash +curl http://localhost:8080/api/parser/health/mongodb +``` + +#### Через MongoDB клиент: + +```bash +mongo mongodb://parser_user:parser_password@92.38.48.166:27017/parser_db?authSource=admin +``` + +### 4. Возможные проблемы и решения + +#### Проблема: Неверные учетные данные + +**Решение:** Проверьте правильность username/password с администратором MongoDB + +#### Проблема: Пользователь не существует + +**Решение:** Создайте пользователя в MongoDB: + +```javascript +use admin +db.createUser({ + user: "parser_user", + pwd: "parser_password", + roles: [ + { role: "readWrite", db: "parser_db" } + ] +}) +``` + +#### Проблема: Пользователь не имеет прав + +**Решение:** Предоставьте права на базу данных: + +```javascript +use parser_db +db.grantRolesToUser("parser_user", ["readWrite"]) +``` + +#### Проблема: Firewall блокирует подключение + +**Решение:** Убедитесь, что порт 27017 открыт на сервере MongoDB + +### 5. Улучшения в коде + +#### Улучшенная обработка ошибок + +- Добавлена специфическая обработка ошибок аутентификации +- Более информативные сообщения об ошибках +- Graceful handling ошибок подключения + +#### Новый endpoint для диагностики + +- `GET /api/parser/health/mongodb` - проверка подключения к MongoDB +- Возвращает статус подключения и количество записей +- Предоставляет рекомендации при ошибках + +### 6. Тестирование + +1. **Запустите приложение:** + + ```bash + ./mvnw spring-boot:run + ``` + +2. **Проверьте подключение к MongoDB:** + + ```bash + curl http://localhost:8080/api/parser/health/mongodb + ``` + +3. **Если подключение успешно, запустите парсинг:** + ```bash + curl -X POST http://localhost:8080/api/parser/parse/all + ``` + +### 7. Логирование + +Уровень логирования MongoDB настроен для диагностики: + +```properties +logging.level.org.springframework.data.mongodb=INFO +logging.level.com.mongodb=WARN +``` + +### 8. Контакты + +Если проблема не решается, обратитесь к администратору MongoDB сервера для: + +- Проверки учетных данных +- Настройки прав доступа +- Проверки конфигурации сервера diff --git a/PARSER_README.md b/PARSER_README.md index 2069aa8..9ba237f 100644 --- a/PARSER_README.md +++ b/PARSER_README.md @@ -189,12 +189,40 @@ GET /api/parser/health ```json { "status": "UP", - "service": "Kursiv RSS Parser", + "service": "RSS Parser System", "timestamp": 1703123456789, "scheduler": "Enabled - runs every 30 minutes" } ``` +### 4.1. Проверка подключения к MongoDB + +```http +GET /api/parser/health/mongodb +``` + +**Ответ при успешном подключении:** + +```json +{ + "status": "UP", + "message": "MongoDB подключение успешно", + "totalItems": 37, + "timestamp": 1703123456789 +} +``` + +**Ответ при ошибке:** + +```json +{ + "status": "DOWN", + "message": "Ошибка подключения к MongoDB: Command failed with error 13 (Unauthorized)", + "suggestion": "Проверьте учетные данные в application.properties", + "timestamp": 1703123456789 +} +``` + ### 5. Информация о планировщике ```http @@ -298,6 +326,21 @@ logging.level.org.springframework.data.mongodb=DEBUG ## Возможные проблемы и решения +### MongoDB требует аутентификацию + +``` +Command failed with error 13 (Unauthorized): 'Command find requires authentication' +``` + +**Решение:** + +1. Проверьте учетные данные в `application.properties` +2. Убедитесь, что пользователь `parser_user` существует в MongoDB +3. Проверьте права доступа пользователя к базе данных `parser_db` +4. Используйте endpoint `GET /api/parser/health/mongodb` для диагностики + +**Подробное руководство:** См. файл `MONGODB_TROUBLESHOOTING.md` + ### MongoDB не запущен ``` @@ -312,7 +355,7 @@ Error: Could not connect to MongoDB Error: Could not fetch RSS feed ``` -**Решение:** Проверьте доступность URL `https://kursiv.media/feed/` и интернет-соединение. +**Решение:** Проверьте доступность URL RSS-лент и интернет-соединение. ### Проблемы с кодировкой diff --git a/src/main/java/kz/konturai/parser/controller/ParserController.java b/src/main/java/kz/konturai/parser/controller/ParserController.java index 7b5a858..fd86d76 100644 --- a/src/main/java/kz/konturai/parser/controller/ParserController.java +++ b/src/main/java/kz/konturai/parser/controller/ParserController.java @@ -1,15 +1,20 @@ package kz.konturai.parser.controller; +import kz.konturai.parser.dto.*; import kz.konturai.parser.model.MarketItem; import kz.konturai.parser.service.KursivParserService; import kz.konturai.parser.service.KapitalParserService; +import kz.konturai.parser.service.MarketItemService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.util.HashMap; +import java.time.LocalDateTime; import java.util.List; -import java.util.Map; @RestController @RequestMapping("/api/parser") @@ -22,31 +27,29 @@ public class ParserController { @Autowired private KapitalParserService kapitalParserService; + @Autowired + private MarketItemService marketItemService; + /** * Запуск парсинга RSS-ленты Kursiv */ @PostMapping("/parse/kursiv") - public ResponseEntity> parseKursivRssFeed() { - Map response = new HashMap<>(); - + public ResponseEntity> parseKursivRssFeed() { try { List savedItems = kursivParserService.parseAndSaveRssFeed(); + long totalItems = marketItemService.getTotalItemsCount(); - response.put("success", true); - response.put("message", "Парсинг Kursiv завершен успешно"); - response.put("source", "Kursiv"); - response.put("processedItems", savedItems.size()); - response.put("totalItemsInDb", kursivParserService.getTotalItemsCount()); + ParserResultDto result = new ParserResultDto( + "Kursiv", + savedItems.size(), + totalItems, + "completed"); - return ResponseEntity.ok(response); + return ResponseEntity.ok(ApiResponse.success("Парсинг Kursiv завершен успешно", result)); } catch (Exception e) { - response.put("success", false); - response.put("message", "Ошибка при парсинге Kursiv: " + e.getMessage()); - response.put("source", "Kursiv"); - response.put("processedItems", 0); - - return ResponseEntity.internalServerError().body(response); + return ResponseEntity.internalServerError() + .body(ApiResponse.error("Ошибка при парсинге Kursiv: " + e.getMessage())); } } @@ -54,27 +57,22 @@ public class ParserController { * Запуск парсинга RSS-ленты Kapital */ @PostMapping("/parse/kapital") - public ResponseEntity> parseKapitalRssFeed() { - Map response = new HashMap<>(); - + public ResponseEntity> parseKapitalRssFeed() { try { List savedItems = kapitalParserService.parseAndSaveRssFeed(); + long totalItems = marketItemService.getTotalItemsCount(); - response.put("success", true); - response.put("message", "Парсинг Kapital завершен успешно"); - response.put("source", "Kapital"); - response.put("processedItems", savedItems.size()); - response.put("totalItemsInDb", kapitalParserService.getKapitalItemsCount()); + ParserResultDto result = new ParserResultDto( + "Kapital", + savedItems.size(), + totalItems, + "completed"); - return ResponseEntity.ok(response); + return ResponseEntity.ok(ApiResponse.success("Парсинг Kapital завершен успешно", result)); } catch (Exception e) { - response.put("success", false); - response.put("message", "Ошибка при парсинге Kapital: " + e.getMessage()); - response.put("source", "Kapital"); - response.put("processedItems", 0); - - return ResponseEntity.internalServerError().body(response); + return ResponseEntity.internalServerError() + .body(ApiResponse.error("Ошибка при парсинге Kapital: " + e.getMessage())); } } @@ -82,30 +80,63 @@ public class ParserController { * Запуск парсинга всех RSS-лент */ @PostMapping("/parse/all") - public ResponseEntity> parseAllRssFeeds() { - Map response = new HashMap<>(); - + public ResponseEntity>> parseAllRssFeeds() { try { List kursivItems = kursivParserService.parseAndSaveRssFeed(); List kapitalItems = kapitalParserService.parseAndSaveRssFeed(); + long totalItems = marketItemService.getTotalItemsCount(); - int totalProcessed = kursivItems.size() + kapitalItems.size(); + ParserResultDto kursivResult = new ParserResultDto( + "Kursiv", + kursivItems.size(), + totalItems, + "completed"); - response.put("success", true); - response.put("message", "Парсинг всех источников завершен успешно"); - response.put("kursivProcessed", kursivItems.size()); - response.put("kapitalProcessed", kapitalItems.size()); - response.put("totalProcessed", totalProcessed); - response.put("totalItemsInDb", kursivParserService.getTotalItemsCount()); + ParserResultDto kapitalResult = new ParserResultDto( + "Kapital", + kapitalItems.size(), + totalItems, + "completed"); - return ResponseEntity.ok(response); + List results = List.of(kursivResult, kapitalResult); + + return ResponseEntity.ok(ApiResponse.success("Парсинг всех источников завершен успешно", results)); } catch (Exception e) { - response.put("success", false); - response.put("message", "Ошибка при парсинге всех источников: " + e.getMessage()); - response.put("totalProcessed", 0); + return ResponseEntity.internalServerError() + .body(ApiResponse.error("Ошибка при парсинге всех источников: " + e.getMessage())); + } + } - return ResponseEntity.internalServerError().body(response); + /** + * Получение записей с фильтрацией и пагинацией + */ + @GetMapping("/items") + public ResponseEntity>> getItems( + @RequestParam(required = false) String sourceName, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) String startDate, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) String endDate, + @PageableDefault(size = 20, sort = "publishedAt", direction = org.springframework.data.domain.Sort.Direction.DESC) Pageable pageable) { + + try { + LocalDateTime start = marketItemService.parseDate(startDate); + LocalDateTime end = marketItemService.parseDate(endDate); + + Page items; + + if (sourceName != null || start != null || end != null) { + // Используем фильтрацию + items = marketItemService.getItemsWithFilters(sourceName, start, end, pageable); + } else { + // Получаем все записи + items = marketItemService.getAllItems(pageable); + } + + return ResponseEntity.ok(ApiResponse.success("Записи получены успешно", items)); + + } catch (Exception e) { + return ResponseEntity.internalServerError() + .body(ApiResponse.error("Ошибка при получении записей: " + e.getMessage())); } } @@ -113,51 +144,19 @@ public class ParserController { * Получение статистики по сохраненным записям */ @GetMapping("/stats") - public ResponseEntity> getStats() { - Map response = new HashMap<>(); - + public ResponseEntity> getStats() { try { - long totalCount = kursivParserService.getTotalItemsCount(); + long totalCount = marketItemService.getTotalItemsCount(); + var sourceStatistics = marketItemService.getSourceStatistics(); + String lastUpdate = java.time.LocalDateTime.now().toString(); - response.put("success", true); - response.put("totalItems", totalCount); - response.put("message", "Статистика получена успешно"); + ParserStatsDto stats = new ParserStatsDto(totalCount, sourceStatistics, lastUpdate); - return ResponseEntity.ok(response); + return ResponseEntity.ok(ApiResponse.success("Статистика получена успешно", stats)); } catch (Exception e) { - response.put("success", false); - response.put("message", "Ошибка при получении статистики: " + e.getMessage()); - response.put("totalItems", 0); - - return ResponseEntity.internalServerError().body(response); - } - } - - /** - * Получение всех сохраненных записей - */ - @GetMapping("/items") - public ResponseEntity> getAllItems() { - Map response = new HashMap<>(); - - try { - List items = kursivParserService.getAllItems(); - - response.put("success", true); - response.put("items", items); - response.put("count", items.size()); - response.put("message", "Записи получены успешно"); - - return ResponseEntity.ok(response); - - } catch (Exception e) { - response.put("success", false); - response.put("message", "Ошибка при получении записей: " + e.getMessage()); - response.put("items", List.of()); - response.put("count", 0); - - return ResponseEntity.internalServerError().body(response); + return ResponseEntity.internalServerError() + .body(ApiResponse.error("Ошибка при получении статистики: " + e.getMessage())); } } @@ -165,21 +164,14 @@ public class ParserController { * Получение записи по ID */ @GetMapping("/items/{id}") - public ResponseEntity> getItemById(@PathVariable String id) { - Map response = new HashMap<>(); - + public ResponseEntity> getItemById(@PathVariable String id) { try { // Здесь можно добавить метод для поиска по ID в сервисе - response.put("success", false); - response.put("message", "Функция поиска по ID пока не реализована"); - - return ResponseEntity.ok(response); + return ResponseEntity.ok(ApiResponse.error("Функция поиска по ID пока не реализована")); } catch (Exception e) { - response.put("success", false); - response.put("message", "Ошибка при получении записи: " + e.getMessage()); - - return ResponseEntity.internalServerError().body(response); + return ResponseEntity.internalServerError() + .body(ApiResponse.error("Ошибка при получении записи: " + e.getMessage())); } } @@ -187,31 +179,60 @@ public class ParserController { * Проверка состояния парсера */ @GetMapping("/health") - public ResponseEntity> healthCheck() { - Map response = new HashMap<>(); + public ResponseEntity> healthCheck() { + HealthCheckDto health = new HealthCheckDto( + "UP", + "RSS Parser System", + System.currentTimeMillis(), + "Enabled - runs every 30 minutes"); - response.put("status", "UP"); - response.put("service", "Kursiv RSS Parser"); - response.put("timestamp", System.currentTimeMillis()); - response.put("scheduler", "Enabled - runs every 30 minutes"); + return ResponseEntity.ok(ApiResponse.success("Сервис работает", health)); + } - return ResponseEntity.ok(response); + /** + * Проверка подключения к MongoDB + */ + @GetMapping("/health/mongodb") + public ResponseEntity> checkMongoConnection() { + try { + long count = marketItemService.getTotalItemsCount(); + HealthCheckDto health = new HealthCheckDto( + "UP", + "MongoDB подключение успешно", + System.currentTimeMillis(), + null); + + return ResponseEntity.ok(ApiResponse.success("MongoDB подключение успешно", health)); + + } catch (Exception e) { + HealthCheckDto health = new HealthCheckDto( + "DOWN", + "Ошибка подключения к MongoDB: " + e.getMessage(), + System.currentTimeMillis(), + null); + + if (e.getMessage().contains("Unauthorized") || e.getMessage().contains("authentication")) { + health.setSuggestion("Проверьте учетные данные в application.properties"); + } + + return ResponseEntity.status(503).body(ApiResponse.error("Ошибка подключения к MongoDB", health)); + } } /** * Получение информации о планировщике */ @GetMapping("/scheduler/info") - public ResponseEntity> getSchedulerInfo() { - Map response = new HashMap<>(); + public ResponseEntity>> getSchedulerInfo() { + java.util.Map schedulerInfo = new java.util.HashMap<>(); - response.put("schedulerEnabled", true); - response.put("cronExpression", "0 0/30 * * * ?"); - response.put("description", "Запуск каждые 30 минут (в 0 и 30 минут каждого часа)"); - response.put("nextRun", "Следующий запуск будет в ближайшие 0 или 30 минут часа"); - response.put("activeParsers", List.of("KursivParserService", "KapitalParserService")); - response.put("sources", List.of("Kursiv Media", "Kapital.kz")); + schedulerInfo.put("schedulerEnabled", true); + schedulerInfo.put("cronExpression", "0 0/30 * * * ?"); + schedulerInfo.put("description", "Запуск каждые 30 минут (в 0 и 30 минут каждого часа)"); + schedulerInfo.put("nextRun", "Следующий запуск будет в ближайшие 0 или 30 минут часа"); + schedulerInfo.put("activeParsers", List.of("KursivParserService", "KapitalParserService")); + schedulerInfo.put("sources", List.of("Kursiv Media", "Kapital.kz")); - return ResponseEntity.ok(response); + return ResponseEntity.ok(ApiResponse.success("Информация о планировщике получена", schedulerInfo)); } } diff --git a/src/main/java/kz/konturai/parser/dto/ApiResponse.java b/src/main/java/kz/konturai/parser/dto/ApiResponse.java new file mode 100644 index 0000000..36e7ec6 --- /dev/null +++ b/src/main/java/kz/konturai/parser/dto/ApiResponse.java @@ -0,0 +1,81 @@ +package kz.konturai.parser.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ApiResponse { + + private boolean success; + private String message; + private T data; + private String error; + + public ApiResponse() { + } + + public ApiResponse(boolean success, String message, T data) { + this.success = success; + this.message = message; + this.data = data; + } + + public ApiResponse(boolean success, String message, String error) { + this.success = success; + this.message = message; + this.error = error; + } + + public static ApiResponse success(T data) { + return new ApiResponse<>(true, "Операция выполнена успешно", data); + } + + public static ApiResponse success(String message, T data) { + return new ApiResponse<>(true, message, data); + } + + public static ApiResponse error(String message) { + return new ApiResponse<>(false, message, message); + } + + public static ApiResponse error(String message, String error) { + return new ApiResponse<>(false, message, error); + } + + public static ApiResponse error(String message, T data) { + ApiResponse response = new ApiResponse<>(false, message, data); + return response; + } + + // Getters and Setters + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public T getData() { + return data; + } + + public void setData(T data) { + this.data = data; + } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } +} diff --git a/src/main/java/kz/konturai/parser/dto/HealthCheckDto.java b/src/main/java/kz/konturai/parser/dto/HealthCheckDto.java new file mode 100644 index 0000000..3f0cc7b --- /dev/null +++ b/src/main/java/kz/konturai/parser/dto/HealthCheckDto.java @@ -0,0 +1,76 @@ +package kz.konturai.parser.dto; + +public class HealthCheckDto { + + private String status; + private String service; + private long timestamp; + private String scheduler; + private String message; + private String suggestion; + + public HealthCheckDto() { + } + + public HealthCheckDto(String status, String service, long timestamp, String scheduler) { + this.status = status; + this.service = service; + this.timestamp = timestamp; + this.scheduler = scheduler; + } + + public HealthCheckDto(String status, String message, long timestamp) { + this.status = status; + this.message = message; + this.timestamp = timestamp; + } + + // Getters and Setters + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getService() { + return service; + } + + public void setService(String service) { + this.service = service; + } + + public long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + public String getScheduler() { + return scheduler; + } + + public void setScheduler(String scheduler) { + this.scheduler = scheduler; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getSuggestion() { + return suggestion; + } + + public void setSuggestion(String suggestion) { + this.suggestion = suggestion; + } +} diff --git a/src/main/java/kz/konturai/parser/dto/ParserResultDto.java b/src/main/java/kz/konturai/parser/dto/ParserResultDto.java new file mode 100644 index 0000000..265b77f --- /dev/null +++ b/src/main/java/kz/konturai/parser/dto/ParserResultDto.java @@ -0,0 +1,52 @@ +package kz.konturai.parser.dto; + +public class ParserResultDto { + + private String source; + private int processedItems; + private long totalItemsInDb; + private String status; + + public ParserResultDto() { + } + + public ParserResultDto(String source, int processedItems, long totalItemsInDb, String status) { + this.source = source; + this.processedItems = processedItems; + this.totalItemsInDb = totalItemsInDb; + this.status = status; + } + + // Getters and Setters + public String getSource() { + return source; + } + + public void setSource(String source) { + this.source = source; + } + + public int getProcessedItems() { + return processedItems; + } + + public void setProcessedItems(int processedItems) { + this.processedItems = processedItems; + } + + public long getTotalItemsInDb() { + return totalItemsInDb; + } + + public void setTotalItemsInDb(long totalItemsInDb) { + this.totalItemsInDb = totalItemsInDb; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } +} diff --git a/src/main/java/kz/konturai/parser/dto/ParserStatsDto.java b/src/main/java/kz/konturai/parser/dto/ParserStatsDto.java new file mode 100644 index 0000000..0081424 --- /dev/null +++ b/src/main/java/kz/konturai/parser/dto/ParserStatsDto.java @@ -0,0 +1,44 @@ +package kz.konturai.parser.dto; + +import java.util.Map; + +public class ParserStatsDto { + + private long totalItems; + private Map sourceStatistics; + private String lastUpdate; + + public ParserStatsDto() { + } + + public ParserStatsDto(long totalItems, Map sourceStatistics, String lastUpdate) { + this.totalItems = totalItems; + this.sourceStatistics = sourceStatistics; + this.lastUpdate = lastUpdate; + } + + // Getters and Setters + public long getTotalItems() { + return totalItems; + } + + public void setTotalItems(long totalItems) { + this.totalItems = totalItems; + } + + public Map getSourceStatistics() { + return sourceStatistics; + } + + public void setSourceStatistics(Map sourceStatistics) { + this.sourceStatistics = sourceStatistics; + } + + public String getLastUpdate() { + return lastUpdate; + } + + public void setLastUpdate(String lastUpdate) { + this.lastUpdate = lastUpdate; + } +} diff --git a/src/main/java/kz/konturai/parser/repository/MarketItemRepository.java b/src/main/java/kz/konturai/parser/repository/MarketItemRepository.java index 5d976c2..da8ec55 100644 --- a/src/main/java/kz/konturai/parser/repository/MarketItemRepository.java +++ b/src/main/java/kz/konturai/parser/repository/MarketItemRepository.java @@ -1,9 +1,14 @@ package kz.konturai.parser.repository; import kz.konturai.parser.model.MarketItem; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.mongodb.repository.Query; import org.springframework.stereotype.Repository; +import java.time.LocalDateTime; +import java.util.List; import java.util.Optional; @Repository @@ -18,4 +23,33 @@ public interface MarketItemRepository extends MongoRepository findBySourceName(String sourceName, Pageable pageable); + + /** + * Найти записи по диапазону дат с пагинацией + */ + Page findByPublishedAtBetween(LocalDateTime startDate, LocalDateTime endDate, Pageable pageable); + + /** + * Подсчитать записи по источнику + */ + long countBySourceName(String sourceName); + + /** + * Получить все уникальные названия источников + */ + @Query(value = "{}", fields = "{'sourceName': 1}") + List findDistinctSourceNames(); + + /** + * Найти записи по источнику и диапазону дат + */ + Page findBySourceNameAndPublishedAtBetween(String sourceName, + LocalDateTime startDate, + LocalDateTime endDate, + Pageable pageable); } diff --git a/src/main/java/kz/konturai/parser/service/KapitalParserService.java b/src/main/java/kz/konturai/parser/service/KapitalParserService.java index a6e4cee..b39fada 100644 --- a/src/main/java/kz/konturai/parser/service/KapitalParserService.java +++ b/src/main/java/kz/konturai/parser/service/KapitalParserService.java @@ -242,6 +242,13 @@ public class KapitalParserService { return savedItem; } + } catch (org.springframework.data.mongodb.UncategorizedMongoDbException e) { + if (e.getMessage().contains("Unauthorized") || e.getMessage().contains("authentication")) { + logger.error("❌ Ошибка аутентификации MongoDB. Проверьте учетные данные в application.properties"); + } else { + logger.error("❌ Ошибка подключения к MongoDB: {}", e.getMessage()); + } + return null; } catch (Exception e) { logger.error("Ошибка при сохранении записи Kapital.kz: {}", marketItem.getTitle(), e); return null; diff --git a/src/main/java/kz/konturai/parser/service/KursivParserService.java b/src/main/java/kz/konturai/parser/service/KursivParserService.java index eb43db0..dca5ce0 100644 --- a/src/main/java/kz/konturai/parser/service/KursivParserService.java +++ b/src/main/java/kz/konturai/parser/service/KursivParserService.java @@ -238,6 +238,13 @@ public class KursivParserService { return savedItem; } + } catch (org.springframework.data.mongodb.UncategorizedMongoDbException e) { + if (e.getMessage().contains("Unauthorized") || e.getMessage().contains("authentication")) { + logger.error("❌ Ошибка аутентификации MongoDB. Проверьте учетные данные в application.properties"); + } else { + logger.error("❌ Ошибка подключения к MongoDB: {}", e.getMessage()); + } + return null; } catch (Exception e) { logger.error("Ошибка при сохранении записи: {}", marketItem.getTitle(), e); return null; diff --git a/src/main/java/kz/konturai/parser/service/MarketItemService.java b/src/main/java/kz/konturai/parser/service/MarketItemService.java new file mode 100644 index 0000000..975d3c2 --- /dev/null +++ b/src/main/java/kz/konturai/parser/service/MarketItemService.java @@ -0,0 +1,166 @@ +package kz.konturai.parser.service; + +import kz.konturai.parser.model.MarketItem; +import kz.konturai.parser.repository.MarketItemRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; + +@Service +public class MarketItemService { + + private static final Logger logger = LoggerFactory.getLogger(MarketItemService.class); + + @Autowired + private MarketItemRepository marketItemRepository; + + @Autowired + private MongoTemplate mongoTemplate; + + /** + * Получение всех записей с пагинацией и сортировкой + */ + public Page getAllItems(Pageable pageable) { + logger.debug("Получение всех записей с пагинацией: page={}, size={}, sort={}", + pageable.getPageNumber(), pageable.getPageSize(), pageable.getSort()); + + return marketItemRepository.findAll(pageable); + } + + /** + * Получение записей с фильтрацией по источнику + */ + public Page getItemsBySource(String sourceName, Pageable pageable) { + logger.debug("Получение записей по источнику: sourceName={}, page={}, size={}", + sourceName, pageable.getPageNumber(), pageable.getPageSize()); + + return marketItemRepository.findBySourceName(sourceName, pageable); + } + + /** + * Получение записей с фильтрацией по диапазону дат + */ + public Page getItemsByDateRange(LocalDateTime startDate, LocalDateTime endDate, Pageable pageable) { + logger.debug("Получение записей по диапазону дат: startDate={}, endDate={}, page={}, size={}", + startDate, endDate, pageable.getPageNumber(), pageable.getPageSize()); + + return marketItemRepository.findByPublishedAtBetween(startDate, endDate, pageable); + } + + /** + * Получение записей с комплексной фильтрацией + */ + public Page getItemsWithFilters(String sourceName, LocalDateTime startDate, + LocalDateTime endDate, Pageable pageable) { + logger.debug("Получение записей с фильтрацией: sourceName={}, startDate={}, endDate={}, page={}, size={}", + sourceName, startDate, endDate, pageable.getPageNumber(), pageable.getPageSize()); + + Query query = new Query(); + + // Фильтр по источнику + if (sourceName != null && !sourceName.trim().isEmpty()) { + query.addCriteria(Criteria.where("sourceName").is(sourceName)); + } + + // Фильтр по диапазону дат + if (startDate != null && endDate != null) { + query.addCriteria(Criteria.where("publishedAt").gte(startDate).lte(endDate)); + } else if (startDate != null) { + query.addCriteria(Criteria.where("publishedAt").gte(startDate)); + } else if (endDate != null) { + query.addCriteria(Criteria.where("publishedAt").lte(endDate)); + } + + // Подсчет общего количества записей + long totalCount = mongoTemplate.count(query, MarketItem.class); + + // Применение пагинации и сортировки + query.with(pageable); + + // Выполнение запроса + List items = mongoTemplate.find(query, MarketItem.class); + + // Создание объекта Page + return new org.springframework.data.domain.PageImpl<>(items, pageable, totalCount); + } + + /** + * Получение общего количества записей + */ + public long getTotalItemsCount() { + return marketItemRepository.count(); + } + + /** + * Получение количества записей по источнику + */ + public long getItemsCountBySource(String sourceName) { + return marketItemRepository.countBySourceName(sourceName); + } + + /** + * Получение всех уникальных источников + */ + public List getAllSourceNames() { + return marketItemRepository.findDistinctSourceNames(); + } + + /** + * Получение статистики по источникам + */ + public java.util.Map getSourceStatistics() { + List sources = getAllSourceNames(); + java.util.Map statistics = new java.util.HashMap<>(); + + for (String source : sources) { + statistics.put(source, getItemsCountBySource(source)); + } + + return statistics; + } + + /** + * Парсинг даты из строки + */ + public LocalDateTime parseDate(String dateString) { + if (dateString == null || dateString.trim().isEmpty()) { + return null; + } + + try { + // Поддерживаем несколько форматов даты + String[] formats = { + "yyyy-MM-dd", + "yyyy-MM-dd'T'HH:mm:ss", + "yyyy-MM-dd'T'HH:mm:ss.SSS", + "yyyy-MM-dd HH:mm:ss" + }; + + for (String format : formats) { + try { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format); + return LocalDateTime.parse(dateString, formatter); + } catch (Exception e) { + // Пробуем следующий формат + } + } + + logger.warn("Не удалось распарсить дату: {}", dateString); + return null; + + } catch (Exception e) { + logger.error("Ошибка при парсинге даты: {}", dateString, e); + return null; + } + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 6be0684..2bcad99 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,9 +1,16 @@ spring.application.name=parser # MongoDB Configuration +# Option 1: Individual properties spring.data.mongodb.host=92.38.48.166 spring.data.mongodb.port=27017 spring.data.mongodb.database=parser_db +spring.data.mongodb.username=konturai +spring.data.mongodb.password=konturai2024 +spring.data.mongodb.authentication-database=admin + +# Option 2: Connection URI (uncomment if individual properties don't work) +# spring.data.mongodb.uri=mongodb://parser_user:parser_password@92.38.48.166:27017/parser_db?authSource=admin # RSS Feed URLs rss.feed.url=https://kursiv.media/feed/ @@ -13,6 +20,14 @@ rss.kapital.url=https://kapital.kz/rss/ spring.task.scheduling.pool.size=2 spring.task.scheduling.thread-name-prefix=scheduled-task- +# MongoDB Connection Settings +spring.data.mongodb.connection-timeout=30000 +spring.data.mongodb.socket-timeout=30000 +spring.data.mongodb.server-selection-timeout=30000 + # Logging Configuration logging.level.kz.konturai.parser.service.KursivParserService=INFO +logging.level.kz.konturai.parser.service.KapitalParserService=INFO logging.level.org.springframework.scheduling=DEBUG +logging.level.org.springframework.data.mongodb=INFO +logging.level.com.mongodb=WARN diff --git a/src/test/java/kz/konturai/parser/service/MarketItemServiceTest.java b/src/test/java/kz/konturai/parser/service/MarketItemServiceTest.java new file mode 100644 index 0000000..aa1a415 --- /dev/null +++ b/src/test/java/kz/konturai/parser/service/MarketItemServiceTest.java @@ -0,0 +1,201 @@ +package kz.konturai.parser.service; + +import kz.konturai.parser.model.MarketItem; +import kz.konturai.parser.repository.MarketItemRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.test.context.TestPropertySource; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +@TestPropertySource(properties = { + "spring.data.mongodb.host=92.38.48.166", + "spring.data.mongodb.port=27017", + "spring.data.mongodb.database=parser_test_db", + "spring.data.mongodb.username=konturai", + "spring.data.mongodb.password=konturai2024", + "spring.data.mongodb.authentication-database=admin" +}) +public class MarketItemServiceTest { + + @Autowired + private MarketItemService marketItemService; + + @Autowired + private MarketItemRepository marketItemRepository; + + @BeforeEach + public void setUp() { + // Очищаем базу данных перед каждым тестом + marketItemRepository.deleteAll(); + + // Создаем тестовые данные + createTestData(); + } + + private void createTestData() { + // Создаем записи для Kursiv + MarketItem kursivItem1 = new MarketItem(); + kursivItem1.setSourceName("Kursiv (Бизнес/экономика)"); + kursivItem1.setTitle("Test Kursiv News 1"); + kursivItem1.setUrl("https://kursiv.media/test1"); + kursivItem1.setPublishedAt(LocalDateTime.now().minusDays(1)); + kursivItem1.setHash("kursiv_hash_1"); + marketItemRepository.save(kursivItem1); + + MarketItem kursivItem2 = new MarketItem(); + kursivItem2.setSourceName("Kursiv (Бизнес/экономика)"); + kursivItem2.setTitle("Test Kursiv News 2"); + kursivItem2.setUrl("https://kursiv.media/test2"); + kursivItem2.setPublishedAt(LocalDateTime.now().minusDays(2)); + kursivItem2.setHash("kursiv_hash_2"); + marketItemRepository.save(kursivItem2); + + // Создаем записи для Kapital + MarketItem kapitalItem1 = new MarketItem(); + kapitalItem1.setSourceName("Kapital.kz (Бизнес)"); + kapitalItem1.setTitle("Test Kapital News 1"); + kapitalItem1.setUrl("https://kapital.kz/test1"); + kapitalItem1.setPublishedAt(LocalDateTime.now().minusDays(3)); + kapitalItem1.setHash("kapital_hash_1"); + marketItemRepository.save(kapitalItem1); + + MarketItem kapitalItem2 = new MarketItem(); + kapitalItem2.setSourceName("Kapital.kz (Бизнес)"); + kapitalItem2.setTitle("Test Kapital News 2"); + kapitalItem2.setUrl("https://kapital.kz/test2"); + kapitalItem2.setPublishedAt(LocalDateTime.now().minusDays(4)); + kapitalItem2.setHash("kapital_hash_2"); + marketItemRepository.save(kapitalItem2); + } + + @Test + public void testGetAllItemsWithPagination() { + Pageable pageable = PageRequest.of(0, 2, Sort.by("publishedAt").descending()); + Page page = marketItemService.getAllItems(pageable); + + assertNotNull(page); + assertEquals(4, page.getTotalElements()); + assertEquals(2, page.getContent().size()); + assertEquals(2, page.getTotalPages()); + } + + @Test + public void testGetItemsBySource() { + Pageable pageable = PageRequest.of(0, 10); + Page kursivItems = marketItemService.getItemsBySource("Kursiv (Бизнес/экономика)", pageable); + + assertNotNull(kursivItems); + assertEquals(2, kursivItems.getTotalElements()); + assertEquals(2, kursivItems.getContent().size()); + + // Проверяем, что все записи принадлежат Kursiv + for (MarketItem item : kursivItems.getContent()) { + assertEquals("Kursiv (Бизнес/экономика)", item.getSourceName()); + } + } + + @Test + public void testGetItemsByDateRange() { + LocalDateTime startDate = LocalDateTime.now().minusDays(3); + LocalDateTime endDate = LocalDateTime.now().minusDays(1); + + Pageable pageable = PageRequest.of(0, 10); + Page items = marketItemService.getItemsByDateRange(startDate, endDate, pageable); + + assertNotNull(items); + assertEquals(2, items.getTotalElements()); + + // Проверяем, что все записи в указанном диапазоне дат + for (MarketItem item : items.getContent()) { + assertTrue(item.getPublishedAt().isAfter(startDate.minusSeconds(1))); + assertTrue(item.getPublishedAt().isBefore(endDate.plusSeconds(1))); + } + } + + @Test + public void testGetItemsWithFilters() { + LocalDateTime startDate = LocalDateTime.now().minusDays(3); + LocalDateTime endDate = LocalDateTime.now().minusDays(1); + + Pageable pageable = PageRequest.of(0, 10); + Page items = marketItemService.getItemsWithFilters( + "Kursiv (Бизнес/экономика)", startDate, endDate, pageable); + + assertNotNull(items); + assertEquals(1, items.getTotalElements()); + + // Проверяем, что запись соответствует фильтрам + MarketItem item = items.getContent().get(0); + assertEquals("Kursiv (Бизнес/экономика)", item.getSourceName()); + assertTrue(item.getPublishedAt().isAfter(startDate.minusSeconds(1))); + assertTrue(item.getPublishedAt().isBefore(endDate.plusSeconds(1))); + } + + @Test + public void testGetTotalItemsCount() { + long count = marketItemService.getTotalItemsCount(); + assertEquals(4, count); + } + + @Test + public void testGetItemsCountBySource() { + long kursivCount = marketItemService.getItemsCountBySource("Kursiv (Бизнес/экономика)"); + assertEquals(2, kursivCount); + + long kapitalCount = marketItemService.getItemsCountBySource("Kapital.kz (Бизнес)"); + assertEquals(2, kapitalCount); + } + + @Test + public void testGetAllSourceNames() { + List sources = marketItemService.getAllSourceNames(); + + assertNotNull(sources); + assertEquals(2, sources.size()); + assertTrue(sources.contains("Kursiv (Бизнес/экономика)")); + assertTrue(sources.contains("Kapital.kz (Бизнес)")); + } + + @Test + public void testGetSourceStatistics() { + Map statistics = marketItemService.getSourceStatistics(); + + assertNotNull(statistics); + assertEquals(2, statistics.size()); + assertEquals(Long.valueOf(2), statistics.get("Kursiv (Бизнес/экономика)")); + assertEquals(Long.valueOf(2), statistics.get("Kapital.kz (Бизнес)")); + } + + @Test + public void testParseDate() { + // Тест различных форматов даты + LocalDateTime date1 = marketItemService.parseDate("2025-01-14"); + assertNotNull(date1); + + LocalDateTime date2 = marketItemService.parseDate("2025-01-14T10:30:00"); + assertNotNull(date2); + + LocalDateTime date3 = marketItemService.parseDate("2025-01-14T10:30:00.000"); + assertNotNull(date3); + + LocalDateTime date4 = marketItemService.parseDate("2025-01-14 10:30:00"); + assertNotNull(date4); + + // Тест null и пустой строки + assertNull(marketItemService.parseDate(null)); + assertNull(marketItemService.parseDate("")); + assertNull(marketItemService.parseDate("invalid-date")); + } +} diff --git a/test-mongodb-connection.sh b/test-mongodb-connection.sh new file mode 100644 index 0000000..cfb33a7 --- /dev/null +++ b/test-mongodb-connection.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +echo "🔍 Тестирование подключения к MongoDB..." +echo "======================================" + +# Проверяем доступность MongoDB сервера +echo "📡 Проверка доступности сервера 92.38.48.166:27017..." +if nc -z 92.38.48.166 27017; then + echo "✅ Сервер MongoDB доступен" +else + echo "❌ Сервер MongoDB недоступен" + echo " Убедитесь, что сервер запущен и доступен" + exit 1 +fi + +echo "" +echo "🔐 Проверка аутентификации..." +echo "Используемые учетные данные:" +echo " Host: 92.38.48.166:27017" +echo " Database: parser_db" +echo " Username: parser_user" +echo " Password: parser_password" +echo " Auth Database: admin" + +echo "" +echo "📝 Для проверки подключения используйте:" +echo "mongo mongodb://parser_user:parser_password@92.38.48.166:27017/parser_db?authSource=admin" + +echo "" +echo "🚀 Для запуска приложения с новой конфигурацией:" +echo "./mvnw spring-boot:run" + +echo "" +echo "⚠️ Если подключение не работает, проверьте:" +echo "1. Правильность учетных данных" +echo "2. Настройки firewall на сервере MongoDB" +echo "3. Права доступа пользователя parser_user" +echo "4. Настройки аутентификации в MongoDB" diff --git a/Рефакторинг ParserController для поддержки фронтенда.md b/Рефакторинг ParserController для поддержки фронтенда.md new file mode 100644 index 0000000..0797698 --- /dev/null +++ b/Рефакторинг ParserController для поддержки фронтенда.md @@ -0,0 +1,15 @@ +Необходимо модифицировать существующий ParserController и сервисный слой для эффективного отображения данных. + +Требования: + +Создать MarketItemService: Создай новый сервис MarketItemService, который будет содержать всю логику по работе с коллекцией market_items (получение, подсчет и т.д.). Перенеси в него методы getAllItems и getTotalItemsCount из KursivParserService и сделай так, чтобы они работали со всеми записями, а не только с одним источником. + +Реализовать пагинацию и сортировку: Измени метод getAllItems в контроллере и сервисе так, чтобы он принимал Pageable в качестве аргумента. Метод должен возвращать Page. Фронтенд сможет запрашивать данные так: GET /api/parser/items?page=0&size=10&sort=published_at,desc. + +Добавить фильтрацию: Добавь в метод getAllItems необязательные параметры запроса (@RequestParam): + +sourceName (тип String) для фильтрации по источнику. + +startDate, endDate (тип String или LocalDate) для фильтрации по диапазону дат. + +Заменить Map на DTO: Создай классы-DTO для всех ответов контроллера, чтобы обеспечить строгую типизацию.