This commit is contained in:
root
2025-09-14 18:33:59 +05:00
parent a0dc055868
commit 95c09289d6
9 changed files with 924 additions and 22 deletions
+525 -2
View File
@@ -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`.