From 95c09289d667eb6beea1e0a3f527606cf79d7afd Mon Sep 17 00:00:00 2001 From: root Date: Sun, 14 Sep 2025 18:33:59 +0500 Subject: [PATCH] . --- FRONTEND_API_GUIDE.md | 527 +++++++++++++++++- .../controller/HealthCheckController.java | 50 ++ .../parser/service/KapitalParserService.java | 29 +- .../parser/service/KursivParserService.java | 29 +- .../parser/service/LsmParserService.java | 29 +- .../parser/service/RbcParserService.java | 29 +- .../service/VedomostiParserService.java | 29 +- .../konturai/parser/util/RssUrlValidator.java | 136 +++++ .../controller/SourceNamesEndpointTest.java | 88 +++ 9 files changed, 924 insertions(+), 22 deletions(-) create mode 100644 src/main/java/kz/konturai/parser/util/RssUrlValidator.java create mode 100644 src/test/java/kz/konturai/parser/controller/SourceNamesEndpointTest.java diff --git a/FRONTEND_API_GUIDE.md b/FRONTEND_API_GUIDE.md index 213da68..39a02e8 100644 --- a/FRONTEND_API_GUIDE.md +++ b/FRONTEND_API_GUIDE.md @@ -145,22 +145,99 @@ GET /api/items/stats } ``` -### 4. Получение списка источников +### 4. Получение списка источников (sourceName) ```http GET /api/items/sources ``` +**Описание:** +Возвращает список всех уникальных источников новостей, которые есть в базе данных. Этот эндпоинт полезен для: + +- Построения фильтров по источникам +- Отображения списка доступных источников в UI +- Динамического создания выпадающих списков + **Ответ:** ```json { "success": true, "message": "Источники получены успешно", - "data": ["Kursiv Media", "Kapital.kz", "LSM.kz", "РБК", "Ведомости"] + "data": [ + "Kursiv (Бизнес/экономика)", + "Kapital.kz (Бизнес)", + "LSM.kz", + "РБК", + "Ведомости" + ] } ``` +**Примеры использования:** + +```javascript +// Получить список всех источников +const response = await fetch('/api/items/sources'); +const result = await response.json(); + +if (result.success) { + const sources = result.data; + console.log('Доступные источники:', sources); + // sources = ["Kursiv (Бизнес/экономика)", "Kapital.kz (Бизнес)", "LSM.kz", "РБК", "Ведомости"] +} + +// Использование в React для создания фильтра +function SourceFilter({ onSourceChange }) { + const [sources, setSources] = useState([]); + + useEffect(() => { + fetch('/api/items/sources') + .then((res) => res.json()) + .then((data) => { + if (data.success) { + setSources(data.data); + } + }); + }, []); + + return ( + + ); +} + +// Использование в Vue.js +export default { + data() { + return { + sources: [], + selectedSource: '', + }; + }, + async mounted() { + const response = await fetch('/api/items/sources'); + const result = await response.json(); + if (result.success) { + this.sources = result.data; + } + }, +}; +``` + +**Важные замечания:** + +- Список источников формируется на основе реальных данных в базе +- Если в базе нет данных от какого-то источника, он не будет включен в список +- Названия источников точно соответствуют тем, что используются в поле `sourceName` при фильтрации +- Порядок источников может изменяться в зависимости от того, как MongoDB возвращает уникальные значения + ### 5. Получение новостей по конкретному источнику ```http @@ -485,6 +562,120 @@ GET /api/health/parsers } ``` +### 6. Проверка доступности RSS-лент + +```http +GET /api/health/rss +``` + +**Описание:** +Проверяет доступность всех RSS-лент и предоставляет рекомендации по альтернативным URL в случае недоступности. + +**Ответ:** + +```json +{ + "success": true, + "message": "Проверка RSS-лент завершена", + "data": { + "overallStatus": "DEGRADED", + "accessibilityResults": { + "kursiv": true, + "kapital": false, + "lsm": true, + "rbc": false, + "vedomosti": true + }, + "totalFeeds": 5, + "accessibleFeeds": 3, + "unaccessibleFeeds": 2, + "recommendations": { + "kapital": "https://kapital.kz/feed/", + "rbc": "https://rbc.ru/rss/" + } + } +} +``` + +**Примеры использования:** + +```javascript +// Проверка доступности RSS-лент +async function checkRssFeeds() { + try { + const response = await fetch('/api/health/rss'); + const result = await response.json(); + + if (result.success) { + const data = result.data; + console.log('Общий статус:', data.overallStatus); + console.log('Доступные ленты:', data.accessibleFeeds); + console.log('Недоступные ленты:', data.unaccessibleFeeds); + + // Показать рекомендации для недоступных лент + if (data.recommendations && Object.keys(data.recommendations).length > 0) { + console.log('Рекомендации по альтернативным URL:'); + Object.entries(data.recommendations).forEach(([source, url]) => { + console.log(`${source}: ${url}`); + }); + } + } + } catch (error) { + console.error('Ошибка проверки RSS-лент:', error); + } +} + +// React компонент для отображения статуса RSS-лент +function RssStatusWidget() { + const [rssStatus, setRssStatus] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetch('/api/health/rss') + .then(res => res.json()) + .then(data => { + if (data.success) { + setRssStatus(data.data); + } + setLoading(false); + }) + .catch(err => { + console.error('Ошибка загрузки статуса RSS:', err); + setLoading(false); + }); + }, []); + + if (loading) return
Загрузка...
; + + return ( +
+

Статус RSS-лент

+
+ {rssStatus.overallStatus === 'UP' ? '✅ Все ленты доступны' : + rssStatus.overallStatus === 'DEGRADED' ? '⚠️ Некоторые ленты недоступны' : + '❌ Ленты недоступны'} +
+ +
+ {Object.entries(rssStatus.accessibilityResults).map(([source, isAccessible]) => ( +
+ {source} + + {isAccessible ? '✅' : '❌'} + + {!isAccessible && rssStatus.recommendations[source] && ( +
+ Рекомендация: {rssStatus.recommendations[source]} +
+ )} +
+ ))} +
+
+ ); +} +``` + --- ## 🚀 Примеры использования для фронтенда @@ -525,6 +716,23 @@ class NewsApiService { return response.json(); } + // Получить источники с обработкой ошибок + async getSourcesWithErrorHandling() { + try { + const response = await fetch(`${this.baseUrl}/api/items/sources`); + const result = await response.json(); + + if (!result.success) { + throw new Error(result.message || 'Ошибка при получении источников'); + } + + return result.data; // Возвращаем только массив источников + } catch (error) { + console.error('Ошибка при получении источников:', error); + return []; // Возвращаем пустой массив в случае ошибки + } + } + // Запустить парсер (админ функция) async runParser(sourceName) { const response = await fetch( @@ -577,6 +785,19 @@ const recentNews = await apiService.getNews({ startDate: weekAgo, sort: 'publishedAt,desc', }); + +// Получить список источников для фильтра +const sources = await apiService.getSourcesWithErrorHandling(); +console.log('Доступные источники:', sources); + +// Получить новости от конкретного источника +if (sources.length > 0) { + const newsFromFirstSource = await apiService.getNews({ + sourceName: sources[0], + page: 0, + size: 10, + }); +} ``` ### Vue.js примеры @@ -608,16 +829,318 @@ export function useNewsApi() { return response.json(); }; + const getSourcesList = async () => { + try { + const response = await fetch(`${baseUrl}/api/items/sources`); + const result = await response.json(); + return result.success ? result.data : []; + } catch (error) { + console.error('Ошибка при получении источников:', error); + return []; + } + }; + return { getNews, getStats, getSources, + getSourcesList, }; } ``` --- +## 🎯 Практические примеры использования эндпоинта источников + +### 1. Создание динамического фильтра источников + +```javascript +// React компонент для фильтрации по источникам +import React, { useState, useEffect } from 'react'; + +function NewsFilter({ onFilterChange }) { + const [sources, setSources] = useState([]); + const [selectedSource, setSelectedSource] = useState(''); + + useEffect(() => { + // Загружаем список источников при монтировании компонента + fetch('/api/items/sources') + .then((res) => res.json()) + .then((data) => { + if (data.success) { + setSources(data.data); + } + }) + .catch((err) => console.error('Ошибка загрузки источников:', err)); + }, []); + + const handleSourceChange = (source) => { + setSelectedSource(source); + onFilterChange({ sourceName: source || undefined }); + }; + + return ( +
+ + +
+ ); +} + +export default NewsFilter; +``` + +### 2. Отображение статистики по источникам + +```javascript +// Vue.js компонент для отображения статистики + + + +``` + +### 3. Автодополнение для поиска по источникам + +```javascript +// Angular сервис для автодополнения +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable, BehaviorSubject } from 'rxjs'; +import { map, catchError } from 'rxjs/operators'; + +@Injectable({ + providedIn: 'root' +}) +export class SourceService { + private sourcesSubject = new BehaviorSubject([]); + public sources$ = this.sourcesSubject.asObservable(); + + constructor(private http: HttpClient) { + this.loadSources(); + } + + private loadSources(): void { + this.http.get('/api/items/sources') + .pipe( + map(response => response.success ? response.data : []), + catchError(error => { + console.error('Ошибка загрузки источников:', error); + return []; + }) + ) + .subscribe(sources => { + this.sourcesSubject.next(sources); + }); + } + + searchSources(query: string): Observable { + return this.sources$.pipe( + map(sources => + sources.filter(source => + source.toLowerCase().includes(query.toLowerCase()) + ) + ) + ); + } + + getSources(): Observable { + return this.sources$; + } +} + +// Компонент автодополнения +@Component({ + selector: 'app-source-autocomplete', + template: ` +
+ +
+
+ {{ source }} +
+
+
+ ` +}) +export class SourceAutocompleteComponent { + searchQuery = ''; + filteredSources: string[] = []; + + constructor(private sourceService: SourceService) {} + + onSearch(query: string): void { + if (query.length > 0) { + this.sourceService.searchSources(query) + .subscribe(sources => { + this.filteredSources = sources; + }); + } else { + this.filteredSources = []; + } + } + + selectSource(source: string): void { + this.searchQuery = source; + this.filteredSources = []; + // Эмитим событие выбора источника + } +} +``` + +### 4. Кэширование списка источников + +```javascript +// Сервис с кэшированием для React +class SourceCacheService { + constructor() { + this.cache = null; + this.cacheTime = null; + this.cacheTimeout = 5 * 60 * 1000; // 5 минут + } + + async getSources() { + // Проверяем, есть ли актуальный кэш + if ( + this.cache && + this.cacheTime && + Date.now() - this.cacheTime < this.cacheTimeout + ) { + return this.cache; + } + + try { + const response = await fetch('/api/items/sources'); + const result = await response.json(); + + if (result.success) { + this.cache = result.data; + this.cacheTime = Date.now(); + return result.data; + } else { + throw new Error(result.message); + } + } catch (error) { + console.error('Ошибка загрузки источников:', error); + // Возвращаем кэш, если есть, даже если он устарел + return this.cache || []; + } + } + + clearCache() { + this.cache = null; + this.cacheTime = null; + } +} + +// Использование в React хуке +function useSources() { + const [sources, setSources] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const sourceService = new SourceCacheService(); + + sourceService + .getSources() + .then((data) => { + setSources(data); + setLoading(false); + }) + .catch((err) => { + setError(err.message); + setLoading(false); + }); + }, []); + + return { sources, loading, error }; +} +``` + +--- + ## 📝 Важные замечания 1. **Пагинация**: Все эндпоинты для получения данных поддерживают пагинацию через параметры `page` и `size`. diff --git a/src/main/java/kz/konturai/parser/controller/HealthCheckController.java b/src/main/java/kz/konturai/parser/controller/HealthCheckController.java index 9940817..4ab9050 100644 --- a/src/main/java/kz/konturai/parser/controller/HealthCheckController.java +++ b/src/main/java/kz/konturai/parser/controller/HealthCheckController.java @@ -4,6 +4,7 @@ import kz.konturai.parser.dto.ApiResponse; import kz.konturai.parser.dto.HealthCheckDto; import kz.konturai.parser.service.MarketItemService; import kz.konturai.parser.service.ParserManagerService; +import kz.konturai.parser.util.RssUrlValidator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -26,6 +27,9 @@ public class HealthCheckController { @Autowired private ParserManagerService parserManagerService; + @Autowired + private RssUrlValidator rssUrlValidator; + /** * Проверка состояния парсера */ @@ -150,4 +154,50 @@ public class HealthCheckController { .body(ApiResponse.error("Ошибка при проверке парсеров: " + e.getMessage())); } } + + /** + * Проверка доступности RSS-лент + */ + @GetMapping("/rss") + public ResponseEntity>> checkRssFeeds() { + try { + // Получаем текущие URL из конфигурации + java.util.Map rssUrls = new java.util.HashMap<>(); + rssUrls.put("kursiv", "https://kursiv.media/feed/"); + rssUrls.put("kapital", "https://kapital.kz/rss/"); + rssUrls.put("lsm", "https://lsm.kz/rss"); + rssUrls.put("rbc", "https://static.feed.rbc.ru/rbc/logical/footer/news.rss"); + rssUrls.put("vedomosti", "https://www.vedomosti.ru/rss/rubric/technology/internet"); + + // Проверяем доступность + java.util.Map accessibilityResults = rssUrlValidator.checkAllRssUrls(rssUrls); + + // Формируем детальный отчет + java.util.Map report = new java.util.HashMap<>(); + report.put("overallStatus", + accessibilityResults.values().stream().allMatch(Boolean::booleanValue) ? "UP" : "DEGRADED"); + report.put("accessibilityResults", accessibilityResults); + report.put("totalFeeds", rssUrls.size()); + report.put("accessibleFeeds", accessibilityResults.values().stream().mapToInt(b -> b ? 1 : 0).sum()); + report.put("unaccessibleFeeds", accessibilityResults.values().stream().mapToInt(b -> b ? 0 : 1).sum()); + + // Добавляем рекомендации по альтернативным URL + java.util.Map recommendations = new java.util.HashMap<>(); + for (String source : rssUrls.keySet()) { + if (!accessibilityResults.getOrDefault(source, false)) { + String alternativeUrl = rssUrlValidator.findWorkingUrl(source); + if (alternativeUrl != null) { + recommendations.put(source, alternativeUrl); + } + } + } + report.put("recommendations", recommendations); + + return ResponseEntity.ok(ApiResponse.success("Проверка RSS-лент завершена", report)); + + } catch (Exception e) { + return ResponseEntity.internalServerError() + .body(ApiResponse.error("Ошибка при проверке RSS-лент: " + e.getMessage())); + } + } } diff --git a/src/main/java/kz/konturai/parser/service/KapitalParserService.java b/src/main/java/kz/konturai/parser/service/KapitalParserService.java index e8fb4b0..3efcc3e 100644 --- a/src/main/java/kz/konturai/parser/service/KapitalParserService.java +++ b/src/main/java/kz/konturai/parser/service/KapitalParserService.java @@ -102,10 +102,31 @@ public class KapitalParserService implements ParserService { * Получение RSS-ленты */ private SyndFeed getRssFeed() throws Exception { - URL feedUrl = URI.create(rssFeedUrl).toURL(); - SyndFeedInput input = new SyndFeedInput(); - try (XmlReader reader = new XmlReader(feedUrl.openStream())) { - return input.build(reader); + try { + URL feedUrl = URI.create(rssFeedUrl).toURL(); + SyndFeedInput input = new SyndFeedInput(); + + // Добавляем таймаут и обработку ошибок + java.net.URLConnection connection = feedUrl.openConnection(); + connection.setConnectTimeout(10000); // 10 секунд + connection.setReadTimeout(30000); // 30 секунд + connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"); + + try (XmlReader reader = new XmlReader(connection.getInputStream())) { + return input.build(reader); + } + } catch (com.rometools.rome.io.ParsingFeedException e) { + logger.error("Ошибка парсинга XML для Kapital.kz: {}", e.getMessage()); + throw new Exception("Невалидный XML в RSS-ленте Kapital.kz: " + e.getMessage(), e); + } catch (java.net.UnknownHostException e) { + logger.error("Не удается подключиться к серверу Kapital.kz: {}", e.getMessage()); + throw new Exception("Сервер Kapital.kz недоступен: " + e.getMessage(), e); + } catch (java.net.SocketTimeoutException e) { + logger.error("Таймаут при подключении к Kapital.kz: {}", e.getMessage()); + throw new Exception("Таймаут подключения к Kapital.kz: " + e.getMessage(), e); + } catch (Exception e) { + logger.error("Общая ошибка при получении RSS-ленты Kapital.kz: {}", e.getMessage()); + throw new Exception("Ошибка получения RSS-ленты Kapital.kz: " + e.getMessage(), e); } } diff --git a/src/main/java/kz/konturai/parser/service/KursivParserService.java b/src/main/java/kz/konturai/parser/service/KursivParserService.java index 82cc3e2..ec3fbfa 100644 --- a/src/main/java/kz/konturai/parser/service/KursivParserService.java +++ b/src/main/java/kz/konturai/parser/service/KursivParserService.java @@ -102,10 +102,31 @@ public class KursivParserService implements ParserService { * Получение RSS-ленты */ private SyndFeed getRssFeed() throws Exception { - URL feedUrl = URI.create(rssFeedUrl).toURL(); - SyndFeedInput input = new SyndFeedInput(); - try (XmlReader reader = new XmlReader(feedUrl.openStream())) { - return input.build(reader); + try { + URL feedUrl = URI.create(rssFeedUrl).toURL(); + SyndFeedInput input = new SyndFeedInput(); + + // Добавляем таймаут и обработку ошибок + java.net.URLConnection connection = feedUrl.openConnection(); + connection.setConnectTimeout(10000); // 10 секунд + connection.setReadTimeout(30000); // 30 секунд + connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"); + + try (XmlReader reader = new XmlReader(connection.getInputStream())) { + return input.build(reader); + } + } catch (com.rometools.rome.io.ParsingFeedException e) { + logger.error("Ошибка парсинга XML для Kursiv: {}", e.getMessage()); + throw new Exception("Невалидный XML в RSS-ленте Kursiv: " + e.getMessage(), e); + } catch (java.net.UnknownHostException e) { + logger.error("Не удается подключиться к серверу Kursiv: {}", e.getMessage()); + throw new Exception("Сервер Kursiv недоступен: " + e.getMessage(), e); + } catch (java.net.SocketTimeoutException e) { + logger.error("Таймаут при подключении к Kursiv: {}", e.getMessage()); + throw new Exception("Таймаут подключения к Kursiv: " + e.getMessage(), e); + } catch (Exception e) { + logger.error("Общая ошибка при получении RSS-ленты Kursiv: {}", e.getMessage()); + throw new Exception("Ошибка получения RSS-ленты Kursiv: " + e.getMessage(), e); } } diff --git a/src/main/java/kz/konturai/parser/service/LsmParserService.java b/src/main/java/kz/konturai/parser/service/LsmParserService.java index 3d9b9fa..6b73e6f 100644 --- a/src/main/java/kz/konturai/parser/service/LsmParserService.java +++ b/src/main/java/kz/konturai/parser/service/LsmParserService.java @@ -102,10 +102,31 @@ public class LsmParserService implements ParserService { * Получение RSS-ленты */ private SyndFeed getRssFeed() throws Exception { - URL feedUrl = URI.create(rssFeedUrl).toURL(); - SyndFeedInput input = new SyndFeedInput(); - try (XmlReader reader = new XmlReader(feedUrl.openStream())) { - return input.build(reader); + try { + URL feedUrl = URI.create(rssFeedUrl).toURL(); + SyndFeedInput input = new SyndFeedInput(); + + // Добавляем таймаут и обработку ошибок + java.net.URLConnection connection = feedUrl.openConnection(); + connection.setConnectTimeout(10000); // 10 секунд + connection.setReadTimeout(30000); // 30 секунд + connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"); + + try (XmlReader reader = new XmlReader(connection.getInputStream())) { + return input.build(reader); + } + } catch (com.rometools.rome.io.ParsingFeedException e) { + logger.error("Ошибка парсинга XML для LSM.kz: {}", e.getMessage()); + throw new Exception("Невалидный XML в RSS-ленте LSM.kz: " + e.getMessage(), e); + } catch (java.net.UnknownHostException e) { + logger.error("Не удается подключиться к серверу LSM.kz: {}", e.getMessage()); + throw new Exception("Сервер LSM.kz недоступен: " + e.getMessage(), e); + } catch (java.net.SocketTimeoutException e) { + logger.error("Таймаут при подключении к LSM.kz: {}", e.getMessage()); + throw new Exception("Таймаут подключения к LSM.kz: " + e.getMessage(), e); + } catch (Exception e) { + logger.error("Общая ошибка при получении RSS-ленты LSM.kz: {}", e.getMessage()); + throw new Exception("Ошибка получения RSS-ленты LSM.kz: " + e.getMessage(), e); } } diff --git a/src/main/java/kz/konturai/parser/service/RbcParserService.java b/src/main/java/kz/konturai/parser/service/RbcParserService.java index c878a8f..bd5b11e 100644 --- a/src/main/java/kz/konturai/parser/service/RbcParserService.java +++ b/src/main/java/kz/konturai/parser/service/RbcParserService.java @@ -102,10 +102,31 @@ public class RbcParserService implements ParserService { * Получение RSS-ленты */ private SyndFeed getRssFeed() throws Exception { - URL feedUrl = URI.create(rssFeedUrl).toURL(); - SyndFeedInput input = new SyndFeedInput(); - try (XmlReader reader = new XmlReader(feedUrl.openStream())) { - return input.build(reader); + try { + URL feedUrl = URI.create(rssFeedUrl).toURL(); + SyndFeedInput input = new SyndFeedInput(); + + // Добавляем таймаут и обработку ошибок + java.net.URLConnection connection = feedUrl.openConnection(); + connection.setConnectTimeout(10000); // 10 секунд + connection.setReadTimeout(30000); // 30 секунд + connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"); + + try (XmlReader reader = new XmlReader(connection.getInputStream())) { + return input.build(reader); + } + } catch (com.rometools.rome.io.ParsingFeedException e) { + logger.error("Ошибка парсинга XML для РБК: {}", e.getMessage()); + throw new Exception("Невалидный XML в RSS-ленте РБК: " + e.getMessage(), e); + } catch (java.net.UnknownHostException e) { + logger.error("Не удается подключиться к серверу РБК: {}", e.getMessage()); + throw new Exception("Сервер РБК недоступен: " + e.getMessage(), e); + } catch (java.net.SocketTimeoutException e) { + logger.error("Таймаут при подключении к РБК: {}", e.getMessage()); + throw new Exception("Таймаут подключения к РБК: " + e.getMessage(), e); + } catch (Exception e) { + logger.error("Общая ошибка при получении RSS-ленты РБК: {}", e.getMessage()); + throw new Exception("Ошибка получения RSS-ленты РБК: " + e.getMessage(), e); } } diff --git a/src/main/java/kz/konturai/parser/service/VedomostiParserService.java b/src/main/java/kz/konturai/parser/service/VedomostiParserService.java index f5203af..6db7dba 100644 --- a/src/main/java/kz/konturai/parser/service/VedomostiParserService.java +++ b/src/main/java/kz/konturai/parser/service/VedomostiParserService.java @@ -102,10 +102,31 @@ public class VedomostiParserService implements ParserService { * Получение RSS-ленты */ private SyndFeed getRssFeed() throws Exception { - URL feedUrl = URI.create(rssFeedUrl).toURL(); - SyndFeedInput input = new SyndFeedInput(); - try (XmlReader reader = new XmlReader(feedUrl.openStream())) { - return input.build(reader); + try { + URL feedUrl = URI.create(rssFeedUrl).toURL(); + SyndFeedInput input = new SyndFeedInput(); + + // Добавляем таймаут и обработку ошибок + java.net.URLConnection connection = feedUrl.openConnection(); + connection.setConnectTimeout(10000); // 10 секунд + connection.setReadTimeout(30000); // 30 секунд + connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"); + + try (XmlReader reader = new XmlReader(connection.getInputStream())) { + return input.build(reader); + } + } catch (com.rometools.rome.io.ParsingFeedException e) { + logger.error("Ошибка парсинга XML для Ведомости: {}", e.getMessage()); + throw new Exception("Невалидный XML в RSS-ленте Ведомости: " + e.getMessage(), e); + } catch (java.net.UnknownHostException e) { + logger.error("Не удается подключиться к серверу Ведомости: {}", e.getMessage()); + throw new Exception("Сервер Ведомости недоступен: " + e.getMessage(), e); + } catch (java.net.SocketTimeoutException e) { + logger.error("Таймаут при подключении к Ведомости: {}", e.getMessage()); + throw new Exception("Таймаут подключения к Ведомости: " + e.getMessage(), e); + } catch (Exception e) { + logger.error("Общая ошибка при получении RSS-ленты Ведомости: {}", e.getMessage()); + throw new Exception("Ошибка получения RSS-ленты Ведомости: " + e.getMessage(), e); } } diff --git a/src/main/java/kz/konturai/parser/util/RssUrlValidator.java b/src/main/java/kz/konturai/parser/util/RssUrlValidator.java new file mode 100644 index 0000000..4d5a7c2 --- /dev/null +++ b/src/main/java/kz/konturai/parser/util/RssUrlValidator.java @@ -0,0 +1,136 @@ +package kz.konturai.parser.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +/** + * Утилита для проверки доступности RSS-лент + */ +@Component +public class RssUrlValidator { + + private static final Logger logger = LoggerFactory.getLogger(RssUrlValidator.class); + + /** + * Проверяет доступность RSS-ленты + */ + public boolean isRssUrlAccessible(String url) { + try { + URL feedUrl = URI.create(url).toURL(); + HttpURLConnection connection = (HttpURLConnection) feedUrl.openConnection(); + connection.setRequestMethod("HEAD"); + connection.setConnectTimeout(10000); + connection.setReadTimeout(10000); + connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"); + + int responseCode = connection.getResponseCode(); + connection.disconnect(); + + return responseCode == 200; + } catch (Exception e) { + logger.warn("RSS URL недоступен: {} - {}", url, e.getMessage()); + return false; + } + } + + /** + * Получает альтернативные URL для RSS-лент + */ + public Map getAlternativeUrls() { + Map alternatives = new HashMap<>(); + + // Альтернативные URL для Kapital.kz + alternatives.put("kapital", new String[] { + "https://kapital.kz/rss/", + "https://kapital.kz/feed/", + "https://kapital.kz/rss.xml" + }); + + // Альтернативные URL для Kursiv + alternatives.put("kursiv", new String[] { + "https://kursiv.media/feed/", + "https://kursiv.media/rss/", + "https://kursiv.media/rss.xml" + }); + + // Альтернативные URL для LSM.kz + alternatives.put("lsm", new String[] { + "https://lsm.kz/rss", + "https://lsm.kz/feed/", + "https://lsm.kz/rss.xml" + }); + + // Альтернативные URL для РБК + alternatives.put("rbc", new String[] { + "https://static.feed.rbc.ru/rbc/logical/footer/news.rss", + "https://rbc.ru/rss/", + "https://rbc.ru/feed/", + "https://www.rbc.ru/rss/", + "https://www.rbc.ru/feed/" + }); + + // Альтернативные URL для Ведомости + alternatives.put("vedomosti", new String[] { + "https://www.vedomosti.ru/rss/rubric/technology/internet", + "https://www.vedomosti.ru/rss/", + "https://www.vedomosti.ru/feed/", + "https://vedomosti.ru/rss/" + }); + + return alternatives; + } + + /** + * Находит первый доступный URL из списка альтернатив + */ + public String findWorkingUrl(String sourceName) { + Map alternatives = getAlternativeUrls(); + String[] urls = alternatives.get(sourceName); + + if (urls == null) { + return null; + } + + for (String url : urls) { + if (isRssUrlAccessible(url)) { + logger.info("Найден рабочий URL для {}: {}", sourceName, url); + return url; + } + } + + logger.warn("Не найдено рабочих URL для источника: {}", sourceName); + return null; + } + + /** + * Проверяет все RSS-ленты и возвращает отчет + */ + public Map checkAllRssUrls(Map rssUrls) { + Map results = new HashMap<>(); + + for (Map.Entry entry : rssUrls.entrySet()) { + String source = entry.getKey(); + String url = entry.getValue(); + + boolean isAccessible = isRssUrlAccessible(url); + results.put(source, isAccessible); + + if (!isAccessible) { + logger.warn("RSS URL недоступен: {} - {}", source, url); + String alternativeUrl = findWorkingUrl(source); + if (alternativeUrl != null) { + logger.info("Рекомендуется использовать альтернативный URL для {}: {}", source, alternativeUrl); + } + } + } + + return results; + } +} diff --git a/src/test/java/kz/konturai/parser/controller/SourceNamesEndpointTest.java b/src/test/java/kz/konturai/parser/controller/SourceNamesEndpointTest.java new file mode 100644 index 0000000..3e071ee --- /dev/null +++ b/src/test/java/kz/konturai/parser/controller/SourceNamesEndpointTest.java @@ -0,0 +1,88 @@ +package kz.konturai.parser.controller; + +import kz.konturai.parser.dto.ApiResponse; +import kz.konturai.parser.service.MarketItemService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class SourceNamesEndpointTest { + + @Mock + private MarketItemService marketItemService; + + @InjectMocks + private MarketItemController marketItemController; + + @BeforeEach + void setUp() { + // Setup common test data + } + + @Test + void testGetSources_Success() { + // Given + List mockSources = List.of( + "Kursiv (Бизнес/экономика)", + "Kapital.kz (Бизнес)", + "LSM.kz", + "РБК", + "Ведомости"); + when(marketItemService.getAllSourceNames()).thenReturn(mockSources); + + // When + ResponseEntity>> response = marketItemController.getSources(); + + // Then + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getBody()); + assertTrue(response.getBody().isSuccess()); + assertEquals(5, response.getBody().getData().size()); + assertTrue(response.getBody().getData().contains("Kursiv (Бизнес/экономика)")); + assertTrue(response.getBody().getData().contains("Kapital.kz (Бизнес)")); + assertTrue(response.getBody().getData().contains("LSM.kz")); + assertTrue(response.getBody().getData().contains("РБК")); + assertTrue(response.getBody().getData().contains("Ведомости")); + } + + @Test + void testGetSources_EmptyList() { + // Given + when(marketItemService.getAllSourceNames()).thenReturn(List.of()); + + // When + ResponseEntity>> response = marketItemController.getSources(); + + // Then + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getBody()); + assertTrue(response.getBody().isSuccess()); + assertTrue(response.getBody().getData().isEmpty()); + } + + @Test + void testGetSources_ServiceError() { + // Given + when(marketItemService.getAllSourceNames()).thenThrow(new RuntimeException("Database error")); + + // When + ResponseEntity>> response = marketItemController.getSources(); + + // Then + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); + assertNotNull(response.getBody()); + assertFalse(response.getBody().isSuccess()); + assertTrue(response.getBody().getMessage().contains("Ошибка при получении источников")); + } +}