Архитектура системы
Общая схема
┌─────────────────────────────────────────────┐
│ Браузер пользователя │
│ 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-сущностей и упрощает тестирование.
Именование компонентов
| Тип | Пример | Назначение |
|---|---|---|
*Controller | ProductController | REST-эндпоинт |
*Service / *ServiceImpl | ProductServiceImpl | Бизнес-логика |
*Repository | ProductRepository | Доступ к БД |
*Entity | ProductEntity | JPA-таблица |
*Model | ImageModel | Внутренний record |
*Request | CreateProductRequest | Тело входящего HTTP запроса |
*Response | ProductResponse | Тело исходящего HTTP ответа |
*Mapper | ProductMapper | MapStruct-маппер |
*Exception | ProductNotFoundException | Доменное исключение |
Фронтенд: маршруты и слои
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):
POST /api/v1/images→ImageService.upload()→ S3Client → SeaweedFS- В таблице
imagesсохраняетсяs3Key,fileName,contentType,fileSize - Связь с товаром:
product_images(таблица N:M сdisplay_order) - Доступ:
GET /api/v1/images/{id}возвращает байты из S3