Статья:

ОБЕСПЕЧЕНИЕ СОГЛАСОВАННОСТИ БРОНИРОВАНИЙ В P2P-СИСТЕМЕ АРЕНДЫ СПОРТИВНОГО ИНВЕНТАРЯ: КЛИЕНТ-СЕРВЕРНАЯ РЕАЛИЗАЦИЯ

Конференция: CCCLIV Студенческая международная научно-практическая конференция «Молодежный научный форум»

Секция: Технические науки

Выходные данные
Зайцев А.С. ОБЕСПЕЧЕНИЕ СОГЛАСОВАННОСТИ БРОНИРОВАНИЙ В P2P-СИСТЕМЕ АРЕНДЫ СПОРТИВНОГО ИНВЕНТАРЯ: КЛИЕНТ-СЕРВЕРНАЯ РЕАЛИЗАЦИЯ // Молодежный научный форум: электр. сб. ст. по мат. CCCLIV междунар. студ. науч.-практ. конф. № 28(354). URL: https://nauchforum.ru/archive/MNF_interdisciplinarity/28(354).pdf (дата обращения: 16.08.2026)
Лауреаты определены. Конференция завершена
Эта статья набрала 2 голоса
Мне нравится
Дипломы
лауреатов
Сертификаты
участников
Дипломы
лауреатов
Сертификаты
участников
на печатьскачать .pdfподелиться

ОБЕСПЕЧЕНИЕ СОГЛАСОВАННОСТИ БРОНИРОВАНИЙ В P2P-СИСТЕМЕ АРЕНДЫ СПОРТИВНОГО ИНВЕНТАРЯ: КЛИЕНТ-СЕРВЕРНАЯ РЕАЛИЗАЦИЯ

Зайцев Алексей Сергеевич
студент, Федеральное государственное автономное образовательное учреждение высшего образования «Московский государственный технологический университет „СТАНКИН“», РФ, г. Москва

 

ВВЕДЕНИЕ

В клиент-серверной системе аренды бронирование связывает арендатора, позицию каталога, конкретный физический экземпляр, временной интервал и стоимость. Для P2P-платформы корректность этой операции критична: один и тот же объект одновременно просматривается несколькими пользователями, а информация на экране Android-приложения может устареть между загрузкой карточки и отправкой запроса. Поэтому мобильный клиент выполняет только предварительную проверку, тогда как окончательное решение о доступности принимает сервер.

Цель статьи заключается в технической конкретизации механизма согласованного бронирования для Android-клиента на Kotlin и серверной части на Spring Boot. Рассматриваются структура REST-запроса, транзакционная обработка, выбор свободного экземпляра в PostgreSQL, защита от пересечения интервалов и преобразование серверного конфликта в состояние пользовательского интерфейса. Временной период задаётся полуинтервалом [s, e): начало включается, окончание не включается. Для нового периода [sₙ, eₙ) и существующего [sₐ, eₐ) пересечение существует при sₙ < eₐ и eₙ > sₐ.

АРХИТЕКТУРА ОПЕРАЦИИ БРОНИРОВАНИЯ

На Android последовательность компонентов имеет вид Jetpack Compose → BookingViewModel → CreateBookingUseCase → BookingRepository → Retrofit. Экран передаёт во ViewModel выбранные дату и время; ViewModel проверяет, что начало предшествует окончанию, переводит интерфейс в состояние загрузки и вызывает доменный сценарий. Репозиторий формирует POST-запрос к ресурсу /api/bookings, а OkHttp-интерцептор добавляет заголовок Authorization: Bearer <token>. Идентификатор пользователя в теле запроса не передаётся: сервер получает его из проверенного JSON Web Token, что исключает подмену арендатора.

Запрос содержит идентификатор позиции инвентаря, начало и окончание аренды и requestId — UUID, создаваемый один раз при нажатии кнопки. Повторная отправка того же requestId после сетевого сбоя не должна создавать вторую запись. На сервере обработка проходит через BookingController, BookingService и репозитории Spring Data JPA. Контроллер выполняет валидацию формата и возвращает HTTP-ответ, сервис содержит бизнес-правила, а репозитории работают с сущностями inventory_items, inventory_units и bookings. Разделение карточки позиции и экземпляра позволяет серверу назначить один из нескольких одинаковых объектов, реально свободный в заданный период.

