Проекты
Проект

Sungulairs

Гранитные Ступени15 декабря 2021 г.0 просмотров

Singularis: мой первый большой pet-проект про маршруты, карты и путешествия

Singularis начался как дипломный проект на курсах Python Developer, но довольно быстро перерос во что-то намного большее. Изначально я хотел сделать сервис, который добавляет немного интерактива в путешествия и берёт на себя самую скучную часть подготовки: построение маршрута из точки А в точку Б, поиск вариантов перелёта, пересадок, ближайших аэропортов и отображение всего этого на карте.Наверное, идея появилась у меня ещё до того, как я нормально познакомился с web-разработкой. Мне всегда были интересны карты, координаты, слои, маршруты, география и вся эта магия, когда абстрактные данные превращаются в понятную визуальную историю на карте.

Поэтому в качестве дипломного проекта я решил сделать не очередной учебный сервис, а приложение, которым потенциально можно было бы пользоваться самому.Первая версия была довольно простой: пользователь вводил точку отправления и точку назначения, приложение геокодировало эти данные, находило ближайшие аэропорты, строило маршрут и отображало его на карте. Для работы с геоданными я использовал GeoDjango, PostGIS, OSM, Folium и разные внешние API. Тогда я ещё не до конца понимал, насколько много подводных камней скрывается в логистике, авиамаршрутах и геокодировании, но именно поэтому проект оказался таким полезным для моего роста.

Почти сразу стало понятно, что одной только карты недостаточно. Если пользователь вводит не конкретный город, улицу или точку, а просто название страны, геокодер может вернуть центр государства. Во время защиты проекта это привело к забавному багу: маршрут был построен не в город, а в условную скалу где-то в центре страны. После этого я добавил дополнительную проверку: приложение стало определять, не совпадает ли пользовательский ввод с названием страны, и если совпадает — подставлять столицу вместо географического центра. Для этого пришлось использовать reverse geocoding и дополнительное API с информацией по странам.
Постепенно проект начал обрастать реальной логикой. Я подключил Amadeus API для получения актуальных офферов по авиабилетам, начал работать с IATA-кодами аэропортов, расписаниями, ценами, пересадками и разными условиями поиска. Приложение научилось получать список вариантов перелёта, отображать маршруты на карте и показывать пользователю не просто одну линию, а связку из наземного и воздушного маршрута.
Со временем появилась разница между анонимными и зарегистрированными пользователями. Анонимный пользователь мог быстро построить маршрут и получить один базовый результат, а зарегистрированный — больше вариантов, историю запросов, расширенные настройки поиска и доступ к дополнительным данным. Я добавил регистрацию, авторизацию, подтверждение почты, восстановление пароля и постепенно начал думать о безопасности, пользовательских данных и нормальной структуре аккаунта.
Отдельным этапом стала работа с формами расширенного поиска. Для зарегистрированных пользователей появились дополнительные параметры: дата вылета, количество взрослых, детей и младенцев, максимальная стоимость билета, прямой рейс, максимальное количество результатов, класс билета и валюта. По сути, это превратило простой поиск маршрута в более гибкий инструмент, где можно получить разные варианты поездки под конкретные условия.
Чем больше данных я получал от Amadeus, тем очевиднее становилось, что хранить всё в одном JSON-поле PostgreSQL — не лучшая идея. В ответе API приходил огромный вложенный JSON, и только один билет мог содержать достаточно сложную структуру: сегменты маршрута, аэропорты, пересадки, цены, валюты, время вылета и прилёта, перевозчиков и другие данные. В среднем по одному запросу могло приходить около восьмидесяти офферов, и обрабатывать это как монструозный массив становилось всё тяжелее.
Тогда я пересмотрел модель данных и начал раскладывать ответ API по нормализованным таблицам. Появилась родительская модель оффера и связанные с ней сущности: маршруты, сегменты, аэропорты, цены и другие части билета. Для этого пришлось переписать значительную часть кода, который отвечал за построение маршрута, обработку ответа API и сохранение данных. Также пришлось написать слой сериализации/десериализации, который превращал внешний JSON в понятную внутреннюю структуру приложения.

