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>
823 lines
35 KiB
Markdown
823 lines
35 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` | Пауза |
|
||
|
||
### 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 |
|