xMerge branch 'zorn-dev' of ssh://git.smartsoltech.kr:2222/SmartSolTech/touchh_bot into zorn-dev

This commit is contained in:
2025-02-02 09:28:20 +09:00
26 changed files with 617 additions and 221 deletions

1
.dockerignore Normal file
View File

@@ -0,0 +1 @@
/var

View File

@@ -1,47 +1,111 @@
kind: pipeline
type: docker
name: Django CI/CD
name: Touchh Hotel AntiFraud Pipeline
namespace: touchh
steps:
# Шаг 1: Установка зависимостей, миграции и тесты
- name: test
image: python:3.10
environment:
DATABASE_URL: mysql://root@R0sebud:0.0.0.0:3306/w1510415_wp832
# Шаг 1: Клонирование репозитория
- name: clone_repo
image: alpine/git
commands:
- python -m venv .venv
- source .venv/bin/activate
- pip install --upgrade pip
- pip install -r requirements.txt
- python manage.py migrate
- flake8 . # Линтер
- pytest # Запуск тестов
- if [ ! -d .git ]; then git clone $DRONE_REPO_URL .; fi
- git fetch --all
- git reset --hard $DRONE_COMMIT
# Шаг 2: Запуск и проверка Telegram-бота
- name: bot-check
image: python:3.10
# Шаг 2: Обновление и запуск с помощью update.sh
- name: deploy_app
image: docker:24
environment:
DATABASE_URL: mysql://root@R0sebud:0.0.0.0:3306/w1510415_wp832
MYSQL_PASSWORD: touchh
volumes:
- name: docker_sock
path: /var/run/docker.sock
commands:
- python -m venv .venv
- source .venv/bin/activate
- pip install --upgrade pip
- apk add --no-cache bash
- chmod +x ./bin/update
- docker-compose up -d
- until docker inspect -f '{{.State.Running}}' src-web-1 | grep true; do echo "Waiting for container to be running..."; sleep 5; done
- git branch --set-upstream-to=origin/PMSManager_refactor PMSManager_refactor || true
- ./bin/update
# Шаг 3: Миграция базы данных
- name: run_migrations
image: docker:24
environment:
MYSQL_PASSWORD: touchh
volumes:
- name: docker_sock
path: /var/run/docker.sock
depends_on:
- web
commands:
- apk add --no-cache bash
- until docker inspect -f '{{.State.Running}}' src-web-1 | grep true; do echo "Waiting for container to be running..."; sleep 5; done
- chmod +x ./bin/cli
- ./bin/cli migrate
# Шаг 4: Тестирование
- name: run_tests
image: python:3.12-alpine
environment:
MYSQL_PASSWORD: touchh
depends_on:
- db
commands:
- apk add --no-cache mariadb-client mariadb-connector-c-dev gcc musl-dev pkgconfig
- pip install -r requirements.txt
- python manage.py run_bot & # Запуск бота в фоне
- sleep 5 # Ждём, чтобы бот запустился
- python test_bot.py # Проверка работы бота
- python manage.py test
# services:
# # Шаг 3: Сервис базы данных MySQL
# - name: mysql
# image: mysql:8
# environment:
# MYSQL_ROOT_PASSWORD: R0sebud
# MYSQL_USER: user
# MYSQL_PASSWORD: password
# MYSQL_DATABASE: w1510415_wp832
services:
# Сервис базы данных
- name: db
image: mariadb:11.6
environment:
MYSQL_RANDOM_ROOT_PASSWORD: 1
MYSQL_DATABASE: touchh
MYSQL_USER: touchh
MYSQL_PASSWORD: touchh
volumes:
- name: mysql_data
temp: {}
trigger:
event:
- push
- pull_request
# Сервис Django (Web)
- name: web
image: touchh-py
environment:
MYSQL_PASSWORD: touchh
command: ['python3', 'manage.py', 'runserver', '0.0.0.0:8000']
ports:
- port: 8000
depends_on:
- db
volumes:
- name: app_volume
path: /app
# Сервис Telegram Bot
- name: bot
image: touchh-py
environment:
MYSQL_PASSWORD: touchh
command: ['python3', 'manage.py', 'run_bot']
depends_on:
- db
# Сервис планировщика задач
- name: scheduler
image: touchh-py
environment:
MYSQL_PASSWORD: touchh
command: ['python3', 'manage.py', 'start_scheduler']
depends_on:
- db
volumes:
- name: docker_sock
host:
path: /var/run/docker.sock
- name: mysql_data
temp: {}
- name: app_volume
host:
path: ./

