338 lines
15 KiB
Markdown
338 lines
15 KiB
Markdown
# pulse-api — Архитектура
|
||
|
||
**Репозиторий:** `https://git.digital-home.site/daniil/pulse-api`
|
||
**Go module:** `github.com/daniil/homelab-api`
|
||
**URL:** `https://api.digital-home.site`
|
||
**Dev:** `http://192.168.31.60:8081`
|
||
|
||
## Общая архитектура
|
||
|
||
Классический Go REST API с разделением на слои:
|
||
|
||
```
|
||
cmd/api/main.go ← точка входа, роутер, инициализация
|
||
internal/
|
||
config/ ← загрузка env-переменных
|
||
repository/ ← работа с БД (SQL-запросы)
|
||
service/ ← бизнес-логика
|
||
handler/ ← HTTP-хендлеры (request/response)
|
||
model/ ← структуры данных (Go structs + JSON)
|
||
middleware/ ← JWT-аутентификация
|
||
bot/ ← Telegram бот
|
||
scheduler/ ← cron-задачи (напоминания)
|
||
```
|
||
|
||
**Стек:**
|
||
- Router: `go-chi/chi v5`
|
||
- ORM: `jmoiron/sqlx` (raw SQL + named queries)
|
||
- БД: PostgreSQL 16
|
||
- JWT: `golang-jwt/jwt v5`
|
||
- Telegram: `go-telegram-bot-api v5`
|
||
- Cron: `robfig/cron v3`
|
||
- Email: Resend API (`service/email.go`)
|
||
- Крипто: `golang.org/x/crypto` (bcrypt для паролей)
|
||
|
||
## Структура папок
|
||
|
||
```
|
||
pulse-api/
|
||
├── cmd/api/main.go # Точка входа: роутер, DI, запуск
|
||
├── internal/
|
||
│ ├── bot/
|
||
│ │ ├── bot.go # Инициализация, Start(), SendMessage()
|
||
│ │ └── handlers.go # Команды и callback кнопки
|
||
│ ├── config/
|
||
│ │ └── config.go # Config struct + Load() из env
|
||
│ ├── handler/
|
||
│ │ ├── auth.go # Register, Login, Refresh, Me, VerifyEmail...
|
||
│ │ ├── tasks.go # CRUD задач + complete/uncomplete
|
||
│ │ ├── habits.go # CRUD привычек + логи + статистика
|
||
│ │ ├── habit_freeze.go # Заморозка привычек
|
||
│ │ ├── finance.go # Категории и транзакции финансов
|
||
│ │ ├── savings.go # Накопления (категории, участники, планы)
|
||
│ │ ├── interest.go # Начисление процентов
|
||
│ │ ├── profile.go # Профиль пользователя
|
||
│ │ └── health.go # GET /health
|
||
│ ├── middleware/
|
||
│ │ └── auth.go # JWT Bearer middleware
|
||
│ ├── model/
|
||
│ │ ├── user.go # User, RegisterRequest, LoginRequest...
|
||
│ │ ├── task.go # Task, CreateTaskRequest...
|
||
│ │ ├── habit.go # Habit, HabitLog, HabitStats...
|
||
│ │ ├── habit_freeze.go # HabitFreeze
|
||
│ │ ├── finance.go # FinanceCategory, FinanceTransaction...
|
||
│ │ ├── savings.go # SavingsCategory, SavingsTransaction...
|
||
│ │ └── email.go # Email templates
|
||
│ ├── repository/
|
||
│ │ ├── db.go # NewDB() + RunMigrations()
|
||
│ │ ├── user.go # UserRepository
|
||
│ │ ├── task.go # TaskRepository
|
||
│ │ ├── habit.go # HabitRepository
|
||
│ │ ├── habit_freeze.go # HabitFreezeRepository
|
||
│ │ ├── finance.go # FinanceRepository
|
||
│ │ ├── savings.go # SavingsRepository
|
||
│ │ └── email_token.go # EmailTokenRepository
|
||
│ ├── service/
|
||
│ │ ├── auth.go # AuthService: register/login/JWT
|
||
│ │ ├── habit.go # HabitService
|
||
│ │ ├── task.go # TaskService
|
||
│ │ ├── finance.go # FinanceService
|
||
│ │ ├── interest.go # Начисление процентов
|
||
│ │ └── email.go # EmailService (Resend)
|
||
│ └── scheduler/
|
||
│ └── scheduler.go # Cron-напоминания через Telegram
|
||
├── docs/
|
||
│ └── SAVINGS.md
|
||
├── go.mod
|
||
├── Dockerfile
|
||
└── docker-compose.yml
|
||
```
|
||
|
||
## API Эндпоинты
|
||
|
||
### Публичные (без авторизации)
|
||
|
||
| Метод | Путь | Описание |
|
||
|-------|------|----------|
|
||
| GET | `/health` | Health check |
|
||
| POST | `/auth/register` | Регистрация: `{email, username, password}` |
|
||
| POST | `/auth/login` | Логин: `{email, password}` → `{access_token, refresh_token, user}` |
|
||
| POST | `/auth/refresh` | Обновление токена: `{refresh_token}` |
|
||
| POST | `/auth/verify-email` | Подтверждение email: `{token}` |
|
||
| POST | `/auth/resend-verification` | Повторная отправка: `{email}` |
|
||
| POST | `/auth/forgot-password` | Сброс пароля: `{email}` |
|
||
| POST | `/auth/reset-password` | Новый пароль: `{token, new_password}` |
|
||
|
||
### Авторизованные (Bearer JWT)
|
||
|
||
#### Аутентификация/Профиль
|
||
|
||
| Метод | Путь | Описание |
|
||
|-------|------|----------|
|
||
| GET | `/auth/me` | Текущий пользователь |
|
||
| PUT | `/auth/me` | Обновить профиль |
|
||
| PUT | `/auth/password` | Сменить пароль: `{old_password, new_password}` |
|
||
| GET | `/profile` | Профиль |
|
||
| PUT | `/profile` | Обновить профиль |
|
||
|
||
#### Задачи
|
||
|
||
| Метод | Путь | Описание |
|
||
|-------|------|----------|
|
||
| GET | `/tasks` | Список задач. Query: `?completed=true/false` |
|
||
| GET | `/tasks/today` | Задачи на сегодня |
|
||
| POST | `/tasks` | Создать: `{title, description?, icon?, color?, due_date?, priority?, reminder_time?, is_recurring?, recurrence_type?, recurrence_interval?, recurrence_end_date?}` |
|
||
| GET | `/tasks/{id}` | Получить задачу |
|
||
| PUT | `/tasks/{id}` | Обновить задачу |
|
||
| DELETE | `/tasks/{id}` | Удалить задачу |
|
||
| POST | `/tasks/{id}/complete` | Отметить выполненной |
|
||
| POST | `/tasks/{id}/uncomplete` | Снять отметку |
|
||
|
||
#### Привычки
|
||
|
||
| Метод | Путь | Описание |
|
||
|-------|------|----------|
|
||
| GET | `/habits` | Список привычек |
|
||
| POST | `/habits` | Создать: `{name, description?, color?, icon?, frequency, target_days?, target_count?, reminder_time?, start_date?}` |
|
||
| GET | `/habits/{id}` | Получить привычку |
|
||
| PUT | `/habits/{id}` | Обновить |
|
||
| DELETE | `/habits/{id}` | Удалить |
|
||
| POST | `/habits/{id}/log` | Отметить: `{date?, count?, note?}` |
|
||
| GET | `/habits/{id}/logs` | История отметок |
|
||
| DELETE | `/habits/{id}/logs/{logId}` | Удалить отметку |
|
||
| GET | `/habits/stats` | Общая статистика |
|
||
| GET | `/habits/{id}/stats` | Статистика привычки |
|
||
| GET | `/habits/{id}/freezes` | Заморозки привычки |
|
||
| POST | `/habits/{id}/freezes` | Создать заморозку |
|
||
| DELETE | `/habits/{id}/freezes/{freezeId}` | Удалить заморозку |
|
||
|
||
#### Финансы
|
||
|
||
| Метод | Путь | Описание |
|
||
|-------|------|----------|
|
||
| GET | `/finance/categories` | Категории расходов/доходов |
|
||
| POST | `/finance/categories` | Создать: `{name, emoji?, type, budget?, color?, sort_order?}` |
|
||
| PUT | `/finance/categories/{id}` | Обновить |
|
||
| DELETE | `/finance/categories/{id}` | Удалить |
|
||
| GET | `/finance/transactions` | Транзакции. Query: `?month=&year=` |
|
||
| POST | `/finance/transactions` | Создать: `{category_id, type, amount, description?, date}` |
|
||
| PUT | `/finance/transactions/{id}` | Обновить |
|
||
| DELETE | `/finance/transactions/{id}` | Удалить |
|
||
| GET | `/finance/summary` | Сводка: баланс, доходы, расходы, по категориям |
|
||
| GET | `/finance/analytics` | Аналитика: тренды по месяцам |
|
||
|
||
#### Накопления
|
||
|
||
| Метод | Путь | Описание |
|
||
|-------|------|----------|
|
||
| GET | `/savings/categories` | Категории накоплений |
|
||
| POST | `/savings/categories` | Создать категорию |
|
||
| GET | `/savings/categories/{id}` | Получить |
|
||
| PUT | `/savings/categories/{id}` | Обновить |
|
||
| DELETE | `/savings/categories/{id}` | Удалить |
|
||
| GET | `/savings/categories/{id}/members` | Участники |
|
||
| POST | `/savings/categories/{id}/members` | Добавить участника |
|
||
| DELETE | `/savings/categories/{id}/members/{userId}` | Удалить участника |
|
||
| GET | `/savings/categories/{id}/recurring-plans` | Регулярные планы |
|
||
| POST | `/savings/categories/{id}/recurring-plans` | Создать план |
|
||
| PUT | `/savings/recurring-plans/{planId}` | Обновить план |
|
||
| DELETE | `/savings/recurring-plans/{planId}` | Удалить план |
|
||
| GET | `/savings/transactions` | Транзакции накоплений |
|
||
| POST | `/savings/transactions` | Создать транзакцию |
|
||
| GET | `/savings/transactions/{id}` | Получить |
|
||
| PUT | `/savings/transactions/{id}` | Обновить |
|
||
| DELETE | `/savings/transactions/{id}` | Удалить |
|
||
| GET | `/savings/stats` | Статистика |
|
||
|
||
## Модели данных
|
||
|
||
### User
|
||
```go
|
||
type User struct {
|
||
ID int64
|
||
Email string
|
||
Username string
|
||
PasswordHash string // bcrypt, в JSON скрыто
|
||
EmailVerified bool
|
||
TelegramChatID *int64 // nullable
|
||
NotificationsEnabled bool
|
||
Timezone string
|
||
MorningReminderTime string // "09:00"
|
||
EveningReminderTime string // "21:00"
|
||
CreatedAt, UpdatedAt time.Time
|
||
}
|
||
```
|
||
|
||
### Task
|
||
```go
|
||
type Task struct {
|
||
ID, UserID int64
|
||
Title, Description string
|
||
Icon, Color string
|
||
DueDate *string // "2026-01-01"
|
||
Priority int // 1=низкий, 2=средний, 3=высокий
|
||
ReminderTime *string // "19:00"
|
||
Completed bool // производное от CompletedAt
|
||
// Повторяющиеся задачи:
|
||
IsRecurring bool
|
||
RecurrenceType *string // "daily", "weekly", "monthly"
|
||
RecurrenceInterval int
|
||
RecurrenceEndDate *string
|
||
ParentTaskID *int64
|
||
CreatedAt, UpdatedAt time.Time
|
||
}
|
||
```
|
||
|
||
### Habit
|
||
```go
|
||
type Habit struct {
|
||
ID, UserID int64
|
||
Name, Description string
|
||
Color, Icon string
|
||
Frequency string // "daily", "weekly", "custom"
|
||
TargetDays []int // дни недели (0=вс...6=сб)
|
||
TargetCount int
|
||
ReminderTime *string // "19:00"
|
||
StartDate *string
|
||
IsArchived bool
|
||
CreatedAt, UpdatedAt time.Time
|
||
}
|
||
|
||
type HabitLog struct {
|
||
ID, HabitID, UserID int64
|
||
Date time.Time
|
||
Count int
|
||
Note string
|
||
CreatedAt time.Time
|
||
}
|
||
```
|
||
|
||
### FinanceCategory / FinanceTransaction
|
||
```go
|
||
type FinanceCategory struct {
|
||
ID, UserID int64
|
||
Name, Emoji string
|
||
Type string // "income" | "expense"
|
||
Budget *float64
|
||
Color string
|
||
SortOrder int
|
||
CreatedAt time.Time
|
||
}
|
||
|
||
type FinanceTransaction struct {
|
||
ID, UserID, CategoryID int64
|
||
Type string // "income" | "expense"
|
||
Amount float64
|
||
Description string
|
||
Date time.Time
|
||
CreatedAt time.Time
|
||
CategoryName, CategoryEmoji string // из JOIN
|
||
}
|
||
```
|
||
|
||
## Аутентификация
|
||
|
||
- **JWT HS256** с двумя типами токенов: `access` (короткий) и `refresh` (длинный)
|
||
- Middleware `Authenticate` парсит `Authorization: Bearer <token>`, проверяет `type == "access"`
|
||
- UserID извлекается из claims и помещается в context: `GetUserID(ctx)`
|
||
- Пароли хешируются bcrypt
|
||
- Email-верификация при регистрации через Resend API
|
||
- Password reset — одноразовые токены в таблице `email_tokens`
|
||
|
||
## Telegram Бот
|
||
|
||
**Команды:**
|
||
| Команда | Действие |
|
||
|---------|---------|
|
||
| `/start` | Показать Chat ID (для привязки к аккаунту Pulse) |
|
||
| `/tasks` | Список задач на сегодня с inline-кнопками |
|
||
| `/habits` | Привычки на сегодня с inline-кнопками |
|
||
| `/done <id>` или `/done_<id>` | Отметить задачу выполненной |
|
||
| `/check <id>` или `/check_<id>` | Отметить привычку |
|
||
| `/help` | Справка |
|
||
|
||
**Callback-кнопки:**
|
||
- `donetask_<id>` — выполнить задачу
|
||
- `deltask_<id>` — удалить задачу
|
||
- `checkhabit_<id>` — отметить привычку сегодня
|
||
- `checkhabit_<id>_yesterday` — отметить привычку за вчера
|
||
|
||
**Привязка:** пользователь вводит `/start`, получает Chat ID, вставляет его в настройки Pulse → `PUT /profile {telegram_chat_id: ...}`
|
||
|
||
## Scheduler (cron-напоминания)
|
||
|
||
`internal/scheduler/scheduler.go` — использует `robfig/cron`:
|
||
- Утренние напоминания (время из настроек пользователя)
|
||
- Вечерние напоминания
|
||
- Напоминания о задачах по `reminder_time`
|
||
- Напоминания о привычках по `reminder_time`
|
||
|
||
## Конфигурация (env переменные)
|
||
|
||
| Переменная | Описание | Default |
|
||
|-----------|----------|---------|
|
||
| `DATABASE_URL` | PostgreSQL DSN | `postgres://homelab:homelab@db:5432/homelab` |
|
||
| `JWT_SECRET` | Секрет для JWT | `change-me-in-production` |
|
||
| `PORT` | Порт сервера | `8080` |
|
||
| `RESEND_API_KEY` | API ключ Resend (email) | — |
|
||
| `FROM_EMAIL` | Email отправителя | `noreply@digital-home.site` |
|
||
| `FROM_NAME` | Имя отправителя | `Homelab` |
|
||
| `APP_URL` | Публичный URL приложения | `https://api.digital-home.site` |
|
||
| `TELEGRAM_BOT_TOKEN` | Токен Telegram бота | — |
|
||
|
||
## Где искать что
|
||
|
||
| Задача | Файл |
|
||
|--------|------|
|
||
| Добавить новый эндпоинт | `handler/<domain>.go` + роут в `cmd/api/main.go` |
|
||
| Изменить модель/таблицу | `model/<domain>.go` + `repository/db.go` (migrations) |
|
||
| Логика уведомлений | `internal/scheduler/scheduler.go` |
|
||
| Telegram команды | `internal/bot/handlers.go` |
|
||
| Финансы (категории/транзакции) | `handler/finance.go`, `service/finance.go`, `repository/finance.go` |
|
||
| Привычки | `handler/habits.go`, `service/habit.go`, `repository/habit.go` |
|
||
| Задачи | `handler/tasks.go`, `service/task.go`, `repository/task.go` |
|
||
| Накопления | `handler/savings.go`, `repository/savings.go` |
|
||
| Email отправка | `service/email.go` |
|
||
| JWT/Auth | `service/auth.go`, `middleware/auth.go` |
|
||
| Конфиг | `config/config.go` |
|