antifroud_check

This commit is contained in:
2024-12-17 21:39:33 +09:00
parent 66750015e2
commit 0bf2bb8dff
17 changed files with 1298 additions and 524 deletions

View File

@@ -5,7 +5,8 @@ from django.shortcuts import redirect, get_object_or_404
from django.contrib import messages from django.contrib import messages
from django.db import transaction from django.db import transaction
from antifroud.models import UserActivityLog, ExternalDBSettings, RoomDiscrepancy, ImportedHotel, SyncLog, ViolationLog from antifroud.models import UserActivityLog, ExternalDBSettings, RoomDiscrepancy, ImportedHotel, SyncLog, ViolationLog
from hotels.models import Hotel
from hotels.models import Hotel, Room
import pymysql import pymysql
import logging import logging
from django.urls import reverse from django.urls import reverse
@@ -114,14 +115,32 @@ class UserActivityLogAdmin(admin.ModelAdmin):
search_fields = ("page_title", "url_parameters") search_fields = ("page_title", "url_parameters")
list_filter = ("page_title", "created") list_filter = ("page_title", "created")
readonly_fields = ("created", "timestamp") readonly_fields = ("created", "timestamp")
def get_hotel_name(self):
"""
Возвращает название отеля на основе связанного page_id.
"""
if self.page_id:
try:
room = Room.objects.get(id=self.page_id)
return room.hotel.name
except Room.DoesNotExist:
return "Отель не найден"
return "Нет данных"
def get_room_number(self):
"""
Возвращает номер комнаты на основе связанного page_id.
"""
if self.page_id:
try:
room = Room.objects.get(id=self.page_id)
return room.number
except Room.DoesNotExist:
return "Комната не найдена"
return "Нет данных"
@admin.register(RoomDiscrepancy) get_hotel_name.short_description = "Отель"
class RoomDiscrepancyAdmin(admin.ModelAdmin): get_room_number.short_description = "Комната"
list_display = ("hotel", "room_number", "booking_id", "check_in_date_expected", "check_in_date_actual", "discrepancy_type", "created_at")
search_fields = ("hotel__name", "room_number", "booking_id")
list_filter = ("discrepancy_type", "created_at")
readonly_fields = ("created_at",)
from .views import import_selected_hotels from .views import import_selected_hotels

View File

