TennisClub
TennisClub to zaawansowana, wielomodułowa platforma internetowa klasy ERP/CRM dedykowana klubom tenisowym, ich menedżerom oraz społeczności graczy. System w pełni digitalizuje i automatyzuje procesy rezerwacji kortów online poprzez interaktywny grafik czasu rzeczywistego (timeline), upraszcza organizację i drabinkowanie turniejów sportowych, automatycznie oblicza punkty i rankingi graczy (Elo/ATP-style), buduje zaangażowanie społeczności (system znajomych, czat) oraz zapewnia nowoczesne integracje ze sprzętem sportowym (Garmin Connect) i powiadomieniami mobilnymi Web Push.
🛠️ Stos Technologiczny (Tech Stack)
Wszystkie warstwy aplikacji zostały zaprojektowane z myślą o skalowalności, bezpieczeństwie danych (data privacy) i stabilności na serwerze produkcyjnym:
- Języki i Frameworki:
**Python 3.12+**•**Django 5.1 (MVC Architecture)**•**Vanilla JS**•**HTML5 / CSS3** - Baza Danych & Cache:
**MySQL**(silnik InnoDB z transakcjami ACID) •**Redis**(pamięć podręczna i broker) /**LocMemCache** - Kolejki Zadań & Workery:
**Django-Q2**(asynchroniczne wysyłanie powiadomień, synchronizacja z Garmin API w tle, zarządzanie zadaniami cyklicznymi) - Szyfrowanie & Bezpieczeństwo:
**django-encrypted-model-fields**(szyfrowanie wrażliwych poświadczeń AES-256 w bazie danych) •**django-allauth**(uwierzytelnianie użytkowników i logowanie przez Google OAuth2) - Powiadomienia & PWA:
**Web Push API**•**py-vapid**(autoryzacja kluczy i obsługa Service Workera) - Infrastruktura & Bezprecedensowy Uptime:
**Docker**•**Nginx**•**Gunicorn**•**Systemd**(zarządzanie procesami aplikacji i workerów w tle) - Testowanie & Jakość:
**pytest / Django TestCase**•**Django Debug Toolbar**• dedykowany system logowania per-moduł (izolowane pliki logów dla każdej aplikacji) - Automatyzacja Wdrożeń: Skrypt
**update.sh**z bezpieczną weryfikacją stanu kodu, automatycznymi migracjami i czyszczeniem pamięci podręcznej Cloudflare
📈 Korzyści Biznesowe (Business Impact)
- Skrócenie czasu rezerwacji i akceptacji o ~75%: Dzięki interaktywnej osi czasu (Timeline w JS) i automatycznym powiadomieniom, gracze mogą zarezerwować kort w kilka sekund, a właściciel obiektu zatwierdza lub odrzuca rezerwację jednym kliknięciem, bez konieczności rozmów telefonicznych i papierowych grafików.
- Bezpieczeństwo danych poświadczeń zewnętrznych (Garmin Connect): Zastosowanie szyfrowania kluczem symetrycznym (AES-256) na poziomie bazy danych gwarantuje pełną prywatność i bezpieczeństwo danych logowania użytkowników synchronizujących swoje zegarki sportowe z aplikacją.
- Wzrost zaangażowania i retencji graczy: Automatycznie obliczane rankingi na podstawie wyników turniejowych (wygrane sety, gemy, bonusy za udział) oraz moduł społecznościowy (wyszukiwanie sparingpartnerów, lista znajomych, czat) zwiększają zaangażowanie członków klubu i motywują do regularnej aktywności.
- Asynchroniczność i optymalizacja serwera: Przeniesienie ciężkich zadań integracyjnych (pobieranie danych z Garmin Connect) oraz komunikacyjnych (wysyłanie powiadomień Push do urządzeń mobilnych) do asynchronicznych workerów
Django-Q2zredukowało czas odpowiedzi serwera (Response Time) i obciążenie wątku głównego.
💡 Architektura i Decyzje Inżynieryjne (The "How")
Projekt TennisClub skupia się na rozwiązywaniu rzeczywistych wyzwań biznesowych i wydajnościowych poprzez zastosowanie odpowiednich wzorców projektowych i optymalizacji bazodanowych.
1. Bezpieczna integracja z Garmin Connect i szyfrowanie poświadczeń (Moduły users i activities)
- Wyzwanie: Pobieranie danych o grze w tenisa bezpośrednio z zegarków użytkowników wymaga podania ich loginu i hasła do serwerów Garmin Connect. Przechowywanie tych haseł otwartym tekstem w bazie danych MySQL stanowi ogromne ryzyko bezpieczeństwa. Ponadto synchronizacja danych przez API zewnętrznego dostawcy podczas żądania HTTP blokowałaby wątek serwera i mogłaby prowadzić do przekroczenia czasu oczekiwania.
- Rozwiązanie: Poświadczenia są automatycznie szyfrowane w bazie danych za pomocą klucza symetrycznego (AES-256) dzięki bibliotece
django-encrypted-model-fields. Cały proces pobierania danych został oddelegowany do kolejki zadańDjango-Q2(modułqcluster). Cykliczne zadanie w tle pobiera listę użytkowników i dla każdego z nich oddzielnie, asynchronicznie wywołuje pobieranie aktywności, parsując zaawansowane metryki tenisowe (np. asy, podwójne błędy, procent pierwszego serwisu) z aplikacji Tennis Studio.
2. Spójność relacji społecznościowych w bloku transakcyjnym (Moduł friends)
- Wyzwanie: Relacja znajomości w bazie danych musi być dwukierunkowa i w pełni spójna (jeśli użytkownik A akceptuje zaproszenie od użytkownika B, system musi stworzyć dwa powiązane rekordy w tabeli
Friend). Wykonywanie oddzielnych zapytań SQL bez kontroli transakcji stwarza ryzyko niespójności danych (np. utworzenie znajomości tylko w jedną stronę przy nagłym przerwaniu połączenia z bazą). - Rozwiązanie: Wdrożono mechanizm transakcji izolowanych (
transaction.atomic()) w widokach zarządzania znajomościami (accept_friend_requestorazremove_friend). Wszelkie operacje zapisu lub kasowania rekordu w tabeliFrienddla obu stron relacji są wykonywane w jednym bloku atomowym, co gwarantuje spójność danych (zasada "wszystko albo nic") na poziomie bazy danych InnoDB.
3. Zaawansowana agregacja statystyk i obliczanie rankingu (Moduł rankings)
- Wyzwanie: Obliczanie punktacji rankingowej zawodników na podstawie wyników turniejów, z uwzględnieniem wielu zmiennych (wygrane mecze, wygrane/przegrane sety, wygrane/przegrane gemy oraz bonusy za udział zależne od rangi turnieju) może skutkować problemem wydajnościowym N+1 przy dużej liczbie graczy.
- Rozwiązanie: Logika obliczeń została w pełni przeniesiona na stronę bazy danych za pomocą zaawansowanych zapytań agregujących Django ORM (Subqueries, Coalesce, Case/When, F-expressions). Zamiast pobierać rekordy do Pythona i pętli, system wykonuje jedno, wysoce zoptymalizowane zapytanie SQL, które oblicza punkty dla wszystkich graczy bezpośrednio w bazie MySQL, uwzględniając dynamiczne mnożniki z modelu
TournamentRankPointsprzypisane do najwyższej rangi turnieju, w którym dany gracz uczestniczył.
4. Mobilne Powiadomienia Push i PWA (Moduł notifications)
- Wyzwanie: Zapewnienie natychmiastowych powiadomień o nowych rezerwacjach, zmianach terminów czy zaproszeniach do znajomych bez konieczności odświeżania strony lub instalowania natywnych aplikacji mobilnych.
- Rozwiązanie: Zaimplementowano architekturę Progressive Web App (PWA) z zarejestrowanym Service Workerem obsługującym Web Push API. Wykorzystano bibliotekę
py-vapiddo bezpiecznej autoryzacji serwera push i asynchronicznego wysyłania powiadomień push bezpośrednio na urządzenia mobilne użytkowników, co daje natywne odczucia z użytkowania systemu.
5. Automatyzacja wdrożeń i restartu usług (Skrypt update.sh)
- Wyzwanie: Ręczna aktualizacja kodu na serwerze produkcyjnym (np. VPS lub Raspberry Pi) niesie ryzyko błędów ludzkich, takich jak pominięcie migracji bazy danych, błędne wdrożenie konfliktowych plików statycznych czy brak przeładowania procesów systemowych (
gunicornidjango-q). - Rozwiązanie: Napisano zaawansowany skrypt wdrożeniowy
update.shobsługujący tryby--safeoraz--hard. Skrypt automatycznie sprząta zmiany lokalne, synchronizuje repozytorium git, instaluje nowe zależności, przeprowadza migracje bazy danych, buduje pliki statyczne oraz bezpiecznie restartuje usługi systemd (tennis-club.serviceoraztennis-club-q.service). Na koniec oczyszcza pamięć podręczną Cloudflare przy użyciu API.
💻 Przykłady Kodu (Code Snippets)
1. Spójne, atomowe akceptowanie zaproszenia do znajomych (concurrency & transactional safety)
Fragment kodu z widoku `friends_list` w module `friends`. Prezentuje bezpieczną, dwukierunkową modyfikację relacji społecznościowych wewnątrz transakcji atomowej `transaction.atomic()`, co eliminuje ryzyko częściowego zapisu i niespójności bazy danych:# apps/friends/views.py
from django.db import transaction
from django.shortcuts import redirect
from django.contrib import messages
from .models import Friend, FriendRequest
# ... wycinek obsługi POST w widoku friends_list ...
elif action == 'accept_friend_request':
request_id = request.POST.get('request_id')
try:
# Pobieramy zaproszenie oczekujące na akceptację przez zalogowanego użytkownika
friend_request = FriendRequest.objects.get(pk=request_id, receiver=request.user, status='pending')
with transaction.atomic():
# Tworzymy relację dwukierunkową (A -> B oraz B -> A)
Friend.objects.create(user=friend_request.sender, friend=friend_request.receiver)
Friend.objects.create(user=friend_request.receiver, friend=friend_request.sender)
# Aktualizujemy status zaproszenia
friend_request.status = 'accepted'
friend_request.save()
messages.success(request, f"Zaakceptowano zaproszenie od {friend_request.sender.username}.")
return redirect('friends_list')
except FriendRequest.DoesNotExist:
messages.error(request, "Zaproszenie nie istnieje lub nie masz uprawnień do jego zaakceptowania.")
except IntegrityError:
messages.error(request, "Wystąpił błąd podczas akceptowania zaproszenia (możliwe, że jesteście już znajomymi).")
2. Asynchroniczna synchronizacja danych sportowych Garmin Connect w tle (Django Q Tasks)
Fragment pliku `tasks.py` z modułu `activities`. Prezentuje uruchamianie asynchronicznych i odseparowanych zadań synchronizacji aktywności sportowych w tle przy użyciu brokera zadań `Django-Q`, co zapobiega blokowaniu wątku głównego serwera:# apps/activities/tasks.py
import logging
from django.contrib.auth import get_user_model
from django_q.tasks import async_task
from garminconnect import Garmin
logger = logging.getLogger(__name__)
User = get_user_model()
def sync_single_user_garmin_data(user_id):
"""
Zadanie wykonywane przez workera dla pojedynczego użytkownika.
Loguje się do Garmin Connect przy użyciu poświadczeń i synchronizuje aktywności.
"""
try:
user = User.objects.get(pk=user_id)
except User.DoesNotExist:
logger.error(f"Sync task failed: User with ID {user_id} does not exist.")
return
profile = getattr(user, 'profile', None)
if not profile or not profile.garmin_login or not profile.garmin_password:
logger.warning(f"Sync task skipped: User {user.username} has no Garmin credentials.")
return
logger.info(f"Rozpoczynanie synchronizacji w tle dla użytkownika: {user.username}")
try:
# Bezpieczne logowanie z użyciem zdeszyfrowanych w locie poświadczeń
client = Garmin(profile.garmin_login, profile.garmin_password)
client.login()
# Wywołanie wewnętrznej logiki synchronizującej i parsującej metryki tenisowe
saved, failed = _sync_user_activities(user, client)
logger.info(f"Zakończono zadanie tła dla {user.username}. Zapisano: {saved}, Błędy {failed}")
except Exception as e:
logger.error(f"Nieoczekiwany błąd podczas synchronizacji w tle dla {user.username}: {e}")
def sync_all_users_garmin_data():
"""
Pobiera wszystkich użytkowników posiadających poświadczenia i zleca
dla każdego z nich osobne, współbieżne zadanie asynchroniczne.
"""
users_with_credentials = User.objects.filter(
profile__garmin_login__isnull=False,
profile__garmin_password__isnull=False
).exclude(profile__garmin_login='').exclude(profile__garmin_password='')
logger.info(f"Znaleziono {users_with_credentials.count()} użytkowników do synchronizacji.")
for user in users_with_credentials:
# Zlecanie asynchronicznego wykonania zadania w klastrze Django Q
async_task('apps.activities.tasks.sync_single_user_garmin_data', user.id)