Это сильно изменило подход к проекту. Данные стали проще доставать, фильтровать, переиспользовать и расширять. Если бы в будущем Amadeus пришлось заменить на другой сервис, приложение уже не зависело бы так сильно от конкретной структуры одного API. Достаточно было бы написать новый адаптер, который приведёт внешний ответ к нужным внутренним моделям. Дальше начались более интересные баги. Например, проблема, которую я для себя называл "Осло — Дуранго". Пользователь выбирал две точки на карте, приложение находило ближайшие аэропорты, отправляло запрос в Amadeus по конкретным IATA-кодам, а в ответе иногда прилетали офферы с другими аэропортами. На первый взгляд это выглядело странно, потому что запрос уходил по одним аэропортам, а результат возвращался уже с другими. Пришлось изменить алгоритм: собирать все IATA-коды из офферов, удалять дубликаты, дополнительно сохранять эти аэропорты и перестраивать наземные участки маршрута уже под каждый конкретный билет. На практике это дало больше гибкости. Если рядом с точкой пользователя есть несколько аэропортов на примерно одинаковом расстоянии, приложение может учитывать разные варианты и строить более реалистичные маршруты. Но, конечно, за это пришлось заплатить дополнительной сложностью в коде и небольшим увеличением времени обработки.

Была и другая проблема — закрытые аэропорты. Amadeus иногда возвращал билеты в аэропорты, которые уже не работают. Например, при построении маршрута Рим — Берлин часть результатов могла вести в Берлин-Тегель, который закрыт с 2020 года. Такую проблему было бы трудно заметить, если бы приложение полностью доверяло только внешнему API. Но у меня уже была собственная база аэропортов мира, где хранились статусы аэропортов, поэтому такие результаты можно было отфильтровывать. Это был хороший урок: даже крупные внешние сервисы могут возвращать данные, которые нужно проверять на своей стороне.

Позже я добавил систему популярных направлений. Идея была в том, чтобы не заставлять пользователя каждый раз ждать тяжёлый процесс геокодирования, поиска аэропортов и загрузки офферов. Для выбранных популярных маршрутов сервер по расписанию заранее загружал актуальные результаты на текущие сутки и сохранял их в базе. Пользователь нажимал на карточку направления и почти мгновенно получал готовые данные. За фоновые процессы отвечали Celery, Celery Beat и Redis.

Также появилась автоматическая очистка просроченных билетов. Каждые полчаса фоновая задача сравнивала текущее время с временем вылета и удаляла устаревшие офферы, чтобы пользователь не видел неактуальную информацию. На этом этапе я начал лучше понимать, насколько важны фоновые задачи, расписания, актуальность данных и аккуратная работа со временем.

Отдельным большим блоком стала работа с часовыми поясами. Все даты и время внутри приложения хранились в UTC, но пользователю не всегда удобно видеть время в таком формате. Поэтому я добавил базу часовых поясов, возможность выбирать свою таймзону, автоматическое определение геоданных по IP и отображение времени в пользовательском часовом поясе. Например, если билет хранится в UTC, пользователь из Москвы должен видеть своё локальное время, а пользователь из другой страны — своё. Это оказалось важно не только для интерфейса, но и для корректной очистки просроченных билетов.

Параллельно я развивал личный кабинет. Зарегистрированный пользователь мог изменять имя, фамилию, почту, пароль, геоданные и часовой пояс. При изменении важных данных отправлялось письмо для подтверждения. Также была добавлена возможность удалить аккаунт, после чего связанные данные каскадно удалялись из системы. Здесь я впервые начал думать не только о функционале, но и о пользовательских данных, согласии на их обработку и будущих политиках хранения информации.

Фронтенд в этом проекте тоже стал для меня отдельным приключением. Я очень хотел, чтобы работа с результатами происходила без постоянных перезагрузок страницы, поэтому начал активно использовать JavaScript и jQuery. При клике на конкретный билет подгружалась нужная карта, обновлялись данные маршрута, появлялись детали поездки. Я сделал кастомный ползунок, который должен был показывать шаги путешествия, и кнопку деталей маршрута, где в будущем планировалось отображать данные по стране, климату, условиям проживания и другим параметрам.

