docs: инфраструктура VM Сервисы + архитектура pulse-api и pulse-web
This commit is contained in:
337
Инфраструктура/pulse-api.md
Normal file
337
Инфраструктура/pulse-api.md
Normal file
@@ -0,0 +1,337 @@
|
||||
# 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` |
|
||||
Reference in New Issue
Block a user