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)
def log_error(self, message):
"""Логирование ошибок."""
logger.error(message)
def fetch_user_logs(self):
"""
Извлекает записи из UserActivityLog за последние 12 часов.
"""
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
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
def find_violations(self, user_logs, hotels):
"""
Сопоставляет логи активности с бронированиями и фиксирует нарушения.
"""
for log in user_logs:
if not log.url_parameters: if not log.url_parameters:
logger.warning(f"Log {i} skipped due to missing URL parameters.") self.log_warning(f"Пропущена запись ID {log.id}: отсутствуют URL-параметры.")
continue # Пропускаем записи без параметров URL
# Парсим параметры URL
params = parse_qs(log.url_parameters)
external_id = params.get("utm_content", [None])[0] # ID отеля
room_number = params.get("utm_term", [None])[0] # Номер комнаты
logger.debug(f"Log {i} parsed parameters: external_id={external_id}, room_number={room_number}")
if not external_id or not room_number:
logger.warning(f"Log {i} skipped due to missing external_id or room_number.")
continue # Пропускаем, если данные не извлечены
try:
# Находим отель по external_id
hotel = Hotel.objects.get(external_id=external_id)
logger.debug(f"Log {i}: Found hotel {hotel.name} with external_id {external_id}.")
except Hotel.DoesNotExist:
logger.error(f"Log {i} skipped: No hotel found with external_id {external_id}.")
continue continue
# Ищем бронирование в Reservation # Парсинг параметров URL
params = parse_qs(log.url_parameters)
hotel_id = params.get("utm_content", [None])[0]
room_number = params.get("utm_term", [None])[0]
print(f"Processing log ID {log.id} with hotel ID {hotel_id} and room number {room_number}")
if not hotel_id or not room_number:
self.log_warning(f"Пропущена запись ID {log.id}: некорректные параметры URL.")
continue
if hotel_id not in hotels:
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( matching_reservations = Reservation.objects.filter(
hotel=hotel, hotel=hotel,
room_number=room_number, room_number=room_number,
check_in__lte=log.created, check_in__lte=log_time,
check_out__gte=log.created check_out__gte=log_time
) )
logger.debug(f"Log {i}: Found {len(matching_reservations)} matching reservations.")
print(f"Found {matching_reservations.count()} matching reservations")
if not matching_reservations.exists(): if not matching_reservations.exists():
# Если бронирование не найдено — записываем нарушение
violation_details = ( violation_details = (
f"Log {i}: No reservation found for room {room_number} in hotel {external_id} at {log.created}." f"Не найдено бронирование для номера {room_number} в отеле '{hotel.name}' на {log_time}."
) )
violations.append(ViolationLog( # Добавляем нарушение, если его ещё нет в базе
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, hotel=hotel,
room_number=room_number, room_number=room_number,
violation_type="missed", violation_type="missed",
violation_details=violation_details violation_details=violation_details
)) ))
logger.warning(f"Log {i}: Violation recorded - {violation_details}") self.log_warning(f"Зафиксировано нарушение: {violation_details}")
# Сохраняем все нарушения в базу def save_violations(self):
if violations: """
ViolationLog.objects.bulk_create(violations) Сохраняет найденные нарушения в базу данных.
logger.info(f"Created {len(violations)} records in violation log.") """
if self.violations:
ViolationLog.objects.bulk_create(self.violations)
self.log_info(f"Создано {len(self.violations)} записей в ViolationLog.")
else: else:
logger.info("No violations found during this check.") self.log_info("Нарушений не обнаружено.")
logger.info("Reservation check completed.") 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,20 +19,28 @@ 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="Описание")
@@ -49,12 +59,10 @@ class Hotel(models.Model):
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
] ]