feat: initial commit — backend API + student cabinet frontend
- 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>
This commit is contained in:
@@ -0,0 +1,358 @@
|
||||
# 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: дашборд администратора
|
||||
Reference in New Issue
Block a user