Files
marketing/FRONTEND_API_GUIDE.md
T
2025-09-15 09:31:51 +05:00

1045 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Руководство по 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": "Все парсеры доступны"
}
}
```
---
## 🚀 Примеры использования для фронтенда
### 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` |