Files
CU_Points/ARCHITECTURE.md
T
emilandClaude Sonnet 4.6 50b3c4198a 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>
2026-05-01 10:03:27 +03:00

14 KiB
Raw Blame History

CU Points — Архитектура проекта

Документ для команды. Обновляется по мере принятия архитектурных решений. Последнее обновление: апрель 2026.


Концепция

Поинтовая система лояльности для студентов ЦУ — аналог Innopolis Club.

Ценность для студента: зарабатывай поинты за учёбу, трать на кофе и еду рядом с кампусом. Ценность для партнёра: гарантированный поток студентов, компенсация от ЦУ. Ценность для ЦУ: инструмент мотивации студенческой активности.


Роли пользователей

Роль Что может
student смотреть баланс, историю, показывать QR для оплаты
partner сканировать QR студентов, списывать поинты
admin начислять поинты, смотреть статистику, управлять партнёрами

Архитектура (высокий уровень)

┌────────────────────────────────────────────────┐
│               КЛИЕНТЫ (браузер)                │
│   Студент (web)   Партнёр (web)   Админ (web)  │
└──────────────────────┬─────────────────────────┘
                       │ HTTPS / REST API
                       │
┌──────────────────────▼─────────────────────────┐
│              Go REST API (:8080)               │
│                                                │
│  /auth    /points    /partners    /admin       │
│                                                │
│  middleware: JWT проверка, role guard          │
└────────┬───────────────────────┬───────────────┘
         │                       │
┌────────▼────────┐    ┌─────────▼──────┐
│  PostgreSQL 16  │    │    Redis 7     │
│  (source of     │    │  - QR-токены   │
│   truth)        │    │  - сессии      │
└─────────────────┘    └────────────────┘

Ключевые флоу

Флоу 1: Студент тратит поинты у партнёра (QR)

Студент (браузер)        Кассир партнёра          Go API
       │                        │                    │
       │ GET /me/qr             │                    │
       │───────────────────────────────────────────▶ │
       │◀─────────────────────────────────────────── │
       │ {qr_token: JWT 5min}   │                    │
       │                        │                    │
       │  [показывает QR]       │                    │
       │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶ │                    │
       │                        │ POST /partner/spend│
       │                        │ {qr_token, amount} │
       │                        │───────────────────▶│
       │                        │                    │ 1. валидировать JWT
       │                        │                    │ 2. проверить одноразовость (Redis)
       │                        │                    │ 3. проверить баланс ≥ amount
       │                        │                    │ 4. BEGIN TRANSACTION
       │                        │                    │    UPDATE users SET balance -= amount
       │                        │                    │    INSERT INTO transactions
       │                        │                    │ 5. COMMIT
       │                        │                    │ 6. записать jti в Redis (TTL 5min)
       │                        │◀───────────────────│
       │                        │ {success, new_bal} │

Почему QR, а не интеграция с кассой: Федеральные сети (Додо, Дринкит) имеют собственное кассовое ПО и не дадут интеграцию стартапу без длительных переговоров. QR-флоу — самодостаточное решение, работает с любым партнёром у которого есть смартфон.

Флоу 2: Начисление поинтов студенту

Источники поинтов (финальный список определят аналитики, здесь — возможные варианты):

Триггер Кто инициирует Примерное количество
Посещение занятия Администратор / интеграция с LMS 510 поинтов
Сдача задания вовремя Администратор / интеграция с LMS 1020 поинтов
Реферал (пригласил друга) Автоматически 50100 поинтов
Ручное начисление (победа в конкурсе и т.д.) Администратор любое

MVP: только ручное начисление через дашборд администратора. V1: интеграция с LMS/системой посещаемости ЦУ.


База данных (полная схема)

-- users: студенты, партнёры, администраторы
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()
);

-- partners: метаданные точек-партнёров
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()
);

-- transactions: 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),
    amount      INTEGER NOT NULL,
    type        TEXT NOT NULL CHECK (type IN ('earn', 'spend', 'admin_grant', 'expire')),
    description TEXT,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- earning_rules: настраиваемые правила начисления
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);

Почему balance в таблице users, а не вычисляется из транзакций: Вычислять баланс через SUM(transactions) — медленно при большой истории. Храним balance как денормализованное поле, обновляем атомарно вместе с транзакцией. PostgreSQL CHECK (balance >= 0) — последняя линия защиты от отрицательного баланса.


