- Go backend: auth (JWT), points earn/spend, QR token generation, partners, admin grant/stats endpoints with chi router - Next.js 14 frontend: login, student dashboard, transaction history, QR display, partners list - PostgreSQL migrations (4 tables), Redis cache, Docker Compose - CORS middleware, role-based route protection, Zustand auth store Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
359 lines
17 KiB
Markdown
359 lines
17 KiB
Markdown
# CU Points — CLAUDE.md
|
||
|
||
## Что это за проект
|
||
|
||
Поинтовая система лояльности для студентов Центрального Университета (Москва).
|
||
Студенты зарабатывают поинты за активность в ЦУ и тратят их у партнёров
|
||
(локальные кофейни, столовые, магазины рядом с кампусом).
|
||
|
||
Аналог Innopolis Club, адаптированный для Москвы. Партнёры — небольшой локальный
|
||
бизнес, не федеральные сети.
|
||
|
||
**Статус:** проект с нуля, чистый репозиторий.
|
||
|
||
---
|
||
|
||
## Команда
|
||
|
||
| Роль | Описание |
|
||
|------|----------|
|
||
| Тимлид / Продакт / Разраб | Emil — основной пользователь Claude Code |
|
||
| Backend-разработчик | разный уровень Go |
|
||
| Frontend-разработчик | разный уровень Next.js |
|
||
| Аналитик × 2 | требования, исследования, метрики |
|
||
| Дизайнер | UI/UX, Figma → компоненты |
|
||
| Экономист | бизнес-модель, партнёрские условия |
|
||
|
||
> **Важно для Claude Code:** в команде разный уровень Go и Next.js.
|
||
> Код должен быть хорошо прокомментирован, структура — предсказуемой,
|
||
> сложные паттерны — объяснены в комментарии над функцией.
|
||
|
||
---
|
||
|
||
## Стек
|
||
|
||
| Слой | Технология |
|
||
|------|-----------|
|
||
| Backend | Go 1.22+ |
|
||
| Frontend | Next.js 14 (App Router) + TypeScript |
|
||
| Стили | Tailwind CSS |
|
||
| БД | PostgreSQL 16 |
|
||
| Кэш / сессии | Redis 7 |
|
||
| Миграции | goose |
|
||
| Контейнеры | Docker + Docker Compose |
|
||
| Репозиторий | GitHub |
|
||
| CI/CD | GitHub Actions |
|
||
| Деплой | Yandex Cloud |
|
||
| Трекер задач | Kanban (Notion) |
|
||
|
||
---
|
||
|
||
## Структура репозитория
|
||
|
||
```
|
||
cu-points/
|
||
├── backend/
|
||
│ ├── cmd/api/
|
||
│ │ └── main.go # точка входа: инициализация, DI, запуск
|
||
│ ├── internal/
|
||
│ │ ├── config/
|
||
│ │ │ └── config.go # конфиг из env-переменных
|
||
│ │ ├── auth/
|
||
│ │ │ ├── handler.go # HTTP-хендлеры (только парсинг запроса/ответа)
|
||
│ │ │ ├── service.go # бизнес-логика аутентификации
|
||
│ │ │ ├── repository.go # SQL-запросы
|
||
│ │ │ └── jwt.go # генерация и валидация JWT
|
||
│ │ ├── points/
|
||
│ │ │ ├── handler.go
|
||
│ │ │ ├── service.go # earn/spend — самый критичный слой
|
||
│ │ │ └── repository.go
|
||
│ │ ├── users/
|
||
│ │ │ ├── handler.go
|
||
│ │ │ ├── service.go
|
||
│ │ │ └── repository.go
|
||
│ │ ├── partners/
|
||
│ │ │ ├── handler.go
|
||
│ │ │ ├── service.go
|
||
│ │ │ └── repository.go
|
||
│ │ ├── admin/
|
||
│ │ │ ├── handler.go
|
||
│ │ │ └── service.go
|
||
│ │ └── middleware/
|
||
│ │ ├── auth.go # проверка JWT, прокидывание user_id в контекст
|
||
│ │ ├── role.go # проверка роли (student/partner/admin)
|
||
│ │ └── logger.go # структурированное логирование запросов
|
||
│ └── pkg/
|
||
│ ├── db/postgres.go # инициализация pgx pool
|
||
│ ├── cache/redis.go # инициализация Redis клиента
|
||
│ └── response/json.go # стандартные JSON-ответы (success/error)
|
||
├── frontend/
|
||
│ ├── app/
|
||
│ │ ├── (auth)/login/page.tsx
|
||
│ │ ├── (student)/
|
||
│ │ │ ├── dashboard/page.tsx # баланс + последние операции
|
||
│ │ │ ├── history/page.tsx # полная история транзакций
|
||
│ │ │ ├── partners/page.tsx # список партнёров
|
||
│ │ │ └── qr/page.tsx # QR-код для оплаты
|
||
│ │ ├── (partner)/
|
||
│ │ │ └── scan/page.tsx # сканер QR + форма суммы
|
||
│ │ └── (admin)/
|
||
│ │ ├── dashboard/page.tsx
|
||
│ │ └── grant/page.tsx # ручное начисление поинтов
|
||
│ ├── components/
|
||
│ │ ├── ui/ # атомарные компоненты (Button, Input...)
|
||
│ │ ├── BalanceCard.tsx
|
||
│ │ ├── TransactionList.tsx
|
||
│ │ ├── QRDisplay.tsx
|
||
│ │ └── PartnerCard.tsx
|
||
│ ├── lib/
|
||
│ │ ├── api.ts # fetch-обёртка с baseURL и токеном
|
||
│ │ ├── store.ts # Zustand: глобальный стейт
|
||
│ │ ├── types.ts # типы для API-ответов
|
||
│ │ └── utils.ts # форматирование дат, чисел
|
||
│ └── middleware.ts # защита роутов по роли
|
||
├── migrations/
|
||
│ ├── 00001_init_users.sql
|
||
│ ├── 00002_init_partners.sql
|
||
│ ├── 00003_init_transactions.sql
|
||
│ └── 00004_init_earning_rules.sql
|
||
├── .github/workflows/
|
||
│ ├── backend-ci.yml
|
||
│ └── frontend-ci.yml
|
||
├── docker-compose.yml
|
||
├── Makefile
|
||
├── .env.example
|
||
└── CLAUDE.md
|
||
```
|
||
|
||
---
|
||
|
||
## Соглашения по коду
|
||
|
||
### Go (Backend)
|
||
|
||
- **Архитектура:** строго `handler → service → repository`. Бизнес-логика **только в service**.
|
||
- **Именование файлов:** `snake_case`. Пакеты: короткие, без подчёркиваний.
|
||
- **Ошибки:** всегда явные, никогда не игнорировать `err`. Оборачивать через `fmt.Errorf("context: %w", err)`.
|
||
- **HTTP-роутер:** стандартная библиотека `net/http` + `chi`. Не использовать gin/echo/fiber без обсуждения с тимлидом.
|
||
- **БД:** только `pgx/v5` или `sqlx` с raw SQL. ORM (gorm и т.п.) — **запрещены**.
|
||
- **Логирование:** `log/slog` (стандартная библиотека Go 1.21+), структурированные поля.
|
||
- **Конфиг:** только через `internal/config/config.go`. Никаких magic strings по всему коду.
|
||
- **Комментарии:** обязательны над каждой экспортируемой функцией и над нетривиальной логикой. На английском.
|
||
|
||
```go
|
||
// SpendPoints debits the given amount from the user's balance
|
||
// and records a transaction atomically in a single DB transaction.
|
||
// Returns ErrInsufficientBalance if balance < amount.
|
||
func (s *Service) SpendPoints(ctx context.Context, req SpendRequest) error {
|
||
```
|
||
|
||
### TypeScript / Next.js (Frontend)
|
||
|
||
- **Роутер:** App Router (не Pages Router).
|
||
- **Стейт:** Zustand — глобальный (user, balance). `useState` — локальный UI-стейт.
|
||
- **Запросы к API:** только через `lib/api.ts`. **Не использовать axios**.
|
||
- **Компоненты:** функциональные, именованные экспорты: `export function BalanceCard(...)`.
|
||
- **Типы:** строгая типизация. **`any` — запрещён**. Типы API-ответов в `lib/types.ts`.
|
||
- **Стили:** Tailwind CSS. Кастомный CSS только если Tailwind не покрывает случай.
|
||
|
||
### Общие правила
|
||
|
||
- Названия переменных, функций, комментарии — **на английском**.
|
||
- Commit messages: `feat:`, `fix:`, `chore:`, `refactor:`, `docs:` + описание на английском.
|
||
Пример: `feat: add QR token generation endpoint`
|
||
- `TODO` только с ссылкой на задачу: `// TODO(notion:TASK-42): handle expired tokens`
|
||
- Все API-эндпоинты с префиксом `/api/v1/`.
|
||
|
||
---
|
||
|
||
## Git-стратегия
|
||
|
||
Trunk-based flow — простой и подходящий для команды нашего размера:
|
||
|
||
```
|
||
main ← всегда стабильная, деплоится автоматически
|
||
└── feature/auth-jwt
|
||
└── feature/points-spend-qr
|
||
└── fix/balance-negative-edge-case
|
||
```
|
||
|
||
- **Никогда не пушить напрямую в `main`.**
|
||
- Каждая задача из Notion — отдельная ветка: `feature/<название>` или `fix/<название>`.
|
||
- Перед мержем — PR с ревью минимум одного человека.
|
||
- CI (lint + tests) должен быть зелёным перед мержем.
|
||
|
||
---
|
||
|
||
## Модель данных
|
||
|
||
```sql
|
||
-- Пользователи (студенты, партнёры, администраторы)
|
||
CREATE TABLE users (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
email TEXT NOT NULL UNIQUE,
|
||
name TEXT NOT NULL,
|
||
student_id TEXT UNIQUE, -- только для студентов ЦУ
|
||
role TEXT NOT NULL CHECK (role IN ('student', 'partner', 'admin')),
|
||
balance INTEGER NOT NULL DEFAULT 0 CHECK (balance >= 0),
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
|
||
-- Партнёры (кофейни, столовые и т.д.)
|
||
CREATE TABLE partners (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
user_id UUID REFERENCES users(id), -- аккаунт кассира
|
||
name TEXT NOT NULL,
|
||
address TEXT NOT NULL,
|
||
max_spend_pct INTEGER NOT NULL DEFAULT 50, -- макс. % покупки оплатить поинтами
|
||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
|
||
-- Транзакции — append-only лог, НИКОГДА не удалять записи
|
||
CREATE TABLE transactions (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
user_id UUID NOT NULL REFERENCES users(id),
|
||
partner_id UUID REFERENCES partners(id), -- NULL для earn-транзакций
|
||
amount INTEGER NOT NULL, -- > 0 earn, < 0 spend
|
||
type TEXT NOT NULL CHECK (type IN ('earn', 'spend', 'admin_grant', 'expire')),
|
||
description TEXT,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
|
||
-- Правила начисления (настраиваются администратором)
|
||
CREATE TABLE earning_rules (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
name TEXT NOT NULL,
|
||
points_amount INTEGER NOT NULL,
|
||
trigger_type TEXT NOT NULL CHECK (trigger_type IN ('attendance', 'assignment', 'referral', 'admin')),
|
||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
|
||
CREATE INDEX ON transactions(user_id, created_at DESC);
|
||
CREATE INDEX ON transactions(partner_id, created_at DESC);
|
||
```
|
||
|
||
---
|
||
|
||
## Бизнес-правила (критически важно)
|
||
|
||
1. **Баланс ≥ 0 всегда** — двойная защита: `CHECK (balance >= 0)` в PostgreSQL + проверка в `points.Service` перед списанием.
|
||
2. **Транзакции атомарны** — списание баланса и запись транзакции — одна SQL-транзакция (`BEGIN / COMMIT`).
|
||
3. **Поинты не конвертируются в рубли** — студент не может вывести поинты деньгами.
|
||
4. **Курс:** 1 поинт = 1 рубль у партнёра. Партнёр получает компенсацию от ЦУ по договору.
|
||
5. **Лимит списания:** не более `max_spend_pct`% (по умолчанию 50%) от суммы покупки.
|
||
6. **QR-токен одноразовый** — JWT с TTL 5 минут. После использования — записать в Redis (ключ = `used_qr:<jti>`, TTL 5 минут) для защиты от повторного использования.
|
||
7. **Публичная оферта** — правила программы доступны без авторизации (требование НК РФ п.68 ст.217).
|
||
|
||
---
|
||
|
||
## API эндпоинты (MVP)
|
||
|
||
```
|
||
POST /api/v1/auth/login # email + password → {access_token, refresh_token}
|
||
POST /api/v1/auth/refresh # {refresh_token} → {access_token}
|
||
|
||
GET /api/v1/me # профиль + текущий баланс
|
||
GET /api/v1/me/transactions # история (query: limit, offset)
|
||
GET /api/v1/me/qr # сгенерировать QR-токен (TTL 5 мин)
|
||
|
||
GET /api/v1/partners # список активных партнёров (публично)
|
||
|
||
POST /api/v1/partner/spend # {qr_token, amount} → списать поинты
|
||
# доступно только роли 'partner'
|
||
|
||
POST /api/v1/admin/points/grant # {user_id, amount, description}
|
||
GET /api/v1/admin/transactions # все транзакции системы
|
||
GET /api/v1/admin/users # список студентов + балансы
|
||
GET /api/v1/admin/stats # агрегированная статистика
|
||
```
|
||
|
||
---
|
||
|
||
## Переменные окружения
|
||
|
||
Файл `.env.example` в корне репозитория:
|
||
|
||
```env
|
||
# Backend
|
||
DATABASE_URL=postgres://user:password@localhost:5432/cupoints?sslmode=disable
|
||
REDIS_URL=redis://localhost:6379
|
||
JWT_SECRET=your-secret-key-minimum-32-characters
|
||
JWT_ACCESS_TTL=15m
|
||
JWT_REFRESH_TTL=168h
|
||
PORT=8080
|
||
ENV=development
|
||
|
||
# Frontend
|
||
NEXT_PUBLIC_API_URL=http://localhost:8080
|
||
```
|
||
|
||
**Никогда не коммитить `.env`** — только `.env.example`.
|
||
|
||
---
|
||
|
||
## Makefile
|
||
|
||
```makefile
|
||
make docker-up # поднять postgres + redis
|
||
make docker-down # остановить
|
||
make migrate-up # применить все миграции
|
||
make migrate-down # откатить последнюю миграцию
|
||
make run-backend # запустить Go API (hot reload через air)
|
||
make run-frontend # запустить Next.js dev
|
||
make test # go test ./... + jest
|
||
make test-coverage # coverage report
|
||
make lint # golangci-lint + eslint + tsc
|
||
```
|
||
|
||
---
|
||
|
||
## Тестирование
|
||
|
||
- **Unit-тесты** обязательны для `internal/points/service.go` и `internal/auth/service.go`.
|
||
- **Integration-тесты** для критических путей: earn, spend, граничный случай (баланс = 0).
|
||
- Тестовая БД: `cupoints_test` в docker-compose.
|
||
- **Целевое покрытие:** ≥ 70% для пакетов `points` и `transactions`.
|
||
|
||
---
|
||
|
||
## CI (GitHub Actions)
|
||
|
||
На каждый PR в `main`:
|
||
1. `golangci-lint`
|
||
2. `go test ./...`
|
||
3. `tsc --noEmit` + `eslint`
|
||
4. Docker build (проверка что образы собираются)
|
||
|
||
Мерж только при зелёном CI.
|
||
|
||
---
|
||
|
||
## Чего НЕ делать
|
||
|
||
- Не хранить баланс только в Redis — PostgreSQL source of truth.
|
||
- Не писать бизнес-логику в хендлерах.
|
||
- Не использовать ORM.
|
||
- Не удалять записи из `transactions`.
|
||
- Не конвертировать поинты в рубли напрямую студенту.
|
||
- Не пушить в `main` напрямую.
|
||
- Не коммитить `.env`, ключи, пароли.
|
||
- Не использовать `any` в TypeScript.
|
||
|
||
---
|
||
|
||
## Приоритет задач (MVP)
|
||
|
||
1. `docker-compose.yml` + `Makefile` + `.env.example`
|
||
2. Миграции (все 4 таблицы)
|
||
3. Auth: login, JWT, middleware проверки роли
|
||
4. Points: earn/spend с атомарностью и проверкой баланса
|
||
5. QR: генерация токена + валидация (одноразовость через Redis)
|
||
6. REST API студента (me, transactions, qr)
|
||
7. REST API партнёра (spend)
|
||
8. REST API администратора (grant, stats)
|
||
9. Next.js: кабинет студента (баланс, история, QR)
|
||
10. Next.js: интерфейс партнёра (сканер + форма суммы)
|
||
11. Next.js: дашборд администратора
|