@@ -1,79 +1,160 @@
import logging import logging
from datetime import datetime, timedelta from datetime import timedelta
from urllib.parse import parse_qs from urllib.parse import parse_qs
from django.utils import timezone
from django.db.models import Q from django.db.models import Q
from hotels.models import Reservation, Hotel from hotels.models import Reservation, Hotel
from .models import UserActivityLog, ViolationLog from .models import UserActivityLog, ViolationLog
# Настройка логирования # Настройка логирования
logging.basicConfig(level=logging.INFO) # Устанавливаем уровень логирования logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) # Создаем логгер для текущего модуля logger = logging.getLogger(__name__)
def check_reservations_and_generate_report(): class ReservationChecker:
now = datetime.now() """
start_time = (now - timedelta(hours=12)) Класс для проверки несоответствий между бронированиями и логами заселения.
end_time = now """
logger.info(f"Starting reservation check from {start_time} to {end_time}")
# Получаем логи активности за период def __init__(self):
user_logs = UserActivityLog.objects.filter(created__range=(start_time, end_time)) """
logger.info(f"Found {len(user_logs)} logs for analysis.") Инициализация времени проверки и списка нарушений.
"""
self.start_time = timezone.now() - timedelta(days=30)
self.end_time = timezone.now()
self.violations = []
violations = [] # Список для записи нарушений def log_info(self, message):
"""Логирование информационных сообщений."""
logger.info(message)
for i, log in enumerate(user_logs, start=1): def log_warning(self, message):
logger.debug(f"Processing log {i}: {log}") """Логирование предупреждений."""
logger.warning(message)
if not log.url_parameters: def log_error(self, message):
logger.warning(f"Log {i} skipped due to missing URL parameters.") """Логирование ошибок."""
continue # Пропускаем записи без параметров URL logger.error(message)
# Парсим параметры URL def fetch_user_logs(self):
params = parse_qs(log.url_parameters) """
external_id = params.get("utm_content", [None])[0] # ID отеля Извлекает записи из UserActivityLog за последние 12 часов.
room_number = params.get("utm_term", [None])[0] # Номер комнаты """
print(f"Fetching user logs from {self.start_time} to {self.end_time}")
user_logs = UserActivityLog.objects.filter(created__range=(self.start_time, self.end_time))
print(f"Found {user_logs.count()} logs for analysis.")
return user_logs
logger.debug(f"Log {i} parsed parameters: external_id={external_id}, room_number={room_number}") def fetch_hotels(self, hotel_ids):
"""
Извлекает отели по hotel_id из логов.
"""
hotels = {hotel.hotel_id: hotel for hotel in Hotel.objects.filter(hotel_id__in=hotel_ids)}
self.log_info(f"Найдено {len(hotels)} отелей для сверки.")
return hotels
if not external_id or not room_number: def find_violations(self, user_logs, hotels):
logger.warning(f"Log {i} skipped due to missing external_id or room_number.") """
continue # Пропускаем, если данные не извлечены Сопоставляет логи активности с бронированиями и фиксирует нарушения.
"""
for log in user_logs:
if not log.url_parameters:
self.log_warning(f"Пропущена запись ID {log.id}: отсутствуют URL-параметры.")
continue
try: # Парсинг параметров URL
# Находим отель по external_id params = parse_qs(log.url_parameters)
hotel = Hotel.objects.get(external_id=external_id) hotel_id = params.get("utm_content", [None])[0]
logger.debug(f"Log {i}: Found hotel {hotel.name} with external_id {external_id}.") room_number = params.get("utm_term", [None])[0]
except Hotel.DoesNotExist:
logger.error(f"Log {i} skipped: No hotel found with external_id {external_id}.")
continue
# Ищем бронирование в Reservation print(f"Processing log ID {log.id} with hotel ID {hotel_id} and room number {room_number}")
matching_reservations = Reservation.objects.filter(
hotel=hotel,
room_number=room_number,
check_in__lte=log.created,
check_out__gte=log.created
)
logger.debug(f"Log {i}: Found {len(matching_reservations)} matching reservations.")
if not matching_reservations.exists(): if not hotel_id or not room_number:
# Если бронирование не найдено — записываем нарушение self.log_warning(f"Пропущена запись ID {log.id}: некорректные параметры URL.")
violation_details = ( continue
f"Log {i}: No reservation found for room {room_number} in hotel {external_id} at {log.created}."
) if hotel_id not in hotels:
violations.append(ViolationLog( self.log_warning(f"Пропущена запись ID {log.id}: отель с ID {hotel_id} не найден.")
continue
hotel = hotels[hotel_id]
log_time = timezone.localtime(log.created)
# Проверка существования бронирования
matching_reservations = Reservation.objects.filter(
hotel=hotel, hotel=hotel,
room_number=room_number, room_number=room_number,
violation_type="missed", check_in__lte=log_time,
violation_details=violation_details check_out__gte=log_time
)) )
logger.warning(f"Log {i}: Violation recorded - {violation_details}")
# Сохраняем все нарушения в базу print(f"Found {matching_reservations.count()} matching reservations")
if violations:
ViolationLog.objects.bulk_create(violations)
logger.info(f"Created {len(violations)} records in violation log.")
else:
logger.info("No violations found during this check.")
logger.info("Reservation check completed.") if not matching_reservations.exists():
violation_details = (
f"Не найдено бронирование для номера {room_number} в отеле '{hotel.name}' на {log_time}."
)
# Добавляем нарушение, если его ещё нет в базе
if not ViolationLog.objects.filter(
hotel=hotel,
room_number=room_number,
violation_type="missed",
violation_details=violation_details
).exists():
self.violations.append(ViolationLog(
hotel=hotel,
room_number=room_number,
violation_type="missed",
violation_details=violation_details
))
self.log_warning(f"Зафиксировано нарушение: {violation_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}.")
try:
# Получаем логи пользователей
user_logs = self.fetch_user_logs()
# Извлекаем hotel_id из логов
hotel_ids = set()
for log in user_logs:
if log.url_parameters:
params = parse_qs(log.url_parameters)
hotel_id = params.get("utm_content", [None])[0]
if hotel_id:
hotel_ids.add(hotel_id)
# Предзагружаем отели
hotels = self.fetch_hotels(hotel_ids)
# Сравниваем логи с бронированиями
self.find_violations(user_logs, hotels)
# Сохраняем результаты
self.save_violations()
except Exception as e:
self.log_error(f"Произошла ошибка при выполнении проверки: {e}")
self.log_info("Проверка бронирований завершена.")
# Функция для запуска проверки из планировщика
def run_reservation_check():
"""
Функция для запуска проверки бронирований.
"""
checker = ReservationChecker()
checker.run_check()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.4 on 2024-12-17 03:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('antifroud', '0012_violationlog'),
]
operations = [
migrations.AlterField(
model_name='useractivitylog',
name='timestamp',
field=models.BigIntegerField(blank=True, null=True, verbose_name='Метка времени'),
),
]

