# User Reader — API и доступ Справочник по HTTP API микросервиса **user-reader**: адреса, аутентификация, эндпоинты, параметры, ответы и типовые сценарии вызова. Общая документация по развёртыванию и модели данных: [DEVELOPERS.md](../DEVELOPERS.md). Интерактивная схема: **`/docs`** (Swagger), **`/redoc`**. --- ## Содержание 1. [Базовый адрес](#базовый-адрес) 2. [Доступ и заголовки](#доступ-и-заголовки) 3. [Матрица доступа к эндпоинтам](#матрица-доступа-к-эндпоинтам) 4. [Общие правила работы с API](#общие-правила-работы-с-api) 5. [Служебные эндпоинты](#1-служебные-эндпоинты) 6. [Сотрудники](#2-сотрудники) 7. [Трудозатраты (чтение)](#3-трудозатраты-чтение) 8. [Табель (чтение)](#4-табель-чтение) 9. [Табель (запись)](#5-табель-запись) 10. [Ошибки и коды ответа](#ошибки-и-коды-ответа) 11. [Типовые сценарии](#типовые-сценарии) 12. [Примеры запросов](#примеры-запросов) --- ## Базовый адрес | Окружение | Base URL | |-----------|----------| | Docker Compose (хост) | `http://localhost:8090` | | Docker Compose (другой контейнер) | `http://user-reader:8090` | | Прод / свой сервер | `http://<хост>:<порт>` | Все пути API начинаются с **`/api/`**. Кодировка: **UTF-8**, формат тела: **JSON** (кроме GET без тела). --- ## Доступ и заголовки ### Уровень 1 — service-to-service (API-ключ) На сервере задаётся переменная окружения **`USER_READER_API_KEY`**. | Состояние ключа | Поведение | |-----------------|-----------| | Непустая строка | Все «защищённые» эндпоинты требуют ключ | | Пустая строка | Проверка отключена (только для локальной отладки) | Передать ключ одним из способов: ```http X-Api-Key: <ваш-ключ> ``` или ```http Authorization: Bearer <ваш-ключ> ``` При отсутствии или неверном ключе: **`401 Unauthorized`**. ```json { "detail": "Требуется ключ: заголовок X-Api-Key или Authorization: Bearer <ключ>" } ``` Локальный ключ в `docker-compose.yml` (сменить на проде): **`local-dev-key-change-in-prod`**. ### Уровень 2 — идентичность сотрудника (табель) Для эндпоинтов табеля (чтение календаря, запись часов и отсутствий) **дополнительно** нужен заголовок: ```http X-Acting-Emp-Id: <целое число > 0> ``` Это id сотрудника в `tMerakomisEmp`, от имени которого выполняется операция (аналог cookie `fg_emp_id` в PHP). Опционально в query или JSON-теле: | Поле | Смысл | |------|--------| | `emp_id` | За кого операция; если не передан — используется `X-Acting-Emp-Id` | Права (админ, РП, делегат…) проверяются на сервере. Перед записью можно вызвать **`GET /api/labor/permissions`**. ### Сводка заголовков | Заголовок | Когда обязателен | |-----------|------------------| | `X-Api-Key` или `Authorization: Bearer` | Все `/api/*`, кроме `/api/health`, если ключ задан на сервере | | `X-Acting-Emp-Id` | Эндпоинты табеля с пометкой **Acting** | | `Content-Type: application/json` | `PUT` с телом | ### CORS Сервис отдаёт `Access-Control-Allow-Origin: *`. Браузерные клиенты с другого домена могут вызывать API при наличии ключа. --- ## Матрица доступа к эндпоинтам | Метод | Путь | API-ключ | X-Acting-Emp-Id | |-------|------|:--------:|:---------------:| | GET | `/api/health` | — | — | | GET | `/api/meta` | ✓ | — | | GET | `/api/employees` | ✓ | — | | GET | `/api/employees/delta` | ✓ | — | | GET | `/api/labor/meta` | ✓ | — | | GET | `/api/projects` | ✓ | — | | GET | `/api/sections` | ✓ | — | | GET | `/api/project-members` | ✓ | — | | GET | `/api/project-sections` | ✓ | — | | GET | `/api/member-roles` | ✓ | — | | GET | `/api/time-entries` | ✓ | — | | GET | `/api/labor-summary` | ✓ | — | | GET | `/api/work-report` | ✓ | — | | GET | `/api/project-report` | ✓ | — | | GET | `/api/calendar-days` | ✓ | — | | GET | `/api/absence-types` | ✓ | — | | GET | `/api/labor/permissions` | ✓ | ✓ | | GET | `/api/time-summary` | ✓ | ✓ | | GET | `/api/time-calendar` | ✓ | ✓ | | PUT | `/api/time-entries` | ✓ | ✓ | | PUT | `/api/project-members` | ✓ | ✓ | | PUT | `/api/absences` | ✓ | ✓ | | PUT | `/api/absences/range` | ✓ | ✓ | ✓ — обязателен, если на сервере включён соответствующий режим (ключ / табель). ### Пакетные запросы и `fetch_all` | Метод | Путь | API-ключ | Описание | |-------|------|:--------:|----------| | POST | `/api/batch` | ✓ | До 25 подзапросов GET в одном HTTP-вызове | Параметр **`fetch_all=true`** (query) на списках и отчётах возвращает **все** строки за один запрос (лимит 50 000). Поддерживается на: `/api/employees`, `/api/project-members`, `/api/time-entries`, `/api/work-report`, `/api/labor-summary`, `/api/project-report`. --- ## Общие правила работы с API ### Даты - В query и JSON: **`YYYY-MM-DD`** (например `2026-06-10`). - В ответах поля `date`, `date_from`, `date_to` — в том же формате. - Поля `datetime` из БД — ISO-строка с пробелом: `2026-06-10 14:30:00`. ### Пагинация Большинство списков поддерживают `limit` и `offset`. **`fetch_all=true`** — вернуть все строки одним ответом (без цикла `offset`). Максимум **50 000** строк; при превышении — `400 too_many_rows`. Рекомендуется для `/api/work-report` за месяц/квартал. ```text GET /api/work-report?date_from=2026-01-01&date_to=2026-03-31&fetch_all=true ``` Классическая пагинация: ```text offset = 0 loop: GET …?limit=L&offset=offset обработать items offset += count пока offset < total ``` В ответе обычно: | Поле | Смысл | |------|--------| | `total` | Всего записей (до пагинации) | | `limit`, `offset` | Эхо запроса | | `count` | Размер массива `items` в текущем ответе (не у всех эндпоинтов) | | `items` | Массив строк | ### Часы и переработки В агрегатах (`work-report`, `labor-summary`, `project-report`): | Поле | Смысл | |------|--------| | `hours` | Рабочие часы (`is_over = 0`) | | `over1` | Переработка в рабочий день | | `over2` | Переработка в выходной/праздник или день отсутствия | | `over` | `over1 + over2` | | `total` | `hours + over` | В сырых записях `/api/time-entries`: поле `is_over` — `0` или `1`. ### Стадия проекта | Поле | Где | Смысл | |------|-----|--------| | `step` | `/api/projects` | Числовой id стадии | | `step_name` | Эндпоинты с `project_name` | Краткое название: ПД, РД, К… | | `status` | `/api/projects`, эндпоинты с `project_name` | Числовой код статуса (0…3) | | `status_name` | Эндпоинты с `project_name` | Подпись: «В работе», «Завершён»… | | `archive` | `/api/projects`, эндпоинты с `project_name` | `0` — активный, `1` — в архиве | | `archive_date` | `/api/projects`, эндпоинты с `project_name` | Дата архивации (`YYYY-MM-DD`) или `null` | ### Id проекта (`project_id`) Во всех ответах, где есть **`project_code`**, **`project_name`** или поле **`project`** (сырой табель), дополнительно отдаётся **`project_id`** — числовой id из `tMerakomisProject`. Используйте его для однозначной идентификации проекта (код может повторяться в истории). | Эндпоинт | Поле с id проекта | |----------|-------------------| | `GET /api/projects` | **`project_id`** | | `GET /api/project-members`, `GET /api/labor-summary` | **`project_id`** | | `GET /api/work-report` | **`project_id`** (`id` — это `emp_id`) | | `GET /api/project-report` | **`project_id`** | | `GET /api/time-entries` | **`project_id`** и `project` (оба — id проекта) | | `GET /api/time-calendar` | **`project_id`** в корне ответа (если фильтр по проекту) | | `GET /api/time-summary` | **`project_id`** в элементах массива `project[]` | Коды **`status`** (порт `eStatus.php`): | `status` | `status_name` | |----------|---------------| | `0` | Не определён | | `1` | В работе | | `2` | Завершён | | `3` | Пауза | ### Секретные данные Пароли и поля с `password` / `secret` / суффиксом `_pass` **не возвращаются** в API сотрудников. --- ## 1. Служебные эндпоинты ### `GET /api/health` Проверка живости и подключения к MySQL. **Ключ не нужен.** **Ответ `200`:** ```json { "ok": true, "db": true, "database": "j7508239_tracker" } ``` При ошибке БД: `ok: false`, `db: false`, поле `error` со строкой. **Использование:** health-check в оркестраторе, перед массовой синхронизацией. --- ### `GET /api/meta` Метаданные таблицы сотрудников для клиента. **Query:** нет. **Ответ `200` (основные поля):** | Поле | Описание | |------|----------| | `schema_mode` | `merakomis_emp` или `generic_user_table` | | `physical_table` | Имя таблицы в MySQL | | `delta_field` | Имя поля в `items` для инкремента (`updated` и т.п.) | | `selected_fields` | Список полей в `/api/employees` | | `where_removed` | Колонка фильтра удалённых (если есть) | **Использование:** один раз при старте интеграции — понять схему и поле для delta. --- ### `GET /api/labor/meta` Имена физических таблиц Merakomis (проекты, время, отсутствия). **Ответ `200`:** ```json { "database": "…", "tables": { "project": "tmerakomisproject", "section": "…", "team_member": "…", "time": "…", "day": "…", "time_absence": "…" }, "overtime": { "is_over_field": "is_over", "over1": "…", "over2": "…" }, "section_source": "tMerakomisTeamMember.section через project.team" } ``` --- ## 2. Сотрудники ### `GET /api/employees` Постраничный список сотрудников. | Query | По умолчанию | Ограничения | |-------|--------------|-------------| | `limit` | `100` | 1…500 | | `offset` | `0` | ≥ 0 | **Ответ `200`:** | Поле | Описание | |------|----------| | `total` | Всего записей (с учётом фильтра `removed = 0` в Merakomis) | | `items[]` | Объекты сотрудника | Дополнительные поля в `items` (Merakomis): | Поле | Описание | |------|----------| | `id`, `name`, `login`, `email`, … | См. `/api/meta` → `selected_fields` | | `department` | Коды отдела через запятую: `ОВ`, `АР`… | | `department_codes` | Массив кодов | | `departments` | `{ department_id, name, short, code }` | | `staffing` | Сырое значение из БД: JSON-массив id должностей | | `staffing_ids` | Массив id из `tMerakomisDStaffing` | | `staffing_names` | Названия должностей: Архитектор, ГИП, ГАП… | | `staffing_title` | Должности одной строкой через запятую | | `staffings` | `{ id, name, text }` — `text` часто аббревиатура (ГИП, ГАП) | Поле **`roles`** в Merakomis — это не должность, а служебный фильтр «подразделение» в UI портала; оргструктура в API — через `departments` / `department`. --- ### `GET /api/employees/delta` Инкрементальная выгрузка: только изменённые записи. | Query | По умолчанию | Описание | |-------|--------------|----------| | `since_updated` | `0` | Unix timestamp (секунды); выбираются строки с `updated > since_updated` | | `limit` | `500` | 1…500 | **Ответ `200`:** | Поле | Описание | |------|----------| | `since_updated` | Эхо запроса | | `max_updated` | Максимум `updated` среди возвращённых строк — передать в следующий запрос | | `delta_column` | Имя поля времени в `items` | | `count` | Число элементов | | `items` | Те же поля, что в `/api/employees` | **Алгоритм синхронизации:** ```text since = сохранённый_у_себя_маркер # 0 при первом запуске повторять: r = GET /api/employees/delta?since_updated={since}&limit=500 upsert каждого r.items по id since = r.max_updated пока r.count == 500 сохранить since ``` --- ## 3. Трудозатраты (чтение) ### `GET /api/projects` Справочник проектов. | Query | По умолчанию | Описание | |-------|--------------|----------| | `limit` | `100` | 1…500 | | `offset` | `0` | | | `include_removed` | `false` | Включать удалённые | | `include_archive` | `true` | Не скрывать архивные | **Поля `items`:** **`project_id`**, `code`, `name`, `director`, `step`, `status`, `status_name`, `team`, `archive`, `archive_date`, `removed`, `date`, `date_end`. --- ### `GET /api/sections` Справочник разделов проекта. | Query | Описание | |-------|----------| | `limit` | 1…1000 (по умолчанию 500) | | `offset` | Пагинация | | `step` | Фильтр по полю `step` раздела | **Поля `items`:** `id`, `name`, `parent`, `step`. --- ### `GET /api/project-members` Участники команд: сотрудник ↔ проект ↔ раздел. | Query | По умолчанию | Описание | |-------|--------------|----------| | `project_id` | — | Фильтр | | `emp_id` | — | Фильтр | | `active_only` | `true` | Только активные в команде | | `limit` | `100` | 1…500 | | `offset` | `0` | | **Поля `items`:** | Поле | Описание | |------|----------| | `member_id` | id TeamMember | | `emp_id`, `emp_name` | Сотрудник | | `project_id`, `project_code`, `project_name` | Проект | | `step_name` | Стадия (ПД, РД…) | | `status`, `status_name` | Статус проекта | | `archive`, `archive_date` | Архив и дата архивации | | `section_id`, `section_name` | Раздел в команде | | `role`, `active` | Роль и активность | **Использование:** список проектов сотрудника для UI, проверка членства перед записью часов. --- ### `GET /api/project-sections` Разделы, настроенные для проекта (`tMerakomisProjectSection`). | Query | Обязательно | |-------|:-----------:| | `project_id` | да | **Ответ `200`:** `project_id`, `total`, `items[]` → `section_id`, `section_name`. **Ошибки:** `404` — проект не найден. --- ### `GET /api/member-roles` Справочник ролей в команде (ГИП, ГАП, Сотрудник…). | Query | Описание | |-------|----------| | `role` | Включить архивную роль (для формы редактирования) | **Ответ:** `total`, `items[]` → `id`, `title`. --- ### `GET /api/time-entries` Сырые строки табеля без агрегации. | Query | По умолчанию | Описание | |-------|--------------|----------| | `date_from`, `date_to` | — | Период `YYYY-MM-DD` | | `emp_id`, `project_id` | — | Фильтры | | `is_over` | — | `0` / `1` | | `limit` | `500` | 1…2000 | | `offset` | `0` | | **Поля `items`:** `id`, `emp`, **`project_id`**, `project`, `date`, `duration`, `is_over`. Ошибка `400`, если `date_from > date_to`. --- ### `GET /api/work-report` ⭐ рекомендуемый для интеграции Агрегат за период: **сотрудник × проект** + отдел + раздел + стадия + часы. | Query | Обязательно | По умолчанию | Описание | |-------|:-----------:|--------------|----------| | `date_from` | да | — | Начало периода | | `date_to` | да | — | Конец периода (включительно) | | `emp_id` | | — | Фильтр | | `project_id` | | — | Фильтр | | `limit` | | `500` | 1…2000 | | `offset` | | `0` | Пагинация | **Поля `items`:** | Поле | Тип | Описание | |------|-----|----------| | `id` | int | `emp_id` | | `employee` | string | ФИО | | `department` | string \| null | Орготдел | | `staffing_title` | string \| null | Должность (Архитектор, ГИП…) | | `section` | string \| null | Раздел в проекте | | **`project_id`** | int | Id проекта Merakomis | | `project_code` | string | | | `step_name` | string | Стадия | | `status` | int \| null | Код статуса проекта | | `status_name` | string \| null | Подпись статуса | | `archive` | int | `0` / `1` | | `archive_date` | string \| null | Дата архивации | | `project_name` | string | | | `hours`, `over`, `over1`, `over2`, `total` | number | Часы | **Особенности:** - Нет delta — каждый запрос пересчитывает весь период. - Раздел берётся из членства в команде, не из строки времени. - При большом объёме обходите `offset` до `total`. --- ### `GET /api/project-report` Агрегат: **проект × отдел × раздел** (сумма по всем сотрудникам). | Query | Обязательно | Описание | |-------|:-----------:|----------| | `date_from`, `date_to` | да | Период | | `project_id` | | Фильтр | | `limit`, `offset` | | 1…2000 | **Поля `items`:** **`project_id`**, `project_code`, `step_name`, `status`, `status_name`, `archive`, `archive_date`, `project_name`, `department`, `section`, `hours`, `over`, `over1`, `over2`, `total`. --- ### `GET /api/labor-summary` Тот же расчёт, что `work-report`, но с внутренними id Merakomis. Для внешних систем предпочтительнее **`/api/work-report`**. | Query | Обязательно | |-------|:-----------:| | `date_from`, `date_to` | да | | `emp_id`, `project_id`, `limit`, `offset` | опционально | **Поля `items`:** `emp_id`, `emp_name`, `department`, `staffing_title`, `project_id`, `project_code`, `step_name`, `status`, `status_name`, `archive`, `archive_date`, `project_name`, `section_id`, `section_name`, `role`, `hours`, `over`, `over1`, `over2`, `total`. --- ## 4. Табель (чтение) Все эндпоинты ниже требуют **`X-Acting-Emp-Id`** и API-ключ. ### `GET /api/calendar-days` Производственный календарь (рабочие/выходные дни). | Query | Обязательно | |-------|:-----------:| | `date_from`, `date_to` | да | **Ответ:** `date_from`, `date_to`, `items[]` — дни с типом и подписью. --- ### `GET /api/absence-types` Справочник типов отсутствий. **Ответ:** `items[]` — всегда есть элемент `{ "id": 0, "title": "—", "code": "" }` для снятия отсутствия. --- ### `GET /api/labor/permissions` Проверка прав до записи. | Query | Обязательно | Описание | |-------|:-----------:|----------| | `target_emp_id` | да | За кого проверяем | | `project_id` | | Для `can_write_time` | **Ответ `200`:** | Поле | Описание | |------|----------| | `can_read` | Чтение табеля | | `can_write_time` | Запись часов в `project_id` (false, если `project_id` не передан) | | `can_write_absence` | Запись отсутствий | | `can_write_member` | Запись состава проекта (`PUT /api/project-members`) | | `is_admin` | Администратор | | `is_delegate_writer` | Делегат на запись чужого табеля | Дополнительные query: `member_id` (опционально) — для уточнения `can_write_member` при редактировании. --- ### `GET /api/time-calendar` Данные календаря табеля (формат, близкий к PHP `getTimeTable`). | Query | По умолчанию | Описание | |-------|--------------|----------| | `emp_id` | acting | Чей табель | | `project_id` | `0` | `0` — сводный; иначе — по проекту | **Ответ (ключевые поля):** `can_edit`, `dates`, `days`, `absence`, `absence_options`, `project`, `total`, `acting_emp_id`, `target_emp_id`. Ошибки: `403` нет прав, `404` проект не найден. --- ### `GET /api/time-summary` Сводка часов за **текущий месяц** и **текущий год** для сотрудника. | Query | По умолчанию | |-------|--------------| | `emp_id` | acting | **Ответ:** объекты `month`, `year` с блоками `project`, `total`, `absence`. --- ## 5. Табель (запись) Требуются **API-ключ** и **`X-Acting-Emp-Id`**. Тело запросов: **`Content-Type: application/json`**. Детали бизнес-логики и портирования PHP: [change-proposal-labor-api-write.md](change-proposal-labor-api-write.md). ### `PUT /api/time-entries` Запись или обновление часов за один день по одному проекту. **Тело:** ```json { "emp_id": 46, "project_id": 86, "date": "2026-06-10", "time": 8, "over": 0 } ``` | Поле | Обязательно | Описание | |------|:-----------:|----------| | `project_id` | да | id проекта | | `date` | да | `YYYY-MM-DD` | | `time` | да | Часы, 0…24 | | `over` | | `0` — рабочие (по умолчанию), `1` — переработка | | `emp_id` | | Целевой сотрудник; иначе = acting | `time: 0` — **удаляет** запись за этот день и тип (`over`). **Ответ `200`:** ```json { "ok": true, "acting_emp_id": 46, "target_emp_id": 46, "id": 12345, "duration": 8, "is_over": 0, "info": { "hours": 8, "over": 0 }, "limits": { }, "cache_rows_deleted": 0 } ``` Ошибки: `403` нет прав, `404` сотрудник не найден, `400` невалидная дата. --- ### `PUT /api/project-members` Добавление или обновление участника команды проекта (upsert по паре `project_id` + `emp_id`). **Тело:** ```json { "project_id": 86, "emp_id": 46, "section_id": 12, "role": 22, "active": true, "text": "" } ``` | Поле | Обязательно | Описание | |------|:-----------:|----------| | `project_id` | да | id проекта | | `emp_id` | да | id сотрудника | | `section_id` | да | id из `GET /api/project-sections` | | `role` | да | id из `GET /api/member-roles` | | `active` | | По умолчанию `true`; `false` — деактивация в команде | | `text` | | Комментарий | **Ответ `200`:** `ok: true` и поля как у элемента `GET /api/project-members` (`member_id`, `emp_name`, `project_code`, `step_name`, `status`, `status_name`, `archive`, `archive_date`, `section_name`, `role`, `active`). Ошибки: `403` нет прав (`Rules::isRwMember`), `404` проект/сотрудник, `400` невалидный раздел, роль или архивный сотрудник. **Превью UI:** `GET /project-member` --- ### `PUT /api/absences` Установка или снятие отсутствия по списку дат. **Тело:** ```json { "emp_id": 46, "type": 3, "dates": ["2026-06-10", "2026-06-11"] } ``` | Поле | Описание | |------|----------| | `type` | id из `/api/absence-types`; `0` — снять отсутствие | | `dates` | Массив дат, минимум одна | При `type != 0` рабочие часы (`is_over = 0`) за эти дни **удаляются**. **Ответ `200`:** `ok`, `results[]` с `date`, `absence_type_id`, `cache_rows_deleted`. --- ### `PUT /api/absences/range` Массовая операция за диапазон дат. **Тело:** ```json { "emp_id": 46, "absence_id": 3, "begin": "2026-06-10", "end": "2026-06-20" } ``` | Поле | Описание | |------|----------| | `absence_id` | `0` — очистить диапазон (удалить отсутствия и рабочие часы) | | `begin`, `end` | Границы включительно; `end >= begin` | **Ответ `200`:** `ok`, `dates_count`, `m`: `"Успешно сохранено"`. --- ## 6. Пакетные запросы (`POST /api/batch`) Один HTTP-вызов — несколько **GET** к `/api/*`. Снижает накладные расходы при выгрузке нескольких периодов или эндпоинтов. **Заголовки:** `X-Api-Key` (обязателен, если настроен); `X-Acting-Emp-Id` пробрасывается в подзапросы. **Тело:** ```json { "requests": [ { "id": "q1", "method": "GET", "path": "/api/work-report?date_from=2026-01-01&date_to=2026-01-31&fetch_all=true" }, { "id": "q2", "method": "GET", "path": "/api/work-report?date_from=2026-02-01&date_to=2026-02-28&fetch_all=true" } ] } ``` | Поле | Описание | |------|----------| | `requests` | 1…25 подзапросов | | `id` | Произвольный идентификатор для сопоставления ответов | | `method` | Только `GET` | | `path` | Относительный путь `/api/...` с query | **Ответ `200`:** ```json { "ok": true, "count": 2, "results": [ { "id": "q1", "status": 200, "ok": true, "path": "…", "body": { } }, { "id": "q2", "status": 200, "ok": true, "path": "…", "body": { } } ] } ``` `ok` на верхнем уровне — `true`, только если **все** подзапросы успешны. Ограничения: рекурсивный `/api/batch` запрещён; абсолютные URL запрещены. --- ## Ошибки и коды ответа | HTTP | Когда | Формат `detail` | |------|--------|-----------------| | `400` | Невалидные параметры, даты, нет `X-Acting-Emp-Id`, delta без колонки времени | Строка или `{ "code", "message" }` | | `401` | Нет/неверный API-ключ | Строка | | `403` | Нет прав на табель | `{ "code": "forbidden", "message": "…" }` | | `404` | Сотрудник/проект не найден | `{ "code": "…_not_found", "message": "…" }` | | `500` | Ошибка БД, отсутствует таблица Merakomis | Строка или `{ "code": "db_error", "message" }` | **Рекомендации клиенту:** - На `401` — проверить ключ и переменную `USER_READER_API_KEY` на сервере. - На `403` — вызвать `/api/labor/permissions` или проверить `X-Acting-Emp-Id`. - На `500` — повтор с backoff; при `DEBUG=1` на сервере в теле может быть traceback (не для прода). --- ## Типовые сценарии ### Проверка перед интеграцией ```text 1. GET /api/health 2. GET /api/meta (с ключом) 3. GET /api/labor/meta (с ключом) ``` ### Ежедневная синхронизация сотрудников ```text GET /api/employees/delta?since_updated={stored}&limit=500 → upsert по id → stored = max_updated ``` ### Отчёт за месяц в BI/другой сервис ```text GET /api/work-report?date_from=2026-05-01&date_to=2026-05-31&fetch_all=true ``` Или несколько месяцев одним HTTP: ```text POST /api/batch → work-report за январь, февраль, … с fetch_all=true ``` ### Запись часов из внешнего UI ```text 1. GET /api/project-members?emp_id={acting}&active_only=true 2. GET /api/labor/permissions?target_emp_id={target}&project_id={pid} → если can_write_time 3. PUT /api/time-entries { project_id, date, time, over } ``` ### Сводка по проектам для руководства ```text GET /api/project-report?date_from=…&date_to=…&limit=2000 ``` ### Управление составом проекта ```text 1. GET /api/project-sections?project_id={pid} 2. GET /api/member-roles 3. GET /api/labor/permissions?target_emp_id={acting}&project_id={pid} → если can_write_member 4. PUT /api/project-members { project_id, emp_id, section_id, role, active } ``` --- ## Примеры запросов Замените `HOST` и ключ. ```bash # Без ключа curl -s "http://HOST:8090/api/health" # С ключом export KEY="local-dev-key-change-in-prod" export H="http://HOST:8090" curl -s -H "X-Api-Key: $KEY" "$H/api/meta" curl -s -H "X-Api-Key: $KEY" \ "$H/api/work-report?date_from=2026-05-01&date_to=2026-05-31&limit=100" curl -s -H "X-Api-Key: $KEY" \ "$H/api/project-members?emp_id=46&active_only=true" # Табель: чтение curl -s -H "X-Api-Key: $KEY" -H "X-Acting-Emp-Id: 46" \ "$H/api/labor/permissions?target_emp_id=46&project_id=86" # Табель: запись curl -s -X PUT "$H/api/time-entries" \ -H "X-Api-Key: $KEY" \ -H "X-Acting-Emp-Id: 46" \ -H "Content-Type: application/json" \ -d '{"project_id":86,"date":"2026-06-10","time":8,"over":0}' # Состав проекта: справочники и запись curl -s -H "X-Api-Key: $KEY" "$H/api/project-sections?project_id=86" curl -s -H "X-Api-Key: $KEY" "$H/api/member-roles" curl -s -X PUT "$H/api/project-members" \ -H "X-Api-Key: $KEY" \ -H "X-Acting-Emp-Id: 1" \ -H "Content-Type: application/json" \ -d '{"project_id":86,"emp_id":46,"section_id":12,"role":22,"active":true}' ``` ### Пагинация `work-report` (Python) ```python import httpx def fetch_all_work_report(base: str, key: str, date_from: str, date_to: str) -> list: headers = {"X-Api-Key": key} items, offset, limit = [], 0, 2000 while True: r = httpx.get( f"{base}/api/work-report", headers=headers, params={ "date_from": date_from, "date_to": date_to, "limit": limit, "offset": offset, }, timeout=120.0, ) r.raise_for_status() data = r.json() items.extend(data["items"]) offset += data["count"] if offset >= data["total"]: break return items ``` --- ## Связанные документы | Документ | Содержание | |----------|------------| | [DEVELOPERS.md](../DEVELOPERS.md) | Развёртывание, модель данных, сборка Docker | | [change-proposal-labor-api-write.md](change-proposal-labor-api-write.md) | Детали write API и PHP-аналоги | | [services/user-reader/README.md](../services/user-reader/README.md) | Быстрый старт контейнера |