817 lines
34 KiB
Markdown
817 lines
34 KiB
Markdown
|
|
# 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` | Пауза |
|
|||
|
|
|
|||
|
|
> **Не путать:** поле `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 |
|