Merge branch 'zorn-dev' into PMSManager_refactor

This commit is contained in:
2025-02-02 09:36:12 +09:00
8 changed files with 169 additions and 99 deletions

View File

@@ -1,9 +1,11 @@
import json
from datetime import timedelta from datetime import timedelta
from urllib.parse import parse_qs from urllib.parse import parse_qs
from django.utils import timezone from django.utils import timezone
from django.db.models import Q from django.db.models import Q
from django.db import connection
from hotels.models import Reservation, Hotel from hotels.models import Reservation, Hotel
from .models import UserActivityLog, ViolationLog from .models import UserActivityLog, ViolationLog, RoomDiscrepancy
from touchh.utils.log import CustomLogger from touchh.utils.log import CustomLogger
# Настройка логирования # Настройка логирования
logger = CustomLogger(__name__).get_logger() logger = CustomLogger(__name__).get_logger()
@@ -18,9 +20,7 @@ class ReservationChecker:
""" """
Инициализация времени проверки и списка нарушений. Инициализация времени проверки и списка нарушений.
""" """
self.start_time = timezone.now() - timedelta(days=30) self.checkin_diff_hours = 3
self.end_time = timezone.now()
self.violations = []
def log_info(self, message): def log_info(self, message):
logger.info(message) logger.info(message)
@@ -31,102 +31,71 @@ class ReservationChecker:
def log_error(self, message): def log_error(self, message):
logger.error(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): def run_check(self):
self.log_info(f"Запуск проверки с {self.start_time} по {self.end_time}.") self.log_info(f"Запуск проверки.")
try: try:
self.find_violations() hotels_map = {}
self.save_violations() 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: except Exception as e:
self.log_error(f"Ошибка при выполнении проверки: {e}") self.log_error(f"Ошибка при выполнении проверки: {e}")
self.log_info("Проверка завершена.") self.log_info("Проверка завершена.")

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) honeypot = models.BooleanField(verbose_name="Метка honeypot", blank=True, null=True)
reply = models.BooleanField(verbose_name="Ответ пользователя", blank=True, null=True) reply = models.BooleanField(verbose_name="Ответ пользователя", blank=True, null=True)
page_url = models.URLField(blank=True, null=True, verbose_name="URL страницы") page_url = models.URLField(blank=True, null=True, verbose_name="URL страницы")
fraud_checked = models.BooleanField(default=False, verbose_name="Проверено на несоответствия", db_index=True)
@property @property
def formatted_timestamp(self): def formatted_timestamp(self):

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="Статус") status = models.CharField(max_length=50, verbose_name="Статус")
price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, 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="Скидка") 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): def clean(self):
if self.check_out and self.check_in and self.check_out <= self.check_in: if self.check_out and self.check_in and self.check_out <= self.check_in:

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()