Skip to content
 
 

Repository files navigation

Go Version CI/CD Go Report Card

Task Service

Сервис для управления задачами с HTTP API на Go.

Требования

  • Go 1.23+
  • Docker и Docker Compose

Быстрый запуск через 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.

CI/CD

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

Swagger UI:

http://localhost:8080/swagger/

OpenAPI JSON:

http://localhost:8080/swagger/openapi.json

API

Базовый префикс API:

/api/v1

Основные маршруты:

  • POST /api/v1/tasks
  • GET /api/v1/tasks
  • GET /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/admin
  • Prometheus http://localhost:9090
  • Метрики http://localhost:8080/metrics
  • taskservice_http_requests_total RPS по эндпоинтам
  • 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 (кто, когда, какие поля изменил).
  • Безопасность: Операция проходит через Update usecase, где проверяются права, статус задачи и доменные ограничения.

LLM по созданию задач

Сервис понимает свободный текст на русском языке и автоматически преобразует его в структурированную задачу с настройками периодичности.

  1. LLM-парсинг: Текст отправляется в локальную модель (Ollama) с кастомным системным промптом. Извлекаются заголовок, описание, дедлайн и правила повторения.
  2. Доменная валидация: Данные проходят проверку инвариантов в 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=... сервис:

  1. Находит активные шаблоны, у которых due_date <= to
  2. Считает ожидаемые даты через CalculateRecurrenceDates
  3. Сверяет с уже существующими экземплярами в БД
  4. Вставляет недостающие (через BatchInsert с ON CONFLICT DO NOTHING)
  5. Возвращает задачи с курсорной пагинацией

Экземпляры становятся обычными нерекурентными:

  • is_recurrence: false
  • recurrence_parent_id - ID шаблона
  • recurrence_type и recurrence_config - null
  • status = 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-эксплуатации запланированы следующие улучшения:

Monitoring

  • Автоматический провижининг Grafana-дашбордов (сейчас grafana/dashboards/taskservice.json импортируется вручную через UI)
  • Интеграция Alertmanager с правилами алертинга (высокий latency, рост 5xx, падение cron-материализации)
  • Кастомные бизнес-метрики (успешность NLU-парсинга, время генерации экземпляров, hit/miss rate кэша)

JWT

  • JWT/OAuth2 middleware с ролевой моделью
  • Аудит-логирование изменений задач (кто, что, когда поменял)
  • Rate limiting & IP throttling для публичных эндпоинтов

DevOps

  • Интеграционные тесты через Testcontainers (PostgreSQL + Ollama без ручного docker compose)

LLM

  • Оценка качества парсинга (precision на датасете реальных медицинских фраз)
  • Версионирование промптов и A/B тестирование шаблонов

Features

  • Real-time уведомления о новых экземплярах задач
  • Расширенная фильтрация GET /tasks
  • Поддержка пользовательских временных зон

50% тестовое покрытие сервиса

Комментарий

Быстро вышел за рамки требований, так как нужно выделиться среди других кандидатов. Для этого составил роадмапу. Быстро прошелся по ней и перешел к интеграции с LLM. Взял готовую модельку, и менял чтобы найти наилучшую. Все равно моделька часто тупит, пытался по разному менять промпт, но думаю для тестового достаточно такой модельки.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages