meraproject/DEVELOPERS.md

823 lines
35 KiB
Markdown
Raw Normal View History

# 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 |