View File

@@ -0,0 +1,68 @@
# Generated by Django 5.1.4 on 2024-12-17 03:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('antifroud', '0013_alter_useractivitylog_timestamp'),
]
operations = [
migrations.AlterField(
model_name='useractivitylog',
name='UAString',
field=models.TextField(blank=True, null=True, verbose_name='User-Agent строка'),
),
migrations.AlterField(
model_name='useractivitylog',
name='agent',
field=models.TextField(blank=True, null=True, verbose_name='Агент пользователя'),
),
migrations.AlterField(
model_name='useractivitylog',
name='created',
field=models.DateTimeField(blank=True, null=True, verbose_name='Дата создания'),
),
migrations.AlterField(
model_name='useractivitylog',
name='date_time',
field=models.DateTimeField(blank=True, null=True, verbose_name='Дата и время'),
),
migrations.AlterField(
model_name='useractivitylog',
name='hits',
field=models.IntegerField(blank=True, null=True, verbose_name='Количество обращений'),
),
migrations.AlterField(
model_name='useractivitylog',
name='honeypot',
field=models.BooleanField(blank=True, null=True, verbose_name='Метка honeypot'),
),
migrations.AlterField(
model_name='useractivitylog',
name='ip',
field=models.GenericIPAddressField(blank=True, null=True, verbose_name='IP-адрес'),
),
migrations.AlterField(
model_name='useractivitylog',
name='last_counter',
field=models.IntegerField(blank=True, null=True, verbose_name='Последний счетчик'),
),
migrations.AlterField(
model_name='useractivitylog',
name='reply',
field=models.BooleanField(blank=True, null=True, verbose_name='Ответ пользователя'),
),
migrations.AlterField(
model_name='useractivitylog',
name='type',
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Тип'),
),
migrations.AlterField(
model_name='useractivitylog',
name='user_id',
field=models.BigIntegerField(blank=True, null=True, verbose_name='ID пользователя'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.4 on 2024-12-17 04:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('antifroud', '0014_alter_useractivitylog_uastring_and_more'),
]
operations = [
migrations.AlterField(
model_name='useractivitylog',
name='page_id',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='ID страницы'),
),
]

View File

@@ -0,0 +1,39 @@
# Generated by Django 5.1.4 on 2024-12-17 05:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('antifroud', '0015_alter_useractivitylog_page_id'),
]
operations = [
migrations.AlterField(
model_name='useractivitylog',
name='created',
field=models.DateTimeField(blank=True, db_index=True, null=True, verbose_name='Дата создания'),
),
migrations.AlterField(
model_name='useractivitylog',
name='external_id',
field=models.CharField(db_index=True, default=1, max_length=255, unique=True, verbose_name='Внешний ID'),
preserve_default=False,
),
migrations.AlterField(
model_name='useractivitylog',
name='ip',
field=models.GenericIPAddressField(blank=True, db_index=True, null=True, verbose_name='IP-адрес'),
),
migrations.AlterField(
model_name='useractivitylog',
name='page_id',
field=models.BigIntegerField(blank=True, db_index=True, null=True, verbose_name='ID страницы'),
),
migrations.AlterField(
model_name='useractivitylog',
name='user_id',
field=models.BigIntegerField(blank=True, db_index=True, null=True, verbose_name='ID пользователя'),
),
]

View File

