Compare commits

..

14 Commits

Author SHA1 Message Date
2ad382f01a ci/cd 2025-07-19 18:35:42 +09:00
67e487c378 ci/cd prepare 2025-07-19 18:34:51 +09:00
0cc3e469cb init commit 2025-02-26 22:35:18 +09:00
d771df32d5 remote changes 2025-02-26 22:29:39 +09:00
51569fb5c6 Merge branch 'zorn-dev' into PMSManager_refactor 2025-02-02 09:36:12 +09:00
6721ae3e8c xMerge branch 'zorn-dev' of ssh://git.smartsoltech.kr:2222/SmartSolTech/touchh_bot into zorn-dev 2025-02-02 09:28:20 +09:00
24f1a40561 RealtyCalendar plugin develop 2025-02-02 09:26:44 +09:00
zorn
d8bb7493e3 ReservationChecker 2025-02-01 22:03:25 +10:00
zorn
aca071f450 Fix migrations 2025-02-01 19:49:46 +10:00
zorn
6b98cda299 Add fraud_checked indexes 2025-02-01 19:38:47 +10:00
d826232dca Merge pull request 'zorn-dev' (#9) from zorn-dev into PMSManager_refactor
Reviewed-on: SmartSolTech/touchh_bot#9
2025-02-01 06:56:02 +00:00
05509f79fb init 2025-02-01 15:53:05 +09:00
157f47d86d some issues 2025-02-01 15:52:16 +09:00
e5e7a7f054 Merge pull request 'zorn-dev' (#8) from zorn-dev into PMSManager_refactor
Reviewed-on: SmartSolTech/touchh_bot#8
2025-01-29 07:53:46 +00:00
25 changed files with 644 additions and 1693 deletions

View File

@@ -12,20 +12,11 @@ steps:
- git reset --hard $DRONE_COMMIT
# Шаг 2: Обновление и запуск с помощью update.sh
- name: deploy_app
image: docker:24
environment:
MYSQL_PASSWORD: touchh
volumes:
- name: docker_sock
path: /var/run/docker.sock
commands:
- 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
- name: docker-build
image: plugins/docker
settings:
repo: trevor198507/touchh-py
dry_run: true
# Шаг 3: Миграция базы данных
- name: run_migrations

View File

@@ -151,6 +151,7 @@ class UserActivityLogAdmin(admin.ModelAdmin):
get_hotel_name.short_description = "Отель"
get_room_number.short_description = "Комната"
# from .views import import_selected_hotels
# # Регистрируем admin класс для ImportedHotel
# @admin.register(ImportedHotel)
@@ -247,4 +248,3 @@ class RoomDiscrepancyAdmin(admin.ModelAdmin):
class Meta:
model = RoomDiscrepancy
fields = ['hotel', 'room_number', 'booking_id','created_at', 'check_in_date_expected','check_in_date_actual','discrepancy_type']

View File

@@ -1,27 +1,149 @@
# import json
# from datetime import timedelta
# from django.utils import timezone
# from django.db.models import Q
# from hotels.models import Reservation, Hotel
# from .models import UserActivityLog, RoomDiscrepancy
# from touchh.utils.log import CustomLogger
# # Настройка логирования
# logger = CustomLogger(__name__).get_logger()
# class ReservationChecker:
# """
# Класс для проверки несоответствий между бронированиями и логами заселения.
# """
# def __init__(self):
# self.checkin_diff_hours = 3
# def log_info(self, message):
# logger.info(message)
# def log_warning(self, message):
# logger.warning(message)
# def log_error(self, message):
# logger.error(message)
# def run_check(self):
# """Запуск проверки фродовых событий."""
# self.log_info("Запуск проверки фродовых данных.")
# try:
# check_in_diff = timedelta(hours=self.checkin_diff_hours)
# # Кэшируем отели в словарь для быстрого доступа
# hotels_map = {hotel.hotel_id: hotel for hotel in Hotel.objects.all()}
# # Загружаем бронирования и активности пользователей
# user_logs = UserActivityLog.objects.filter(fraud_checked=False)
# reservations = Reservation.objects.filter(fraud_checked=False).select_related('hotel')
# # Преобразуем бронирования в словарь для быстрого поиска
# reservations_map = {
# (res.hotel.hotel_id, res.room_number): res for res in reservations
# }
# violations = []
# missing_reservations = set(reservations) # Сет для поиска пропавших бронирований
# for user_log in user_logs:
# try:
# params = json.loads(user_log.url_parameters.replace("'", '"')) if user_log.url_parameters else {}
# hotel_id = params.get('utm_content')
# room = params.get('utm_term')
# if not hotel_id or not room:
# continue # Пропускаем записи без нужных параметров
# key = (hotel_id, room)
# reserv = reservations_map.get(key)
# discrepancy_type = None
# if reserv:
# if reserv in missing_reservations:
# missing_reservations.remove(reserv)
# if user_log.date_time < reserv.check_in:
# discrepancy_type = 'early'
# elif user_log.date_time > reserv.check_in + check_in_diff:
# discrepancy_type = 'late'
# else:
# discrepancy_type = 'no_booking'
# if discrepancy_type:
# violations.append(RoomDiscrepancy(
# hotel=hotels_map.get(hotel_id),
# room_number=room,
# discrepancy_type=discrepancy_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 json.JSONDecodeError:
# self.log_error(f"Ошибка декодирования JSON в URL-параметрах: {user_log.url_parameters}")
# except Exception as e:
# self.log_error(f"Ошибка при обработке логов: {e}")
# # Добавляем пропущенные бронирования
# for miss_reserv in missing_reservations:
# 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,
# ))
# # Массово сохраняем нарушения
# if violations:
# RoomDiscrepancy.objects.bulk_create(violations)
# self.log_info(f"Записано {len(violations)} новых несоответствий.")
# # Обновляем флаги fraud_checked
# UserActivityLog.objects.filter(id__in=[log.id for log in user_logs]).update(fraud_checked=True)
# Reservation.objects.filter(id__in=[res.id for res in reservations]).update(fraud_checked=True)
# except Exception as e:
# self.log_error(f"Ошибка при выполнении проверки: {e}")
# self.log_info("Проверка завершена.")
# # Функция для запуска из планировщика
# def run_reservation_check():
# """Запуск проверки через планировщик."""
# logger.info("Планировщик вызывает run_reservation_check.")
# try:
# checker = ReservationChecker()
# checker.run_check()
# except Exception as e:
# logger.error(f"Ошибка при запуске проверки: {e}")
# logger.info("run_reservation_check завершена.")
import json
from datetime import timedelta
from urllib.parse import parse_qs
from django.utils import timezone
from django.db.models import Q
from hotels.models import Reservation, Hotel
from .models import UserActivityLog, ViolationLog, RoomDiscrepancy
from .models import UserActivityLog, RoomDiscrepancy
from touchh.utils.log import CustomLogger
# Настройка логирования
logger = CustomLogger(__name__).get_logger()
class ReservationChecker:
"""
Класс для проверки несоответствий между бронированиями и логами заселения.
"""
def __init__(self):
"""
Инициализация времени проверки и списка нарушений.
"""
self.start_time = timezone.now() - timedelta(days=30)
self.end_time = timezone.now()
self.violations = []
self.checkin_diff_hours = 3
self.checkin_diff_hours = 3 # Разрешенное отклонение от времени заселения
def log_info(self, message):
logger.info(message)
@@ -32,112 +154,106 @@ 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("🔍 Запуск проверки фродовых данных.")
try:
self.find_violations()
self.save_violations()
check_in_diff = timedelta(hours=self.checkin_diff_hours)
# Кэшируем отели в словарь для быстрого доступа
hotels_map = {hotel.hotel_id: hotel for hotel in Hotel.objects.all()}
# Загружаем бронирования и активности пользователей
user_logs = UserActivityLog.objects.filter(fraud_checked=False)
reservations = Reservation.objects.filter(fraud_checked=False).select_related('hotel')
# Преобразуем бронирования в словарь для быстрого поиска
reservations_map = {
(res.hotel.hotel_id, res.room_number): res for res in reservations
}
violations = []
checked_reservations = set() # Сет для бронирований, которые были проверены
self.log_info(f"✅ Загружено {len(user_logs)} логов активности и {len(reservations)} бронирований.")
for user_log in user_logs:
try:
params = json.loads(user_log.url_parameters.replace("'", '"')) if user_log.url_parameters else {}
hotel_id = params.get('utm_content')
room = params.get('utm_term')
if not hotel_id or not room:
self.log_warning(f"🚫 Пропущен лог без hotel_id или room_number: {user_log.url_parameters}")
continue # Пропускаем записи без нужных параметров
key = (hotel_id, room)
reserv = reservations_map.get(key)
discrepancy_type = "match" # По умолчанию считаем, что всё соответствует
if reserv:
checked_reservations.add(reserv)
if user_log.date_time < reserv.check_in:
discrepancy_type = 'early'
self.log_warning(f"⚠️ Обнаружено раннее заселение: {user_log.date_time} < {reserv.check_in}")
elif user_log.date_time > reserv.check_in + check_in_diff:
discrepancy_type = 'late'
self.log_warning(f"⚠️ Обнаружено позднее заселение: {user_log.date_time} > {reserv.check_in + check_in_diff}")
else:
discrepancy_type = 'no_booking'
self.log_warning(f"🚨 Заселение без бронирования: {user_log.date_time} (Отель {hotel_id}, Комната {room})")
violations.append(RoomDiscrepancy(
hotel=hotels_map.get(hotel_id),
room_number=room,
discrepancy_type=discrepancy_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 json.JSONDecodeError:
self.log_error(f"❌ Ошибка декодирования JSON в URL-параметрах: {user_log.url_parameters}")
except Exception as e:
self.log_error(f"Ошибка при выполнении проверки: {e}")
self.log_info("Проверка завершена.")
self.log_error(f"Ошибка при обработке логов: {e}")
# Добавляем пропущенные бронирования (неявки)
for reserv in reservations:
if reserv not in checked_reservations:
violations.append(RoomDiscrepancy(
hotel=reserv.hotel,
room_number=reserv.room_number,
discrepancy_type='missed',
booking_id=reserv.reservation_id,
check_in_date_expected=reserv.check_in,
))
self.log_warning(f"⚠️ Обнаружена неявка (missed) | Отель: {reserv.hotel.hotel_id}, Номер: {reserv.room_number}, Ожидаемая дата заезда: {reserv.check_in}")
# Массово сохраняем все записи, включая корректные совпадения
if violations:
RoomDiscrepancy.objects.bulk_create(violations)
self.log_info(f"✅ Записано {len(violations)} новых записей в RoomDiscrepancy.")
# Обновляем флаги fraud_checked
UserActivityLog.objects.filter(id__in=[log.id for log in user_logs]).update(fraud_checked=True)
Reservation.objects.filter(id__in=[res.id for res in reservations]).update(fraud_checked=True)
except Exception as e:
self.log_error(f"❌ Ошибка при выполнении проверки: {e}")
self.log_info("✅ Проверка фродовых данных завершена.")
# Функция для запуска из планировщика
def run_reservation_check():
logger.info("Планировщик вызывает run_reservation_check.")
"""Запуск проверки через планировщик."""
logger.info("📅 Планировщик вызывает run_reservation_check.")
try:
checker = ReservationChecker()
checker.run_check()
except Exception as e:
logger.error(f"Ошибка при запуске проверки: {e}")
logger.info("run_reservation_check завершена.")
logger.error(f"Ошибка при запуске проверки: {e}")
logger.info("run_reservation_check завершена.")

View File

@@ -314,7 +314,7 @@ def scheduled_sync():
except Exception as e:
logger.error(f"Error syncing connection {db_settings}: {e}")
with ThreadPoolExecutor(max_workers=5) as executor:
with ThreadPoolExecutor(max_workers=10) as executor:
futures = [executor.submit(sync_task, db_settings) for db_settings in active_db_settings]
for future in futures:
try:

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,7 +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="Проверено на несоответствия")
fraud_checked = models.BooleanField(default=False, verbose_name="Проверено на несоответствия", db_index=True)
@property
def formatted_timestamp(self):
@@ -79,10 +79,10 @@ class UserActivityLog(models.Model):
except AddressNotFoundError:
return "IP-адрес не найден в базе"
except FileNotFoundError:
logger.error(f"Файл базы данных GeoIP не найден по пути: {db_path}")
# logger.error(f"Файл базы данных GeoIP не найден по пути: {db_path}")
return "Файл базы данных GeoIP не найден"
except Exception as e:
logger.error(f"Ошибка при определении местоположения: {e}")
# logger.error(f"Ошибка при определении местоположения: {e}")
return "Местоположение недоступно"
class ExternalDBSettings(models.Model):
name = models.CharField(max_length=255, unique=True, help_text="Имя подключения для идентификации.")
@@ -117,7 +117,6 @@ class RoomDiscrepancy(models.Model):
verbose_name="Тип несоответствия"
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
fraud_checked = models.BooleanField(default=False, verbose_name="Проверено на несоответствия")
def __str__(self):
return f"{self.hotel.name} - Room {self.room_number}: {self.discrepancy_type}"

View File

@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -85,14 +85,14 @@ class HotelAdmin(admin.ModelAdmin):
class UserHotelAdmin(admin.ModelAdmin):
list_display = ('user', 'hotel')
search_fields = ('user__username', 'hotel__name')
# list_filter = ('hotel',)
# ordering = ('-hotel',)
list_filter = ('hotel',)
ordering = ('-hotel',)
@admin.register(Reservation)
class ReservationAdmin(admin.ModelAdmin):
list_display = ('reservation_id', 'hotel', 'room_number', 'room_type', 'check_in', 'check_out', 'status', 'price', 'discount')
search_fields = ('hotel', 'room_number', 'room_type', 'check_in', 'check_out', 'status', 'price', 'discount')
list_filter = ('hotel', 'room_number', 'room_type', 'check_in', 'check_out', 'status', 'price', 'discount')
list_display = ('reservation_id', 'hotel', 'room_number', 'room_type', 'check_in', 'check_out', 'status', 'fraud_checked')
search_fields = ('hotel', 'room_number', 'room_type', 'check_in', 'check_out', 'status')
list_filter = ('hotel', 'room_number', 'room_type', 'check_in', 'check_out', 'status')
ordering = ('-check_in',)
@admin.register(Room)

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

@@ -0,0 +1,23 @@
# Generated by Django 5.1.4 on 2025-02-02 00:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hotels', '0004_reservation_fraud_checked'),
]
operations = [
migrations.AlterField(
model_name='reservation',
name='check_in',
field=models.DateTimeField(blank=True, null=True, verbose_name='Дата заезда'),
),
migrations.AlterField(
model_name='reservation',
name='check_out',
field=models.DateTimeField(blank=True, null=True, verbose_name='Дата выезда'),
),
]

View File

@@ -111,11 +111,12 @@ class Reservation(models.Model):
reservation_id = models.BigIntegerField(unique=True, verbose_name="ID бронирования")
room_number = models.CharField(max_length=255, null=True, blank=True, verbose_name="Номер комнаты")
room_type = models.CharField(max_length=255, verbose_name="Тип комнаты")
check_in = models.DateTimeField(verbose_name="Дата заезда")
check_out = models.DateTimeField(verbose_name="Дата выезда")
check_in = models.DateTimeField(verbose_name="Дата заезда", null=True, blank=True)
check_out = models.DateTimeField(verbose_name="Дата выезда", null=True, blank=True)
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

@@ -1,4 +1,144 @@
# # ecvi_pms.py
# import logging
# import requests
# import json
# import os
# from datetime import datetime, timedelta
# from asgiref.sync import sync_to_async
# from hotels.models import Hotel, Reservation
# from .base_plugin import BasePMSPlugin
# class EcviPMSPlugin(BasePMSPlugin):
# """
# Плагин для интеграции с PMS Ecvi.
# """
# def __init__(self, hotel):
# super().__init__(hotel.pms)
# self.hotel = hotel
# if not self.hotel.pms:
# raise ValueError(f"Отель {self.hotel.name} не имеет связанной PMS конфигурации.")
# self.api_url = self.hotel.pms.url.rstrip("/")
# self.token = self.hotel.pms.token
# self.username = self.hotel.pms.username
# self.password = self.hotel.pms.password
# self.logger = logging.getLogger(self.__class__.__name__)
# handler_console = logging.StreamHandler()
# handler_file = logging.FileHandler('var/log/ecvi_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):
# return {
# "field_mapping": {
# "check_in": "checkin",
# "check_out": "checkout",
# "room_number": "room_name",
# "room_type_name": "room_type",
# "status": "occupancy",
# },
# "date_format": "%Y-%m-%d %H:%M:%S"
# }
# async def _fetch_data(self):
# headers = {"Content-Type": "application/json"}
# data = {"token": self.token}
# try:
# response = await sync_to_async(requests.post)(
# self.api_url, headers=headers, json=data, auth=(self.username, self.password)
# )
# response.raise_for_status()
# response_data = response.json()
# self.logger.debug(f"Полученные данные с API: {response_data}")
# # Группировка данных по номеру комнаты
# structured_data = {}
# for item in response_data:
# room_number = item.get("room_name", "unknown")
# if room_number not in structured_data:
# structured_data[room_number] = []
# structured_data[room_number].append(item)
# # Сохранение данных во временный JSON-файл
# temp_dir = os.path.join("temp", "ecvi")
# os.makedirs(temp_dir, exist_ok=True)
# temp_file = os.path.join(temp_dir, f"ecvi_data_{datetime.now().strftime('%Y%m%d%H%M%S')}.json")
# with open(temp_file, 'w') as file:
# json.dump(structured_data, file, indent=4, ensure_ascii=False)
# return await self._process_data(response_data)
# 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 = item['checkin']
# checkout = item['checkout']
# if checkin in [None, "0000-00-00 00:00:00"] or checkout in [None, "0000-00-00 00:00:00"]:
# continue
# checkin = self._parse_date(checkin, date_formats)
# checkout = self._parse_date(checkout, date_formats)
# reservation, created = await sync_to_async(Reservation.objects.update_or_create)(
# reservation_id=item['task_id'],
# defaults={
# 'room_number': item['room_name'],
# 'room_type': item['room_type'],
# 'check_in': checkin,
# 'check_out': checkout,
# 'status': item['occupancy'],
# 'hotel': self.hotel,
# }
# )
# 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
import logging
import os
import json
from datetime import datetime, timedelta
import requests
from asgiref.sync import sync_to_async
@@ -11,20 +151,17 @@ class EcviPMSPlugin(BasePMSPlugin):
"""
def __init__(self, hotel):
super().__init__(hotel.pms) # Передаем PMS-конфигурацию в базовый класс
self.hotel = hotel # Сохраняем объект отеля
super().__init__(hotel.pms)
self.hotel = hotel
# Проверка PMS-конфигурации
if not self.hotel.pms:
raise ValueError(f"Отель {self.hotel.name} не имеет связанной PMS конфигурации.")
# Инициализация параметров API
self.api_url = self.hotel.pms.url
self.token = self.hotel.pms.token
self.username = self.hotel.pms.username
self.password = self.hotel.pms.password
# Настройка логгера
self.logger = logging.getLogger(self.__class__.__name__)
handler_console = logging.StreamHandler()
handler_file = logging.FileHandler('var/log/ecvi_pms_plugin.log')
@@ -35,6 +172,10 @@ class EcviPMSPlugin(BasePMSPlugin):
self.logger.addHandler(handler_file)
self.logger.setLevel(logging.WARNING)
# Директория для сохранения JSON-файлов
self.data_dir = "var/data/ecvi"
os.makedirs(self.data_dir, exist_ok=True)
def get_default_parser_settings(self):
"""
Возвращает настройки парсера по умолчанию.
@@ -47,7 +188,7 @@ class EcviPMSPlugin(BasePMSPlugin):
"room_type_name": "room_type",
"status": "occupancy",
},
"date_format": "%Y-%m-%d %H:%M:%S" # Формат изменен на соответствующий данным
"date_format": "%Y-%m-%d %H:%M:%S"
}
async def _fetch_data(self):
@@ -58,14 +199,23 @@ class EcviPMSPlugin(BasePMSPlugin):
data = {"token": self.token}
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()
response_data = response.json()
self.logger.debug(f"Полученные данные с API: {response_data}")
# Сохраняем весь ответ API в файл для анализа
file_name = f"ecvi_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
file_path = os.path.join(self.data_dir, file_name)
with open(file_path, "w", encoding="utf-8") as f:
json.dump(response_data, f, ensure_ascii=False, indent=4)
self.logger.info(f"API-ответ сохранен в файл: {file_path}")
return await self._process_data(response_data)
except requests.exceptions.RequestException as e:
self.logger.error(f"Ошибка API: {e}")
return {
@@ -80,22 +230,62 @@ class EcviPMSPlugin(BasePMSPlugin):
"""
processed_items = 0
errors = []
date_formats = ["%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"]
unix_epoch = datetime(1970, 1, 1, 0, 0, 0)
date_formats = ["%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"] # Поддержка нескольких форматов даты
valid_reservations = []
print(data)
for item in data:
try:
# Парсинг даты с поддержкой нескольких форматов
checkin = self._parse_date(item['checkin'], date_formats)
checkout = self._parse_date(item['checkout'], date_formats)
checkin = item.get('checkin')
checkout = item.get('checkout')
# Фильтруем записи с некорректными датами
if checkin in [None, "0000-00-00 00:00:00", "1970-01-01 00:00:00", 0] or \
checkout in [None, "0000-00-00 00:00:00", "1970-01-01 00:00:00", 0]:
self.logger.warning(f"Игнорируется запись с некорректной датой: {item}")
continue
checkin = self._parse_date(checkin, date_formats)
checkout = self._parse_date(checkout, date_formats)
# Проверяем на Unix epoch
if checkin == unix_epoch or checkout == unix_epoch:
self.logger.warning(f"Игнорируется запись с Unix epoch датой: {item}")
continue
# Проверяем timestamp
if checkin.timestamp() == 0 or checkout.timestamp() == 0:
self.logger.warning(f"Игнорируется запись с timestamp = 0: {item}")
continue
valid_reservations.append(item)
except Exception as e:
self.logger.error(f"Ошибка обработки записи: {e}")
errors.append(str(e))
# Логируем количество отфильтрованных записей
self.logger.info(f"Обработано бронирований: {len(valid_reservations)}")
# Сохранение валидных бронирований в JSON для проверки
valid_file_name = f"valid_reservations_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
valid_file_path = os.path.join(self.data_dir, valid_file_name)
with open(valid_file_path, "w", encoding="utf-8") as f:
json.dump(valid_reservations, f, ensure_ascii=False, indent=4)
self.logger.info(f"Валидные бронирования сохранены в файл: {valid_file_path}")
# Сохранение данных в БД
for item in valid_reservations:
try:
reservation, created = await sync_to_async(Reservation.objects.update_or_create)(
reservation_id=item['task_id'],
defaults={
'room_number': item['room_name'],
'room_type': item['room_type'],
'check_in': checkin,
'check_out': checkout,
'check_in': self._parse_date(item['checkin'], date_formats),
'check_out': self._parse_date(item['checkout'], date_formats),
'status': item['occupancy'],
'hotel': self.hotel,
}
@@ -109,7 +299,7 @@ class EcviPMSPlugin(BasePMSPlugin):
processed_items += 1
except Exception as e:
self.logger.error(f"Ошибка обработки записи: {e}")
self.logger.error(f"Ошибка сохранения бронирования: {e}")
errors.append(str(e))
return {

View File

@@ -1,313 +1,111 @@
import logging
import requests
import hashlib
import json
from .base_plugin import BasePMSPlugin
import os
from datetime import datetime, timedelta
from asgiref.sync import sync_to_async
from touchh.utils.log import CustomLogger
from hotels.models import Hotel, Reservation
from app_settings.models import GlobalHotelSettings
from django.utils import timezone
from .base_plugin import BasePMSPlugin
class RealtyCalendarPlugin(BasePMSPlugin):
def __init__(self, config):
super().__init__(config)
self.public_key = config.public_key
self.private_key = config.private_key
self.api_url = config.url.rstrip("/")
self.logger = CustomLogger(name="RealtyCalendarPlugin", log_level="DEBUG").get_logger()
if not self.public_key or not self.private_key:
raise ValueError("Публичный или приватный ключ отсутствует для RealtyCalendar")
class RealtyCalendarPMSPlugin(BasePMSPlugin):
def __init__(self, hotel):
super().__init__(hotel.pms)
self.hotel = hotel
if not self.hotel.pms:
raise ValueError(f"Отель {self.hotel.name} не имеет связанной PMS конфигурации.")
self.api_url = self.hotel.pms.url.rstrip("/")
self.public_key = self.hotel.pms.public_key
self.private_key = self.hotel.pms.private_key
self.logger = logging.getLogger(self.__class__.__name__)
handler_console = logging.StreamHandler()
handler_file = logging.FileHandler('var/log/realtycalendar_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):
"""
Возвращает настройки по умолчанию для обработки данных.
"""
return {
"date_format": "%Y-%m-%dT%H:%M:%S",
"timezone": "UTC"
"field_mapping": {
"check_in": "begin_date",
"check_out": "end_date",
"room_number": "apartment_id",
"room_type_name": "notes",
"status": "status",
},
"date_format": "%Y-%m-%d %H:%M:%S"
}
def _get_sorted_keys(self, obj):
sorted_keys = sorted(obj.keys())
self.logger.debug(f"Отсортированные ключи: {sorted_keys}")
return sorted_keys
def _generate_data_string(self, obj):
sorted_keys = self._get_sorted_keys(obj)
string = "".join(f"{key}={obj[key]}" for key in sorted_keys)
self.logger.debug(f"Сформированная строка данных: {string}")
return string + self.private_key
def _generate_md5(self, string):
md5_hash = hashlib.md5(string.encode("utf-8")).hexdigest()
self.logger.debug(f"Сформированный MD5-хеш: {md5_hash}")
return md5_hash
def _generate_sign(self, data):
data_string = self._generate_data_string(data)
self.logger.debug(f"Строка для подписи: {data_string}")
sign = self._generate_md5(data_string)
self.logger.debug(f"Подпись: {sign}")
return sign
# async def _fetch_data(self):
# self.logger.debug("Начало выполнения функции _fetch_data")
# base_url = f"{self.api_url}/api/v1/bookings/{self.public_key}/"
# headers = {
# "Accept": "application/json",
# "Content-Type": "application/json",
# }
# now = datetime.now()
# data = {
# "begin_date": (now - timedelta(days=7)).strftime("%Y-%m-%d"),
# "end_date": now.strftime("%Y-%m-%d"),
# }
# data["sign"] = self._generate_sign(data)
# try:
# response = requests.post(url=base_url, headers=headers, json=data)
# self.logger.debug(f"Статус ответа: {response.status_code}")
# if response.status_code != 200:
# self.logger.error(f"Ошибка API: {response.status_code}, {response.text}")
# raise ValueError(f"Ошибка API RealtyCalendar: {response.status_code}")
# try:
# response_data = response.json()
# bookings = response_data.get("bookings", [])
# # self.logger.debug(f"Полученные данные: {bookings}")
# if not isinstance(bookings, list):
# self.logger.error(f"Ожидался список, но получен: {type(bookings)}")
# raise ValueError("Некорректный формат данных для bookings")
# except json.JSONDecodeError as e:
# self.logger.error(f"Ошибка декодирования JSON: {e}")
# raise ValueError("Ответ не является корректным JSON.")
# except Exception as e:
# self.logger.error(f"Ошибка обработки ответа API: {e}")
# raise
# except Exception as e:
# self.logger.error(f"Ошибка запроса к API RealtyCalendar: {e}")
# raise
# try:
# hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config)
# hotel_tz = hotel.timezone
# self.logger.debug(f"Настройки отеля: {hotel.name}, Timezone: {hotel_tz}")
# hotel_settings = await sync_to_async(GlobalHotelSettings.objects.first)()
# check_in_time = hotel_settings.check_in_time.strftime("%H:%M:%S") if hotel_settings else "14:00:00"
# check_out_time = hotel_settings.check_out_time.strftime("%H:%M:%S") if hotel_settings else "12:00:00"
# except Exception as e:
# self.logger.error(f"Ошибка получения настроек отеля: {e}")
# check_in_time, check_out_time = "14:00:00", "12:00:00"
# filtered_data = []
# for item in bookings:
# try:
# if not isinstance(item, dict):
# self.logger.error(f"Некорректный формат элемента: {item}")
# continue
# reservation_id = item.get('id')
# if not reservation_id:
# self.logger.error(f"ID резервации отсутствует: {item}")
# continue
# begin_date = item.get('begin_date')
# end_date = item.get('end_date')
# if not begin_date or not end_date:
# self.logger.error(f"Отсутствуют даты в записи: {item}")
# continue
# checkin = timezone.make_aware(
# datetime.strptime(f"{begin_date} {check_in_time}", "%Y-%m-%d %H:%M:%S")
# )
# checkout = timezone.make_aware(
# datetime.strptime(f"{end_date} {check_out_time}", "%Y-%m-%d %H:%M:%S")
# )
# filtered_data.append({
# 'reservation_id': reservation_id,
# 'checkin': checkin,
# 'checkout': checkout,
# 'room_number': item.get('apartment_id'),
# 'room_type': item.get('notes', 'Описание отсутствует'),
# 'status': item.get('status')
# })
# except Exception as e:
# self.logger.error(f"Ошибка обработки элемента: {e}")
# # self.logger.debug(f"Отфильтрованные данные: {filtered_data}")
# await self._save_to_db(filtered_data)
async def _fetch_data(self):
self.logger.debug("Начало выполнения функции _fetch_data")
base_url = f"{self.api_url}/api/v1/bookings/{self.public_key}/"
headers = {
"Accept": "application/json",
"Content-Type": "application/json",
}
headers = {"Accept": "application/json", "Content-Type": "application/json"}
now = datetime.now()
data = {
"begin_date": (now - timedelta(days=7)).strftime("%Y-%m-%d"),
"end_date": now.strftime("%Y-%m-%d"),
"sign": self._generate_sign()
}
data["sign"] = self._generate_sign(data)
try:
response = requests.post(url=base_url, headers=headers, json=data)
self.logger.debug(f"Статус ответа: {response.status_code}")
if response.status_code != 200:
self.logger.error(f"Ошибка API: {response.status_code}, {response.text}")
return {
"processed_intervals": 0,
"processed_items": 0,
"errors": [f"Ошибка API RealtyCalendar: {response.status_code}"]
}
response = await sync_to_async(requests.post)(
f"{self.api_url}/api/v1/bookings/{self.public_key}/",
headers=headers, json=data
)
response.raise_for_status()
response_data = response.json()
bookings = response_data.get("bookings", [])
return await self._process_data(response_data)
except requests.exceptions.RequestException as e:
self.logger.error(f"Ошибка API: {e}")
return {"processed_intervals": 0, "processed_items": 0, "errors": [str(e)]}
if not isinstance(bookings, list):
self.logger.error(f"Ожидался список, но получен: {type(bookings)}")
return {
"processed_intervals": 0,
"processed_items": 0,
"errors": ["Некорректный формат данных для bookings"]
}
except json.JSONDecodeError as e:
self.logger.error(f"Ошибка декодирования JSON: {e}")
return {
"processed_intervals": 0,
"processed_items": 0,
"errors": ["Ответ не является корректным JSON."]
}
except Exception as e:
self.logger.error(f"Ошибка запроса к API RealtyCalendar: {e}")
return {
"processed_intervals": 0,
"processed_items": 0,
"errors": [str(e)]
}
# Получение настроек отеля
try:
hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config)
hotel_tz = hotel.timezone
self.logger.debug(f"Настройки отеля: {hotel.name}, Timezone: {hotel_tz}")
hotel_settings = await sync_to_async(GlobalHotelSettings.objects.first)()
check_in_time = hotel_settings.check_in_time.strftime("%H:%M:%S") if hotel_settings else "14:00:00"
check_out_time = hotel_settings.check_out_time.strftime("%H:%M:%S") if hotel_settings else "12:00:00"
except Exception as e:
self.logger.error(f"Ошибка получения настроек отеля: {e}")
check_in_time, check_out_time = "14:00:00", "12:00:00"
# Обработка записей
async def _process_data(self, data):
processed_items = 0
errors = []
filtered_data = []
date_formats = ["%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"]
for item in bookings:
for item in data.get("bookings", []):
try:
if not isinstance(item, dict):
raise ValueError(f"Некорректный формат элемента: {item}")
checkin = self._parse_date(item['begin_date'], date_formats)
checkout = self._parse_date(item['end_date'], date_formats)
reservation_id = item.get('id')
if not reservation_id:
raise ValueError(f"ID резервации отсутствует: {item}")
begin_date = item.get('begin_date')
end_date = item.get('end_date')
if not begin_date or not end_date:
raise ValueError(f"Отсутствуют даты в записи: {item}")
checkin = timezone.make_aware(
datetime.strptime(f"{begin_date} {check_in_time}", "%Y-%m-%d %H:%M:%S")
)
checkout = timezone.make_aware(
datetime.strptime(f"{end_date} {check_out_time}", "%Y-%m-%d %H:%M:%S")
)
filtered_data.append({
'reservation_id': reservation_id,
'checkin': checkin,
'checkout': checkout,
'room_number': item.get('apartment_id'),
reservation, created = await sync_to_async(Reservation.objects.update_or_create)(
reservation_id=item['id'],
defaults={
'room_number': item['apartment_id'],
'room_type': item.get('notes', 'Описание отсутствует'),
'status': item.get('status')
})
'check_in': checkin,
'check_out': checkout,
'status': item['status'],
'hotel': self.hotel,
}
)
processed_items += 1
except Exception as e:
self.logger.error(f"Ошибка обработки элемента: {e}")
self.logger.error(f"Ошибка обработки записи: {e}")
errors.append(str(e))
# Сохранение в БД
return {"processed_intervals": 1, "processed_items": processed_items, "errors": errors}
def _generate_sign(self):
return hashlib.md5((self.public_key + self.private_key).encode("utf-8")).hexdigest()
@staticmethod
def _parse_date(date_str, formats):
for fmt in formats:
try:
await self._save_to_db(filtered_data)
except Exception as e:
self.logger.error(f"Ошибка сохранения данных в БД: {e}")
errors.append(f"Ошибка сохранения данных в БД: {str(e)}")
# Формирование отчета
report = {
"processed_intervals": 1, # Пример значения
"processed_items": processed_items,
"errors": errors
}
self.logger.debug(f"Сформированный отчет: {report}")
return report
async def _save_to_db(self, data):
if not isinstance(data, list):
self.logger.error(f"Ожидался список записей, но получен {type(data).__name__}")
return
for index, item in enumerate(data, start=1):
try:
hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config)
reservation_id = item.get('reservation_id')
if not reservation_id:
self.logger.error(f"Пропущена запись {index}: отсутствует 'id'")
return datetime.strptime(date_str, fmt)
except ValueError:
continue
existing_reservation = await sync_to_async(Reservation.objects.filter)(reservation_id=reservation_id)
existing_reservation = await sync_to_async(existing_reservation.first)()
defaults = {
'room_number': item['room_number'],
'room_type': item['room_type'],
'check_in': item['checkin'],
'check_out': item['checkout'],
'status': item['status'],
'hotel': hotel
}
if existing_reservation:
await sync_to_async(Reservation.objects.update_or_create)(
reservation_id=reservation_id, defaults=defaults
)
self.logger.debug(f"Резервация {reservation_id} обновлена. ")
else:
await sync_to_async(Reservation.objects.create)(
reservation_id=reservation_id, **defaults
)
self.logger.debug(f"Создана новая резервация {reservation_id}")
except Exception as e:
self.logger.error(f"Ошибка при обработке записи {index}: {e}")
raise ValueError(f"Дата '{date_str}' не соответствует ожидаемым форматам: {formats}")
def validate_plugin(self):
required_methods = ["fetch_data", "get_default_parser_settings", "_fetch_data"]
for m in required_methods:
if not hasattr(self, m):
raise ValueError(f"Плагин {type(self).__name__} не реализует метод {m}.")
self.logger.debug(f"Плагин {self.__class__.__name__} прошел валидацию.")
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

View File

@@ -1,6 +0,0 @@
def test_function():
"""тестовая функция для проверки планировщика
"""
print("Hello, World!")
return "Hello, World!"

View File

@@ -1,3 +1,9 @@
from django.test import TestCase
# Create your tests here.
def test_function():
"""тестовая функция для проверки планировщика
"""
print("Hello, World!")
return "Hello, World!"

View File

@@ -0,0 +1,18 @@
.ml-4 {
margin-left: 1rem !important;
}
.ml-6 {
margin-left: 1.5rem !important;
}
.nav-sidebar ul.nav-treeview .nav-link {
padding-left: 2rem;
}
.nav-sidebar .nav-link {
display: flex;
}
.nav-sidebar .nav-link .nav-icon {
margin-top: .2rem;
margin-right: .3rem;
}

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

18
tmp/tests.py Normal file
View File

@@ -0,0 +1,18 @@
import os
import django
import unittest
from antifroud.check_fraud import run_reservation_check
# Установка переменной окружения для Django settings
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "your_project.settings")
django.setup()
class FraudCheckTests(unittest.TestCase):
def test_run_reservation_check(self):
"""Тест проверки бронирования на мошенничество"""
result = run_reservation_check()
self.assertIsNotNone(result) # Проверяем, что результат не None
self.assertIsInstance(result, dict) # Проверяем, что возвращается словарь
if __name__ == "__main__":
unittest.main()

File diff suppressed because it is too large Load Diff