REST-сервис для учета времени сотрудников по задачам.
Проект выполнен на Java 17, Spring Boot 3, MyBatis, PostgreSQL, Docker.
- Управление задачами (
Task):- создание задачи;
- получение задачи по
id; - получение списка всех задач;
- изменение статуса задачи (
NEW,IN_PROGRESS,DONE).
- Учет времени (
TimeRecord):- создание записи о начале работы;
- завершение записи (установка времени окончания);
- получение записи по
id; - получение записей сотрудника за период;
- получение всех записей по задаче.
- Валидация входных DTO через Bean Validation.
- Централизованная обработка ошибок через
@RestControllerAdvice. - OpenAPI/Swagger-документация.
- Unit-тесты (JUnit 5, Mockito/MyBatis).
Java 17Spring Boot 3.5.13MyBatisPostgreSQL 15MavenSpringDoc OpenAPI (Swagger UI)JUnit 5Docker,docker-compose
- Установлен и запущен Docker Desktop.
Из корня проекта выполните:
docker-compose up --buildПосле запуска доступны:
- API:
http://localhost:8080 - Swagger UI:
http://localhost:8080/swagger-ui.html - OpenAPI JSON:
http://localhost:8080/api-docs - PostgreSQL:
localhost:5432
Параметры БД (из docker-compose.yml):
- DB:
time_tracker - User:
postgres - Password:
postgres
docker-compose down- Запустить сервис командой
docker-compose up --build. - Открыть
http://localhost:8080/swagger-ui.html. - Выполнить запросы в порядке:
POST /api/tasks(создать задачу);PUT /api/tasks/{id}/status(изменить статус);POST /api/time-records(начать работу);PUT /api/time-records/{id}/end(завершить работу);GET /api/time-records/employee/{employeeId}(получить записи за период).
Создайте коллекцию и добавьте запросы ниже.
В проекте подготовлена коллекция Postman:
- Файл:
Time Tracker API.postman_collection.json - Название коллекции:
Time Tracker API
Как импортировать:
- Откройте Postman.
- Нажмите
Import. - Выберите файл
Time Tracker API.postman_collection.json. - Запустите контейнеры
docker-compose up --build. - Выполняйте запросы коллекции по порядку (сначала создание Task, потом TimeRecord).
Что уже есть в коллекции:
Create TaskGet Task by IDGet All TasksUpdate Task StatusStart WorkEnd WorkGet Employee Time RecordsGet Task Time Records
Примечание: в запросе Get Employee Time Records используется employeeId=100, а в Start Work — employeeId=23. Для корректной проверки отчета за период укажите одинаковый employeeId в обоих запросах.
- Method:
POST - URL:
http://localhost:8080/api/tasks - Body (JSON):
{
"title": "Подготовить отчет",
"description": "Сформировать недельный отчет по времени"
}- Method:
GET - URL:
http://localhost:8080/api/tasks/1
- Method:
GET - URL:
http://localhost:8080/api/tasks
- Method:
PUT - URL:
http://localhost:8080/api/tasks/1/status?status=IN_PROGRESS
Допустимые значения status: NEW, IN_PROGRESS, DONE.
- Method:
POST - URL:
http://localhost:8080/api/time-records - Body (JSON):
{
"employeeId": 1001,
"taskId": 1,
"startTime": "2026-04-21T10:00:00",
"description": "Начал выполнение задачи"
}- Method:
GET - URL:
http://localhost:8080/api/time-records/1
- Method:
PUT - URL:
http://localhost:8080/api/time-records/1/end - Body (JSON):
{
"endTime": "2026-04-21T12:30:00",
"description": "Работа завершена"
}- Method:
GET - URL:
http://localhost:8080/api/time-records/employee/1001?from=2026-04-01T00:00:00&to=2026-04-30T23:59:59
- Method:
GET - URL:
http://localhost:8080/api/time-records/task/1
В текущем Dockerfile используется сборка с -DskipTests, поэтому тесты не запускаются автоматически при docker-compose up --build.
- Убедитесь, что контейнер БД запущен:
docker-compose up -d db- Запустите тесты во временном Maven-контейнере:
docker run --rm -v "${PWD}:/app" -w /app --network time-tracker_default maven:3.9-eclipse-temurin-17 mvn testЕсли тесты прошли успешно, в конце будет BUILD SUCCESS.
docker exec -it time-tracker-db psql -U postgres -d time_trackerПолезные команды:
\dt— список таблицSELECT * FROM tasks;SELECT * FROM time_records;\q— выход
- Host:
localhost - Port:
5432 - Database:
time_tracker - User:
postgres - Password:
postgres
idtitledescriptionstatus(NEW,IN_PROGRESS,DONE)createdAt
idemployeeIdtaskIdstartTimeendTimedescription
src/main/java/.../controller— REST-контроллерыsrc/main/java/.../service— логика приложенияsrc/main/java/.../mapper— MyBatis mapperssrc/main/resources/mapper— SQL-маппинги MyBatissrc/main/resources/schema.sql— схема БДsrc/test/java— unit-тесты
- Назначение: точка входа Spring Boot приложения.
- Основной метод:
main(String[] args)— запускает приложение черезSpringApplication.run(...).
- Назначение: конфигурация OpenAPI/Swagger.
- Основной метод:
openAPI()— создает бинOpenAPIс метаданными API (title,version).
- Назначение: REST API для работы с задачами.
- Основная переменная:
taskService— сервисный слой задач.
- Основные методы:
createTask(TaskCreateRequest request)— создает задачу (POST /api/tasks).getTask(Long id)— получает задачу по ID (GET /api/tasks/{id}).getAllTasks()— получает список всех задач (GET /api/tasks).updateStatus(Long id, Task.TaskStatus status)— меняет статус (PUT /api/tasks/{id}/status).
- Назначение: REST API для учета рабочего времени.
- Основная переменная:
timeRecordService— сервисный слой записей времени.
- Основные методы:
startWork(TimeRecordRequest request)— создает запись времени (POST /api/time-records).endWork(Long id, TimeRecordUpdateRequest request)— завершает запись (PUT /api/time-records/{id}/end).getTimeRecord(Long id)— получает запись по ID (GET /api/time-records/{id}).getEmployeeTimeRecords(Long employeeId, LocalDateTime from, LocalDateTime to)— записи сотрудника за период (GET /api/time-records/employee/{employeeId}).getTaskTimeRecords(Long taskId)— записи по задаче (GET /api/time-records/task/{taskId}).
- Назначение: DTO для создания задачи.
- Основные поля:
title— название задачи; валидация@NotBlank,@Size(max = 255).description— описание; валидация@Size(max = 1000).
- Назначение: DTO для создания записи времени.
- Основные поля:
employeeId— ID сотрудника;@NotNull,@Positive.taskId— ID задачи;@NotNull,@Positive.startTime— время начала (может бытьnull, тогда ставится текущее время).description— описание работы;@Size(max = 500).
- Назначение: DTO для завершения записи времени.
- Основные поля:
endTime— время окончания (может бытьnull, тогда ставится текущее время).description— описание работы;@Size(max = 500).
- Назначение: глобальная обработка исключений REST API.
- Основные методы:
handleValidationExceptions(MethodArgumentNotValidException ex)— возвращает400и карту ошибок валидации по полям.handleNotFound(NoSuchElementException ex)— возвращает404.handleIllegalArgument(IllegalArgumentException ex)— возвращает400.handleGenericException(Exception ex)— возвращает500.handleResourceNotFound(ResourceNotFoundException ex)— возвращает404.
- Общий формат ответа:
timestamp,status,error,message/errors.
- Назначение: пользовательское исключение "ресурс не найден".
- Основной элемент:
- конструктор
ResourceNotFoundException(String message).
- конструктор
- Назначение: MyBatis mapper для таблицы
tasks. - Основные методы:
insert(Task task)— вставка задачи с генерацией ID.findById(Long id)— поиск задачи по ID.findAll()— список всех задач.updateStatus(Long id, Task.TaskStatus status)— обновление статуса.deleteById(Long id)— удаление задачи.
- Назначение: MyBatis mapper для таблицы
time_records. - Основные методы:
insert(TimeRecord timeRecord)— вставка записи времени.findById(Long id)— поиск записи по ID.findByEmployeeAndPeriod(Long employeeId, LocalDateTime from, LocalDateTime to)— записи сотрудника за период.updateEndTime(Long id, LocalDateTime endTime, String description)— завершение записи.findByTaskId(Long taskId)— записи по задаче.
- Назначение: модель задачи.
- Основные поля:
id,title,description,status,createdAt.
- Дополнительные элементы:
TaskStatus— enum статусов (NEW,IN_PROGRESS,DONE).- конструкторы: пустой и
Task(String title, String description). - геттеры/сеттеры всех полей.
toString()— строковое представление объекта.
- Назначение: модель записи рабочего времени.
- Основные поля:
id,employeeId,taskId,startTime,endTime,description.
- Дополнительные элементы:
- конструкторы: пустой и
TimeRecord(Long employeeId, Long taskId, LocalDateTime startTime, String description). - геттеры/сеттеры всех полей.
toString()— строковое представление объекта.
- конструкторы: пустой и
- Назначение: логика задач.
- Основная переменная:
taskMapper— доступ к данным задач.
- Основные методы:
createTask(Task task)— принудительно ставит статусNEW, сохраняет задачу, затем читает ее из БД.findById(Long id)— возвращает задачу по ID.findAll()— возвращает все задачи.updateStatus(Long id, Task.TaskStatus status)— обновляет статус задачи.
- Назначение: логика учета рабочего времени.
- Основная переменная:
timeRecordMapper— доступ к данным записей времени.
- Основные методы:
startWork(TimeRecord record)— при пустомstartTimeставит текущее время, сохраняет запись и читает ее из БД.endWork(Long id, LocalDateTime endTime, String description)— при пустомendTimeставит текущее время, завершает запись.findById(Long id)— возвращает запись по ID.findByEmployeeAndPeriod(Long employeeId, LocalDateTime from, LocalDateTime to)— возвращает записи сотрудника за период.findByTaskId(Long taskId)— возвращает записи по задаче.
- Назначение: smoke-тест поднятия Spring-контекста.
- Основной метод:
contextLoads()— проверяет, что контекст приложения стартует без ошибок.
- Подпись в документации:
contextLoads()→ "Контекст Spring Boot успешно поднимается".
- Назначение: веб-тесты контроллера задач (
MockMvc,@WebMvcTest). - Основные переменные:
mockMvc— выполнение HTTP-запросов в тесте.taskService— мок сервиса.objectMapper— сериализация JSON.
- Основные тест-методы:
createTask_withValidRequest_EXPECT_statusCreated()→ "POST /api/tasks возвращает 201 для валидного запроса".createTask_withEmptyTitle_EXPECT_statusBadRequest()→ "POST /api/tasks возвращает 400 при пустом title".getTask_whenTaskExists_EXPECT_statusOk()→ "GET /api/tasks/{id} возвращает 200 если задача найдена".getTask_whenTaskNotExists_EXPECT_statusNotFound()→ "GET /api/tasks/{id} возвращает 404 если задача не найдена".
- Назначение: веб-тесты контроллера учета времени (
MockMvc,@WebMvcTest). - Основные переменные:
mockMvc,timeRecordService(mock),objectMapper.
- Основные тест-методы:
startWork_withValidRequest_EXPECT_statusCreated()→ "POST /api/time-records возвращает 201 для валидного запроса".startWork_withoutEmployeeId_EXPECT_statusBadRequest()→ "POST /api/time-records возвращает 400 без employeeId".startWork_withoutTaskId_EXPECT_statusBadRequest()→ "POST /api/time-records возвращает 400 без taskId".startWork_withNegativeEmployeeId_EXPECT_statusBadRequest()→ "POST /api/time-records возвращает 400 при отрицательном employeeId".endWork_withValidRequest_EXPECT_statusOk()→ "PUT /api/time-records/{id}/end возвращает 200 для валидного запроса".endWork_withoutDescription_EXPECT_statusOk()→ "PUT /api/time-records/{id}/end возвращает 200 без description".getTimeRecord_whenRecordExists_EXPECT_statusOk()→ "GET /api/time-records/{id} возвращает 200 если запись найдена".getTimeRecord_whenRecordNotExists_EXPECT_statusNotFound()→ "GET /api/time-records/{id} возвращает 404 если запись не найдена".getEmployeeTimeRecords_EXPECT_statusOk()→ "GET /api/time-records/employee/{employeeId} возвращает 200".getTaskTimeRecords_EXPECT_statusOk()→ "GET /api/time-records/task/{taskId} возвращает 200".
- Назначение: unit-тесты сервиса задач (
MockitoExtension). - Основные переменные:
taskMapper— mock.taskService— тестируемый сервис.
- Основные тест-методы:
createTask_EXPECT_statusSetToNew()→ "createTask устанавливает статус NEW".createTask_EXPECT_insertCalledOnce()→ "createTask вызывает insert один раз".findById_whenTaskExists_EXPECT_returnTask()→ "findById возвращает задачу если она существует".findById_whenTaskNotExists_EXPECT_returnEmpty()→ "findById возвращает пустой результат если задача не найдена".updateStatus_EXPECT_mapperCalledWithCorrectParams()→ "updateStatus вызывает mapper с корректными параметрами".findAll_EXPECT_mapperFindAllCalledOnce()→ "findAll вызывает mapper.findAll один раз".
- Назначение: unit-тесты сервиса учета времени (
MockitoExtension). - Основные переменные:
timeRecordMapper— mock.timeRecordService— тестируемый сервис.
- Основные тест-методы:
startWork_whenStartTimeIsNull_EXPECT_startTimeSetToNow()→ "startWork устанавливает текущее время, если startTime не передан".startWork_whenStartTimeProvided_EXPECT_useProvidedTime()→ "startWork использует переданное startTime".startWork_EXPECT_insertCalledOnce()→ "startWork вызывает insert один раз".endWork_whenEndTimeIsNull_EXPECT_endTimeSetToNow()→ "endWork устанавливает текущее время, если endTime не передан".endWork_whenEndTimeProvided_EXPECT_useProvidedTime()→ "endWork использует переданное endTime".endWork_EXPECT_mapperCalledWithCorrectDescription()→ "endWork передает корректное описание в mapper".findById_whenRecordExists_EXPECT_returnRecord()→ "findById возвращает запись если она существует".findById_whenRecordNotExists_EXPECT_returnEmpty()→ "findById возвращает пустой результат если запись не найдена".findByEmployeeAndPeriod_EXPECT_mapperCalledWithCorrectParams()→ "findByEmployeeAndPeriod вызывает mapper с корректными параметрами".findByTaskId_EXPECT_mapperCalledWithCorrectTaskId()→ "findByTaskId вызывает mapper с корректным taskId".
- При создании проекта использовался чат-бот ИИ для следующих задач:
- Генерация нескольких тестов для приложения.
- Исправление ошибок с несовместимостью версий Swagger.
- Создание большей части документации для проекта.
- Приложение инициализирует схему через
schema.sqlпри старте. - Подключение к БД в Docker настроено на хост
db(имя сервиса изdocker-compose.yml). - Если порт
8080или5432занят, измените проброс портов вdocker-compose.yml.