@@ -4,30 +4,40 @@ from hotels.models import Reservation
class UserActivityLog(models.Model): class UserActivityLog(models.Model):
external_id = models.CharField(max_length=255, null=True, blank=True) external_id = models.CharField(max_length=255, unique=True, verbose_name="Внешний ID", db_index=True)
user_id = models.BigIntegerField(verbose_name="ID пользователя") user_id = models.BigIntegerField(verbose_name="ID пользователя", blank=True, null=True, db_index=True)
ip = models.GenericIPAddressField(verbose_name="IP-адрес") ip = models.GenericIPAddressField(verbose_name="IP-адрес", blank=True, null=True, db_index=True)
created = models.DateTimeField(verbose_name="Дата создания") created = models.DateTimeField(verbose_name="Дата создания", blank=True, null=True, db_index=True)
timestamp = models.BigIntegerField(verbose_name="Метка времени") timestamp = models.BigIntegerField(verbose_name="Метка времени", blank=True, null=True)
date_time = models.DateTimeField(verbose_name="Дата и время") date_time = models.DateTimeField(verbose_name="Дата и время", blank=True, null=True)
referred = models.TextField(blank=True, null=True, verbose_name="Реферальная ссылка") referred = models.TextField(blank=True, null=True, verbose_name="Реферальная ссылка")
agent = models.TextField(verbose_name="Агент пользователя") agent = models.TextField(verbose_name="Агент пользователя", blank=True, null=True)
platform = models.CharField(max_length=255, blank=True, null=True, verbose_name="Платформа") platform = models.CharField(max_length=255, blank=True, null=True, verbose_name="Платформа")
version = models.CharField(max_length=255, blank=True, null=True, verbose_name="Версия") version = models.CharField(max_length=255, blank=True, null=True, verbose_name="Версия")
model = models.CharField(max_length=255, blank=True, null=True, verbose_name="Модель устройства") model = models.CharField(max_length=255, blank=True, null=True, verbose_name="Модель устройства")
device = models.CharField(max_length=255, blank=True, null=True, verbose_name="Тип устройства") device = models.CharField(max_length=255, blank=True, null=True, verbose_name="Тип устройства")
UAString = models.TextField(verbose_name="User-Agent строка") UAString = models.TextField(verbose_name="User-Agent строка", blank=True, null=True)
location = models.CharField(max_length=255, blank=True, null=True, verbose_name="Местоположение") location = models.CharField(max_length=255, blank=True, null=True, verbose_name="Местоположение")
page_id = models.BigIntegerField(blank=True, null=True, verbose_name="ID страницы") page_id = models.BigIntegerField(blank=True, null=True, verbose_name="ID страницы", db_index=True)
url_parameters = models.TextField(blank=True, null=True, verbose_name="Параметры URL") url_parameters = models.TextField(blank=True, null=True, verbose_name="Параметры URL")
page_title = models.TextField(blank=True, null=True, verbose_name="Заголовок страницы") page_title = models.TextField(blank=True, null=True, verbose_name="Заголовок страницы")
type = models.CharField(max_length=50, verbose_name="Тип") type = models.CharField(max_length=50, verbose_name="Тип", blank=True, null=True)
last_counter = models.IntegerField(verbose_name="Последний счетчик") last_counter = models.IntegerField(verbose_name="Последний счетчик", blank=True, null=True)
hits = models.IntegerField(verbose_name="Количество обращений") hits = models.IntegerField(verbose_name="Количество обращений", blank=True, null=True)
honeypot = models.BooleanField(verbose_name="Метка honeypot") honeypot = models.BooleanField(verbose_name="Метка honeypot", blank=True, null=True)
reply = models.BooleanField(verbose_name="Ответ пользователя") 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 страницы")
class Meta:
indexes = [
models.Index(fields=["external_id"], name="idx_external_id"),
models.Index(fields=["user_id"], name="idx_user_id"),
models.Index(fields=["ip"], name="idx_ip"),
models.Index(fields=["created"], name="idx_created"),
models.Index(fields=["page_id"], name="idx_page_id"),
]
verbose_name = "Лог активности пользователя"
verbose_name_plural = "Логи активности пользователей"
def __str__(self): def __str__(self):
return f"UserActivityLog {self.id}: {self.page_title}" return f"UserActivityLog {self.id}: {self.page_title}"

View File

