Сервис для управления задачами с HTTP API на Go.
- Go
1.23+ - Docker и Docker Compose
docker compose up --build
docker compose exec ollama ollama pull qwen2.5:3b # опционально, если хотим использовать llm
docker compose up --build # опционально, если хотим использовать llmПосле запуска сервис будет доступен по адресу http://localhost:8080.
Если postgres уже запускался ранее со старой схемой, пересоздай volume:
docker compose down -v
docker compose up --buildПричина в том, что SQL-файл из migrations/0001_create_tasks.up.sql монтируется в docker-entrypoint-initdb.d и применяется только при инициализации пустого data volume.
GitHub Actions: lint - race - test - build на каждый push в develop
A[Push/PR] --> B[quality]
A --> C[test]
B --> D # Все зелёные?
C --> D
D -->|Yes | E[docker: build & push]
D -->|No | F[Fail]| quality | - стиль кода, потенциальные баги. golangci-lint, gosec
| test | - прогонка тестов. go test -race -cover.
| docker | - сборка образа, кэширование слоёв, мультиарх. buildx, cache gha, linux/amd64,arm64
Swagger UI:
http://localhost:8080/swagger/
OpenAPI JSON:
http://localhost:8080/swagger/openapi.json
Базовый префикс API:
/api/v1
Основные маршруты:
POST /api/v1/tasksGET /api/v1/tasksGET /api/v1/tasks/{id}PUT /api/v1/tasks/{id}DELETE /api/v1/tasks/{id}POST /api/v1/tasks/parse-recurrence-- LLM-парсинг и создание задачиPOST /api/v1/tasks/:id/tags-- Добавить теги к задачеDELETE /api/v1/tasks/:id/tags-- Удалить теги из задачиPOST /api/v1/tasks/:id/transfer-- Перенести задачу (смена дедлайна/ответственного)GET /metrics
Grafana http://localhost:3000 admin/adminPrometheus http://localhost:9090Метрики http://localhost:8080/metricstaskservice_http_requests_totalRPS по эндпоинтамtaskservice_http_request_duration_secondsзадержкаtaskservice_cron_materialized_totalзадачи, созданные крономtaskservice_cleanup_deleted_totalзадачи, удалённые очисткой
- Junction-таблица
task_tags(task_id, tag_id)с жёсткимиFKиON DELETE CASCADE. - Составной B-tree индекс
(tag_id, task_id)обеспечиваетIndex Only Scanпри фильтрации и JOIN. - LLM-парсер автоматически извлекает теги из контекста:
"обход палаты, срочно, кардиология"→tags: ["urgent", "cardiology"].
- Эндпоинт
POST /api/v1/tasks/:id/transferпринимает{"new_due_date": "RFC3339", "new_assignee": "uuid"}. - Валидация инвариантов:
- Экземпляры нельзя переносить дальше даты окончания шаблона (
until). - При смене дедлайна крон автоматически скорректирует расписание на ближайший тик.
- Все переносы логируются в
task_audit_log(кто, когда, какие поля изменил).
- Экземпляры нельзя переносить дальше даты окончания шаблона (
- Безопасность: Операция проходит через
Updateusecase, где проверяются права, статус задачи и доменные ограничения.
Сервис понимает свободный текст на русском языке и автоматически преобразует его в структурированную задачу с настройками периодичности.
- LLM-парсинг: Текст отправляется в локальную модель (Ollama) с кастомным системным промптом. Извлекаются заголовок, описание, дедлайн и правила повторения.
- Доменная валидация: Данные проходят проверку инвариантов в UseCase и сохраняются в PostgreSQL.
| Фраза пользователя | Распознанный тип | Конфигурация |
|---|---|---|
"обойти Иванова каждые 3 дня" |
daily |
interval_days: 3 |
"проверять давление по нечётным числам" |
even_odd |
parity: "odd" |
"визит 15 и 20 числа" |
yearly |
dates_of_year: ["01-15", "05-20"] |
"ежедневно в 09:00" |
daily |
interval_days: 1, due_date: ... |
"бухнуть 1 января" |
- |
due_date: ... |
{
"id": 42,
"title": "Сикс-севен",
"description": "делать сикс-севен 6 марта и 7 апреля каждого года",
"is_recurrence": true,
"recurrence_type": "yearly",
"recurrence_config": { "dates_of_year": ["03-06", "04-07"] },
"status": "new",
"due_date": "2026-04-18T20:00:00Z"
}# Запустить smoke-тесты
./test.sh
# Юнит-тесты
go test ./... -vЕсли создать задачу с is_recurrence: true, она становится родительским шаблоном. Инстансы не создаются сразу — они генерируются лениво, в момент запроса списка задач на нужный период.
Пример создания ежедневной задачи:
POST /api/v1/tasks
{
"title": "six seven",
"due_date": "2026-05-01T10:00:00Z",
"is_recurrence": true,
"recurrence_type": "daily",
"recurrence_config": { "interval_days": 1 }
}При запросе GET /api/v1/tasks?from=...&to=... сервис:
- Находит активные шаблоны, у которых due_date <= to
- Считает ожидаемые даты через CalculateRecurrenceDates
- Сверяет с уже существующими экземплярами в БД
- Вставляет недостающие (через BatchInsert с ON CONFLICT DO NOTHING)
- Возвращает задачи с курсорной пагинацией
Экземпляры становятся обычными нерекурентными:
is_recurrence: falserecurrence_parent_id- ID шаблонаrecurrence_typeиrecurrence_config-nullstatus=newпри создании
| Тип | Конфиг | Пример |
|---|---|---|
daily |
{"interval_days": 3} |
Каждые 3 дня от базовой даты |
monthly |
{"days_of_month": [1, 15, 31]} |
1, 15 и 31-го числа; несуществующие дни (31 февраля) переносятся на соответсвующее число (3 марта) |
specific |
на конкретные даты — задачи создаются только на указанные даты | |
even_odd |
{"parity": "even"} |
Чётные или нечётные дни месяца |
GET /api/v1/tasks?from=...&to=...&limit=...&cursor=...
| Параметр | По умолчанию | Описание |
|---|---|---|
from |
сегодня | Начало диапазона (RFC3339) |
to |
завтра | Конец диапазона (макс. +365 дней от from) |
limit |
31 | Количество задач (1–100) |
cursor |
- | Токен для следующей страницы, берётся из ответа |
- Concurrency
errgroupв материализации тасков, lock-free счётчики, изоляция ошибок родителей. - Одна таблица
tasks. Шаблоны и экземпляры хранятся вместе, различаются по полямis_recurrenceиrecurrence_parent_id. Это упрощает выборку, обновление и удаление. - Лимит диапазона
365дней. Защита от случайных или злонамеренных запросов на годы вперёд. - Защита от дублей. Уникальный индекс
(recurrence_parent_id, due_date) + ON CONFLICT DO NOTHINGгарантирует, что рестарт сервиса или параллельные запросы не создадут дубликаты. - Удаление шаблона удаляет экземпляры автоматически.
- Сервис запускает фоновый процесс, который каждую минуту проверяет активные шаблоны на ближайшие 48 часов и создаёт недостающие экземпляры. Это снижает задержку при первом открытии календаря пользователем.
- Ошибки материализации не прерывают фоновый цикл и не влияют на HTTP-запросы.
- Фоновая материализация работает только в окне
[now, now+48h]. Запросы в далёкое будущее обрабатываются синхронно при вызовеGET /tasks. - Очистка данных - независимая горутина, batch-удаление
- Специально не обновлял golang, так как в реальных проектах это может быть невыгодно, для начала нужно все обсудить с командой. На примере этого хотел показать качество командной работы и понимания рисков.
Проект готов к демо и тестированию, но для production-эксплуатации запланированы следующие улучшения:
- Автоматический провижининг Grafana-дашбордов (сейчас
grafana/dashboards/taskservice.jsonимпортируется вручную через UI) - Интеграция Alertmanager с правилами алертинга (высокий latency, рост 5xx, падение cron-материализации)
- Кастомные бизнес-метрики (успешность NLU-парсинга, время генерации экземпляров, hit/miss rate кэша)
- JWT/OAuth2 middleware с ролевой моделью
- Аудит-логирование изменений задач (кто, что, когда поменял)
- Rate limiting & IP throttling для публичных эндпоинтов
- Интеграционные тесты через Testcontainers (PostgreSQL + Ollama без ручного
docker compose)
- Оценка качества парсинга (precision на датасете реальных медицинских фраз)
- Версионирование промптов и A/B тестирование шаблонов
- Real-time уведомления о новых экземплярах задач
- Расширенная фильтрация
GET /tasks - Поддержка пользовательских временных зон
50% тестовое покрытие сервиса
Быстро вышел за рамки требований, так как нужно выделиться среди других кандидатов. Для этого составил роадмапу. Быстро прошелся по ней и перешел к интеграции с LLM. Взял готовую модельку, и менял чтобы найти наилучшую. Все равно моделька часто тупит, пытался по разному менять промпт, но думаю для тестового достаточно такой модельки.