# User Reader — документация для разработчиков HTTP-сервис на **FastAPI**, читает данные Merakomis из **MySQL** и отдаёт JSON. Предназначен для потребления **другими микросервисами и внешними приложениями**: синхронизация каталога сотрудников, справочники проектов, сводки трудозатрат, запись табеля. Интерактивная схема: **`/docs`** (Swagger UI), **`/redoc`**. **Справочник только по API, доступу и работе с эндпоинтами:** [docs/user-reader-api.md](docs/user-reader-api.md). --- ## Содержание 1. [Быстрый старт для интеграции](#быстрый-старт-для-интеграции) 2. [Сеть и развёртывание](#сеть-и-развёртывание) 3. [Аутентификация](#аутентификация) 4. [Общие соглашения API](#общие-соглашения-api) 5. [Модель данных Merakomis](#модель-данных-merakomis) 6. [Справочник эндпоинтов](#справочник-эндпоинтов) 7. [Сотрудники](#сотрудники) 8. [Трудозатраты (read)](#трудозатраты-read) 9. [Табель (read / write)](#табель-read--write) 10. [Сценарии интеграции](#сценарии-интеграции) 11. [Коды ошибок](#коды-ошибок) 12. [Примеры запросов](#примеры-запросов) 13. [Запуск и сборка](#запуск-и-сборка) --- ## Быстрый старт для интеграции Минимальный цикл подключения другого сервиса: 1. **Проверить доступность** — `GET /api/health` (без ключа). 2. **Узнать схему сотрудников** — `GET /api/meta` с API-ключом. 3. **Синхронизировать сотрудников** — `GET /api/employees` или инкремент `GET /api/employees/delta`. 4. **Получить сводку часов за период** — `GET /api/work-report?date_from=…&date_to=…`. 5. **(Опционально) Записать часы** — `PUT /api/time-entries` с заголовками `X-Api-Key` и `X-Acting-Emp-Id`. ```text Ваш сервис user-reader (:8090) MySQL | X-Api-Key: … | | | GET /api/work-report -------->| SELECT tMerakomisTime … | |<-------- JSON items -----------| | ``` **Рекомендуемые эндпоинты для интеграции:** | Задача | Эндпоинт | |--------|----------| | Каталог сотрудников | `/api/employees`, `/api/employees/delta` | | Сводка часов (основной контракт) | `/api/work-report` | | Сводка по проектам | `/api/project-report` | | Участники проектов | `/api/project-members` | | Запись в состав проекта | `PUT /api/project-members` | | Разделы проекта (для формы) | `/api/project-sections` | | Запись часов | `PUT /api/time-entries` | --- ## Сеть и развёртывание ### Docker Compose (этот репозиторий) | Сервис | Контейнер | Порт на хосте | Внутри сети compose | |--------|-----------|---------------|---------------------| | user-reader | `meraproject-user-reader` | **8090** | `http://user-reader:8090` | | MySQL | `meraproject-mysqldb` | 3307 | `db:3306` | Переменные окружения user-reader заданы в `docker-compose.yml`: ```yaml MYSQL_HOST: db MYSQL_DATABASE: j7508239_tracker USER_READER_API_KEY: "local-dev-key-change-in-prod" # сменить на проде ``` **С другого контейнера в той же compose-сети** обращайтесь к `http://user-reader:8090`. **С хоста или другого сервера** — `http://:8090`. **CORS:** включён для всех origin (`*`). Браузерные клиенты с другого домена могут вызывать API при наличии ключа. ### uvicorn локально ```bash cd services/user-reader pip install -r requirements.txt # задать MYSQL_* и USER_READER_API_KEY uvicorn app.main:app --host 0.0.0.0 --port 8090 ``` ### Статические страницы-превью (без отдельного фронтенда) | URL | Назначение | |-----|------------| | `GET /` | Сотрудники | | `GET /labor` | Проекты, разделы, сырые записи | | `GET /summary` | UI над `/api/work-report` | | `GET /project-report` | UI над `/api/project-report` | | `GET /time-entry` | Форма записи часов (`PUT /api/time-entries`) | | `GET /project-member` | Форма записи состава (`PUT /api/project-members`) | --- ## Аутентификация Два независимых уровня. ### 1. Service-to-service: API-ключ Если задан непустой **`USER_READER_API_KEY`**, все эндпоинты с пометкой «защищённый» требуют: - заголовок **`X-Api-Key: <ключ>`**, или - заголовок **`Authorization: Bearer <ключ>`** Если ключ **не задан** (пустая строка) — проверки нет (только для локальной отладки). Ответ при ошибке: **`401`**, тело FastAPI `{"detail": "…"}`. ### 2. Табель: идентичность сотрудника Эндпоинты записи и чтения табеля (кроме справочников) дополнительно требуют: | Заголовок / поле | Назначение | |------------------|------------| | **`X-Acting-Emp-Id`** | Кто действует (аналог PHP-сессии `fg_emp_id`) | | **`emp_id`** в query/body | За кого операция; если опущен → равен `acting` | Права проверяются по правилам Merakomis (админ, РП, делегат и т.д.) — см. `GET /api/labor/permissions`. --- ## Общие соглашения API ### Формат ответов - Кодировка: **UTF-8**, тело — **JSON**. - Даты в query: **`YYYY-MM-DD`**. - Пагинация: `limit` + `offset`; в ответе обычно есть `total`, `count`. - Числа с плавающей точкой (часы): до 2 знаков в агрегатах. ### Сериализация значений из MySQL | Тип MySQL | JSON | |-----------|------| | `datetime` | ISO-строка с пробелом и секундами | | `date` | `YYYY-MM-DD` | | `Decimal` | `float` | | `bytes` | UTF-8 с заменой ошибок | ### Секретные поля В выдаче сотрудников **никогда** не попадают колонки с паролями: суффикс `_pass`, подстроки `password`, `secret`. ### Имена полей - В таблицах MySQL колонки имеют префикс `tMerakomisProject_code` и т.п. - В JSON API — **короткие алиасы**: `code`, `project_name`, `step_name`. --- ## Модель данных Merakomis Ключевые таблицы (физические имена в MySQL обычно в нижнем регистре). | Логическое имя | Таблица | Назначение | |----------------|---------|------------| | Сотрудник | `tMerakomisEmp` | Каталог сотрудников | | Проект | `tMerakomisProject` | Проекты компании | | Стадия проекта | `tMerakomisDStep` | Справочник стадий (ПД, РД, К…) | | Раздел | `tMerakomisDSection` | Разделы внутри проекта (ОВ1, ТХ…) | | Участник команды | `tMerakomisTeamMember` | Сотрудник ↔ команда ↔ раздел | | Запись времени | `tMerakomisTime` | Часы по дням | | Отсутствие | `tMerakomisTimeAbsence` | Отпуск, больничный… | | Тип отсутствия | `tMerakomisDAbsence` | Справочник типов | | Должность / отдел | `tMerakomisPost` → `tMerakomisDDepartment` | Орготдел сотрудника (АР, ОВ…) | ### Стадия проекта (`step` / `step_name`) - В проекте: колонка **`tMerakomisProject_step`** → в API поле **`step`** (целое id). - Название стадии: справочник **`tMerakomisDStep`** (`id`, `name`, `text`). - `name` — краткое обозначение: **ПД**, **РД**, **К**, **АН**… - `text` — полное описание («Проектная документация»). - В эндпоинтах, где есть **`project_name`**, дополнительно отдаётся **`step_name`** (краткое название из справочника; пустая строка, если стадия не задана). ### Статус проекта (`status` / `status_name` / `archive`) - В проекте: **`tMerakomisProject_status`** → в API **`status`** (целое 0…3), **`status_name`** — подпись из портала Merakomis (`eStatus.php`). - Архив: **`tMerakomisProject_archive`** (`0`/`1`), **`tMerakomisProject_archive_date`** → **`archive_date`** (`YYYY-MM-DD` или `null`). - В эндпоинтах с **`project_name`** (сводки, состав команды) поля **`status`**, **`status_name`**, **`archive`**, **`archive_date`** идут рядом со стадией. | `status` | `status_name` | |----------|---------------| | `0` | Не определён | | `1` | В работе | | `2` | Завершён | | `3` | Пауза | > **Не путать:** поле `step` в **`tMerakomisDSection`** — фильтр разделов по стадии, это другая сущность. ### Переработки (`is_over`) | `is_over` | Смысл | |-----------|--------| | `0` | Рабочие часы | | `1` | Переработка | В агрегатах: - **`over1`** — переработка в рабочий день; - **`over2`** — переработка в выходной/праздник или день отсутствия; - **`over`** = `over1` + `over2`; - **`total`** = `hours` + `over`. Раздел проекта в сводках берётся из **членства в команде** (`TeamMember.section`), а не из строки времени. --- ## Справочник эндпоинтов | Метод | Путь | Ключ | Acting | Назначение | |-------|------|------|--------|------------| | 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` | ✓ | ✓ | Запись состава проекта | | POST | `/api/batch` | ✓ | — | Пакет до 25 GET-запросов | | PUT | `/api/absences` | ✓ | ✓ | Отсутствия по датам | | PUT | `/api/absences/range` | ✓ | ✓ | Отсутствия за диапазон | ✓ в колонке «Ключ» = нужен `USER_READER_API_KEY`, если он задан в окружении. ✓ в колонке «Acting» = обязателен заголовок `X-Acting-Emp-Id`. --- ## Переменные окружения | Переменная | Назначение | |------------|------------| | `MYSQL_HOST`, `MYSQL_PORT`, `MYSQL_USER`, `MYSQL_PASSWORD`, `MYSQL_DATABASE` | Подключение к MySQL | | `USER_READER_API_KEY` | Service-to-service ключ; пусто = без проверки | | `EMP_TABLE` | Явное имя таблицы сотрудников | | `USER_TABLE` / `USER_READER_FALLBACK_TABLE` | Резервная таблица, если нет `tMerakomisEmp` | | `DEBUG` | `1` — в ответе 500 может быть traceback | --- ## Сотрудники ### Режимы схемы таблицы Сервис определяет таблицу через `resolve_emp_table()`: 1. **`merakomis_emp`** — колонки `tMerakomisEmp_*`; в JSON поля без префикса. Фильтр `removed = 0`, если колонка есть. 2. **`generic_user_table`** — произвольная таблица; все несекретные колонки. PHP-ориентир: **`php_model`**: `"themes/merakomis/emp/model.php"`. ### `GET /api/health` Без API-ключа. ```json { "ok": true, "db": true, "database": "j7508239_tracker" } ``` ### `GET /api/meta` — защищённый Метаданные: физическая таблица, режим схемы, поля выдачи, колонка для delta. | Поле | Описание | |------|----------| | `schema_mode` | `merakomis_emp` или `generic_user_table` | | `delta_field` | Поле для инкрементальной синхронизации (`updated` или эвристика) | | `selected_fields` | Имена полей в `/api/employees` | | `where_removed` | Колонка «удалён» в БД | ### `GET /api/employees` — защищённый | Параметр | По умолчанию | Ограничения | |----------|--------------|-------------| | `limit` | `100` | 1…500 | | `offset` | `0` | ≥ 0 | **Ответ:** `total`, `limit`, `offset`, `items[]`. Дополнительные поля (Merakomis): | Поле | Описание | |------|----------| | `department` | Коды отдела через запятую: **АР**, **ГП**, **ОВ**… | | `department_codes` | Массив кодов | | `departments` | `department_id`, `name`, `short`, `code` | | `staffing_ids`, `staffing_names`, `staffing_title`, `staffings` | Должность (Архитектор, ГИП, ГАП…) из `tMerakomisDStaffing` | | `staffing` | Сырой JSON-массив id должностей в БД | Источник отделов: `tMerakomisPost` → `tMerakomisDDepartment`. Поле `roles` — не должность, а служебный фильтр подразделения в портале. ### `GET /api/employees/delta` — защищённый Инкремент: строки, у которых время изменения **строго больше** `since_updated` (Unix, секунды). | Параметр | По умолчанию | |----------|--------------| | `since_updated` | `0` | | `limit` | `500` | **Ответ:** `since_updated`, `max_updated`, `delta_column`, `count`, `items`. **Цикл синхронизации:** ```text since = 0 loop: GET /api/employees/delta?since_updated={since}&limit=500 обработать items since = max_updated пока count == limit ``` --- ## Трудозатраты (read) Мета-информация: **`GET /api/labor/meta`** — физические имена таблиц. ### `GET /api/projects` — защищённый Справочник `tMerakomisProject`. | Параметр | По умолчанию | Описание | |----------|--------------|----------| | `limit` | `100` | 1…500 | | `offset` | `0` | | | `include_removed` | `false` | Включать `removed = 1` | | `include_archive` | `true` | Не фильтровать архивные | **Поля `items`:** `id`, `code`, `name`, `director`, `step` (id стадии), `status`, `status_name`, `team`, `archive`, `archive_date`, `removed`, `date`, `date_end`. Для человекочитаемой стадии и статуса используйте join через `/api/project-members` или агрегаты ниже (`step_name`, `status_name`). ### `GET /api/sections` — защищённый Справочник `tMerakomisDSection`: `id`, `name`, `parent`, `step`. | Параметр | Описание | |----------|----------| | `step` | Фильтр разделов по стадии (id из `tMerakomisDSection.step`) | ### `GET /api/project-members` — защищённый Связь сотрудник ↔ проект ↔ раздел команды. | Параметр | По умолчанию | Описание | |----------|--------------|----------| | `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` | Активен в команде | ### `GET /api/project-sections` — защищённый Разделы, допустимые для проекта (`tMerakomisProjectSection` → `tMerakomisDSection`). | Параметр | Обязательно | |----------|:-----------:| | `project_id` | да | **Ответ:** `project_id`, `total`, `items[]` с `section_id`, `section_name`. ### `GET /api/member-roles` — защищённый Справочник ролей в команде (порт `eMemberRole`). | Параметр | Описание | |----------|----------| | `role` | Включить архивную роль при редактировании | **Ответ:** `total`, `items[]` с `id`, `title`. ### `PUT /api/project-members` — защищённый + Acting Upsert участника команды: если `(team, emp)` уже есть — обновление, иначе вставка. **Тело:** ```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` | | `text` | | Комментарий | Права: порт `Rules::isRwMember` (админ, РП, ГИП, директор отдела). **Ответ `200`:** `ok: true` и поля как у элемента `GET /api/project-members`. ### `GET /api/time-entries` — защищённый Сырые строки `tMerakomisTime`. | Параметр | Описание | |----------|----------| | `date_from`, `date_to` | `YYYY-MM-DD` | | `emp_id`, `project_id` | Фильтры | | `is_over` | `0` — рабочие, `1` — переработка | | `limit` | 1…2000 (по умолчанию 500) | **Поля:** `id`, `emp`, `project`, `date`, `duration`, `is_over`. ### `GET /api/work-report` — защищённый ⭐ основной контракт Агрегат за период: **сотрудник × проект** с орготделом, разделом, стадией и часами. **Обязательно:** `date_from`, `date_to` (`YYYY-MM-DD`, включительно). | Параметр | По умолчанию | Описание | |----------|--------------|----------| | `emp_id` | — | Фильтр по сотруднику | | `project_id` | — | Фильтр по проекту | | `limit` | `500` | 1…2000 | | `offset` | `0` | Пагинация | **Поля элемента `items`:** | Поле | Тип | Описание | |------|-----|----------| | `id` | int | ID сотрудника | | `employee` | string | ФИО | | `department` | string \| null | Орготдел (коды через запятую) | | `staffing_title` | string \| null | Должность (Архитектор, ГИП…) | | `section` | string \| null | Раздел в проекте | | `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 | Часы (см. модель данных) | **Пример строки:** ```json { "id": 46, "employee": "Агапова Анастасия Михайловна", "department": "ОВ", "staffing_title": "Инженер", "section": "ОВ1", "project_code": "2025-0016", "step_name": "РД", "status": 1, "status_name": "В работе", "archive": 0, "archive_date": null, "project_name": "ЖК Машкова", "hours": 8.0, "over": 0.0, "over1": 0.0, "over2": 0.0, "total": 8.0 } ``` **Пагинация длинного периода:** увеличивайте `offset` шагами до `limit` пока `offset + count < total`. Delta-режима нет — каждый запрос пересчитывает период. ### `GET /api/project-report` — защищённый Суммарные часы по **проекту** с разбивкой по орготделу и разделу (все сотрудники). **Обязательно:** `date_from`, `date_to`. | Параметр | Описание | |----------|----------| | `project_id` | Фильтр по проекту | | `limit`, `offset` | Пагинация (1…2000) | **Поля `items`:** `id` (project_id), `project_code`, **`step_name`**, **`status`**, **`status_name`**, **`archive`**, **`archive_date`**, `project_name`, `department`, `section`, `hours`, `over`, `over1`, `over2`, `total`. Одна строка = уникальная тройка **проект + department + section**. ### `GET /api/labor-summary` — защищённый Низкоуровневый агрегат (те же расчёты, что у `/api/work-report`), с внутренними id Merakomis. Для интеграции предпочтительнее **`/api/work-report`**. **Поля строки:** `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`. --- ## Табель (read / write) Полная спецификация write-логики и портирования PHP: `docs/change-proposal-labor-api-write.md`. ### `GET /api/calendar-days` — защищённый Производственный календарь за период. | Параметр | Обязательно | |----------|-------------| | `date_from`, `date_to` | да | ### `GET /api/absence-types` — защищённый Справочник типов отсутствий. В `items` всегда есть элемент `id: 0` («снять отсутствие»). ### `GET /api/labor/permissions` — защищённый + Acting | Параметр | Описание | |----------|----------| | `target_emp_id` | За кого проверяем (обязательно) | | `project_id` | Для проверки записи часов в проект | **Ответ:** `can_read`, `can_write_time`, `can_write_absence`, **`can_write_member`**, `is_admin`, `is_delegate_writer`. | Параметр | Описание | |----------|----------| | `target_emp_id` | Обязательно | | `project_id` | Для `can_write_time` и `can_write_member` | | `member_id` | Опционально, для проверки прав на редактирование записи | ### `GET /api/time-calendar` — защищённый + Acting Данные календаря табеля сотрудника (формат, близкий к PHP `getTimeTable`). | Параметр | По умолчанию | Описание | |----------|--------------|----------| | `emp_id` | acting | Чей табель | | `project_id` | `0` | `0` — сводный табель; иначе — по проекту | ### `GET /api/time-summary` — защищённый + Acting Сводка за текущий месяц и год (блоки `month`, `year` с разбивкой по проектам). ### `PUT /api/time-entries` — защищённый + Acting Запись или обновление часов за день (аналог PHP `addFromCalendar`). **Тело (JSON):** ```json { "emp_id": 46, "project_id": 86, "date": "2026-06-10", "time": 8, "over": 0 } ``` | Поле | Описание | |------|----------| | `emp_id` | Опционально; по умолчанию = acting | | `project_id` | Обязательно | | `date` | `YYYY-MM-DD` | | `time` | Часы, 0…24 | | `over` | `0` — рабочие, `1` — переработка | `time: 0` удаляет запись за этот день/тип. **Ответ `200`:** `ok`, `id`, `duration`, `info`, `limits` (применённые лимиты дня). ### `PUT /api/absences` — защищённый + Acting ```json { "emp_id": 46, "type": 3, "dates": ["2026-06-10", "2026-06-11"] } ``` `type: 0` — снять отсутствие. При установке отсутствия рабочие часы за день удаляются. ### `PUT /api/absences/range` — защищённый + Acting ```json { "emp_id": 46, "absence_id": 3, "begin": "2026-06-10", "end": "2026-06-20" } ``` `absence_id: 0` — очистить диапазон (удалить отсутствия и рабочие часы). --- ## Сценарии интеграции ### A. Периодическая синхронизация сотрудников 1. Храните у себя `last_since_updated` (Unix). 2. Вызывайте `/api/employees/delta?since_updated={last}&limit=500`. 3. Upsert по `id`; обновляйте `last_since_updated = max_updated`. 4. Раз в сутки — полная сверка через `/api/employees` с пагинацией. ### B. Отчёт по трудозатратам за месяц 1. `GET /api/work-report?date_from=2026-05-01&date_to=2026-05-31&fetch_all=true` — один запрос на весь период. 2. Или `POST /api/batch` с несколькими периодами в `requests[]`. 3. Группируйте у себя по `department`, `step_name`, `project_code` при необходимости. ### C. Список проектов сотрудника для UI `GET /api/project-members?emp_id={id}&active_only=true` — в ответе `project_code`, `step_name`, `status_name`, `archive`, `project_name`. ### D. Запись часов из мобильного/бота 1. Проверить права: `GET /api/labor/permissions?target_emp_id={id}&project_id={pid}`. 2. Записать: `PUT /api/time-entries` с `X-Acting-Emp-Id` и телом. 3. Обработать `403` (нет прав), `400` (невалидная дата). ### E. Сводка по проектам для руководства `GET /api/project-report?date_from=…&date_to=…` — агрегат по проект + отдел + раздел. ### F. Управление составом проекта из внешнего приложения 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` с `X-Acting-Emp-Id` и телом. 5. Проверка: `GET /api/project-members?project_id={pid}`. --- ## Коды ошибок | Код | Когда | |-----|--------| | `400` | Невалидные параметры, `date_from > date_to`, нет колонки для delta, нет `X-Acting-Emp-Id` | | `401` | Неверный или отсутствующий API-ключ | | `403` | Нет прав на табель / запись | | `404` | Сотрудник или проект не найден | | `500` | Ошибка MySQL, не найдена таблица Merakomis | Write-эндпоинты часто возвращают структурированный `detail`: ```json { "code": "forbidden", "message": "Нет прав на запись часов" } ``` --- ## Примеры запросов Локальный ключ из `docker-compose.yml`: **`local-dev-key-change-in-prod`**. Замените `HOST` на IP/имя хоста. ```bash # Health (без ключа) curl -s http://HOST:8090/api/health # Мета сотрудников curl -s -H "X-Api-Key: local-dev-key-change-in-prod" \ http://HOST:8090/api/meta # Сводка часов (основной контракт интеграции) curl -s -H "X-Api-Key: local-dev-key-change-in-prod" \ "http://HOST:8090/api/work-report?date_from=2026-05-01&date_to=2026-05-31&limit=2000" # Участники проекта со стадией curl -s -H "X-Api-Key: local-dev-key-change-in-prod" \ "http://HOST:8090/api/project-members?emp_id=46&active_only=true" # Состав проекта: запись curl -s -X PUT "http://HOST:8090/api/project-members" \ -H "X-Api-Key: local-dev-key-change-in-prod" \ -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}' # Инкремент сотрудников curl -s -H "X-Api-Key: local-dev-key-change-in-prod" \ "http://HOST:8090/api/employees/delta?since_updated=0&limit=500" # Запись часов curl -s -X PUT "http://HOST:8090/api/time-entries" \ -H "X-Api-Key: local-dev-key-change-in-prod" \ -H "X-Acting-Emp-Id: 46" \ -H "Content-Type: application/json" \ -d '{"project_id":86,"date":"2026-06-10","time":8,"over":0}' ``` ### Пример на Python (httpx) ```python import httpx BASE = "http://user-reader:8090" HEADERS = {"X-Api-Key": "local-dev-key-change-in-prod"} def fetch_work_report(date_from: str, date_to: str) -> list[dict]: items: list[dict] = [] offset = 0 limit = 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=60.0, ) r.raise_for_status() data = r.json() items.extend(data["items"]) offset += data["count"] if offset >= data["total"]: break return items ``` --- ## Запуск и сборка Зависимости: `services/user-reader/requirements.txt`. Точка входа: **`app.main:app`** (uvicorn). Исходники схем: | Модуль | Назначение | |--------|------------| | `app/emp_schema.py` | Сотрудники | | `app/merakomis_schema.py` | Проекты, время, стадии | | `app/labor.py` | Read API трудозатрат | | `app/labor_write.py` | Write API табеля | | `app/project_members_read.py` | Read: project-sections, member-roles | | `app/project_members_write.py` | Write: PUT project-members | | `app/labor_calendar.py` | Read API табеля | | `app/labor_permissions.py` | Права | ### Сборка образа и ошибка `failed to solve` При `docker compose build user-reader` сообщение **`failed to solve: python:...`** — Docker не смог скачать базовый образ с Hub. Базовый образ: **`python:3.12-slim-bookworm`**. **Вариант A** — есть интернет: ```bash docker pull python:3.12-slim-bookworm docker compose build user-reader docker compose up -d --no-deps user-reader ``` **Вариант B** — сервер без Hub: ```powershell .\services\user-reader\scripts\export-image.ps1 # скопировать user-reader-image.tar на сервер .\services\user-reader\scripts\import-image.ps1 ``` **Вариант C** — зеркало registry в `daemon.json`. --- ## Связанные документы | Документ | Содержание | |----------|------------| | [docs/user-reader-api.md](docs/user-reader-api.md) | **API, доступ, эндпоинты, сценарии вызова** | | `docs/change-proposal-labor-api-write.md` | Детальная спецификация write API и портирования PHP | | `docs/integration-external-app-employees.md` | Legacy PHP API (cookie); для новых интеграций используйте user-reader |