2026-06-24 08:04:05 +00:00
# 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` |
2026-07-07 16:01:33 +00:00
### 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[]` |
2026-06-24 08:04:05 +00:00
Коды ** `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` | Н е скрывать архивные |
2026-07-07 16:01:33 +00:00
**Поля `items` :** ** `project_id` **, `code` , `name` , `director` , `step` , `status` , `status_name` , `team` , `archive` , `archive_date` , `removed` , `date` , `date_end` .
2026-06-24 08:04:05 +00:00
---
### `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` | |
2026-07-07 16:01:33 +00:00
**Поля `items` :** `id` , `emp` , ** `project_id` **, `project` , `date` , `duration` , `is_over` .
2026-06-24 08:04:05 +00:00
Ошибка `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 | Раздел в проекте |
2026-07-07 16:01:33 +00:00
| ** `project_id` ** | int | Id проекта Merakomis |
2026-06-24 08:04:05 +00:00
| `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 |
2026-07-07 16:01:33 +00:00
**Поля `items` :** ** `project_id` **, `project_code` , `step_name` , `status` , `status_name` , `archive` , `archive_date` , `project_name` , `department` , `section` , `hours` , `over` , `over1` , `over2` , `total` .
2026-06-24 08:04:05 +00:00
---
### `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 ) | Быстрый старт контейнера |