154 lines
9.0 KiB
Markdown
154 lines
9.0 KiB
Markdown
# CLAUDE.md — Архитектурный журнал
|
||
|
||
Этот файл ведётся автоматически. Фиксирует архитектурные решения,
|
||
отступления от ТЗ и причины этих отступлений.
|
||
|
||
---
|
||
|
||
## Стек
|
||
|
||
| Слой | Выбор | Версия |
|
||
|---|---|---|
|
||
| Runtime | Node.js + NestJS | NestJS 10 |
|
||
| HTTP-сервер | Fastify (не Express) | fastify 4 |
|
||
| WebSocket | Socket.io (`@nestjs/platform-socket.io`) | socket.io 4 |
|
||
| ORM | Drizzle ORM | 0.38.4 |
|
||
| База данных | PostgreSQL | — |
|
||
| Кеш / очереди | Redis (ioredis) | ioredis 5 |
|
||
| Хранилище файлов | MinIO (S3-совместимое) | minio 8 |
|
||
| Push-уведомления | Firebase Admin SDK (FCM) | firebase-admin 12 |
|
||
| Авторизация | JWT (passport-jwt) + refresh-token в Redis | — |
|
||
| Пакетный менеджер | pnpm | 11.x |
|
||
| API-документация | OpenAPI через `@nestjs/swagger` | swagger 7 |
|
||
|
||
---
|
||
|
||
## Архитектурные решения
|
||
|
||
### 1. Глобальный JWT-guard через `APP_GUARD`
|
||
|
||
Все маршруты закрыты по умолчанию. Публичные эндпоинты помечаются
|
||
декоратором `@Public()`. Это предотвращает случайное открытие нового
|
||
роута без авторизации.
|
||
|
||
### 2. Глобальный response wrapper `{ data: ... }`
|
||
|
||
`TransformInterceptor` оборачивает все успешные ответы в `{ data: T }`.
|
||
Унифицирует контракт клиента: клиент всегда читает `response.data`.
|
||
|
||
### 3. Refresh-токены в Redis, не в БД
|
||
|
||
Refresh-токен хранится в Redis с TTL 30 дней (`refresh_token:<userId>`).
|
||
При logout ключ удаляется — токен немедленно инвалидируется без
|
||
изменений в PostgreSQL. Это быстрее и не засоряет основную БД.
|
||
|
||
### 4. `user.active_chat_id` — ограничение на уровне приложения, не FK
|
||
|
||
В схеме `activeChatId` — простой UUID без `REFERENCES chat(id)`.
|
||
Причина: Drizzle не поддерживает отложенные FK (DEFERRABLE), а при
|
||
создании чата нужно сначала INSERT в `chat`, потом UPDATE `user`.
|
||
Добавление жёсткого FK создало бы проблему курицы и яйца.
|
||
Инвариант «один активный чат» соблюдается на уровне сервиса
|
||
(`chat.service.ts → createChat`).
|
||
|
||
### 5. `chat.profile1_id / profile2_id` хранят `user_id`, не `profile_id`
|
||
|
||
**Отступление от ТЗ.** ТЗ называет поля `profile1_id → profile`,
|
||
но семантически чат привязан к паре пользователей (через матч).
|
||
Поиск участника чата по userId напрямую не требует JOIN с `profile`.
|
||
Схема сохраняет исходные имена полей, но ссылается на `user.id`.
|
||
Если потребуется строгая привязка к профилю — нужен отдельный FK.
|
||
|
||
### 6. Лимит матчей проверяется при лайке, не при входе в ленту
|
||
|
||
ТЗ: «если у пользователя больше N матчей — поиск становится неактивным».
|
||
Реализовано как `BadRequestException` в `LikesService.createLike()`.
|
||
Лента (`FeedService`) возвращает профили без проверки лимита — клиент
|
||
сам скрывает кнопку лайка на основе флага из профиля пользователя.
|
||
Значение N задаётся через `MAX_MATCHES_BEFORE_PAUSE` в `.env`.
|
||
|
||
### 7. Сообщения отправляются и через REST, и через WebSocket
|
||
|
||
Клиент может отправить сообщение двумя путями:
|
||
- `POST /api/v1/chats/:id/messages` — REST (HTTP)
|
||
- событие `send_message` через Socket.io
|
||
|
||
Оба пути вызывают `ChatService.sendMessage()`. Socket.io-событие
|
||
дополнительно бродкастит `new_message` всем участникам комнаты.
|
||
REST-ответ не бродкастит — клиент должен сам обновить UI или слушать WS.
|
||
|
||
### 8. Файловые загрузки через Fastify Multipart, не Multer
|
||
|
||
ТЗ не оговаривает конкретный механизм. Multer работает только с Express.
|
||
Поскольку выбран Fastify, используется `@fastify/multipart`, зарегистрированный
|
||
в `main.ts`. Файл читается вручную через `req.file()` в контроллере.
|
||
|
||
### 9. WebSocket-gateway работает в пространстве имён `/chat`
|
||
|
||
Одно пространство имён вместо нескольких (чат + уведомления).
|
||
Уведомления о матчах и системные события отправляются через
|
||
`ChatGateway.emitToUser()` напрямую по `socketId`.
|
||
Если потребуется изолировать уведомления — вынести в отдельный gateway.
|
||
|
||
### 10. Feed использует `ORDER BY RANDOM()`
|
||
|
||
Лента случайно перемешивает профили при каждом запросе. Это дёшево
|
||
на малых данных и соответствует духу ТЗ («уникальная лента»).
|
||
При росте таблицы нужно заменить на cursor-based с детерминированным
|
||
seed (например, хешем userId + дата).
|
||
|
||
---
|
||
|
||
## Отступления от ТЗ
|
||
|
||
| # | Пункт ТЗ | Как реализовано | Причина |
|
||
|---|---|---|---|
|
||
| 1 | `chat.profile1_id → profile` | Поле хранит `user_id` | Матч создаётся между пользователями, а не профилями; JOIN с профилем не нужен на горячем пути |
|
||
| 2 | Поиск «неактивен» при превышении лимита матчей | `BadRequestException` при лайке | Проще контракт с клиентом: ошибка явная, не нужно отдельного флага `searchActive` |
|
||
| 3 | Тарифный план один, но оплата предусмотрена | Таблицы `tariff` и `payment` созданы, логика оплаты не реализована | ТЗ: «регистрация открыта для всех, тарифный план один» — бизнес-логики оплаты нет |
|
||
| 4 | `radiusKm` в фильтре ленты | Параметр принимается, но фильтрация по радиусу не применяется | Требует PostGIS или формулы Haversine; добавить в следующей итерации |
|
||
|
||
---
|
||
|
||
## Известные технические долги
|
||
|
||
| Файл | Проблема | Причина |
|
||
|---|---|---|
|
||
| `*/services/*.ts` | `as any` в `.insert().values()` и `.update().set()` | Drizzle ORM 0.38 не включает nullable и enum-колонки с `.default()` в TypeScript insert-тип; при апгрейде Drizzle — убрать касты |
|
||
| `feed.service.ts` | N+1 запрос: теги загружаются по одному на профиль | Достаточно для MVP; исправить через один JOIN с GROUP BY при росте нагрузки |
|
||
| `chat.service.ts` | `getMyChats` возвращает только `active` чаты | Закрытые чаты недоступны через API; добавить query-параметр `status` |
|
||
| `pnpm-workspace.yaml` | `allowBuilds` пополняется вручную при новых зависимостях с postinstall | Требование безопасности pnpm 11 |
|
||
|
||
---
|
||
|
||
## Команды
|
||
|
||
```bash
|
||
# Инфраструктура
|
||
docker-compose up -d
|
||
|
||
# Зависимости
|
||
pnpm install
|
||
|
||
# Первый запуск БД
|
||
pnpm db:generate # создать SQL-миграции
|
||
pnpm db:migrate # применить миграции
|
||
pnpm db:seed # заполнить справочники (roles, tariffs, date_statuses, greetings)
|
||
|
||
# Разработка
|
||
pnpm start:dev # hot-reload dev-сервер
|
||
pnpm build # компиляция в dist/
|
||
|
||
# Инструменты
|
||
pnpm db:studio # Drizzle Studio (GUI для БД)
|
||
```
|
||
|
||
## Точки входа
|
||
|
||
| URL | Описание |
|
||
|---|---|
|
||
| `http://localhost:3000/api/v1` | REST API |
|
||
| `http://localhost:3000/api/docs` | Swagger UI (только dev/staging) |
|
||
| `ws://localhost:3000/chat` | Socket.io namespace |
|
||
| `http://localhost:9001` | MinIO Console |
|