diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..c4c53c80 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +/var diff --git a/.drone.yml b/.drone.yml index 89e94830..351cd1bc 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1,47 +1,102 @@ kind: pipeline -type: docker -name: Django CI/CD +name: Touchh Hotel AntiFraud Pipeline +namespace: touchh steps: - # Шаг 1: Установка зависимостей, миграции и тесты - - name: test - image: python:3.10 - environment: - DATABASE_URL: mysql://root@R0sebud:0.0.0.0:3306/w1510415_wp832 + # Шаг 1: Клонирование репозитория + - name: clone_repo + image: alpine/git commands: - - python -m venv .venv - - source .venv/bin/activate - - pip install --upgrade pip - - pip install -r requirements.txt - - python manage.py migrate - - flake8 . # Линтер - - pytest # Запуск тестов + - if [ ! -d .git ]; then git clone $DRONE_REPO_URL .; fi + - git fetch --all + - git reset --hard $DRONE_COMMIT - # Шаг 2: Запуск и проверка Telegram-бота - - name: bot-check - image: python:3.10 + # Шаг 2: Обновление и запуск с помощью update.sh + - name: docker-build + image: plugins/docker + settings: + repo: trevor198507/touchh-py + dry_run: true + + # Шаг 3: Миграция базы данных + - name: run_migrations + image: docker:24 environment: - DATABASE_URL: mysql://root@R0sebud:0.0.0.0:3306/w1510415_wp832 + MYSQL_PASSWORD: touchh + volumes: + - name: docker_sock + path: /var/run/docker.sock + depends_on: + - web commands: - - python -m venv .venv - - source .venv/bin/activate - - pip install --upgrade pip + - apk add --no-cache bash + - until docker inspect -f '{{.State.Running}}' src-web-1 | grep true; do echo "Waiting for container to be running..."; sleep 5; done + - chmod +x ./bin/cli + - ./bin/cli migrate + + # Шаг 4: Тестирование + - name: run_tests + image: python:3.12-alpine + environment: + MYSQL_PASSWORD: touchh + depends_on: + - db + commands: + - apk add --no-cache mariadb-client mariadb-connector-c-dev gcc musl-dev pkgconfig - pip install -r requirements.txt - - python manage.py run_bot & # Запуск бота в фоне - - sleep 5 # Ждём, чтобы бот запустился - - python test_bot.py # Проверка работы бота + - python manage.py test -# services: -# # Шаг 3: Сервис базы данных MySQL -# - name: mysql -# image: mysql:8 -# environment: -# MYSQL_ROOT_PASSWORD: R0sebud -# MYSQL_USER: user -# MYSQL_PASSWORD: password -# MYSQL_DATABASE: w1510415_wp832 +services: + # Сервис базы данных + - name: db + image: mariadb:11.6 + environment: + MYSQL_RANDOM_ROOT_PASSWORD: 1 + MYSQL_DATABASE: touchh + MYSQL_USER: touchh + MYSQL_PASSWORD: touchh + volumes: + - name: mysql_data + temp: {} -trigger: - event: - - push - - pull_request + # Сервис Django (Web) + - name: web + image: touchh-py + environment: + MYSQL_PASSWORD: touchh + command: ['python3', 'manage.py', 'runserver', '0.0.0.0:8000'] + ports: + - port: 8000 + depends_on: + - db + volumes: + - name: app_volume + path: /app + + # Сервис Telegram Bot + - name: bot + image: touchh-py + environment: + MYSQL_PASSWORD: touchh + command: ['python3', 'manage.py', 'run_bot'] + depends_on: + - db + + # Сервис планировщика задач + - name: scheduler + image: touchh-py + environment: + MYSQL_PASSWORD: touchh + command: ['python3', 'manage.py', 'start_scheduler'] + depends_on: + - db + +volumes: + - name: docker_sock + host: + path: /var/run/docker.sock + - name: mysql_data + temp: {} + - name: app_volume + host: + path: ./ diff --git a/.gitignore b/.gitignore index d4590b6a..940f1fba 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ db.sqlite3 # Ignore files .fake docker-compose.override.yaml +tmp/* +tmp_data/* \ No newline at end of file diff --git a/README.md b/README.md index 3c9ac9ae..7df2d18f 100644 --- a/README.md +++ b/README.md @@ -161,4 +161,114 @@ python manage.py runserver #### Проверка интеграции с PMS -Для каждого отеля можно проверять статус интеграции с PMS (Bnovo, Travel Line, Realty) и получать ответ о доступности PMS. \ No newline at end of file +Для каждого отеля можно проверять статус интеграции с PMS (Bnovo, Travel Line, Realty) и получать ответ о доступности PMS. + + +#### Разработка плагинов для интеграции с PMS + +Для разработки плагина используются следующие инструменты: + +- Django +- Python +- Pydantic + +*код примера для плагина* + +```python +from datetime import datetime, timedelta +import requests +from asgiref.sync import sync_to_async +from hotels.models import Reservation +from .base_plugin import BasePMSPlugin +import logging + +class ExamplePMSPlugin(BasePMSPlugin): + """ + Плагин для интеграции с PMS Example. + """ + + def __init__(self, hotel): + super().__init__(hotel.pms) + self.hotel = hotel + self.api_url = self.hotel.pms.url + self.token = self.hotel.pms.token + self.logger = self._configure_logger() + + def _configure_logger(self): + logger = logging.getLogger(self.__class__.__name__) + handler_console = logging.StreamHandler() + handler_file = logging.FileHandler(f'{self.__class__.__name__.lower()}.log') + formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + handler_console.setFormatter(formatter) + handler_file.setFormatter(formatter) + logger.addHandler(handler_console) + logger.addHandler(handler_file) + logger.setLevel(logging.DEBUG) + return logger + + def get_default_parser_settings(self): + """ + Возвращает настройки для обработки данных. + """ + return { + "field_mapping": { + "check_in": "arrival_date", + "check_out": "departure_date", + "room_number": "room", + "status": "status", + }, + "date_format": "%Y-%m-%d %H:%M:%S" + } + + async def _fetch_data(self): + """ + Получает данные из API Example PMS. + """ + headers = { + "Authorization": f"Bearer {self.token}", + "Content-Type": "application/json", + } + now = datetime.now() + payload = { + "from_date": (now - timedelta(days=7)).strftime("%Y-%m-%d"), + "to_date": now.strftime("%Y-%m-%d"), + } + + try: + response = await sync_to_async(requests.post)( + self.api_url, json=payload, headers=headers + ) + response.raise_for_status() + data = response.json() + return await self._process_data(data) + except requests.exceptions.RequestException as e: + self.logger.error(f"Ошибка API: {e}") + return {"processed_items": 0, "errors": [str(e)]} + + async def _process_data(self, data): + """ + Обрабатывает и сохраняет данные в базу. + """ + processed_items = 0 + errors = [] + + for item in data.get("bookings", []): + try: + reservation, created = await sync_to_async(Reservation.objects.update_or_create)( + reservation_id=item['id'], + defaults={ + 'room_number': item['room'], + 'check_in': datetime.strptime(item['arrival_date'], "%Y-%m-%d"), + 'check_out': datetime.strptime(item['departure_date'], "%Y-%m-%d"), + 'status': item['status'], + 'hotel': self.hotel, + } + ) + processed_items += 1 + except Exception as e: + self.logger.error(f"Ошибка обработки записи {item['id']}: {e}") + errors.append(str(e)) + + return {"processed_items": processed_items, "errors": errors} + +``` diff --git a/antifroud/admin.py b/antifroud/admin.py index d91c877c..c59ee693 100644 --- a/antifroud/admin.py +++ b/antifroud/admin.py @@ -151,6 +151,7 @@ class UserActivityLogAdmin(admin.ModelAdmin): get_hotel_name.short_description = "Отель" get_room_number.short_description = "Комната" + # from .views import import_selected_hotels # # Регистрируем admin класс для ImportedHotel # @admin.register(ImportedHotel) @@ -247,4 +248,3 @@ class RoomDiscrepancyAdmin(admin.ModelAdmin): class Meta: model = RoomDiscrepancy fields = ['hotel', 'room_number', 'booking_id','created_at', 'check_in_date_expected','check_in_date_actual','discrepancy_type'] - \ No newline at end of file diff --git a/antifroud/check_fraud.py b/antifroud/check_fraud.py index 190358da..a1405184 100644 --- a/antifroud/check_fraud.py +++ b/antifroud/check_fraud.py @@ -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 hotels.models import Reservation, Hotel -from .models import UserActivityLog, ViolationLog +from .models import UserActivityLog, RoomDiscrepancy from touchh.utils.log import CustomLogger + # Настройка логирования logger = CustomLogger(__name__).get_logger() - class ReservationChecker: """ Класс для проверки несоответствий между бронированиями и логами заселения. """ def __init__(self): - """ - Инициализация времени проверки и списка нарушений. - """ - self.start_time = timezone.now() - timedelta(days=30) - self.end_time = timezone.now() - self.violations = [] + self.checkin_diff_hours = 3 # Разрешенное отклонение от времени заселения def log_info(self, message): logger.info(message) @@ -31,112 +154,106 @@ class ReservationChecker: def log_error(self, message): logger.error(message) - def fetch_user_logs(self): - try: - self.log_info("Начинается извлечение логов активности пользователей.") - user_logs = UserActivityLog.objects.filter(created__range=(self.start_time, self.end_time)) - self.log_info(f"Найдено {user_logs.count()} логов активности для анализа.") - return user_logs - except Exception as e: - self.log_error(f"Ошибка при извлечении логов активности: {e}") - return UserActivityLog.objects.none() - - def fetch_reservations(self): - try: - self.log_info("Начинается извлечение бронирований.") - reservations = Reservation.objects.filter( - Q(check_in__lte=self.end_time) & Q(check_out__gte=self.start_time) - ) - self.log_info(f"Найдено {reservations.count()} бронирований для анализа.") - return reservations - except Exception as e: - self.log_error(f"Ошибка при извлечении бронирований: {e}") - return Reservation.objects.none() - - def find_violations(self): - self.log_info("Начинается анализ несоответствий.") - user_logs = self.fetch_user_logs() - reservations = self.fetch_reservations() - - log_lookup = {} - for log in user_logs: - params = parse_qs(log.url_parameters or "") - hotel_id = params.get("utm_content", [None])[0] - room_number = params.get("utm_term", [None])[0] - if hotel_id and room_number: - key = (hotel_id, room_number) - log_lookup.setdefault(key, []).append(log) - - for reservation in reservations: - key = (reservation.hotel.hotel_id, reservation.room_number) - logs = log_lookup.get(key, []) - - if reservation.status == "заселен" and not logs: - self.record_violation( - hotel=reservation.hotel, - room_number=reservation.room_number, - violation_type="no_qr_scan", - details=f"Бронирование для номера {reservation.room_number} в отеле '{reservation.hotel.name}' " - f"не имеет записи сканирования QR." - ) - - for log in logs: - if log.created < reservation.check_in: - self.record_violation( - hotel=reservation.hotel, - room_number=reservation.room_number, - violation_type="early_check_in", - details=f"Раннее заселение: сканирование QR {log.created} раньше времени заезда " - f"{reservation.check_in} для номера {reservation.room_number}." - ) - - for (hotel_id, room_number), logs in log_lookup.items(): - matching_reservations = reservations.filter( - hotel__hotel_id=hotel_id, - room_number=room_number - ) - if not matching_reservations.exists(): - for log in logs: - self.record_violation( - hotel=Hotel.objects.filter(hotel_id=hotel_id).first(), - room_number=room_number, - violation_type="no_reservation", - details=f"Сканирование QR {log.created} для номера {room_number} в отеле с ID '{hotel_id}' " - f"не соответствует ни одному бронированию." - ) - - def record_violation(self, hotel, room_number, violation_type, details): - if hotel: - self.violations.append(ViolationLog( - hotel=hotel, - room_number=room_number, - violation_type=violation_type, - violation_details=details - )) - self.log_warning(f"Зафиксировано нарушение: {details}") - - def save_violations(self): - if self.violations: - ViolationLog.objects.bulk_create(self.violations) - self.log_info(f"Создано {len(self.violations)} записей в ViolationLog.") - else: - self.log_info("Нарушений не обнаружено.") - def run_check(self): - self.log_info(f"Запуск проверки с {self.start_time} по {self.end_time}.") + """Запуск проверки фродовых событий.""" + self.log_info("🔍 Запуск проверки фродовых данных.") try: - self.find_violations() - self.save_violations() + check_in_diff = timedelta(hours=self.checkin_diff_hours) + + # Кэшируем отели в словарь для быстрого доступа + hotels_map = {hotel.hotel_id: hotel for hotel in Hotel.objects.all()} + + # Загружаем бронирования и активности пользователей + user_logs = UserActivityLog.objects.filter(fraud_checked=False) + reservations = Reservation.objects.filter(fraud_checked=False).select_related('hotel') + + # Преобразуем бронирования в словарь для быстрого поиска + reservations_map = { + (res.hotel.hotel_id, res.room_number): res for res in reservations + } + + violations = [] + checked_reservations = set() # Сет для бронирований, которые были проверены + + self.log_info(f"✅ Загружено {len(user_logs)} логов активности и {len(reservations)} бронирований.") + + for user_log in user_logs: + try: + params = json.loads(user_log.url_parameters.replace("'", '"')) if user_log.url_parameters else {} + hotel_id = params.get('utm_content') + room = params.get('utm_term') + + if not hotel_id or not room: + self.log_warning(f"🚫 Пропущен лог без hotel_id или room_number: {user_log.url_parameters}") + continue # Пропускаем записи без нужных параметров + + key = (hotel_id, room) + reserv = reservations_map.get(key) + + discrepancy_type = "match" # По умолчанию считаем, что всё соответствует + + if reserv: + checked_reservations.add(reserv) + + if user_log.date_time < reserv.check_in: + discrepancy_type = 'early' + self.log_warning(f"⚠️ Обнаружено раннее заселение: {user_log.date_time} < {reserv.check_in}") + elif user_log.date_time > reserv.check_in + check_in_diff: + discrepancy_type = 'late' + self.log_warning(f"⚠️ Обнаружено позднее заселение: {user_log.date_time} > {reserv.check_in + check_in_diff}") + else: + discrepancy_type = 'no_booking' + self.log_warning(f"🚨 Заселение без бронирования: {user_log.date_time} (Отель {hotel_id}, Комната {room})") + + violations.append(RoomDiscrepancy( + hotel=hotels_map.get(hotel_id), + room_number=room, + discrepancy_type=discrepancy_type, + booking_id=reserv.reservation_id if reserv else None, + check_in_date_expected=reserv.check_in if reserv else None, + check_in_date_actual=user_log.date_time, + )) + + user_log.fraud_checked = True # Отмечаем логи как проверенные + + except json.JSONDecodeError: + self.log_error(f"❌ Ошибка декодирования JSON в URL-параметрах: {user_log.url_parameters}") + except Exception as e: + self.log_error(f"❌ Ошибка при обработке логов: {e}") + + # Добавляем пропущенные бронирования (неявки) + for reserv in reservations: + if reserv not in checked_reservations: + violations.append(RoomDiscrepancy( + hotel=reserv.hotel, + room_number=reserv.room_number, + discrepancy_type='missed', + booking_id=reserv.reservation_id, + check_in_date_expected=reserv.check_in, + )) + self.log_warning(f"⚠️ Обнаружена неявка (missed) | Отель: {reserv.hotel.hotel_id}, Номер: {reserv.room_number}, Ожидаемая дата заезда: {reserv.check_in}") + + # Массово сохраняем все записи, включая корректные совпадения + if violations: + RoomDiscrepancy.objects.bulk_create(violations) + self.log_info(f"✅ Записано {len(violations)} новых записей в RoomDiscrepancy.") + + # Обновляем флаги fraud_checked + UserActivityLog.objects.filter(id__in=[log.id for log in user_logs]).update(fraud_checked=True) + Reservation.objects.filter(id__in=[res.id for res in reservations]).update(fraud_checked=True) + except Exception as e: - self.log_error(f"Ошибка при выполнении проверки: {e}") - self.log_info("Проверка завершена.") + 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 завершена.") \ No newline at end of file + logger.error(f"❌ Ошибка при запуске проверки: {e}") + logger.info("✅ run_reservation_check завершена.") diff --git a/antifroud/data_sync.py b/antifroud/data_sync.py index 54baeae8..4f5f3dcc 100644 --- a/antifroud/data_sync.py +++ b/antifroud/data_sync.py @@ -15,7 +15,7 @@ from django.db.models import F class DatabaseConnector: def __init__(self, db_settings_id): self.db_settings_id = db_settings_id - self.logger = CustomLogger(name="DatabaseConnector", log_level="DEBUG").get_logger() + self.logger = CustomLogger(name="DatabaseConnector", log_level="WARNING").get_logger() self.connection = None self.db_settings = self.get_db_settings() @@ -314,7 +314,7 @@ def scheduled_sync(): except Exception as e: logger.error(f"Error syncing connection {db_settings}: {e}") - with ThreadPoolExecutor(max_workers=5) as executor: + with ThreadPoolExecutor(max_workers=10) as executor: futures = [executor.submit(sync_task, db_settings) for db_settings in active_db_settings] for future in futures: try: diff --git a/antifroud/management/commands/fraud_check.py b/antifroud/management/commands/fraud_check.py new file mode 100644 index 00000000..58e82d2a --- /dev/null +++ b/antifroud/management/commands/fraud_check.py @@ -0,0 +1,8 @@ +from django.core.management.base import BaseCommand +from antifroud.check_fraud import run_reservation_check + +class Command(BaseCommand): + help = "Запуск проверки на несоответствия" + + def handle(self, *args, **options): + run_reservation_check() diff --git a/antifroud/migrations/0003_alter_roomdiscrepancy_booking_id_and_more.py b/antifroud/migrations/0003_alter_roomdiscrepancy_booking_id_and_more.py new file mode 100644 index 00000000..f3b61fed --- /dev/null +++ b/antifroud/migrations/0003_alter_roomdiscrepancy_booking_id_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 5.1.4 on 2025-02-01 06:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('antifroud', '0002_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='roomdiscrepancy', + name='booking_id', + field=models.CharField(max_length=255, null=True, verbose_name='ID бронирования'), + ), + migrations.AlterField( + model_name='roomdiscrepancy', + name='check_in_date_actual', + field=models.DateField(null=True, verbose_name='Фактическая дата заселения'), + ), + migrations.AlterField( + model_name='roomdiscrepancy', + name='check_in_date_expected', + field=models.DateField(null=True, verbose_name='Ожидаемая дата заселения'), + ), + ] diff --git a/antifroud/migrations/0004_alter_roomdiscrepancy_discrepancy_type.py b/antifroud/migrations/0004_alter_roomdiscrepancy_discrepancy_type.py new file mode 100644 index 00000000..8f3c2879 --- /dev/null +++ b/antifroud/migrations/0004_alter_roomdiscrepancy_discrepancy_type.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.4 on 2025-02-01 06:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('antifroud', '0003_alter_roomdiscrepancy_booking_id_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='roomdiscrepancy', + name='discrepancy_type', + field=models.CharField(choices=[('early', 'Раннее заселение'), ('late', 'Позднее заселение'), ('missed', 'Неявка'), ('no_booking', 'Без брони')], max_length=50, verbose_name='Тип несоответствия'), + ), + ] diff --git a/antifroud/migrations/0005_roomdiscrepancy_fraud_checked_and_more.py b/antifroud/migrations/0005_roomdiscrepancy_fraud_checked_and_more.py new file mode 100644 index 00000000..7c458466 --- /dev/null +++ b/antifroud/migrations/0005_roomdiscrepancy_fraud_checked_and_more.py @@ -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='Проверено на несоответствия'), + ), + ] diff --git a/antifroud/migrations/0006_alter_roomdiscrepancy_fraud_checked_and_more.py b/antifroud/migrations/0006_alter_roomdiscrepancy_fraud_checked_and_more.py new file mode 100644 index 00000000..8b8486a7 --- /dev/null +++ b/antifroud/migrations/0006_alter_roomdiscrepancy_fraud_checked_and_more.py @@ -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='Проверено на несоответствия'), + ), + ] diff --git a/antifroud/migrations/0007_remove_roomdiscrepancy_fraud_checked.py b/antifroud/migrations/0007_remove_roomdiscrepancy_fraud_checked.py new file mode 100644 index 00000000..db4f7a53 --- /dev/null +++ b/antifroud/migrations/0007_remove_roomdiscrepancy_fraud_checked.py @@ -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', + ), + ] diff --git a/antifroud/models.py b/antifroud/models.py index 0f7a861e..ffefc37f 100644 --- a/antifroud/models.py +++ b/antifroud/models.py @@ -31,6 +31,7 @@ class UserActivityLog(models.Model): honeypot = models.BooleanField(verbose_name="Метка honeypot", blank=True, null=True) reply = models.BooleanField(verbose_name="Ответ пользователя", blank=True, null=True) page_url = models.URLField(blank=True, null=True, verbose_name="URL страницы") + fraud_checked = models.BooleanField(default=False, verbose_name="Проверено на несоответствия", db_index=True) @property def formatted_timestamp(self): @@ -42,7 +43,7 @@ class UserActivityLog(models.Model): return "Нет данных" # Изменение имени столбца - + class Meta: indexes = [ models.Index(fields=["external_id"], name="idx_external_id"), @@ -55,7 +56,7 @@ class UserActivityLog(models.Model): verbose_name_plural = "Логи активности пользователей" def __str__(self): return f"UserActivityLog {self.id}: {self.page_title}" - + class Meta: verbose_name = "Регистрация посетителей" verbose_name_plural = "Регистрации посетителей" @@ -63,7 +64,7 @@ class UserActivityLog(models.Model): def get_location(self): if not self.ip: return "IP-адрес отсутствует" - + try: db_path = f"{settings.GEOIP_PATH}/GeoLite2-City.mmdb" geoip_reader = Reader(db_path) @@ -78,10 +79,10 @@ class UserActivityLog(models.Model): except AddressNotFoundError: return "IP-адрес не найден в базе" except FileNotFoundError: - logger.error(f"Файл базы данных GeoIP не найден по пути: {db_path}") + # logger.error(f"Файл базы данных GeoIP не найден по пути: {db_path}") return "Файл базы данных GeoIP не найден" except Exception as e: - logger.error(f"Ошибка при определении местоположения: {e}") + # logger.error(f"Ошибка при определении местоположения: {e}") return "Местоположение недоступно" class ExternalDBSettings(models.Model): name = models.CharField(max_length=255, unique=True, help_text="Имя подключения для идентификации.") @@ -107,12 +108,12 @@ class ExternalDBSettings(models.Model): class RoomDiscrepancy(models.Model): hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE, verbose_name="Отель") room_number = models.CharField(max_length=50, verbose_name="Номер комнаты") - booking_id = models.CharField(max_length=255, verbose_name="ID бронирования") - check_in_date_expected = models.DateField(verbose_name="Ожидаемая дата заселения") - check_in_date_actual = models.DateField(verbose_name="Фактическая дата заселения") + booking_id = models.CharField(max_length=255, null=True, verbose_name="ID бронирования") + check_in_date_expected = models.DateField(null=True, verbose_name="Ожидаемая дата заселения") + check_in_date_actual = models.DateField(null=True, verbose_name="Фактическая дата заселения") discrepancy_type = models.CharField( max_length=50, - choices=[("early", "Раннее заселение"), ("late", "Позднее заселение"), ("missed", "Неявка")], + choices=[("early", "Раннее заселение"), ("late", "Позднее заселение"), ("missed", "Неявка"), ("no_booking", "Без брони")], verbose_name="Тип несоответствия" ) created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания") @@ -216,8 +217,8 @@ class SyncLog(models.Model): def __str__(self): return f"Отель: {self.hotel.name} | Получено: {self.recieved_records} | Обработано: {self.processed_records}" - - + + class ViolationLog(models.Model): hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE, verbose_name="Отель") room_number = models.CharField(max_length=50, verbose_name="Номер комнаты", null=True, blank=True) diff --git a/antifroud/tests.py b/antifroud/tests.py index 7ce503c2..e69de29b 100644 --- a/antifroud/tests.py +++ b/antifroud/tests.py @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/docker-compose.yaml b/docker-compose.yaml index 46e790e7..5f09d867 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -7,6 +7,11 @@ services: - MYSQL_DATABASE=touchh - MYSQL_USER=touchh - MYSQL_PASSWORD=${MYSQL_PASSWORD:-touchh} + healthcheck: + test: mariadb -utouchh -p${MYSQL_PASSWORD:-touchh} -e 'SELECT 1;' touchh + interval: 1s + timeout: 10s + retries: 10 volumes: - ./var/mysql:/var/lib/mysql bot: @@ -15,7 +20,9 @@ services: image: touchh-py restart: on-failure command: ['python3', 'manage.py', 'run_bot'] - depends_on: ['db'] + depends_on: + db: + condition: service_healthy stop_signal: SIGINT volumes: - .:/app diff --git a/hotels/admin.py b/hotels/admin.py index 48ae9d4f..2eb86cc7 100644 --- a/hotels/admin.py +++ b/hotels/admin.py @@ -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) diff --git a/hotels/migrations/0004_reservation_fraud_checked.py b/hotels/migrations/0004_reservation_fraud_checked.py new file mode 100644 index 00000000..cddb349b --- /dev/null +++ b/hotels/migrations/0004_reservation_fraud_checked.py @@ -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='Проверено на несоответствия'), + ), + ] diff --git a/hotels/migrations/0005_alter_reservation_check_in_and_more.py b/hotels/migrations/0005_alter_reservation_check_in_and_more.py new file mode 100644 index 00000000..05f86c38 --- /dev/null +++ b/hotels/migrations/0005_alter_reservation_check_in_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1.4 on 2025-02-02 00:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hotels', '0004_reservation_fraud_checked'), + ] + + operations = [ + migrations.AlterField( + model_name='reservation', + name='check_in', + field=models.DateTimeField(blank=True, null=True, verbose_name='Дата заезда'), + ), + migrations.AlterField( + model_name='reservation', + name='check_out', + field=models.DateTimeField(blank=True, null=True, verbose_name='Дата выезда'), + ), + ] diff --git a/hotels/models.py b/hotels/models.py index d2d9c9d6..73b69bfa 100644 --- a/hotels/models.py +++ b/hotels/models.py @@ -111,11 +111,12 @@ class Reservation(models.Model): reservation_id = models.BigIntegerField(unique=True, verbose_name="ID бронирования") room_number = models.CharField(max_length=255, null=True, blank=True, verbose_name="Номер комнаты") room_type = models.CharField(max_length=255, verbose_name="Тип комнаты") - check_in = models.DateTimeField(verbose_name="Дата заезда") - check_out = models.DateTimeField(verbose_name="Дата выезда") + check_in = models.DateTimeField(verbose_name="Дата заезда", null=True, blank=True) + check_out = models.DateTimeField(verbose_name="Дата выезда", null=True, blank=True) status = models.CharField(max_length=50, verbose_name="Статус") price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name="Цена") discount = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name="Скидка") + fraud_checked = models.BooleanField(default=False, verbose_name="Проверено на несоответствия", db_index=True) def clean(self): if self.check_out and self.check_in and self.check_out <= self.check_in: diff --git a/pms_integration/manager.py b/pms_integration/manager.py index 67142006..08f86cda 100644 --- a/pms_integration/manager.py +++ b/pms_integration/manager.py @@ -52,6 +52,7 @@ class PMSIntegrationManager: """ Загружает плагин, соответствующий PMS конфигурации отеля. """ +<<<<<<< HEAD pms_name = self.hotel.pms.plugin_name.lower() if pms_name in self.plugins: plugin_class = self.plugins[pms_name] @@ -60,6 +61,28 @@ class PMSIntegrationManager: else: raise ValueError(f"Неизвестный PMS: {pms_name}") +======= + pms_name = self.hotel.pms.plugin_name.lower() # Приводим название плагина к нижнему регистру + + # Формируем имя модуля и класса плагина + plugin_module_name = f"pms_integration.plugins.{pms_name}_pms" + plugin_class_name = f"{pms_name.capitalize()}PMSPlugin" + + try: + # Динамически импортируем модуль плагина + plugin_module = importlib.import_module(plugin_module_name) + + # Динамически получаем класс плагина + plugin_class = getattr(plugin_module, plugin_class_name, None) + if not plugin_class or not issubclass(plugin_class, BasePMSPlugin): + raise ImportError(f"Класс {plugin_class_name} не найден или не является наследником BasePMSPlugin.") + + # Инициализируем плагин + self.plugin = plugin_class(self.hotel) + + except ImportError as e: + raise ValueError(f"Ошибка загрузки плагина для PMS {pms_name}: {e}") +>>>>>>> PMSManager_refactor def fetch_data(self): """ Получает данные из PMS с использованием загруженного плагина. diff --git a/pms_integration/plugins/bnovo_pms.py b/pms_integration/plugins/bnovo_pms.py index 9728ef43..9f50f129 100644 --- a/pms_integration/plugins/bnovo_pms.py +++ b/pms_integration/plugins/bnovo_pms.py @@ -520,6 +520,7 @@ class BnovoPMSPlugin(BasePMSPlugin): headers = {"Content-Type": "application/json"} await self._save_token_to_db(self.token) +<<<<<<< HEAD response = requests.post(url, json=payload, headers=headers, allow_redirects=False) if response.status_code == 302: self.token = response.cookies.get("SID") @@ -527,6 +528,19 @@ class BnovoPMSPlugin(BasePMSPlugin): else: logger.error(f"Ошибка авторизации: {response.status_code}, {response.text}") raise ValueError("Ошибка авторизации") +======= + async def _fetch_data(self): + """Получение данных о бронированиях с помощью эндпоинта /dashboard.""" + logger.info("Начало процесса получения данных о бронированиях.") + + # Вызов функции получения данных аккаунта + try: + account_data = await self._fetch_and_log_account_data() + logger.info(f"Данные аккаунта успешно получены:") + except Exception as e: + logger.error(f"Ошибка получения данных аккаунта: {e}") + raise +>>>>>>> PMSManager_refactor async def _fetch_paginated_data(self): """ diff --git a/pms_integration/plugins/ecvi_pms.py b/pms_integration/plugins/ecvi_pms.py index fb4a8849..5d6144a1 100644 --- a/pms_integration/plugins/ecvi_pms.py +++ b/pms_integration/plugins/ecvi_pms.py @@ -1,4 +1,144 @@ +# # ecvi_pms.py +# import logging +# import requests +# import json +# import os +# from datetime import datetime, timedelta +# from asgiref.sync import sync_to_async +# from hotels.models import Hotel, Reservation +# from .base_plugin import BasePMSPlugin + +# class EcviPMSPlugin(BasePMSPlugin): +# """ +# Плагин для интеграции с PMS Ecvi. +# """ + +# def __init__(self, hotel): +# super().__init__(hotel.pms) +# self.hotel = hotel + +# if not self.hotel.pms: +# raise ValueError(f"Отель {self.hotel.name} не имеет связанной PMS конфигурации.") + +# self.api_url = self.hotel.pms.url.rstrip("/") +# self.token = self.hotel.pms.token +# self.username = self.hotel.pms.username +# self.password = self.hotel.pms.password + +# self.logger = logging.getLogger(self.__class__.__name__) +# handler_console = logging.StreamHandler() +# handler_file = logging.FileHandler('var/log/ecvi_pms_plugin.log') +# formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') +# handler_console.setFormatter(formatter) +# handler_file.setFormatter(formatter) +# self.logger.addHandler(handler_console) +# self.logger.addHandler(handler_file) +# self.logger.setLevel(logging.WARNING) + +# def get_default_parser_settings(self): +# return { +# "field_mapping": { +# "check_in": "checkin", +# "check_out": "checkout", +# "room_number": "room_name", +# "room_type_name": "room_type", +# "status": "occupancy", +# }, +# "date_format": "%Y-%m-%d %H:%M:%S" +# } + +# async def _fetch_data(self): +# headers = {"Content-Type": "application/json"} +# data = {"token": self.token} + +# try: +# response = await sync_to_async(requests.post)( +# self.api_url, headers=headers, json=data, auth=(self.username, self.password) +# ) +# response.raise_for_status() +# response_data = response.json() +# self.logger.debug(f"Полученные данные с API: {response_data}") + +# # Группировка данных по номеру комнаты +# structured_data = {} +# for item in response_data: +# room_number = item.get("room_name", "unknown") +# if room_number not in structured_data: +# structured_data[room_number] = [] +# structured_data[room_number].append(item) + +# # Сохранение данных во временный JSON-файл +# temp_dir = os.path.join("temp", "ecvi") +# os.makedirs(temp_dir, exist_ok=True) +# temp_file = os.path.join(temp_dir, f"ecvi_data_{datetime.now().strftime('%Y%m%d%H%M%S')}.json") +# with open(temp_file, 'w') as file: +# json.dump(structured_data, file, indent=4, ensure_ascii=False) + +# return await self._process_data(response_data) +# except requests.exceptions.RequestException as e: +# self.logger.error(f"Ошибка API: {e}") +# return { +# "processed_intervals": 0, +# "processed_items": 0, +# "errors": [str(e)] +# } + +# async def _process_data(self, data): +# processed_items = 0 +# errors = [] +# date_formats = ["%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"] + +# for item in data: +# try: +# checkin = item['checkin'] +# checkout = item['checkout'] +# if checkin in [None, "0000-00-00 00:00:00"] or checkout in [None, "0000-00-00 00:00:00"]: +# continue +# checkin = self._parse_date(checkin, date_formats) +# checkout = self._parse_date(checkout, date_formats) + +# reservation, created = await sync_to_async(Reservation.objects.update_or_create)( +# reservation_id=item['task_id'], +# defaults={ +# 'room_number': item['room_name'], +# 'room_type': item['room_type'], +# 'check_in': checkin, +# 'check_out': checkout, +# 'status': item['occupancy'], +# 'hotel': self.hotel, +# } +# ) +# processed_items += 1 +# except Exception as e: +# self.logger.error(f"Ошибка обработки записи: {e}") +# errors.append(str(e)) + +# return { +# "processed_intervals": 1, +# "processed_items": processed_items, +# "errors": errors +# } + +# @staticmethod +# def _parse_date(date_str, formats): +# for fmt in formats: +# try: +# return datetime.strptime(date_str, fmt) +# except ValueError: +# continue +# raise ValueError(f"Дата '{date_str}' не соответствует ожидаемым форматам: {formats}") + +# def validate_plugin(self): +# required_methods = ["fetch_data", "get_default_parser_settings", "_fetch_data"] +# for method in required_methods: +# if not hasattr(self, method): +# raise ValueError(f"Плагин {type(self).__name__} не реализует метод {method}.") +# self.logger.debug(f"Плагин {self.__class__.__name__} прошел валидацию.") +# return True + import logging +import os +import json from datetime import datetime, timedelta import requests from asgiref.sync import sync_to_async @@ -11,20 +151,17 @@ class EcviPMSPlugin(BasePMSPlugin): """ def __init__(self, hotel): - super().__init__(hotel.pms) # Передаем PMS-конфигурацию в базовый класс - self.hotel = hotel # Сохраняем объект отеля + super().__init__(hotel.pms) + self.hotel = hotel - # Проверка PMS-конфигурации if not self.hotel.pms: raise ValueError(f"Отель {self.hotel.name} не имеет связанной PMS конфигурации.") - # Инициализация параметров API self.api_url = self.hotel.pms.url self.token = self.hotel.pms.token self.username = self.hotel.pms.username self.password = self.hotel.pms.password - # Настройка логгера self.logger = logging.getLogger(self.__class__.__name__) handler_console = logging.StreamHandler() handler_file = logging.FileHandler('var/log/ecvi_pms_plugin.log') @@ -33,7 +170,11 @@ class EcviPMSPlugin(BasePMSPlugin): handler_file.setFormatter(formatter) self.logger.addHandler(handler_console) self.logger.addHandler(handler_file) - self.logger.setLevel(logging.DEBUG) + self.logger.setLevel(logging.WARNING) + + # Директория для сохранения JSON-файлов + self.data_dir = "var/data/ecvi" + os.makedirs(self.data_dir, exist_ok=True) def get_default_parser_settings(self): """ @@ -47,7 +188,7 @@ class EcviPMSPlugin(BasePMSPlugin): "room_type_name": "room_type", "status": "occupancy", }, - "date_format": "%Y-%m-%d %H:%M:%S" # Формат изменен на соответствующий данным + "date_format": "%Y-%m-%d %H:%M:%S" } async def _fetch_data(self): @@ -58,14 +199,23 @@ class EcviPMSPlugin(BasePMSPlugin): data = {"token": self.token} try: - # Запрос данных из PMS API response = await sync_to_async(requests.post)( self.api_url, headers=headers, json=data, auth=(self.username, self.password) ) response.raise_for_status() response_data = response.json() self.logger.debug(f"Полученные данные с API: {response_data}") + + # Сохраняем весь ответ API в файл для анализа + file_name = f"ecvi_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" + file_path = os.path.join(self.data_dir, file_name) + with open(file_path, "w", encoding="utf-8") as f: + json.dump(response_data, f, ensure_ascii=False, indent=4) + + self.logger.info(f"API-ответ сохранен в файл: {file_path}") + return await self._process_data(response_data) + except requests.exceptions.RequestException as e: self.logger.error(f"Ошибка API: {e}") return { @@ -80,22 +230,62 @@ class EcviPMSPlugin(BasePMSPlugin): """ processed_items = 0 errors = [] + date_formats = ["%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"] + unix_epoch = datetime(1970, 1, 1, 0, 0, 0) - date_formats = ["%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"] # Поддержка нескольких форматов даты - + valid_reservations = [] + print(data) for item in data: try: - # Парсинг даты с поддержкой нескольких форматов - checkin = self._parse_date(item['checkin'], date_formats) - checkout = self._parse_date(item['checkout'], date_formats) + checkin = item.get('checkin') + checkout = item.get('checkout') + # Фильтруем записи с некорректными датами + if checkin in [None, "0000-00-00 00:00:00", "1970-01-01 00:00:00", 0] or \ + checkout in [None, "0000-00-00 00:00:00", "1970-01-01 00:00:00", 0]: + self.logger.warning(f"Игнорируется запись с некорректной датой: {item}") + continue + + checkin = self._parse_date(checkin, date_formats) + checkout = self._parse_date(checkout, date_formats) + + # Проверяем на Unix epoch + if checkin == unix_epoch or checkout == unix_epoch: + self.logger.warning(f"Игнорируется запись с Unix epoch датой: {item}") + continue + + # Проверяем timestamp + if checkin.timestamp() == 0 or checkout.timestamp() == 0: + self.logger.warning(f"Игнорируется запись с timestamp = 0: {item}") + continue + + valid_reservations.append(item) + + except Exception as e: + self.logger.error(f"Ошибка обработки записи: {e}") + errors.append(str(e)) + + # Логируем количество отфильтрованных записей + self.logger.info(f"Обработано бронирований: {len(valid_reservations)}") + + # Сохранение валидных бронирований в JSON для проверки + valid_file_name = f"valid_reservations_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" + valid_file_path = os.path.join(self.data_dir, valid_file_name) + with open(valid_file_path, "w", encoding="utf-8") as f: + json.dump(valid_reservations, f, ensure_ascii=False, indent=4) + + self.logger.info(f"Валидные бронирования сохранены в файл: {valid_file_path}") + + # Сохранение данных в БД + for item in valid_reservations: + try: reservation, created = await sync_to_async(Reservation.objects.update_or_create)( reservation_id=item['task_id'], defaults={ 'room_number': item['room_name'], 'room_type': item['room_type'], - 'check_in': checkin, - 'check_out': checkout, + 'check_in': self._parse_date(item['checkin'], date_formats), + 'check_out': self._parse_date(item['checkout'], date_formats), 'status': item['occupancy'], 'hotel': self.hotel, } @@ -109,7 +299,7 @@ class EcviPMSPlugin(BasePMSPlugin): processed_items += 1 except Exception as e: - self.logger.error(f"Ошибка обработки записи: {e}") + self.logger.error(f"Ошибка сохранения бронирования: {e}") errors.append(str(e)) return { diff --git a/pms_integration/plugins/realtycalendar_pms.py b/pms_integration/plugins/realtycalendar_pms.py index 0b440aef..3a9b2f1b 100644 --- a/pms_integration/plugins/realtycalendar_pms.py +++ b/pms_integration/plugins/realtycalendar_pms.py @@ -1,313 +1,111 @@ +import logging import requests -import hashlib import json -from .base_plugin import BasePMSPlugin +import os from datetime import datetime, timedelta from asgiref.sync import sync_to_async -from touchh.utils.log import CustomLogger from hotels.models import Hotel, Reservation -from app_settings.models import GlobalHotelSettings -from django.utils import timezone +from .base_plugin import BasePMSPlugin -class RealtyCalendarPlugin(BasePMSPlugin): - def __init__(self, config): - super().__init__(config) - self.public_key = config.public_key - self.private_key = config.private_key - self.api_url = config.url.rstrip("/") - self.logger = CustomLogger(name="RealtyCalendarPlugin", log_level="DEBUG").get_logger() - if not self.public_key or not self.private_key: - raise ValueError("Публичный или приватный ключ отсутствует для RealtyCalendar") +class RealtyCalendarPMSPlugin(BasePMSPlugin): + def __init__(self, hotel): + super().__init__(hotel.pms) + self.hotel = hotel + + if not self.hotel.pms: + raise ValueError(f"Отель {self.hotel.name} не имеет связанной PMS конфигурации.") + + self.api_url = self.hotel.pms.url.rstrip("/") + self.public_key = self.hotel.pms.public_key + self.private_key = self.hotel.pms.private_key + + self.logger = logging.getLogger(self.__class__.__name__) + handler_console = logging.StreamHandler() + handler_file = logging.FileHandler('var/log/realtycalendar_pms_plugin.log') + formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + handler_console.setFormatter(formatter) + handler_file.setFormatter(formatter) + self.logger.addHandler(handler_console) + self.logger.addHandler(handler_file) + self.logger.setLevel(logging.WARNING) def get_default_parser_settings(self): - """ - Возвращает настройки по умолчанию для обработки данных. - """ return { - "date_format": "%Y-%m-%dT%H:%M:%S", - "timezone": "UTC" + "field_mapping": { + "check_in": "begin_date", + "check_out": "end_date", + "room_number": "apartment_id", + "room_type_name": "notes", + "status": "status", + }, + "date_format": "%Y-%m-%d %H:%M:%S" } - def _get_sorted_keys(self, obj): - sorted_keys = sorted(obj.keys()) - self.logger.debug(f"Отсортированные ключи: {sorted_keys}") - return sorted_keys - - def _generate_data_string(self, obj): - sorted_keys = self._get_sorted_keys(obj) - string = "".join(f"{key}={obj[key]}" for key in sorted_keys) - self.logger.debug(f"Сформированная строка данных: {string}") - return string + self.private_key - - def _generate_md5(self, string): - md5_hash = hashlib.md5(string.encode("utf-8")).hexdigest() - self.logger.debug(f"Сформированный MD5-хеш: {md5_hash}") - return md5_hash - - def _generate_sign(self, data): - data_string = self._generate_data_string(data) - self.logger.debug(f"Строка для подписи: {data_string}") - sign = self._generate_md5(data_string) - self.logger.debug(f"Подпись: {sign}") - return sign - - # async def _fetch_data(self): - # self.logger.debug("Начало выполнения функции _fetch_data") - # base_url = f"{self.api_url}/api/v1/bookings/{self.public_key}/" - # headers = { - # "Accept": "application/json", - # "Content-Type": "application/json", - # } - - # now = datetime.now() - # data = { - # "begin_date": (now - timedelta(days=7)).strftime("%Y-%m-%d"), - # "end_date": now.strftime("%Y-%m-%d"), - # } - # data["sign"] = self._generate_sign(data) - - # try: - # response = requests.post(url=base_url, headers=headers, json=data) - # self.logger.debug(f"Статус ответа: {response.status_code}") - - # if response.status_code != 200: - # self.logger.error(f"Ошибка API: {response.status_code}, {response.text}") - # raise ValueError(f"Ошибка API RealtyCalendar: {response.status_code}") - - # try: - # response_data = response.json() - # bookings = response_data.get("bookings", []) - # # self.logger.debug(f"Полученные данные: {bookings}") - - # if not isinstance(bookings, list): - # self.logger.error(f"Ожидался список, но получен: {type(bookings)}") - # raise ValueError("Некорректный формат данных для bookings") - # except json.JSONDecodeError as e: - # self.logger.error(f"Ошибка декодирования JSON: {e}") - # raise ValueError("Ответ не является корректным JSON.") - # except Exception as e: - # self.logger.error(f"Ошибка обработки ответа API: {e}") - # raise - - # except Exception as e: - # self.logger.error(f"Ошибка запроса к API RealtyCalendar: {e}") - # raise - - # try: - # hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config) - # hotel_tz = hotel.timezone - # self.logger.debug(f"Настройки отеля: {hotel.name}, Timezone: {hotel_tz}") - - # hotel_settings = await sync_to_async(GlobalHotelSettings.objects.first)() - # check_in_time = hotel_settings.check_in_time.strftime("%H:%M:%S") if hotel_settings else "14:00:00" - # check_out_time = hotel_settings.check_out_time.strftime("%H:%M:%S") if hotel_settings else "12:00:00" - # except Exception as e: - # self.logger.error(f"Ошибка получения настроек отеля: {e}") - # check_in_time, check_out_time = "14:00:00", "12:00:00" - - # filtered_data = [] - # for item in bookings: - # try: - # if not isinstance(item, dict): - # self.logger.error(f"Некорректный формат элемента: {item}") - # continue - - # reservation_id = item.get('id') - # if not reservation_id: - # self.logger.error(f"ID резервации отсутствует: {item}") - # continue - - # begin_date = item.get('begin_date') - # end_date = item.get('end_date') - # if not begin_date or not end_date: - # self.logger.error(f"Отсутствуют даты в записи: {item}") - # continue - - # checkin = timezone.make_aware( - # datetime.strptime(f"{begin_date} {check_in_time}", "%Y-%m-%d %H:%M:%S") - # ) - # checkout = timezone.make_aware( - # datetime.strptime(f"{end_date} {check_out_time}", "%Y-%m-%d %H:%M:%S") - # ) - - # filtered_data.append({ - # 'reservation_id': reservation_id, - # 'checkin': checkin, - # 'checkout': checkout, - # 'room_number': item.get('apartment_id'), - # 'room_type': item.get('notes', 'Описание отсутствует'), - # 'status': item.get('status') - # }) - # except Exception as e: - # self.logger.error(f"Ошибка обработки элемента: {e}") - - # # self.logger.debug(f"Отфильтрованные данные: {filtered_data}") - # await self._save_to_db(filtered_data) async def _fetch_data(self): - self.logger.debug("Начало выполнения функции _fetch_data") - base_url = f"{self.api_url}/api/v1/bookings/{self.public_key}/" - headers = { - "Accept": "application/json", - "Content-Type": "application/json", - } - + headers = {"Accept": "application/json", "Content-Type": "application/json"} now = datetime.now() data = { "begin_date": (now - timedelta(days=7)).strftime("%Y-%m-%d"), "end_date": now.strftime("%Y-%m-%d"), + "sign": self._generate_sign() } - data["sign"] = self._generate_sign(data) try: - response = requests.post(url=base_url, headers=headers, json=data) - self.logger.debug(f"Статус ответа: {response.status_code}") - - if response.status_code != 200: - self.logger.error(f"Ошибка API: {response.status_code}, {response.text}") - return { - "processed_intervals": 0, - "processed_items": 0, - "errors": [f"Ошибка API RealtyCalendar: {response.status_code}"] - } - + response = await sync_to_async(requests.post)( + f"{self.api_url}/api/v1/bookings/{self.public_key}/", + headers=headers, json=data + ) + response.raise_for_status() response_data = response.json() - bookings = response_data.get("bookings", []) + return await self._process_data(response_data) + except requests.exceptions.RequestException as e: + self.logger.error(f"Ошибка API: {e}") + return {"processed_intervals": 0, "processed_items": 0, "errors": [str(e)]} - if not isinstance(bookings, list): - self.logger.error(f"Ожидался список, но получен: {type(bookings)}") - return { - "processed_intervals": 0, - "processed_items": 0, - "errors": ["Некорректный формат данных для bookings"] - } - - except json.JSONDecodeError as e: - self.logger.error(f"Ошибка декодирования JSON: {e}") - return { - "processed_intervals": 0, - "processed_items": 0, - "errors": ["Ответ не является корректным JSON."] - } - except Exception as e: - self.logger.error(f"Ошибка запроса к API RealtyCalendar: {e}") - return { - "processed_intervals": 0, - "processed_items": 0, - "errors": [str(e)] - } - - # Получение настроек отеля - try: - hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config) - hotel_tz = hotel.timezone - self.logger.debug(f"Настройки отеля: {hotel.name}, Timezone: {hotel_tz}") - - hotel_settings = await sync_to_async(GlobalHotelSettings.objects.first)() - check_in_time = hotel_settings.check_in_time.strftime("%H:%M:%S") if hotel_settings else "14:00:00" - check_out_time = hotel_settings.check_out_time.strftime("%H:%M:%S") if hotel_settings else "12:00:00" - except Exception as e: - self.logger.error(f"Ошибка получения настроек отеля: {e}") - check_in_time, check_out_time = "14:00:00", "12:00:00" - - # Обработка записей + async def _process_data(self, data): processed_items = 0 errors = [] - filtered_data = [] + date_formats = ["%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"] - for item in bookings: + for item in data.get("bookings", []): try: - if not isinstance(item, dict): - raise ValueError(f"Некорректный формат элемента: {item}") + checkin = self._parse_date(item['begin_date'], date_formats) + checkout = self._parse_date(item['end_date'], date_formats) - reservation_id = item.get('id') - if not reservation_id: - raise ValueError(f"ID резервации отсутствует: {item}") - - begin_date = item.get('begin_date') - end_date = item.get('end_date') - if not begin_date or not end_date: - raise ValueError(f"Отсутствуют даты в записи: {item}") - - checkin = timezone.make_aware( - datetime.strptime(f"{begin_date} {check_in_time}", "%Y-%m-%d %H:%M:%S") + reservation, created = await sync_to_async(Reservation.objects.update_or_create)( + reservation_id=item['id'], + defaults={ + 'room_number': item['apartment_id'], + 'room_type': item.get('notes', 'Описание отсутствует'), + 'check_in': checkin, + 'check_out': checkout, + 'status': item['status'], + 'hotel': self.hotel, + } ) - checkout = timezone.make_aware( - datetime.strptime(f"{end_date} {check_out_time}", "%Y-%m-%d %H:%M:%S") - ) - - filtered_data.append({ - 'reservation_id': reservation_id, - 'checkin': checkin, - 'checkout': checkout, - 'room_number': item.get('apartment_id'), - 'room_type': item.get('notes', 'Описание отсутствует'), - 'status': item.get('status') - }) processed_items += 1 except Exception as e: - self.logger.error(f"Ошибка обработки элемента: {e}") + self.logger.error(f"Ошибка обработки записи: {e}") errors.append(str(e)) - # Сохранение в БД - try: - await self._save_to_db(filtered_data) - except Exception as e: - self.logger.error(f"Ошибка сохранения данных в БД: {e}") - errors.append(f"Ошибка сохранения данных в БД: {str(e)}") + return {"processed_intervals": 1, "processed_items": processed_items, "errors": errors} - # Формирование отчета - report = { - "processed_intervals": 1, # Пример значения - "processed_items": processed_items, - "errors": errors - } - self.logger.debug(f"Сформированный отчет: {report}") - return report + def _generate_sign(self): + return hashlib.md5((self.public_key + self.private_key).encode("utf-8")).hexdigest() - - async def _save_to_db(self, data): - if not isinstance(data, list): - self.logger.error(f"Ожидался список записей, но получен {type(data).__name__}") - return - - for index, item in enumerate(data, start=1): + @staticmethod + def _parse_date(date_str, formats): + for fmt in formats: try: - hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config) - reservation_id = item.get('reservation_id') - if not reservation_id: - self.logger.error(f"Пропущена запись {index}: отсутствует 'id'") - continue - - existing_reservation = await sync_to_async(Reservation.objects.filter)(reservation_id=reservation_id) - existing_reservation = await sync_to_async(existing_reservation.first)() - - defaults = { - 'room_number': item['room_number'], - 'room_type': item['room_type'], - 'check_in': item['checkin'], - 'check_out': item['checkout'], - 'status': item['status'], - 'hotel': hotel - } - - if existing_reservation: - await sync_to_async(Reservation.objects.update_or_create)( - reservation_id=reservation_id, defaults=defaults - ) - self.logger.debug(f"Резервация {reservation_id} обновлена. ") - else: - await sync_to_async(Reservation.objects.create)( - reservation_id=reservation_id, **defaults - ) - self.logger.debug(f"Создана новая резервация {reservation_id}") - - except Exception as e: - self.logger.error(f"Ошибка при обработке записи {index}: {e}") + 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 m in required_methods: - if not hasattr(self, m): - raise ValueError(f"Плагин {type(self).__name__} не реализует метод {m}.") - self.logger.debug(f"Плагин {self.__class__.__name__} прошел валидацию.") + for method in required_methods: + if not hasattr(self, method): + raise ValueError(f"Плагин {type(self).__name__} не реализует метод {method}.") + self.logger.debug(f"Плагин {self.__class__.__name__} успешно прошел валидацию.") return True diff --git a/pms_integration/plugins/shelter_pms.py b/pms_integration/plugins/shelter_pms.py index 0d8e2132..a653e89e 100644 --- a/pms_integration/plugins/shelter_pms.py +++ b/pms_integration/plugins/shelter_pms.py @@ -127,11 +127,13 @@ import logging import requests +import json +import os from datetime import datetime, timedelta from asgiref.sync import sync_to_async -from pms_integration.models import PMSConfiguration from hotels.models import Hotel, Reservation from .base_plugin import BasePMSPlugin +<<<<<<< HEAD class ShelterPMSPlugin(BasePMSPlugin): """ @@ -149,6 +151,36 @@ class ShelterPMSPlugin(BasePMSPlugin): self.api_url = "https://pms.frontdesk24.ru/sheltercloudapi/Reservations/" self.access_token = "679CA9C5-9847-4151-883E-5F61181AA37E" +======= +from touchh.utils.log import CustomLogger +class ShelterPMSPlugin(BasePMSPlugin): + """ + Плагин для интеграции с PMS Shelter. + """ + + def __init__(self, hotel): + super().__init__(hotel.pms) # Передаем PMS-конфигурацию в базовый класс + self.hotel = hotel # Сохраняем объект отеля + + # Проверка PMS-конфигурации + if not self.hotel.pms: + raise ValueError(f"Отель {self.hotel.name} не имеет связанной PMS конфигурации.") + + # Инициализация параметров API + self.api_url = self.hotel.pms.url + self.token = self.hotel.pms.token + + # Настройка логгера + self.logger = CustomLogger(name="ShelterPMSPlugin", log_level="WARNING").get_logger() + handler_console = logging.StreamHandler() + handler_file = logging.FileHandler('var/log/shelter_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) +>>>>>>> PMSManager_refactor def get_default_parser_settings(self): """ @@ -156,16 +188,25 @@ class ShelterPMSPlugin(BasePMSPlugin): """ return { "field_mapping": { +<<<<<<< HEAD "check_in": "check_in", "check_out": "check_out", "room_number": "room_number", "status": "status", +======= + "check_in": "from", + "check_out": "until", + "room_number": "roomNumber", + "room_type_name": "roomTypeName", + "status": "checkInStatus", +>>>>>>> PMSManager_refactor }, "date_format": "%Y-%m-%dT%H:%M:%S", } async def _fetch_data(self): """ +<<<<<<< HEAD Получает данные бронирований с API PMS Shelter. """ try: @@ -197,6 +238,136 @@ class ShelterPMSPlugin(BasePMSPlugin): # Выполняем запрос response = requests.post(self.api_url, json=data, headers=headers) response.raise_for_status() +======= + Получает данные из PMS API и сохраняет их в базу. + """ + now = datetime.now() + start_date = (now - timedelta(days=60)).strftime('%Y-%m-%dT%H:%M:%SZ') + end_date = (now + timedelta(days=60)).strftime('%Y-%m-%dT%H:%M:%SZ') + + headers = { + "Content-Type": "application/json", + "Accept": "text/plain", + "Authorization": f"Bearer {self.token}" + } + + from_index = 0 + count_per_request = 50 + all_items = [] + + try: + while True: + data = { + "from": start_date, + "until": end_date, + "pagination": { + "from": from_index, + "count": count_per_request + } + } + + response = await sync_to_async(requests.post)(self.api_url, headers=headers, data=json.dumps(data)) + response.raise_for_status() + response_data = response.json() + + items = response_data.get("items", []) + all_items.extend(items) + + total_count = response_data.get("count", 0) + from_index += len(items) + + if from_index >= total_count: + break + + self.logger.info(f"Получено записей: {len(all_items)}") + + # Сохранение данных во временный файл + temp_dir = os.path.join("temp", "shelter") + os.makedirs(temp_dir, exist_ok=True) + temp_file = os.path.join(temp_dir, f"shelter_data_{datetime.now().strftime('%Y%m%d%H%M%S')}.json") + + with open(temp_file, 'w') as file: + json.dump(all_items, file) + + self.logger.info(f"Данные сохранены во временный файл: {temp_file}") + + return await self._process_data(all_items) + + 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 = self._parse_date(item['from'], date_formats) + checkout = self._parse_date(item['until'], date_formats) + + reservation, created = await sync_to_async(Reservation.objects.update_or_create)( + reservation_id=item['id'], + defaults={ + 'room_number': item.get('roomNumber'), + 'room_type': item.get('roomTypeName'), + 'check_in': checkin, + 'check_out': checkout, + 'status': item.get('checkInStatus'), + '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}") + 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 +>>>>>>> PMSManager_refactor # Обрабатываем ответ bookings = response.json() diff --git a/scheduler/test_module.py b/scheduler/test_module.py index 09ac24cc..e69de29b 100644 --- a/scheduler/test_module.py +++ b/scheduler/test_module.py @@ -1,6 +0,0 @@ -def test_function(): - """тестовая функция для проверки планировщика - - """ - print("Hello, World!") - return "Hello, World!" diff --git a/scheduler/tests.py b/scheduler/tests.py index 7ce503c2..fefa7a59 100644 --- a/scheduler/tests.py +++ b/scheduler/tests.py @@ -1,3 +1,9 @@ from django.test import TestCase # Create your tests here. +def test_function(): + """тестовая функция для проверки планировщика + + """ + print("Hello, World!") + return "Hello, World!" diff --git a/staticfiles/admin/custom.css b/staticfiles/admin/custom.css new file mode 100644 index 00000000..a80e23a1 --- /dev/null +++ b/staticfiles/admin/custom.css @@ -0,0 +1,18 @@ +.ml-4 { + margin-left: 1rem !important; +} + +.ml-6 { + margin-left: 1.5rem !important; +} + +.nav-sidebar ul.nav-treeview .nav-link { + padding-left: 2rem; +} +.nav-sidebar .nav-link { + display: flex; +} +.nav-sidebar .nav-link .nav-icon { + margin-top: .2rem; + margin-right: .3rem; +} diff --git a/tmp/tests.py b/tmp/tests.py new file mode 100644 index 00000000..f80ad1dc --- /dev/null +++ b/tmp/tests.py @@ -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() diff --git a/tmp_data/wpts_user_activity_log.json b/tmp_data/wpts_user_activity_log.json deleted file mode 100644 index 46358fac..00000000 --- a/tmp_data/wpts_user_activity_log.json +++ /dev/null @@ -1,1252 +0,0 @@ -[ - { - "id": "50115", - "user_id": "0", - "ip": "5.196.29.194", - "created": "2024-11-24 17:19:50", - "timestamp": 1732468790, - "date_time": "2024-11-24 17:19:50", - "referred": "", - "agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_2_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_2_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1", - "location": "Unknown", - "page_id": "3081", - "url_parameters": "utm_medium=qr&utm_content=intermark&utm_term=1702", - "page_title": "Intermark", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "https://app.touchh.ru/intermark/" - }, - { - "id": "50116", - "user_id": "0", - "ip": "94.243.186.45", - "created": "2024-11-24 17:20:41", - "timestamp": 1732468841, - "date_time": "2024-11-24 17:20:41", - "referred": "", - "agent": "Mozilla/5.0 (Linux; Android 10; HarmonyOS; LYA-AL00; HMSCore 6.14.0.322; GMSCore 24.45.33) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.196 HuaweiBrowser/15.0.6.301 Mobile Safari/537.36", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (Linux; Android 10; HarmonyOS; LYA-AL00; HMSCore 6.14.0.322; GMSCore 24.45.33) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.196 HuaweiBrowser/15.0.6.301 Mobile Safari/537.36", - "location": "Unknown", - "page_id": "3081", - "url_parameters": "utm_medium=qr&utm_content=intermark&utm_term=1521", - "page_title": "Intermark", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "https://app.touchh.ru/intermark/" - }, - { - "id": "50117", - "user_id": "0", - "ip": "94.243.186.45", - "created": "2024-11-24 17:20:45", - "timestamp": 1732468845, - "date_time": "2024-11-24 17:20:45", - "referred": "", - "agent": "Mozilla/5.0 (Linux; Android 10; HarmonyOS; LYA-AL00; HMSCore 6.14.0.322; GMSCore 24.45.33) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.196 HuaweiBrowser/15.0.6.301 Mobile Safari/537.36", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (Linux; Android 10; HarmonyOS; LYA-AL00; HMSCore 6.14.0.322; GMSCore 24.45.33) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.196 HuaweiBrowser/15.0.6.301 Mobile Safari/537.36", - "location": "Unknown", - "page_id": "3081", - "url_parameters": "utm_medium=qr&utm_content=intermark&utm_term=1521", - "page_title": "Intermark", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "https://app.touchh.ru/intermark/" - }, - { - "id": "50118", - "user_id": "0", - "ip": "123.6.49.12", - "created": "2024-11-24 17:20:56", - "timestamp": 1732468856, - "date_time": "2024-11-24 17:20:56", - "referred": "http://baidu.com/", - "agent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Mobile Safari/537.36", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Mobile Safari/537.36", - "location": "Unknown", - "page_id": "3081", - "url_parameters": "utm_medium=qr&utm_content=intermark&utm_term=1521", - "page_title": "Intermark", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "https://app.touchh.ru/intermark/" - }, - { - "id": "50119", - "user_id": "0", - "ip": "212.233.85.61", - "created": "2024-11-24 17:21:17", - "timestamp": 1732468877, - "date_time": "2024-11-24 17:21:17", - "referred": "", - "agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 YaBrowser/24.10.5.295.10 SA/3 Mobile/15E148 Safari/604.1", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 YaBrowser/24.10.5.295.10 SA/3 Mobile/15E148 Safari/604.1", - "location": "Unknown", - "page_id": "3079", - "url_parameters": "utm_medium=qr&utm_content=amberup&utm_term=211", - "page_title": "Amber Shore", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "https://app.touchh.ru/amber-shore/" - }, - { - "id": "50120", - "user_id": "0", - "ip": "212.233.85.61", - "created": "2024-11-24 17:21:20", - "timestamp": 1732468880, - "date_time": "2024-11-24 17:21:20", - "referred": "", - "agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 CriOS/128.0.6613.295 YaBrowser/24.10.5.295.10 SA/3 Mobile/15E148 Safari/604.1", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 CriOS/128.0.6613.295 YaBrowser/24.10.5.295.10 SA/3 Mobile/15E148 Safari/604.1", - "location": "Unknown", - "page_id": "0", - "url_parameters": "", - "page_title": "", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "" - }, - { - "id": "50121", - "user_id": "0", - "ip": "217.66.154.191", - "created": "2024-11-24 17:23:33", - "timestamp": 1732469013, - "date_time": "2024-11-24 17:23:33", - "referred": "", - "agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0.1 Mobile/15E148 Safari/604.1", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0.1 Mobile/15E148 Safari/604.1", - "location": "Unknown", - "page_id": "0", - "url_parameters": "utm_medium=qr-2&utm_content=letyourflat&utm_term=FUSION", - "page_title": "", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "" - }, - { - "id": "50122", - "user_id": "0", - "ip": "217.66.154.191", - "created": "2024-11-24 17:23:33", - "timestamp": 1732469013, - "date_time": "2024-11-24 17:23:33", - "referred": "https://app.touchh.ru/let-your-flat3/?utm_medium=qr-2&utm_content=letyourflat&utm_term=FUSION", - "agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0.1 Mobile/15E148 Safari/604.1", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0.1 Mobile/15E148 Safari/604.1", - "location": "Unknown", - "page_id": "0", - "url_parameters": "", - "page_title": "", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "" - }, - { - "id": "50123", - "user_id": "0", - "ip": "217.66.154.191", - "created": "2024-11-24 17:23:43", - "timestamp": 1732469023, - "date_time": "2024-11-24 17:23:43", - "referred": "", - "agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0.1 Mobile/15E148 Safari/604.1", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0.1 Mobile/15E148 Safari/604.1", - "location": "Unknown", - "page_id": "0", - "url_parameters": "utm_medium=qr-2&utm_content=letyourflat&utm_term=FUSION", - "page_title": "", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "" - }, - { - "id": "50124", - "user_id": "0", - "ip": "217.66.154.191", - "created": "2024-11-24 17:23:54", - "timestamp": 1732469034, - "date_time": "2024-11-24 17:23:54", - "referred": "", - "agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0.1 Mobile/15E148 Safari/604.1", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0.1 Mobile/15E148 Safari/604.1", - "location": "Unknown", - "page_id": "0", - "url_parameters": "utm_medium=qr-2&utm_content=letyourflat&utm_term=FUSION", - "page_title": "", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "" - }, - { - "id": "50125", - "user_id": "0", - "ip": "2.133.77.228", - "created": "2024-11-24 17:32:39", - "timestamp": 1732469559, - "date_time": "2024-11-24 17:32:39", - "referred": "", - "agent": "NetworkingExtension/8619.2.8.10.7 Network/4277.42.2 iOS/18.1", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "NetworkingExtension/8619.2.8.10.7 Network/4277.42.2 iOS/18.1", - "location": "Unknown", - "page_id": "0", - "url_parameters": "", - "page_title": "", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "" - }, - { - "id": "50126", - "user_id": "0", - "ip": "2.133.77.228", - "created": "2024-11-24 17:32:39", - "timestamp": 1732469559, - "date_time": "2024-11-24 17:32:39", - "referred": "", - "agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/601.2.4 (KHTML, like Gecko) Version/9.0.1 Safari/601.2.4 facebookexternalhit/1.1 Facebot Twitterbot/1.0", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/601.2.4 (KHTML, like Gecko) Version/9.0.1 Safari/601.2.4 facebookexternalhit/1.1 Facebot Twitterbot/1.0", - "location": "Unknown", - "page_id": "3248", - "url_parameters": "utm_medium=qr&utm_content=forton&utm_term=402", - "page_title": "??????", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "https://app.touchh.ru/forton/" - }, - { - "id": "50127", - "user_id": "0", - "ip": "2.133.77.228", - "created": "2024-11-24 17:32:40", - "timestamp": 1732469560, - "date_time": "2024-11-24 17:32:40", - "referred": "", - "agent": "NetworkingExtension/8619.2.8.10.7 Network/4277.42.2 iOS/18.1", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "NetworkingExtension/8619.2.8.10.7 Network/4277.42.2 iOS/18.1", - "location": "Unknown", - "page_id": "0", - "url_parameters": "", - "page_title": "", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "" - }, - { - "id": "50128", - "user_id": "0", - "ip": "2.133.77.228", - "created": "2024-11-24 17:32:40", - "timestamp": 1732469560, - "date_time": "2024-11-24 17:32:40", - "referred": "", - "agent": "NetworkingExtension/8619.2.8.10.7 Network/4277.42.2 iOS/18.1", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "NetworkingExtension/8619.2.8.10.7 Network/4277.42.2 iOS/18.1", - "location": "Unknown", - "page_id": "0", - "url_parameters": "", - "page_title": "", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "" - }, - { - "id": "50129", - "user_id": "0", - "ip": "213.87.133.124", - "created": "2024-11-24 17:40:05", - "timestamp": 1732470005, - "date_time": "2024-11-24 17:40:05", - "referred": "", - "agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1.1 Mobile/15E148 Safari/604.1", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1.1 Mobile/15E148 Safari/604.1", - "location": "Unknown", - "page_id": "6378", - "url_parameters": "utm_medium=qr-1&utm_content=orsha-river-club&utm_term=3", - "page_title": "Orsha river club", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "https://app.touchh.ru/orsha-river-club/" - }, - { - "id": "50130", - "user_id": "0", - "ip": "178.176.217.85", - "created": "2024-11-24 17:48:55", - "timestamp": 1732470535, - "date_time": "2024-11-24 17:48:55", - "referred": "", - "agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36", - "location": "Unknown", - "page_id": "9922", - "url_parameters": "utm_medium=qr&utm_content=oharahotel&utm_term=406", - "page_title": "????????? O’hara", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "https://app.touchh.ru/oharahotel/" - }, - { - "id": "50131", - "user_id": "0", - "ip": "66.102.9.131", - "created": "2024-11-24 17:49:03", - "timestamp": 1732470543, - "date_time": "2024-11-24 17:49:03", - "referred": "", - "agent": "Mozilla/5.0 (Linux; Android 7.0; SM-G930V Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.125 Mobile Safari/537.36 (compatible; Google-Read-Aloud; +https://support.google.com/webmasters/answer/1061943)", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (Linux; Android 7.0; SM-G930V Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.125 Mobile Safari/537.36 (compatible; Google-Read-Aloud; +https://support.google.com/webmasters/answer/1061943)", - "location": "Unknown", - "page_id": "9922", - "url_parameters": "utm_medium=qr&utm_content=oharahotel&utm_term=406", - "page_title": "????????? O’hara", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "https://app.touchh.ru/oharahotel/" - }, - { - "id": "50132", - "user_id": "0", - "ip": "66.102.9.132", - "created": "2024-11-24 17:49:05", - "timestamp": 1732470545, - "date_time": "2024-11-24 17:49:05", - "referred": "", - "agent": "Mozilla/5.0 (Linux; Android 7.0; SM-G930V Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.125 Mobile Safari/537.36 (compatible; Google-Read-Aloud; +https://support.google.com/webmasters/answer/1061943)", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (Linux; Android 7.0; SM-G930V Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.125 Mobile Safari/537.36 (compatible; Google-Read-Aloud; +https://support.google.com/webmasters/answer/1061943)", - "location": "Unknown", - "page_id": "9922", - "url_parameters": "", - "page_title": "????????? O’hara", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "https://app.touchh.ru/oharahotel/" - }, - { - "id": "50133", - "user_id": "0", - "ip": "66.102.9.130", - "created": "2024-11-24 17:49:05", - "timestamp": 1732470545, - "date_time": "2024-11-24 17:49:05", - "referred": "", - "agent": "Mozilla/5.0 (Linux; Android 7.0; SM-G930V Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.125 Mobile Safari/537.36 (compatible; Google-Read-Aloud; +https://support.google.com/webmasters/answer/1061943)", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (Linux; Android 7.0; SM-G930V Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.125 Mobile Safari/537.36 (compatible; Google-Read-Aloud; +https://support.google.com/webmasters/answer/1061943)", - "location": "Unknown", - "page_id": "9922", - "url_parameters": "", - "page_title": "????????? O’hara", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "https://app.touchh.ru/oharahotel/" - }, - { - "id": "50134", - "user_id": "0", - "ip": "83.219.139.45", - "created": "2024-11-24 17:55:13", - "timestamp": 1732470913, - "date_time": "2024-11-24 17:55:13", - "referred": "", - "agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 YaBrowser/24.10.5.295.10 SA/3 Mobile/15E148 Safari/604.1", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 YaBrowser/24.10.5.295.10 SA/3 Mobile/15E148 Safari/604.1", - "location": "Unknown", - "page_id": "3079", - "url_parameters": "utm_medium=qr&utm_content=amberup&utm_term=211", - "page_title": "Amber Shore", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "https://app.touchh.ru/amber-shore/" - }, - { - "id": "50135", - "user_id": "0", - "ip": "176.59.52.216", - "created": "2024-11-24 17:58:10", - "timestamp": 1732471090, - "date_time": "2024-11-24 17:58:10", - "referred": "", - "agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/27.0 Chrome/125.0.0.0 Mobile Safari/537.36", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/27.0 Chrome/125.0.0.0 Mobile Safari/537.36", - "location": "Unknown", - "page_id": "8164", - "url_parameters": "utm_medium=qr&utm_content=vostok&utm_term=537", - "page_title": "????????? ??????", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "https://app.touchh.ru/vostok/" - }, - { - "id": "50136", - "user_id": "0", - "ip": "176.59.57.80", - "created": "2024-11-24 18:07:05", - "timestamp": 1732471625, - "date_time": "2024-11-24 18:07:05", - "referred": "", - "agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Mobile/15E148 Safari/604.1", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Mobile/15E148 Safari/604.1", - "location": "Unknown", - "page_id": "3256", - "url_parameters": "utm_medium=qr&utm_content=intermark&utm_term=2408", - "page_title": "Intermark – 2408", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "https://app.touchh.ru/intermark-2408/" - }, - { - "id": "50137", - "user_id": "0", - "ip": "95.108.213.98", - "created": "2024-11-24 18:07:11", - "timestamp": 1732471631, - "date_time": "2024-11-24 18:07:11", - "referred": "", - "agent": "Mozilla/5.0 (compatible; YandexBot/3.0; +http://yandex.com/bots)", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (compatible; YandexBot/3.0; +http://yandex.com/bots)", - "location": "Unknown", - "page_id": "0", - "url_parameters": "", - "page_title": "", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "" - }, - { - "id": "50138", - "user_id": "0", - "ip": "213.180.203.77", - "created": "2024-11-24 18:07:14", - "timestamp": 1732471634, - "date_time": "2024-11-24 18:07:14", - "referred": "", - "agent": "Mozilla/5.0 (compatible; YandexBot/3.0; +http://yandex.com/bots)", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (compatible; YandexBot/3.0; +http://yandex.com/bots)", - "location": "Unknown", - "page_id": "6236", - "url_parameters": "", - "page_title": "Touchh ???????", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "https://app.touchh.ru/" - }, - { - "id": "50139", - "user_id": "0", - "ip": "3.139.101.130", - "created": "2024-11-24 18:07:14", - "timestamp": 1732471634, - "date_time": "2024-11-24 18:07:14", - "referred": "", - "agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Maxthon/5.0.4.3000 Chrome/47.0.2526.73 Safari/537.36", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Maxthon/5.0.4.3000 Chrome/47.0.2526.73 Safari/537.36", - "location": "Unknown", - "page_id": "6236", - "url_parameters": "", - "page_title": "Touchh ???????", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "https://app.touchh.ru/" - }, - { - "id": "50140", - "user_id": "0", - "ip": "176.15.164.13", - "created": "2024-11-24 18:16:04", - "timestamp": 1732472164, - "date_time": "2024-11-24 18:16:04", - "referred": "", - "agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1", - "location": "Unknown", - "page_id": "0", - "url_parameters": "utm_medium=qr&utm_content=postoyalyy-dvor-yam&utm_term=204", - "page_title": "", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "" - }, - { - "id": "50141", - "user_id": "0", - "ip": "176.15.164.13", - "created": "2024-11-24 18:16:08", - "timestamp": 1732472168, - "date_time": "2024-11-24 18:16:08", - "referred": "https://app.touchh.ru/postoyalyy-dvor-yam/?utm_medium=qr&utm_content=postoyalyy-dvor-yam&utm_term=204", - "agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1", - "location": "Unknown", - "page_id": "0", - "url_parameters": "", - "page_title": "", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "" - }, - { - "id": "50142", - "user_id": "0", - "ip": "176.15.164.13", - "created": "2024-11-24 18:16:15", - "timestamp": 1732472175, - "date_time": "2024-11-24 18:16:15", - "referred": "", - "agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1", - "location": "Unknown", - "page_id": "0", - "url_parameters": "utm_medium=qr&utm_content=postoyalyy-dvor-yam&utm_term=204", - "page_title": "", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "" - }, - { - "id": "50143", - "user_id": "0", - "ip": "176.15.164.13", - "created": "2024-11-24 18:16:32", - "timestamp": 1732472192, - "date_time": "2024-11-24 18:16:32", - "referred": "", - "agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1", - "location": "Unknown", - "page_id": "0", - "url_parameters": "utm_medium=qr&utm_content=postoyalyy-dvor-yam&utm_term=204", - "page_title": "", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "" - }, - { - "id": "50144", - "user_id": "0", - "ip": "176.15.164.13", - "created": "2024-11-24 18:16:35", - "timestamp": 1732472195, - "date_time": "2024-11-24 18:16:35", - "referred": "", - "agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1", - "location": "Unknown", - "page_id": "0", - "url_parameters": "utm_medium=qr&utm_content=postoyalyy-dvor-yam&utm_term=204", - "page_title": "", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "" - }, - { - "id": "50145", - "user_id": "0", - "ip": "176.15.164.13", - "created": "2024-11-24 18:16:58", - "timestamp": 1732472218, - "date_time": "2024-11-24 18:16:58", - "referred": "", - "agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1", - "location": "Unknown", - "page_id": "0", - "url_parameters": "utm_medium=qr&utm_content=postoyalyy-dvor-yam&utm_term=204", - "page_title": "", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "" - }, - { - "id": "50146", - "user_id": "0", - "ip": "195.9.124.10", - "created": "2024-11-24 18:18:13", - "timestamp": 1732472293, - "date_time": "2024-11-24 18:18:13", - "referred": "", - "agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1", - "location": "Unknown", - "page_id": "3248", - "url_parameters": "utm_medium=qr&utm_content=forton&utm_term=401", - "page_title": "??????", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "https://app.touchh.ru/forton/" - }, - { - "id": "50147", - "user_id": "0", - "ip": "195.9.124.10", - "created": "2024-11-24 18:18:15", - "timestamp": 1732472295, - "date_time": "2024-11-24 18:18:15", - "referred": "", - "agent": "MobileSafari/8614.1.25.0.31 CFNetwork/1390 Darwin/22.0.0", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "MobileSafari/8614.1.25.0.31 CFNetwork/1390 Darwin/22.0.0", - "location": "Unknown", - "page_id": "0", - "url_parameters": "", - "page_title": "", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "" - }, - { - "id": "50148", - "user_id": "0", - "ip": "195.9.124.10", - "created": "2024-11-24 18:18:16", - "timestamp": 1732472296, - "date_time": "2024-11-24 18:18:16", - "referred": "", - "agent": "MobileSafari/8614.1.25.0.31 CFNetwork/1390 Darwin/22.0.0", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "MobileSafari/8614.1.25.0.31 CFNetwork/1390 Darwin/22.0.0", - "location": "Unknown", - "page_id": "0", - "url_parameters": "", - "page_title": "", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "" - }, - { - "id": "50149", - "user_id": "0", - "ip": "5.167.108.226", - "created": "2024-11-24 18:20:49", - "timestamp": 1732472449, - "date_time": "2024-11-24 18:20:49", - "referred": "https://amo.si/", - "agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1", - "location": "Unknown", - "page_id": "6246", - "url_parameters": "", - "page_title": "Orsha river club welcome", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "https://app.touchh.ru/orsha-river-club-welcome/" - }, - { - "id": "50150", - "user_id": "0", - "ip": "178.66.158.176", - "created": "2024-11-24 18:23:57", - "timestamp": 1732472637, - "date_time": "2024-11-24 18:23:57", - "referred": "", - "agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Mobile/15E148 Safari/604.1", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Mobile/15E148 Safari/604.1", - "location": "Unknown", - "page_id": "0", - "url_parameters": "utm_medium=qr-7&utm_content=letyourflat&utm_term=BENUA", - "page_title": "", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "" - }, - { - "id": "50151", - "user_id": "0", - "ip": "178.66.158.176", - "created": "2024-11-24 18:24:00", - "timestamp": 1732472640, - "date_time": "2024-11-24 18:24:00", - "referred": "https://app.touchh.ru/let-your-flat3/?utm_medium=qr-7&utm_content=letyourflat&utm_term=BENUA", - "agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Mobile/15E148 Safari/604.1", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Mobile/15E148 Safari/604.1", - "location": "Unknown", - "page_id": "0", - "url_parameters": "", - "page_title": "", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "" - }, - { - "id": "50152", - "user_id": "0", - "ip": "2a00:1fa0:153:756a:6d4a:5c71:d3c8:2a88", - "created": "2024-11-24 18:39:02", - "timestamp": 1732473542, - "date_time": "2024-11-24 18:39:02", - "referred": "", - "agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/27.1 Chrome/125.0.0.0 Mobile Safari/537.36", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/27.1 Chrome/125.0.0.0 Mobile Safari/537.36", - "location": "Unknown", - "page_id": "3081", - "url_parameters": "utm_medium=qr&utm_content=intermark&utm_term=1703", - "page_title": "Intermark", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "https://app.touchh.ru/intermark/" - }, - { - "id": "50153", - "user_id": "0", - "ip": "2a03:d000:5090:a060:24f3:30ff:fe53:84f3", - "created": "2024-11-24 18:40:50", - "timestamp": 1732473650, - "date_time": "2024-11-24 18:40:50", - "referred": "", - "agent": "Mozilla/5.0 (Linux; Android 14; 2203129G) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/123.0.6312.118 Mobile Safari/537.36", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (Linux; Android 14; 2203129G) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/123.0.6312.118 Mobile Safari/537.36", - "location": "Unknown", - "page_id": "11274", - "url_parameters": "utm_medium=qr-1&utm_content=daynight74&utm_term=%D0%AD%D0%BD%D0%B3%D0%B5%D0%BB%D1%8C%D1%81%D0%B0%2C47-%D0%91", - "page_title": "??????????? ???? ? ???? [1] Welcome", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "https://app.touchh.ru/daynight74-welcome/" - }, - { - "id": "50154", - "user_id": "0", - "ip": "223.104.253.31", - "created": "2024-11-24 18:40:52", - "timestamp": 1732473652, - "date_time": "2024-11-24 18:40:52", - "referred": "", - "agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Mobile/15E148 Safari/604.1", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Mobile/15E148 Safari/604.1", - "location": "Unknown", - "page_id": "3081", - "url_parameters": "utm_medium=qr&utm_content=intermark&utm_term=1703", - "page_title": "Intermark", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "https://app.touchh.ru/intermark/" - }, - { - "id": "50155", - "user_id": "0", - "ip": "2a03:d000:5090:a060:24f3:30ff:fe53:84f3", - "created": "2024-11-24 18:40:56", - "timestamp": 1732473656, - "date_time": "2024-11-24 18:40:56", - "referred": "", - "agent": "Mozilla/5.0 (Linux; Android 14; 2203129G) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/123.0.6312.118 Mobile Safari/537.36", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (Linux; Android 14; 2203129G) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/123.0.6312.118 Mobile Safari/537.36", - "location": "Unknown", - "page_id": "11274", - "url_parameters": "utm_medium=qr-1&utm_content=daynight74&utm_term=%D0%AD%D0%BD%D0%B3%D0%B5%D0%BB%D1%8C%D1%81%D0%B0%2C47-%D0%91", - "page_title": "??????????? ???? ? ???? [1] Welcome", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "https://app.touchh.ru/daynight74-welcome/" - }, - { - "id": "50156", - "user_id": "0", - "ip": "176.59.43.123", - "created": "2024-11-24 18:41:34", - "timestamp": 1732473694, - "date_time": "2024-11-24 18:41:34", - "referred": "", - "agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/27.0 Chrome/125.0.0.0 Mobile Safari/537.36", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/27.0 Chrome/125.0.0.0 Mobile Safari/537.36", - "location": "Unknown", - "page_id": "8164", - "url_parameters": "utm_medium=qr&utm_content=vostok&utm_term=%D0%A5%D0%BE%D0%BB%D0%BB", - "page_title": "????????? ??????", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "https://app.touchh.ru/vostok/" - }, - { - "id": "50157", - "user_id": "0", - "ip": "176.59.43.123", - "created": "2024-11-24 18:41:34", - "timestamp": 1732473694, - "date_time": "2024-11-24 18:41:34", - "referred": "", - "agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/27.0 Chrome/125.0.0.0 Mobile Safari/537.36", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/27.0 Chrome/125.0.0.0 Mobile Safari/537.36", - "location": "Unknown", - "page_id": "8164", - "url_parameters": "utm_medium=qr&utm_content=vostok&utm_term=%D0%A5%D0%BE%D0%BB%D0%BB", - "page_title": "????????? ??????", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "https://app.touchh.ru/vostok/" - }, - { - "id": "50158", - "user_id": "0", - "ip": "89.113.159.132", - "created": "2024-11-24 18:48:24", - "timestamp": 1732474104, - "date_time": "2024-11-24 18:48:24", - "referred": "", - "agent": "QR Scanner Android", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "QR Scanner Android", - "location": "Unknown", - "page_id": "3248", - "url_parameters": "utm_medium=qr&utm_content=forton&utm_term=503", - "page_title": "??????", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "https://app.touchh.ru/forton/" - }, - { - "id": "50159", - "user_id": "0", - "ip": "89.113.159.132", - "created": "2024-11-24 18:48:37", - "timestamp": 1732474117, - "date_time": "2024-11-24 18:48:37", - "referred": "", - "agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36", - "location": "Unknown", - "page_id": "3248", - "url_parameters": "utm_medium=qr&utm_content=forton&utm_term=503", - "page_title": "??????", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "https://app.touchh.ru/forton/" - }, - { - "id": "50160", - "user_id": "0", - "ip": "95.24.145.9", - "created": "2024-11-24 18:49:16", - "timestamp": 1732474156, - "date_time": "2024-11-24 18:49:16", - "referred": "", - "agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36", - "location": "Unknown", - "page_id": "8164", - "url_parameters": "utm_medium=qr&utm_content=vostok&utm_term=614", - "page_title": "????????? ??????", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "https://app.touchh.ru/vostok/" - }, - { - "id": "50161", - "user_id": "0", - "ip": "66.102.9.130", - "created": "2024-11-24 18:49:22", - "timestamp": 1732474162, - "date_time": "2024-11-24 18:49:22", - "referred": "", - "agent": "Mozilla/5.0 (Linux; Android 7.0; SM-G930V Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.125 Mobile Safari/537.36 (compatible; Google-Read-Aloud; +https://support.google.com/webmasters/answer/1061943)", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (Linux; Android 7.0; SM-G930V Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.125 Mobile Safari/537.36 (compatible; Google-Read-Aloud; +https://support.google.com/webmasters/answer/1061943)", - "location": "Unknown", - "page_id": "8164", - "url_parameters": "utm_medium=qr&utm_content=vostok&utm_term=614", - "page_title": "????????? ??????", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "https://app.touchh.ru/vostok/" - }, - { - "id": "50162", - "user_id": "0", - "ip": "66.102.9.131", - "created": "2024-11-24 18:49:23", - "timestamp": 1732474163, - "date_time": "2024-11-24 18:49:23", - "referred": "", - "agent": "Mozilla/5.0 (Linux; Android 7.0; SM-G930V Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.125 Mobile Safari/537.36 (compatible; Google-Read-Aloud; +https://support.google.com/webmasters/answer/1061943)", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (Linux; Android 7.0; SM-G930V Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.125 Mobile Safari/537.36 (compatible; Google-Read-Aloud; +https://support.google.com/webmasters/answer/1061943)", - "location": "Unknown", - "page_id": "8164", - "url_parameters": "", - "page_title": "????????? ??????", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "https://app.touchh.ru/vostok/" - }, - { - "id": "50163", - "user_id": "0", - "ip": "66.102.9.132", - "created": "2024-11-24 18:49:23", - "timestamp": 1732474163, - "date_time": "2024-11-24 18:49:23", - "referred": "", - "agent": "Mozilla/5.0 (Linux; Android 7.0; SM-G930V Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.125 Mobile Safari/537.36 (compatible; Google-Read-Aloud; +https://support.google.com/webmasters/answer/1061943)", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (Linux; Android 7.0; SM-G930V Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.125 Mobile Safari/537.36 (compatible; Google-Read-Aloud; +https://support.google.com/webmasters/answer/1061943)", - "location": "Unknown", - "page_id": "8164", - "url_parameters": "", - "page_title": "????????? ??????", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "https://app.touchh.ru/vostok/" - }, - { - "id": "50164", - "user_id": "0", - "ip": "89.113.159.132", - "created": "2024-11-24 18:58:55", - "timestamp": 1732474735, - "date_time": "2024-11-24 18:58:55", - "referred": "", - "agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36", - "platform": "Unknown", - "version": "Unknown", - "model": "Unknown", - "device": "Unknown", - "UAString": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36", - "location": "Unknown", - "page_id": "3248", - "url_parameters": "utm_medium=qr&utm_content=forton&utm_term=503", - "page_title": "??????", - "type": "page_view", - "last_counter": 1, - "hits": 1, - "honeypot": 0, - "reply": 0, - "page_url": "https://app.touchh.ru/forton/" - } -] \ No newline at end of file