meraproject/DEVELOPERS.md
keboss-m 9fa9d5e3ed Add project_id to read API responses and remove duplicate id fields.
Expose project_id across labor and calendar endpoints for unambiguous project matching. Use only project_id in /api/projects and /api/project-report; update docs, UI tables, and tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 19:01:33 +03:00

823 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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://<IP_хоста>: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` | Пауза |
### Id проекта (`project_id`)
Во всех read-ответах с данными проекта обязательно есть **`project_id`**. Поле `id` в **`/api/work-report`** — это id **сотрудника**; id проекта — только **`project_id`**. В **`/api/projects`** и **`/api/project-report`** — только **`project_id`**.
> **Не путать:** поле `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`:** **`project_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_id`**, `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_id`** | int | Id проекта |
| `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_id": 86,
"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`:** **`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_id`**, `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 |