Перейти к основному содержимому

Архитектура системы

Общая схема

┌─────────────────────────────────────────────┐
│ Браузер пользователя │
│ Next.js (SSR + CSR, :3000) │
└──────────────────┬──────────────────────────┘
│ REST/JSON (Axios)

┌─────────────────────────────────────────────┐
│ Spring Boot API (:8080) │
│ │
│ JwtAuthFilter → Controller → Service │
│ ↕ ↕ │
│ SecurityContext Repository (JPA) │
│ ↕ │
│ PostgreSQL (:45432) │
│ │
│ ImageService → S3Client → SeaweedFS (:8333)│
│ │
│ SyncService ──────────────────────────────→│
│ ↓ ScheduledJob │
│ MphoneApiClient → mock-api / mphone (:8010)│
└─────────────────────────────────────────────┘

Бэкенд: слои приложения

Бэкенд использует строгую многоуровневую архитектуру:

HTTP Request
→ Controller — валидация (@Valid), вызов Service
→ Service — бизнес-логика (@Transactional)
→ Repository — Spring Data JPA + Specifications
← Entity — JPA-сущность из БД
← Mapper — MapStruct: Entity → Model → Response DTO
→ Controller — возврат Response DTO клиенту

Промежуточный слой Model — простые Java-record'ы, передаются между Service и Controller. Это изолирует HTTP-контракт от JPA-сущностей и упрощает тестирование.

Именование компонентов

ТипПримерНазначение
*ControllerProductControllerREST-эндпоинт
*Service / *ServiceImplProductServiceImplБизнес-логика
*RepositoryProductRepositoryДоступ к БД
*EntityProductEntityJPA-таблица
*ModelImageModelВнутренний record
*RequestCreateProductRequestТело входящего HTTP запроса
*ResponseProductResponseТело исходящего HTTP ответа
*MapperProductMapperMapStruct-маппер
*ExceptionProductNotFoundExceptionДоменное исключение

Фронтенд: маршруты и слои

Next.js App Router
├── (site)/ — публичная витрина
│ ├── page.tsx — главная страница (ISR)
│ ├── catalog/ — каталог с фильтрами
│ └── products/[id] — карточка товара
└── admin/ — административная панель
├── layout.tsx — боковое меню + авторизация
├── products/ — управление товарами
├── orders/ — управление заказами
└── ...

Получение данных:

  • SSR/ISR (server-side): публичные страницы — fetch() напрямую к бэкенду с revalidate
  • CSR (client-side): все операции в админ-панели — через Axios-инстанс (src/lib/api.ts)

Поток аутентификации

POST /api/v1/auth/login
→ JwtService.generateToken()
→ Ответ: { token, user }
→ localStorage.setItem('dm_shop_token', token)

Последующие запросы:
Axios Interceptor → Authorization: Bearer <token>
→ JwtAuthFilter.doFilterInternal()
→ JwtService.extractUsername()
→ UserDetailsService.loadUserByUsername()
→ SecurityContextHolder.setAuthentication()

Синхронизация каталога

@Scheduled(fixedDelay=30_000)
SyncServiceImpl.run()
→ syncCategories() → MphoneApiClient.listCategories()
→ syncBrands() → MphoneApiClient.listBrends()
→ syncProducts() → MphoneApiClient.listProducts()

Каждый метод:
1. Читает lastSyncTime из таблицы sync_state
2. Запрашивает изменённые записи с dateFrom
3. Обновляет или создаёт сущности
4. Обновляет sync_state

Обработка исключений

Все доменные исключения перехватываются в GlobalExceptionHandler и возвращаются клиенту в формате ApiErrorResponse:

{
"status": 404,
"error": "Not Found",
"message": "Product not found with id: 123"
}

Каждое новое исключение должно быть зарегистрировано в GlobalExceptionHandler (common/exception/).

Кеширование

Caffeine Cache (in-memory):

  • Максимум 1000 записей
  • TTL 30 секунд
  • Используется для часто читаемых справочников (категории, конфигурация)

Хранение файлов

Изображения загружаются в SeaweedFS через AWS SDK v2 (S3-совместимый API):

  1. POST /api/v1/imagesImageService.upload() → S3Client → SeaweedFS
  2. В таблице images сохраняется s3Key, fileName, contentType, fileSize
  3. Связь с товаром: product_images (таблица N:M с display_order)
  4. Доступ: GET /api/v1/images/{id} возвращает байты из S3