1189 lines
32 KiB
Markdown
1189 lines
32 KiB
Markdown
# Руководство по API для фронтенда
|
||
|
||
## Обзор изменений
|
||
|
||
После рефакторинга старый `ParserController` был разделен на три специализированных контроллера для лучшей организации и масштабируемости:
|
||
|
||
- **`MarketItemController`** (`/api/items`) - для получения данных
|
||
- **`ParserAdminController`** (`/api/admin/parsers`) - для управления парсерами
|
||
- **`HealthCheckController`** (`/api/health`) - для мониторинга системы
|
||
|
||
---
|
||
|
||
## 📊 MarketItemController - Получение данных
|
||
|
||
**Базовый URL**: `/api/items`
|
||
|
||
### 1. Получение списка новостей с фильтрацией и пагинацией
|
||
|
||
```http
|
||
GET /api/items
|
||
```
|
||
|
||
**Параметры запроса:**
|
||
|
||
- `sourceName` (optional) - фильтр по источнику (например: "Kursiv Media", "Kapital.kz", "LSM.kz", "РБК", "Ведомости")
|
||
- `startDate` (optional) - начальная дата в формате ISO (например: "2024-01-01T00:00:00")
|
||
- `endDate` (optional) - конечная дата в формате ISO (например: "2024-12-31T23:59:59")
|
||
- `page` (optional) - номер страницы (по умолчанию: 0)
|
||
- `size` (optional) - размер страницы (по умолчанию: 20)
|
||
- `sort` (optional) - сортировка (по умолчанию: "publishedAt,desc")
|
||
|
||
**Примеры запросов:**
|
||
|
||
```javascript
|
||
// Получить все новости
|
||
fetch('/api/items');
|
||
|
||
// Получить новости с пагинацией
|
||
fetch('/api/items?page=0&size=10');
|
||
|
||
// Получить новости от конкретного источника
|
||
fetch('/api/items?sourceName=Kursiv Media');
|
||
|
||
// Получить новости за последние 7 дней
|
||
const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
||
fetch(`/api/items?startDate=${weekAgo}`);
|
||
|
||
// Получить новости с сортировкой по дате (старые сначала)
|
||
fetch('/api/items?sort=publishedAt,asc');
|
||
```
|
||
|
||
**Ответ:**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Записи получены успешно",
|
||
"data": {
|
||
"content": [
|
||
{
|
||
"id": "64f1a2b3c4d5e6f7g8h9i0j1",
|
||
"sourceName": "Kursiv Media",
|
||
"url": "https://kursiv.media/news/example",
|
||
"title": "Заголовок новости",
|
||
"publishedAt": "2024-01-15T10:30:00",
|
||
"addedAt": "2024-01-15T10:35:00",
|
||
"rawText": "Текст новости без HTML тегов",
|
||
"hash": "abc123def456...",
|
||
"category": "Бизнес",
|
||
"analytics": {
|
||
"summary": null,
|
||
"sentiment": null,
|
||
"tags": [],
|
||
"entities": {}
|
||
}
|
||
}
|
||
],
|
||
"pageable": {
|
||
"sort": {
|
||
"sorted": true,
|
||
"unsorted": false
|
||
},
|
||
"pageNumber": 0,
|
||
"pageSize": 20,
|
||
"offset": 0,
|
||
"paged": true,
|
||
"unpaged": false
|
||
},
|
||
"totalElements": 150,
|
||
"totalPages": 8,
|
||
"last": false,
|
||
"first": true,
|
||
"numberOfElements": 20,
|
||
"size": 20,
|
||
"number": 0,
|
||
"empty": false
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2. Получение новости по ID
|
||
|
||
```http
|
||
GET /api/items/{id}
|
||
```
|
||
|
||
**Пример:**
|
||
|
||
```javascript
|
||
fetch('/api/items/64f1a2b3c4d5e6f7g8h9i0j1');
|
||
```
|
||
|
||
**Ответ:**
|
||
|
||
```json
|
||
{
|
||
"success": false,
|
||
"message": "Функция поиска по ID пока не реализована"
|
||
}
|
||
```
|
||
|
||
### 3. Получение статистики
|
||
|
||
```http
|
||
GET /api/items/stats
|
||
```
|
||
|
||
**Ответ:**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Статистика получена успешно",
|
||
"data": {
|
||
"totalItems": 1250,
|
||
"sourceStatistics": {
|
||
"Kursiv Media": 300,
|
||
"Kapital.kz": 250,
|
||
"LSM.kz": 200,
|
||
"РБК": 300,
|
||
"Ведомости": 200
|
||
},
|
||
"lastUpdate": "2024-01-15T10:35:00"
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4. Получение списка источников (sourceName)
|
||
|
||
```http
|
||
GET /api/items/sources
|
||
```
|
||
|
||
**Описание:**
|
||
Возвращает список всех уникальных источников новостей, которые есть в базе данных. Этот эндпоинт полезен для:
|
||
|
||
- Построения фильтров по источникам
|
||
- Отображения списка доступных источников в UI
|
||
- Динамического создания выпадающих списков
|
||
|
||
**Ответ:**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Источники получены успешно",
|
||
"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
|
||
GET /api/items/source/{sourceName}
|
||
```
|
||
|
||
**Пример:**
|
||
|
||
```javascript
|
||
fetch('/api/items/source/Kursiv Media?page=0&size=10');
|
||
```
|
||
|
||
**Ответ:** Аналогичен ответу от `/api/items`, но только с новостями от указанного источника.
|
||
|
||
---
|
||
|
||
## ⚙️ ParserAdminController - Управление парсерами
|
||
|
||
**Базовый URL**: `/api/admin/parsers`
|
||
|
||
> ⚠️ **Важно**: Эти эндпоинты предназначены для административных функций и могут потребовать аутентификации в будущем.
|
||
|
||
### 1. Запуск конкретного парсера
|
||
|
||
```http
|
||
POST /api/admin/parsers/parse/{sourceName}
|
||
```
|
||
|
||
**Доступные источники:**
|
||
|
||
- `kursiv`
|
||
- `kapital`
|
||
- `lsm`
|
||
- `rbc`
|
||
- `vedomosti`
|
||
|
||
**Пример:**
|
||
|
||
```javascript
|
||
// Запустить парсер Kursiv
|
||
fetch('/api/admin/parsers/parse/kursiv', { method: 'POST' });
|
||
|
||
// Запустить парсер РБК
|
||
fetch('/api/admin/parsers/parse/rbc', { method: 'POST' });
|
||
```
|
||
|
||
**Ответ:**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Парсинг kursiv завершен",
|
||
"data": {
|
||
"source": "kursiv",
|
||
"processedItems": 15,
|
||
"totalItemsInDb": 1265,
|
||
"status": "completed"
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2. Запуск всех парсеров
|
||
|
||
```http
|
||
POST /api/admin/parsers/parse/all
|
||
```
|
||
|
||
**Пример:**
|
||
|
||
```javascript
|
||
fetch('/api/admin/parsers/parse/all', { method: 'POST' });
|
||
```
|
||
|
||
**Ответ:**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Парсинг всех источников завершен",
|
||
"data": [
|
||
{
|
||
"source": "kursiv",
|
||
"processedItems": 12,
|
||
"totalItemsInDb": 1277,
|
||
"status": "completed"
|
||
},
|
||
{
|
||
"source": "kapital",
|
||
"processedItems": 8,
|
||
"totalItemsInDb": 1285,
|
||
"status": "completed"
|
||
},
|
||
{
|
||
"source": "lsm",
|
||
"processedItems": 5,
|
||
"totalItemsInDb": 1290,
|
||
"status": "completed"
|
||
},
|
||
{
|
||
"source": "rbc",
|
||
"processedItems": 20,
|
||
"totalItemsInDb": 1310,
|
||
"status": "completed"
|
||
},
|
||
{
|
||
"source": "vedomosti",
|
||
"processedItems": 7,
|
||
"totalItemsInDb": 1317,
|
||
"status": "completed"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
### 3. Получение списка доступных парсеров
|
||
|
||
```http
|
||
GET /api/admin/parsers
|
||
```
|
||
|
||
**Ответ:**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Список парсеров получен",
|
||
"data": ["kapital", "kursiv", "lsm", "rbc", "vedomosti"]
|
||
}
|
||
```
|
||
|
||
### 4. Получение информации о парсерах
|
||
|
||
```http
|
||
GET /api/admin/parsers/info
|
||
```
|
||
|
||
**Ответ:**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Информация о парсерах получена",
|
||
"data": {
|
||
"totalParsers": 5,
|
||
"availableParsers": ["kapital", "kursiv", "lsm", "rbc", "vedomosti"],
|
||
"totalItems": 1317,
|
||
"sourceStatistics": {
|
||
"Kursiv Media": 300,
|
||
"Kapital.kz": 250,
|
||
"LSM.kz": 200,
|
||
"РБК": 300,
|
||
"Ведомости": 200
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 5. Проверка существования парсера
|
||
|
||
```http
|
||
GET /api/admin/parsers/exists/{sourceName}
|
||
```
|
||
|
||
**Пример:**
|
||
|
||
```javascript
|
||
fetch('/api/admin/parsers/exists/kursiv');
|
||
```
|
||
|
||
**Ответ:**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Проверка завершена",
|
||
"data": true
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🔍 HealthCheckController - Мониторинг системы
|
||
|
||
**Базовый URL**: `/api/health`
|
||
|
||
### 1. Базовая проверка состояния
|
||
|
||
```http
|
||
GET /api/health
|
||
```
|
||
|
||
**Ответ:**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Сервис работает",
|
||
"data": {
|
||
"status": "UP",
|
||
"service": "RSS Parser System",
|
||
"timestamp": 1705312500000,
|
||
"details": "Enabled - runs every 30 minutes"
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2. Проверка подключения к MongoDB
|
||
|
||
```http
|
||
GET /api/health/mongodb
|
||
```
|
||
|
||
**Ответ (успех):**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "MongoDB подключение успешно",
|
||
"data": {
|
||
"status": "UP",
|
||
"service": "MongoDB подключение успешно",
|
||
"timestamp": 1705312500000,
|
||
"details": "Всего записей: 1317"
|
||
}
|
||
}
|
||
```
|
||
|
||
**Ответ (ошибка):**
|
||
|
||
```json
|
||
{
|
||
"success": false,
|
||
"message": "Ошибка подключения к MongoDB",
|
||
"data": {
|
||
"status": "DOWN",
|
||
"service": "Ошибка подключения к MongoDB: Connection refused",
|
||
"timestamp": 1705312500000,
|
||
"suggestion": "Проверьте учетные данные в application.properties"
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3. Информация о планировщике
|
||
|
||
```http
|
||
GET /api/health/scheduler
|
||
```
|
||
|
||
**Ответ:**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Информация о планировщике получена",
|
||
"data": {
|
||
"schedulerEnabled": true,
|
||
"cronExpression": "0 0/30 * * * ?",
|
||
"description": "Запуск каждые 30 минут (в 0 и 30 минут каждого часа)",
|
||
"nextRun": "Следующий запуск будет в ближайшие 0 или 30 минут часа",
|
||
"activeParsers": ["kapital", "kursiv", "lsm", "rbc", "vedomosti"],
|
||
"totalParsers": 5,
|
||
"sources": ["Kursiv Media", "Kapital.kz", "LSM.kz", "РБК", "Ведомости"]
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4. Общая информация о системе
|
||
|
||
```http
|
||
GET /api/health/system
|
||
```
|
||
|
||
**Ответ:**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Информация о системе получена",
|
||
"data": {
|
||
"parsers": {
|
||
"total": 5,
|
||
"available": ["kapital", "kursiv", "lsm", "rbc", "vedomosti"]
|
||
},
|
||
"data": {
|
||
"totalItems": 1317,
|
||
"sourceStatistics": {
|
||
"Kursiv Media": 300,
|
||
"Kapital.kz": 250,
|
||
"LSM.kz": 200,
|
||
"РБК": 300,
|
||
"Ведомости": 200
|
||
}
|
||
},
|
||
"system": {
|
||
"javaVersion": "17.0.2",
|
||
"osName": "Linux",
|
||
"osVersion": "5.4.0-74-generic",
|
||
"uptime": 1705312500000
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 5. Проверка состояния парсеров
|
||
|
||
```http
|
||
GET /api/health/parsers
|
||
```
|
||
|
||
**Ответ:**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "Проверка парсеров завершена",
|
||
"data": {
|
||
"totalParsers": 5,
|
||
"availableParsers": ["kapital", "kursiv", "lsm", "rbc", "vedomosti"],
|
||
"status": "UP",
|
||
"message": "Все парсеры доступны"
|
||
}
|
||
}
|
||
```
|
||
|
||
### 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>
|
||
);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🚀 Примеры использования для фронтенда
|
||
|
||
### React/JavaScript примеры
|
||
|
||
```javascript
|
||
// Сервис для работы с API
|
||
class NewsApiService {
|
||
constructor(baseUrl = '') {
|
||
this.baseUrl = baseUrl;
|
||
}
|
||
|
||
// Получить новости с фильтрацией
|
||
async getNews(filters = {}) {
|
||
const params = new URLSearchParams();
|
||
|
||
if (filters.sourceName) params.append('sourceName', filters.sourceName);
|
||
if (filters.startDate) params.append('startDate', filters.startDate);
|
||
if (filters.endDate) params.append('endDate', filters.endDate);
|
||
if (filters.page !== undefined) params.append('page', filters.page);
|
||
if (filters.size) params.append('size', filters.size);
|
||
if (filters.sort) params.append('sort', filters.sort);
|
||
|
||
const response = await fetch(`${this.baseUrl}/api/items?${params}`);
|
||
return response.json();
|
||
}
|
||
|
||
// Получить статистику
|
||
async getStats() {
|
||
const response = await fetch(`${this.baseUrl}/api/items/stats`);
|
||
return response.json();
|
||
}
|
||
|
||
// Получить источники
|
||
async getSources() {
|
||
const response = await fetch(`${this.baseUrl}/api/items/sources`);
|
||
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(
|
||
`${this.baseUrl}/api/admin/parsers/parse/${sourceName}`,
|
||
{
|
||
method: 'POST',
|
||
}
|
||
);
|
||
return response.json();
|
||
}
|
||
|
||
// Запустить все парсеры (админ функция)
|
||
async runAllParsers() {
|
||
const response = await fetch(
|
||
`${this.baseUrl}/api/admin/parsers/parse/all`,
|
||
{
|
||
method: 'POST',
|
||
}
|
||
);
|
||
return response.json();
|
||
}
|
||
|
||
// Проверить состояние системы
|
||
async getHealthStatus() {
|
||
const response = await fetch(`${this.baseUrl}/api/health`);
|
||
return response.json();
|
||
}
|
||
}
|
||
|
||
// Использование
|
||
const apiService = new NewsApiService();
|
||
|
||
// Получить последние новости
|
||
const news = await apiService.getNews({
|
||
page: 0,
|
||
size: 20,
|
||
sort: 'publishedAt,desc',
|
||
});
|
||
|
||
// Получить новости от конкретного источника
|
||
const kursivNews = await apiService.getNews({
|
||
sourceName: 'Kursiv Media',
|
||
page: 0,
|
||
size: 10,
|
||
});
|
||
|
||
// Получить новости за последнюю неделю
|
||
const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
||
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 примеры
|
||
|
||
```javascript
|
||
// composable для работы с API
|
||
export function useNewsApi() {
|
||
const baseUrl = '';
|
||
|
||
const getNews = async (filters = {}) => {
|
||
const params = new URLSearchParams();
|
||
Object.entries(filters).forEach(([key, value]) => {
|
||
if (value !== undefined && value !== null) {
|
||
params.append(key, value);
|
||
}
|
||
});
|
||
|
||
const response = await fetch(`${baseUrl}/api/items?${params}`);
|
||
return response.json();
|
||
};
|
||
|
||
const getStats = async () => {
|
||
const response = await fetch(`${baseUrl}/api/items/stats`);
|
||
return response.json();
|
||
};
|
||
|
||
const getSources = async () => {
|
||
const response = await fetch(`${baseUrl}/api/items/sources`);
|
||
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`.
|
||
|
||
2. **Сортировка**: По умолчанию новости сортируются по дате публикации (новые сначала). Можно изменить через параметр `sort`.
|
||
|
||
3. **Фильтрация**: Поддерживается фильтрация по источнику и диапазону дат.
|
||
|
||
4. **Формат дат**: Используйте ISO 8601 формат для дат (например: "2024-01-15T10:30:00").
|
||
|
||
5. **Обработка ошибок**: Все ответы содержат поле `success` для проверки успешности операции.
|
||
|
||
6. **Административные функции**: Эндпоинты `/api/admin/parsers/*` предназначены для административных функций.
|
||
|
||
7. **Мониторинг**: Используйте эндпоинты `/api/health/*` для проверки состояния системы.
|
||
|
||
---
|
||
|
||
## 🔄 Миграция с старого API
|
||
|
||
Если у вас был код, использующий старый `ParserController`, вот соответствие эндпоинтов:
|
||
|
||
| Старый эндпоинт | Новый эндпоинт |
|
||
| ---------------------------------- | ----------------------------------------- |
|
||
| `POST /api/parser/parse/kursiv` | `POST /api/admin/parsers/parse/kursiv` |
|
||
| `POST /api/parser/parse/kapital` | `POST /api/admin/parsers/parse/kapital` |
|
||
| `POST /api/parser/parse/lsm` | `POST /api/admin/parsers/parse/lsm` |
|
||
| `POST /api/parser/parse/rbc` | `POST /api/admin/parsers/parse/rbc` |
|
||
| `POST /api/parser/parse/vedomosti` | `POST /api/admin/parsers/parse/vedomosti` |
|
||
| `POST /api/parser/parse/all` | `POST /api/admin/parsers/parse/all` |
|
||
| `GET /api/parser/items` | `GET /api/items` |
|
||
| `GET /api/parser/stats` | `GET /api/items/stats` |
|
||
| `GET /api/parser/health` | `GET /api/health` |
|
||
| `GET /api/parser/health/mongodb` | `GET /api/health/mongodb` |
|
||
| `GET /api/parser/scheduler/info` | `GET /api/health/scheduler` |
|