Конечно, сейчас я понимаю, что многие вещи на фронтенде можно было бы сделать аккуратнее и современнее, но тогда этот проект стал для меня настоящим полигоном. Я учился не по абстрактным задачам, а через собственную боль: хотел, чтобы конкретная кнопка работала без перезагрузки, чтобы карта обновлялась плавно, чтобы пользователь видел не просто сухой результат, а понятный сценарий путешествия.

Со временем проект потребовал более серьёзного подхода к качеству кода. Я начал вести задачи в TODO-листах, работать через dev-ветку, отказался от прямых изменений в main, стал больше думать о структуре проекта и постепенно провёл крупный рефакторинг. Часть кода была переписана, часть логики вынесена в отдельные сервисы, а сам проект стал ближе к нормальному приложению, а не просто дипломной работе, которая разрослась во все стороны.

Большим открытием для меня стала оптимизация запросов. Долгое время я не устанавливал Django Debug Toolbar, потому что больше думал о функционале и общей идее приложения. Но когда дошли руки до производительности, я обнаружил классическую проблему N+1. На обработке массива примерно из ста билетов количество SQL-запросов доходило до 1300. После оптимизации удалось сократить их примерно до 30. Для этого я начал использовать select_related, prefetch_related, __in, bulk_create, bulk_update, only, values, values_list и transaction.atomic.

Этот этап оказался особенно полезным. Я начал лучше понимать, как Django ORM превращается в SQL, почему нельзя бездумно делать запросы в циклах, как работают транзакции и почему структура моделей напрямую влияет на производительность. Кажется, именно на таких моментах pet-проект перестаёт быть просто "проектом для портфолио" и становится настоящей учебной средой, где каждая ошибка заставляет тебя расти.

В проекте также появились unit-тесты. Я тестировал формы, модели, представления и отдельные функции. Покрытие доходило примерно до 70%, что для моего первого большого проекта было очень важным этапом. До этого тесты казались чем-то формальным, но когда приложение стало достаточно большим, я начал понимать, зачем они действительно нужны. Любое изменение в алгоритме маршрута могло сломать что-то в другом месте, и без тестов поддерживать проект становилось всё сложнее.

Если собрать весь стек, то в разное время в проекте использовались Python, Django, GeoDjango, Django ORM, PostgreSQL, PostGIS, Redis, Celery, Celery Beat, Amadeus API, OSM, Folium, GeoPy, JavaScript, jQuery, Bootstrap, Docker, VPS-деплой и внешние API для геокодирования, стран, аэропортов и часовых поясов.

Но для меня Singularis важен не только стеком. Это был проект, на котором я впервые начал думать как backend-разработчик. Не просто "как вывести страницу", а как спроектировать данные, как обработать внешний API, как не доверять слепо чужим ответам, как хранить сложные структуры, как запускать фоновые задачи, как чистить устаревшие данные, как оптимизировать запросы и как сделать так, чтобы приложение можно было развивать дальше.

Со временем я понял, что в этом проекте было много наивных решений. Где-то не хватало опыта, где-то хотелось быстрее увидеть результат, где-то архитектура рождалась уже после того, как функционал был написан. Но именно поэтому этот проект оказался для меня таким ценным. Он был не идеальным, зато настоящим. В нём были ошибки, ночные рефакторинги, внезапные баги, переписывание половины логики из-за одного странного ответа API, радость от первых работающих маршрутов и постоянное ощущение, что каждая новая фича открывает ещё десять тем, в которых нужно разобраться.

Сейчас я смотрю на Singularis как на одну из главных ступеней своего перехода в backend. Он начался как дипломный проект, потом стал pet-проектом, потом — полноценной тренировочной площадкой, где я постепенно учился работать с данными, API, архитектурой, картами, асинхронными задачами и производительностью.

Открываем раздел...