@@ -5,7 +5,7 @@ from .models import (
UserHotel, UserHotel,
APIConfiguration, APIConfiguration,
Reservation, Reservation,
FraudLog Room
) )
from django.urls import path from django.urls import path
from django.shortcuts import redirect from django.shortcuts import redirect
@@ -27,8 +27,9 @@ class HotelForm(forms.ModelForm):
@admin.register(Hotel) @admin.register(Hotel)
class HotelAdmin(admin.ModelAdmin): class HotelAdmin(admin.ModelAdmin):
list_display = ['name', 'hotel_id', 'pms', 'timezone', 'description'] list_display = ['name', 'hotel_id','room_count', 'pms', 'timezone', 'description']
list_filter = ['name', 'pms', 'timezone'] list_filter = ['name', 'pms', 'timezone']
list_sorting = ['name', 'pms', 'room_count', 'timezone']
def sync_button(self, obj): def sync_button(self, obj):
return format_html( return format_html(
'<a class="button" href="{}">Синхронизировать</a>', '<a class="button" href="{}">Синхронизировать</a>',
@@ -41,8 +42,36 @@ class HotelAdmin(admin.ModelAdmin):
path('sync/<int:hotel_id>/', self.sync_hotel_data), path('sync/<int:hotel_id>/', self.sync_hotel_data),
] ]
return custom_urls + urls return custom_urls + urls
def room_count(self, obj):
"""
Подсчитывает количество комнат, связанных с отелем.
"""
return Room.objects.filter(hotel=obj).count()
room_count.short_description = "Количество комнат"
def sync_button(self, obj):
"""
Кнопка синхронизации данных.
"""
return format_html(
'<a class="button" href="{}">Синхронизировать</a>',
f"/admin/hotels/sync/{obj.id}/"
)
def get_urls(self):
"""
Добавление кастомного URL для синхронизации.
"""
urls = super().get_urls()
custom_urls = [
path('sync/<int:hotel_id>/', self.sync_hotel_data),
]
return custom_urls + urls
def sync_hotel_data(self, request, hotel_id): def sync_hotel_data(self, request, hotel_id):
"""
Метод синхронизации данных отеля.
"""
try: try:
hotel = Hotel.objects.get(id=hotel_id) hotel = Hotel.objects.get(id=hotel_id)
client = APIClient(hotel.pms) client = APIClient(hotel.pms)
@@ -66,4 +95,9 @@ class ReservationAdmin(admin.ModelAdmin):
list_filter = ('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')
ordering = ('-check_in',) ordering = ('-check_in',)
@admin.register(Room)
class RoomAdmin(admin.ModelAdmin):
list_display = ('hotel', 'number', 'external_id', 'description', 'created_at', 'updated_at')
search_fields = ('hotel', 'number', 'external_id', 'description')
list_filter = ('hotel', 'number', 'external_id','description', 'created_at', 'updated_at')
ordering = ('-hotel', '-number')

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.4 on 2024-12-17 11:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hotels', '0011_room_alter_fraudlog_check_in_date_and_more'),
]
operations = [
migrations.AlterField(
model_name='room',
name='number',
field=models.CharField(max_length=50, unique=True, verbose_name='Номер комнаты'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.4 on 2024-12-17 11:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hotels', '0012_alter_room_number'),
]
operations = [
migrations.AlterField(
model_name='room',
name='number',
field=models.CharField(max_length=50, verbose_name='Номер комнаты'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.4 on 2024-12-17 11:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hotels', '0013_alter_room_number'),
]
operations = [
migrations.AlterField(
model_name='room',
name='number',
field=models.CharField(max_length=50, unique=True, verbose_name='Номер комнаты'),
),
]

View File

@@ -1,5 +1,7 @@
from django.db import models from django.db import models
from django.core.validators import MinValueValidator, MaxValueValidator from django.core.validators import MinValueValidator, MaxValueValidator, RegexValidator
from django.core.exceptions import ValidationError
import pytz
class APIConfiguration(models.Model): class APIConfiguration(models.Model):
@@ -17,25 +19,33 @@ class APIConfiguration(models.Model):
verbose_name = "Конфигурация API" verbose_name = "Конфигурация API"
verbose_name_plural = "Конфигурации API" verbose_name_plural = "Конфигурации API"
import pytz
class Hotel(models.Model): class Hotel(models.Model):
name = models.CharField(max_length=255, verbose_name="Название отеля") name = models.CharField(max_length=255, verbose_name="Название отеля")
hotel_id = models.CharField(max_length=255, unique=True, null=True, blank=True, verbose_name="ID отеля") hotel_id = models.CharField(max_length=255, unique=True, null=True, blank=True, verbose_name="ID отеля")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Создан") created_at = models.DateTimeField(auto_now_add=True, verbose_name="Создан")
phone= models.CharField(max_length=50, null=True, blank=True, verbose_name="Телефон") phone = models.CharField(
max_length=50,
null=True,
blank=True,
verbose_name="Телефон",
validators=[
RegexValidator(regex=r'^\+?1?\d{9,15}$', message="Введите корректный номер телефона (до 15 цифр).")
],
)
email = models.EmailField(null=True, blank=True, verbose_name="Email") email = models.EmailField(null=True, blank=True, verbose_name="Email")
address = models.CharField(max_length=255, null=True, blank=True, verbose_name="Адрес") address = models.CharField(max_length=255, null=True, blank=True, verbose_name="Адрес")
city = models.CharField(max_length=255, null=True, blank=True, verbose_name="Город") city = models.CharField(max_length=255, null=True, blank=True, verbose_name="Город")
timezone = models.CharField( timezone = models.CharField(
max_length=63, max_length=63,
choices=[(tz, tz) for tz in pytz.all_timezones], # Список всех часовых поясов choices=[(tz, tz) for tz in pytz.all_timezones],
default='UTC', # Значение по умолчанию default='UTC',
verbose_name="Часовой пояс",
) )
description = models.TextField(null=True, blank=True, verbose_name="Описание") description = models.TextField(null=True, blank=True, verbose_name="Описание")
pms = models.ForeignKey( pms = models.ForeignKey(
'pms_integration.PMSConfiguration', 'pms_integration.PMSConfiguration',
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
blank=True, blank=True,
@@ -48,13 +58,11 @@ class Hotel(models.Model):
class Meta: class Meta:
verbose_name = "Отель" verbose_name = "Отель"
verbose_name_plural = "Отели" verbose_name_plural = "Отели"
class Room(models.Model): class Room(models.Model):
"""
Модель номера отеля.
"""
hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE, related_name="rooms", verbose_name="Отель") hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE, related_name="rooms", verbose_name="Отель")
number = models.CharField(max_length=50, verbose_name="Номер комнаты") number = models.CharField(max_length=50, unique=True, verbose_name="Номер комнаты")
external_id = models.CharField(max_length=255, unique=True, verbose_name="Внешний ID комнаты") external_id = models.CharField(max_length=255, unique=True, verbose_name="Внешний ID комнаты")
description = models.TextField(blank=True, null=True, verbose_name="Описание") description = models.TextField(blank=True, null=True, verbose_name="Описание")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания") created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
@@ -66,17 +74,23 @@ class Room(models.Model):
class Meta: class Meta:
verbose_name = "Номер" verbose_name = "Номер"
verbose_name_plural = "Номера" verbose_name_plural = "Номера"
unique_together = ("hotel", "number") # Уникальность пары (отель, номер) constraints = [
models.UniqueConstraint(fields=["hotel", "number"], name="unique_hotel_room")
]
indexes = [
models.Index(fields=["hotel", "number"]),
]
class UserHotel(models.Model): class UserHotel(models.Model):
user = models.ForeignKey( user = models.ForeignKey(
'users.User', # Используем строковую ссылку 'users.User',
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name="user_hotels", related_name="user_hotels",
verbose_name="Пользователь" verbose_name="Пользователь"
) )
hotel = models.ForeignKey( hotel = models.ForeignKey(
'hotels.Hotel', Hotel,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name="hotel_users", related_name="hotel_users",
verbose_name="Отель" verbose_name="Отель"
@@ -90,6 +104,76 @@ class UserHotel(models.Model):
verbose_name_plural = "Пользователи отелей" verbose_name_plural = "Пользователи отелей"
class Reservation(models.Model):
id = models.BigAutoField(primary_key=True, auto_created=True, verbose_name="ID")
hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE, verbose_name="Отель")
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="Дата выезда")
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="Скидка")
def clean(self):
if self.check_out and self.check_in and self.check_out <= self.check_in:
raise ValidationError("Дата выезда должна быть позже даты заезда.")
def __str__(self):
return f"Бронирование {self.reservation_id} - {self.hotel.name}"
class Meta:
verbose_name = "Бронирование"
verbose_name_plural = "Бронирования"
indexes = [
models.Index(fields=["hotel", "check_in", "check_out"]),
]
class Guest(models.Model):
reservation = models.ForeignKey(Reservation, on_delete=models.CASCADE, related_name="guests", verbose_name="Бронирование")
name = models.CharField(max_length=255, verbose_name="Имя гостя")
birthdate = models.DateField(null=True, blank=True, verbose_name="Дата рождения")
phone = models.CharField(
max_length=50,
null=True,
blank=True,
verbose_name="Телефон",
validators=[
RegexValidator(regex=r'^\+?1?\d{9,15}$', message="Введите корректный номер телефона (до 15 цифр).")
],
)
email = models.EmailField(null=True, blank=True, verbose_name="Email")
def __str__(self):
return f"{self.name} ({self.birthdate})" if self.birthdate else self.name
class Meta:
verbose_name = "Гость"
verbose_name_plural = "Гости"
class FraudLog(models.Model):
hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE, related_name="frauds")
reservation_id = models.BigIntegerField(verbose_name="ID бронирования")
guest_name = models.CharField(max_length=255, null=True, blank=True)
check_in_date = models.DateField(verbose_name="Дата заезда")
detected_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата обнаружения")
message = models.TextField(verbose_name="Сообщение")
def __str__(self):
return f"FRAUD: {self.guest_name} ({self.check_in_date})"
class Meta:
verbose_name = "Журнал мошенничества"
verbose_name_plural = "Журналы мошенничества"
indexes = [
models.Index(fields=["reservation_id"]),
models.Index(fields=["detected_at"]),
]
class APIRequestLog(models.Model): class APIRequestLog(models.Model):
api = models.ForeignKey(APIConfiguration, on_delete=models.CASCADE, verbose_name="API") api = models.ForeignKey(APIConfiguration, on_delete=models.CASCADE, verbose_name="API")
request_time = models.DateTimeField(auto_now_add=True, verbose_name="Время запроса") request_time = models.DateTimeField(auto_now_add=True, verbose_name="Время запроса")
@@ -103,57 +187,6 @@ class APIRequestLog(models.Model):
verbose_name = "Журнал запросов API" verbose_name = "Журнал запросов API"
verbose_name_plural = "Журналы запросов API" verbose_name_plural = "Журналы запросов API"
indexes = [ indexes = [
models.Index(fields=['api']), models.Index(fields=["api"]),
models.Index(fields=['request_time']), models.Index(fields=["request_time"]),
] ]
class Reservation(models.Model):
id = models.BigAutoField(primary_key=True, auto_created=True, verbose_name="ID")
hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE, verbose_name="Отель")
reservation_id = models.BigIntegerField(unique=True, verbose_name="ID бронирования")
room_number = models.CharField(max_length=255, null=True, blank=True)
room_type = models.CharField(max_length=255, verbose_name="Тип комнаты")
check_in = models.DateTimeField(verbose_name="Дата заезда")
check_out = models.DateTimeField(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="Цена")
discount = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name="Скидка")
def __str__(self):
return f"Бронирование {self.reservation_id} - {self.hotel.name}"
class Meta:
verbose_name = "Бронирование"
verbose_name_plural = "Бронирования"
class Guest(models.Model):
reservation = models.ForeignKey(Reservation, on_delete=models.CASCADE, related_name="guests", verbose_name="Бронирование")
name = models.CharField(max_length=255, verbose_name="Имя гостя")
birthdate = models.DateField(null=True, blank=True, verbose_name="Дата рождения")
phone = models.CharField(max_length=50, null=True, blank=True, verbose_name="Телефон")
email = models.EmailField(null=True, blank=True, verbose_name="Email")
def __str__(self):
return self.name
class Meta:
verbose_name = "Гость"
verbose_name_plural = "Гости"
class FraudLog(models.Model):
hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE, related_name="frauds")
reservation_id = models.BigIntegerField(unique=True, verbose_name="ID бронирования")
guest_name = models.CharField(max_length=255, null=True, blank=True)
check_in_date = models.DateField()
detected_at = models.DateTimeField(auto_now_add=True)
message = models.TextField()
def __str__(self):
return f"FRAUD: {self.guest_name} ({self.check_in_date})"
class Meta:
verbose_name = "Журнал мошенничества"
verbose_name_plural = "Журналы мошенничества"