2
.gitignore vendored
View File

@@ -16,3 +16,5 @@ db.sqlite3
# Ignore files
.fake
docker-compose.override.yaml
tmp/*
tmp_data/*

112
README.md
View File

@@ -161,4 +161,114 @@ python manage.py runserver
#### Проверка интеграции с PMS
Для каждого отеля можно проверять статус интеграции с PMS (Bnovo, Travel Line, Realty) и получать ответ о доступности PMS.
Для каждого отеля можно проверять статус интеграции с PMS (Bnovo, Travel Line, Realty) и получать ответ о доступности PMS.
#### Разработка плагинов для интеграции с PMS
Для разработки плагина используются следующие инструменты:
- Django
- Python
- Pydantic
*код примера для плагина*
```python
from datetime import datetime, timedelta
import requests
from asgiref.sync import sync_to_async
from hotels.models import Reservation
from .base_plugin import BasePMSPlugin
import logging
class ExamplePMSPlugin(BasePMSPlugin):
"""
Плагин для интеграции с PMS Example.
"""
def __init__(self, hotel):
super().__init__(hotel.pms)
self.hotel = hotel
self.api_url = self.hotel.pms.url
self.token = self.hotel.pms.token
self.logger = self._configure_logger()
def _configure_logger(self):
logger = logging.getLogger(self.__class__.__name__)
handler_console = logging.StreamHandler()
handler_file = logging.FileHandler(f'{self.__class__.__name__.lower()}.log')
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
handler_console.setFormatter(formatter)
handler_file.setFormatter(formatter)
logger.addHandler(handler_console)
logger.addHandler(handler_file)
logger.setLevel(logging.DEBUG)
return logger
def get_default_parser_settings(self):
"""
Возвращает настройки для обработки данных.
"""
return {
"field_mapping": {
"check_in": "arrival_date",
"check_out": "departure_date",
"room_number": "room",
"status": "status",
},
"date_format": "%Y-%m-%d %H:%M:%S"
}
async def _fetch_data(self):
"""
Получает данные из API Example PMS.
"""
headers = {
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json",
}
now = datetime.now()
payload = {
"from_date": (now - timedelta(days=7)).strftime("%Y-%m-%d"),
"to_date": now.strftime("%Y-%m-%d"),
}
try:
response = await sync_to_async(requests.post)(
self.api_url, json=payload, headers=headers
)
response.raise_for_status()
data = response.json()
return await self._process_data(data)
except requests.exceptions.RequestException as e:
self.logger.error(f"Ошибка API: {e}")
return {"processed_items": 0, "errors": [str(e)]}
async def _process_data(self, data):
"""
Обрабатывает и сохраняет данные в базу.
"""
processed_items = 0
errors = []
for item in data.get("bookings", []):
try:
reservation, created = await sync_to_async(Reservation.objects.update_or_create)(
reservation_id=item['id'],
defaults={
'room_number': item['room'],
'check_in': datetime.strptime(item['arrival_date'], "%Y-%m-%d"),
'check_out': datetime.strptime(item['departure_date'], "%Y-%m-%d"),
'status': item['status'],
'hotel': self.hotel,
}
)
processed_items += 1
except Exception as e:
self.logger.error(f"Ошибка обработки записи {item['id']}: {e}")
errors.append(str(e))
return {"processed_items": processed_items, "errors": errors}
```

View File

@@ -1,9 +1,11 @@
import json
from datetime import timedelta
from urllib.parse import parse_qs
from django.utils import timezone
from django.db.models import Q
from django.db import connection
from hotels.models import Reservation, Hotel
from .models import UserActivityLog, ViolationLog
from .models import UserActivityLog, ViolationLog, RoomDiscrepancy
from touchh.utils.log import CustomLogger
# Настройка логирования
logger = CustomLogger(__name__).get_logger()
@@ -18,9 +20,7 @@ class ReservationChecker:
"""
Инициализация времени проверки и списка нарушений.
"""
self.start_time = timezone.now() - timedelta(days=30)
self.end_time = timezone.now()
self.violations = []
self.checkin_diff_hours = 3
def log_info(self, message):
logger.info(message)
@@ -31,102 +31,71 @@ class ReservationChecker:
def log_error(self, message):
logger.error(message)
def fetch_user_logs(self):
try:
self.log_info("Начинается извлечение логов активности пользователей.")
user_logs = UserActivityLog.objects.filter(created__range=(self.start_time, self.end_time))
self.log_info(f"Найдено {user_logs.count()} логов активности для анализа.")
return user_logs
except Exception as e:
self.log_error(f"Ошибка при извлечении логов активности: {e}")
return UserActivityLog.objects.none()
def fetch_reservations(self):
try:
self.log_info("Начинается извлечение бронирований.")
reservations = Reservation.objects.filter(
Q(check_in__lte=self.end_time) & Q(check_out__gte=self.start_time)
)
self.log_info(f"Найдено {reservations.count()} бронирований для анализа.")
return reservations
except Exception as e:
self.log_error(f"Ошибка при извлечении бронирований: {e}")
return Reservation.objects.none()
def find_violations(self):
self.log_info("Начинается анализ несоответствий.")
user_logs = self.fetch_user_logs()
reservations = self.fetch_reservations()
log_lookup = {}
for log in user_logs:
params = parse_qs(log.url_parameters or "")
hotel_id = params.get("utm_content", [None])[0]
room_number = params.get("utm_term", [None])[0]
if hotel_id and room_number:
key = (hotel_id, room_number)
log_lookup.setdefault(key, []).append(log)
for reservation in reservations:
key = (reservation.hotel.hotel_id, reservation.room_number)
logs = log_lookup.get(key, [])
if reservation.status == "заселен" and not logs:
self.record_violation(
hotel=reservation.hotel,
room_number=reservation.room_number,
violation_type="no_qr_scan",
details=f"Бронирование для номера {reservation.room_number} в отеле '{reservation.hotel.name}' "
f"не имеет записи сканирования QR."
)
for log in logs:
if log.created < reservation.check_in:
self.record_violation(
hotel=reservation.hotel,
room_number=reservation.room_number,
violation_type="early_check_in",
details=f"Раннее заселение: сканирование QR {log.created} раньше времени заезда "
f"{reservation.check_in} для номера {reservation.room_number}."
)
for (hotel_id, room_number), logs in log_lookup.items():
matching_reservations = reservations.filter(
hotel__hotel_id=hotel_id,
room_number=room_number
)
if not matching_reservations.exists():
for log in logs:
self.record_violation(
hotel=Hotel.objects.filter(hotel_id=hotel_id).first(),
room_number=room_number,
violation_type="no_reservation",
details=f"Сканирование QR {log.created} для номера {room_number} в отеле с ID '{hotel_id}' "
f"не соответствует ни одному бронированию."
)
def record_violation(self, hotel, room_number, violation_type, details):
if hotel:
self.violations.append(ViolationLog(
hotel=hotel,
room_number=room_number,
violation_type=violation_type,
violation_details=details
))
self.log_warning(f"Зафиксировано нарушение: {details}")
def save_violations(self):
if self.violations:
ViolationLog.objects.bulk_create(self.violations)
self.log_info(f"Создано {len(self.violations)} записей в ViolationLog.")
else:
self.log_info("Нарушений не обнаружено.")
def run_check(self):
self.log_info(f"Запуск проверки с {self.start_time} по {self.end_time}.")
self.log_info(f"Запуск проверки.")
try:
self.find_violations()
self.save_violations()
hotels_map = {}
hotels = Hotel.objects.all()
for hotel in hotels:
hotels_map[hotel.hotel_id] = hotel
user_logs = UserActivityLog.objects.filter(fraud_checked=False)
reservations = Reservation.objects.filter(fraud_checked=False).select_related('hotel')
missing = list(reservations)
violations = []
check_in_diff = timedelta(hours=self.checkin_diff_hours)
for user_log in user_logs:
try:
params = json.loads(user_log.url_parameters.replace("'", '"'))
hotel_id = params['utm_content']
room = params['utm_term']
reserv = next((x for x in reservations
if x.hotel.hotel_id == hotel_id and x.room_number == room
and user_log.date_time >= x.check_in - check_in_diff and user_log.date_time < x.check_out
), None)
v_type = None
if reserv:
if reserv in missing:
missing.remove(reserv)
if user_log.date_time < reserv.check_in:
v_type = 'early'
if user_log.date_time > reserv.check_in + check_in_diff:
v_type = 'late'
else:
v_type = 'no_booking'
if v_type:
violations.append(RoomDiscrepancy(
hotel=hotels_map[hotel_id],
room_number=room,
discrepancy_type=v_type,
booking_id=reserv.reservation_id if reserv else None,
check_in_date_expected=reserv.check_in if reserv else None,
check_in_date_actual=user_log.date_time,
))
user_log.fraud_checked = True
except Exception as e:
logger.error(e)
for miss_reserv in missing:
violations.append(RoomDiscrepancy(
hotel=miss_reserv.hotel,
room_number=miss_reserv.room_number,
discrepancy_type='missed',
booking_id=miss_reserv.reservation_id,
check_in_date_expected=miss_reserv.check_in,
))
for reserv in reservations:
reserv.fraud_checked = True
RoomDiscrepancy.objects.bulk_create(violations)
UserActivityLog.objects.bulk_update(user_logs, ['fraud_checked'], 1000)
Reservation.objects.bulk_update(reservations, ['fraud_checked'], 1000)
except Exception as e:
self.log_error(f"Ошибка при выполнении проверки: {e}")
self.log_info("Проверка завершена.")
@@ -139,4 +108,4 @@ def run_reservation_check():
checker.run_check()
except Exception as e:
logger.error(f"Ошибка при запуске проверки: {e}")
logger.info("run_reservation_check завершена.")
logger.info("run_reservation_check завершена.")

View File

@@ -15,7 +15,7 @@ from django.db.models import F
class DatabaseConnector:
def __init__(self, db_settings_id):
self.db_settings_id = db_settings_id
self.logger = CustomLogger(name="DatabaseConnector", log_level="DEBUG").get_logger()
self.logger = CustomLogger(name="DatabaseConnector", log_level="WARNING").get_logger()
self.connection = None
self.db_settings = self.get_db_settings()

View File

@@ -0,0 +1,8 @@
from django.core.management.base import BaseCommand
from antifroud.check_fraud import run_reservation_check
class Command(BaseCommand):
help = "Запуск проверки на несоответствия"
def handle(self, *args, **options):
run_reservation_check()

View File

@@ -0,0 +1,28 @@
# Generated by Django 5.1.4 on 2025-02-01 06:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('antifroud', '0002_initial'),
]
operations = [
migrations.AlterField(
model_name='roomdiscrepancy',
name='booking_id',
field=models.CharField(max_length=255, null=True, verbose_name='ID бронирования'),
),
migrations.AlterField(
model_name='roomdiscrepancy',
name='check_in_date_actual',
field=models.DateField(null=True, verbose_name='Фактическая дата заселения'),
),
migrations.AlterField(
model_name='roomdiscrepancy',
name='check_in_date_expected',
field=models.DateField(null=True, verbose_name='Ожидаемая дата заселения'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.4 on 2025-02-01 06:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('antifroud', '0003_alter_roomdiscrepancy_booking_id_and_more'),
]
operations = [
migrations.AlterField(
model_name='roomdiscrepancy',
name='discrepancy_type',
field=models.CharField(choices=[('early', 'Раннее заселение'), ('late', 'Позднее заселение'), ('missed', 'Неявка'), ('no_booking', 'Без брони')], max_length=50, verbose_name='Тип несоответствия'),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.1.4 on 2025-02-01 09:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('antifroud', '0004_alter_roomdiscrepancy_discrepancy_type'),
]
operations = [
migrations.AddField(
model_name='roomdiscrepancy',
name='fraud_checked',
field=models.BooleanField(default=False, verbose_name='Проверено на несоответствия'),
),
migrations.AddField(
model_name='useractivitylog',
name='fraud_checked',
field=models.BooleanField(default=False, verbose_name='Проверено на несоответствия'),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.1.4 on 2025-02-01 09:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('antifroud', '0005_roomdiscrepancy_fraud_checked_and_more'),
]
operations = [
migrations.AlterField(
model_name='roomdiscrepancy',
name='fraud_checked',
field=models.BooleanField(db_index=True, default=False, verbose_name='Проверено на несоответствия'),
),
migrations.AlterField(
model_name='useractivitylog',
name='fraud_checked',
field=models.BooleanField(db_index=True, default=False, verbose_name='Проверено на несоответствия'),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.1.4 on 2025-02-01 09:48
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('antifroud', '0006_alter_roomdiscrepancy_fraud_checked_and_more'),
]
operations = [
migrations.RemoveField(
model_name='roomdiscrepancy',
name='fraud_checked',
),
]

View File

@@ -31,6 +31,7 @@ class UserActivityLog(models.Model):
honeypot = models.BooleanField(verbose_name="Метка honeypot", blank=True, null=True)
reply = models.BooleanField(verbose_name="Ответ пользователя", blank=True, null=True)
page_url = models.URLField(blank=True, null=True, verbose_name="URL страницы")
fraud_checked = models.BooleanField(default=False, verbose_name="Проверено на несоответствия", db_index=True)
@property
def formatted_timestamp(self):
@@ -42,7 +43,7 @@ class UserActivityLog(models.Model):
return "Нет данных"
# Изменение имени столбца
class Meta:
indexes = [
models.Index(fields=["external_id"], name="idx_external_id"),
@@ -55,7 +56,7 @@ class UserActivityLog(models.Model):
verbose_name_plural = "Логи активности пользователей"
def __str__(self):
return f"UserActivityLog {self.id}: {self.page_title}"
class Meta:
verbose_name = "Регистрация посетителей"
verbose_name_plural = "Регистрации посетителей"
@@ -63,7 +64,7 @@ class UserActivityLog(models.Model):
def get_location(self):
if not self.ip:
return "IP-адрес отсутствует"
try:
db_path = f"{settings.GEOIP_PATH}/GeoLite2-City.mmdb"
geoip_reader = Reader(db_path)
@@ -78,10 +79,8 @@ class UserActivityLog(models.Model):
except AddressNotFoundError:
return "IP-адрес не найден в базе"
except FileNotFoundError:
logger.error(f"Файл базы данных GeoIP не найден по пути: {db_path}")
return "Файл базы данных GeoIP не найден"
except Exception as e:
logger.error(f"Ошибка при определении местоположения: {e}")
return "Местоположение недоступно"
class ExternalDBSettings(models.Model):
name = models.CharField(max_length=255, unique=True, help_text="Имя подключения для идентификации.")
@@ -107,12 +106,12 @@ class ExternalDBSettings(models.Model):
class RoomDiscrepancy(models.Model):
hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE, verbose_name="Отель")
room_number = models.CharField(max_length=50, verbose_name="Номер комнаты")
booking_id = models.CharField(max_length=255, verbose_name="ID бронирования")
check_in_date_expected = models.DateField(verbose_name="Ожидаемая дата заселения")
check_in_date_actual = models.DateField(verbose_name="Фактическая дата заселения")
booking_id = models.CharField(max_length=255, null=True, verbose_name="ID бронирования")
check_in_date_expected = models.DateField(null=True, verbose_name="Ожидаемая дата заселения")
check_in_date_actual = models.DateField(null=True, verbose_name="Фактическая дата заселения")
discrepancy_type = models.CharField(
max_length=50,
choices=[("early", "Раннее заселение"), ("late", "Позднее заселение"), ("missed", "Неявка")],
choices=[("early", "Раннее заселение"), ("late", "Позднее заселение"), ("missed", "Неявка"), ("no_booking", "Без брони")],
verbose_name="Тип несоответствия"
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
@@ -216,8 +215,8 @@ class SyncLog(models.Model):
def __str__(self):
return f"Отель: {self.hotel.name} | Получено: {self.recieved_records} | Обработано: {self.processed_records}"
class ViolationLog(models.Model):
hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE, verbose_name="Отель")
room_number = models.CharField(max_length=50, verbose_name="Номер комнаты", null=True, blank=True)

View File

@@ -7,6 +7,11 @@ services:
- MYSQL_DATABASE=touchh
- MYSQL_USER=touchh
- MYSQL_PASSWORD=${MYSQL_PASSWORD:-touchh}
healthcheck:
test: mariadb -utouchh -p${MYSQL_PASSWORD:-touchh} -e 'SELECT 1;' touchh
interval: 1s
timeout: 10s
retries: 10
volumes:
- ./var/mysql:/var/lib/mysql
bot:
@@ -15,7 +20,9 @@ services:
image: touchh-py
restart: on-failure
command: ['python3', 'manage.py', 'run_bot']
depends_on: ['db']
depends_on:
db:
condition: service_healthy
stop_signal: SIGINT
volumes:
- .:/app

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.4 on 2025-02-01 09:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hotels', '0003_rename_external_id_hotel_external_id_pms_and_more'),
]
operations = [
migrations.AddField(
model_name='reservation',
name='fraud_checked',
field=models.BooleanField(db_index=True, default=False, verbose_name='Проверено на несоответствия'),
),
]

View File

@@ -116,6 +116,7 @@ class Reservation(models.Model):
status = models.CharField(max_length=50, verbose_name="Статус")
price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name="Цена")
discount = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name="Скидка")
fraud_checked = models.BooleanField(default=False, verbose_name="Проверено на несоответствия", db_index=True)
def clean(self):
if self.check_out and self.check_in and self.check_out <= self.check_in:

View File

@@ -53,11 +53,25 @@ class PMSIntegrationManager:
Загружает плагин, соответствующий PMS конфигурации отеля.
"""
pms_name = self.hotel.pms.plugin_name.lower() # Приводим название плагина к нижнему регистру
if pms_name == "ecvi_intermark" or pms_name == "ecvi":
from pms_integration.plugins.ecvi_pms import EcviPMSPlugin
self.plugin = EcviPMSPlugin(self.hotel)
else:
raise ValueError(f"Неизвестный PMS: {pms_name}")
# Формируем имя модуля и класса плагина
plugin_module_name = f"pms_integration.plugins.{pms_name}_pms"
plugin_class_name = f"{pms_name.capitalize()}PMSPlugin"
try:
# Динамически импортируем модуль плагина
plugin_module = importlib.import_module(plugin_module_name)
# Динамически получаем класс плагина
plugin_class = getattr(plugin_module, plugin_class_name, None)
if not plugin_class or not issubclass(plugin_class, BasePMSPlugin):
raise ImportError(f"Класс {plugin_class_name} не найден или не является наследником BasePMSPlugin.")
# Инициализируем плагин
self.plugin = plugin_class(self.hotel)
except ImportError as e:
raise ValueError(f"Ошибка загрузки плагина для PMS {pms_name}: {e}")
def fetch_data(self):
"""
Получает данные из PMS с использованием загруженного плагина.

View File

@@ -176,13 +176,13 @@ class BnovoPMSPlugin(BasePMSPlugin):
"""Получение данных о бронированиях с помощью эндпоинта /dashboard."""
logger.info("Начало процесса получения данных о бронированиях.")
# # Вызов функции получения данных аккаунта
# try:
# account_data = await self._fetch_and_log_account_data()
# logger.info(f"Данные аккаунта успешно получены:")
# except Exception as e:
# logger.error(f"Ошибка получения данных аккаунта: {e}")
# raise
# Вызов функции получения данных аккаунта
try:
account_data = await self._fetch_and_log_account_data()
logger.info(f"Данные аккаунта успешно получены:")
except Exception as e:
logger.error(f"Ошибка получения данных аккаунта: {e}")
raise
url = f"{self.api_url}/dashboard"
now = datetime.now()

View File

@@ -33,7 +33,7 @@ class EcviPMSPlugin(BasePMSPlugin):
handler_file.setFormatter(formatter)
self.logger.addHandler(handler_console)
self.logger.addHandler(handler_file)
self.logger.setLevel(logging.DEBUG)
self.logger.setLevel(logging.WARNING)
def get_default_parser_settings(self):
"""

View File

@@ -1,34 +1,39 @@
import logging
import requests
import json
import os
from datetime import datetime, timedelta
from asgiref.sync import sync_to_async
from pms_integration.models import PMSConfiguration
from hotels.models import Hotel, Reservation
from .base_plugin import BasePMSPlugin
class EcviPMSPlugin(BasePMSPlugin):
from touchh.utils.log import CustomLogger
class ShelterPMSPlugin(BasePMSPlugin):
"""
Плагин для интеграции с PMS Ecvi (интерфейс для получения данных об отеле).
Плагин для интеграции с PMS Shelter.
"""
def __init__(self, pms_config):
super().__init__(pms_config)
# Инициализация логгера
self.logger = logging.getLogger(self.__class__.__name__) # Логгер с именем класса
handler = logging.StreamHandler() # Потоковый обработчик для вывода в консоль
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
self.logger.addHandler(handler)
self.logger.setLevel(logging.DEBUG) # Уровень логирования
def __init__(self, hotel):
super().__init__(hotel.pms) # Передаем PMS-конфигурацию в базовый класс
self.hotel = hotel # Сохраняем объект отеля
# Проверка PMS-конфигурации
if not self.hotel.pms:
raise ValueError(f"Отель {self.hotel.name} не имеет связанной PMS конфигурации.")
# Инициализация параметров API
self.api_url = pms_config.url
self.token = pms_config.token
self.username = pms_config.username
self.password = pms_config.password
self.pagination_count = 50 # Максимальное количество записей на страницу (если используется пагинация)
self.api_url = self.hotel.pms.url
self.token = self.hotel.pms.token
# Настройка логгера
self.logger = CustomLogger(name="ShelterPMSPlugin", log_level="WARNING").get_logger()
handler_console = logging.StreamHandler()
handler_file = logging.FileHandler('var/log/shelter_pms_plugin.log')
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
handler_console.setFormatter(formatter)
handler_file.setFormatter(formatter)
self.logger.addHandler(handler_console)
self.logger.addHandler(handler_file)
self.logger.setLevel(logging.WARNING)
def get_default_parser_settings(self):
"""
@@ -36,62 +41,145 @@ class EcviPMSPlugin(BasePMSPlugin):
"""
return {
"field_mapping": {
"check_in": "checkin",
"check_out": "checkout",
"room_number": "room_name",
"room_type_name": "room_type",
"status": "occupancy",
"check_in": "from",
"check_out": "until",
"room_number": "roomNumber",
"room_type_name": "roomTypeName",
"status": "checkInStatus",
},
"date_format": "%Y-%m-%dT%H:%M:%S"
}
async def _fetch_data(self):
"""
Получает данные из PMS API, фильтрует и сохраняет в базу данных.
Получает данные из PMS API и сохраняет их в базу.
"""
now = datetime.now()
current_date = now.strftime('%Y-%m-%d')
yesterday_date = (now - timedelta(days=1)).strftime('%Y-%m-%d')
start_date = (now - timedelta(days=60)).strftime('%Y-%m-%dT%H:%M:%SZ')
end_date = (now + timedelta(days=60)).strftime('%Y-%m-%dT%H:%M:%SZ')
headers = {
"Content-Type": "application/json",
"Accept": "text/plain",
"Authorization": f"Bearer {self.token}"
}
data = {
"token": self.token,
}
from_index = 0
count_per_request = 50
all_items = []
try:
# Запрос данных из PMS API
response = await sync_to_async(requests.post)(self.api_url, headers=headers, json=data, auth=(self.username, self.password))
response.raise_for_status() # Если ошибка, выбросит исключение
data = response.json() # Преобразуем ответ в JSON
self.logger.debug(f"Получены данные с API: {data}")
except requests.exceptions.RequestException as e:
self.logger.error(f"Ошибка запроса: {e}")
return []
# Фильтрация данных
filtered_data = []
for item in data:
if item.get('occupancy') in ['проживание', 'под выезд', 'под заезд']:
filtered_item = {
'checkin': datetime.strptime(item.get('checkin'), '%Y-%m-%d %H:%M:%S'),
'checkout': datetime.strptime(item.get('checkout'), '%Y-%m-%d %H:%M:%S'),
'room_number': item.get('room_name'),
'room_type': item.get('room_type'),
'status': item.get('occupancy')
while True:
data = {
"from": start_date,
"until": end_date,
"pagination": {
"from": from_index,
"count": count_per_request
}
}
filtered_data.append(filtered_item)
# Логируем результат фильтрации
self.logger.debug(f"Отфильтрованные данные: {filtered_data}")
response = await sync_to_async(requests.post)(self.api_url, headers=headers, data=json.dumps(data))
response.raise_for_status()
response_data = response.json()
# Сохранение данных в базу данных
for item in filtered_data:
await self._save_to_db(item)
items = response_data.get("items", [])
all_items.extend(items)
self.logger.debug(f"Данные успешно сохранены.")
return filtered_data
total_count = response_data.get("count", 0)
from_index += len(items)
if from_index >= total_count:
break
self.logger.info(f"Получено записей: {len(all_items)}")
# Сохранение данных во временный файл
temp_dir = os.path.join("temp", "shelter")
os.makedirs(temp_dir, exist_ok=True)
temp_file = os.path.join(temp_dir, f"shelter_data_{datetime.now().strftime('%Y%m%d%H%M%S')}.json")
with open(temp_file, 'w') as file:
json.dump(all_items, file)
self.logger.info(f"Данные сохранены во временный файл: {temp_file}")
return await self._process_data(all_items)
except requests.exceptions.RequestException as e:
self.logger.error(f"Ошибка API: {e}")
return {
"processed_intervals": 0,
"processed_items": 0,
"errors": [str(e)]
}
async def _process_data(self, data):
"""
Обрабатывает данные и сохраняет их в базу.
"""
processed_items = 0
errors = []
date_formats = ["%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"] # Поддержка нескольких форматов даты
for item in data:
try:
# Парсинг даты с поддержкой нескольких форматов
checkin = self._parse_date(item['from'], date_formats)
checkout = self._parse_date(item['until'], date_formats)
reservation, created = await sync_to_async(Reservation.objects.update_or_create)(
reservation_id=item['id'],
defaults={
'room_number': item.get('roomNumber'),
'room_type': item.get('roomTypeName'),
'check_in': checkin,
'check_out': checkout,
'status': item.get('checkInStatus'),
'hotel': self.hotel,
}
)
if created:
self.logger.debug(f"Создана новая резервация: {reservation.reservation_id}")
else:
self.logger.debug(f"Обновлена существующая резервация: {reservation.reservation_id}")
processed_items += 1
except Exception as e:
self.logger.error(f"Ошибка обработки записи: {e}")
errors.append(str(e))
return {
"processed_intervals": 1,
"processed_items": processed_items,
"errors": errors
}
@staticmethod
def _parse_date(date_str, formats):
"""
Парсит дату, пытаясь использовать несколько форматов.
"""
for fmt in formats:
try:
return datetime.strptime(date_str, fmt)
except ValueError:
continue
raise ValueError(f"Дата '{date_str}' не соответствует ожидаемым форматам: {formats}")
def validate_plugin(self):
"""
Проверка корректности реализации плагина.
"""
required_methods = ["fetch_data", "get_default_parser_settings", "_fetch_data"]
for method in required_methods:
if not hasattr(self, method):
raise ValueError(f"Плагин {type(self).__name__} не реализует метод {method}.")
self.logger.debug(f"Плагин {self.__class__.__name__} прошел валидацию.")
return True
async def _save_to_db(self, item):
"""

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long