Интерфейс Retrofit объявляет suspend-метод @POST("api/bookings") с телом CreateBookingRequest и заголовком Idempotency-Key. Ответ обрабатывается как Response<BookingResponse>, чтобы репозиторий мог различать 201, 400, 401 и 409 без передачи HTTP-деталей в пользовательский интерфейс.

РЕАЛИЗАЦИЯ НА СЕРВЕРНОЙ СТОРОНЕ

Метод BookingService.createBooking помечается @Transactional. Внутри одной транзакции выполняются: проверка startTime < endTime; получение активной позиции каталога и её цены; выбор свободного экземпляра; расчёт стоимости на сервере; сохранение Booking со статусом CREATED. Цена из Android-запроса не принимается, поскольку клиентские данные не являются доверенными. Для дат используется единый формат ISO 8601, а на уровне PostgreSQL — тип timestamp with time zone, что исключает неоднозначность при сравнении периодов.

Листинг 1. Транзакционный метод сервисного слоя

@Transactional

fun createBooking(userId: Long, r: CreateBookingRequest): BookingDto {

  validateInterval(r)

  bookingRepo.findByRequest(userId, r.requestId)?.let { return it.toDto() }

  val unitId = unitRepo.lockFreeUnit(r) ?: throw BookingConflictException()

  val booking = Booking(userId, unitId, r, priceService.calculate(r), CREATED)

  return bookingRepo.save(booking).toDto()

}

Свободный экземпляр выбирается нативным запросом с блокировкой строк. Условие NOT EXISTS исключает единицы, для которых имеется бронирование в блокирующем статусе CREATED, PAID или ISSUED и выполняется условие пересечения. Конструкция FOR UPDATE SKIP LOCKED не позволяет параллельной транзакции выбрать уже захваченный экземпляр; ORDER BY задаёт одинаковый порядок блокировок и снижает вероятность взаимной блокировки. Если строка не найдена, сервис возбуждает BookingConflictException.

Листинг 2. Выбор и блокировка свободного экземпляра

SELECT iu.id FROM inventory_units iu

WHERE iu.item_id = :itemId AND iu.status = 'AVAILABLE'

  AND NOT EXISTS (SELECT 1 FROM bookings b

      WHERE b.inventory_unit_id = iu.id

        AND b.status IN ('CREATED','PAID','ISSUED')

        AND b.start_time < :endTime AND b.end_time > :startTime)

ORDER BY iu.id FOR UPDATE SKIP LOCKED LIMIT 1;

При успешном сохранении контроллер возвращает 201 Created и объект {bookingId, status, unitId, totalPrice}. Исключение об отсутствии экземпляра преобразуется классом @RestControllerAdvice в 409 Conflict с кодом BOOKING_INTERVAL_CONFLICT. Нарушение уникальности requestId обрабатывается как повтор запроса: сервер находит ранее созданную операцию по паре renter_id и request_id и возвращает её результат. Это обеспечивает идемпотентность при тайм-ауте, когда ответ потерян, но транзакция уже зафиксирована.

Прикладная проверка дополняется инвариантом базы данных. В миграции PostgreSQL создаётся GiST-ограничение, запрещающее пересечение диапазонов для одной inventory_unit_id. Оно является последним уровнем защиты: даже при ошибке в сервисном коде противоречивые записи не фиксируются. Возникшее DataIntegrityViolationException также преобразуется в 409 Conflict [3–5].

Листинг 3. Ограничение пересекающихся периодов в PostgreSQL

CREATE EXTENSION IF NOT EXISTS btree_gist;

ALTER TABLE bookings ADD CONSTRAINT bookings_no_overlap

EXCLUDE USING gist (inventory_unit_id WITH =,
  tstzrange(start_time, end_time, '[)') WITH &&)

