REST API сервис для управления задачами в командах с поддержкой ролевой модели, истории изменений и сложными SQL-запросами.
| Компонент | Технология |
|---|---|
| Язык | Go 1.26 |
| БД | MySQL 8 |
| Кеш | Redis 7 |
| Контейнеризация | Docker + Docker Compose |
| Аутентификация | JWT (golang-jwt/jwt/v5) |
| Метрики | Prometheus (prometheus/client_golang) |
| Rate Limiting | golang.org/x/time/rate |
| Circuit Breaker | sony/gobreaker |
| Миграции | pressly/goose |
| Тесты | go.uber.org/mock, dockertest |
users — Пользователи
teams — Команды
team_members — Связь пользователь ↔ команда (M:N) + роль
tasks — Задачи
task_history — История изменений задач (аудит)
task_comments — Комментарии к задачам
teams.created_by → users.id
team_members.user_id → users.id
team_members.team_id → teams.id
tasks.assignee_id → users.id
tasks.team_id → teams.id
tasks.created_by → users.id
task_history.task_id → tasks.id
task_history.changed_by → users.id
task_comments.task_id → tasks.id
task_comments.user_id → users.id
idx_tasks_team_status— (team_id, status)idx_tasks_assignee_status— (assignee_id, status)uq_team_member— (team_id, user_id) UNIQUE
Проект построен на трёхуровневой архитектуре (Handler → Service → Store) с ручным constructor injection — без DI-контейнеров, ORM или лишних абстракций. Каждый слой имеет ровно одну ответственность и тестируется независимо.
- Handler Layer — принимает HTTP-запросы, парсит параметры, возвращает JSON. Без бизнес-логики. Хендлер получает сервис через конструктор.
- Service Layer — бизнес-логика: валидация прав (проверка членства в команде, ролевая модель), координация между store-запросами. Нет прямого доступа к БД — только через Store interface.
- Store Layer — доступ к данным (MySQL) и кеш (Redis). Чистые SQL-запросы без ORM. Единственный слой, который знает о БД.
Сборка компонентов происходит в main() — это единственное место, где слои пересекаются:
main()
→ config.Load() # конфигурация (YAML + ENV)
→ initDB(), initRedis() # подключение к MySQL + Redis
→ NewUserStore(db) # Store layer
→ NewTeamStore(db, rdb)
→ NewTaskStore(db, rdb)
→ NewReportStore(db)
→ NewAuthService(userStore) # Service layer (через интерфейсы)
→ NewTeamService(teamStore)
→ NewTaskService(taskStore)
→ NewAuthHandler(authSvc) # Handler layer (через сервисы)
→ NewTeamHandler(teamSvc)
→ NewTaskHandler(taskSvc)
→ NewReportHandler(reportStore)
→ registerRoutes() # привязка к http.ServeMux
→ runServer() # запуск + graceful shutdown
Разделение подтверждено статическим анализом: три слоя формируют изолированные кластеры зависимостей, а main() выступает единственным «мостом» между ними.
┌─────────────────────────────────────────────────────────┐
│ HTTP Request │
└─────────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Handler Layer │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Auth │ │ Team │ │ Task │ │ Report │ │
│ │ Handler │ │ Handler │ │ Handler │ │ Handler │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │ │
│ ┌────┴─────────────┴────────────┴─────────────┴────┐ │
│ │ Middleware Layer │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ │
│ │ │ Auth JWT │ │ Rate │ │ Prometheus │ │ │
│ │ │ │ │ Limiter │ │ Metrics │ │ │
│ │ └──────────┘ └──────────┘ └──────────────────┘ │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Service Layer │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Auth │ │ Team │ │ Task │ │
│ │ Service │ │ Service │ │ Service │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
└───────┼─────────────┼────────────┼──────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────┐
│ Store Layer │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ User │ │ Team │ │ Task │ │ Report │ │
│ │ Store │ │ Store │ │ Store │ │ Store │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │ │
│ ┌────┴─────────────┴────────────┴─────────────┴────┐ │
│ │ MySQL + Redis Cache (TTL 5 мин) │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
Клиент → HTTP Request
│
▼
┌─────────────────┐
│ withLogging() │ ← Логирование метода, пути, длительности
└────────┬────────┘
│
▼
┌─────────────────┐
│ Metrics.Middleware │ ← Подсчёт запросов, ошибок, длительности
└────────┬────────┘
│
▼
┌─────────────────┐
│ Auth.Middleware │ ← Валидация JWT, извлечение userID
└────────┬────────┘
│
▼
┌─────────────────┐
│ RateLimiter │ ← Проверка лимита (100 req/min/user)
│ .Middleware │
└────────┬────────┘
│
▼
┌─────────────────┐
│ HandlerFunc │ ← Бизнес-логика запроса
└────────┬────────┘
│
▼
┌─────────────────┐
│ Service │ ← Валидация, проверка прав, трансформация
└────────┬────────┘
│
▼
┌─────────────────┐
│ Store │ ← SQL-запросы, кеш Redis
└────────┬────────┘
│
▼
┌─────────────────┐
│ MySQL / Redis │
└─────────────────┘
┌──────────────────┐
│ main() start │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Загрузка │ ← config.yaml + ENV overrides
│ конфигурации │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Подключение │ ← sql.Open + SetMaxOpenConns/IdleConns
│ к MySQL │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Запуск │ ← goose.Up (автоматические миграции)
│ миграций │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Подключение │ ← redis.NewClient
│ к Redis │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Инициализация │ ← Store → Service → Handler
│ компонентов │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Регистрация │ ← mux.HandleFunc / mux.Handle
│ маршрутов │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Запуск HTTP │ ← srv.ListenAndServe()
│ сервера │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Ожидание │ ← signal.Notify(SIGINT, SIGTERM)
│ сигнала │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Graceful │ ← srv.Shutdown(ctx) + Close DB/Redis
│ Shutdown │
└──────────────────┘
| Метод | Путь | Описание |
|---|---|---|
| POST | /api/v1/register |
Регистрация нового пользователя |
| POST | /api/v1/login |
Аутентификация, возврат JWT |
| Метод | Путь | Описание | Доступ |
|---|---|---|---|
| POST | /api/v1/teams |
Создать команду (создатель = owner) | Авторизован |
| GET | /api/v1/teams |
Список команд пользователя | Авторизован |
| POST | /api/v1/teams/{id}/invite |
Пригласить пользователя | owner/admin |
| Метод | Путь | Описание | Доступ |
|---|---|---|---|
| POST | /api/v1/tasks |
Создать задачу | Член команды |
| GET | /api/v1/tasks |
Список задач (фильтрация + пагинация) | Авторизован |
| PUT | /api/v1/tasks/{id} |
Обновить задачу | Член команды |
| GET | /api/v1/tasks/{id}/history |
История изменений | Член команды |
Параметры фильтрации задач:
team_id— ID командыstatus— статус (todo, in_progress, done)assignee_id— ID исполнителяpage— номер страницы (по умолчанию 1)per_page— элементов на странице (по умолчанию 20)
| Метод | Путь | Описание | Доступ |
|---|---|---|---|
| GET | /api/v1/teams/{id}/dashboard |
Дашборд команды | Авторизован |
| GET | /api/v1/reports/top-users |
Топ-3 создателей задач за месяц | Авторизован |
| GET | /api/v1/reports/inefficient-tasks |
Задачи с нарушением целостности | Авторизован |
| Метод | Путь | Описание |
|---|---|---|
| GET | /health |
Health check |
| GET | /metrics |
Prometheus метрики |
-- Количество задач по статусам
SELECT status, COUNT(*) as cnt FROM tasks WHERE team_id = ? GROUP BY status
-- Задачи done за 7 дней
SELECT COUNT(*) FROM tasks
WHERE team_id = ? AND status = 'done'
AND updated_at >= NOW() - INTERVAL 7 DAY
-- Количество участников команды
SELECT COUNT(*) FROM team_members WHERE team_id = ?
-- Статистика по пользователям (JOIN users, team_members, tasks)
SELECT u.id, u.name, COUNT(t.id) as task_count
FROM users u
JOIN team_members tm ON tm.user_id = u.id
LEFT JOIN tasks t ON t.assignee_id = u.id AND t.team_id = ?
WHERE tm.team_id = ?
GROUP BY u.id, u.name
ORDER BY task_count DESCSELECT team_id, user_id, user_name, task_count, pos
FROM (
SELECT
t.team_id,
t.created_by as user_id,
u.name as user_name,
COUNT(*) as task_count,
ROW_NUMBER() OVER (PARTITION BY t.team_id ORDER BY COUNT(*) DESC) as pos
FROM tasks t
JOIN users u ON u.id = t.created_by
WHERE t.created_at >= DATE_FORMAT(NOW(), '%Y-%m-01')
GROUP BY t.team_id, t.created_by, u.name
) ranked
WHERE pos <= 3
ORDER BY team_id, pos-- Задачи, где assignee не является членом команды
SELECT t.id, t.title, t.assignee_id, u.name, t.team_id, tm.name
FROM tasks t
JOIN users u ON u.id = t.assignee_id
JOIN teams tm ON tm.id = t.team_id
WHERE t.assignee_id IS NOT NULL
AND t.assignee_id NOT IN (
SELECT tm2.user_id FROM team_members tm2 WHERE tm2.team_id = t.team_id
)| Компонент | Реализация |
|---|---|
| Redis кеш | Список команд пользователя + список задач команды, TTL 5 мин, инвалидация при мутациях |
| Индексы | 16 индексов, включая составные (team_id, status), (assignee_id, status) |
| Connection pooling | MaxOpenConns=25, MaxIdleConns=10, MaxLifetime=300с |
| Пагинация | LIMIT/OFFSET на уровне БД |
# Все тесты
go test ./...
# Только service layer
go test ./internal/service/...
# Только handler layer
go test ./internal/handler/...
# С coverage
go test ./internal/... -cover
# Интеграционные тесты (требуют Docker)
go test ./internal/store/...| Слой | Coverage |
|---|---|
| service/ | 87.6% |
| handler/ | 81.5% |
| lib/ | 92.6% |
| config/ | 80.0% |
| store/ | 47.8% (интеграционные) |
cmd/api/
main.go — Точка входа, wire-up компонентов
db.go — initDB, initRedis, connection pool
server.go — registerRoutes, runServer, shutdown, withLogging
migrations/ — SQL миграции (goose)
internal/
config/
config.go — YAML/ENV конфигурация
handler/
auth.go — HTTP хендлеры аутентификации
team.go — HTTP хендлеры команд
task.go — HTTP хендлеры задач
report.go — HTTP хендлеры отчётов
metrics.go — Prometheus метрики
ratelimit.go — Rate limiter (token bucket)
lib/
jwt.go — Генерация/валидация JWT
email.go — Circuit breaker + mock email sender
migrate/
migrate.go — Запуск миграций (goose)
service/
auth.go — Бизнес-логика аутентификации
team.go — Бизнес-логика команд
task.go — Бизнес-логика задач
interfaces.go — Интерфейсы для тестирования
errors.go — Бизнес-ошибки
store/
user.go — DAO пользователей (MySQL)
team.go — DAO команд (MySQL + Redis кеш)
task.go — DAO задач (MySQL)
report.go — SQL-запросы отчётов
docker-compose.yml — Локальный стек (MySQL, Redis, phpMyAdmin)
config.example.yaml — Шаблон конфигурации
| Компонент | Минимальная версия | Проверка |
|---|---|---|
| Go | 1.26+ | go version |
| Docker | 20.10+ | docker --version |
| Docker Compose | 2.0+ | docker compose version |
| Git | 2.30+ | git --version |
git clone https://github.com/turkprogrammer/taskflow
cd task_flowgo mod downloadcp config.example.yaml config.yamlОтредактируйте config.yaml при необходимости:
server:
port: 8080 # Порт API сервера
db:
host: localhost # Адрес MySQL
port: 3306 # Порт MySQL
user: taskflow # Пользователь MySQL
password: taskflow # Пароль MySQL
name: taskflow # Имя базы данных
max_open_conns: 25 # Максимум открытых соединений
max_idle_conns: 10 # Максимум idle соединений
max_lifetime_seconds: 300 # Время жизни соединения (сек)
redis:
addr: localhost:6379 # Адрес Redis
password: "" # Пароль Redis (пусто если нет)
db: 0 # Номер БД Redis
jwt:
secret: change-me-in-production # Секрет для подписи JWT
ttl_hours: 72 # Время жизни токена (часы)Переменные окружения перезаписывают значения из YAML:
| Переменная | Описание | Значение по умолчанию |
|---|---|---|
DB_HOST |
Адрес MySQL | localhost |
DB_PORT |
Порт MySQL | 3306 |
DB_USER |
Пользователь MySQL | taskflow |
DB_PASSWORD |
Пароль MySQL | taskflow |
DB_NAME |
Имя базы данных | taskflow |
REDIS_ADDR |
Адрес Redis | localhost:6379 |
REDIS_PASSWORD |
Пароль Redis | (пусто) |
JWT_SECRET |
Секрет JWT | change-me-in-production |
SERVER_PORT |
Порт сервера | 8080 |
docker compose up -dЭто запустит:
- MySQL 8 на порту
3306 - Redis 7 на порту
6379 - phpMyAdmin на порту
8081(для управления БД)
Проверка статуса контейнеров:
docker compose psgo run ./cmd/api# Health check
curl http://localhost:8080/health
# Ответ: {"status":"ok"}
# Регистрация пользователя
curl -X POST http://localhost:8080/api/v1/register \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"secret123","name":"Test User"}'
# Авторизация
curl -X POST http://localhost:8080/api/v1/login \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"secret123"}'Пример запуска с переменными окружения:
DB_HOST=192.168.1.100 JWT_SECRET=my-secret go run ./cmd/apiMIT