View File

@@ -0,0 +1,33 @@
# Generated by Django 5.1.4 on 2024-12-17 11:21
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('settings', '0006_remove_globalhotelsettings_timezone_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='emailsettings',
options={'verbose_name': 'E-mail', 'verbose_name_plural': 'E-mails'},
),
migrations.AlterModelOptions(
name='globalhotelsettings',
options={'verbose_name': 'Настройки отеля', 'verbose_name_plural': 'Настройки отеля'},
),
migrations.AlterModelOptions(
name='globalsystemsettings',
options={'verbose_name': 'Настройки системы', 'verbose_name_plural': 'Настройки системы'},
),
migrations.AlterModelOptions(
name='localdatabase',
options={'verbose_name': 'База данных', 'verbose_name_plural': 'Базы данных'},
),
migrations.AlterModelOptions(
name='telegramsettings',
options={'verbose_name': 'Telegram', 'verbose_name_plural': 'Telegram'},
),
]

View File

@@ -15,8 +15,8 @@ class LocalDatabase(models.Model):
return self.name return self.name
class Meta: class Meta:
verbose_name = "Локальная база данных" verbose_name = "База данных"
verbose_name_plural = "Локальные базы данных" verbose_name_plural = "Базы данных"
class TelegramSettings(models.Model): class TelegramSettings(models.Model):
bot_token = models.CharField(max_length=255, help_text="Токен вашего бота Telegram") bot_token = models.CharField(max_length=255, help_text="Токен вашего бота Telegram")
@@ -27,8 +27,8 @@ class TelegramSettings(models.Model):
return f"Telegram Bot ({self.username})" return f"Telegram Bot ({self.username})"
class Meta: class Meta:
verbose_name = "Настройки Telegram" verbose_name = "Telegram"
verbose_name_plural = "Настройки Telegram" verbose_name_plural = "Telegram"
class EmailSettings(models.Model): class EmailSettings(models.Model):
@@ -39,8 +39,8 @@ class EmailSettings(models.Model):
from_email = models.EmailField(help_text="Email для отправки сообщений") from_email = models.EmailField(help_text="Email для отправки сообщений")
class Meta: class Meta:
verbose_name = "Настройки почты" verbose_name = "E-mail"
verbose_name_plural = "Настройки почты" verbose_name_plural = "E-mails"
def __str__(self): def __str__(self):
return f"Email Settings for {self.from_email}" return f"Email Settings for {self.from_email}"
@@ -56,11 +56,11 @@ class GlobalHotelSettings(models.Model):
) )
def __str__(self): def __str__(self):
return "Глобальные настройки отеля" return "Настройки отеля"
class Meta: class Meta:
verbose_name = "Глобальные настройки отеля" verbose_name = "Настройки отеля"
verbose_name_plural = "Глобальные настройки отеля" verbose_name_plural = "Настройки отеля"
class GlobalSystemSettings(models.Model): class GlobalSystemSettings(models.Model):
system_name = models.CharField(max_length=255, help_text="Название системы") system_name = models.CharField(max_length=255, help_text="Название системы")
@@ -71,9 +71,9 @@ class GlobalSystemSettings(models.Model):
default='UTC', # Значение по умолчанию default='UTC', # Значение по умолчанию
) )
def __str__(self): def __str__(self):
return "Глобальные настройки системы" return "Настройки системы"
class Meta: class Meta:
verbose_name = "Глобальные настройки системы" verbose_name = "Настройки системы"
verbose_name_plural = "Глобальные настройки системы" verbose_name_plural = "Настройки системы"

View File

@@ -31,10 +31,10 @@ SECRET_KEY = 'django-insecure-l_8uu8#p*^zf)9zry80)6u+!+2g1a4tg!wx7@^!uw(+^axyh&h
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True
ALLOWED_HOSTS = ['0.0.0.0', '192.168.219.140', '127.0.0.1', '192.168.219.114', '5dec-182-226-158-253.ngrok-free.app', '*.ngrok-free.app'] ALLOWED_HOSTS = ['0.0.0.0', '192.168.219.140', '127.0.0.1', '192.168.219.114', 'a66a-182-226-158-253.ngrok-free.app', '*.ngrok-free.app']
CSRF_TRUSTED_ORIGINS = [ CSRF_TRUSTED_ORIGINS = [
'http://5dec-182-226-158-253.ngrok-free.app', 'http://a66a-182-226-158-253.ngrok-free.app',
'https://*.ngrok-free.app', # Это подойдет для любых URL, связанных с ngrok 'https://*.ngrok-free.app', # Это подойдет для любых URL, связанных с ngrok
] ]