.
This commit is contained in:
+525
-2
@@ -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 (
|
||||
<select onChange={(e) => onSourceChange(e.target.value)}>
|
||||
<option value=''>Все источники</option>
|
||||
{sources.map((source) => (
|
||||
<option key={source} value={source}>
|
||||
{source}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
// Использование в 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 <div>Загрузка...</div>;
|
||||
|
||||
return (
|
||||
<div className="rss-status">
|
||||
<h3>Статус RSS-лент</h3>
|
||||
<div className={`status ${rssStatus.overallStatus.toLowerCase()}`}>
|
||||
{rssStatus.overallStatus === 'UP' ? '✅ Все ленты доступны' :
|
||||
rssStatus.overallStatus === 'DEGRADED' ? '⚠️ Некоторые ленты недоступны' :
|
||||
'❌ Ленты недоступны'}
|
||||
</div>
|
||||
|
||||
<div className="feeds-list">
|
||||
{Object.entries(rssStatus.accessibilityResults).map(([source, isAccessible]) => (
|
||||
<div key={source} className={`feed-item ${isAccessible ? 'accessible' : 'inaccessible'}`}>
|
||||
<span className="source">{source}</span>
|
||||
<span className="status">
|
||||
{isAccessible ? '✅' : '❌'}
|
||||
</span>
|
||||
{!isAccessible && rssStatus.recommendations[source] && (
|
||||
<div className="recommendation">
|
||||
Рекомендация: {rssStatus.recommendations[source]}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Примеры использования для фронтенда
|
||||
@@ -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 (
|
||||
<div className='news-filter'>
|
||||
<label htmlFor='source-filter'>Фильтр по источнику:</label>
|
||||
<select
|
||||
id='source-filter'
|
||||
value={selectedSource}
|
||||
onChange={(e) => handleSourceChange(e.target.value)}
|
||||
>
|
||||
<option value=''>Все источники</option>
|
||||
{sources.map((source) => (
|
||||
<option key={source} value={source}>
|
||||
{source}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default NewsFilter;
|
||||
```
|
||||
|
||||
### 2. Отображение статистики по источникам
|
||||
|
||||
```javascript
|
||||
// Vue.js компонент для отображения статистики
|
||||
<template>
|
||||
<div class="sources-stats">
|
||||
<h3>Статистика по источникам</h3>
|
||||
<div v-if="loading" class="loading">Загрузка...</div>
|
||||
<div v-else class="stats-grid">
|
||||
<div
|
||||
v-for="source in sources"
|
||||
:key="source"
|
||||
class="stat-item"
|
||||
@click="filterBySource(source)"
|
||||
>
|
||||
<span class="source-name">{{ source }}</span>
|
||||
<span class="source-count">{{ getSourceCount(source) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
sources: [],
|
||||
loading: true,
|
||||
stats: {}
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadSources();
|
||||
await this.loadStats();
|
||||
this.loading = false;
|
||||
},
|
||||
methods: {
|
||||
async loadSources() {
|
||||
try {
|
||||
const response = await fetch('/api/items/sources');
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
this.sources = result.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки источников:', error);
|
||||
}
|
||||
},
|
||||
async loadStats() {
|
||||
try {
|
||||
const response = await fetch('/api/items/stats');
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
this.stats = result.data.sourceStatistics;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки статистики:', error);
|
||||
}
|
||||
},
|
||||
getSourceCount(source) {
|
||||
return this.stats[source] || 0;
|
||||
},
|
||||
filterBySource(source) {
|
||||
this.$emit('filter-change', { sourceName: source });
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 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<string[]>([]);
|
||||
public sources$ = this.sourcesSubject.asObservable();
|
||||
|
||||
constructor(private http: HttpClient) {
|
||||
this.loadSources();
|
||||
}
|
||||
|
||||
private loadSources(): void {
|
||||
this.http.get<any>('/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<string[]> {
|
||||
return this.sources$.pipe(
|
||||
map(sources =>
|
||||
sources.filter(source =>
|
||||
source.toLowerCase().includes(query.toLowerCase())
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
getSources(): Observable<string[]> {
|
||||
return this.sources$;
|
||||
}
|
||||
}
|
||||
|
||||
// Компонент автодополнения
|
||||
@Component({
|
||||
selector: 'app-source-autocomplete',
|
||||
template: `
|
||||
<div class="autocomplete">
|
||||
<input
|
||||
[(ngModel)]="searchQuery"
|
||||
(input)="onSearch($event.target.value)"
|
||||
placeholder="Поиск источника..."
|
||||
class="search-input"
|
||||
/>
|
||||
<div *ngIf="filteredSources.length > 0" class="suggestions">
|
||||
<div
|
||||
*ngFor="let source of filteredSources"
|
||||
(click)="selectSource(source)"
|
||||
class="suggestion-item"
|
||||
>
|
||||
{{ source }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
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`.
|
||||
|
||||
Reference in New Issue
Block a user