meraproject/docs/user-reader-api.md

962 lines
33 KiB
Markdown
Raw Normal View History

# 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) на списках и отчётах возвращает **все** строки за один запрос (лимит 50000). Поддерживается на:
`/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`). Максимум **50000** строк; при превышении — `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) | Быстрый старт контейнера |