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 компонент для отображения статистики
+
+
+
Статистика по источникам
+
Загрузка...
+
+
+ {{ source }}
+ {{ getSourceCount(source) }}
+
+
+
+
+
+
+```
+
+### 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: `
+
+
+
0" class="suggestions">
+
+ {{ 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("Ошибка при получении источников"));
+ }
+}