WHERE (status IN ('CREATED', 'PAID', 'ISSUED'));

ОБРАБОТКА РЕЗУЛЬТАТА В ANDROID-ПРИЛОЖЕНИИ

BookingViewModel хранит экранное состояние в StateFlow. Используются состояния Editing, Loading, Success, Conflict и Error. Jetpack Compose подписывается на поток через collectAsStateWithLifecycle; при Loading кнопка бронирования блокируется и отображается индикатор. Запрос запускается в viewModelScope, поэтому сетевая операция не блокирует главный поток, а состояние сохраняется при пересоздании Activity [1], [2].

BookingRepositoryImpl вызывает Retrofit API и преобразует ответ в доменный результат. Код 201 формирует BookingResult.Success, 409 — BookingResult.Conflict, 401 — Unauthorized, остальные ошибки — Failure. Благодаря этому ViewModel не зависит от Retrofit и может тестироваться с fake-репозиторием.

Листинг 4. Изменение состояния экрана во ViewModel

fun submit(draft: BookingDraft) = viewModelScope.launch {

  uiState.value = Loading(draft)

  when (val result = createBookingUseCase(draft)) {

    is Success -> uiState.value = Success(result.booking)

    is Conflict -> { availabilityRepository.refresh(draft.inventoryId);

                    uiState.value = Conflict(draft) }

    is Unauthorized -> events.emit(OpenLogin)

    is Failure -> uiState.value = Error(draft)

  }

}

В состоянии Conflict приложение показывает сообщение «Интервал уже занят», повторно загружает доступность и сохраняет выбранные параметры, чтобы пользователь изменил только время. При 401 выполняется обновление токена либо переход к авторизации; при сетевой ошибке запрос повторяется с тем же requestId. Локальный кэш Room используется для каталога и истории, но не является источником истины при подтверждении доступности.

ПРОВЕРКА КОНКУРЕНТНЫХ СЦЕНАРИЕВ

Проверка включает граничные интервалы и параллельные обращения. Периоды 10:00–12:00 и 12:00–14:00 допустимы, поскольку общая граница не образует пересечения; запросы 10:00–12:00 и 11:00–13:00 конфликтуют. В интеграционном сценарии два потока одновременно отправляют одинаковые POST-запросы для позиции с одним экземпляром. После снятия синхронизирующего барьера один запрос фиксирует Booking и получает 201, второй не находит свободную строку либо нарушает исключающее ограничение и получает 409. Проверка базы данных подтверждает наличие одной активной записи. Для корзины блокировки выполняются по возрастанию идентификаторов, а отсутствие хотя бы одной единицы приводит к откату всего заказа.

ЗАКЛЮЧЕНИЕ

Согласованность бронирований обеспечивается тремя уровнями: Android-клиент валидирует ввод и отображает конфликт; Spring Boot выполняет авторизацию, выбор экземпляра, расчёт стоимости и сохранение в одной транзакции; PostgreSQL блокирует конкурентный выбор и запрещает пересечение диапазонов. Решение позволяет предсказуемо обрабатывать одновременные запросы и не зависит от состояния мобильного интерфейса.

 

Список литературы:
1. Гриффитс, Д. Head First. Программирование для Android на Kotlin / Д. Гриффитс, Д. Гриффитс. – Санкт-Петербург : Питер, 2025. – 912 с. – Текст : непосредственный.
2. Мартин, Р. С. Чистая архитектура / Р. С. Мартин. – Санкт-Петербург : Питер, 2018. – 352 с. – Текст : непосредственный.
3. Уоллс, К. Spring в действии / К. Уоллс. – Москва : ДМК Пресс, 2022. – 544 с. – Текст : непосредственный.
4. PostgreSQL : диапазонные типы и ограничения пересечения. – Текст : электронный. – URL: https://www.postgresql.org/docs/current/rangetypes.html (дата обращения: 26.07.2026).
5. Spring Data JPA : блокировки при выполнении запросов. – Текст : электронный. – URL: https://docs.spring.io/spring-data/jpa/reference/jpa/locking.html (дата обращения: 26.07.2026).