Skip to content

Use Case #UC-027.2: Расчет физического износа и Горячая замена ⚖️ (Движок физики износа)

1. Жизненная ситуация (Боль пользователя)

Представь: Велосипедист совершил отличную тренировку в виртуальном мире Zwift (VirtualRide) на станке. Его велосипед закреплен в трейлере: колеса не крутятся по асфальту, тормоза не используются, передняя вилка не работает. Однако цепь и кассета изнашиваются так же интенсивно, как и на улице. Или другой сценарий: райдер едет на мощном электровелосипеде (E-Bike). Электромотор передает колоссальный крутящий момент на цепь, из-за чего трансмиссия изнашивается в 1.5 раза быстрее обычной.

Как он мучается сейчас: Существующие трекеры (Strava, Garmin) просто прибавляют километраж ко всему велосипеду без разбора. После домашней тренировки на станке у пользователя "изнашиваются" тормоза и передняя покрышка, которой даже не было на колесе! А пробег цепи на электробайке считается обычным, из-за чего она неожиданно рвется посреди леса.

Мечта юзера: Приложение должно автоматически разделять износ. Виртуальная поездка? Изнашивается только трансмиссия. Поездка на электробайке? Пробег цепи умножается на коэффициент нагрузки. А сразу после завершения поездки приложение предлагает удобное окно подтверждения (Checkout), где можно в один клик "провести замену" растянувшейся цепи на новую.


2. Как это ДОЛЖНО работать у тебя (UX-Магия)

Интерфейс импорта тренировок становится интерактивным чек-листом.

Сценарий (Что видит юзер):

  1. Пользователь заходит в раздел Входящие (Inbox) и видит новую поездку из Strava.
  2. Он нажимает кнопку [ Подтвердить ]. Вместо мгновенного сохранения открывается красивое модальное окно Post-Ride Checkout.
  3. В окне перечислены все установленные на выбранный байк детали. Напротив каждой стоит галочка [✓].
  4. Умное авто-заполнение:
    • Если поездка помечена в Strava как VirtualRide (Zwift / Rouvy), приложение автоматически снимает галочки со всех деталей, кроме трансмиссии (DRIVETRAIN).
  5. Горячая замена (Hot Swapping):
    • Напротив любой детали есть кнопка 🔄 [ Заменить ].
    • Если пользователь заменил цепь сразу после поездки, он нажимает эту кнопку, выбирает из выпадающего списка совместимую новую цепь со склада, и система автоматически фиксирует замену именно в момент этой тренировки!
  6. Пользователь нажимает [ Завершить поездку ]. Тренировка подтверждается, а ресурс деталей обновляется с ювелирной точностью.

3. СЕКРЕТ ДЛЯ ПРОГРАММИСТА (Физический движок и транзакции)

Расчет износа производится на сервере в транзакции подтверждения активности POST /api/strava/activities/:id/confirm:

  1. Формат Payload:

    typescript
    {
      localBikeId: string,
      customDistanceKm?: number,
      wearComponentIds: string[], // компоненты, на которые пойдет износ
      swaps?: { unmountId: string, mountId: string }[] // горячие замены
    }
  2. Алгоритм Физического Движка:

    • Шаг 1: Горячие замены. Сначала обрабатывается массив swaps. У старого компонента unmountId статус сбрасывается в 'INVENTORY', а bikeId зануляется. Новый компонент mountId получает статус 'MOUNTED' и привязывается к localBikeId. Это происходит ДО начисления пробега, чтобы износ пошел уже на свежую деталь!
    • Шаг 2: Расчет дистанции и времени.
      • baseKm = customDistanceKm ?? (activity.distance / 1000)
      • durationHours = activity.moving_time / 3600
    • Шаг 3: Начисление износа по правилам физики.
      • Правило Zwift (VirtualRide): Если тип активности 'VirtualRide' и категория компонента НЕ трансмиссия ('DRIVETRAIN'), деталь получает ровно 0 износа по километрам и часам.
      • Правило E-Bike (EBikeRide): Если тип активности 'EBikeRide' и категория компонента равна 'DRIVETRAIN', износ по километрам умножается на коэффициент нагрузки: deltaKm = baseKm * 1.5. В остальных случаях deltaKm = baseKm.
    • Шаг 4: Обновление счетчиков. Добавляем полученные дельты к currentLifespanKm, currentServiceKm, currentLifespanHours, currentServiceHours выбранных компонентов.
    • Шаг 5: Обновление шасси. Увеличиваем общий пробег рамы (bike.totalKm) на baseKm. Помечаем активность как 'CONFIRMED'. Все операции выполняются строго внутри одной базы данных SQL-транзакции.

4. Защита от "дурака" (Бизнес-политики безопасности)

  • Защита транзакции: Если любой шаг горячей замены или начисления износа падает с ошибкой, вся транзакция откатывается (rollback), гарантируя отсутствие "фантомного" пробега и рассинхронизации.
  • Фильтрация деталей: В модальном окне выбора замены отображаются только компоненты со статусом 'INVENTORY', которые явным образом совместимы с данным велосипедом (их ID содержится в массиве compatibleBikeIds компонента).

Эпики и Задачи (Epics & Tasks)

Epic 1: Серверный Физический Движок (apps/api)

  • Task 1.1: Расширить схему валидации эндпоинта подтверждения активности Strava.
  • Task 1.2: Реализовать обработку горячих замен компонентов внутри транзакции.
  • Task 1.3: Реализовать физические правила Zwift (0x для рамы/тормозов) и E-Bike (1.5x для цепи).
  • Task 1.4: Добавить обновление пробега шасси велосипеда и статуса активности.

Epic 2: Пользовательский интерфейс Checkout (apps/web)

  • Task 2.1: Разработать модальное окно PostRideCheckoutModal с отображением списка установленных деталей.
  • Task 2.2: Добавить авто-исключение не-трансмиссионных деталей при виртуальных тренировках.
  • Task 2.3: Сверстать селектор горячей замены деталей со склада.

Epic 3: Автоматическое тестирование

  • Task 3.1: Написать интеграционные тесты для API, эмулируя VirtualRide и EBikeRide, и сравнивая начисленный пробег.
  • Task 3.2: Протестировать корректность сборки payload в модальном окне фронтенда при отправке на подтверждение.