parser is ready
This commit is contained in:
@@ -0,0 +1,340 @@
|
|||||||
|
# Руководство по API для фронтенда
|
||||||
|
|
||||||
|
## Обзор
|
||||||
|
|
||||||
|
После рефакторинга ParserController теперь поддерживает эффективную работу с фронтендом через строго типизированные DTO и расширенные возможности фильтрации и пагинации.
|
||||||
|
|
||||||
|
## Основные изменения
|
||||||
|
|
||||||
|
### 1. Строгая типизация ответов
|
||||||
|
|
||||||
|
Все ответы API теперь используют DTO классы вместо `Map<String, Object>`:
|
||||||
|
|
||||||
|
- `ApiResponse<T>` - универсальный wrapper для всех ответов
|
||||||
|
- `ParserResultDto` - результат парсинга
|
||||||
|
- `ParserStatsDto` - статистика системы
|
||||||
|
- `HealthCheckDto` - состояние сервиса
|
||||||
|
|
||||||
|
### 2. Пагинация и сортировка
|
||||||
|
|
||||||
|
Поддержка Spring Data пагинации с параметрами:
|
||||||
|
|
||||||
|
- `page` - номер страницы (начиная с 0)
|
||||||
|
- `size` - количество элементов на странице
|
||||||
|
- `sort` - поле для сортировки и направление
|
||||||
|
|
||||||
|
### 3. Фильтрация данных
|
||||||
|
|
||||||
|
Возможность фильтрации по:
|
||||||
|
|
||||||
|
- `sourceName` - источнику новостей
|
||||||
|
- `startDate` - начальной дате
|
||||||
|
- `endDate` - конечной дате
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### 1. Получение записей с фильтрацией и пагинацией
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/parser/items?page=0&size=10&sort=publishedAt,desc&sourceName=Kursiv&startDate=2025-01-01&endDate=2025-01-31
|
||||||
|
```
|
||||||
|
|
||||||
|
**Параметры:**
|
||||||
|
|
||||||
|
- `page` (optional, default: 0) - номер страницы
|
||||||
|
- `size` (optional, default: 20) - размер страницы
|
||||||
|
- `sort` (optional, default: "publishedAt,desc") - сортировка
|
||||||
|
- `sourceName` (optional) - фильтр по источнику
|
||||||
|
- `startDate` (optional) - начальная дата (ISO format)
|
||||||
|
- `endDate` (optional) - конечная дата (ISO format)
|
||||||
|
|
||||||
|
**Ответ:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Записи получены успешно",
|
||||||
|
"data": {
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"id": "...",
|
||||||
|
"sourceName": "Kursiv (Бизнес/экономика)",
|
||||||
|
"title": "Заголовок новости",
|
||||||
|
"url": "https://kursiv.media/...",
|
||||||
|
"publishedAt": "2025-01-14T10:30:00",
|
||||||
|
"addedAt": "2025-01-14T12:00:00",
|
||||||
|
"rawText": "Текст статьи...",
|
||||||
|
"hash": "...",
|
||||||
|
"category": "Бизнес",
|
||||||
|
"analytics": {
|
||||||
|
"summary": null,
|
||||||
|
"sentiment": null,
|
||||||
|
"tags": [],
|
||||||
|
"entities": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pageable": {
|
||||||
|
"sort": {
|
||||||
|
"sorted": true,
|
||||||
|
"unsorted": false,
|
||||||
|
"empty": false
|
||||||
|
},
|
||||||
|
"pageNumber": 0,
|
||||||
|
"pageSize": 10,
|
||||||
|
"offset": 0,
|
||||||
|
"paged": true,
|
||||||
|
"unpaged": false
|
||||||
|
},
|
||||||
|
"totalElements": 25,
|
||||||
|
"totalPages": 3,
|
||||||
|
"last": false,
|
||||||
|
"first": true,
|
||||||
|
"numberOfElements": 10,
|
||||||
|
"size": 10,
|
||||||
|
"number": 0,
|
||||||
|
"sort": {
|
||||||
|
"sorted": true,
|
||||||
|
"unsorted": false,
|
||||||
|
"empty": false
|
||||||
|
},
|
||||||
|
"empty": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Статистика системы
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/parser/stats
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ответ:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Статистика получена успешно",
|
||||||
|
"data": {
|
||||||
|
"totalItems": 150,
|
||||||
|
"sourceStatistics": {
|
||||||
|
"Kursiv (Бизнес/экономика)": 75,
|
||||||
|
"Kapital.kz (Бизнес)": 75
|
||||||
|
},
|
||||||
|
"lastUpdate": "2025-01-14T12:00:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Запуск парсинга
|
||||||
|
|
||||||
|
#### Парсинг всех источников
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/parser/parse/all
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ответ:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Парсинг всех источников завершен успешно",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"source": "Kursiv",
|
||||||
|
"processedItems": 15,
|
||||||
|
"totalItemsInDb": 90,
|
||||||
|
"status": "completed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "Kapital",
|
||||||
|
"processedItems": 12,
|
||||||
|
"totalItemsInDb": 90,
|
||||||
|
"status": "completed"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Парсинг конкретного источника
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/parser/parse/kursiv
|
||||||
|
POST /api/parser/parse/kapital
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Проверка состояния
|
||||||
|
|
||||||
|
#### Общее состояние
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/parser/health
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ответ:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Сервис работает",
|
||||||
|
"data": {
|
||||||
|
"status": "UP",
|
||||||
|
"service": "RSS Parser System",
|
||||||
|
"timestamp": 1705123456789,
|
||||||
|
"scheduler": "Enabled - runs every 30 minutes"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Состояние MongoDB
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/parser/health/mongodb
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ответ при успехе:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "MongoDB подключение успешно",
|
||||||
|
"data": {
|
||||||
|
"status": "UP",
|
||||||
|
"message": "MongoDB подключение успешно",
|
||||||
|
"timestamp": 1705123456789
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ответ при ошибке:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Ошибка подключения к MongoDB",
|
||||||
|
"data": {
|
||||||
|
"status": "DOWN",
|
||||||
|
"message": "Ошибка подключения к MongoDB: Command failed with error 13 (Unauthorized)",
|
||||||
|
"timestamp": 1705123456789,
|
||||||
|
"suggestion": "Проверьте учетные данные в application.properties"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Примеры использования
|
||||||
|
|
||||||
|
### 1. Получение последних новостей с пагинацией
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Получить первую страницу с 10 записями, отсортированными по дате публикации
|
||||||
|
fetch('/api/parser/items?page=0&size=10&sort=publishedAt,desc')
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data.success) {
|
||||||
|
const items = data.data.content;
|
||||||
|
const totalPages = data.data.totalPages;
|
||||||
|
// Обработка данных
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Фильтрация по источнику
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Получить только новости от Kursiv
|
||||||
|
fetch('/api/parser/items?sourceName=Kursiv (Бизнес/экономика)&page=0&size=20')
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data.success) {
|
||||||
|
const kursivNews = data.data.content;
|
||||||
|
// Обработка данных
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Фильтрация по датам
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Получить новости за последнюю неделю
|
||||||
|
const endDate = new Date().toISOString();
|
||||||
|
const startDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
||||||
|
|
||||||
|
fetch(`/api/parser/items?startDate=${startDate}&endDate=${endDate}`)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data.success) {
|
||||||
|
const recentNews = data.data.content;
|
||||||
|
// Обработка данных
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Комплексная фильтрация
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Получить новости Kapital за последний месяц, отсортированные по дате
|
||||||
|
const endDate = new Date().toISOString();
|
||||||
|
const startDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
|
||||||
|
|
||||||
|
fetch(
|
||||||
|
`/api/parser/items?sourceName=Kapital.kz (Бизнес)&startDate=${startDate}&endDate=${endDate}&sort=publishedAt,desc&page=0&size=50`
|
||||||
|
)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data.success) {
|
||||||
|
const kapitalNews = data.data.content;
|
||||||
|
const totalCount = data.data.totalElements;
|
||||||
|
// Обработка данных
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Получение статистики
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
fetch('/api/parser/stats')
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data.success) {
|
||||||
|
const stats = data.data;
|
||||||
|
console.log(`Всего новостей: ${stats.totalItems}`);
|
||||||
|
console.log('По источникам:', stats.sourceStatistics);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Поддерживаемые форматы дат
|
||||||
|
|
||||||
|
API поддерживает следующие форматы дат:
|
||||||
|
|
||||||
|
- `yyyy-MM-dd` (например: 2025-01-14)
|
||||||
|
- `yyyy-MM-dd'T'HH:mm:ss` (например: 2025-01-14T10:30:00)
|
||||||
|
- `yyyy-MM-dd'T'HH:mm:ss.SSS` (например: 2025-01-14T10:30:00.000)
|
||||||
|
- `yyyy-MM-dd HH:mm:ss` (например: 2025-01-14 10:30:00)
|
||||||
|
|
||||||
|
## Обработка ошибок
|
||||||
|
|
||||||
|
Все ответы API следуют единому формату:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Описание ошибки",
|
||||||
|
"error": "Детальная информация об ошибке"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
HTTP статус коды:
|
||||||
|
|
||||||
|
- `200` - Успешный запрос
|
||||||
|
- `500` - Внутренняя ошибка сервера
|
||||||
|
- `503` - Сервис недоступен (например, MongoDB)
|
||||||
|
|
||||||
|
## Рекомендации для фронтенда
|
||||||
|
|
||||||
|
1. **Кэширование**: Используйте кэширование для статистики и часто запрашиваемых данных
|
||||||
|
2. **Пагинация**: Реализуйте бесконечную прокрутку или традиционную пагинацию
|
||||||
|
3. **Фильтры**: Предоставьте пользователю удобные фильтры по источнику и датам
|
||||||
|
4. **Обновление**: Используйте WebSocket или polling для обновления данных в реальном времени
|
||||||
|
5. **Обработка ошибок**: Всегда проверяйте поле `success` в ответе
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
# Решение проблем с подключением к MongoDB
|
||||||
|
|
||||||
|
## Проблема: Ошибка аутентификации
|
||||||
|
|
||||||
|
```
|
||||||
|
Command failed with error 13 (Unauthorized): 'Command find requires authentication'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Причина
|
||||||
|
|
||||||
|
MongoDB сервер на `92.38.48.166:27017` требует аутентификацию, но в конфигурации не указаны учетные данные.
|
||||||
|
|
||||||
|
## Решение
|
||||||
|
|
||||||
|
### 1. Обновлена конфигурация MongoDB
|
||||||
|
|
||||||
|
В файле `src/main/resources/application.properties` добавлены настройки аутентификации:
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# MongoDB Configuration
|
||||||
|
spring.data.mongodb.host=92.38.48.166
|
||||||
|
spring.data.mongodb.port=27017
|
||||||
|
spring.data.mongodb.database=parser_db
|
||||||
|
spring.data.mongodb.username=parser_user
|
||||||
|
spring.data.mongodb.password=parser_password
|
||||||
|
spring.data.mongodb.authentication-database=admin
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Альтернативный способ подключения
|
||||||
|
|
||||||
|
Если отдельные параметры не работают, используйте URI подключения:
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# Закомментируйте отдельные параметры и раскомментируйте URI
|
||||||
|
# spring.data.mongodb.host=92.38.48.166
|
||||||
|
# spring.data.mongodb.port=27017
|
||||||
|
# spring.data.mongodb.database=parser_db
|
||||||
|
# spring.data.mongodb.username=parser_user
|
||||||
|
# spring.data.mongodb.password=parser_password
|
||||||
|
# spring.data.mongodb.authentication-database=admin
|
||||||
|
|
||||||
|
# Используйте URI подключения
|
||||||
|
spring.data.mongodb.uri=mongodb://parser_user:parser_password@92.38.48.166:27017/parser_db?authSource=admin
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Проверка подключения
|
||||||
|
|
||||||
|
#### Через API:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8080/api/parser/health/mongodb
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Через MongoDB клиент:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mongo mongodb://parser_user:parser_password@92.38.48.166:27017/parser_db?authSource=admin
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Возможные проблемы и решения
|
||||||
|
|
||||||
|
#### Проблема: Неверные учетные данные
|
||||||
|
|
||||||
|
**Решение:** Проверьте правильность username/password с администратором MongoDB
|
||||||
|
|
||||||
|
#### Проблема: Пользователь не существует
|
||||||
|
|
||||||
|
**Решение:** Создайте пользователя в MongoDB:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
use admin
|
||||||
|
db.createUser({
|
||||||
|
user: "parser_user",
|
||||||
|
pwd: "parser_password",
|
||||||
|
roles: [
|
||||||
|
{ role: "readWrite", db: "parser_db" }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Проблема: Пользователь не имеет прав
|
||||||
|
|
||||||
|
**Решение:** Предоставьте права на базу данных:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
use parser_db
|
||||||
|
db.grantRolesToUser("parser_user", ["readWrite"])
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Проблема: Firewall блокирует подключение
|
||||||
|
|
||||||
|
**Решение:** Убедитесь, что порт 27017 открыт на сервере MongoDB
|
||||||
|
|
||||||
|
### 5. Улучшения в коде
|
||||||
|
|
||||||
|
#### Улучшенная обработка ошибок
|
||||||
|
|
||||||
|
- Добавлена специфическая обработка ошибок аутентификации
|
||||||
|
- Более информативные сообщения об ошибках
|
||||||
|
- Graceful handling ошибок подключения
|
||||||
|
|
||||||
|
#### Новый endpoint для диагностики
|
||||||
|
|
||||||
|
- `GET /api/parser/health/mongodb` - проверка подключения к MongoDB
|
||||||
|
- Возвращает статус подключения и количество записей
|
||||||
|
- Предоставляет рекомендации при ошибках
|
||||||
|
|
||||||
|
### 6. Тестирование
|
||||||
|
|
||||||
|
1. **Запустите приложение:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./mvnw spring-boot:run
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Проверьте подключение к MongoDB:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8080/api/parser/health/mongodb
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Если подключение успешно, запустите парсинг:**
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/api/parser/parse/all
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Логирование
|
||||||
|
|
||||||
|
Уровень логирования MongoDB настроен для диагностики:
|
||||||
|
|
||||||
|
```properties
|
||||||
|
logging.level.org.springframework.data.mongodb=INFO
|
||||||
|
logging.level.com.mongodb=WARN
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. Контакты
|
||||||
|
|
||||||
|
Если проблема не решается, обратитесь к администратору MongoDB сервера для:
|
||||||
|
|
||||||
|
- Проверки учетных данных
|
||||||
|
- Настройки прав доступа
|
||||||
|
- Проверки конфигурации сервера
|
||||||
+45
-2
@@ -189,12 +189,40 @@ GET /api/parser/health
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"status": "UP",
|
"status": "UP",
|
||||||
"service": "Kursiv RSS Parser",
|
"service": "RSS Parser System",
|
||||||
"timestamp": 1703123456789,
|
"timestamp": 1703123456789,
|
||||||
"scheduler": "Enabled - runs every 30 minutes"
|
"scheduler": "Enabled - runs every 30 minutes"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 4.1. Проверка подключения к MongoDB
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/parser/health/mongodb
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ответ при успешном подключении:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "UP",
|
||||||
|
"message": "MongoDB подключение успешно",
|
||||||
|
"totalItems": 37,
|
||||||
|
"timestamp": 1703123456789
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ответ при ошибке:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "DOWN",
|
||||||
|
"message": "Ошибка подключения к MongoDB: Command failed with error 13 (Unauthorized)",
|
||||||
|
"suggestion": "Проверьте учетные данные в application.properties",
|
||||||
|
"timestamp": 1703123456789
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### 5. Информация о планировщике
|
### 5. Информация о планировщике
|
||||||
|
|
||||||
```http
|
```http
|
||||||
@@ -298,6 +326,21 @@ logging.level.org.springframework.data.mongodb=DEBUG
|
|||||||
|
|
||||||
## Возможные проблемы и решения
|
## Возможные проблемы и решения
|
||||||
|
|
||||||
|
### MongoDB требует аутентификацию
|
||||||
|
|
||||||
|
```
|
||||||
|
Command failed with error 13 (Unauthorized): 'Command find requires authentication'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Решение:**
|
||||||
|
|
||||||
|
1. Проверьте учетные данные в `application.properties`
|
||||||
|
2. Убедитесь, что пользователь `parser_user` существует в MongoDB
|
||||||
|
3. Проверьте права доступа пользователя к базе данных `parser_db`
|
||||||
|
4. Используйте endpoint `GET /api/parser/health/mongodb` для диагностики
|
||||||
|
|
||||||
|
**Подробное руководство:** См. файл `MONGODB_TROUBLESHOOTING.md`
|
||||||
|
|
||||||
### MongoDB не запущен
|
### MongoDB не запущен
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -312,7 +355,7 @@ Error: Could not connect to MongoDB
|
|||||||
Error: Could not fetch RSS feed
|
Error: Could not fetch RSS feed
|
||||||
```
|
```
|
||||||
|
|
||||||
**Решение:** Проверьте доступность URL `https://kursiv.media/feed/` и интернет-соединение.
|
**Решение:** Проверьте доступность URL RSS-лент и интернет-соединение.
|
||||||
|
|
||||||
### Проблемы с кодировкой
|
### Проблемы с кодировкой
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
package kz.konturai.parser.controller;
|
package kz.konturai.parser.controller;
|
||||||
|
|
||||||
|
import kz.konturai.parser.dto.*;
|
||||||
import kz.konturai.parser.model.MarketItem;
|
import kz.konturai.parser.model.MarketItem;
|
||||||
import kz.konturai.parser.service.KursivParserService;
|
import kz.konturai.parser.service.KursivParserService;
|
||||||
import kz.konturai.parser.service.KapitalParserService;
|
import kz.konturai.parser.service.KapitalParserService;
|
||||||
|
import kz.konturai.parser.service.MarketItemService;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.web.PageableDefault;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/parser")
|
@RequestMapping("/api/parser")
|
||||||
@@ -22,31 +27,29 @@ public class ParserController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private KapitalParserService kapitalParserService;
|
private KapitalParserService kapitalParserService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MarketItemService marketItemService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Запуск парсинга RSS-ленты Kursiv
|
* Запуск парсинга RSS-ленты Kursiv
|
||||||
*/
|
*/
|
||||||
@PostMapping("/parse/kursiv")
|
@PostMapping("/parse/kursiv")
|
||||||
public ResponseEntity<Map<String, Object>> parseKursivRssFeed() {
|
public ResponseEntity<ApiResponse<ParserResultDto>> parseKursivRssFeed() {
|
||||||
Map<String, Object> response = new HashMap<>();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
List<MarketItem> savedItems = kursivParserService.parseAndSaveRssFeed();
|
List<MarketItem> savedItems = kursivParserService.parseAndSaveRssFeed();
|
||||||
|
long totalItems = marketItemService.getTotalItemsCount();
|
||||||
|
|
||||||
response.put("success", true);
|
ParserResultDto result = new ParserResultDto(
|
||||||
response.put("message", "Парсинг Kursiv завершен успешно");
|
"Kursiv",
|
||||||
response.put("source", "Kursiv");
|
savedItems.size(),
|
||||||
response.put("processedItems", savedItems.size());
|
totalItems,
|
||||||
response.put("totalItemsInDb", kursivParserService.getTotalItemsCount());
|
"completed");
|
||||||
|
|
||||||
return ResponseEntity.ok(response);
|
return ResponseEntity.ok(ApiResponse.success("Парсинг Kursiv завершен успешно", result));
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
response.put("success", false);
|
return ResponseEntity.internalServerError()
|
||||||
response.put("message", "Ошибка при парсинге Kursiv: " + e.getMessage());
|
.body(ApiResponse.error("Ошибка при парсинге Kursiv: " + e.getMessage()));
|
||||||
response.put("source", "Kursiv");
|
|
||||||
response.put("processedItems", 0);
|
|
||||||
|
|
||||||
return ResponseEntity.internalServerError().body(response);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,27 +57,22 @@ public class ParserController {
|
|||||||
* Запуск парсинга RSS-ленты Kapital
|
* Запуск парсинга RSS-ленты Kapital
|
||||||
*/
|
*/
|
||||||
@PostMapping("/parse/kapital")
|
@PostMapping("/parse/kapital")
|
||||||
public ResponseEntity<Map<String, Object>> parseKapitalRssFeed() {
|
public ResponseEntity<ApiResponse<ParserResultDto>> parseKapitalRssFeed() {
|
||||||
Map<String, Object> response = new HashMap<>();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
List<MarketItem> savedItems = kapitalParserService.parseAndSaveRssFeed();
|
List<MarketItem> savedItems = kapitalParserService.parseAndSaveRssFeed();
|
||||||
|
long totalItems = marketItemService.getTotalItemsCount();
|
||||||
|
|
||||||
response.put("success", true);
|
ParserResultDto result = new ParserResultDto(
|
||||||
response.put("message", "Парсинг Kapital завершен успешно");
|
"Kapital",
|
||||||
response.put("source", "Kapital");
|
savedItems.size(),
|
||||||
response.put("processedItems", savedItems.size());
|
totalItems,
|
||||||
response.put("totalItemsInDb", kapitalParserService.getKapitalItemsCount());
|
"completed");
|
||||||
|
|
||||||
return ResponseEntity.ok(response);
|
return ResponseEntity.ok(ApiResponse.success("Парсинг Kapital завершен успешно", result));
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
response.put("success", false);
|
return ResponseEntity.internalServerError()
|
||||||
response.put("message", "Ошибка при парсинге Kapital: " + e.getMessage());
|
.body(ApiResponse.error("Ошибка при парсинге Kapital: " + e.getMessage()));
|
||||||
response.put("source", "Kapital");
|
|
||||||
response.put("processedItems", 0);
|
|
||||||
|
|
||||||
return ResponseEntity.internalServerError().body(response);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,30 +80,63 @@ public class ParserController {
|
|||||||
* Запуск парсинга всех RSS-лент
|
* Запуск парсинга всех RSS-лент
|
||||||
*/
|
*/
|
||||||
@PostMapping("/parse/all")
|
@PostMapping("/parse/all")
|
||||||
public ResponseEntity<Map<String, Object>> parseAllRssFeeds() {
|
public ResponseEntity<ApiResponse<List<ParserResultDto>>> parseAllRssFeeds() {
|
||||||
Map<String, Object> response = new HashMap<>();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
List<MarketItem> kursivItems = kursivParserService.parseAndSaveRssFeed();
|
List<MarketItem> kursivItems = kursivParserService.parseAndSaveRssFeed();
|
||||||
List<MarketItem> kapitalItems = kapitalParserService.parseAndSaveRssFeed();
|
List<MarketItem> kapitalItems = kapitalParserService.parseAndSaveRssFeed();
|
||||||
|
long totalItems = marketItemService.getTotalItemsCount();
|
||||||
|
|
||||||
int totalProcessed = kursivItems.size() + kapitalItems.size();
|
ParserResultDto kursivResult = new ParserResultDto(
|
||||||
|
"Kursiv",
|
||||||
|
kursivItems.size(),
|
||||||
|
totalItems,
|
||||||
|
"completed");
|
||||||
|
|
||||||
response.put("success", true);
|
ParserResultDto kapitalResult = new ParserResultDto(
|
||||||
response.put("message", "Парсинг всех источников завершен успешно");
|
"Kapital",
|
||||||
response.put("kursivProcessed", kursivItems.size());
|
kapitalItems.size(),
|
||||||
response.put("kapitalProcessed", kapitalItems.size());
|
totalItems,
|
||||||
response.put("totalProcessed", totalProcessed);
|
"completed");
|
||||||
response.put("totalItemsInDb", kursivParserService.getTotalItemsCount());
|
|
||||||
|
|
||||||
return ResponseEntity.ok(response);
|
List<ParserResultDto> results = List.of(kursivResult, kapitalResult);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(ApiResponse.success("Парсинг всех источников завершен успешно", results));
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
response.put("success", false);
|
return ResponseEntity.internalServerError()
|
||||||
response.put("message", "Ошибка при парсинге всех источников: " + e.getMessage());
|
.body(ApiResponse.error("Ошибка при парсинге всех источников: " + e.getMessage()));
|
||||||
response.put("totalProcessed", 0);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return ResponseEntity.internalServerError().body(response);
|
/**
|
||||||
|
* Получение записей с фильтрацией и пагинацией
|
||||||
|
*/
|
||||||
|
@GetMapping("/items")
|
||||||
|
public ResponseEntity<ApiResponse<Page<MarketItem>>> getItems(
|
||||||
|
@RequestParam(required = false) String sourceName,
|
||||||
|
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) String startDate,
|
||||||
|
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) String endDate,
|
||||||
|
@PageableDefault(size = 20, sort = "publishedAt", direction = org.springframework.data.domain.Sort.Direction.DESC) Pageable pageable) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
LocalDateTime start = marketItemService.parseDate(startDate);
|
||||||
|
LocalDateTime end = marketItemService.parseDate(endDate);
|
||||||
|
|
||||||
|
Page<MarketItem> items;
|
||||||
|
|
||||||
|
if (sourceName != null || start != null || end != null) {
|
||||||
|
// Используем фильтрацию
|
||||||
|
items = marketItemService.getItemsWithFilters(sourceName, start, end, pageable);
|
||||||
|
} else {
|
||||||
|
// Получаем все записи
|
||||||
|
items = marketItemService.getAllItems(pageable);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok(ApiResponse.success("Записи получены успешно", items));
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.internalServerError()
|
||||||
|
.body(ApiResponse.error("Ошибка при получении записей: " + e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,51 +144,19 @@ public class ParserController {
|
|||||||
* Получение статистики по сохраненным записям
|
* Получение статистики по сохраненным записям
|
||||||
*/
|
*/
|
||||||
@GetMapping("/stats")
|
@GetMapping("/stats")
|
||||||
public ResponseEntity<Map<String, Object>> getStats() {
|
public ResponseEntity<ApiResponse<ParserStatsDto>> getStats() {
|
||||||
Map<String, Object> response = new HashMap<>();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
long totalCount = kursivParserService.getTotalItemsCount();
|
long totalCount = marketItemService.getTotalItemsCount();
|
||||||
|
var sourceStatistics = marketItemService.getSourceStatistics();
|
||||||
|
String lastUpdate = java.time.LocalDateTime.now().toString();
|
||||||
|
|
||||||
response.put("success", true);
|
ParserStatsDto stats = new ParserStatsDto(totalCount, sourceStatistics, lastUpdate);
|
||||||
response.put("totalItems", totalCount);
|
|
||||||
response.put("message", "Статистика получена успешно");
|
|
||||||
|
|
||||||
return ResponseEntity.ok(response);
|
return ResponseEntity.ok(ApiResponse.success("Статистика получена успешно", stats));
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
response.put("success", false);
|
return ResponseEntity.internalServerError()
|
||||||
response.put("message", "Ошибка при получении статистики: " + e.getMessage());
|
.body(ApiResponse.error("Ошибка при получении статистики: " + e.getMessage()));
|
||||||
response.put("totalItems", 0);
|
|
||||||
|
|
||||||
return ResponseEntity.internalServerError().body(response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Получение всех сохраненных записей
|
|
||||||
*/
|
|
||||||
@GetMapping("/items")
|
|
||||||
public ResponseEntity<Map<String, Object>> getAllItems() {
|
|
||||||
Map<String, Object> response = new HashMap<>();
|
|
||||||
|
|
||||||
try {
|
|
||||||
List<MarketItem> items = kursivParserService.getAllItems();
|
|
||||||
|
|
||||||
response.put("success", true);
|
|
||||||
response.put("items", items);
|
|
||||||
response.put("count", items.size());
|
|
||||||
response.put("message", "Записи получены успешно");
|
|
||||||
|
|
||||||
return ResponseEntity.ok(response);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
response.put("success", false);
|
|
||||||
response.put("message", "Ошибка при получении записей: " + e.getMessage());
|
|
||||||
response.put("items", List.of());
|
|
||||||
response.put("count", 0);
|
|
||||||
|
|
||||||
return ResponseEntity.internalServerError().body(response);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,21 +164,14 @@ public class ParserController {
|
|||||||
* Получение записи по ID
|
* Получение записи по ID
|
||||||
*/
|
*/
|
||||||
@GetMapping("/items/{id}")
|
@GetMapping("/items/{id}")
|
||||||
public ResponseEntity<Map<String, Object>> getItemById(@PathVariable String id) {
|
public ResponseEntity<ApiResponse<MarketItem>> getItemById(@PathVariable String id) {
|
||||||
Map<String, Object> response = new HashMap<>();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Здесь можно добавить метод для поиска по ID в сервисе
|
// Здесь можно добавить метод для поиска по ID в сервисе
|
||||||
response.put("success", false);
|
return ResponseEntity.ok(ApiResponse.error("Функция поиска по ID пока не реализована"));
|
||||||
response.put("message", "Функция поиска по ID пока не реализована");
|
|
||||||
|
|
||||||
return ResponseEntity.ok(response);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
response.put("success", false);
|
return ResponseEntity.internalServerError()
|
||||||
response.put("message", "Ошибка при получении записи: " + e.getMessage());
|
.body(ApiResponse.error("Ошибка при получении записи: " + e.getMessage()));
|
||||||
|
|
||||||
return ResponseEntity.internalServerError().body(response);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,31 +179,60 @@ public class ParserController {
|
|||||||
* Проверка состояния парсера
|
* Проверка состояния парсера
|
||||||
*/
|
*/
|
||||||
@GetMapping("/health")
|
@GetMapping("/health")
|
||||||
public ResponseEntity<Map<String, Object>> healthCheck() {
|
public ResponseEntity<ApiResponse<HealthCheckDto>> healthCheck() {
|
||||||
Map<String, Object> response = new HashMap<>();
|
HealthCheckDto health = new HealthCheckDto(
|
||||||
|
"UP",
|
||||||
|
"RSS Parser System",
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
"Enabled - runs every 30 minutes");
|
||||||
|
|
||||||
response.put("status", "UP");
|
return ResponseEntity.ok(ApiResponse.success("Сервис работает", health));
|
||||||
response.put("service", "Kursiv RSS Parser");
|
}
|
||||||
response.put("timestamp", System.currentTimeMillis());
|
|
||||||
response.put("scheduler", "Enabled - runs every 30 minutes");
|
|
||||||
|
|
||||||
return ResponseEntity.ok(response);
|
/**
|
||||||
|
* Проверка подключения к MongoDB
|
||||||
|
*/
|
||||||
|
@GetMapping("/health/mongodb")
|
||||||
|
public ResponseEntity<ApiResponse<HealthCheckDto>> checkMongoConnection() {
|
||||||
|
try {
|
||||||
|
long count = marketItemService.getTotalItemsCount();
|
||||||
|
HealthCheckDto health = new HealthCheckDto(
|
||||||
|
"UP",
|
||||||
|
"MongoDB подключение успешно",
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
null);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(ApiResponse.success("MongoDB подключение успешно", health));
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
HealthCheckDto health = new HealthCheckDto(
|
||||||
|
"DOWN",
|
||||||
|
"Ошибка подключения к MongoDB: " + e.getMessage(),
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
null);
|
||||||
|
|
||||||
|
if (e.getMessage().contains("Unauthorized") || e.getMessage().contains("authentication")) {
|
||||||
|
health.setSuggestion("Проверьте учетные данные в application.properties");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.status(503).body(ApiResponse.error("Ошибка подключения к MongoDB", health));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Получение информации о планировщике
|
* Получение информации о планировщике
|
||||||
*/
|
*/
|
||||||
@GetMapping("/scheduler/info")
|
@GetMapping("/scheduler/info")
|
||||||
public ResponseEntity<Map<String, Object>> getSchedulerInfo() {
|
public ResponseEntity<ApiResponse<java.util.Map<String, Object>>> getSchedulerInfo() {
|
||||||
Map<String, Object> response = new HashMap<>();
|
java.util.Map<String, Object> schedulerInfo = new java.util.HashMap<>();
|
||||||
|
|
||||||
response.put("schedulerEnabled", true);
|
schedulerInfo.put("schedulerEnabled", true);
|
||||||
response.put("cronExpression", "0 0/30 * * * ?");
|
schedulerInfo.put("cronExpression", "0 0/30 * * * ?");
|
||||||
response.put("description", "Запуск каждые 30 минут (в 0 и 30 минут каждого часа)");
|
schedulerInfo.put("description", "Запуск каждые 30 минут (в 0 и 30 минут каждого часа)");
|
||||||
response.put("nextRun", "Следующий запуск будет в ближайшие 0 или 30 минут часа");
|
schedulerInfo.put("nextRun", "Следующий запуск будет в ближайшие 0 или 30 минут часа");
|
||||||
response.put("activeParsers", List.of("KursivParserService", "KapitalParserService"));
|
schedulerInfo.put("activeParsers", List.of("KursivParserService", "KapitalParserService"));
|
||||||
response.put("sources", List.of("Kursiv Media", "Kapital.kz"));
|
schedulerInfo.put("sources", List.of("Kursiv Media", "Kapital.kz"));
|
||||||
|
|
||||||
return ResponseEntity.ok(response);
|
return ResponseEntity.ok(ApiResponse.success("Информация о планировщике получена", schedulerInfo));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package kz.konturai.parser.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class ApiResponse<T> {
|
||||||
|
|
||||||
|
private boolean success;
|
||||||
|
private String message;
|
||||||
|
private T data;
|
||||||
|
private String error;
|
||||||
|
|
||||||
|
public ApiResponse() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public ApiResponse(boolean success, String message, T data) {
|
||||||
|
this.success = success;
|
||||||
|
this.message = message;
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ApiResponse(boolean success, String message, String error) {
|
||||||
|
this.success = success;
|
||||||
|
this.message = message;
|
||||||
|
this.error = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> ApiResponse<T> success(T data) {
|
||||||
|
return new ApiResponse<>(true, "Операция выполнена успешно", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> ApiResponse<T> success(String message, T data) {
|
||||||
|
return new ApiResponse<>(true, message, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> ApiResponse<T> error(String message) {
|
||||||
|
return new ApiResponse<>(false, message, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> ApiResponse<T> error(String message, String error) {
|
||||||
|
return new ApiResponse<>(false, message, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> ApiResponse<T> error(String message, T data) {
|
||||||
|
ApiResponse<T> response = new ApiResponse<>(false, message, data);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public boolean isSuccess() {
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSuccess(boolean success) {
|
||||||
|
this.success = success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMessage(String message) {
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public T getData() {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setData(T data) {
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getError() {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setError(String error) {
|
||||||
|
this.error = error;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package kz.konturai.parser.dto;
|
||||||
|
|
||||||
|
public class HealthCheckDto {
|
||||||
|
|
||||||
|
private String status;
|
||||||
|
private String service;
|
||||||
|
private long timestamp;
|
||||||
|
private String scheduler;
|
||||||
|
private String message;
|
||||||
|
private String suggestion;
|
||||||
|
|
||||||
|
public HealthCheckDto() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public HealthCheckDto(String status, String service, long timestamp, String scheduler) {
|
||||||
|
this.status = status;
|
||||||
|
this.service = service;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.scheduler = scheduler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HealthCheckDto(String status, String message, long timestamp) {
|
||||||
|
this.status = status;
|
||||||
|
this.message = message;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public String getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(String status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getService() {
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setService(String service) {
|
||||||
|
this.service = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTimestamp() {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTimestamp(long timestamp) {
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getScheduler() {
|
||||||
|
return scheduler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setScheduler(String scheduler) {
|
||||||
|
this.scheduler = scheduler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMessage(String message) {
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSuggestion() {
|
||||||
|
return suggestion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSuggestion(String suggestion) {
|
||||||
|
this.suggestion = suggestion;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package kz.konturai.parser.dto;
|
||||||
|
|
||||||
|
public class ParserResultDto {
|
||||||
|
|
||||||
|
private String source;
|
||||||
|
private int processedItems;
|
||||||
|
private long totalItemsInDb;
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
public ParserResultDto() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public ParserResultDto(String source, int processedItems, long totalItemsInDb, String status) {
|
||||||
|
this.source = source;
|
||||||
|
this.processedItems = processedItems;
|
||||||
|
this.totalItemsInDb = totalItemsInDb;
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public String getSource() {
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSource(String source) {
|
||||||
|
this.source = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getProcessedItems() {
|
||||||
|
return processedItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProcessedItems(int processedItems) {
|
||||||
|
this.processedItems = processedItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTotalItemsInDb() {
|
||||||
|
return totalItemsInDb;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTotalItemsInDb(long totalItemsInDb) {
|
||||||
|
this.totalItemsInDb = totalItemsInDb;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(String status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package kz.konturai.parser.dto;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class ParserStatsDto {
|
||||||
|
|
||||||
|
private long totalItems;
|
||||||
|
private Map<String, Long> sourceStatistics;
|
||||||
|
private String lastUpdate;
|
||||||
|
|
||||||
|
public ParserStatsDto() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public ParserStatsDto(long totalItems, Map<String, Long> sourceStatistics, String lastUpdate) {
|
||||||
|
this.totalItems = totalItems;
|
||||||
|
this.sourceStatistics = sourceStatistics;
|
||||||
|
this.lastUpdate = lastUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public long getTotalItems() {
|
||||||
|
return totalItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTotalItems(long totalItems) {
|
||||||
|
this.totalItems = totalItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Long> getSourceStatistics() {
|
||||||
|
return sourceStatistics;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSourceStatistics(Map<String, Long> sourceStatistics) {
|
||||||
|
this.sourceStatistics = sourceStatistics;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLastUpdate() {
|
||||||
|
return lastUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastUpdate(String lastUpdate) {
|
||||||
|
this.lastUpdate = lastUpdate;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
package kz.konturai.parser.repository;
|
package kz.konturai.parser.repository;
|
||||||
|
|
||||||
import kz.konturai.parser.model.MarketItem;
|
import kz.konturai.parser.model.MarketItem;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||||
|
import org.springframework.data.mongodb.repository.Query;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
@@ -18,4 +23,33 @@ public interface MarketItemRepository extends MongoRepository<MarketItem, String
|
|||||||
* Проверить существование записи по хэшу
|
* Проверить существование записи по хэшу
|
||||||
*/
|
*/
|
||||||
boolean existsByHash(String hash);
|
boolean existsByHash(String hash);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Найти записи по источнику с пагинацией
|
||||||
|
*/
|
||||||
|
Page<MarketItem> findBySourceName(String sourceName, Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Найти записи по диапазону дат с пагинацией
|
||||||
|
*/
|
||||||
|
Page<MarketItem> findByPublishedAtBetween(LocalDateTime startDate, LocalDateTime endDate, Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Подсчитать записи по источнику
|
||||||
|
*/
|
||||||
|
long countBySourceName(String sourceName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить все уникальные названия источников
|
||||||
|
*/
|
||||||
|
@Query(value = "{}", fields = "{'sourceName': 1}")
|
||||||
|
List<String> findDistinctSourceNames();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Найти записи по источнику и диапазону дат
|
||||||
|
*/
|
||||||
|
Page<MarketItem> findBySourceNameAndPublishedAtBetween(String sourceName,
|
||||||
|
LocalDateTime startDate,
|
||||||
|
LocalDateTime endDate,
|
||||||
|
Pageable pageable);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -242,6 +242,13 @@ public class KapitalParserService {
|
|||||||
return savedItem;
|
return savedItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} catch (org.springframework.data.mongodb.UncategorizedMongoDbException e) {
|
||||||
|
if (e.getMessage().contains("Unauthorized") || e.getMessage().contains("authentication")) {
|
||||||
|
logger.error("❌ Ошибка аутентификации MongoDB. Проверьте учетные данные в application.properties");
|
||||||
|
} else {
|
||||||
|
logger.error("❌ Ошибка подключения к MongoDB: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Ошибка при сохранении записи Kapital.kz: {}", marketItem.getTitle(), e);
|
logger.error("Ошибка при сохранении записи Kapital.kz: {}", marketItem.getTitle(), e);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -238,6 +238,13 @@ public class KursivParserService {
|
|||||||
return savedItem;
|
return savedItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} catch (org.springframework.data.mongodb.UncategorizedMongoDbException e) {
|
||||||
|
if (e.getMessage().contains("Unauthorized") || e.getMessage().contains("authentication")) {
|
||||||
|
logger.error("❌ Ошибка аутентификации MongoDB. Проверьте учетные данные в application.properties");
|
||||||
|
} else {
|
||||||
|
logger.error("❌ Ошибка подключения к MongoDB: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Ошибка при сохранении записи: {}", marketItem.getTitle(), e);
|
logger.error("Ошибка при сохранении записи: {}", marketItem.getTitle(), e);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
package kz.konturai.parser.service;
|
||||||
|
|
||||||
|
import kz.konturai.parser.model.MarketItem;
|
||||||
|
import kz.konturai.parser.repository.MarketItemRepository;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.mongodb.core.MongoTemplate;
|
||||||
|
import org.springframework.data.mongodb.core.query.Criteria;
|
||||||
|
import org.springframework.data.mongodb.core.query.Query;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class MarketItemService {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(MarketItemService.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MarketItemRepository marketItemRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MongoTemplate mongoTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение всех записей с пагинацией и сортировкой
|
||||||
|
*/
|
||||||
|
public Page<MarketItem> getAllItems(Pageable pageable) {
|
||||||
|
logger.debug("Получение всех записей с пагинацией: page={}, size={}, sort={}",
|
||||||
|
pageable.getPageNumber(), pageable.getPageSize(), pageable.getSort());
|
||||||
|
|
||||||
|
return marketItemRepository.findAll(pageable);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение записей с фильтрацией по источнику
|
||||||
|
*/
|
||||||
|
public Page<MarketItem> getItemsBySource(String sourceName, Pageable pageable) {
|
||||||
|
logger.debug("Получение записей по источнику: sourceName={}, page={}, size={}",
|
||||||
|
sourceName, pageable.getPageNumber(), pageable.getPageSize());
|
||||||
|
|
||||||
|
return marketItemRepository.findBySourceName(sourceName, pageable);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение записей с фильтрацией по диапазону дат
|
||||||
|
*/
|
||||||
|
public Page<MarketItem> getItemsByDateRange(LocalDateTime startDate, LocalDateTime endDate, Pageable pageable) {
|
||||||
|
logger.debug("Получение записей по диапазону дат: startDate={}, endDate={}, page={}, size={}",
|
||||||
|
startDate, endDate, pageable.getPageNumber(), pageable.getPageSize());
|
||||||
|
|
||||||
|
return marketItemRepository.findByPublishedAtBetween(startDate, endDate, pageable);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение записей с комплексной фильтрацией
|
||||||
|
*/
|
||||||
|
public Page<MarketItem> getItemsWithFilters(String sourceName, LocalDateTime startDate,
|
||||||
|
LocalDateTime endDate, Pageable pageable) {
|
||||||
|
logger.debug("Получение записей с фильтрацией: sourceName={}, startDate={}, endDate={}, page={}, size={}",
|
||||||
|
sourceName, startDate, endDate, pageable.getPageNumber(), pageable.getPageSize());
|
||||||
|
|
||||||
|
Query query = new Query();
|
||||||
|
|
||||||
|
// Фильтр по источнику
|
||||||
|
if (sourceName != null && !sourceName.trim().isEmpty()) {
|
||||||
|
query.addCriteria(Criteria.where("sourceName").is(sourceName));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Фильтр по диапазону дат
|
||||||
|
if (startDate != null && endDate != null) {
|
||||||
|
query.addCriteria(Criteria.where("publishedAt").gte(startDate).lte(endDate));
|
||||||
|
} else if (startDate != null) {
|
||||||
|
query.addCriteria(Criteria.where("publishedAt").gte(startDate));
|
||||||
|
} else if (endDate != null) {
|
||||||
|
query.addCriteria(Criteria.where("publishedAt").lte(endDate));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Подсчет общего количества записей
|
||||||
|
long totalCount = mongoTemplate.count(query, MarketItem.class);
|
||||||
|
|
||||||
|
// Применение пагинации и сортировки
|
||||||
|
query.with(pageable);
|
||||||
|
|
||||||
|
// Выполнение запроса
|
||||||
|
List<MarketItem> items = mongoTemplate.find(query, MarketItem.class);
|
||||||
|
|
||||||
|
// Создание объекта Page
|
||||||
|
return new org.springframework.data.domain.PageImpl<>(items, pageable, totalCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение общего количества записей
|
||||||
|
*/
|
||||||
|
public long getTotalItemsCount() {
|
||||||
|
return marketItemRepository.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение количества записей по источнику
|
||||||
|
*/
|
||||||
|
public long getItemsCountBySource(String sourceName) {
|
||||||
|
return marketItemRepository.countBySourceName(sourceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение всех уникальных источников
|
||||||
|
*/
|
||||||
|
public List<String> getAllSourceNames() {
|
||||||
|
return marketItemRepository.findDistinctSourceNames();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение статистики по источникам
|
||||||
|
*/
|
||||||
|
public java.util.Map<String, Long> getSourceStatistics() {
|
||||||
|
List<String> sources = getAllSourceNames();
|
||||||
|
java.util.Map<String, Long> statistics = new java.util.HashMap<>();
|
||||||
|
|
||||||
|
for (String source : sources) {
|
||||||
|
statistics.put(source, getItemsCountBySource(source));
|
||||||
|
}
|
||||||
|
|
||||||
|
return statistics;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Парсинг даты из строки
|
||||||
|
*/
|
||||||
|
public LocalDateTime parseDate(String dateString) {
|
||||||
|
if (dateString == null || dateString.trim().isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Поддерживаем несколько форматов даты
|
||||||
|
String[] formats = {
|
||||||
|
"yyyy-MM-dd",
|
||||||
|
"yyyy-MM-dd'T'HH:mm:ss",
|
||||||
|
"yyyy-MM-dd'T'HH:mm:ss.SSS",
|
||||||
|
"yyyy-MM-dd HH:mm:ss"
|
||||||
|
};
|
||||||
|
|
||||||
|
for (String format : formats) {
|
||||||
|
try {
|
||||||
|
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format);
|
||||||
|
return LocalDateTime.parse(dateString, formatter);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Пробуем следующий формат
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warn("Не удалось распарсить дату: {}", dateString);
|
||||||
|
return null;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Ошибка при парсинге даты: {}", dateString, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,16 @@
|
|||||||
spring.application.name=parser
|
spring.application.name=parser
|
||||||
|
|
||||||
# MongoDB Configuration
|
# MongoDB Configuration
|
||||||
|
# Option 1: Individual properties
|
||||||
spring.data.mongodb.host=92.38.48.166
|
spring.data.mongodb.host=92.38.48.166
|
||||||
spring.data.mongodb.port=27017
|
spring.data.mongodb.port=27017
|
||||||
spring.data.mongodb.database=parser_db
|
spring.data.mongodb.database=parser_db
|
||||||
|
spring.data.mongodb.username=konturai
|
||||||
|
spring.data.mongodb.password=konturai2024
|
||||||
|
spring.data.mongodb.authentication-database=admin
|
||||||
|
|
||||||
|
# Option 2: Connection URI (uncomment if individual properties don't work)
|
||||||
|
# spring.data.mongodb.uri=mongodb://parser_user:parser_password@92.38.48.166:27017/parser_db?authSource=admin
|
||||||
|
|
||||||
# RSS Feed URLs
|
# RSS Feed URLs
|
||||||
rss.feed.url=https://kursiv.media/feed/
|
rss.feed.url=https://kursiv.media/feed/
|
||||||
@@ -13,6 +20,14 @@ rss.kapital.url=https://kapital.kz/rss/
|
|||||||
spring.task.scheduling.pool.size=2
|
spring.task.scheduling.pool.size=2
|
||||||
spring.task.scheduling.thread-name-prefix=scheduled-task-
|
spring.task.scheduling.thread-name-prefix=scheduled-task-
|
||||||
|
|
||||||
|
# MongoDB Connection Settings
|
||||||
|
spring.data.mongodb.connection-timeout=30000
|
||||||
|
spring.data.mongodb.socket-timeout=30000
|
||||||
|
spring.data.mongodb.server-selection-timeout=30000
|
||||||
|
|
||||||
# Logging Configuration
|
# Logging Configuration
|
||||||
logging.level.kz.konturai.parser.service.KursivParserService=INFO
|
logging.level.kz.konturai.parser.service.KursivParserService=INFO
|
||||||
|
logging.level.kz.konturai.parser.service.KapitalParserService=INFO
|
||||||
logging.level.org.springframework.scheduling=DEBUG
|
logging.level.org.springframework.scheduling=DEBUG
|
||||||
|
logging.level.org.springframework.data.mongodb=INFO
|
||||||
|
logging.level.com.mongodb=WARN
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
package kz.konturai.parser.service;
|
||||||
|
|
||||||
|
import kz.konturai.parser.model.MarketItem;
|
||||||
|
import kz.konturai.parser.repository.MarketItemRepository;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.test.context.TestPropertySource;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@TestPropertySource(properties = {
|
||||||
|
"spring.data.mongodb.host=92.38.48.166",
|
||||||
|
"spring.data.mongodb.port=27017",
|
||||||
|
"spring.data.mongodb.database=parser_test_db",
|
||||||
|
"spring.data.mongodb.username=konturai",
|
||||||
|
"spring.data.mongodb.password=konturai2024",
|
||||||
|
"spring.data.mongodb.authentication-database=admin"
|
||||||
|
})
|
||||||
|
public class MarketItemServiceTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MarketItemService marketItemService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MarketItemRepository marketItemRepository;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setUp() {
|
||||||
|
// Очищаем базу данных перед каждым тестом
|
||||||
|
marketItemRepository.deleteAll();
|
||||||
|
|
||||||
|
// Создаем тестовые данные
|
||||||
|
createTestData();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createTestData() {
|
||||||
|
// Создаем записи для Kursiv
|
||||||
|
MarketItem kursivItem1 = new MarketItem();
|
||||||
|
kursivItem1.setSourceName("Kursiv (Бизнес/экономика)");
|
||||||
|
kursivItem1.setTitle("Test Kursiv News 1");
|
||||||
|
kursivItem1.setUrl("https://kursiv.media/test1");
|
||||||
|
kursivItem1.setPublishedAt(LocalDateTime.now().minusDays(1));
|
||||||
|
kursivItem1.setHash("kursiv_hash_1");
|
||||||
|
marketItemRepository.save(kursivItem1);
|
||||||
|
|
||||||
|
MarketItem kursivItem2 = new MarketItem();
|
||||||
|
kursivItem2.setSourceName("Kursiv (Бизнес/экономика)");
|
||||||
|
kursivItem2.setTitle("Test Kursiv News 2");
|
||||||
|
kursivItem2.setUrl("https://kursiv.media/test2");
|
||||||
|
kursivItem2.setPublishedAt(LocalDateTime.now().minusDays(2));
|
||||||
|
kursivItem2.setHash("kursiv_hash_2");
|
||||||
|
marketItemRepository.save(kursivItem2);
|
||||||
|
|
||||||
|
// Создаем записи для Kapital
|
||||||
|
MarketItem kapitalItem1 = new MarketItem();
|
||||||
|
kapitalItem1.setSourceName("Kapital.kz (Бизнес)");
|
||||||
|
kapitalItem1.setTitle("Test Kapital News 1");
|
||||||
|
kapitalItem1.setUrl("https://kapital.kz/test1");
|
||||||
|
kapitalItem1.setPublishedAt(LocalDateTime.now().minusDays(3));
|
||||||
|
kapitalItem1.setHash("kapital_hash_1");
|
||||||
|
marketItemRepository.save(kapitalItem1);
|
||||||
|
|
||||||
|
MarketItem kapitalItem2 = new MarketItem();
|
||||||
|
kapitalItem2.setSourceName("Kapital.kz (Бизнес)");
|
||||||
|
kapitalItem2.setTitle("Test Kapital News 2");
|
||||||
|
kapitalItem2.setUrl("https://kapital.kz/test2");
|
||||||
|
kapitalItem2.setPublishedAt(LocalDateTime.now().minusDays(4));
|
||||||
|
kapitalItem2.setHash("kapital_hash_2");
|
||||||
|
marketItemRepository.save(kapitalItem2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetAllItemsWithPagination() {
|
||||||
|
Pageable pageable = PageRequest.of(0, 2, Sort.by("publishedAt").descending());
|
||||||
|
Page<MarketItem> page = marketItemService.getAllItems(pageable);
|
||||||
|
|
||||||
|
assertNotNull(page);
|
||||||
|
assertEquals(4, page.getTotalElements());
|
||||||
|
assertEquals(2, page.getContent().size());
|
||||||
|
assertEquals(2, page.getTotalPages());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetItemsBySource() {
|
||||||
|
Pageable pageable = PageRequest.of(0, 10);
|
||||||
|
Page<MarketItem> kursivItems = marketItemService.getItemsBySource("Kursiv (Бизнес/экономика)", pageable);
|
||||||
|
|
||||||
|
assertNotNull(kursivItems);
|
||||||
|
assertEquals(2, kursivItems.getTotalElements());
|
||||||
|
assertEquals(2, kursivItems.getContent().size());
|
||||||
|
|
||||||
|
// Проверяем, что все записи принадлежат Kursiv
|
||||||
|
for (MarketItem item : kursivItems.getContent()) {
|
||||||
|
assertEquals("Kursiv (Бизнес/экономика)", item.getSourceName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetItemsByDateRange() {
|
||||||
|
LocalDateTime startDate = LocalDateTime.now().minusDays(3);
|
||||||
|
LocalDateTime endDate = LocalDateTime.now().minusDays(1);
|
||||||
|
|
||||||
|
Pageable pageable = PageRequest.of(0, 10);
|
||||||
|
Page<MarketItem> items = marketItemService.getItemsByDateRange(startDate, endDate, pageable);
|
||||||
|
|
||||||
|
assertNotNull(items);
|
||||||
|
assertEquals(2, items.getTotalElements());
|
||||||
|
|
||||||
|
// Проверяем, что все записи в указанном диапазоне дат
|
||||||
|
for (MarketItem item : items.getContent()) {
|
||||||
|
assertTrue(item.getPublishedAt().isAfter(startDate.minusSeconds(1)));
|
||||||
|
assertTrue(item.getPublishedAt().isBefore(endDate.plusSeconds(1)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetItemsWithFilters() {
|
||||||
|
LocalDateTime startDate = LocalDateTime.now().minusDays(3);
|
||||||
|
LocalDateTime endDate = LocalDateTime.now().minusDays(1);
|
||||||
|
|
||||||
|
Pageable pageable = PageRequest.of(0, 10);
|
||||||
|
Page<MarketItem> items = marketItemService.getItemsWithFilters(
|
||||||
|
"Kursiv (Бизнес/экономика)", startDate, endDate, pageable);
|
||||||
|
|
||||||
|
assertNotNull(items);
|
||||||
|
assertEquals(1, items.getTotalElements());
|
||||||
|
|
||||||
|
// Проверяем, что запись соответствует фильтрам
|
||||||
|
MarketItem item = items.getContent().get(0);
|
||||||
|
assertEquals("Kursiv (Бизнес/экономика)", item.getSourceName());
|
||||||
|
assertTrue(item.getPublishedAt().isAfter(startDate.minusSeconds(1)));
|
||||||
|
assertTrue(item.getPublishedAt().isBefore(endDate.plusSeconds(1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetTotalItemsCount() {
|
||||||
|
long count = marketItemService.getTotalItemsCount();
|
||||||
|
assertEquals(4, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetItemsCountBySource() {
|
||||||
|
long kursivCount = marketItemService.getItemsCountBySource("Kursiv (Бизнес/экономика)");
|
||||||
|
assertEquals(2, kursivCount);
|
||||||
|
|
||||||
|
long kapitalCount = marketItemService.getItemsCountBySource("Kapital.kz (Бизнес)");
|
||||||
|
assertEquals(2, kapitalCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetAllSourceNames() {
|
||||||
|
List<String> sources = marketItemService.getAllSourceNames();
|
||||||
|
|
||||||
|
assertNotNull(sources);
|
||||||
|
assertEquals(2, sources.size());
|
||||||
|
assertTrue(sources.contains("Kursiv (Бизнес/экономика)"));
|
||||||
|
assertTrue(sources.contains("Kapital.kz (Бизнес)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetSourceStatistics() {
|
||||||
|
Map<String, Long> statistics = marketItemService.getSourceStatistics();
|
||||||
|
|
||||||
|
assertNotNull(statistics);
|
||||||
|
assertEquals(2, statistics.size());
|
||||||
|
assertEquals(Long.valueOf(2), statistics.get("Kursiv (Бизнес/экономика)"));
|
||||||
|
assertEquals(Long.valueOf(2), statistics.get("Kapital.kz (Бизнес)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testParseDate() {
|
||||||
|
// Тест различных форматов даты
|
||||||
|
LocalDateTime date1 = marketItemService.parseDate("2025-01-14");
|
||||||
|
assertNotNull(date1);
|
||||||
|
|
||||||
|
LocalDateTime date2 = marketItemService.parseDate("2025-01-14T10:30:00");
|
||||||
|
assertNotNull(date2);
|
||||||
|
|
||||||
|
LocalDateTime date3 = marketItemService.parseDate("2025-01-14T10:30:00.000");
|
||||||
|
assertNotNull(date3);
|
||||||
|
|
||||||
|
LocalDateTime date4 = marketItemService.parseDate("2025-01-14 10:30:00");
|
||||||
|
assertNotNull(date4);
|
||||||
|
|
||||||
|
// Тест null и пустой строки
|
||||||
|
assertNull(marketItemService.parseDate(null));
|
||||||
|
assertNull(marketItemService.parseDate(""));
|
||||||
|
assertNull(marketItemService.parseDate("invalid-date"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "🔍 Тестирование подключения к MongoDB..."
|
||||||
|
echo "======================================"
|
||||||
|
|
||||||
|
# Проверяем доступность MongoDB сервера
|
||||||
|
echo "📡 Проверка доступности сервера 92.38.48.166:27017..."
|
||||||
|
if nc -z 92.38.48.166 27017; then
|
||||||
|
echo "✅ Сервер MongoDB доступен"
|
||||||
|
else
|
||||||
|
echo "❌ Сервер MongoDB недоступен"
|
||||||
|
echo " Убедитесь, что сервер запущен и доступен"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🔐 Проверка аутентификации..."
|
||||||
|
echo "Используемые учетные данные:"
|
||||||
|
echo " Host: 92.38.48.166:27017"
|
||||||
|
echo " Database: parser_db"
|
||||||
|
echo " Username: parser_user"
|
||||||
|
echo " Password: parser_password"
|
||||||
|
echo " Auth Database: admin"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "📝 Для проверки подключения используйте:"
|
||||||
|
echo "mongo mongodb://parser_user:parser_password@92.38.48.166:27017/parser_db?authSource=admin"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🚀 Для запуска приложения с новой конфигурацией:"
|
||||||
|
echo "./mvnw spring-boot:run"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "⚠️ Если подключение не работает, проверьте:"
|
||||||
|
echo "1. Правильность учетных данных"
|
||||||
|
echo "2. Настройки firewall на сервере MongoDB"
|
||||||
|
echo "3. Права доступа пользователя parser_user"
|
||||||
|
echo "4. Настройки аутентификации в MongoDB"
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
Необходимо модифицировать существующий ParserController и сервисный слой для эффективного отображения данных.
|
||||||
|
|
||||||
|
Требования:
|
||||||
|
|
||||||
|
Создать MarketItemService: Создай новый сервис MarketItemService, который будет содержать всю логику по работе с коллекцией market_items (получение, подсчет и т.д.). Перенеси в него методы getAllItems и getTotalItemsCount из KursivParserService и сделай так, чтобы они работали со всеми записями, а не только с одним источником.
|
||||||
|
|
||||||
|
Реализовать пагинацию и сортировку: Измени метод getAllItems в контроллере и сервисе так, чтобы он принимал Pageable в качестве аргумента. Метод должен возвращать Page<MarketItem>. Фронтенд сможет запрашивать данные так: GET /api/parser/items?page=0&size=10&sort=published_at,desc.
|
||||||
|
|
||||||
|
Добавить фильтрацию: Добавь в метод getAllItems необязательные параметры запроса (@RequestParam):
|
||||||
|
|
||||||
|
sourceName (тип String) для фильтрации по источнику.
|
||||||
|
|
||||||
|
startDate, endDate (тип String или LocalDate) для фильтрации по диапазону дат.
|
||||||
|
|
||||||
|
Заменить Map<String, Object> на DTO: Создай классы-DTO для всех ответов контроллера, чтобы обеспечить строгую типизацию.
|
||||||
Reference in New Issue
Block a user