first commit
This commit is contained in:
153
CLAUDE.md
Normal file
153
CLAUDE.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# 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 |
|
||||
Reference in New Issue
Block a user