remote changes

This commit is contained in:
2025-02-26 22:29:39 +09:00
parent 51569fb5c6
commit d771df32d5
11 changed files with 430 additions and 1345 deletions

View File

@@ -1,26 +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 django.db import connection
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.checkin_diff_hours = 3
self.checkin_diff_hours = 3 # Разрешенное отклонение от времени заселения
def log_info(self, message):
logger.info(message)
@@ -32,80 +155,105 @@ class ReservationChecker:
logger.error(message)
def run_check(self):
self.log_info(f"Запуск проверки.")
"""Запуск проверки фродовых событий."""
self.log_info("🔍 Запуск проверки фродовых данных.")
try:
hotels_map = {}
hotels = Hotel.objects.all()
for hotel in hotels:
hotels_map[hotel.hotel_id] = hotel
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')
missing = list(reservations)
# Преобразуем бронирования в словарь для быстрого поиска
reservations_map = {
(res.hotel.hotel_id, res.room_number): res for res in reservations
}
violations = []
check_in_diff = timedelta(hours=self.checkin_diff_hours)
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("'", '"'))
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'
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})")
if v_type:
violations.append(RoomDiscrepancy(
hotel=hotels_map[hotel_id],
hotel=hotels_map.get(hotel_id),
room_number=room,
discrepancy_type=v_type,
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 Exception as e:
logger.error(e)
user_log.fraud_checked = True # Отмечаем логи как проверенные
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,
))
except json.JSONDecodeError:
self.log_error(f"❌ Ошибка декодирования JSON в URL-параметрах: {user_log.url_parameters}")
except Exception as e:
self.log_error(f"❌ Ошибка при обработке логов: {e}")
# Добавляем пропущенные бронирования (неявки)
for reserv in reservations:
reserv.fraud_checked = True
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)
UserActivityLog.objects.bulk_update(user_logs, ['fraud_checked'], 1000)
Reservation.objects.bulk_update(reservations, ['fraud_checked'], 1000)
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("Проверка завершена.")
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

@@ -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

@@ -111,8 +111,8 @@ 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="Скидка")

View File

@@ -1,9 +1,146 @@
# ecvi_pms.py
# # 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 requests
import json
import os
import json
from datetime import datetime, timedelta
import requests
from asgiref.sync import sync_to_async
from hotels.models import Hotel, Reservation
from .base_plugin import BasePMSPlugin
@@ -20,7 +157,7 @@ class EcviPMSPlugin(BasePMSPlugin):
if not self.hotel.pms:
raise ValueError(f"Отель {self.hotel.name} не имеет связанной PMS конфигурации.")
self.api_url = self.hotel.pms.url.rstrip("/")
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
@@ -35,7 +172,14 @@ 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):
"""
Возвращает настройки парсера по умолчанию.
"""
return {
"field_mapping": {
"check_in": "checkin",
@@ -48,6 +192,9 @@ class EcviPMSPlugin(BasePMSPlugin):
}
async def _fetch_data(self):
"""
Получает данные из PMS API и сохраняет их в базу.
"""
headers = {"Content-Type": "application/json"}
data = {"token": self.token}
@@ -59,22 +206,16 @@ class EcviPMSPlugin(BasePMSPlugin):
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)
# Сохраняем весь ответ 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)
# Сохранение данных во временный 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)
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 {
@@ -84,33 +225,81 @@ class EcviPMSPlugin(BasePMSPlugin):
}
async def _process_data(self, data):
"""
Обрабатывает данные и сохраняет их в базу.
"""
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)
valid_reservations = []
print(data)
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"]:
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,
}
)
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}")
self.logger.error(f"Ошибка сохранения бронирования: {e}")
errors.append(str(e))
return {
@@ -121,6 +310,9 @@ class EcviPMSPlugin(BasePMSPlugin):
@staticmethod
def _parse_date(date_str, formats):
"""
Парсит дату, пытаясь использовать несколько форматов.
"""
for fmt in formats:
try:
return datetime.strptime(date_str, fmt)
@@ -129,6 +321,9 @@ class EcviPMSPlugin(BasePMSPlugin):
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):

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

File diff suppressed because it is too large Load Diff