Структура Go-бэкенда (детально)

backend/internal/points/service.go  ←  самый важный файл в проекте

// Пример структуры сервиса
type Service struct {
    repo   Repository      // интерфейс для моков в тестах
    cache  cache.Client
    db     *pgxpool.Pool
}

func (s *Service) SpendPoints(ctx context.Context, req SpendRequest) error {
    // 1. Проверить QR-токен (одноразовость через Redis)
    // 2. Загрузить пользователя
    // 3. Проверить баланс >= req.Amount
    // 4. Проверить лимит (amount <= purchase_total * max_spend_pct / 100)
    // 5. BEGIN TRANSACTION
    //    UPDATE users SET balance = balance - req.Amount WHERE id = req.UserID
    //    INSERT INTO transactions (...)
    // 6. COMMIT
    // 7. Записать jti в Redis (TTL 5 минут)
}

Паттерн Repository: каждый пакет определяет интерфейс Repository, что позволяет писать unit-тесты без реальной БД (mock-реализация).


Безопасность

Угроза Защита
Отрицательный баланс CHECK (balance >= 0) + проверка в сервисе
Повторное использование QR Redis: used_qr:<jti> с TTL 5 мин
Подделка роли Role guard middleware + JWT claims
SQL-инъекции Только параметризованные запросы (pgx/sqlx)
Утечка токенов Access token TTL = 15 минут
Brute force логина Rate limiting по IP (middleware)

Деплой (Yandex Cloud)

Yandex Cloud
├── Application Load Balancer
│   └── TLS-терминация (сертификат от Let's Encrypt через YC Certificate Manager)
├── Container Registry
│   ├── cu-points-backend:latest
│   └── cu-points-frontend:latest
├── Compute Cloud (или Serverless Containers)
│   ├── backend   (2 реплики, 1 vCPU / 1 GB RAM каждая)
│   └── frontend  (2 реплики)
├── Managed Service for PostgreSQL
│   └── HA-кластер (1 мастер + 1 реплика)
└── Managed Service for Redis
    └── 1 инстанс

Локальная разработка:

# docker-compose.yml (минимальный)
services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: cupoints
      POSTGRES_USER: dev
      POSTGRES_PASSWORD: dev
    ports: ["5432:5432"]
    volumes: ["pgdata:/var/lib/postgresql/data"]

  redis:
    image: redis:7-alpine
    ports: ["6379:6379"]

volumes:
  pgdata:

Правовые требования

Чтобы поинты не облагались НДФЛ у студентов (п.68 ст.217 НК РФ):

  1. Публичная оферта — правила программы опубликованы на сайте ЦУ, доступны без авторизации.
  2. Срок акцепта ≥ 30 дней — прописать в условиях программы.
  3. Студенты ≠ сотрудники — поинты за учёбу, не за трудовые обязательства.
  4. Юрлицо-оператор — ООО (можно ЦУ или отдельное юрлицо), которое заключает договоры с партнёрами и компенсирует им потраченные студентами поинты.

Дорожная карта

MVP

  • Инфраструктура (docker-compose, миграции, CI)
  • Auth (JWT, роли)
  • Модель транзакций + баланс
  • QR-флоу списания у партнёра
  • Кабинет студента (баланс, история, QR)
  • Ручное начисление администратором
  • 2–3 партнёра-пилота рядом с кампусом

V1 (после MVP)

  • Автоначисление через интеграцию с LMS ЦУ
  • Дашборд аналитики (для аналитиков и руководства)
  • Реферальная программа
  • PWA для мобильных (без нативного приложения)

V2

  • Срок жизни поинтов (expire)
  • Уровни участников (Silver / Gold / Platinum)
  • Push-уведомления
  • API для партнёров (без QR — прямая интеграция для тех кто готов)

Открытые вопросы (для команды)

  • Откуда студент получает поинты? Финальный список триггеров — на аналитиках.
  • Кто оператор программы? Нужно ли отдельное ООО или достаточно ЦУ?
  • Как партнёр получает компенсацию? Ежемесячный акт или автоматически? — на экономисте.
  • Лимит 50% от суммы покупки — нужно согласовать с партнёрами.
  • Дизайн — Figma-макеты от дизайнера до начала разработки фронтенда.